Compare commits

..

8 Commits

Author SHA1 Message Date
Nik
fe942daf30 refactor(be): migrate search_settings & image_generation to OnyxError
- Replace all HTTPException raises with OnyxError in:
  - backend/onyx/server/manage/search_settings.py (6 occurrences)
  - backend/onyx/server/manage/image_generation/api.py (17 occurrences)
- Update integration tests for new response shape (detail → message)
- "already exists" now returns 409 (DUPLICATE_RESOURCE) instead of 400

Error code mappings:
- 501 NOT_IMPLEMENTED → OnyxErrorCode.NOT_IMPLEMENTED
- 400 validation → OnyxErrorCode.VALIDATION_ERROR
- 404 not found → OnyxErrorCode.NOT_FOUND
- 400 duplicate → OnyxErrorCode.DUPLICATE_RESOURCE (409)
- 400/401 bad creds → OnyxErrorCode.CREDENTIAL_INVALID
2026-03-04 14:17:27 -08:00
Nik
edad23a7b7 fix(test): update persona access tests for OnyxError response shape 2026-03-04 12:55:33 -08:00
Nik
94ab65e47d fix(test): update integration tests for OnyxError response shape
- "detail" → "message" in response body assertions
- 400 → 409 for duplicate provider (DUPLICATE_RESOURCE)
2026-03-04 12:03:09 -08:00
Nik
cce2a2c2d4 fix(test): update api_base tests to expect OnyxError instead of HTTPException 2026-03-04 11:37:00 -08:00
Nik
628b88740f fix(test): update LLM provider tests to expect OnyxError
Update 3 test assertions from pytest.raises(HTTPException) to
pytest.raises(OnyxError) to match the migrated error handling.
2026-03-04 11:15:30 -08:00
Nik
06cf7c5bdd fix(be): address Greptile review feedback
- Bedrock credential/config errors → CREDENTIAL_INVALID (not BAD_GATEWAY)
- Move guard check outside try block in delete_llm_provider for clarity
2026-03-04 10:52:31 -08:00
Nik
b9c7c1cd3b fix(be): use semantic error codes instead of blanket VALIDATION_ERROR
- "already exists" → DUPLICATE_RESOURCE (409)
- "does not exist" on update → NOT_FOUND (404)
- External service failures (Bedrock/Ollama/OpenRouter) → BAD_GATEWAY (502)
2026-03-04 10:33:02 -08:00
Nik
4dfc64d6cf refactor(be): migrate LLM & embedding management to OnyxError
Replace all HTTPException raises in manage/llm/api.py (25) and
manage/embedding/api.py (2) with OnyxError using standardized error
codes. Part of the ongoing OnyxError rollout.
2026-03-04 10:17:25 -08:00
275 changed files with 1487 additions and 4185 deletions

View File

@@ -335,6 +335,7 @@ jobs:
# TODO(Nik): https://linear.app/onyx-app/issue/ENG-1/update-test-infra-to-use-test-license
LICENSE_ENFORCEMENT_ENABLED=false
CHECK_TTL_MANAGEMENT_TASK_FREQUENCY_IN_HOURS=0.001
USE_LIGHTWEIGHT_BACKGROUND_WORKER=false
EOF
fi

View File

@@ -268,11 +268,10 @@ jobs:
persist-credentials: false
- name: Setup node
# zizmor: ignore[cache-poisoning] ephemeral runners; no release artifacts
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # ratchet:actions/setup-node@v4
with:
node-version: 22
cache: "npm" # zizmor: ignore[cache-poisoning]
cache: "npm"
cache-dependency-path: ./web/package-lock.json
- name: Install node dependencies
@@ -280,7 +279,6 @@ jobs:
run: npm ci
- name: Cache playwright cache
# zizmor: ignore[cache-poisoning] ephemeral runners; no release artifacts
uses: runs-on/cache@50350ad4242587b6c8c2baa2e740b1bc11285ff4 # ratchet:runs-on/cache@v4
with:
path: ~/.cache/ms-playwright
@@ -592,108 +590,6 @@ jobs:
name: docker-logs-${{ matrix.project }}-${{ github.run_id }}
path: ${{ github.workspace }}/docker-compose.log
playwright-tests-lite:
needs: [build-web-image, build-backend-image]
name: Playwright Tests (lite)
runs-on:
- runs-on
- runner=4cpu-linux-arm64
- "run-id=${{ github.run_id }}-playwright-tests-lite"
- "extras=ecr-cache"
timeout-minutes: 30
steps:
- uses: runs-on/action@cd2b598b0515d39d78c38a02d529db87d2196d1e # ratchet:runs-on/action@v2
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # ratchet:actions/checkout@v6
with:
persist-credentials: false
- name: Setup node
# zizmor: ignore[cache-poisoning] ephemeral runners; no release artifacts
uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # ratchet:actions/setup-node@v4
with:
node-version: 22
cache: "npm" # zizmor: ignore[cache-poisoning]
cache-dependency-path: ./web/package-lock.json
- name: Install node dependencies
working-directory: ./web
run: npm ci
- name: Cache playwright cache
# zizmor: ignore[cache-poisoning] ephemeral runners; no release artifacts
uses: runs-on/cache@50350ad4242587b6c8c2baa2e740b1bc11285ff4 # ratchet:runs-on/cache@v4
with:
path: ~/.cache/ms-playwright
key: ${{ runner.os }}-playwright-npm-${{ hashFiles('web/package-lock.json') }}
restore-keys: |
${{ runner.os }}-playwright-npm-
- name: Install playwright browsers
working-directory: ./web
run: npx playwright install --with-deps
- name: Create .env file for Docker Compose
env:
OPENAI_API_KEY_VALUE: ${{ env.OPENAI_API_KEY }}
ECR_CACHE: ${{ env.RUNS_ON_ECR_CACHE }}
RUN_ID: ${{ github.run_id }}
run: |
cat <<EOF > deployment/docker_compose/.env
ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=true
LICENSE_ENFORCEMENT_ENABLED=false
AUTH_TYPE=basic
INTEGRATION_TESTS_MODE=true
GEN_AI_API_KEY=${OPENAI_API_KEY_VALUE}
MOCK_LLM_RESPONSE=true
REQUIRE_EMAIL_VERIFICATION=false
DISABLE_TELEMETRY=true
ONYX_BACKEND_IMAGE=${ECR_CACHE}:playwright-test-backend-${RUN_ID}
ONYX_WEB_SERVER_IMAGE=${ECR_CACHE}:playwright-test-web-${RUN_ID}
EOF
# needed for pulling external images otherwise, we hit the "Unauthenticated users" limit
# https://docs.docker.com/docker-hub/usage/
- name: Login to Docker Hub
uses: docker/login-action@c94ce9fb468520275223c153574b00df6fe4bcc9 # ratchet:docker/login-action@v3
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_TOKEN }}
- name: Start Docker containers (lite)
run: |
cd deployment/docker_compose
docker compose -f docker-compose.yml -f docker-compose.onyx-lite.yml -f docker-compose.dev.yml up -d
id: start_docker
- name: Run Playwright tests (lite)
working-directory: ./web
run: npx playwright test --project lite
- uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
if: always()
with:
name: playwright-test-results-lite-${{ github.run_id }}
path: ./web/output/playwright/
retention-days: 30
- name: Save Docker logs
if: success() || failure()
env:
WORKSPACE: ${{ github.workspace }}
run: |
cd deployment/docker_compose
docker compose logs > docker-compose.log
mv docker-compose.log ${WORKSPACE}/docker-compose.log
- name: Upload logs
if: success() || failure()
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f
with:
name: docker-logs-lite-${{ github.run_id }}
path: ${{ github.workspace }}/docker-compose.log
# Post a single combined visual regression comment after all matrix jobs finish
visual-regression-comment:
needs: [playwright-tests]
@@ -790,7 +686,7 @@ jobs:
# NOTE: Github-hosted runners have about 20s faster queue times and are preferred here.
runs-on: ubuntu-slim
timeout-minutes: 45
needs: [playwright-tests, playwright-tests-lite]
needs: [playwright-tests]
if: ${{ always() }}
steps:
- name: Check job status

43
.vscode/launch.json vendored
View File

