mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-03-04 15:15:46 +00:00
Compare commits
26 Commits
bb
...
minor_misc
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3b234341b4 | ||
|
|
523fa76390 | ||
|
|
3afc7b0aa1 | ||
|
|
3406a8385d | ||
|
|
0ff1573f30 | ||
|
|
cfc43d27a3 | ||
|
|
c87261cda7 | ||
|
|
e030b0a6fc | ||
|
|
61136975ad | ||
|
|
0c74bbf9ed | ||
|
|
12b2126e69 | ||
|
|
037943c6ff | ||
|
|
f9485b1325 | ||
|
|
552a0630fe | ||
|
|
5bf520d8b8 | ||
|
|
7dc5a77946 | ||
|
|
03abd4a1bc | ||
|
|
16d6d708f6 | ||
|
|
9740ed32b5 | ||
|
|
b56877cc2e | ||
|
|
da5c83a96d | ||
|
|
818225c60e | ||
|
|
5a4d007cf9 | ||
|
|
5e32f9d922 | ||
|
|
fb931ee4de | ||
|
|
bc2c56dfb6 |
@@ -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' }}
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]]:
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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,
|
||||
),
|
||||
]
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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}
|
||||
```
|
||||
```
|
||||
|
||||
@@ -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
|
||||
"Default Persona" 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
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -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 />
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
)}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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={() => {
|
||||
|
||||
@@ -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'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()}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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" ||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 &&
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -39,7 +39,6 @@ export function UserSettingsModal({
|
||||
onClose: () => void;
|
||||
defaultModel: string | null;
|
||||
}) {
|
||||
const { inputPrompts, refreshInputPrompts } = useChatContext();
|
||||
const {
|
||||
refreshUser,
|
||||
user,
|
||||
|
||||
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
|
||||
65
web/src/app/chat/shared_chat_search/FunctionalWrapper.tsx
Normal file
65
web/src/app/chat/shared_chat_search/FunctionalWrapper.tsx
Normal 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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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 () => {
|
||||
|
||||
@@ -151,7 +151,7 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
|
||||
cursor-pointer
|
||||
${
|
||||
isSelected
|
||||
? "bg-background-strong"
|
||||
? "bg-background-200"
|
||||
: "hover:bg-accent-background-hovered"
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -151,7 +151,7 @@ export function AccessTypeGroupSelector({
|
||||
cursor-pointer
|
||||
${
|
||||
isSelected
|
||||
? "bg-background-strong"
|
||||
? "bg-background-200"
|
||||
: "hover:bg-accent-background-hovered"
|
||||
}
|
||||
`}
|
||||
|
||||
@@ -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 })}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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)}
|
||||
|
||||
@@ -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>
|
||||
))}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 };
|
||||
};
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
interface ChatContextProps {
|
||||
chatSessions: ChatSession[];
|
||||
toggledSidebar: boolean;
|
||||
sidebarInitiallyVisible: boolean;
|
||||
availableSources: ValidSources[];
|
||||
ccPairs: CCPairBasicInfo[];
|
||||
tags: Tag[];
|
||||
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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.."
|
||||
|
||||
@@ -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}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"
|
||||
}`}
|
||||
>
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = ({
|
||||
|
||||
67
web/src/components/modals/ConfirmEntityModal.tsx
Normal file
67
web/src/components/modals/ConfirmEntityModal.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -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>
|
||||
);
|
||||
};
|
||||
@@ -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 ? (
|
||||
|
||||
@@ -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: {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -31,3 +31,7 @@ export const LocalStorageKeys = {
|
||||
SHOW_SHORTCUTS: "showShortcuts",
|
||||
USE_ONYX_AS_NEW_TAB: "useOnyxAsDefaultNewTab",
|
||||
};
|
||||
|
||||
export const SEARCH_PARAMS = {
|
||||
DEFAULT_SIDEBAR_OFF: "defaultSidebarOff",
|
||||
};
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)",
|
||||
|
||||
Reference in New Issue
Block a user