Compare commits

..

15 Commits

Author SHA1 Message Date
pablodanswer
04cedcda3e docker compose spacing 2024-12-17 13:25:11 -08:00
pablodanswer
33aae6450c update configs 2024-12-17 09:20:41 -08:00
pablodanswer
54ba45452e k 2024-12-16 18:31:07 -08:00
pablodanswer
9c65041825 k 2024-12-16 18:31:07 -08:00
pablodanswer
5c93d83d46 nit 2024-12-16 18:31:07 -08:00
pablodanswer
454eb11195 additional clarity for db session operations 2024-12-16 18:31:07 -08:00
pablodanswer
9e454410c6 minor cleanup 2024-12-16 18:31:07 -08:00
pablodanswer
7e90112460 quick clean up 2024-12-16 18:31:07 -08:00
pablodanswer
0c3c0a31bb cleanup 2024-12-16 18:31:07 -08:00
pablodanswer
0a006b49dd add deployment options 2024-12-16 18:31:07 -08:00
pablodanswer
ba95e88f65 improve typing 2024-12-16 18:31:07 -08:00
pablodanswer
140d10414a k 2024-12-16 18:31:07 -08:00
pablodanswer
ff8378525b k 2024-12-16 18:31:07 -08:00
pablodanswer
326ee120a4 functional iam auth 2024-12-16 18:31:07 -08:00
pablodanswer
fcc6b52a23 k 2024-12-16 18:31:07 -08:00
53 changed files with 536 additions and 1330 deletions

View File

