mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-04 23:25:46 +00:00
Compare commits
16 Commits
table-prim
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2d996e05a4 | ||
|
|
b2956f795b | ||
|
|
b272085543 | ||
|
|
8193aa4fd0 | ||
|
|
52db41a00b | ||
|
|
f1cf3c4589 | ||
|
|
5322aeed90 | ||
|
|
5da8870fd2 | ||
|
|
57d3ab3b40 | ||
|
|
649c7fe8b9 | ||
|
|
e5e2bc6149 | ||
|
|
b148065e1d | ||
|
|
367808951c | ||
|
|
0f74da3302 | ||
|
|
96f7cbd25a | ||
|
|
c627cea17d |
1
.github/workflows/pr-integration-tests.yml
vendored
1
.github/workflows/pr-integration-tests.yml
vendored
@@ -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
|
||||
|
||||
|
||||
108
.github/workflows/pr-playwright-tests.yml
vendored
108
.github/workflows/pr-playwright-tests.yml
vendored
@@ -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
43
.vscode/launch.json
vendored
@@ -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",
|
||||
|
||||
31
AGENTS.md
31
AGENTS.md
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
)
|
||||
)
|
||||
@@ -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
|
||||
@@ -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",
|
||||
)
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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] = {"&": "&", "<": "<", ">": ">"}
|
||||
|
||||
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 " 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"
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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())
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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"]
|
||||
|
||||
@@ -104,3 +104,102 @@ def test_format_slack_message_ampersand_not_double_escaped() -> None:
|
||||
|
||||
assert "&" in formatted
|
||||
assert """ 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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -23,3 +23,4 @@ url = "2.5"
|
||||
[features]
|
||||
default = ["custom-protocol"]
|
||||
custom-protocol = ["tauri/custom-protocol"]
|
||||
devtools = ["tauri/devtools"]
|
||||
|
||||
@@ -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
6
uv.lock
generated
@@ -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]]
|
||||
|
||||
@@ -11,6 +11,8 @@ export {
|
||||
Interactive,
|
||||
type InteractiveBaseProps,
|
||||
type InteractiveBaseVariantProps,
|
||||
type InteractiveBaseSidebarVariantProps,
|
||||
type InteractiveBaseSidebarProminenceTypes,
|
||||
type InteractiveContainerProps,
|
||||
type InteractiveContainerRoundingVariant,
|
||||
} from "@opal/core/interactive/components";
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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";
|
||||
|
||||
22
web/lib/opal/src/icons/user-sync.tsx
Normal file
22
web/lib/opal/src/icons/user-sync.tsx
Normal 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;
|
||||
@@ -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}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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/,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
@@ -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
|
||||
/>
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -48,6 +48,7 @@ export default function ConnectorWrapper({
|
||||
<HeaderTitle>
|
||||
<p>‘{connector}’ 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"
|
||||
|
||||
@@ -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()}
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user