Compare commits

..

26 Commits

Author SHA1 Message Date
pablodanswer
3b234341b4 improve validation schema 2025-02-13 09:34:10 -08:00
pablodanswer
523fa76390 k 2025-02-12 20:01:45 -08:00
pablodanswer
3afc7b0aa1 quick nit 2025-02-12 19:16:53 -08:00
pablodanswer
3406a8385d k 2025-02-12 18:44:44 -08:00
pablodanswer
0ff1573f30 nit 2025-02-12 18:00:48 -08:00
pablodanswer
cfc43d27a3 minor misc ux 2025-02-12 18:00:47 -08:00
Yuhong Sun
c87261cda7 Fix edge case with run functions in parallel 2025-02-12 17:57:39 -08:00
pablonyx
e030b0a6fc Address (#3955) 2025-02-12 13:53:13 -08:00
Yuhong Sun
61136975ad Don't build model server every night (#3973) 2025-02-12 13:08:05 -08:00
Weves
0c74bbf9ed Clean illegal chars in metadata 2025-02-12 11:49:16 -08:00
pablonyx
12b2126e69 Update assistants visibility, minor UX, .. (#3965)
* update assistant logic

* quick nit

* k

* fix "featured" logic

* Small tweaks

* k

---------

Co-authored-by: Weves <chrisweaver101@gmail.com>
2025-02-12 00:43:20 +00:00
Chris Weaver
037943c6ff Support share/view IDs for Airtable (#3967) 2025-02-11 16:19:38 -08:00
pablonyx
f9485b1325 Ensure sidepanel defaults sidebar off (#3844)
* ensure sidepanel defaults sidepanel off

* address comment

* reformat

* initial visible
2025-02-11 22:22:56 +00:00
rkuo-danswer
552a0630fe Merge pull request #3948 from onyx-dot-app/feature/beat_rtvar
refactoring and update multiplier in real time
2025-02-11 14:05:14 -08:00
Richard Kuo (Danswer)
5bf520d8b8 comments 2025-02-11 14:04:49 -08:00
Weves
7dc5a77946 Improve starter message splitting 2025-02-11 11:10:13 -08:00
rkuo-danswer
03abd4a1bc Merge pull request #3938 from onyx-dot-app/feature/model_server_logs
improve gpu detection functions and logging in model server
2025-02-11 09:43:25 -08:00
Richard Kuo (Danswer)
16d6d708f6 update logging 2025-02-11 09:15:39 -08:00
Richard Kuo
9740ed32b5 fix reading redis values as floats 2025-02-10 20:48:55 -08:00
rkuo-danswer
b56877cc2e Bugfix/dedupe ids (#3952)
* dedupe make_private_persona and update test

* add comment

* comments, and just have duplicate user id's for the test instead of modifying edit

* found the magic word

---------

Co-authored-by: Richard Kuo (Danswer) <rkuo@onyx.app>
2025-02-11 02:27:55 +00:00
pablodanswer
da5c83a96d k 2025-02-10 17:45:00 -08:00
Weves
818225c60e Fix starter message overflow 2025-02-10 17:17:31 -08:00
Richard Kuo (Danswer)
5a4d007cf9 comments 2025-02-10 15:03:59 -08:00
Richard Kuo (Danswer)
5e32f9d922 refactoring and update multiplier in real time 2025-02-10 11:20:38 -08:00
Richard Kuo (Danswer)
fb931ee4de fixes 2025-02-07 17:28:17 -08:00
Richard Kuo (Danswer)
bc2c56dfb6 improve gpu detection functions and logging in model server 2025-02-07 16:59:02 -08:00
89 changed files with 1715 additions and 791 deletions

View File

@@ -4,6 +4,9 @@ on:
push:
tags:
- "*"
paths:
- 'backend/model_server/**'
- 'backend/Dockerfile.model_server'
env:
REGISTRY_IMAGE: ${{ contains(github.ref_name, 'cloud') && 'onyxdotapp/onyx-model-server-cloud' || 'onyxdotapp/onyx-model-server' }}

View File

@@ -0,0 +1,32 @@
"""set built in to default
Revision ID: 2cdeff6d8c93
Revises: f5437cc136c5
Create Date: 2025-02-11 14:57:51.308775
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "2cdeff6d8c93"
down_revision = "f5437cc136c5"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Prior to this migration / point in the codebase history,
# built in personas were implicitly treated as default personas (with no option to change this)
# This migration makes that explicit
op.execute(
"""
UPDATE persona
SET is_default_persona = TRUE
WHERE builtin_persona = TRUE
"""
)
def downgrade() -> None:
pass

View File

@@ -3,42 +3,44 @@ from typing import Any
from onyx.background.celery.tasks.beat_schedule import BEAT_EXPIRES_DEFAULT
from onyx.background.celery.tasks.beat_schedule import (
cloud_tasks_to_schedule as base_cloud_tasks_to_schedule,
beat_system_tasks as base_beat_system_tasks,
)
from onyx.background.celery.tasks.beat_schedule import (
tasks_to_schedule as base_tasks_to_schedule,
beat_task_templates as base_beat_task_templates,
)
from onyx.background.celery.tasks.beat_schedule import generate_cloud_tasks
from onyx.background.celery.tasks.beat_schedule import (
get_tasks_to_schedule as base_get_tasks_to_schedule,
)
from onyx.configs.constants import ONYX_CLOUD_CELERY_TASK_PREFIX
from onyx.configs.constants import OnyxCeleryPriority
from onyx.configs.constants import OnyxCeleryTask
from shared_configs.configs import MULTI_TENANT
ee_cloud_tasks_to_schedule = [
{
"name": f"{ONYX_CLOUD_CELERY_TASK_PREFIX}_autogenerate-usage-report",
"task": OnyxCeleryTask.CLOUD_BEAT_TASK_GENERATOR,
"schedule": timedelta(days=30),
"options": {
"priority": OnyxCeleryPriority.HIGHEST,
"expires": BEAT_EXPIRES_DEFAULT,
ee_beat_system_tasks: list[dict] = []
ee_beat_task_templates: list[dict] = []
ee_beat_task_templates.extend(
[
{
"name": "autogenerate-usage-report",
"task": OnyxCeleryTask.AUTOGENERATE_USAGE_REPORT_TASK,
"schedule": timedelta(days=30),
"options": {
"priority": OnyxCeleryPriority.MEDIUM,
"expires": BEAT_EXPIRES_DEFAULT,
},
},
"kwargs": {
"task_name": OnyxCeleryTask.AUTOGENERATE_USAGE_REPORT_TASK,
{
"name": "check-ttl-management",
"task": OnyxCeleryTask.CHECK_TTL_MANAGEMENT_TASK,
"schedule": timedelta(hours=1),
"options": {
"priority": OnyxCeleryPriority.MEDIUM,
"expires": BEAT_EXPIRES_DEFAULT,
},
},
},
{
"name": f"{ONYX_CLOUD_CELERY_TASK_PREFIX}_check-ttl-management",
"task": OnyxCeleryTask.CLOUD_BEAT_TASK_GENERATOR,
"schedule": timedelta(hours=1),
"options": {
"priority": OnyxCeleryPriority.HIGHEST,
"expires": BEAT_EXPIRES_DEFAULT,
},
"kwargs": {
"task_name": OnyxCeleryTask.CHECK_TTL_MANAGEMENT_TASK,
},
},
]
]
)
ee_tasks_to_schedule: list[dict] = []
@@ -65,9 +67,14 @@ if not MULTI_TENANT:
]
def get_cloud_tasks_to_schedule() -> list[dict[str, Any]]:
return ee_cloud_tasks_to_schedule + base_cloud_tasks_to_schedule
def get_cloud_tasks_to_schedule(beat_multiplier: float) -> list[dict[str, Any]]:
beat_system_tasks = ee_beat_system_tasks + base_beat_system_tasks
beat_task_templates = ee_beat_task_templates + base_beat_task_templates
cloud_tasks = generate_cloud_tasks(
beat_system_tasks, beat_task_templates, beat_multiplier
)
return cloud_tasks
def get_tasks_to_schedule() -> list[dict[str, Any]]:
return ee_tasks_to_schedule + base_tasks_to_schedule
return ee_tasks_to_schedule + base_get_tasks_to_schedule()

View File

@@ -15,6 +15,9 @@ def make_persona_private(
group_ids: list[int] | None,
db_session: Session,
) -> None:
"""NOTE(rkuo): This function batches all updates into a single commit. If we don't
dedupe the inputs, the commit will exception."""
db_session.query(Persona__User).filter(
Persona__User.persona_id == persona_id
).delete(synchronize_session="fetch")
@@ -23,19 +26,22 @@ def make_persona_private(
).delete(synchronize_session="fetch")
if user_ids:
for user_uuid in user_ids:
db_session.add(Persona__User(persona_id=persona_id, user_id=user_uuid))
user_ids_set = set(user_ids)
for user_id in user_ids_set:
db_session.add(Persona__User(persona_id=persona_id, user_id=user_id))
create_notification(
user_id=user_uuid,
user_id=user_id,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if group_ids:
for group_id in group_ids:
group_ids_set = set(group_ids)
for group_id in group_ids_set:
db_session.add(
Persona__UserGroup(persona_id=persona_id, user_group_id=group_id)
)

View File

@@ -28,3 +28,9 @@ class EmbeddingModelTextType:
@staticmethod
def get_type(provider: EmbeddingProvider, text_type: EmbedTextType) -> str:
return EmbeddingModelTextType.PROVIDER_TEXT_TYPE_MAP[provider][text_type]
class GPUStatus:
CUDA = "cuda"
MAC_MPS = "mps"
NONE = "none"

View File

@@ -12,6 +12,7 @@ import voyageai # type: ignore
from cohere import AsyncClient as CohereAsyncClient
from fastapi import APIRouter
from fastapi import HTTPException
from fastapi import Request
from google.oauth2 import service_account # type: ignore
from litellm import aembedding
from litellm.exceptions import RateLimitError
@@ -320,6 +321,7 @@ async def embed_text(
prefix: str | None,
api_url: str | None,
api_version: str | None,
gpu_type: str = "UNKNOWN",
) -> list[Embedding]:
if not all(texts):
logger.error("Empty strings provided for embedding")
@@ -373,8 +375,11 @@ async def embed_text(
elapsed = time.monotonic() - start
logger.info(
f"Successfully embedded {len(texts)} texts with {total_chars} total characters "
f"with provider {provider_type} in {elapsed:.2f}"
f"event=embedding_provider "
f"texts={len(texts)} "
f"chars={total_chars} "
f"provider={provider_type} "
f"elapsed={elapsed:.2f}"
)
elif model_name is not None:
logger.info(
@@ -403,6 +408,14 @@ async def embed_text(
f"Successfully embedded {len(texts)} texts with {total_chars} total characters "
f"with local model {model_name} in {elapsed:.2f}"
)
logger.info(
f"event=embedding_model "
f"texts={len(texts)} "
f"chars={total_chars} "
f"model={model_name} "
f"gpu={gpu_type} "
f"elapsed={elapsed:.2f}"
)
else:
logger.error("Neither model name nor provider specified for embedding")
raise ValueError(
@@ -455,8 +468,15 @@ async def litellm_rerank(
@router.post("/bi-encoder-embed")
async def process_embed_request(
async def route_bi_encoder_embed(
request: Request,
embed_request: EmbedRequest,
) -> EmbedResponse:
return await process_embed_request(embed_request, request.app.state.gpu_type)
async def process_embed_request(
embed_request: EmbedRequest, gpu_type: str = "UNKNOWN"
) -> EmbedResponse:
if not embed_request.texts:
raise HTTPException(status_code=400, detail="No texts to be embedded")
@@ -484,6 +504,7 @@ async def process_embed_request(
api_url=embed_request.api_url,
api_version=embed_request.api_version,
prefix=prefix,
gpu_type=gpu_type,
)
return EmbedResponse(embeddings=embeddings)
except RateLimitError as e:

View File

@@ -16,6 +16,7 @@ from model_server.custom_models import router as custom_models_router
from model_server.custom_models import warm_up_intent_model
from model_server.encoders import router as encoders_router
from model_server.management_endpoints import router as management_router
from model_server.utils import get_gpu_type
from onyx import __version__
from onyx.utils.logger import setup_logger
from shared_configs.configs import INDEXING_ONLY
@@ -58,12 +59,10 @@ def _move_files_recursively(source: Path, dest: Path, overwrite: bool = False) -
@asynccontextmanager
async def lifespan(app: FastAPI) -> AsyncGenerator:
if torch.cuda.is_available():
logger.notice("CUDA GPU is available")
elif torch.backends.mps.is_available():
logger.notice("Mac MPS is available")
else:
logger.notice("GPU is not available, using CPU")
gpu_type = get_gpu_type()
logger.notice(f"Torch GPU Detection: gpu_type={gpu_type}")
app.state.gpu_type = gpu_type
if TEMP_HF_CACHE_PATH.is_dir():
logger.notice("Moving contents of temp_huggingface to huggingface cache.")

View File

@@ -1,7 +1,9 @@
import torch
from fastapi import APIRouter
from fastapi import Response
from model_server.constants import GPUStatus
from model_server.utils import get_gpu_type
router = APIRouter(prefix="/api")
@@ -11,10 +13,7 @@ async def healthcheck() -> Response:
@router.get("/gpu-status")
async def gpu_status() -> dict[str, bool | str]:
if torch.cuda.is_available():
return {"gpu_available": True, "type": "cuda"}
elif torch.backends.mps.is_available():
return {"gpu_available": True, "type": "mps"}
else:
return {"gpu_available": False, "type": "none"}
async def route_gpu_status() -> dict[str, bool | str]:
gpu_type = get_gpu_type()
gpu_available = gpu_type != GPUStatus.NONE
return {"gpu_available": gpu_available, "type": gpu_type}

View File

@@ -8,6 +8,9 @@ from typing import Any
from typing import cast
from typing import TypeVar
import torch
from model_server.constants import GPUStatus
from onyx.utils.logger import setup_logger
logger = setup_logger()
@@ -58,3 +61,12 @@ def simple_log_function_time(
return cast(F, wrapped_sync_func)
return decorator
def get_gpu_type() -> str:
if torch.cuda.is_available():
return GPUStatus.CUDA
if torch.backends.mps.is_available():
return GPUStatus.MAC_MPS
return GPUStatus.NONE

View File

@@ -1,41 +1,56 @@
from datetime import timedelta
from typing import Any
from typing import cast
from celery import Celery
from celery import signals
from celery.beat import PersistentScheduler # type: ignore
from celery.signals import beat_init
from celery.utils.log import get_task_logger
import onyx.background.celery.apps.app_base as app_base
from onyx.background.celery.tasks.beat_schedule import CLOUD_BEAT_MULTIPLIER_DEFAULT
from onyx.configs.constants import ONYX_CLOUD_REDIS_RUNTIME
from onyx.configs.constants import ONYX_CLOUD_TENANT_ID
from onyx.configs.constants import POSTGRES_CELERY_BEAT_APP_NAME
from onyx.db.engine import get_all_tenant_ids
from onyx.db.engine import SqlEngine
from onyx.utils.logger import setup_logger
from onyx.redis.redis_pool import get_redis_replica_client
from onyx.utils.variable_functionality import fetch_versioned_implementation
from shared_configs.configs import IGNORED_SYNCING_TENANT_LIST
from shared_configs.configs import MULTI_TENANT
logger = setup_logger(__name__)
task_logger = get_task_logger(__name__)
celery_app = Celery(__name__)
celery_app.config_from_object("onyx.background.celery.configs.beat")
class DynamicTenantScheduler(PersistentScheduler):
"""This scheduler is useful because we can dynamically adjust task generation rates
through it."""
RELOAD_INTERVAL = 60
def __init__(self, *args: Any, **kwargs: Any) -> None:
logger.info("Initializing DynamicTenantScheduler")
super().__init__(*args, **kwargs)
self._reload_interval = timedelta(minutes=2)
self.last_beat_multiplier = CLOUD_BEAT_MULTIPLIER_DEFAULT
self._reload_interval = timedelta(
seconds=DynamicTenantScheduler.RELOAD_INTERVAL
)
self._last_reload = self.app.now() - self._reload_interval
# Let the parent class handle store initialization
self.setup_schedule()
self._try_updating_schedule()
logger.info(f"Set reload interval to {self._reload_interval}")
task_logger.info(
f"DynamicTenantScheduler initialized: reload_interval={self._reload_interval}"
)
def setup_schedule(self) -> None:
logger.info("Setting up initial schedule")
super().setup_schedule()
logger.info("Initial schedule setup complete")
def tick(self) -> float:
retval = super().tick()
@@ -44,36 +59,35 @@ class DynamicTenantScheduler(PersistentScheduler):
self._last_reload is None
or (now - self._last_reload) > self._reload_interval
):
logger.info("Reload interval reached, initiating task update")
task_logger.debug("Reload interval reached, initiating task update")
try:
self._try_updating_schedule()
except (AttributeError, KeyError) as e:
logger.exception(f"Failed to process task configuration: {str(e)}")
except Exception as e:
logger.exception(f"Unexpected error updating tasks: {str(e)}")
except (AttributeError, KeyError):
task_logger.exception("Failed to process task configuration")
except Exception:
task_logger.exception("Unexpected error updating tasks")
self._last_reload = now
logger.info("Task update completed, reset reload timer")
return retval
def _generate_schedule(
self, tenant_ids: list[str] | list[None]
self, tenant_ids: list[str] | list[None], beat_multiplier: float
) -> dict[str, dict[str, Any]]:
"""Given a list of tenant id's, generates a new beat schedule for celery."""
logger.info("Fetching tasks to schedule")
new_schedule: dict[str, dict[str, Any]] = {}
if MULTI_TENANT:
# cloud tasks only need the single task beat across all tenants
# cloud tasks are system wide and thus only need to be on the beat schedule
# once for all tenants
get_cloud_tasks_to_schedule = fetch_versioned_implementation(
"onyx.background.celery.tasks.beat_schedule",
"get_cloud_tasks_to_schedule",
)
cloud_tasks_to_schedule: list[
dict[str, Any]
] = get_cloud_tasks_to_schedule()
cloud_tasks_to_schedule: list[dict[str, Any]] = get_cloud_tasks_to_schedule(
beat_multiplier
)
for task in cloud_tasks_to_schedule:
task_name = task["name"]
cloud_task = {
@@ -82,11 +96,14 @@ class DynamicTenantScheduler(PersistentScheduler):
"kwargs": task.get("kwargs", {}),
}
if options := task.get("options"):
logger.debug(f"Adding options to task {task_name}: {options}")
task_logger.debug(f"Adding options to task {task_name}: {options}")
cloud_task["options"] = options
new_schedule[task_name] = cloud_task
# regular task beats are multiplied across all tenants
# note that currently this just schedules for a single tenant in self hosted
# and doesn't do anything in the cloud because it's much more scalable
# to schedule a single cloud beat task to dispatch per tenant tasks.
get_tasks_to_schedule = fetch_versioned_implementation(
"onyx.background.celery.tasks.beat_schedule", "get_tasks_to_schedule"
)
@@ -95,7 +112,7 @@ class DynamicTenantScheduler(PersistentScheduler):
for tenant_id in tenant_ids:
if IGNORED_SYNCING_TENANT_LIST and tenant_id in IGNORED_SYNCING_TENANT_LIST:
logger.info(
task_logger.debug(
f"Skipping tenant {tenant_id} as it is in the ignored syncing list"
)
continue
@@ -104,14 +121,14 @@ class DynamicTenantScheduler(PersistentScheduler):
task_name = task["name"]
tenant_task_name = f"{task['name']}-{tenant_id}"
logger.debug(f"Creating task configuration for {tenant_task_name}")
task_logger.debug(f"Creating task configuration for {tenant_task_name}")
tenant_task = {
"task": task["task"],
"schedule": task["schedule"],
"kwargs": {"tenant_id": tenant_id},
}
if options := task.get("options"):
logger.debug(
task_logger.debug(
f"Adding options to task {tenant_task_name}: {options}"
)
tenant_task["options"] = options
@@ -121,44 +138,57 @@ class DynamicTenantScheduler(PersistentScheduler):
def _try_updating_schedule(self) -> None:
"""Only updates the actual beat schedule on the celery app when it changes"""
do_update = False
logger.info("_try_updating_schedule starting")
r = get_redis_replica_client(tenant_id=ONYX_CLOUD_TENANT_ID)
task_logger.debug("_try_updating_schedule starting")
tenant_ids = get_all_tenant_ids()
logger.info(f"Found {len(tenant_ids)} IDs")
task_logger.debug(f"Found {len(tenant_ids)} IDs")
# get current schedule and extract current tenants
current_schedule = self.schedule.items()
# there are no more per tenant beat tasks, so comment this out
# NOTE: we may not actualy need this scheduler any more and should
# test reverting to a regular beat schedule implementation
# get potential new state
beat_multiplier = CLOUD_BEAT_MULTIPLIER_DEFAULT
beat_multiplier_raw = r.get(f"{ONYX_CLOUD_REDIS_RUNTIME}:beat_multiplier")
if beat_multiplier_raw is not None:
try:
beat_multiplier_bytes = cast(bytes, beat_multiplier_raw)
beat_multiplier = float(beat_multiplier_bytes.decode())
except ValueError:
task_logger.error(
f"Invalid beat_multiplier value: {beat_multiplier_raw}"
)
# current_tenants = set()
# for task_name, _ in current_schedule:
# task_name = cast(str, task_name)
# if task_name.startswith(ONYX_CLOUD_CELERY_TASK_PREFIX):
# continue
new_schedule = self._generate_schedule(tenant_ids, beat_multiplier)
# if "_" in task_name:
# # example: "check-for-condition-tenant_12345678-abcd-efgh-ijkl-12345678"
# # -> "12345678-abcd-efgh-ijkl-12345678"
# current_tenants.add(task_name.split("_")[-1])
# logger.info(f"Found {len(current_tenants)} existing items in schedule")
# if the schedule or beat multiplier has changed, update
while True:
if beat_multiplier != self.last_beat_multiplier:
do_update = True
break
# for tenant_id in tenant_ids:
# if tenant_id not in current_tenants:
# logger.info(f"Processing new tenant: {tenant_id}")
if not DynamicTenantScheduler._compare_schedules(
current_schedule, new_schedule
):
do_update = True
break
new_schedule = self._generate_schedule(tenant_ids)
break
if DynamicTenantScheduler._compare_schedules(current_schedule, new_schedule):
logger.info(
"_try_updating_schedule: Current schedule is up to date, no changes needed"
if not do_update:
# exit early if nothing changed
task_logger.info(
f"_try_updating_schedule - Schedule unchanged: "
f"tasks={len(new_schedule)} "
f"beat_multiplier={beat_multiplier}"
)
return
logger.info(
# schedule needs updating
task_logger.debug(
"Schedule update required",
extra={
"new_tasks": len(new_schedule),
@@ -185,11 +215,19 @@ class DynamicTenantScheduler(PersistentScheduler):
# Ensure changes are persisted
self.sync()
logger.info("_try_updating_schedule: Schedule updated successfully")
task_logger.info(
f"_try_updating_schedule - Schedule updated: "
f"prev_num_tasks={len(current_schedule)} "
f"prev_beat_multiplier={self.last_beat_multiplier} "
f"tasks={len(new_schedule)} "
f"beat_multiplier={beat_multiplier}"
)
self.last_beat_multiplier = beat_multiplier
@staticmethod
def _compare_schedules(schedule1: dict, schedule2: dict) -> bool:
"""Compare schedules to determine if an update is needed.
"""Compare schedules by task name only to determine if an update is needed.
True if equivalent, False if not."""
current_tasks = set(name for name, _ in schedule1)
new_tasks = set(schedule2.keys())
@@ -201,7 +239,7 @@ class DynamicTenantScheduler(PersistentScheduler):
@beat_init.connect
def on_beat_init(sender: Any, **kwargs: Any) -> None:
logger.info("beat_init signal received.")
task_logger.info("beat_init signal received.")
# Celery beat shouldn't touch the db at all. But just setting a low minimum here.
SqlEngine.set_app_name(POSTGRES_CELERY_BEAT_APP_NAME)

View File

@@ -1,3 +1,4 @@
import copy
from datetime import timedelta
from typing import Any
@@ -18,7 +19,7 @@ BEAT_EXPIRES_DEFAULT = 15 * 60 # 15 minutes (in seconds)
# hack to slow down task dispatch in the cloud until
# we have a better implementation (backpressure, etc)
CLOUD_BEAT_SCHEDULE_MULTIPLIER = 8
CLOUD_BEAT_MULTIPLIER_DEFAULT = 8.0
# tasks that run in either self-hosted on cloud
beat_task_templates: list[dict] = []
@@ -121,7 +122,7 @@ def make_cloud_generator_task(task: dict[str, Any]) -> dict[str, Any]:
# constant options for cloud beat task generators
task_schedule: timedelta = task["schedule"]
cloud_task["schedule"] = task_schedule * CLOUD_BEAT_SCHEDULE_MULTIPLIER
cloud_task["schedule"] = task_schedule
cloud_task["options"] = {}
cloud_task["options"]["priority"] = OnyxCeleryPriority.HIGHEST
cloud_task["options"]["expires"] = BEAT_EXPIRES_DEFAULT
@@ -141,9 +142,9 @@ def make_cloud_generator_task(task: dict[str, Any]) -> dict[str, Any]:
# tasks that only run in the cloud
# the name attribute must start with ONYX_CLOUD_CELERY_TASK_PREFIX = "cloud" to be filtered
# by the DynamicTenantScheduler
cloud_tasks_to_schedule: list[dict] = [
# the name attribute must start with ONYX_CLOUD_CELERY_TASK_PREFIX = "cloud" to be seen
# by the DynamicTenantScheduler as system wide task and not a per tenant task
beat_system_tasks: list[dict] = [
# cloud specific tasks
{
"name": f"{ONYX_CLOUD_CELERY_TASK_PREFIX}_check-alembic",
@@ -157,18 +158,45 @@ cloud_tasks_to_schedule: list[dict] = [
},
]
# generate our cloud and self-hosted beat tasks from the templates
for beat_task_template in beat_task_templates:
cloud_task = make_cloud_generator_task(beat_task_template)
cloud_tasks_to_schedule.append(cloud_task)
tasks_to_schedule: list[dict] = []
if not MULTI_TENANT:
tasks_to_schedule = beat_task_templates
def get_cloud_tasks_to_schedule() -> list[dict[str, Any]]:
return cloud_tasks_to_schedule
def generate_cloud_tasks(
beat_tasks: list[dict], beat_templates: list[dict], beat_multiplier: float
) -> list[dict[str, Any]]:
"""
beat_tasks: system wide tasks that can be sent as is
beat_templates: task templates that will be transformed into per tenant tasks via
the cloud_beat_task_generator
beat_multiplier: a multiplier that can be applied on top of the task schedule
to speed up or slow down the task generation rate. useful in production.
Returns a list of cloud tasks, which consists of incoming tasks + tasks generated
from incoming templates.
"""
if beat_multiplier <= 0:
raise ValueError("beat_multiplier must be positive!")
# start with the incoming beat tasks
cloud_tasks: list[dict] = copy.deepcopy(beat_tasks)
# generate our cloud tasks from the templates
for beat_template in beat_templates:
cloud_task = make_cloud_generator_task(beat_template)
cloud_tasks.append(cloud_task)
# factor in the cloud multiplier
for cloud_task in cloud_tasks:
cloud_task["schedule"] = cloud_task["schedule"] * beat_multiplier
return cloud_tasks
def get_cloud_tasks_to_schedule(beat_multiplier: float) -> list[dict[str, Any]]:
return generate_cloud_tasks(beat_system_tasks, beat_task_templates, beat_multiplier)
def get_tasks_to_schedule() -> list[dict[str, Any]]:

View File

@@ -346,6 +346,9 @@ ONYX_CLOUD_CELERY_TASK_PREFIX = "cloud"
# the tenant id we use for system level redis operations
ONYX_CLOUD_TENANT_ID = "cloud"
# the redis namespace for runtime variables
ONYX_CLOUD_REDIS_RUNTIME = "runtime"
class OnyxCeleryTask:
DEFAULT = "celery"

View File

@@ -65,10 +65,25 @@ class AirtableConnector(LoadConnector):
base_id: str,
table_name_or_id: str,
treat_all_non_attachment_fields_as_metadata: bool = False,
view_id: str | None = None,
share_id: str | None = None,
batch_size: int = INDEX_BATCH_SIZE,
) -> None:
"""Initialize an AirtableConnector.
Args:
base_id: The ID of the Airtable base to connect to
table_name_or_id: The name or ID of the table to index
treat_all_non_attachment_fields_as_metadata: If True, all fields except attachments will be treated as metadata.
If False, only fields with types in DEFAULT_METADATA_FIELD_TYPES will be treated as metadata.
view_id: Optional ID of a specific view to use
share_id: Optional ID of a "share" to use for generating record URLs (https://airtable.com/developers/web/api/list-shares)
batch_size: Number of records to process in each batch
"""
self.base_id = base_id
self.table_name_or_id = table_name_or_id
self.view_id = view_id
self.share_id = share_id
self.batch_size = batch_size
self._airtable_client: AirtableApi | None = None
self.treat_all_non_attachment_fields_as_metadata = (
@@ -85,6 +100,39 @@ class AirtableConnector(LoadConnector):
raise AirtableClientNotSetUpError()
return self._airtable_client
@classmethod
def _get_record_url(
cls,
base_id: str,
table_id: str,
record_id: str,
share_id: str | None,
view_id: str | None,
field_id: str | None = None,
attachment_id: str | None = None,
) -> str:
"""Constructs the URL for a record, optionally including field and attachment IDs
Full possible structure is:
https://airtable.com/BASE_ID/SHARE_ID/TABLE_ID/VIEW_ID/RECORD_ID/FIELD_ID/ATTACHMENT_ID
"""
# If we have a shared link, use that view for better UX
if share_id:
base_url = f"https://airtable.com/{base_id}/{share_id}/{table_id}"
else:
base_url = f"https://airtable.com/{base_id}/{table_id}"
if view_id:
base_url = f"{base_url}/{view_id}"
base_url = f"{base_url}/{record_id}"
if field_id and attachment_id:
return f"{base_url}/{field_id}/{attachment_id}?blocks=hide"
return base_url
def _extract_field_values(
self,
field_id: str,
@@ -110,8 +158,10 @@ class AirtableConnector(LoadConnector):
if field_type == "multipleRecordLinks":
return []
# default link to use for non-attachment fields
default_link = f"https://airtable.com/{base_id}/{table_id}/{record_id}"
# Get the base URL for this record
default_link = self._get_record_url(
base_id, table_id, record_id, self.share_id, self.view_id or view_id
)
if field_type == "multipleAttachments":
attachment_texts: list[tuple[str, str]] = []
@@ -165,17 +215,16 @@ class AirtableConnector(LoadConnector):
extension=file_ext,
)
if attachment_text:
# slightly nicer loading experience if we can specify the view ID
if view_id:
attachment_link = (
f"https://airtable.com/{base_id}/{table_id}/{view_id}/{record_id}"
f"/{field_id}/{attachment_id}?blocks=hide"
)
else:
attachment_link = (
f"https://airtable.com/{base_id}/{table_id}/{record_id}"
f"/{field_id}/{attachment_id}?blocks=hide"
)
# Use the helper method to construct attachment URLs
attachment_link = self._get_record_url(
base_id,
table_id,
record_id,
self.share_id,
self.view_id or view_id,
field_id,
attachment_id,
)
attachment_texts.append(
(f"{filename}:\n{attachment_text}", attachment_link)
)

View File

@@ -204,6 +204,14 @@ def create_update_persona(
if not all_prompt_ids:
raise ValueError("No prompt IDs provided")
# Default persona validation
if create_persona_request.is_default_persona:
if not create_persona_request.is_public:
raise ValueError("Cannot make a default persona non public")
if user and user.role != UserRole.ADMIN:
raise ValueError("Only admins can make a default persona")
persona = upsert_persona(
persona_id=persona_id,
user=user,
@@ -510,6 +518,7 @@ def upsert_persona(
existing_persona.is_visible = is_visible
existing_persona.search_start_date = search_start_date
existing_persona.labels = labels or []
existing_persona.is_default_persona = is_default_persona
# Do not delete any associations manually added unless
# a new updated list is provided
if document_sets is not None:
@@ -590,6 +599,23 @@ def delete_old_default_personas(
db_session.commit()
def update_persona_is_default(
persona_id: int,
is_default: bool,
db_session: Session,
user: User | None = None,
) -> None:
persona = fetch_persona_by_id_for_user(
db_session=db_session, persona_id=persona_id, user=user, get_editable=True
)
if not persona.is_public:
persona.is_public = True
persona.is_default_persona = is_default
db_session.commit()
def update_persona_visibility(
persona_id: int,
is_visible: bool,

View File

@@ -146,6 +146,23 @@ def _index_vespa_chunk(
title = document.get_title_for_document_index()
metadata_json = document.metadata
cleaned_metadata_json: dict[str, str | list[str]] = {}
for key, value in metadata_json.items():
cleaned_key = remove_invalid_unicode_chars(key)
if isinstance(value, list):
cleaned_metadata_json[cleaned_key] = [
remove_invalid_unicode_chars(item) for item in value
]
else:
cleaned_metadata_json[cleaned_key] = remove_invalid_unicode_chars(value)
metadata_list = document.get_metadata_str_attributes()
if metadata_list:
metadata_list = [
remove_invalid_unicode_chars(metadata) for metadata in metadata_list
]
vespa_document_fields = {
DOCUMENT_ID: document.id,
CHUNK_ID: chunk.chunk_id,
@@ -166,10 +183,10 @@ def _index_vespa_chunk(
SEMANTIC_IDENTIFIER: remove_invalid_unicode_chars(document.semantic_identifier),
SECTION_CONTINUATION: chunk.section_continuation,
LARGE_CHUNK_REFERENCE_IDS: chunk.large_chunk_reference_ids,
METADATA: json.dumps(document.metadata),
METADATA: json.dumps(cleaned_metadata_json),
# Save as a list for efficient extraction as an Attribute
METADATA_LIST: chunk.source_document.get_metadata_str_attributes(),
METADATA_SUFFIX: chunk.metadata_suffix_keyword,
METADATA_LIST: metadata_list,
METADATA_SUFFIX: remove_invalid_unicode_chars(chunk.metadata_suffix_keyword),
EMBEDDINGS: embeddings_name_vector_map,
TITLE_EMBEDDING: chunk.title_embedding,
DOC_UPDATED_AT: _vespa_get_updated_at_attribute(document.doc_updated_at),

View File

@@ -396,9 +396,14 @@ class DefaultMultiLLM(LLM):
self._record_call(processed_prompt)
try:
print(
"model is",
f"{self.config.model_provider}/{self.config.deployment_name or self.config.model_name}",
)
return litellm.completion(
mock_response=MOCK_LLM_RESPONSE,
# model choice
# model="openai/gpt-4",
model=f"{self.config.model_provider}/{self.config.deployment_name or self.config.model_name}",
# NOTE: have to pass in None instead of empty string for these
# otherwise litellm can have some issues with bedrock

View File

@@ -162,6 +162,11 @@ def load_personas_from_yaml(
else persona.get("is_visible")
),
db_session=db_session,
is_default_persona=(
existing_persona.is_default_persona
if existing_persona is not None
else persona.get("is_default_persona", False)
),
)

View File

@@ -41,6 +41,7 @@ personas:
icon_color: "#6FB1FF"
display_priority: 0
is_visible: true
is_default_persona: true
starter_messages:
- name: "Give me an overview of what's here"
message: "Sample some documents and tell me what you find."
@@ -66,6 +67,7 @@ personas:
icon_color: "#FF6F6F"
display_priority: 1
is_visible: true
is_default_persona: true
starter_messages:
- name: "Summarize a document"
message: "If I have provided a document please summarize it for me. If not, please ask me to upload a document either by dragging it into the input bar or clicking the +file icon."
@@ -91,6 +93,7 @@ personas:
icon_color: "#6FFF8D"
display_priority: 2
is_visible: false
is_default_persona: true
starter_messages:
- name: "Document Search"
message: "Hi! Could you help me find information about our team structure and reporting lines from our internal documents?"
@@ -117,6 +120,7 @@ personas:
image_generation: true
display_priority: 3
is_visible: true
is_default_persona: true
starter_messages:
- name: "Create visuals for a presentation"
message: "Generate someone presenting a graph which clearly demonstrates an upwards trajectory."

View File

@@ -32,6 +32,7 @@ from onyx.db.persona import get_personas_for_user
from onyx.db.persona import mark_persona_as_deleted
from onyx.db.persona import mark_persona_as_not_deleted
from onyx.db.persona import update_all_personas_display_priority
from onyx.db.persona import update_persona_is_default
from onyx.db.persona import update_persona_label
from onyx.db.persona import update_persona_public_status
from onyx.db.persona import update_persona_shared_users
@@ -56,7 +57,6 @@ from onyx.tools.utils import is_image_generation_available
from onyx.utils.logger import setup_logger
from onyx.utils.telemetry import create_milestone_and_report
logger = setup_logger()
@@ -72,6 +72,10 @@ class IsPublicRequest(BaseModel):
is_public: bool
class IsDefaultRequest(BaseModel):
is_default_persona: bool
@admin_router.patch("/{persona_id}/visible")
def patch_persona_visibility(
persona_id: int,
@@ -106,6 +110,25 @@ def patch_user_presona_public_status(
raise HTTPException(status_code=403, detail=str(e))
@admin_router.patch("/{persona_id}/default")
def patch_persona_default_status(
persona_id: int,
is_default_request: IsDefaultRequest,
user: User | None = Depends(current_curator_or_admin_user),
db_session: Session = Depends(get_session),
) -> None:
try:
update_persona_is_default(
persona_id=persona_id,
is_default=is_default_request.is_default_persona,
db_session=db_session,
user=user,
)
except ValueError as e:
logger.exception("Failed to update persona default status")
raise HTTPException(status_code=403, detail=str(e))
@admin_router.put("/display-priority")
def patch_persona_display_priority(
display_priority_request: DisplayPriorityRequest,

View File

@@ -86,7 +86,10 @@ def run_functions_in_parallel(
Executes a list of FunctionCalls in parallel and stores the results in a dictionary where the keys
are the result_id of the FunctionCall and the values are the results of the call.
"""
results = {}
results: dict[str, Any] = {}
if len(function_calls) == 0:
return results
with ThreadPoolExecutor(max_workers=len(function_calls)) as executor:
future_to_id = {

View File

@@ -9,6 +9,8 @@ from onyx.connectors.airtable.airtable_connector import AirtableConnector
from onyx.connectors.models import Document
from onyx.connectors.models import Section
BASE_VIEW_ID = "viwVUEJjWPd8XYjh8"
class AirtableConfig(BaseModel):
base_id: str
@@ -46,6 +48,8 @@ def create_test_document(
days_since_status_change: int | None,
attachments: list[tuple[str, str]] | None = None,
all_fields_as_metadata: bool = False,
share_id: str | None = None,
view_id: str | None = None,
) -> Document:
base_id = os.environ.get("AIRTABLE_TEST_BASE_ID")
table_id = os.environ.get("AIRTABLE_TEST_TABLE_ID")
@@ -60,7 +64,13 @@ def create_test_document(
f"Required environment variables not set: {', '.join(missing_vars)}. "
"These variables are required to run Airtable connector tests."
)
link_base = f"https://airtable.com/{base_id}/{table_id}"
link_base = f"https://airtable.com/{base_id}"
if share_id:
link_base = f"{link_base}/{share_id}"
link_base = f"{link_base}/{table_id}"
if view_id:
link_base = f"{link_base}/{view_id}"
sections = []
if not all_fields_as_metadata:
@@ -214,6 +224,7 @@ def test_airtable_connector_basic(
assignee="Chris Weaver (chris@onyx.app)",
submitted_by="Chris Weaver (chris@onyx.app)",
all_fields_as_metadata=False,
view_id=BASE_VIEW_ID,
),
create_test_document(
id="reccSlIA4pZEFxPBg",
@@ -234,6 +245,7 @@ def test_airtable_connector_basic(
)
],
all_fields_as_metadata=False,
view_id=BASE_VIEW_ID,
),
]
@@ -285,6 +297,81 @@ def test_airtable_connector_all_metadata(
)
],
all_fields_as_metadata=True,
view_id=BASE_VIEW_ID,
),
]
# Compare documents using the utility function
compare_documents(doc_batch, expected_docs)
def test_airtable_connector_with_share_and_view(
mock_get_unstructured_api_key: MagicMock, airtable_config: AirtableConfig
) -> None:
"""Test behavior when using share_id and view_id for URL generation."""
SHARE_ID = "shrkfjEzDmLaDtK83"
connector = AirtableConnector(
base_id=airtable_config.base_id,
table_name_or_id=airtable_config.table_identifier,
treat_all_non_attachment_fields_as_metadata=False,
share_id=SHARE_ID,
view_id=BASE_VIEW_ID,
)
connector.load_credentials(
{
"airtable_access_token": airtable_config.access_token,
}
)
doc_batch_generator = connector.load_from_state()
doc_batch = next(doc_batch_generator)
with pytest.raises(StopIteration):
next(doc_batch_generator)
assert len(doc_batch) == 2
expected_docs = [
create_test_document(
id="rec8BnxDLyWeegOuO",
title="Slow Internet",
description="The internet connection is very slow.",
priority="Medium",
status="In Progress",
ticket_id="2",
created_time="2024-12-24T21:02:49.000Z",
status_last_changed="2024-12-24T21:02:49.000Z",
days_since_status_change=0,
assignee="Chris Weaver (chris@onyx.app)",
submitted_by="Chris Weaver (chris@onyx.app)",
all_fields_as_metadata=False,
share_id=SHARE_ID,
view_id=BASE_VIEW_ID,
),
create_test_document(
id="reccSlIA4pZEFxPBg",
title="Printer Issue",
description="The office printer is not working.",
priority="High",
status="Open",
ticket_id="1",
created_time="2024-12-24T21:02:49.000Z",
status_last_changed="2024-12-24T21:02:49.000Z",
days_since_status_change=0,
assignee="Chris Weaver (chris@onyx.app)",
submitted_by="Chris Weaver (chris@onyx.app)",
attachments=[
(
"Test.pdf:\ntesting!!!",
(
f"https://airtable.com/{airtable_config.base_id}/{SHARE_ID}/"
f"{os.environ['AIRTABLE_TEST_TABLE_ID']}/{BASE_VIEW_ID}/reccSlIA4pZEFxPBg/"
"fld1u21zkJACIvAEF/attlj2UBWNEDZngCc?blocks=hide"
),
)
],
all_fields_as_metadata=False,
share_id=SHARE_ID,
view_id=BASE_VIEW_ID,
),
]

View File

@@ -66,7 +66,7 @@ class PersonaManager:
response = requests.post(
f"{API_SERVER_URL}/persona",
json=persona_creation_request.model_dump(),
json=persona_creation_request.model_dump(mode="json"),
headers=user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS,
@@ -119,6 +119,7 @@ class PersonaManager:
) -> DATestPersona:
system_prompt = system_prompt or f"System prompt for {persona.name}"
task_prompt = task_prompt or f"Task prompt for {persona.name}"
persona_update_request = PersonaUpsertRequest(
name=name or persona.name,
description=description or persona.description,
@@ -146,7 +147,7 @@ class PersonaManager:
response = requests.patch(
f"{API_SERVER_URL}/persona/{persona.id}",
json=persona_update_request.model_dump(),
json=persona_update_request.model_dump(mode="json"),
headers=user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS,

View File

@@ -58,6 +58,7 @@ def test_persona_permissions(reset: None) -> None:
description="A persona created by basic user",
is_public=False,
groups=[],
users=[admin_user.id],
user_performing_action=basic_user,
)
PersonaManager.verify(basic_user_persona, user_performing_action=basic_user)
@@ -139,9 +140,14 @@ def test_persona_permissions(reset: None) -> None:
"""Test admin permissions"""
# Admin can edit any persona
# the persona was shared with the admin user on creation
# this edit call will simulate having the same user in the list twice.
# The server side should dedupe and handle this correctly (prior bug)
PersonaManager.edit(
persona=basic_user_persona,
description="Updated by admin",
description="Updated by admin 2",
users=[admin_user.id, admin_user.id],
user_performing_action=admin_user,
)
PersonaManager.verify(basic_user_persona, user_performing_action=admin_user)

View File

@@ -23,12 +23,12 @@ _Note:_ if you are having problems accessing the ^, try setting the `WEB_DOMAIN`
`http://127.0.0.1:3000` and accessing it there.
## Testing
This testing process will reset your application into a clean state.
This testing process will reset your application into a clean state.
Don't run these tests if you don't want to do this!
Bring up the entire application.
1. Reset the instance
```cd backend
@@ -59,4 +59,4 @@ may use this for local troubleshooting and testing.
```
cd web
npx chromatic --playwright --project-token={your token here}
```
```

View File

@@ -3,7 +3,13 @@
import React from "react";
import { Option } from "@/components/Dropdown";
import { generateRandomIconShape } from "@/lib/assistantIconUtils";
import { CCPairBasicInfo, DocumentSet, User, UserGroup } from "@/lib/types";
import {
CCPairBasicInfo,
DocumentSet,
User,
UserGroup,
UserRole,
} from "@/lib/types";
import { Separator } from "@/components/ui/separator";
import { Button } from "@/components/ui/button";
import { ArrayHelpers, FieldArray, Form, Formik, FormikProps } from "formik";
@@ -33,9 +39,8 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useRouter, useSearchParams } from "next/navigation";
import { useEffect, useMemo, useState } from "react";
import { FiInfo } from "react-icons/fi";
import * as Yup from "yup";
import CollapsibleSection from "./CollapsibleSection";
import { SuccessfulPersonaUpdateRedirectType } from "./enums";
@@ -71,11 +76,11 @@ import {
Option as DropdownOption,
} from "@/components/Dropdown";
import { SourceChip } from "@/app/chat/input/ChatInputBar";
import { TagIcon, UserIcon, XIcon } from "lucide-react";
import { TagIcon, UserIcon, XIcon, InfoIcon } from "lucide-react";
import { LLMSelector } from "@/components/llm/LLMSelector";
import useSWR from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
import Title from "@/components/ui/title";
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
@@ -127,6 +132,8 @@ export function AssistantEditor({
}) {
const { refreshAssistants, isImageGenerationAvailable } = useAssistants();
const router = useRouter();
const searchParams = useSearchParams();
const isAdminPage = searchParams.get("admin") === "true";
const { popup, setPopup } = usePopup();
const { labels, refreshLabels, createLabel, updateLabel, deleteLabel } =
@@ -216,6 +223,8 @@ export function AssistantEditor({
enabledToolsMap[tool.id] = personaCurrentToolIds.includes(tool.id);
});
const [showVisibilityWarning, setShowVisibilityWarning] = useState(false);
const initialValues = {
name: existingPersona?.name ?? "",
description: existingPersona?.description ?? "",
@@ -252,6 +261,7 @@ export function AssistantEditor({
(u) => u.id !== existingPersona.owner?.id
) ?? [],
selectedGroups: existingPersona?.groups ?? [],
is_default_persona: existingPersona?.is_default_persona ?? false,
};
interface AssistantPrompt {
@@ -308,24 +318,12 @@ export function AssistantEditor({
const [isRequestSuccessful, setIsRequestSuccessful] = useState(false);
const { data: userGroups } = useUserGroups();
// const { data: allUsers } = useUsers({ includeApiKeys: false }) as {
// data: MinimalUserSnapshot[] | undefined;
// };
const { data: users } = useSWR<MinimalUserSnapshot[]>(
"/api/users",
errorHandlingFetcher
);
const mapUsersToMinimalSnapshot = (users: any): MinimalUserSnapshot[] => {
if (!users || !Array.isArray(users.users)) return [];
return users.users.map((user: any) => ({
id: user.id,
name: user.name,
email: user.email,
}));
};
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
if (!labels) {
@@ -346,9 +344,7 @@ export function AssistantEditor({
if (response.ok) {
await refreshAssistants();
router.push(
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN
? `/admin/assistants?u=${Date.now()}`
: `/chat`
isAdminPage ? `/admin/assistants?u=${Date.now()}` : `/chat`
);
} else {
setPopup({
@@ -374,8 +370,9 @@ export function AssistantEditor({
<BackButton />
</div>
)}
{labelToDelete && (
<DeleteEntityModal
<ConfirmEntityModal
entityType="label"
entityName={labelToDelete.name}
onClose={() => setLabelToDelete(null)}
@@ -398,7 +395,7 @@ export function AssistantEditor({
/>
)}
{deleteModalOpen && existingPersona && (
<DeleteEntityModal
<ConfirmEntityModal
entityType="Persona"
entityName={existingPersona.name}
onClose={closeDeleteModal}
@@ -439,6 +436,7 @@ export function AssistantEditor({
label_ids: Yup.array().of(Yup.number()),
selectedUsers: Yup.array().of(Yup.object()),
selectedGroups: Yup.array().of(Yup.number()),
is_default_persona: Yup.boolean().required(),
})
.test(
"system-prompt-or-task-prompt",
@@ -459,6 +457,19 @@ export function AssistantEditor({
"Must provide either Instructions or Reminders (Advanced)",
});
}
)
.test(
"default-persona-public",
"Default persona must be public",
function (values) {
if (values.is_default_persona && !values.is_public) {
return this.createError({
path: "is_public",
message: "Default persona must be public",
});
}
return true;
}
)}
onSubmit={async (values, formikHelpers) => {
if (
@@ -499,7 +510,6 @@ export function AssistantEditor({
const submissionData: PersonaUpsertParameters = {
...values,
existing_prompt_id: existingPrompt?.id ?? null,
is_default_persona: admin!,
starter_messages: starterMessages,
groups: groups,
users: values.is_public
@@ -563,8 +573,9 @@ export function AssistantEditor({
}
await refreshAssistants();
router.push(
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN
isAdminPage
? `/admin/assistants?u=${Date.now()}`
: `/chat?assistantId=${assistantId}`
);
@@ -1005,6 +1016,22 @@ export function AssistantEditor({
{showAdvancedOptions && (
<>
<div className="max-w-4xl w-full">
{user?.role == UserRole.ADMIN && (
<BooleanFormField
onChange={(checked) => {
if (checked) {
setFieldValue("is_public", true);
setFieldValue("is_default_persona", true);
}
}}
name="is_default_persona"
label="Featured Assistant"
subtext="If set, this assistant will be pinned for all new users and appear in the Featured list in the assistant explorer. This also makes the assistant public."
/>
)}
<Separator />
<div className="flex gap-x-2 items-center ">
<div className="block font-medium text-sm">Access</div>
</div>
@@ -1014,22 +1041,60 @@ export function AssistantEditor({
<div className="min-h-[100px]">
<div className="flex items-center mb-2">
<SwitchField
name="is_public"
size="md"
onCheckedChange={(checked) => {
setFieldValue("is_public", checked);
if (checked) {
setFieldValue("selectedUsers", []);
setFieldValue("selectedGroups", []);
}
}}
/>
<TooltipProvider delayDuration={0}>
<Tooltip>
<TooltipTrigger asChild>
<div>
<SwitchField
name="is_public"
size="md"
onCheckedChange={(checked) => {
if (values.is_default_persona && !checked) {
setShowVisibilityWarning(true);
} else {
setFieldValue("is_public", checked);
if (!checked) {
// Even though this code path should not be possible,
// we set the default persona to false to be safe
setFieldValue(
"is_default_persona",
false
);
}
if (checked) {
setFieldValue("selectedUsers", []);
setFieldValue("selectedGroups", []);
}
}
}}
disabled={values.is_default_persona}
/>
</div>
</TooltipTrigger>
{values.is_default_persona && (
<TooltipContent side="top" align="center">
Default persona must be public. Set
&quot;Default Persona&quot; to false to change
visibility.
</TooltipContent>
)}
</Tooltip>
</TooltipProvider>
<span className="text-sm ml-2">
{values.is_public ? "Public" : "Private"}
</span>
</div>
{showVisibilityWarning && (
<div className="flex items-center text-warning mt-2">
<InfoIcon size={16} className="mr-2" />
<span className="text-sm">
Default persona must be public. Visibility has been
automatically set to public.
</span>
</div>
)}
{values.is_public ? (
<p className="text-sm text-text-dark">
Anyone from your organization can view and use this

View File

@@ -11,13 +11,14 @@ import { DraggableTable } from "@/components/table/DraggableTable";
import {
deletePersona,
personaComparator,
togglePersonaDefault,
togglePersonaVisibility,
} from "./lib";
import { FiEdit2 } from "react-icons/fi";
import { TrashIcon } from "@/components/icons/icons";
import { useUser } from "@/components/user/UserProvider";
import { useAssistants } from "@/components/context/AssistantsContext";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
if (persona.builtin_persona) {
@@ -56,6 +57,9 @@ export function PersonasTable() {
const [finalPersonas, setFinalPersonas] = useState<Persona[]>([]);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [personaToDelete, setPersonaToDelete] = useState<Persona | null>(null);
const [defaultModalOpen, setDefaultModalOpen] = useState(false);
const [personaToToggleDefault, setPersonaToToggleDefault] =
useState<Persona | null>(null);
useEffect(() => {
const editable = editablePersonas.sort(personaComparator);
@@ -126,11 +130,39 @@ export function PersonasTable() {
}
};
const openDefaultModal = (persona: Persona) => {
setPersonaToToggleDefault(persona);
setDefaultModalOpen(true);
};
const closeDefaultModal = () => {
setDefaultModalOpen(false);
setPersonaToToggleDefault(null);
};
const handleToggleDefault = async () => {
if (personaToToggleDefault) {
const response = await togglePersonaDefault(
personaToToggleDefault.id,
personaToToggleDefault.is_default_persona
);
if (response.ok) {
await refreshAssistants();
closeDefaultModal();
} else {
setPopup({
type: "error",
message: `Failed to update persona - ${await response.text()}`,
});
}
}
};
return (
<div>
{popup}
{deleteModalOpen && personaToDelete && (
<DeleteEntityModal
<ConfirmEntityModal
entityType="Persona"
entityName={personaToDelete.name}
onClose={closeDeleteModal}
@@ -138,8 +170,35 @@ export function PersonasTable() {
/>
)}
{defaultModalOpen && personaToToggleDefault && (
<ConfirmEntityModal
variant="action"
entityType="Assistant"
entityName={personaToToggleDefault.name}
onClose={closeDefaultModal}
onSubmit={handleToggleDefault}
actionButtonText={
personaToToggleDefault.is_default_persona
? "Remove Featured"
: "Set as Featured"
}
additionalDetails={
personaToToggleDefault.is_default_persona
? `Removing "${personaToToggleDefault.name}" as a featured assistant will not affect its visibility or accessibility.`
: `Setting "${personaToToggleDefault.name}" as a featured assistant will make it public and visible to all users. This action cannot be undone.`
}
/>
)}
<DraggableTable
headers={["Name", "Description", "Type", "Is Visible", "Delete"]}
headers={[
"Name",
"Description",
"Type",
"Featured Assistant",
"Is Visible",
"Delete",
]}
isAdmin={isAdmin}
rows={finalPersonas.map((persona) => {
const isEditable = editablePersonas.includes(persona);
@@ -152,7 +211,9 @@ export function PersonasTable() {
className="mr-1 my-auto cursor-pointer"
onClick={() =>
router.push(
`/admin/assistants/${persona.id}?u=${Date.now()}`
`/assistants/edit/${
persona.id
}?u=${Date.now()}&admin=true`
)
}
/>
@@ -168,6 +229,30 @@ export function PersonasTable() {
{persona.description}
</p>,
<PersonaTypeDisplay key={persona.id} persona={persona} />,
<div
key="is_default_persona"
onClick={() => {
if (isEditable) {
openDefaultModal(persona);
}
}}
className={`px-1 py-0.5 rounded flex ${
isEditable
? "hover:bg-accent-background-hovered cursor-pointer"
: ""
} select-none w-fit`}
>
<div className="my-auto flex-none w-22">
{!persona.is_default_persona ? (
<div className="text-error">Not Featured</div>
) : (
"Featured"
)}
</div>
<div className="ml-1 my-auto">
<CustomCheckbox checked={persona.is_default_persona} />
</div>
</div>,
<div
key="is_visible"
onClick={async () => {

View File

@@ -1,36 +0,0 @@
"use client";
import { Button } from "@/components/ui/button";
import { deletePersona } from "../lib";
import { useRouter } from "next/navigation";
import { SuccessfulPersonaUpdateRedirectType } from "../enums";
export function DeletePersonaButton({
personaId,
redirectType,
}: {
personaId: number;
redirectType: SuccessfulPersonaUpdateRedirectType;
}) {
const router = useRouter();
return (
<Button
variant="destructive"
onClick={async () => {
const response = await deletePersona(personaId);
if (response.ok) {
router.push(
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN
? `/admin/assistants?u=${Date.now()}`
: `/chat`
);
} else {
alert(`Failed to delete persona - ${await response.text()}`);
}
}}
>
Delete
</Button>
);
}

View File

@@ -1,43 +0,0 @@
import { ErrorCallout } from "@/components/ErrorCallout";
import { AssistantEditor } from "../AssistantEditor";
import { BackButton } from "@/components/BackButton";
import { DeletePersonaButton } from "./DeletePersonaButton";
import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
import { SuccessfulPersonaUpdateRedirectType } from "../enums";
import { RobotIcon } from "@/components/icons/icons";
import { AdminPageTitle } from "@/components/admin/Title";
import CardSection from "@/components/admin/CardSection";
import Title from "@/components/ui/title";
export default async function Page(props: { params: Promise<{ id: string }> }) {
const params = await props.params;
const [values, error] = await fetchAssistantEditorInfoSS(params.id);
let body;
if (!values) {
body = (
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
);
} else {
body = (
<>
<CardSection className="!border-none !bg-transparent !ring-none">
<AssistantEditor
{...values}
admin
defaultPublic={true}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>
</CardSection>
</>
);
}
return (
<div className="w-full">
<AdminPageTitle title="Edit Assistant" icon={<RobotIcon size={32} />} />
{body}
</div>
);
}

View File

@@ -261,6 +261,22 @@ export function personaComparator(a: Persona, b: Persona) {
return closerToZeroNegativesFirstComparator(a.id, b.id);
}
export const togglePersonaDefault = async (
personaId: number,
isDefault: boolean
) => {
const response = await fetch(`/api/admin/persona/${personaId}/default`, {
method: "PATCH",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
is_default_persona: !isDefault,
}),
});
return response;
};
export const togglePersonaVisibility = async (
personaId: number,
isVisible: boolean

View File

@@ -1,25 +0,0 @@
import { AssistantEditor } from "../AssistantEditor";
import { ErrorCallout } from "@/components/ErrorCallout";
import { fetchAssistantEditorInfoSS } from "@/lib/assistants/fetchPersonaEditorInfoSS";
import { SuccessfulPersonaUpdateRedirectType } from "../enums";
export default async function Page() {
const [values, error] = await fetchAssistantEditorInfoSS();
if (!values) {
return (
<ErrorCallout errorTitle="Something went wrong :(" errorMsg={error} />
);
} else {
return (
<div className="w-full">
<AssistantEditor
{...values}
admin
defaultPublic={true}
redirectType={SuccessfulPersonaUpdateRedirectType.ADMIN}
/>
</div>
);
}
}

View File

@@ -29,7 +29,7 @@ export default async function Page() {
<Separator />
<Title>Create an Assistant</Title>
<CreateButton href="/admin/assistants/new" text="New Assistant" />
<CreateButton href="/assistants/new?admin=true" text="New Assistant" />
<Separator />

View File

@@ -100,7 +100,7 @@ export function SlackChannelConfigsTable({
slackChannelConfig.persona
) ? (
<Link
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
href={`/assistants/${slackChannelConfig.persona.id}`}
className="text-primary hover:underline"
>
{slackChannelConfig.persona.name}

View File

@@ -502,9 +502,10 @@ export default function AddConnector({
{oauthSupportedSources.includes(connector) &&
(NEXT_PUBLIC_CLOUD_ENABLED ||
NEXT_PUBLIC_TEST_ENV) && (
<button
<Button
variant="navigate"
onClick={handleAuthorize}
className="mt-6 text-sm bg-blue-500 px-2 py-1.5 flex text-text-200 flex-none rounded"
className="mt-6 "
disabled={isAuthorizing}
hidden={!isAuthorizeVisible}
>
@@ -513,7 +514,7 @@ export default function AddConnector({
: `Authorize with ${getSourceDisplayName(
connector
)}`}
</button>
</Button>
)}
</div>
)}

View File

@@ -5,7 +5,7 @@ import useSWR, { mutate } from "swr";
import { FetchError, errorHandlingFetcher } from "@/lib/fetcher";
import { ErrorCallout } from "@/components/ErrorCallout";
import { LoadingAnimation } from "@/components/Loading";
import { usePopup } from "@/components/admin/connectors/Popup";
import { PopupSpec, usePopup } from "@/components/admin/connectors/Popup";
import { ValidSources } from "@/lib/types";
import { usePublicCredentials } from "@/lib/hooks";
import Title from "@/components/ui/title";
@@ -32,7 +32,11 @@ const useConnectorsByCredentialId = (credential_id: number | null) => {
};
};
const GDriveMain = ({}: {}) => {
const GDriveMain = ({
setPopup,
}: {
setPopup: (popup: PopupSpec | null) => void;
}) => {
const { isAdmin, user } = useUser();
// tries getting the uploaded credential json
@@ -97,8 +101,6 @@ const GDriveMain = ({}: {}) => {
refreshConnectorsByCredentialId,
} = useConnectorsByCredentialId(credential_id);
const { popup, setPopup } = usePopup();
const appCredentialSuccessfullyFetched =
appCredentialData ||
(isAppCredentialError && isAppCredentialError.status === 404);
@@ -173,10 +175,7 @@ const GDriveMain = ({}: {}) => {
return (
<>
{popup}
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 1: Provide your Credentials
</Title>
<Title className="mb-2 mt-6">Step 1: Provide your Credentials</Title>
<DriveJsonUploadSection
setPopup={setPopup}
appCredentialData={appCredentialData}
@@ -186,9 +185,7 @@ const GDriveMain = ({}: {}) => {
{isAdmin && (
<>
<Title className="mb-2 mt-6 ml-auto mr-auto">
Step 2: Authenticate with Onyx
</Title>
<Title className="mb-2 mt-6">Step 2: Authenticate with Onyx</Title>
<DriveAuthSection
setPopup={setPopup}
refreshCredentials={refreshCredentials}

View File

@@ -188,7 +188,7 @@ export const DocumentSetCreationForm = ({
flex
cursor-pointer ` +
(isSelected
? " bg-background-strong"
? " bg-background-200"
: " hover:bg-accent-background-hovered")
}
onClick={() => {
@@ -304,7 +304,7 @@ export const DocumentSetCreationForm = ({
flex
cursor-pointer ` +
(isSelected
? " bg-background-strong"
? " bg-background-200"
: " hover:bg-accent-background-hovered")
}
onClick={() => {

View File

@@ -235,8 +235,8 @@ export function EmbeddingModelSelection({
onClick={() => setModelTab(null)}
className={`mr-4 p-2 font-bold ${
!modelTab
? "rounded bg-background-900 dark:bg-neutral-900 text-text-100 dark:text-neutral-100 underline"
: " hover:underline bg-background-100 dark:bg-neutral-700"
? "rounded bg-neutral-900 dark:bg-neutral-950 text-neutral-100 dark:text-neutral-300 underline"
: " hover:underline bg-neutral-100 dark:bg-neutral-900"
}`}
>
Current
@@ -246,8 +246,8 @@ export function EmbeddingModelSelection({
onClick={() => setModelTab("cloud")}
className={`mx-2 p-2 font-bold ${
modelTab == "cloud"
? "rounded bg-background-900 dark:bg-neutral-900 text-text-100 dark:text-neutral-100 underline"
: " hover:underline bg-background-100 dark:bg-neutral-700"
? "rounded bg-neutral-900 dark:bg-neutral-950 text-neutral-100 dark:text-neutral-300 underline"
: " hover:underline bg-neutral-100 dark:bg-neutral-900"
}`}
>
Cloud-based
@@ -258,8 +258,8 @@ export function EmbeddingModelSelection({
onClick={() => setModelTab("open")}
className={` mx-2 p-2 font-bold ${
modelTab == "open"
? "rounded bg-background-900 dark:bg-neutral-900 text-text-100 dark:text-neutral-100 underline"
: "hover:underline bg-background-100 dark:bg-neutral-700"
? "rounded bg-neutral-900 dark:bg-neutral-950 text-neutral-100 dark:text-neutral-300 underline"
: "hover:underline bg-neutral-100 dark:bg-neutral-900"
}`}
>
Self-hosted

View File

@@ -116,8 +116,8 @@ const RerankingDetailsForm = forwardRef<
onClick={() => setModelTab("cloud")}
className={`mr-2 p-2 font-bold ${
modelTab == "cloud"
? "rounded bg-background-900 text-text-100 underline"
: " hover:underline bg-background-100"
? "rounded bg-neutral-900 dark:bg-neutral-950 text-neutral-100 dark:text-neutral-300 underline"
: " hover:underline bg-neutral-100 dark:bg-neutral-900"
}`}
>
Cloud-based
@@ -129,8 +129,8 @@ const RerankingDetailsForm = forwardRef<
onClick={() => setModelTab("open")}
className={` mx-2 p-2 font-bold ${
modelTab == "open"
? "rounded bg-background-900 text-text-100 underline"
: "hover:underline bg-background-100"
? "rounded bg-neutral-900 dark:bg-neutral-950 text-neutral-100 dark:text-neutral-300 underline"
: "hover:underline bg-neutral-100 dark:bg-neutral-900"
}`}
>
Self-hosted
@@ -140,7 +140,7 @@ const RerankingDetailsForm = forwardRef<
<div className="px-2">
<button
onClick={() => resetRerankingValues()}
className="mx-2 p-2 font-bold rounded bg-background-100 text-text-900 hover:underline"
className={`mx-2 p-2 font-bold rounded bg-neutral-100 dark:bg-neutral-900 text-neutral-900 dark:text-neutral-100 hover:underline`}
>
Remove Reranking
</button>
@@ -177,7 +177,7 @@ const RerankingDetailsForm = forwardRef<
key={`${card.rerank_provider_type}-${card.modelName}`}
className={`p-4 border rounded-lg cursor-pointer transition-all duration-200 ${
isSelected
? "border-blue-500 bg-blue-50 dark:bg-blue-900 dark:border-blue-700 shadow-md"
? "border-blue-800 bg-blue-50 dark:bg-blue-950 dark:border-blue-700 shadow-md"
: "border-background-200 hover:border-blue-300 hover:shadow-sm dark:border-neutral-700 dark:hover:border-blue-300"
}`}
onClick={() => {

View File

@@ -19,7 +19,7 @@ import { Dispatch, SetStateAction, useEffect, useState } from "react";
import { CustomEmbeddingModelForm } from "@/components/embedding/CustomEmbeddingModelForm";
import { deleteSearchSettings } from "./utils";
import { usePopup } from "@/components/admin/connectors/Popup";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
import { AdvancedSearchConfiguration } from "../interfaces";
import CardSection from "@/components/admin/CardSection";
@@ -322,7 +322,7 @@ export default function CloudEmbeddingPage({
OpenAI for embeddings.
</Text>
<div className="flex items-center text-sm text-text-700">
<FiInfo className="text-text-400 mr-2" size={16} />
<FiInfo className="text-neutral-400 mr-2" size={16} />
<Text>
You&apos;ll need: API version, base URL, API key, model
name, and deployment name.
@@ -456,7 +456,7 @@ export function CloudModelCard({
>
{popup}
{showDeleteModel && (
<DeleteEntityModal
<ConfirmEntityModal
entityName={model.model_name}
entityType="embedding model configuration"
onSubmit={() => deleteModel()}

View File

@@ -33,7 +33,7 @@ export default function SidebarWrapper<T extends object>({
size = "sm",
children,
}: SidebarWrapperProps<T>) {
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
const [sidebarVisible, setSidebarVisible] = useState(initiallyToggled);
const [showDocSidebar, setShowDocSidebar] = useState(false); // State to track if sidebar is open
// Used to maintain a "time out" for history sidebar so our existing refs can have time to process change
const [untoggled, setUntoggled] = useState(false);
@@ -41,13 +41,13 @@ export default function SidebarWrapper<T extends object>({
const toggleSidebar = useCallback(() => {
Cookies.set(
SIDEBAR_TOGGLED_COOKIE_NAME,
String(!toggledSidebar).toLocaleLowerCase()
String(!sidebarVisible).toLocaleLowerCase()
),
{
path: "/",
};
setToggledSidebar((toggledSidebar) => !toggledSidebar);
}, [toggledSidebar]);
setSidebarVisible((sidebarVisible) => !sidebarVisible);
}, [sidebarVisible]);
const sidebarElementRef = useRef<HTMLDivElement>(null);
const { folders, openedFolders, chatSessions } = useChatContext();
@@ -63,7 +63,7 @@ export default function SidebarWrapper<T extends object>({
const settings = useContext(SettingsContext);
useSidebarVisibility({
toggledSidebar,
sidebarVisible,
sidebarElementRef,
showDocSidebar,
setShowDocSidebar,
@@ -94,7 +94,7 @@ export default function SidebarWrapper<T extends object>({
duration-300
ease-in-out
${
!untoggled && (showDocSidebar || toggledSidebar)
!untoggled && (showDocSidebar || sidebarVisible)
? "opacity-100 w-[250px] translate-x-0"
: "opacity-0 w-[200px] pointer-events-none -translate-x-10"
}`}
@@ -107,7 +107,7 @@ export default function SidebarWrapper<T extends object>({
explicitlyUntoggle={explicitlyUntoggle}
ref={sidebarElementRef}
toggleSidebar={toggleSidebar}
toggled={toggledSidebar}
toggled={sidebarVisible}
existingChats={chatSessions}
currentChatSession={null}
folders={folders}
@@ -117,7 +117,7 @@ export default function SidebarWrapper<T extends object>({
<div className="absolute px-2 left-0 w-full top-0">
<FunctionalHeader
sidebarToggled={toggledSidebar}
sidebarToggled={sidebarVisible}
toggleSidebar={toggleSidebar}
page="chat"
/>
@@ -132,7 +132,7 @@ export default function SidebarWrapper<T extends object>({
bg-opacity-80
duration-300
ease-in-out
${toggledSidebar ? "w-[250px]" : "w-[0px]"}`}
${sidebarVisible ? "w-[250px]" : "w-[0px]"}`}
/>
<div
@@ -144,7 +144,7 @@ export default function SidebarWrapper<T extends object>({
</div>
</div>
</div>
<FixedLogo backgroundToggled={toggledSidebar || showDocSidebar} />
<FixedLogo backgroundToggled={sidebarVisible || showDocSidebar} />
</div>
);
}

View File

@@ -1,4 +1,4 @@
import React, { useContext, useState, useRef, useLayoutEffect } from "react";
import React, { useState, useRef, useLayoutEffect } from "react";
import { useRouter } from "next/navigation";
import {
FiMoreHorizontal,
@@ -8,7 +8,7 @@ import {
FiLock,
FiUnlock,
} from "react-icons/fi";
import { FaHashtag } from "react-icons/fa";
import {
Popover,
PopoverTrigger,
@@ -26,14 +26,12 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { PinnedIcon } from "@/components/icons/icons";
import {
deletePersona,
togglePersonaPublicStatus,
} from "@/app/admin/assistants/lib";
import { deletePersona } from "@/app/admin/assistants/lib";
import { PencilIcon } from "lucide-react";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { usePaidEnterpriseFeaturesEnabled } from "@/components/settings/usePaidEnterpriseFeaturesEnabled";
import { truncateString } from "@/lib/utils";
import { usePopup } from "@/components/admin/connectors/Popup";
import { Button } from "@/components/ui/button";
export const AssistantBadge = ({
text,
@@ -63,6 +61,7 @@ const AssistantCard: React.FC<{
const { user, toggleAssistantPinnedStatus } = useUser();
const router = useRouter();
const { refreshAssistants, pinnedAssistants } = useAssistants();
const { popup, setPopup } = usePopup();
const isOwnedByUser = checkUserOwnsAssistant(user, persona);
@@ -72,7 +71,34 @@ const AssistantCard: React.FC<{
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
const handleDelete = () => setActivePopover("delete");
const [isDeleteConfirmation, setIsDeleteConfirmation] = useState(false);
const handleDelete = () => {
setIsDeleteConfirmation(true);
};
const confirmDelete = async () => {
const response = await deletePersona(persona.id);
if (response.ok) {
await refreshAssistants();
setActivePopover(null);
setIsDeleteConfirmation(false);
setPopup({
message: `${persona.name} has been successfully deleted.`,
type: "success",
});
} else {
setPopup({
message: `Failed to delete assistant - ${await response.text()}`,
type: "error",
});
}
};
const cancelDelete = () => {
setIsDeleteConfirmation(false);
};
const handleEdit = () => {
router.push(`/assistants/edit/${persona.id}`);
setActivePopover(null);
@@ -100,6 +126,7 @@ const AssistantCard: React.FC<{
return (
<div className="w-full text-text-800 p-2 overflow-visible pb-4 pt-3 bg-transparent dark:bg-neutral-800/80 rounded shadow-[0px_0px_4px_0px_rgba(0,0,0,0.25)] flex flex-col">
{popup}
<div className="w-full flex">
<div className="ml-2 flex-none mr-2 mt-1 w-10 h-10">
<AssistantIcon assistant={persona} size="large" />
@@ -157,55 +184,84 @@ const AssistantCard: React.FC<{
<FiMoreHorizontal size={16} />
</button>
</PopoverTrigger>
<PopoverContent className={`w-32 z-[10000] p-2`}>
<div className="flex flex-col text-sm space-y-1">
<button
onClick={isOwnedByUser ? handleEdit : undefined}
className={`w-full flex items-center text-left px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral-700"
: "opacity-50 cursor-not-allowed"
}`}
disabled={!isOwnedByUser}
>
<FiEdit size={12} className="inline mr-2" />
Edit
</button>
{isPaidEnterpriseFeaturesEnabled && isOwnedByUser && (
<PopoverContent
className={`${
isDeleteConfirmation ? "w-64" : "w-32"
} z-[10000] p-2`}
>
{!isDeleteConfirmation ? (
<div className="flex flex-col text-sm space-y-1">
<button
onClick={
onClick={isOwnedByUser ? handleEdit : undefined}
className={`w-full flex items-center text-left px-2 py-1 rounded ${
isOwnedByUser
? () => {
router.push(
`/assistants/stats/${persona.id}`
);
closePopover();
}
: undefined
}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral-800"
? "hover:bg-neutral-200 dark:hover:bg-neutral-700"
: "opacity-50 cursor-not-allowed"
}`}
disabled={!isOwnedByUser}
>
<FiBarChart size={12} className="inline mr-2" />
Stats
<FiEdit size={12} className="inline mr-2" />
Edit
</button>
)}
<button
onClick={isOwnedByUser ? handleDelete : undefined}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral- text-red-600 dark:text-red-400"
: "opacity-50 cursor-not-allowed text-red-300 dark:text-red-500"
}`}
disabled={!isOwnedByUser}
>
<FiTrash size={12} className="inline mr-2" />
Delete
</button>
</div>
{isPaidEnterpriseFeaturesEnabled && isOwnedByUser && (
<button
onClick={
isOwnedByUser
? () => {
router.push(
`/assistants/stats/${persona.id}`
);
closePopover();
}
: undefined
}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral-800"
: "opacity-50 cursor-not-allowed"
}`}
>
<FiBarChart size={12} className="inline mr-2" />
Stats
</button>
)}
<button
onClick={isOwnedByUser ? handleDelete : undefined}
className={`w-full text-left items-center px-2 py-1 rounded ${
isOwnedByUser
? "hover:bg-neutral-200 dark:hover:bg-neutral- text-red-600 dark:text-red-400"
: "opacity-50 cursor-not-allowed text-red-300 dark:text-red-500"
}`}
disabled={!isOwnedByUser}
>
<FiTrash size={12} className="inline mr-2" />
Delete
</button>
</div>
) : (
<div className="w-full">
<p className="text-sm mb-3">
Are you sure you want to delete assistant{" "}
<b>{persona.name}</b>?
</p>
<div className="flex justify-center gap-2">
<Button
variant="secondary"
size="sm"
onClick={cancelDelete}
>
Cancel
</Button>
<Button
variant="destructive"
size="sm"
onClick={confirmDelete}
>
Delete
</Button>
</div>
</div>
)}
</PopoverContent>
</Popover>
</div>

View File

@@ -5,10 +5,8 @@ import { useRouter } from "next/navigation";
import AssistantCard from "./AssistantCard";
import { useAssistants } from "@/components/context/AssistantsContext";
import { useUser } from "@/components/user/UserProvider";
import { FilterIcon } from "lucide-react";
import { FilterIcon, XIcon } from "lucide-react";
import { checkUserOwnsAssistant } from "@/lib/assistants/checkOwnership";
import { Dialog, DialogContent } from "@/components/ui/dialog";
import { Modal } from "@/components/Modal";
export const AssistantBadgeSelector = ({
text,
@@ -24,8 +22,8 @@ export const AssistantBadgeSelector = ({
className={`
select-none ${
selected
? "bg-neutral-900 text-white"
: "bg-transparent text-neutral-900"
? "bg-background-900 text-white"
: "bg-transparent text-text-900"
} w-12 h-5 text-center px-1 py-0.5 rounded-lg cursor-pointer text-[12px] font-normal leading-[10px] border border-black justify-center items-center gap-1 inline-flex`}
onClick={toggleFilter}
>
@@ -109,16 +107,20 @@ export function AssistantModal({
const featuredAssistants = [
...memoizedCurrentlyVisibleAssistants.filter(
(assistant) => assistant.builtin_persona || assistant.is_default_persona
(assistant) => assistant.is_default_persona
),
];
const allAssistants = memoizedCurrentlyVisibleAssistants.filter(
(assistant) => !assistant.builtin_persona && !assistant.is_default_persona
(assistant) => !assistant.is_default_persona
);
return (
<div className="fixed inset-0 bg-neutral-950/80 bg-opacity-50 flex items-center justify-center z-50">
<div
onClick={hideModal}
className="fixed inset-0 bg-neutral-950/80 bg-opacity-50 flex items-center justify-center z-50"
>
<div
onClick={(e) => e.stopPropagation()}
className="p-0 max-w-4xl overflow-hidden max-h-[80vh] w-[95%] bg-background rounded-md shadow-2xl transform transition-all duration-300 ease-in-out relative w-11/12 max-w-4xl pt-10 pb-10 px-10 overflow-hidden flex flex-col"
style={{
position: "fixed",
@@ -128,129 +130,142 @@ export function AssistantModal({
margin: 0,
}}
>
<div className="absolute top-2 right-2">
<button
onClick={hideModal}
className="cursor-pointer text-neutral-500 hover:text-neutral-700 dark:text-neutral-400 dark:hover:text-neutral-300 transition-colors duration-200 p-2"
aria-label="Close modal"
>
<XIcon className="w-5 h-5" />
</button>
</div>
<div className="flex overflow-hidden flex-col h-full">
<div className="flex flex-col sticky top-0 z-10">
<div className="flex px-2 justify-between items-center gap-x-2 mb-0">
<div className="h-12 w-full rounded-lg flex-col justify-center items-start gap-2.5 inline-flex">
<div className="h-12 rounded-md w-full shadow-[0px_0px_2px_0px_rgba(0,0,0,0.25)] border border-[#dcdad4] flex items-center px-3">
{!isSearchFocused && (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-gray-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
)}
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
type="text"
className="w-full h-full bg-transparent outline-none text-black"
/>
</div>
</div>
<button
onClick={() => router.push("/assistants/new")}
className="h-10 cursor-pointer px-6 py-3 bg-black rounded-md border border-black justify-center items-center gap-2.5 inline-flex"
>
<div className="text-[#fffcf4] text-lg font-normal leading-normal">
Create
</div>
</button>
</div>
<div className="px-2 flex py-4 items-center gap-x-2 flex-wrap">
<FilterIcon size={16} />
<AssistantBadgeSelector
text="Pinned"
selected={assistantFilters[AssistantFilter.Pinned]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Pinned)
}
/>
<AssistantBadgeSelector
text="Mine"
selected={assistantFilters[AssistantFilter.Mine]}
toggleFilter={() => toggleAssistantFilter(AssistantFilter.Mine)}
/>
<AssistantBadgeSelector
text="Private"
selected={assistantFilters[AssistantFilter.Private]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Private)
}
/>
<AssistantBadgeSelector
text="Public"
selected={assistantFilters[AssistantFilter.Public]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Public)
}
/>
</div>
<div className="w-full border-t border-neutral-200" />
</div>
<div className="flex-grow overflow-y-auto">
<h2 className="text-2xl font-semibold text-gray-800 mb-2 px-4 py-2">
Featured Assistants
</h2>
<div className="w-full px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{featuredAssistants.length > 0 ? (
featuredAssistants.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={pinnedAssistants
.map((a) => a.id)
.includes(assistant.id)}
persona={assistant}
closeModal={hideModal}
<div className="flex overflow-hidden flex-col h-full">
<div className="flex flex-col sticky top-0 z-10">
<div className="flex px-2 justify-between items-center gap-x-2 mb-0">
<div className="h-12 w-full rounded-lg flex-col justify-center items-start gap-2.5 inline-flex">
<div className="h-12 rounded-md w-full shadow-[0px_0px_2px_0px_rgba(0,0,0,0.25)] border border-background-300 flex items-center px-3">
{!isSearchFocused && (
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 text-text-400"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth={2}
d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z"
/>
</svg>
)}
<input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
onFocus={() => setIsSearchFocused(true)}
onBlur={() => setIsSearchFocused(false)}
type="text"
className="w-full h-full bg-transparent outline-none text-black"
/>
</div>
))
) : (
<div className="col-span-2 text-center text-gray-500">
No featured assistants match filters
</div>
)}
<button
onClick={() => router.push("/assistants/new")}
className="h-10 cursor-pointer px-6 py-3 bg-background-800 hover:bg-black rounded-md border border-black justify-center items-center gap-2.5 inline-flex"
>
<div className="text-text-50 text-lg font-normal leading-normal">
Create
</div>
</button>
</div>
<div className="px-2 flex py-4 items-center gap-x-2 flex-wrap">
<FilterIcon className="text-text-800" size={16} />
<AssistantBadgeSelector
text="Pinned"
selected={assistantFilters[AssistantFilter.Pinned]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Pinned)
}
/>
<AssistantBadgeSelector
text="Mine"
selected={assistantFilters[AssistantFilter.Mine]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Mine)
}
/>
<AssistantBadgeSelector
text="Private"
selected={assistantFilters[AssistantFilter.Private]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Private)
}
/>
<AssistantBadgeSelector
text="Public"
selected={assistantFilters[AssistantFilter.Public]}
toggleFilter={() =>
toggleAssistantFilter(AssistantFilter.Public)
}
/>
</div>
<div className="w-full border-t border-background-200" />
</div>
{allAssistants && allAssistants.length > 0 && (
<>
<h2 className="text-2xl font-semibold text-gray-800 mt-4 mb-2 px-4 py-2">
All Assistants
</h2>
<div className="flex-grow overflow-y-auto">
<h2 className="text-2xl font-semibold text-text-800 mb-2 px-4 py-2">
Featured Assistants
</h2>
<div className="w-full mt-2 px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{allAssistants
.sort((a, b) => b.id - a.id)
.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={
user?.preferences?.pinned_assistants?.includes(
assistant.id
) ?? false
}
persona={assistant}
closeModal={hideModal}
/>
</div>
))}
</div>
</>
)}
<div className="w-full px-2 pb-10 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{featuredAssistants.length > 0 ? (
featuredAssistants.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={pinnedAssistants
.map((a) => a.id)
.includes(assistant.id)}
persona={assistant}
closeModal={hideModal}
/>
</div>
))
) : (
<div className="col-span-2 text-center text-text-500">
No featured assistants match filters
</div>
)}
</div>
{allAssistants && allAssistants.length > 0 && (
<>
<h2 className="text-2xl font-semibold text-text-800 mt-4 mb-2 px-4 py-2">
All Assistants
</h2>
<div className="w-full mt-2 px-2 pb-2 grid grid-cols-1 md:grid-cols-2 gap-x-6 gap-y-6">
{allAssistants
.sort((a, b) => b.id - a.id)
.map((assistant, index) => (
<div key={index}>
<AssistantCard
pinned={
user?.preferences?.pinned_assistants?.includes(
assistant.id
) ?? false
}
persona={assistant}
closeModal={hideModal}
/>
</div>
))}
</div>
</>
)}
</div>
</div>
</div>
</div>

View File

@@ -97,7 +97,6 @@ import {
} from "@/components/resizable/constants";
import FixedLogo from "../../components/logo/FixedLogo";
import { DeleteEntityModal } from "../../components/modals/DeleteEntityModal";
import { MinimalMarkdown } from "@/components/chat/MinimalMarkdown";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
@@ -130,6 +129,7 @@ import {
useSidebarShortcut,
} from "@/lib/browserUtilities";
import { Button } from "@/components/ui/button";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
const TEMP_USER_MESSAGE_ID = -1;
const TEMP_ASSISTANT_MESSAGE_ID = -2;
@@ -138,12 +138,12 @@ const SYSTEM_MESSAGE_ID = -3;
export function ChatPage({
toggle,
documentSidebarInitialWidth,
toggledSidebar,
sidebarVisible,
firstMessage,
}: {
toggle: (toggled?: boolean) => void;
documentSidebarInitialWidth?: number;
toggledSidebar: boolean;
sidebarVisible: boolean;
firstMessage?: string;
}) {
const router = useRouter();
@@ -204,7 +204,7 @@ export function ChatPage({
const settings = useContext(SettingsContext);
const enterpriseSettings = settings?.enterpriseSettings;
const [documentSidebarToggled, setDocumentSidebarToggled] = useState(false);
const [documentSidebarVisible, setDocumentSidebarVisible] = useState(false);
const [proSearchEnabled, setProSearchEnabled] = useState(proSearchToggled);
const [streamingAllowed, setStreamingAllowed] = useState(false);
const toggleProSearch = () => {
@@ -243,7 +243,7 @@ export function ChatPage({
if (user?.is_anonymous_user) {
Cookies.set(
SIDEBAR_TOGGLED_COOKIE_NAME,
String(!toggledSidebar).toLocaleLowerCase()
String(!sidebarVisible).toLocaleLowerCase()
);
toggle(false);
}
@@ -1024,10 +1024,10 @@ export function ChatPage({
if (
(!personaIncludesRetrieval &&
(!selectedDocuments || selectedDocuments.length === 0) &&
documentSidebarToggled) ||
documentSidebarVisible) ||
chatSessionIdRef.current == undefined
) {
setDocumentSidebarToggled(false);
setDocumentSidebarVisible(false);
}
clientScrollToBottom();
}, [chatSessionIdRef.current]);
@@ -1122,6 +1122,7 @@ export function ChatPage({
"Continue Generating (pick up exactly where you left off)",
});
};
const [gener, setFinishedStreaming] = useState(false);
const onSubmit = async ({
messageIdToResend,
@@ -1272,6 +1273,7 @@ export function ChatPage({
let finalMessage: BackendMessage | null = null;
let toolCall: ToolCallMetadata | null = null;
let isImprovement: boolean | undefined = undefined;
let isStreamingQuestions = true;
let initialFetchDetails: null | {
user_message_id: number;
@@ -1442,11 +1444,22 @@ export function ChatPage({
Object.hasOwn(packet, "stop_reason") &&
Object.hasOwn(packet, "level_question_num")
) {
if ((packet as StreamStopInfo).stream_type == "main_answer") {
setFinishedStreaming(true);
updateChatState("streaming", frozenSessionId);
}
if (
(packet as StreamStopInfo).stream_type == "sub_questions" &&
(packet as StreamStopInfo).level_question_num == undefined
) {
isStreamingQuestions = false;
}
sub_questions = constructSubQuestions(
sub_questions,
packet as StreamStopInfo
);
} else if (Object.hasOwn(packet, "sub_question")) {
updateChatState("toolBuilding", frozenSessionId);
is_generating = true;
sub_questions = constructSubQuestions(
sub_questions,
@@ -1606,6 +1619,7 @@ export function ChatPage({
latestChildMessageId: initialFetchDetails.assistant_message_id,
},
{
isStreamingQuestions: isStreamingQuestions,
is_generating: is_generating,
isImprovement: isImprovement,
messageId: initialFetchDetails.assistant_message_id!,
@@ -1613,7 +1627,7 @@ export function ChatPage({
second_level_message: second_level_answer,
type: error ? "error" : "assistant",
retrievalType,
query: finalMessage?.rephrased_query,
query: finalMessage?.rephrased_query || query,
documents: documents,
citations: finalMessage?.citations || {},
files: finalMessage?.files || aiMessageImages || [],
@@ -1805,7 +1819,7 @@ export function ChatPage({
}
Cookies.set(
SIDEBAR_TOGGLED_COOKIE_NAME,
String(!toggledSidebar).toLocaleLowerCase()
String(!sidebarVisible).toLocaleLowerCase()
),
{
path: "/",
@@ -1822,7 +1836,7 @@ export function ChatPage({
const sidebarElementRef = useRef<HTMLDivElement>(null);
useSidebarVisibility({
toggledSidebar,
sidebarVisible,
sidebarElementRef,
showDocSidebar: showHistorySidebar,
setShowDocSidebar: setShowHistorySidebar,
@@ -2003,7 +2017,7 @@ export function ChatPage({
useEffect(() => {
if (!retrievalEnabled) {
setDocumentSidebarToggled(false);
setDocumentSidebarVisible(false);
}
}, [retrievalEnabled]);
@@ -2068,10 +2082,10 @@ export function ChatPage({
const [showAssistantsModal, setShowAssistantsModal] = useState(false);
const toggleDocumentSidebar = () => {
if (!documentSidebarToggled) {
setDocumentSidebarToggled(true);
if (!documentSidebarVisible) {
setDocumentSidebarVisible(true);
} else {
setDocumentSidebarToggled(false);
setDocumentSidebarVisible(false);
}
};
@@ -2122,7 +2136,7 @@ export function ChatPage({
<ChatPopup />
{showDeleteAllModal && (
<DeleteEntityModal
<ConfirmEntityModal
entityType="All Chats"
entityName="all your chat sessions"
onClose={() => setShowDeleteAllModal(false)}
@@ -2178,11 +2192,11 @@ export function ChatPage({
/>
)}
{retrievalEnabled && documentSidebarToggled && settings?.isMobile && (
{retrievalEnabled && documentSidebarVisible && settings?.isMobile && (
<div className="md:hidden">
<Modal
hideDividerForTitle
onOutsideClick={() => setDocumentSidebarToggled(false)}
onOutsideClick={() => setDocumentSidebarVisible(false)}
title="Sources"
>
<DocumentResults
@@ -2198,7 +2212,7 @@ export function ChatPage({
modal={true}
ref={innerSidebarElementRef}
closeSidebar={() => {
setDocumentSidebarToggled(false);
setDocumentSidebarVisible(false);
}}
selectedMessage={aiMessage}
selectedDocuments={selectedDocuments}
@@ -2277,22 +2291,24 @@ export function ChatPage({
bg-opacity-80
duration-300
ease-in-out
${
!untoggled && (showHistorySidebar || toggledSidebar)
!untoggled && (showHistorySidebar || sidebarVisible)
? "opacity-100 w-[250px] translate-x-0"
: "opacity-0 w-[250px] pointer-events-none -translate-x-10"
}`}
>
<div className="w-full relative">
<HistorySidebar
liveAssistant={liveAssistant}
setShowAssistantsModal={setShowAssistantsModal}
explicitlyUntoggle={explicitlyUntoggle}
reset={() => setMessage("")}
page="chat"
ref={innerSidebarElementRef}
toggleSidebar={toggleSidebar}
toggled={toggledSidebar}
currentAssistantId={liveAssistant?.id}
toggled={sidebarVisible}
existingChats={chatSessions}
currentChatSession={selectedChatSession}
folders={folders}
@@ -2314,7 +2330,7 @@ export function ChatPage({
duration-300
ease-in-out
${
documentSidebarToggled &&
documentSidebarVisible &&
!settings?.isMobile &&
"opacity-100 w-[350px]"
}`}
@@ -2339,7 +2355,7 @@ export function ChatPage({
ease-in-out
h-full
${
documentSidebarToggled && !settings?.isMobile
documentSidebarVisible && !settings?.isMobile
? "w-[400px]"
: "w-[0px]"
}
@@ -2358,7 +2374,7 @@ export function ChatPage({
modal={false}
ref={innerSidebarElementRef}
closeSidebar={() =>
setTimeout(() => setDocumentSidebarToggled(false), 300)
setTimeout(() => setDocumentSidebarVisible(false), 300)
}
selectedMessage={aiMessage}
selectedDocuments={selectedDocuments}
@@ -2367,12 +2383,12 @@ export function ChatPage({
selectedDocumentTokens={selectedDocumentTokens}
maxTokens={maxTokens}
initialWidth={400}
isOpen={documentSidebarToggled && !settings?.isMobile}
isOpen={documentSidebarVisible && !settings?.isMobile}
/>
</div>
<BlurBackground
visible={!untoggled && (showHistorySidebar || toggledSidebar)}
visible={!untoggled && (showHistorySidebar || sidebarVisible)}
onClick={() => toggleSidebar()}
/>
@@ -2387,7 +2403,7 @@ export function ChatPage({
{liveAssistant && (
<FunctionalHeader
toggleUserSettings={() => setUserSettingsToggled(true)}
sidebarToggled={toggledSidebar}
sidebarToggled={sidebarVisible}
reset={() => setMessage("")}
page="chat"
setSharingModalVisible={
@@ -2395,8 +2411,8 @@ export function ChatPage({
? setSharingModalVisible
: undefined
}
documentSidebarToggled={
documentSidebarToggled && !settings?.isMobile
documentSidebarVisible={
documentSidebarVisible && !settings?.isMobile
}
toggleSidebar={toggleSidebar}
currentChatSession={selectedChatSession}
@@ -2424,7 +2440,7 @@ export function ChatPage({
duration-300
ease-in-out
h-full
${toggledSidebar ? "w-[200px]" : "w-[0px]"}
${sidebarVisible ? "w-[200px]" : "w-[0px]"}
`}
></div>
)}
@@ -2450,7 +2466,7 @@ export function ChatPage({
duration-300
ease-in-out
h-full
${toggledSidebar ? "w-[200px]" : "w-[0px]"}
${sidebarVisible ? "w-[200px]" : "w-[0px]"}
`}
></div>
)}
@@ -2635,8 +2651,14 @@ export function ChatPage({
{message.sub_questions &&
message.sub_questions.length > 0 ? (
<AgenticMessage
isStreamingQuestions={
message.isStreamingQuestions ?? false
}
isGenerating={
message.is_generating ?? false
}
docSidebarToggled={
documentSidebarToggled &&
documentSidebarVisible &&
(selectedMessageForDocDisplay ==
message.messageId ||
selectedMessageForDocDisplay ==
@@ -2732,7 +2754,8 @@ export function ChatPage({
setMessageAsLatest(messageId);
}}
isActive={
messageHistory.length - 1 == i
messageHistory.length - 1 == i ||
messageHistory.length - 2 == i
}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={(
@@ -2740,8 +2763,8 @@ export function ChatPage({
) => {
if (
(!second &&
!documentSidebarToggled) ||
(documentSidebarToggled &&
!documentSidebarVisible) ||
(documentSidebarVisible &&
selectedMessageForDocDisplay ===
message.messageId)
) {
@@ -2749,8 +2772,8 @@ export function ChatPage({
}
if (
(second &&
!documentSidebarToggled) ||
(documentSidebarToggled &&
!documentSidebarVisible) ||
(documentSidebarVisible &&
selectedMessageForDocDisplay ===
secondLevelMessage?.messageId)
) {
@@ -2851,8 +2874,8 @@ export function ChatPage({
selectedDocuments={selectedDocuments}
toggleDocumentSelection={() => {
if (
!documentSidebarToggled ||
(documentSidebarToggled &&
!documentSidebarVisible ||
(documentSidebarVisible &&
selectedMessageForDocDisplay ===
message.messageId)
) {
@@ -3070,12 +3093,13 @@ export function ChatPage({
<div className="mx-auto w-fit !pointer-events-none flex sticky justify-center">
<button
onClick={() => clientScrollToBottom()}
className="p-1 pointer-events-auto rounded-2xl bg-background-strong border border-border mx-auto "
className="p-1 pointer-events-auto text-neutral-700 dark:text-neutral-800 rounded-2xl bg-neutral-200 border border-border mx-auto "
>
<FiArrowDown size={18} />
</button>
</div>
)}
<div className="pointer-events-auto w-[95%] mx-auto relative mb-8">
<ChatInputBar
proSearchEnabled={proSearchEnabled}
@@ -3147,7 +3171,7 @@ export function ChatPage({
ease-in-out
h-full
${
documentSidebarToggled && !settings?.isMobile
documentSidebarVisible && !settings?.isMobile
? "w-[350px]"
: "w-[0px]"
}
@@ -3162,7 +3186,7 @@ export function ChatPage({
style={{ transition: "width 0.30s ease-out" }}
className={`flex-none bg-transparent transition-all bg-opacity-80 duration-300 ease-in-out h-full
${
toggledSidebar && !settings?.isMobile
sidebarVisible && !settings?.isMobile
? "w-[250px] "
: "w-[0px]"
}`}
@@ -3174,7 +3198,7 @@ export function ChatPage({
)}
</div>
</div>
<FixedLogo backgroundToggled={toggledSidebar || showHistorySidebar} />
<FixedLogo backgroundToggled={sidebarVisible || showHistorySidebar} />
</div>
{/* Right Sidebar - DocumentSidebar */}
</div>

View File

@@ -115,21 +115,61 @@ export function RefinemenetBadge({
const isDone = displayedPhases.includes(StreamingPhase.COMPLETE);
// Expand/collapse, hover states
const [expanded, setExpanded] = useState(true);
const [expanded] = useState(true);
const [toolTipHoveredInternal, setToolTipHoveredInternal] = useState(false);
const [isHovered, setIsHovered] = useState(false);
const [shouldShow, setShouldShow] = useState(true);
// Refs for bounding area checks
const containerRef = useRef<HTMLDivElement>(null);
const tooltipRef = useRef<HTMLDivElement>(null);
// Keep the tooltip open if hovered on container or tooltip
// Remove the old onMouseLeave calls and rely on bounding area checks
useEffect(() => {
function handleMouseMove(e: MouseEvent) {
if (!containerRef.current || !tooltipRef.current) return;
const containerRect = containerRef.current.getBoundingClientRect();
const tooltipRect = tooltipRef.current.getBoundingClientRect();
const [x, y] = [e.clientX, e.clientY];
const inContainer =
x >= containerRect.left &&
x <= containerRect.right &&
y >= containerRect.top &&
y <= containerRect.bottom;
const inTooltip =
x >= tooltipRect.left &&
x <= tooltipRect.right &&
y >= tooltipRect.top &&
y <= tooltipRect.bottom;
// If not hovering in either region, close tooltip
if (!inContainer && !inTooltip) {
setToolTipHoveredInternal(false);
setToolTipHovered(false);
setIsHovered(false);
}
}
window.addEventListener("mousemove", handleMouseMove);
return () => {
window.removeEventListener("mousemove", handleMouseMove);
};
}, [setToolTipHovered]);
// Once "done", hide after a short delay if not hovered
useEffect(() => {
if (isDone) {
const timer = setTimeout(() => {
setShouldShow(false);
setCanShowResponse(true);
}, 800); // e.g. 0.8s
}, 800);
return () => clearTimeout(timer);
}
}, [isDone, isHovered]);
}, [isDone, isHovered, setCanShowResponse]);
if (!shouldShow) {
return null; // entire box disappears
@@ -137,13 +177,22 @@ export function RefinemenetBadge({
return (
<TooltipProvider delayDuration={0}>
{/*
IMPORTANT: We rely on open={ isHovered || toolTipHoveredInternal }
to keep the tooltip visible if either the badge or tooltip is hovered.
*/}
<Tooltip open={isHovered || toolTipHoveredInternal}>
<div
className="relative w-fit max-w-sm"
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
ref={containerRef}
// onMouseEnter keeps the tooltip open
onMouseEnter={() => {
setIsHovered(true);
setToolTipHoveredInternal(true);
setToolTipHovered(true);
}}
// Remove the explicit onMouseLeave the global bounding check will close it
>
{/* Original snippet's tooltip usage */}
<TooltipTrigger asChild>
<div className="flex items-center gap-x-1 text-black text-sm font-medium cursor-pointer hover:text-blue-600 transition-colors duration-200">
<p className="text-sm loading-text font-medium">
@@ -159,36 +208,32 @@ export function RefinemenetBadge({
</TooltipTrigger>
{expanded && (
<TooltipContent
ref={tooltipRef}
// onMouseEnter keeps the tooltip open when cursor enters tooltip
onMouseEnter={() => {
setToolTipHoveredInternal(true);
setToolTipHovered(true);
}}
onMouseLeave={() => {
setToolTipHoveredInternal(false);
}}
// Remove onMouseLeave and rely on bounding box logic to close
side="bottom"
align="start"
className="w-fit -mt-1 p-4 bg-white border-2 border-border shadow-lg rounded-md"
width="w-fit"
className=" -mt-1 p-4 bg-[#fff] dark:bg-[#000] border-2 border-border dark:border-neutral-800 shadow-lg rounded-md"
>
{/* If not done, show the "Refining" box + a chevron */}
{/* Expanded area: each displayed phase in order */}
<div className="items-start flex flex-col gap-y-2">
{currentState !== StreamingPhase.WAITING ? (
Array.from(new Set(displayedPhases)).map((phase, index) => {
const phaseIndex = displayedPhases.indexOf(phase);
// The last displayed item is "running" if not COMPLETE
let status = ToggleState.Done;
if (
index ===
Array.from(new Set(displayedPhases)).length - 1
Array.from(new Set(displayedPhases)).length - 1 &&
phase !== StreamingPhase.COMPLETE
) {
status = ToggleState.InProgress;
}
if (phase === StreamingPhase.COMPLETE) {
status = ToggleState.Done;
}
return (
<div
@@ -338,6 +383,7 @@ export function StatusRefinement({
onMouseLeave={() => setToolTipHovered(false)}
side="bottom"
align="start"
width="w-fit"
className="w-fit p-4 bg-[#fff] border-2 border-border dark:border-neutral-800 shadow-lg rounded-md"
>
{/* If not done, show the "Refining" box + a chevron */}
@@ -355,7 +401,6 @@ export function StatusRefinement({
</div>
<span className="text-neutral-800 text-sm font-medium">
{StreamingPhaseText[phase]}
LLL
</span>
</div>
))}

View File

@@ -5,18 +5,22 @@ import FunctionalWrapper from "../../components/chat/FunctionalWrapper";
export default function WrappedChat({
firstMessage,
defaultSidebarOff,
}: {
firstMessage?: string;
// This is required for the chrome extension side panel
// we don't want to show the sidebar by default when the user opens the side panel
defaultSidebarOff?: boolean;
}) {
const { toggledSidebar } = useChatContext();
const { sidebarInitiallyVisible } = useChatContext();
return (
<FunctionalWrapper
initiallyToggled={toggledSidebar}
content={(toggledSidebar, toggle) => (
initiallyVisible={sidebarInitiallyVisible && !defaultSidebarOff}
content={(sidebarVisible, toggle) => (
<ChatPage
toggle={toggle}
toggledSidebar={toggledSidebar}
sidebarVisible={sidebarVisible}
firstMessage={firstMessage}
/>
)}

View File

@@ -86,19 +86,20 @@ export function AgenticToggle({
</TooltipTrigger>
<TooltipContent
side="top"
className="w-72 p-4 bg-white rounded-lg shadow-lg border border-background-200 dark:border-neutral-900"
width="w-72"
className="p-4 bg-white rounded-lg shadow-lg border border-background-200 dark:border-neutral-900"
>
<div className="flex items-center space-x-2 mb-3">
<h3 className="text-sm font-semibold text-text-900">
<h3 className="text-sm font-semibold text-neutral-900">
Agent Search (BETA)
</h3>
</div>
<p className="text-xs text-text-600 mb-2">
<p className="text-xs text-neutarl-600 dark:text-neutral-700 mb-2">
Use AI agents to break down questions and run deep iterative
research through promising pathways. Gives more thorough and
accurate responses but takes slightly longer.
</p>
<ul className="text-xs text-text-600 list-disc list-inside">
<ul className="text-xs text-text-600 dark:text-neutral-700 list-disc list-inside">
<li>Improved accuracy of search results</li>
<li>Less hallucinations</li>
<li>More comprehensive answers</li>

View File

@@ -819,27 +819,17 @@ export function ChatInputBar({
chatState == "toolBuilding" ||
chatState == "loading"
? chatState != "streaming"
? "bg-neutral-900 dark:bg-neutral-400 "
: "bg-neutral-500 dark:bg-neutral-50"
: ""
? "bg-neutral-500 dark:bg-neutral-400 "
: "bg-neutral-900 dark:bg-neutral-50"
: "bg-red-200"
} h-[22px] w-[22px] rounded-full`}
onClick={() => {
if (
chatState == "streaming" ||
chatState == "toolBuilding" ||
chatState == "loading"
) {
if (chatState == "streaming") {
stopGenerating();
} else if (message) {
onSubmit();
}
}}
disabled={
(chatState == "streaming" ||
chatState == "toolBuilding" ||
chatState == "loading") &&
chatState != "streaming"
}
>
{chatState == "streaming" ||
chatState == "toolBuilding" ||

View File

@@ -110,6 +110,7 @@ export interface Message {
second_level_message?: string;
second_level_subquestions?: SubQuestionDetail[] | null;
isImprovement?: boolean | null;
isStreamingQuestions?: boolean;
}
export interface BackendChatSession {
@@ -219,6 +220,7 @@ export interface SubQuestionDetail extends BaseQuestionIdentifier {
context_docs?: { top_documents: OnyxDocument[] } | null;
is_complete?: boolean;
is_stopped?: boolean;
answer_streaming?: boolean;
}
export interface SubQueryDetail {
@@ -245,9 +247,6 @@ export const constructSubQuestions = (
}
const updatedSubQuestions = [...subQuestions];
// .filter(
// (sq) => sq.level_question_num !== 0
// );
if ("stop_reason" in newDetail) {
const { level, level_question_num } = newDetail;
@@ -255,8 +254,12 @@ export const constructSubQuestions = (
(sq) => sq.level === level && sq.level_question_num === level_question_num
);
if (subQuestion) {
subQuestion.is_complete = true;
subQuestion.is_stopped = true;
if (newDetail.stream_type == "sub_answer") {
subQuestion.answer_streaming = false;
} else {
subQuestion.is_complete = true;
subQuestion.is_stopped = true;
}
}
} else if ("top_documents" in newDetail) {
const { level, level_question_num, top_documents } = newDetail;

View File

@@ -31,7 +31,7 @@ export default async function Layout({
llmProviders,
folders,
openedFolders,
toggleSidebar,
sidebarInitiallyVisible,
defaultAssistantId,
shouldShowWelcomeModal,
ccPairs,
@@ -47,7 +47,7 @@ export default async function Layout({
proSearchToggled,
inputPrompts,
chatSessions,
toggledSidebar: toggleSidebar,
sidebarInitiallyVisible,
availableSources,
ccPairs,
documentSets,

View File

@@ -48,9 +48,10 @@ import rehypeKatex from "rehype-katex";
import "katex/dist/katex.min.css";
import SubQuestionsDisplay from "./SubQuestionsDisplay";
import { StatusRefinement } from "../Refinement";
import SubQuestionProgress from "./SubQuestionProgress";
export const AgenticMessage = ({
isStreamingQuestions,
isGenerating,
docSidebarToggled,
isImprovement,
secondLevelAssistantMessage,
@@ -81,6 +82,8 @@ export const AgenticMessage = ({
secondLevelSubquestions,
toggleDocDisplay,
}: {
isStreamingQuestions: boolean;
isGenerating: boolean;
docSidebarToggled?: boolean;
isImprovement?: boolean | null;
secondLevelSubquestions?: SubQuestionDetail[] | null;
@@ -230,6 +233,13 @@ export const AgenticMessage = ({
);
const [currentlyOpenQuestion, setCurrentlyOpenQuestion] =
useState<BaseQuestionIdentifier | null>(null);
const [finishedGenerating, setFinishedGenerating] = useState(!isGenerating);
useEffect(() => {
if (streamedContent.length == finalContent.length && !isGenerating) {
setFinishedGenerating(true);
}
}, [streamedContent, finalContent, isGenerating]);
const openQuestion = useCallback(
(question: SubQuestionDetail) => {
@@ -400,12 +410,10 @@ export const AgenticMessage = ({
<div className="w-full desktop:ml-4">
{subQuestions && subQuestions.length > 0 && (
<SubQuestionsDisplay
isStreamingQuestions={isStreamingQuestions}
allowDocuments={() => setAllowDocuments(true)}
docSidebarToggled={docSidebarToggled || false}
finishedGenerating={
finalContent.length > 2 &&
streamedContent.length == finalContent.length
}
finishedGenerating={finishedGenerating}
overallAnswerGenerating={
!!(
secondLevelSubquestions &&

View File

@@ -185,7 +185,7 @@ export const AIMessage = ({
onMessageSelection,
setPresentingDocument,
index,
toggledDocumentSidebar,
documentSidebarVisible,
}: {
index?: number;
shared?: boolean;
@@ -205,7 +205,7 @@ export const AIMessage = ({
citedDocuments?: [string, OnyxDocument][] | null;
toolCall?: ToolCallMetadata | null;
isComplete?: boolean;
toggledDocumentSidebar?: boolean;
documentSidebarVisible?: boolean;
hasDocs?: boolean;
handleFeedback?: (feedbackType: FeedbackType) => void;
handleSearchQueryEdit?: (query: string) => void;
@@ -508,7 +508,7 @@ export const AIMessage = ({
/>
))}
<SeeMoreBlock
toggled={toggledDocumentSidebar!}
toggled={documentSidebarVisible!}
toggleDocumentSelection={toggleDocumentSelection!}
docs={docs}
webSourceDomains={webSourceDomains}
@@ -957,7 +957,7 @@ export const HumanMessage = ({
min-h-[38px]
py-2
px-3
hover:bg-accent-hover
hover:bg-agent-hovered
`}
onClick={handleEditSubmit}
>
@@ -974,7 +974,7 @@ export const HumanMessage = ({
py-2
px-3
w-fit
bg-background-strong
bg-background-200
text-sm
rounded-lg
hover:bg-accent-background-hovered-emphasis

View File

@@ -56,12 +56,19 @@ const DOC_DELAY_MS = 100;
export const useStreamingMessages = (
subQuestions: SubQuestionDetail[],
allowStreaming: () => void,
onComplete: () => void
onComplete: () => void,
isStreamingQuestions: boolean
) => {
const [dynamicSubQuestions, setDynamicSubQuestions] = useState<
SubQuestionDetail[]
>([]);
const isStreamingQuestionsRef = useRef(isStreamingQuestions);
useEffect(() => {
isStreamingQuestionsRef.current = isStreamingQuestions;
}, [isStreamingQuestions]);
const subQuestionsRef = useRef<SubQuestionDetail[]>(subQuestions);
useEffect(() => {
subQuestionsRef.current = subQuestions;
@@ -121,6 +128,7 @@ export const useStreamingMessages = (
// Stream high-level questions sequentially
let didStreamQuestion = false;
let allQuestionsComplete = true;
for (let i = 0; i < actualSubQs.length; i++) {
const sq = actualSubQs[i];
const p = progressRef.current[i];
@@ -138,6 +146,8 @@ export const useStreamingMessages = (
p.questionDone = true;
}
didStreamQuestion = true;
allQuestionsComplete = false;
// Break after streaming one question to ensure sequential behavior
break;
}
@@ -149,7 +159,11 @@ export const useStreamingMessages = (
}
}
if (allQuestionsComplete && !didStreamQuestion) {
if (
allQuestionsComplete &&
!didStreamQuestion &&
!isStreamingQuestionsRef.current
) {
onComplete();
}
@@ -163,6 +177,8 @@ export const useStreamingMessages = (
for (let i = 0; i < actualSubQs.length; i++) {
const sq = actualSubQs[i];
const dynSQ = dynamicSubQuestionsRef.current[i];
dynSQ.answer_streaming = sq.answer_streaming;
const p = progressRef.current[i];
// Wait for subquestion #0 or the previous subquestion's progress

View File

@@ -65,6 +65,7 @@ export interface TemporaryDisplay {
tinyQuestion: string;
}
interface SubQuestionsDisplayProps {
isStreamingQuestions: boolean;
docSidebarToggled: boolean;
finishedGenerating: boolean;
currentlyOpenQuestion?: BaseQuestionIdentifier | null;
@@ -152,7 +153,8 @@ const SubQuestionDisplay: React.FC<{
content = content.replace(/\]\](?!\()/g, "]]()");
return (
preprocessLaTeX(content) + (!subQuestion?.is_complete ? " [*]() " : "")
preprocessLaTeX(content) +
(subQuestion?.answer_streaming ? " [*]() " : "")
);
};
@@ -461,6 +463,7 @@ const SubQuestionDisplay: React.FC<{
};
const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
isStreamingQuestions,
finishedGenerating,
subQuestions,
allowStreaming,
@@ -477,23 +480,29 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
const [showSummarizing, setShowSummarizing] = useState(
finishedGenerating && !overallAnswerGenerating
);
const [initiallyFinishedGenerating, setInitiallyFinishedGenerating] =
useState(finishedGenerating);
// const []
const { dynamicSubQuestions } = useStreamingMessages(
subQuestions,
() => {},
() => {
setShowSummarizing(true);
}
},
isStreamingQuestions
);
const { dynamicSubQuestions: dynamicSecondLevelQuestions } =
useStreamingMessages(
secondLevelQuestions || [],
() => {},
() => {}
() => {},
false
);
const memoizedSubQuestions = useMemo(() => {
return finishedGenerating ? subQuestions : dynamicSubQuestions;
}, [finishedGenerating, dynamicSubQuestions, subQuestions]);
// const memoizedSubQuestions = dynamicSubQuestions;
return initiallyFinishedGenerating ? subQuestions : dynamicSubQuestions;
}, [initiallyFinishedGenerating, dynamicSubQuestions, subQuestions]);
const memoizedSecondLevelQuestions = useMemo(() => {
return overallAnswerGenerating
? dynamicSecondLevelQuestions
@@ -509,12 +518,6 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
(subQuestion) => (subQuestion?.sub_queries || [])?.length > 0
).length == 0;
const overallAnswer =
memoizedSubQuestions.length > 0 &&
memoizedSubQuestions.filter(
(subQuestion) => subQuestion?.answer.length > 10
).length == memoizedSubQuestions.length;
const [streamedText, setStreamedText] = useState(
finishedGenerating ? "Summarize findings" : ""
);
@@ -524,12 +527,15 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
const [shownDocuments, setShownDocuments] = useState(documents);
useEffect(() => {
if (documents && documents.length > 0) {
setTimeout(() => {
setShownDocuments(documents);
}, 800);
if (canShowSummarizing && documents && documents.length > 0) {
setTimeout(
() => {
setShownDocuments(documents);
},
finishedGenerating ? 0 : 800
);
}
}, [documents]);
}, [documents, canShowSummarizing]);
useEffect(() => {
if (

View File

@@ -39,7 +39,6 @@ export function UserSettingsModal({
onClose: () => void;
defaultModel: string | null;
}) {
const { inputPrompts, refreshInputPrompts } = useChatContext();
const {
refreshUser,
user,

View File

@@ -1,3 +1,4 @@
import { SEARCH_PARAMS } from "@/lib/extension/constants";
import WrappedChat from "./WrappedChat";
export default async function Page(props: {
@@ -5,6 +6,13 @@ export default async function Page(props: {
}) {
const searchParams = await props.searchParams;
const firstMessage = searchParams.firstMessage;
const defaultSidebarOff =
searchParams[SEARCH_PARAMS.DEFAULT_SIDEBAR_OFF] === "true";
return <WrappedChat firstMessage={firstMessage} />;
return (
<WrappedChat
firstMessage={firstMessage}
defaultSidebarOff={defaultSidebarOff}
/>
);
}

View File

@@ -50,10 +50,12 @@ import {
} from "@dnd-kit/sortable";
import { useSortable } from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import { CircleX } from "lucide-react";
import { CirclePlus, CircleX, PinIcon } from "lucide-react";
import { restrictToVerticalAxis } from "@dnd-kit/modifiers";
import { turborepoTraceAccess } from "next/dist/build/turborepo-access-trace";
interface HistorySidebarProps {
liveAssistant?: Persona | null;
page: pageType;
existingChats?: ChatSession[];
currentChatSession?: ChatSession | null | undefined;
@@ -66,22 +68,23 @@ interface HistorySidebarProps {
showDeleteModal?: (chatSession: ChatSession) => void;
explicitlyUntoggle: () => void;
showDeleteAllModal?: () => void;
currentAssistantId?: number | null;
setShowAssistantsModal: (show: boolean) => void;
}
interface SortableAssistantProps {
assistant: Persona;
currentAssistantId: number | null | undefined;
active: boolean;
onClick: () => void;
onUnpin: (e: React.MouseEvent) => void;
onPinAction: (e: React.MouseEvent) => void;
pinned?: boolean;
}
const SortableAssistant: React.FC<SortableAssistantProps> = ({
assistant,
currentAssistantId,
active,
onClick,
onUnpin,
onPinAction,
pinned = true,
}) => {
const {
attributes,
@@ -126,7 +129,9 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
>
<DragHandle
size={16}
className="w-3 ml-[2px] mr-[2px] group-hover:visible invisible flex-none cursor-grab"
className={`w-3 ml-[2px] mr-[2px] group-hover:visible invisible flex-none cursor-grab ${
!pinned ? "opacity-0" : ""
}`}
/>
<div
data-testid={`assistant-[${assistant.id}]`}
@@ -137,9 +142,7 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
}
}}
className={`cursor-pointer w-full group hover:bg-background-chat-hover ${
currentAssistantId === assistant.id
? "bg-background-chat-hover/60"
: ""
active ? "bg-accent-background-selected" : ""
} relative flex items-center gap-x-2 py-1 px-2 rounded-md`}
>
<AssistantIcon assistant={assistant} size={16} className="flex-none" />
@@ -164,15 +167,36 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
>
{assistant.name}
</span>
<button
onClick={(e) => {
e.stopPropagation();
onUnpin(e);
}}
className="group-hover:block hidden absolute right-2"
>
<CircleX size={16} className="text-text-history-sidebar-button" />
</button>
<TooltipProvider>
<Tooltip>
<TooltipTrigger asChild>
<button
onClick={(e) => {
e.stopPropagation();
onPinAction(e);
}}
className="group-hover:block hidden absolute right-2"
>
{pinned ? (
<CircleX
size={16}
className="text-text-history-sidebar-button"
/>
) : (
<PinIcon
size={16}
className="text-text-history-sidebar-button"
/>
)}
</button>
</TooltipTrigger>
<TooltipContent>
{pinned
? "Unpin this assistant from the sidebar"
: "Pin this assistant to the sidebar"}
</TooltipContent>
</Tooltip>
</TooltipProvider>
</div>
</div>
);
@@ -181,6 +205,7 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
(
{
liveAssistant,
reset = () => null,
setShowAssistantsModal = () => null,
toggled,
@@ -194,7 +219,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
showShareModal,
showDeleteModal,
showDeleteAllModal,
currentAssistantId,
},
ref: ForwardedRef<HTMLDivElement>
) => {
@@ -353,13 +377,13 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
<SortableAssistant
key={assistant.id === 0 ? "assistant-0" : assistant.id}
assistant={assistant}
currentAssistantId={currentAssistantId}
active={assistant.id === liveAssistant?.id}
onClick={() => {
router.push(
buildChatUrl(searchParams, null, assistant.id)
);
}}
onUnpin={async (e: React.MouseEvent) => {
onPinAction={async (e: React.MouseEvent) => {
e.stopPropagation();
await toggleAssistantPinnedStatus(
pinnedAssistants.map((a) => a.id),
@@ -373,6 +397,31 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
</div>
</SortableContext>
</DndContext>
{!pinnedAssistants.some((a) => a.id === liveAssistant?.id) &&
liveAssistant && (
<div className="w-full mt-1 pr-4">
<SortableAssistant
pinned={false}
assistant={liveAssistant}
active={liveAssistant.id === liveAssistant?.id}
onClick={() => {
router.push(
buildChatUrl(searchParams, null, liveAssistant.id)
);
}}
onPinAction={async (e: React.MouseEvent) => {
e.stopPropagation();
await toggleAssistantPinnedStatus(
[...pinnedAssistants.map((a) => a.id)],
liveAssistant.id,
true
);
await refreshAssistants();
}}
/>
</div>
)}
<div className="w-full px-4">
<button
onClick={() => setShowAssistantsModal(true)}

View File

@@ -21,13 +21,13 @@ import TextView from "@/components/chat/TextView";
import { DocumentResults } from "../../documentSidebar/DocumentResults";
import { Modal } from "@/components/Modal";
import FunctionalHeader from "@/components/chat/Header";
import FixedLogo from "../../../../components/logo/FixedLogo";
import FixedLogo from "@/components/logo/FixedLogo";
import { useRouter } from "next/navigation";
function BackToOnyxButton({
documentSidebarToggled,
documentSidebarVisible,
}: {
documentSidebarToggled: boolean;
documentSidebarVisible: boolean;
}) {
const router = useRouter();
const enterpriseSettings = useContext(SettingsContext)?.enterpriseSettings;
@@ -47,7 +47,7 @@ function BackToOnyxButton({
transition-all
duration-300
ease-in-out
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
${documentSidebarVisible ? "w-[400px]" : "w-[0px]"}
`}
></div>
</div>
@@ -62,7 +62,7 @@ export function SharedChatDisplay({
persona: Persona;
}) {
const settings = useContext(SettingsContext);
const [documentSidebarToggled, setDocumentSidebarToggled] = useState(false);
const [documentSidebarVisible, setDocumentSidebarVisible] = useState(false);
const [selectedMessageForDocDisplay, setSelectedMessageForDocDisplay] =
useState<number | null>(null);
const [isReady, setIsReady] = useState(false);
@@ -70,7 +70,7 @@ export function SharedChatDisplay({
useState<OnyxDocument | null>(null);
const toggleDocumentSidebar = () => {
setDocumentSidebarToggled(!documentSidebarToggled);
setDocumentSidebarVisible(!documentSidebarVisible);
};
useEffect(() => {
@@ -85,7 +85,7 @@ export function SharedChatDisplay({
Did not find a shared chat with the specified ID.
</Callout>
</div>
<BackToOnyxButton documentSidebarToggled={documentSidebarToggled} />
<BackToOnyxButton documentSidebarVisible={documentSidebarVisible} />
</div>
);
}
@@ -102,7 +102,7 @@ export function SharedChatDisplay({
onClose={() => setPresentingDocument(null)}
/>
)}
{documentSidebarToggled && settings?.isMobile && (
{documentSidebarVisible && settings?.isMobile && (
<div className="md:hidden">
<Modal noPadding noScroll>
<DocumentResults
@@ -117,7 +117,7 @@ export function SharedChatDisplay({
: null
}
toggleDocumentSelection={() => {
setDocumentSidebarToggled(true);
setDocumentSidebarVisible(true);
}}
selectedDocuments={[]}
clearSelectedDocuments={() => {}}
@@ -128,7 +128,7 @@ export function SharedChatDisplay({
setPresentingDocument={setPresentingDocument}
modal={true}
closeSidebar={() => {
setDocumentSidebarToggled(false);
setDocumentSidebarVisible(false);
}}
/>
</Modal>
@@ -158,7 +158,7 @@ export function SharedChatDisplay({
duration-300
ease-in-out
h-full
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
${documentSidebarVisible ? "w-[400px]" : "w-[0px]"}
`}
>
<DocumentResults
@@ -174,7 +174,7 @@ export function SharedChatDisplay({
: null
}
toggleDocumentSelection={() => {
setDocumentSidebarToggled(true);
setDocumentSidebarVisible(true);
}}
clearSelectedDocuments={() => {}}
selectedDocumentTokens={0}
@@ -183,7 +183,7 @@ export function SharedChatDisplay({
isOpen={true}
setPresentingDocument={setPresentingDocument}
closeSidebar={() => {
setDocumentSidebarToggled(false);
setDocumentSidebarVisible(false);
}}
selectedDocuments={[]}
/>
@@ -214,7 +214,7 @@ export function SharedChatDisplay({
bg-gradient-to-b via-50% z-[-1] from-background via-background to-background/10 flex
transition-all duration-300 ease-in-out
${
documentSidebarToggled
documentSidebarVisible
? "left-[200px] transform -translate-x-[calc(50%+100px)]"
: "left-1/2 transform -translate-x-1/2"
}
@@ -263,6 +263,8 @@ export function SharedChatDisplay({
) {
return (
<AgenticMessage
isStreamingQuestions={false}
isGenerating={false}
shared
key={message.messageId}
isImprovement={message.isImprovement}
@@ -311,13 +313,13 @@ export function SharedChatDisplay({
selectedDocuments={[]}
toggleDocumentSelection={() => {
if (
!documentSidebarToggled ||
(documentSidebarToggled &&
!documentSidebarVisible ||
(documentSidebarVisible &&
selectedMessageForDocDisplay ===
message.messageId)
) {
setDocumentSidebarToggled(
!documentSidebarToggled
setDocumentSidebarVisible(
!documentSidebarVisible
);
}
setSelectedMessageForDocDisplay(
@@ -351,13 +353,13 @@ export function SharedChatDisplay({
selectedDocuments={[]}
toggleDocumentSelection={() => {
if (
!documentSidebarToggled ||
(documentSidebarToggled &&
!documentSidebarVisible ||
(documentSidebarVisible &&
selectedMessageForDocDisplay ===
message.messageId)
) {
setDocumentSidebarToggled(
!documentSidebarToggled
setDocumentSidebarVisible(
!documentSidebarVisible
);
}
setSelectedMessageForDocDisplay(
@@ -373,6 +375,8 @@ export function SharedChatDisplay({
<div key={message.messageId}>
<AgenticMessage
shared
isStreamingQuestions={false}
isGenerating={false}
subQuestions={message.sub_questions || []}
currentPersona={persona}
messageId={message.messageId}
@@ -404,7 +408,7 @@ export function SharedChatDisplay({
transition-all
duration-300
ease-in-out
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
${documentSidebarVisible ? "w-[400px]" : "w-[0px]"}
`}
></div>
)}
@@ -412,7 +416,7 @@ export function SharedChatDisplay({
</div>
<FixedLogo backgroundToggled={false} />
<BackToOnyxButton documentSidebarToggled={documentSidebarToggled} />
<BackToOnyxButton documentSidebarVisible={documentSidebarVisible} />
</div>
</div>
</>

View File

@@ -0,0 +1,65 @@
"use client";
import React, { ReactNode, useEffect, useState } from "react";
import { useRouter } from "next/navigation";
export default function FunctionalWrapper({
sidebarInitiallyVisible,
content,
}: {
content: (
sidebarVisible: boolean,
toggle: (toggled?: boolean) => void
) => ReactNode;
sidebarInitiallyVisible: boolean;
}) {
const router = useRouter();
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
const newPage = event.shiftKey;
switch (event.key.toLowerCase()) {
case "d":
event.preventDefault();
if (newPage) {
window.open("/chat", "_blank");
} else {
router.push("/chat");
}
break;
case "s":
event.preventDefault();
if (newPage) {
window.open("/search", "_blank");
} else {
router.push("/search");
}
break;
}
}
};
window.addEventListener("keydown", handleKeyDown);
return () => {
window.removeEventListener("keydown", handleKeyDown);
};
}, [router]);
const [sidebarVisible, setSidebarVisible] = useState(sidebarInitiallyVisible);
const toggle = (value?: boolean) => {
setSidebarVisible((sidebarVisible) =>
value !== undefined ? value : !sidebarVisible
);
};
return (
<>
{" "}
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
{content(sidebarVisible, toggle)}
</div>
</>
);
}

View File

@@ -26,7 +26,7 @@ export default async function GalleryPage(props: {
chatSessions,
folders,
openedFolders,
toggleSidebar,
sidebarInitiallyVisible,
shouldShowWelcomeModal,
availableSources,
ccPairs,
@@ -43,8 +43,8 @@ export default async function GalleryPage(props: {
value={{
inputPrompts,
chatSessions,
toggledSidebar: toggleSidebar,
proSearchToggled,
sidebarInitiallyVisible,
availableSources,
ccPairs,
documentSets,

View File

@@ -70,6 +70,7 @@
--accent-foreground: 0 0% 9%;
--accent-background: #f0eee8;
--accent-background-hovered: #e5e3dd;
--accent-background-selected: #eae8e2;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--ring: 0 0% 3.9%;
@@ -120,6 +121,7 @@
/* agent references */
--agent-sidebar: #be5d0e;
--agent-hovered: #d16b10;
--agent: #e47011;
--lighter-agent: #f59e0b;
@@ -247,6 +249,7 @@
--accent-background: #333333;
--accent-background-hovered: #2f2f2f;
--accent-background-selected: #222222;
--text-darker: #f0f0f0;
@@ -339,6 +342,7 @@
/* Agent references */
--agent-sidebar: #be5d0e; /* You can keep or lighten/darken if desired */
--agent-hovered: #f07c13;
--agent: #e47011;
--lighter-agent: #f59e0b;
}
@@ -656,6 +660,11 @@ ul > li > p {
color: white;
}
.dark li {
.prose.dark li,
.prose.dark h1,
.prose.dark h2,
.prose.dark h3,
.prose.dark h4,
.prose.dark h5 {
color: #e5e5e5;
}

View File

@@ -38,7 +38,7 @@ export function EditableValue({
onSubmit(initialValue);
}
}}
className="border bg-background-strong border-background-300 rounded py-1 px-1 w-12 h-4 my-auto"
className="border bg-background-200 border-background-300 rounded py-1 px-1 w-12 h-4 my-auto"
/>
<div
onClick={async () => {

View File

@@ -151,7 +151,7 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
cursor-pointer
${
isSelected
? "bg-background-strong"
? "bg-background-200"
: "hover:bg-accent-background-hovered"
}
`}

View File

@@ -67,7 +67,7 @@ const PageLink = ({
first:ml-0
first:rounded-l-md
last:rounded-r-md
${active ? "bg-background-strong" : ""}
${active ? "bg-background-200" : ""}
`}
onClick={() => {
if (pageChangeHandler) {

View File

@@ -74,7 +74,7 @@ export function IndexAttemptStatus({
);
} else if (status === "not_started") {
badge = (
<Badge variant="purple" icon={FiClock}>
<Badge variant="not_started" icon={FiClock}>
Scheduled
</Badge>
);

View File

@@ -57,7 +57,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
llmProviders,
folders,
openedFolders,
toggleSidebar,
sidebarInitiallyVisible,
defaultAssistantId,
shouldShowWelcomeModal,
ccPairs,
@@ -71,7 +71,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
inputPrompts,
chatSessions,
proSearchToggled,
toggledSidebar: toggleSidebar,
sidebarInitiallyVisible,
availableSources,
ccPairs,
documentSets,

View File

@@ -151,7 +151,7 @@ export function AccessTypeGroupSelector({
cursor-pointer
${
isSelected
? "bg-background-strong"
? "bg-background-200"
: "hover:bg-accent-background-hovered"
}
`}

View File

@@ -86,7 +86,7 @@ export const ConnectorTitle = ({
);
}
const mainSectionClassName = "text-blue-500 flex w-fit";
const mainSectionClassName = "text-blue-500 dark:text-blue-100 flex w-fit";
const mainDisplay = (
<>
{sourceMetadata.icon({ size: 20 })}

View File

@@ -4,7 +4,7 @@ import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
import useSWRMutation from "swr/mutation";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
const DeleteUserButton = ({
user,
@@ -38,7 +38,7 @@ const DeleteUserButton = ({
return (
<>
{showDeleteModal && (
<DeleteEntityModal
<ConfirmEntityModal
entityType="user"
entityName={user.email}
onClose={() => setShowDeleteModal(false)}

View File

@@ -4,7 +4,7 @@ import userMutationFetcher from "@/lib/admin/users/userMutationFetcher";
import useSWRMutation from "swr/mutation";
import { Button } from "@/components/ui/button";
import { useState } from "react";
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
import { useRouter } from "next/navigation";
export const LeaveOrganizationButton = ({
@@ -46,8 +46,9 @@ export const LeaveOrganizationButton = ({
return (
<>
{showLeaveModal && (
<DeleteEntityModal
deleteButtonText="Leave"
<ConfirmEntityModal
variant="action"
actionButtonText="Leave"
entityType="organization"
entityName="your organization"
onClose={() => setShowLeaveModal(false)}

View File

@@ -48,7 +48,6 @@ export function StarterMessages({
flex-col gap-2 rounded-md
text-input-text hover:text-text
border
dark:bg-transparent
dark:border-neutral-700
dark:hover:bg-background-150
@@ -58,11 +57,16 @@ export function StarterMessages({
text-[15px] shadow-xs transition
enabled:hover:bg-background-dark/75
disabled:cursor-not-allowed
line-clamp-3
overflow-hidden
break-words
truncate
text-ellipsis
`}
style={{ height: "5.4rem" }}
style={{ height: "5.6rem" }}
>
{starterMessage.name}
<div className="overflow-hidden text-ellipsis line-clamp-3 pr-1 pb-1">
{starterMessage.name}
</div>
</button>
</div>
))}

View File

@@ -3,27 +3,29 @@
import React, { ReactNode, useState } from "react";
export default function FunctionalWrapper({
initiallyToggled,
initiallyVisible,
content,
}: {
content: (
toggledSidebar: boolean,
toggle: (toggled?: boolean) => void
sidebarVisible: boolean,
toggle: (visible?: boolean) => void
) => ReactNode;
initiallyToggled: boolean;
initiallyVisible?: boolean;
}) {
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
const [sidebarVisible, setSidebarVisible] = useState(
initiallyVisible || false
);
const toggle = (value?: boolean) => {
setToggledSidebar((toggledSidebar) =>
value !== undefined ? value : !toggledSidebar
setSidebarVisible((sidebarVisible) =>
value !== undefined ? value : !sidebarVisible
);
};
return (
<>
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
{content(toggledSidebar, toggle)}
{content(sidebarVisible, toggle)}
</div>
</>
);

View File

@@ -17,7 +17,7 @@ export default function FunctionalHeader({
currentChatSession,
setSharingModalVisible,
toggleSidebar = () => null,
documentSidebarToggled,
documentSidebarVisible,
reset = () => null,
sidebarToggled,
toggleUserSettings,
@@ -31,7 +31,7 @@ export default function FunctionalHeader({
toggleSidebar?: () => void;
toggleUserSettings?: () => void;
hideUserDropdown?: boolean;
documentSidebarToggled?: boolean;
documentSidebarVisible?: boolean;
}) {
const settings = useContext(SettingsContext);
useEffect(() => {
@@ -89,6 +89,7 @@ export default function FunctionalHeader({
duration-300
ease-in-out
h-full
${sidebarToggled ? "w-[250px]" : "w-[0px]"}
`}
/>
@@ -97,19 +98,19 @@ export default function FunctionalHeader({
className={`
absolute
${
documentSidebarToggled &&
documentSidebarVisible &&
sidebarToggled &&
"left-[calc(50%-75px)]"
}
${
documentSidebarToggled && !sidebarToggled
documentSidebarVisible && !sidebarToggled
? "left-[calc(50%-175px)]"
: !documentSidebarToggled && sidebarToggled
: !documentSidebarVisible && sidebarToggled
? "left-[calc(50%+100px)]"
: "left-1/2"
}
${
documentSidebarToggled || sidebarToggled
documentSidebarVisible || sidebarToggled
? "mobile:w-[40vw] max-w-[40vw]"
: "mobile:w-[50vw] max-w-[60vw]"
}
@@ -190,7 +191,7 @@ export default function FunctionalHeader({
duration-300
ease-in-out
h-full
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
${documentSidebarVisible ? "w-[400px]" : "w-[0px]"}
`}
/>
</div>

View File

@@ -1,7 +1,7 @@
import { Dispatch, SetStateAction, useEffect, useRef } from "react";
interface UseSidebarVisibilityProps {
toggledSidebar: boolean;
sidebarVisible: boolean;
sidebarElementRef: React.RefObject<HTMLElement>;
showDocSidebar: boolean;
setShowDocSidebar: Dispatch<SetStateAction<boolean>>;
@@ -11,7 +11,7 @@ interface UseSidebarVisibilityProps {
}
export const useSidebarVisibility = ({
toggledSidebar,
sidebarVisible,
sidebarElementRef,
setShowDocSidebar,
setToggled,
@@ -55,7 +55,7 @@ export const useSidebarVisibility = ({
currentXPosition > 100 &&
showDocSidebar &&
!isWithinSidebar &&
!toggledSidebar
!sidebarVisible
) {
setTimeout(() => {
setShowDocSidebar((showDocSidebar) => {
@@ -88,7 +88,7 @@ export const useSidebarVisibility = ({
}
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [showDocSidebar, toggledSidebar, sidebarElementRef, mobile]);
}, [showDocSidebar, sidebarVisible, sidebarElementRef, mobile]);
return { showDocSidebar };
};

View File

@@ -60,7 +60,7 @@ export const AssistantsProvider: React.FC<{
.map((id) => assistants.find((assistant) => assistant.id === id))
.filter((assistant): assistant is Persona => assistant !== undefined);
} else {
return assistants.filter((a) => a.builtin_persona);
return assistants.filter((a) => a.is_default_persona);
}
});
@@ -71,7 +71,7 @@ export const AssistantsProvider: React.FC<{
.map((id) => assistants.find((assistant) => assistant.id === id))
.filter((assistant): assistant is Persona => assistant !== undefined);
} else {
return assistants.filter((a) => a.builtin_persona);
return assistants.filter((a) => a.is_default_persona);
}
});
}, [user?.preferences?.pinned_assistants, assistants]);

View File

@@ -16,7 +16,7 @@ import { useRouter } from "next/navigation";
interface ChatContextProps {
chatSessions: ChatSession[];
toggledSidebar: boolean;
sidebarInitiallyVisible: boolean;
availableSources: ValidSources[];
ccPairs: CCPairBasicInfo[];
tags: Tag[];

View File

@@ -4,9 +4,7 @@ import { ValidSources } from "@/lib/types";
import useSWR, { mutate } from "swr";
import { errorHandlingFetcher } from "@/lib/fetcher";
import { FaSwatchbook } from "react-icons/fa";
import { NewChatIcon } from "@/components/icons/icons";
import { useState } from "react";
import { useUserGroups } from "@/lib/hooks";
import {
deleteCredential,
swapCredential,
@@ -207,7 +205,7 @@ export default function CredentialSection({
{showCreateCredential && (
<Modal
onOutsideClick={closeCreateCredential}
className="max-w-3xl rounded-lg"
className="max-w-3xl flex flex-col items-start rounded-lg"
title={`Create ${getSourceDisplayName(sourceType)} Credential`}
>
{oauthDetailsLoading ? (

View File

@@ -111,9 +111,15 @@ export default function CreateCredential({
const { name, is_public, groups, ...credentialValues } = values;
const filteredCredentialValues = Object.fromEntries(
Object.entries(credentialValues).filter(
([_, value]) => value !== null && value !== ""
)
);
try {
const response = await submitCredential({
credential_json: credentialValues,
credential_json: filteredCredentialValues,
admin_public: true,
curator_public: is_public,
groups: groups,
@@ -160,7 +166,7 @@ export default function CreateCredential({
}
if (sourceType == "google_drive") {
return <GDriveMain />;
return <GDriveMain setPopup={setPopup} />;
}
const credentialTemplate: dictionaryType = credentialTemplates[sourceType];
@@ -194,7 +200,7 @@ export default function CreateCredential({
for information on setting up this connector.
</p>
)}
<CardSection className="w-full !border-0 mt-4 flex flex-col gap-y-6">
<CardSection className="w-full items-start dark:!border-0 mt-4 flex flex-col gap-y-6">
<TextFormField
name="name"
placeholder="(Optional) credential name.."

View File

@@ -6,18 +6,32 @@ import {
getDisplayNameForCredentialKey,
} from "@/lib/connectors/credentials";
export function createValidationSchema(json_values: dictionaryType) {
const schemaFields: { [key: string]: Yup.StringSchema } = {};
export function createValidationSchema(json_values: Record<string, any>) {
const schemaFields: Record<string, Yup.AnySchema> = {};
for (const key in json_values) {
if (Object.prototype.hasOwnProperty.call(json_values, key)) {
if (json_values[key] === null) {
schemaFields[key] = Yup.string().optional();
} else {
schemaFields[key] = Yup.string().required(
`Please enter your ${getDisplayNameForCredentialKey(key)}`
);
}
if (!Object.prototype.hasOwnProperty.call(json_values, key)) {
continue;
}
const displayName = getDisplayNameForCredentialKey(key);
if (json_values[key] === null) {
// Field is optional:
schemaFields[key] = Yup.string()
.trim()
// Transform empty strings to null
.transform((value) => (value === "" ? null : value))
.nullable()
.notRequired();
} else {
// Field is required:
schemaFields[key] = Yup.string()
.trim()
// This ensures user cannot enter an empty string:
.min(1, `${displayName} cannot be empty`)
// The required message is shown if the field is missing
.required(`Please enter your ${displayName}`);
}
}

View File

@@ -50,7 +50,7 @@ export function ModelOption({
<div
className={`p-4 w-96 border rounded-lg transition-all duration-200 ${
selected
? "border-blue-500 bg-blue-50 dark:bg-blue-900 dark:border-blue-700 shadow-md"
? "border-blue-800 bg-blue-50 dark:bg-blue-950 dark:border-blue-700 shadow-md"
: "border-background-200 hover:border-blue-300 hover:shadow-sm"
}`}
>

View File

@@ -7,8 +7,9 @@ import {
MicrosoftIcon,
NomicIcon,
OpenAIIcon,
OpenAIISVG,
OpenSourceIcon,
VoyageIcon,
VoyageIconSVG,
} from "@/components/icons/icons";
export enum EmbeddingProvider {
@@ -216,7 +217,7 @@ export const AVAILABLE_CLOUD_PROVIDERS: CloudEmbeddingProvider[] = [
{
provider_type: EmbeddingProvider.OPENAI,
website: "https://openai.com",
icon: OpenAIIcon,
icon: OpenAIISVG,
description: "AI industry leader known for ChatGPT and DALL-E",
apiLink: "https://platform.openai.com/api-keys",
docsLink: "https://docs.onyx.app/guides/embedding_providers#openai-models",
@@ -295,7 +296,7 @@ export const AVAILABLE_CLOUD_PROVIDERS: CloudEmbeddingProvider[] = [
{
provider_type: EmbeddingProvider.VOYAGE,
website: "https://www.voyageai.com",
icon: VoyageIcon,
icon: VoyageIconSVG,
description: "Advanced NLP research startup born from Stanford AI Labs",
docsLink: "https://docs.onyx.app/guides/embedding_providers#voyage-models",
apiLink: "https://www.voyageai.com/dashboard",

View File

@@ -1162,11 +1162,114 @@ export const MistralIcon = ({
<LogoIcon size={size} className={className} src={mistralSVG} />
);
export const VoyageIcon = ({
export const VoyageIconSVG = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => (
<LogoIcon size={size} className={className} src={voyageIcon} />
<svg
style={{ width: `${size}px`, height: `${size}px` }}
className={`w-[${size}px] h-[${size}px] ` + className}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 200 200"
width="200"
height="200"
>
<path
d="M0 0 C18.56364691 14.8685395 31.52865476 35.60458591 34.68359375 59.39453125 C36.85790415 84.17093249 31.86661083 108.64738046 15.83569336 128.38696289 C-0.18749615 147.32766215 -21.13158775 159.50726579 -46 162 C-70.46026633 163.68595557 -94.53744209 157.16585411 -113.375 141.1875 C-131.5680983 125.12913912 -143.31327081 103.12304227 -145.16845703 78.79052734 C-146.52072106 52.74671426 -138.40787353 29.42123969 -121 10 C-120.39929688 9.30519531 -119.79859375 8.61039063 -119.1796875 7.89453125 C-88.7732111 -25.07872563 -34.66251161 -26.29920259 0 0 Z M-111 6 C-111.96292969 6.76441406 -112.92585938 7.52882813 -113.91796875 8.31640625 C-129.12066 21.0326872 -138.48510826 41.64930525 -141 61 C-142.57102569 86.19086606 -137.40498471 109.10013392 -120.54980469 128.68505859 C-106.05757815 144.84161953 -85.8110604 156.92053779 -63.68798828 158.12597656 C-39.72189393 158.83868932 -17.08757891 154.40601729 1.1875 137.6875 C3.15800523 135.82115685 5.07881363 133.91852176 7 132 C8.22396484 130.7934375 8.22396484 130.7934375 9.47265625 129.5625 C26.2681901 112.046746 31.70691205 89.639394 31.3125 66 C30.4579168 43.32505919 19.07700136 22.58412979 3 7 C-29.27431062 -21.68827611 -78.26536136 -21.67509486 -111 6 Z "
fill="currentColor"
transform="translate(155,29)"
/>
<path
d="M0 0 C2.62278901 2.33427271 3.96735488 4.64596813 5.4453125 7.81640625 C6.10080078 9.20956055 6.10080078 9.20956055 6.76953125 10.63085938 C7.21683594 11.59830078 7.66414063 12.56574219 8.125 13.5625 C8.58003906 14.53380859 9.03507812 15.50511719 9.50390625 16.50585938 C10.34430119 18.30011504 11.18198346 20.09564546 12.01611328 21.89282227 C12.65935931 23.27045415 13.32005367 24.64010734 14 26 C12.02 26 10.04 26 8 26 C6.515 22.535 6.515 22.535 5 19 C1.7 19 -1.6 19 -5 19 C-5.99 21.31 -6.98 23.62 -8 26 C-9.32 26 -10.64 26 -12 26 C-10.34176227 20.46347949 -7.92776074 15.38439485 -5.4375 10.1875 C-5.02564453 9.31673828 -4.61378906 8.44597656 -4.18945312 7.54882812 C-1.13502139 1.13502139 -1.13502139 1.13502139 0 0 Z M-1 8 C-3.2013866 11.80427492 -3.2013866 11.80427492 -4 16 C-1.69 16 0.62 16 3 16 C2.43260132 11.87026372 2.43260132 11.87026372 1 8 C0.34 8 -0.32 8 -1 8 Z "
fill="currentColor"
transform="translate(158,86)"
/>
<path
d="M0 0 C2.64453125 1.0234375 2.64453125 1.0234375 4.4453125 4.296875 C4.96971298 5.65633346 5.47294966 7.0241056 5.95703125 8.3984375 C6.22064453 9.08421875 6.48425781 9.77 6.75585938 10.4765625 C7.8687821 13.4482107 8.64453125 15.82826389 8.64453125 19.0234375 C9.30453125 19.0234375 9.96453125 19.0234375 10.64453125 19.0234375 C10.75667969 18.34925781 10.86882813 17.67507812 10.984375 16.98046875 C11.77373626 13.44469078 12.95952974 10.10400184 14.20703125 6.7109375 C14.44099609 6.06576172 14.67496094 5.42058594 14.91601562 4.75585938 C15.48900132 3.17722531 16.06632589 1.60016724 16.64453125 0.0234375 C17.96453125 0.0234375 19.28453125 0.0234375 20.64453125 0.0234375 C20.11164835 5.93359329 17.66052325 10.65458241 15.08203125 15.8984375 C14.65728516 16.77757813 14.23253906 17.65671875 13.79492188 18.5625 C12.75156566 20.71955106 11.70131241 22.87294038 10.64453125 25.0234375 C9.65453125 25.0234375 8.66453125 25.0234375 7.64453125 25.0234375 C6.36851794 22.52596727 5.09866954 20.02565814 3.83203125 17.5234375 C3.29739258 16.47929688 3.29739258 16.47929688 2.75195312 15.4140625 C0.37742917 10.70858383 -1.58321849 5.98797449 -3.35546875 1.0234375 C-2.35546875 0.0234375 -2.35546875 0.0234375 0 0 Z "
fill="currentColor"
transform="translate(23.35546875,86.9765625)"
/>
<path
d="M0 0 C4.56944444 2.13888889 4.56944444 2.13888889 6 5 C6.58094684 9.76376411 6.98189835 13.6696861 4.0625 17.625 C-0.08290736 19.4862033 -3.52913433 19.80184004 -8 19 C-11.18487773 17.20850628 -12.56721386 16.06753914 -13.9375 12.6875 C-14.04047475 8.25958558 -13.25966827 4.50191217 -10.375 1.0625 C-6.92547207 -0.48070986 -3.67744273 -0.55453501 0 0 Z M-7.66796875 3.21484375 C-9.3387892 5.45403713 -9.40271257 6.72874309 -9.375 9.5 C-9.38273437 10.2734375 -9.39046875 11.046875 -9.3984375 11.84375 C-8.90844456 14.49547648 -8.12507645 15.38331504 -6 17 C-3.17884512 17.42317323 -1.66049093 17.38718434 0.8125 15.9375 C2.65621741 12.92932949 2.30257262 10.44932782 2 7 C1.54910181 4.59436406 1.54910181 4.59436406 0 3 C-4.00690889 1.63330935 -4.00690889 1.63330935 -7.66796875 3.21484375 Z "
fill="currentColor"
transform="translate(58,93)"
/>
<path
d="M0 0 C0.91007812 0.00902344 1.82015625 0.01804687 2.7578125 0.02734375 C3.45648438 0.03894531 4.15515625 0.05054687 4.875 0.0625 C5.205 1.3825 5.535 2.7025 5.875 4.0625 C4.6375 3.815 3.4 3.5675 2.125 3.3125 C-1.0391959 2.93032359 -1.83705309 2.89394571 -4.6875 4.5625 C-6.71059726 8.08093001 -6.12332701 10.21181009 -5.125 14.0625 C-3.22744856 16.41223818 -3.22744856 16.41223818 0 16.1875 C0.94875 16.14625 1.8975 16.105 2.875 16.0625 C2.875 14.4125 2.875 12.7625 2.875 11.0625 C4.525 11.3925 6.175 11.7225 7.875 12.0625 C8.1875 14.375 8.1875 14.375 7.875 17.0625 C5.25185816 19.29988569 3.33979578 19.9932751 -0.0625 20.5 C-3.96030088 19.9431713 -6.06489651 18.49667323 -9.125 16.0625 C-11.6165904 12.3251144 -11.58293285 10.48918417 -11.125 6.0625 C-7.83836921 1.02299945 -5.86190884 -0.07515268 0 0 Z "
fill="currentColor"
transform="translate(113.125,92.9375)"
/>
<path
d="M0 0 C4.28705043 1.42901681 5.23208702 4.57025431 7.1875 8.375 C7.55552734 9.06078125 7.92355469 9.7465625 8.30273438 10.453125 C11 15.59744608 11 15.59744608 11 19 C9.35 19 7.7 19 6 19 C5.67 17.68 5.34 16.36 5 15 C2.03 14.67 -0.94 14.34 -4 14 C-4.33 15.65 -4.66 17.3 -5 19 C-5.99 19 -6.98 19 -8 19 C-7.38188466 14.44684052 -5.53234107 10.71540233 -3.4375 6.6875 C-2.9434668 5.71973633 -2.9434668 5.71973633 -2.43945312 4.73242188 C-1.63175745 3.15214772 -0.81662387 1.57567895 0 0 Z M0 6 C-0.33 7.65 -0.66 9.3 -1 11 C0.32 11 1.64 11 3 11 C2.34 9.35 1.68 7.7 1 6 C0.67 6 0.34 6 0 6 Z "
fill="currentColor"
transform="translate(90,93)"
/>
<path
d="M0 0 C3.63 0 7.26 0 11 0 C11 0.66 11 1.32 11 2 C8.69 2 6.38 2 4 2 C4 3.98 4 5.96 4 8 C5.98 8 7.96 8 10 8 C9.67 8.99 9.34 9.98 9 11 C7.68 11 6.36 11 5 11 C4.67 12.98 4.34 14.96 4 17 C7.465 16.505 7.465 16.505 11 16 C11 16.99 11 17.98 11 19 C7.37 19 3.74 19 0 19 C0 12.73 0 6.46 0 0 Z "
fill="currentColor"
transform="translate(124,93)"
/>
<path
d="M0 0 C2.25 -0.3125 2.25 -0.3125 5 0 C9 4.10810811 9 4.10810811 9 7 C9.78375 6.21625 10.5675 5.4325 11.375 4.625 C12.91666667 3.08333333 14.45833333 1.54166667 16 0 C16.99 0 17.98 0 19 0 C17.84356383 2.5056117 16.63134741 4.4803655 14.9375 6.6875 C12.52118995 10.81861073 12.20924288 14.29203528 12 19 C10.68 19 9.36 19 8 19 C8.00902344 18.443125 8.01804687 17.88625 8.02734375 17.3125 C7.78294047 11.0217722 5.92390505 8.0388994 1.49609375 3.62890625 C0 2 0 2 0 0 Z "
fill="currentColor"
transform="translate(64,93)"
/>
<path
d="M0 0 C1.32 0 2.64 0 4 0 C4 8.25 4 16.5 4 25 C2.68 25 1.36 25 0 25 C0 16.75 0 8.5 0 0 Z "
fill="currentColor"
transform="translate(173,87)"
/>
<path
d="M0 0 C0.66 0.33 1.32 0.66 2 1 C1.125 5.75 1.125 5.75 0 8 C1.093125 7.95875 2.18625 7.9175 3.3125 7.875 C7 8 7 8 10 10 C4.555 10.495 4.555 10.495 -1 11 C-1.99 13.31 -2.98 15.62 -4 18 C-5.32 18 -6.64 18 -8 18 C-6.65150163 13.64029169 -4.95092154 9.68658562 -2.875 5.625 C-2.33617187 4.56539063 -1.79734375 3.50578125 -1.2421875 2.4140625 C-0.83226562 1.61742188 -0.42234375 0.82078125 0 0 Z "
fill="currentColor"
transform="translate(154,94)"
/>
<path
d="M0 0 C0.66 0.33 1.32 0.66 2 1 C2 1.66 2 2.32 2 3 C1.34 3 0.68 3 0 3 C-0.05429959 4.74965358 -0.09292823 6.49979787 -0.125 8.25 C-0.14820313 9.22453125 -0.17140625 10.1990625 -0.1953125 11.203125 C0.00137219 14.0196498 0.55431084 15.60949036 2 18 C1.34 18.33 0.68 18.66 0 19 C-4.69653179 15.74855491 -4.69653179 15.74855491 -5.9375 12.6875 C-6.02161912 9.07037805 -5.30970069 6.36780178 -4 3 C-1.875 1.0625 -1.875 1.0625 0 0 Z "
fill="currentColor"
transform="translate(50,93)"
/>
<path
d="M0 0 C2.79192205 -0.05380578 5.5828141 -0.09357669 8.375 -0.125 C9.1690625 -0.14175781 9.963125 -0.15851563 10.78125 -0.17578125 C12.85492015 -0.19335473 14.92883241 -0.10335168 17 0 C17.66 0.66 18.32 1.32 19 2 C17 4 17 4 13.0859375 4.1953125 C11.51550649 4.18200376 9.94513779 4.15813602 8.375 4.125 C7.57320312 4.11597656 6.77140625 4.10695312 5.9453125 4.09765625 C3.96341477 4.07406223 1.98167019 4.03819065 0 4 C0 2.68 0 1.36 0 0 Z "
fill="currentColor"
transform="translate(92,187)"
/>
<path
d="M0 0 C0.99 0.33 1.98 0.66 3 1 C1.66666667 4.33333333 0.33333333 7.66666667 -1 11 C0.65 11 2.3 11 4 11 C4 11.33 4 11.66 4 12 C1.36 12.33 -1.28 12.66 -4 13 C-4.33 14.98 -4.66 16.96 -5 19 C-5.99 19 -6.98 19 -8 19 C-7.38188466 14.44684052 -5.53234107 10.71540233 -3.4375 6.6875 C-2.9434668 5.71973633 -2.9434668 5.71973633 -2.43945312 4.73242188 C-1.63175745 3.15214772 -0.81662387 1.57567895 0 0 Z "
fill="currentColor"
transform="translate(90,93)"
/>
<path
d="M0 0 C0.99 0 1.98 0 3 0 C2.43454163 3.95820859 1.19097652 6.6659053 -1 10 C-1.66 9.67 -2.32 9.34 -3 9 C-2.44271087 5.65626525 -1.64826111 2.96687001 0 0 Z "
fill="currentColor"
transform="translate(37,97)"
/>
<path
d="M0 0 C4.92127034 -0.16682272 8.50343896 -0.24828052 13 2 C9.60268371 4.09065618 6.95730595 4.42098999 3 4 C1.125 2.5625 1.125 2.5625 0 1 C0 0.67 0 0.34 0 0 Z "
fill="currentColor"
transform="translate(110,12)"
/>
<path
d="M0 0 C0 0.99 0 1.98 0 3 C-3.08888522 5.05925681 -3.70935927 5.2390374 -7.1875 5.125 C-9.0746875 5.063125 -9.0746875 5.063125 -11 5 C-10.67 4.34 -10.34 3.68 -10 3 C-7.96875 2.40234375 -7.96875 2.40234375 -5.5 1.9375 C-2.46226779 1.54135157 -2.46226779 1.54135157 0 0 Z "
fill="currentColor"
transform="translate(62,107)"
/>
<path
d="M0 0 C0.66 0.33 1.32 0.66 2 1 C1.25 5.75 1.25 5.75 -1 8 C-1.66 8 -2.32 8 -3 8 C-1.125 1.125 -1.125 1.125 0 0 Z "
fill="currentColor"
transform="translate(154,94)"
/>
<path
d="M0 0 C2.64 0 5.28 0 8 0 C8.33 1.32 8.66 2.64 9 4 C6.03 3.01 3.06 2.02 0 1 C0 0.67 0 0.34 0 0 Z "
fill="currentColor"
transform="translate(110,93)"
/>
<path
d="M0 0 C1.67542976 0.28604898 3.34385343 0.61781233 5 1 C4.67 2.32 4.34 3.64 4 5 C2.0625 4.6875 2.0625 4.6875 0 4 C-0.33 3.01 -0.66 2.02 -1 1 C-0.67 0.67 -0.34 0.34 0 0 Z "
fill="currentColor"
transform="translate(21,87)"
/>
</svg>
);
export const GoogleIcon = ({
@@ -1291,12 +1394,25 @@ export const GoogleSitesIcon = ({
}: IconProps) => (
<LogoIcon size={size} className={className} src={googleSitesIcon} />
);
export const ZendeskIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => (
<LogoIcon size={size} className={className} src={zendeskIcon} />
<div
className="rounded-full overflow-visible dark:overflow-hidden flex items-center justify-center dark:bg-[#fff]/90"
style={{ width: size, height: size }}
>
<LogoIcon
size={
typeof window !== "undefined" &&
window.matchMedia("(prefers-color-scheme: dark)").matches
? size * 0.8
: size
}
className={`${className}`}
src={zendeskIcon}
/>
</div>
);
export const DropboxIcon = ({

View File

@@ -0,0 +1,67 @@
import { Modal } from "../Modal";
import { Button } from "../ui/button";
export const ConfirmEntityModal = ({
onClose,
onSubmit,
entityType,
entityName,
additionalDetails,
actionButtonText,
includeCancelButton = true,
variant = "delete",
}: {
entityType: string;
entityName: string;
onClose: () => void;
onSubmit: () => void;
additionalDetails?: string;
actionButtonText?: string;
includeCancelButton?: boolean;
variant?: "delete" | "action";
}) => {
const isDeleteVariant = variant === "delete";
const defaultButtonText = isDeleteVariant ? "Delete" : "Confirm";
const buttonText = actionButtonText || defaultButtonText;
const getActionText = () => {
if (isDeleteVariant) {
return "delete";
}
switch (entityType) {
case "Default Persona":
return "change the default status of";
default:
return "modify";
}
};
return (
<Modal width="rounded max-w-sm w-full" onOutsideClick={onClose}>
<>
<div className="flex mb-4">
<h2 className="my-auto text-2xl font-bold">
{buttonText} {entityType}
</h2>
</div>
<p className="mb-4">
Are you sure you want to {getActionText()} <b>{entityName}</b>?
</p>
{additionalDetails && <p className="mb-4">{additionalDetails}</p>}
<div className="flex justify-end gap-2">
{includeCancelButton && (
<Button onClick={onClose} variant="outline">
Cancel
</Button>
)}
<Button
onClick={onSubmit}
variant={isDeleteVariant ? "destructive" : "default"}
>
{buttonText}
</Button>
</div>
</>
</Modal>
);
};

View File

@@ -1,51 +0,0 @@
import { FiTrash, FiX } from "react-icons/fi";
import { BasicClickable } from "@/components/BasicClickable";
import { Modal } from "../Modal";
import { Button } from "../ui/button";
export const DeleteEntityModal = ({
onClose,
onSubmit,
entityType,
entityName,
additionalDetails,
deleteButtonText,
includeCancelButton = true,
}: {
entityType: string;
entityName: string;
onClose: () => void;
onSubmit: () => void;
additionalDetails?: string;
deleteButtonText?: string;
includeCancelButton?: boolean;
}) => {
return (
<Modal width="rounded max-w-sm w-full" onOutsideClick={onClose}>
<>
<div className="flex mb-4">
<h2 className="my-auto text-2xl font-bold">
{deleteButtonText || `Delete`} {entityType}
</h2>
</div>
<p className="mb-4">
Are you sure you want to {deleteButtonText || "delete"}{" "}
<b>{entityName}</b>?
</p>
{additionalDetails && <p className="mb-4">{additionalDetails}</p>}
<div className="flex items-end justify-end">
<div className="flex gap-x-2">
{includeCancelButton && (
<Button variant="outline" onClick={onClose}>
<div className="flex mx-2">Cancel</div>
</Button>
)}
<Button size="sm" variant="destructive" onClick={onSubmit}>
<div className="flex mx-2">{deleteButtonText || "Delete"}</div>
</Button>
</div>
</div>
</>
</Modal>
);
};

View File

@@ -65,7 +65,7 @@ export function Citation({
</span>
</TooltipTrigger>
<TooltipContent
className="dark:border dark:!bg-[#000] border-neutral-700"
className="border border-neutral-300 hover:text-neutral-900 bg-neutral-100 dark:!bg-[#000] dark:border-neutral-700"
width="mb-2 max-w-lg"
>
{document_info?.document ? (

View File

@@ -37,7 +37,7 @@ const badgeVariants = cva(
destructive:
"border-red-200 bg-red-50 text-red-600 dark:border-red-700 dark:bg-red-900 dark:text-neutral-50",
not_started:
"border-neutral-200 bg-neutral-50 text-neutral-600 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100",
"border-purple-200 bg-purple-50 text-purple-700 dark:border-purple-700 dark:bg-purple-900 dark:text-purple-100",
},
},
defaultVariants: {

View File

@@ -42,7 +42,7 @@ interface FetchChatDataResult {
folders: Folder[];
openedFolders: Record<string, boolean>;
defaultAssistantId?: number;
toggleSidebar: boolean;
sidebarInitiallyVisible: boolean;
finalDocumentSidebarInitialWidth?: number;
shouldShowWelcomeModal: boolean;
inputPrompts: InputPrompt[];
@@ -182,7 +182,7 @@ export async function fetchChatData(searchParams: {
"true";
// IF user is an anoymous user, we don't want to show the sidebar (they have no access to chat history)
const toggleSidebar =
const sidebarInitiallyVisible =
!user?.is_anonymous_user &&
(sidebarToggled
? sidebarToggled.value.toLocaleLowerCase() == "true" || false
@@ -230,7 +230,7 @@ export async function fetchChatData(searchParams: {
openedFolders,
defaultAssistantId,
finalDocumentSidebarInitialWidth,
toggleSidebar,
sidebarInitiallyVisible,
shouldShowWelcomeModal,
inputPrompts,
proSearchToggled,

View File

@@ -1115,7 +1115,24 @@ For example, specifying .*-support.* as a "channel" will cause the connector to
optional: false,
},
],
advanced_values: [],
advanced_values: [
{
type: "text",
label: "View ID",
name: "view_id",
optional: true,
description:
"If you need to link to a specific View, put that ID here e.g. viwVUEJjWPd8XYjh8.",
},
{
type: "text",
label: "Share ID",
name: "share_id",
optional: true,
description:
"If you need to link to a specific Share, put that ID here e.g. shrkfjEzDmLaDtK83.",
},
],
overrideDefaultFreq: 60 * 60 * 24,
},
};

View File

@@ -31,3 +31,7 @@ export const LocalStorageKeys = {
SHOW_SHORTCUTS: "showShortcuts",
USE_ONYX_AS_NEW_TAB: "useOnyxAsDefaultNewTab",
};
export const SEARCH_PARAMS = {
DEFAULT_SIDEBAR_OFF: "defaultSidebarOff",
};

View File

@@ -76,6 +76,7 @@ export interface StreamStopInfo {
stop_reason: StreamStopReason;
level?: number;
level_question_num?: number;
stream_type?: "sub_answer" | "sub_questions" | "main_answer";
}
export interface ErrorMessagePacket {

View File

@@ -108,6 +108,7 @@ module.exports = {
"input-option-hover": "var(--input-option-hover)",
"accent-background": "var(--accent-background)",
"accent-background-hovered": "var(--accent-background-hovered)",
"accent-background-selected": "var(--accent-background-selected)",
"background-dark": "var(--off-white)",
"background-100": "var(--neutral-100-border-light)",
"background-125": "var(--neutral-125)",
@@ -213,7 +214,7 @@ module.exports = {
input: "var(--white-card-popover)",
text: "var(--neutral-900)",
text: "var(--neutral-950)",
"text-darker": "var(--text-darker)",
"text-dark": "var(--text-dark)",
"sidebar-border": "var(--neutral-200-border)",
@@ -262,7 +263,7 @@ module.exports = {
"agent-sidebar": "var(--agent-sidebar)",
agent: "var(--agent)",
"lighter-agent": "var(--lighter-agent)",
"agent-hovered": "var(--agent-hovered)",
// hover
"hover-light": "var(--hover-light)",
"hover-lightish": "var(--neutral-125)",