@@ -40,7 +40,19 @@
}
},
{
"name": "Celery",
"name": "Celery (lightweight mode)",
"configurations": [
"Celery primary",
"Celery background",
"Celery beat"
],
"presentation": {
"group": "1"
},
"stopAll": true
},
{
"name": "Celery (standard mode)",
"configurations": [
"Celery primary",
"Celery light",
@@ -241,6 +253,35 @@
},
"consoleTitle": "Celery light Console"
},
{
"name": "Celery background",
"type": "debugpy",
"request": "launch",
"module": "celery",
"cwd": "${workspaceFolder}/backend",
"envFile": "${workspaceFolder}/.vscode/.env",
"env": {
"LOG_LEVEL": "INFO",
"PYTHONUNBUFFERED": "1",
"PYTHONPATH": "."
},
"args": [
"-A",
"onyx.background.celery.versioned_apps.background",
"worker",
"--pool=threads",
"--concurrency=20",
"--prefetch-multiplier=4",
"--loglevel=INFO",
"--hostname=background@%n",
"-Q",
"vespa_metadata_sync,connector_deletion,doc_permissions_upsert,checkpoint_cleanup,index_attempt_cleanup,docprocessing,connector_doc_fetching,connector_pruning,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,kg_processing,monitoring,user_file_processing,user_file_project_sync,user_file_delete,opensearch_migration"
],
"presentation": {
"group": "2"
},
"consoleTitle": "Celery background Console"
},
{
"name": "Celery heavy",
"type": "debugpy",

View File

@@ -86,6 +86,37 @@ Onyx uses Celery for asynchronous task processing with multiple specialized work
- Monitoring tasks (every 5 minutes)
- Cleanup tasks (hourly)
#### Worker Deployment Modes
Onyx supports two deployment modes for background workers, controlled by the `USE_LIGHTWEIGHT_BACKGROUND_WORKER` environment variable:
**Lightweight Mode** (default, `USE_LIGHTWEIGHT_BACKGROUND_WORKER=true`):
- Runs a single consolidated `background` worker that handles all background tasks:
- Light worker tasks (Vespa operations, permissions sync, deletion)
- Document processing (indexing pipeline)
- Document fetching (connector data retrieval)
- Pruning operations (from `heavy` worker)
- Knowledge graph processing (from `kg_processing` worker)
- Monitoring tasks (from `monitoring` worker)
- User file processing (from `user_file_processing` worker)
- Lower resource footprint (fewer worker processes)
- Suitable for smaller deployments or development environments
- Default concurrency: 20 threads (increased to handle combined workload)
**Standard Mode** (`USE_LIGHTWEIGHT_BACKGROUND_WORKER=false`):
- Runs separate specialized workers as documented above (light, docprocessing, docfetching, heavy, kg_processing, monitoring, user_file_processing)
- Better isolation and scalability
- Can scale individual workers independently based on workload
- Suitable for production deployments with higher load
The deployment mode affects:
- **Backend**: Worker processes spawned by supervisord or dev scripts
- **Helm**: Which Kubernetes deployments are created
- **Dev Environment**: Which workers `dev_run_background_jobs.py` spawns
#### Key Features
- **Thread-based Workers**: All workers use thread pools (not processes) for stability

View File

@@ -0,0 +1,15 @@
from onyx.background.celery.apps import app_base
from onyx.background.celery.apps.background import celery_app
celery_app.autodiscover_tasks(
app_base.filter_task_modules(
[
"ee.onyx.background.celery.tasks.doc_permission_syncing",
"ee.onyx.background.celery.tasks.external_group_syncing",
"ee.onyx.background.celery.tasks.cleanup",
"ee.onyx.background.celery.tasks.tenant_provisioning",
"ee.onyx.background.celery.tasks.query_history",
]
)
)

View File

@@ -15,7 +15,6 @@ from sqlalchemy.orm import Session
from ee.onyx.server.user_group.models import SetCuratorRequest
from ee.onyx.server.user_group.models import UserGroupCreate
from ee.onyx.server.user_group.models import UserGroupUpdate
from onyx.configs.app_configs import DISABLE_VECTOR_DB
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
from onyx.db.enums import AccessType
from onyx.db.enums import ConnectorCredentialPairStatus
@@ -472,9 +471,7 @@ def _add_user_group__cc_pair_relationships__no_commit(
def insert_user_group(db_session: Session, user_group: UserGroupCreate) -> UserGroup:
db_user_group = UserGroup(
name=user_group.name,
time_last_modified_by_user=func.now(),
is_up_to_date=DISABLE_VECTOR_DB,
name=user_group.name, time_last_modified_by_user=func.now()
)
db_session.add(db_user_group)
db_session.flush() # give the group an ID
@@ -777,7 +774,8 @@ def update_user_group(
cc_pair_ids=user_group_update.cc_pair_ids,
)
if cc_pairs_updated and not DISABLE_VECTOR_DB:
# only needs to sync with Vespa if the cc_pairs have been updated
if cc_pairs_updated:
db_user_group.is_up_to_date = False
removed_users = db_session.scalars(

View File

@@ -223,15 +223,6 @@ def get_active_scim_token(
token = dal.get_active_token()
if not token:
raise HTTPException(status_code=404, detail="No active SCIM token")
# Derive the IdP domain from the first synced user as a heuristic.
idp_domain: str | None = None
mappings, _total = dal.list_user_mappings(start_index=1, count=1)
if mappings:
user = dal.get_user(mappings[0].user_id)
if user and "@" in user.email:
idp_domain = user.email.rsplit("@", 1)[1]
return ScimTokenResponse(
id=token.id,
name=token.name,
@@ -239,7 +230,6 @@ def get_active_scim_token(
is_active=token.is_active,
created_at=token.created_at,
last_used_at=token.last_used_at,
idp_domain=idp_domain,
)

View File

@@ -365,7 +365,6 @@ class ScimTokenResponse(BaseModel):
is_active: bool
created_at: datetime
last_used_at: datetime | None = None
idp_domain: str | None = None
class ScimTokenCreatedResponse(ScimTokenResponse):

View File

@@ -5,8 +5,6 @@ from sqlalchemy.exc import IntegrityError
from sqlalchemy.orm import Session
from ee.onyx.db.user_group import add_users_to_user_group
from ee.onyx.db.user_group import delete_user_group as db_delete_user_group
from ee.onyx.db.user_group import fetch_user_group
from ee.onyx.db.user_group import fetch_user_groups
from ee.onyx.db.user_group import fetch_user_groups_for_user
from ee.onyx.db.user_group import insert_user_group
@@ -22,7 +20,6 @@ from ee.onyx.server.user_group.models import UserGroupUpdate
from onyx.auth.users import current_admin_user
from onyx.auth.users import current_curator_or_admin_user
from onyx.auth.users import current_user
from onyx.configs.app_configs import DISABLE_VECTOR_DB
from onyx.configs.constants import PUBLIC_API_TAGS
from onyx.db.engine.sql_engine import get_session
from onyx.db.models import User
@@ -156,8 +153,3 @@ def delete_user_group(
prepare_user_group_for_deletion(db_session, user_group_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
if DISABLE_VECTOR_DB:
user_group = fetch_user_group(db_session, user_group_id)
if user_group:
db_delete_user_group(db_session, user_group)

View File

@@ -0,0 +1,142 @@
from typing import Any
from typing import cast
from celery import Celery
from celery import signals
from celery import Task
from celery.apps.worker import Worker
from celery.signals import celeryd_init
from celery.signals import worker_init
from celery.signals import worker_process_init
from celery.signals import worker_ready
from celery.signals import worker_shutdown
import onyx.background.celery.apps.app_base as app_base
from onyx.background.celery.celery_utils import httpx_init_vespa_pool
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 POSTGRES_CELERY_WORKER_BACKGROUND_APP_NAME
from onyx.db.engine.sql_engine import SqlEngine
from onyx.utils.logger import setup_logger
from shared_configs.configs import MULTI_TENANT
logger = setup_logger()
celery_app = Celery(__name__)
celery_app.config_from_object("onyx.background.celery.configs.background")
celery_app.Task = app_base.TenantAwareTask # type: ignore [misc]
@signals.task_prerun.connect
def on_task_prerun(
sender: Any | None = None,
task_id: str | None = None,
task: Task | None = None,
args: tuple | None = None,
kwargs: dict | None = None,
**kwds: Any,
) -> None:
app_base.on_task_prerun(sender, task_id, task, args, kwargs, **kwds)
@signals.task_postrun.connect
def on_task_postrun(
sender: Any | None = None,
task_id: str | None = None,
task: Task | None = None,
args: tuple | None = None,
kwargs: dict | None = None,
retval: Any | None = None,
state: str | None = None,
**kwds: Any,
) -> None:
app_base.on_task_postrun(sender, task_id, task, args, kwargs, retval, state, **kwds)
@celeryd_init.connect
def on_celeryd_init(sender: str, conf: Any = None, **kwargs: Any) -> None:
app_base.on_celeryd_init(sender, conf, **kwargs)
@worker_init.connect
def on_worker_init(sender: Worker, **kwargs: Any) -> None:
EXTRA_CONCURRENCY = 8 # small extra fudge factor for connection limits
logger.info("worker_init signal received for consolidated background worker.")
SqlEngine.set_app_name(POSTGRES_CELERY_WORKER_BACKGROUND_APP_NAME)
pool_size = cast(int, sender.concurrency) # type: ignore
SqlEngine.init_engine(pool_size=pool_size, max_overflow=EXTRA_CONCURRENCY)
# Initialize Vespa httpx pool (needed for light worker tasks)
if MANAGED_VESPA:
httpx_init_vespa_pool(
sender.concurrency + EXTRA_CONCURRENCY, # type: ignore
ssl_cert=VESPA_CLOUD_CERT_PATH,
ssl_key=VESPA_CLOUD_KEY_PATH,
)
else:
httpx_init_vespa_pool(sender.concurrency + EXTRA_CONCURRENCY) # type: ignore
app_base.wait_for_redis(sender, **kwargs)
app_base.wait_for_db(sender, **kwargs)
app_base.wait_for_vespa_or_shutdown(sender, **kwargs)
# Less startup checks in multi-tenant case
if MULTI_TENANT:
return
app_base.on_secondary_worker_init(sender, **kwargs)
@worker_ready.connect
def on_worker_ready(sender: Any, **kwargs: Any) -> None:
app_base.on_worker_ready(sender, **kwargs)
@worker_shutdown.connect
def on_worker_shutdown(sender: Any, **kwargs: Any) -> None:
app_base.on_worker_shutdown(sender, **kwargs)
@worker_process_init.connect
def init_worker(**kwargs: Any) -> None: # noqa: ARG001
SqlEngine.reset_engine()
@signals.setup_logging.connect
def on_setup_logging(
loglevel: Any, logfile: Any, format: Any, colorize: Any, **kwargs: Any
) -> None:
app_base.on_setup_logging(loglevel, logfile, format, colorize, **kwargs)
base_bootsteps = app_base.get_bootsteps()
for bootstep in base_bootsteps:
celery_app.steps["worker"].add(bootstep)
celery_app.autodiscover_tasks(
app_base.filter_task_modules(
[
# Original background worker tasks
"onyx.background.celery.tasks.pruning",
"onyx.background.celery.tasks.monitoring",
"onyx.background.celery.tasks.user_file_processing",
"onyx.background.celery.tasks.llm_model_update",
# Light worker tasks
"onyx.background.celery.tasks.shared",
"onyx.background.celery.tasks.vespa",
"onyx.background.celery.tasks.connector_deletion",
"onyx.background.celery.tasks.doc_permission_syncing",
"onyx.background.celery.tasks.opensearch_migration",
# Docprocessing worker tasks
"onyx.background.celery.tasks.docprocessing",
# Docfetching worker tasks
"onyx.background.celery.tasks.docfetching",
# Sandbox cleanup tasks (isolated in build feature)
"onyx.server.features.build.sandbox.tasks",
]
)
)

View File

@@ -0,0 +1,23 @@
import onyx.background.celery.configs.base as shared_config
from onyx.configs.app_configs import CELERY_WORKER_BACKGROUND_CONCURRENCY
broker_url = shared_config.broker_url
broker_connection_retry_on_startup = shared_config.broker_connection_retry_on_startup
broker_pool_limit = shared_config.broker_pool_limit
broker_transport_options = shared_config.broker_transport_options
redis_socket_keepalive = shared_config.redis_socket_keepalive
redis_retry_on_timeout = shared_config.redis_retry_on_timeout
redis_backend_health_check_interval = shared_config.redis_backend_health_check_interval
result_backend = shared_config.result_backend
result_expires = shared_config.result_expires # 86400 seconds is the default
task_default_priority = shared_config.task_default_priority
task_acks_late = shared_config.task_acks_late
worker_concurrency = CELERY_WORKER_BACKGROUND_CONCURRENCY
worker_pool = "threads"
# Increased from 1 to 4 to handle fast light worker tasks more efficiently
# This allows the worker to prefetch multiple tasks per thread
worker_prefetch_multiplier = 4

View File

@@ -0,0 +1,10 @@
from celery import Celery
from onyx.utils.variable_functionality import fetch_versioned_implementation
from onyx.utils.variable_functionality import set_is_ee_based_on_env_variable
set_is_ee_based_on_env_variable()
app: Celery = fetch_versioned_implementation(
"onyx.background.celery.apps.background",
"celery_app",
)

View File

@@ -495,7 +495,14 @@ CELERY_WORKER_PRIMARY_POOL_OVERFLOW = int(
os.environ.get("CELERY_WORKER_PRIMARY_POOL_OVERFLOW") or 4
)
# Individual worker concurrency settings
# Consolidated background worker (light, docprocessing, docfetching, heavy, monitoring, user_file_processing)
# separate workers' defaults: light=24, docprocessing=6, docfetching=1, heavy=4, kg=2, monitoring=1, user_file=2
# Total would be 40, but we use a more conservative default of 20 for the consolidated worker
CELERY_WORKER_BACKGROUND_CONCURRENCY = int(
os.environ.get("CELERY_WORKER_BACKGROUND_CONCURRENCY") or 20
)
# Individual worker concurrency settings (used when USE_LIGHTWEIGHT_BACKGROUND_WORKER is False or on Kuberenetes deployments)
CELERY_WORKER_HEAVY_CONCURRENCY = int(
os.environ.get("CELERY_WORKER_HEAVY_CONCURRENCY") or 4
)

View File

@@ -84,6 +84,7 @@ POSTGRES_CELERY_WORKER_LIGHT_APP_NAME = "celery_worker_light"
POSTGRES_CELERY_WORKER_DOCPROCESSING_APP_NAME = "celery_worker_docprocessing"
POSTGRES_CELERY_WORKER_DOCFETCHING_APP_NAME = "celery_worker_docfetching"
POSTGRES_CELERY_WORKER_INDEXING_CHILD_APP_NAME = "celery_worker_indexing_child"
POSTGRES_CELERY_WORKER_BACKGROUND_APP_NAME = "celery_worker_background"
POSTGRES_CELERY_WORKER_HEAVY_APP_NAME = "celery_worker_heavy"
POSTGRES_CELERY_WORKER_MONITORING_APP_NAME = "celery_worker_monitoring"
POSTGRES_CELERY_WORKER_USER_FILE_PROCESSING_APP_NAME = (

View File

@@ -13,7 +13,6 @@ from sqlalchemy.orm import aliased
from sqlalchemy.orm import selectinload
from sqlalchemy.orm import Session
from onyx.configs.app_configs import DISABLE_VECTOR_DB
from onyx.db.connector_credential_pair import get_cc_pair_groups_for_ids
from onyx.db.connector_credential_pair import get_connector_credential_pairs
from onyx.db.enums import AccessType
@@ -247,7 +246,6 @@ def insert_document_set(
description=document_set_creation_request.description,
user_id=user_id,
is_public=document_set_creation_request.is_public,
is_up_to_date=DISABLE_VECTOR_DB,
time_last_modified_by_user=func.now(),
)
db_session.add(new_document_set_row)
@@ -338,8 +336,7 @@ def update_document_set(
)
document_set_row.description = document_set_update_request.description
if not DISABLE_VECTOR_DB:
document_set_row.is_up_to_date = False
document_set_row.is_up_to_date = False
document_set_row.is_public = document_set_update_request.is_public
document_set_row.time_last_modified_by_user = func.now()
versioned_private_doc_set_fn = fetch_versioned_implementation(

View File

@@ -130,7 +130,7 @@ def format_slack_message(message: str | None) -> str:
message = _transform_outside_code_blocks(message, _sanitize_html)
message = _convert_slack_links_to_markdown(message)
normalized_message = _normalize_link_destinations(message)
md = create_markdown(renderer=SlackRenderer(), plugins=["strikethrough", "table"])
md = create_markdown(renderer=SlackRenderer(), plugins=["strikethrough"])
result = md(normalized_message)
# With HTMLRenderer, result is always str (not AST list)
assert isinstance(result, str)
@@ -146,11 +146,6 @@ class SlackRenderer(HTMLRenderer):
SPECIALS: dict[str, str] = {"&": "&amp;", "<": "&lt;", ">": "&gt;"}
def __init__(self) -> None:
super().__init__()
self._table_headers: list[str] = []
self._current_row_cells: list[str] = []
def escape_special(self, text: str) -> str:
for special, replacement in self.SPECIALS.items():
text = text.replace(special, replacement)
@@ -223,48 +218,5 @@ class SlackRenderer(HTMLRenderer):
# as literal &quot; text since Slack doesn't recognize that entity.
return self.escape_special(text)
# -- Table rendering (converts markdown tables to vertical cards) --
def table_cell(
self, text: str, align: str | None = None, head: bool = False # noqa: ARG002
) -> str:
if head:
self._table_headers.append(text.strip())
else:
self._current_row_cells.append(text.strip())
return ""
def table_head(self, text: str) -> str: # noqa: ARG002
self._current_row_cells = []
return ""
def table_row(self, text: str) -> str: # noqa: ARG002
cells = self._current_row_cells
self._current_row_cells = []
# First column becomes the bold title, remaining columns are bulleted fields
lines: list[str] = []
if cells:
title = cells[0]
if title:
# Avoid double-wrapping if cell already contains bold markup
if title.startswith("*") and title.endswith("*") and len(title) > 1:
lines.append(title)
else:
lines.append(f"*{title}*")
for i, cell in enumerate(cells[1:], start=1):
if i < len(self._table_headers):
lines.append(f"{self._table_headers[i]}: {cell}")
else:
lines.append(f"{cell}")
return "\n".join(lines) + "\n\n"
def table_body(self, text: str) -> str:
return text
def table(self, text: str) -> str:
self._table_headers = []
self._current_row_cells = []
return text + "\n"
def paragraph(self, text: str) -> str:
return f"{text}\n\n"

View File

@@ -7424,9 +7424,9 @@
}
},
"node_modules/hono": {
"version": "4.12.5",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz",
"integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==",
"version": "4.11.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"

View File

@@ -11,7 +11,6 @@ from onyx.configs.app_configs import DISABLE_VECTOR_DB
from onyx.configs.constants import OnyxCeleryPriority
from onyx.configs.constants import OnyxCeleryTask
from onyx.db.document_set import check_document_sets_are_public
from onyx.db.document_set import delete_document_set as db_delete_document_set
from onyx.db.document_set import fetch_all_document_sets_for_user
from onyx.db.document_set import get_document_set_by_id
from onyx.db.document_set import insert_document_set
@@ -143,10 +142,7 @@ def delete_document_set(
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
if DISABLE_VECTOR_DB:
db_session.refresh(document_set)
db_delete_document_set(document_set, db_session)
else:
if not DISABLE_VECTOR_DB:
client_app.send_task(
OnyxCeleryTask.CHECK_FOR_VESPA_SYNC_TASK,
kwargs={"tenant_id": tenant_id},

View File

@@ -1,6 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
@@ -15,6 +14,8 @@ from onyx.db.llm import remove_llm_provider__no_commit
from onyx.db.models import LLMProvider as LLMProviderModel
from onyx.db.models import ModelConfiguration
from onyx.db.models import User
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.image_gen.exceptions import ImageProviderCredentialsError
from onyx.image_gen.factory import get_image_generation_provider
from onyx.image_gen.factory import validate_credentials
@@ -74,9 +75,9 @@ def _build_llm_provider_request(
# Clone mode: Only use API key from source provider
source_provider = db_session.get(LLMProviderModel, source_llm_provider_id)
if not source_provider:
raise HTTPException(
status_code=404,
detail=f"Source LLM provider with id {source_llm_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"Source LLM provider with id {source_llm_provider_id} not found",
)
_validate_llm_provider_change(
@@ -110,9 +111,9 @@ def _build_llm_provider_request(
)
if not provider:
raise HTTPException(
status_code=400,
detail="No provider or source llm provided",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No provider or source llm provided",
)
credentials = ImageGenerationProviderCredentials(
@@ -124,9 +125,9 @@ def _build_llm_provider_request(
)
if not validate_credentials(provider, credentials):
raise HTTPException(
status_code=400,
detail=f"Incorrect credentials for {provider}",
raise OnyxError(
OnyxErrorCode.CREDENTIAL_INVALID,
f"Incorrect credentials for {provider}",
)
return LLMProviderUpsertRequest(
@@ -215,9 +216,9 @@ def test_image_generation(
LLMProviderModel, test_request.source_llm_provider_id
)
if not source_provider:
raise HTTPException(
status_code=404,
detail=f"Source LLM provider with id {test_request.source_llm_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"Source LLM provider with id {test_request.source_llm_provider_id} not found",
)
_validate_llm_provider_change(
@@ -236,9 +237,9 @@ def test_image_generation(
provider = source_provider.provider
if provider is None:
raise HTTPException(
status_code=400,
detail="No provider or source llm provided",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No provider or source llm provided",
)
try:
@@ -257,14 +258,14 @@ def test_image_generation(
),
)
except ValueError:
raise HTTPException(
status_code=404,
detail=f"Invalid image generation provider: {provider}",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"Invalid image generation provider: {provider}",
)
except ImageProviderCredentialsError:
raise HTTPException(
status_code=401,
detail="Invalid image generation credentials",
raise OnyxError(
OnyxErrorCode.CREDENTIAL_INVALID,
"Invalid image generation credentials",
)
quality = _get_test_quality_for_model(test_request.model_name)
@@ -276,15 +277,15 @@ def test_image_generation(
n=1,
quality=quality,
)
except HTTPException:
except OnyxError:
raise
except Exception as e:
# Log only exception type to avoid exposing sensitive data
# (LiteLLM errors may contain URLs with API keys or auth tokens)
logger.warning(f"Image generation test failed: {type(e).__name__}")
raise HTTPException(
status_code=400,
detail=f"Image generation test failed: {type(e).__name__}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Image generation test failed: {type(e).__name__}",
)
@@ -309,9 +310,9 @@ def create_config(
db_session, config_create.image_provider_id
)
if existing_config:
raise HTTPException(
status_code=400,
detail=f"ImageGenerationConfig with image_provider_id '{config_create.image_provider_id}' already exists",
raise OnyxError(
OnyxErrorCode.DUPLICATE_RESOURCE,
f"ImageGenerationConfig with image_provider_id '{config_create.image_provider_id}' already exists",
)
try:
@@ -345,10 +346,10 @@ def create_config(
db_session.commit()
db_session.refresh(config)
return ImageGenerationConfigView.from_model(config)
except HTTPException:
except OnyxError:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
@admin_router.get("/config")
@@ -373,9 +374,9 @@ def get_config_credentials(
"""
config = get_image_generation_config(db_session, image_provider_id)
if not config:
raise HTTPException(
status_code=404,
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
)
return ImageGenerationCredentials.from_model(config)
@@ -401,9 +402,9 @@ def update_config(
# 1. Get existing config
existing_config = get_image_generation_config(db_session, image_provider_id)
if not existing_config:
raise HTTPException(
status_code=404,
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
)
old_llm_provider_id = existing_config.model_configuration.llm_provider_id
@@ -472,10 +473,10 @@ def update_config(
db_session.refresh(existing_config)
return ImageGenerationConfigView.from_model(existing_config)
except HTTPException:
except OnyxError:
raise
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
@admin_router.delete("/config/{image_provider_id}")
@@ -489,9 +490,9 @@ def delete_config(
# Get the config first to find the associated LLM provider
existing_config = get_image_generation_config(db_session, image_provider_id)
if not existing_config:
raise HTTPException(
status_code=404,
detail=f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"ImageGenerationConfig with image_provider_id {image_provider_id} not found",
)
llm_provider_id = existing_config.model_configuration.llm_provider_id
@@ -503,10 +504,10 @@ def delete_config(
remove_llm_provider__no_commit(db_session, llm_provider_id)
db_session.commit()
except HTTPException:
except OnyxError:
raise
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))
@admin_router.post("/config/{image_provider_id}/default")
@@ -519,7 +520,7 @@ def set_config_as_default(
try:
set_default_image_generation_config(db_session, image_provider_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))
@admin_router.delete("/config/{image_provider_id}/default")
@@ -532,4 +533,4 @@ def unset_config_as_default(
try:
unset_default_image_generation_config(db_session, image_provider_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))

View File

@@ -1,7 +1,5 @@
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import status
from sqlalchemy.orm import Session
from onyx.auth.users import current_admin_user
@@ -21,6 +19,8 @@ from onyx.db.search_settings import get_secondary_search_settings
from onyx.db.search_settings import update_current_search_settings
from onyx.db.search_settings import update_search_settings_status
from onyx.document_index.factory import get_default_document_index
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.file_processing.unstructured import delete_unstructured_api_key
from onyx.file_processing.unstructured import get_unstructured_api_key
from onyx.file_processing.unstructured import update_unstructured_api_key
@@ -48,9 +48,9 @@ def set_new_search_settings(
# NOTE Enable integration external dependency tests in test_search_settings.py
# when this is reenabled. They are currently skipped
logger.error("Setting new search settings is temporarily disabled.")
raise HTTPException(
status_code=status.HTTP_501_NOT_IMPLEMENTED,
detail="Setting new search settings is temporarily disabled.",
raise OnyxError(
OnyxErrorCode.NOT_IMPLEMENTED,
"Setting new search settings is temporarily disabled.",
)
# if search_settings_new.index_name:
# logger.warning("Index name was specified by request, this is not suggested")
@@ -191,7 +191,7 @@ def delete_search_settings_endpoint(
search_settings_id=deletion_request.search_settings_id,
)
except ValueError as e:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
@router.get("/get-current-search-settings")
@@ -241,9 +241,9 @@ def update_saved_search_settings(
) -> None:
# Disallow contextual RAG for cloud deployments
if MULTI_TENANT and search_settings.enable_contextual_rag:
raise HTTPException(
status_code=status.HTTP_400_BAD_REQUEST,
detail="Contextual RAG disabled in Onyx Cloud",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Contextual RAG disabled in Onyx Cloud",
)
validate_contextual_rag_model(
@@ -297,7 +297,7 @@ def validate_contextual_rag_model(
model_name=model_name,
db_session=db_session,
):
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=error_msg)
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, error_msg)
def _validate_contextual_rag_model(

View File

@@ -1,5 +1,3 @@
from __future__ import annotations
import json
import time
from collections.abc import Generator
@@ -86,19 +84,6 @@ class CodeInterpreterClient:
raise ValueError("CODE_INTERPRETER_BASE_URL not configured")
self.base_url = base_url.rstrip("/")
self.session = requests.Session()
self._closed = False
def __enter__(self) -> CodeInterpreterClient:
return self
def __exit__(self, *args: object) -> None:
self.close()
def close(self) -> None:
if self._closed:
return
self.session.close()
self._closed = True
def _build_payload(
self,
@@ -192,11 +177,8 @@ class CodeInterpreterClient:
yield from self._batch_as_stream(code, stdin, timeout_ms, files)
return
try:
response.raise_for_status()
yield from self._parse_sse(response)
finally:
response.close()
response.raise_for_status()
yield from self._parse_sse(response)
def _parse_sse(
self, response: requests.Response

View File

@@ -111,8 +111,8 @@ class PythonTool(Tool[PythonToolOverrideKwargs]):
if not server.server_enabled:
return False
with CodeInterpreterClient() as client:
return client.health(use_cache=True)
client = CodeInterpreterClient()
return client.health(use_cache=True)
def tool_definition(self) -> dict:
return {
@@ -176,203 +176,196 @@ class PythonTool(Tool[PythonToolOverrideKwargs]):
)
)
# Create Code Interpreter client — context manager ensures
# session.close() is called on every exit path.
with CodeInterpreterClient() as client:
# Stage chat files for execution
files_to_stage: list[FileInput] = []
for ind, chat_file in enumerate(chat_files):
file_name = chat_file.filename or f"file_{ind}"
try:
# Upload to Code Interpreter
ci_file_id = client.upload_file(chat_file.content, file_name)
# Stage for execution
files_to_stage.append({"path": file_name, "file_id": ci_file_id})
logger.info(f"Staged file for Python execution: {file_name}")
except Exception as e:
logger.warning(f"Failed to stage file {file_name}: {e}")
# Create Code Interpreter client
client = CodeInterpreterClient()
# Stage chat files for execution
files_to_stage: list[FileInput] = []
for ind, chat_file in enumerate(chat_files):
file_name = chat_file.filename or f"file_{ind}"
try:
logger.debug(f"Executing code: {code}")
# Upload to Code Interpreter
ci_file_id = client.upload_file(chat_file.content, file_name)
# Execute code with streaming (falls back to batch if unavailable)
stdout_parts: list[str] = []
stderr_parts: list[str] = []
result_event: StreamResultEvent | None = None
# Stage for execution
files_to_stage.append({"path": file_name, "file_id": ci_file_id})
for event in client.execute_streaming(
code=code,
timeout_ms=CODE_INTERPRETER_DEFAULT_TIMEOUT_MS,
files=files_to_stage or None,
):
if isinstance(event, StreamOutputEvent):
if event.stream == "stdout":
stdout_parts.append(event.data)
else:
stderr_parts.append(event.data)
# Emit incremental delta to frontend
self.emitter.emit(
Packet(
placement=placement,
obj=PythonToolDelta(
stdout=(
event.data if event.stream == "stdout" else ""
),
stderr=(
event.data if event.stream == "stderr" else ""
),
),
)
)
elif isinstance(event, StreamResultEvent):
result_event = event
elif isinstance(event, StreamErrorEvent):
raise RuntimeError(f"Code interpreter error: {event.message}")
logger.info(f"Staged file for Python execution: {file_name}")
if result_event is None:
raise RuntimeError(
"Code interpreter stream ended without a result event"
)
except Exception as e:
logger.warning(f"Failed to stage file {file_name}: {e}")
full_stdout = "".join(stdout_parts)
full_stderr = "".join(stderr_parts)
try:
logger.debug(f"Executing code: {code}")
# Truncate output for LLM consumption
truncated_stdout = _truncate_output(
full_stdout, CODE_INTERPRETER_MAX_OUTPUT_LENGTH, "stdout"
)
truncated_stderr = _truncate_output(
full_stderr, CODE_INTERPRETER_MAX_OUTPUT_LENGTH, "stderr"
)
# Execute code with streaming (falls back to batch if unavailable)
stdout_parts: list[str] = []
stderr_parts: list[str] = []
result_event: StreamResultEvent | None = None
# Handle generated files
generated_files: list[PythonExecutionFile] = []
generated_file_ids: list[str] = []
file_ids_to_cleanup: list[str] = []
file_store = get_default_file_store()
for workspace_file in result_event.files:
if workspace_file.kind != "file" or not workspace_file.file_id:
continue
try:
# Download file from Code Interpreter
file_content = client.download_file(workspace_file.file_id)
# Determine MIME type from file extension
filename = workspace_file.path.split("/")[-1]
mime_type, _ = mimetypes.guess_type(filename)
# Default to binary if we can't determine the type
mime_type = mime_type or "application/octet-stream"
# Save to Onyx file store
onyx_file_id = file_store.save_file(
content=BytesIO(file_content),
display_name=filename,
file_origin=FileOrigin.CHAT_UPLOAD,
file_type=mime_type,
)
generated_files.append(
PythonExecutionFile(
filename=filename,
file_link=build_full_frontend_file_url(onyx_file_id),
)
)
generated_file_ids.append(onyx_file_id)
# Mark for cleanup
file_ids_to_cleanup.append(workspace_file.file_id)
except Exception as e:
logger.error(
f"Failed to handle generated file "
f"{workspace_file.path}: {e}"
)
# Cleanup Code Interpreter files (generated files)
for ci_file_id in file_ids_to_cleanup:
try:
client.delete_file(ci_file_id)
except Exception as e:
logger.error(
f"Failed to delete Code Interpreter generated "
f"file {ci_file_id}: {e}"
)
# Cleanup staged input files
for file_mapping in files_to_stage:
try:
client.delete_file(file_mapping["file_id"])
except Exception as e:
logger.error(
f"Failed to delete Code Interpreter staged "
f"file {file_mapping['file_id']}: {e}"
)
# Emit file_ids once files are processed
if generated_file_ids:
for event in client.execute_streaming(
code=code,
timeout_ms=CODE_INTERPRETER_DEFAULT_TIMEOUT_MS,
files=files_to_stage or None,
):
if isinstance(event, StreamOutputEvent):
if event.stream == "stdout":
stdout_parts.append(event.data)
else:
stderr_parts.append(event.data)
# Emit incremental delta to frontend
self.emitter.emit(
Packet(
placement=placement,
obj=PythonToolDelta(file_ids=generated_file_ids),
obj=PythonToolDelta(
stdout=event.data if event.stream == "stdout" else "",
stderr=event.data if event.stream == "stderr" else "",
),
)
)
elif isinstance(event, StreamResultEvent):
result_event = event
elif isinstance(event, StreamErrorEvent):
raise RuntimeError(f"Code interpreter error: {event.message}")
# Build result
result = LlmPythonExecutionResult(
stdout=truncated_stdout,
stderr=truncated_stderr,
exit_code=result_event.exit_code,
timed_out=result_event.timed_out,
generated_files=generated_files,
error=(None if result_event.exit_code == 0 else truncated_stderr),
if result_event is None:
raise RuntimeError(
"Code interpreter stream ended without a result event"
)
# Serialize result for LLM
adapter = TypeAdapter(LlmPythonExecutionResult)
llm_response = adapter.dump_json(result).decode()
full_stdout = "".join(stdout_parts)
full_stderr = "".join(stderr_parts)
return ToolResponse(
rich_response=PythonToolRichResponse(
generated_files=generated_files,
),
llm_facing_response=llm_response,
)
# Truncate output for LLM consumption
truncated_stdout = _truncate_output(
full_stdout, CODE_INTERPRETER_MAX_OUTPUT_LENGTH, "stdout"
)
truncated_stderr = _truncate_output(
full_stderr, CODE_INTERPRETER_MAX_OUTPUT_LENGTH, "stderr"
)
except Exception as e:
logger.error(f"Python execution failed: {e}")
error_msg = str(e)
# Handle generated files
generated_files: list[PythonExecutionFile] = []
generated_file_ids: list[str] = []
file_ids_to_cleanup: list[str] = []
file_store = get_default_file_store()
# Emit error delta
for workspace_file in result_event.files:
if workspace_file.kind != "file" or not workspace_file.file_id:
continue
try:
# Download file from Code Interpreter
file_content = client.download_file(workspace_file.file_id)
# Determine MIME type from file extension
filename = workspace_file.path.split("/")[-1]
mime_type, _ = mimetypes.guess_type(filename)
# Default to binary if we can't determine the type
mime_type = mime_type or "application/octet-stream"
# Save to Onyx file store
onyx_file_id = file_store.save_file(
content=BytesIO(file_content),
display_name=filename,
file_origin=FileOrigin.CHAT_UPLOAD,
file_type=mime_type,
)
generated_files.append(
PythonExecutionFile(
filename=filename,
file_link=build_full_frontend_file_url(onyx_file_id),
)
)
generated_file_ids.append(onyx_file_id)
# Mark for cleanup
file_ids_to_cleanup.append(workspace_file.file_id)
except Exception as e:
logger.error(
f"Failed to handle generated file {workspace_file.path}: {e}"
)
# Cleanup Code Interpreter files (generated files)
for ci_file_id in file_ids_to_cleanup:
try:
client.delete_file(ci_file_id)
except Exception as e:
logger.error(
f"Failed to delete Code Interpreter generated file {ci_file_id}: {e}"
)
# Cleanup staged input files
for file_mapping in files_to_stage:
try:
client.delete_file(file_mapping["file_id"])
except Exception as e:
logger.error(
f"Failed to delete Code Interpreter staged file {file_mapping['file_id']}: {e}"
)
# Emit file_ids once files are processed
if generated_file_ids:
self.emitter.emit(
Packet(
placement=placement,
obj=PythonToolDelta(
stdout="",
stderr=error_msg,
file_ids=[],
),
obj=PythonToolDelta(file_ids=generated_file_ids),
)
)
# Return error result
result = LlmPythonExecutionResult(
stdout="",
stderr=error_msg,
exit_code=-1,
timed_out=False,
generated_files=[],
error=error_msg,
)
# Build result
result = LlmPythonExecutionResult(
stdout=truncated_stdout,
stderr=truncated_stderr,
exit_code=result_event.exit_code,
timed_out=result_event.timed_out,
generated_files=generated_files,
error=None if result_event.exit_code == 0 else truncated_stderr,
)
adapter = TypeAdapter(LlmPythonExecutionResult)
llm_response = adapter.dump_json(result).decode()
# Serialize result for LLM
adapter = TypeAdapter(LlmPythonExecutionResult)
llm_response = adapter.dump_json(result).decode()
return ToolResponse(
rich_response=None,
llm_facing_response=llm_response,
return ToolResponse(
rich_response=PythonToolRichResponse(
generated_files=generated_files,
),
llm_facing_response=llm_response,
)
except Exception as e:
logger.error(f"Python execution failed: {e}")
error_msg = str(e)
# Emit error delta
self.emitter.emit(
Packet(
placement=placement,
obj=PythonToolDelta(
stdout="",
stderr=error_msg,
file_ids=[],
),
)
)
# Return error result
result = LlmPythonExecutionResult(
stdout="",
stderr=error_msg,
exit_code=-1,
timed_out=False,
generated_files=[],
error=error_msg,
)
adapter = TypeAdapter(LlmPythonExecutionResult)
llm_response = adapter.dump_json(result).decode()
return ToolResponse(
rich_response=None,
llm_facing_response=llm_response,
)

View File

@@ -596,7 +596,7 @@ mypy-extensions==1.0.0
# typing-inspect
nest-asyncio==1.6.0
# via onyx
nltk==3.9.3
nltk==3.9.1
# via unstructured
numpy==2.4.1
# via

View File

@@ -16,6 +16,10 @@ def monitor_process(process_name: str, process: subprocess.Popen) -> None:
def run_jobs() -> None:
# Check if we should use lightweight mode, defaults to True, change to False to use separate background workers
use_lightweight = True
# command setup
cmd_worker_primary = [
"celery",
"-A",
@@ -70,48 +74,6 @@ def run_jobs() -> None:
"--queues=connector_doc_fetching",
]
cmd_worker_heavy = [
"celery",
"-A",
"onyx.background.celery.versioned_apps.heavy",
"worker",
"--pool=threads",
"--concurrency=4",
"--prefetch-multiplier=1",
"--loglevel=INFO",
"--hostname=heavy@%n",
"-Q",
"connector_pruning,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,sandbox",
]
cmd_worker_monitoring = [
"celery",
"-A",
"onyx.background.celery.versioned_apps.monitoring",
"worker",
"--pool=threads",
"--concurrency=1",
"--prefetch-multiplier=1",
"--loglevel=INFO",
"--hostname=monitoring@%n",
"-Q",
"monitoring",
]
cmd_worker_user_file_processing = [
"celery",
"-A",
"onyx.background.celery.versioned_apps.user_file_processing",
"worker",
"--pool=threads",
"--concurrency=2",
"--prefetch-multiplier=1",
"--loglevel=INFO",
"--hostname=user_file_processing@%n",
"-Q",
"user_file_processing,user_file_project_sync,user_file_delete",
]
cmd_beat = [
"celery",
"-A",
@@ -120,31 +82,144 @@ def run_jobs() -> None:
"--loglevel=INFO",
]
all_workers = [
("PRIMARY", cmd_worker_primary),
("LIGHT", cmd_worker_light),
("DOCPROCESSING", cmd_worker_docprocessing),
("DOCFETCHING", cmd_worker_docfetching),
("HEAVY", cmd_worker_heavy),
("MONITORING", cmd_worker_monitoring),
("USER_FILE_PROCESSING", cmd_worker_user_file_processing),
("BEAT", cmd_beat),
]
# Prepare background worker commands based on mode
if use_lightweight:
print("Starting workers in LIGHTWEIGHT mode (single background worker)")
cmd_worker_background = [
"celery",
"-A",
"onyx.background.celery.versioned_apps.background",
"worker",
"--pool=threads",
"--concurrency=6",
"--prefetch-multiplier=1",
"--loglevel=INFO",
"--hostname=background@%n",
"-Q",
"connector_pruning,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,monitoring,user_file_processing,user_file_project_sync,user_file_delete,opensearch_migration",
]
background_workers = [("BACKGROUND", cmd_worker_background)]
else:
print("Starting workers in STANDARD mode (separate background workers)")
cmd_worker_heavy = [
"celery",
"-A",
"onyx.background.celery.versioned_apps.heavy",
"worker",
"--pool=threads",
"--concurrency=4",
"--prefetch-multiplier=1",
"--loglevel=INFO",
"--hostname=heavy@%n",
"-Q",
"connector_pruning,sandbox",
]
cmd_worker_monitoring = [
"celery",
"-A",
"onyx.background.celery.versioned_apps.monitoring",
"worker",
"--pool=threads",
"--concurrency=1",
"--prefetch-multiplier=1",
"--loglevel=INFO",
"--hostname=monitoring@%n",
"-Q",
"monitoring",
]
cmd_worker_user_file_processing = [
"celery",
"-A",
"onyx.background.celery.versioned_apps.user_file_processing",
"worker",
"--pool=threads",
"--concurrency=2",
"--prefetch-multiplier=1",
"--loglevel=INFO",
"--hostname=user_file_processing@%n",
"-Q",
"user_file_processing,user_file_project_sync,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,user_file_delete",
]
background_workers = [
("HEAVY", cmd_worker_heavy),
("MONITORING", cmd_worker_monitoring),
("USER_FILE_PROCESSING", cmd_worker_user_file_processing),
]
processes = []
for name, cmd in all_workers:
# spawn processes
worker_primary_process = subprocess.Popen(
cmd_worker_primary, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
worker_light_process = subprocess.Popen(
cmd_worker_light, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
worker_docprocessing_process = subprocess.Popen(
cmd_worker_docprocessing,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
worker_docfetching_process = subprocess.Popen(
cmd_worker_docfetching,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
)
beat_process = subprocess.Popen(
cmd_beat, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
# Spawn background worker processes based on mode
background_processes = []
for name, cmd in background_workers:
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
processes.append((name, process))
background_processes.append((name, process))
threads = []
for name, process in processes:
# monitor threads
worker_primary_thread = threading.Thread(
target=monitor_process, args=("PRIMARY", worker_primary_process)
)
worker_light_thread = threading.Thread(
target=monitor_process, args=("LIGHT", worker_light_process)
)
worker_docprocessing_thread = threading.Thread(
target=monitor_process, args=("DOCPROCESSING", worker_docprocessing_process)
)
worker_docfetching_thread = threading.Thread(
target=monitor_process, args=("DOCFETCHING", worker_docfetching_process)
)
beat_thread = threading.Thread(target=monitor_process, args=("BEAT", beat_process))
# Create monitor threads for background workers
background_threads = []
for name, process in background_processes:
thread = threading.Thread(target=monitor_process, args=(name, process))
threads.append(thread)
background_threads.append(thread)
# Start all threads
worker_primary_thread.start()
worker_light_thread.start()
worker_docprocessing_thread.start()
worker_docfetching_thread.start()
beat_thread.start()
for thread in background_threads:
thread.start()
for thread in threads:
# Wait for all threads
worker_primary_thread.join()
worker_light_thread.join()
worker_docprocessing_thread.join()
worker_docfetching_thread.join()
beat_thread.join()
for thread in background_threads:
thread.join()

View File

@@ -1,5 +1,23 @@
#!/bin/sh
# Entrypoint script for supervisord
# Entrypoint script for supervisord that sets environment variables
# for controlling which celery workers to start
# Default to lightweight mode if not set
if [ -z "$USE_LIGHTWEIGHT_BACKGROUND_WORKER" ]; then
export USE_LIGHTWEIGHT_BACKGROUND_WORKER="true"
fi
# Set the complementary variable for supervisord
# because it doesn't support %(not ENV_USE_LIGHTWEIGHT_BACKGROUND_WORKER) syntax
if [ "$USE_LIGHTWEIGHT_BACKGROUND_WORKER" = "true" ]; then
export USE_SEPARATE_BACKGROUND_WORKERS="false"
else
export USE_SEPARATE_BACKGROUND_WORKERS="true"
fi
echo "Worker mode configuration:"
echo " USE_LIGHTWEIGHT_BACKGROUND_WORKER=$USE_LIGHTWEIGHT_BACKGROUND_WORKER"
echo " USE_SEPARATE_BACKGROUND_WORKERS=$USE_SEPARATE_BACKGROUND_WORKERS"
# Launch supervisord with environment variables available
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

View File

@@ -39,6 +39,7 @@ autorestart=true
startsecs=10
stopasgroup=true
# Standard mode: Light worker for fast operations
# NOTE: only allowing configuration here and not in the other celery workers,
# since this is often the bottleneck for "sync" jobs (e.g. document set syncing,
# user group syncing, deletion, etc.)
@@ -53,7 +54,26 @@ redirect_stderr=true
autorestart=true
startsecs=10
stopasgroup=true
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
# Lightweight mode: single consolidated background worker
# Used when USE_LIGHTWEIGHT_BACKGROUND_WORKER=true (default)
# Consolidates: light, docprocessing, docfetching, heavy, monitoring, user_file_processing
[program:celery_worker_background]
command=celery -A onyx.background.celery.versioned_apps.background worker
--loglevel=INFO
--hostname=background@%%n
-Q vespa_metadata_sync,connector_deletion,doc_permissions_upsert,checkpoint_cleanup,index_attempt_cleanup,sandbox,docprocessing,connector_doc_fetching,connector_pruning,connector_doc_permissions_sync,connector_external_group_sync,csv_generation,monitoring,user_file_processing,user_file_project_sync,opensearch_migration
stdout_logfile=/var/log/celery_worker_background.log
stdout_logfile_maxbytes=16MB
redirect_stderr=true
autorestart=true
startsecs=10
stopasgroup=true
autostart=%(ENV_USE_LIGHTWEIGHT_BACKGROUND_WORKER)s
# Standard mode: separate workers for different background tasks
# Used when USE_LIGHTWEIGHT_BACKGROUND_WORKER=false
[program:celery_worker_heavy]
command=celery -A onyx.background.celery.versioned_apps.heavy worker
--loglevel=INFO
@@ -65,7 +85,9 @@ redirect_stderr=true
autorestart=true
startsecs=10
stopasgroup=true
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
# Standard mode: Document processing worker
[program:celery_worker_docprocessing]
command=celery -A onyx.background.celery.versioned_apps.docprocessing worker
--loglevel=INFO
@@ -77,6 +99,7 @@ redirect_stderr=true
autorestart=true
startsecs=10
stopasgroup=true
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
[program:celery_worker_user_file_processing]
command=celery -A onyx.background.celery.versioned_apps.user_file_processing worker
@@ -89,7 +112,9 @@ redirect_stderr=true
autorestart=true
startsecs=10
stopasgroup=true
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
# Standard mode: Document fetching worker
[program:celery_worker_docfetching]
command=celery -A onyx.background.celery.versioned_apps.docfetching worker
--loglevel=INFO
@@ -101,6 +126,7 @@ redirect_stderr=true
autorestart=true
startsecs=10
stopasgroup=true
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
[program:celery_worker_monitoring]
command=celery -A onyx.background.celery.versioned_apps.monitoring worker
@@ -113,6 +139,7 @@ redirect_stderr=true
autorestart=true
startsecs=10
stopasgroup=true
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
# Job scheduler for periodic tasks
@@ -170,6 +197,7 @@ command=tail -qF
/var/log/celery_beat.log
/var/log/celery_worker_primary.log
/var/log/celery_worker_light.log
/var/log/celery_worker_background.log
/var/log/celery_worker_heavy.log
/var/log/celery_worker_docprocessing.log
/var/log/celery_worker_monitoring.log

View File

@@ -114,8 +114,8 @@ def test_create_duplicate_config_fails(
headers=admin_user.headers,
)
assert response.status_code == 400
assert "already exists" in response.json()["detail"]
assert response.status_code == 409
assert "already exists" in response.json()["message"]
def test_get_all_configs(
@@ -292,7 +292,7 @@ def test_update_config_source_provider_not_found(
)
assert response.status_code == 404
assert "not found" in response.json()["detail"]
assert "not found" in response.json()["message"]
def test_delete_config(
@@ -468,7 +468,7 @@ def test_create_config_missing_credentials(
)
assert response.status_code == 400
assert "No provider or source llm provided" in response.json()["detail"]
assert "No provider or source llm provided" in response.json()["message"]
def test_create_config_source_provider_not_found(
@@ -488,4 +488,4 @@ def test_create_config_source_provider_not_found(
)
assert response.status_code == 404
assert "not found" in response.json()["detail"]
assert "not found" in response.json()["message"]

View File

@@ -300,7 +300,7 @@ def test_update_contextual_rag_nonexistent_provider(
headers=admin_user.headers,
)
assert response.status_code == 400
assert "Provider nonexistent-provider not found" in response.json()["detail"]
assert "Provider nonexistent-provider not found" in response.json()["message"]
def test_update_contextual_rag_nonexistent_model(
@@ -322,7 +322,7 @@ def test_update_contextual_rag_nonexistent_model(
assert response.status_code == 400
assert (
f"Model nonexistent-model not found in provider {llm_provider.name}"
in response.json()["detail"]
in response.json()["message"]
)
@@ -342,7 +342,7 @@ def test_update_contextual_rag_missing_provider_name(
headers=admin_user.headers,
)
assert response.status_code == 400
assert "Provider name and model name are required" in response.json()["detail"]
assert "Provider name and model name are required" in response.json()["message"]
def test_update_contextual_rag_missing_model_name(
@@ -362,7 +362,7 @@ def test_update_contextual_rag_missing_model_name(
headers=admin_user.headers,
)
assert response.status_code == 400
assert "Provider name and model name are required" in response.json()["detail"]
assert "Provider name and model name are required" in response.json()["message"]
@pytest.mark.skip(reason="Set new search settings is temporarily disabled.")

View File

@@ -104,102 +104,3 @@ def test_format_slack_message_ampersand_not_double_escaped() -> None:
assert "&amp;" in formatted
assert "&quot;" not in formatted
# -- Table rendering tests --
def test_table_renders_as_vertical_cards() -> None:
message = (
"| Feature | Status | Owner |\n"
"|---------|--------|-------|\n"
"| Auth | Done | Alice |\n"
"| Search | In Progress | Bob |\n"
)
formatted = format_slack_message(message)
assert "*Auth*\n • Status: Done\n • Owner: Alice" in formatted
assert "*Search*\n • Status: In Progress\n • Owner: Bob" in formatted
# Cards separated by blank line
assert "Owner: Alice\n\n*Search*" in formatted
# No raw pipe-and-dash table syntax
assert "---|" not in formatted
def test_table_single_column() -> None:
message = "| Name |\n|------|\n| Alice |\n| Bob |\n"
formatted = format_slack_message(message)
assert "*Alice*" in formatted
assert "*Bob*" in formatted
def test_table_embedded_in_text() -> None:
message = (
"Here are the results:\n\n"
"| Item | Count |\n"
"|------|-------|\n"
"| Apples | 5 |\n"
"\n"
"That's all."
)
formatted = format_slack_message(message)
assert "Here are the results:" in formatted
assert "*Apples*\n • Count: 5" in formatted
assert "That's all." in formatted
def test_table_with_formatted_cells() -> None:
message = (
"| Name | Link |\n"
"|------|------|\n"
"| **Alice** | [profile](https://example.com) |\n"
)
formatted = format_slack_message(message)
# Bold cell should not double-wrap: *Alice* not **Alice**
assert "*Alice*" in formatted
assert "**Alice**" not in formatted
assert "<https://example.com|profile>" in formatted
def test_table_with_alignment_specifiers() -> None:
message = (
"| Left | Center | Right |\n" "|:-----|:------:|------:|\n" "| a | b | c |\n"
)
formatted = format_slack_message(message)
assert "*a*\n • Center: b\n • Right: c" in formatted
def test_two_tables_in_same_message_use_independent_headers() -> None:
message = (
"| A | B |\n"
"|---|---|\n"
"| 1 | 2 |\n"
"\n"
"| X | Y | Z |\n"
"|---|---|---|\n"
"| p | q | r |\n"
)
formatted = format_slack_message(message)
assert "*1*\n • B: 2" in formatted
assert "*p*\n • Y: q\n • Z: r" in formatted
def test_table_empty_first_column_no_bare_asterisks() -> None:
message = "| Name | Status |\n" "|------|--------|\n" "| | Done |\n"
formatted = format_slack_message(message)
# Empty title should not produce "**" (bare asterisks)
assert "**" not in formatted
assert " • Status: Done" in formatted

View File

@@ -87,8 +87,7 @@ def test_python_tool_available_when_health_check_passes(
mock_client = MagicMock()
mock_client.health.return_value = True
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
mock_client_cls.return_value = mock_client
db_session = MagicMock(spec=Session)
assert PythonTool.is_available(db_session) is True
@@ -110,8 +109,7 @@ def test_python_tool_unavailable_when_health_check_fails(
mock_client = MagicMock()
mock_client.health.return_value = False
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
mock_client_cls.return_value = mock_client
db_session = MagicMock(spec=Session)
assert PythonTool.is_available(db_session) is False

View File

@@ -138,6 +138,7 @@ services:
- indexing_model_server
restart: unless-stopped
environment:
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
- ENABLE_PAID_ENTERPRISE_EDITION_FEATURES=true
- MULTI_TENANT=true
- LOG_LEVEL=DEBUG

View File

@@ -52,6 +52,7 @@ services:
- indexing_model_server
restart: unless-stopped
environment:
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
- AUTH_TYPE=${AUTH_TYPE:-oidc}
- POSTGRES_HOST=relational_db
- VESPA_HOST=index

View File

@@ -65,6 +65,7 @@ services:
- indexing_model_server
restart: unless-stopped
environment:
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
- AUTH_TYPE=${AUTH_TYPE:-oidc}
- POSTGRES_HOST=relational_db
- VESPA_HOST=index

View File

@@ -70,6 +70,7 @@ services:
- indexing_model_server
restart: unless-stopped
environment:
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
- AUTH_TYPE=${AUTH_TYPE:-oidc}
- POSTGRES_HOST=relational_db
- VESPA_HOST=index

View File

@@ -58,6 +58,7 @@ services:
env_file:
- .env_eval
environment:
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
- AUTH_TYPE=disabled
- POSTGRES_HOST=relational_db
- VESPA_HOST=index

View File

@@ -146,6 +146,7 @@ services:
- indexing_model_server
restart: unless-stopped
environment:
- USE_LIGHTWEIGHT_BACKGROUND_WORKER=${USE_LIGHTWEIGHT_BACKGROUND_WORKER:-true}
- FILE_STORE_BACKEND=${FILE_STORE_BACKEND:-s3}
- POSTGRES_HOST=${POSTGRES_HOST:-relational_db}
- VESPA_HOST=${VESPA_HOST:-index}

View File

@@ -14,32 +14,30 @@ Built with [Tauri](https://tauri.app) for minimal bundle size (~10MB vs Electron
## Keyboard Shortcuts
| Shortcut | Action |
| -------- | ---------------- |
| `⌘ N` | New Chat |
| `⌘ ⇧ N` | New Window |
| `⌘ R` | Reload |
| `⌘ [` | Go Back |
| `⌘ ]` | Go Forward |
| `⌘ ,` | Open Config File |
| `⌘ W` | Close Window |
| `⌘ Q` | Quit |
| Shortcut | Action |
|----------|--------|
| `⌘ N` | New Chat |
| `⌘ ⇧ N` | New Window |
| `⌘ R` | Reload |
| `⌘ [` | Go Back |
| `⌘ ]` | Go Forward |
| `⌘ ,` | Open Config File |
| `⌘ W` | Close Window |
| `⌘ Q` | Quit |
## Prerequisites
1. **Rust** (latest stable)
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source $HOME/.cargo/env
```
2. **Node.js** (18+)
```bash
# Using homebrew
brew install node
# Or using nvm
nvm install 18
```
@@ -57,21 +55,16 @@ npm install
# Run in development mode
npm run dev
# Run in debug mode
npm run debug
```
## Building
### Build for current architecture
```bash
npm run build
```
### Build Universal Binary (Intel + Apple Silicon)
```bash
# First, add the targets
rustup target add x86_64-apple-darwin
@@ -110,7 +103,6 @@ Before building, add your app icons to `src-tauri/icons/`:
- `icon.ico` (Windows, optional)
You can generate these from a 1024x1024 source image using:
```bash
# Using tauri's icon generator
npm run tauri icon path/to/your-icon.png
@@ -123,7 +115,6 @@ npm run tauri icon path/to/your-icon.png
The app defaults to `https://cloud.onyx.app` but supports any Onyx instance.
**Config file location:**
- macOS: `~/Library/Application Support/app.onyx.desktop/config.json`
- Linux: `~/.config/app.onyx.desktop/config.json`
- Windows: `%APPDATA%/app.onyx.desktop/config.json`
@@ -144,7 +135,6 @@ The app defaults to `https://cloud.onyx.app` but supports any Onyx instance.
4. Restart the app
**Quick edit via terminal:**
```bash
# macOS
open -t ~/Library/Application\ Support/app.onyx.desktop/config.json
@@ -156,7 +146,6 @@ code ~/Library/Application\ Support/app.onyx.desktop/config.json
### Change the default URL in build
Edit `src-tauri/tauri.conf.json`:
```json
{
"app": {
@@ -176,7 +165,6 @@ Edit `src-tauri/src/main.rs` in the `setup_shortcuts` function.
### Window appearance
Modify the window configuration in `src-tauri/tauri.conf.json`:
- `titleBarStyle`: `"Overlay"` (macOS native) or `"Visible"`
- `decorations`: Window chrome
- `transparent`: For custom backgrounds
@@ -184,20 +172,16 @@ Modify the window configuration in `src-tauri/tauri.conf.json`:
## Troubleshooting
### "Unable to resolve host"
Make sure you have an internet connection. The app loads content from `cloud.onyx.app`.
### Build fails on M1/M2 Mac
```bash
# Ensure you have the right target
rustup target add aarch64-apple-darwin
```
### Code signing for distribution
For distributing outside the App Store, you'll need to:
1. Get an Apple Developer certificate
2. Sign the app: `codesign --deep --force --sign "Developer ID" target/release/bundle/macos/Onyx.app`
3. Notarize with Apple

View File

@@ -4,7 +4,6 @@
"description": "Lightweight desktop app for Onyx Cloud",
"scripts": {
"dev": "tauri dev",
"debug": "tauri dev -- -- --debug",
"build": "tauri build",
"build:dmg": "tauri build --target universal-apple-darwin",
"build:linux": "tauri build --bundles deb,rpm"

View File

@@ -23,4 +23,3 @@ url = "2.5"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
devtools = ["tauri/devtools"]

View File

@@ -6,9 +6,7 @@ use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::{Mutex, RwLock};
use std::io::Write as IoWrite;
use std::time::SystemTime;
use std::sync::RwLock;
#[cfg(target_os = "macos")]
use std::time::Duration;
use tauri::image::Image;
@@ -232,63 +230,6 @@ const MENU_KEY_HANDLER_SCRIPT: &str = r#"
})();
"#;
const CONSOLE_CAPTURE_SCRIPT: &str = r#"
(() => {
if (window.__ONYX_CONSOLE_CAPTURE__) return;
window.__ONYX_CONSOLE_CAPTURE__ = true;
const levels = ['log', 'warn', 'error', 'info', 'debug'];
const originals = {};
levels.forEach(level => {
originals[level] = console[level];
console[level] = function(...args) {
originals[level].apply(console, args);
try {
const invoke =
window.__TAURI__?.core?.invoke || window.__TAURI_INTERNALS__?.invoke;
if (typeof invoke === 'function') {
const message = args.map(a => {
try { return typeof a === 'string' ? a : JSON.stringify(a); }
catch { return String(a); }
}).join(' ');
invoke('log_from_frontend', { level, message });
}
} catch {}
};
});
window.addEventListener('error', (event) => {
try {
const invoke =
window.__TAURI__?.core?.invoke || window.__TAURI_INTERNALS__?.invoke;
if (typeof invoke === 'function') {
invoke('log_from_frontend', {
level: 'error',
message: `[uncaught] ${event.message} at ${event.filename}:${event.lineno}:${event.colno}`
});
}
} catch {}
});
window.addEventListener('unhandledrejection', (event) => {
try {
const invoke =
window.__TAURI__?.core?.invoke || window.__TAURI_INTERNALS__?.invoke;
if (typeof invoke === 'function') {
invoke('log_from_frontend', {
level: 'error',
message: `[unhandled rejection] ${event.reason}`
});
}
} catch {}
});
})();
"#;
const MENU_TOGGLE_DEVTOOLS_ID: &str = "toggle_devtools";
const MENU_OPEN_DEBUG_LOG_ID: &str = "open_debug_log";
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct AppConfig {
pub server_url: String,
@@ -370,87 +311,12 @@ fn save_config(config: &AppConfig) -> Result<(), String> {
Ok(())
}
// ============================================================================
// Debug Mode
// ============================================================================
fn is_debug_mode() -> bool {
std::env::args().any(|arg| arg == "--debug") || std::env::var("ONYX_DEBUG").is_ok()
}
fn get_debug_log_path() -> Option<PathBuf> {
get_config_dir().map(|dir| dir.join("frontend_debug.log"))
}
fn init_debug_log_file() -> Option<fs::File> {
let log_path = get_debug_log_path()?;
if let Some(parent) = log_path.parent() {
let _ = fs::create_dir_all(parent);
}
fs::OpenOptions::new()
.create(true)
.append(true)
.open(&log_path)
.ok()
}
fn format_utc_timestamp() -> String {
let now = SystemTime::now()
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or_default();
let total_secs = now.as_secs();
let millis = now.subsec_millis();
let days = total_secs / 86400;
let secs_of_day = total_secs % 86400;
let hours = secs_of_day / 3600;
let mins = (secs_of_day % 3600) / 60;
let secs = secs_of_day % 60;
// Days since Unix epoch -> Y/M/D via civil calendar arithmetic
let z = days as i64 + 719468;
let era = z / 146097;
let doe = z - era * 146097;
let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
let y = yoe + era * 400;
let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
let mp = (5 * doy + 2) / 153;
let d = doy - (153 * mp + 2) / 5 + 1;
let m = if mp < 10 { mp + 3 } else { mp - 9 };
let y = if m <= 2 { y + 1 } else { y };
format!(
"{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
y, m, d, hours, mins, secs, millis
)
}
fn inject_console_capture(webview: &Webview) {
let _ = webview.eval(CONSOLE_CAPTURE_SCRIPT);
}
fn maybe_open_devtools(app: &AppHandle, window: &tauri::WebviewWindow) {
#[cfg(any(debug_assertions, feature = "devtools"))]
{
let state = app.state::<ConfigState>();
if state.debug_mode {
window.open_devtools();
}
}
#[cfg(not(any(debug_assertions, feature = "devtools")))]
{
let _ = (app, window);
}
}
// Global config state
struct ConfigState {
config: RwLock<AppConfig>,
config_initialized: RwLock<bool>,
app_base_url: RwLock<Option<Url>>,
menu_temporarily_visible: RwLock<bool>,
debug_mode: bool,
debug_log_file: Mutex<Option<fs::File>>,
}
fn focus_main_window(app: &AppHandle) {
@@ -506,7 +372,6 @@ fn trigger_new_window(app: &AppHandle) {
}
apply_settings_to_window(&handle, &window);
maybe_open_devtools(&handle, &window);
let _ = window.set_focus();
}
});
@@ -602,65 +467,10 @@ fn inject_chat_link_intercept(webview: &Webview) {
let _ = webview.eval(CHAT_LINK_INTERCEPT_SCRIPT);
}
fn handle_toggle_devtools(app: &AppHandle) {
#[cfg(any(debug_assertions, feature = "devtools"))]
{
let windows: Vec<_> = app.webview_windows().into_values().collect();
let any_open = windows.iter().any(|w| w.is_devtools_open());
for window in &windows {
if any_open {
window.close_devtools();
} else {
window.open_devtools();
}
}
}
#[cfg(not(any(debug_assertions, feature = "devtools")))]
{
let _ = app;
}
}
fn handle_open_debug_log() {
let log_path = match get_debug_log_path() {
Some(p) => p,
None => return,
};
if !log_path.exists() {
eprintln!("[ONYX DEBUG] Log file does not exist yet: {:?}", log_path);
return;
}
let url_path = log_path.to_string_lossy().replace('\\', "/");
let _ = open_in_default_browser(&format!(
"file:///{}",
url_path.trim_start_matches('/')
));
}
// ============================================================================
// Tauri Commands
// ============================================================================
#[tauri::command]
fn log_from_frontend(level: String, message: String, state: tauri::State<ConfigState>) {
if !state.debug_mode {
return;
}
let timestamp = format_utc_timestamp();
let log_line = format!("[{}] [{}] {}", timestamp, level.to_uppercase(), message);
eprintln!("{}", log_line);
if let Ok(mut guard) = state.debug_log_file.lock() {
if let Some(ref mut file) = *guard {
let _ = writeln!(file, "{}", log_line);
let _ = file.flush();
}
}
}
/// Get the current server URL
#[tauri::command]
fn get_server_url(state: tauri::State<ConfigState>) -> String {
@@ -847,7 +657,6 @@ async fn new_window(app: AppHandle, state: tauri::State<'_, ConfigState>) -> Res
}
apply_settings_to_window(&app, &window);
maybe_open_devtools(&app, &window);
Ok(())
}
@@ -1127,30 +936,6 @@ fn setup_app_menu(app: &AppHandle) -> tauri::Result<()> {
menu.append(&help_menu)?;
}
let state = app.state::<ConfigState>();
if state.debug_mode {
let toggle_devtools_item = MenuItem::with_id(
app,
MENU_TOGGLE_DEVTOOLS_ID,
"Toggle DevTools",
true,
Some("F12"),
)?;
let open_log_item = MenuItem::with_id(
app,
MENU_OPEN_DEBUG_LOG_ID,
"Open Debug Log",
true,
None::<&str>,
)?;
let debug_menu = SubmenuBuilder::new(app, "Debug")
.item(&toggle_devtools_item)
.item(&open_log_item)
.build()?;
menu.append(&debug_menu)?;
}
app.set_menu(menu)?;
Ok(())
}
@@ -1242,20 +1027,8 @@ fn setup_tray_icon(app: &AppHandle) -> tauri::Result<()> {
// ============================================================================
fn main() {
// Load config at startup
let (config, config_initialized) = load_config();
let debug_mode = is_debug_mode();
let debug_log_file = if debug_mode {
eprintln!("[ONYX DEBUG] Debug mode enabled");
if let Some(path) = get_debug_log_path() {
eprintln!("[ONYX DEBUG] Frontend logs: {}", path.display());
}
eprintln!("[ONYX DEBUG] DevTools will open automatically");
eprintln!("[ONYX DEBUG] Capturing console.log/warn/error/info/debug from webview");
init_debug_log_file()
} else {
None
};
tauri::Builder::default()
.plugin(tauri_plugin_shell::init())
@@ -1286,8 +1059,6 @@ fn main() {
config_initialized: RwLock::new(config_initialized),
app_base_url: RwLock::new(None),
menu_temporarily_visible: RwLock::new(false),
debug_mode,
debug_log_file: Mutex::new(debug_log_file),
})
.invoke_handler(tauri::generate_handler![
get_server_url,
@@ -1306,8 +1077,7 @@ fn main() {
start_drag_window,
toggle_menu_bar,
show_menu_bar_temporarily,
hide_menu_bar_temporary,
log_from_frontend
hide_menu_bar_temporary
])
.on_menu_event(|app, event| match event.id().as_ref() {
"open_docs" => open_docs(),
@@ -1316,8 +1086,6 @@ fn main() {
"open_settings" => open_settings(app),
"show_menu_bar" => handle_menu_bar_toggle(app),
"hide_window_decorations" => handle_decorations_toggle(app),
MENU_TOGGLE_DEVTOOLS_ID => handle_toggle_devtools(app),
MENU_OPEN_DEBUG_LOG_ID => handle_open_debug_log(),
_ => {}
})
.setup(move |app| {
@@ -1351,7 +1119,6 @@ fn main() {
inject_titlebar(window.clone());
apply_settings_to_window(&app_handle, &window);
maybe_open_devtools(&app_handle, &window);
let _ = window.set_focus();
}
@@ -1361,14 +1128,6 @@ fn main() {
.on_page_load(|webview: &Webview, _payload: &PageLoadPayload| {
inject_chat_link_intercept(webview);
{
let app = webview.app_handle();
let state = app.state::<ConfigState>();
if state.debug_mode {
inject_console_capture(webview);
}
}
#[cfg(not(target_os = "macos"))]
{
let _ = webview.eval(MENU_KEY_HANDLER_SCRIPT);

6
uv.lock generated
View File

@@ -4106,7 +4106,7 @@ wheels = [
[[package]]
name = "nltk"
version = "3.9.3"
version = "3.9.1"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "click" },
@@ -4114,9 +4114,9 @@ dependencies = [
{ name = "regex" },
{ name = "tqdm" },
]
sdist = { url = "https://files.pythonhosted.org/packages/e1/8f/915e1c12df07c70ed779d18ab83d065718a926e70d3ea33eb0cd66ffb7c0/nltk-3.9.3.tar.gz", hash = "sha256:cb5945d6424a98d694c2b9a0264519fab4363711065a46aa0ae7a2195b92e71f", size = 2923673, upload-time = "2026-02-24T12:05:53.833Z" }
sdist = { url = "https://files.pythonhosted.org/packages/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" }
wheels = [
{ url = "https://files.pythonhosted.org/packages/c2/7e/9af5a710a1236e4772de8dfcc6af942a561327bb9f42b5b4a24d0cf100fd/nltk-3.9.3-py3-none-any.whl", hash = "sha256:60b3db6e9995b3dd976b1f0fa7dec22069b2677e759c28eb69b62ddd44870522", size = 1525385, upload-time = "2026-02-24T12:05:46.54Z" },
{ url = "https://files.pythonhosted.org/packages/4d/66/7d9e26593edda06e8cb531874633f7c2372279c3b0f46235539fe546df8b/nltk-3.9.1-py3-none-any.whl", hash = "sha256:4fa26829c5b00715afe3061398a8989dc643b92ce7dd93fb4585a70930d168a1", size = 1505442, upload-time = "2024-08-18T19:48:21.909Z" },
]
[[package]]

View File

@@ -1,22 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgColumn = ({ 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="M6 14H3.33333C2.59695 14 2 13.403 2 12.6667V3.33333C2 2.59695 2.59695 2 3.33333 2H6M6 14V2M6 14H10M6 2H10M10 2H12.6667C13.403 2 14 2.59695 14 3.33333V12.6667C14 13.403 13.403 14 12.6667 14H10M10 2V14"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgColumn;

View File

@@ -1,20 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgHandle = ({ size = 16, ...props }: IconProps) => (
<svg
width={Math.round((size * 3) / 17)}
height={size}
viewBox="0 0 3 17"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M0.5 0.5V16.5M2.5 0.5V16.5"
stroke="currentColor"
strokeLinecap="round"
/>
</svg>
);
export default SvgHandle;

View File

@@ -49,7 +49,6 @@ export { default as SvgClock } from "@opal/icons/clock";
export { default as SvgClockHandsSmall } from "@opal/icons/clock-hands-small";
export { default as SvgCloud } from "@opal/icons/cloud";
export { default as SvgCode } from "@opal/icons/code";
export { default as SvgColumn } from "@opal/icons/column";
export { default as SvgCopy } from "@opal/icons/copy";
export { default as SvgCornerRightUpDot } from "@opal/icons/corner-right-up-dot";
export { default as SvgCpu } from "@opal/icons/cpu";
@@ -80,7 +79,6 @@ export { default as SvgFolderPartialOpen } from "@opal/icons/folder-partial-open
export { default as SvgFolderPlus } from "@opal/icons/folder-plus";
export { default as SvgGemini } from "@opal/icons/gemini";
export { default as SvgGlobe } from "@opal/icons/globe";
export { default as SvgHandle } from "@opal/icons/handle";
export { default as SvgHardDrive } from "@opal/icons/hard-drive";
export { default as SvgHashSmall } from "@opal/icons/hash-small";
export { default as SvgHash } from "@opal/icons/hash";
@@ -148,8 +146,6 @@ export { default as SvgSlack } from "@opal/icons/slack";
export { default as SvgSlash } from "@opal/icons/slash";
export { default as SvgSliders } from "@opal/icons/sliders";
export { default as SvgSlidersSmall } from "@opal/icons/sliders-small";
export { default as SvgSort } from "@opal/icons/sort";
export { default as SvgSortOrder } from "@opal/icons/sort-order";
export { default as SvgSparkle } from "@opal/icons/sparkle";
export { default as SvgStar } from "@opal/icons/star";
export { default as SvgStep1 } from "@opal/icons/step1";
@@ -173,7 +169,6 @@ export { default as SvgUploadCloud } from "@opal/icons/upload-cloud";
export { default as SvgUser } from "@opal/icons/user";
export { default as SvgUserManage } from "@opal/icons/user-manage";
export { default as SvgUserPlus } from "@opal/icons/user-plus";
export { default as SvgUserSync } from "@opal/icons/user-sync";
export { default as SvgUsers } from "@opal/icons/users";
export { default as SvgWallet } from "@opal/icons/wallet";
export { default as SvgWorkflow } from "@opal/icons/workflow";

View File

@@ -1,21 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgSortOrder = ({ 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="M2.66675 12L7.67009 12.0001M2.66675 8H10.5001M2.66675 4H13.3334"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgSortOrder;

View File

@@ -1,27 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgSort = ({ 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="M2 4.5H10M2 8H7M2 11.5H5"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M12 5V12M12 12L14 10M12 12L10 10"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgSort;

View File

@@ -1,22 +0,0 @@
import type { IconProps } from "@opal/types";
const SvgUserSync = ({ 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="M1 14C1 13.6667 1 13.3333 1 13C1 11.3431 2.34316 10 4.00002 10H7M11 8.5L9.5 10L14.5 9.99985M13 14L14.5 12.5L9.5 12.5M8.75 4.75C8.75 6.26878 7.51878 7.5 6 7.5C4.48122 7.5 3.25 6.26878 3.25 4.75C3.25 3.23122 4.48122 2 6 2C7.51878 2 8.75 3.23122 8.75 4.75Z"
strokeWidth={1.5}
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
);
export default SvgUserSync;

View File

@@ -41,7 +41,7 @@ export default defineConfig({
viewport: { width: 1280, height: 720 },
storageState: "admin_auth.json",
},
grepInvert: [/@exclusive/, /@lite/],
grepInvert: /@exclusive/,
},
{
// this suite runs independently and serially + slower
@@ -55,15 +55,5 @@ export default defineConfig({
grep: /@exclusive/,
workers: 1,
},
{
// runs against the Onyx Lite stack (DISABLE_VECTOR_DB=true, no Vespa/Redis)
name: "lite",
use: {
...devices["Desktop Chrome"],
viewport: { width: 1280, height: 720 },
storageState: "admin_auth.json",
},
grep: /@lite/,
},
],
});

View File

@@ -2,7 +2,7 @@
import * as SettingsLayouts from "@/layouts/settings-layouts";
import { SourceCategory, SourceMetadata } from "@/lib/search/interfaces";
import { listSourceMetadata } from "@/lib/sources";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import {
useCallback,
useContext,
@@ -254,7 +254,9 @@ export default function Page() {
icon={route.icon}
title={route.title}
rightChildren={
<Button href="/admin/indexing/status">See Connectors</Button>
<Button href="/admin/indexing/status" primary>
See Connectors
</Button>
}
separator
/>

View File

@@ -16,8 +16,9 @@ import {
} from "./lib";
import { FiEdit2 } from "react-icons/fi";
import { useUser } from "@/providers/UserProvider";
import { Button } from "@opal/components";
import { Button as OpalButton } from "@opal/components";
import ConfirmationModalLayout from "@/refresh-components/layouts/ConfirmationModalLayout";
import Button from "@/refresh-components/buttons/Button";
import { SvgAlertCircle, SvgTrash } from "@opal/icons";
import type { Route } from "next";
@@ -299,7 +300,7 @@ export function PersonasTable({
<div key="edit" className="flex">
<div className="mr-auto my-auto">
{!persona.builtin_persona && isEditable ? (
<Button
<OpalButton
icon={SvgTrash}
prominence="tertiary"
onClick={() => openDeleteModal(persona)}

View File

@@ -2,7 +2,7 @@ import { Form, Formik } from "formik";
import { toast } from "@/hooks/useToast";
import { createApiKey, updateApiKey } from "./lib";
import Modal from "@/refresh-components/Modal";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import Text from "@/refresh-components/texts/Text";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import InputComboBox from "@/refresh-components/inputs/InputComboBox";

View File

@@ -27,7 +27,7 @@ import {
DISCORD_SERVICE_API_KEY_NAME,
} from "@/app/admin/api-key/types";
import CreateButton from "@/refresh-components/buttons/CreateButton";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import CopyIconButton from "@/refresh-components/buttons/CopyIconButton";
import Text from "@/refresh-components/texts/Text";
import { SvgEdit, SvgKey, SvgRefreshCw } from "@opal/icons";
@@ -161,11 +161,11 @@ function Main() {
<TableRow key={apiKey.api_key_id}>
<TableCell>
<Button
prominence="internal"
internal
onClick={() => handleEdit(apiKey)}
icon={SvgEdit}
leftIcon={SvgEdit}
>
{apiKey.api_key_name || "null"}
{apiKey.api_key_name || <i>null</i>}
</Button>
</TableCell>
<TableCell className="max-w-64">
@@ -176,8 +176,8 @@ function Main() {
</TableCell>
<TableCell>
<Button
prominence="internal"
icon={SvgRefreshCw}
internal
leftIcon={SvgRefreshCw}
onClick={async () => {
setKeyIsGenerating(true);
const response = await regenerateApiKey(apiKey);

View File

@@ -7,7 +7,6 @@ import { Content } from "@opal/layouts";
import * as InputLayouts from "@/layouts/input-layouts";
import Card from "@/refresh-components/cards/Card";
import Button from "@/refresh-components/buttons/Button";
import { Button as OpalButton } from "@opal/components";
import Text from "@/refresh-components/texts/Text";
import Message from "@/refresh-components/messages/Message";
import InfoBlock from "@/refresh-components/messages/InfoBlock";
@@ -245,20 +244,25 @@ function SubscriptionCard({
to make changes.
</Text>
) : disabled ? (
<OpalButton
prominence="secondary"
<Button
main
secondary
onClick={handleReconnect}
rightIcon={SvgArrowRight}
disabled={isReconnecting}
>
{isReconnecting ? "Connecting..." : "Connect to Stripe"}
</OpalButton>
</Button>
) : (
<OpalButton onClick={handleManagePlan} rightIcon={SvgExternalLink}>
<Button
main
primary
onClick={handleManagePlan}
rightIcon={SvgExternalLink}
>
Manage Plan
</OpalButton>
</Button>
)}
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button tertiary onClick={onViewPlans} className="billing-text-link">
<Text secondaryBody text03>
View Plan Details
@@ -375,13 +379,9 @@ function SeatsCard({
sizePreset="main-content"
variant="section"
/>
<OpalButton
prominence="secondary"
onClick={handleCancel}
disabled={isSubmitting}
>
<Button main secondary onClick={handleCancel} disabled={isSubmitting}>
Cancel
</OpalButton>
</Button>
</Section>
<div className="billing-content-area">
@@ -463,14 +463,16 @@ function SeatsCard({
No changes to your billing.
</Text>
)}
<OpalButton
<Button
main
primary
onClick={handleConfirm}
disabled={
isSubmitting || newSeatCount === totalSeats || isBelowMinimum
}
>
{isSubmitting ? "Saving..." : "Confirm Change"}
</OpalButton>
</Button>
</Section>
</Card>
);
@@ -500,22 +502,19 @@ function SeatsCard({
height="auto"
width="auto"
>
<OpalButton
prominence="tertiary"
href="/admin/users"
icon={SvgExternalLink}
>
<Button main tertiary href="/admin/users" leftIcon={SvgExternalLink}>
View Users
</OpalButton>
</Button>
{!hideUpdateSeats && (
<OpalButton
prominence="secondary"
<Button
main
secondary
onClick={handleStartEdit}
icon={SvgPlus}
leftIcon={SvgPlus}
disabled={isLoadingUsers || disabled || !billing}
>
Update Seats
</OpalButton>
</Button>
)}
</Section>
</Section>
@@ -567,13 +566,14 @@ function PaymentSection({ billing }: { billing: BillingInformation }) {
title="Visa ending in 1234"
description="Payment method"
/>
<OpalButton
prominence="tertiary"
<Button
main
tertiary
onClick={handleOpenPortal}
rightIcon={SvgExternalLink}
>
Update
</OpalButton>
</Button>
</Section>
</Card>
{lastPaymentDate && (
@@ -589,13 +589,14 @@ function PaymentSection({ billing }: { billing: BillingInformation }) {
title={lastPaymentDate}
description="Last payment"
/>
<OpalButton
prominence="tertiary"
<Button
main
tertiary
onClick={handleOpenPortal}
rightIcon={SvgExternalLink}
>
View Invoice
</OpalButton>
</Button>
</Section>
</Card>
)}

View File

@@ -3,7 +3,7 @@
import { useState, useMemo, useEffect } from "react";
import { Section } from "@/layouts/general-layouts";
import * as InputLayouts from "@/layouts/input-layouts";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import Text from "@/refresh-components/texts/Text";
import Card from "@/refresh-components/cards/Card";
import Separator from "@/refresh-components/Separator";
@@ -176,7 +176,7 @@ export default function CheckoutView({ onAdjustPlan }: CheckoutViewProps) {
Business
</Text>
</Section>
<Button prominence="secondary" onClick={onAdjustPlan}>
<Button secondary onClick={onAdjustPlan}>
Adjust Plan
</Button>
</Section>
@@ -262,7 +262,7 @@ export default function CheckoutView({ onAdjustPlan }: CheckoutViewProps) {
// Empty div to maintain space-between alignment
<div></div>
)}
<Button onClick={handleSubmit} disabled={isSubmitting}>
<Button main primary onClick={handleSubmit} disabled={isSubmitting}>
{isSubmitting ? "Loading..." : "Continue to Payment"}
</Button>
</Section>

View File

@@ -2,7 +2,7 @@
import { useState } from "react";
import Card from "@/refresh-components/cards/Card";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import Text from "@/refresh-components/texts/Text";
import InputFile from "@/refresh-components/inputs/InputFile";
import { Section } from "@/layouts/general-layouts";
@@ -119,11 +119,11 @@ export default function LicenseActivationCard({
</Text>
</Section>
<Section flexDirection="row" gap={0.5} height="auto" width="auto">
<Button prominence="secondary" onClick={() => setShowInput(true)}>
<Button main secondary onClick={() => setShowInput(true)}>
Update Key
</Button>
{!hideClose && (
<Button prominence="tertiary" onClick={handleClose}>
<Button main tertiary onClick={handleClose}>
Close
</Button>
)}
@@ -146,11 +146,7 @@ export default function LicenseActivationCard({
<Text headingH3>
{hasLicense ? "Update License Key" : "Activate License Key"}
</Text>
<Button
prominence="secondary"
onClick={handleClose}
disabled={isActivating}
>
<Button secondary onClick={handleClose} disabled={isActivating}>
Cancel
</Button>
</Section>
@@ -223,6 +219,8 @@ export default function LicenseActivationCard({
{/* Footer */}
<Section flexDirection="row" justifyContent="end" padding={1}>
<Button
main
primary
onClick={handleActivate}
disabled={isActivating || !licenseKey.trim() || success}
>

View File

@@ -21,7 +21,6 @@ import "@/app/admin/billing/billing.css";
import type { IconProps } from "@opal/types";
import Card from "@/refresh-components/cards/Card";
import Button from "@/refresh-components/buttons/Button";
import { Button as OpalButton } from "@opal/components";
import Text from "@/refresh-components/texts/Text";
import { Section } from "@/layouts/general-layouts";
@@ -147,27 +146,26 @@ function PlanCard({
{/* Button */}
<div className="plan-card-button">
{isCurrentPlan ? (
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
<Button tertiary transient className="pointer-events-none">
<Text mainUiAction text03>
Your Current Plan
</Text>
</Button>
) : href ? (
<OpalButton
prominence="secondary"
<Button
main
secondary
href={href}
target="_blank"
rel="noopener noreferrer"
>
{buttonLabel}
</OpalButton>
</Button>
) : onClick ? (
<OpalButton onClick={onClick} icon={ButtonIcon}>
<Button main primary onClick={onClick} leftIcon={ButtonIcon}>
{buttonLabel}
</OpalButton>
</Button>
) : (
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
<Button tertiary transient className="pointer-events-none">
<Text mainUiAction text03>
Included in your plan

View File

@@ -66,7 +66,6 @@ function FooterLinks({
<Text secondaryBody text03>
Have a license key?
</Text>
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button action tertiary onClick={onActivateLicense}>
<Text secondaryBody text05 className="underline">
{licenseText}
@@ -74,7 +73,6 @@ function FooterLinks({
</Button>
</>
)}
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button
action
tertiary

View File

@@ -10,7 +10,7 @@ import { SourceIcon } from "@/components/SourceIcon";
import { EditableStringFieldDisplay } from "@/components/EditableStringFieldDisplay";
import { deleteSlackBot } from "./new/lib";
import GenericConfirmModal from "@/components/modals/GenericConfirmModal";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { cn } from "@/lib/utils";
import { SvgChevronDownSmall, SvgTrash } from "@opal/icons";
@@ -106,20 +106,20 @@ export const ExistingSlackBotForm = ({
<div className="flex flex-col" ref={dropdownRef}>
<div className="flex items-center gap-4">
<Button
prominence="secondary"
icon={({ className }) => (
leftIcon={({ className }) => (
<SvgChevronDownSmall
className={cn(className, !isExpanded && "-rotate-90")}
/>
)}
onClick={() => setIsExpanded(!isExpanded)}
secondary
>
Update Tokens
</Button>
<Button
variant="danger"
danger
onClick={() => setShowDeleteModal(true)}
icon={SvgTrash}
leftIcon={SvgTrash}
>
Delete
</Button>

View File

@@ -4,7 +4,7 @@ import { TextFormField } from "@/components/Field";
import { Form, Formik } from "formik";
import * as Yup from "yup";
import { createSlackBot, updateSlackBot } from "./new/lib";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import Separator from "@/refresh-components/Separator";
import { useEffect } from "react";
import { DOCS_ADMINS_PATH } from "@/lib/constants";

View File

@@ -17,8 +17,9 @@ import type { Route } from "next";
import { useState } from "react";
import { deleteSlackChannelConfig, isPersonaASlackBotPersona } from "./lib";
import { Card } from "@/components/ui/card";
import Button from "@/refresh-components/buttons/Button";
import CreateButton from "@/refresh-components/buttons/CreateButton";
import { Button } from "@opal/components";
import { Button as OpalButton } from "@opal/components";
import { SvgSettings, SvgTrash } from "@opal/icons";
const numToDisplay = 50;
@@ -44,11 +45,11 @@ export default function SlackChannelConfigsTable({
<div className="space-y-8">
<div className="flex justify-between items-center mb-6">
<Button
prominence="secondary"
onClick={() => {
window.location.href = `/admin/bots/${slackBotId}/channels/${defaultConfig?.id}`;
}}
icon={SvgSettings}
secondary
leftIcon={SvgSettings}
>
Edit Default Configuration
</Button>
@@ -120,7 +121,7 @@ export default function SlackChannelConfigsTable({
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<Button
<OpalButton
onClick={async (e) => {
e.stopPropagation();
const response = await deleteSlackChannelConfig(

View File

@@ -11,7 +11,7 @@ import {
TextArrayField,
TextFormField,
} from "@/components/Field";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { MinimalPersonaSnapshot } from "@/app/admin/agents/interfaces";
import DocumentSetCard from "@/sections/cards/DocumentSetCard";
import CollapsibleSection from "@/app/admin/agents/CollapsibleSection";
@@ -598,7 +598,7 @@ export function SlackChannelConfigFormFields({
</TooltipProvider>
)}
<Button type="submit">{isUpdate ? "Update" : "Create"}</Button>
<Button prominence="secondary" onClick={() => router.back()}>
<Button secondary onClick={() => router.back()}>
Cancel
</Button>
</div>

View File

@@ -2,7 +2,7 @@
import { useState } from "react";
import CardSection from "@/components/admin/CardSection";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import useSWR from "swr";
import { ThreeDotsLoader } from "@/components/Loading";
@@ -129,7 +129,7 @@ function Main() {
<div className="flex flex-col gap-2 desktop:flex-row desktop:items-center desktop:gap-2">
{isApiKeySet ? (
<>
<Button variant="danger" onClick={handleDelete}>
<Button onClick={handleDelete} danger>
Delete API Key
</Button>
<Text as="p" mainContentBody text04 className="desktop:mt-0">
@@ -137,7 +137,7 @@ function Main() {
</Text>
</>
) : (
<Button variant="action" onClick={handleSave}>
<Button onClick={handleSave} action>
Save API Key
</Button>
)}

View File

@@ -10,7 +10,6 @@ import {
import Text from "@/components/ui/text";
import Title from "@/components/ui/title";
import Button from "@/refresh-components/buttons/Button";
import { Button as OpalButton } from "@opal/components";
import { useMemo, useState } from "react";
import useSWR, { mutate } from "swr";
import { ReindexingProgressTable } from "../../../../components/embedding/ReindexingProgressTable";
@@ -24,7 +23,6 @@ import { FailedReIndexAttempts } from "@/components/embedding/FailedReIndexAttem
import { useConnectorIndexingStatusWithPagination } from "@/lib/hooks";
import { SvgX } from "@opal/icons";
import { ConnectorCredentialPairStatus } from "@/app/admin/connector/[ccPairId]/types";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
export default function UpgradingPage({
futureEmbeddingModel,
@@ -32,12 +30,11 @@ export default function UpgradingPage({
futureEmbeddingModel: CloudEmbeddingModel | HostedEmbeddingModel;
}) {
const [isCancelling, setIsCancelling] = useState<boolean>(false);
const vectorDbEnabled = useVectorDbEnabled();
const { data: connectors, isLoading: isLoadingConnectors } = useSWR<
Connector<any>[]
>(vectorDbEnabled ? "/api/manage/connector" : null, errorHandlingFetcher, {
refreshInterval: 5000,
>("/api/manage/connector", errorHandlingFetcher, {
refreshInterval: 5000, // 5 seconds
});
const {
@@ -45,8 +42,7 @@ export default function UpgradingPage({
isLoading: isLoadingOngoingReIndexingStatus,
} = useConnectorIndexingStatusWithPagination(
{ secondary_index: true, get_all_connectors: true },
5000,
vectorDbEnabled
5000
) as {
data: ConnectorIndexingStatusLiteResponse[];
isLoading: boolean;
@@ -55,11 +51,9 @@ export default function UpgradingPage({
const { data: failedIndexingStatus } = useSWR<
FailedConnectorIndexingStatus[]
>(
vectorDbEnabled
? "/api/manage/admin/connector/failed-indexing-status?secondary_index=true"
: null,
"/api/manage/admin/connector/failed-indexing-status?secondary_index=true",
errorHandlingFetcher,
{ refreshInterval: 5000 }
{ refreshInterval: 5000 } // 5 seconds
);
const onCancel = async () => {
@@ -146,13 +140,10 @@ export default function UpgradingPage({
</div>
</Modal.Body>
<Modal.Footer>
<OpalButton onClick={onCancel}>Confirm</OpalButton>
<OpalButton
prominence="secondary"
onClick={() => setIsCancelling(false)}
>
<Button onClick={onCancel}>Confirm</Button>
<Button onClick={() => setIsCancelling(false)} secondary>
Cancel
</OpalButton>
</Button>
</Modal.Footer>
</Modal.Content>
</Modal>
@@ -167,7 +158,6 @@ export default function UpgradingPage({
{futureEmbeddingModel.model_name}
</div>
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button
danger
className="mt-4"

View File

@@ -5,7 +5,7 @@ import { errorHandlingFetcher } from "@/lib/fetcher";
import * as SettingsLayouts from "@/layouts/settings-layouts";
import Text from "@/components/ui/text";
import Title from "@/components/ui/title";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import useSWR from "swr";
import { ModelPreview } from "@/components/embedding/ModelSelector";
import {
@@ -130,7 +130,7 @@ function Main() {
</CardSection>
<div className="mt-4">
<Button variant="action" href="/admin/embeddings">
<Button action href="/admin/embeddings">
Update Search Settings
</Button>
</div>

View File

@@ -6,7 +6,7 @@ import { FormField } from "@/refresh-components/form/FormField";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";
import PasswordInputTypeIn from "@/refresh-components/inputs/PasswordInputTypeIn";
import Modal from "@/refresh-components/Modal";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { SvgArrowExchange, SvgOnyxLogo } from "@opal/icons";
import type { IconProps } from "@opal/types";
@@ -244,11 +244,13 @@ export const WebProviderSetupModal = memo(
)}
</Modal.Body>
<Modal.Footer>
<Button prominence="secondary" type="button" onClick={onClose}>
<Button type="button" main secondary onClick={onClose}>
Cancel
</Button>
<Button
type="button"
main
primary
disabled={!canConnect || isProcessing}
onClick={onConnect}
>

View File

@@ -91,7 +91,6 @@ function HoverIconButton({
}: HoverIconButtonProps) {
return (
<div onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
{/* TODO(@raunakab): migrate to opal Button once HoverIconButtonProps typing is resolved */}
<Button {...buttonProps} rightIcon={isHovered ? SvgX : SvgCheckSquare}>
{children}
</Button>
@@ -1011,8 +1010,9 @@ export default function Page() {
{buttonState.label}
</HoverIconButton>
) : (
<OpalButton
prominence="tertiary"
<Button
action={false}
tertiary
disabled={
buttonState.disabled || !buttonState.onClick
}
@@ -1029,7 +1029,7 @@ export default function Page() {
}
>
{buttonState.label}
</OpalButton>
</Button>
)}
</div>
</div>
@@ -1202,8 +1202,9 @@ export default function Page() {
{buttonState.label}
</HoverIconButton>
) : (
<OpalButton
prominence="tertiary"
<Button
action={false}
tertiary
disabled={
buttonState.disabled || !buttonState.onClick
}
@@ -1220,7 +1221,7 @@ export default function Page() {
}
>
{buttonState.label}
</OpalButton>
</Button>
)}
</div>
</div>

View File

@@ -5,7 +5,8 @@ import { useState } from "react";
import { ValidSources } from "@/lib/types";
import { Section } from "@/layouts/general-layouts";
import Text from "@/refresh-components/texts/Text";
import { Button } from "@opal/components";
import IconButton from "@/refresh-components/buttons/IconButton";
import Button from "@/refresh-components/buttons/Button";
import Separator from "@/refresh-components/Separator";
import { SvgChevronUp, SvgChevronDown, SvgEdit } from "@opal/icons";
import Truncated from "@/refresh-components/texts/Truncated";
@@ -118,21 +119,16 @@ function ConfigItem({ label, value, onEdit }: ConfigItemProps) {
{isExpandable && (
<Button
prominence="tertiary"
tertiary
size="md"
icon={isExpanded ? SvgChevronUp : SvgChevronDown}
leftIcon={isExpanded ? SvgChevronUp : SvgChevronDown}
onClick={() => setIsExpanded(!isExpanded)}
>
{isExpanded ? "Show less" : `Show all (${value.length} items)`}
</Button>
)}
{onEdit && (
<Button
prominence="tertiary"
icon={SvgEdit}
onClick={onEdit}
tooltip="Edit"
/>
<IconButton icon={SvgEdit} tertiary onClick={onEdit} tooltip="Edit" />
)}
</Section>
</Section>

View File

@@ -229,7 +229,6 @@ export default function IndexAttemptErrorsModal({
<div className="flex w-full">
<div className="flex gap-2 ml-auto">
{hasUnresolvedErrors && !isResolvingErrors && (
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
<Button
onClick={onResolveAll}
className="ml-4 whitespace-nowrap"

View File

@@ -1,7 +1,8 @@
"use client";
import { useState, useRef } from "react";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { Button as OpalButton } from "@opal/components";
import {
Table,
TableBody,
@@ -175,25 +176,26 @@ export default function InlineFileManagement({
<div className="flex gap-2">
{!isEditing ? (
<Button
prominence="secondary"
onClick={() => setIsEditing(true)}
icon={SvgEdit}
secondary
leftIcon={SvgEdit}
>
Edit
</Button>
) : (
<>
<Button
prominence="secondary"
onClick={handleCancel}
icon={SvgX}
secondary
leftIcon={SvgX}
disabled={isSaving}
>
Cancel
</Button>
<Button
onClick={handleSaveClick}
icon={SvgCheck}
primary
leftIcon={SvgCheck}
disabled={
isSaving ||
(selectedFilesToRemove.size === 0 && filesToAdd.length === 0)
@@ -293,7 +295,7 @@ export default function InlineFileManagement({
>
{isEditing && (
<TableCell>
<Button
<OpalButton
icon={SvgX}
variant="danger"
prominence="tertiary"
@@ -333,9 +335,9 @@ export default function InlineFileManagement({
id={`file-upload-${connectorId}`}
/>
<Button
prominence="secondary"
onClick={() => fileInputRef.current?.click()}
icon={SvgPlusCircle}
secondary
leftIcon={SvgPlusCircle}
disabled={isSaving}
>
Add Files
@@ -396,8 +398,8 @@ export default function InlineFileManagement({
<Modal.Footer>
<Button
prominence="secondary"
onClick={() => setShowSaveConfirm(false)}
secondary
disabled={isSaving}
>
Cancel

View File

@@ -1,6 +1,6 @@
"use client";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { useState } from "react";
import { toast } from "@/hooks/useToast";
import { triggerIndexing } from "@/app/admin/connector/[ccPairId]/lib";

View File

@@ -62,7 +62,7 @@ import { DropdownMenuItemWithTooltip } from "@/components/ui/dropdown-menu-with-
import { timeAgo } from "@/lib/time";
import { useStatusChange } from "./useStatusChange";
import { useReIndexModal } from "./ReIndexModal";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { SvgSettings } from "@opal/icons";
import { UserRole } from "@/lib/types";
import { useUser } from "@/providers/UserProvider";
@@ -456,7 +456,7 @@ function Main({ ccPairId }: { ccPairId: number }) {
{ccPair.is_editable_for_current_user && (
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button prominence="secondary" icon={SvgSettings}>
<Button leftIcon={SvgSettings} secondary>
Manage
</Button>
</DropdownMenuTrigger>

View File

@@ -55,7 +55,7 @@ import {
} from "@/lib/connectors/oauth";
import { CreateStdOAuthCredential } from "@/components/credentials/actions/CreateStdOAuthCredential";
import { Spinner } from "@/components/Spinner";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { deleteConnector } from "@/lib/connector";
import ConnectorDocsLink from "@/components/admin/connectors/ConnectorDocsLink";
import Text from "@/refresh-components/texts/Text";
@@ -580,7 +580,7 @@ export default function AddConnector({
{oauthSupportedSources.includes(connector) &&
(NEXT_PUBLIC_CLOUD_ENABLED || NEXT_PUBLIC_TEST_ENV) && (
<Button
variant="action"
action
onClick={handleAuthorize}
disabled={isAuthorizing}
hidden={!isAuthorizeVisible}

View File

@@ -48,7 +48,6 @@ export default function ConnectorWrapper({
<HeaderTitle>
<p>&lsquo;{connector}&rsquo; is not a valid Connector Type!</p>
</HeaderTitle>
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button
onClick={() => window.open("/admin/indexing/status", "_self")}
className="mr-auto"

View File

@@ -1,5 +1,5 @@
import { useFormContext } from "@/components/context/FormContext";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { SvgArrowLeft, SvgArrowRight, SvgPlusCircle } from "@opal/icons";
const NavigationRow = ({
@@ -22,11 +22,7 @@ const NavigationRow = ({
<div>
{((formStep > 0 && !noCredentials) ||
(formStep > 1 && !noAdvanced)) && (
<Button
prominence="secondary"
onClick={prevFormStep}
icon={SvgArrowLeft}
>
<Button secondary onClick={prevFormStep} leftIcon={SvgArrowLeft}>
Previous
</Button>
)}
@@ -45,7 +41,7 @@ const NavigationRow = ({
<div className="flex justify-end">
{formStep === 0 && (
<Button
variant="action"
action
disabled={!activatedCredential}
rightIcon={SvgArrowRight}
onClick={() => nextFormStep()}
@@ -55,7 +51,7 @@ const NavigationRow = ({
)}
{!noAdvanced && formStep === 1 && (
<Button
prominence="secondary"
secondary
disabled={!isValid}
rightIcon={SvgArrowRight}
onClick={() => nextFormStep()}

View File

@@ -3,7 +3,7 @@
import { useEffect, useState } from "react";
import { usePathname, useRouter, useSearchParams } from "next/navigation";
import { AdminPageTitle } from "@/components/admin/Title";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { getSourceMetadata, isValidSource } from "@/lib/sources";
import { ConfluenceAccessibleResource, ValidSources } from "@/lib/types";
import CardSection from "@/components/admin/CardSection";

View File

@@ -1,7 +1,7 @@
import React from "react";
import NumberInput from "./ConnectorInput/NumberInput";
import { TextFormField } from "@/components/Field";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { SvgTrash } from "@opal/icons";
export default function AdvancedFormPage() {
return (
@@ -35,7 +35,7 @@ export default function AdvancedFormPage() {
name="indexingStart"
/>
<div className="mt-4 flex w-full mx-auto max-w-2xl justify-start">
<Button variant="danger" icon={SvgTrash} type="submit">
<Button leftIcon={SvgTrash} danger type="submit">
Reset
</Button>
</div>

View File

@@ -10,7 +10,7 @@ import { DOCS_ADMINS_PATH } from "@/lib/constants";
import { TextFormField, SectionHeader } from "@/components/Field";
import { Form, Formik } from "formik";
import { User } from "@/lib/types";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import {
Credential,
GoogleDriveCredentialJson,
@@ -314,7 +314,7 @@ export const DriveJsonUploadSection = ({
{isAdmin && !existingAuthCredential && (
<div className="mt-2">
<Button
variant="danger"
danger
onClick={async () => {
const endpoint =
localServiceAccountData?.service_account_email
@@ -467,7 +467,7 @@ export const DriveAuthSection = ({
</div>
</div>
<Button
variant="danger"
danger
onClick={async () => {
handleRevokeAccess(
connectorAssociated,

View File

@@ -1,4 +1,4 @@
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { toast } from "@/hooks/useToast";
import React, { useState, useEffect } from "react";
import { useSWRConfig } from "swr";
@@ -314,7 +314,7 @@ export const GmailJsonUploadSection = ({
{isAdmin && !existingAuthCredential && (
<div className="mt-2">
<Button
variant="danger"
danger
onClick={async () => {
const endpoint =
localServiceAccountData?.service_account_email
@@ -470,7 +470,7 @@ export const GmailAuthSection = ({
</div>
<Section flexDirection="row" justifyContent="between" height="fit">
<Button
variant="danger"
danger
onClick={async () => {
handleRevokeAccess(
connectorExists,
@@ -482,7 +482,10 @@ export const GmailAuthSection = ({
Revoke Access
</Button>
{buildMode && onCredentialCreated && (
<Button onClick={() => onCredentialCreated(existingCredential)}>
<Button
primary
onClick={() => onCredentialCreated(existingCredential)}
>
Continue
</Button>
)}

View File

@@ -11,7 +11,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { Card } from "@/components/ui/card";
import Text from "@/components/ui/text";
import { Spinner } from "@/components/Spinner";
@@ -99,9 +99,9 @@ function Main() {
<TableCell className="font-medium">{category}</TableCell>
<TableCell>
<Button
prominence="secondary"
onClick={() => handleDownload(category)}
icon={SvgDownloadCloud}
secondary
leftIcon={SvgDownloadCloud}
>
Download Logs
</Button>

View File

@@ -4,7 +4,7 @@ import { useState } from "react";
import { Section } from "@/layouts/general-layouts";
import Text from "@/refresh-components/texts/Text";
import Card from "@/refresh-components/cards/Card";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { Badge } from "@/components/ui/badge";
import PasswordInputTypeIn from "@/refresh-components/inputs/PasswordInputTypeIn";
import { ThreeDotsLoader } from "@/components/Loading";
@@ -126,9 +126,9 @@ export function BotConfigCard() {
disabled={!hasServerConfigs}
>
<Button
variant="danger"
onClick={() => setShowDeleteConfirm(true)}
disabled={isSubmitting || hasServerConfigs}
danger
>
Delete Discord Token
</Button>

View File

@@ -12,7 +12,7 @@ import {
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { DeleteButton } from "@/components/DeleteButton";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import Switch from "@/refresh-components/inputs/Switch";
import { SvgEdit, SvgServer } from "@opal/icons";
import EmptyMessage from "@/refresh-components/EmptyMessage";
@@ -116,10 +116,10 @@ export function DiscordGuildsTable({ guilds, onRefresh }: Props) {
<TableRow key={guild.id}>
<TableCell>
<Button
prominence="internal"
internal
disabled={!guild.guild_id}
onClick={() => router.push(`/admin/discord-bot/${guild.id}`)}
icon={SvgEdit}
leftIcon={SvgEdit}
>
{guild.guild_name || `Server #${guild.id}`}
</Button>

View File

@@ -12,7 +12,7 @@ import Text from "@/refresh-components/texts/Text";
import Card from "@/refresh-components/cards/Card";
import { Callout } from "@/components/ui/callout";
import Message from "@/refresh-components/messages/Message";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { SvgServer } from "@opal/icons";
import InputSelect from "@/refresh-components/inputs/InputSelect";
import {
@@ -104,17 +104,13 @@ function GuildDetailContent({
width="fit"
gap={0.5}
>
<Button
prominence="secondary"
onClick={handleEnableAll}
disabled={disabled}
>
<Button onClick={handleEnableAll} disabled={disabled} secondary>
Enable All
</Button>
<Button
prominence="secondary"
onClick={handleDisableAll}
disabled={disabled}
secondary
>
Disable All
</Button>

View File

@@ -200,7 +200,6 @@ function RetrievalSourceSection() {
</InputSelect>
{hasChanges && (
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
<Button
className="self-center"
onClick={handleUpdate}

View File

@@ -257,7 +257,6 @@ export const DocumentSetCreationForm = ({
</div>
<div className="flex mt-6 pt-4 border-t border-border-02">
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button
type="submit"
disabled={props.isSubmitting}

View File

@@ -10,11 +10,9 @@ import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import CardSection from "@/components/admin/CardSection";
import { DocumentSetCreationForm } from "../DocumentSetCreationForm";
import { useRouter } from "next/navigation";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
function Main({ documentSetId }: { documentSetId: number }) {
const router = useRouter();
const vectorDbEnabled = useVectorDbEnabled();
const {
data: documentSets,
@@ -26,16 +24,12 @@ function Main({ documentSetId }: { documentSetId: number }) {
data: ccPairs,
isLoading: isCCPairsLoading,
error: ccPairsError,
} = useConnectorStatus(30000, vectorDbEnabled);
} = useConnectorStatus();
// EE only
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
if (
isDocumentSetsLoading ||
(vectorDbEnabled && isCCPairsLoading) ||
userGroupsIsLoading
) {
if (isDocumentSetsLoading || isCCPairsLoading || userGroupsIsLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<ThreeDotsLoader />
@@ -52,7 +46,7 @@ function Main({ documentSetId }: { documentSetId: number }) {
);
}
if (vectorDbEnabled && (ccPairsError || !ccPairs)) {
if (ccPairsError || !ccPairs) {
return (
<ErrorCallout
errorTitle="Failed to fetch Connectors"
@@ -76,7 +70,7 @@ function Main({ documentSetId }: { documentSetId: number }) {
return (
<CardSection>
<DocumentSetCreationForm
ccPairs={ccPairs ?? []}
ccPairs={ccPairs}
userGroups={userGroups}
onClose={() => {
refreshDocumentSets();

View File

@@ -9,22 +9,20 @@ import { ErrorCallout } from "@/components/ErrorCallout";
import { useRouter } from "next/navigation";
import { refreshDocumentSets } from "../hooks";
import CardSection from "@/components/admin/CardSection";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
function Main() {
const router = useRouter();
const vectorDbEnabled = useVectorDbEnabled();
const {
data: ccPairs,
isLoading: isCCPairsLoading,
error: ccPairsError,
} = useConnectorStatus(30000, vectorDbEnabled);
} = useConnectorStatus();
// EE only
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
if ((vectorDbEnabled && isCCPairsLoading) || userGroupsIsLoading) {
if (isCCPairsLoading || userGroupsIsLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<ThreeDotsLoader />
@@ -32,7 +30,7 @@ function Main() {
);
}
if (vectorDbEnabled && (ccPairsError || !ccPairs)) {
if (ccPairsError || !ccPairs) {
return (
<ErrorCallout
errorTitle="Failed to fetch Connectors"
@@ -45,7 +43,7 @@ function Main() {
<>
<CardSection>
<DocumentSetCreationForm
ccPairs={ccPairs ?? []}
ccPairs={ccPairs}
userGroups={userGroups}
onClose={() => {
refreshDocumentSets();

View File

@@ -27,7 +27,7 @@ import {
EMBEDDING_PROVIDERS_ADMIN_URL,
} from "@/lib/llmConfig/constants";
import { AdvancedSearchConfiguration } from "@/app/admin/embeddings/interfaces";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
export interface EmbeddingDetails {
api_key?: string;
@@ -279,7 +279,7 @@ export default function EmbeddingModelSelection({
{currentEmbeddingModel?.provider_type && (
<div className="mt-2">
<Button
prominence="secondary"
secondary
onClick={() => {
const allProviders = [
...AVAILABLE_CLOUD_PROVIDERS,

View File

@@ -21,7 +21,7 @@ import {
MixedBreadIcon,
} from "@/components/icons/icons";
import Modal from "@/refresh-components/Modal";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { TextFormField } from "@/components/Field";
import { SettingsContext } from "@/providers/SettingsProvider";
import { SvgAlertTriangle, SvgKey } from "@opal/icons";

View File

@@ -1,5 +1,5 @@
import Modal from "@/refresh-components/Modal";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { CloudEmbeddingModel } from "../../../../components/embedding/interfaces";
import { SvgCheck } from "@opal/icons";

View File

@@ -268,7 +268,6 @@ export default function ChangeCredentialsModal({
</Callout>
)}
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button
className="mr-auto mt-4"
onClick={() => handleSubmit()}
@@ -290,7 +289,6 @@ export default function ChangeCredentialsModal({
embedding type!
</Text>
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button className="mr-auto" onClick={handleDelete} danger>
Delete Configuration
</Button>

View File

@@ -1,6 +1,6 @@
import Modal from "@/refresh-components/Modal";
import Text from "@/refresh-components/texts/Text";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { Callout } from "@/components/ui/callout";
import {
CloudEmbeddingProvider,
@@ -38,10 +38,10 @@ export default function DeleteCredentialsModal({
<Callout type="danger" title="Point of No Return" />
</Modal.Body>
<Modal.Footer>
<Button prominence="secondary" onClick={onCancel}>
<Button secondary onClick={onCancel}>
Keep Credentials
</Button>
<Button variant="danger" onClick={onConfirm}>
<Button danger onClick={onConfirm}>
Delete Credentials
</Button>
</Modal.Footer>

View File

@@ -1,5 +1,5 @@
import Modal from "@/refresh-components/Modal";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import Text from "@/refresh-components/texts/Text";
import { SvgAlertTriangle } from "@opal/icons";
export interface InstantSwitchConfirmModalProps {
@@ -31,7 +31,7 @@ export default function InstantSwitchConfirmModal({
</Modal.Body>
<Modal.Footer>
<Button onClick={onConfirm}>Confirm</Button>
<Button prominence="secondary" onClick={onClose}>
<Button secondary onClick={onClose}>
Cancel
</Button>
</Modal.Footer>

View File

@@ -1,7 +1,7 @@
import Modal from "@/refresh-components/Modal";
import Text from "@/refresh-components/texts/Text";
import { Callout } from "@/components/ui/callout";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { HostedEmbeddingModel } from "@/components/embedding/interfaces";
import { SvgServer } from "@opal/icons";
@@ -57,7 +57,7 @@ export default function ModelSelectionConfirmationModal({
</Modal.Body>
<Modal.Footer>
<Button onClick={onConfirm}>Confirm</Button>
<Button prominence="secondary" onClick={onCancel}>
<Button secondary onClick={onCancel}>
Cancel
</Button>
</Modal.Footer>

View File

@@ -1,10 +1,11 @@
import React, { useRef, useState } from "react";
import Text from "@/refresh-components/texts/Text";
import { Callout } from "@/components/ui/callout";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { Formik, Form } from "formik";
import * as Yup from "yup";
import { Label, TextFormField } from "@/components/Field";
import { LoadingAnimation } from "@/components/Loading";
import {
CloudEmbeddingProvider,
EmbeddingProvider,
@@ -13,7 +14,6 @@ import {
import { EMBEDDING_PROVIDERS_ADMIN_URL } from "@/lib/llmConfig/constants";
import Modal from "@/refresh-components/Modal";
import { SvgSettings } from "@opal/icons";
import SimpleLoader from "@/refresh-components/loaders/SimpleLoader";
export interface ProviderCreationModalProps {
updateCurrentModel: (
newModel: string,
@@ -39,6 +39,7 @@ export default function ProviderCreationModal({
const useFileUpload =
selectedProvider.provider_type == EmbeddingProvider.GOOGLE;
const [isProcessing, setIsProcessing] = useState(false);
const [errorMsg, setErrorMsg] = useState<string>("");
const [fileName, setFileName] = useState<string>("");
@@ -109,6 +110,7 @@ export default function ProviderCreationModal({
values: any,
{ setSubmitting }: { setSubmitting: (isSubmitting: boolean) => void }
) => {
setIsProcessing(true);
setErrorMsg("");
try {
const customConfig = Object.fromEntries(values.custom_config);
@@ -139,6 +141,7 @@ export default function ProviderCreationModal({
if (!initialResponse.ok) {
const errorMsg = (await initialResponse.json()).detail;
setErrorMsg(errorMsg);
setIsProcessing(false);
setSubmitting(false);
return;
}
@@ -176,6 +179,7 @@ export default function ProviderCreationModal({
setErrorMsg("An unknown error occurred");
}
} finally {
setIsProcessing(false);
setSubmitting(false);
}
};
@@ -298,15 +302,16 @@ export default function ProviderCreationModal({
<Button
type="submit"
width="full"
className="w-full"
disabled={isSubmitting}
icon={isSubmitting ? SimpleLoader : undefined}
>
{isSubmitting
? "Submitting"
: existingProvider
? "Update"
: "Create"}
{isProcessing ? (
<LoadingAnimation />
) : existingProvider ? (
"Update"
) : (
"Create"
)}
</Button>
</Form>
)}

View File

@@ -1,5 +1,5 @@
import Modal from "@/refresh-components/Modal";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import Text from "@/refresh-components/texts/Text";
import { CloudEmbeddingModel } from "@/components/embedding/interfaces";
import { SvgServer } from "@opal/icons";
@@ -32,7 +32,7 @@ export default function SelectModelModal({
</Modal.Body>
<Modal.Footer>
<Button onClick={onConfirm}>Confirm</Button>
<Button prominence="secondary" onClick={onCancel}>
<Button secondary onClick={onCancel}>
Cancel
</Button>
</Modal.Footer>

View File

@@ -6,7 +6,6 @@ import EmbeddingModelSelection from "../EmbeddingModelSelectionForm";
import { useCallback, useEffect, useMemo, useState, useRef } from "react";
import Text from "@/refresh-components/texts/Text";
import Button from "@/refresh-components/buttons/Button";
import { Button as OpalButton } from "@opal/components";
import { WarningCircle, Warning, CaretDownIcon } from "@phosphor-icons/react";
import {
CloudEmbeddingModel,
@@ -248,7 +247,6 @@ export default function EmbeddingForm() {
return needsReIndex ? (
<div className="flex mx-auto gap-x-1 ml-auto items-center">
<div className="flex items-center h-fit">
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button
onClick={() => {
if (switchoverType == SwitchoverType.INSTANT) {
@@ -270,7 +268,6 @@ export default function EmbeddingForm() {
</Button>
<DropdownMenu>
<DropdownMenuTrigger asChild>
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button
disabled={!isOverallFormValid}
action
@@ -376,7 +373,7 @@ export default function EmbeddingForm() {
</div>
) : (
<div className="flex mx-auto gap-x-1 ml-auto items-center">
<OpalButton
<Button
onClick={() => {
updateSearch();
navigateToEmbeddingPage("search settings");
@@ -384,7 +381,7 @@ export default function EmbeddingForm() {
disabled={!isOverallFormValid}
>
Update Search
</OpalButton>
</Button>
{!isOverallFormValid &&
Object.keys(combinedFormErrors).length > 0 && (
<div className="relative group">
@@ -510,8 +507,7 @@ export default function EmbeddingForm() {
/>
</CardSection>
<div className="mt-4 flex w-full justify-end">
<OpalButton
variant="action"
<Button
onClick={() => {
if (
selectedProvider.model_name.includes("e5") &&
@@ -526,9 +522,10 @@ export default function EmbeddingForm() {
}
}}
rightIcon={SvgArrowRight}
action
>
Continue
</OpalButton>
</Button>
</div>
</>
)}
@@ -560,13 +557,10 @@ export default function EmbeddingForm() {
</div>
</Modal.Body>
<Modal.Footer>
<OpalButton
prominence="secondary"
onClick={() => setShowPoorModel(false)}
>
<Button secondary onClick={() => setShowPoorModel(false)}>
Cancel update
</OpalButton>
<OpalButton
</Button>
<Button
onClick={() => {
setShowPoorModel(false);
// Skip reranking step (step 1), go directly to advanced settings (step 2)
@@ -575,7 +569,7 @@ export default function EmbeddingForm() {
}}
>
{`Continue with ${selectedProvider.model_name}`}
</OpalButton>
</Button>
</Modal.Footer>
</Modal.Content>
</Modal>
@@ -621,26 +615,26 @@ export default function EmbeddingForm() {
</CardSection>
<div className={`mt-4 w-full grid grid-cols-3`}>
<OpalButton
prominence="secondary"
icon={SvgArrowLeft}
<Button
leftIcon={SvgArrowLeft}
onClick={() => prevFormStep()}
secondary
>
Previous
</OpalButton>
</Button>
<ReIndexingButton needsReIndex={needsReIndex} />
<div className="flex w-full justify-end">
<OpalButton
prominence="secondary"
<Button
onClick={() => {
nextFormStep();
}}
rightIcon={SvgArrowRight}
secondary
>
Advanced
</OpalButton>
</Button>
</div>
</div>
</>
@@ -666,17 +660,17 @@ export default function EmbeddingForm() {
</CardSection>
<div className={`mt-4 grid grid-cols-3 w-full `}>
<OpalButton
prominence="secondary"
<Button
onClick={() => {
// Skip reranking step (step 1), go back to embedding model (step 0)
prevFormStep();
prevFormStep();
}}
icon={SvgArrowLeft}
leftIcon={SvgArrowLeft}
secondary
>
Previous
</OpalButton>
</Button>
<ReIndexingButton needsReIndex={needsReIndex} />
</div>

View File

@@ -62,7 +62,6 @@ export default function OpenEmbeddingPage({
Onyx team.
</Text>
{!configureModel && (
// TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved
<Button
onClick={() => setConfigureModel(true)}
className="mt-4"

View File

@@ -10,10 +10,11 @@ import {
DropdownMenuTrigger,
DropdownMenuCheckboxItem,
} from "@/components/ui/dropdown-menu";
import Button from "@/refresh-components/buttons/Button";
import { Input } from "@/components/ui/input";
import { Badge } from "@/components/ui/badge";
import { AccessType, ValidStatuses } from "@/lib/types";
import { Button } from "@opal/components";
import { Button as OpalButton } from "@opal/components";
import { SvgFilter } from "@opal/icons";
export interface FilterOptions {
accessType: AccessType[] | null;
@@ -127,7 +128,11 @@ export const FilterComponent = forwardRef<
<div className="relative">
<DropdownMenu open={isOpen} onOpenChange={handleOpenChange}>
<DropdownMenuTrigger asChild>
<Button icon={SvgFilter} prominence="secondary" transient={isOpen} />
<OpalButton
icon={SvgFilter}
prominence="secondary"
transient={isOpen}
/>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
@@ -237,7 +242,7 @@ export const FilterComponent = forwardRef<
>
<div className="flex gap-2">
<Button
prominence={docsOperator !== ">" ? "secondary" : "primary"}
secondary={docsOperator !== ">"}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -248,7 +253,7 @@ export const FilterComponent = forwardRef<
&gt;
</Button>
<Button
prominence={docsOperator !== "<" ? "secondary" : "primary"}
secondary={docsOperator !== "<"}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -259,7 +264,7 @@ export const FilterComponent = forwardRef<
&lt;
</Button>
<Button
prominence={docsOperator !== "=" ? "secondary" : "primary"}
secondary={docsOperator !== "="}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
@@ -281,7 +286,7 @@ export const FilterComponent = forwardRef<
</div>
<div className="px-2 py-1.5">
<Button
width="full"
className="w-full"
onClick={(e) => {
e.preventDefault();
e.stopPropagation();

View File

@@ -1,7 +1,7 @@
"use client";
import { useState, useEffect } from "react";
import { Button } from "@opal/components";
import Button from "@/refresh-components/buttons/Button";
import { Badge } from "@/components/ui/badge";
import { FilterComponent, FilterOptions } from "./FilterComponent";
import InputTypeIn from "@/refresh-components/inputs/InputTypeIn";

View File

@@ -8,8 +8,7 @@ import { ADMIN_ROUTE_CONFIG, ADMIN_PATHS } from "@/lib/admin-routes";
import Text from "@/components/ui/text";
import { useConnectorIndexingStatusWithPagination } from "@/lib/hooks";
import { useToastFromQuery } from "@/hooks/useToast";
import { Button } from "@opal/components";
import { useVectorDbEnabled } from "@/providers/SettingsProvider";
import Button from "@/refresh-components/buttons/Button";
import { useState, useRef, useMemo, RefObject } from "react";
import { FilterOptions } from "./FilterComponent";
import { ValidSources } from "@/lib/types";
@@ -19,8 +18,6 @@ import { ConnectorStaggeredSkeleton } from "./ConnectorRowSkeleton";
import { IndexingStatusRequest } from "@/lib/types";
function Main() {
const vectorDbEnabled = useVectorDbEnabled();
// State for filter management
const [filterOptions, setFilterOptions] = useState<FilterOptions>({
accessType: null,
@@ -68,7 +65,7 @@ function Main() {
sourcePages,
sourceLoadingStates,
resetPagination,
} = useConnectorIndexingStatusWithPagination(request, 30000, vectorDbEnabled);
} = useConnectorIndexingStatusWithPagination(request, 30000);
// Check if filters are active
const hasActiveFilters = useMemo(() => {

Some files were not shown because too many files have changed in this diff Show More