Compare commits

..

16 Commits

Author SHA1 Message Date
Bo-Onyx
2d996e05a4 chore(fe): opal button migration (#8864) 2026-03-04 22:52:49 +00:00
Nikolas Garza
b2956f795b refactor: migrate LLM & embedding management to OnyxError (#9025) 2026-03-04 22:09:25 +00:00
Danelegend
b272085543 fix: Code Interpreter Client session clean up (#9028)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 21:58:00 +00:00
Justin Tahara
8193aa4fd0 fix(ui): Persist agent sharing changes immediately for existing agents (#9024) 2026-03-04 21:34:50 +00:00
dependabot[bot]
52db41a00b chore(deps): bump nltk from 3.9.1 to 3.9.3 (#9045)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Jamison Lahman <jamison@lahman.dev>
2026-03-04 21:21:37 +00:00
SubashMohan
f1cf3c4589 feat(table): add table primitive components and styles (#9017) 2026-03-04 21:06:53 +00:00
dependabot[bot]
5322aeed90 chore(deps): bump hono from 4.11.7 to 4.12.5 in /backend/onyx/server/features/build/sandbox/kubernetes/docker/templates/outputs/web (#9044)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-04 12:51:05 -08:00
Evan Lohn
5da8870fd2 fix: stop calling unsupported endpoints no vectordb (#9012) 2026-03-04 20:18:09 +00:00
Nikolas Garza
57d3ab3b40 feat: add SCIM token management page (#9001) 2026-03-04 19:48:37 +00:00
Nikolas Garza
649c7fe8b9 feat(slack): convert markdown tables to Slack-friendly format (#8999) 2026-03-04 19:16:50 +00:00
Jamison Lahman
e5e2bc6149 chore(fe): "Share Chat"->"Share" (#9022) 2026-03-04 11:08:14 -08:00
Jamison Lahman
b148065e1d chore(devtools): --debug mode for desktop (#9027) 2026-03-04 11:07:52 -08:00
Evan Lohn
367808951c chore: remove lightweight mode (#9014) 2026-03-04 18:26:05 +00:00
Jamison Lahman
0f74da3302 fix(fe): dont align center modals on small screens (#8988) 2026-03-04 17:46:35 +00:00
Raunak Bhagat
96f7cbd25a fix: Use IllustrationContent for empty search results (#9013) 2026-03-04 16:54:23 +00:00
Raunak Bhagat
c627cea17d feat(opal): add sidebar variant to Interactive + refactor SidebarTab (#9016) 2026-03-04 15:52:56 +00:00
281 changed files with 3119 additions and 4102 deletions

View File

@@ -335,7 +335,6 @@ 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,10 +268,11 @@ 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"
cache: "npm" # zizmor: ignore[cache-poisoning]
cache-dependency-path: ./web/package-lock.json
- name: Install node dependencies
@@ -279,6 +280,7 @@ 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
@@ -590,6 +592,108 @@ 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]
@@ -686,7 +790,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]
needs: [playwright-tests, playwright-tests-lite]
if: ${{ always() }}
steps:
- name: Check job status

43
.vscode/launch.json vendored
View File

@@ -40,19 +40,7 @@
}
},
{
"name": "Celery (lightweight mode)",
"configurations": [
"Celery primary",
"Celery background",
"Celery beat"
],
"presentation": {
"group": "1"
},
"stopAll": true
},
{
"name": "Celery (standard mode)",
"name": "Celery",
"configurations": [
"Celery primary",
"Celery light",
@@ -253,35 +241,6 @@
},
"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,37 +86,6 @@ 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

@@ -1,15 +0,0 @@
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,6 +15,7 @@ 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
@@ -471,7 +472,9 @@ 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()
name=user_group.name,
time_last_modified_by_user=func.now(),
is_up_to_date=DISABLE_VECTOR_DB,
)
db_session.add(db_user_group)
db_session.flush() # give the group an ID
@@ -774,8 +777,7 @@ def update_user_group(
cc_pair_ids=user_group_update.cc_pair_ids,
)
# only needs to sync with Vespa if the cc_pairs have been updated
if cc_pairs_updated:
if cc_pairs_updated and not DISABLE_VECTOR_DB:
db_user_group.is_up_to_date = False
removed_users = db_session.scalars(

View File

@@ -223,6 +223,15 @@ 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,
@@ -230,6 +239,7 @@ 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,6 +365,7 @@ 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,6 +5,8 @@ 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
@@ -20,6 +22,7 @@ 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
@@ -153,3 +156,8 @@ 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

@@ -1,142 +0,0 @@
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

@@ -1,23 +0,0 @@
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

@@ -1,10 +0,0 @@
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,14 +495,7 @@ CELERY_WORKER_PRIMARY_POOL_OVERFLOW = int(
os.environ.get("CELERY_WORKER_PRIMARY_POOL_OVERFLOW") or 4
)
# 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)
# Individual worker concurrency settings
CELERY_WORKER_HEAVY_CONCURRENCY = int(
os.environ.get("CELERY_WORKER_HEAVY_CONCURRENCY") or 4
)

View File

@@ -84,7 +84,6 @@ 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,6 +13,7 @@ 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
@@ -246,6 +247,7 @@ 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)
@@ -336,7 +338,8 @@ def update_document_set(
)
document_set_row.description = document_set_update_request.description
document_set_row.is_up_to_date = False
if not DISABLE_VECTOR_DB:
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"])
md = create_markdown(renderer=SlackRenderer(), plugins=["strikethrough", "table"])
result = md(normalized_message)
# With HTMLRenderer, result is always str (not AST list)
assert isinstance(result, str)
@@ -146,6 +146,11 @@ 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)
@@ -218,5 +223,48 @@ 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.11.7",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.11.7.tgz",
"integrity": "sha512-l7qMiNee7t82bH3SeyUCt9UF15EVmaBvsppY2zQtrbIhl/yzBTny+YUxsVjSjQ6gaqaeVtZmGocom8TzBlA4Yw==",
"version": "4.12.5",
"resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz",
"integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==",
"license": "MIT",
"engines": {
"node": ">=16.9.0"

View File

@@ -11,6 +11,7 @@ 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
@@ -142,7 +143,10 @@ def delete_document_set(
except Exception as e:
raise HTTPException(status_code=400, detail=str(e))
if not DISABLE_VECTOR_DB:
if DISABLE_VECTOR_DB:
db_session.refresh(document_set)
db_delete_document_set(document_set, db_session)
else:
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
@@ -11,6 +10,8 @@ from onyx.db.llm import upsert_cloud_embedding_provider
from onyx.db.models import User
from onyx.db.search_settings import get_all_search_settings
from onyx.db.search_settings import get_current_db_embedding_provider
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.indexing.models import EmbeddingModelDetail
from onyx.natural_language_processing.search_nlp_models import EmbeddingModel
from onyx.server.manage.embedding.models import CloudEmbeddingProvider
@@ -59,7 +60,7 @@ def test_embedding_configuration(
except Exception as e:
error_msg = "An error occurred while testing your embedding model. Please check your configuration."
logger.error(f"{error_msg} Error message: {e}", exc_info=True)
raise HTTPException(status_code=400, detail=error_msg)
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, error_msg)
@admin_router.get("", response_model=list[EmbeddingModelDetail])
@@ -93,8 +94,9 @@ def delete_embedding_provider(
embedding_provider is not None
and provider_type == embedding_provider.provider_type
):
raise HTTPException(
status_code=400, detail="You can't delete a currently active model"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"You can't delete a currently active model",
)
remove_embedding_provider(db_session, provider_type=provider_type)

View File

@@ -11,7 +11,6 @@ from botocore.exceptions import ClientError
from botocore.exceptions import NoCredentialsError
from fastapi import APIRouter
from fastapi import Depends
from fastapi import HTTPException
from fastapi import Query
from pydantic import ValidationError
from sqlalchemy.orm import Session
@@ -38,6 +37,8 @@ from onyx.db.llm import upsert_llm_provider
from onyx.db.llm import validate_persona_ids_exist
from onyx.db.models import User
from onyx.db.persona import user_can_access_persona
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.llm.factory import get_default_llm
from onyx.llm.factory import get_llm
from onyx.llm.factory import get_max_input_tokens_from_llm_provider
@@ -186,7 +187,7 @@ def _validate_llm_provider_change(
Only enforced in MULTI_TENANT mode.
Raises:
HTTPException: If api_base or custom_config changed without changing API key
OnyxError: If api_base or custom_config changed without changing API key
"""
if not MULTI_TENANT or api_key_changed:
return
@@ -200,9 +201,9 @@ def _validate_llm_provider_change(
)
if api_base_changed or custom_config_changed:
raise HTTPException(
status_code=400,
detail="API base and/or custom config cannot be changed without changing the API key",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"API base and/or custom config cannot be changed without changing the API key",
)
@@ -222,7 +223,7 @@ def fetch_llm_provider_options(
for well_known_llm in well_known_llms:
if well_known_llm.name == provider_name:
return well_known_llm
raise HTTPException(status_code=404, detail=f"Provider {provider_name} not found")
raise OnyxError(OnyxErrorCode.NOT_FOUND, f"Provider {provider_name} not found")
@admin_router.post("/test")
@@ -281,7 +282,7 @@ def test_llm_configuration(
error_msg = test_llm(llm)
if error_msg:
raise HTTPException(status_code=400, detail=error_msg)
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, error_msg)
@admin_router.post("/test/default")
@@ -292,11 +293,11 @@ def test_default_provider(
llm = get_default_llm()
except ValueError:
logger.exception("Failed to fetch default LLM Provider")
raise HTTPException(status_code=400, detail="No LLM Provider setup")
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, "No LLM Provider setup")
error = test_llm(llm)
if error:
raise HTTPException(status_code=400, detail=str(error))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(error))
@admin_router.get("/provider")
@@ -362,35 +363,31 @@ def put_llm_provider(
# Check name constraints
# TODO: Once port from name to id is complete, unique name will no longer be required
if existing_provider and llm_provider_upsert_request.name != existing_provider.name:
raise HTTPException(
status_code=400,
detail="Renaming providers is not currently supported",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Renaming providers is not currently supported",
)
found_provider = fetch_existing_llm_provider(
name=llm_provider_upsert_request.name, db_session=db_session
)
if found_provider is not None and found_provider is not existing_provider:
raise HTTPException(
status_code=400,
detail=f"Provider with name={llm_provider_upsert_request.name} already exists",
raise OnyxError(
OnyxErrorCode.DUPLICATE_RESOURCE,
f"Provider with name={llm_provider_upsert_request.name} already exists",
)
if existing_provider and is_creation:
raise HTTPException(
status_code=400,
detail=(
f"LLM Provider with name {llm_provider_upsert_request.name} and "
f"id={llm_provider_upsert_request.id} already exists"
),
raise OnyxError(
OnyxErrorCode.DUPLICATE_RESOURCE,
f"LLM Provider with name {llm_provider_upsert_request.name} and "
f"id={llm_provider_upsert_request.id} already exists",
)
elif not existing_provider and not is_creation:
raise HTTPException(
status_code=400,
detail=(
f"LLM Provider with name {llm_provider_upsert_request.name} and "
f"id={llm_provider_upsert_request.id} does not exist"
),
raise OnyxError(
OnyxErrorCode.NOT_FOUND,
f"LLM Provider with name {llm_provider_upsert_request.name} and "
f"id={llm_provider_upsert_request.id} does not exist",
)
# SSRF Protection: Validate api_base and custom_config match stored values
@@ -415,9 +412,9 @@ def put_llm_provider(
db_session, persona_ids
)
if missing_personas:
raise HTTPException(
status_code=400,
detail=f"Invalid persona IDs: {', '.join(map(str, missing_personas))}",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
f"Invalid persona IDs: {', '.join(map(str, missing_personas))}",
)
# Remove duplicates while preserving order
seen: set[int] = set()
@@ -473,7 +470,7 @@ def put_llm_provider(
return result
except ValueError as e:
logger.exception("Failed to upsert LLM Provider")
raise HTTPException(status_code=400, detail=str(e))
raise OnyxError(OnyxErrorCode.VALIDATION_ERROR, str(e))
@admin_router.delete("/provider/{provider_id}")
@@ -483,19 +480,19 @@ def delete_llm_provider(
_: User = Depends(current_admin_user),
db_session: Session = Depends(get_session),
) -> None:
if not force:
model = fetch_default_llm_model(db_session)
if model and model.llm_provider_id == provider_id:
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"Cannot delete the default LLM provider",
)
try:
if not force:
model = fetch_default_llm_model(db_session)
if model and model.llm_provider_id == provider_id:
raise HTTPException(
status_code=400,
detail="Cannot delete the default LLM provider",
)
remove_llm_provider(db_session, provider_id)
except ValueError as e:
raise HTTPException(status_code=404, detail=str(e))
raise OnyxError(OnyxErrorCode.NOT_FOUND, str(e))
@admin_router.post("/default")
@@ -535,9 +532,9 @@ def get_auto_config(
"""
config = fetch_llm_recommendations_from_github()
if not config:
raise HTTPException(
status_code=502,
detail="Failed to fetch configuration from GitHub",
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
"Failed to fetch configuration from GitHub",
)
return config.model_dump()
@@ -694,13 +691,13 @@ def list_llm_providers_for_persona(
persona = fetch_persona_with_groups(db_session, persona_id)
if not persona:
raise HTTPException(status_code=404, detail="Persona not found")
raise OnyxError(OnyxErrorCode.PERSONA_NOT_FOUND, "Persona not found")
# Verify user has access to this persona
if not user_can_access_persona(db_session, persona_id, user, get_editable=False):
raise HTTPException(
status_code=403,
detail="You don't have access to this assistant",
raise OnyxError(
OnyxErrorCode.INSUFFICIENT_PERMISSIONS,
"You don't have access to this assistant",
)
is_admin = user.role == UserRole.ADMIN
@@ -854,9 +851,9 @@ def get_bedrock_available_models(
try:
bedrock = session.client("bedrock")
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Failed to create Bedrock client: {e}. Check AWS credentials and region.",
raise OnyxError(
OnyxErrorCode.CREDENTIAL_INVALID,
f"Failed to create Bedrock client: {e}. Check AWS credentials and region.",
)
# Build model info dict from foundation models (modelId -> metadata)
@@ -975,14 +972,14 @@ def get_bedrock_available_models(
return results
except (ClientError, NoCredentialsError, BotoCoreError) as e:
raise HTTPException(
status_code=400,
detail=f"Failed to connect to AWS Bedrock: {e}",
raise OnyxError(
OnyxErrorCode.CREDENTIAL_INVALID,
f"Failed to connect to AWS Bedrock: {e}",
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Unexpected error fetching Bedrock models: {e}",
raise OnyxError(
OnyxErrorCode.INTERNAL_ERROR,
f"Unexpected error fetching Bedrock models: {e}",
)
@@ -994,9 +991,9 @@ def _get_ollama_available_model_names(api_base: str) -> set[str]:
response.raise_for_status()
response_json = response.json()
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Failed to fetch Ollama models: {e}",
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
f"Failed to fetch Ollama models: {e}",
)
models = response_json.get("models", [])
@@ -1013,9 +1010,9 @@ def get_ollama_available_models(
cleaned_api_base = request.api_base.strip().rstrip("/")
if not cleaned_api_base:
raise HTTPException(
status_code=400,
detail="API base URL is required to fetch Ollama models.",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"API base URL is required to fetch Ollama models.",
)
# NOTE: most people run Ollama locally, so we don't disallow internal URLs
@@ -1024,9 +1021,9 @@ def get_ollama_available_models(
# with the same response format
model_names = _get_ollama_available_model_names(cleaned_api_base)
if not model_names:
raise HTTPException(
status_code=400,
detail="No models found from your Ollama server",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No models found from your Ollama server",
)
all_models_with_context_size_and_vision: list[OllamaFinalModelResponse] = []
@@ -1128,9 +1125,9 @@ def _get_openrouter_models_response(api_base: str, api_key: str) -> dict:
response.raise_for_status()
return response.json()
except Exception as e:
raise HTTPException(
status_code=400,
detail=f"Failed to fetch OpenRouter models: {e}",
raise OnyxError(
OnyxErrorCode.BAD_GATEWAY,
f"Failed to fetch OpenRouter models: {e}",
)
@@ -1151,9 +1148,9 @@ def get_openrouter_available_models(
data = response_json.get("data", [])
if not isinstance(data, list) or len(data) == 0:
raise HTTPException(
status_code=400,
detail="No models found from your OpenRouter endpoint",
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No models found from your OpenRouter endpoint",
)
results: list[OpenRouterFinalModelResponse] = []
@@ -1188,8 +1185,9 @@ def get_openrouter_available_models(
)
if not results:
raise HTTPException(
status_code=400, detail="No compatible models found from OpenRouter"
raise OnyxError(
OnyxErrorCode.VALIDATION_ERROR,
"No compatible models found from OpenRouter",
)
sorted_results = sorted(results, key=lambda m: m.name.lower())

View File

@@ -1,3 +1,5 @@
from __future__ import annotations
import json
import time
from collections.abc import Generator
@@ -84,6 +86,19 @@ 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,
@@ -177,8 +192,11 @@ class CodeInterpreterClient:
yield from self._batch_as_stream(code, stdin, timeout_ms, files)
return
response.raise_for_status()
yield from self._parse_sse(response)
try:
response.raise_for_status()
yield from self._parse_sse(response)
finally:
response.close()
def _parse_sse(
self, response: requests.Response

View File

@@ -111,8 +111,8 @@ class PythonTool(Tool[PythonToolOverrideKwargs]):
if not server.server_enabled:
return False
client = CodeInterpreterClient()
return client.health(use_cache=True)
with CodeInterpreterClient() as client:
return client.health(use_cache=True)
def tool_definition(self) -> dict:
return {
@@ -176,196 +176,203 @@ class PythonTool(Tool[PythonToolOverrideKwargs]):
)
)
# Create Code Interpreter client
client = CodeInterpreterClient()
# 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}")
# 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)
logger.debug(f"Executing code: {code}")
# Stage for execution
files_to_stage.append({"path": file_name, "file_id": ci_file_id})
# Execute code with streaming (falls back to batch if unavailable)
stdout_parts: list[str] = []
stderr_parts: list[str] = []
result_event: StreamResultEvent | None = None
logger.info(f"Staged file for Python execution: {file_name}")
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}")
except Exception as e:
logger.warning(f"Failed to stage file {file_name}: {e}")
if result_event is None:
raise RuntimeError(
"Code interpreter stream ended without a result event"
)
try:
logger.debug(f"Executing code: {code}")
full_stdout = "".join(stdout_parts)
full_stderr = "".join(stderr_parts)
# Execute code with streaming (falls back to batch if unavailable)
stdout_parts: list[str] = []
stderr_parts: list[str] = []
result_event: StreamResultEvent | None = None
# 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"
)
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
# 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:
self.emitter.emit(
Packet(
placement=placement,
obj=PythonToolDelta(
stdout=event.data if event.stream == "stdout" else "",
stderr=event.data if event.stream == "stderr" else "",
),
obj=PythonToolDelta(file_ids=generated_file_ids),
)
)
elif isinstance(event, StreamResultEvent):
result_event = event
elif isinstance(event, StreamErrorEvent):
raise RuntimeError(f"Code interpreter error: {event.message}")
if result_event is None:
raise RuntimeError(
"Code interpreter stream ended without a result event"
# 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),
)
full_stdout = "".join(stdout_parts)
full_stderr = "".join(stderr_parts)
# Serialize result for LLM
adapter = TypeAdapter(LlmPythonExecutionResult)
llm_response = adapter.dump_json(result).decode()
# 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"
)
return ToolResponse(
rich_response=PythonToolRichResponse(
generated_files=generated_files,
),
llm_facing_response=llm_response,
)
# Handle generated files
generated_files: list[PythonExecutionFile] = []
generated_file_ids: list[str] = []
file_ids_to_cleanup: list[str] = []
file_store = get_default_file_store()
except Exception as e:
logger.error(f"Python execution failed: {e}")
error_msg = str(e)
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:
# Emit error delta
self.emitter.emit(
Packet(
placement=placement,
obj=PythonToolDelta(file_ids=generated_file_ids),
obj=PythonToolDelta(
stdout="",
stderr=error_msg,
file_ids=[],
),
)
)
# 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,
)
# Serialize result for LLM
adapter = TypeAdapter(LlmPythonExecutionResult)
llm_response = adapter.dump_json(result).decode()
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,
)
)
# 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()
adapter = TypeAdapter(LlmPythonExecutionResult)
llm_response = adapter.dump_json(result).decode()
return ToolResponse(
rich_response=None,
llm_facing_response=llm_response,
)
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.1
nltk==3.9.3
# via unstructured
numpy==2.4.1
# via

View File

@@ -16,10 +16,6 @@ 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",
@@ -74,6 +70,48 @@ 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",
@@ -82,144 +120,31 @@ def run_jobs() -> None:
"--loglevel=INFO",
]
# 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),
]
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),
]
# 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:
processes = []
for name, cmd in all_workers:
process = subprocess.Popen(
cmd, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, text=True
)
background_processes.append((name, process))
processes.append((name, process))
# 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:
threads = []
for name, process in processes:
thread = threading.Thread(target=monitor_process, args=(name, process))
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:
threads.append(thread)
thread.start()
# 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:
for thread in threads:
thread.join()

View File

@@ -1,23 +1,5 @@
#!/bin/sh
# 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"
# Entrypoint script for supervisord
# Launch supervisord with environment variables available
exec /usr/bin/supervisord -c /etc/supervisor/conf.d/supervisord.conf

View File

@@ -39,7 +39,6 @@ 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.)
@@ -54,26 +53,7 @@ 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
@@ -85,9 +65,7 @@ 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
@@ -99,7 +77,6 @@ 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
@@ -112,9 +89,7 @@ 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
@@ -126,7 +101,6 @@ 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
@@ -139,7 +113,6 @@ redirect_stderr=true
autorestart=true
startsecs=10
stopasgroup=true
autostart=%(ENV_USE_SEPARATE_BACKGROUND_WORKERS)s
# Job scheduler for periodic tasks
@@ -197,7 +170,6 @@ 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

@@ -11,7 +11,6 @@ from unittest.mock import patch
from uuid import uuid4
import pytest
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.db.enums import LLMModelFlowType
@@ -20,6 +19,8 @@ from onyx.db.llm import remove_llm_provider
from onyx.db.llm import update_default_provider
from onyx.db.llm import upsert_llm_provider
from onyx.db.models import UserRole
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.llm.constants import LlmProviderNames
from onyx.llm.interfaces import LLM
from onyx.server.manage.llm.api import (
@@ -122,16 +123,16 @@ class TestLLMConfigurationEndpoint:
finally:
db_session.rollback()
def test_failed_llm_test_raises_http_exception(
def test_failed_llm_test_raises_onyx_error(
self,
db_session: Session,
provider_name: str, # noqa: ARG002
) -> None:
"""
Test that a failed LLM test raises an HTTPException with status 400.
Test that a failed LLM test raises an OnyxError with VALIDATION_ERROR.
When test_llm returns an error message, the endpoint should raise
an HTTPException with the error details.
an OnyxError with the error details.
"""
error_message = "Invalid API key: Authentication failed"
@@ -143,7 +144,7 @@ class TestLLMConfigurationEndpoint:
with patch(
"onyx.server.manage.llm.api.test_llm", side_effect=mock_test_llm_failure
):
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(OnyxError) as exc_info:
run_test_llm_configuration(
test_llm_request=LLMTestRequest(
provider=LlmProviderNames.OPENAI,
@@ -156,9 +157,8 @@ class TestLLMConfigurationEndpoint:
db_session=db_session,
)
# Verify the exception details
assert exc_info.value.status_code == 400
assert exc_info.value.detail == error_message
assert exc_info.value.error_code == OnyxErrorCode.VALIDATION_ERROR
assert exc_info.value.message == error_message
finally:
db_session.rollback()
@@ -536,11 +536,11 @@ class TestDefaultProviderEndpoint:
remove_llm_provider(db_session, provider.id)
# Now run_test_default_provider should fail
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(OnyxError) as exc_info:
run_test_default_provider(_=_create_mock_admin())
assert exc_info.value.status_code == 400
assert "No LLM Provider setup" in exc_info.value.detail
assert exc_info.value.error_code == OnyxErrorCode.VALIDATION_ERROR
assert "No LLM Provider setup" in exc_info.value.message
finally:
db_session.rollback()
@@ -581,11 +581,11 @@ class TestDefaultProviderEndpoint:
with patch(
"onyx.server.manage.llm.api.test_llm", side_effect=mock_test_llm_failure
):
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(OnyxError) as exc_info:
run_test_default_provider(_=_create_mock_admin())
assert exc_info.value.status_code == 400
assert exc_info.value.detail == error_message
assert exc_info.value.error_code == OnyxErrorCode.VALIDATION_ERROR
assert exc_info.value.message == error_message
finally:
db_session.rollback()

View File

@@ -16,13 +16,14 @@ from unittest.mock import patch
from uuid import uuid4
import pytest
from fastapi import HTTPException
from sqlalchemy.orm import Session
from onyx.db.llm import fetch_existing_llm_provider
from onyx.db.llm import remove_llm_provider
from onyx.db.llm import upsert_llm_provider
from onyx.db.models import UserRole
from onyx.error_handling.error_codes import OnyxErrorCode
from onyx.error_handling.exceptions import OnyxError
from onyx.llm.constants import LlmProviderNames
from onyx.server.manage.llm.api import _mask_string
from onyx.server.manage.llm.api import put_llm_provider
@@ -100,7 +101,7 @@ class TestLLMProviderChanges:
api_base="https://attacker.example.com",
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(OnyxError) as exc_info:
put_llm_provider(
llm_provider_upsert_request=update_request,
is_creation=False,
@@ -108,9 +109,9 @@ class TestLLMProviderChanges:
db_session=db_session,
)
assert exc_info.value.status_code == 400
assert exc_info.value.error_code == OnyxErrorCode.VALIDATION_ERROR
assert "cannot be changed without changing the API key" in str(
exc_info.value.detail
exc_info.value.message
)
finally:
_cleanup_provider(db_session, provider_name)
@@ -236,7 +237,7 @@ class TestLLMProviderChanges:
api_base=None,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(OnyxError) as exc_info:
put_llm_provider(
llm_provider_upsert_request=update_request,
is_creation=False,
@@ -244,9 +245,9 @@ class TestLLMProviderChanges:
db_session=db_session,
)
assert exc_info.value.status_code == 400
assert exc_info.value.error_code == OnyxErrorCode.VALIDATION_ERROR
assert "cannot be changed without changing the API key" in str(
exc_info.value.detail
exc_info.value.message
)
finally:
_cleanup_provider(db_session, provider_name)
@@ -339,7 +340,7 @@ class TestLLMProviderChanges:
custom_config_changed=True,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(OnyxError) as exc_info:
put_llm_provider(
llm_provider_upsert_request=update_request,
is_creation=False,
@@ -347,9 +348,9 @@ class TestLLMProviderChanges:
db_session=db_session,
)
assert exc_info.value.status_code == 400
assert exc_info.value.error_code == OnyxErrorCode.VALIDATION_ERROR
assert "cannot be changed without changing the API key" in str(
exc_info.value.detail
exc_info.value.message
)
finally:
_cleanup_provider(db_session, provider_name)
@@ -375,7 +376,7 @@ class TestLLMProviderChanges:
custom_config_changed=True,
)
with pytest.raises(HTTPException) as exc_info:
with pytest.raises(OnyxError) as exc_info:
put_llm_provider(
llm_provider_upsert_request=update_request,
is_creation=False,
@@ -383,9 +384,9 @@ class TestLLMProviderChanges:
db_session=db_session,
)
assert exc_info.value.status_code == 400
assert exc_info.value.error_code == OnyxErrorCode.VALIDATION_ERROR
assert "cannot be changed without changing the API key" in str(
exc_info.value.detail
exc_info.value.message
)
finally:
_cleanup_provider(db_session, provider_name)

View File

@@ -427,7 +427,7 @@ def test_delete_default_llm_provider_rejected(reset: None) -> None: # noqa: ARG
headers=admin_user.headers,
)
assert delete_response.status_code == 400
assert "Cannot delete the default LLM provider" in delete_response.json()["detail"]
assert "Cannot delete the default LLM provider" in delete_response.json()["message"]
# Verify provider still exists
provider_data = _get_provider_by_id(admin_user, created_provider["id"])
@@ -673,8 +673,8 @@ def test_duplicate_provider_name_rejected(reset: None) -> None: # noqa: ARG001
headers=admin_user.headers,
json=base_payload,
)
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_rename_provider_rejected(reset: None) -> None: # noqa: ARG001
@@ -711,7 +711,7 @@ def test_rename_provider_rejected(reset: None) -> None: # noqa: ARG001
json=update_payload,
)
assert response.status_code == 400
assert "not currently supported" in response.json()["detail"]
assert "not currently supported" in response.json()["message"]
# Verify no duplicate was created — only the original provider should exist
provider = _get_provider_by_id(admin_user, provider_id)

View File

@@ -69,7 +69,7 @@ def test_unauthorized_persona_access_returns_403(
# Should return 403 Forbidden
assert response.status_code == 403
assert "don't have access to this assistant" in response.json()["detail"]
assert "don't have access to this assistant" in response.json()["message"]
def test_authorized_persona_access_returns_filtered_providers(
@@ -245,4 +245,4 @@ def test_nonexistent_persona_returns_404(
# Should return 404
assert response.status_code == 404
assert "Persona not found" in response.json()["detail"]
assert "Persona not found" in response.json()["message"]

View File

@@ -104,3 +104,102 @@ 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,7 +87,8 @@ def test_python_tool_available_when_health_check_passes(
mock_client = MagicMock()
mock_client.health.return_value = True
mock_client_cls.return_value = mock_client
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
db_session = MagicMock(spec=Session)
assert PythonTool.is_available(db_session) is True
@@ -109,7 +110,8 @@ def test_python_tool_unavailable_when_health_check_fails(
mock_client = MagicMock()
mock_client.health.return_value = False
mock_client_cls.return_value = mock_client
mock_client_cls.return_value.__enter__ = MagicMock(return_value=mock_client)
mock_client_cls.return_value.__exit__ = MagicMock(return_value=False)
db_session = MagicMock(spec=Session)
assert PythonTool.is_available(db_session) is False

View File

@@ -138,7 +138,6 @@ 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,7 +52,6 @@ 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,7 +65,6 @@ 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,7 +70,6 @@ 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,7 +58,6 @@ 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,7 +146,6 @@ 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,30 +14,32 @@ 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
```
@@ -55,16 +57,21 @@ 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
@@ -103,6 +110,7 @@ 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
@@ -115,6 +123,7 @@ 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`
@@ -135,6 +144,7 @@ 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
@@ -146,6 +156,7 @@ code ~/Library/Application\ Support/app.onyx.desktop/config.json
### Change the default URL in build
Edit `src-tauri/tauri.conf.json`:
```json
{
"app": {
@@ -165,6 +176,7 @@ 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
@@ -172,16 +184,20 @@ 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,6 +4,7 @@
"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,3 +23,4 @@ url = "2.5"
[features]
default = ["custom-protocol"]
custom-protocol = ["tauri/custom-protocol"]
devtools = ["tauri/devtools"]

View File

@@ -6,7 +6,9 @@ use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
use std::process::Command;
use std::sync::RwLock;
use std::sync::{Mutex, RwLock};
use std::io::Write as IoWrite;
use std::time::SystemTime;
#[cfg(target_os = "macos")]
use std::time::Duration;
use tauri::image::Image;
@@ -230,6 +232,63 @@ 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,
@@ -311,12 +370,87 @@ 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) {
@@ -372,6 +506,7 @@ fn trigger_new_window(app: &AppHandle) {
}
apply_settings_to_window(&handle, &window);
maybe_open_devtools(&handle, &window);
let _ = window.set_focus();
}
});
@@ -467,10 +602,65 @@ 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 {
@@ -657,6 +847,7 @@ async fn new_window(app: AppHandle, state: tauri::State<'_, ConfigState>) -> Res
}
apply_settings_to_window(&app, &window);
maybe_open_devtools(&app, &window);
Ok(())
}
@@ -936,6 +1127,30 @@ 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(())
}
@@ -1027,8 +1242,20 @@ 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())
@@ -1059,6 +1286,8 @@ 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,
@@ -1077,7 +1306,8 @@ fn main() {
start_drag_window,
toggle_menu_bar,
show_menu_bar_temporarily,
hide_menu_bar_temporary
hide_menu_bar_temporary,
log_from_frontend
])
.on_menu_event(|app, event| match event.id().as_ref() {
"open_docs" => open_docs(),
@@ -1086,6 +1316,8 @@ 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| {
@@ -1119,6 +1351,7 @@ fn main() {
inject_titlebar(window.clone());
apply_settings_to_window(&app_handle, &window);
maybe_open_devtools(&app_handle, &window);
let _ = window.set_focus();
}
@@ -1128,6 +1361,14 @@ 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.1"
version = "3.9.3"
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/3c/87/db8be88ad32c2d042420b6fd9ffd4a149f9a0d7f0e86b3f543be2eeeedd2/nltk-3.9.1.tar.gz", hash = "sha256:87d127bd3de4bd89a4f81265e5fa59cb1b199b27440175370f7417d2bc7ae868", size = 2904691, upload-time = "2024-08-18T19:48:37.769Z" }
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" }
wheels = [
{ 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" },
{ 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" },
]
[[package]]

View File

@@ -11,6 +11,8 @@ export {
Interactive,
type InteractiveBaseProps,
type InteractiveBaseVariantProps,
type InteractiveBaseSidebarVariantProps,
type InteractiveBaseSidebarProminenceTypes,
type InteractiveContainerProps,
type InteractiveContainerRoundingVariant,
} from "@opal/core/interactive/components";

View File

@@ -104,6 +104,44 @@ The foundational layer for all clickable surfaces in the design system. Defines
| **Active** | `action-link-05` | `action-link-05` |
| **Disabled** | `action-link-03` | `action-link-03` |
### Sidebar (unselected)
> No CSS `:active` state — only hover/transient and selected.
**Background**
| | Light |
|---|---|
| **Rest** | `transparent` |
| **Hover / Transient** | `background-tint-03` |
| **Disabled** | `transparent` |
**Foreground**
| | Light |
|---|---|
| **Rest** | `text-03` |
| **Hover / Transient** | `text-04` |
| **Disabled** | `text-01` |
### Sidebar (selected)
> Completely static — hover and transient have no effect.
**Background**
| | Light |
|---|---|
| **All states** | `background-tint-00` |
| **Disabled** | `transparent` |
**Foreground**
| | Light |
|---|---|
| **All states** | `text-03` (icon: `text-02`) |
| **Disabled** | `text-01` |
## Sub-components
| Sub-component | Role |

View File

@@ -1,3 +1,5 @@
import Link from "next/link";
import type { Route } from "next";
import "@opal/core/interactive/styles.css";
import React from "react";
import { Slot } from "@radix-ui/react-slot";
@@ -26,18 +28,28 @@ type InteractiveBaseSelectVariantProps = {
selected?: boolean;
};
type InteractiveBaseSidebarProminenceTypes = "light";
type InteractiveBaseSidebarVariantProps = {
variant: "sidebar";
prominence?: InteractiveBaseSidebarProminenceTypes;
selected?: boolean;
};
/**
* Discriminated union tying `variant` to `prominence`.
*
* - `"none"` accepts no prominence (`prominence` must not be provided)
* - `"select"` accepts an optional prominence (defaults to `"light"`) and
* an optional `selected` boolean that switches foreground to action-link colours
* - `"sidebar"` accepts an optional prominence (defaults to `"light"`) and
* an optional `selected` boolean for the focused/active-item state
* - `"default"`, `"action"`, and `"danger"` accept an optional prominence
* (defaults to `"primary"`)
*/
type InteractiveBaseVariantProps =
| { variant?: "none"; prominence?: never; selected?: never }
| InteractiveBaseSelectVariantProps
| InteractiveBaseSidebarVariantProps
| {
variant?: InteractiveBaseVariantTypes;
prominence?: InteractiveBaseProminenceTypes;
@@ -218,7 +230,8 @@ function InteractiveBase({
...props
}: InteractiveBaseProps) {
const effectiveProminence =
prominence ?? (variant === "select" ? "light" : "primary");
prominence ??
(variant === "select" || variant === "sidebar" ? "light" : "primary");
const classes = cn(
"interactive",
!props.onClick && !href && "!cursor-default !select-auto",
@@ -417,9 +430,9 @@ function InteractiveContainer({
// so all styling (backgrounds, rounding, overflow) lives on one element.
if (href) {
return (
<a
<Link
ref={ref as React.Ref<HTMLAnchorElement>}
href={href}
href={href as Route}
target={target}
rel={rel}
{...(sharedProps as React.HTMLAttributes<HTMLAnchorElement>)}
@@ -482,6 +495,8 @@ export {
type InteractiveBaseProps,
type InteractiveBaseVariantProps,
type InteractiveBaseSelectVariantProps,
type InteractiveBaseSidebarVariantProps,
type InteractiveBaseSidebarProminenceTypes,
type InteractiveContainerProps,
type InteractiveContainerRoundingVariant,
};

View File

@@ -419,3 +419,23 @@
) {
@apply bg-background-tint-00;
}
/* ---------------------------------------------------------------------------
Sidebar + Light
--------------------------------------------------------------------------- */
.interactive[data-interactive-base-variant="sidebar"][data-interactive-base-prominence="light"] {
@apply bg-transparent;
}
.interactive[data-interactive-base-variant="sidebar"][data-interactive-base-prominence="light"]:hover:not(
[data-disabled]
),
.interactive[data-interactive-base-variant="sidebar"][data-interactive-base-prominence="light"][data-transient="true"]:not(
[data-disabled]
) {
@apply bg-background-tint-03;
}
.interactive[data-interactive-base-variant="sidebar"][data-interactive-base-prominence="light"][data-selected="true"]:not(
[data-disabled]
) {
@apply bg-background-tint-00;
}

View File

@@ -173,6 +173,7 @@ 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

@@ -0,0 +1,22 @@
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

@@ -9,7 +9,7 @@ import { cn } from "@opal/utils";
type ContentSmSizePreset = "main-content" | "main-ui" | "secondary";
type ContentSmOrientation = "vertical" | "inline" | "reverse";
type ContentSmProminence = "default" | "muted";
type ContentSmProminence = "default" | "muted" | "muted-2x";
interface ContentSmPresetConfig {
/** Icon width/height (CSS value). */
@@ -82,13 +82,12 @@ function ContentSm({
prominence = "default",
}: ContentSmProps) {
const config = CONTENT_SM_PRESETS[sizePreset];
const titleColorClass =
prominence === "muted" ? "text-text-03" : "text-text-04";
return (
<div
className="opal-content-sm"
data-orientation={orientation}
data-prominence={prominence}
style={{ gap: config.gap }}
>
{Icon && (
@@ -100,18 +99,14 @@ function ContentSm({
style={{ minHeight: config.lineHeight }}
>
<Icon
className="opal-content-sm-icon text-text-03"
className="opal-content-sm-icon"
style={{ width: config.iconSize, height: config.iconSize }}
/>
</div>
)}
<span
className={cn(
"opal-content-sm-title",
config.titleFont,
titleColorClass
)}
className={cn("opal-content-sm-title", config.titleFont)}
style={{ height: config.lineHeight }}
>
{title}

View File

@@ -323,7 +323,7 @@
reverse : flex-row-reverse — title left, icon right
Icon color is always text-03. Title color varies by prominence
(text-04 default, text-03 muted) and is applied via Tailwind class.
(text-04 default, text-03 muted, text-02 muted-2x) via data-prominence.
=========================================================================== */
/* ---------------------------------------------------------------------------
@@ -331,7 +331,8 @@
--------------------------------------------------------------------------- */
.opal-content-sm {
@apply flex items-start;
/* since `ContentSm` doesn't have a description, it's possible to center-align the icon and text */
@apply flex items-center;
}
.opal-content-sm[data-orientation="inline"] {
@@ -356,15 +357,31 @@
justify-content: center;
}
.opal-content-sm-icon {
@apply text-text-03;
}
.opal-content-sm[data-prominence="muted-2x"] .opal-content-sm-icon {
@apply text-text-02 stroke-text-02;
}
/* ---------------------------------------------------------------------------
Title
--------------------------------------------------------------------------- */
.opal-content-sm-title {
@apply text-left overflow-hidden;
@apply text-left overflow-hidden text-text-04 truncate;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
padding: 0 0.125rem;
min-width: 0.0625rem;
}
.opal-content-sm[data-prominence="muted"] .opal-content-sm-title {
@apply text-text-03;
}
.opal-content-sm[data-prominence="muted-2x"] .opal-content-sm-title {
@apply text-text-02;
}

View File

@@ -37,7 +37,6 @@ interface IllustrationContentProps {
* │ └───────────────────┘ │
* │ (0.75rem gap) │
* │ title (center) │
* │ (0.75rem gap) │
* │ description (center) │
* │ (1.25rem pad) │
* └─────────────────────────────────┘
@@ -68,10 +67,12 @@ function IllustrationContent({
className="shrink-0 w-[7.5rem] h-[7.5rem]"
/>
)}
<p className="font-main-content-emphasis text-text-04">{title}</p>
{description && (
<p className="font-secondary-body text-text-03">{description}</p>
)}
<div className="flex flex-col items-center text-center">
<p className="font-main-content-emphasis text-text-04">{title}</p>
{description && (
<p className="font-secondary-body text-text-03">{description}</p>
)}
</div>
</div>
);
}

View File

@@ -41,7 +41,7 @@ export default defineConfig({
viewport: { width: 1280, height: 720 },
storageState: "admin_auth.json",
},
grepInvert: /@exclusive/,
grepInvert: [/@exclusive/, /@lite/],
},
{
// this suite runs independently and serially + slower
@@ -55,5 +55,15 @@ 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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
import {
useCallback,
useContext,
@@ -254,9 +254,7 @@ export default function Page() {
icon={route.icon}
title={route.title}
rightChildren={
<Button href="/admin/indexing/status" primary>
See Connectors
</Button>
<Button href="/admin/indexing/status">See Connectors</Button>
}
separator
/>

View File

@@ -16,9 +16,8 @@ import {
} from "./lib";
import { FiEdit2 } from "react-icons/fi";
import { useUser } from "@/providers/UserProvider";
import { Button as OpalButton } from "@opal/components";
import { Button } 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";
@@ -300,7 +299,7 @@ export function PersonasTable({
<div key="edit" className="flex">
<div className="mr-auto my-auto">
{!persona.builtin_persona && isEditable ? (
<OpalButton
<Button
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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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
internal
prominence="internal"
onClick={() => handleEdit(apiKey)}
leftIcon={SvgEdit}
icon={SvgEdit}
>
{apiKey.api_key_name || <i>null</i>}
{apiKey.api_key_name || "null"}
</Button>
</TableCell>
<TableCell className="max-w-64">
@@ -176,8 +176,8 @@ function Main() {
</TableCell>
<TableCell>
<Button
internal
leftIcon={SvgRefreshCw}
prominence="internal"
icon={SvgRefreshCw}
onClick={async () => {
setKeyIsGenerating(true);
const response = await regenerateApiKey(apiKey);

View File

@@ -7,6 +7,7 @@ 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";
@@ -244,25 +245,20 @@ function SubscriptionCard({
to make changes.
</Text>
) : disabled ? (
<Button
main
secondary
<OpalButton
prominence="secondary"
onClick={handleReconnect}
rightIcon={SvgArrowRight}
disabled={isReconnecting}
>
{isReconnecting ? "Connecting..." : "Connect to Stripe"}
</Button>
</OpalButton>
) : (
<Button
main
primary
onClick={handleManagePlan}
rightIcon={SvgExternalLink}
>
<OpalButton onClick={handleManagePlan} rightIcon={SvgExternalLink}>
Manage Plan
</Button>
</OpalButton>
)}
{/* 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
@@ -379,9 +375,13 @@ function SeatsCard({
sizePreset="main-content"
variant="section"
/>
<Button main secondary onClick={handleCancel} disabled={isSubmitting}>
<OpalButton
prominence="secondary"
onClick={handleCancel}
disabled={isSubmitting}
>
Cancel
</Button>
</OpalButton>
</Section>
<div className="billing-content-area">
@@ -463,16 +463,14 @@ function SeatsCard({
No changes to your billing.
</Text>
)}
<Button
main
primary
<OpalButton
onClick={handleConfirm}
disabled={
isSubmitting || newSeatCount === totalSeats || isBelowMinimum
}
>
{isSubmitting ? "Saving..." : "Confirm Change"}
</Button>
</OpalButton>
</Section>
</Card>
);
@@ -502,19 +500,22 @@ function SeatsCard({
height="auto"
width="auto"
>
<Button main tertiary href="/admin/users" leftIcon={SvgExternalLink}>
<OpalButton
prominence="tertiary"
href="/admin/users"
icon={SvgExternalLink}
>
View Users
</Button>
</OpalButton>
{!hideUpdateSeats && (
<Button
main
secondary
<OpalButton
prominence="secondary"
onClick={handleStartEdit}
leftIcon={SvgPlus}
icon={SvgPlus}
disabled={isLoadingUsers || disabled || !billing}
>
Update Seats
</Button>
</OpalButton>
)}
</Section>
</Section>
@@ -566,14 +567,13 @@ function PaymentSection({ billing }: { billing: BillingInformation }) {
title="Visa ending in 1234"
description="Payment method"
/>
<Button
main
tertiary
<OpalButton
prominence="tertiary"
onClick={handleOpenPortal}
rightIcon={SvgExternalLink}
>
Update
</Button>
</OpalButton>
</Section>
</Card>
{lastPaymentDate && (
@@ -589,14 +589,13 @@ function PaymentSection({ billing }: { billing: BillingInformation }) {
title={lastPaymentDate}
description="Last payment"
/>
<Button
main
tertiary
<OpalButton
prominence="tertiary"
onClick={handleOpenPortal}
rightIcon={SvgExternalLink}
>
View Invoice
</Button>
</OpalButton>
</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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 secondary onClick={onAdjustPlan}>
<Button prominence="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 main primary onClick={handleSubmit} disabled={isSubmitting}>
<Button 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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 main secondary onClick={() => setShowInput(true)}>
<Button prominence="secondary" onClick={() => setShowInput(true)}>
Update Key
</Button>
{!hideClose && (
<Button main tertiary onClick={handleClose}>
<Button prominence="tertiary" onClick={handleClose}>
Close
</Button>
)}
@@ -146,7 +146,11 @@ export default function LicenseActivationCard({
<Text headingH3>
{hasLicense ? "Update License Key" : "Activate License Key"}
</Text>
<Button secondary onClick={handleClose} disabled={isActivating}>
<Button
prominence="secondary"
onClick={handleClose}
disabled={isActivating}
>
Cancel
</Button>
</Section>
@@ -219,8 +223,6 @@ 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,6 +21,7 @@ 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";
@@ -146,26 +147,27 @@ 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 ? (
<Button
main
secondary
<OpalButton
prominence="secondary"
href={href}
target="_blank"
rel="noopener noreferrer"
>
{buttonLabel}
</Button>
</OpalButton>
) : onClick ? (
<Button main primary onClick={onClick} leftIcon={ButtonIcon}>
<OpalButton onClick={onClick} icon={ButtonIcon}>
{buttonLabel}
</Button>
</OpalButton>
) : (
// 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,6 +66,7 @@ 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}
@@ -73,6 +74,7 @@ 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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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
leftIcon={({ className }) => (
prominence="secondary"
icon={({ className }) => (
<SvgChevronDownSmall
className={cn(className, !isExpanded && "-rotate-90")}
/>
)}
onClick={() => setIsExpanded(!isExpanded)}
secondary
>
Update Tokens
</Button>
<Button
danger
variant="danger"
onClick={() => setShowDeleteModal(true)}
leftIcon={SvgTrash}
icon={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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
import Separator from "@/refresh-components/Separator";
import { useEffect } from "react";
import { DOCS_ADMINS_PATH } from "@/lib/constants";

View File

@@ -17,9 +17,8 @@ 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 as OpalButton } from "@opal/components";
import { Button } from "@opal/components";
import { SvgSettings, SvgTrash } from "@opal/icons";
const numToDisplay = 50;
@@ -45,11 +44,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}`;
}}
secondary
leftIcon={SvgSettings}
icon={SvgSettings}
>
Edit Default Configuration
</Button>
@@ -121,7 +120,7 @@ export default function SlackChannelConfigsTable({
</div>
</TableCell>
<TableCell onClick={(e) => e.stopPropagation()}>
<OpalButton
<Button
onClick={async (e) => {
e.stopPropagation();
const response = await deleteSlackChannelConfig(

View File

@@ -11,7 +11,7 @@ import {
TextArrayField,
TextFormField,
} from "@/components/Field";
import Button from "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 secondary onClick={() => router.back()}>
<Button prominence="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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 onClick={handleDelete} danger>
<Button variant="danger" onClick={handleDelete}>
Delete API Key
</Button>
<Text as="p" mainContentBody text04 className="desktop:mt-0">
@@ -137,7 +137,7 @@ function Main() {
</Text>
</>
) : (
<Button onClick={handleSave} action>
<Button variant="action" onClick={handleSave}>
Save API Key
</Button>
)}

View File

@@ -10,6 +10,7 @@ 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";
@@ -23,6 +24,7 @@ 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,
@@ -30,11 +32,12 @@ export default function UpgradingPage({
futureEmbeddingModel: CloudEmbeddingModel | HostedEmbeddingModel;
}) {
const [isCancelling, setIsCancelling] = useState<boolean>(false);
const vectorDbEnabled = useVectorDbEnabled();
const { data: connectors, isLoading: isLoadingConnectors } = useSWR<
Connector<any>[]
>("/api/manage/connector", errorHandlingFetcher, {
refreshInterval: 5000, // 5 seconds
>(vectorDbEnabled ? "/api/manage/connector" : null, errorHandlingFetcher, {
refreshInterval: 5000,
});
const {
@@ -42,7 +45,8 @@ export default function UpgradingPage({
isLoading: isLoadingOngoingReIndexingStatus,
} = useConnectorIndexingStatusWithPagination(
{ secondary_index: true, get_all_connectors: true },
5000
5000,
vectorDbEnabled
) as {
data: ConnectorIndexingStatusLiteResponse[];
isLoading: boolean;
@@ -51,9 +55,11 @@ export default function UpgradingPage({
const { data: failedIndexingStatus } = useSWR<
FailedConnectorIndexingStatus[]
>(
"/api/manage/admin/connector/failed-indexing-status?secondary_index=true",
vectorDbEnabled
? "/api/manage/admin/connector/failed-indexing-status?secondary_index=true"
: null,
errorHandlingFetcher,
{ refreshInterval: 5000 } // 5 seconds
{ refreshInterval: 5000 }
);
const onCancel = async () => {
@@ -140,10 +146,13 @@ export default function UpgradingPage({
</div>
</Modal.Body>
<Modal.Footer>
<Button onClick={onCancel}>Confirm</Button>
<Button onClick={() => setIsCancelling(false)} secondary>
<OpalButton onClick={onCancel}>Confirm</OpalButton>
<OpalButton
prominence="secondary"
onClick={() => setIsCancelling(false)}
>
Cancel
</Button>
</OpalButton>
</Modal.Footer>
</Modal.Content>
</Modal>
@@ -158,6 +167,7 @@ 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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
import useSWR from "swr";
import { ModelPreview } from "@/components/embedding/ModelSelector";
import {
@@ -130,7 +130,7 @@ function Main() {
</CardSection>
<div className="mt-4">
<Button action href="/admin/embeddings">
<Button variant="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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
import { SvgArrowExchange, SvgOnyxLogo } from "@opal/icons";
import type { IconProps } from "@opal/types";
@@ -244,13 +244,11 @@ export const WebProviderSetupModal = memo(
)}
</Modal.Body>
<Modal.Footer>
<Button type="button" main secondary onClick={onClose}>
<Button prominence="secondary" type="button" onClick={onClose}>
Cancel
</Button>
<Button
type="button"
main
primary
disabled={!canConnect || isProcessing}
onClick={onConnect}
>

View File

@@ -91,6 +91,7 @@ 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>
@@ -1010,9 +1011,8 @@ export default function Page() {
{buttonState.label}
</HoverIconButton>
) : (
<Button
action={false}
tertiary
<OpalButton
prominence="tertiary"
disabled={
buttonState.disabled || !buttonState.onClick
}
@@ -1029,7 +1029,7 @@ export default function Page() {
}
>
{buttonState.label}
</Button>
</OpalButton>
)}
</div>
</div>
@@ -1202,9 +1202,8 @@ export default function Page() {
{buttonState.label}
</HoverIconButton>
) : (
<Button
action={false}
tertiary
<OpalButton
prominence="tertiary"
disabled={
buttonState.disabled || !buttonState.onClick
}
@@ -1221,7 +1220,7 @@ export default function Page() {
}
>
{buttonState.label}
</Button>
</OpalButton>
)}
</div>
</div>

View File

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

View File

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

View File

@@ -48,6 +48,7 @@ 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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
import { SvgArrowLeft, SvgArrowRight, SvgPlusCircle } from "@opal/icons";
const NavigationRow = ({
@@ -22,7 +22,11 @@ const NavigationRow = ({
<div>
{((formStep > 0 && !noCredentials) ||
(formStep > 1 && !noAdvanced)) && (
<Button secondary onClick={prevFormStep} leftIcon={SvgArrowLeft}>
<Button
prominence="secondary"
onClick={prevFormStep}
icon={SvgArrowLeft}
>
Previous
</Button>
)}
@@ -41,7 +45,7 @@ const NavigationRow = ({
<div className="flex justify-end">
{formStep === 0 && (
<Button
action
variant="action"
disabled={!activatedCredential}
rightIcon={SvgArrowRight}
onClick={() => nextFormStep()}
@@ -51,7 +55,7 @@ const NavigationRow = ({
)}
{!noAdvanced && formStep === 1 && (
<Button
secondary
prominence="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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 leftIcon={SvgTrash} danger type="submit">
<Button variant="danger" icon={SvgTrash} 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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
import {
Credential,
GoogleDriveCredentialJson,
@@ -314,7 +314,7 @@ export const DriveJsonUploadSection = ({
{isAdmin && !existingAuthCredential && (
<div className="mt-2">
<Button
danger
variant="danger"
onClick={async () => {
const endpoint =
localServiceAccountData?.service_account_email
@@ -467,7 +467,7 @@ export const DriveAuthSection = ({
</div>
</div>
<Button
danger
variant="danger"
onClick={async () => {
handleRevokeAccess(
connectorAssociated,

View File

@@ -1,4 +1,4 @@
import Button from "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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
danger
variant="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
danger
variant="danger"
onClick={async () => {
handleRevokeAccess(
connectorExists,
@@ -482,10 +482,7 @@ export const GmailAuthSection = ({
Revoke Access
</Button>
{buildMode && onCredentialCreated && (
<Button
primary
onClick={() => onCredentialCreated(existingCredential)}
>
<Button onClick={() => onCredentialCreated(existingCredential)}>
Continue
</Button>
)}

View File

@@ -11,7 +11,7 @@ import {
TableHeader,
TableRow,
} from "@/components/ui/table";
import Button from "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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)}
secondary
leftIcon={SvgDownloadCloud}
icon={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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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
internal
prominence="internal"
disabled={!guild.guild_id}
onClick={() => router.push(`/admin/discord-bot/${guild.id}`)}
leftIcon={SvgEdit}
icon={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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
import { SvgServer } from "@opal/icons";
import InputSelect from "@/refresh-components/inputs/InputSelect";
import {
@@ -104,13 +104,17 @@ function GuildDetailContent({
width="fit"
gap={0.5}
>
<Button onClick={handleEnableAll} disabled={disabled} secondary>
<Button
prominence="secondary"
onClick={handleEnableAll}
disabled={disabled}
>
Enable All
</Button>
<Button
prominence="secondary"
onClick={handleDisableAll}
disabled={disabled}
secondary
>
Disable All
</Button>

View File

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

View File

@@ -257,6 +257,7 @@ 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,9 +10,11 @@ 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,
@@ -24,12 +26,16 @@ function Main({ documentSetId }: { documentSetId: number }) {
data: ccPairs,
isLoading: isCCPairsLoading,
error: ccPairsError,
} = useConnectorStatus();
} = useConnectorStatus(30000, vectorDbEnabled);
// EE only
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
if (isDocumentSetsLoading || isCCPairsLoading || userGroupsIsLoading) {
if (
isDocumentSetsLoading ||
(vectorDbEnabled && isCCPairsLoading) ||
userGroupsIsLoading
) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<ThreeDotsLoader />
@@ -46,7 +52,7 @@ function Main({ documentSetId }: { documentSetId: number }) {
);
}
if (ccPairsError || !ccPairs) {
if (vectorDbEnabled && (ccPairsError || !ccPairs)) {
return (
<ErrorCallout
errorTitle="Failed to fetch Connectors"
@@ -70,7 +76,7 @@ function Main({ documentSetId }: { documentSetId: number }) {
return (
<CardSection>
<DocumentSetCreationForm
ccPairs={ccPairs}
ccPairs={ccPairs ?? []}
userGroups={userGroups}
onClose={() => {
refreshDocumentSets();

View File

@@ -9,20 +9,22 @@ 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();
} = useConnectorStatus(30000, vectorDbEnabled);
// EE only
const { data: userGroups, isLoading: userGroupsIsLoading } = useUserGroups();
if (isCCPairsLoading || userGroupsIsLoading) {
if ((vectorDbEnabled && isCCPairsLoading) || userGroupsIsLoading) {
return (
<div className="flex justify-center items-center min-h-[400px]">
<ThreeDotsLoader />
@@ -30,7 +32,7 @@ function Main() {
);
}
if (ccPairsError || !ccPairs) {
if (vectorDbEnabled && (ccPairsError || !ccPairs)) {
return (
<ErrorCallout
errorTitle="Failed to fetch Connectors"
@@ -43,7 +45,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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
export interface EmbeddingDetails {
api_key?: string;
@@ -279,7 +279,7 @@ export default function EmbeddingModelSelection({
{currentEmbeddingModel?.provider_type && (
<div className="mt-2">
<Button
secondary
prominence="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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
import { CloudEmbeddingModel } from "../../../../components/embedding/interfaces";
import { SvgCheck } from "@opal/icons";

View File

@@ -268,6 +268,7 @@ export default function ChangeCredentialsModal({
</Callout>
)}
{/* TODO(@raunakab): migrate to opal Button once className/iconClassName is resolved */}
<Button
className="mr-auto mt-4"
onClick={() => handleSubmit()}
@@ -289,6 +290,7 @@ 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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 secondary onClick={onCancel}>
<Button prominence="secondary" onClick={onCancel}>
Keep Credentials
</Button>
<Button danger onClick={onConfirm}>
<Button variant="danger" onClick={onConfirm}>
Delete Credentials
</Button>
</Modal.Footer>

View File

@@ -1,5 +1,5 @@
import Modal from "@/refresh-components/Modal";
import Button from "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 secondary onClick={onClose}>
<Button prominence="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 "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 secondary onClick={onCancel}>
<Button prominence="secondary" onClick={onCancel}>
Cancel
</Button>
</Modal.Footer>

View File

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

View File

@@ -1,5 +1,5 @@
import Modal from "@/refresh-components/Modal";
import Button from "@/refresh-components/buttons/Button";
import { Button } from "@opal/components";
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 secondary onClick={onCancel}>
<Button prominence="secondary" onClick={onCancel}>
Cancel
</Button>
</Modal.Footer>

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