Compare commits

..

15 Commits

Author SHA1 Message Date
Dane Urban
5848975679 Remove comment 2026-01-08 19:21:24 -08:00
Dane Urban
dcc330010e Remove comment 2026-01-08 19:21:08 -08:00
Dane Urban
d0f5f1f5ae Handle error and log 2026-01-08 19:20:28 -08:00
Dane Urban
3e475993ff Change which event loop we get 2026-01-08 19:16:12 -08:00
Dane Urban
7c2b5fa822 Change loggin 2026-01-08 17:29:00 -08:00
Dane Urban
409cfdc788 nits 2026-01-08 17:23:08 -08:00
dependabot[bot]
7a9a132739 chore(deps): bump werkzeug from 3.1.4 to 3.1.5 in /backend/requirements (#7300)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jamison Lahman <jamison@lahman.dev>
2026-01-09 00:08:17 +00:00
dependabot[bot]
33bad8c37b chore(deps): bump authlib from 1.6.5 to 1.6.6 in /backend/requirements (#7299)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jamison Lahman <jamison@lahman.dev>
2026-01-08 23:28:19 +00:00
Raunak Bhagat
9241ff7a75 refactor: migrate hooks to /hooks directory and update imports (#7295) 2026-01-08 14:57:06 -08:00
Chris Weaver
0a25bc30ec fix: auto-pause (#7289) 2026-01-08 14:45:30 -08:00
Raunak Bhagat
e359732f4c feat: add SvgEmpty icon and alphabetize icon exports (#7294) 2026-01-08 21:40:55 +00:00
Evan Lohn
be47866a4d chore: logging confluence perm sync errors better (#7291) 2026-01-08 20:24:03 +00:00
Wenxi
8a20540559 fix: use tag constraint name instead of index elements (#7288) 2026-01-08 18:52:12 +00:00
Jamison Lahman
e6e1f2860a chore(fe): remove items-center from onboarding cards (#7285) 2026-01-08 18:28:36 +00:00
Evan Lohn
fc3f433df7 fix: usage limits for indexing (#7287) 2026-01-08 18:26:52 +00:00
38 changed files with 583 additions and 319 deletions

View File

@@ -12,6 +12,7 @@ from celery import Celery
from celery import shared_task
from celery import Task
from celery.exceptions import SoftTimeLimitExceeded
from fastapi import HTTPException
from pydantic import BaseModel
from redis import Redis
from redis.lock import Lock as RedisLock
@@ -40,9 +41,11 @@ from onyx.background.indexing.checkpointing_utils import (
)
from onyx.background.indexing.index_attempt_utils import cleanup_index_attempts
from onyx.background.indexing.index_attempt_utils import get_old_index_attempts
from onyx.configs.app_configs import AUTH_TYPE
from onyx.configs.app_configs import MANAGED_VESPA
from onyx.configs.app_configs import VESPA_CLOUD_CERT_PATH
from onyx.configs.app_configs import VESPA_CLOUD_KEY_PATH
from onyx.configs.constants import AuthType
from onyx.configs.constants import CELERY_GENERIC_BEAT_LOCK_TIMEOUT
from onyx.configs.constants import CELERY_INDEXING_LOCK_TIMEOUT
from onyx.configs.constants import MilestoneRecordType
@@ -61,6 +64,7 @@ from onyx.db.connector_credential_pair import (
)
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
from onyx.db.connector_credential_pair import set_cc_pair_repeated_error_state
from onyx.db.connector_credential_pair import update_connector_credential_pair_from_id
from onyx.db.engine.sql_engine import get_session_with_current_tenant
from onyx.db.engine.time_utils import get_db_current_time
from onyx.db.enums import ConnectorCredentialPairStatus
@@ -83,7 +87,6 @@ from onyx.db.models import SearchSettings
from onyx.db.search_settings import get_current_search_settings
from onyx.db.search_settings import get_secondary_search_settings
from onyx.db.swap_index import check_and_perform_index_swap
from onyx.db.usage import UsageLimitExceededError
from onyx.document_index.factory import get_default_document_index
from onyx.file_store.document_batch_storage import DocumentBatchStorage
from onyx.file_store.document_batch_storage import get_document_batch_storage
@@ -828,16 +831,39 @@ def check_for_indexing(self: Task, *, tenant_id: str) -> int | None:
for cc_pair_id in primary_cc_pair_ids:
lock_beat.reacquire()
if is_in_repeated_error_state(
cc_pair_id=cc_pair_id,
search_settings_id=current_search_settings.id,
cc_pair = get_connector_credential_pair_from_id(
db_session=db_session,
cc_pair_id=cc_pair_id,
)
# if already in repeated error state, don't do anything
# this is important so that we don't keep pausing the connector
# immediately upon a user un-pausing it to manually re-trigger and
# recover.
if (
cc_pair
and not cc_pair.in_repeated_error_state
and is_in_repeated_error_state(
cc_pair=cc_pair,
search_settings_id=current_search_settings.id,
db_session=db_session,
)
):
set_cc_pair_repeated_error_state(
db_session=db_session,
cc_pair_id=cc_pair_id,
in_repeated_error_state=True,
)
# When entering repeated error state, also pause the connector
# to prevent continued indexing retry attempts burning through embedding credits.
# NOTE: only for Cloud, since most self-hosted users use self-hosted embedding
# models. Also, they are more prone to repeated failures -> eventual success.
if AUTH_TYPE == AuthType.CLOUD:
update_connector_credential_pair_from_id(
db_session=db_session,
cc_pair_id=cc_pair.id,
status=ConnectorCredentialPairStatus.PAUSED,
)
# NOTE: At this point, we haven't done heavy checks on whether or not the CC pairs should actually be indexed
# Heavy check, should_index(), is called in _kickoff_indexing_tasks
@@ -1270,19 +1296,14 @@ def _check_chunk_usage_limit(tenant_id: str) -> None:
if not USAGE_LIMITS_ENABLED:
return
from onyx.db.usage import check_usage_limit
from onyx.db.usage import UsageType
from onyx.server.usage_limits import get_limit_for_usage_type
from onyx.server.usage_limits import is_tenant_on_trial_fn
is_trial = is_tenant_on_trial_fn(tenant_id)
limit = get_limit_for_usage_type(UsageType.CHUNKS_INDEXED, is_trial, tenant_id)
from onyx.server.usage_limits import check_usage_and_raise
with get_session_with_current_tenant() as db_session:
check_usage_limit(
check_usage_and_raise(
db_session=db_session,
usage_type=UsageType.CHUNKS_INDEXED,
limit=limit,
tenant_id=tenant_id,
pending_amount=0, # Just check current usage
)
@@ -1302,7 +1323,7 @@ def _docprocessing_task(
if USAGE_LIMITS_ENABLED:
try:
_check_chunk_usage_limit(tenant_id)
except UsageLimitExceededError as e:
except HTTPException as e:
# Log the error and fail the indexing attempt
task_logger.error(
f"Chunk indexing usage limit exceeded for tenant {tenant_id}: {e}"

View File

@@ -10,7 +10,6 @@ from sqlalchemy.orm import Session
from onyx.configs.app_configs import DISABLE_INDEX_UPDATE_ON_SWAP
from onyx.configs.constants import CELERY_GENERIC_BEAT_LOCK_TIMEOUT
from onyx.configs.constants import DocumentSource
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
from onyx.db.engine.time_utils import get_db_current_time
from onyx.db.enums import ConnectorCredentialPairStatus
from onyx.db.enums import IndexingStatus
@@ -126,18 +125,9 @@ class IndexingCallback(IndexingHeartbeatInterface):
def is_in_repeated_error_state(
cc_pair_id: int, search_settings_id: int, db_session: Session
cc_pair: ConnectorCredentialPair, search_settings_id: int, db_session: Session
) -> bool:
"""Checks if the cc pair / search setting combination is in a repeated error state."""
cc_pair = get_connector_credential_pair_from_id(
db_session=db_session,
cc_pair_id=cc_pair_id,
)
if not cc_pair:
raise RuntimeError(
f"is_in_repeated_error_state - could not find cc_pair with id={cc_pair_id}"
)
# if the connector doesn't have a refresh_freq, a single failed attempt is enough
number_of_failed_attempts_in_a_row_needed = (
NUM_REPEAT_ERRORS_BEFORE_REPEATED_ERROR_STATE
@@ -146,7 +136,7 @@ def is_in_repeated_error_state(
)
most_recent_index_attempts = get_recent_attempts_for_cc_pair(
cc_pair_id=cc_pair_id,
cc_pair_id=cc_pair.id,
search_settings_id=search_settings_id,
limit=number_of_failed_attempts_in_a_row_needed,
db_session=db_session,
@@ -180,7 +170,7 @@ def should_index(
db_session=db_session,
)
all_recent_errored = is_in_repeated_error_state(
cc_pair_id=cc_pair.id,
cc_pair=cc_pair,
search_settings_id=search_settings_instance.id,
db_session=db_session,
)

View File

@@ -961,14 +961,20 @@ def get_user_email_from_username__server(
try:
response = confluence_client.get_mobile_parameters(user_name)
email = response.get("email")
except Exception:
logger.warning(f"failed to get confluence email for {user_name}")
except HTTPError as e:
status_code = e.response.status_code if e.response is not None else "N/A"
logger.warning(
f"Failed to get confluence email for {user_name}: "
f"HTTP {status_code} - {e}"
)
# For now, we'll just return None and log a warning. This means
# we will keep retrying to get the email every group sync.
email = None
# We may want to just return a string that indicates failure so we dont
# keep retrying
# email = f"FAILED TO GET CONFLUENCE EMAIL FOR {user_name}"
except Exception as e:
logger.warning(
f"Failed to get confluence email for {user_name}: {type(e).__name__} - {e}"
)
email = None
_USER_EMAIL_CACHE[user_name] = email
return _USER_EMAIL_CACHE[user_name]

View File

@@ -14,9 +14,7 @@ from sqlalchemy.orm import joinedload
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
from onyx.configs.app_configs import AUTH_TYPE
from onyx.configs.app_configs import DISABLE_AUTH
from onyx.configs.constants import AuthType
from onyx.configs.constants import DocumentSource
from onyx.db.connector import fetch_connector_by_id
from onyx.db.credentials import fetch_credential_by_id
@@ -428,27 +426,10 @@ def set_cc_pair_repeated_error_state(
cc_pair_id: int,
in_repeated_error_state: bool,
) -> None:
values: dict = {"in_repeated_error_state": in_repeated_error_state}
# When entering repeated error state, also pause the connector
# to prevent continued indexing retry attempts burning through embedding credits.
# However, don't pause if there's an active manual indexing trigger,
# which indicates the user wants to retry immediately.
# NOTE: only for Cloud, since most self-hosted users use self-hosted embedding
# models. Also, they are more prone to repeated failures -> eventual success.
if in_repeated_error_state and AUTH_TYPE == AuthType.CLOUD:
cc_pair = get_connector_credential_pair_from_id(
db_session=db_session,
cc_pair_id=cc_pair_id,
)
# Only pause if there's no manual indexing trigger active
if cc_pair and cc_pair.indexing_trigger is None:
values["status"] = ConnectorCredentialPairStatus.PAUSED
stmt = (
update(ConnectorCredentialPair)
.where(ConnectorCredentialPair.id == cc_pair_id)
.values(**values)
.values(in_repeated_error_state=in_repeated_error_state)
)
db_session.execute(stmt)
db_session.commit()

View File

@@ -52,7 +52,7 @@ def create_or_add_document_tag(
is_list=False,
)
insert_stmt = insert_stmt.on_conflict_do_nothing(
index_elements=["tag_key", "tag_value", "source", "is_list"]
constraint="_tag_key_value_source_list_uc"
)
db_session.execute(insert_stmt)
@@ -98,7 +98,7 @@ def create_or_add_document_tag_list(
is_list=True,
)
insert_stmt = insert_stmt.on_conflict_do_nothing(
index_elements=["tag_key", "tag_value", "source", "is_list"]
constraint="_tag_key_value_source_list_uc"
)
db_session.execute(insert_stmt)

View File

@@ -1,6 +1,8 @@
import asyncio
import datetime
import json
import os
from collections.abc import AsyncGenerator
from collections.abc import Generator
from datetime import timedelta
from uuid import UUID
@@ -103,6 +105,7 @@ from onyx.server.utils import PUBLIC_API_TAGS
from onyx.utils.headers import get_custom_tool_additional_request_headers
from onyx.utils.logger import setup_logger
from onyx.utils.telemetry import mt_cloud_telemetry
from onyx.utils.threadpool_concurrency import run_in_background
from shared_configs.contextvars import get_current_tenant_id
logger = setup_logger()
@@ -507,7 +510,7 @@ def handle_new_chat_message(
@router.post("/send-chat-message", response_model=None, tags=PUBLIC_API_TAGS)
def handle_send_chat_message(
async def handle_send_chat_message(
chat_message_req: SendMessageRequest,
request: Request,
user: User | None = Depends(current_chat_accessible_user),
@@ -572,34 +575,63 @@ def handle_send_chat_message(
# Note: LLM cost tracking is now handled in multi_llm.py
return result
# Streaming path, normal Onyx UI behavior
def stream_generator() -> Generator[str, None, None]:
# Use prod-cons pattern to continue processing even if request stops yielding
buffer: asyncio.Queue[str | None] = asyncio.Queue()
loop = asyncio.get_running_loop()
# Capture headers before spawning thread
litellm_headers = extract_headers(request.headers, LITELLM_PASS_THROUGH_HEADERS)
custom_tool_headers = get_custom_tool_additional_request_headers(request.headers)
def producer() -> None:
"""
Producer function that runs handle_stream_message_objects in a loop
and writes results to the buffer.
"""
state_container = ChatStateContainer()
try:
logger.debug("Producer started")
with get_session_with_current_tenant() as db_session:
for obj in handle_stream_message_objects(
new_msg_req=chat_message_req,
user=user,
db_session=db_session,
litellm_additional_headers=extract_headers(
request.headers, LITELLM_PASS_THROUGH_HEADERS
),
custom_tool_additional_headers=get_custom_tool_additional_request_headers(
request.headers
),
litellm_additional_headers=litellm_headers,
custom_tool_additional_headers=custom_tool_headers,
external_state_container=state_container,
):
yield get_json_line(obj.model_dump())
# Thread-safe put into the asyncio queue
loop.call_soon_threadsafe(
buffer.put_nowait, get_json_line(obj.model_dump())
)
# Note: LLM cost tracking is now handled in multi_llm.py
except Exception as e:
logger.exception("Error in chat message streaming")
yield json.dumps({"error": str(e)})
loop.call_soon_threadsafe(buffer.put_nowait, json.dumps({"error": str(e)}))
finally:
logger.debug("Stream generator finished")
# Signal end of stream
loop.call_soon_threadsafe(buffer.put_nowait, None)
logger.debug("Producer finished")
return StreamingResponse(stream_generator(), media_type="text/event-stream")
async def stream_from_buffer() -> AsyncGenerator[str, None]:
"""
Async generator that reads from the buffer and yields to the client.
"""
try:
while True:
item = await buffer.get()
if item is None:
# End of stream signal
break
yield item
except asyncio.CancelledError:
logger.warning("Stream cancelled (Consumer disconnected)")
finally:
logger.debug("Stream consumer finished")
run_in_background(producer)
return StreamingResponse(stream_from_buffer(), media_type="text/event-stream")
@router.put("/set-message-as-latest")

View File

@@ -54,7 +54,7 @@ attrs==25.4.0
# jsonschema
# referencing
# zeep
authlib==1.6.5
authlib==1.6.6
# via fastmcp
babel==2.17.0
# via courlan
@@ -1248,7 +1248,7 @@ websockets==15.0.1
# via
# fastmcp
# google-genai
werkzeug==3.1.4
werkzeug==3.1.5
# via sendgrid
wrapt==1.17.3
# via

View File

@@ -1,142 +1,70 @@
# Helm Chart for Onyx
# Dependency updates (when subchart versions are bumped)
* If updating subcharts, you need to run this before committing!
* cd charts/onyx
* helm dependency update .
## Updating Dependencies
# Local testing
When subchart versions are bumped, rebuild the dependency lock file before committing:
## One time setup
* brew install kind
* Ensure you have no config at ~/.kube/config
* kind create cluster
* mv ~/.kube/config ~/.kube/kind-config
```bash
cd deployment/helm/charts/onyx
helm dependency update .
```
## Automated install and test with ct
* export KUBECONFIG=~/.kube/kind-config
* kubectl config use-context kind-kind
* from source root run the following. This does a very basic test against the web server
* ct install --all --helm-extra-set-args="--set=nginx.enabled=false" --debug --config ct.yaml
---
## Output template to file and inspect
* cd charts/onyx
* helm template test-output . > test-output.yaml
# Local Testing with Kind
## Test the entire cluster manually
* cd charts/onyx
* helm install onyx . -n onyx --set postgresql.primary.persistence.enabled=false
* the postgres flag is to keep the storage ephemeral for testing. You probably don't want to set that in prod.
* no flag for ephemeral vespa storage yet, might be good for testing
* kubectl -n onyx port-forward service/onyx-nginx 8080:80
* this will forward the local port 8080 to the installed chart for you to run tests, etc.
* When you are finished
* helm uninstall onyx -n onyx
* Vespa leaves behind a PVC. Delete it if you are completely done.
* k -n onyx get pvc
* k -n onyx delete pvc vespa-storage-da-vespa-0
* If you didn't disable Postgres persistence earlier, you may want to delete that PVC too.
## One-Time Setup
## Run as non-root user
By default, some onyx containers run as root. If you'd like to explicitly run the onyx containers as a non-root user, update the values.yaml file for the following components:
* `celery_shared`, `api`, `webserver`, `indexCapability`, `inferenceCapability`
```yaml
securityContext:
runAsNonRoot: true
runAsUser: 1001
```
* `vespa`
```yaml
podSecurityContext:
fsGroup: 1000
securityContext:
privileged: false
runAsUser: 1000
```
Install [kind](https://kind.sigs.k8s.io/) (Kubernetes in Docker) and create a local cluster:
## Resourcing
In the helm charts, we have resource suggestions for all Onyx-owned components.
These are simply initial suggestions, and may need to be tuned for your specific use case.
```bash
# macOS
brew install kind
Please talk to us in Slack if you have any questions!
# Linux (amd64) - see https://kind.sigs.k8s.io/docs/user/quick-start for other architectures
curl -Lo ./kind https://kind.sigs.k8s.io/releases/latest/download/kind-linux-amd64
chmod +x ./kind
sudo mv ./kind /usr/local/bin/kind
```
## Autoscaling options
The chart renders Kubernetes HorizontalPodAutoscalers by default. To keep this behavior, leave
`autoscaling.engine` as `hpa` and adjust the per-component `autoscaling.*` values as needed.
Create a cluster:
If you would like to use KEDA ScaledObjects instead:
```bash
kind create cluster --name onyx
```
1. Install and manage the KEDA operator in your cluster yourself (for example via the official KEDA Helm chart). KEDA is no longer packaged as a dependency of the Onyx chart.
2. Set `autoscaling.engine: keda` in your `values.yaml` and enable autoscaling for the components you want to scale.
## Automated Testing with chart-testing (ct)
From the repo root, run the chart-testing tool:
```bash
ct install --all --helm-extra-set-args="--set=nginx.enabled=false" --debug --config ct.yaml
```
> **Note:** nginx is disabled because kind lacks LoadBalancer support.
## Render Templates Locally
Preview the rendered Kubernetes manifests without installing:
```bash
cd deployment/helm/charts/onyx
helm template test-output . > test-output.yaml
```
## Manual Cluster Testing
Install the chart into your kind cluster:
```bash
cd deployment/helm/charts/onyx
helm install onyx . \
--namespace onyx \
--create-namespace \
--set=postgresql.nameOverride=cloudnative-pg
```
Forward the nginx service to access the UI locally:
```bash
kubectl -n onyx port-forward service/onyx-nginx 8080:80
```
Then open http://localhost:8080 in your browser.
### Cleanup
Uninstall the release:
```bash
helm uninstall onyx -n onyx
```
PVCs are not automatically deleted. Remove them if you're done testing:
```bash
kubectl -n onyx delete pvc --all
```
Or delete the entire namespace:
```bash
kubectl delete namespace onyx
```
To tear down the kind cluster entirely:
```bash
kind delete cluster --name onyx
```
---
# Configuration
## Running as Non-Root
By default, some containers run as root. To run as a non-root user, add to your `values.yaml`:
For `api`, `webserver`, `indexCapability`, `inferenceCapability`, and `celery_shared`:
```yaml
securityContext:
runAsNonRoot: true
runAsUser: 1001
```
For `vespa`:
```yaml
podSecurityContext:
fsGroup: 1000
securityContext:
privileged: false
runAsUser: 1000
```
## Resource Tuning
The chart includes resource requests/limits for all Onyx components. These are starting points—tune them based on your workload and cluster capacity.
## Autoscaling
The chart renders **HorizontalPodAutoscalers** by default (`autoscaling.engine: hpa`).
To use **KEDA ScaledObjects** instead:
1. Install the [KEDA operator](https://keda.sh/) separately (it's not bundled with this chart)
2. Set in your `values.yaml`:
```yaml
autoscaling:
engine: keda
```
When `autoscaling.engine` is set to `keda`, the chart will render the existing ScaledObject templates; otherwise HPAs will be rendered.

12
uv.lock generated
View File

@@ -347,14 +347,14 @@ wheels = [
[[package]]
name = "authlib"
version = "1.6.5"
version = "1.6.6"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cryptography" },
]
sdist = { url = "https://files.pythonhosted.org/packages/cd/3f/1d3bbd0bf23bdd99276d4def22f29c27a914067b4cf66f753ff9b8bbd0f3/authlib-1.6.5.tar.gz", hash = "sha256:6aaf9c79b7cc96c900f0b284061691c5d4e61221640a948fe690b556a6d6d10b", size = 164553, upload-time = "2025-10-02T13:36:09.489Z" }
sdist = { url = "https://files.pythonhosted.org/packages/bb/9b/b1661026ff24bc641b76b78c5222d614776b0c085bcfdac9bd15a1cb4b35/authlib-1.6.6.tar.gz", hash = "sha256:45770e8e056d0f283451d9996fbb59b70d45722b45d854d58f32878d0a40c38e", size = 164894, upload-time = "2025-12-12T08:01:41.464Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f8/aa/5082412d1ee302e9e7d80b6949bc4d2a8fa1149aaab610c5fc24709605d6/authlib-1.6.5-py2.py3-none-any.whl", hash = "sha256:3e0e0507807f842b02175507bdee8957a1d5707fd4afb17c32fb43fee90b6e3a", size = 243608, upload-time = "2025-10-02T13:36:07.637Z" },
{ url = "https://files.pythonhosted.org/packages/54/51/321e821856452f7386c4e9df866f196720b1ad0c5ea1623ea7399969ae3b/authlib-1.6.6-py2.py3-none-any.whl", hash = "sha256:7d9e9bc535c13974313a87f53e8430eb6ea3d1cf6ae4f6efcd793f2e949143fd", size = 244005, upload-time = "2025-12-12T08:01:40.209Z" },
]
[[package]]
@@ -6702,14 +6702,14 @@ wheels = [
[[package]]
name = "werkzeug"
version = "3.1.4"
version = "3.1.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markupsafe" },
]
sdist = { url = "https://files.pythonhosted.org/packages/45/ea/b0f8eeb287f8df9066e56e831c7824ac6bab645dd6c7a8f4b2d767944f9b/werkzeug-3.1.4.tar.gz", hash = "sha256:cd3cd98b1b92dc3b7b3995038826c68097dcb16f9baa63abe35f20eafeb9fe5e", size = 864687, upload-time = "2025-11-29T02:15:22.841Z" }
sdist = { url = "https://files.pythonhosted.org/packages/5a/70/1469ef1d3542ae7c2c7b72bd5e3a4e6ee69d7978fa8a3af05a38eca5becf/werkzeug-3.1.5.tar.gz", hash = "sha256:6a548b0e88955dd07ccb25539d7d0cc97417ee9e179677d22c7041c8f078ce67", size = 864754, upload-time = "2026-01-08T17:49:23.247Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2f/f9/9e082990c2585c744734f85bec79b5dae5df9c974ffee58fe421652c8e91/werkzeug-3.1.4-py3-none-any.whl", hash = "sha256:2ad50fb9ed09cc3af22c54698351027ace879a0b60a3b5edf5730b2f7d876905", size = 224960, upload-time = "2025-11-29T02:15:21.13Z" },
{ url = "https://files.pythonhosted.org/packages/ad/e4/8d97cca767bcc1be76d16fb76951608305561c6e056811587f36cb1316a8/werkzeug-3.1.5-py3-none-any.whl", hash = "sha256:5111e36e91086ece91f93268bb39b4a35c1e6f1feac762c9c822ded0a4e322dc", size = 225025, upload-time = "2026-01-08T17:49:21.859Z" },
]
[[package]]

View File

@@ -0,0 +1,20 @@
import type { IconProps } from "@opal/types";
const SvgEmpty = ({ size, ...props }: IconProps) => (
<svg
width={size}
height={size}
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
{...props}
>
<path
d="M14 10V12.6667C14 13.3929 13.3929 14 12.6667 14H3.33333C2.60711 14 2 13.3929 2 12.6667V10M8 2V5M13.5 4.5L11.5 6.5M2.5 4.5L4.5 6.5"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgEmpty;

View File

@@ -6,33 +6,36 @@ export { default as SvgAlertCircle } from "@opal/icons/alert-circle";
export { default as SvgAlertTriangle } from "@opal/icons/alert-triangle";
export { default as SvgArrowDownDot } from "@opal/icons/arrow-down-dot";
export { default as SvgArrowExchange } from "@opal/icons/arrow-exchange";
export { default as SvgArrowLeftDot } from "@opal/icons/arrow-left-dot";
export { default as SvgArrowLeft } from "@opal/icons/arrow-left";
export { default as SvgArrowLeftDot } from "@opal/icons/arrow-left-dot";
export { default as SvgArrowRight } from "@opal/icons/arrow-right";
export { default as SvgArrowRightCircle } from "@opal/icons/arrow-right-circle";
export { default as SvgArrowRightDot } from "@opal/icons/arrow-right-dot";
export { default as SvgArrowRight } from "@opal/icons/arrow-right";
export { default as SvgArrowUp } from "@opal/icons/arrow-up";
export { default as SvgArrowUpDot } from "@opal/icons/arrow-up-dot";
export { default as SvgArrowUpRight } from "@opal/icons/arrow-up-right";
export { default as SvgArrowUp } from "@opal/icons/arrow-up";
export { default as SvgArrowWallRight } from "@opal/icons/arrow-wall-right";
export { default as SvgAudioEqSmall } from "@opal/icons/audio-eq-small";
export { default as SvgAws } from "@opal/icons/aws";
export { default as SvgBarChart } from "@opal/icons/bar-chart";
export { default as SvgBarChartSmall } from "@opal/icons/bar-chart-small";
export { default as SvgBell } from "@opal/icons/bell";
export { default as SvgBookOpen } from "@opal/icons/book-open";
export { default as SvgBooksLineSmall } from "@opal/icons/books-line-small";
export { default as SvgBooksStackSmall } from "@opal/icons/books-stack-small";
export { default as SvgBracketCurly } from "@opal/icons/bracket-curly";
export { default as SvgBubbleText } from "@opal/icons/bubble-text";
export { default as SvgCalendar } from "@opal/icons/calendar";
export { default as SvgCheckCircle } from "@opal/icons/check-circle";
export { default as SvgCheckSquare } from "@opal/icons/check-square";
export { default as SvgCheck } from "@opal/icons/check";
export { default as SvgCheckCircle } from "@opal/icons/check-circle";
export { default as SvgCheckSmall } from "@opal/icons/check-small";
export { default as SvgChevronDownSmall } from "@opal/icons/chevron-down-small";
export { default as SvgCheckSquare } from "@opal/icons/check-square";
export { default as SvgChevronDown } from "@opal/icons/chevron-down";
export { default as SvgChevronDownSmall } from "@opal/icons/chevron-down-small";
export { default as SvgChevronLeft } from "@opal/icons/chevron-left";
export { default as SvgChevronRight } from "@opal/icons/chevron-right";
export { default as SvgChevronUpSmall } from "@opal/icons/chevron-up-small";
export { default as SvgChevronUp } from "@opal/icons/chevron-up";
export { default as SvgChevronUpSmall } from "@opal/icons/chevron-up-small";
export { default as SvgClaude } from "@opal/icons/claude";
export { default as SvgClipboard } from "@opal/icons/clipboard";
export { default as SvgClock } from "@opal/icons/clock";
@@ -44,28 +47,26 @@ export { default as SvgCornerRightUpDot } from "@opal/icons/corner-right-up-dot"
export { default as SvgCpu } from "@opal/icons/cpu";
export { default as SvgDevKit } from "@opal/icons/dev-kit";
export { default as SvgDownloadCloud } from "@opal/icons/download-cloud";
export { default as SvgEditBig } from "@opal/icons/edit-big";
export { default as SvgExpand } from "@opal/icons/expand";
export { default as SvgEdit } from "@opal/icons/edit";
export { default as SvgEditBig } from "@opal/icons/edit-big";
export { default as SvgEmpty } from "@opal/icons/empty";
export { default as SvgExpand } from "@opal/icons/expand";
export { default as SvgExternalLink } from "@opal/icons/external-link";
export { default as SvgEyeClosed } from "@opal/icons/eye-closed";
export { default as SvgEye } from "@opal/icons/eye";
export { default as SvgEyeClosed } from "@opal/icons/eye-closed";
export { default as SvgFiles } from "@opal/icons/files";
export { default as SvgFileSmall } from "@opal/icons/file-small";
export { default as SvgFileText } from "@opal/icons/file-text";
export { default as SvgFiles } from "@opal/icons/files";
export { default as SvgFilter } from "@opal/icons/filter";
export { default as SvgFold } from "@opal/icons/fold";
export { default as SvgFolder } from "@opal/icons/folder";
export { default as SvgFolderIn } from "@opal/icons/folder-in";
export { default as SvgFolderOpen } from "@opal/icons/folder-open";
export { default as SvgFolderPartialOpen } from "@opal/icons/folder-partial-open";
export { default as SvgFolderPlus } from "@opal/icons/folder-plus";
export { default as SvgFolder } from "@opal/icons/folder";
export { default as SvgGlobe } from "@opal/icons/globe";
export { default as SvgBookOpen } from "@opal/icons/book-open";
export { default as SvgBooksLineSmall } from "@opal/icons/books-line-small";
export { default as SvgBooksStackSmall } from "@opal/icons/books-stack-small";
export { default as SvgHashSmall } from "@opal/icons/hash-small";
export { default as SvgHardDrive } from "@opal/icons/hard-drive";
export { default as SvgHashSmall } from "@opal/icons/hash-small";
export { default as SvgHeadsetMic } from "@opal/icons/headset-mic";
export { default as SvgHourglass } from "@opal/icons/hourglass";
export { default as SvgImage } from "@opal/icons/image";
@@ -81,18 +82,21 @@ export { default as SvgLoader } from "@opal/icons/loader";
export { default as SvgLock } from "@opal/icons/lock";
export { default as SvgLogOut } from "@opal/icons/log-out";
export { default as SvgMaximize2 } from "@opal/icons/maximize-2";
export { default as SvgMcp } from "@opal/icons/mcp";
export { default as SvgMenu } from "@opal/icons/menu";
export { default as SvgMinusCircle } from "@opal/icons/minus-circle";
export { default as SvgMinus } from "@opal/icons/minus";
export { default as SvgMinusCircle } from "@opal/icons/minus-circle";
export { default as SvgMoon } from "@opal/icons/moon";
export { default as SvgMoreHorizontal } from "@opal/icons/more-horizontal";
export { default as SvgMusicSmall } from "@opal/icons/music-small";
export { default as SvgNotificationBubble } from "@opal/icons/notification-bubble";
export { default as SvgOllama } from "@opal/icons/ollama";
export { default as SvgOnyxLogo } from "@opal/icons/onyx-logo";
export { default as SvgOnyxOctagon } from "@opal/icons/onyx-octagon";
export { default as SvgOpenai } from "@opal/icons/openai";
export { default as SvgOpenrouter } from "@opal/icons/openrouter";
export { default as SvgOrganization } from "@opal/icons/organization";
export { default as SvgPaintBrush } from "@opal/icons/paint-brush";
export { default as SvgPaperclip } from "@opal/icons/paperclip";
export { default as SvgPauseCircle } from "@opal/icons/pause-circle";
export { default as SvgPenSmall } from "@opal/icons/pen-small";
@@ -101,14 +105,15 @@ export { default as SvgPin } from "@opal/icons/pin";
export { default as SvgPinned } from "@opal/icons/pinned";
export { default as SvgPlayCircle } from "@opal/icons/play-circle";
export { default as SvgPlug } from "@opal/icons/plug";
export { default as SvgPlusCircle } from "@opal/icons/plus-circle";
export { default as SvgPlus } from "@opal/icons/plus";
export { default as SvgPlusCircle } from "@opal/icons/plus-circle";
export { default as SvgQuestionMarkSmall } from "@opal/icons/question-mark-small";
export { default as SvgQuoteEnd } from "@opal/icons/quote-end";
export { default as SvgQuoteStart } from "@opal/icons/quote-start";
export { default as SvgRefreshCw } from "@opal/icons/refresh-cw";
export { default as SvgSearchMenu } from "@opal/icons/search-menu";
export { default as SvgRevert } from "@opal/icons/revert";
export { default as SvgSearch } from "@opal/icons/search";
export { default as SvgSearchMenu } from "@opal/icons/search-menu";
export { default as SvgSearchSmall } from "@opal/icons/search-small";
export { default as SvgServer } from "@opal/icons/server";
export { default as SvgSettings } from "@opal/icons/settings";
@@ -122,10 +127,10 @@ export { default as SvgSlidersSmall } from "@opal/icons/sliders-small";
export { default as SvgSparkle } from "@opal/icons/sparkle";
export { default as SvgStep1 } from "@opal/icons/step1";
export { default as SvgStep2 } from "@opal/icons/step2";
export { default as SvgStep3End } from "@opal/icons/step3-end";
export { default as SvgStep3 } from "@opal/icons/step3";
export { default as SvgStopCircle } from "@opal/icons/stop-circle";
export { default as SvgStep3End } from "@opal/icons/step3-end";
export { default as SvgStop } from "@opal/icons/stop";
export { default as SvgStopCircle } from "@opal/icons/stop-circle";
export { default as SvgSun } from "@opal/icons/sun";
export { default as SvgTerminalSmall } from "@opal/icons/terminal-small";
export { default as SvgTextLinesSmall } from "@opal/icons/text-lines-small";
@@ -135,17 +140,13 @@ export { default as SvgTrash } from "@opal/icons/trash";
export { default as SvgTwoLineSmall } from "@opal/icons/two-line-small";
export { default as SvgUnplug } from "@opal/icons/unplug";
export { default as SvgUploadCloud } from "@opal/icons/upload-cloud";
export { default as SvgUserPlus } from "@opal/icons/user-plus";
export { default as SvgUser } from "@opal/icons/user";
export { default as SvgUserPlus } from "@opal/icons/user-plus";
export { default as SvgUsers } from "@opal/icons/users";
export { default as SvgWallet } from "@opal/icons/wallet";
export { default as SvgWorkflow } from "@opal/icons/workflow";
export { default as SvgX } from "@opal/icons/x";
export { default as SvgXCircle } from "@opal/icons/x-circle";
export { default as SvgXOctagon } from "@opal/icons/x-octagon";
export { default as SvgX } from "@opal/icons/x";
export { default as SvgZoomIn } from "@opal/icons/zoom-in";
export { default as SvgZoomOut } from "@opal/icons/zoom-out";
export { default as SvgMcp } from "@opal/icons/mcp";
export { default as SvgPaintBrush } from "@opal/icons/paint-brush";
export { default as SvgRevert } from "@opal/icons/revert";
export { default as SvgNotificationBubble } from "@opal/icons/notification-bubble";

View File

@@ -19,7 +19,7 @@ import ChatInputBar, {
ChatInputBarHandle,
} from "@/app/chat/components/input/ChatInputBar";
import useChatSessions from "@/hooks/useChatSessions";
import { useCCPairs } from "@/lib/hooks/useCCPairs";
import useCCPairs from "@/hooks/useCCPairs";
import { useTags } from "@/lib/hooks/useTags";
import { useDocumentSets } from "@/lib/hooks/useDocumentSets";
import { useAgents } from "@/hooks/useAgents";
@@ -209,7 +209,7 @@ export default function ChatPage({ firstMessage }: ChatPageProps) {
const noAssistants = liveAssistant === null || liveAssistant === undefined;
const availableSources: ValidSources[] = useMemo(() => {
return (ccPairs ?? []).map((ccPair) => ccPair.source);
return ccPairs.map((ccPair) => ccPair.source);
}, [ccPairs]);
const sources: SourceMetadata[] = useMemo(() => {

View File

@@ -11,8 +11,8 @@ import { MinimalPersonaSnapshot } from "@/app/admin/assistants/interfaces";
import LLMPopover from "@/refresh-components/popovers/LLMPopover";
import { InputPrompt } from "@/app/chat/interfaces";
import { FilterManager, LlmManager, useFederatedConnectors } from "@/lib/hooks";
import { useInputPrompts } from "@/lib/hooks/useInputPrompts";
import { useCCPairs } from "@/lib/hooks/useCCPairs";
import usePromptShortcuts from "@/hooks/usePromptShortcuts";
import useCCPairs from "@/hooks/useCCPairs";
import { DocumentIcon2, FileIcon } from "@/components/icons/icons";
import { OnyxDocument, MinimalOnyxDocument } from "@/lib/search/interfaces";
import { ChatState } from "@/app/chat/interfaces";
@@ -256,7 +256,7 @@ const ChatInputBar = React.memo(
[setCurrentMessageFiles]
);
const { inputPrompts } = useInputPrompts();
const { promptShortcuts } = usePromptShortcuts();
const { ccPairs, isLoading: ccPairsLoading } = useCCPairs();
const { data: federatedConnectorsData, isLoading: federatedLoading } =
useFederatedConnectors();
@@ -272,7 +272,7 @@ const ChatInputBar = React.memo(
// Memoize availableSources to prevent unnecessary re-renders
const memoizedAvailableSources = useMemo(
() => [
...(ccPairs ?? []).map((ccPair) => ccPair.source),
...ccPairs.map((ccPair) => ccPair.source),
...(federatedConnectorsData?.map((connector) => connector.source) ||
[]),
],
@@ -332,12 +332,12 @@ const ChatInputBar = React.memo(
const filteredPrompts = useMemo(
() =>
inputPrompts.filter(
promptShortcuts.filter(
(prompt) =>
prompt.active &&
prompt.prompt.toLowerCase().startsWith(startFilterSlash)
),
[inputPrompts, startFilterSlash]
[promptShortcuts, startFilterSlash]
);
// Determine if we should hide processing state based on context limits

View File

@@ -25,7 +25,7 @@ import { useNightTime } from "@/lib/dateUtils";
import { useFilters } from "@/lib/hooks";
import { uploadFilesForChat } from "../services/lib";
import { ChatFileType, FileDescriptor } from "../interfaces";
import { useCCPairs } from "@/lib/hooks/useCCPairs";
import useCCPairs from "@/hooks/useCCPairs";
import { useDocumentSets } from "@/lib/hooks/useDocumentSets";
import { useTags } from "@/lib/hooks/useTags";
import { useLLMProviders } from "@/lib/hooks/useLLMProviders";

View File

@@ -9,7 +9,7 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
import { getSourceMetadata } from "@/lib/sources";
import { useRouter } from "next/navigation";
import type { Route } from "next";
import { useFederatedOAuthStatus } from "@/lib/hooks/useFederatedOAuthStatus";
import useFederatedOAuthStatus from "@/hooks/useFederatedOAuthStatus";
import Text from "@/refresh-components/texts/Text";
import { SvgLink } from "@opal/icons";
export interface FederatedConnectorOAuthStatus {

View File

@@ -1,3 +1,5 @@
"use client";
import { SEARCH_PARAM_NAMES } from "@/app/chat/services/searchParams";
import { useRouter, useSearchParams } from "next/navigation";
import type { Route } from "next";

View File

@@ -1,3 +1,5 @@
"use client";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { buildApiPath } from "@/lib/urlBuilder";

View File

@@ -1,3 +1,5 @@
"use client";
import useSWR from "swr";
import { useState, useEffect, useMemo, useCallback } from "react";
import {

View File

@@ -1,3 +1,5 @@
"use client";
import useSWR from "swr";
import { ToolSnapshot } from "@/lib/tools/interfaces";
import { errorHandlingFetcher } from "@/lib/fetcher";

View File

@@ -1,3 +1,5 @@
"use client";
import { useRef, useEffect, useCallback, useState } from "react";
export function useBoundingBox() {

View File

@@ -0,0 +1,81 @@
"use client";
import useSWR from "swr";
import { CCPairBasicInfo } from "@/lib/types";
import { errorHandlingFetcher } from "@/lib/fetcher";
/**
* Hook for fetching connector-credential pairs (CC Pairs).
*
* Retrieves all connector-credential pairs configured in the system. CC Pairs
* represent connections between data sources (connectors) and their authentication
* credentials, used for indexing content from various sources like Confluence,
* Slack, Google Drive, etc. Uses SWR for caching and automatic revalidation.
*
* @returns Object containing:
* - ccPairs: Array of CCPairBasicInfo objects
* - isLoading: Boolean indicating if data is being fetched
* - error: Error object if the fetch failed
* - refetch: Function to manually reload CC pairs
*
* @example
* ```tsx
* // Display list of connected data sources
* const ConnectorList = () => {
* const { ccPairs, isLoading, error } = useCCPairs();
*
* if (isLoading) return <Spinner />;
* if (error) return <Error message="Failed to load connectors" />;
*
* return (
* <ul>
* {ccPairs.map(pair => (
* <li key={pair.id}>
* {pair.name} - {pair.source}
* </li>
* ))}
* </ul>
* );
* };
* ```
*
* @example
* ```tsx
* // Filter connectors by source type
* const SlackConnectors = () => {
* const { ccPairs } = useCCPairs();
*
* const slackPairs = ccPairs.filter(pair => pair.source === 'slack');
*
* return <ConnectorGrid connectors={slackPairs} />;
* };
* ```
*
* @example
* ```tsx
* // Refresh list after connecting a new source
* const ConnectSourceButton = () => {
* const { refetch } = useCCPairs();
*
* const handleConnect = async () => {
* await connectNewSource();
* refetch(); // Refresh the list
* };
*
* return <Button onClick={handleConnect}>Connect Source</Button>;
* };
* ```
*/
export default function useCCPairs() {
const { data, error, isLoading, mutate } = useSWR<CCPairBasicInfo[]>(
"/api/manage/connector-status",
errorHandlingFetcher
);
return {
ccPairs: data ?? [],
isLoading,
error,
refetch: mutate,
};
}

View File

@@ -1,3 +1,5 @@
"use client";
import { useEffect, RefObject } from "react";
/**

View File

@@ -1,3 +1,5 @@
"use client";
import { useRef, useEffect, useState } from "react";
interface ContentSize {

View File

@@ -0,0 +1,100 @@
"use client";
import { useMemo } from "react";
import useSWR from "swr";
import { FederatedConnectorOAuthStatus } from "@/components/chat/FederatedOAuthModal";
import { errorHandlingFetcher } from "@/lib/fetcher";
/**
* Hook for fetching federated OAuth connector authentication status.
*
* Retrieves the authentication status for all federated connectors (e.g., Gmail,
* Google Drive, Slack) and provides utilities to identify which connectors need
* OAuth authentication. Uses SWR for caching and automatic revalidation.
*
* @returns Object containing:
* - connectors: Array of all federated connector statuses
* - needsAuth: Array of connectors that lack OAuth tokens
* - hasUnauthenticatedConnectors: Boolean indicating if any connectors need auth
* - isLoading: Boolean indicating if data is being fetched
* - error: Error object if the fetch failed
* - refetch: Function to manually reload connector statuses
*
* @example
* ```tsx
* // Display connectors requiring authentication
* const OAuthPrompt = () => {
* const { needsAuth, isLoading } = useFederatedOAuthStatus();
*
* if (isLoading) return <Spinner />;
* if (needsAuth.length === 0) return null;
*
* return (
* <div>
* <h3>Connect your accounts:</h3>
* {needsAuth.map(connector => (
* <ConnectButton key={connector.source} connector={connector} />
* ))}
* </div>
* );
* };
* ```
*
* @example
* ```tsx
* // Show warning banner if any connectors need authentication
* const AuthWarningBanner = () => {
* const { hasUnauthenticatedConnectors } = useFederatedOAuthStatus();
*
* if (!hasUnauthenticatedConnectors) return null;
*
* return (
* <Banner variant="warning">
* Some connectors need authentication to access your data.
* </Banner>
* );
* };
* ```
*
* @example
* ```tsx
* // List all connectors with their auth status
* const ConnectorList = () => {
* const { connectors, refetch } = useFederatedOAuthStatus();
*
* return (
* <div>
* {connectors.map(connector => (
* <ConnectorRow
* key={connector.source}
* connector={connector}
* authenticated={connector.has_oauth_token}
* onReconnect={refetch}
* />
* ))}
* </div>
* );
* };
* ```
*/
export default function useFederatedOAuthStatus() {
const { data, error, isLoading, mutate } = useSWR<
FederatedConnectorOAuthStatus[]
>("/api/federated/oauth-status", errorHandlingFetcher);
const connectors = data ?? [];
const needsAuth = useMemo(
() => (data ?? []).filter((c) => !c.has_oauth_token),
[data]
);
const hasUnauthenticatedConnectors = needsAuth.length > 0;
return {
connectors,
needsAuth,
hasUnauthenticatedConnectors,
isLoading,
error,
refetch: mutate,
};
}

View File

@@ -1,3 +1,5 @@
"use client";
import { useMemo, useState } from "react";
/**

View File

@@ -1,3 +1,5 @@
"use client";
import { useCallback } from "react";
import { useDropzone, DropzoneOptions, FileRejection } from "react-dropzone";

View File

@@ -1,3 +1,5 @@
"use client";
import { useEffect, useState } from "react";
/**

View File

@@ -1,3 +1,5 @@
"use client";
import { useCallback, useEffect, useState, useRef, useMemo } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import type { Route } from "next";

View File

@@ -0,0 +1,86 @@
"use client";
import useSWR from "swr";
import { InputPrompt } from "@/app/chat/interfaces";
import { errorHandlingFetcher } from "@/lib/fetcher";
/**
* Hook for fetching user-created prompt shortcuts.
*
* Retrieves prompt shortcuts that can be used to quickly insert common prompts
* in chat. Automatically filters out public/system prompts, returning only
* prompts created by the current user. Uses SWR for caching and automatic
* revalidation.
*
* @returns Object containing:
* - promptShortcuts: Array of InputPrompt objects (user's shortcuts only)
* - isLoading: Boolean indicating if data is being fetched
* - error: Error object if the fetch failed
* - refetch: Function to manually reload the prompts
*
* @example
* ```tsx
* // Basic usage with loading state
* const MyComponent = () => {
* const { promptShortcuts, isLoading, error } = usePromptShortcuts();
*
* if (isLoading) return <Spinner />;
* if (error) return <Error />;
*
* return (
* <ul>
* {promptShortcuts.map(shortcut => (
* <li key={shortcut.id}>{shortcut.prompt}</li>
* ))}
* </ul>
* );
* };
* ```
*
* @example
* ```tsx
* // With refetch after creating a new shortcut
* const ShortcutManager = () => {
* const { promptShortcuts, refetch } = usePromptShortcuts();
*
* const handleCreate = async (newShortcut) => {
* await createShortcut(newShortcut);
* refetch(); // Refresh the list
* };
*
* return <ShortcutsList shortcuts={promptShortcuts} onCreate={handleCreate} />;
* };
* ```
*
* @example
* ```tsx
* // Filtering active shortcuts for slash command menu
* const SlashCommandMenu = ({ searchTerm }) => {
* const { promptShortcuts } = usePromptShortcuts();
*
* const activeShortcuts = promptShortcuts.filter(
* (shortcut) =>
* shortcut.active &&
* shortcut.prompt.toLowerCase().startsWith(searchTerm)
* );
*
* return <CommandMenu items={activeShortcuts} />;
* };
* ```
*/
export default function usePromptShortcuts() {
const { data, error, isLoading, mutate } = useSWR<InputPrompt[]>(
"/api/input_prompt",
errorHandlingFetcher
);
// Filter to only user-created prompts (exclude public/system prompts)
const promptShortcuts = data?.filter((p) => !p.is_public) ?? [];
return {
promptShortcuts,
isLoading,
error,
refetch: mutate,
};
}

View File

@@ -1,3 +1,5 @@
"use client";
import useSWR, { KeyedMutator } from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { getActionIcon } from "@/lib/tools/mcpUtils";

View File

@@ -1,3 +1,5 @@
"use client";
import { useState, useEffect, useRef } from "react";
import { User } from "@/lib/types";
import { NO_AUTH_USER_ID } from "@/lib/extension/constants";

View File

@@ -1,3 +1,5 @@
"use client";
import { useCallback, useEffect, useMemo, useState } from "react";
import { User, UserPersonalization } from "@/lib/types";
@@ -27,7 +29,64 @@ interface UseUserPersonalizationOptions {
onError?: (error: unknown) => void;
}
export function useUserPersonalization(
/**
* Hook for managing user personalization settings
*
* Handles user personalization data including name, role, and memories.
* Provides state management and persistence for personalization fields with
* optimistic updates and error handling.
*
* @param user - The current user object containing personalization data
* @param persistPersonalization - Async function to persist personalization changes to the server
* @param options - Optional callbacks for success and error handling
* @param options.onSuccess - Callback invoked when personalization is successfully saved
* @param options.onError - Callback invoked when personalization save fails
* @returns Object containing personalization state and handler functions
*
* @example
* ```tsx
* import useUserPersonalization from "@/hooks/useUserPersonalization";
* import { useUser } from "@/components/user/UserProvider";
*
* function PersonalizationSettings() {
* const { user, updateUserPersonalization } = useUser();
* const {
* personalizationValues,
* updatePersonalizationField,
* toggleUseMemories,
* updateMemoryAtIndex,
* addMemory,
* handleSavePersonalization,
* isSavingPersonalization
* } = useUserPersonalization(user, updateUserPersonalization, {
* onSuccess: () => console.log("Saved!"),
* onError: () => console.log("Failed!")
* });
*
* return (
* <div>
* <input
* value={personalizationValues.name}
* onChange={(e) => updatePersonalizationField("name", e.target.value)}
* />
* <button
* onClick={handleSavePersonalization}
* disabled={isSavingPersonalization}
* >
* Save
* </button>
* </div>
* );
* }
* ```
*
* @remarks
* - Changes are optimistic - UI updates immediately before server persistence
* - On error, state reverts to the last known good value from the user object
* - Memories are automatically trimmed and filtered (empty strings removed) on save
* - The hook synchronizes with user prop changes to stay in sync with external updates
*/
export default function useUserPersonalization(
user: User | null,
persistPersonalization: (
personalization: UserPersonalization

View File

@@ -1,17 +0,0 @@
import useSWR from "swr";
import { CCPairBasicInfo } from "@/lib/types";
import { errorHandlingFetcher } from "@/lib/fetcher";
export function useCCPairs() {
const { data, error, mutate } = useSWR<CCPairBasicInfo[]>(
"/api/manage/connector-status",
errorHandlingFetcher
);
return {
ccPairs: data,
isLoading: !error && !data,
error,
refetch: mutate,
};
}

View File

@@ -1,27 +0,0 @@
import { useMemo } from "react";
import useSWR from "swr";
import { FederatedConnectorOAuthStatus } from "@/components/chat/FederatedOAuthModal";
import { errorHandlingFetcher } from "@/lib/fetcher";
export function useFederatedOAuthStatus() {
const { data, error, mutate } = useSWR<FederatedConnectorOAuthStatus[]>(
"/api/federated/oauth-status",
errorHandlingFetcher
);
const connectors = data ?? [];
const needsAuth = useMemo(
() => connectors.filter((c) => !c.has_oauth_token),
[connectors]
);
const hasUnauthenticatedConnectors = needsAuth.length > 0;
return {
connectors,
needsAuth,
hasUnauthenticatedConnectors,
loading: !error && !data,
error,
refetch: mutate,
};
}

View File

@@ -1,21 +0,0 @@
import useSWR from "swr";
import { InputPrompt } from "@/app/chat/interfaces";
import { errorHandlingFetcher } from "@/lib/fetcher";
export function useInputPrompts() {
const { data, error, mutate } = useSWR<InputPrompt[]>(
"/api/input_prompt?include_public=true",
errorHandlingFetcher,
{
revalidateOnFocus: false,
dedupingInterval: 60000,
}
);
return {
inputPrompts: data ?? [],
isLoading: !error && !data,
error,
refresh: mutate,
};
}

View File

@@ -66,7 +66,7 @@ function LLMProviderCardInner({
disabled && "opacity-50 cursor-not-allowed"
)}
>
<div className="flex items-center gap-1 p-1 flex-1 min-w-0">
<div className="flex gap-1 p-1 flex-1 min-w-0">
<div className="flex items-start h-full pt-0.5">
{providerName ? (
<ProviderIcon provider={providerName} size={16} className="" />

View File

@@ -26,7 +26,7 @@ import { ValidSources } from "@/lib/types";
import { SourceMetadata } from "@/lib/search/interfaces";
import { SourceIcon } from "@/components/SourceIcon";
import { useAvailableTools } from "@/hooks/useAvailableTools";
import { useCCPairs } from "@/lib/hooks/useCCPairs";
import useCCPairs from "@/hooks/useCCPairs";
import IconButton from "@/refresh-components/buttons/IconButton";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import { useToolOAuthStatus } from "@/lib/hooks/useToolOAuthStatus";
@@ -147,7 +147,7 @@ export default function ActionsPopover({
const availableToolIds = availableTools.map((tool) => tool.id);
// Check if there are any connectors available
const hasNoConnectors = !ccPairs || ccPairs.length === 0;
const hasNoConnectors = ccPairs.length === 0;
const assistantPreference = assistantPreferences?.[selectedAssistant.id];
const disabledToolIds = assistantPreference?.disabled_tool_ids || [];

View File

@@ -19,10 +19,10 @@ import { deleteAllChatSessions } from "@/app/chat/services/lib";
import { SourceIcon } from "@/components/SourceIcon";
import { ValidSources } from "@/lib/types";
import { getSourceMetadata } from "@/lib/sources";
import { useFederatedOAuthStatus } from "@/lib/hooks/useFederatedOAuthStatus";
import { useCCPairs } from "@/lib/hooks/useCCPairs";
import useFederatedOAuthStatus from "@/hooks/useFederatedOAuthStatus";
import useCCPairs from "@/hooks/useCCPairs";
import { useLLMProviders } from "@/lib/hooks/useLLMProviders";
import { useUserPersonalization } from "@/lib/hooks/useUserPersonalization";
import useUserPersonalization from "@/hooks/useUserPersonalization";
import Text from "@/refresh-components/texts/Text";
import PATManagement from "@/components/user/PATManagement";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
@@ -85,7 +85,7 @@ export default function UserSettings() {
const {
connectors: federatedConnectors,
refetch: refetchFederatedConnectors,
loading: isFederatedLoading,
isLoading: isFederatedLoading,
} = useFederatedOAuthStatus();
const { ccPairs, isLoading: isCCPairsLoading } = useCCPairs();
@@ -102,9 +102,7 @@ export default function UserSettings() {
// Use currentDefaultModel for display, falling back to defaultModel
const displayModel = currentDefaultModel ?? defaultModel;
const hasConnectors =
(ccPairs && ccPairs.length > 0) ||
(federatedConnectors && federatedConnectors.length > 0);
const hasConnectors = ccPairs.length > 0 || federatedConnectors.length > 0;
const isLoadingConnectors = isCCPairsLoading || isFederatedLoading;