@@ -1,121 +0,0 @@
"""properly_cascade
Revision ID: 35e518e0ddf4
Revises: 91a0a4d62b14
Create Date: 2024-09-20 21:24:04.891018
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "35e518e0ddf4"
down_revision = "91a0a4d62b14"
branch_labels = None
depends_on = None
def upgrade() -> None:
# Update chat_message foreign key constraint
op.drop_constraint(
"chat_message_chat_session_id_fkey", "chat_message", type_="foreignkey"
)
op.create_foreign_key(
"chat_message_chat_session_id_fkey",
"chat_message",
"chat_session",
["chat_session_id"],
["id"],
ondelete="CASCADE",
)
# Update chat_message__search_doc foreign key constraints
op.drop_constraint(
"chat_message__search_doc_chat_message_id_fkey",
"chat_message__search_doc",
type_="foreignkey",
)
op.drop_constraint(
"chat_message__search_doc_search_doc_id_fkey",
"chat_message__search_doc",
type_="foreignkey",
)
op.create_foreign_key(
"chat_message__search_doc_chat_message_id_fkey",
"chat_message__search_doc",
"chat_message",
["chat_message_id"],
["id"],
ondelete="CASCADE",
)
op.create_foreign_key(
"chat_message__search_doc_search_doc_id_fkey",
"chat_message__search_doc",
"search_doc",
["search_doc_id"],
["id"],
ondelete="CASCADE",
)
# Add CASCADE delete for tool_call foreign key
op.drop_constraint("tool_call_message_id_fkey", "tool_call", type_="foreignkey")
op.create_foreign_key(
"tool_call_message_id_fkey",
"tool_call",
"chat_message",
["message_id"],
["id"],
ondelete="CASCADE",
)
def downgrade() -> None:
# Revert chat_message foreign key constraint
op.drop_constraint(
"chat_message_chat_session_id_fkey", "chat_message", type_="foreignkey"
)
op.create_foreign_key(
"chat_message_chat_session_id_fkey",
"chat_message",
"chat_session",
["chat_session_id"],
["id"],
)
# Revert chat_message__search_doc foreign key constraints
op.drop_constraint(
"chat_message__search_doc_chat_message_id_fkey",
"chat_message__search_doc",
type_="foreignkey",
)
op.drop_constraint(
"chat_message__search_doc_search_doc_id_fkey",
"chat_message__search_doc",
type_="foreignkey",
)
op.create_foreign_key(
"chat_message__search_doc_chat_message_id_fkey",
"chat_message__search_doc",
"chat_message",
["chat_message_id"],
["id"],
)
op.create_foreign_key(
"chat_message__search_doc_search_doc_id_fkey",
"chat_message__search_doc",
"search_doc",
["search_doc_id"],
["id"],
)
# Revert tool_call foreign key constraint
op.drop_constraint("tool_call_message_id_fkey", "tool_call", type_="foreignkey")
op.create_foreign_key(
"tool_call_message_id_fkey",
"tool_call",
"chat_message",
["message_id"],
["id"],
)

View File

@@ -1,87 +0,0 @@
"""delete workspace
Revision ID: c0aab6edb6dd
Revises: 35e518e0ddf4
Create Date: 2024-12-17 14:37:07.660631
"""
from alembic import op
# revision identifiers, used by Alembic.
revision = "c0aab6edb6dd"
down_revision = "35e518e0ddf4"
branch_labels = None
depends_on = None
def upgrade() -> None:
op.execute(
"""
UPDATE connector
SET connector_specific_config = connector_specific_config - 'workspace'
WHERE source = 'SLACK'
"""
)
def downgrade() -> None:
import json
from sqlalchemy import text
from slack_sdk import WebClient
conn = op.get_bind()
# Fetch all Slack credentials
creds_result = conn.execute(
text("SELECT id, credential_json FROM credential WHERE source = 'SLACK'")
)
all_slack_creds = creds_result.fetchall()
if not all_slack_creds:
return
for cred_row in all_slack_creds:
credential_id, credential_json = cred_row
credential_json = (
credential_json.tobytes().decode("utf-8")
if isinstance(credential_json, memoryview)
else credential_json.decode("utf-8")
)
credential_data = json.loads(credential_json)
slack_bot_token = credential_data.get("slack_bot_token")
if not slack_bot_token:
print(
f"No slack_bot_token found for credential {credential_id}. "
"Your Slack connector will not function until you upgrade and provide a valid token."
)
continue
client = WebClient(token=slack_bot_token)
try:
auth_response = client.auth_test()
workspace = auth_response["url"].split("//")[1].split(".")[0]
# Update only the connectors linked to this credential
# (and which are Slack connectors).
op.execute(
f"""
UPDATE connector AS c
SET connector_specific_config = jsonb_set(
connector_specific_config,
'{{workspace}}',
to_jsonb('{workspace}'::text)
)
FROM connector_credential_pair AS ccp
WHERE ccp.connector_id = c.id
AND c.source = 'SLACK'
AND ccp.credential_id = {credential_id}
"""
)
except Exception:
print(
f"We were unable to get the workspace url for your Slack Connector with id {credential_id}."
)
print("This connector will no longer work until you upgrade.")
continue

View File

@@ -53,5 +53,3 @@ OAUTH_GOOGLE_DRIVE_CLIENT_SECRET = os.environ.get(
# when the capture is called. These defaults prevent Posthog issues from breaking the Onyx app
POSTHOG_API_KEY = os.environ.get("POSTHOG_API_KEY") or "FooBar"
POSTHOG_HOST = os.environ.get("POSTHOG_HOST") or "https://us.i.posthog.com"
HUBSPOT_TRACKING_URL = os.environ.get("HUBSPOT_TRACKING_URL")

View File

@@ -3,15 +3,12 @@ import logging
import uuid
import aiohttp # Async HTTP client
import httpx
from fastapi import HTTPException
from fastapi import Request
from sqlalchemy import select
from sqlalchemy.orm import Session
from ee.onyx.configs.app_configs import ANTHROPIC_DEFAULT_API_KEY
from ee.onyx.configs.app_configs import COHERE_DEFAULT_API_KEY
from ee.onyx.configs.app_configs import HUBSPOT_TRACKING_URL
from ee.onyx.configs.app_configs import OPENAI_DEFAULT_API_KEY
from ee.onyx.server.tenants.access import generate_data_plane_token
from ee.onyx.server.tenants.models import TenantCreationPayload
@@ -50,16 +47,13 @@ from shared_configs.enums import EmbeddingProvider
logger = logging.getLogger(__name__)
async def get_or_provision_tenant(
email: str, referral_source: str | None = None, request: Request | None = None
async def get_or_create_tenant_id(
email: str, referral_source: str | None = None
) -> str:
"""Get existing tenant ID for an email or create a new tenant if none exists."""
if not MULTI_TENANT:
return POSTGRES_DEFAULT_SCHEMA
if referral_source and request:
await submit_to_hubspot(email, referral_source, request)
try:
tenant_id = get_tenant_id_for_email(email)
except exceptions.UserNotExists:
@@ -287,36 +281,3 @@ def configure_default_api_keys(db_session: Session) -> None:
logger.info(
"COHERE_DEFAULT_API_KEY not set, skipping Cohere embedding provider configuration"
)
async def submit_to_hubspot(
email: str, referral_source: str | None, request: Request
) -> None:
if not HUBSPOT_TRACKING_URL:
logger.info("HUBSPOT_TRACKING_URL not set, skipping HubSpot submission")
return
# HubSpot tracking cookie
hubspot_cookie = request.cookies.get("hubspotutk")
# IP address
ip_address = request.client.host if request.client else None
data = {
"fields": [
{"name": "email", "value": email},
{"name": "referral_source", "value": referral_source or ""},
],
"context": {
"hutk": hubspot_cookie,
"ipAddress": ip_address,
"pageUri": str(request.url),
"pageName": "User Registration",
},
}
async with httpx.AsyncClient() as client:
response = await client.post(HUBSPOT_TRACKING_URL, json=data)
if response.status_code != 200:
logger.error(f"Failed to submit to HubSpot: {response.text}")

View File

@@ -2,9 +2,6 @@ from posthog import Posthog
from ee.onyx.configs.app_configs import POSTHOG_API_KEY
from ee.onyx.configs.app_configs import POSTHOG_HOST
from onyx.utils.logger import setup_logger
logger = setup_logger()
posthog = Posthog(project_api_key=POSTHOG_API_KEY, host=POSTHOG_HOST)
@@ -14,5 +11,4 @@ def event_telemetry(
event: str,
properties: dict | None = None,
) -> None:
logger.info(f"Capturing Posthog event: {distinct_id} {event} {properties}")
posthog.capture(distinct_id, event, properties)

View File

@@ -27,8 +27,8 @@ from shared_configs.configs import SENTRY_DSN
os.environ["TOKENIZERS_PARALLELISM"] = "false"
os.environ["HF_HUB_DISABLE_TELEMETRY"] = "1"
HF_CACHE_PATH = Path(os.path.expanduser("~")) / ".cache/huggingface"
TEMP_HF_CACHE_PATH = Path(os.path.expanduser("~")) / ".cache/temp_huggingface"
HF_CACHE_PATH = Path("/root/.cache/huggingface/")
TEMP_HF_CACHE_PATH = Path("/root/.cache/temp_huggingface/")
transformer_logging.set_verbosity_error()

View File

@@ -229,20 +229,17 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
request: Optional[Request] = None,
) -> User:
user_count: int | None = None
referral_source = (
request.cookies.get("referral_source", None)
if request is not None
else None
)
referral_source = None
if request is not None:
referral_source = request.cookies.get("referral_source", None)
tenant_id = await fetch_ee_implementation_or_noop(
"onyx.server.tenants.provisioning",
"get_or_provision_tenant",
"get_or_create_tenant_id",
async_return_default_schema,
)(
email=user_create.email,
referral_source=referral_source,
request=request,
)
async with get_async_session_with_tenant(tenant_id) as db_session:
@@ -285,6 +282,25 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
finally:
CURRENT_TENANT_ID_CONTEXTVAR.reset(token)
# Blocking but this should be very quick
with get_session_with_tenant(tenant_id) as db_session:
if not user_count:
create_milestone_and_report(
user=user,
distinct_id=user.email,
event_type=MilestoneRecordType.USER_SIGNED_UP,
properties=None,
db_session=db_session,
)
else:
create_milestone_and_report(
user=user,
distinct_id=user.email,
event_type=MilestoneRecordType.MULTIPLE_USERS,
properties=None,
db_session=db_session,
)
return user
async def validate_password(self, password: str, _: schemas.UC | models.UP) -> None:
@@ -330,18 +346,17 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
associate_by_email: bool = False,
is_verified_by_default: bool = False,
) -> User:
referral_source = (
getattr(request.state, "referral_source", None) if request else None
)
referral_source = None
if request:
referral_source = getattr(request.state, "referral_source", None)
tenant_id = await fetch_ee_implementation_or_noop(
"onyx.server.tenants.provisioning",
"get_or_provision_tenant",
"get_or_create_tenant_id",
async_return_default_schema,
)(
email=account_email,
referral_source=referral_source,
request=request,
)
if not tenant_id:
@@ -403,7 +418,6 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# Add OAuth account
await self.user_db.add_oauth_account(user, oauth_account_dict)
await self.on_after_register(user, request)
else:
@@ -457,39 +471,6 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
async def on_after_register(
self, user: User, request: Optional[Request] = None
) -> None:
tenant_id = await fetch_ee_implementation_or_noop(
"onyx.server.tenants.provisioning",
"get_or_provision_tenant",
async_return_default_schema,
)(
email=user.email,
request=request,
)
token = CURRENT_TENANT_ID_CONTEXTVAR.set(tenant_id)
try:
user_count = await get_user_count()
with get_session_with_tenant(tenant_id=tenant_id) as db_session:
if user_count == 1:
create_milestone_and_report(
user=user,
distinct_id=user.email,
event_type=MilestoneRecordType.USER_SIGNED_UP,
properties=None,
db_session=db_session,
)
else:
create_milestone_and_report(
user=user,
distinct_id=user.email,
event_type=MilestoneRecordType.MULTIPLE_USERS,
properties=None,
db_session=db_session,
)
finally:
CURRENT_TENANT_ID_CONTEXTVAR.reset(token)
logger.notice(f"User {user.id} has registered.")
optional_telemetry(
record_type=RecordType.SIGN_UP,
@@ -521,7 +502,7 @@ class UserManager(UUIDIDMixin, BaseUserManager[User, uuid.UUID]):
# Get tenant_id from mapping table
tenant_id = await fetch_ee_implementation_or_noop(
"onyx.server.tenants.provisioning",
"get_or_provision_tenant",
"get_or_create_tenant_id",
async_return_default_schema,
)(
email=email,
@@ -582,7 +563,7 @@ class TenantAwareJWTStrategy(JWTStrategy):
async def _create_token_data(self, user: User, impersonate: bool = False) -> dict:
tenant_id = await fetch_ee_implementation_or_noop(
"onyx.server.tenants.provisioning",
"get_or_provision_tenant",
"get_or_create_tenant_id",
async_return_default_schema,
)(
email=user.email,

View File

@@ -1,6 +1,4 @@
# These are helper objects for tracking the keys we need to write in redis
import json
from typing import Any
from typing import cast
from redis import Redis
@@ -25,25 +23,3 @@ def celery_get_queue_length(queue: str, r: Redis) -> int:
total_length += cast(int, length)
return total_length
def celery_find_task(task_id: str, queue: str, r: Redis) -> int:
"""This is a redis specific way to find a task for a particular queue in redis.
It is priority aware and knows how to look through the multiple redis lists
used to implement task prioritization.
This operation is not atomic.
This is a linear search O(n) ... so be careful using it when the task queues can be larger.
Returns true if the id is in the queue, False if not.
"""
for priority in range(len(OnyxCeleryPriority)):
queue_name = f"{queue}{CELERY_SEPARATOR}{priority}" if priority > 0 else queue
tasks = cast(list[bytes], r.lrange(queue_name, 0, -1))
for task in tasks:
task_dict: dict[str, Any] = json.loads(task.decode("utf-8"))
if task_dict.get("headers", {}).get("id") == task_id:
return True
return False

View File

@@ -4,80 +4,55 @@ from typing import Any
from onyx.configs.constants import OnyxCeleryPriority
from onyx.configs.constants import OnyxCeleryTask
# we set expires because it isn't necessary to queue up these tasks
# it's only important that they run relatively regularly
tasks_to_schedule = [
{
"name": "check-for-vespa-sync",
"task": OnyxCeleryTask.CHECK_FOR_VESPA_SYNC_TASK,
"schedule": timedelta(seconds=20),
"options": {
"priority": OnyxCeleryPriority.HIGH,
"expires": 60,
},
"options": {"priority": OnyxCeleryPriority.HIGH},
},
{
"name": "check-for-connector-deletion",
"task": OnyxCeleryTask.CHECK_FOR_CONNECTOR_DELETION,
"schedule": timedelta(seconds=20),
"options": {
"priority": OnyxCeleryPriority.HIGH,
"expires": 60,
},
"options": {"priority": OnyxCeleryPriority.HIGH},
},
{
"name": "check-for-indexing",
"task": OnyxCeleryTask.CHECK_FOR_INDEXING,
"schedule": timedelta(seconds=15),
"options": {
"priority": OnyxCeleryPriority.HIGH,
"expires": 60,
},
"options": {"priority": OnyxCeleryPriority.HIGH},
},
{
"name": "check-for-prune",
"task": OnyxCeleryTask.CHECK_FOR_PRUNING,
"schedule": timedelta(seconds=15),
"options": {
"priority": OnyxCeleryPriority.HIGH,
"expires": 60,
},
"options": {"priority": OnyxCeleryPriority.HIGH},
},
{
"name": "kombu-message-cleanup",
"task": OnyxCeleryTask.KOMBU_MESSAGE_CLEANUP_TASK,
"schedule": timedelta(seconds=3600),
"options": {
"priority": OnyxCeleryPriority.LOWEST,
"expires": 60,
},
"options": {"priority": OnyxCeleryPriority.LOWEST},
},
{
"name": "monitor-vespa-sync",
"task": OnyxCeleryTask.MONITOR_VESPA_SYNC,
"schedule": timedelta(seconds=5),
"options": {
"priority": OnyxCeleryPriority.HIGH,
"expires": 60,
},
"options": {"priority": OnyxCeleryPriority.HIGH},
},
{
"name": "check-for-doc-permissions-sync",
"task": OnyxCeleryTask.CHECK_FOR_DOC_PERMISSIONS_SYNC,
"schedule": timedelta(seconds=30),
"options": {
"priority": OnyxCeleryPriority.HIGH,
"expires": 60,
},
"options": {"priority": OnyxCeleryPriority.HIGH},
},
{
"name": "check-for-external-group-sync",
"task": OnyxCeleryTask.CHECK_FOR_EXTERNAL_GROUP_SYNC,
"schedule": timedelta(seconds=20),
"options": {
"priority": OnyxCeleryPriority.HIGH,
"expires": 60,
},
"options": {"priority": OnyxCeleryPriority.HIGH},
},
]

View File

@@ -1,9 +1,7 @@
import time
from datetime import datetime
from datetime import timezone
from http import HTTPStatus
from time import sleep
from typing import Any
import redis
import sentry_sdk
@@ -17,7 +15,6 @@ from redis.lock import Lock as RedisLock
from sqlalchemy.orm import Session
from onyx.background.celery.apps.app_base import task_logger
from onyx.background.celery.celery_redis import celery_find_task
from onyx.background.indexing.job_client import SimpleJobClient
from onyx.background.indexing.run_indexing import run_indexing_entrypoint
from onyx.configs.app_configs import DISABLE_INDEX_UPDATE_ON_SWAP
@@ -29,7 +26,6 @@ from onyx.configs.constants import OnyxCeleryPriority
from onyx.configs.constants import OnyxCeleryQueues
from onyx.configs.constants import OnyxCeleryTask
from onyx.configs.constants import OnyxRedisLocks
from onyx.configs.constants import OnyxRedisSignals
from onyx.db.connector import mark_ccpair_with_indexing_trigger
from onyx.db.connector_credential_pair import fetch_connector_credential_pairs
from onyx.db.connector_credential_pair import get_connector_credential_pair_from_id
@@ -166,19 +162,11 @@ def get_unfenced_index_attempt_ids(db_session: Session, r: redis.Redis) -> list[
bind=True,
)
def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
"""a lightweight task used to kick off indexing tasks.
Occcasionally does some validation of existing state to clear up error conditions"""
time_start = time.monotonic()
tasks_created = 0
locked = False
redis_client = get_redis_client(tenant_id=tenant_id)
r = get_redis_client(tenant_id=tenant_id)
# we need to use celery's redis client to access its redis data
# (which lives on a different db number)
redis_client_celery: Redis = self.app.broker_connection().channel().client # type: ignore
lock_beat: RedisLock = redis_client.lock(
lock_beat: RedisLock = r.lock(
OnyxRedisLocks.CHECK_INDEXING_BEAT_LOCK,
timeout=CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT,
)
@@ -283,7 +271,7 @@ def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
search_settings_instance,
reindex,
db_session,
redis_client,
r,
tenant_id,
)
if attempt_id:
@@ -298,9 +286,7 @@ def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
# Fail any index attempts in the DB that don't have fences
# This shouldn't ever happen!
with get_session_with_tenant(tenant_id) as db_session:
unfenced_attempt_ids = get_unfenced_index_attempt_ids(
db_session, redis_client
)
unfenced_attempt_ids = get_unfenced_index_attempt_ids(db_session, r)
for attempt_id in unfenced_attempt_ids:
lock_beat.reacquire()
@@ -318,22 +304,6 @@ def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
mark_attempt_failed(
attempt.id, db_session, failure_reason=failure_reason
)
# we want to run this less frequently than the overall task
if not redis_client.exists(OnyxRedisSignals.VALIDATE_INDEXING_FENCES):
# clear any indexing fences that don't have associated celery tasks in progress
# tasks can be in the queue in redis, in reserved tasks (prefetched by the worker),
# or be currently executing
try:
task_logger.info("Validating indexing fences...")
validate_indexing_fences(
tenant_id, self.app, redis_client, redis_client_celery, lock_beat
)
except Exception:
task_logger.exception("Exception while validating indexing fences")
redis_client.set(OnyxRedisSignals.VALIDATE_INDEXING_FENCES, 1, ex=60)
except SoftTimeLimitExceeded:
task_logger.info(
"Soft time limit exceeded, task is being terminated gracefully."
@@ -350,190 +320,9 @@ def check_for_indexing(self: Task, *, tenant_id: str | None) -> int | None:
f"tenant={tenant_id}"
)
time_elapsed = time.monotonic() - time_start
task_logger.info(f"check_for_indexing finished: elapsed={time_elapsed:.2f}")
return tasks_created
def validate_indexing_fences(
tenant_id: str | None,
celery_app: Celery,
r: Redis,
r_celery: Redis,
lock_beat: RedisLock,
) -> None:
reserved_indexing_tasks: set[str] = set()
active_indexing_tasks: set[str] = set()
indexing_worker_names: list[str] = []
# filter for and create an indexing specific inspect object
inspect = celery_app.control.inspect()
workers: dict[str, Any] = inspect.ping() # type: ignore
if not workers:
raise ValueError("No workers found!")
for worker_name in list(workers.keys()):
if "indexing" in worker_name:
indexing_worker_names.append(worker_name)
if len(indexing_worker_names) == 0:
raise ValueError("No indexing workers found!")
inspect_indexing = celery_app.control.inspect(destination=indexing_worker_names)
# NOTE: each dict entry is a map of worker name to a list of tasks
# we want sets for reserved task and active task id's to optimize
# subsequent validation lookups
# get the list of reserved tasks
reserved_tasks: dict[str, list] | None = inspect_indexing.reserved() # type: ignore
if reserved_tasks is None:
raise ValueError("inspect_indexing.reserved() returned None!")
for _, task_list in reserved_tasks.items():
for task in task_list:
reserved_indexing_tasks.add(task["id"])
# get the list of active tasks
active_tasks: dict[str, list] | None = inspect_indexing.active() # type: ignore
if active_tasks is None:
raise ValueError("inspect_indexing.active() returned None!")
for _, task_list in active_tasks.items():
for task in task_list:
active_indexing_tasks.add(task["id"])
# validate all existing indexing jobs
for key_bytes in r.scan_iter(RedisConnectorIndex.FENCE_PREFIX + "*"):
lock_beat.reacquire()
with get_session_with_tenant(tenant_id) as db_session:
validate_indexing_fence(
tenant_id,
key_bytes,
reserved_indexing_tasks,
active_indexing_tasks,
r_celery,
db_session,
)
return
def validate_indexing_fence(
tenant_id: str | None,
key_bytes: bytes,
reserved_tasks: set[str],
active_tasks: set[str],
r_celery: Redis,
db_session: Session,
) -> None:
"""Checks for the error condition where an indexing fence is set but the associated celery tasks don't exist.
This can happen if the indexing worker hard crashes or is terminated.
Being in this bad state means the fence will never clear without help, so this function
gives the help.
How this works:
1. Active signal is renewed with a 5 minute TTL
1.1 When the fence is created
1.2. When the task is seen in the redis queue
1.3. When the task is seen in the reserved or active list for a worker
2. The TTL allows us to get through the transitions on fence startup
and when the task starts executing.
More TTL clarification: it is seemingly impossible to exactly query Celery for
whether a task is in the queue or currently executing.
1. An unknown task id is always returned as state PENDING.
2. Redis can be inspected for the task id, but the task id is gone between the time a worker receives the task
and the time it actually starts on the worker.
"""
# if the fence doesn't exist, there's nothing to do
fence_key = key_bytes.decode("utf-8")
composite_id = RedisConnector.get_id_from_fence_key(fence_key)
if composite_id is None:
task_logger.warning(
f"validate_indexing_fence - could not parse composite_id from {fence_key}"
)
return
# parse out metadata and initialize the helper class with it
parts = composite_id.split("/")
if len(parts) != 2:
return
cc_pair_id = int(parts[0])
search_settings_id = int(parts[1])
redis_connector = RedisConnector(tenant_id, cc_pair_id)
redis_connector_index = redis_connector.new_index(search_settings_id)
if not redis_connector_index.fenced:
return
payload = redis_connector_index.payload
if not payload:
return
# OK, there's actually something for us to validate
if payload.celery_task_id is None:
# the fence is just barely set up.
if redis_connector_index.active():
return
# it would be odd to get here as there isn't that much that can go wrong during
# initial fence setup, but it's still worth making sure we can recover
logger.info(
f"validate_indexing_fence - Resetting fence in basic state without any activity: fence={fence_key}"
)
redis_connector_index.reset()
return
found = celery_find_task(
payload.celery_task_id, OnyxCeleryQueues.CONNECTOR_INDEXING, r_celery
)
if found:
# the celery task exists in the redis queue
redis_connector_index.set_active()
return
if payload.celery_task_id in reserved_tasks:
# the celery task was prefetched and is reserved within the indexing worker
redis_connector_index.set_active()
return
if payload.celery_task_id in active_tasks:
# the celery task is active (aka currently executing)
redis_connector_index.set_active()
return
# we may want to enable this check if using the active task list somehow isn't good enough
# if redis_connector_index.generator_locked():
# logger.info(f"{payload.celery_task_id} is currently executing.")
# we didn't find any direct indication that associated celery tasks exist, but they still might be there
# due to gaps in our ability to check states during transitions
# Rely on the active signal (which has a duration that allows us to bridge those gaps)
if redis_connector_index.active():
return
# celery tasks don't exist and the active signal has expired, possibly due to a crash. Clean it up.
logger.warning(
f"validate_indexing_fence - Resetting fence because no associated celery tasks were found: fence={fence_key}"
)
if payload.index_attempt_id:
try:
mark_attempt_failed(
payload.index_attempt_id,
db_session,
"validate_indexing_fence - Canceling index attempt due to missing celery tasks",
)
except Exception:
logger.exception(
"validate_indexing_fence - Exception while marking index attempt as failed."
)
redis_connector_index.reset()
return
def _should_index(
cc_pair: ConnectorCredentialPair,
last_index: IndexAttempt | None,
@@ -680,7 +469,6 @@ def try_creating_indexing_task(
celery_task_id=None,
)
redis_connector_index.set_active()
redis_connector_index.set_fence(payload)
# create the index attempt for tracking purposes
@@ -714,8 +502,6 @@ def try_creating_indexing_task(
raise RuntimeError("send_task for connector_indexing_proxy_task failed.")
# now fill out the fence with the rest of the data
redis_connector_index.set_active()
payload.index_attempt_id = index_attempt_id
payload.celery_task_id = result.id
redis_connector_index.set_fence(payload)
@@ -856,7 +642,7 @@ def connector_indexing_proxy_task(
if job.process:
exit_code = job.process.exitcode
# seeing odd behavior where spawned tasks usually return exit code 1 in the cloud,
# seeing non-deterministic behavior where spawned tasks occasionally return exit code 1
# even though logging clearly indicates that they completed successfully
# to work around this, we ignore the job error state if the completion signal is OK
status_int = redis_connector_index.get_completion()
@@ -1086,7 +872,6 @@ def connector_indexing_task(
f"search_settings={search_settings_id}"
)
# This is where the heavy/real work happens
run_indexing_entrypoint(
index_attempt_id,
tenant_id,

View File

@@ -1,4 +1,3 @@
import time
import traceback
from datetime import datetime
from datetime import timezone
@@ -90,11 +89,10 @@ logger = setup_logger()
def check_for_vespa_sync_task(self: Task, *, tenant_id: str | None) -> None:
"""Runs periodically to check if any document needs syncing.
Generates sets of tasks for Celery if syncing is needed."""
time_start = time.monotonic()
r = get_redis_client(tenant_id=tenant_id)
lock_beat: RedisLock = r.lock(
lock_beat = r.lock(
OnyxRedisLocks.CHECK_VESPA_SYNC_BEAT_LOCK,
timeout=CELERY_VESPA_SYNC_BEAT_LOCK_TIMEOUT,
)
@@ -163,10 +161,6 @@ def check_for_vespa_sync_task(self: Task, *, tenant_id: str | None) -> None:
if lock_beat.owned():
lock_beat.release()
time_elapsed = time.monotonic() - time_start
task_logger.info(f"check_for_vespa_sync_task finished: elapsed={time_elapsed:.2f}")
return
def try_generate_stale_document_sync_tasks(
celery_app: Celery,
@@ -736,7 +730,6 @@ def monitor_vespa_sync(self: Task, tenant_id: str | None) -> bool:
Returns True if the task actually did work, False if it exited early to prevent overlap
"""
time_start = time.monotonic()
r = get_redis_client(tenant_id=tenant_id)
lock_beat: RedisLock = r.lock(
@@ -831,8 +824,6 @@ def monitor_vespa_sync(self: Task, tenant_id: str | None) -> bool:
if lock_beat.owned():
lock_beat.release()
time_elapsed = time.monotonic() - time_start
task_logger.info(f"monitor_vespa_sync finished: elapsed={time_elapsed:.2f}")
return True

View File

@@ -1,7 +1,6 @@
import json
import os
import urllib.parse
from typing import cast
from onyx.configs.constants import AuthType
from onyx.configs.constants import DocumentIndexType
@@ -488,21 +487,6 @@ SYSTEM_RECURSION_LIMIT = int(os.environ.get("SYSTEM_RECURSION_LIMIT") or "1000")
PARSE_WITH_TRAFILATURA = os.environ.get("PARSE_WITH_TRAFILATURA", "").lower() == "true"
# allow for custom error messages for different errors returned by litellm
# for example, can specify: {"Violated content safety policy": "EVIL REQUEST!!!"}
# to make it so that if an LLM call returns an error containing "Violated content safety policy"
# the end user will see "EVIL REQUEST!!!" instead of the default error message.
_LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS = os.environ.get(
"LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS", ""
)
LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS: dict[str, str] | None = None
try:
LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS = cast(
dict[str, str], json.loads(_LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS)
)
except json.JSONDecodeError:
pass
#####
# Enterprise Edition Configs
#####

View File

@@ -275,10 +275,6 @@ class OnyxRedisLocks:
SLACK_BOT_HEARTBEAT_PREFIX = "da_heartbeat:slack_bot"
class OnyxRedisSignals:
VALIDATE_INDEXING_FENCES = "signal:validate_indexing_fences"
class OnyxCeleryPriority(int, Enum):
HIGHEST = 0
HIGH = auto()

View File

@@ -4,7 +4,6 @@ from datetime import timezone
from googleapiclient.discovery import build # type: ignore
from googleapiclient.errors import HttpError # type: ignore
from markitdown import MarkItDown # type: ignore
from onyx.configs.app_configs import CONTINUE_ON_CONNECTOR_FAILURE
from onyx.configs.constants import DocumentSource
@@ -27,9 +26,9 @@ from onyx.file_processing.unstructured import get_unstructured_api_key
from onyx.file_processing.unstructured import unstructured_to_text
from onyx.utils.logger import setup_logger
logger = setup_logger()
# these errors don't represent a failure in the connector, but simply files
# that can't / shouldn't be indexed
ERRORS_TO_CONTINUE_ON = [
@@ -39,41 +38,177 @@ ERRORS_TO_CONTINUE_ON = [
]
def _extract_sections_basic(
file: dict[str, str], service: GoogleDriveService
) -> list[Section]:
mime_type = file["mimeType"]
link = file["webViewLink"]
if mime_type not in set(item.value for item in GDriveMimeType):
# Unsupported file types can still have a title, finding this way is still useful
return [Section(link=link, text=UNSUPPORTED_FILE_TYPE_CONTENT)]
try:
if mime_type == GDriveMimeType.SPREADSHEET.value:
try:
sheets_service = build(
"sheets", "v4", credentials=service._http.credentials
)
spreadsheet = (
sheets_service.spreadsheets()
.get(spreadsheetId=file["id"])
.execute()
)
sections = []
for sheet in spreadsheet["sheets"]:
sheet_name = sheet["properties"]["title"]
sheet_id = sheet["properties"]["sheetId"]
# Get sheet dimensions
grid_properties = sheet["properties"].get("gridProperties", {})
row_count = grid_properties.get("rowCount", 1000)
column_count = grid_properties.get("columnCount", 26)
# Convert column count to letter (e.g., 26 -> Z, 27 -> AA)
end_column = ""
while column_count:
column_count, remainder = divmod(column_count - 1, 26)
end_column = chr(65 + remainder) + end_column
range_name = f"'{sheet_name}'!A1:{end_column}{row_count}"
try:
result = (
sheets_service.spreadsheets()
.values()
.get(spreadsheetId=file["id"], range=range_name)
.execute()
)
values = result.get("values", [])
if values:
text = f"Sheet: {sheet_name}\n"
for row in values:
text += "\t".join(str(cell) for cell in row) + "\n"
sections.append(
Section(
link=f"{link}#gid={sheet_id}",
text=text,
)
)
except HttpError as e:
logger.warning(
f"Error fetching data for sheet '{sheet_name}': {e}"
)
continue
return sections
except Exception as e:
logger.warning(
f"Ran into exception '{e}' when pulling data from Google Sheet '{file['name']}'."
" Falling back to basic extraction."
)
if mime_type in [
GDriveMimeType.DOC.value,
GDriveMimeType.PPT.value,
GDriveMimeType.SPREADSHEET.value,
]:
export_mime_type = (
"text/plain"
if mime_type != GDriveMimeType.SPREADSHEET.value
else "text/csv"
)
text = (
service.files()
.export(fileId=file["id"], mimeType=export_mime_type)
.execute()
.decode("utf-8")
)
return [Section(link=link, text=text)]
elif mime_type in [
GDriveMimeType.PLAIN_TEXT.value,
GDriveMimeType.MARKDOWN.value,
]:
return [
Section(
link=link,
text=service.files()
.get_media(fileId=file["id"])
.execute()
.decode("utf-8"),
)
]
if mime_type in [
GDriveMimeType.WORD_DOC.value,
GDriveMimeType.POWERPOINT.value,
GDriveMimeType.PDF.value,
]:
response = service.files().get_media(fileId=file["id"]).execute()
if get_unstructured_api_key():
return [
Section(
link=link,
text=unstructured_to_text(
file=io.BytesIO(response),
file_name=file.get("name", file["id"]),
),
)
]
if mime_type == GDriveMimeType.WORD_DOC.value:
return [
Section(link=link, text=docx_to_text(file=io.BytesIO(response)))
]
elif mime_type == GDriveMimeType.PDF.value:
text, _ = read_pdf_file(file=io.BytesIO(response))
return [Section(link=link, text=text)]
elif mime_type == GDriveMimeType.POWERPOINT.value:
return [
Section(link=link, text=pptx_to_text(file=io.BytesIO(response)))
]
return [Section(link=link, text=UNSUPPORTED_FILE_TYPE_CONTENT)]
except Exception:
return [Section(link=link, text=UNSUPPORTED_FILE_TYPE_CONTENT)]
def convert_drive_item_to_document(
file: GoogleDriveFileType,
drive_service: GoogleDriveService,
docs_service: GoogleDocsService,
) -> Document | None:
"""
Converts a Google Drive file into an internal Document object, extracting
the text and organizing it into sections. Uses specialized methods for Google Docs
to preserve structure. Falls back to basic extraction for all other formats.
"""
try:
# Skip shortcuts and folders
# Skip files that are shortcuts
if file.get("mimeType") == DRIVE_SHORTCUT_TYPE:
logger.info("Ignoring Drive Shortcut Filetype")
return None
# Skip files that are folders
if file.get("mimeType") == DRIVE_FOLDER_TYPE:
logger.info("Ignoring Drive Folder Filetype")
return None
sections: list[Section] = []
# Special handling for Google Docs to preserve structure
# Special handling for Google Docs to preserve structure, link
# to headers
if file.get("mimeType") == GDriveMimeType.DOC.value:
try:
sections = get_document_sections(docs_service, file["id"])
except Exception as e:
logger.warning(
f"Exception '{e}' when pulling sections from Google Doc '{file['name']}'. "
"Falling back to basic extraction."
f"Ran into exception '{e}' when pulling sections from Google Doc '{file['name']}'."
" Falling back to basic extraction."
)
# If not a GDoc or GDoc extraction failed
# NOTE: this will run for either (1) the above failed or (2) the file is not a Google Doc
if not sections:
try:
# For all other file types just extract the text
sections = _extract_sections_basic(file, drive_service)
except HttpError as e:
reason = e.error_details[0]["reason"] if e.error_details else e.reason
message = e.error_details[0]["message"] if e.error_details else e.reason
@@ -82,8 +217,8 @@ def convert_drive_item_to_document(
f"Could not export file '{file['name']}' due to '{message}', skipping..."
)
return None
raise
raise
if not sections:
return None
@@ -103,248 +238,9 @@ def convert_drive_item_to_document(
except Exception as e:
if not CONTINUE_ON_CONNECTOR_FAILURE:
raise e
logger.exception("Ran into exception when pulling a file from Google Drive")
return None
def _extract_sections_basic(
file: GoogleDriveFileType, service: GoogleDriveService
) -> list[Section]:
"""
Extracts text from a Google Drive file based on its MIME type.
"""
mime_type = file["mimeType"]
link = file["webViewLink"]
# Handle unsupported MIME types
if mime_type not in {item.value for item in GDriveMimeType}:
logger.debug(
f"Unsupported MIME type '{mime_type}' for file '{file.get('name')}'"
)
return [Section(link=link, text=UNSUPPORTED_FILE_TYPE_CONTENT)]
# Specialized handling for Google Sheets
if mime_type == GDriveMimeType.SPREADSHEET.value:
try:
return _extract_google_sheets(file, service)
except Exception as e:
logger.warning(
f"Error extracting data from Google Sheet '{file['name']}': {e}. "
"Falling back to basic content extraction."
)
# For other types
return _extract_general_content(file, service)
def _extract_google_sheets(
file: dict[str, str], service: GoogleDriveService
) -> list[Section]:
"""
Specialized extraction logic for Google Sheets.
Iterates through each sheet, fetches all data, and returns a list of Section objects.
"""
link = file["webViewLink"]
file_id = file["id"]
sheets_service = build("sheets", "v4", credentials=service._http.credentials)
spreadsheet = sheets_service.spreadsheets().get(spreadsheetId=file_id).execute()
sections: list[Section] = []
for sheet in spreadsheet.get("sheets", []):
sheet_name = sheet["properties"]["title"]
sheet_id = sheet["properties"]["sheetId"]
grid_props = sheet["properties"].get("gridProperties", {})
row_count = grid_props.get("rowCount", 1000)
column_count = grid_props.get("columnCount", 26)
# Convert a number to a spreadsheet column letter (1->A, 26->Z, 27->AA,...)
end_column = ""
col_count = column_count
while col_count > 0:
col_count, remainder = divmod(col_count - 1, 26)
end_column = chr(65 + remainder) + end_column
range_name = f"'{sheet_name}'!A1:{end_column}{row_count}"
try:
result = (
sheets_service.spreadsheets()
.values()
.get(spreadsheetId=file_id, range=range_name)
.execute()
)
values = result.get("values", [])
if values:
text = f"Sheet: {sheet_name}\n"
for row in values:
text += "\t".join(str(cell) for cell in row) + "\n"
sections.append(Section(link=f"{link}#gid={sheet_id}", text=text))
except HttpError as e:
logger.warning(
f"Error fetching data for sheet '{sheet_name}' in '{file.get('name')}' : {e}"
)
continue
return sections
def _extract_general_content(
file: dict[str, str], service: GoogleDriveService
) -> list[Section]:
"""
Extracts general file content for files other than Google Sheets.
- PDF: Revert to read_pdf_file
- DOCX: Unstructured, then docx_to_text, then MarkItDown.
- PPTX: Unstructured, then pptx_to_text, then MarkItDown.
- TXT: Decode the content; if empty, log.
- Google Docs/Slides: Export as text/plain and return directly.
"""
link = file["webViewLink"]
mime_type = file["mimeType"]
file_id = file["id"]
file_name = file.get("name", file_id)
try:
# Google Docs and Google Slides (internal GDrive formats)
if (
mime_type == GDriveMimeType.DOC.value
or mime_type == GDriveMimeType.PPT.value
):
logger.debug(f"Extracting Google-native doc/presentation: {file_name}")
export_mime_type = "text/plain"
content = (
service.files()
.export(fileId=file_id, mimeType=export_mime_type)
.execute()
)
text = content.decode("utf-8", errors="replace").strip()
if not text:
logger.warning(
f"No text extracted from Google Docs/Slides file '{file_name}'."
)
text = UNSUPPORTED_FILE_TYPE_CONTENT
return [Section(link=link, text=text)]
# For all other formats, get raw content
content = service.files().get_media(fileId=file_id).execute()
if mime_type == GDriveMimeType.PDF.value:
# Revert to original PDF extraction
logger.debug(f"Extracting PDF content for '{file_name}'")
text, _ = read_pdf_file(file=io.BytesIO(content))
if not text:
logger.warning(
f"No text extracted from PDF '{file_name}' with read_pdf_file."
)
text = UNSUPPORTED_FILE_TYPE_CONTENT
return [Section(link=link, text=text)]
if mime_type == GDriveMimeType.WORD_DOC.value:
logger.debug(f"Extracting DOCX content for '{file_name}'")
return [
Section(link=link, text=_extract_docx_pptx_txt(content, file, "docx"))
]
if mime_type == GDriveMimeType.POWERPOINT.value:
logger.debug(f"Extracting PPTX content for '{file_name}'")
return [
Section(link=link, text=_extract_docx_pptx_txt(content, file, "pptx"))
]
if (
mime_type == GDriveMimeType.PLAIN_TEXT.value
or mime_type == GDriveMimeType.MARKDOWN.value
):
logger.debug(f"Extracting plain text/markdown content for '{file_name}'")
text = content.decode("utf-8", errors="replace").strip()
if not text:
logger.warning(
f"No text extracted from TXT/MD '{file_name}'. Returning unsupported message."
)
text = UNSUPPORTED_FILE_TYPE_CONTENT
return [Section(link=link, text=text)]
# If we reach here, it's some other format supported by MarkItDown/unstructured
logger.debug(f"Trying MarkItDown/unstructured fallback for '{file_name}'")
text = _extract_docx_pptx_txt(content, file, None) # generic fallback
return [Section(link=link, text=text)]
except Exception as e:
logger.error(
f"Error extracting file content for '{file_name}': {e}", exc_info=True
)
return [Section(link=link, text=UNSUPPORTED_FILE_TYPE_CONTENT)]
def _extract_docx_pptx_txt(
content: bytes, file: dict[str, str], file_type: str | None
) -> str:
"""
Attempts to extract text from DOCX, PPTX, or any supported format using:
1. unstructured (if configured)
2. docx_to_text/pptx_to_text if known format
3. MarkItDown fallback
"""
file_name = file.get("name", file["id"])
# 1. Try unstructured first
if get_unstructured_api_key():
try:
logger.debug(f"Attempting unstructured extraction for '{file_name}'...")
text = unstructured_to_text(io.BytesIO(content), file_name)
if text.strip():
return text
else:
logger.warning(f"Unstructured returned empty text for '{file_name}'.")
except Exception as e:
logger.warning(f"Unstructured extraction failed for '{file_name}': {e}")
# 2. If format is docx or pptx, try direct extraction methods
if file_type == "docx":
try:
logger.debug(f"Trying docx_to_text for '{file_name}'...")
text = docx_to_text(file=io.BytesIO(content))
if text.strip():
return text
else:
logger.warning(f"docx_to_text returned empty for '{file_name}'.")
except Exception as e:
logger.warning(f"docx_to_text failed for '{file_name}': {e}")
if file_type == "pptx":
try:
logger.debug(f"Trying pptx_to_text for '{file_name}'...")
text = pptx_to_text(file=io.BytesIO(content))
if text.strip():
return text
else:
logger.warning(f"pptx_to_text returned empty for '{file_name}'.")
except Exception as e:
logger.warning(f"pptx_to_text failed for '{file_name}': {e}")
# 3. Fallback to MarkItDown
try:
logger.debug(f"Falling back to MarkItDown for '{file_name}'...")
md = MarkItDown()
result = md.convert(io.BytesIO(content))
if result and result.text_content and result.text_content.strip():
return result.text_content
else:
logger.warning(f"MarkItDown returned empty text for '{file_name}'.")
except Exception as e:
logger.error(
f"MarkItDown conversion failed for '{file_name}': {e}", exc_info=True
)
# If all methods fail or return empty, return unsupported message
logger.error(
f"All extraction methods failed for '{file_name}', returning unsupported file message."
)
return UNSUPPORTED_FILE_TYPE_CONTENT
return None
def build_slim_document(file: GoogleDriveFileType) -> SlimDocument | None:

View File

@@ -316,23 +316,6 @@ def update_chat_session(
return chat_session
def delete_all_chat_sessions_for_user(
user: User | None, db_session: Session, hard_delete: bool = HARD_DELETE_CHATS
) -> None:
user_id = user.id if user is not None else None
query = db_session.query(ChatSession).filter(
ChatSession.user_id == user_id, ChatSession.onyxbot_flow.is_(False)
)
if hard_delete:
query.delete(synchronize_session=False)
else:
query.update({ChatSession.deleted: True}, synchronize_session=False)
db_session.commit()
def delete_chat_session(
user_id: UUID | None,
chat_session_id: UUID,

View File

@@ -1010,7 +1010,7 @@ class ChatSession(Base):
"ChatFolder", back_populates="chat_sessions"
)
messages: Mapped[list["ChatMessage"]] = relationship(
"ChatMessage", back_populates="chat_session", cascade="all, delete-orphan"
"ChatMessage", back_populates="chat_session"
)
persona: Mapped["Persona"] = relationship("Persona")
@@ -1078,8 +1078,6 @@ class ChatMessage(Base):
"SearchDoc",
secondary=ChatMessage__SearchDoc.__table__,
back_populates="chat_messages",
cascade="all, delete-orphan",
single_parent=True,
)
tool_call: Mapped["ToolCall"] = relationship(

View File

@@ -543,10 +543,6 @@ def upsert_persona(
if tools is not None:
existing_persona.tools = tools or []
# We should only update display priority if it is not already set
if existing_persona.display_priority is None:
existing_persona.display_priority = display_priority
persona = existing_persona
else:

View File

@@ -14,9 +14,10 @@ from typing import IO
import chardet
import docx # type: ignore
import openpyxl # type: ignore
import pptx # type: ignore
from docx import Document
from fastapi import UploadFile
from markitdown import MarkItDown # type: ignore
from pypdf import PdfReader
from pypdf.errors import PdfStreamError
@@ -59,9 +60,6 @@ VALID_FILE_EXTENSIONS = PLAIN_TEXT_FILE_EXTENSIONS + [
".html",
]
# These are the file extensions that we use markitdown for
MARKITDOWN_FILE_EXTENSIONS = [".docx", ".pptx", ".xlsx"]
def is_text_file_extension(file_name: str) -> bool:
return any(file_name.endswith(ext) for ext in PLAIN_TEXT_FILE_EXTENSIONS)
@@ -76,10 +74,6 @@ def is_valid_file_ext(ext: str) -> bool:
return ext in VALID_FILE_EXTENSIONS
def is_markitdown_file_ext(ext: str) -> bool:
return ext in MARKITDOWN_FILE_EXTENSIONS
def is_text_file(file: IO[bytes]) -> bool:
"""
checks if the first 1024 bytes only contain printable or whitespace characters
@@ -191,6 +185,13 @@ def read_text_file(
return file_content_raw, metadata
def pdf_to_text(file: IO[Any], pdf_pass: str | None = None) -> str:
"""Extract text from a PDF file."""
# Return only the extracted text from read_pdf_file
text, _ = read_pdf_file(file, pdf_pass)
return text
def read_pdf_file(
file: IO[Any],
pdf_pass: str | None = None,
@@ -298,11 +299,16 @@ def pptx_to_text(file: IO[Any]) -> str:
return TEXT_SECTION_SEPARATOR.join(text_content)
def pdf_to_text(file: IO[Any], pdf_pass: str | None = None) -> str:
"""Extract text from a PDF file."""
# Return only the extracted text from read_pdf_file
text, _ = read_pdf_file(file, pdf_pass)
return text
def xlsx_to_text(file: IO[Any]) -> str:
workbook = openpyxl.load_workbook(file, read_only=True)
text_content = []
for sheet in workbook.worksheets:
sheet_string = "\n".join(
",".join(map(str, row))
for row in sheet.iter_rows(min_row=1, values_only=True)
)
text_content.append(sheet_string)
return TEXT_SECTION_SEPARATOR.join(text_content)
def eml_to_text(file: IO[Any]) -> str:
@@ -340,6 +346,9 @@ def extract_file_text(
) -> str:
extension_to_function: dict[str, Callable[[IO[Any]], str]] = {
".pdf": pdf_to_text,
".docx": docx_to_text,
".pptx": pptx_to_text,
".xlsx": xlsx_to_text,
".eml": eml_to_text,
".epub": epub_to_text,
".html": parse_html_page_basic,
@@ -349,8 +358,6 @@ def extract_file_text(
if get_unstructured_api_key():
return unstructured_to_text(file, file_name)
md = MarkItDown()
if file_name or extension:
if extension is not None:
final_extension = extension
@@ -358,12 +365,6 @@ def extract_file_text(
final_extension = get_file_ext(file_name)
if is_valid_file_ext(final_extension):
if is_markitdown_file_ext(final_extension):
with BytesIO(file.read()) as file_like_object:
result = md.convert_stream(
file_like_object, file_extension=final_extension
)
return result.text_content
return extension_to_function.get(final_extension, file_io_to_text)(file)
# Either the file somehow has no name or the extension is not one that we recognize
@@ -381,37 +382,29 @@ def extract_file_text(
return ""
def convert_docx_to_markdown(
def convert_docx_to_txt(
file: UploadFile, file_store: FileStore, file_path: str
) -> None:
try:
# Read the file content
file_content = file.file.read()
file.file.seek(0)
docx_content = file.file.read()
doc = Document(BytesIO(docx_content))
if not file_content:
raise ValueError(f"File {file.filename} is empty")
# Extract text from the document
full_text = []
for para in doc.paragraphs:
full_text.append(para.text)
# Reset the file pointer to the beginning
file.file.seek(0)
# Join the extracted text
text_content = "\n".join(full_text)
text_content = extract_file_text(
file=file.file, file_name=file.filename or "", extension=".docx"
)
if not text_content:
raise ValueError(f"Failed to extract text from {file.filename}")
txt_file_path = docx_to_txt_filename(file_path)
file_store.save_file(
file_name=txt_file_path,
content=BytesIO(text_content.encode("utf-8")),
display_name=file.filename,
file_origin=FileOrigin.CONNECTOR,
file_type="text/plain",
)
except Exception as e:
logger.error(f"Error converting DOCX to Markdown: {str(e)}")
raise RuntimeError(f"Failed to process file {file.filename}: {str(e)}") from e
txt_file_path = docx_to_txt_filename(file_path)
file_store.save_file(
file_name=txt_file_path,
content=BytesIO(text_content.encode("utf-8")),
display_name=file.filename,
file_origin=FileOrigin.CONNECTOR,
file_type="text/plain",
)
def docx_to_txt_filename(file_path: str) -> str:

View File

@@ -28,7 +28,6 @@ from litellm.exceptions import RateLimitError # type: ignore
from litellm.exceptions import Timeout # type: ignore
from litellm.exceptions import UnprocessableEntityError # type: ignore
from onyx.configs.app_configs import LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS
from onyx.configs.constants import MessageType
from onyx.configs.model_configs import GEN_AI_MAX_TOKENS
from onyx.configs.model_configs import GEN_AI_MODEL_FALLBACK_MAX_TOKENS
@@ -46,19 +45,10 @@ logger = setup_logger()
def litellm_exception_to_error_msg(
e: Exception,
llm: LLM,
fallback_to_error_msg: bool = False,
custom_error_msg_mappings: dict[str, str]
| None = LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS,
e: Exception, llm: LLM, fallback_to_error_msg: bool = False
) -> str:
error_msg = str(e)
if custom_error_msg_mappings:
for error_msg_pattern, custom_error_msg in custom_error_msg_mappings.items():
if error_msg_pattern in error_msg:
return custom_error_msg
if isinstance(e, BadRequestError):
error_msg = "Bad request: The server couldn't process your request. Please check your input."
elif isinstance(e, AuthenticationError):

View File

@@ -31,10 +31,6 @@ class RedisConnectorIndex:
TERMINATE_PREFIX = PREFIX + "_terminate" # connectorindexing_terminate
# used to signal the overall workflow is still active
# it's difficult to prevent
ACTIVE_PREFIX = PREFIX + "_active"
def __init__(
self,
tenant_id: str | None,
@@ -58,7 +54,6 @@ class RedisConnectorIndex:
f"{self.GENERATOR_LOCK_PREFIX}_{id}/{search_settings_id}"
)
self.terminate_key = f"{self.TERMINATE_PREFIX}_{id}/{search_settings_id}"
self.active_key = f"{self.ACTIVE_PREFIX}_{id}/{search_settings_id}"
@classmethod
def fence_key_with_ids(cls, cc_pair_id: int, search_settings_id: int) -> str:
@@ -112,26 +107,6 @@ class RedisConnectorIndex:
# 10 minute TTL is good.
self.redis.set(f"{self.terminate_key}_{celery_task_id}", 0, ex=600)
def set_active(self) -> None:
"""This sets a signal to keep the indexing flow from getting cleaned up within
the expiration time.
The slack in timing is needed to avoid race conditions where simply checking
the celery queue and task status could result in race conditions."""
self.redis.set(self.active_key, 0, ex=300)
def active(self) -> bool:
if self.redis.exists(self.active_key):
return True
return False
def generator_locked(self) -> bool:
if self.redis.exists(self.generator_lock_key):
return True
return False
def set_generator_complete(self, payload: int | None) -> None:
if not payload:
self.redis.delete(self.generator_complete_key)
@@ -163,7 +138,6 @@ class RedisConnectorIndex:
return status
def reset(self) -> None:
self.redis.delete(self.active_key)
self.redis.delete(self.generator_lock_key)
self.redis.delete(self.generator_progress_key)
self.redis.delete(self.generator_complete_key)

View File

@@ -48,7 +48,6 @@ def load_personas_from_yaml(
data = yaml.safe_load(file)
all_personas = data.get("personas", [])
for persona in all_personas:
doc_set_names = persona["document_sets"]
doc_sets: list[DocumentSetDBModel] = [
@@ -128,7 +127,6 @@ def load_personas_from_yaml(
display_priority=(
existing_persona.display_priority
if existing_persona is not None
and persona.get("display_priority") is None
else persona.get("display_priority")
),
is_visible=(

View File

@@ -87,7 +87,7 @@ from onyx.db.models import SearchSettings
from onyx.db.models import User
from onyx.db.search_settings import get_current_search_settings
from onyx.db.search_settings import get_secondary_search_settings
from onyx.file_processing.extract_file_text import convert_docx_to_markdown
from onyx.file_processing.extract_file_text import convert_docx_to_txt
from onyx.file_store.file_store import get_default_file_store
from onyx.key_value_store.interface import KvKeyNotFoundError
from onyx.redis.redis_connector import RedisConnector
@@ -396,12 +396,11 @@ def upload_files(
file_origin=FileOrigin.CONNECTOR,
file_type=file.content_type or "text/plain",
)
file.file.seek(0)
if file.content_type and file.content_type.startswith(
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"
):
convert_docx_to_markdown(file, file_store, file_path)
convert_docx_to_txt(file, file_store, file_path)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))

View File

@@ -35,7 +35,6 @@ from onyx.configs.model_configs import LITELLM_PASS_THROUGH_HEADERS
from onyx.db.chat import add_chats_to_session_from_slack_thread
from onyx.db.chat import create_chat_session
from onyx.db.chat import create_new_chat_message
from onyx.db.chat import delete_all_chat_sessions_for_user
from onyx.db.chat import delete_chat_session
from onyx.db.chat import duplicate_chat_session_for_user_from_slack
from onyx.db.chat import get_chat_message
@@ -281,17 +280,6 @@ def patch_chat_session(
return None
@router.delete("/delete-all-chat-sessions")
def delete_all_chat_sessions(
user: User | None = Depends(current_user),
db_session: Session = Depends(get_session),
) -> None:
try:
delete_all_chat_sessions_for_user(user=user, db_session=db_session)
except ValueError as e:
raise HTTPException(status_code=400, detail=str(e))
@router.delete("/delete-chat-session/{session_id}")
def delete_chat_session_by_id(
session_id: UUID,

View File

@@ -11,7 +11,6 @@ from onyx.chat.models import RetrievalDocs
from onyx.configs.constants import DocumentSource
from onyx.configs.constants import MessageType
from onyx.configs.constants import SearchFeedbackType
from onyx.configs.constants import SessionType
from onyx.context.search.models import BaseFilters
from onyx.context.search.models import ChunkContext
from onyx.context.search.models import RerankingDetails
@@ -152,10 +151,6 @@ class ChatSessionUpdateRequest(BaseModel):
sharing_status: ChatSessionSharedStatus
class DeleteAllSessionsRequest(BaseModel):
session_type: SessionType
class RenameChatSessionResponse(BaseModel):
new_name: str # This is only really useful if the name is generated

View File

@@ -81,5 +81,4 @@ stripe==10.12.0
urllib3==2.2.3
mistune==0.8.4
sentry-sdk==2.14.0
prometheus_client==0.21.0
markitdown==0.0.1a3
prometheus_client==0.21.0

View File

@@ -92,7 +92,6 @@ services:
- LOG_POSTGRES_LATENCY=${LOG_POSTGRES_LATENCY:-}
- LOG_POSTGRES_CONN_COUNTS=${LOG_POSTGRES_CONN_COUNTS:-}
- CELERY_BROKER_POOL_LIMIT=${CELERY_BROKER_POOL_LIMIT:-}
- LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS=${LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS:-}
# Analytics Configs
- SENTRY_DSN=${SENTRY_DSN:-}

View File

@@ -84,7 +84,6 @@ services:
# (time spent on finding the right docs + time spent fetching summaries from disk)
- LOG_VESPA_TIMING_INFORMATION=${LOG_VESPA_TIMING_INFORMATION:-}
- CELERY_BROKER_POOL_LIMIT=${CELERY_BROKER_POOL_LIMIT:-}
- LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS=${LITELLM_CUSTOM_ERROR_MESSAGE_MAPPINGS:-}
# Chat Configs
- HARD_DELETE_CHATS=${HARD_DELETE_CHATS:-}

View File

@@ -2,7 +2,7 @@ import { useFormContext } from "@/components/context/FormContext";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { SettingsIcon } from "@/components/icons/icons";
import { Logo } from "@/components/logo/Logo";
import { Logo } from "@/components/Logo";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { credentialTemplates } from "@/lib/connectors/credentials";
import Link from "next/link";

View File

@@ -6,7 +6,7 @@ import { useCallback, useEffect, useState } from "react";
import Text from "@/components/ui/text";
import { RequestNewVerificationEmail } from "../waiting-on-verification/RequestNewVerificationEmail";
import { User } from "@/lib/types";
import { Logo } from "@/components/logo/Logo";
import { Logo } from "@/components/Logo";
export function Verify({ user }: { user: User | null }) {
const searchParams = useSearchParams();

View File

@@ -8,7 +8,7 @@ import { HealthCheckBanner } from "@/components/health/healthcheck";
import { User } from "@/lib/types";
import Text from "@/components/ui/text";
import { RequestNewVerificationEmail } from "./RequestNewVerificationEmail";
import { Logo } from "@/components/logo/Logo";
import { Logo } from "@/components/Logo";
export default async function Page() {
// catch cases where the backend is completely unreachable here

View File

@@ -27,7 +27,6 @@ import {
buildLatestMessageChain,
checkAnyAssistantHasSearch,
createChatSession,
deleteAllChatSessions,
deleteChatSession,
getCitedDocumentsFromMessage,
getHumanAndAIMessageFromMessageNumber,
@@ -1838,7 +1837,6 @@ export function ChatPage({
const innerSidebarElementRef = useRef<HTMLDivElement>(null);
const [settingsToggled, setSettingsToggled] = useState(false);
const [showDeleteAllModal, setShowDeleteAllModal] = useState(false);
const currentPersona = alternativeAssistant || liveAssistant;
useEffect(() => {
@@ -1905,6 +1903,11 @@ export function ChatPage({
const showShareModal = (chatSession: ChatSession) => {
setSharedChatSession(chatSession);
};
const [documentSelection, setDocumentSelection] = useState(false);
// const toggleDocumentSelectionAspects = () => {
// setDocumentSelection((documentSelection) => !documentSelection);
// setShowDocSidebar(false);
// };
const toggleDocumentSidebar = () => {
if (!documentSidebarToggled) {
@@ -1969,32 +1972,6 @@ export function ChatPage({
<ChatPopup />
{showDeleteAllModal && (
<DeleteEntityModal
entityType="All Chats"
entityName="all your chat sessions"
onClose={() => setShowDeleteAllModal(false)}
additionalDetails="This action cannot be undone. All your chat sessions will be deleted."
onSubmit={async () => {
const response = await deleteAllChatSessions("Chat");
if (response.ok) {
setShowDeleteAllModal(false);
setPopup({
message: "All your chat sessions have been deleted.",
type: "success",
});
refreshChatSessions();
router.push("/chat");
} else {
setPopup({
message: "Failed to delete all chat sessions.",
type: "error",
});
}
}}
/>
)}
{currentFeedback && (
<FeedbackModal
feedbackType={currentFeedback[0]}
@@ -2146,7 +2123,7 @@ export function ChatPage({
page="chat"
ref={innerSidebarElementRef}
toggleSidebar={toggleSidebar}
toggled={toggledSidebar}
toggled={toggledSidebar && !settings?.isMobile}
backgroundToggled={toggledSidebar || showHistorySidebar}
existingChats={chatSessions}
currentChatSession={selectedChatSession}
@@ -2155,7 +2132,6 @@ export function ChatPage({
removeToggle={removeToggle}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
showDeleteAllModal={() => setShowDeleteAllModal(true)}
/>
</div>
</div>
@@ -2168,6 +2144,7 @@ export function ChatPage({
fixed
right-0
z-[1000]
bg-background
h-screen
transition-all
@@ -2217,6 +2194,8 @@ export function ChatPage({
{liveAssistant && (
<FunctionalHeader
toggleUserSettings={() => setUserSettingsToggled(true)}
liveAssistant={liveAssistant}
onAssistantChange={onAssistantChange}
sidebarToggled={toggledSidebar}
reset={() => setMessage("")}
page="chat"
@@ -2228,6 +2207,7 @@ export function ChatPage({
toggleSidebar={toggleSidebar}
currentChatSession={selectedChatSession}
documentSidebarToggled={documentSidebarToggled}
llmOverrideManager={llmOverrideManager}
/>
)}
@@ -2762,10 +2742,6 @@ export function ChatPage({
removeDocs={() => {
clearSelectedDocuments();
}}
showDocs={() => {
setFiltersToggled(false);
setDocumentSidebarToggled(true);
}}
removeFilters={() => {
filterManager.setSelectedSources([]);
filterManager.setSelectedTags([]);
@@ -2778,6 +2754,7 @@ export function ChatPage({
chatState={currentSessionChatState}
stopGenerating={stopGenerating}
openModelSettings={() => setSettingsToggled(true)}
showDocs={() => setDocumentSelection(true)}
selectedDocuments={selectedDocuments}
// assistant stuff
selectedAssistant={liveAssistant}

View File

@@ -81,8 +81,6 @@ export function ChatDocumentDisplay({
}
};
const hasMetadata =
document.updated_at || Object.keys(document.metadata).length > 0;
return (
<div className={`opacity-100 ${modal ? "w-[90vw]" : "w-full"}`}>
<div
@@ -109,14 +107,8 @@ export function ChatDocumentDisplay({
: document.semantic_identifier || document.document_id}
</div>
</div>
{hasMetadata && (
<DocumentMetadataBlock modal={modal} document={document} />
)}
<div
className={`line-clamp-3 text-sm font-normal leading-snug text-gray-600 ${
hasMetadata ? "mt-2" : ""
}`}
>
<DocumentMetadataBlock modal={modal} document={document} />
<div className="line-clamp-3 pt-2 text-sm font-normal leading-snug text-gray-600">
{buildDocumentSummaryDisplay(
document.match_highlights,
document.blurb

View File

@@ -31,7 +31,14 @@ import { SettingsContext } from "@/components/settings/SettingsProvider";
import { ChatState } from "../types";
import UnconfiguredProviderText from "@/components/chat_search/UnconfiguredProviderText";
import { useAssistants } from "@/components/context/AssistantsContext";
import AnimatedToggle from "@/components/search/SearchBar";
import { Popup } from "@/components/admin/connectors/Popup";
import { AssistantsTab } from "../modal/configuration/AssistantsTab";
import { IconType } from "react-icons";
import { LlmTab } from "../modal/configuration/LlmTab";
import { XIcon } from "lucide-react";
import { FilterPills } from "./FilterPills";
import { Tag } from "@/lib/types";
import FiltersDisplay from "./FilterDisplay";
const MAX_INPUT_HEIGHT = 200;
@@ -40,6 +47,7 @@ interface ChatInputBarProps {
removeFilters: () => void;
removeDocs: () => void;
openModelSettings: () => void;
showDocs: () => void;
showConfigureAPIKey: () => void;
selectedDocuments: OnyxDocument[];
message: string;
@@ -49,7 +57,6 @@ interface ChatInputBarProps {
filterManager: FilterManager;
llmOverrideManager: LlmOverrideManager;
chatState: ChatState;
showDocs: () => void;
alternativeAssistant: Persona | null;
// assistants
selectedAssistant: Persona;
@@ -68,8 +75,8 @@ export function ChatInputBar({
removeFilters,
removeDocs,
openModelSettings,
showConfigureAPIKey,
showDocs,
showConfigureAPIKey,
selectedDocuments,
message,
setMessage,
@@ -277,6 +284,10 @@ export function ChatInputBar({
</div>
)}
{/* <div>
<SelectedFilterDisplay filterManager={filterManager} />
</div> */}
<UnconfiguredProviderText showConfigureAPIKey={showConfigureAPIKey} />
<div
@@ -417,7 +428,9 @@ export function ChatInputBar({
style={{ scrollbarWidth: "thin" }}
role="textarea"
aria-multiline
placeholder="Ask me anything.."
placeholder={`Send a message ${
!settings?.isMobile ? "or try using @ or /" : ""
}`}
value={message}
onKeyDown={(event) => {
if (

View File

@@ -278,16 +278,6 @@ export async function deleteChatSession(chatSessionId: string) {
return response;
}
export async function deleteAllChatSessions(sessionType: "Chat" | "Search") {
const response = await fetch(`/api/chat/delete-all-chat-sessions`, {
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
});
return response;
}
export async function* simulateLLMResponse(input: string, delay: number = 30) {
// Split the input string into tokens. This is a simple example, and in real use case, tokenization can be more complex.
// Iterate over tokens and yield them one by one

View File

@@ -11,10 +11,13 @@ import { createFolder } from "../folders/FolderManagement";
import { usePopup } from "@/components/admin/connectors/Popup";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { AssistantsIconSkeleton } from "@/components/icons/icons";
import {
AssistantsIconSkeleton,
ClosedBookIcon,
} from "@/components/icons/icons";
import { PagesTab } from "./PagesTab";
import { pageType } from "./types";
import LogoWithText from "@/components/header/LogoWithText";
import LogoType from "@/components/header/LogoType";
interface HistorySidebarProps {
page: pageType;
@@ -30,7 +33,6 @@ interface HistorySidebarProps {
showDeleteModal?: (chatSession: ChatSession) => void;
stopGenerating?: () => void;
explicitlyUntoggle: () => void;
showDeleteAllModal?: () => void;
backgroundToggled?: boolean;
}
@@ -50,7 +52,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
stopGenerating = () => null,
showShareModal,
showDeleteModal,
showDeleteAllModal,
backgroundToggled,
},
ref: ForwardedRef<HTMLDivElement>
@@ -99,19 +100,16 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
flex
flex-col relative
h-screen
pt-2
transition-transform
`}
>
<div className="pl-2">
<LogoWithText
showArrow={true}
toggled={toggled}
page={page}
toggleSidebar={toggleSidebar}
explicitlyUntoggle={explicitlyUntoggle}
/>
</div>
<LogoType
showArrow={true}
toggled={toggled}
page={page}
toggleSidebar={toggleSidebar}
explicitlyUntoggle={explicitlyUntoggle}
/>
{page == "chat" && (
<div className="mx-3 mt-4 gap-y-1 flex-col text-text-history-sidebar-button flex gap-x-1.5 items-center items-center">
<Link
@@ -178,7 +176,6 @@ export const HistorySidebar = forwardRef<HTMLDivElement, HistorySidebarProps>(
currentChatId={currentChatId}
folders={folders}
openedFolders={openedFolders}
showDeleteAllModal={showDeleteAllModal}
/>
</div>
</>

View File

@@ -9,8 +9,6 @@ import { usePopup } from "@/components/admin/connectors/Popup";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { pageType } from "./types";
import { FiTrash2 } from "react-icons/fi";
import { NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED } from "@/lib/constants";
export function PagesTab({
page,
@@ -22,7 +20,6 @@ export function PagesTab({
newFolderId,
showShareModal,
showDeleteModal,
showDeleteAllModal,
}: {
page: pageType;
existingChats?: ChatSession[];
@@ -33,7 +30,6 @@ export function PagesTab({
newFolderId: number | null;
showShareModal?: (chatSession: ChatSession) => void;
showDeleteModal?: (chatSession: ChatSession) => void;
showDeleteAllModal?: () => void;
}) {
const groupedChatSessions = existingChats
? groupSessionsByDateRange(existingChats)
@@ -67,98 +63,82 @@ export function PagesTab({
const isHistoryEmpty = !existingChats || existingChats.length === 0;
return (
<div className="flex flex-col relative h-full overflow-y-auto mb-1 ml-3 miniscroll mobile:pb-40">
<div
className={` flex-grow overflow-y-auto ${
NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && "pb-20 "
}`}
>
{folders && folders.length > 0 && (
<div className="py-2 border-b border-border">
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-bold">
Chat Folders
</div>
<FolderList
newFolderId={newFolderId}
folders={folders}
currentChatId={currentChatId}
openedFolders={openedFolders}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
/>
<div className="mb-1 text-text-sidebar ml-3 relative miniscroll mobile:pb-40 overflow-y-auto h-full">
{folders && folders.length > 0 && (
<div className="py-2 border-b border-border">
<div className="text-xs text-subtle flex pb-0.5 mb-1.5 mt-2 font-bold">
Chat Folders
</div>
)}
<div
onDragOver={(event) => {
event.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDropToRemoveFromFolder}
className={`pt-1 transition duration-300 ease-in-out mr-3 ${
isDragOver ? "bg-hover" : ""
} rounded-md`}
>
{(page == "chat" || page == "search") && (
<p className="my-2 text-xs text-sidebar-subtle flex font-bold">
{page == "chat" && "Chat "}
{page == "search" && "Search "}
History
</p>
)}
{isHistoryEmpty ? (
<p className="text-sm mt-2 w-[250px]">
Try sending a message! Your chat history will appear here.
</p>
) : (
Object.entries(groupedChatSessions).map(
([dateRange, chatSessions], ind) => {
if (chatSessions.length > 0) {
return (
<div key={dateRange}>
<div
className={`text-xs text-text-sidebar-subtle ${
ind != 0 && "mt-5"
} flex pb-0.5 mb-1.5 font-medium`}
>
{dateRange}
</div>
{chatSessions
.filter((chat) => chat.folder_id === null)
.map((chat) => {
const isSelected = currentChatId === chat.id;
return (
<div key={`${chat.id}-${chat.name}`}>
<ChatSessionDisplay
showDeleteModal={showDeleteModal}
showShareModal={showShareModal}
closeSidebar={closeSidebar}
search={page == "search"}
chatSession={chat}
isSelected={isSelected}
skipGradient={isDragOver}
/>
</div>
);
})}
</div>
);
}
}
)
)}
<FolderList
newFolderId={newFolderId}
folders={folders}
currentChatId={currentChatId}
openedFolders={openedFolders}
showShareModal={showShareModal}
showDeleteModal={showDeleteModal}
/>
</div>
{showDeleteAllModal && NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED && (
<div className="absolute w-full border-t border-t-border bg-background-100 bottom-0 left-0 p-4">
<button
className="w-full py-2 px-4 text-text-600 hover:text-text-800 bg-background-125 border border-border-strong/50 shadow-sm rounded-md transition-colors duration-200 flex items-center justify-center text-sm"
onClick={showDeleteAllModal}
>
<FiTrash2 className="mr-2" size={14} />
Clear All History
</button>
</div>
)}
<div
onDragOver={(event) => {
event.preventDefault();
setIsDragOver(true);
}}
onDragLeave={() => setIsDragOver(false)}
onDrop={handleDropToRemoveFromFolder}
className={`pt-1 transition duration-300 ease-in-out mr-3 ${
isDragOver ? "bg-hover" : ""
} rounded-md`}
>
{(page == "chat" || page == "search") && (
<p className="my-2 text-xs text-sidebar-subtle flex font-bold">
{page == "chat" && "Chat "}
{page == "search" && "Search "}
History
</p>
)}
{isHistoryEmpty ? (
<p className="text-sm mt-2 w-[250px]">
{page === "search"
? "Try running a search! Your search history will appear here."
: "Try sending a message! Your chat history will appear here."}
</p>
) : (
Object.entries(groupedChatSessions).map(
([dateRange, chatSessions], ind) => {
if (chatSessions.length > 0) {
return (
<div key={dateRange}>
<div
className={`text-xs text-text-sidebar-subtle ${
ind != 0 && "mt-5"
} flex pb-0.5 mb-1.5 font-medium`}
>
{dateRange}
</div>
{chatSessions
.filter((chat) => chat.folder_id === null)
.map((chat) => {
const isSelected = currentChatId === chat.id;
return (
<div key={`${chat.id}-${chat.name}`}>
<ChatSessionDisplay
showDeleteModal={showDeleteModal}
showShareModal={showShareModal}
closeSidebar={closeSidebar}
search={page == "search"}
chatSession={chat}
isSelected={isSelected}
skipGradient={isDragOver}
/>
</div>
);
})}
</div>
);
}
}
)
)}
</div>
</div>

View File

@@ -1,74 +1,50 @@
"use client";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { Logo } from "@/components/logo/Logo";
import { Logo } from "@/components/Logo";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constants";
import Link from "next/link";
import { useContext } from "react";
import { FiSidebar } from "react-icons/fi";
import { LogoType } from "@/components/logo/Logo";
import { EnterpriseSettings } from "@/app/admin/settings/interfaces";
import { useRouter } from "next/navigation";
export function LogoComponent({
enterpriseSettings,
backgroundToggled,
show,
isAdmin,
}: {
enterpriseSettings: EnterpriseSettings | null;
backgroundToggled?: boolean;
show?: boolean;
isAdmin?: boolean;
}) {
const router = useRouter();
return (
<button
onClick={isAdmin ? () => router.push("/chat") : () => {}}
className={`max-w-[200px] ${
!show && "mobile:hidden"
} flex items-center gap-x-1`}
>
{enterpriseSettings && enterpriseSettings.application_name ? (
<>
<div className="flex-none my-auto">
<Logo height={24} width={24} />
</div>
<div className="w-full">
<HeaderTitle backgroundToggled={backgroundToggled}>
{enterpriseSettings.application_name}
</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-left text-subtle">Powered by Onyx</p>
)}
</div>
</>
) : (
<LogoType />
)}
</button>
);
}
export default function FixedLogo({
// Whether the sidebar is toggled or not
backgroundToggled,
}: {
backgroundToggled?: boolean;
}) {
const combinedSettings = useContext(SettingsContext);
const settings = combinedSettings?.settings;
const enterpriseSettings = combinedSettings?.enterpriseSettings;
return (
<>
<Link
href="/chat"
className="fixed cursor-pointer flex z-40 left-4 top-3 h-8"
className="fixed cursor-pointer flex z-40 left-4 top-2 h-8"
>
<LogoComponent
enterpriseSettings={enterpriseSettings!}
backgroundToggled={backgroundToggled}
/>
<div className="max-w-[200px] mobile:hidden flex items-center gap-x-1 my-auto">
<div className="flex-none my-auto">
<Logo height={24} width={24} />
</div>
<div className="w-full">
{enterpriseSettings && enterpriseSettings.application_name ? (
<div>
<HeaderTitle backgroundToggled={backgroundToggled}>
{enterpriseSettings.application_name}
</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-subtle">Powered by Onyx</p>
)}
</div>
) : (
<HeaderTitle backgroundToggled={backgroundToggled}>
Onyx
</HeaderTitle>
)}
</div>
</div>
</Link>
<div className="mobile:hidden fixed left-4 bottom-4">
<FiSidebar

View File

@@ -14,6 +14,7 @@ import { buildClientUrl } from "@/lib/utilsSS";
import { Inter } from "next/font/google";
import { EnterpriseSettings, GatingType } from "./admin/settings/interfaces";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { Logo } from "@/components/Logo";
import { fetchAssistantData } from "@/lib/chat/fetchAssistantdata";
import { AppProvider } from "@/components/context/AppProvider";
import { PHProvider } from "./providers";
@@ -22,7 +23,6 @@ import CardSection from "@/components/admin/CardSection";
import { Suspense } from "react";
import PostHogPageView from "./PostHogPageView";
import Script from "next/script";
import { LogoType } from "@/components/logo/Logo";
const inter = Inter({
subsets: ["latin"],
@@ -115,7 +115,8 @@ export default async function RootLayout({
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 />
<HeaderTitle>Onyx</HeaderTitle>
<Logo height={40} width={40} />
</div>
<CardSection className="max-w-md">
@@ -123,8 +124,7 @@ export default async function RootLayout({
<p className="text-text-500">
Your Onyx instance was not configured properly and your settings
could not be loaded. This could be due to an admin configuration
issue, an incomplete setup, or backend services that may not be up
and running yet.
issue or an incomplete setup.
</p>
<p className="mt-4">
If you&apos;re an admin, please check{" "}
@@ -144,7 +144,7 @@ export default async function RootLayout({
community on{" "}
<a
className="text-link"
href="https://join.slack.com/t/danswer/shared_invite/zt-1w76msxmd-HJHLe3KNFIAIzk_0dSOKaQ"
href="https://onyx.app?utm_source=app&utm_medium=error_page&utm_campaign=config_error"
target="_blank"
rel="noopener noreferrer"
>
@@ -160,7 +160,8 @@ export default async function RootLayout({
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 />
<HeaderTitle>Onyx</HeaderTitle>
<Logo height={40} width={40} />
</div>
<CardSection className="w-full max-w-md">
<h1 className="text-2xl font-bold mb-4 text-error">

View File

@@ -1,7 +1,7 @@
"use client";
import { useContext } from "react";
import { SettingsContext } from "../settings/SettingsProvider";
import { SettingsContext } from "./settings/SettingsProvider";
import Image from "next/image";
export function Logo({
@@ -45,10 +45,10 @@ export function Logo({
);
}
export function LogoType() {
export default function LogoType() {
return (
<Image
className="max-h-8 w-full mr-auto "
className="max-h-8 mr-auto "
src="/logotype.png"
alt="Logo"
width={2640}

View File

@@ -0,0 +1,38 @@
import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constants";
import { HeaderTitle } from "./header/HeaderTitle";
import LogoType, { Logo } from "./Logo";
import { EnterpriseSettings } from "@/app/admin/settings/interfaces";
export default function LogoTypeContainer({
enterpriseSettings,
}: {
enterpriseSettings: EnterpriseSettings | null;
}) {
const onlyLogo =
!enterpriseSettings ||
!enterpriseSettings.use_custom_logo ||
!enterpriseSettings.application_name;
return (
<div className="flex justify-start items-start w-full gap-x-1 my-auto">
<div className="flex-none w-fit mr-auto my-auto">
{onlyLogo ? <LogoType /> : <Logo height={24} width={24} />}
</div>
{!onlyLogo && (
<div className="w-full">
{enterpriseSettings && enterpriseSettings.application_name ? (
<div>
<HeaderTitle>{enterpriseSettings.application_name}</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-subtle">Powered by Onyx</p>
)}
</div>
) : (
<HeaderTitle>Onyx</HeaderTitle>
)}
</div>
)}
</div>
);
}

View File

@@ -27,9 +27,7 @@ export function MetadataBadge({
size: 12,
className: flexNone ? "flex-none" : "mr-0.5 my-auto",
})}
<p className="max-w-[6rem] text-ellipsis overflow-hidden truncate whitespace-nowrap">
{value}lllaasfasdf
</p>
<div className="my-auto flex">{value}</div>
</div>
);
}

View File

@@ -1,4 +1,4 @@
import { Logo } from "./logo/Logo";
import { Logo } from "./Logo";
import { useContext } from "react";
import { SettingsContext } from "./settings/SettingsProvider";

View File

@@ -2,7 +2,7 @@
"use client";
import React, { useContext } from "react";
import Link from "next/link";
import { Logo } from "@/components/logo/Logo";
import { Logo } from "@/components/Logo";
import { NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED } from "@/lib/constants";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { SettingsContext } from "@/components/settings/SettingsProvider";
@@ -14,8 +14,6 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { CgArrowsExpandUpLeft } from "react-icons/cg";
import LogoWithText from "@/components/header/LogoWithText";
import { LogoComponent } from "@/app/chat/shared_chat_search/FixedLogo";
interface Item {
name: string | JSX.Element;
@@ -34,22 +32,36 @@ export function AdminSidebar({ collections }: { collections: Collection[] }) {
return null;
}
const settings = combinedSettings.settings;
const enterpriseSettings = combinedSettings.enterpriseSettings;
return (
<div className="text-text-settings-sidebar pl-0">
<nav className="space-y-2">
<div className="w-full ml-4 mt-1 h-8 justify-start mb-4 flex">
<LogoComponent
show={true}
enterpriseSettings={enterpriseSettings!}
backgroundToggled={false}
isAdmin={true}
/>
<div className="w-full ml-4 h-8 justify-start mb-4 flex">
<div className="flex items-center gap-x-1 my-auto">
<div className="flex-none my-auto">
<Logo height={24} width={24} />
</div>
<div className="w-full">
{enterpriseSettings && enterpriseSettings.application_name ? (
<div>
<HeaderTitle>
{enterpriseSettings.application_name}
</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-subtle">Powered by Onyx</p>
)}
</div>
) : (
<HeaderTitle>Onyx</HeaderTitle>
)}
</div>
</div>
</div>
<div className="flex w-full justify-center">
<Link href="/chat">
<button className="text-sm hover:bg-background-settings-hover flex items-center block w-52 py-2.5 flex px-2 text-left hover:bg-opacity-80 cursor-pointer rounded">
<button className="text-sm flex items-center block w-52 py-2.5 flex px-2 text-left hover:bg-opacity-80 cursor-pointer rounded">
<CgArrowsExpandUpLeft className="my-auto" size={18} />
<p className="ml-1 break-words line-clamp-2 ellipsis leading-none">
Exit Admin

View File

@@ -1,4 +1,4 @@
import { Logo } from "../logo/Logo";
import { Logo } from "../Logo";
export default function AuthFlowContainer({
children,

View File

@@ -1,16 +1,18 @@
"use client";
import { User } from "@/lib/types";
import { UserDropdown } from "../UserDropdown";
import { FiShare2 } from "react-icons/fi";
import { SetStateAction, useContext, useEffect } from "react";
import { NewChatIcon } from "../icons/icons";
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
import { ChatSession } from "@/app/chat/interfaces";
import Link from "next/link";
import { pageType } from "@/app/chat/sessionSidebar/types";
import { useRouter } from "next/navigation";
import { ChatBanner } from "@/app/chat/ChatBanner";
import LogoWithText from "../header/LogoWithText";
import { NewChatIcon } from "../icons/icons";
import { SettingsContext } from "../settings/SettingsProvider";
import LogoType from "../header/LogoType";
import { Persona } from "@/app/admin/assistants/interfaces";
import { LlmOverrideManager } from "@/lib/hooks";
export default function FunctionalHeader({
page,
@@ -19,6 +21,9 @@ export default function FunctionalHeader({
toggleSidebar = () => null,
reset = () => null,
sidebarToggled,
liveAssistant,
onAssistantChange,
llmOverrideManager,
documentSidebarToggled,
toggleUserSettings,
}: {
@@ -29,9 +34,11 @@ export default function FunctionalHeader({
currentChatSession?: ChatSession | null | undefined;
setSharingModalVisible?: (value: SetStateAction<boolean>) => void;
toggleSidebar?: () => void;
liveAssistant?: Persona;
onAssistantChange?: (assistant: Persona) => void;
llmOverrideManager?: LlmOverrideManager;
toggleUserSettings?: () => void;
}) {
const settings = useContext(SettingsContext);
useEffect(() => {
const handleKeyDown = (event: KeyboardEvent) => {
if (event.metaKey || event.ctrlKey) {
@@ -69,11 +76,10 @@ export default function FunctionalHeader({
return (
<div className="left-0 sticky top-0 z-20 w-full relative flex">
<div className="items-end flex mt-2 cursor-pointer text-text-700 relative flex w-full">
<LogoWithText
<LogoType
assistantId={currentChatSession?.persona_id}
page={page}
toggleSidebar={toggleSidebar}
toggled={sidebarToggled && !settings?.isMobile}
handleNewChat={handleNewChat}
/>
<div className="mt-2 flex w-full h-8">
@@ -97,19 +103,18 @@ export default function FunctionalHeader({
</div>
<div className="invisible">
<LogoWithText
<LogoType
page={page}
toggled={sidebarToggled}
toggleSidebar={toggleSidebar}
handleNewChat={handleNewChat}
/>
</div>
<div className="absolute right-0 mobile:top-2 desktop:top-0 flex">
<div className="absolute right-0 top-0 flex gap-x-2">
{setSharingModalVisible && (
<div
onClick={() => setSharingModalVisible(true)}
className="mobile:hidden mr-2 my-auto rounded cursor-pointer hover:bg-hover-light"
className="mobile:hidden my-auto rounded cursor-pointer hover:bg-hover-light"
>
<FiShare2 size="18" />
</div>
@@ -121,7 +126,7 @@ export default function FunctionalHeader({
/>
</div>
<Link
className="desktop:hidden ml-2 my-auto"
className="desktop:hidden my-auto"
href={
`/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA &&

View File

@@ -6,7 +6,6 @@ import {
} from "@/app/chat/message/MemoizedTextComponents";
import React, { useMemo } from "react";
import ReactMarkdown from "react-markdown";
import rehypePrism from "rehype-prism-plus";
import remarkGfm from "remark-gfm";
interface MinimalMarkdownProps {
@@ -36,10 +35,9 @@ export const MinimalMarkdown: React.FC<MinimalMarkdownProps> = ({
return (
<ReactMarkdown
className={`prose max-w-full text-base ${className}`}
className={`w-full text-wrap break-word ${className}`}
components={markdownComponents}
remarkPlugins={[remarkGfm]}
rehypePlugins={[[rehypePrism, { ignoreMissing: true }]]}
>
{content}
</ReactMarkdown>

View File

@@ -21,11 +21,11 @@ export default function TextView({
onClose,
}: TextViewProps) {
const [zoom, setZoom] = useState(100);
const [fileContent, setFileContent] = useState("");
const [fileUrl, setFileUrl] = useState("");
const [fileName, setFileName] = useState("");
const [fileContent, setFileContent] = useState<string>("");
const [fileUrl, setFileUrl] = useState<string>("");
const [fileName, setFileName] = useState<string>("");
const [isLoading, setIsLoading] = useState(true);
const [fileType, setFileType] = useState("application/octet-stream");
const [fileType, setFileType] = useState<string>("application/octet-stream");
const isMarkdownFormat = (mimeType: string): boolean => {
const markdownFormats = [
@@ -51,17 +51,18 @@ export default function TextView({
const fetchFile = useCallback(async () => {
setIsLoading(true);
const fileId = presentingDocument.document_id.split("__")[1];
try {
const fileId = presentingDocument.document_id.split("__")[1];
const response = await fetch(
`/api/chat/file/${encodeURIComponent(fileId)}`
`/api/chat/file/${encodeURIComponent(fileId)}`,
{
method: "GET",
}
);
const blob = await response.blob();
const url = window.URL.createObjectURL(blob);
setFileUrl(url);
setFileName(presentingDocument.semantic_identifier || "document");
const contentType =
response.headers.get("Content-Type") || "application/octet-stream";
setFileType(contentType);
@@ -69,28 +70,9 @@ export default function TextView({
if (isMarkdownFormat(blob.type)) {
const text = await blob.text();
setFileContent(text);
} else if (blob.type === "application/octet-stream") {
try {
const text = await blob.text();
let nonPrintingCount = 0;
for (let i = 0; i < text.length; i++) {
const code = text.charCodeAt(i);
if (code < 32 && ![9, 10, 13].includes(code)) {
nonPrintingCount++;
}
}
const ratio = nonPrintingCount / text.length;
if (ratio < 0.05) {
setFileContent(text);
setFileType("text/plain");
}
} catch (err) {
console.error("Failed to parse octet-stream as text", err);
}
}
} catch (err) {
console.error("Error fetching file:", err);
} catch (error) {
console.error("Error fetching file:", error);
} finally {
setTimeout(() => {
setIsLoading(false);
@@ -155,7 +137,7 @@ export default function TextView({
</div>
) : (
<div
className="w-full h-full transform origin-center transition-transform duration-300 ease-in-out"
className={`w-full h-full transform origin-center transition-transform duration-300 ease-in-out`}
style={{ transform: `scale(${zoom / 100})` }}
>
{isSupportedIframeFormat(fileType) ? (
@@ -164,7 +146,7 @@ export default function TextView({
className="w-full h-full border-none"
title="File Viewer"
/>
) : isMarkdownFormat(fileType) || fileType === "text/plain" ? (
) : isMarkdownFormat(fileType) ? (
<div className="w-full h-full p-6 overflow-y-scroll overflow-x-hidden">
<MinimalMarkdown
content={fileContent}

View File

@@ -2,7 +2,7 @@ import { useEmbeddingFormContext } from "@/components/context/EmbeddingContext";
import { HeaderTitle } from "@/components/header/HeaderTitle";
import { SettingsIcon } from "@/components/icons/icons";
import { Logo } from "@/components/logo/Logo";
import { Logo } from "@/components/Logo";
import { SettingsContext } from "@/components/settings/SettingsProvider";
import Link from "next/link";
import { useContext } from "react";

View File

@@ -10,8 +10,7 @@ export function HeaderTitle({
backgroundToggled?: boolean;
}) {
const isString = typeof children === "string";
const textSize =
isString && children.length > 10 ? "text-lg mb-[4px] " : "text-2xl";
const textSize = isString && children.length > 10 ? "text-xl" : "text-2xl";
return (
<h1
@@ -19,7 +18,7 @@ export function HeaderTitle({
backgroundToggled
? "text-text-sidebar-toggled-header"
: "text-text-sidebar-header"
} break-words text-left line-clamp-2 ellipsis text-strong overflow-hidden leading-none font-bold`}
} break-words line-clamp-2 ellipsis text-strong overflow-visible leading-none font-bold`}
>
{children}
</h1>

View File

@@ -2,7 +2,10 @@
import { useContext } from "react";
import { FiSidebar } from "react-icons/fi";
import { SettingsContext } from "../settings/SettingsProvider";
import { NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA } from "@/lib/constants";
import {
NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED,
NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA,
} from "@/lib/constants";
import { LeftToLineIcon, NewChatIcon, RightToLineIcon } from "../icons/icons";
import {
Tooltip,
@@ -11,11 +14,11 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { pageType } from "@/app/chat/sessionSidebar/types";
import { Logo } from "../logo/Logo";
import { Logo } from "../Logo";
import { HeaderTitle } from "./HeaderTitle";
import Link from "next/link";
import { LogoComponent } from "@/app/chat/shared_chat_search/FixedLogo";
export default function LogoWithText({
export default function LogoType({
toggleSidebar,
hideOnMobile,
handleNewChat,
@@ -36,48 +39,57 @@ export default function LogoWithText({
}) {
const combinedSettings = useContext(SettingsContext);
const enterpriseSettings = combinedSettings?.enterpriseSettings;
const useLogoType =
!enterpriseSettings?.use_custom_logo &&
!enterpriseSettings?.application_name;
return (
<div
className={`${
hideOnMobile && "mobile:hidden"
} z-[100] ml-2 mt-1 h-8 mb-auto shrink-0 flex gap-x-0 items-center text-xl`}
} z-[100] mt-2 h-8 mb-auto shrink-0 flex items-center text-xl`}
>
{toggleSidebar && page == "chat" ? (
<button
onClick={() => toggleSidebar()}
className="flex gap-x-2 items-center ml-0 desktop:hidden "
className="flex gap-x-2 items-center ml-4 desktop:invisible "
>
{!toggled ? (
<Logo className="desktop:hidden -my-2" height={24} width={24} />
) : (
<LogoComponent
show={toggled}
enterpriseSettings={enterpriseSettings!}
backgroundToggled={toggled}
/>
)}
<FiSidebar
size={20}
className={`text-text-mobile-sidebar ${toggled && "mobile:hidden"}`}
className={`${
toggled
? "text-text-mobile-sidebar-toggled"
: "text-text-mobile-sidebar-untoggled"
}`}
/>
{!showArrow && (
<Logo className="desktop:hidden -my-2" height={24} width={24} />
)}
</button>
) : (
<div className="mr-1 invisible mb-auto h-6 w-6">
<Logo height={24} width={24} />
lll
</div>
)}
<div
className={`${
showArrow ? "desktop:invisible" : "invisible"
} break-words inline-block w-fit text-text-700 text-xl`}
} break-words inline-block w-fit ml-2 text-text-700 text-xl`}
>
<LogoComponent
enterpriseSettings={enterpriseSettings!}
backgroundToggled={toggled}
/>
<div className="max-w-[175px]">
{enterpriseSettings && enterpriseSettings.application_name ? (
<div className="w-full">
<HeaderTitle backgroundToggled={toggled}>
{enterpriseSettings.application_name}
</HeaderTitle>
{!NEXT_PUBLIC_DO_NOT_USE_TOGGLE_OFF_DANSWER_POWERED && (
<p className="text-xs text-subtle">Powered by Onyx</p>
)}
</div>
) : (
<HeaderTitle backgroundToggled={toggled}>Onyx</HeaderTitle>
)}
</div>
</div>
{page == "chat" && !showArrow && (
@@ -85,7 +97,7 @@ export default function LogoWithText({
<Tooltip>
<TooltipTrigger asChild>
<Link
className="my-auto mobile:hidden"
className="mb-auto mobile:hidden"
href={
`/${page}` +
(NEXT_PUBLIC_NEW_CHAT_DIRECTS_TO_SAME_PERSONA && assistantId
@@ -125,14 +137,10 @@ export default function LogoWithText({
}}
>
{!toggled && !combinedSettings?.isMobile ? (
<RightToLineIcon className="mobile:hidden text-sidebar-toggle" />
<RightToLineIcon className="text-sidebar-toggle" />
) : (
<LeftToLineIcon className="mobile:hidden text-sidebar-toggle" />
<LeftToLineIcon className="text-sidebar-toggle" />
)}
<FiSidebar
size={20}
className="hidden mobile:block text-text-mobile-sidebar"
/>
</button>
</TooltipTrigger>
<TooltipContent>

View File

@@ -22,7 +22,8 @@ export const DeleteEntityModal = ({
<h2 className="my-auto text-2xl font-bold">Delete {entityType}?</h2>
</div>
<p className="mb-4">
Click below to confirm that you want to delete <b>{entityName}</b>
Click below to confirm that you want to delete{" "}
<b>&quot;{entityName}&quot;</b>
</p>
{additionalDetails && <p className="mb-4">{additionalDetails}</p>}
<div className="flex">

View File

@@ -75,6 +75,3 @@ export const REGISTRATION_URL =
process.env.INTERNAL_URL || "http://127.0.0.1:3001";
export const TEST_ENV = process.env.TEST_ENV?.toLowerCase() === "true";
export const NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED =
process.env.NEXT_PUBLIC_DELETE_ALL_CHATS_ENABLED?.toLowerCase() === "true";

View File

@@ -143,7 +143,7 @@ module.exports = {
// Background for chat messages (user bubbles)
user: "var(--user-bubble)",
"userdropdown-background": "var(--background-150)",
"userdropdown-background": "var(--background-100)",
"text-mobile-sidebar-toggled": "var(--text-800)",
"text-mobile-sidebar-untoggled": "var(--text-500)",
"text-editing-message": "var(--text-800)",