1
0
forked from github/onyx

Feature/openapi (#4710)

* starting openapi support

* fix app / app_fn

* send gitignore

* dedupe function names

* add readme

* modify gitignore

* update launch template

* fix unused path param

* fix mypy

* local tests pass

* first pass at making integration tests work

* fixes

* fix script path

* set python path

* try full path

* fix output dir

* fix integration test

* more build fixes

* add generated directory

* use the config

* add a comment

* add

* modify tsconfig.json

* fix index linting bugs

* tons of lint fixes

* new gitignore

* remove generated dir

* add tasks template

* check for undefined explicitly

* fix hooks.ts

* refactor destructureValue

* improve readme

---------

Co-authored-by: Richard Kuo (Onyx) <rkuo@onyx.app>
This commit is contained in:
rkuo-danswer
2025-05-20 14:33:18 -07:00
committed by GitHub
parent 0593d045bf
commit e92c418e0f
96 changed files with 1219 additions and 481 deletions

View File

@@ -26,6 +26,39 @@ jobs:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: |
backend/requirements/default.txt
backend/requirements/dev.txt
backend/requirements/ee.txt
- run: |
python -m pip install --upgrade pip
pip install --retries 5 --timeout 30 -r backend/requirements/default.txt
pip install --retries 5 --timeout 30 -r backend/requirements/dev.txt
pip install --retries 5 --timeout 30 -r backend/requirements/ee.txt
- name: Generate OpenAPI schema
working-directory: ./backend
env:
PYTHONPATH: "."
run: |
python scripts/onyx_openapi_schema.py --filename generated/openapi.json
- name: Generate OpenAPI Python client
working-directory: ./backend
run: |
docker run --rm \
-v "${{ github.workspace }}/backend/generated:/local" \
openapitools/openapi-generator-cli generate \
-i /local/openapi.json \
-g python \
-o /local/onyx_openapi_client \
--package-name onyx_openapi_client
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -24,7 +24,38 @@ jobs:
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Python
uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: "pip"
cache-dependency-path: |
backend/requirements/default.txt
backend/requirements/dev.txt
- run: |
python -m pip install --upgrade pip
pip install --retries 5 --timeout 30 -r backend/requirements/default.txt
pip install --retries 5 --timeout 30 -r backend/requirements/dev.txt
- name: Generate OpenAPI schema
working-directory: ./backend
env:
PYTHONPATH: "."
run: |
python scripts/onyx_openapi_schema.py --filename generated/openapi.json
- name: Generate OpenAPI Python client
working-directory: ./backend
run: |
docker run --rm \
-v "${{ github.workspace }}/backend/generated:/local" \
openapitools/openapi-generator-cli generate \
-i /local/openapi.json \
-g python \
-o /local/onyx_openapi_client \
--package-name onyx_openapi_client
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

View File

@@ -31,6 +31,24 @@ jobs:
pip install --retries 5 --timeout 30 -r backend/requirements/dev.txt
pip install --retries 5 --timeout 30 -r backend/requirements/model_server.txt
- name: Generate OpenAPI schema
working-directory: ./backend
env:
PYTHONPATH: "."
run: |
python scripts/onyx_openapi_schema.py --filename generated/openapi.json
- name: Generate OpenAPI Python client
working-directory: ./backend
run: |
docker run --rm \
-v "${{ github.workspace }}/backend/generated:/local" \
openapitools/openapi-generator-cli generate \
-i /local/openapi.json \
-g python \
-o /local/onyx_openapi_client \
--package-name onyx_openapi_client
- name: Run MyPy
run: |
cd backend

View File

@@ -412,6 +412,23 @@
"group": "3"
}
},
{
// script to generate the openapi schema
"name": "Onyx OpenAPI Schema Generator",
"type": "debugpy",
"request": "launch",
"program": "scripts/onyx_openapi_schema.py",
"cwd": "${workspaceFolder}/backend",
"envFile": "${workspaceFolder}/.env",
"env": {
"PYTHONUNBUFFERED": "1",
"PYTHONPATH": "."
},
"args": [
"--filename",
"generated/openapi.json",
]
},
{
"name": "Debug React Web App in Chrome",
"type": "chrome",

101
.vscode/tasks.template.jsonc vendored Normal file
View File

@@ -0,0 +1,101 @@
{
"version": "2.0.0",
"tasks": [
{
"type": "austin",
"label": "Profile celery beat",
"envFile": "${workspaceFolder}/.env",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"command": [
"sudo",
"-E"
],
"args": [
"celery",
"-A",
"onyx.background.celery.versioned_apps.beat",
"beat",
"--loglevel=INFO"
]
},
{
"type": "shell",
"label": "Generate Onyx OpenAPI Python client",
"cwd": "${workspaceFolder}/backend",
"envFile": "${workspaceFolder}/.env",
"options": {
"cwd": "${workspaceFolder}/backend"
},
"command": [
"openapi-generator"
],
"args": [
"generate",
"-i",
"generated/openapi.json",
"-g",
"python",
"-o",
"generated/onyx_openapi_client",
"--package-name",
"onyx_openapi_client",
]
},
{
"type": "shell",
"label": "Generate Typescript Fetch client (openapi-generator)",
"envFile": "${workspaceFolder}/.env",
"options": {
"cwd": "${workspaceFolder}"
},
"command": [
"openapi-generator"
],
"args": [
"generate",
"-i",
"backend/generated/openapi.json",
"-g",
"typescript-fetch",
"-o",
"${workspaceFolder}/web/src/lib/generated/onyx_api",
"--additional-properties=disallowAdditionalPropertiesIfNotPresent=false,legacyDiscriminatorBehavior=false,supportsES6=true",
]
},
{
"type": "shell",
"label": "Generate TypeScript Client (openapi-ts)",
"envFile": "${workspaceFolder}/.env",
"options": {
"cwd": "${workspaceFolder}/web"
},
"command": [
"npx"
],
"args": [
"openapi-typescript",
"../backend/generated/openapi.json",
"--output",
"./src/lib/generated/onyx-schema.ts",
]
},
{
"type": "shell",
"label": "Generate TypeScript Client (orval)",
"envFile": "${workspaceFolder}/.env",
"options": {
"cwd": "${workspaceFolder}/web"
},
"command": [
"npx"
],
"args": [
"orval",
"--config",
"orval.config.js",
]
}
]
}

2
backend/.gitignore vendored
View File

@@ -11,4 +11,4 @@ dynamic_config_storage/
celerybeat-schedule*
onyx/connectors/salesforce/data/
.test.env
/generated

View File

@@ -51,6 +51,7 @@ from onyx.main import get_application as get_application_base
from onyx.main import include_auth_router_with_prefix
from onyx.main import include_router_with_global_prefix_prepended
from onyx.main import lifespan as lifespan_base
from onyx.main import use_route_function_names_as_operation_ids
from onyx.utils.logger import setup_logger
from onyx.utils.variable_functionality import global_version
from shared_configs.configs import MULTI_TENANT
@@ -192,4 +193,6 @@ def get_application() -> FastAPI:
# for route in application.router.routes:
# print(f"Path: {route.path}, Methods: {route.methods}")
use_route_function_names_as_operation_ids(application)
return application

View File

@@ -114,14 +114,14 @@ async def refresh_access_token(
@admin_router.put("")
def put_settings(
def admin_ee_put_settings(
settings: EnterpriseSettings, _: User | None = Depends(current_admin_user)
) -> None:
store_settings(settings)
@basic_router.get("")
def fetch_settings() -> EnterpriseSettings:
def ee_fetch_settings() -> EnterpriseSettings:
if MULTI_TENANT:
tenant_id = get_current_tenant_id()
if not tenant_id or tenant_id == POSTGRES_DEFAULT_SCHEMA:

View File

@@ -147,7 +147,7 @@ def snapshot_from_chat_session(
@router.get("/admin/chat-sessions")
def get_user_chat_sessions(
def admin_get_chat_sessions(
user_id: UUID,
_: User | None = Depends(current_admin_user),
db_session: Session = Depends(get_session),

View File

@@ -0,0 +1,2 @@
- Generated Files
* Generated files live here. This directory should be git ignored.

View File

@@ -16,6 +16,7 @@ from fastapi import status
from fastapi.exceptions import RequestValidationError
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from fastapi.routing import APIRoute
from httpx_oauth.clients.google import GoogleOAuth2
from prometheus_fastapi_instrumentator import Instrumentator
from sentry_sdk.integrations.fastapi import FastApiIntegration
@@ -157,6 +158,20 @@ def value_error_handler(_: Request, exc: Exception) -> JSONResponse:
)
def use_route_function_names_as_operation_ids(app: FastAPI) -> None:
"""
OpenAPI generation defaults to naming the operation with the
function + route + HTTP method, which usually looks very redundant.
This function changes the operation IDs to be just the function name.
Should be called only after all routes have been added.
"""
for route in app.routes:
if isinstance(route, APIRoute):
route.operation_id = route.name
def include_router_with_global_prefix_prepended(
application: FastAPI, router: APIRouter, **kwargs: Any
) -> None:
@@ -308,7 +323,6 @@ def get_application(lifespan_override: Lifespan | None = None) -> FastAPI:
include_router_with_global_prefix_prepended(application, admin_query_router)
include_router_with_global_prefix_prepended(application, admin_router)
include_router_with_global_prefix_prepended(application, connector_router)
include_router_with_global_prefix_prepended(application, user_router)
include_router_with_global_prefix_prepended(application, credential_router)
include_router_with_global_prefix_prepended(application, input_prompt_router)
include_router_with_global_prefix_prepended(application, admin_input_prompt_router)
@@ -444,6 +458,8 @@ def get_application(lifespan_override: Lifespan | None = None) -> FastAPI:
# Initialize and instrument the app
Instrumentator().instrument(application).expose(application)
use_route_function_names_as_operation_ids(application)
return application

View File

@@ -329,6 +329,7 @@ def list_runs(
@router.get("/threads/{thread_id}/runs/{run_id}/steps")
def list_run_steps(
thread_id: UUID,
run_id: str,
limit: int = 20,
order: Literal["asc", "desc"] = "desc",

View File

@@ -32,7 +32,7 @@ basic_router = APIRouter(prefix="/settings")
@admin_router.put("")
def put_settings(
def admin_put_settings(
settings: Settings, _: User | None = Depends(current_admin_user)
) -> None:
store_settings(settings)

View File

@@ -86,7 +86,7 @@ def create_folder(
@router.get(
"/user/folder",
)
def get_folders(
def user_get_folders(
user: User = Depends(current_user),
db_session: Session = Depends(get_session),
) -> list[UserFolderSnapshot]:

View File

@@ -5,6 +5,9 @@ explicit_package_bases = true
disallow_untyped_defs = true
enable_error_code = ["possibly-undefined"]
strict_equality = true
exclude = [
"^generated/",
]
[[tool.mypy.overrides]]
module = "alembic.versions.*"
@@ -14,6 +17,10 @@ disable_error_code = ["var-annotated"]
module = "alembic_tenants.versions.*"
disable_error_code = ["var-annotated"]
[[tool.mypy.overrides]]
module = "generated.*"
follow_imports = "silent"
[tool.ruff]
ignore = []
line-length = 130

View File

@@ -1,5 +1,7 @@
[pytest]
pythonpath = .
pythonpath =
.
generated/onyx_openapi_client
markers =
slow: marks tests as slow
filterwarnings =

View File

@@ -0,0 +1,44 @@
# export openapi schema without having to start the actual web server
# helpful tips: https://github.com/fastapi/fastapi/issues/1173
import argparse
import json
from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi
from onyx.main import app as app_fn
def go(filename: str) -> None:
with open(filename, "w") as f:
app: FastAPI = app_fn()
json.dump(
get_openapi(
title=app.title,
version=app.version,
openapi_version=app.openapi_version,
description=app.description,
routes=app.routes,
),
f,
)
print(f"Wrote OpenAPI schema to {filename}.")
def main() -> None:
parser = argparse.ArgumentParser(
description="Export OpenAPI schema for Onyx API (does not require starting API server)"
)
parser.add_argument(
"--filename", "-f", help="Filename to write to", default="openapi.json"
)
args = parser.parse_args()
go(args.filename)
if __name__ == "__main__":
main()

View File

@@ -3,6 +3,7 @@ from collections.abc import Generator
from typing import Any
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from onyx.main import fetch_versioned_implementation
@@ -17,7 +18,7 @@ def client() -> Generator[TestClient, Any, None]:
os.environ["ENABLE_PAID_ENTERPRISE_EDITION_FEATURES"] = "True"
# Initialize TestClient with the FastAPI app
app = fetch_versioned_implementation(
app: FastAPI = fetch_versioned_implementation(
module="onyx.main", attribute="get_application"
)()
client = TestClient(app)

View File

@@ -3,6 +3,7 @@ from collections.abc import Generator
from typing import Any
import pytest
from fastapi import FastAPI
from fastapi.testclient import TestClient
from onyx.main import fetch_versioned_implementation
@@ -17,7 +18,7 @@ def client() -> Generator[TestClient, Any, None]:
os.environ["ENABLE_PAID_ENTERPRISE_EDITION_FEATURES"] = "True"
# Initialize TestClient with the FastAPI app
app = fetch_versioned_implementation(
app: FastAPI = fetch_versioned_implementation(
module="onyx.main", attribute="get_application"
)()
client = TestClient(app)

View File

@@ -71,6 +71,7 @@ COPY supervisord.conf /etc/supervisor/conf.d/supervisord.conf
# Set up application files
COPY ./onyx /app/onyx
COPY ./generated /app/generated
COPY ./shared_configs /app/shared_configs
COPY ./alembic_tenants /app/alembic_tenants
COPY ./alembic /app/alembic

View File

@@ -0,0 +1,4 @@
import generated.onyx_openapi_client.onyx_openapi_client as onyx_api
from tests.integration.common_utils.constants import API_SERVER_URL
api_config = onyx_api.Configuration(host=API_SERVER_URL)

View File

@@ -5,6 +5,7 @@ from uuid import uuid4
import requests
import generated.onyx_openapi_client.onyx_openapi_client as api
from onyx.connectors.models import InputType
from onyx.db.enums import AccessType
from onyx.db.enums import ConnectorCredentialPairStatus
@@ -13,6 +14,7 @@ from onyx.server.documents.models import ConnectorCredentialPairIdentifier
from onyx.server.documents.models import ConnectorIndexingStatus
from onyx.server.documents.models import DocumentSource
from onyx.server.documents.models import DocumentSyncStatus
from tests.integration.common_utils.config import api_config
from tests.integration.common_utils.constants import API_SERVER_URL
from tests.integration.common_utils.constants import GENERAL_HEADERS
from tests.integration.common_utils.constants import MAX_DELAY
@@ -32,24 +34,27 @@ def _cc_pair_creator(
) -> DATestCCPair:
name = f"{name}-cc-pair" if name else f"test-cc-pair-{uuid4()}"
request = {
"name": name,
"access_type": access_type,
"groups": groups or [],
}
response = requests.put(
url=f"{API_SERVER_URL}/manage/connector/{connector_id}/credential/{credential_id}",
json=request,
headers=(
with api.ApiClient(api_config) as api_client:
api_instance = api.DefaultApi(api_client)
connector_credential_pair_metadata = api.ConnectorCredentialPairMetadata(
name=name, access_type=access_type, groups=groups or []
)
headers = (
user_performing_action.headers
if user_performing_action
else GENERAL_HEADERS
),
)
response.raise_for_status()
)
api_response: api.StatusResponseInt = (
api_instance.associate_credential_to_connector(
connector_id,
credential_id,
connector_credential_pair_metadata,
_headers=headers,
)
)
return DATestCCPair(
id=response.json()["data"],
id=int(api_response.data),
name=name,
connector_id=connector_id,
credential_id=credential_id,

View File

@@ -1,3 +1,7 @@
# TODO(rkuo): All of the downgrade_postgres and upgrade_postgres operations here
# are vulnerable to deadlocks. We could deal with them similar to reset_postgres
# where we retry out of process
import json
import pytest

File diff suppressed because one or more lines are too long

3
web/.gitignore vendored
View File

@@ -40,3 +40,6 @@ next-env.d.ts
/user_auth.json
/build-archive.log
/test-results
# generated clients ... in particular, the API to the Onyx backend itself!
/src/lib/generated

View File

@@ -43,6 +43,19 @@ cd web
npx playwright test
```
To run a single test:
```
npx playwright test landing-page.spec.ts
```
If running locally, interactive options can help you see exactly what is happening in
the test.
```
npx playwright test --ui
npx playwright test --headed
```
3. Inspect results
By default, playwright.config.ts is configured to output the results to:

View File

@@ -89,7 +89,7 @@ function ActionForm({
setMethodSpecs(response.data);
setDefinitionError(null);
}
} catch (error) {
} catch {
setMethodSpecs(null);
setDefinitionError("Invalid JSON format");
}
@@ -143,7 +143,7 @@ function ActionForm({
parseJsonWithTrailingCommas(definition)
);
setFieldValue("definition", formatted);
} catch (error) {
} catch {
alert("Invalid JSON format");
}
}
@@ -414,7 +414,7 @@ export function ActionEditor({ tool }: { tool?: ToolSnapshot }) {
let definition: any;
try {
definition = parseJsonWithTrailingCommas(values.definition);
} catch (error) {
} catch {
setDefinitionError("Invalid JSON in action definition");
return;
}

View File

@@ -88,6 +88,7 @@ export default function Page() {
);
if (
filteredCategories.length > 0 &&
filteredCategories[0] !== undefined &&
filteredCategories[0][1].length > 0
) {
const firstSource = filteredCategories[0][1][0];

View File

@@ -25,7 +25,7 @@ import { getDisplayNameForModel, useLabels } from "@/lib/hooks";
import { DocumentSetSelectable } from "@/components/documentSet/DocumentSetSelectable";
import { addAssistantToList } from "@/lib/assistants/updateAssistantPreferences";
import {
destructureValue,
parseLlmDescriptor,
modelSupportsImageInput,
structureValue,
} from "@/lib/llm/utils";
@@ -548,6 +548,7 @@ export function AssistantEditor({
const submissionData: PersonaUpsertParameters = {
...values,
icon_color: values.icon_color ?? null,
existing_prompt_id: existingPrompt?.id ?? null,
starter_messages: starterMessages,
groups: groups,
@@ -1163,7 +1164,7 @@ export function AssistantEditor({
setFieldValue("llm_model_provider_override", null);
} else {
const { modelName, provider, name } =
destructureValue(selected);
parseLlmDescriptor(selected);
if (modelName && name) {
setFieldValue(
"llm_model_version_override",

View File

@@ -36,12 +36,12 @@ export default function StarterMessagesList({
if (value && index === values.length - 1 && values.length < 4) {
arrayHelpers.push({ message: "" });
} else if (
!value &&
index === values.length - 2 &&
!values[values.length - 1].message
) {
arrayHelpers.pop();
} else if (!value && index === values.length - 2) {
const lastItem = values[values.length - 1];
if (lastItem !== undefined && !lastItem.message) {
// Check if lastItem's message is also empty
arrayHelpers.pop();
}
}
};

View File

@@ -124,8 +124,12 @@ export function ModelConfigurationField({
for (const key in newErrors) {
const numKey = Number(key);
if (numKey > index) {
newErrors[numKey - 1] = newErrors[key];
delete newErrors[numKey];
const errorValue = newErrors[key];
if (errorValue !== undefined) {
// Ensure the value is not undefined
newErrors[numKey - 1] = errorValue;
delete newErrors[numKey];
}
}
}
}

View File

@@ -11,7 +11,10 @@ import {
OpenAISVG,
} from "@/components/icons/icons";
export const getProviderIcon = (providerName: string, modelName?: string) => {
export const getProviderIcon = (
providerName: string,
modelName?: string
): (({ size, className }: IconProps) => JSX.Element) => {
const iconMap: Record<
string,
({ size, className }: IconProps) => JSX.Element
@@ -32,8 +35,12 @@ export const getProviderIcon = (providerName: string, modelName?: string) => {
};
// First check if provider name directly matches an icon
if (providerName.toLowerCase() in iconMap) {
return iconMap[providerName.toLowerCase()];
const lowerProviderName = providerName.toLowerCase();
if (lowerProviderName in iconMap) {
const icon = iconMap[lowerProviderName];
if (icon) {
return icon;
}
}
// Then check if model name contains any of the keys

View File

@@ -49,63 +49,68 @@ const TabsField: FC<TabsFieldProps> = ({
</div>
)}
<Tabs
defaultValue={tabField.tabs[0].value}
className="w-full"
onValueChange={(newTab) => {
// Clear values from other tabs but preserve defaults
tabField.tabs.forEach((tab) => {
if (tab.value !== newTab) {
tab.fields.forEach((field) => {
// Only clear if not default value
if (values[field.name] !== field.default) {
values[field.name] = field.default;
}
});
}
});
}}
>
<TabsList>
{tabField.tabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabField.tabs.map((tab) => (
<TabsContent key={tab.value} value={tab.value} className="">
{tab.fields.map((subField, index, array) => {
// Check visibility condition first
if (
subField.visibleCondition &&
!subField.visibleCondition(values, currentCredential)
) {
return null;
}
return (
<div
key={subField.name}
className={
index < array.length - 1 && subField.type !== "string_tab"
? "mb-4"
: ""
{/* Ensure there's at least one tab before rendering */}
{tabField.tabs.length === 0 ? (
<div className="text-sm text-muted-foreground">No tabs to display.</div>
) : (
<Tabs
defaultValue={tabField.tabs[0]?.value} // Optional chaining for safety, though the length check above handles it
className="w-full"
onValueChange={(newTab) => {
// Clear values from other tabs but preserve defaults
tabField.tabs.forEach((tab) => {
if (tab.value !== newTab) {
tab.fields.forEach((field) => {
// Only clear if not default value
if (values[field.name] !== field.default) {
values[field.name] = field.default;
}
>
<RenderField
});
}
});
}}
>
<TabsList>
{tabField.tabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabField.tabs.map((tab) => (
<TabsContent key={tab.value} value={tab.value} className="">
{tab.fields.map((subField, index, array) => {
// Check visibility condition first
if (
subField.visibleCondition &&
!subField.visibleCondition(values, currentCredential)
) {
return null;
}
return (
<div
key={subField.name}
field={subField}
values={values}
connector={connector}
currentCredential={currentCredential}
/>
</div>
);
})}
</TabsContent>
))}
</Tabs>
className={
index < array.length - 1 && subField.type !== "string_tab"
? "mb-4"
: ""
}
>
<RenderField
key={subField.name}
field={subField}
values={values}
connector={connector}
currentCredential={currentCredential}
/>
</div>
);
})}
</TabsContent>
))}
</Tabs>
)}
</div>
);
};

View File

@@ -168,7 +168,10 @@ export const DriveJsonUpload = ({
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.type === "application/json" || file.name.endsWith(".json")) {
if (
file !== undefined &&
(file.type === "application/json" || file.name.endsWith(".json"))
) {
handleFileUpload(file);
} else {
setPopup({
@@ -224,6 +227,9 @@ export const DriveJsonUpload = ({
return;
}
const file = event.target.files[0];
if (file === undefined) {
return;
}
handleFileUpload(file);
}}
/>

View File

@@ -166,7 +166,10 @@ const GmailCredentialUpload = ({
const files = e.dataTransfer.files;
if (files.length > 0) {
const file = files[0];
if (file.type === "application/json" || file.name.endsWith(".json")) {
if (
file !== undefined &&
(file.type === "application/json" || file.name.endsWith(".json"))
) {
handleFileUpload(file);
} else {
setPopup({
@@ -222,6 +225,9 @@ const GmailCredentialUpload = ({
return;
}
const file = event.target.files[0];
if (file === undefined) {
return;
}
handleFileUpload(file);
}}
/>

View File

@@ -36,6 +36,25 @@ export const submitGoogleSite = async (
}
const filePaths = responseJson.file_paths as string[];
if (!filePaths || filePaths.length === 0) {
setPopup({
message:
"File upload was successful, but no file path was returned. Cannot create connector.",
type: "error",
});
return false;
}
const filePath = filePaths[0];
if (filePath === undefined) {
setPopup({
message:
"File upload was successful, but file path is undefined. Cannot create connector.",
type: "error",
});
return false;
}
const [connectorErrorMsg, connector] =
await createConnector<GoogleSitesConfig>({
name: name ? name : `GoogleSitesConnector-${base_url}`,
@@ -43,7 +62,7 @@ export const submitGoogleSite = async (
input_type: "load_state",
connector_specific_config: {
base_url: base_url,
zip_path: filePaths[0],
zip_path: filePath,
},
access_type: access_type,
refresh_freq: refreshFreq,

View File

@@ -114,6 +114,11 @@ export function ChangeCredentialsModal({
.toLowerCase()
.split(" ")[0];
if (!normalizedProviderType) {
setTestError("Provider type is invalid or missing.");
return;
}
try {
const testResponse = await testEmbedding({
provider_type: normalizedProviderType,

View File

@@ -4,7 +4,7 @@ import {
fetchVisionProviders,
setDefaultVisionProvider,
} from "@/lib/llm/visionLLM";
import { destructureValue, structureValue } from "@/lib/llm/utils";
import { parseLlmDescriptor, structureValue } from "@/lib/llm/utils";
// Define a type for the popup setter function
type SetPopup = (popup: {
@@ -73,7 +73,7 @@ export function useVisionProviders(setPopup: SetPopup) {
}
try {
const { name, modelName } = destructureValue(llmValue);
const { name, modelName } = parseLlmDescriptor(llmValue);
// Find the provider ID
const providerObj = visionProviders.find((p) => p.name === name);

View File

@@ -36,7 +36,9 @@ export const TokenRateLimitTable = ({
isAdmin,
}: TokenRateLimitTableArgs) => {
const shouldRenderGroupName = () =>
tokenRateLimits.length > 0 && tokenRateLimits[0].group_name !== undefined;
tokenRateLimits.length > 0 &&
tokenRateLimits[0] !== undefined &&
tokenRateLimits[0].group_name !== undefined;
const handleEnabledChange = (id: number) => {
const tokenRateLimit = tokenRateLimits.find(

View File

@@ -544,6 +544,7 @@ export function ChatPage({
// if this is a seeded chat, then kick off the AI message generation
if (
newMessageHistory.length === 1 &&
newMessageHistory[0] !== undefined &&
!submitOnLoadPerformed.current &&
searchParams?.get(SEARCH_PARAM_NAMES.SEEDED) === "true"
) {
@@ -649,7 +650,7 @@ export function ChatPage({
completeMessageMapOverride || currentMessageMap(completeMessageDetail);
const newCompleteMessageMap = structuredClone(frozenCompleteMessageMap);
if (newCompleteMessageMap.size === 0) {
if (messages[0] !== undefined && newCompleteMessageMap.size === 0) {
const systemMessageId = messages[0].parentMessageId || SYSTEM_MESSAGE_ID;
const firstMessageId = messages[0].messageId;
const dummySystemMessage: Message = {
@@ -690,7 +691,7 @@ export function ChatPage({
frozenCompleteMessageMap
);
const latestMessage = currentMessageChain[currentMessageChain.length - 1];
if (latestMessage) {
if (messages[0] !== undefined && latestMessage) {
newCompleteMessageMap.get(
latestMessage.messageId
)!.latestChildMessageId = messages[0].messageId;
@@ -1379,7 +1380,11 @@ export function ChatPage({
} else if (alternativeAssistant) {
currentAssistantId = alternativeAssistant.id;
} else {
currentAssistantId = liveAssistant.id;
if (liveAssistant) {
currentAssistantId = liveAssistant.id;
} else {
currentAssistantId = 0; // Fallback if no assistant is live
}
}
resetInputBar();
@@ -1438,8 +1443,8 @@ export function ChatPage({
filterManager.selectedDocumentSets,
filterManager.timeRange,
filterManager.selectedTags,
selectedFiles.map((file) => file.id),
selectedFolders.map((folder) => folder.id)
selectedFiles.map((file) => file.id)
// selectedFolders.map((folder) => folder.id)
),
selectedDocumentIds: selectedDocuments
.filter(
@@ -1957,7 +1962,7 @@ export function ChatPage({
) => {
const [_, llmModel] = getFinalLLM(
llmProviders,
liveAssistant,
liveAssistant ?? null,
llmManager.currentLlm
);
const llmAcceptsImages = modelSupportsImageInput(llmProviders, llmModel);
@@ -1984,7 +1989,7 @@ export function ChatPage({
formData.append("files", file);
const response: FileResponse[] = await uploadFile(formData, null);
if (response.length > 0) {
if (response.length > 0 && response[0] !== undefined) {
const uploadedFile = response[0];
if (intent == UploadIntent.ADD_TO_DOCUMENTS) {
@@ -2392,14 +2397,14 @@ export function ChatPage({
? true
: false
}
humanMessage={humanMessage}
humanMessage={humanMessage ?? null}
setPresentingDocument={setPresentingDocument}
modal={true}
ref={innerSidebarElementRef}
closeSidebar={() => {
setDocumentSidebarVisible(false);
}}
selectedMessage={aiMessage}
selectedMessage={aiMessage ?? null}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={toggleDocumentSelection}
clearSelectedDocuments={clearSelectedDocuments}
@@ -2548,7 +2553,7 @@ export function ChatPage({
`}
>
<DocumentResults
humanMessage={humanMessage}
humanMessage={humanMessage ?? null}
agenticMessage={
aiMessage?.sub_questions?.length! > 0 ||
messageHistory.find(
@@ -2563,7 +2568,7 @@ export function ChatPage({
closeSidebar={() =>
setTimeout(() => setDocumentSidebarVisible(false), 300)
}
selectedMessage={aiMessage}
selectedMessage={aiMessage ?? null}
selectedDocuments={selectedDocuments}
toggleDocumentSelection={toggleDocumentSelection}
clearSelectedDocuments={clearSelectedDocuments}
@@ -2675,14 +2680,16 @@ export function ChatPage({
<div className="h-full w-[95%] mx-auto flex flex-col justify-center items-center">
<ChatIntro selectedPersona={liveAssistant} />
<StarterMessages
currentPersona={currentPersona}
onSubmit={(messageOverride) =>
onSubmit({
messageOverride,
})
}
/>
{currentPersona && (
<StarterMessages
currentPersona={currentPersona}
onSubmit={(messageOverride) =>
onSubmit({
messageOverride,
})
}
/>
)}
</div>
)}
<div

View File

@@ -15,6 +15,7 @@ import {
StreamingPhaseText,
} from "./message/StreamingMessages";
import { Badge } from "@/components/ui/badge";
import next from "next";
export function useOrderedPhases(externalPhase: StreamingPhase) {
const [phaseQueue, setPhaseQueue] = useState<StreamingPhase[]>([]);
@@ -62,7 +63,9 @@ export function useOrderedPhases(externalPhase: StreamingPhase) {
setPhaseQueue((prevQueue) => {
if (prevQueue.length > 0) {
const [nextPhase, ...rest] = prevQueue;
setDisplayedPhases((prev) => [...prev, nextPhase]);
if (nextPhase !== undefined) {
setDisplayedPhases((prev) => [...prev, nextPhase]);
}
lastDisplayTimeRef.current = Date.now();
return rest;
}

View File

@@ -6,7 +6,7 @@ import {
} from "@/lib/hooks";
import { Persona } from "@/app/admin/assistants/interfaces";
import { destructureValue } from "@/lib/llm/utils";
import { parseLlmDescriptor } from "@/lib/llm/utils";
import { useState } from "react";
import { Hoverable } from "@/components/Hoverable";
import { IconType } from "react-icons";
@@ -53,7 +53,9 @@ export default function RegenerateOption({
</div>
}
onSelect={(value) => {
const { name, provider, modelName } = destructureValue(value as string);
const { name, provider, modelName } = parseLlmDescriptor(
value as string
);
regenerate({
name: name,
provider: provider,

View File

@@ -29,7 +29,7 @@ export function useIntersectionObserver({
const observer = new IntersectionObserver((entries) => {
const [entry] = entries;
if (entry.isIntersecting) {
if (entry !== undefined && entry.isIntersecting) {
onIntersect();
}
}, options);

View File

@@ -347,12 +347,14 @@ export const FolderList = ({
showDeleteModal={showDeleteModal}
/>
))}
{folders.length == 1 && folders[0].chat_sessions.length == 0 && (
<p className="text-sm font-normal text-subtle mt-2">
{" "}
Drag a chat into a folder to save for later{" "}
</p>
)}
{folders.length == 1 &&
folders[0] &&
folders[0].chat_sessions.length == 0 && (
<p className="text-sm font-normal text-subtle mt-2">
{" "}
Drag a chat into a folder to save for later{" "}
</p>
)}
</div>
);
};

View File

@@ -262,8 +262,9 @@ export function ChatInputBar({
if (items) {
const pastedFiles = [];
for (let i = 0; i < items.length; i++) {
if (items[i].kind === "file") {
const file = items[i].getAsFile();
const item = items[i];
if (item && item.kind === "file") {
const file = item.getAsFile();
if (file) pastedFiles.push(file);
}
}
@@ -360,26 +361,35 @@ export function ChatInputBar({
handlePromptInput(text);
};
let startFilterAt = "";
if (message !== undefined) {
const message_segments = message
.slice(message.lastIndexOf("@") + 1)
.split(/\s/);
if (message_segments[0]) {
startFilterAt = message_segments[0].toLowerCase();
}
}
const assistantTagOptions = assistantOptions.filter((assistant) =>
assistant.name.toLowerCase().startsWith(
message
.slice(message.lastIndexOf("@") + 1)
.split(/\s/)[0]
.toLowerCase()
)
assistant.name.toLowerCase().startsWith(startFilterAt)
);
let startFilterSlash = "";
if (message !== undefined) {
const message_segments = message
.slice(message.lastIndexOf("/") + 1)
.split(/\s/);
if (message_segments[0]) {
startFilterSlash = message_segments[0].toLowerCase();
}
}
const [tabbingIconIndex, setTabbingIconIndex] = useState(0);
const filteredPrompts = inputPrompts.filter(
(prompt) =>
prompt.active &&
prompt.prompt.toLowerCase().startsWith(
message
.slice(message.lastIndexOf("/") + 1)
.split(/\s/)[0]
.toLowerCase()
)
prompt.active && prompt.prompt.toLowerCase().startsWith(startFilterSlash)
);
const handleKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
@@ -402,11 +412,15 @@ export function ChatInputBar({
if (showPrompts) {
const selectedPrompt =
filteredPrompts[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
updateInputPrompt(selectedPrompt);
if (selectedPrompt) {
updateInputPrompt(selectedPrompt);
}
} else {
const option =
assistantTagOptions[tabbingIconIndex >= 0 ? tabbingIconIndex : 0];
updatedTaggedAssistant(option);
if (option) {
updatedTaggedAssistant(option);
}
}
}
}

View File

@@ -7,7 +7,7 @@ import {
import { getDisplayNameForModel } from "@/lib/hooks";
import {
modelSupportsImageInput,
destructureValue,
parseLlmDescriptor,
structureValue,
} from "@/lib/llm/utils";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
@@ -74,18 +74,21 @@ export default function LLMPopover({
modelConfiguration.is_visible
) {
uniqueModelNames.add(modelConfiguration.name);
llmOptionsByProvider[llmProvider.provider].push({
name: modelConfiguration.name,
value: structureValue(
llmProvider.name,
llmProvider.provider,
modelConfiguration.name
),
icon: getProviderIcon(
llmProvider.provider,
modelConfiguration.name
),
});
const options = llmOptionsByProvider[llmProvider.provider];
if (options) {
options.push({
name: modelConfiguration.name,
value: structureValue(
llmProvider.name,
llmProvider.provider,
modelConfiguration.name
),
icon: getProviderIcon(
llmProvider.provider,
modelConfiguration.name
),
});
}
}
});
});
@@ -121,12 +124,18 @@ export default function LLMPopover({
// Use useCallback to prevent function recreation
const handleTemperatureChange = useCallback((value: number[]) => {
setLocalTemperature(value[0]);
const value_0 = value[0];
if (value_0 !== undefined) {
setLocalTemperature(value_0);
}
}, []);
const handleTemperatureChangeComplete = useCallback(
(value: number[]) => {
llmManager.updateTemperature(value[0]);
const value_0 = value[0];
if (value_0 !== undefined) {
llmManager.updateTemperature(value_0);
}
},
[llmManager]
);
@@ -187,7 +196,7 @@ export default function LLMPopover({
: "text-text-darker"
}`}
onClick={() => {
llmManager.updateCurrentLlm(destructureValue(value));
llmManager.updateCurrentLlm(parseLlmDescriptor(value));
onSelect?.(value);
setIsOpen(false);
}}

View File

@@ -56,8 +56,9 @@ export function SimplifiedChatInputBar({
if (items) {
const pastedFiles = [];
for (let i = 0; i < items.length; i++) {
if (items[i].kind === "file") {
const file = items[i].getAsFile();
const item = items[i];
if (item && item.kind === "file") {
const file = item.getAsFile();
if (file) pastedFiles.push(file);
}
}

View File

@@ -377,14 +377,18 @@ export function getHumanAndAIMessageFromMessageNumber(
if (messageInd !== -1) {
const matchingMessage = messageHistory[messageInd];
const pairedMessage =
matchingMessage.type === "user"
matchingMessage && matchingMessage.type === "user"
? messageHistory[messageInd + 1]
: messageHistory[messageInd - 1];
const humanMessage =
matchingMessage.type === "user" ? matchingMessage : pairedMessage;
matchingMessage && matchingMessage.type === "user"
? matchingMessage
: pairedMessage;
const aiMessage =
matchingMessage.type === "user" ? pairedMessage : matchingMessage;
matchingMessage && matchingMessage.type === "user"
? pairedMessage
: matchingMessage;
return {
humanMessage,
@@ -433,13 +437,25 @@ export function groupSessionsByDateRange(chatSessions: ChatSession[]) {
const diffDays = diffTime / (1000 * 3600 * 24); // Convert time difference to days
if (diffDays < 1) {
groups["Today"].push(chatSession);
const groups_today = groups["Today"];
if (groups_today) {
groups_today.push(chatSession);
}
} else if (diffDays <= 7) {
groups["Previous 7 Days"].push(chatSession);
const groups_7 = groups["Previous 7 Days"];
if (groups_7) {
groups_7.push(chatSession);
}
} else if (diffDays <= 30) {
groups["Previous 30 days"].push(chatSession);
const groups_30 = groups["Previous 30 Days"];
if (groups_30) {
groups_30.push(chatSession);
}
} else {
groups["Over 30 days"].push(chatSession);
const groups_over_30 = groups["Over 30 days"];
if (groups_over_30) {
groups_over_30.push(chatSession);
}
}
});
@@ -560,7 +576,11 @@ export function buildLatestMessageChain(
//
// remove system message
if (finalMessageList.length > 0 && finalMessageList[0].type === "system") {
if (
finalMessageList.length > 0 &&
finalMessageList[0] &&
finalMessageList[0].type === "system"
) {
finalMessageList = finalMessageList.slice(1);
}
return finalMessageList.concat(additionalMessagesOnMainline);

View File

@@ -163,7 +163,7 @@ export const AgenticMessage = ({
}, processed);
const lastMatch = matches[matches.length - 1];
if (!lastMatch.endsWith("```")) {
if (lastMatch && !lastMatch.endsWith("```")) {
processed = preprocessLaTeX(processed);
}
}
@@ -403,6 +403,11 @@ export const AgenticMessage = ({
otherMessagesCanSwitchTo &&
otherMessagesCanSwitchTo.length > 1;
let otherMessage: number | undefined = undefined;
if (currentMessageInd && otherMessagesCanSwitchTo) {
otherMessage = otherMessagesCanSwitchTo[currentMessageInd - 1];
}
useEffect(() => {
if (!allowStreaming) {
return;
@@ -587,28 +592,21 @@ export const AgenticMessage = ({
>
<TooltipGroup>
<div className="flex justify-start w-full gap-x-0.5">
{includeMessageSwitcher && (
<div className="-mx-1 mr-auto">
<MessageSwitcher
currentPage={currentMessageInd + 1}
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
onMessageSelection(
otherMessagesCanSwitchTo[
currentMessageInd - 1
]
);
}}
handleNext={() => {
onMessageSelection(
otherMessagesCanSwitchTo[
currentMessageInd + 1
]
);
}}
/>
</div>
)}
{includeMessageSwitcher &&
otherMessage !== undefined && (
<div className="-mx-1 mr-auto">
<MessageSwitcher
currentPage={currentMessageInd + 1}
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
onMessageSelection(otherMessage!);
}}
handleNext={() => {
onMessageSelection(otherMessage!);
}}
/>
</div>
)}
</div>
<CustomTooltip showTick line content="Copy">
<CopyButton
@@ -675,28 +673,21 @@ export const AgenticMessage = ({
>
<TooltipGroup>
<div className="flex justify-start w-full gap-x-0.5">
{includeMessageSwitcher && (
<div className="-mx-1 mr-auto">
<MessageSwitcher
currentPage={currentMessageInd + 1}
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
onMessageSelection(
otherMessagesCanSwitchTo[
currentMessageInd - 1
]
);
}}
handleNext={() => {
onMessageSelection(
otherMessagesCanSwitchTo[
currentMessageInd + 1
]
);
}}
/>
</div>
)}
{includeMessageSwitcher &&
otherMessage !== undefined && (
<div className="-mx-1 mr-auto">
<MessageSwitcher
currentPage={currentMessageInd + 1}
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
onMessageSelection(otherMessage!);
}}
handleNext={() => {
onMessageSelection(otherMessage!);
}}
/>
</div>
)}
</div>
<CustomTooltip showTick line content="Copy">
<CopyButton

View File

@@ -34,77 +34,83 @@ export const MemoizedAnchor = memo(
if (value?.startsWith("[") && value?.endsWith("]")) {
const match = value.match(/\[(D|Q)?(\d+)\]/);
if (match) {
const isUserFileCitation = userFiles?.length && userFiles.length > 0;
if (isUserFileCitation) {
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>;
}
} else if (!isUserFileCitation) {
const index = parseInt(match[2], 10) - 1;
const associatedDoc = docs?.[index];
if (!associatedDoc) {
return <a href={children as string}>{children}</a>;
}
} else {
const index = parseInt(match[2], 10) - 1;
const associatedSubQuestion = subQuestions?.[index];
if (!associatedSubQuestion) {
return <a href={href || (children as string)}>{children}</a>;
const match_item = match[2];
if (match_item !== undefined) {
const isUserFileCitation = userFiles?.length && userFiles.length > 0;
if (isUserFileCitation) {
const index = Math.min(
parseInt(match_item, 10) - 1,
userFiles?.length - 1
);
const associatedUserFile = userFiles?.[index];
if (!associatedUserFile) {
return <a href={children as string}>{children}</a>;
}
} else if (!isUserFileCitation) {
const index = parseInt(match_item, 10) - 1;
const associatedDoc = docs?.[index];
if (!associatedDoc) {
return <a href={children as string}>{children}</a>;
}
} else {
const index = parseInt(match_item, 10) - 1;
const associatedSubQuestion = subQuestions?.[index];
if (!associatedSubQuestion) {
return <a href={href || (children as string)}>{children}</a>;
}
}
}
}
if (match) {
const isSubQuestion = match[1] === "Q";
const isDocument = !isSubQuestion;
const match_item = match[2];
if (match_item !== undefined) {
const isSubQuestion = match[1] === "Q";
const isDocument = !isSubQuestion;
// Fix: parseInt now uses match[2], which is the numeric part
const index = parseInt(match[2], 10) - 1;
// Fix: parseInt now uses match[2], which is the numeric part
const index = parseInt(match_item, 10) - 1;
const associatedDoc = isDocument ? docs?.[index] : null;
const associatedSubQuestion = isSubQuestion
? subQuestions?.[index]
: undefined;
const associatedDoc = isDocument ? docs?.[index] : null;
const associatedSubQuestion = isSubQuestion
? subQuestions?.[index]
: undefined;
if (!associatedDoc && !associatedSubQuestion) {
return <>{children}</>;
}
if (!associatedDoc && !associatedSubQuestion) {
return <>{children}</>;
}
let icon: React.ReactNode = null;
if (associatedDoc?.source_type === "web") {
icon = <WebResultIcon url={associatedDoc.link} />;
} else {
icon = (
<SourceIcon
sourceType={associatedDoc?.source_type as ValidSources}
iconSize={18}
/>
let icon: React.ReactNode = null;
if (associatedDoc?.source_type === "web") {
icon = <WebResultIcon url={associatedDoc.link} />;
} else {
icon = (
<SourceIcon
sourceType={associatedDoc?.source_type as ValidSources}
iconSize={18}
/>
);
}
const associatedDocInfo = associatedDoc
? {
...associatedDoc,
icon: icon as any,
link: associatedDoc.link,
}
: undefined;
return (
<MemoizedLink
updatePresentingDocument={updatePresentingDocument}
href={href}
document={associatedDocInfo}
question={associatedSubQuestion}
openQuestion={openQuestion}
>
{children}
</MemoizedLink>
);
}
const associatedDocInfo = associatedDoc
? {
...associatedDoc,
icon: icon as any,
link: associatedDoc.link,
}
: undefined;
return (
<MemoizedLink
updatePresentingDocument={updatePresentingDocument}
href={href}
document={associatedDocInfo}
question={associatedSubQuestion}
openQuestion={openQuestion}
>
{children}
</MemoizedLink>
);
}
}
return (

View File

@@ -336,7 +336,7 @@ export const AIMessage = ({
}, content);
const lastMatch = matches[matches.length - 1];
if (!lastMatch.endsWith("```")) {
if (lastMatch && !lastMatch.endsWith("```")) {
return preprocessLaTeX(content);
}
}
@@ -490,6 +490,11 @@ export const AIMessage = ({
otherMessagesCanSwitchTo &&
otherMessagesCanSwitchTo.length > 1;
let otherMessage: number | undefined = undefined;
if (currentMessageInd && otherMessagesCanSwitchTo) {
otherMessage = otherMessagesCanSwitchTo[currentMessageInd - 1];
}
return (
<div
id={isComplete ? "onyx-ai-message" : undefined}
@@ -732,28 +737,21 @@ export const AIMessage = ({
>
<TooltipGroup>
<div className="flex justify-start w-full gap-x-0.5">
{includeMessageSwitcher && (
<div className="-mx-1 mr-auto">
<MessageSwitcher
currentPage={currentMessageInd + 1}
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
onMessageSelection(
otherMessagesCanSwitchTo[
currentMessageInd - 1
]
);
}}
handleNext={() => {
onMessageSelection(
otherMessagesCanSwitchTo[
currentMessageInd + 1
]
);
}}
/>
</div>
)}
{includeMessageSwitcher &&
otherMessage !== undefined && (
<div className="-mx-1 mr-auto">
<MessageSwitcher
currentPage={currentMessageInd + 1}
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
onMessageSelection(otherMessage!);
}}
handleNext={() => {
onMessageSelection(otherMessage!);
}}
/>
</div>
)}
</div>
<CustomTooltip showTick line content="Copy">
<CopyButton
@@ -814,28 +812,21 @@ export const AIMessage = ({
>
<TooltipGroup>
<div className="flex justify-start w-full gap-x-0.5">
{includeMessageSwitcher && (
<div className="-mx-1 mr-auto">
<MessageSwitcher
currentPage={currentMessageInd + 1}
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
onMessageSelection(
otherMessagesCanSwitchTo[
currentMessageInd - 1
]
);
}}
handleNext={() => {
onMessageSelection(
otherMessagesCanSwitchTo[
currentMessageInd + 1
]
);
}}
/>
</div>
)}
{includeMessageSwitcher &&
otherMessage !== undefined && (
<div className="-mx-1 mr-auto">
<MessageSwitcher
currentPage={currentMessageInd + 1}
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
onMessageSelection(otherMessage!);
}}
handleNext={() => {
onMessageSelection(otherMessage!);
}}
/>
</div>
)}
</div>
<CustomTooltip showTick line content="Copy">
<CopyButton
@@ -1021,6 +1012,11 @@ export const HumanMessage = ({
? otherMessagesCanSwitchTo?.indexOf(messageId)
: undefined;
let otherMessage: number | undefined = undefined;
if (currentMessageInd && otherMessagesCanSwitchTo) {
otherMessage = otherMessagesCanSwitchTo[currentMessageInd - 1];
}
return (
<div
id="onyx-human-message"
@@ -1216,6 +1212,7 @@ export const HumanMessage = ({
<div className="flex flex-col md:flex-row gap-x-0.5 mt-1">
{currentMessageInd !== undefined &&
otherMessage !== undefined &&
onMessageSelection &&
otherMessagesCanSwitchTo &&
otherMessagesCanSwitchTo.length > 1 && (
@@ -1226,15 +1223,11 @@ export const HumanMessage = ({
totalPages={otherMessagesCanSwitchTo.length}
handlePrevious={() => {
stopGenerating();
onMessageSelection(
otherMessagesCanSwitchTo[currentMessageInd - 1]
);
onMessageSelection(otherMessage!);
}}
handleNext={() => {
stopGenerating();
onMessageSelection(
otherMessagesCanSwitchTo[currentMessageInd + 1]
);
onMessageSelection(otherMessage!);
}}
/>
</div>

View File

@@ -131,26 +131,50 @@ export const useStreamingMessages = (
for (let i = 0; i < actualSubQs.length; i++) {
const sq = actualSubQs[i];
if (sq === undefined) {
continue;
}
const p = progressRef.current[i];
if (p === undefined) {
continue;
}
const dynSQ = dynamicSubQuestionsRef.current[i];
if (dynSQ === undefined) {
continue;
}
if (i < 0) {
continue;
}
if (i > 0) {
const p2 = progressRef.current[i - 1];
if (p2 === undefined) {
continue;
}
if (!p2.questionDone) {
continue;
}
}
// Always stream the first subquestion (index 0)
// For others, only stream if the previous question is complete
if (i === 0 || (i > 0 && progressRef.current[i - 1].questionDone)) {
if (sq.question) {
const nextIndex = p.questionCharIndex + 1;
if (nextIndex <= sq.question.length) {
dynSQ.question = sq.question.slice(0, nextIndex);
p.questionCharIndex = nextIndex;
if (nextIndex >= sq.question.length && sq.is_stopped) {
p.questionDone = true;
}
didStreamQuestion = true;
allQuestionsComplete = false;
// Break after streaming one question to ensure sequential behavior
break;
if (sq.question) {
const nextIndex = p.questionCharIndex + 1;
if (nextIndex <= sq.question.length) {
dynSQ.question = sq.question.slice(0, nextIndex);
p.questionCharIndex = nextIndex;
if (nextIndex >= sq.question.length && sq.is_stopped) {
p.questionDone = true;
}
didStreamQuestion = true;
allQuestionsComplete = false;
// Break after streaming one question to ensure sequential behavior
break;
}
}
@@ -176,10 +200,21 @@ export const useStreamingMessages = (
// 2) Handle SUB_QUERIES → CONTEXT_DOCS → ANSWER → COMPLETE
for (let i = 0; i < actualSubQs.length; i++) {
const sq = actualSubQs[i];
if (sq === undefined) {
continue;
}
const dynSQ = dynamicSubQuestionsRef.current[i];
if (dynSQ === undefined) {
continue;
}
dynSQ.answer_streaming = sq.answer_streaming;
const p = progressRef.current[i];
if (p === undefined) {
continue;
}
// Wait for subquestion #0 or the previous subquestion's progress
if (p.currentPhase === StreamingPhase.WAITING) {
@@ -192,8 +227,9 @@ export const useStreamingMessages = (
} else {
const prevP = progressRef.current[i - 1];
if (
prevP.currentPhase === StreamingPhase.ANSWER ||
prevP.currentPhase === StreamingPhase.COMPLETE
prevP !== undefined &&
(prevP.currentPhase === StreamingPhase.ANSWER ||
prevP.currentPhase === StreamingPhase.COMPLETE)
) {
// Can only proceed if we've spent enough time in WAITING
if (canTransition(p) && !p.waitingTimeoutSet) {
@@ -216,18 +252,35 @@ export const useStreamingMessages = (
// "Stream" the subqueries (in this code, it just sets them all at once)
while (dynSQ.sub_queries!.length < subQueries.length) {
const subquery_detail = subQueries[0];
if (subquery_detail === undefined) {
continue;
}
const subquery_id = subquery_detail.query_id;
dynSQ.sub_queries!.push({
query: "",
query_id: subQueries[0].query_id,
query_id: subquery_id,
});
}
for (let j = 0; j < subQueries.length; j++) {
const dyn_subquery_detail = dynSQ.sub_queries![j];
if (dyn_subquery_detail === undefined) {
continue;
}
const subquery_detail = subQueries[j];
if (subquery_detail === undefined) {
continue;
}
if (
dynSQ.sub_queries![j].query.length < subQueries[j].query.length
dyn_subquery_detail.query.length < subquery_detail.query.length
) {
dynSQ.sub_queries![j].query = subQueries[j].query;
dyn_subquery_detail.query = subquery_detail.query;
} else {
// console.log("NOT STEAMING");
// console.log("NOT STREAMING");
}
}
// console.log(subQueries);

View File

@@ -137,7 +137,7 @@ const SubQuestionDisplay: React.FC<{
}, content);
const lastMatch = matches[matches.length - 1];
if (!lastMatch.endsWith("```")) {
if (lastMatch && !lastMatch.endsWith("```")) {
return preprocessLaTeX(content);
}
}

View File

@@ -18,7 +18,10 @@ export function extractCodeText(
// Match code block with optional language declaration
const codeBlockMatch = codeText.match(/^```[^\n]*\n([\s\S]*?)\n?```$/);
if (codeBlockMatch) {
codeText = codeBlockMatch[1];
const codeTextMatch = codeBlockMatch[1];
if (codeTextMatch !== undefined) {
codeText = codeTextMatch;
}
}
// Normalize indentation
@@ -133,7 +136,7 @@ export const preprocessLaTeX = (content: string) => {
// Restore code blocks
const restoredCodeBlocks = restoredDollars.replace(
/___CODE_BLOCK_(\d+)___/g,
(_, index) => codeBlocks[parseInt(index)]
(_, index) => codeBlocks[parseInt(index)] ?? ""
);
return restoredCodeBlocks;

View File

@@ -3,7 +3,7 @@ import { Modal } from "@/components/Modal";
import { getDisplayNameForModel, LlmDescriptor } from "@/lib/hooks";
import { LLMProviderDescriptor } from "@/app/admin/configuration/llm/interfaces";
import { destructureValue, structureValue } from "@/lib/llm/utils";
import { parseLlmDescriptor, structureValue } from "@/lib/llm/utils";
import { setUserDefaultModel } from "@/lib/users/UserSettings";
import { usePathname, useRouter } from "next/navigation";
import { PopupSpec } from "@/components/admin/connectors/Popup";
@@ -96,7 +96,7 @@ export function UserSettingsModal({
}, [onClose]);
const defaultModelDestructured = defaultModel
? destructureValue(defaultModel)
? parseLlmDescriptor(defaultModel)
: null;
const modelOptionsByProvider = new Map<
string,
@@ -125,14 +125,17 @@ export function UserSettingsModal({
llmProvider.model_configurations.forEach((modelConfiguration) => {
if (!uniqueModelNames.has(modelConfiguration.name)) {
uniqueModelNames.add(modelConfiguration.name);
llmOptionsByProvider[llmProvider.provider].push({
name: modelConfiguration.name,
value: structureValue(
llmProvider.name,
llmProvider.provider,
modelConfiguration.name
),
});
const llmOptions = llmOptionsByProvider[llmProvider.provider];
if (llmOptions) {
llmOptions.push({
name: modelConfiguration.name,
value: structureValue(
llmProvider.name,
llmProvider.provider,
modelConfiguration.name
),
});
}
}
});
});
@@ -143,7 +146,7 @@ export function UserSettingsModal({
if (response.ok) {
if (defaultModel && setCurrentLlm) {
setCurrentLlm(destructureValue(defaultModel));
setCurrentLlm(parseLlmDescriptor(defaultModel));
}
setPopup({
message: "Default model updated successfully",
@@ -361,9 +364,9 @@ export function UserSettingsModal({
currentLlm={
defaultModel
? structureValue(
destructureValue(defaultModel).provider,
parseLlmDescriptor(defaultModel).provider,
"",
destructureValue(defaultModel).modelName
parseLlmDescriptor(defaultModel).modelName
)
: null
}
@@ -373,7 +376,7 @@ export function UserSettingsModal({
handleChangedefaultModel(null);
} else {
const { modelName, provider, name } =
destructureValue(selected);
parseLlmDescriptor(selected);
if (modelName && name) {
handleChangedefaultModel(
structureValue(provider, "", modelName)

View File

@@ -181,7 +181,6 @@ export default function UserFolderContent({ folderId }: { folderId: number }) {
},
});
const [selectedModel, setSelectedModel] = useState(modelDescriptors[0]);
const [uploadingFiles, setUploadingFiles] = useState<string[]>([]);
const [uploadProgress, setUploadProgress] = useState<UploadProgress[]>([]);
const [isCleanupModalOpen, setIsCleanupModalOpen] = useState(false);
@@ -237,6 +236,21 @@ export default function UserFolderContent({ folderId }: { folderId: number }) {
);
}
if (selectedModel === undefined) {
return (
<div className="min-h-full w-full min-w-0 flex-1 mx-auto max-w-5xl px-4 pb-20 md:pl-8 mt-6 md:pr-8 2xl:pr-14">
<div className="text-left space-y-4">
<h2 className="flex items-center gap-1.5 text-lg font-medium leading-tight tracking-tight max-md:hidden">
No Models defined
</h2>
<p className="text-neutral-600">
This page requires models to be available.
</p>
</div>
</div>
);
}
const totalTokens = folderDetails.files.reduce(
(acc, file) => acc + (file.token_count || 0),
0

View File

@@ -24,7 +24,9 @@ export const ModelSelector: React.FC<ModelSelectorProps> = ({
<Select
value={selectedModel.modelName}
onValueChange={(value) =>
onSelectModel(models.find((m) => m.modelName === value) || models[0])
onSelectModel(
models.find((m) => m.modelName === value) || models[0] || selectedModel
)
}
>
<SelectTrigger className="w-full">

View File

@@ -72,8 +72,13 @@ export const FileListItem: React.FC<FileListItemProps> = ({
useEffect(() => {
const checkStatus = async () => {
const status = await getFilesIndexingStatus([file.id]);
setIndexingStatus(status[file.id]);
const status_by_file_id = await getFilesIndexingStatus([file.id]);
if (status_by_file_id) {
const status = status_by_file_id[file.id];
if (status !== undefined) {
setIndexingStatus(status);
}
}
};
checkStatus();

View File

@@ -343,6 +343,14 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
const [activeId, setActiveId] = useState<string | null>(null);
const [isHoveringRight, setIsHoveringRight] = useState(false);
let activeSplit = "";
if (activeId !== undefined && activeId !== null) {
const active_part_1 = activeId.split("-")[1];
if (active_part_1) {
activeSplit = active_part_1;
}
}
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
@@ -364,13 +372,16 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
const { setPopup } = usePopup();
// Create model descriptors and selectedModel state
// why is this hardcoded here?
const modelDescriptors: LLMModelDescriptor[] = [
{ modelName: "Claude 3 Opus", maxTokens: 200000 },
{ modelName: "Claude 3 Sonnet", maxTokens: 180000 },
{ modelName: "GPT-4", maxTokens: 128000 },
];
const [selectedModel, setSelectedModel] = useState(modelDescriptors[0]);
const firstModelDescriptor = modelDescriptors[0]!;
const [selectedModel, setSelectedModel] = useState(firstModelDescriptor);
// Add a new state for tracking uploads
const [uploadStartTime, setUploadStartTime] = useState<number | null>(null);
@@ -811,6 +822,9 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
const addUploadedFileToContext = async (files: FileList) => {
for (let i = 0; i < files.length; i++) {
const file = files[i];
if (file === undefined) {
continue;
}
// Add file to uploading files state
setUploadingFiles((prev) => [...prev, { name: file.name, progress: 0 }]);
const formData = new FormData();
@@ -819,7 +833,10 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
if (response.length > 0) {
const uploadedFile = response[0];
addSelectedFile(uploadedFile);
if (uploadedFile !== undefined) {
addSelectedFile(uploadedFile);
}
markFileComplete(file.name);
}
}
@@ -1214,29 +1231,23 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
</SortableContext>
<DragOverlay>
{activeId ? (
{activeId && activeSplit ? (
<DraggableItem
id={activeId}
type={activeId.startsWith("folder") ? "folder" : "file"}
item={
activeId.startsWith("folder")
? folders.find(
(f) =>
f.id === parseInt(activeId.split("-")[1], 10)
(f) => f.id === parseInt(activeSplit, 10)
)!
: currentFolderFiles.find(
(f) =>
f.id === parseInt(activeId.split("-")[1], 10)
(f) => f.id === parseInt(activeSplit, 10)
)!
}
isSelected={
activeId.startsWith("folder")
? selectedFolderIds.has(
parseInt(activeId.split("-")[1], 10)
)
: selectedFileIds.has(
parseInt(activeId.split("-")[1], 10)
)
? selectedFolderIds.has(parseInt(activeSplit, 10))
: selectedFileIds.has(parseInt(activeSplit, 10))
}
/>
) : null}
@@ -1334,8 +1345,10 @@ export const FilePickerModal: React.FC<FilePickerModalProps> = ({
// Extract domain from URL to help with detection
const urlObj = new URL(url);
const createdFile: FileResponse = response[0];
addSelectedFile(createdFile);
const createdFile = response[0];
if (createdFile !== undefined) {
addSelectedFile(createdFile);
}
// Make sure to remove the uploading file indicator when done
markFileComplete(url);
}

View File

@@ -94,6 +94,21 @@ export function SharedChatDisplay({
processRawChatHistory(chatSession.messages)
);
const firstMessage = messages[0];
if (firstMessage === undefined) {
return (
<div className="min-h-full w-full">
<div className="mx-auto w-fit pt-8">
<Callout type="danger" title="Shared Chat Not Found">
No messages found in shared chat.
</Callout>
</div>
<BackToOnyxButton documentSidebarVisible={documentSidebarVisible} />
</div>
);
}
return (
<>
{presentingDocument && (
@@ -106,7 +121,7 @@ export function SharedChatDisplay({
<div className="md:hidden">
<Modal noPadding noScroll>
<DocumentResults
humanMessage={messages[0]}
humanMessage={firstMessage}
agenticMessage={false}
isSharedChat={true}
selectedMessage={
@@ -163,7 +178,7 @@ export function SharedChatDisplay({
`}
>
<DocumentResults
humanMessage={messages[0]}
humanMessage={firstMessage}
agenticMessage={false}
modal={false}
isSharedChat={true}

View File

@@ -65,7 +65,9 @@ export function getDatesList(startDate: Date): string[] {
for (let d = new Date(startDate); d <= endDate; d.setDate(d.getDate() + 1)) {
const dateStr = d.toISOString().split("T")[0]; // convert date object to 'YYYY-MM-DD' format
datesList.push(dateStr);
if (dateStr !== undefined) {
datesList.push(dateStr);
}
}
return datesList;

View File

@@ -25,7 +25,11 @@ export function FeedbackChart({
<ThreeDotsLoader />
</div>
);
} else if (!queryAnalyticsData || queryAnalyticsError) {
} else if (
!queryAnalyticsData ||
queryAnalyticsData[0] === undefined ||
queryAnalyticsError
) {
chart = (
<div className="h-80 text-red-600 text-bold flex flex-col">
<p className="m-auto">Failed to fetch feedback data...</p>

View File

@@ -24,7 +24,11 @@ export function OnyxBotChart({
<ThreeDotsLoader />
</div>
);
} else if (!onyxBotAnalyticsData || onyxBotAnalyticsError) {
} else if (
!onyxBotAnalyticsData ||
onyxBotAnalyticsData[0] == undefined ||
onyxBotAnalyticsError
) {
chart = (
<div className="h-80 text-red-600 text-bold flex flex-col">
<p className="m-auto">Failed to fetch feedback data...</p>

View File

@@ -73,9 +73,12 @@ export function PersonaMessagesChart({
highlightedIndex >= 0 &&
highlightedIndex < filteredPersonaList.length
) {
setSelectedPersonaId(filteredPersonaList[highlightedIndex].id);
setSearchQuery("");
setHighlightedIndex(-1);
const filteredPersona = filteredPersonaList[highlightedIndex];
if (filteredPersona !== undefined) {
setSelectedPersonaId(filteredPersona.id);
setSearchQuery("");
setHighlightedIndex(-1);
}
}
break;
case "Escape":

View File

@@ -33,6 +33,7 @@ export function QueryPerformanceChart({
);
} else if (
!queryAnalyticsData ||
queryAnalyticsData[0] === undefined ||
!userAnalyticsData ||
queryAnalyticsError ||
userAnalyticsError

View File

@@ -14,6 +14,16 @@ async function Page(props: { params: Promise<{ id: string }> }) {
];
const [standardAnswersResponse, standardAnswerCategoriesResponse] =
await Promise.all(tasks);
if (standardAnswersResponse === undefined) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch standard answers.`}
/>
);
}
if (!standardAnswersResponse.ok) {
return (
<ErrorCallout
@@ -37,6 +47,15 @@ async function Page(props: { params: Promise<{ id: string }> }) {
);
}
if (standardAnswerCategoriesResponse === undefined) {
return (
<ErrorCallout
errorTitle="Something went wrong :("
errorMsg={`Failed to fetch standard answer categories.`}
/>
);
}
if (!standardAnswerCategoriesResponse.ok) {
return (
<ErrorCallout

View File

@@ -33,10 +33,20 @@ export function ImageUpload({
type: "error",
message: "Only one file can be uploaded at a time",
});
return;
}
setTmpImageUrl(URL.createObjectURL(acceptedFiles[0]));
setSelectedFile(acceptedFiles[0]);
const acceptedFile = acceptedFiles[0];
if (acceptedFile === undefined) {
setPopup({
type: "error",
message: "acceptedFile cannot be undefined",
});
return;
}
setTmpImageUrl(URL.createObjectURL(acceptedFile));
setSelectedFile(acceptedFile);
setDragActive(false);
}}
onDragLeave={() => setDragActive(false)}

View File

@@ -39,7 +39,11 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
if (!isUserAdmin) {
formikProps.setFieldValue("is_public", false);
}
if (userGroups.length === 1 && !isUserAdmin) {
if (
userGroups.length === 1 &&
userGroups[0] !== undefined &&
!isUserAdmin
) {
formikProps.setFieldValue("groups", [userGroups[0].id]);
setShouldHideContent(true);
} else if (formikProps.values.is_public) {
@@ -58,13 +62,21 @@ export const IsPublicGroupSelector = <T extends IsPublicGroupSelectorFormType>({
return null;
}
let firstUserGroupName = "Unknown";
if (userGroups) {
const userGroup = userGroups[0];
if (userGroup) {
firstUserGroupName = userGroup.name;
}
}
if (shouldHideContent && enforceGroupSelection) {
return (
<>
{userGroups && (
<div className="mb-1 font-medium text-base">
This {objectName} will be assigned to group{" "}
<b>{userGroups[0].name}</b>.
<b>{firstUserGroupName}</b>.
</div>
)}
</>

View File

@@ -157,7 +157,9 @@ export function UserDropdown({
text-base
"
>
{user && user.email ? user.email[0].toUpperCase() : "A"}
{user && user.email
? user.email[0] !== undefined && user.email[0].toUpperCase()
: "A"}
</div>
{notifications && notifications.length > 0 && (
<div className="absolute -right-0.5 -top-0.5 w-3 h-3 bg-red-500 rounded-full"></div>

View File

@@ -56,6 +56,7 @@ export function AccessTypeGroupSelector({
if (
access_type.value === "private" &&
userGroups.length === 1 &&
userGroups[0] !== undefined &&
!isUserAdmin
) {
groups_helpers.setValue([userGroups[0].id]);
@@ -87,7 +88,7 @@ export function AccessTypeGroupSelector({
if (shouldHideContent) {
return (
<>
{userGroups && (
{userGroups && userGroups[0] !== undefined && (
<div className="mb-1 font-medium text-base">
This Connector will be assigned to group <b>{userGroups[0].name}</b>
.

View File

@@ -27,8 +27,20 @@ export const FileUpload: FC<FileUploadProps> = ({
<div>
<Dropzone
onDrop={(acceptedFiles) => {
const filesToSet = multiple ? acceptedFiles : [acceptedFiles[0]];
setSelectedFiles(filesToSet);
let filesToSet: File[] = [];
if (multiple) {
filesToSet = acceptedFiles;
} else {
const acceptedFile = acceptedFiles[0];
if (acceptedFile !== undefined) {
filesToSet = [acceptedFile];
}
}
if (filesToSet !== undefined) {
setSelectedFiles(filesToSet);
}
setDragActive(false);
if (name) {
setFieldValue(name, multiple ? filesToSet : filesToSet[0]);

View File

@@ -100,12 +100,14 @@ export function getUniqueIcons(docs: OnyxDocument[]): JSX.Element[] {
while (uniqueIcons.length < 3) {
// The last icon in the array
const lastIcon = uniqueIcons[uniqueIcons.length - 1];
// Clone it with a new key
uniqueIcons.push(
React.cloneElement(lastIcon, {
key: `${lastIcon.key}-dup-${uniqueIcons.length}`,
})
);
if (lastIcon) {
// Clone it with a new key
uniqueIcons.push(
React.cloneElement(lastIcon, {
key: `${lastIcon.key}-dup-${uniqueIcons.length}`,
})
);
}
}
// Slice to just the first 3 if there are more than 3
@@ -172,12 +174,14 @@ export function getUniqueFileIcons(files: FileResponse[]): JSX.Element[] {
while (uniqueIcons.length < 3) {
// The last icon in the array
const lastIcon = uniqueIcons[uniqueIcons.length - 1];
// Clone it with a new key
uniqueIcons.push(
React.cloneElement(lastIcon, {
key: `${lastIcon.key}-dup-${uniqueIcons.length}`,
})
);
if (lastIcon) {
// Clone it with a new key
uniqueIcons.push(
React.cloneElement(lastIcon, {
key: `${lastIcon.key}-dup-${uniqueIcons.length}`,
})
);
}
}
// Slice to just the first 3 if there are more than 3

View File

@@ -56,7 +56,10 @@ export const ChatProvider: React.FC<{
setFolders(
folders.map((folder) => {
if (folder.folder_id) {
folder.display_priority = displayPriorityMap[folder.folder_id];
const display_priority = displayPriorityMap[folder.folder_id];
if (display_priority !== undefined) {
folder.display_priority = display_priority;
}
}
return folder;
})

View File

@@ -50,6 +50,9 @@ function useLocalStorageState<T>(
return [state, setValue];
}
const firstLightExtensionImage = lightExtensionImages[0]!;
const firstDarkExtensionImage = darkExtensionImages[0]!;
export function NRFPreferencesProvider({
children,
}: {
@@ -62,12 +65,12 @@ export function NRFPreferencesProvider({
const [defaultLightBackgroundUrl, setDefaultLightBackgroundUrl] =
useLocalStorageState<string>(
LocalStorageKeys.LIGHT_BG_URL,
lightExtensionImages[0]
firstLightExtensionImage
);
const [defaultDarkBackgroundUrl, setDefaultDarkBackgroundUrl] =
useLocalStorageState<string>(
LocalStorageKeys.DARK_BG_URL,
darkExtensionImages[0]
firstDarkExtensionImage
);
const [shortcuts, setShortcuts] = useLocalStorageState<Shortcut[]>(
LocalStorageKeys.SHORTCUTS,

View File

@@ -108,10 +108,17 @@ export function ModelSelector({
const groupedModelOptions = modelOptions.reduce(
(acc, model) => {
const [type] = model.model_name.split("/");
if (!acc[type]) {
acc[type] = [];
if (type !== undefined) {
if (!acc[type]) {
acc[type] = [];
}
const acc_by_type = acc[type];
if (acc_by_type !== undefined) {
acc_by_type.push(model);
}
}
acc[type].push(model);
return acc;
},
{} as Record<string, HostedEmbeddingModel[]>

View File

@@ -16,7 +16,7 @@ export const ApiKeyForm = ({
setPopup: (popup: PopupSpec) => void;
hideSuccess?: boolean;
}) => {
const defaultProvider = providerOptions[0]?.name;
const defaultProvider = providerOptions[0]!.name;
const providerNameToIndexMap = new Map<string, number>();
providerOptions.forEach((provider, index) => {
providerNameToIndexMap.set(provider.name, index);

View File

@@ -1,7 +1,7 @@
import React from "react";
import { getDisplayNameForModel } from "@/lib/hooks";
import {
destructureValue,
parseLlmDescriptor,
modelSupportsImageInput,
structureValue,
} from "@/lib/llm/utils";
@@ -63,7 +63,7 @@ export const LLMSelector: React.FC<LLMSelectorProps> = ({
: null;
const destructuredCurrentValue = currentLlm
? destructureValue(currentLlm)
? parseLlmDescriptor(currentLlm)
: null;
const currentLlmName = destructuredCurrentValue?.modelName;

View File

@@ -22,7 +22,9 @@ export const usePopupFromQuery = (messages: PopupMessages) => {
if (messageValue && messageValue in messages) {
const popupMessage = messages[messageValue];
router.replace(window.location.pathname);
setPopup(popupMessage);
if (popupMessage !== undefined) {
setPopup(popupMessage);
}
}
}, []);

View File

@@ -74,12 +74,18 @@ export const buildDocumentSummaryDisplay = (
sections.push(["...", false, false]);
}
});
if (sections.length == 0) {
return;
}
let previousIsContinuation = sections[0][2];
let previousIsBold = sections[0][1];
const firstSection = sections[0];
if (firstSection === undefined) {
return;
}
let previousIsContinuation = firstSection[2];
let previousIsBold = firstSection[1];
let currentText = "";
const finalJSX = [] as (JSX.Element | string)[];
sections.forEach(([word, shouldBeBold, isContinuation], index) => {

View File

@@ -33,9 +33,21 @@ export function Citation({
children?: JSX.Element | string | null | ReactNode;
index?: number;
}) {
const innerText = children
? children?.toString().split("[")[1].split("]")[0]
: index;
let innerText = "";
if (index !== undefined) {
innerText = index.toString();
}
if (children) {
const childrenString = children.toString();
const childrenSegment1 = childrenString.split("[")[1];
if (childrenSegment1 !== undefined) {
const childrenSegment1_0 = childrenSegment1.split("]")[0];
if (childrenSegment1_0 !== undefined) {
innerText = childrenSegment1_0;
}
}
}
if (!document_info && !question_info) {
return <>{children}</>;

View File

@@ -42,8 +42,14 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
const results = await Promise.all(tasks);
let settings: Settings;
if (!results[0].ok) {
if (results[0].status === 403 || results[0].status === 401) {
const result_0 = results[0];
if (!result_0) {
throw new Error("Standard settings fetch failed.");
}
if (!result_0.ok) {
if (result_0.status === 403 || result_0.status === 401) {
settings = {
auto_scroll: true,
application_status: ApplicationStatus.ACTIVE,
@@ -59,41 +65,51 @@ export async function fetchSettingsSS(): Promise<CombinedSettings | null> {
} else {
throw new Error(
`fetchStandardSettingsSS failed: status=${
results[0].status
} body=${await results[0].text()}`
result_0.status
} body=${await result_0.text()}`
);
}
} else {
settings = await results[0].json();
settings = await result_0.json();
}
let enterpriseSettings: EnterpriseSettings | null = null;
if (tasks.length > 1) {
if (!results[1].ok) {
if (results[1].status !== 403 && results[1].status !== 401) {
const result_1 = results[1];
if (!result_1) {
throw new Error("fetchEnterpriseSettingsSS failed.");
}
if (!result_1.ok) {
if (result_1.status !== 403 && result_1.status !== 401) {
throw new Error(
`fetchEnterpriseSettingsSS failed: status=${
results[1].status
} body=${await results[1].text()}`
result_1.status
} body=${await result_1.text()}`
);
}
} else {
enterpriseSettings = await results[1].json();
enterpriseSettings = await result_1.json();
}
}
let customAnalyticsScript: string | null = null;
if (tasks.length > 2) {
if (!results[2].ok) {
if (results[2].status !== 403) {
const result_2 = results[2];
if (!result_2) {
throw new Error("fetchCustomAnalyticsScriptSS failed.");
}
if (!result_2.ok) {
if (result_2.status !== 403) {
throw new Error(
`fetchCustomAnalyticsScriptSS failed: status=${
results[2].status
} body=${await results[2].text()}`
result_2.status
} body=${await result_2.text()}`
);
}
} else {
customAnalyticsScript = await results[2].json();
customAnalyticsScript = await result_2.json();
}
}

View File

@@ -43,14 +43,21 @@ const CsvContent: React.FC<ContentComponentProps> = ({
const csvData = await response.text();
const rows = csvData.trim().split("\n");
const parsedHeaders = rows[0].split(",");
const firstRow = rows[0];
if (!firstRow) {
throw new Error("CSV file is empty");
}
const parsedHeaders = firstRow.split(",");
setHeaders(parsedHeaders);
const parsedData: Record<string, string>[] = rows.slice(1).map((row) => {
const values = row.split(",");
return parsedHeaders.reduce<Record<string, string>>(
(obj, header, index) => {
obj[header] = values[index];
const val = values[index];
if (val !== undefined) {
obj[header] = val;
}
return obj;
},
{}

View File

@@ -139,7 +139,7 @@ const ChartTooltipContent = React.forwardRef<
}
const [item] = payload;
const key = `${labelKey || item.dataKey || item.name || "value"}`;
const key = `${labelKey || item?.dataKey || item?.name || "value"}`;
const itemConfig = getPayloadConfigFromPayload(config, item, key);
const value =
!labelKey && typeof label === "string"

View File

@@ -199,9 +199,13 @@ function usePaginatedFetch<T extends PaginatedType>({
useEffect(() => {
const { batchNum, batchPageNum } = batchAndPageIndices;
if (cachedBatches[batchNum] && cachedBatches[batchNum][batchPageNum]) {
setCurrentPageData(cachedBatches[batchNum][batchPageNum]);
setIsLoading(false);
const cachedBatch = cachedBatches[batchNum];
if (cachedBatch !== undefined) {
const cachedBatchPage = cachedBatch[batchPageNum];
if (cachedBatchPage !== undefined) {
setCurrentPageData(cachedBatchPage);
setIsLoading(false);
}
}
}, [currentPage, cachedBatches, pagesPerBatch]);

View File

@@ -21,7 +21,7 @@ export function generateRandomIconShape(): GridShape {
.fill(null)
.map(() => Array(4).fill(false));
const centerSquares = [
const centerSquares: number[][] = [
[1, 1],
[1, 2],
[2, 1],
@@ -31,14 +31,33 @@ export function generateRandomIconShape(): GridShape {
shuffleArray(centerSquares);
const centerFillCount = Math.floor(Math.random() * 2) + 3; // 3 or 4
for (let i = 0; i < centerFillCount; i++) {
const [row, col] = centerSquares[i];
grid[row][col] = true;
const centerSquare: number[] | undefined = centerSquares[i];
if (centerSquare === undefined) {
continue;
}
const [row, col] = centerSquare;
if (row === undefined || col === undefined) {
continue;
}
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
grid_row[col] = true;
}
// Randomly fill remaining squares up to 10 total
const remainingSquares = [];
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (!grid[row][col]) {
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
if (!grid_row[col]) {
remainingSquares.push([row, col]);
}
}
@@ -47,15 +66,29 @@ export function generateRandomIconShape(): GridShape {
let filledSquares = centerFillCount;
for (const [row, col] of remainingSquares) {
if (row === undefined || col == undefined) {
continue;
}
if (filledSquares >= 10) break;
grid[row][col] = true;
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
grid_row[col] = true;
filledSquares++;
}
let path = "";
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (grid[row][col]) {
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
if (grid_row[col]) {
const x = col * 12;
const y = row * 12;
path += `M ${x} ${y} L ${x + 12} ${y} L ${x + 12} ${y + 12} L ${x} ${
@@ -72,7 +105,12 @@ function encodeGrid(grid: boolean[][]): number {
let encoded = 0;
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (grid[row][col]) {
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
if (grid_row[col]) {
encoded |= 1 << (row * 4 + col);
}
}
@@ -86,8 +124,13 @@ function decodeGrid(encoded: number): boolean[][] {
.map(() => Array(4).fill(false));
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
if (encoded & (1 << (row * 4 + col))) {
grid[row][col] = true;
grid_row[col] = true;
}
}
}
@@ -106,7 +149,12 @@ export function createSVG(
let path = "";
for (let row = 0; row < 4; row++) {
for (let col = 0; col < 4; col++) {
if (grid[row][col]) {
const grid_row = grid[row];
if (grid_row === undefined) {
continue;
}
if (grid_row[col]) {
const x = col * 12;
const y = row * 12;
path += `M ${x} ${y} L ${x + 12} ${y} L ${x + 12} ${y + 12} L ${x} ${

View File

@@ -48,10 +48,14 @@ export async function moveAssistantUp(
): Promise<boolean> {
const index = chosenAssistants.indexOf(assistantId);
if (index > 0) {
[chosenAssistants[index - 1], chosenAssistants[index]] = [
chosenAssistants[index],
chosenAssistants[index - 1],
];
const chosenAssistantPrev = chosenAssistants[index - 1];
const chosenAssistant = chosenAssistants[index];
if (chosenAssistantPrev === undefined || chosenAssistant === undefined) {
return false;
}
chosenAssistants[index - 1] = chosenAssistant;
chosenAssistants[index] = chosenAssistantPrev;
return updateUserAssistantList(chosenAssistants);
}
return false;
@@ -63,10 +67,15 @@ export async function moveAssistantDown(
): Promise<boolean> {
const index = chosenAssistants.indexOf(assistantId);
if (index < chosenAssistants.length - 1) {
[chosenAssistants[index + 1], chosenAssistants[index]] = [
chosenAssistants[index],
chosenAssistants[index + 1],
];
const chosenAssistantNext = chosenAssistants[index + 1];
const chosenAssistant = chosenAssistants[index];
if (chosenAssistantNext === undefined || chosenAssistant === undefined) {
return false;
}
chosenAssistants[index + 1] = chosenAssistant;
chosenAssistants[index] = chosenAssistantNext;
return updateUserAssistantList(chosenAssistants);
}
return false;

View File

@@ -135,6 +135,7 @@ export async function deleteConnectorIfExistsAndIsUnlinked({
);
if (
matchingConnectors.length > 0 &&
matchingConnectors[0] &&
matchingConnectors[0].credential_ids.length === 0
) {
const errorMsg = await deleteConnector(matchingConnectors[0].id);

View File

@@ -18,6 +18,10 @@ export function objectsAreEquivalent(
for (let i = 0; i < aProps.length; i++) {
const propName = aProps[i];
if (propName === undefined) {
continue;
}
if (a[propName] !== b[propName]) {
return false;
}
@@ -41,8 +45,20 @@ export function isEventWithinRef(
if (!ref.current) return false;
const rect = ref.current.getBoundingClientRect();
const clientX = "touches" in event ? event.touches[0].clientX : event.clientX;
const clientY = "touches" in event ? event.touches[0].clientY : event.clientY;
let clientX: number;
let clientY: number;
if (event instanceof TouchEvent) {
const touches_0 = event.touches[0];
if (touches_0 === undefined) {
throw new Error("Touch event must exist!");
}
clientX = touches_0.clientX;
clientY = touches_0.clientY;
} else {
clientX = event.clientX;
clientY = event.clientY;
}
return (
clientX >= rect.left &&

View File

@@ -0,0 +1,2 @@
- Generated Files
* Generated files live here. This directory should be git ignored.

View File

@@ -92,7 +92,7 @@ export const filterUploadedCredentials = <
credential.credential_json.authentication_method !== "oauth_interactive"
);
if (uploadedCredentials.length > 0) {
if (uploadedCredentials.length > 0 && uploadedCredentials[0]) {
credential_id = uploadedCredentials[0].id;
}
}

View File

@@ -14,7 +14,7 @@ import { useContext, useEffect, useMemo, useState } from "react";
import { DateRangePickerValue } from "@/components/dateRangeSelectors/AdminDateRangeSelector";
import { SourceMetadata } from "./search/interfaces";
import {
destructureValue,
parseLlmDescriptor,
findProviderForModel,
structureValue,
} from "./llm/utils";
@@ -480,7 +480,7 @@ export function useLlmManager(
modelName: string | null | undefined
): LlmDescriptor => {
if (modelName) {
const model = destructureValue(modelName);
const model = parseLlmDescriptor(modelName);
if (!(model.modelName && model.modelName.length > 0)) {
const provider = llmProviders.find((p) =>
p.model_configurations
@@ -810,7 +810,12 @@ export function getDisplayNameForModel(modelName: string): string {
if (modelName.startsWith("bedrock/")) {
const parts = modelName.split("/");
const lastPart = parts[parts.length - 1];
return MODEL_DISPLAY_NAMES[lastPart] || lastPart;
if (lastPart === undefined) {
return "";
}
const displayName = MODEL_DISPLAY_NAMES[lastPart];
return displayName || lastPart;
}
return MODEL_DISPLAY_NAMES[modelName] || modelName;

View File

@@ -75,12 +75,16 @@ export const structureValue = (
return `${name}__${provider}__${modelName}`;
};
export const destructureValue = (value: string): LlmDescriptor => {
export const parseLlmDescriptor = (value: string): LlmDescriptor => {
const [displayName, provider, modelName] = value.split("__");
if (displayName === undefined) {
return { name: "Unknown", provider: "", modelName: "" };
}
return {
name: displayName,
provider,
modelName,
provider: provider ?? "",
modelName: modelName ?? "",
};
};

View File

@@ -7,8 +7,8 @@ export const buildFilters = (
documentSets: string[],
timeRange: DateRangePickerValue | null,
tags: Tag[],
userFileIds?: number[] | null,
userFolderIds?: number[] | null
userFileIds?: number[] | null
// userFolderIds?: number[] | null
): Filters => {
const filters = {
source_type:

View File

@@ -28,7 +28,7 @@ export function transformLinkUri(href: string) {
) {
return url;
}
} catch (e) {
} catch {
// If it's not a valid URL with protocol, return the original href
return href;
}

View File

@@ -21,7 +21,7 @@ export class UrlBuilder {
constructor(baseUrl: string) {
try {
this.url = new URL(baseUrl);
} catch (e) {
} catch {
// Handle relative URLs by prepending a base
this.url = new URL(baseUrl, "http://placeholder.com");
}

View File

@@ -8,8 +8,9 @@
"forceConsistentCasingInFileNames": true,
"noEmit": true,
"esModuleInterop": true,
"module": "esnext",
"module": "ESNext",
"moduleResolution": "node",
"noUncheckedIndexedAccess": true,
"resolveJsonModule": true,
"isolatedModules": true,
"jsx": "preserve",