mirror of
https://github.com/onyx-dot-app/onyx.git
synced 2026-02-19 08:45:47 +00:00
Compare commits
1 Commits
billing_fi
...
bb
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8fed028cc1 |
@@ -4,9 +4,6 @@ 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' }}
|
||||
|
||||
@@ -1,32 +0,0 @@
|
||||
"""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,44 +3,42 @@ from typing import Any
|
||||
|
||||
from onyx.background.celery.tasks.beat_schedule import BEAT_EXPIRES_DEFAULT
|
||||
from onyx.background.celery.tasks.beat_schedule import (
|
||||
beat_system_tasks as base_beat_system_tasks,
|
||||
cloud_tasks_to_schedule as base_cloud_tasks_to_schedule,
|
||||
)
|
||||
from onyx.background.celery.tasks.beat_schedule import (
|
||||
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,
|
||||
tasks_to_schedule as base_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_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,
|
||||
},
|
||||
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,
|
||||
},
|
||||
{
|
||||
"name": "check-ttl-management",
|
||||
"task": OnyxCeleryTask.CHECK_TTL_MANAGEMENT_TASK,
|
||||
"schedule": timedelta(hours=1),
|
||||
"options": {
|
||||
"priority": OnyxCeleryPriority.MEDIUM,
|
||||
"expires": BEAT_EXPIRES_DEFAULT,
|
||||
},
|
||||
"kwargs": {
|
||||
"task_name": OnyxCeleryTask.AUTOGENERATE_USAGE_REPORT_TASK,
|
||||
},
|
||||
]
|
||||
)
|
||||
},
|
||||
{
|
||||
"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] = []
|
||||
|
||||
@@ -67,14 +65,9 @@ if not MULTI_TENANT:
|
||||
]
|
||||
|
||||
|
||||
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_cloud_tasks_to_schedule() -> list[dict[str, Any]]:
|
||||
return ee_cloud_tasks_to_schedule + base_cloud_tasks_to_schedule
|
||||
|
||||
|
||||
def get_tasks_to_schedule() -> list[dict[str, Any]]:
|
||||
return ee_tasks_to_schedule + base_get_tasks_to_schedule()
|
||||
return ee_tasks_to_schedule + base_tasks_to_schedule
|
||||
|
||||
@@ -77,5 +77,3 @@ POSTHOG_HOST = os.environ.get("POSTHOG_HOST") or "https://us.i.posthog.com"
|
||||
HUBSPOT_TRACKING_URL = os.environ.get("HUBSPOT_TRACKING_URL")
|
||||
|
||||
ANONYMOUS_USER_COOKIE_NAME = "onyx_anonymous_user"
|
||||
|
||||
GATED_TENANTS_KEY = "gated_tenants"
|
||||
|
||||
@@ -15,9 +15,6 @@ 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")
|
||||
@@ -26,22 +23,19 @@ def make_persona_private(
|
||||
).delete(synchronize_session="fetch")
|
||||
|
||||
if user_ids:
|
||||
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))
|
||||
for user_uuid in user_ids:
|
||||
db_session.add(Persona__User(persona_id=persona_id, user_id=user_uuid))
|
||||
|
||||
create_notification(
|
||||
user_id=user_id,
|
||||
user_id=user_uuid,
|
||||
notif_type=NotificationType.PERSONA_SHARED,
|
||||
db_session=db_session,
|
||||
additional_data=PersonaSharedNotificationData(
|
||||
persona_id=persona_id,
|
||||
).model_dump(),
|
||||
)
|
||||
|
||||
if group_ids:
|
||||
group_ids_set = set(group_ids)
|
||||
for group_id in group_ids_set:
|
||||
for group_id in group_ids:
|
||||
db_session.add(
|
||||
Persona__UserGroup(persona_id=persona_id, user_group_id=group_id)
|
||||
)
|
||||
|
||||
@@ -18,16 +18,11 @@ from ee.onyx.server.tenants.anonymous_user_path import (
|
||||
from ee.onyx.server.tenants.anonymous_user_path import modify_anonymous_user_path
|
||||
from ee.onyx.server.tenants.anonymous_user_path import validate_anonymous_user_path
|
||||
from ee.onyx.server.tenants.billing import fetch_billing_information
|
||||
from ee.onyx.server.tenants.billing import fetch_stripe_checkout_session
|
||||
from ee.onyx.server.tenants.billing import fetch_tenant_stripe_information
|
||||
from ee.onyx.server.tenants.models import AnonymousUserPath
|
||||
from ee.onyx.server.tenants.models import BillingInformation
|
||||
from ee.onyx.server.tenants.models import ImpersonateRequest
|
||||
from ee.onyx.server.tenants.models import ProductGatingRequest
|
||||
from ee.onyx.server.tenants.models import ProductGatingResponse
|
||||
from ee.onyx.server.tenants.models import SubscriptionSessionResponse
|
||||
from ee.onyx.server.tenants.models import SubscriptionStatusResponse
|
||||
from ee.onyx.server.tenants.product_gating import store_product_gating
|
||||
from ee.onyx.server.tenants.provisioning import delete_user_from_control_plane
|
||||
from ee.onyx.server.tenants.user_mapping import get_tenant_id_for_email
|
||||
from ee.onyx.server.tenants.user_mapping import remove_all_users_from_tenant
|
||||
@@ -44,9 +39,12 @@ from onyx.db.auth import get_user_count
|
||||
from onyx.db.engine import get_current_tenant_id
|
||||
from onyx.db.engine import get_session
|
||||
from onyx.db.engine import get_session_with_tenant
|
||||
from onyx.db.notification import create_notification
|
||||
from onyx.db.users import delete_user_from_db
|
||||
from onyx.db.users import get_user_by_email
|
||||
from onyx.server.manage.models import UserByEmail
|
||||
from onyx.server.settings.store import load_settings
|
||||
from onyx.server.settings.store import store_settings
|
||||
from onyx.utils.logger import setup_logger
|
||||
from shared_configs.contextvars import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
|
||||
@@ -128,29 +126,37 @@ async def login_as_anonymous_user(
|
||||
@router.post("/product-gating")
|
||||
def gate_product(
|
||||
product_gating_request: ProductGatingRequest, _: None = Depends(control_plane_dep)
|
||||
) -> ProductGatingResponse:
|
||||
) -> None:
|
||||
"""
|
||||
Gating the product means that the product is not available to the tenant.
|
||||
They will be directed to the billing page.
|
||||
We gate the product when their subscription has ended.
|
||||
We gate the product when
|
||||
1) User has ended free trial without adding payment method
|
||||
2) User's card has declined
|
||||
"""
|
||||
try:
|
||||
store_product_gating(
|
||||
product_gating_request.tenant_id, product_gating_request.application_status
|
||||
)
|
||||
return ProductGatingResponse(updated=True, error=None)
|
||||
tenant_id = product_gating_request.tenant_id
|
||||
token = CURRENT_TENANT_ID_CONTEXTVAR.set(tenant_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to gate product")
|
||||
return ProductGatingResponse(updated=False, error=str(e))
|
||||
settings = load_settings()
|
||||
settings.product_gating = product_gating_request.product_gating
|
||||
store_settings(settings)
|
||||
|
||||
if product_gating_request.notification:
|
||||
with get_session_with_tenant(tenant_id) as db_session:
|
||||
create_notification(None, product_gating_request.notification, db_session)
|
||||
|
||||
if token is not None:
|
||||
CURRENT_TENANT_ID_CONTEXTVAR.reset(token)
|
||||
|
||||
|
||||
@router.get("/billing-information")
|
||||
@router.get("/billing-information", response_model=BillingInformation)
|
||||
async def billing_information(
|
||||
_: User = Depends(current_admin_user),
|
||||
) -> BillingInformation | SubscriptionStatusResponse:
|
||||
) -> BillingInformation:
|
||||
logger.info("Fetching billing information")
|
||||
return fetch_billing_information(CURRENT_TENANT_ID_CONTEXTVAR.get())
|
||||
return BillingInformation(
|
||||
**fetch_billing_information(CURRENT_TENANT_ID_CONTEXTVAR.get())
|
||||
)
|
||||
|
||||
|
||||
@router.post("/create-customer-portal-session")
|
||||
@@ -163,10 +169,9 @@ async def create_customer_portal_session(_: User = Depends(current_admin_user))
|
||||
if not stripe_customer_id:
|
||||
raise HTTPException(status_code=400, detail="Stripe customer ID not found")
|
||||
logger.info(stripe_customer_id)
|
||||
|
||||
portal_session = stripe.billing_portal.Session.create(
|
||||
customer=stripe_customer_id,
|
||||
return_url=f"{WEB_DOMAIN}/admin/billing",
|
||||
return_url=f"{WEB_DOMAIN}/admin/cloud-settings",
|
||||
)
|
||||
logger.info(portal_session)
|
||||
return {"url": portal_session.url}
|
||||
@@ -175,20 +180,6 @@ async def create_customer_portal_session(_: User = Depends(current_admin_user))
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/create-subscription-session")
|
||||
async def create_subscription_session(
|
||||
_: User = Depends(current_admin_user),
|
||||
) -> SubscriptionSessionResponse:
|
||||
try:
|
||||
tenant_id = CURRENT_TENANT_ID_CONTEXTVAR.get()
|
||||
session_id = fetch_stripe_checkout_session(tenant_id)
|
||||
return SubscriptionSessionResponse(sessionId=session_id)
|
||||
|
||||
except Exception as e:
|
||||
logger.exception("Failed to create resubscription session")
|
||||
raise HTTPException(status_code=500, detail=str(e))
|
||||
|
||||
|
||||
@router.post("/impersonate")
|
||||
async def impersonate_user(
|
||||
impersonate_request: ImpersonateRequest,
|
||||
|
||||
@@ -6,7 +6,6 @@ import stripe
|
||||
from ee.onyx.configs.app_configs import STRIPE_PRICE_ID
|
||||
from ee.onyx.configs.app_configs import STRIPE_SECRET_KEY
|
||||
from ee.onyx.server.tenants.access import generate_data_plane_token
|
||||
from ee.onyx.server.tenants.models import BillingInformation
|
||||
from onyx.configs.app_configs import CONTROL_PLANE_API_BASE_URL
|
||||
from onyx.utils.logger import setup_logger
|
||||
|
||||
@@ -15,19 +14,6 @@ stripe.api_key = STRIPE_SECRET_KEY
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def fetch_stripe_checkout_session(tenant_id: str) -> str:
|
||||
token = generate_data_plane_token()
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
url = f"{CONTROL_PLANE_API_BASE_URL}/create-checkout-session"
|
||||
params = {"tenant_id": tenant_id}
|
||||
response = requests.post(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
return response.json()["sessionId"]
|
||||
|
||||
|
||||
def fetch_tenant_stripe_information(tenant_id: str) -> dict:
|
||||
token = generate_data_plane_token()
|
||||
headers = {
|
||||
@@ -41,7 +27,7 @@ def fetch_tenant_stripe_information(tenant_id: str) -> dict:
|
||||
return response.json()
|
||||
|
||||
|
||||
def fetch_billing_information(tenant_id: str) -> BillingInformation:
|
||||
def fetch_billing_information(tenant_id: str) -> dict:
|
||||
logger.info("Fetching billing information")
|
||||
token = generate_data_plane_token()
|
||||
headers = {
|
||||
@@ -52,7 +38,7 @@ def fetch_billing_information(tenant_id: str) -> BillingInformation:
|
||||
params = {"tenant_id": tenant_id}
|
||||
response = requests.get(url, headers=headers, params=params)
|
||||
response.raise_for_status()
|
||||
billing_info = BillingInformation(**response.json())
|
||||
billing_info = response.json()
|
||||
return billing_info
|
||||
|
||||
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
from datetime import datetime
|
||||
|
||||
from pydantic import BaseModel
|
||||
|
||||
from onyx.server.settings.models import ApplicationStatus
|
||||
from onyx.configs.constants import NotificationType
|
||||
from onyx.server.settings.models import GatingType
|
||||
|
||||
|
||||
class CheckoutSessionCreationRequest(BaseModel):
|
||||
@@ -16,24 +15,15 @@ class CreateTenantRequest(BaseModel):
|
||||
|
||||
class ProductGatingRequest(BaseModel):
|
||||
tenant_id: str
|
||||
application_status: ApplicationStatus
|
||||
|
||||
|
||||
class SubscriptionStatusResponse(BaseModel):
|
||||
subscribed: bool
|
||||
product_gating: GatingType
|
||||
notification: NotificationType | None = None
|
||||
|
||||
|
||||
class BillingInformation(BaseModel):
|
||||
stripe_subscription_id: str
|
||||
status: str
|
||||
current_period_start: datetime
|
||||
current_period_end: datetime
|
||||
number_of_seats: int
|
||||
cancel_at_period_end: bool
|
||||
canceled_at: datetime | None
|
||||
trial_start: datetime | None
|
||||
trial_end: datetime | None
|
||||
seats: int
|
||||
subscription_status: str
|
||||
billing_start: str
|
||||
billing_end: str
|
||||
payment_method_enabled: bool
|
||||
|
||||
|
||||
@@ -58,12 +48,3 @@ class TenantDeletionPayload(BaseModel):
|
||||
|
||||
class AnonymousUserPath(BaseModel):
|
||||
anonymous_user_path: str | None
|
||||
|
||||
|
||||
class ProductGatingResponse(BaseModel):
|
||||
updated: bool
|
||||
error: str | None
|
||||
|
||||
|
||||
class SubscriptionSessionResponse(BaseModel):
|
||||
sessionId: str
|
||||
|
||||
@@ -1,50 +0,0 @@
|
||||
from typing import cast
|
||||
|
||||
from ee.onyx.configs.app_configs import GATED_TENANTS_KEY
|
||||
from onyx.configs.constants import ONYX_CLOUD_TENANT_ID
|
||||
from onyx.redis.redis_pool import get_redis_replica_client
|
||||
from onyx.server.settings.models import ApplicationStatus
|
||||
from onyx.server.settings.store import load_settings
|
||||
from onyx.server.settings.store import store_settings
|
||||
from onyx.setup import setup_logger
|
||||
from shared_configs.contextvars import CURRENT_TENANT_ID_CONTEXTVAR
|
||||
|
||||
logger = setup_logger()
|
||||
|
||||
|
||||
def update_tenant_gating(tenant_id: str, status: ApplicationStatus) -> None:
|
||||
redis_client = get_redis_replica_client(tenant_id=ONYX_CLOUD_TENANT_ID)
|
||||
|
||||
# Store the full status
|
||||
status_key = f"tenant:{tenant_id}:status"
|
||||
redis_client.set(status_key, status.value)
|
||||
|
||||
# Maintain the GATED_ACCESS set
|
||||
if status == ApplicationStatus.GATED_ACCESS:
|
||||
redis_client.sadd(GATED_TENANTS_KEY, tenant_id)
|
||||
else:
|
||||
redis_client.srem(GATED_TENANTS_KEY, tenant_id)
|
||||
|
||||
|
||||
def store_product_gating(tenant_id: str, application_status: ApplicationStatus) -> None:
|
||||
try:
|
||||
token = CURRENT_TENANT_ID_CONTEXTVAR.set(tenant_id)
|
||||
|
||||
settings = load_settings()
|
||||
settings.application_status = application_status
|
||||
store_settings(settings)
|
||||
|
||||
# Store gated tenant information in Redis
|
||||
update_tenant_gating(tenant_id, application_status)
|
||||
|
||||
if token is not None:
|
||||
CURRENT_TENANT_ID_CONTEXTVAR.reset(token)
|
||||
|
||||
except Exception:
|
||||
logger.exception("Failed to gate product")
|
||||
raise
|
||||
|
||||
|
||||
def get_gated_tenants() -> set[str]:
|
||||
redis_client = get_redis_replica_client(tenant_id=ONYX_CLOUD_TENANT_ID)
|
||||
return cast(set[str], redis_client.smembers(GATED_TENANTS_KEY))
|
||||
@@ -28,9 +28,3 @@ 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,7 +12,6 @@ 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
|
||||
@@ -321,7 +320,6 @@ 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")
|
||||
@@ -375,11 +373,8 @@ async def embed_text(
|
||||
|
||||
elapsed = time.monotonic() - start
|
||||
logger.info(
|
||||
f"event=embedding_provider "
|
||||
f"texts={len(texts)} "
|
||||
f"chars={total_chars} "
|
||||
f"provider={provider_type} "
|
||||
f"elapsed={elapsed:.2f}"
|
||||
f"Successfully embedded {len(texts)} texts with {total_chars} total characters "
|
||||
f"with provider {provider_type} in {elapsed:.2f}"
|
||||
)
|
||||
elif model_name is not None:
|
||||
logger.info(
|
||||
@@ -408,14 +403,6 @@ 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(
|
||||
@@ -468,15 +455,8 @@ async def litellm_rerank(
|
||||
|
||||
|
||||
@router.post("/bi-encoder-embed")
|
||||
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"
|
||||
embed_request: EmbedRequest,
|
||||
) -> EmbedResponse:
|
||||
if not embed_request.texts:
|
||||
raise HTTPException(status_code=400, detail="No texts to be embedded")
|
||||
@@ -504,7 +484,6 @@ 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,7 +16,6 @@ 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
|
||||
@@ -59,10 +58,12 @@ def _move_files_recursively(source: Path, dest: Path, overwrite: bool = False) -
|
||||
|
||||
@asynccontextmanager
|
||||
async def lifespan(app: FastAPI) -> AsyncGenerator:
|
||||
gpu_type = get_gpu_type()
|
||||
logger.notice(f"Torch GPU Detection: gpu_type={gpu_type}")
|
||||
|
||||
app.state.gpu_type = gpu_type
|
||||
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")
|
||||
|
||||
if TEMP_HF_CACHE_PATH.is_dir():
|
||||
logger.notice("Moving contents of temp_huggingface to huggingface cache.")
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
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")
|
||||
|
||||
|
||||
@@ -13,7 +11,10 @@ async def healthcheck() -> Response:
|
||||
|
||||
|
||||
@router.get("/gpu-status")
|
||||
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}
|
||||
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"}
|
||||
|
||||
@@ -8,9 +8,6 @@ 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()
|
||||
@@ -61,12 +58,3 @@ 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,56 +1,41 @@
|
||||
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.redis.redis_pool import get_redis_replica_client
|
||||
from onyx.utils.logger import setup_logger
|
||||
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
|
||||
|
||||
task_logger = get_task_logger(__name__)
|
||||
logger = setup_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.last_beat_multiplier = CLOUD_BEAT_MULTIPLIER_DEFAULT
|
||||
|
||||
self._reload_interval = timedelta(
|
||||
seconds=DynamicTenantScheduler.RELOAD_INTERVAL
|
||||
)
|
||||
self._reload_interval = timedelta(minutes=2)
|
||||
self._last_reload = self.app.now() - self._reload_interval
|
||||
|
||||
# Let the parent class handle store initialization
|
||||
self.setup_schedule()
|
||||
self._try_updating_schedule()
|
||||
task_logger.info(
|
||||
f"DynamicTenantScheduler initialized: reload_interval={self._reload_interval}"
|
||||
)
|
||||
logger.info(f"Set reload interval to {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()
|
||||
@@ -59,35 +44,36 @@ class DynamicTenantScheduler(PersistentScheduler):
|
||||
self._last_reload is None
|
||||
or (now - self._last_reload) > self._reload_interval
|
||||
):
|
||||
task_logger.debug("Reload interval reached, initiating task update")
|
||||
logger.info("Reload interval reached, initiating task update")
|
||||
try:
|
||||
self._try_updating_schedule()
|
||||
except (AttributeError, KeyError):
|
||||
task_logger.exception("Failed to process task configuration")
|
||||
except Exception:
|
||||
task_logger.exception("Unexpected error updating tasks")
|
||||
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)}")
|
||||
|
||||
self._last_reload = now
|
||||
|
||||
logger.info("Task update completed, reset reload timer")
|
||||
return retval
|
||||
|
||||
def _generate_schedule(
|
||||
self, tenant_ids: list[str] | list[None], beat_multiplier: float
|
||||
self, tenant_ids: list[str] | list[None]
|
||||
) -> 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 are system wide and thus only need to be on the beat schedule
|
||||
# once for all tenants
|
||||
# cloud tasks only need the single task beat across 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(
|
||||
beat_multiplier
|
||||
)
|
||||
cloud_tasks_to_schedule: list[
|
||||
dict[str, Any]
|
||||
] = get_cloud_tasks_to_schedule()
|
||||
for task in cloud_tasks_to_schedule:
|
||||
task_name = task["name"]
|
||||
cloud_task = {
|
||||
@@ -96,14 +82,11 @@ class DynamicTenantScheduler(PersistentScheduler):
|
||||
"kwargs": task.get("kwargs", {}),
|
||||
}
|
||||
if options := task.get("options"):
|
||||
task_logger.debug(f"Adding options to task {task_name}: {options}")
|
||||
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"
|
||||
)
|
||||
@@ -112,7 +95,7 @@ class DynamicTenantScheduler(PersistentScheduler):
|
||||
|
||||
for tenant_id in tenant_ids:
|
||||
if IGNORED_SYNCING_TENANT_LIST and tenant_id in IGNORED_SYNCING_TENANT_LIST:
|
||||
task_logger.debug(
|
||||
logger.info(
|
||||
f"Skipping tenant {tenant_id} as it is in the ignored syncing list"
|
||||
)
|
||||
continue
|
||||
@@ -121,14 +104,14 @@ class DynamicTenantScheduler(PersistentScheduler):
|
||||
task_name = task["name"]
|
||||
tenant_task_name = f"{task['name']}-{tenant_id}"
|
||||
|
||||
task_logger.debug(f"Creating task configuration for {tenant_task_name}")
|
||||
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"):
|
||||
task_logger.debug(
|
||||
logger.debug(
|
||||
f"Adding options to task {tenant_task_name}: {options}"
|
||||
)
|
||||
tenant_task["options"] = options
|
||||
@@ -138,57 +121,44 @@ 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
|
||||
|
||||
r = get_redis_replica_client(tenant_id=ONYX_CLOUD_TENANT_ID)
|
||||
|
||||
task_logger.debug("_try_updating_schedule starting")
|
||||
logger.info("_try_updating_schedule starting")
|
||||
|
||||
tenant_ids = get_all_tenant_ids()
|
||||
task_logger.debug(f"Found {len(tenant_ids)} IDs")
|
||||
logger.info(f"Found {len(tenant_ids)} IDs")
|
||||
|
||||
# get current schedule and extract current tenants
|
||||
current_schedule = self.schedule.items()
|
||||
|
||||
# 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}"
|
||||
)
|
||||
# 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
|
||||
|
||||
new_schedule = self._generate_schedule(tenant_ids, beat_multiplier)
|
||||
# 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
|
||||
|
||||
# if the schedule or beat multiplier has changed, update
|
||||
while True:
|
||||
if beat_multiplier != self.last_beat_multiplier:
|
||||
do_update = True
|
||||
break
|
||||
# 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 not DynamicTenantScheduler._compare_schedules(
|
||||
current_schedule, new_schedule
|
||||
):
|
||||
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}")
|
||||
|
||||
break
|
||||
new_schedule = self._generate_schedule(tenant_ids)
|
||||
|
||||
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}"
|
||||
if DynamicTenantScheduler._compare_schedules(current_schedule, new_schedule):
|
||||
logger.info(
|
||||
"_try_updating_schedule: Current schedule is up to date, no changes needed"
|
||||
)
|
||||
return
|
||||
|
||||
# schedule needs updating
|
||||
task_logger.debug(
|
||||
logger.info(
|
||||
"Schedule update required",
|
||||
extra={
|
||||
"new_tasks": len(new_schedule),
|
||||
@@ -215,19 +185,11 @@ class DynamicTenantScheduler(PersistentScheduler):
|
||||
# Ensure changes are persisted
|
||||
self.sync()
|
||||
|
||||
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
|
||||
logger.info("_try_updating_schedule: Schedule updated successfully")
|
||||
|
||||
@staticmethod
|
||||
def _compare_schedules(schedule1: dict, schedule2: dict) -> bool:
|
||||
"""Compare schedules by task name only to determine if an update is needed.
|
||||
"""Compare schedules 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())
|
||||
@@ -239,7 +201,7 @@ class DynamicTenantScheduler(PersistentScheduler):
|
||||
|
||||
@beat_init.connect
|
||||
def on_beat_init(sender: Any, **kwargs: Any) -> None:
|
||||
task_logger.info("beat_init signal received.")
|
||||
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,4 +1,3 @@
|
||||
import copy
|
||||
from datetime import timedelta
|
||||
from typing import Any
|
||||
|
||||
@@ -19,7 +18,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_MULTIPLIER_DEFAULT = 8.0
|
||||
CLOUD_BEAT_SCHEDULE_MULTIPLIER = 8
|
||||
|
||||
# tasks that run in either self-hosted on cloud
|
||||
beat_task_templates: list[dict] = []
|
||||
@@ -122,7 +121,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_task["schedule"] = task_schedule * CLOUD_BEAT_SCHEDULE_MULTIPLIER
|
||||
cloud_task["options"] = {}
|
||||
cloud_task["options"]["priority"] = OnyxCeleryPriority.HIGHEST
|
||||
cloud_task["options"]["expires"] = BEAT_EXPIRES_DEFAULT
|
||||
@@ -142,9 +141,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 seen
|
||||
# by the DynamicTenantScheduler as system wide task and not a per tenant task
|
||||
beat_system_tasks: list[dict] = [
|
||||
# the name attribute must start with ONYX_CLOUD_CELERY_TASK_PREFIX = "cloud" to be filtered
|
||||
# by the DynamicTenantScheduler
|
||||
cloud_tasks_to_schedule: list[dict] = [
|
||||
# cloud specific tasks
|
||||
{
|
||||
"name": f"{ONYX_CLOUD_CELERY_TASK_PREFIX}_check-alembic",
|
||||
@@ -158,45 +157,18 @@ beat_system_tasks: 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 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_cloud_tasks_to_schedule() -> list[dict[str, Any]]:
|
||||
return cloud_tasks_to_schedule
|
||||
|
||||
|
||||
def get_tasks_to_schedule() -> list[dict[str, Any]]:
|
||||
|
||||
@@ -8,7 +8,6 @@ from celery.exceptions import SoftTimeLimitExceeded
|
||||
from redis.lock import Lock as RedisLock
|
||||
from tenacity import RetryError
|
||||
|
||||
from ee.onyx.server.tenants.product_gating import get_gated_tenants
|
||||
from onyx.access.access import get_access_for_document
|
||||
from onyx.background.celery.apps.app_base import task_logger
|
||||
from onyx.background.celery.tasks.beat_schedule import BEAT_EXPIRES_DEFAULT
|
||||
@@ -253,11 +252,7 @@ def cloud_beat_task_generator(
|
||||
|
||||
try:
|
||||
tenant_ids = get_all_tenant_ids()
|
||||
gated_tenants = get_gated_tenants()
|
||||
for tenant_id in tenant_ids:
|
||||
if tenant_id in gated_tenants:
|
||||
continue
|
||||
|
||||
current_time = time.monotonic()
|
||||
if current_time - last_lock_time >= (CELERY_GENERIC_BEAT_LOCK_TIMEOUT / 4):
|
||||
lock_beat.reacquire()
|
||||
|
||||
@@ -346,9 +346,6 @@ 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,25 +65,10 @@ 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 = (
|
||||
@@ -100,39 +85,6 @@ 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,
|
||||
@@ -158,10 +110,8 @@ class AirtableConnector(LoadConnector):
|
||||
if field_type == "multipleRecordLinks":
|
||||
return []
|
||||
|
||||
# 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
|
||||
)
|
||||
# default link to use for non-attachment fields
|
||||
default_link = f"https://airtable.com/{base_id}/{table_id}/{record_id}"
|
||||
|
||||
if field_type == "multipleAttachments":
|
||||
attachment_texts: list[tuple[str, str]] = []
|
||||
@@ -215,16 +165,17 @@ class AirtableConnector(LoadConnector):
|
||||
extension=file_ext,
|
||||
)
|
||||
if attachment_text:
|
||||
# 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,
|
||||
)
|
||||
# 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"
|
||||
)
|
||||
attachment_texts.append(
|
||||
(f"{filename}:\n{attachment_text}", attachment_link)
|
||||
)
|
||||
|
||||
@@ -204,14 +204,6 @@ 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,
|
||||
@@ -518,7 +510,6 @@ 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:
|
||||
@@ -599,23 +590,6 @@ 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,23 +146,6 @@ 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,
|
||||
@@ -183,10 +166,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(cleaned_metadata_json),
|
||||
METADATA: json.dumps(document.metadata),
|
||||
# Save as a list for efficient extraction as an Attribute
|
||||
METADATA_LIST: metadata_list,
|
||||
METADATA_SUFFIX: remove_invalid_unicode_chars(chunk.metadata_suffix_keyword),
|
||||
METADATA_LIST: chunk.source_document.get_metadata_str_attributes(),
|
||||
METADATA_SUFFIX: 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),
|
||||
|
||||
@@ -32,7 +32,6 @@ 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
|
||||
@@ -57,6 +56,7 @@ 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,10 +72,6 @@ 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,
|
||||
@@ -110,25 +106,6 @@ 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,
|
||||
|
||||
@@ -12,10 +12,10 @@ class PageType(str, Enum):
|
||||
SEARCH = "search"
|
||||
|
||||
|
||||
class ApplicationStatus(str, Enum):
|
||||
PAYMENT_REMINDER = "payment_reminder"
|
||||
GATED_ACCESS = "gated_access"
|
||||
ACTIVE = "active"
|
||||
class GatingType(str, Enum):
|
||||
FULL = "full" # Complete restriction of access to the product or service
|
||||
PARTIAL = "partial" # Full access but warning (no credit card on file)
|
||||
NONE = "none" # No restrictions, full access to all features
|
||||
|
||||
|
||||
class Notification(BaseModel):
|
||||
@@ -43,7 +43,7 @@ class Settings(BaseModel):
|
||||
|
||||
maximum_chat_retention_days: int | None = None
|
||||
gpu_enabled: bool | None = None
|
||||
application_status: ApplicationStatus = ApplicationStatus.ACTIVE
|
||||
product_gating: GatingType = GatingType.NONE
|
||||
anonymous_user_enabled: bool | None = None
|
||||
pro_search_disabled: bool | None = None
|
||||
auto_scroll: bool | None = None
|
||||
|
||||
@@ -86,10 +86,7 @@ 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: dict[str, Any] = {}
|
||||
|
||||
if len(function_calls) == 0:
|
||||
return results
|
||||
results = {}
|
||||
|
||||
with ThreadPoolExecutor(max_workers=len(function_calls)) as executor:
|
||||
future_to_id = {
|
||||
|
||||
@@ -9,8 +9,6 @@ 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
|
||||
@@ -48,8 +46,6 @@ 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")
|
||||
@@ -64,13 +60,7 @@ 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}"
|
||||
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}"
|
||||
|
||||
link_base = f"https://airtable.com/{base_id}/{table_id}"
|
||||
sections = []
|
||||
|
||||
if not all_fields_as_metadata:
|
||||
@@ -224,7 +214,6 @@ 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",
|
||||
@@ -245,7 +234,6 @@ def test_airtable_connector_basic(
|
||||
)
|
||||
],
|
||||
all_fields_as_metadata=False,
|
||||
view_id=BASE_VIEW_ID,
|
||||
),
|
||||
]
|
||||
|
||||
@@ -297,81 +285,6 @@ 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(mode="json"),
|
||||
json=persona_creation_request.model_dump(),
|
||||
headers=user_performing_action.headers
|
||||
if user_performing_action
|
||||
else GENERAL_HEADERS,
|
||||
@@ -119,7 +119,6 @@ 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,
|
||||
@@ -147,7 +146,7 @@ class PersonaManager:
|
||||
|
||||
response = requests.patch(
|
||||
f"{API_SERVER_URL}/persona/{persona.id}",
|
||||
json=persona_update_request.model_dump(mode="json"),
|
||||
json=persona_update_request.model_dump(),
|
||||
headers=user_performing_action.headers
|
||||
if user_performing_action
|
||||
else GENERAL_HEADERS,
|
||||
|
||||
@@ -58,7 +58,6 @@ 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)
|
||||
@@ -140,14 +139,9 @@ 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 2",
|
||||
users=[admin_user.id, admin_user.id],
|
||||
description="Updated by admin",
|
||||
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,13 +3,7 @@
|
||||
import React from "react";
|
||||
import { Option } from "@/components/Dropdown";
|
||||
import { generateRandomIconShape } from "@/lib/assistantIconUtils";
|
||||
import {
|
||||
CCPairBasicInfo,
|
||||
DocumentSet,
|
||||
User,
|
||||
UserGroup,
|
||||
UserRole,
|
||||
} from "@/lib/types";
|
||||
import { CCPairBasicInfo, DocumentSet, User, UserGroup } from "@/lib/types";
|
||||
import { Separator } from "@/components/ui/separator";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { ArrayHelpers, FieldArray, Form, Formik, FormikProps } from "formik";
|
||||
@@ -39,8 +33,9 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import Link from "next/link";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useRouter } 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";
|
||||
@@ -76,11 +71,11 @@ import {
|
||||
Option as DropdownOption,
|
||||
} from "@/components/Dropdown";
|
||||
import { SourceChip } from "@/app/chat/input/ChatInputBar";
|
||||
import { TagIcon, UserIcon, XIcon, InfoIcon } from "lucide-react";
|
||||
import { TagIcon, UserIcon, XIcon } from "lucide-react";
|
||||
import { LLMSelector } from "@/components/llm/LLMSelector";
|
||||
import useSWR from "swr";
|
||||
import { errorHandlingFetcher } from "@/lib/fetcher";
|
||||
import { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import Title from "@/components/ui/title";
|
||||
import { SEARCH_TOOL_ID } from "@/app/chat/tools/constants";
|
||||
|
||||
@@ -132,8 +127,6 @@ 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 } =
|
||||
@@ -223,8 +216,6 @@ export function AssistantEditor({
|
||||
enabledToolsMap[tool.id] = personaCurrentToolIds.includes(tool.id);
|
||||
});
|
||||
|
||||
const [showVisibilityWarning, setShowVisibilityWarning] = useState(false);
|
||||
|
||||
const initialValues = {
|
||||
name: existingPersona?.name ?? "",
|
||||
description: existingPersona?.description ?? "",
|
||||
@@ -261,7 +252,6 @@ export function AssistantEditor({
|
||||
(u) => u.id !== existingPersona.owner?.id
|
||||
) ?? [],
|
||||
selectedGroups: existingPersona?.groups ?? [],
|
||||
is_default_persona: existingPersona?.is_default_persona ?? false,
|
||||
};
|
||||
|
||||
interface AssistantPrompt {
|
||||
@@ -318,12 +308,24 @@ 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) {
|
||||
@@ -344,7 +346,9 @@ export function AssistantEditor({
|
||||
if (response.ok) {
|
||||
await refreshAssistants();
|
||||
router.push(
|
||||
isAdminPage ? `/admin/assistants?u=${Date.now()}` : `/chat`
|
||||
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN
|
||||
? `/admin/assistants?u=${Date.now()}`
|
||||
: `/chat`
|
||||
);
|
||||
} else {
|
||||
setPopup({
|
||||
@@ -370,9 +374,8 @@ export function AssistantEditor({
|
||||
<BackButton />
|
||||
</div>
|
||||
)}
|
||||
|
||||
{labelToDelete && (
|
||||
<ConfirmEntityModal
|
||||
<DeleteEntityModal
|
||||
entityType="label"
|
||||
entityName={labelToDelete.name}
|
||||
onClose={() => setLabelToDelete(null)}
|
||||
@@ -395,7 +398,7 @@ export function AssistantEditor({
|
||||
/>
|
||||
)}
|
||||
{deleteModalOpen && existingPersona && (
|
||||
<ConfirmEntityModal
|
||||
<DeleteEntityModal
|
||||
entityType="Persona"
|
||||
entityName={existingPersona.name}
|
||||
onClose={closeDeleteModal}
|
||||
@@ -436,7 +439,6 @@ 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",
|
||||
@@ -457,19 +459,6 @@ 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 (
|
||||
@@ -510,6 +499,7 @@ 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
|
||||
@@ -573,9 +563,8 @@ export function AssistantEditor({
|
||||
}
|
||||
|
||||
await refreshAssistants();
|
||||
|
||||
router.push(
|
||||
isAdminPage
|
||||
redirectType === SuccessfulPersonaUpdateRedirectType.ADMIN
|
||||
? `/admin/assistants?u=${Date.now()}`
|
||||
: `/chat?assistantId=${assistantId}`
|
||||
);
|
||||
@@ -1016,22 +1005,6 @@ 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>
|
||||
@@ -1041,60 +1014,22 @@ export function AssistantEditor({
|
||||
|
||||
<div className="min-h-[100px]">
|
||||
<div className="flex items-center mb-2">
|
||||
<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>
|
||||
<SwitchField
|
||||
name="is_public"
|
||||
size="md"
|
||||
onCheckedChange={(checked) => {
|
||||
setFieldValue("is_public", checked);
|
||||
if (checked) {
|
||||
setFieldValue("selectedUsers", []);
|
||||
setFieldValue("selectedGroups", []);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<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,14 +11,13 @@ 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 { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
|
||||
function PersonaTypeDisplay({ persona }: { persona: Persona }) {
|
||||
if (persona.builtin_persona) {
|
||||
@@ -57,9 +56,6 @@ 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);
|
||||
@@ -130,39 +126,11 @@ 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 && (
|
||||
<ConfirmEntityModal
|
||||
<DeleteEntityModal
|
||||
entityType="Persona"
|
||||
entityName={personaToDelete.name}
|
||||
onClose={closeDeleteModal}
|
||||
@@ -170,35 +138,8 @@ 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",
|
||||
"Featured Assistant",
|
||||
"Is Visible",
|
||||
"Delete",
|
||||
]}
|
||||
headers={["Name", "Description", "Type", "Is Visible", "Delete"]}
|
||||
isAdmin={isAdmin}
|
||||
rows={finalPersonas.map((persona) => {
|
||||
const isEditable = editablePersonas.includes(persona);
|
||||
@@ -211,9 +152,7 @@ export function PersonasTable() {
|
||||
className="mr-1 my-auto cursor-pointer"
|
||||
onClick={() =>
|
||||
router.push(
|
||||
`/assistants/edit/${
|
||||
persona.id
|
||||
}?u=${Date.now()}&admin=true`
|
||||
`/admin/assistants/${persona.id}?u=${Date.now()}`
|
||||
)
|
||||
}
|
||||
/>
|
||||
@@ -229,30 +168,6 @@ 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 () => {
|
||||
|
||||
36
web/src/app/admin/assistants/[id]/DeletePersonaButton.tsx
Normal file
36
web/src/app/admin/assistants/[id]/DeletePersonaButton.tsx
Normal file
@@ -0,0 +1,36 @@
|
||||
"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>
|
||||
);
|
||||
}
|
||||
43
web/src/app/admin/assistants/[id]/page.tsx
Normal file
43
web/src/app/admin/assistants/[id]/page.tsx
Normal file
@@ -0,0 +1,43 @@
|
||||
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,22 +261,6 @@ 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
|
||||
|
||||
25
web/src/app/admin/assistants/new/page.tsx
Normal file
25
web/src/app/admin/assistants/new/page.tsx
Normal file
@@ -0,0 +1,25 @@
|
||||
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="/assistants/new?admin=true" text="New Assistant" />
|
||||
<CreateButton href="/admin/assistants/new" text="New Assistant" />
|
||||
|
||||
<Separator />
|
||||
|
||||
|
||||
@@ -100,7 +100,7 @@ export function SlackChannelConfigsTable({
|
||||
slackChannelConfig.persona
|
||||
) ? (
|
||||
<Link
|
||||
href={`/assistants/${slackChannelConfig.persona.id}`}
|
||||
href={`/admin/assistants/${slackChannelConfig.persona.id}`}
|
||||
className="text-primary hover:underline"
|
||||
>
|
||||
{slackChannelConfig.persona.name}
|
||||
|
||||
@@ -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 { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { AdvancedSearchConfiguration } from "../interfaces";
|
||||
import CardSection from "@/components/admin/CardSection";
|
||||
|
||||
@@ -456,7 +456,7 @@ export function CloudModelCard({
|
||||
>
|
||||
{popup}
|
||||
{showDeleteModel && (
|
||||
<ConfirmEntityModal
|
||||
<DeleteEntityModal
|
||||
entityName={model.model_name}
|
||||
entityType="embedding model configuration"
|
||||
onSubmit={() => deleteModel()}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
export enum ApplicationStatus {
|
||||
PAYMENT_REMINDER = "payment_reminder",
|
||||
GATED_ACCESS = "gated_access",
|
||||
ACTIVE = "active",
|
||||
export enum GatingType {
|
||||
FULL = "full",
|
||||
PARTIAL = "partial",
|
||||
NONE = "none",
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
@@ -11,7 +11,7 @@ export interface Settings {
|
||||
needs_reindexing: boolean;
|
||||
gpu_enabled: boolean;
|
||||
pro_search_disabled: boolean | null;
|
||||
application_status: ApplicationStatus;
|
||||
product_gating: GatingType;
|
||||
auto_scroll: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
size = "sm",
|
||||
children,
|
||||
}: SidebarWrapperProps<T>) {
|
||||
const [sidebarVisible, setSidebarVisible] = useState(initiallyToggled);
|
||||
const [toggledSidebar, setToggledSidebar] = 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(!sidebarVisible).toLocaleLowerCase()
|
||||
String(!toggledSidebar).toLocaleLowerCase()
|
||||
),
|
||||
{
|
||||
path: "/",
|
||||
};
|
||||
setSidebarVisible((sidebarVisible) => !sidebarVisible);
|
||||
}, [sidebarVisible]);
|
||||
setToggledSidebar((toggledSidebar) => !toggledSidebar);
|
||||
}, [toggledSidebar]);
|
||||
|
||||
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({
|
||||
sidebarVisible,
|
||||
toggledSidebar,
|
||||
sidebarElementRef,
|
||||
showDocSidebar,
|
||||
setShowDocSidebar,
|
||||
@@ -94,7 +94,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
duration-300
|
||||
ease-in-out
|
||||
${
|
||||
!untoggled && (showDocSidebar || sidebarVisible)
|
||||
!untoggled && (showDocSidebar || toggledSidebar)
|
||||
? "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={sidebarVisible}
|
||||
toggled={toggledSidebar}
|
||||
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={sidebarVisible}
|
||||
sidebarToggled={toggledSidebar}
|
||||
toggleSidebar={toggleSidebar}
|
||||
page="chat"
|
||||
/>
|
||||
@@ -132,7 +132,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
bg-opacity-80
|
||||
duration-300
|
||||
ease-in-out
|
||||
${sidebarVisible ? "w-[250px]" : "w-[0px]"}`}
|
||||
${toggledSidebar ? "w-[250px]" : "w-[0px]"}`}
|
||||
/>
|
||||
|
||||
<div
|
||||
@@ -144,7 +144,7 @@ export default function SidebarWrapper<T extends object>({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<FixedLogo backgroundToggled={sidebarVisible || showDocSidebar} />
|
||||
<FixedLogo backgroundToggled={toggledSidebar || showDocSidebar} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import React, { useState, useRef, useLayoutEffect } from "react";
|
||||
import React, { useContext, 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,12 +26,14 @@ import {
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { PinnedIcon } from "@/components/icons/icons";
|
||||
import { deletePersona } from "@/app/admin/assistants/lib";
|
||||
import {
|
||||
deletePersona,
|
||||
togglePersonaPublicStatus,
|
||||
} 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,
|
||||
@@ -61,7 +63,6 @@ const AssistantCard: React.FC<{
|
||||
const { user, toggleAssistantPinnedStatus } = useUser();
|
||||
const router = useRouter();
|
||||
const { refreshAssistants, pinnedAssistants } = useAssistants();
|
||||
const { popup, setPopup } = usePopup();
|
||||
|
||||
const isOwnedByUser = checkUserOwnsAssistant(user, persona);
|
||||
|
||||
@@ -71,34 +72,7 @@ const AssistantCard: React.FC<{
|
||||
|
||||
const isPaidEnterpriseFeaturesEnabled = usePaidEnterpriseFeaturesEnabled();
|
||||
|
||||
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 handleDelete = () => setActivePopover("delete");
|
||||
const handleEdit = () => {
|
||||
router.push(`/assistants/edit/${persona.id}`);
|
||||
setActivePopover(null);
|
||||
@@ -126,7 +100,6 @@ 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" />
|
||||
@@ -184,84 +157,55 @@ const AssistantCard: React.FC<{
|
||||
<FiMoreHorizontal size={16} />
|
||||
</button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent
|
||||
className={`${
|
||||
isDeleteConfirmation ? "w-64" : "w-32"
|
||||
} z-[10000] p-2`}
|
||||
>
|
||||
{!isDeleteConfirmation ? (
|
||||
<div className="flex flex-col text-sm space-y-1">
|
||||
<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 && (
|
||||
<button
|
||||
onClick={isOwnedByUser ? handleEdit : undefined}
|
||||
className={`w-full flex items-center text-left px-2 py-1 rounded ${
|
||||
onClick={
|
||||
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 && (
|
||||
<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}
|
||||
? () => {
|
||||
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- text-red-600 dark:text-red-400"
|
||||
: "opacity-50 cursor-not-allowed text-red-300 dark:text-red-500"
|
||||
? "hover:bg-neutral-200 dark:hover:bg-neutral-800"
|
||||
: "opacity-50 cursor-not-allowed"
|
||||
}`}
|
||||
disabled={!isOwnedByUser}
|
||||
>
|
||||
<FiTrash size={12} className="inline mr-2" />
|
||||
Delete
|
||||
<FiBarChart size={12} className="inline mr-2" />
|
||||
Stats
|
||||
</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>
|
||||
)}
|
||||
)}
|
||||
<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>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
</div>
|
||||
|
||||
@@ -5,8 +5,10 @@ import { useRouter } from "next/navigation";
|
||||
import AssistantCard from "./AssistantCard";
|
||||
import { useAssistants } from "@/components/context/AssistantsContext";
|
||||
import { useUser } from "@/components/user/UserProvider";
|
||||
import { FilterIcon, XIcon } from "lucide-react";
|
||||
import { FilterIcon } 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,
|
||||
@@ -22,8 +24,8 @@ export const AssistantBadgeSelector = ({
|
||||
className={`
|
||||
select-none ${
|
||||
selected
|
||||
? "bg-background-900 text-white"
|
||||
: "bg-transparent text-text-900"
|
||||
? "bg-neutral-900 text-white"
|
||||
: "bg-transparent text-neutral-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}
|
||||
>
|
||||
@@ -107,20 +109,16 @@ export function AssistantModal({
|
||||
|
||||
const featuredAssistants = [
|
||||
...memoizedCurrentlyVisibleAssistants.filter(
|
||||
(assistant) => assistant.is_default_persona
|
||||
(assistant) => assistant.builtin_persona || assistant.is_default_persona
|
||||
),
|
||||
];
|
||||
const allAssistants = memoizedCurrentlyVisibleAssistants.filter(
|
||||
(assistant) => !assistant.is_default_persona
|
||||
(assistant) => !assistant.builtin_persona && !assistant.is_default_persona
|
||||
);
|
||||
|
||||
return (
|
||||
<div
|
||||
onClick={hideModal}
|
||||
className="fixed inset-0 bg-neutral-950/80 bg-opacity-50 flex items-center justify-center z-50"
|
||||
>
|
||||
<div 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",
|
||||
@@ -130,142 +128,129 @@ 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 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 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>
|
||||
))
|
||||
) : (
|
||||
<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>
|
||||
|
||||
<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 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>
|
||||
|
||||
{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="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>
|
||||
|
||||
@@ -97,6 +97,7 @@ 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";
|
||||
|
||||
@@ -129,7 +130,6 @@ 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,
|
||||
sidebarVisible,
|
||||
toggledSidebar,
|
||||
firstMessage,
|
||||
}: {
|
||||
toggle: (toggled?: boolean) => void;
|
||||
documentSidebarInitialWidth?: number;
|
||||
sidebarVisible: boolean;
|
||||
toggledSidebar: boolean;
|
||||
firstMessage?: string;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
@@ -204,7 +204,7 @@ export function ChatPage({
|
||||
const settings = useContext(SettingsContext);
|
||||
const enterpriseSettings = settings?.enterpriseSettings;
|
||||
|
||||
const [documentSidebarVisible, setDocumentSidebarVisible] = useState(false);
|
||||
const [documentSidebarToggled, setDocumentSidebarToggled] = 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(!sidebarVisible).toLocaleLowerCase()
|
||||
String(!toggledSidebar).toLocaleLowerCase()
|
||||
);
|
||||
toggle(false);
|
||||
}
|
||||
@@ -1024,10 +1024,10 @@ export function ChatPage({
|
||||
if (
|
||||
(!personaIncludesRetrieval &&
|
||||
(!selectedDocuments || selectedDocuments.length === 0) &&
|
||||
documentSidebarVisible) ||
|
||||
documentSidebarToggled) ||
|
||||
chatSessionIdRef.current == undefined
|
||||
) {
|
||||
setDocumentSidebarVisible(false);
|
||||
setDocumentSidebarToggled(false);
|
||||
}
|
||||
clientScrollToBottom();
|
||||
}, [chatSessionIdRef.current]);
|
||||
@@ -1122,7 +1122,6 @@ export function ChatPage({
|
||||
"Continue Generating (pick up exactly where you left off)",
|
||||
});
|
||||
};
|
||||
const [gener, setFinishedStreaming] = useState(false);
|
||||
|
||||
const onSubmit = async ({
|
||||
messageIdToResend,
|
||||
@@ -1273,7 +1272,6 @@ 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;
|
||||
@@ -1444,22 +1442,11 @@ 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,
|
||||
@@ -1619,7 +1606,6 @@ export function ChatPage({
|
||||
latestChildMessageId: initialFetchDetails.assistant_message_id,
|
||||
},
|
||||
{
|
||||
isStreamingQuestions: isStreamingQuestions,
|
||||
is_generating: is_generating,
|
||||
isImprovement: isImprovement,
|
||||
messageId: initialFetchDetails.assistant_message_id!,
|
||||
@@ -1819,7 +1805,7 @@ export function ChatPage({
|
||||
}
|
||||
Cookies.set(
|
||||
SIDEBAR_TOGGLED_COOKIE_NAME,
|
||||
String(!sidebarVisible).toLocaleLowerCase()
|
||||
String(!toggledSidebar).toLocaleLowerCase()
|
||||
),
|
||||
{
|
||||
path: "/",
|
||||
@@ -1836,7 +1822,7 @@ export function ChatPage({
|
||||
const sidebarElementRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useSidebarVisibility({
|
||||
sidebarVisible,
|
||||
toggledSidebar,
|
||||
sidebarElementRef,
|
||||
showDocSidebar: showHistorySidebar,
|
||||
setShowDocSidebar: setShowHistorySidebar,
|
||||
@@ -2017,7 +2003,7 @@ export function ChatPage({
|
||||
|
||||
useEffect(() => {
|
||||
if (!retrievalEnabled) {
|
||||
setDocumentSidebarVisible(false);
|
||||
setDocumentSidebarToggled(false);
|
||||
}
|
||||
}, [retrievalEnabled]);
|
||||
|
||||
@@ -2082,10 +2068,10 @@ export function ChatPage({
|
||||
const [showAssistantsModal, setShowAssistantsModal] = useState(false);
|
||||
|
||||
const toggleDocumentSidebar = () => {
|
||||
if (!documentSidebarVisible) {
|
||||
setDocumentSidebarVisible(true);
|
||||
if (!documentSidebarToggled) {
|
||||
setDocumentSidebarToggled(true);
|
||||
} else {
|
||||
setDocumentSidebarVisible(false);
|
||||
setDocumentSidebarToggled(false);
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2136,7 +2122,7 @@ export function ChatPage({
|
||||
<ChatPopup />
|
||||
|
||||
{showDeleteAllModal && (
|
||||
<ConfirmEntityModal
|
||||
<DeleteEntityModal
|
||||
entityType="All Chats"
|
||||
entityName="all your chat sessions"
|
||||
onClose={() => setShowDeleteAllModal(false)}
|
||||
@@ -2192,11 +2178,11 @@ export function ChatPage({
|
||||
/>
|
||||
)}
|
||||
|
||||
{retrievalEnabled && documentSidebarVisible && settings?.isMobile && (
|
||||
{retrievalEnabled && documentSidebarToggled && settings?.isMobile && (
|
||||
<div className="md:hidden">
|
||||
<Modal
|
||||
hideDividerForTitle
|
||||
onOutsideClick={() => setDocumentSidebarVisible(false)}
|
||||
onOutsideClick={() => setDocumentSidebarToggled(false)}
|
||||
title="Sources"
|
||||
>
|
||||
<DocumentResults
|
||||
@@ -2212,7 +2198,7 @@ export function ChatPage({
|
||||
modal={true}
|
||||
ref={innerSidebarElementRef}
|
||||
closeSidebar={() => {
|
||||
setDocumentSidebarVisible(false);
|
||||
setDocumentSidebarToggled(false);
|
||||
}}
|
||||
selectedMessage={aiMessage}
|
||||
selectedDocuments={selectedDocuments}
|
||||
@@ -2292,21 +2278,21 @@ export function ChatPage({
|
||||
duration-300
|
||||
ease-in-out
|
||||
${
|
||||
!untoggled && (showHistorySidebar || sidebarVisible)
|
||||
!untoggled && (showHistorySidebar || toggledSidebar)
|
||||
? "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={sidebarVisible}
|
||||
toggled={toggledSidebar}
|
||||
currentAssistantId={liveAssistant?.id}
|
||||
existingChats={chatSessions}
|
||||
currentChatSession={selectedChatSession}
|
||||
folders={folders}
|
||||
@@ -2328,7 +2314,7 @@ export function ChatPage({
|
||||
duration-300
|
||||
ease-in-out
|
||||
${
|
||||
documentSidebarVisible &&
|
||||
documentSidebarToggled &&
|
||||
!settings?.isMobile &&
|
||||
"opacity-100 w-[350px]"
|
||||
}`}
|
||||
@@ -2353,7 +2339,7 @@ export function ChatPage({
|
||||
ease-in-out
|
||||
h-full
|
||||
${
|
||||
documentSidebarVisible && !settings?.isMobile
|
||||
documentSidebarToggled && !settings?.isMobile
|
||||
? "w-[400px]"
|
||||
: "w-[0px]"
|
||||
}
|
||||
@@ -2372,7 +2358,7 @@ export function ChatPage({
|
||||
modal={false}
|
||||
ref={innerSidebarElementRef}
|
||||
closeSidebar={() =>
|
||||
setTimeout(() => setDocumentSidebarVisible(false), 300)
|
||||
setTimeout(() => setDocumentSidebarToggled(false), 300)
|
||||
}
|
||||
selectedMessage={aiMessage}
|
||||
selectedDocuments={selectedDocuments}
|
||||
@@ -2381,12 +2367,12 @@ export function ChatPage({
|
||||
selectedDocumentTokens={selectedDocumentTokens}
|
||||
maxTokens={maxTokens}
|
||||
initialWidth={400}
|
||||
isOpen={documentSidebarVisible && !settings?.isMobile}
|
||||
isOpen={documentSidebarToggled && !settings?.isMobile}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<BlurBackground
|
||||
visible={!untoggled && (showHistorySidebar || sidebarVisible)}
|
||||
visible={!untoggled && (showHistorySidebar || toggledSidebar)}
|
||||
onClick={() => toggleSidebar()}
|
||||
/>
|
||||
|
||||
@@ -2401,7 +2387,7 @@ export function ChatPage({
|
||||
{liveAssistant && (
|
||||
<FunctionalHeader
|
||||
toggleUserSettings={() => setUserSettingsToggled(true)}
|
||||
sidebarToggled={sidebarVisible}
|
||||
sidebarToggled={toggledSidebar}
|
||||
reset={() => setMessage("")}
|
||||
page="chat"
|
||||
setSharingModalVisible={
|
||||
@@ -2409,8 +2395,8 @@ export function ChatPage({
|
||||
? setSharingModalVisible
|
||||
: undefined
|
||||
}
|
||||
documentSidebarVisible={
|
||||
documentSidebarVisible && !settings?.isMobile
|
||||
documentSidebarToggled={
|
||||
documentSidebarToggled && !settings?.isMobile
|
||||
}
|
||||
toggleSidebar={toggleSidebar}
|
||||
currentChatSession={selectedChatSession}
|
||||
@@ -2438,7 +2424,7 @@ export function ChatPage({
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
${sidebarVisible ? "w-[200px]" : "w-[0px]"}
|
||||
${toggledSidebar ? "w-[200px]" : "w-[0px]"}
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
@@ -2464,7 +2450,7 @@ export function ChatPage({
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
${sidebarVisible ? "w-[200px]" : "w-[0px]"}
|
||||
${toggledSidebar ? "w-[200px]" : "w-[0px]"}
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
@@ -2649,14 +2635,8 @@ export function ChatPage({
|
||||
{message.sub_questions &&
|
||||
message.sub_questions.length > 0 ? (
|
||||
<AgenticMessage
|
||||
isStreamingQuestions={
|
||||
message.isStreamingQuestions ?? false
|
||||
}
|
||||
isGenerating={
|
||||
message.is_generating ?? false
|
||||
}
|
||||
docSidebarToggled={
|
||||
documentSidebarVisible &&
|
||||
documentSidebarToggled &&
|
||||
(selectedMessageForDocDisplay ==
|
||||
message.messageId ||
|
||||
selectedMessageForDocDisplay ==
|
||||
@@ -2752,8 +2732,7 @@ export function ChatPage({
|
||||
setMessageAsLatest(messageId);
|
||||
}}
|
||||
isActive={
|
||||
messageHistory.length - 1 == i ||
|
||||
messageHistory.length - 2 == i
|
||||
messageHistory.length - 1 == i
|
||||
}
|
||||
selectedDocuments={selectedDocuments}
|
||||
toggleDocumentSelection={(
|
||||
@@ -2761,8 +2740,8 @@ export function ChatPage({
|
||||
) => {
|
||||
if (
|
||||
(!second &&
|
||||
!documentSidebarVisible) ||
|
||||
(documentSidebarVisible &&
|
||||
!documentSidebarToggled) ||
|
||||
(documentSidebarToggled &&
|
||||
selectedMessageForDocDisplay ===
|
||||
message.messageId)
|
||||
) {
|
||||
@@ -2770,8 +2749,8 @@ export function ChatPage({
|
||||
}
|
||||
if (
|
||||
(second &&
|
||||
!documentSidebarVisible) ||
|
||||
(documentSidebarVisible &&
|
||||
!documentSidebarToggled) ||
|
||||
(documentSidebarToggled &&
|
||||
selectedMessageForDocDisplay ===
|
||||
secondLevelMessage?.messageId)
|
||||
) {
|
||||
@@ -2872,8 +2851,8 @@ export function ChatPage({
|
||||
selectedDocuments={selectedDocuments}
|
||||
toggleDocumentSelection={() => {
|
||||
if (
|
||||
!documentSidebarVisible ||
|
||||
(documentSidebarVisible &&
|
||||
!documentSidebarToggled ||
|
||||
(documentSidebarToggled &&
|
||||
selectedMessageForDocDisplay ===
|
||||
message.messageId)
|
||||
) {
|
||||
@@ -3091,7 +3070,7 @@ 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 text-neutral-700 dark:text-neutral-800 rounded-2xl bg-neutral-200 border border-border mx-auto "
|
||||
className="p-1 pointer-events-auto rounded-2xl bg-background-strong border border-border mx-auto "
|
||||
>
|
||||
<FiArrowDown size={18} />
|
||||
</button>
|
||||
@@ -3168,7 +3147,7 @@ export function ChatPage({
|
||||
ease-in-out
|
||||
h-full
|
||||
${
|
||||
documentSidebarVisible && !settings?.isMobile
|
||||
documentSidebarToggled && !settings?.isMobile
|
||||
? "w-[350px]"
|
||||
: "w-[0px]"
|
||||
}
|
||||
@@ -3183,7 +3162,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
|
||||
${
|
||||
sidebarVisible && !settings?.isMobile
|
||||
toggledSidebar && !settings?.isMobile
|
||||
? "w-[250px] "
|
||||
: "w-[0px]"
|
||||
}`}
|
||||
@@ -3195,7 +3174,7 @@ export function ChatPage({
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<FixedLogo backgroundToggled={sidebarVisible || showHistorySidebar} />
|
||||
<FixedLogo backgroundToggled={toggledSidebar || showHistorySidebar} />
|
||||
</div>
|
||||
{/* Right Sidebar - DocumentSidebar */}
|
||||
</div>
|
||||
|
||||
@@ -115,61 +115,21 @@ export function RefinemenetBadge({
|
||||
const isDone = displayedPhases.includes(StreamingPhase.COMPLETE);
|
||||
|
||||
// Expand/collapse, hover states
|
||||
const [expanded] = useState(true);
|
||||
const [expanded, setExpanded] = 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);
|
||||
}, 800); // e.g. 0.8s
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [isDone, isHovered, setCanShowResponse]);
|
||||
}, [isDone, isHovered]);
|
||||
|
||||
if (!shouldShow) {
|
||||
return null; // entire box disappears
|
||||
@@ -177,22 +137,13 @@ 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"
|
||||
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
|
||||
onMouseEnter={() => setIsHovered(true)}
|
||||
onMouseLeave={() => setIsHovered(false)}
|
||||
>
|
||||
{/* 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">
|
||||
@@ -208,32 +159,36 @@ export function RefinemenetBadge({
|
||||
</TooltipTrigger>
|
||||
{expanded && (
|
||||
<TooltipContent
|
||||
ref={tooltipRef}
|
||||
// onMouseEnter keeps the tooltip open when cursor enters tooltip
|
||||
onMouseEnter={() => {
|
||||
setToolTipHoveredInternal(true);
|
||||
setToolTipHovered(true);
|
||||
}}
|
||||
// Remove onMouseLeave and rely on bounding box logic to close
|
||||
onMouseLeave={() => {
|
||||
setToolTipHoveredInternal(false);
|
||||
}}
|
||||
side="bottom"
|
||||
align="start"
|
||||
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"
|
||||
className="w-fit -mt-1 p-4 bg-white border-2 border-border 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 &&
|
||||
phase !== StreamingPhase.COMPLETE
|
||||
Array.from(new Set(displayedPhases)).length - 1
|
||||
) {
|
||||
status = ToggleState.InProgress;
|
||||
}
|
||||
if (phase === StreamingPhase.COMPLETE) {
|
||||
status = ToggleState.Done;
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
@@ -383,7 +338,6 @@ 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 */}
|
||||
@@ -401,6 +355,7 @@ export function StatusRefinement({
|
||||
</div>
|
||||
<span className="text-neutral-800 text-sm font-medium">
|
||||
{StreamingPhaseText[phase]}
|
||||
LLL
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -5,22 +5,18 @@ 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 { sidebarInitiallyVisible } = useChatContext();
|
||||
const { toggledSidebar } = useChatContext();
|
||||
|
||||
return (
|
||||
<FunctionalWrapper
|
||||
initiallyVisible={sidebarInitiallyVisible && !defaultSidebarOff}
|
||||
content={(sidebarVisible, toggle) => (
|
||||
initiallyToggled={toggledSidebar}
|
||||
content={(toggledSidebar, toggle) => (
|
||||
<ChatPage
|
||||
toggle={toggle}
|
||||
sidebarVisible={sidebarVisible}
|
||||
toggledSidebar={toggledSidebar}
|
||||
firstMessage={firstMessage}
|
||||
/>
|
||||
)}
|
||||
|
||||
@@ -819,17 +819,27 @@ export function ChatInputBar({
|
||||
chatState == "toolBuilding" ||
|
||||
chatState == "loading"
|
||||
? chatState != "streaming"
|
||||
? "bg-neutral-500 dark:bg-neutral-400 "
|
||||
: "bg-neutral-900 dark:bg-neutral-50"
|
||||
: "bg-red-200"
|
||||
? "bg-neutral-900 dark:bg-neutral-400 "
|
||||
: "bg-neutral-500 dark:bg-neutral-50"
|
||||
: ""
|
||||
} h-[22px] w-[22px] rounded-full`}
|
||||
onClick={() => {
|
||||
if (chatState == "streaming") {
|
||||
if (
|
||||
chatState == "streaming" ||
|
||||
chatState == "toolBuilding" ||
|
||||
chatState == "loading"
|
||||
) {
|
||||
stopGenerating();
|
||||
} else if (message) {
|
||||
onSubmit();
|
||||
}
|
||||
}}
|
||||
disabled={
|
||||
(chatState == "streaming" ||
|
||||
chatState == "toolBuilding" ||
|
||||
chatState == "loading") &&
|
||||
chatState != "streaming"
|
||||
}
|
||||
>
|
||||
{chatState == "streaming" ||
|
||||
chatState == "toolBuilding" ||
|
||||
|
||||
@@ -110,7 +110,6 @@ export interface Message {
|
||||
second_level_message?: string;
|
||||
second_level_subquestions?: SubQuestionDetail[] | null;
|
||||
isImprovement?: boolean | null;
|
||||
isStreamingQuestions?: boolean;
|
||||
}
|
||||
|
||||
export interface BackendChatSession {
|
||||
@@ -220,7 +219,6 @@ export interface SubQuestionDetail extends BaseQuestionIdentifier {
|
||||
context_docs?: { top_documents: OnyxDocument[] } | null;
|
||||
is_complete?: boolean;
|
||||
is_stopped?: boolean;
|
||||
answer_streaming?: boolean;
|
||||
}
|
||||
|
||||
export interface SubQueryDetail {
|
||||
@@ -247,6 +245,9 @@ export const constructSubQuestions = (
|
||||
}
|
||||
|
||||
const updatedSubQuestions = [...subQuestions];
|
||||
// .filter(
|
||||
// (sq) => sq.level_question_num !== 0
|
||||
// );
|
||||
|
||||
if ("stop_reason" in newDetail) {
|
||||
const { level, level_question_num } = newDetail;
|
||||
@@ -254,12 +255,8 @@ export const constructSubQuestions = (
|
||||
(sq) => sq.level === level && sq.level_question_num === level_question_num
|
||||
);
|
||||
if (subQuestion) {
|
||||
if (newDetail.stream_type == "sub_answer") {
|
||||
subQuestion.answer_streaming = false;
|
||||
} else {
|
||||
subQuestion.is_complete = true;
|
||||
subQuestion.is_stopped = true;
|
||||
}
|
||||
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,
|
||||
sidebarInitiallyVisible,
|
||||
toggleSidebar,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
ccPairs,
|
||||
@@ -47,7 +47,7 @@ export default async function Layout({
|
||||
proSearchToggled,
|
||||
inputPrompts,
|
||||
chatSessions,
|
||||
sidebarInitiallyVisible,
|
||||
toggledSidebar: toggleSidebar,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
|
||||
@@ -48,10 +48,9 @@ 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,
|
||||
@@ -82,8 +81,6 @@ export const AgenticMessage = ({
|
||||
secondLevelSubquestions,
|
||||
toggleDocDisplay,
|
||||
}: {
|
||||
isStreamingQuestions: boolean;
|
||||
isGenerating: boolean;
|
||||
docSidebarToggled?: boolean;
|
||||
isImprovement?: boolean | null;
|
||||
secondLevelSubquestions?: SubQuestionDetail[] | null;
|
||||
@@ -233,13 +230,6 @@ 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) => {
|
||||
@@ -410,10 +400,12 @@ export const AgenticMessage = ({
|
||||
<div className="w-full desktop:ml-4">
|
||||
{subQuestions && subQuestions.length > 0 && (
|
||||
<SubQuestionsDisplay
|
||||
isStreamingQuestions={isStreamingQuestions}
|
||||
allowDocuments={() => setAllowDocuments(true)}
|
||||
docSidebarToggled={docSidebarToggled || false}
|
||||
finishedGenerating={finishedGenerating}
|
||||
finishedGenerating={
|
||||
finalContent.length > 2 &&
|
||||
streamedContent.length == finalContent.length
|
||||
}
|
||||
overallAnswerGenerating={
|
||||
!!(
|
||||
secondLevelSubquestions &&
|
||||
|
||||
@@ -185,7 +185,7 @@ export const AIMessage = ({
|
||||
onMessageSelection,
|
||||
setPresentingDocument,
|
||||
index,
|
||||
documentSidebarVisible,
|
||||
toggledDocumentSidebar,
|
||||
}: {
|
||||
index?: number;
|
||||
shared?: boolean;
|
||||
@@ -205,7 +205,7 @@ export const AIMessage = ({
|
||||
citedDocuments?: [string, OnyxDocument][] | null;
|
||||
toolCall?: ToolCallMetadata | null;
|
||||
isComplete?: boolean;
|
||||
documentSidebarVisible?: boolean;
|
||||
toggledDocumentSidebar?: boolean;
|
||||
hasDocs?: boolean;
|
||||
handleFeedback?: (feedbackType: FeedbackType) => void;
|
||||
handleSearchQueryEdit?: (query: string) => void;
|
||||
@@ -508,7 +508,7 @@ export const AIMessage = ({
|
||||
/>
|
||||
))}
|
||||
<SeeMoreBlock
|
||||
toggled={documentSidebarVisible!}
|
||||
toggled={toggledDocumentSidebar!}
|
||||
toggleDocumentSelection={toggleDocumentSelection!}
|
||||
docs={docs}
|
||||
webSourceDomains={webSourceDomains}
|
||||
@@ -957,7 +957,7 @@ export const HumanMessage = ({
|
||||
min-h-[38px]
|
||||
py-2
|
||||
px-3
|
||||
hover:bg-agent-hovered
|
||||
hover:bg-accent-hover
|
||||
`}
|
||||
onClick={handleEditSubmit}
|
||||
>
|
||||
|
||||
@@ -56,19 +56,12 @@ const DOC_DELAY_MS = 100;
|
||||
export const useStreamingMessages = (
|
||||
subQuestions: SubQuestionDetail[],
|
||||
allowStreaming: () => void,
|
||||
onComplete: () => void,
|
||||
isStreamingQuestions: boolean
|
||||
onComplete: () => void
|
||||
) => {
|
||||
const [dynamicSubQuestions, setDynamicSubQuestions] = useState<
|
||||
SubQuestionDetail[]
|
||||
>([]);
|
||||
|
||||
const isStreamingQuestionsRef = useRef(isStreamingQuestions);
|
||||
|
||||
useEffect(() => {
|
||||
isStreamingQuestionsRef.current = isStreamingQuestions;
|
||||
}, [isStreamingQuestions]);
|
||||
|
||||
const subQuestionsRef = useRef<SubQuestionDetail[]>(subQuestions);
|
||||
useEffect(() => {
|
||||
subQuestionsRef.current = subQuestions;
|
||||
@@ -128,7 +121,6 @@ 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];
|
||||
@@ -146,8 +138,6 @@ export const useStreamingMessages = (
|
||||
p.questionDone = true;
|
||||
}
|
||||
didStreamQuestion = true;
|
||||
allQuestionsComplete = false;
|
||||
|
||||
// Break after streaming one question to ensure sequential behavior
|
||||
break;
|
||||
}
|
||||
@@ -159,11 +149,7 @@ export const useStreamingMessages = (
|
||||
}
|
||||
}
|
||||
|
||||
if (
|
||||
allQuestionsComplete &&
|
||||
!didStreamQuestion &&
|
||||
!isStreamingQuestionsRef.current
|
||||
) {
|
||||
if (allQuestionsComplete && !didStreamQuestion) {
|
||||
onComplete();
|
||||
}
|
||||
|
||||
@@ -177,8 +163,6 @@ 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,7 +65,6 @@ export interface TemporaryDisplay {
|
||||
tinyQuestion: string;
|
||||
}
|
||||
interface SubQuestionsDisplayProps {
|
||||
isStreamingQuestions: boolean;
|
||||
docSidebarToggled: boolean;
|
||||
finishedGenerating: boolean;
|
||||
currentlyOpenQuestion?: BaseQuestionIdentifier | null;
|
||||
@@ -153,8 +152,7 @@ const SubQuestionDisplay: React.FC<{
|
||||
content = content.replace(/\]\](?!\()/g, "]]()");
|
||||
|
||||
return (
|
||||
preprocessLaTeX(content) +
|
||||
(subQuestion?.answer_streaming ? " [*]() " : "")
|
||||
preprocessLaTeX(content) + (!subQuestion?.is_complete ? " [*]() " : "")
|
||||
);
|
||||
};
|
||||
|
||||
@@ -463,7 +461,6 @@ const SubQuestionDisplay: React.FC<{
|
||||
};
|
||||
|
||||
const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
||||
isStreamingQuestions,
|
||||
finishedGenerating,
|
||||
subQuestions,
|
||||
allowStreaming,
|
||||
@@ -480,29 +477,23 @@ 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 initiallyFinishedGenerating ? subQuestions : dynamicSubQuestions;
|
||||
}, [initiallyFinishedGenerating, dynamicSubQuestions, subQuestions]);
|
||||
|
||||
return finishedGenerating ? subQuestions : dynamicSubQuestions;
|
||||
}, [finishedGenerating, dynamicSubQuestions, subQuestions]);
|
||||
// const memoizedSubQuestions = dynamicSubQuestions;
|
||||
const memoizedSecondLevelQuestions = useMemo(() => {
|
||||
return overallAnswerGenerating
|
||||
? dynamicSecondLevelQuestions
|
||||
@@ -518,6 +509,12 @@ 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" : ""
|
||||
);
|
||||
@@ -527,15 +524,12 @@ const SubQuestionsDisplay: React.FC<SubQuestionsDisplayProps> = ({
|
||||
const [shownDocuments, setShownDocuments] = useState(documents);
|
||||
|
||||
useEffect(() => {
|
||||
if (canShowSummarizing && documents && documents.length > 0) {
|
||||
setTimeout(
|
||||
() => {
|
||||
setShownDocuments(documents);
|
||||
},
|
||||
finishedGenerating ? 0 : 800
|
||||
);
|
||||
if (documents && documents.length > 0) {
|
||||
setTimeout(() => {
|
||||
setShownDocuments(documents);
|
||||
}, 800);
|
||||
}
|
||||
}, [documents, canShowSummarizing]);
|
||||
}, [documents]);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
||||
@@ -39,6 +39,7 @@ export function UserSettingsModal({
|
||||
onClose: () => void;
|
||||
defaultModel: string | null;
|
||||
}) {
|
||||
const { inputPrompts, refreshInputPrompts } = useChatContext();
|
||||
const {
|
||||
refreshUser,
|
||||
user,
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { SEARCH_PARAMS } from "@/lib/extension/constants";
|
||||
import WrappedChat from "./WrappedChat";
|
||||
|
||||
export default async function Page(props: {
|
||||
@@ -6,13 +5,6 @@ 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}
|
||||
defaultSidebarOff={defaultSidebarOff}
|
||||
/>
|
||||
);
|
||||
return <WrappedChat firstMessage={firstMessage} />;
|
||||
}
|
||||
|
||||
@@ -50,12 +50,10 @@ import {
|
||||
} from "@dnd-kit/sortable";
|
||||
import { useSortable } from "@dnd-kit/sortable";
|
||||
import { CSS } from "@dnd-kit/utilities";
|
||||
import { CirclePlus, CircleX, PinIcon } from "lucide-react";
|
||||
import { CircleX } 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;
|
||||
@@ -68,23 +66,22 @@ interface HistorySidebarProps {
|
||||
showDeleteModal?: (chatSession: ChatSession) => void;
|
||||
explicitlyUntoggle: () => void;
|
||||
showDeleteAllModal?: () => void;
|
||||
currentAssistantId?: number | null;
|
||||
setShowAssistantsModal: (show: boolean) => void;
|
||||
}
|
||||
|
||||
interface SortableAssistantProps {
|
||||
assistant: Persona;
|
||||
active: boolean;
|
||||
currentAssistantId: number | null | undefined;
|
||||
onClick: () => void;
|
||||
onPinAction: (e: React.MouseEvent) => void;
|
||||
pinned?: boolean;
|
||||
onUnpin: (e: React.MouseEvent) => void;
|
||||
}
|
||||
|
||||
const SortableAssistant: React.FC<SortableAssistantProps> = ({
|
||||
assistant,
|
||||
active,
|
||||
currentAssistantId,
|
||||
onClick,
|
||||
onPinAction,
|
||||
pinned = true,
|
||||
onUnpin,
|
||||
}) => {
|
||||
const {
|
||||
attributes,
|
||||
@@ -129,9 +126,7 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
|
||||
>
|
||||
<DragHandle
|
||||
size={16}
|
||||
className={`w-3 ml-[2px] mr-[2px] group-hover:visible invisible flex-none cursor-grab ${
|
||||
!pinned ? "opacity-0" : ""
|
||||
}`}
|
||||
className="w-3 ml-[2px] mr-[2px] group-hover:visible invisible flex-none cursor-grab"
|
||||
/>
|
||||
<div
|
||||
data-testid={`assistant-[${assistant.id}]`}
|
||||
@@ -142,7 +137,9 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
|
||||
}
|
||||
}}
|
||||
className={`cursor-pointer w-full group hover:bg-background-chat-hover ${
|
||||
active ? "bg-accent-background-selected" : ""
|
||||
currentAssistantId === assistant.id
|
||||
? "bg-background-chat-hover/60"
|
||||
: ""
|
||||
} relative flex items-center gap-x-2 py-1 px-2 rounded-md`}
|
||||
>
|
||||
<AssistantIcon assistant={assistant} size={16} className="flex-none" />
|
||||
@@ -167,36 +164,15 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
|
||||
>
|
||||
{assistant.name}
|
||||
</span>
|
||||
<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>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -205,7 +181,6 @@ const SortableAssistant: React.FC<SortableAssistantProps> = ({
|
||||
export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
(
|
||||
{
|
||||
liveAssistant,
|
||||
reset = () => null,
|
||||
setShowAssistantsModal = () => null,
|
||||
toggled,
|
||||
@@ -219,6 +194,7 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
showShareModal,
|
||||
showDeleteModal,
|
||||
showDeleteAllModal,
|
||||
currentAssistantId,
|
||||
},
|
||||
ref: ForwardedRef<HTMLDivElement>
|
||||
) => {
|
||||
@@ -377,13 +353,13 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
|
||||
<SortableAssistant
|
||||
key={assistant.id === 0 ? "assistant-0" : assistant.id}
|
||||
assistant={assistant}
|
||||
active={assistant.id === liveAssistant?.id}
|
||||
currentAssistantId={currentAssistantId}
|
||||
onClick={() => {
|
||||
router.push(
|
||||
buildChatUrl(searchParams, null, assistant.id)
|
||||
);
|
||||
}}
|
||||
onPinAction={async (e: React.MouseEvent) => {
|
||||
onUnpin={async (e: React.MouseEvent) => {
|
||||
e.stopPropagation();
|
||||
await toggleAssistantPinnedStatus(
|
||||
pinnedAssistants.map((a) => a.id),
|
||||
@@ -397,31 +373,6 @@ 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({
|
||||
documentSidebarVisible,
|
||||
documentSidebarToggled,
|
||||
}: {
|
||||
documentSidebarVisible: boolean;
|
||||
documentSidebarToggled: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const enterpriseSettings = useContext(SettingsContext)?.enterpriseSettings;
|
||||
@@ -47,7 +47,7 @@ function BackToOnyxButton({
|
||||
transition-all
|
||||
duration-300
|
||||
ease-in-out
|
||||
${documentSidebarVisible ? "w-[400px]" : "w-[0px]"}
|
||||
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
|
||||
`}
|
||||
></div>
|
||||
</div>
|
||||
@@ -62,7 +62,7 @@ export function SharedChatDisplay({
|
||||
persona: Persona;
|
||||
}) {
|
||||
const settings = useContext(SettingsContext);
|
||||
const [documentSidebarVisible, setDocumentSidebarVisible] = useState(false);
|
||||
const [documentSidebarToggled, setDocumentSidebarToggled] = 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 = () => {
|
||||
setDocumentSidebarVisible(!documentSidebarVisible);
|
||||
setDocumentSidebarToggled(!documentSidebarToggled);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
@@ -85,7 +85,7 @@ export function SharedChatDisplay({
|
||||
Did not find a shared chat with the specified ID.
|
||||
</Callout>
|
||||
</div>
|
||||
<BackToOnyxButton documentSidebarVisible={documentSidebarVisible} />
|
||||
<BackToOnyxButton documentSidebarToggled={documentSidebarToggled} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -102,7 +102,7 @@ export function SharedChatDisplay({
|
||||
onClose={() => setPresentingDocument(null)}
|
||||
/>
|
||||
)}
|
||||
{documentSidebarVisible && settings?.isMobile && (
|
||||
{documentSidebarToggled && settings?.isMobile && (
|
||||
<div className="md:hidden">
|
||||
<Modal noPadding noScroll>
|
||||
<DocumentResults
|
||||
@@ -117,7 +117,7 @@ export function SharedChatDisplay({
|
||||
: null
|
||||
}
|
||||
toggleDocumentSelection={() => {
|
||||
setDocumentSidebarVisible(true);
|
||||
setDocumentSidebarToggled(true);
|
||||
}}
|
||||
selectedDocuments={[]}
|
||||
clearSelectedDocuments={() => {}}
|
||||
@@ -128,7 +128,7 @@ export function SharedChatDisplay({
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
modal={true}
|
||||
closeSidebar={() => {
|
||||
setDocumentSidebarVisible(false);
|
||||
setDocumentSidebarToggled(false);
|
||||
}}
|
||||
/>
|
||||
</Modal>
|
||||
@@ -158,7 +158,7 @@ export function SharedChatDisplay({
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
${documentSidebarVisible ? "w-[400px]" : "w-[0px]"}
|
||||
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
|
||||
`}
|
||||
>
|
||||
<DocumentResults
|
||||
@@ -174,7 +174,7 @@ export function SharedChatDisplay({
|
||||
: null
|
||||
}
|
||||
toggleDocumentSelection={() => {
|
||||
setDocumentSidebarVisible(true);
|
||||
setDocumentSidebarToggled(true);
|
||||
}}
|
||||
clearSelectedDocuments={() => {}}
|
||||
selectedDocumentTokens={0}
|
||||
@@ -183,7 +183,7 @@ export function SharedChatDisplay({
|
||||
isOpen={true}
|
||||
setPresentingDocument={setPresentingDocument}
|
||||
closeSidebar={() => {
|
||||
setDocumentSidebarVisible(false);
|
||||
setDocumentSidebarToggled(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
|
||||
${
|
||||
documentSidebarVisible
|
||||
documentSidebarToggled
|
||||
? "left-[200px] transform -translate-x-[calc(50%+100px)]"
|
||||
: "left-1/2 transform -translate-x-1/2"
|
||||
}
|
||||
@@ -263,8 +263,6 @@ export function SharedChatDisplay({
|
||||
) {
|
||||
return (
|
||||
<AgenticMessage
|
||||
isStreamingQuestions={false}
|
||||
isGenerating={false}
|
||||
shared
|
||||
key={message.messageId}
|
||||
isImprovement={message.isImprovement}
|
||||
@@ -313,13 +311,13 @@ export function SharedChatDisplay({
|
||||
selectedDocuments={[]}
|
||||
toggleDocumentSelection={() => {
|
||||
if (
|
||||
!documentSidebarVisible ||
|
||||
(documentSidebarVisible &&
|
||||
!documentSidebarToggled ||
|
||||
(documentSidebarToggled &&
|
||||
selectedMessageForDocDisplay ===
|
||||
message.messageId)
|
||||
) {
|
||||
setDocumentSidebarVisible(
|
||||
!documentSidebarVisible
|
||||
setDocumentSidebarToggled(
|
||||
!documentSidebarToggled
|
||||
);
|
||||
}
|
||||
setSelectedMessageForDocDisplay(
|
||||
@@ -353,13 +351,13 @@ export function SharedChatDisplay({
|
||||
selectedDocuments={[]}
|
||||
toggleDocumentSelection={() => {
|
||||
if (
|
||||
!documentSidebarVisible ||
|
||||
(documentSidebarVisible &&
|
||||
!documentSidebarToggled ||
|
||||
(documentSidebarToggled &&
|
||||
selectedMessageForDocDisplay ===
|
||||
message.messageId)
|
||||
) {
|
||||
setDocumentSidebarVisible(
|
||||
!documentSidebarVisible
|
||||
setDocumentSidebarToggled(
|
||||
!documentSidebarToggled
|
||||
);
|
||||
}
|
||||
setSelectedMessageForDocDisplay(
|
||||
@@ -375,8 +373,6 @@ export function SharedChatDisplay({
|
||||
<div key={message.messageId}>
|
||||
<AgenticMessage
|
||||
shared
|
||||
isStreamingQuestions={false}
|
||||
isGenerating={false}
|
||||
subQuestions={message.sub_questions || []}
|
||||
currentPersona={persona}
|
||||
messageId={message.messageId}
|
||||
@@ -408,7 +404,7 @@ export function SharedChatDisplay({
|
||||
transition-all
|
||||
duration-300
|
||||
ease-in-out
|
||||
${documentSidebarVisible ? "w-[400px]" : "w-[0px]"}
|
||||
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
|
||||
`}
|
||||
></div>
|
||||
)}
|
||||
@@ -416,7 +412,7 @@ export function SharedChatDisplay({
|
||||
</div>
|
||||
|
||||
<FixedLogo backgroundToggled={false} />
|
||||
<BackToOnyxButton documentSidebarVisible={documentSidebarVisible} />
|
||||
<BackToOnyxButton documentSidebarToggled={documentSidebarToggled} />
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -1,65 +0,0 @@
|
||||
"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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
import React from "react";
|
||||
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert";
|
||||
import { CircleAlert, Info } from "lucide-react";
|
||||
import { BillingInformation, BillingStatus } from "./interfaces";
|
||||
|
||||
export function BillingAlerts({
|
||||
billingInformation,
|
||||
}: {
|
||||
billingInformation: BillingInformation;
|
||||
}) {
|
||||
const isTrialing = billingInformation.status === BillingStatus.TRIALING;
|
||||
const isCancelled = billingInformation.cancel_at_period_end;
|
||||
const isExpired =
|
||||
new Date(billingInformation.current_period_end) < new Date();
|
||||
const noPaymentMethod = !billingInformation.payment_method_enabled;
|
||||
|
||||
const messages: string[] = [];
|
||||
|
||||
if (isExpired) {
|
||||
messages.push(
|
||||
"Your subscription has expired. Please resubscribe to continue using the service."
|
||||
);
|
||||
}
|
||||
if (isCancelled && !isExpired) {
|
||||
messages.push(
|
||||
`Your subscription will cancel on ${new Date(
|
||||
billingInformation.current_period_end
|
||||
).toLocaleDateString()}. You can resubscribe before this date to remain uninterrupted.`
|
||||
);
|
||||
}
|
||||
if (isTrialing) {
|
||||
messages.push(
|
||||
`You're currently on a trial. Your trial ends on ${
|
||||
billingInformation.trial_end
|
||||
? new Date(billingInformation.trial_end).toLocaleDateString()
|
||||
: "N/A"
|
||||
}.`
|
||||
);
|
||||
}
|
||||
if (noPaymentMethod) {
|
||||
messages.push(
|
||||
"You currently have no payment method on file. Please add one to avoid service interruption."
|
||||
);
|
||||
}
|
||||
|
||||
const variant = isExpired || noPaymentMethod ? "destructive" : "default";
|
||||
|
||||
if (messages.length === 0) return null;
|
||||
|
||||
return (
|
||||
<Alert variant={variant}>
|
||||
<AlertTitle className="flex items-center space-x-2">
|
||||
{variant === "destructive" ? (
|
||||
<CircleAlert className="h-4 w-4" />
|
||||
) : (
|
||||
<Info className="h-4 w-4" />
|
||||
)}
|
||||
<span>
|
||||
{variant === "destructive"
|
||||
? "Important Subscription Notice"
|
||||
: "Subscription Notice"}
|
||||
</span>
|
||||
</AlertTitle>
|
||||
<AlertDescription>
|
||||
<ul className="list-disc list-inside space-y-1 mt-2">
|
||||
{messages.map((msg, idx) => (
|
||||
<li key={idx}>{msg}</li>
|
||||
))}
|
||||
</ul>
|
||||
</AlertDescription>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
@@ -1,21 +1,18 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useEffect } from "react";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { fetchCustomerPortal, useBillingInformation } from "./utils";
|
||||
|
||||
import {
|
||||
Card,
|
||||
CardContent,
|
||||
CardDescription,
|
||||
CardHeader,
|
||||
CardTitle,
|
||||
} from "@/components/ui/card";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { CreditCard, ArrowFatUp } from "@phosphor-icons/react";
|
||||
import { SubscriptionSummary } from "./SubscriptionSummary";
|
||||
import { BillingAlerts } from "./BillingAlerts";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import { usePopup } from "@/components/admin/connectors/Popup";
|
||||
import { SettingsIcon } from "@/components/icons/icons";
|
||||
import {
|
||||
updateSubscriptionQuantity,
|
||||
fetchCustomerPortal,
|
||||
statusToDisplay,
|
||||
useBillingInformation,
|
||||
} from "./utils";
|
||||
import { useEffect } from "react";
|
||||
|
||||
export default function BillingInformationPage() {
|
||||
const router = useRouter();
|
||||
@@ -27,6 +24,9 @@ export default function BillingInformationPage() {
|
||||
isLoading,
|
||||
} = useBillingInformation();
|
||||
|
||||
if (error) {
|
||||
console.error("Failed to fetch billing information:", error);
|
||||
}
|
||||
useEffect(() => {
|
||||
const url = new URL(window.location.href);
|
||||
if (url.searchParams.has("session_id")) {
|
||||
@@ -35,33 +35,22 @@ export default function BillingInformationPage() {
|
||||
"Congratulations! Your subscription has been updated successfully.",
|
||||
type: "success",
|
||||
});
|
||||
// Remove the session_id from the URL
|
||||
url.searchParams.delete("session_id");
|
||||
window.history.replaceState({}, "", url.toString());
|
||||
// You might want to refresh the billing information here
|
||||
// by calling an API endpoint to get the latest data
|
||||
}
|
||||
}, [setPopup]);
|
||||
|
||||
if (isLoading) {
|
||||
return <div className="text-center py-8">Loading...</div>;
|
||||
}
|
||||
|
||||
if (error) {
|
||||
console.error("Failed to fetch billing information:", error);
|
||||
return (
|
||||
<div className="text-center py-8 text-red-500">
|
||||
Error loading billing information. Please try again later.
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!billingInformation) {
|
||||
return (
|
||||
<div className="text-center py-8">No billing information available.</div>
|
||||
);
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
const handleManageSubscription = async () => {
|
||||
try {
|
||||
const response = await fetchCustomerPortal();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
@@ -72,9 +61,11 @@ export default function BillingInformationPage() {
|
||||
}
|
||||
|
||||
const { url } = await response.json();
|
||||
|
||||
if (!url) {
|
||||
throw new Error("No portal URL returned from the server");
|
||||
}
|
||||
|
||||
router.push(url);
|
||||
} catch (error) {
|
||||
console.error("Error creating customer portal session:", error);
|
||||
@@ -84,39 +75,138 @@ export default function BillingInformationPage() {
|
||||
});
|
||||
}
|
||||
};
|
||||
if (!billingInformation) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{popup}
|
||||
<Card className="shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-2xl font-bold flex items-center">
|
||||
<CreditCard className="mr-4 text-muted-foreground" size={24} />
|
||||
Subscription Details
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<SubscriptionSummary billingInformation={billingInformation} />
|
||||
<BillingAlerts billingInformation={billingInformation} />
|
||||
</CardContent>
|
||||
</Card>
|
||||
<div className="bg-background-50 rounded-lg p-8 border border-background-200">
|
||||
{popup}
|
||||
|
||||
<Card className="shadow-md">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl font-semibold">
|
||||
Manage Subscription
|
||||
</CardTitle>
|
||||
<CardDescription>
|
||||
View your plan, update payment, or change subscription
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Button onClick={handleManageSubscription} className="w-full">
|
||||
<ArrowFatUp className="mr-2" size={16} />
|
||||
Manage Subscription
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
<h2 className="text-2xl font-bold mb-6 text-text-800 flex items-center">
|
||||
{/* <CreditCard className="mr-4 text-text-600" size={24} /> */}
|
||||
Subscription Details
|
||||
</h2>
|
||||
|
||||
<div className="space-y-4">
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">Seats</p>
|
||||
<p className="text-sm text-text-500">
|
||||
Number of licensed users
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{billingInformation.seats}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
Subscription Status
|
||||
</p>
|
||||
<p className="text-sm text-text-500">
|
||||
Current state of your subscription
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{statusToDisplay(billingInformation.subscription_status)}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
Billing Start
|
||||
</p>
|
||||
<p className="text-sm text-text-500">
|
||||
Start date of current billing cycle
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{new Date(
|
||||
billingInformation.billing_start
|
||||
).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">Billing End</p>
|
||||
<p className="text-sm text-text-500">
|
||||
End date of current billing cycle
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{new Date(billingInformation.billing_end).toLocaleDateString()}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!billingInformation.payment_method_enabled && (
|
||||
<div className="mt-4 p-4 bg-yellow-100 border-l-4 border-yellow-500 text-yellow-700">
|
||||
<p className="font-bold">Notice:</p>
|
||||
<p>
|
||||
You'll need to add a payment method before your trial ends to
|
||||
continue using the service.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{billingInformation.subscription_status === "trialing" ? (
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md mt-8">
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
No cap on users during trial
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="flex items-center space-x-4 mt-8">
|
||||
<div className="flex items-center space-x-4">
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
Current Seats:
|
||||
</p>
|
||||
<p className="text-xl font-semibold text-text-900">
|
||||
{billingInformation.seats}
|
||||
</p>
|
||||
</div>
|
||||
<p className="text-sm text-text-500">
|
||||
Seats automatically update based on adding, removing, or inviting
|
||||
users.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="bg-white p-5 rounded-lg shadow-sm transition-all duration-300 hover:shadow-md">
|
||||
<div className="flex justify-between items-center mb-4">
|
||||
<div>
|
||||
<p className="text-lg font-medium text-text-700">
|
||||
Manage Subscription
|
||||
</p>
|
||||
<p className="text-sm text-text-500">
|
||||
View your plan, update payment, or change subscription
|
||||
</p>
|
||||
</div>
|
||||
<SettingsIcon className="text-text-600" size={20} />
|
||||
</div>
|
||||
<button
|
||||
onClick={handleManageSubscription}
|
||||
className="bg-background-600 text-white px-4 py-2 rounded-md hover:bg-background-700 transition duration-300 ease-in-out focus:outline-none focus:ring-2 focus:ring-text-500 focus:ring-opacity-50 font-medium shadow-sm text-sm flex items-center justify-center"
|
||||
>
|
||||
<ArrowFatUp className="mr-2" size={16} />
|
||||
Manage Subscription
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import React from "react";
|
||||
|
||||
interface InfoItemProps {
|
||||
title: string;
|
||||
value: string;
|
||||
}
|
||||
|
||||
export function InfoItem({ title, value }: InfoItemProps) {
|
||||
return (
|
||||
<div className="bg-muted p-4 rounded-lg">
|
||||
<p className="text-sm font-medium text-muted-foreground mb-1">{title}</p>
|
||||
<p className="text-lg font-semibold text-foreground dark:text-white">
|
||||
{value}
|
||||
</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
import React from "react";
|
||||
import { InfoItem } from "./InfoItem";
|
||||
import { statusToDisplay } from "./utils";
|
||||
|
||||
interface SubscriptionSummaryProps {
|
||||
billingInformation: any;
|
||||
}
|
||||
|
||||
export function SubscriptionSummary({
|
||||
billingInformation,
|
||||
}: SubscriptionSummaryProps) {
|
||||
return (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<InfoItem
|
||||
title="Subscription Status"
|
||||
value={statusToDisplay(billingInformation.status)}
|
||||
/>
|
||||
<InfoItem title="Seats" value={billingInformation.seats.toString()} />
|
||||
<InfoItem
|
||||
title="Billing Start"
|
||||
value={new Date(
|
||||
billingInformation.current_period_start
|
||||
).toLocaleDateString()}
|
||||
/>
|
||||
<InfoItem
|
||||
title="Billing End"
|
||||
value={new Date(
|
||||
billingInformation.current_period_end
|
||||
).toLocaleDateString()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,19 +0,0 @@
|
||||
export interface BillingInformation {
|
||||
status: string;
|
||||
trial_end: Date | null;
|
||||
current_period_end: Date;
|
||||
payment_method_enabled: boolean;
|
||||
cancel_at_period_end: boolean;
|
||||
current_period_start: Date;
|
||||
number_of_seats: number;
|
||||
canceled_at: Date | null;
|
||||
trial_start: Date | null;
|
||||
seats: number;
|
||||
}
|
||||
|
||||
export enum BillingStatus {
|
||||
TRIALING = "trialing",
|
||||
ACTIVE = "active",
|
||||
CANCELLED = "cancelled",
|
||||
EXPIRED = "expired",
|
||||
}
|
||||
@@ -3,16 +3,10 @@ import BillingInformationPage from "./BillingInformationPage";
|
||||
import { MdOutlineCreditCard } from "react-icons/md";
|
||||
|
||||
export interface BillingInformation {
|
||||
stripe_subscription_id: string;
|
||||
status: string;
|
||||
current_period_start: Date;
|
||||
current_period_end: Date;
|
||||
number_of_seats: number;
|
||||
cancel_at_period_end: boolean;
|
||||
canceled_at: Date | null;
|
||||
trial_start: Date | null;
|
||||
trial_end: Date | null;
|
||||
seats: number;
|
||||
subscription_status: string;
|
||||
billing_start: Date;
|
||||
billing_end: Date;
|
||||
payment_method_enabled: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -35,16 +35,9 @@ export const statusToDisplay = (status: string) => {
|
||||
|
||||
export const useBillingInformation = () => {
|
||||
const url = "/api/tenants/billing-information";
|
||||
const swrResponse = useSWR<BillingInformation>(url, async (url: string) => {
|
||||
const res = await fetch(url);
|
||||
if (!res.ok) {
|
||||
const errorData = await res.json();
|
||||
throw new Error(
|
||||
errorData.message || "Failed to fetch billing information"
|
||||
);
|
||||
}
|
||||
return res.json();
|
||||
});
|
||||
const swrResponse = useSWR<BillingInformation>(url, (url: string) =>
|
||||
fetch(url).then((res) => res.json())
|
||||
);
|
||||
|
||||
return {
|
||||
...swrResponse,
|
||||
|
||||
@@ -26,7 +26,7 @@ export default async function GalleryPage(props: {
|
||||
chatSessions,
|
||||
folders,
|
||||
openedFolders,
|
||||
sidebarInitiallyVisible,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
@@ -43,8 +43,8 @@ export default async function GalleryPage(props: {
|
||||
value={{
|
||||
inputPrompts,
|
||||
chatSessions,
|
||||
toggledSidebar: toggleSidebar,
|
||||
proSearchToggled,
|
||||
sidebarInitiallyVisible,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
|
||||
@@ -70,7 +70,6 @@
|
||||
--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%;
|
||||
@@ -121,7 +120,6 @@
|
||||
|
||||
/* agent references */
|
||||
--agent-sidebar: #be5d0e;
|
||||
--agent-hovered: #d16b10;
|
||||
--agent: #e47011;
|
||||
--lighter-agent: #f59e0b;
|
||||
|
||||
@@ -249,7 +247,6 @@
|
||||
--accent-background: #333333;
|
||||
|
||||
--accent-background-hovered: #2f2f2f;
|
||||
--accent-background-selected: #222222;
|
||||
|
||||
--text-darker: #f0f0f0;
|
||||
|
||||
@@ -342,7 +339,6 @@
|
||||
|
||||
/* Agent references */
|
||||
--agent-sidebar: #be5d0e; /* You can keep or lighten/darken if desired */
|
||||
--agent-hovered: #f07c13;
|
||||
--agent: #e47011;
|
||||
--lighter-agent: #f59e0b;
|
||||
}
|
||||
@@ -660,11 +656,6 @@ ul > li > p {
|
||||
color: white;
|
||||
}
|
||||
|
||||
.dark li,
|
||||
.dark h1,
|
||||
.dark h2,
|
||||
.dark h3,
|
||||
.dark h4,
|
||||
.dark h5 {
|
||||
.dark li {
|
||||
color: #e5e5e5;
|
||||
}
|
||||
|
||||
@@ -13,10 +13,7 @@ import {
|
||||
import { Metadata } from "next";
|
||||
import { buildClientUrl } from "@/lib/utilsSS";
|
||||
import { Inter } from "next/font/google";
|
||||
import {
|
||||
EnterpriseSettings,
|
||||
ApplicationStatus,
|
||||
} from "./admin/settings/interfaces";
|
||||
import { EnterpriseSettings, GatingType } from "./admin/settings/interfaces";
|
||||
import { fetchAssistantData } from "@/lib/chat/fetchAssistantdata";
|
||||
import { AppProvider } from "@/components/context/AppProvider";
|
||||
import { PHProvider } from "./providers";
|
||||
@@ -31,7 +28,6 @@ import { WebVitals } from "./web-vitals";
|
||||
import { ThemeProvider } from "next-themes";
|
||||
import CloudError from "@/components/errorPages/CloudErrorPage";
|
||||
import Error from "@/components/errorPages/ErrorPage";
|
||||
import AccessRestrictedPage from "@/components/errorPages/AccessRestrictedPage";
|
||||
|
||||
const inter = Inter({
|
||||
subsets: ["latin"],
|
||||
@@ -79,7 +75,7 @@ export default async function RootLayout({
|
||||
]);
|
||||
|
||||
const productGating =
|
||||
combinedSettings?.settings.application_status ?? ApplicationStatus.ACTIVE;
|
||||
combinedSettings?.settings.product_gating ?? GatingType.NONE;
|
||||
|
||||
const getPageContent = async (content: React.ReactNode) => (
|
||||
<html
|
||||
@@ -134,16 +130,40 @@ export default async function RootLayout({
|
||||
</html>
|
||||
);
|
||||
|
||||
if (productGating === ApplicationStatus.GATED_ACCESS) {
|
||||
return getPageContent(<AccessRestrictedPage />);
|
||||
}
|
||||
|
||||
if (!combinedSettings) {
|
||||
return getPageContent(
|
||||
NEXT_PUBLIC_CLOUD_ENABLED ? <CloudError /> : <Error />
|
||||
);
|
||||
}
|
||||
|
||||
if (productGating === GatingType.FULL) {
|
||||
return getPageContent(
|
||||
<div className="flex flex-col items-center justify-center min-h-screen">
|
||||
<div className="mb-2 flex items-center max-w-[175px]">
|
||||
<LogoType />
|
||||
</div>
|
||||
<CardSection className="w-full max-w-md">
|
||||
<h1 className="text-2xl font-bold mb-4 text-error">
|
||||
Access Restricted
|
||||
</h1>
|
||||
<p className="text-text-500 mb-4">
|
||||
We regret to inform you that your access to Onyx has been
|
||||
temporarily suspended due to a lapse in your subscription.
|
||||
</p>
|
||||
<p className="text-text-500 mb-4">
|
||||
To reinstate your access and continue benefiting from Onyx's
|
||||
powerful features, please update your payment information.
|
||||
</p>
|
||||
<p className="text-text-500">
|
||||
If you're an admin, you can resolve this by visiting the
|
||||
billing section. For other users, please reach out to your
|
||||
administrator to address this matter.
|
||||
</p>
|
||||
</CardSection>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const { assistants, hasAnyConnectors, hasImageCompatibleModel } =
|
||||
assistantsData;
|
||||
|
||||
|
||||
@@ -74,7 +74,7 @@ export function IndexAttemptStatus({
|
||||
);
|
||||
} else if (status === "not_started") {
|
||||
badge = (
|
||||
<Badge variant="not_started" icon={FiClock}>
|
||||
<Badge variant="purple" icon={FiClock}>
|
||||
Scheduled
|
||||
</Badge>
|
||||
);
|
||||
|
||||
@@ -33,9 +33,6 @@ import { MdOutlineCreditCard } from "react-icons/md";
|
||||
import { UserSettingsModal } from "@/app/chat/modal/UserSettingsModal";
|
||||
import { usePopup } from "./connectors/Popup";
|
||||
import { useChatContext } from "../context/ChatContext";
|
||||
import { ApplicationStatus } from "@/app/admin/settings/interfaces";
|
||||
import Link from "next/link";
|
||||
import { Button } from "../ui/button";
|
||||
|
||||
export function ClientLayout({
|
||||
user,
|
||||
@@ -77,23 +74,6 @@ export function ClientLayout({
|
||||
defaultModel={user?.preferences?.default_model!}
|
||||
/>
|
||||
)}
|
||||
{settings?.settings.application_status ===
|
||||
ApplicationStatus.PAYMENT_REMINDER && (
|
||||
<div className="fixed top-2 left-1/2 transform -translate-x-1/2 bg-amber-400 dark:bg-amber-500 text-gray-900 dark:text-gray-100 p-4 rounded-lg shadow-lg z-50 max-w-md text-center">
|
||||
<strong className="font-bold">Warning:</strong> Your trial ends in
|
||||
less than 2 days and no payment method has been added.
|
||||
<div className="mt-2">
|
||||
<Link href="/admin/billing">
|
||||
<Button
|
||||
variant="default"
|
||||
className="bg-amber-600 hover:bg-amber-700 text-white"
|
||||
>
|
||||
Update Billing Information
|
||||
</Button>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="default-scrollbar flex-none text-text-settings-sidebar bg-background-sidebar dark:bg-[#000] w-[250px] overflow-x-hidden z-20 pt-2 pb-8 h-full border-r border-border dark:border-none miniscroll overflow-auto">
|
||||
<AdminSidebar
|
||||
|
||||
@@ -57,7 +57,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
llmProviders,
|
||||
folders,
|
||||
openedFolders,
|
||||
sidebarInitiallyVisible,
|
||||
toggleSidebar,
|
||||
defaultAssistantId,
|
||||
shouldShowWelcomeModal,
|
||||
ccPairs,
|
||||
@@ -71,7 +71,7 @@ export async function Layout({ children }: { children: React.ReactNode }) {
|
||||
inputPrompts,
|
||||
chatSessions,
|
||||
proSearchToggled,
|
||||
sidebarInitiallyVisible,
|
||||
toggledSidebar: toggleSidebar,
|
||||
availableSources,
|
||||
ccPairs,
|
||||
documentSets,
|
||||
|
||||
@@ -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 { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
|
||||
const DeleteUserButton = ({
|
||||
user,
|
||||
@@ -38,7 +38,7 @@ const DeleteUserButton = ({
|
||||
return (
|
||||
<>
|
||||
{showDeleteModal && (
|
||||
<ConfirmEntityModal
|
||||
<DeleteEntityModal
|
||||
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 { ConfirmEntityModal } from "@/components/modals/ConfirmEntityModal";
|
||||
import { DeleteEntityModal } from "@/components/modals/DeleteEntityModal";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export const LeaveOrganizationButton = ({
|
||||
@@ -46,9 +46,8 @@ export const LeaveOrganizationButton = ({
|
||||
return (
|
||||
<>
|
||||
{showLeaveModal && (
|
||||
<ConfirmEntityModal
|
||||
variant="action"
|
||||
actionButtonText="Leave"
|
||||
<DeleteEntityModal
|
||||
deleteButtonText="Leave"
|
||||
entityType="organization"
|
||||
entityName="your organization"
|
||||
onClose={() => setShowLeaveModal(false)}
|
||||
|
||||
@@ -48,6 +48,7 @@ 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
|
||||
@@ -57,16 +58,11 @@ export function StarterMessages({
|
||||
text-[15px] shadow-xs transition
|
||||
enabled:hover:bg-background-dark/75
|
||||
disabled:cursor-not-allowed
|
||||
overflow-hidden
|
||||
break-words
|
||||
truncate
|
||||
text-ellipsis
|
||||
line-clamp-3
|
||||
`}
|
||||
style={{ height: "5.6rem" }}
|
||||
style={{ height: "5.4rem" }}
|
||||
>
|
||||
<div className="overflow-hidden text-ellipsis line-clamp-3 pr-1 pb-1">
|
||||
{starterMessage.name}
|
||||
</div>
|
||||
{starterMessage.name}
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -3,29 +3,27 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
export default function FunctionalWrapper({
|
||||
initiallyVisible,
|
||||
initiallyToggled,
|
||||
content,
|
||||
}: {
|
||||
content: (
|
||||
sidebarVisible: boolean,
|
||||
toggle: (visible?: boolean) => void
|
||||
toggledSidebar: boolean,
|
||||
toggle: (toggled?: boolean) => void
|
||||
) => ReactNode;
|
||||
initiallyVisible?: boolean;
|
||||
initiallyToggled: boolean;
|
||||
}) {
|
||||
const [sidebarVisible, setSidebarVisible] = useState(
|
||||
initiallyVisible || false
|
||||
);
|
||||
const [toggledSidebar, setToggledSidebar] = useState(initiallyToggled);
|
||||
|
||||
const toggle = (value?: boolean) => {
|
||||
setSidebarVisible((sidebarVisible) =>
|
||||
value !== undefined ? value : !sidebarVisible
|
||||
setToggledSidebar((toggledSidebar) =>
|
||||
value !== undefined ? value : !toggledSidebar
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="overscroll-y-contain overflow-y-scroll overscroll-contain left-0 top-0 w-full h-svh">
|
||||
{content(sidebarVisible, toggle)}
|
||||
{content(toggledSidebar, toggle)}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -17,7 +17,7 @@ export default function FunctionalHeader({
|
||||
currentChatSession,
|
||||
setSharingModalVisible,
|
||||
toggleSidebar = () => null,
|
||||
documentSidebarVisible,
|
||||
documentSidebarToggled,
|
||||
reset = () => null,
|
||||
sidebarToggled,
|
||||
toggleUserSettings,
|
||||
@@ -31,7 +31,7 @@ export default function FunctionalHeader({
|
||||
toggleSidebar?: () => void;
|
||||
toggleUserSettings?: () => void;
|
||||
hideUserDropdown?: boolean;
|
||||
documentSidebarVisible?: boolean;
|
||||
documentSidebarToggled?: boolean;
|
||||
}) {
|
||||
const settings = useContext(SettingsContext);
|
||||
useEffect(() => {
|
||||
@@ -89,7 +89,6 @@ export default function FunctionalHeader({
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
|
||||
${sidebarToggled ? "w-[250px]" : "w-[0px]"}
|
||||
`}
|
||||
/>
|
||||
@@ -98,19 +97,19 @@ export default function FunctionalHeader({
|
||||
className={`
|
||||
absolute
|
||||
${
|
||||
documentSidebarVisible &&
|
||||
documentSidebarToggled &&
|
||||
sidebarToggled &&
|
||||
"left-[calc(50%-75px)]"
|
||||
}
|
||||
${
|
||||
documentSidebarVisible && !sidebarToggled
|
||||
documentSidebarToggled && !sidebarToggled
|
||||
? "left-[calc(50%-175px)]"
|
||||
: !documentSidebarVisible && sidebarToggled
|
||||
: !documentSidebarToggled && sidebarToggled
|
||||
? "left-[calc(50%+100px)]"
|
||||
: "left-1/2"
|
||||
}
|
||||
${
|
||||
documentSidebarVisible || sidebarToggled
|
||||
documentSidebarToggled || sidebarToggled
|
||||
? "mobile:w-[40vw] max-w-[40vw]"
|
||||
: "mobile:w-[50vw] max-w-[60vw]"
|
||||
}
|
||||
@@ -191,7 +190,7 @@ export default function FunctionalHeader({
|
||||
duration-300
|
||||
ease-in-out
|
||||
h-full
|
||||
${documentSidebarVisible ? "w-[400px]" : "w-[0px]"}
|
||||
${documentSidebarToggled ? "w-[400px]" : "w-[0px]"}
|
||||
`}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { Dispatch, SetStateAction, useEffect, useRef } from "react";
|
||||
|
||||
interface UseSidebarVisibilityProps {
|
||||
sidebarVisible: boolean;
|
||||
toggledSidebar: boolean;
|
||||
sidebarElementRef: React.RefObject<HTMLElement>;
|
||||
showDocSidebar: boolean;
|
||||
setShowDocSidebar: Dispatch<SetStateAction<boolean>>;
|
||||
@@ -11,7 +11,7 @@ interface UseSidebarVisibilityProps {
|
||||
}
|
||||
|
||||
export const useSidebarVisibility = ({
|
||||
sidebarVisible,
|
||||
toggledSidebar,
|
||||
sidebarElementRef,
|
||||
setShowDocSidebar,
|
||||
setToggled,
|
||||
@@ -55,7 +55,7 @@ export const useSidebarVisibility = ({
|
||||
currentXPosition > 100 &&
|
||||
showDocSidebar &&
|
||||
!isWithinSidebar &&
|
||||
!sidebarVisible
|
||||
!toggledSidebar
|
||||
) {
|
||||
setTimeout(() => {
|
||||
setShowDocSidebar((showDocSidebar) => {
|
||||
@@ -88,7 +88,7 @@ export const useSidebarVisibility = ({
|
||||
}
|
||||
};
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [showDocSidebar, sidebarVisible, sidebarElementRef, mobile]);
|
||||
}, [showDocSidebar, toggledSidebar, 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.is_default_persona);
|
||||
return assistants.filter((a) => a.builtin_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.is_default_persona);
|
||||
return assistants.filter((a) => a.builtin_persona);
|
||||
}
|
||||
});
|
||||
}, [user?.preferences?.pinned_assistants, assistants]);
|
||||
|
||||
@@ -16,7 +16,7 @@ import { useRouter } from "next/navigation";
|
||||
|
||||
interface ChatContextProps {
|
||||
chatSessions: ChatSession[];
|
||||
sidebarInitiallyVisible: boolean;
|
||||
toggledSidebar: boolean;
|
||||
availableSources: ValidSources[];
|
||||
ccPairs: CCPairBasicInfo[];
|
||||
tags: Tag[];
|
||||
|
||||
@@ -1,148 +0,0 @@
|
||||
"use client";
|
||||
import { FiLock } from "react-icons/fi";
|
||||
import ErrorPageLayout from "./ErrorPageLayout";
|
||||
import { fetchCustomerPortal } from "@/app/ee/admin/billing/utils";
|
||||
import { useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { logout } from "@/lib/user";
|
||||
import { loadStripe } from "@stripe/stripe-js";
|
||||
import { NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY } from "@/lib/constants";
|
||||
|
||||
const fetchResubscriptionSession = async () => {
|
||||
const response = await fetch("/api/tenants/create-subscription-session", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error("Failed to create resubscription session");
|
||||
}
|
||||
return response.json();
|
||||
};
|
||||
|
||||
export default function AccessRestricted() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const router = useRouter();
|
||||
|
||||
const handleManageSubscription = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const response = await fetchCustomerPortal();
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(
|
||||
`Failed to create customer portal session: ${
|
||||
errorData.message || response.statusText
|
||||
}`
|
||||
);
|
||||
}
|
||||
|
||||
const { url } = await response.json();
|
||||
|
||||
if (!url) {
|
||||
throw new Error("No portal URL returned from the server");
|
||||
}
|
||||
|
||||
router.push(url);
|
||||
} catch (error) {
|
||||
console.error("Error creating customer portal session:", error);
|
||||
setError("Error opening customer portal. Please try again later.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleResubscribe = async () => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
if (!NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY) {
|
||||
setError("Stripe public key not found");
|
||||
setIsLoading(false);
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const { sessionId } = await fetchResubscriptionSession();
|
||||
const stripe = await loadStripe(NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY);
|
||||
|
||||
if (stripe) {
|
||||
await stripe.redirectToCheckout({ sessionId });
|
||||
} else {
|
||||
throw new Error("Stripe failed to load");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating resubscription session:", error);
|
||||
setError("Error opening resubscription page. Please try again later.");
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<ErrorPageLayout>
|
||||
<h1 className="text-2xl font-semibold flex items-center gap-2 mb-4 text-gray-800 dark:text-gray-200">
|
||||
<p>Access Restricted</p>
|
||||
<FiLock className="text-error inline-block" />
|
||||
</h1>
|
||||
<div className="space-y-4 text-gray-600 dark:text-gray-300">
|
||||
<p>
|
||||
We regret to inform you that your access to Onyx has been temporarily
|
||||
suspended due to a lapse in your subscription.
|
||||
</p>
|
||||
<p>
|
||||
To reinstate your access and continue benefiting from Onyx's
|
||||
powerful features, please update your payment information.
|
||||
</p>
|
||||
<p>
|
||||
If you're an admin, you can manage your subscription by clicking
|
||||
the button below. For other users, please reach out to your
|
||||
administrator to address this matter.
|
||||
</p>
|
||||
<div className="flex flex-col space-y-4 sm:flex-row sm:space-y-0 sm:space-x-4">
|
||||
<Button
|
||||
onClick={handleResubscribe}
|
||||
disabled={isLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
{isLoading ? "Loading..." : "Resubscribe"}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleManageSubscription}
|
||||
disabled={isLoading}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Manage Existing Subscription
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={async () => {
|
||||
await logout();
|
||||
window.location.reload();
|
||||
}}
|
||||
className="w-full sm:w-auto"
|
||||
>
|
||||
Log out
|
||||
</Button>
|
||||
</div>
|
||||
{error && <p className="text-error">{error}</p>}
|
||||
<p>
|
||||
Need help? Join our{" "}
|
||||
<a
|
||||
className="text-blue-500 hover:text-blue-700 dark:text-blue-400 dark:hover:text-blue-300"
|
||||
href="https://join.slack.com/t/danswer/shared_invite/zt-1w76msxmd-HJHLe3KNFIAIzk_0dSOKaQ"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
>
|
||||
Slack community
|
||||
</a>{" "}
|
||||
for support.
|
||||
</p>
|
||||
</div>
|
||||
</ErrorPageLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,67 +0,0 @@
|
||||
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>
|
||||
);
|
||||
};
|
||||
51
web/src/components/modals/DeleteEntityModal.tsx
Normal file
51
web/src/components/modals/DeleteEntityModal.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
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="border border-neutral-300 hover:text-neutral-900 bg-neutral-100 dark:!bg-[#000] dark:border-neutral-700"
|
||||
className="dark:border dark:!bg-[#000] border-neutral-700"
|
||||
width="mb-2 max-w-lg"
|
||||
>
|
||||
{document_info?.document ? (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import {
|
||||
CombinedSettings,
|
||||
EnterpriseSettings,
|
||||
ApplicationStatus,
|
||||
GatingType,
|
||||
Settings,
|
||||
} from "@/app/admin/settings/interfaces";
|
||||
import {
|
||||
@@ -45,7 +45,7 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
|
||||
if (results[0].status === 403 || results[0].status === 401) {
|
||||
settings = {
|
||||
auto_scroll: true,
|
||||
application_status: ApplicationStatus.ACTIVE,
|
||||
product_gating: GatingType.NONE,
|
||||
gpu_enabled: false,
|
||||
maximum_chat_retention_days: null,
|
||||
notifications: [],
|
||||
|
||||
@@ -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-purple-200 bg-purple-50 text-purple-700 dark:border-purple-700 dark:bg-purple-900 dark:text-purple-100",
|
||||
"border-neutral-200 bg-neutral-50 text-neutral-600 dark:border-neutral-700 dark:bg-neutral-800 dark:text-neutral-100",
|
||||
},
|
||||
},
|
||||
defaultVariants: {
|
||||
|
||||
@@ -42,7 +42,7 @@ interface FetchChatDataResult {
|
||||
folders: Folder[];
|
||||
openedFolders: Record<string, boolean>;
|
||||
defaultAssistantId?: number;
|
||||
sidebarInitiallyVisible: boolean;
|
||||
toggleSidebar: 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 sidebarInitiallyVisible =
|
||||
const toggleSidebar =
|
||||
!user?.is_anonymous_user &&
|
||||
(sidebarToggled
|
||||
? sidebarToggled.value.toLocaleLowerCase() == "true" || false
|
||||
@@ -230,7 +230,7 @@ export async function fetchChatData(searchParams: {
|
||||
openedFolders,
|
||||
defaultAssistantId,
|
||||
finalDocumentSidebarInitialWidth,
|
||||
sidebarInitiallyVisible,
|
||||
toggleSidebar,
|
||||
shouldShowWelcomeModal,
|
||||
inputPrompts,
|
||||
proSearchToggled,
|
||||
|
||||
@@ -1115,24 +1115,7 @@ For example, specifying .*-support.* as a "channel" will cause the connector to
|
||||
optional: false,
|
||||
},
|
||||
],
|
||||
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.",
|
||||
},
|
||||
],
|
||||
advanced_values: [],
|
||||
overrideDefaultFreq: 60 * 60 * 24,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -91,6 +91,3 @@ export const NEXT_PUBLIC_ENABLE_CHROME_EXTENSION =
|
||||
export const NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK =
|
||||
process.env.NEXT_PUBLIC_INCLUDE_ERROR_POPUP_SUPPORT_LINK?.toLowerCase() ===
|
||||
"true";
|
||||
|
||||
export const NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY =
|
||||
process.env.NEXT_PUBLIC_STRIPE_PUBLISHABLE_KEY;
|
||||
|
||||
@@ -31,7 +31,3 @@ export const LocalStorageKeys = {
|
||||
SHOW_SHORTCUTS: "showShortcuts",
|
||||
USE_ONYX_AS_NEW_TAB: "useOnyxAsDefaultNewTab",
|
||||
};
|
||||
|
||||
export const SEARCH_PARAMS = {
|
||||
DEFAULT_SIDEBAR_OFF: "defaultSidebarOff",
|
||||
};
|
||||
|
||||
@@ -76,7 +76,6 @@ export interface StreamStopInfo {
|
||||
stop_reason: StreamStopReason;
|
||||
level?: number;
|
||||
level_question_num?: number;
|
||||
stream_type?: "sub_answer" | "sub_questions" | "main_answer";
|
||||
}
|
||||
|
||||
export interface ErrorMessagePacket {
|
||||
|
||||
@@ -108,7 +108,6 @@ 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)",
|
||||
@@ -263,7 +262,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