My docs cleanup (#4519)

* update

* improved my docs

* nit

* nit

* k

* push changes

* update

* looking good

* k

* fix preprocessing

* try a fix

* k

* update

* nit

* k

* quick nits

* Cleanup / fixes

* Fixes

* Fix build

* fix

* fix quality checks

---------

Co-authored-by: Weves <chrisweaver101@gmail.com>
This commit is contained in:
pablonyx 2025-04-24 22:20:33 -07:00 committed by GitHub
parent 115cfb6ae9
commit df67ca18d8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
58 changed files with 1591 additions and 1568 deletions

View File

@ -11,6 +11,7 @@ from onyx.server.features.persona.models import PersonaSharedNotificationData
def make_persona_private(
persona_id: int,
creator_user_id: UUID | None,
user_ids: list[UUID] | None,
group_ids: list[int] | None,
db_session: Session,
@ -29,15 +30,15 @@ def make_persona_private(
user_ids_set = set(user_ids)
for user_id in user_ids_set:
db_session.add(Persona__User(persona_id=persona_id, user_id=user_id))
create_notification(
user_id=user_id,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if user_id != creator_user_id:
create_notification(
user_id=user_id,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if group_ids:
group_ids_set = set(group_ids)

View File

@ -40,6 +40,7 @@ def process_llm_stream(
# This stream will be the llm answer if no tool is chosen. When a tool is chosen,
# the stream will contain AIMessageChunks with tool call information.
for message in messages:
answer_piece = message.content
if not isinstance(answer_piece, str):
# this is only used for logging, so fine to

View File

@ -51,7 +51,6 @@ def _parse_agent_event(
Parse the event into a typed object.
Return None if we are not interested in the event.
"""
event_type = event["event"]
# We always just yield the event data, but this piece is useful for two development reasons:

View File

@ -112,7 +112,7 @@ def pre_provision_tenant() -> None:
r = get_redis_client(tenant_id=ONYX_CLOUD_TENANT_ID)
lock_provision: RedisLock = r.lock(
OnyxRedisLocks.PRE_PROVISION_TENANT_LOCK,
OnyxRedisLocks.CLOUD_PRE_PROVISION_TENANT_LOCK,
timeout=_TENANT_PROVISIONING_SOFT_TIME_LIMIT,
)

View File

@ -167,7 +167,6 @@ class Answer:
break
processed_stream.append(packet)
yield packet
self._processed_stream = processed_stream
@property

View File

@ -334,7 +334,7 @@ class OnyxRedisLocks:
CHECK_USER_FILE_FOLDER_SYNC_BEAT_LOCK = "da_lock:check_user_file_folder_sync_beat"
MONITOR_BACKGROUND_PROCESSES_LOCK = "da_lock:monitor_background_processes"
CHECK_AVAILABLE_TENANTS_LOCK = "da_lock:check_available_tenants"
PRE_PROVISION_TENANT_LOCK = "da_lock:pre_provision_tenant"
CLOUD_PRE_PROVISION_TENANT_LOCK = "da_lock:pre_provision_tenant"
CONNECTOR_DOC_PERMISSIONS_SYNC_LOCK_PREFIX = (
"da_lock:connector_doc_permissions_sync"
@ -405,7 +405,6 @@ class OnyxCeleryTask:
f"{ONYX_CLOUD_CELERY_TASK_PREFIX}_monitor_celery_pidbox"
)
# Tenant pre-provisioning
UPDATE_USER_FILE_FOLDER_METADATA = "update_user_file_folder_metadata"
CHECK_FOR_CONNECTOR_DELETION = "check_for_connector_deletion_task"

View File

@ -3,7 +3,6 @@ from datetime import datetime
from uuid import UUID
from fastapi import HTTPException
from sqlalchemy import delete
from sqlalchemy import exists
from sqlalchemy import func
from sqlalchemy import not_
@ -168,6 +167,7 @@ def _get_persona_by_name(
def make_persona_private(
persona_id: int,
creator_user_id: UUID | None,
user_ids: list[UUID] | None,
group_ids: list[int] | None,
db_session: Session,
@ -179,15 +179,15 @@ def make_persona_private(
for user_uuid in user_ids:
db_session.add(Persona__User(persona_id=persona_id, user_id=user_uuid))
create_notification(
user_id=user_uuid,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
if user_uuid != creator_user_id:
create_notification(
user_id=user_uuid,
notif_type=NotificationType.PERSONA_SHARED,
db_session=db_session,
additional_data=PersonaSharedNotificationData(
persona_id=persona_id,
).model_dump(),
)
db_session.commit()
@ -262,6 +262,7 @@ def create_update_persona(
# Privatize Persona
versioned_make_persona_private(
persona_id=persona.id,
creator_user_id=user.id if user else None,
user_ids=create_persona_request.users,
group_ids=create_persona_request.groups,
db_session=db_session,
@ -297,6 +298,7 @@ def update_persona_shared_users(
# Privatize Persona
versioned_make_persona_private(
persona_id=persona_id,
creator_user_id=user.id if user else None,
user_ids=user_ids,
group_ids=None,
db_session=db_session,
@ -770,8 +772,10 @@ def get_personas_by_ids(
def delete_persona_by_name(
persona_name: str, db_session: Session, is_default: bool = True
) -> None:
stmt = delete(Persona).where(
Persona.name == persona_name, Persona.builtin_persona == is_default
stmt = (
update(Persona)
.where(Persona.name == persona_name, Persona.builtin_persona == is_default)
.values(deleted=True)
)
db_session.execute(stmt)

View File

@ -69,7 +69,8 @@ def test_llm_configuration(
existing_provider = fetch_existing_llm_provider(
name=test_llm_request.name, db_session=db_session
)
if existing_provider:
# if an API key is not provided, use the existing provider's API key
if existing_provider and test_api_key is None:
test_api_key = existing_provider.api_key
# For this "testing" workflow, we do *not* need the actual `max_input_tokens`.

View File

@ -90,8 +90,25 @@ def get_folders(
db_session: Session = Depends(get_session),
) -> list[UserFolderSnapshot]:
user_id = user.id if user else None
folders = db_session.query(UserFolder).filter(UserFolder.user_id == user_id).all()
return [UserFolderSnapshot.from_model(folder) for folder in folders]
# Get folders that belong to the user or have the RECENT_DOCS_FOLDER_ID
folders = (
db_session.query(UserFolder)
.filter(
(UserFolder.user_id == user_id) | (UserFolder.id == RECENT_DOCS_FOLDER_ID)
)
.all()
)
# For each folder, filter files to only include those belonging to the current user
result = []
for folder in folders:
folder_snapshot = UserFolderSnapshot.from_model(folder)
folder_snapshot.files = [
file for file in folder_snapshot.files if file.user_id == user_id
]
result.append(folder_snapshot)
return result
@router.get("/user/folder/{folder_id}")
@ -103,13 +120,25 @@ def get_folder(
user_id = user.id if user else None
folder = (
db_session.query(UserFolder)
.filter(UserFolder.id == folder_id, UserFolder.user_id == user_id)
.filter(
UserFolder.id == folder_id,
(
(UserFolder.user_id == user_id)
| (UserFolder.id == RECENT_DOCS_FOLDER_ID)
),
)
.first()
)
if not folder:
raise HTTPException(status_code=404, detail="Folder not found")
return UserFolderSnapshot.from_model(folder)
folder_snapshot = UserFolderSnapshot.from_model(folder)
# Filter files to only include those belonging to the current user
folder_snapshot.files = [
file for file in folder_snapshot.files if file.user_id == user_id
]
return folder_snapshot
RECENT_DOCS_FOLDER_ID = -1

View File

@ -27,7 +27,15 @@ ONYX_REQUEST_ID_CONTEXTVAR: contextvars.ContextVar[str | None] = contextvars.Con
def get_current_tenant_id() -> str:
tenant_id = CURRENT_TENANT_ID_CONTEXTVAR.get()
if tenant_id is None:
import traceback
if not MULTI_TENANT:
return POSTGRES_DEFAULT_SCHEMA
raise RuntimeError("Tenant ID is not set. This should never happen.")
stack_trace = traceback.format_stack()
error_message = (
"Tenant ID is not set. This should never happen.\nStack trace:\n"
+ "".join(stack_trace)
)
raise RuntimeError(error_message)
return tenant_id

70
web/package-lock.json generated
View File

@ -1886,7 +1886,6 @@
},
"node_modules/@jridgewell/source-map": {
"version": "0.3.6",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/gen-mapping": "^0.3.5",
@ -2711,7 +2710,7 @@
"version": "1.51.1",
"resolved": "https://registry.npmjs.org/@playwright/test/-/test-1.51.1.tgz",
"integrity": "sha512-nM+kEaTSAoVlXmMPH10017vn3FSiFqr/bh4fKg9vmAdMfd9SDqRZNvPSiAHADc/itWak+qPvMPZQOPwCBW7k7Q==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright": "1.51.1"
@ -4812,7 +4811,6 @@
},
"node_modules/@types/eslint": {
"version": "9.6.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/estree": "*",
@ -4821,7 +4819,6 @@
},
"node_modules/@types/eslint-scope": {
"version": "3.7.7",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint": "*",
@ -4921,7 +4918,6 @@
},
"node_modules/@types/json-schema": {
"version": "7.0.15",
"dev": true,
"license": "MIT"
},
"node_modules/@types/json5": {
@ -5276,7 +5272,6 @@
},
"node_modules/@webassemblyjs/ast": {
"version": "1.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/helper-numbers": "1.13.2",
@ -5285,22 +5280,18 @@
},
"node_modules/@webassemblyjs/floating-point-hex-parser": {
"version": "1.13.2",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-api-error": {
"version": "1.13.2",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-buffer": {
"version": "1.14.1",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-numbers": {
"version": "1.13.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/floating-point-hex-parser": "1.13.2",
@ -5310,12 +5301,10 @@
},
"node_modules/@webassemblyjs/helper-wasm-bytecode": {
"version": "1.13.2",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/helper-wasm-section": {
"version": "1.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
@ -5326,7 +5315,6 @@
},
"node_modules/@webassemblyjs/ieee754": {
"version": "1.13.2",
"dev": true,
"license": "MIT",
"dependencies": {
"@xtuc/ieee754": "^1.2.0"
@ -5334,7 +5322,6 @@
},
"node_modules/@webassemblyjs/leb128": {
"version": "1.13.2",
"dev": true,
"license": "Apache-2.0",
"dependencies": {
"@xtuc/long": "4.2.2"
@ -5342,12 +5329,10 @@
},
"node_modules/@webassemblyjs/utf8": {
"version": "1.13.2",
"dev": true,
"license": "MIT"
},
"node_modules/@webassemblyjs/wasm-edit": {
"version": "1.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
@ -5362,7 +5347,6 @@
},
"node_modules/@webassemblyjs/wasm-gen": {
"version": "1.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
@ -5374,7 +5358,6 @@
},
"node_modules/@webassemblyjs/wasm-opt": {
"version": "1.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
@ -5385,7 +5368,6 @@
},
"node_modules/@webassemblyjs/wasm-parser": {
"version": "1.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
@ -5398,7 +5380,6 @@
},
"node_modules/@webassemblyjs/wast-printer": {
"version": "1.14.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@webassemblyjs/ast": "1.14.1",
@ -5407,12 +5388,10 @@
},
"node_modules/@xtuc/ieee754": {
"version": "1.2.0",
"dev": true,
"license": "BSD-3-Clause"
},
"node_modules/@xtuc/long": {
"version": "4.2.2",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/acorn": {
@ -5467,7 +5446,6 @@
},
"node_modules/ajv-formats": {
"version": "2.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
@ -5483,7 +5461,6 @@
},
"node_modules/ajv-formats/node_modules/ajv": {
"version": "8.17.1",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@ -5498,7 +5475,6 @@
},
"node_modules/ajv-formats/node_modules/json-schema-traverse": {
"version": "1.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/ajv-keywords": {
@ -6186,7 +6162,6 @@
},
"node_modules/buffer-from": {
"version": "1.1.2",
"dev": true,
"license": "MIT"
},
"node_modules/busboy": {
@ -6419,7 +6394,6 @@
},
"node_modules/chrome-trace-event": {
"version": "1.0.4",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.0"
@ -7253,7 +7227,6 @@
},
"node_modules/enhanced-resolve": {
"version": "5.18.1",
"dev": true,
"license": "MIT",
"dependencies": {
"graceful-fs": "^4.2.4",
@ -7386,7 +7359,6 @@
},
"node_modules/es-module-lexer": {
"version": "1.6.0",
"dev": true,
"license": "MIT"
},
"node_modules/es-object-atoms": {
@ -7949,7 +7921,6 @@
},
"node_modules/esrecurse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"estraverse": "^5.2.0"
@ -7960,7 +7931,6 @@
},
"node_modules/estraverse": {
"version": "5.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@ -7992,7 +7962,6 @@
},
"node_modules/events": {
"version": "3.3.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.8.x"
@ -8048,7 +8017,6 @@
},
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"dev": true,
"license": "MIT"
},
"node_modules/fast-equals": {
@ -8094,7 +8062,6 @@
},
"node_modules/fast-uri": {
"version": "3.0.6",
"dev": true,
"funding": [
{
"type": "github",
@ -8560,7 +8527,6 @@
},
"node_modules/glob-to-regexp": {
"version": "0.4.1",
"dev": true,
"license": "BSD-2-Clause"
},
"node_modules/glob/node_modules/brace-expansion": {
@ -8617,7 +8583,6 @@
},
"node_modules/graceful-fs": {
"version": "4.2.11",
"dev": true,
"license": "ISC"
},
"node_modules/graphemer": {
@ -10752,7 +10717,6 @@
},
"node_modules/loader-runner": {
"version": "4.3.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6.11.5"
@ -11198,7 +11162,6 @@
},
"node_modules/merge-stream": {
"version": "2.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/merge2": {
@ -11755,7 +11718,6 @@
},
"node_modules/mime-db": {
"version": "1.52.0",
"dev": true,
"license": "MIT",
"engines": {
"node": ">= 0.6"
@ -11763,7 +11725,6 @@
},
"node_modules/mime-types": {
"version": "2.1.35",
"dev": true,
"license": "MIT",
"dependencies": {
"mime-db": "1.52.0"
@ -11846,7 +11807,6 @@
},
"node_modules/neo-async": {
"version": "2.6.2",
"dev": true,
"license": "MIT"
},
"node_modules/next": {
@ -15033,7 +14993,7 @@
"version": "1.51.1",
"resolved": "https://registry.npmjs.org/playwright/-/playwright-1.51.1.tgz",
"integrity": "sha512-kkx+MB2KQRkyxjYPc3a0wLZZoDczmppyGJIvQ43l+aZihkaVvmu/21kiyaHeHjiFxjxNNFnUncKmcGIyOojsaw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"playwright-core": "1.51.1"
@ -15052,7 +15012,7 @@
"version": "1.51.1",
"resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.51.1.tgz",
"integrity": "sha512-/crRMj8+j/Nq5s8QcvegseuyeZPxpQCZb6HNk3Sos3BlZyAknRjoyJPFWkpNn8v0+P3WiwqFF8P+zQo4eqiNuw==",
"dev": true,
"devOptional": true,
"license": "Apache-2.0",
"bin": {
"playwright-core": "cli.js"
@ -15531,7 +15491,6 @@
},
"node_modules/randombytes": {
"version": "2.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"safe-buffer": "^5.1.0"
@ -16123,7 +16082,6 @@
},
"node_modules/require-from-string": {
"version": "2.0.2",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=0.10.0"
@ -16294,7 +16252,6 @@
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"dev": true,
"funding": [
{
"type": "github",
@ -16383,7 +16340,6 @@
},
"node_modules/serialize-javascript": {
"version": "6.0.2",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"randombytes": "^2.1.0"
@ -16591,7 +16547,6 @@
},
"node_modules/source-map": {
"version": "0.6.1",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
@ -17205,7 +17160,6 @@
},
"node_modules/tapable": {
"version": "2.2.1",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=6"
@ -17213,7 +17167,6 @@
},
"node_modules/terser": {
"version": "5.39.0",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"@jridgewell/source-map": "^0.3.3",
@ -17230,7 +17183,6 @@
},
"node_modules/terser-webpack-plugin": {
"version": "5.3.14",
"dev": true,
"license": "MIT",
"dependencies": {
"@jridgewell/trace-mapping": "^0.3.25",
@ -17263,7 +17215,6 @@
},
"node_modules/terser-webpack-plugin/node_modules/ajv": {
"version": "8.17.1",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@ -17278,7 +17229,6 @@
},
"node_modules/terser-webpack-plugin/node_modules/ajv-keywords": {
"version": "5.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
@ -17289,7 +17239,6 @@
},
"node_modules/terser-webpack-plugin/node_modules/jest-worker": {
"version": "27.5.1",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/node": "*",
@ -17302,12 +17251,10 @@
},
"node_modules/terser-webpack-plugin/node_modules/json-schema-traverse": {
"version": "1.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/terser-webpack-plugin/node_modules/schema-utils": {
"version": "4.3.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.9",
@ -17325,7 +17272,6 @@
},
"node_modules/terser-webpack-plugin/node_modules/supports-color": {
"version": "8.1.1",
"dev": true,
"license": "MIT",
"dependencies": {
"has-flag": "^4.0.0"
@ -17339,12 +17285,10 @@
},
"node_modules/terser/node_modules/commander": {
"version": "2.20.3",
"dev": true,
"license": "MIT"
},
"node_modules/terser/node_modules/source-map-support": {
"version": "0.5.21",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
@ -18144,7 +18088,6 @@
},
"node_modules/watchpack": {
"version": "2.4.2",
"dev": true,
"license": "MIT",
"dependencies": {
"glob-to-regexp": "^0.4.1",
@ -18172,7 +18115,6 @@
},
"node_modules/webpack": {
"version": "5.98.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/eslint-scope": "^3.7.7",
@ -18315,7 +18257,6 @@
},
"node_modules/webpack/node_modules/ajv": {
"version": "8.17.1",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3",
@ -18330,7 +18271,6 @@
},
"node_modules/webpack/node_modules/ajv-keywords": {
"version": "5.1.0",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.3"
@ -18341,7 +18281,6 @@
},
"node_modules/webpack/node_modules/eslint-scope": {
"version": "5.1.1",
"dev": true,
"license": "BSD-2-Clause",
"dependencies": {
"esrecurse": "^4.3.0",
@ -18353,7 +18292,6 @@
},
"node_modules/webpack/node_modules/estraverse": {
"version": "4.3.0",
"dev": true,
"license": "BSD-2-Clause",
"engines": {
"node": ">=4.0"
@ -18361,12 +18299,10 @@
},
"node_modules/webpack/node_modules/json-schema-traverse": {
"version": "1.0.0",
"dev": true,
"license": "MIT"
},
"node_modules/webpack/node_modules/schema-utils": {
"version": "4.3.0",
"dev": true,
"license": "MIT",
"dependencies": {
"@types/json-schema": "^7.0.9",

View File

@ -25,6 +25,7 @@ import {
TooltipTrigger,
} from "@/components/ui/tooltip";
import { useAuthType } from "@/lib/hooks";
import { InfoIcon } from "lucide-react";
function parseJsonWithTrailingCommas(jsonString: string) {
// Regular expression to remove trailing commas before } or ]
@ -159,25 +160,14 @@ function ActionForm({
component="div"
className="mb-4 text-error text-sm"
/>
<div className="mt-4 text-sm bg-blue-50 p-4 rounded-md border border-blue-200">
<div className="mt-4 text-sm bg-blue-50 text-blue-700 dark:text-blue-300 dark:bg-blue-900 p-4 rounded-md border border-blue-200 dark:border-blue-800">
<Link
href="https://docs.onyx.app/tools/custom"
className="text-link hover:underline flex items-center"
target="_blank"
rel="noopener noreferrer"
>
<svg
xmlns="http://www.w3.org/2000/svg"
className="h-5 w-5 mr-2"
viewBox="0 0 20 20"
fill="currentColor"
>
<path
fillRule="evenodd"
d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7-4a1 1 0 11-2 0 1 1 0 012 0zM9 9a1 1 0 000 2v3a1 1 0 001 1h1a1 1 0 100-2v-3a1 1 0 00-1-1H9z"
clipRule="evenodd"
/>
</svg>
<InfoIcon className="w-4 h-4 mr-2 " />
Learn more about actions in our documentation
</Link>
</div>
@ -367,7 +357,7 @@ interface ToolFormValues {
}
const ToolSchema = Yup.object().shape({
definition: Yup.string().required("Tool definition is required"),
definition: Yup.string().required("Action definition is required"),
customHeaders: Yup.array()
.of(
Yup.object().shape({

View File

@ -27,15 +27,17 @@ export default async function Page(props: {
);
} else {
body = (
<div className="w-full my-8">
<div className="w-full mt-8 pb-8">
<div>
<div>
<CardSection>
<ActionEditor tool={tool} />
</CardSection>
<Title className="mt-12">Delete Tool</Title>
<Text>Click the button below to permanently delete this tool.</Text>
<Title className="mt-12">Delete Action</Title>
<Text>
Click the button below to permanently delete this action.
</Text>
<div className="flex mt-6">
<DeleteToolButton toolId={tool.id} />
</div>
@ -50,7 +52,7 @@ export default async function Page(props: {
<BackButton />
<AdminPageTitle
title="Edit Tool"
title="Edit Action"
icon={<ToolIcon size={32} className="my-auto" />}
/>

View File

@ -41,11 +41,8 @@ function NewApiKeyModal({
const [copyClicked, setCopyClicked] = useState(false);
return (
<Modal onOutsideClick={onClose}>
<Modal title="New API Key" onOutsideClick={onClose}>
<div className="px-8 py-8">
<div className="flex w-full border-b border-border mb-4 pb-4">
<Title>New API Key</Title>
</div>
<div className="h-32">
<Text className="mb-4">
Make sure you copy your new API key. You wont be able to see this

File diff suppressed because it is too large Load Diff

View File

@ -14,6 +14,7 @@ import {
} from "@/components/ui/table";
import { Badge } from "@/components/ui/badge";
import { SlackBot } from "@/lib/types";
import { EditIcon } from "@/components/icons/icons";
const NUM_IN_PAGE = 20;
@ -83,7 +84,7 @@ export const SlackBotTable = ({ slackBots }: { slackBots: SlackBot[] }) => {
>
<TableCell>
<div className="flex items-center">
<FiEdit className="mr-4" />
<EditIcon className="mr-4" />
{slackBot.name}
</div>
</TableCell>

View File

@ -424,7 +424,6 @@ export function LLMProviderUpdateForm({
/>
</div>
)}
<IsPublicGroupSelector
formikProps={formikProps}
objectName="LLM Provider"

View File

@ -168,7 +168,7 @@ export function Explorer({
return (
<div>
{popup}
<div className="justify-center py-2">
<div className="justify-center pt-2">
<div className="flex items-center w-full border-2 border-border rounded-lg px-4 py-2 focus-within:border-accent bg-background-search dark:bg-transparent">
<MagnifyingGlass />
<textarea

View File

@ -17,6 +17,7 @@ import { getErrorMsg } from "@/lib/fetchUtils";
import { HoverPopup } from "@/components/HoverPopup";
import { CustomCheckbox } from "@/components/CustomCheckbox";
import { ScoreSection } from "../ScoreEditor";
import { truncateString } from "@/lib/utils";
const IsVisibleSection = ({
document,
@ -109,12 +110,12 @@ export const DocumentFeedbackTable = ({
<TableRow key={document.document_id}>
<TableCell className="whitespace-normal break-all">
<a
className="text-blue-600"
className="text-blue-600 dark:text-blue-300"
href={document.link}
target="_blank"
rel="noopener noreferrer"
>
{document.semantic_id}
{truncateString(document.semantic_id, 100)}
</a>
</TableCell>
<TableCell>

View File

@ -144,6 +144,7 @@ function Main() {
/>
{isPaidEnterpriseFeaturesEnabled && (
<Tabs
className="mt-2"
value={tabIndex.toString()}
onValueChange={(val) => setTabIndex(parseInt(val))}
>

View File

@ -14,7 +14,7 @@ const Page = () => {
Authentication Error
</h2>
<p className="text-text-700 text-center">
We encountered an issue while attempting to log you in.
There was a problem with your login attempt.
</p>
<div className="bg-red-50 dark:bg-red-950/30 border border-red-200 dark:border-red-800 rounded-lg p-4 shadow-sm">
<h3 className="text-red-800 dark:text-red-400 font-semibold mb-2">
@ -46,8 +46,11 @@ const Page = () => {
please reach out to your system administrator for assistance.
{NEXT_PUBLIC_CLOUD_ENABLED && (
<span className="block mt-1 text-blue-600">
A member of our team has been automatically notified about this
issue.
If you continue to experience problems please reach out to the
Onyx team at{" "}
<a href="mailto:support@onyx.app" className="text-blue-600">
support@onyx.app
</a>
</span>
)}
</p>

View File

@ -141,9 +141,6 @@ export function EmailPasswordForm({
name="password"
label="Password"
type="password"
includeForgotPassword={
NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && !isSignup
}
placeholder="**************"
/>

View File

@ -29,38 +29,52 @@ export default function LoginPage({
useSendAuthRequiredMessage();
return (
<div className="flex flex-col w-full justify-center">
{authUrl && authTypeMetadata && (
<>
{authUrl &&
authTypeMetadata &&
authTypeMetadata.authType !== "cloud" &&
// basic auth is handled below w/ the EmailPasswordForm
authTypeMetadata.authType !== "basic" && (
<>
<h2 className="text-center text-xl text-strong font-bold">
<LoginText />
</h2>
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
</>
)}
{authTypeMetadata?.authType === "cloud" && (
<div className="w-full justify-center">
<h2 className="text-center text-xl text-strong font-bold">
<LoginText />
</h2>
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
</>
)}
{authTypeMetadata?.authType === "cloud" && (
<div className="mt-4 w-full justify-center">
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-background-300"></div>
<span className="px-4 text-text-500">or</span>
<div className="flex-grow border-t border-background-300"></div>
</div>
<EmailPasswordForm shouldVerify={true} nextUrl={nextUrl} />
{NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED && (
<div className="flex mt-4 justify-between">
<Link
href="/auth/forgot-password"
className="text-link font-medium"
className="ml-auto text-link font-medium"
>
Reset Password
</Link>
</div>
)}
{authUrl && authTypeMetadata && (
<>
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-background-300"></div>
<span className="px-4 text-text-500">or</span>
<div className="flex-grow border-t border-background-300"></div>
</div>
<SignInButton
authorizeUrl={authUrl}
authType={authTypeMetadata?.authType}
/>
</>
)}
</div>
)}

View File

@ -40,10 +40,19 @@ const Page = async (props: {
// if user is already logged in, take them to the main app page
if (currentUser && currentUser.is_active && !currentUser.is_anonymous_user) {
console.log("Login page: User is logged in, redirecting to chat", {
userId: currentUser.id,
is_active: currentUser.is_active,
is_anonymous: currentUser.is_anonymous_user,
});
if (authTypeMetadata?.requiresVerification && !currentUser.is_verified) {
return redirect("/auth/waiting-on-verification");
}
return redirect("/chat");
// Add a query parameter to indicate this is a redirect from login
// This will help prevent redirect loops
return redirect("/chat?from=login");
}
// get where to send the user to authenticate

View File

@ -82,23 +82,22 @@ const Page = async (props: {
</>
)}
{cloud && authUrl && (
<div className="w-full justify-center">
<SignInButton authorizeUrl={authUrl} authType="cloud" />
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-background-300"></div>
<span className="px-4 text-text-500">or</span>
<div className="flex-grow border-t border-background-300"></div>
</div>
</div>
)}
<EmailPasswordForm
isSignup
shouldVerify={authTypeMetadata?.requiresVerification}
nextUrl={nextUrl}
defaultEmail={defaultEmail}
/>
{cloud && authUrl && (
<div className="w-full justify-center">
<div className="flex items-center w-full my-4">
<div className="flex-grow border-t border-background-300"></div>
<span className="px-4 text-text-500">or</span>
<div className="flex-grow border-t border-background-300"></div>
</div>
<SignInButton authorizeUrl={authUrl} authType="cloud" />
</div>
)}
</div>
</>
</AuthFlowContainer>

View File

@ -104,9 +104,7 @@ import {
SIDEBAR_TOGGLED_COOKIE_NAME,
} from "@/components/resizable/constants";
import FixedLogo from "@/components/logo/FixedLogo";
import ExceptionTraceModal from "@/components/modals/ExceptionTraceModal";
import { SEARCH_TOOL_ID, SEARCH_TOOL_NAME } from "./tools/constants";
import { useUser } from "@/components/user/UserProvider";
import { ApiKeyModal } from "@/components/llm/ApiKeyModal";
@ -178,12 +176,10 @@ export function ChatPage({
selectedFolders,
addSelectedFile,
addSelectedFolder,
removeSelectedFolder,
clearSelectedItems,
folders: userFolders,
files: allUserFiles,
uploadFile,
removeSelectedFile,
currentMessageFiles,
setCurrentMessageFiles,
} = useDocumentsContext();
@ -224,7 +220,6 @@ export function ChatPage({
const settings = useContext(SettingsContext);
const enterpriseSettings = settings?.enterpriseSettings;
const [viewingFilePicker, setViewingFilePicker] = useState(false);
const [toggleDocSelection, setToggleDocSelection] = useState(false);
const [documentSidebarVisible, setDocumentSidebarVisible] = useState(false);
const [proSearchEnabled, setProSearchEnabled] = useState(proSearchToggled);
@ -706,6 +701,7 @@ export function ChatPage({
chatSessionId || currentSessionId(),
newCompleteMessageMap
);
console.log(newCompleteMessageDetail);
return newCompleteMessageDetail;
};
@ -1171,6 +1167,13 @@ export function ChatPage({
};
}, [autoScrollEnabled, screenHeight, currentSessionHasSentLocalUserMessage]);
const reset = () => {
setMessage("");
setCurrentMessageFiles([]);
clearSelectedItems();
setLoadingError(null);
};
const onSubmit = async ({
messageIdToResend,
messageOverride,
@ -1201,6 +1204,61 @@ export function ChatPage({
// Mark that we've sent a message for this session in the current page load
markSessionMessageSent(frozenSessionId);
// Check if the last message was an error and remove it before proceeding with a new message
// Ensure this isn't a regeneration or resend, as those operations should preserve the history leading up to the point of regeneration/resend.
let currentMap = currentMessageMap(completeMessageDetail);
let currentHistory = buildLatestMessageChain(currentMap);
let lastMessage = currentHistory[currentHistory.length - 1];
if (
lastMessage &&
lastMessage.type === "error" &&
!messageIdToResend &&
!regenerationRequest
) {
const newMap = new Map(currentMap);
const parentId = lastMessage.parentMessageId;
// Remove the error message itself
newMap.delete(lastMessage.messageId);
// Remove the parent message + update the parent of the parent to no longer
// link to the parent
if (parentId !== null && parentId !== undefined) {
const parentOfError = newMap.get(parentId);
if (parentOfError) {
const grandparentId = parentOfError.parentMessageId;
if (grandparentId !== null && grandparentId !== undefined) {
const grandparent = newMap.get(grandparentId);
if (grandparent) {
// Update grandparent to no longer link to parent
const updatedGrandparent = {
...grandparent,
childrenMessageIds: (
grandparent.childrenMessageIds || []
).filter((id) => id !== parentId),
latestChildMessageId:
grandparent.latestChildMessageId === parentId
? null
: grandparent.latestChildMessageId,
};
newMap.set(grandparentId, updatedGrandparent);
}
}
// Remove the parent message
newMap.delete(parentId);
}
}
// Update the state immediately so subsequent logic uses the cleaned map
updateCompleteMessageDetail(frozenSessionId, newMap);
console.log("Removed previous error message ID:", lastMessage.messageId);
// update state for the new world (with the error message removed)
currentHistory = buildLatestMessageChain(newMap);
currentMap = newMap;
lastMessage = currentHistory[currentHistory.length - 1];
}
if (currentChatState() != "input") {
if (currentChatState() == "uploading") {
setPopup({
@ -1270,11 +1328,10 @@ export function ChatPage({
currentSessionId()
);
}
const messageMap = currentMessageMap(completeMessageDetail);
const messageToResendParent =
messageToResend?.parentMessageId !== null &&
messageToResend?.parentMessageId !== undefined
? messageMap.get(messageToResend.parentMessageId)
? currentMap.get(messageToResend.parentMessageId)
: null;
const messageToResendIndex = messageToResend
? messageHistory.indexOf(messageToResend)
@ -1301,15 +1358,15 @@ export function ChatPage({
const currMessageHistory =
messageToResendIndex !== null
? messageHistory.slice(0, messageToResendIndex)
: messageHistory;
? currentHistory.slice(0, messageToResendIndex)
: currentHistory;
let parentMessage =
messageToResendParent ||
(currMessageHistory.length > 0
? currMessageHistory[currMessageHistory.length - 1]
: null) ||
(messageMap.size === 1 ? Array.from(messageMap.values())[0] : null);
(currentMap.size === 1 ? Array.from(currentMap.values())[0] : null);
let currentAssistantId;
if (alternativeAssistantOverride) {
@ -1356,13 +1413,9 @@ export function ChatPage({
frozenMessageMap: Map<number, Message>;
} = null;
try {
const mapKeys = Array.from(
currentMessageMap(completeMessageDetail).keys()
);
const systemMessage = Math.min(...mapKeys);
const mapKeys = Array.from(currentMap.keys());
const lastSuccessfulMessageId =
getLastSuccessfulMessageId(currMessageHistory) || systemMessage;
getLastSuccessfulMessageId(currMessageHistory);
const stack = new CurrentMessageFIFO();
@ -1479,11 +1532,12 @@ export function ChatPage({
upsertToCompleteMessageMap({
messages: messageUpdates,
chatSessionId: currChatSessionId,
completeMessageMapOverride: currentMap,
});
currentMap = currentFrozenMessageMap;
const frozenMessageMap = currentFrozenMessageMap;
initialFetchDetails = {
frozenMessageMap,
frozenMessageMap: currentMap,
assistant_message_id,
user_message_id,
};
@ -1715,14 +1769,18 @@ export function ChatPage({
] as [number, number][])
: null;
return upsertToCompleteMessageMap({
const newMessageDetails = upsertToCompleteMessageMap({
messages: messages,
replacementsMap: replacementsMap,
completeMessageMapOverride: frozenMessageMap,
// Pass the latest map state
completeMessageMapOverride: currentMap,
chatSessionId: frozenSessionId!,
});
currentMap = newMessageDetails.messageMap;
return newMessageDetails;
};
const systemMessageId = Math.min(...mapKeys);
updateFn([
{
messageId: regenerationRequest
@ -1732,7 +1790,8 @@ export function ChatPage({
type: "user",
files: files,
toolCall: null,
parentMessageId: error ? null : lastSuccessfulMessageId,
// in the frontend, every message should have a parent ID
parentMessageId: lastSuccessfulMessageId ?? systemMessageId,
childrenMessageIds: [
...(regenerationRequest?.parentMessage?.childrenMessageIds ||
[]),
@ -1786,7 +1845,7 @@ export function ChatPage({
} catch (e: any) {
console.log("Error:", e);
const errorMsg = e.message;
upsertToCompleteMessageMap({
const newMessageDetails = upsertToCompleteMessageMap({
messages: [
{
messageId:
@ -1809,8 +1868,9 @@ export function ChatPage({
initialFetchDetails?.user_message_id || TEMP_USER_MESSAGE_ID,
},
],
completeMessageMapOverride: currentMessageMap(completeMessageDetail),
completeMessageMapOverride: currentMap,
});
currentMap = newMessageDetails.messageMap;
}
console.log("Finished streaming");
setAgenticGenerating(false);
@ -1909,15 +1969,21 @@ export function ChatPage({
updateChatState("uploading", currentSessionId());
const [uploadedFiles, error] = await uploadFilesForChat(acceptedFiles);
if (error) {
setPopup({
type: "error",
message: error,
});
}
for (let file of acceptedFiles) {
const formData = new FormData();
formData.append("files", file);
const response = await uploadFile(formData, null);
setCurrentMessageFiles((prev) => [...prev, ...uploadedFiles]);
if (response.length > 0) {
const uploadedFile = response[0];
addSelectedFile(uploadedFile);
} else {
setPopup({
type: "error",
message: "Failed to upload file",
});
}
}
updateChatState("input", currentSessionId());
};
@ -2396,7 +2462,7 @@ export function ChatPage({
liveAssistant={liveAssistant}
setShowAssistantsModal={setShowAssistantsModal}
explicitlyUntoggle={explicitlyUntoggle}
reset={() => setMessage("")}
reset={reset}
page="chat"
ref={innerSidebarElementRef}
toggleSidebar={toggleSidebar}
@ -3294,7 +3360,7 @@ export function ChatPage({
: "w-[0px]"
}
`}
></div>
/>
</div>
)}
</Dropzone>

View File

@ -149,7 +149,7 @@ export const FolderDropdown = forwardRef<HTMLDivElement, FolderDropdownProps>(
ref={setNodeRef}
style={style}
{...attributes}
className="overflow-visible mt-2 w-full"
className="overflow-visible pt-2 w-full"
onDragOver={handleDragOver}
onDrop={handleDrop}
>
@ -159,13 +159,13 @@ export const FolderDropdown = forwardRef<HTMLDivElement, FolderDropdownProps>(
>
<div
ref={ref}
className="flex overflow-visible items-center w-full text-text-darker rounded-md p-1 relative sticky top-0"
className="flex overflow-visible items-center w-full text-text-darker rounded-md p-1 bg-background-sidebar dark:bg-[#000] relative sticky top-0"
style={{ zIndex: 10 - index }}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<button
className="flex overflow-hidden items-center flex-grow"
className="flex overflow-hidden bg-background-sidebar dark:bg-[#000] items-center flex-grow"
onClick={() => !isEditing && setIsOpen(!isOpen)}
{...(isEditing ? {} : listeners)}
>

View File

@ -402,17 +402,16 @@ export function ChatInputBar({
}
}
}
if (!showPrompts && !showSuggestions) {
return;
}
if (e.key === "ArrowDown") {
e.preventDefault();
setTabbingIconIndex((tabbingIconIndex) =>
Math.min(
tabbingIconIndex + 1,
// showPrompts ? filteredPrompts.length :
assistantTagOptions.length
showPrompts ? filteredPrompts.length : assistantTagOptions.length
)
);
} else if (e.key === "ArrowUp") {
@ -440,13 +439,14 @@ export function ChatInputBar({
ref={suggestionsRef}
className="text-sm absolute w-[calc(100%-2rem)] top-0 transform -translate-y-full"
>
<div className="rounded-lg py-1 sm-1.5 bg-input-background border border-border dark:border-none shadow-lg px-1.5 mt-2 z-10">
<div className="rounded-lg py-1 overflow-y-auto max-h-[200px] sm-1.5 bg-input-background border border-border dark:border-none shadow-lg px-1.5 mt-2 z-10">
{assistantTagOptions.map((currentAssistant, index) => (
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index && "bg-neutral-200"
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-neutral-200/90 cursor-pointer`}
tabbingIconIndex == index &&
"bg-neutral-200 dark:bg-neutral-800"
} rounded items-center rounded-lg content-start flex gap-x-1 py-2 w-full hover:bg-neutral-200/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
onClick={() => {
updatedTaggedAssistant(currentAssistant);
}}
@ -468,8 +468,8 @@ export function ChatInputBar({
target="_self"
className={`${
tabbingIconIndex == assistantTagOptions.length &&
"bg-neutral-200"
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-neutral-200/90 cursor-pointer`}
"bg-neutral-200 dark:bg-neutral-800"
} rounded rounded-lg px-3 flex gap-x-1 py-2 w-full items-center hover:bg-neutral-200/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
href="/assistants/new"
>
<FiPlus size={17} />
@ -484,14 +484,15 @@ export function ChatInputBar({
ref={suggestionsRef}
className="text-sm absolute inset-x-0 top-0 w-full transform -translate-y-full"
>
<div className="rounded-lg py-1.5 bg-input-background dark:border-none border border-border shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
<div className="rounded-lg overflow-y-auto max-h-[200px] py-1.5 bg-input-background dark:border-none border border-border shadow-lg mx-2 px-1.5 mt-2 rounded z-10">
{filteredPrompts.map(
(currentPrompt: InputPrompt, index: number) => (
<button
key={index}
className={`px-2 ${
tabbingIconIndex == index && "bg-background-dark/75"
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-background-dark/90 cursor-pointer`}
tabbingIconIndex == index &&
"bg-background-dark/75 dark:bg-neutral-800/75"
} rounded content-start flex gap-x-1 py-1.5 w-full hover:bg-background-dark/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
onClick={() => {
updateInputPrompt(currentPrompt);
}}
@ -509,8 +510,8 @@ export function ChatInputBar({
target="_self"
className={`${
tabbingIconIndex == filteredPrompts.length &&
"bg-background-dark/75"
} px-3 flex gap-x-1 py-2 w-full rounded-lg items-center hover:bg-background-dark/90 cursor-pointer`}
"bg-background-dark/75 dark:bg-neutral-800/75"
} px-3 flex gap-x-1 py-2 w-full rounded-lg items-center hover:bg-background-dark/90 dark:hover:bg-neutral-800/90 cursor-pointer`}
href="/chat/input-prompts"
>
<FiPlus size={17} />

View File

@ -18,6 +18,7 @@ export default async function Layout({
);
if ("redirect" in data) {
console.log("redirect", data.redirect);
redirect(data.redirect);
}

View File

@ -36,7 +36,10 @@ export const MemoizedAnchor = memo(
if (match) {
const isUserFileCitation = userFiles?.length && userFiles.length > 0;
if (isUserFileCitation) {
const index = parseInt(match[2], 10) - 1;
const index = Math.min(
parseInt(match[2], 10) - 1,
userFiles?.length - 1
);
const associatedUserFile = userFiles?.[index];
if (!associatedUserFile) {
return <a href={children as string}>{children}</a>;

View File

@ -342,12 +342,7 @@ export const AIMessage = ({
}
const processed = preprocessLaTeX(content);
// Escape $ that are preceded by a space and followed by a non-$ character
const escapedDollarSigns = processed.replace(/([\s])\$([^\$])/g, "$1\\$$2");
return (
escapedDollarSigns + (!isComplete && !toolCallGenerating ? " [*]() " : "")
);
return processed + (!isComplete && !toolCallGenerating ? " [*]() " : "");
};
const finalContentProcessed = processContent(finalContent as string);

View File

@ -5,7 +5,7 @@ import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces"
import { destructureValue, structureValue } from "@/lib/llm/utils";
import { setUserDefaultModel } from "@/lib/users/UserSettings";
import { useRouter } from "next/navigation";
import { usePathname, useRouter } from "next/navigation";
import { PopupSpec } from "@/components/admin/connectors/Popup";
import { useUser } from "@/components/user/UserProvider";
import { Separator } from "@/components/ui/separator";
@ -207,6 +207,8 @@ export function UserSettingsModal({
setIsLoading(false);
}
};
const pathname = usePathname();
const showPasswordSection = user?.password_configured;
const handleDeleteAllChats = async () => {
@ -219,7 +221,9 @@ export function UserSettingsModal({
type: "success",
});
refreshChatSessions();
router.push("/chat");
if (pathname.includes("/chat")) {
router.push("/chat");
}
} else {
throw new Error("Failed to delete all chat sessions");
}
@ -382,7 +386,7 @@ export function UserSettingsModal({
<div className="pt-4 border-t border-border">
{!showDeleteConfirmation ? (
<div className="space-y-3">
<p className="text-sm text-neutral-600 ">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
This will permanently delete all your chat sessions and
cannot be undone.
</p>
@ -397,7 +401,7 @@ export function UserSettingsModal({
</div>
) : (
<div className="space-y-3">
<p className="text-sm text-neutral-600 ">
<p className="text-sm text-neutral-600 dark:text-neutral-400">
Are you sure you want to delete all your chat sessions?
</p>
<div className="flex gap-2">

View File

@ -160,6 +160,7 @@ export const DocumentsProvider: React.FC<DocumentsProviderProps> = ({
const refreshFolders = async () => {
try {
console.log("fetching folders");
const data = await documentsService.fetchFolders();
setFolders(data);
} catch (error) {

View File

@ -1,6 +1,6 @@
"use client";
import React, { useMemo, useState, useTransition } from "react";
import React, { useEffect, useMemo, useState, useTransition } from "react";
import { useRouter, useSearchParams } from "next/navigation";
import {
Plus,
@ -68,11 +68,23 @@ export default function MyDocuments() {
const [sortDirection, setSortDirection] = useState<SortDirection>(
SortDirection.Descending
);
const pageLimit = 10;
const searchParams = useSearchParams();
const router = useRouter();
const { popup, setPopup } = usePopup();
const [isCreateFolderOpen, setIsCreateFolderOpen] = useState(false);
useEffect(() => {
const createFolder = searchParams.get("createFolder");
if (createFolder) {
setIsCreateFolderOpen(true);
const newSearchParams = new URLSearchParams(searchParams);
newSearchParams.delete("createFolder");
router.replace(`?${newSearchParams.toString()}`);
}
}, [searchParams]);
const [isPending, startTransition] = useTransition();
const [hoveredColumn, setHoveredColumn] = useState<SortType | null>(null);
@ -118,120 +130,24 @@ export default function MyDocuments() {
};
const handleDeleteItem = async (itemId: number, isFolder: boolean) => {
if (!isFolder) {
// For files, keep the old confirmation
const confirmDelete = window.confirm(
`Are you sure you want to delete this file?`
);
if (confirmDelete) {
try {
await deleteItem(itemId, isFolder);
setPopup({
message: `File deleted successfully`,
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Error deleting item:", error);
setPopup({
message: `Failed to delete file`,
type: "error",
});
}
}
}
// If it's a folder, the SharedFolderItem component will handle it
};
const handleMoveItem = async (
itemId: number,
currentFolderId: number | null,
isFolder: boolean
) => {
const availableFolders = folders
.filter((folder) => folder.id !== itemId)
.map((folder) => `${folder.id}: ${folder.name}`)
.join("\n");
const promptMessage = `Enter the ID of the destination folder:\n\nAvailable folders:\n${availableFolders}\n\nEnter 0 to move to the root folder.`;
const destinationFolderId = prompt(promptMessage);
if (destinationFolderId !== null) {
const newFolderId = parseInt(destinationFolderId, 10);
if (isNaN(newFolderId)) {
setPopup({
message: "Invalid folder ID",
type: "error",
});
return;
}
try {
await moveItem(
itemId,
newFolderId === 0 ? null : newFolderId,
isFolder
);
setPopup({
message: `${
isFolder ? "Knowledge Group" : "File"
} moved successfully`,
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Error moving item:", error);
setPopup({
message: "Failed to move item",
type: "error",
});
}
}
};
const handleDownloadItem = async (documentId: string) => {
try {
await downloadItem(documentId);
} catch (error) {
console.error("Error downloading file:", error);
await deleteItem(itemId, isFolder);
setPopup({
message: "Failed to download file",
message: isFolder
? `Folder deleted successfully`
: `File deleted successfully`,
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Error deleting item:", error);
setPopup({
message: `Failed to delete ${isFolder ? "folder" : "file"}`,
type: "error",
});
}
};
const onRenameItem = async (
itemId: number,
currentName: string,
isFolder: boolean
) => {
const newName = prompt(
`Enter new name for ${isFolder ? "Knowledge Group" : "File"}:`,
currentName
);
if (newName && newName !== currentName) {
try {
await renameItem(itemId, newName, isFolder);
setPopup({
message: `${
isFolder ? "Knowledge Group" : "File"
} renamed successfully`,
type: "success",
});
await refreshFolders();
} catch (error) {
console.error("Error renaming item:", error);
setPopup({
message: `Failed to rename ${isFolder ? "Knowledge Group" : "File"}`,
type: "error",
});
}
}
};
const filteredFolders = useMemo(() => {
return folders
.filter(
@ -438,11 +354,7 @@ export default function MyDocuments() {
onClick={handleFolderClick}
description={folder.description}
lastUpdated={folder.created_at}
onRename={() => onRenameItem(folder.id, folder.name, true)}
onDelete={() => handleDeleteItem(folder.id, true)}
onMove={() =>
handleMoveItem(folder.id, currentFolder, true)
}
/>
))}
</div>

View File

@ -588,29 +588,6 @@ export default function UserFolderContent({ folderId }: { folderId: number }) {
{/* Invalid file message */}
{/* Add a visual overlay when dragging files */}
{isDraggingOver && (
<div className="fixed inset-0 bg-neutral-950/10 backdrop-blur-sm z-50 pointer-events-none flex items-center justify-center transition-all duration-200 ease-in-out">
<div className="bg-white dark:bg-neutral-900 rounded-lg p-8 shadow-lg text-center border border-neutral-200 dark:border-neutral-800 max-w-md mx-auto">
<div className="bg-neutral-100 dark:bg-neutral-800 p-4 rounded-full w-20 h-20 mx-auto mb-5 flex items-center justify-center">
<Upload
className="w-10 h-10 text-neutral-600 dark:text-neutral-300"
strokeWidth={1.5}
/>
</div>
<h3 className="text-xl font-medium mb-2 text-neutral-900 dark:text-neutral-50">
Drop files to upload
</h3>
<p className="text-neutral-500 dark:text-neutral-400 text-sm">
Files will be uploaded to{" "}
<span className="font-medium text-neutral-900 dark:text-neutral-200">
{folderDetails?.name || "this folder"}
</span>
</p>
</div>
</div>
)}
<DeleteEntityModal
isOpen={isDeleteModalOpen}
onClose={() => setIsDeleteModalOpen(false)}
@ -634,9 +611,9 @@ export default function UserFolderContent({ folderId }: { folderId: number }) {
<div className="flex -mt-[1px] flex-col w-full">
<div className="flex items-center mb-3">
<nav className="flex text-lg gap-x-1 items-center">
<nav className="flex text-base md:text-lg gap-x-1 items-center">
<span
className="font-medium leading-tight tracking-tight text-lg text-neutral-800 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-100 cursor-pointer flex items-center text-base"
className="font-medium leading-tight tracking-tight text-neutral-800 dark:text-neutral-300 hover:text-neutral-900 dark:hover:text-neutral-100 cursor-pointer flex items-center"
onClick={handleBack}
>
My Documents

View File

@ -255,7 +255,7 @@ export const FileListItem: React.FC<FileListItemProps> = ({
<PopoverTrigger asChild>
<Button
variant="ghost"
className="group-hover:visible invisible h-8 w-8 p-0"
className="group-hover:visible mobile:visible invisible h-8 w-8 p-0"
>
<MoreHorizontal className="h-4 w-4" />
</Button>

View File

@ -104,7 +104,7 @@ const DraggableItem: React.FC<{
<div className="w-6 flex items-center justify-center shrink-0">
<div
className={`${
isSelected ? "" : "opacity-0 group-hover:opacity-100"
isSelected ? "" : "desktop:opacity-0 group-hover:opacity-100"
} transition-opacity duration-150`}
onClick={(e) => {
e.stopPropagation();
@ -199,7 +199,7 @@ const FilePickerFolderItem: React.FC<{
className={`transition-opacity duration-150 ${
isSelected || allFilesSelected
? "opacity-100"
: "opacity-0 group-hover:opacity-100"
: "desktop:opacity-0 group-hover:opacity-100"
}`}
onClick={(e) => {
e.preventDefault();
@ -276,7 +276,10 @@ const FilePickerFolderItem: React.FC<{
export interface FilePickerModalProps {
isOpen: boolean;
onClose: () => void;
onSave: () => void;
onSave: (
selectedFiles: FileResponse[],
selectedFolders: FolderResponse[]
) => void;
buttonContent: string;
setPresentingDocument: (onyxDocument: MinimalOnyxDocument) => void;
}
@ -323,8 +326,6 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
createFileFromLink,
} = useDocumentsContext();
const router = useRouter();
const [linkUrl, setLinkUrl] = useState("");
const [isCreatingFileFromLink, setIsCreatingFileFromLink] = useState(false);
const [isUploadingFile, setIsUploadingFile] = useState(false);
@ -395,12 +396,6 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
}
}, [isOpen, selectedFiles, selectedFolders]);
useEffect(() => {
if (isOpen) {
refreshFolders();
}
}, [isOpen, refreshFolders]);
useEffect(() => {
if (currentFolder) {
if (currentFolder === -1) {
@ -1087,7 +1082,7 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
}
>
<div className="h-[calc(70vh-5rem)] flex overflow-visible flex-col">
<div className="grid overflow-x-visible h-full overflow-y-hidden flex-1 w-full divide-x divide-neutral-200 dark:divide-neutral-700 grid-cols-2">
<div className="grid overflow-x-visible h-full overflow-y-hidden flex-1 w-full divide-x divide-neutral-200 dark:divide-neutral-700 desktop:grid-cols-2">
<div className="w-full h-full pb-4 overflow-hidden ">
<div className="px-6 sticky flex flex-col gap-y-2 z-[1000] top-0 mb-2 flex gap-x-2 w-full pr-4">
<div className="w-full relative">
@ -1251,16 +1246,16 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
) : folders.length > 0 ? (
<div className="flex-grow overflow-y-auto px-4">
<p className="text-text-subtle dark:text-neutral-400">
No groups found
No folders found
</p>
</div>
) : (
<div className="flex-grow flex-col overflow-y-auto px-4 flex items-start justify-start gap-y-2">
<p className="text-sm text-muted-foreground dark:text-neutral-400">
No groups found
No folders found
</p>
<a
href="/chat/my-documents"
href="/chat/my-documents?createFolder=true"
className="inline-flex items-center text-sm justify-center text-neutral-600 dark:text-neutral-400 hover:underline"
>
<FolderIcon className="mr-2 h-4 w-4" />
@ -1270,14 +1265,20 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
)}
</div>
<div
className={`w-full h-full flex flex-col ${
className={`mobile:hidden overflow-y-auto w-full h-full flex flex-col ${
isHoveringRight ? "bg-neutral-100 dark:bg-neutral-800/30" : ""
}`}
onDragEnter={() => setIsHoveringRight(true)}
onDragLeave={() => setIsHoveringRight(false)}
>
<div className="px-5 pb-5 flex-1 flex flex-col">
<div className="shrink default-scrollbar flex h-full overflow-y-auto mb-3">
<div className="px-5 h-full flex flex-col">
{/* Top section: scrollable, takes remaining space */}
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">
Selected Items
</h3>
</div>
<div className="flex-1 min-h-0 overflow-y-auto">
<SelectedItemsList
uploadingFiles={uploadingFiles}
setPresentingDocument={setPresentingDocument}
@ -1288,69 +1289,68 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
/>
</div>
<div className="flex flex-col space-y-3">
<div className="flex flex-col space-y-2">
<FileUploadSection
disabled={isUploadingFile || isCreatingFileFromLink}
onUpload={(files: File[]) => {
setIsUploadingFile(true);
setUploadStartTime(Date.now()); // Record start time
{/* Bottom section: fixed height, doesn't flex */}
<div className="flex-none py-2">
<FileUploadSection
disabled={isUploadingFile || isCreatingFileFromLink}
onUpload={(files: File[]) => {
setIsUploadingFile(true);
setUploadStartTime(Date.now()); // Record start time
// Add files to uploading files state
// Start the refresh interval to simulate progress
startRefreshInterval();
// Start the refresh interval to simulate progress
startRefreshInterval();
// Convert File[] to FileList for addUploadedFileToContext
const fileListArray = Array.from(files);
const fileList = new DataTransfer();
fileListArray.forEach((file) => fileList.items.add(file));
// Convert File[] to FileList for addUploadedFileToContext
const fileListArray = Array.from(files);
const fileList = new DataTransfer();
fileListArray.forEach((file) => fileList.items.add(file));
addUploadedFileToContext(fileList.files)
.then(() => refreshFolders())
.finally(() => {
setIsUploadingFile(false);
});
}}
onUrlUpload={async (url: string) => {
setIsCreatingFileFromLink(true);
setUploadStartTime(Date.now()); // Record start time
addUploadedFileToContext(fileList.files)
.then(() => refreshFolders())
.finally(() => {
setIsUploadingFile(false);
});
}}
onUrlUpload={async (url: string) => {
setIsCreatingFileFromLink(true);
setUploadStartTime(Date.now()); // Record start time
// Add URL to uploading files
setUploadingFiles((prev) => [
...prev,
{ name: url, progress: 0 },
]);
// Add URL to uploading files
setUploadingFiles((prev) => [
...prev,
{ name: url, progress: 0 },
]);
// Start the refresh interval to simulate progress
startRefreshInterval();
// Start the refresh interval to simulate progress
startRefreshInterval();
try {
const response: FileResponse[] = await createFileFromLink(
url,
-1
);
try {
const response: FileResponse[] =
await createFileFromLink(url, -1);
if (response.length > 0) {
// Extract domain from URL to help with detection
const urlObj = new URL(url);
if (response.length > 0) {
// Extract domain from URL to help with detection
const urlObj = new URL(url);
const createdFile: FileResponse = response[0];
addSelectedFile(createdFile);
// Make sure to remove the uploading file indicator when done
markFileComplete(url);
}
await refreshFolders();
} catch (e) {
console.error("Error creating file from link:", e);
// Also remove the uploading indicator on error
const createdFile: FileResponse = response[0];
addSelectedFile(createdFile);
// Make sure to remove the uploading file indicator when done
markFileComplete(url);
} finally {
setIsCreatingFileFromLink(false);
}
}}
isUploading={isUploadingFile || isCreatingFileFromLink}
/>
</div>
await refreshFolders();
} catch (e) {
console.error("Error creating file from link:", e);
// Also remove the uploading indicator on error
markFileComplete(url);
} finally {
setIsCreatingFileFromLink(false);
}
}}
isUploading={isUploadingFile || isCreatingFileFromLink}
/>
</div>
</div>
</div>
@ -1375,7 +1375,10 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
<TooltipTrigger asChild>
<div>
<Button
onClick={onSave}
type="button"
onClick={() =>
onSave(selectedItems.files, selectedItems.folders)
}
className="px-8 py-2 w-48"
disabled={
isUploadingFile ||

View File

@ -26,7 +26,8 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
onRemoveFolder,
setPresentingDocument,
}) => {
const hasItems = folders.length > 0 || files.length > 0;
const hasItems =
folders.length > 0 || files.length > 0 || uploadingFiles.length > 0;
const openFile = (file: FileResponse) => {
if (file.link_url) {
window.open(file.link_url, "_blank");
@ -40,89 +41,143 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
return (
<div className="h-full w-full flex flex-col">
<div className="flex items-center justify-between mb-3">
<h3 className="text-sm font-semibold text-neutral-800 dark:text-neutral-100">
Selected Items
</h3>
</div>
<ScrollArea className="h-[200px] flex-grow pr-1">
<div className="space-y-2.5">
{folders.length > 0 && (
<div className="space-y-2.5">
{folders.map((folder: FolderResponse) => (
<div key={folder.id} className="group flex items-center gap-2">
<div
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-100/80 border-neutral-200 hover:bg-neutral-200/60",
"dark:bg-neutral-800/80 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150"
)}
>
<div className="flex items-center min-w-0 flex-1">
<FolderIcon className="h-5 w-5 mr-2 text-black dark:text-black shrink-0 fill-black dark:fill-black" />
<span className="text-sm font-medium truncate text-neutral-800 dark:text-neutral-100">
{truncateString(folder.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveFolder(folder)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove folder ${folder.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
))}
</div>
)}
{files.length > 0 && (
<div className="space-y-2.5 ">
{files.map((file: FileResponse) => (
<div className="space-y-2.5 pb-2">
{folders.length > 0 && (
<div className="space-y-2.5">
{folders.map((folder: FolderResponse) => (
<div key={folder.id} className="group flex items-center gap-2">
<div
key={file.id}
className="group w-full flex items-center gap-2"
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-100/80 border-neutral-200 hover:bg-neutral-200/60",
"dark:bg-neutral-800/80 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150"
)}
>
<div
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150",
"cursor-pointer"
)}
onClick={() => openFile(file)}
>
<div className="flex items-center min-w-0 flex-1">
{getFileIconFromFileNameAndLink(file.name, file.link_url)}
<span className="text-sm truncate text-neutral-700 dark:text-neutral-200 ml-2.5">
{truncateString(file.name, 34)}
<div className="flex items-center min-w-0 flex-1">
<FolderIcon className="h-5 w-5 mr-2 text-black dark:text-black shrink-0 fill-black dark:fill-black" />
<span className="text-sm font-medium truncate text-neutral-800 dark:text-neutral-100">
{truncateString(folder.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveFolder(folder)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove folder ${folder.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
))}
</div>
)}
{files.length > 0 && (
<div className="space-y-2.5 ">
{files.map((file: FileResponse) => (
<div
key={file.id}
className="group w-full flex items-center gap-2"
>
<div
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150",
"cursor-pointer"
)}
onClick={() => openFile(file)}
>
<div className="flex items-center min-w-0 flex-1">
{getFileIconFromFileNameAndLink(file.name, file.link_url)}
<span className="text-sm truncate text-neutral-700 dark:text-neutral-200 ml-2.5">
{truncateString(file.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveFile(file)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove file ${file.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
))}
</div>
)}
<div className="max-w-full space-y-2.5">
{uploadingFiles
.filter(
(uploadingFile) =>
!files.map((file) => file.name).includes(uploadingFile.name)
)
.map((uploadingFile, index) => (
<div key={index} className="mr-8 flex items-center gap-2">
<div
key={`uploading-${index}`}
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150",
"cursor-pointer"
)}
>
<div className="flex items-center min-w-0 flex-1">
<div className="flex items-center gap-2 min-w-0">
{uploadingFile.name.startsWith("http") ? (
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
) : (
<CircularProgress
progress={uploadingFile.progress}
size={18}
showPercentage={false}
/>
)}
<span className="truncate text-sm text-text-dark dark:text-text-dark">
{uploadingFile.name.startsWith("http")
? `${uploadingFile.name.substring(0, 30)}${
uploadingFile.name.length > 30 ? "..." : ""
}`
: truncateString(uploadingFile.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
onClick={() => onRemoveFile(file)}
// onClick={() => onRemoveFile(file)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
@ -132,82 +187,20 @@ export const SelectedItemsList: React.FC<SelectedItemsListProps> = ({
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
aria-label={`Remove file ${file.name}`}
// aria-label={`Remove file ${file.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
))}
</div>
)}
<div className="max-w-full space-y-2.5">
{uploadingFiles
.filter(
(uploadingFile) =>
!files.map((file) => file.name).includes(uploadingFile.name)
)
.map((uploadingFile, index) => (
<div key={index} className="mr-8 flex items-center gap-2">
<div
key={`uploading-${index}`}
className={cn(
"group flex-1 flex items-center rounded-md border p-2.5",
"bg-neutral-50 border-neutral-200 hover:bg-neutral-100",
"dark:bg-neutral-800/70 dark:border-neutral-700 dark:hover:bg-neutral-750",
"dark:focus:ring-1 dark:focus:ring-neutral-500 dark:focus:border-neutral-600",
"dark:active:bg-neutral-700 dark:active:border-neutral-600",
"transition-colors duration-150",
"cursor-pointer"
)}
>
<div className="flex items-center min-w-0 flex-1">
<div className="flex items-center gap-2 min-w-0">
{uploadingFile.name.startsWith("http") ? (
<Loader2 className="w-4 h-4 animate-spin text-blue-500" />
) : (
<CircularProgress
progress={uploadingFile.progress}
size={18}
showPercentage={false}
/>
)}
<span className="truncate text-sm text-text-dark dark:text-text-dark">
{uploadingFile.name.startsWith("http")
? `${uploadingFile.name.substring(0, 30)}${
uploadingFile.name.length > 30 ? "..." : ""
}`
: truncateString(uploadingFile.name, 34)}
</span>
</div>
</div>
<Button
variant="ghost"
size="sm"
// onClick={() => onRemoveFile(file)}
className={cn(
"bg-transparent hover:bg-transparent opacity-0 group-hover:opacity-100",
"h-6 w-6 p-0 rounded-full shrink-0",
"hover:text-neutral-700",
"dark:text-neutral-300 dark:hover:text-neutral-100",
"dark:focus:ring-1 dark:focus:ring-neutral-500",
"dark:active:bg-neutral-500 dark:active:text-white",
"transition-all duration-150 ease-in-out"
)}
// aria-label={`Remove file ${file.name}`}
>
<X className="h-3 w-3 dark:text-neutral-200" />
</Button>
</div>
</div>
))}
</div>
{!hasItems && (
<div className="flex items-center justify-center h-24 text-sm text-neutral-500 dark:text-neutral-400 italic bg-neutral-50/50 dark:bg-neutral-800/30 rounded-md border border-neutral-200/50 dark:border-neutral-700/50">
No items selected
</div>
)}
</div>
))}
</div>
</ScrollArea>
{!hasItems && (
<div className="flex items-center justify-center h-24 text-sm text-neutral-500 dark:text-neutral-400 italic bg-neutral-50/50 dark:bg-neutral-800/30 rounded-md border border-neutral-200/50 dark:border-neutral-700/50">
No items selected
</div>
)}
</div>
</div>
);
};

View File

@ -26,9 +26,7 @@ interface SharedFolderItemProps {
onClick: (folderId: number) => void;
description?: string;
lastUpdated?: string;
onRename: () => void;
onDelete: () => void;
onMove: () => void;
}
export const SharedFolderItem: React.FC<SharedFolderItemProps> = ({
@ -36,9 +34,7 @@ export const SharedFolderItem: React.FC<SharedFolderItemProps> = ({
onClick,
description,
lastUpdated,
onRename,
onDelete,
onMove,
}) => {
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
@ -99,7 +95,7 @@ export const SharedFolderItem: React.FC<SharedFolderItemProps> = ({
<PopoverTrigger asChild>
<Button
variant="ghost"
className={`group-hover:visible invisible h-8 w-8 p-0 ${
className={`group-hover:visible mobile:visible invisible h-8 w-8 p-0 ${
folder.id === -1 ? "!invisible pointer-events-none" : ""
}`}
>
@ -108,14 +104,6 @@ export const SharedFolderItem: React.FC<SharedFolderItemProps> = ({
</PopoverTrigger>
<PopoverContent className="!p-0 w-40">
<div className="space-y-0">
{/* <Button variant="menu" onClick={onMove}>
<FiArrowDown className="h-4 w-4" />
Move
</Button>
<Button variant="menu" onClick={onRename}>
<FiEdit className="h-4 w-4" />
Rename
</Button> */}
<Button variant="menu" onClick={handleDeleteClick}>
<FiTrash className="h-4 w-4" />
Delete

View File

@ -23,21 +23,21 @@ import { Modal } from "@/components/Modal";
import FunctionalHeader from "@/components/chat/Header";
import FixedLogo from "@/components/logo/FixedLogo";
import { useRouter } from "next/navigation";
import Link from "next/link";
function BackToOnyxButton({
documentSidebarVisible,
}: {
documentSidebarVisible: boolean;
}) {
const router = useRouter();
const enterpriseSettings = useContext(SettingsContext)?.enterpriseSettings;
return (
<div className="absolute bottom-0 bg-background w-full flex border-t border-border py-4">
<div className="mx-auto">
<Button onClick={() => router.push("/chat")}>
<Link href="/chat">
Back to {enterpriseSettings?.application_name || "Onyx Chat"}
</Button>
</Link>
</div>
<div
style={{ transition: "width 0.30s ease-out" }}

View File

@ -124,10 +124,16 @@ const StandardAnswersTableRow = ({
? `\`${standardAnswer.keyword}\``
: standardAnswer.keyword}
</ReactMarkdown>,
<CustomCheckbox
<div
key={`match_regex-${standardAnswer.id}`}
checked={standardAnswer.match_regex}
/>,
className="flex items-center"
>
{standardAnswer.match_regex ? (
<span className="text-green-500 font-medium">Yes</span>
) : (
<span className="text-gray-500">No</span>
)}
</div>,
<ReactMarkdown
key={`answer-${standardAnswer.id}`}
className="prose dark:prose-invert"
@ -290,8 +296,8 @@ const StandardAnswersTable = ({
))}
</div>
</div>
<div className="mx-auto">
<Table className="w-full flex items-stretch">
<div className="flex flex-col w-full mx-auto">
<Table className="w-full">
<TableHeader>
<TableRow>
{columns.map((column) => (
@ -314,11 +320,13 @@ const StandardAnswersTable = ({
)}
</TableBody>
</Table>
{paginatedStandardAnswers.length === 0 && (
<div className="flex justify-center">
<Text>No matching standard answers found...</Text>
</div>
)}
<div>
{paginatedStandardAnswers.length === 0 && (
<div className="flex justify-center">
<Text>No matching standard answers found...</Text>
</div>
)}
</div>
{paginatedStandardAnswers.length > 0 && (
<>
<div className="mt-4">

View File

@ -144,51 +144,52 @@ export function WhitelabelingForm() {
placeholder="Custom name which will replace 'Onyx'"
disabled={isSubmitting}
/>
<div>
<Label className="mt-4">Custom Logo</Label>
<Label className="mt-4">Custom Logo</Label>
{values.use_custom_logo ? (
<div className="mt-3">
<SubLabel>Current Custom Logo: </SubLabel>
<img
src={"/api/enterprise-settings/logo?u=" + Date.now()}
alt="logo"
style={{ objectFit: "contain" }}
className="w-32 h-32 mb-10 mt-4"
/>
{values.use_custom_logo ? (
<div className="mt-3">
<SubLabel>Current Custom Logo: </SubLabel>
<img
src={"/api/enterprise-settings/logo?u=" + Date.now()}
alt="logo"
style={{ objectFit: "contain" }}
className="w-32 h-32 mb-10 mt-4"
/>
<Button
variant="destructive"
size="sm"
type="button"
className="mb-8"
onClick={async () => {
const valuesWithoutLogo = {
...values,
use_custom_logo: false,
};
await updateEnterpriseSettings(valuesWithoutLogo);
setValues(valuesWithoutLogo);
}}
>
Delete
</Button>
<Button
variant="destructive"
size="sm"
type="button"
className="mb-8"
onClick={async () => {
const valuesWithoutLogo = {
...values,
use_custom_logo: false,
};
await updateEnterpriseSettings(valuesWithoutLogo);
setValues(valuesWithoutLogo);
}}
>
Delete
</Button>
<SubLabel>
Override the current custom logo by uploading a new image
below and clicking the Update button.
</SubLabel>
</div>
) : (
<SubLabel>
Override the current custom logo by uploading a new image
below and clicking the Update button.
Specify your own logo to replace the standard Onyx logo.
</SubLabel>
</div>
) : (
<SubLabel>
Specify your own logo to replace the standard Onyx logo.
</SubLabel>
)}
)}
<ImageUpload
selectedFile={selectedLogo}
setSelectedFile={setSelectedLogo}
/>
<ImageUpload
selectedFile={selectedLogo}
setSelectedFile={setSelectedLogo}
/>
</div>
<Separator />

View File

@ -674,3 +674,7 @@ ul > li > p {
.animate-fadeIn {
animation: fadeIn 0.2s ease-out forwards;
}
.container {
margin-bottom: 1rem;
}

View File

@ -115,17 +115,17 @@ export const ConnectorMultiSelect = ({
<div className="flex flex-col w-full space-y-2 mb-4">
{label && <Label className="text-base font-medium">{label}</Label>}
<p className="text-xs text-neutral-500 ">
<p className="text-xs text-neutral-500 dark:text-neutral-400">
All documents indexed by the selected connectors will be part of this
document set.
</p>
<div className="relative">
<div
className={`flex items-center border border-input rounded-md border border-neutral-200 ${
allConnectorsSelected ? "bg-neutral-50" : ""
} focus-within:ring-1 focus-within:ring-ring focus-within:border-neutral-400 transition-colors`}
className={`flex items-center border border-input rounded-md border-neutral-200 dark:border-neutral-700 ${
allConnectorsSelected ? "bg-neutral-50 dark:bg-neutral-800" : ""
} focus-within:ring-1 focus-within:ring-ring focus-within:border-neutral-400 dark:focus-within:border-neutral-500 transition-colors`}
>
<Search className="absolute left-3 h-4 w-4 text-neutral-500" />
<Search className="absolute left-3 h-4 w-4 text-neutral-500 dark:text-neutral-400" />
<input
ref={inputRef}
type="text"
@ -141,8 +141,10 @@ export const ConnectorMultiSelect = ({
}}
onKeyDown={handleKeyDown}
placeholder={effectivePlaceholder}
className={`h-9 w-full pl-9 pr-10 py-2 bg-transparent text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
allConnectorsSelected ? "text-neutral-500" : ""
className={`h-9 w-full pl-9 pr-10 py-2 bg-transparent dark:bg-transparent text-sm outline-none disabled:cursor-not-allowed disabled:opacity-50 ${
allConnectorsSelected
? "text-neutral-500 dark:text-neutral-400"
: ""
}`}
disabled={isInputDisabled}
/>
@ -151,10 +153,10 @@ export const ConnectorMultiSelect = ({
{open && !allConnectorsSelected && (
<div
ref={dropdownRef}
className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 bg-white shadow-md default-scrollbar max-h-[300px] overflow-auto"
className="absolute z-50 w-full mt-1 rounded-md border border-neutral-200 dark:border-neutral-700 bg-white dark:bg-neutral-900 shadow-md default-scrollbar max-h-[300px] overflow-auto"
>
{filteredUnselectedConnectors.length === 0 ? (
<div className="py-4 text-center text-xs text-neutral-500">
<div className="py-4 text-center text-xs text-neutral-500 dark:text-neutral-400">
{searchQuery
? "No matching connectors found"
: "No more connectors available"}
@ -164,7 +166,7 @@ export const ConnectorMultiSelect = ({
{filteredUnselectedConnectors.map((connector) => (
<div
key={connector.cc_pair_id}
className="flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-neutral-50 text-xs"
className="flex items-center justify-between py-2 px-3 cursor-pointer hover:bg-neutral-50 dark:hover:bg-neutral-800 text-xs"
onClick={() => selectConnector(connector.cc_pair_id)}
>
<div className="flex items-center truncate mr-2">
@ -185,12 +187,12 @@ export const ConnectorMultiSelect = ({
</div>
{selectedConnectors.length > 0 ? (
<div className="mt-3 ">
<div className="mt-3">
<div className="flex flex-wrap gap-1.5">
{selectedConnectors.map((connector) => (
<div
key={connector.cc_pair_id}
className="flex items-center bg-white rounded-md border border-neutral-300 transition-all px-2 py-1 max-w-full group text-xs"
className="flex items-center bg-white dark:bg-neutral-800 rounded-md border border-neutral-300 dark:border-neutral-700 transition-all px-2 py-1 max-w-full group text-xs"
>
<div className="flex items-center overflow-hidden">
<div className="flex-shrink-0 text-xs">
@ -204,7 +206,7 @@ export const ConnectorMultiSelect = ({
</div>
</div>
<button
className="ml-1 flex-shrink-0 rounded-full w-4 h-4 flex items-center justify-center bg-neutral-100 text-neutral-500 hover:bg-neutral-200 hover:text-neutral-700 transition-colors group-hover:bg-neutral-200"
className="ml-1 flex-shrink-0 rounded-full w-4 h-4 flex items-center justify-center bg-neutral-100 dark:bg-neutral-700 text-neutral-500 dark:text-neutral-400 hover:bg-neutral-200 dark:hover:bg-neutral-600 hover:text-neutral-700 dark:hover:text-neutral-300 transition-colors group-hover:bg-neutral-200 dark:group-hover:bg-neutral-600"
onClick={() => removeConnector(connector.cc_pair_id)}
aria-label="Remove connector"
>
@ -215,7 +217,7 @@ export const ConnectorMultiSelect = ({
</div>
</div>
) : (
<div className="mt-3 p-3 border border-dashed border-neutral-300 rounded-md bg-neutral-50 text-neutral-500 text-xs">
<div className="mt-3 p-3 border border-dashed border-neutral-300 dark:border-neutral-700 rounded-md bg-neutral-50 dark:bg-neutral-800 text-neutral-500 dark:text-neutral-400 text-xs">
No connectors selected. Search and select connectors above.
</div>
)}
@ -224,7 +226,7 @@ export const ConnectorMultiSelect = ({
<ErrorMessage
name={name}
component="div"
className="text-red-500 text-xs mt-1"
className="text-red-500 dark:text-red-400 text-xs mt-1"
/>
)}
</div>

View File

@ -92,8 +92,9 @@ export function Modal({
${className || ""}
flex
flex-col
${heightOverride ? `h-${heightOverride}` : "max-h-[90vh]"}
${hideOverflow ? "overflow-hidden" : "overflow-auto"}
${hideOverflow ? "overflow-hidden" : "overflow-visible"}
`}
>
{onOutsideClick && !hideCloseButton && (

View File

@ -69,7 +69,7 @@ const MultiSelectDropdown = ({
};
return (
<div className="flex flex-col space-y-4 mb-4">
<div className="flex flex-col text-white space-y-4 mb-4">
<Label>{label}</Label>
{creatable ? (
<CreatableSelect

View File

@ -27,9 +27,9 @@ export function TokenDisplay({
<Tooltip>
<TooltipTrigger asChild>
<div className="flex items-center space-x-3 bg-neutral-100 dark:bg-neutral-800 rounded-full px-4 py-1.5">
<div className="relative w-36 h-2 bg-neutral-200 dark:bg-neutral-700 rounded-full overflow-hidden">
<div className="hidden sm:block relative w-24 h-2 bg-neutral-200 dark:bg-neutral-700 rounded-full overflow-hidden">
<div
className={`absolute top-0 left-0 h-full rounded-full ${
className={` absolute top-0 left-0 h-full rounded-full ${
tokenPercentage >= 100
? "bg-yellow-500 dark:bg-yellow-600"
: "bg-green-500 dark:bg-green-600"

View File

@ -379,18 +379,24 @@ export function ClientLayout({
},
]
: []),
{
name: (
<div className="flex">
<FiBarChart2
className="text-text-700"
size={18}
/>
<div className="ml-1">Custom Analytics</div>
</div>
),
link: "/admin/performance/custom-analytics",
},
...(!enableCloud
? [
{
name: (
<div className="flex">
<FiBarChart2
className="text-text-700"
size={18}
/>
<div className="ml-1">
Custom Analytics
</div>
</div>
),
link: "/admin/performance/custom-analytics",
},
]
: []),
],
},
]
@ -453,7 +459,7 @@ export function ClientLayout({
<div className="fixed left-0 gap-x-4 px-4 top-4 h-8 px-0 mb-auto w-full items-start flex justify-end">
<UserDropdown toggleUserSettings={toggleUserSettings} />
</div>
<div className="pt-20 flex w-full overflow-y-auto overflow-x-hidden h-full px-4 md:px-12">
<div className="pt-20 pb-4 flex w-full overflow-y-auto overflow-x-hidden h-full px-4 md:px-12">
{children}
</div>
</div>

View File

@ -201,7 +201,6 @@ export function TextFormField({
maxWidth,
removeLabel,
min,
includeForgotPassword,
onChange,
width,
vertical,
@ -229,7 +228,6 @@ export function TextFormField({
explanationLink?: string;
small?: boolean;
min?: number;
includeForgotPassword?: boolean;
onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void;
width?: string;
vertical?: boolean;
@ -339,14 +337,6 @@ export function TextFormField({
placeholder={placeholder}
autoComplete={autoCompleteDisabled ? "off" : undefined}
/>
{includeForgotPassword && (
<Link
href="/auth/forgot-password"
className="absolute right-3 top-1/2 mt-[3px] transform -translate-y-1/2 text-xs text-blue-500 cursor-pointer"
>
Forgot password?
</Link>
)}
</div>
{explanationText && (

View File

@ -16,7 +16,6 @@ interface TextViewProps {
presentingDocument: MinimalOnyxDocument;
onClose: () => void;
}
export default function TextView({
presentingDocument,
onClose,
@ -27,6 +26,13 @@ export default function TextView({
const [fileName, setFileName] = useState("");
const [isLoading, setIsLoading] = useState(true);
const [fileType, setFileType] = useState("application/octet-stream");
const [renderCount, setRenderCount] = useState(0);
// Log render count on each render
useEffect(() => {
setRenderCount((prevCount) => prevCount + 1);
console.log(`TextView component rendered ${renderCount + 1} times`);
}, []);
// Detect if a given MIME type is one of the recognized markdown formats
const isMarkdownFormat = (mimeType: string): boolean => {
@ -63,6 +69,7 @@ export default function TextView({
};
const fetchFile = useCallback(async () => {
console.log("fetching file");
setIsLoading(true);
const fileId =
presentingDocument.document_id.split("__")[1] ||
@ -107,13 +114,14 @@ export default function TextView({
// Keep the slight delay for a smoother loading experience
setTimeout(() => {
setIsLoading(false);
console.log("finished loading");
}, 1000);
}
}, [presentingDocument]);
useEffect(() => {
fetchFile();
}, [fetchFile]);
}, []);
const handleDownload = () => {
const link = document.createElement("a");

View File

@ -327,7 +327,7 @@ export function HorizontalFilters({
);
return (
<div>
<div className="b">
<div className="flex gap-x-3">
<div className="w-52">
<DateRangeSelector value={timeRange} onValueChange={setTimeRange} />
@ -387,7 +387,7 @@ export function HorizontalFilters({
)}
</div>
<div className="flex pb-4 mt-2 h-12">
<div className="flex mt-2">
<div className="flex flex-wrap gap-x-2">
{timeRange && timeRange.selectValue && (
<SelectedBubble onClick={() => setTimeRange(null)}>

View File

@ -1303,7 +1303,7 @@ export const ProductboardIcon = ({
size = 16,
className = defaultTailwindCSS,
}: IconProps) => (
<LogoIcon size={size} className={className} src="/Productboard.webp" />
<LogoIcon size={size} className={className} src="/Productboard.png" />
);
export const AzureIcon = ({

View File

@ -45,7 +45,7 @@ export default function CreateEntityModal({
return (
<Dialog open={open} onOpenChange={setOpen}>
<DialogTrigger asChild>{trigger}</DialogTrigger>
<DialogContent className="sm:max-w-[425px]">
<DialogContent className="max-w-[95%] sm:max-w-[425px]">
<DialogHeader>
<DialogTitle>{title}</DialogTitle>
</DialogHeader>

View File

@ -437,19 +437,20 @@ export function CompactDocumentCard({
url?: string;
updatePresentingDocument: (document: OnyxDocument) => void;
}) {
console.log("document", document);
return (
<div
onClick={() => {
openDocument(document, updatePresentingDocument);
}}
className="max-w-[200px] gap-y-0 cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0 "
className="max-w-[250px] gap-y-1 cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0 "
>
<div className="text-sm !pb-0 !mb-0 font-semibold flex items-center gap-x-1 text-text-900 pt-0 mt-0 truncate w-full">
<div className="text-sm flex gap-x-2 !pb-0 !mb-0 font-semibold flex items-center gap-x-1 text-text-900 pt-0 mt-0 w-full">
{icon}
{(document.semantic_identifier || document.document_id).slice(0, 40)}
{(document.semantic_identifier || document.document_id).length > 40 &&
"..."}
<p className="gap-0 p-0 m-0 line-clamp-2">
{(document.semantic_identifier || document.document_id).slice(0, 40)}
{(document.semantic_identifier || document.document_id).length > 40 &&
"..."}
</p>
</div>
{document.blurb && (
<div className="text-xs mb-0 text-neutral-600 dark:text-neutral-300 line-clamp-2">
@ -479,7 +480,7 @@ export function CompactQuestionCard({
return (
<div
onClick={() => openQuestion(question)}
className="max-w-[250px] gap-y-0 cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0"
className="max-w-[350px] gap-y-1 cursor-pointer pb-0 pt-0 mt-0 flex gap-y-0 flex-col content-start items-start gap-0"
>
<div className="text-sm !pb-0 !mb-0 font-semibold flex items-center gap-x-1 text-text-900 pt-0 mt-0 truncate w-full">
Question

View File

@ -9,6 +9,7 @@ import {
} from "@/components/ui/tooltip";
import { openDocument } from "@/lib/search/utils";
import { SubQuestionDetail } from "@/app/chat/interfaces";
import { getFileIconFromFileNameAndLink } from "@/lib/assistantIconUtils";
export interface DocumentCardProps {
document: LoadedOnyxDocument;
@ -39,6 +40,13 @@ export function Citation({
if (!document_info && !question_info) {
return <>{children}</>;
}
const icon = document_info?.document
? getFileIconFromFileNameAndLink(
document_info.document.semantic_identifier || "",
document_info.document.link || ""
)
: null;
return (
<TooltipProvider delayDuration={0}>
<Tooltip>
@ -72,7 +80,7 @@ export function Citation({
<CompactDocumentCard
updatePresentingDocument={document_info.updatePresentingDocument}
url={document_info.url}
icon={document_info.icon}
icon={icon}
document={document_info.document}
/>
) : (

View File

@ -112,7 +112,24 @@ export async function fetchChatData(searchParams: {
? `${fullUrl}?${searchParamsString}`
: fullUrl;
if (!NEXT_PUBLIC_ENABLE_CHROME_EXTENSION) {
// Check the referrer to prevent redirect loops
const referrer = headersList.get("referer") || "";
const isComingFromLogin = referrer.includes("/auth/login");
// Also check for the from=login query parameter
const isRedirectedFromLogin = searchParams["from"] === "login";
console.log(
`Auth check: authDisabled=${authDisabled}, user=${!!user}, referrer=${referrer}, fromLogin=${isRedirectedFromLogin}`
);
// Only redirect if we're not already coming from the login page
if (
!NEXT_PUBLIC_ENABLE_CHROME_EXTENSION &&
!isComingFromLogin &&
!isRedirectedFromLogin
) {
console.log("Redirecting to login from chat page");
return {
redirect: `/auth/login?next=${encodeURIComponent(redirectUrl)}`,
};

View File

@ -77,7 +77,8 @@ export const SERVER_SIDE_ONLY__CLOUD_ENABLED =
process.env.NEXT_PUBLIC_CLOUD_ENABLED?.toLowerCase() === "true";
export const NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED =
process.env.NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED?.toLowerCase() === "true";
process.env.NEXT_PUBLIC_FORGOT_PASSWORD_ENABLED?.toLowerCase() === "true" &&
!NEXT_PUBLIC_CLOUD_ENABLED;
export const NEXT_PUBLIC_TEST_ENV =
process.env.NEXT_PUBLIC_TEST_ENV?.toLowerCase() === "true";

View File

@ -130,12 +130,10 @@ export async function renameItem(
}
export async function downloadItem(documentId: string): Promise<Blob> {
const response = await fetch(
`/api/chat/file/${encodeURIComponent(documentId)}`,
{
method: "GET",
}
);
const fileId = documentId.split("__")[1] || documentId;
const response = await fetch(`/api/chat/file/${encodeURIComponent(fileId)}`, {
method: "GET",
});
if (!response.ok) {
throw new Error("Failed to fetch file");
}