mirror of
https://github.com/MODSetter/SurfSense.git
synced 2026-04-25 00:36:31 +02:00
feat: add processing mode support for document uploads and ETL pipeline, improded error handling ux
Some checks are pending
Build and Push Docker Images / tag_release (push) Waiting to run
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Blocked by required conditions
Some checks are pending
Build and Push Docker Images / tag_release (push) Waiting to run
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_backend, ./surfsense_backend/Dockerfile, backend, surfsense-backend, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-24.04-arm, linux/arm64, arm64) (push) Blocked by required conditions
Build and Push Docker Images / build (./surfsense_web, ./surfsense_web/Dockerfile, web, surfsense-web, ubuntu-latest, linux/amd64, amd64) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (backend, surfsense-backend) (push) Blocked by required conditions
Build and Push Docker Images / create_manifest (web, surfsense-web) (push) Blocked by required conditions
- Introduced a `ProcessingMode` enum to differentiate between basic and premium processing modes. - Updated `EtlRequest` to include a `processing_mode` field, defaulting to basic. - Enhanced ETL pipeline services to utilize the selected processing mode for Azure Document Intelligence and LlamaCloud parsing. - Modified various routes and services to handle processing mode, affecting document upload and indexing tasks. - Improved error handling and logging to include processing mode details. - Added tests to validate processing mode functionality and its impact on ETL operations.
This commit is contained in:
parent
b659f41bab
commit
656e061f84
104 changed files with 1900 additions and 909 deletions
|
|
@ -1,6 +1,5 @@
|
|||
"use client";
|
||||
|
||||
import type React from "react";
|
||||
import { useAtomValue } from "jotai";
|
||||
import {
|
||||
Bot,
|
||||
|
|
@ -11,12 +10,13 @@ import {
|
|||
ChevronUp,
|
||||
Edit3,
|
||||
ImageIcon,
|
||||
ScanEye,
|
||||
Layers,
|
||||
Plus,
|
||||
ScanEye,
|
||||
Search,
|
||||
Zap,
|
||||
} from "lucide-react";
|
||||
import type React from "react";
|
||||
import { Fragment, useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { toast } from "sonner";
|
||||
import {
|
||||
|
|
@ -178,8 +178,7 @@ function formatProviderName(provider: string): string {
|
|||
const key = provider.toUpperCase();
|
||||
return (
|
||||
PROVIDER_NAMES[key] ??
|
||||
provider.charAt(0).toUpperCase() +
|
||||
provider.slice(1).toLowerCase().replace(/_/g, " ")
|
||||
provider.charAt(0).toUpperCase() + provider.slice(1).toLowerCase().replace(/_/g, " ")
|
||||
);
|
||||
}
|
||||
|
||||
|
|
@ -202,14 +201,12 @@ interface ConfigBase {
|
|||
function filterAndScore<T extends ConfigBase>(
|
||||
configs: T[],
|
||||
selectedProvider: string,
|
||||
searchQuery: string,
|
||||
searchQuery: string
|
||||
): T[] {
|
||||
let result = configs;
|
||||
|
||||
if (selectedProvider !== "all") {
|
||||
result = result.filter(
|
||||
(c) => c.provider.toUpperCase() === selectedProvider,
|
||||
);
|
||||
result = result.filter((c) => c.provider.toUpperCase() === selectedProvider);
|
||||
}
|
||||
|
||||
if (!searchQuery.trim()) return result;
|
||||
|
|
@ -218,9 +215,7 @@ function filterAndScore<T extends ConfigBase>(
|
|||
const tokens = normalized.split(/\s+/).filter(Boolean);
|
||||
|
||||
const scored = result.map((c) => {
|
||||
const aggregate = normalizeText(
|
||||
[c.name, c.model_name, c.provider].join(" "),
|
||||
);
|
||||
const aggregate = normalizeText([c.name, c.model_name, c.provider].join(" "));
|
||||
let score = 0;
|
||||
if (aggregate.includes(normalized)) score += 5;
|
||||
for (const token of tokens) {
|
||||
|
|
@ -244,20 +239,11 @@ interface DisplayItem {
|
|||
// ─── Component ──────────────────────────────────────────────────────
|
||||
|
||||
interface ModelSelectorProps {
|
||||
onEditLLM: (
|
||||
config: NewLLMConfigPublic | GlobalNewLLMConfig,
|
||||
isGlobal: boolean,
|
||||
) => void;
|
||||
onEditLLM: (config: NewLLMConfigPublic | GlobalNewLLMConfig, isGlobal: boolean) => void;
|
||||
onAddNewLLM: (provider?: string) => void;
|
||||
onEditImage?: (
|
||||
config: ImageGenerationConfig | GlobalImageGenConfig,
|
||||
isGlobal: boolean,
|
||||
) => void;
|
||||
onEditImage?: (config: ImageGenerationConfig | GlobalImageGenConfig, isGlobal: boolean) => void;
|
||||
onAddNewImage?: (provider?: string) => void;
|
||||
onEditVision?: (
|
||||
config: VisionLLMConfig | GlobalVisionLLMConfig,
|
||||
isGlobal: boolean,
|
||||
) => void;
|
||||
onEditVision?: (config: VisionLLMConfig | GlobalVisionLLMConfig, isGlobal: boolean) => void;
|
||||
onAddNewVision?: (provider?: string) => void;
|
||||
className?: string;
|
||||
}
|
||||
|
|
@ -272,9 +258,7 @@ export function ModelSelector({
|
|||
className,
|
||||
}: ModelSelectorProps) {
|
||||
const [open, setOpen] = useState(false);
|
||||
const [activeTab, setActiveTab] = useState<"llm" | "image" | "vision">(
|
||||
"llm",
|
||||
);
|
||||
const [activeTab, setActiveTab] = useState<"llm" | "image" | "vision">("llm");
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [selectedProvider, setSelectedProvider] = useState<string>("all");
|
||||
const [focusedIndex, setFocusedIndex] = useState(-1);
|
||||
|
|
@ -292,18 +276,21 @@ export function ModelSelector({
|
|||
setModelScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle");
|
||||
}, []);
|
||||
|
||||
const handleSidebarScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
|
||||
const el = e.currentTarget;
|
||||
if (isMobile) {
|
||||
const atStart = el.scrollLeft <= 2;
|
||||
const atEnd = el.scrollWidth - el.scrollLeft - el.clientWidth <= 2;
|
||||
setSidebarScrollPos(atStart ? "top" : atEnd ? "bottom" : "middle");
|
||||
} else {
|
||||
const atTop = el.scrollTop <= 2;
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2;
|
||||
setSidebarScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle");
|
||||
}
|
||||
}, [isMobile]);
|
||||
const handleSidebarScroll = useCallback(
|
||||
(e: React.UIEvent<HTMLDivElement>) => {
|
||||
const el = e.currentTarget;
|
||||
if (isMobile) {
|
||||
const atStart = el.scrollLeft <= 2;
|
||||
const atEnd = el.scrollWidth - el.scrollLeft - el.clientWidth <= 2;
|
||||
setSidebarScrollPos(atStart ? "top" : atEnd ? "bottom" : "middle");
|
||||
} else {
|
||||
const atTop = el.scrollTop <= 2;
|
||||
const atBottom = el.scrollHeight - el.scrollTop - el.clientHeight <= 2;
|
||||
setSidebarScrollPos(atTop ? "top" : atBottom ? "bottom" : "middle");
|
||||
}
|
||||
},
|
||||
[isMobile]
|
||||
);
|
||||
|
||||
// Reset search + provider when tab changes
|
||||
// biome-ignore lint/correctness/useExhaustiveDependencies: activeTab is intentionally used as a trigger
|
||||
|
|
@ -344,22 +331,18 @@ export function ModelSelector({
|
|||
}, [open, isMobile, activeTab]);
|
||||
|
||||
// ─── Data ───
|
||||
const { data: llmUserConfigs, isLoading: llmUserLoading } =
|
||||
useAtomValue(newLLMConfigsAtom);
|
||||
const { data: llmUserConfigs, isLoading: llmUserLoading } = useAtomValue(newLLMConfigsAtom);
|
||||
const { data: llmGlobalConfigs, isLoading: llmGlobalLoading } =
|
||||
useAtomValue(globalNewLLMConfigsAtom);
|
||||
const { data: preferences, isLoading: prefsLoading } =
|
||||
useAtomValue(llmPreferencesAtom);
|
||||
const { data: preferences, isLoading: prefsLoading } = useAtomValue(llmPreferencesAtom);
|
||||
const searchSpaceId = useAtomValue(activeSearchSpaceIdAtom);
|
||||
const { mutateAsync: updatePreferences } = useAtomValue(
|
||||
updateLLMPreferencesMutationAtom,
|
||||
);
|
||||
const { mutateAsync: updatePreferences } = useAtomValue(updateLLMPreferencesMutationAtom);
|
||||
const { data: imageGlobalConfigs, isLoading: imageGlobalLoading } =
|
||||
useAtomValue(globalImageGenConfigsAtom);
|
||||
const { data: imageUserConfigs, isLoading: imageUserLoading } =
|
||||
useAtomValue(imageGenConfigsAtom);
|
||||
const { data: visionGlobalConfigs, isLoading: visionGlobalLoading } =
|
||||
useAtomValue(globalVisionLLMConfigsAtom);
|
||||
const { data: imageUserConfigs, isLoading: imageUserLoading } = useAtomValue(imageGenConfigsAtom);
|
||||
const { data: visionGlobalConfigs, isLoading: visionGlobalLoading } = useAtomValue(
|
||||
globalVisionLLMConfigsAtom
|
||||
);
|
||||
const { data: visionUserConfigs, isLoading: visionUserLoading } =
|
||||
useAtomValue(visionLLMConfigsAtom);
|
||||
|
||||
|
|
@ -382,9 +365,7 @@ export function ModelSelector({
|
|||
}, [preferences, llmGlobalConfigs, llmUserConfigs]);
|
||||
|
||||
const isLLMAutoMode =
|
||||
currentLLMConfig &&
|
||||
"is_auto_mode" in currentLLMConfig &&
|
||||
currentLLMConfig.is_auto_mode;
|
||||
currentLLMConfig && "is_auto_mode" in currentLLMConfig && currentLLMConfig.is_auto_mode;
|
||||
|
||||
const currentImageConfig = useMemo(() => {
|
||||
if (!preferences) return null;
|
||||
|
|
@ -398,9 +379,7 @@ export function ModelSelector({
|
|||
}, [preferences, imageGlobalConfigs, imageUserConfigs]);
|
||||
|
||||
const isImageAutoMode =
|
||||
currentImageConfig &&
|
||||
"is_auto_mode" in currentImageConfig &&
|
||||
currentImageConfig.is_auto_mode;
|
||||
currentImageConfig && "is_auto_mode" in currentImageConfig && currentImageConfig.is_auto_mode;
|
||||
|
||||
const currentVisionConfig = useMemo(() => {
|
||||
if (!preferences) return null;
|
||||
|
|
@ -420,83 +399,47 @@ export function ModelSelector({
|
|||
|
||||
// ─── Filtered configs (separate global / user for section headers) ───
|
||||
const filteredLLMGlobal = useMemo(
|
||||
() =>
|
||||
filterAndScore(llmGlobalConfigs ?? [], selectedProvider, searchQuery),
|
||||
[llmGlobalConfigs, selectedProvider, searchQuery],
|
||||
() => filterAndScore(llmGlobalConfigs ?? [], selectedProvider, searchQuery),
|
||||
[llmGlobalConfigs, selectedProvider, searchQuery]
|
||||
);
|
||||
const filteredLLMUser = useMemo(
|
||||
() =>
|
||||
filterAndScore(llmUserConfigs ?? [], selectedProvider, searchQuery),
|
||||
[llmUserConfigs, selectedProvider, searchQuery],
|
||||
() => filterAndScore(llmUserConfigs ?? [], selectedProvider, searchQuery),
|
||||
[llmUserConfigs, selectedProvider, searchQuery]
|
||||
);
|
||||
const filteredImageGlobal = useMemo(
|
||||
() =>
|
||||
filterAndScore(
|
||||
imageGlobalConfigs ?? [],
|
||||
selectedProvider,
|
||||
searchQuery,
|
||||
),
|
||||
[imageGlobalConfigs, selectedProvider, searchQuery],
|
||||
() => filterAndScore(imageGlobalConfigs ?? [], selectedProvider, searchQuery),
|
||||
[imageGlobalConfigs, selectedProvider, searchQuery]
|
||||
);
|
||||
const filteredImageUser = useMemo(
|
||||
() =>
|
||||
filterAndScore(
|
||||
imageUserConfigs ?? [],
|
||||
selectedProvider,
|
||||
searchQuery,
|
||||
),
|
||||
[imageUserConfigs, selectedProvider, searchQuery],
|
||||
() => filterAndScore(imageUserConfigs ?? [], selectedProvider, searchQuery),
|
||||
[imageUserConfigs, selectedProvider, searchQuery]
|
||||
);
|
||||
const filteredVisionGlobal = useMemo(
|
||||
() =>
|
||||
filterAndScore(
|
||||
visionGlobalConfigs ?? [],
|
||||
selectedProvider,
|
||||
searchQuery,
|
||||
),
|
||||
[visionGlobalConfigs, selectedProvider, searchQuery],
|
||||
() => filterAndScore(visionGlobalConfigs ?? [], selectedProvider, searchQuery),
|
||||
[visionGlobalConfigs, selectedProvider, searchQuery]
|
||||
);
|
||||
const filteredVisionUser = useMemo(
|
||||
() =>
|
||||
filterAndScore(
|
||||
visionUserConfigs ?? [],
|
||||
selectedProvider,
|
||||
searchQuery,
|
||||
),
|
||||
[visionUserConfigs, selectedProvider, searchQuery],
|
||||
() => filterAndScore(visionUserConfigs ?? [], selectedProvider, searchQuery),
|
||||
[visionUserConfigs, selectedProvider, searchQuery]
|
||||
);
|
||||
|
||||
// Combined display list for keyboard navigation
|
||||
const currentDisplayItems: DisplayItem[] = useMemo(() => {
|
||||
const toItems = (
|
||||
configs: ConfigBase[],
|
||||
isGlobal: boolean,
|
||||
): DisplayItem[] =>
|
||||
const toItems = (configs: ConfigBase[], isGlobal: boolean): DisplayItem[] =>
|
||||
configs.map((c) => ({
|
||||
config: c as ConfigBase & Record<string, unknown>,
|
||||
isGlobal,
|
||||
isAutoMode:
|
||||
isGlobal &&
|
||||
"is_auto_mode" in c &&
|
||||
!!(c as Record<string, unknown>).is_auto_mode,
|
||||
isGlobal && "is_auto_mode" in c && !!(c as Record<string, unknown>).is_auto_mode,
|
||||
}));
|
||||
|
||||
switch (activeTab) {
|
||||
case "llm":
|
||||
return [
|
||||
...toItems(filteredLLMGlobal, true),
|
||||
...toItems(filteredLLMUser, false),
|
||||
];
|
||||
return [...toItems(filteredLLMGlobal, true), ...toItems(filteredLLMUser, false)];
|
||||
case "image":
|
||||
return [
|
||||
...toItems(filteredImageGlobal, true),
|
||||
...toItems(filteredImageUser, false),
|
||||
];
|
||||
return [...toItems(filteredImageGlobal, true), ...toItems(filteredImageUser, false)];
|
||||
case "vision":
|
||||
return [
|
||||
...toItems(filteredVisionGlobal, true),
|
||||
...toItems(filteredVisionUser, false),
|
||||
];
|
||||
return [...toItems(filteredVisionGlobal, true), ...toItems(filteredVisionUser, false)];
|
||||
}
|
||||
}, [
|
||||
activeTab,
|
||||
|
|
@ -513,19 +456,10 @@ export function ModelSelector({
|
|||
const configuredProviderSet = useMemo(() => {
|
||||
const configs =
|
||||
activeTab === "llm"
|
||||
? [
|
||||
...(llmGlobalConfigs ?? []),
|
||||
...(llmUserConfigs ?? []),
|
||||
]
|
||||
? [...(llmGlobalConfigs ?? []), ...(llmUserConfigs ?? [])]
|
||||
: activeTab === "image"
|
||||
? [
|
||||
...(imageGlobalConfigs ?? []),
|
||||
...(imageUserConfigs ?? []),
|
||||
]
|
||||
: [
|
||||
...(visionGlobalConfigs ?? []),
|
||||
...(visionUserConfigs ?? []),
|
||||
];
|
||||
? [...(imageGlobalConfigs ?? []), ...(imageUserConfigs ?? [])]
|
||||
: [...(visionGlobalConfigs ?? []), ...(visionUserConfigs ?? [])];
|
||||
const set = new Set<string>();
|
||||
for (const c of configs) {
|
||||
if (c.provider) set.add(c.provider.toUpperCase());
|
||||
|
|
@ -544,31 +478,18 @@ export function ModelSelector({
|
|||
// Show only providers valid for the active tab; configured ones first
|
||||
const activeProviders = useMemo(() => {
|
||||
const tabKeys = PROVIDER_KEYS_BY_TAB[activeTab] ?? LLM_PROVIDER_KEYS;
|
||||
const configured = tabKeys.filter((p) =>
|
||||
configuredProviderSet.has(p),
|
||||
);
|
||||
const unconfigured = tabKeys.filter(
|
||||
(p) => !configuredProviderSet.has(p),
|
||||
);
|
||||
const configured = tabKeys.filter((p) => configuredProviderSet.has(p));
|
||||
const unconfigured = tabKeys.filter((p) => !configuredProviderSet.has(p));
|
||||
return ["all", ...configured, ...unconfigured];
|
||||
}, [activeTab, configuredProviderSet]);
|
||||
|
||||
const providerModelCounts = useMemo(() => {
|
||||
const allConfigs =
|
||||
activeTab === "llm"
|
||||
? [
|
||||
...(llmGlobalConfigs ?? []),
|
||||
...(llmUserConfigs ?? []),
|
||||
]
|
||||
? [...(llmGlobalConfigs ?? []), ...(llmUserConfigs ?? [])]
|
||||
: activeTab === "image"
|
||||
? [
|
||||
...(imageGlobalConfigs ?? []),
|
||||
...(imageUserConfigs ?? []),
|
||||
]
|
||||
: [
|
||||
...(visionGlobalConfigs ?? []),
|
||||
...(visionUserConfigs ?? []),
|
||||
];
|
||||
? [...(imageGlobalConfigs ?? []), ...(imageUserConfigs ?? [])]
|
||||
: [...(visionGlobalConfigs ?? []), ...(visionUserConfigs ?? [])];
|
||||
const counts: Record<string, number> = { all: allConfigs.length };
|
||||
for (const c of allConfigs) {
|
||||
const p = c.provider.toUpperCase();
|
||||
|
|
@ -607,7 +528,7 @@ export function ModelSelector({
|
|||
toast.error("Failed to switch model");
|
||||
}
|
||||
},
|
||||
[currentLLMConfig, searchSpaceId, updatePreferences],
|
||||
[currentLLMConfig, searchSpaceId, updatePreferences]
|
||||
);
|
||||
|
||||
const handleSelectImage = useCallback(
|
||||
|
|
@ -631,7 +552,7 @@ export function ModelSelector({
|
|||
toast.error("Failed to switch image model");
|
||||
}
|
||||
},
|
||||
[currentImageConfig, searchSpaceId, updatePreferences],
|
||||
[currentImageConfig, searchSpaceId, updatePreferences]
|
||||
);
|
||||
|
||||
const handleSelectVision = useCallback(
|
||||
|
|
@ -655,16 +576,14 @@ export function ModelSelector({
|
|||
toast.error("Failed to switch vision model");
|
||||
}
|
||||
},
|
||||
[currentVisionConfig, searchSpaceId, updatePreferences],
|
||||
[currentVisionConfig, searchSpaceId, updatePreferences]
|
||||
);
|
||||
|
||||
const handleSelectItem = useCallback(
|
||||
(item: DisplayItem) => {
|
||||
switch (activeTab) {
|
||||
case "llm":
|
||||
handleSelectLLM(
|
||||
item.config as NewLLMConfigPublic | GlobalNewLLMConfig,
|
||||
);
|
||||
handleSelectLLM(item.config as NewLLMConfigPublic | GlobalNewLLMConfig);
|
||||
break;
|
||||
case "image":
|
||||
handleSelectImage(item.config.id);
|
||||
|
|
@ -674,7 +593,7 @@ export function ModelSelector({
|
|||
break;
|
||||
}
|
||||
},
|
||||
[activeTab, handleSelectLLM, handleSelectImage, handleSelectVision],
|
||||
[activeTab, handleSelectLLM, handleSelectImage, handleSelectVision]
|
||||
);
|
||||
|
||||
const handleEditItem = useCallback(
|
||||
|
|
@ -683,26 +602,17 @@ export function ModelSelector({
|
|||
setOpen(false);
|
||||
switch (activeTab) {
|
||||
case "llm":
|
||||
onEditLLM(
|
||||
item.config as NewLLMConfigPublic | GlobalNewLLMConfig,
|
||||
item.isGlobal,
|
||||
);
|
||||
onEditLLM(item.config as NewLLMConfigPublic | GlobalNewLLMConfig, item.isGlobal);
|
||||
break;
|
||||
case "image":
|
||||
onEditImage?.(
|
||||
item.config as ImageGenerationConfig | GlobalImageGenConfig,
|
||||
item.isGlobal,
|
||||
);
|
||||
onEditImage?.(item.config as ImageGenerationConfig | GlobalImageGenConfig, item.isGlobal);
|
||||
break;
|
||||
case "vision":
|
||||
onEditVision?.(
|
||||
item.config as VisionLLMConfig | GlobalVisionLLMConfig,
|
||||
item.isGlobal,
|
||||
);
|
||||
onEditVision?.(item.config as VisionLLMConfig | GlobalVisionLLMConfig, item.isGlobal);
|
||||
break;
|
||||
}
|
||||
},
|
||||
[activeTab, onEditLLM, onEditImage, onEditVision],
|
||||
[activeTab, onEditLLM, onEditImage, onEditVision]
|
||||
);
|
||||
|
||||
// ─── Keyboard navigation ───
|
||||
|
|
@ -713,8 +623,7 @@ export function ModelSelector({
|
|||
|
||||
useEffect(() => {
|
||||
if (focusedIndex < 0 || !modelListRef.current) return;
|
||||
const items =
|
||||
modelListRef.current.querySelectorAll("[data-model-index]");
|
||||
const items = modelListRef.current.querySelectorAll("[data-model-index]");
|
||||
items[focusedIndex]?.scrollIntoView({
|
||||
block: "nearest",
|
||||
behavior: "smooth",
|
||||
|
|
@ -734,13 +643,11 @@ export function ModelSelector({
|
|||
if (e.key === "ArrowLeft") {
|
||||
next = idx > 0 ? idx - 1 : providers.length - 1;
|
||||
} else {
|
||||
next =
|
||||
idx < providers.length - 1 ? idx + 1 : 0;
|
||||
next = idx < providers.length - 1 ? idx + 1 : 0;
|
||||
}
|
||||
setSelectedProvider(providers[next]);
|
||||
if (providerSidebarRef.current) {
|
||||
const buttons =
|
||||
providerSidebarRef.current.querySelectorAll("button");
|
||||
const buttons = providerSidebarRef.current.querySelectorAll("button");
|
||||
buttons[next]?.scrollIntoView({
|
||||
block: "nearest",
|
||||
inline: "nearest",
|
||||
|
|
@ -755,15 +662,11 @@ export function ModelSelector({
|
|||
switch (e.key) {
|
||||
case "ArrowDown":
|
||||
e.preventDefault();
|
||||
setFocusedIndex((prev) =>
|
||||
prev < count - 1 ? prev + 1 : 0,
|
||||
);
|
||||
setFocusedIndex((prev) => (prev < count - 1 ? prev + 1 : 0));
|
||||
break;
|
||||
case "ArrowUp":
|
||||
e.preventDefault();
|
||||
setFocusedIndex((prev) =>
|
||||
prev > 0 ? prev - 1 : count - 1,
|
||||
);
|
||||
setFocusedIndex((prev) => (prev > 0 ? prev - 1 : count - 1));
|
||||
break;
|
||||
case "Enter":
|
||||
e.preventDefault();
|
||||
|
|
@ -781,13 +684,7 @@ export function ModelSelector({
|
|||
break;
|
||||
}
|
||||
},
|
||||
[
|
||||
currentDisplayItems,
|
||||
focusedIndex,
|
||||
activeProviders,
|
||||
selectedProvider,
|
||||
handleSelectItem,
|
||||
],
|
||||
[currentDisplayItems, focusedIndex, activeProviders, selectedProvider, handleSelectItem]
|
||||
);
|
||||
|
||||
// ─── Render: Provider sidebar ───
|
||||
|
|
@ -798,7 +695,7 @@ export function ModelSelector({
|
|||
<div
|
||||
className={cn(
|
||||
"shrink-0 border-border/50 flex",
|
||||
isMobile ? "flex-row items-center border-b border-border/40" : "flex-col w-10 border-r",
|
||||
isMobile ? "flex-row items-center border-b border-border/40" : "flex-col w-10 border-r"
|
||||
)}
|
||||
>
|
||||
{!isMobile && sidebarScrollPos !== "top" && (
|
||||
|
|
@ -817,29 +714,29 @@ export function ModelSelector({
|
|||
className={cn(
|
||||
isMobile
|
||||
? "flex flex-row gap-0.5 px-1 py-1.5 overflow-x-auto [&::-webkit-scrollbar]:h-0 [&::-webkit-scrollbar-track]:bg-transparent"
|
||||
: "flex flex-col gap-0.5 p-1 overflow-y-auto flex-1 [&::-webkit-scrollbar]:w-0 [&::-webkit-scrollbar-track]:bg-transparent",
|
||||
: "flex flex-col gap-0.5 p-1 overflow-y-auto flex-1 [&::-webkit-scrollbar]:w-0 [&::-webkit-scrollbar-track]:bg-transparent"
|
||||
)}
|
||||
style={isMobile ? {
|
||||
maskImage: `linear-gradient(to right, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 24px, black calc(100% - 24px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
WebkitMaskImage: `linear-gradient(to right, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 24px, black calc(100% - 24px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
} : {
|
||||
maskImage: `linear-gradient(to bottom, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 32px, black calc(100% - 32px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
WebkitMaskImage: `linear-gradient(to bottom, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 32px, black calc(100% - 32px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
}}
|
||||
style={
|
||||
isMobile
|
||||
? {
|
||||
maskImage: `linear-gradient(to right, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 24px, black calc(100% - 24px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
WebkitMaskImage: `linear-gradient(to right, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 24px, black calc(100% - 24px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
}
|
||||
: {
|
||||
maskImage: `linear-gradient(to bottom, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 32px, black calc(100% - 32px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
WebkitMaskImage: `linear-gradient(to bottom, ${sidebarScrollPos === "top" ? "black" : "transparent"}, black 32px, black calc(100% - 32px), ${sidebarScrollPos === "bottom" ? "black" : "transparent"})`,
|
||||
}
|
||||
}
|
||||
>
|
||||
{activeProviders.map((provider, idx) => {
|
||||
const isAll = provider === "all";
|
||||
const isActive = selectedProvider === provider;
|
||||
const count = providerModelCounts[provider] || 0;
|
||||
const isConfigured =
|
||||
isAll || configuredProviderSet.has(provider);
|
||||
const isConfigured = isAll || configuredProviderSet.has(provider);
|
||||
|
||||
// Separator between configured and unconfigured providers
|
||||
// idx 0 is "all", configured run from 1..configuredCount, unconfigured start at configuredCount+1
|
||||
const showSeparator =
|
||||
!isAll &&
|
||||
idx === configuredCount + 1 &&
|
||||
configuredCount > 0;
|
||||
const showSeparator = !isAll && idx === configuredCount + 1 && configuredCount > 0;
|
||||
|
||||
return (
|
||||
<Fragment key={provider}>
|
||||
|
|
@ -853,20 +750,16 @@ export function ModelSelector({
|
|||
<TooltipTrigger asChild>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() =>
|
||||
setSelectedProvider(provider)
|
||||
}
|
||||
onClick={() => setSelectedProvider(provider)}
|
||||
tabIndex={-1}
|
||||
className={cn(
|
||||
"relative flex items-center justify-center rounded-md transition-all duration-150",
|
||||
isMobile
|
||||
? "p-2 shrink-0"
|
||||
: "p-1.5 w-full",
|
||||
isMobile ? "p-2 shrink-0" : "p-1.5 w-full",
|
||||
isActive
|
||||
? "bg-primary/10 text-primary"
|
||||
: isConfigured
|
||||
? "hover:bg-accent/60 text-muted-foreground hover:text-foreground"
|
||||
: "opacity-50 hover:opacity-80 hover:bg-accent/40 text-muted-foreground",
|
||||
: "opacity-50 hover:opacity-80 hover:bg-accent/40 text-muted-foreground"
|
||||
)}
|
||||
>
|
||||
{isAll ? (
|
||||
|
|
@ -878,19 +771,9 @@ export function ModelSelector({
|
|||
)}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent
|
||||
side={
|
||||
isMobile ? "bottom" : "right"
|
||||
}
|
||||
>
|
||||
{isAll
|
||||
? "All Models"
|
||||
: formatProviderName(
|
||||
provider,
|
||||
)}
|
||||
{isConfigured
|
||||
? ` (${count})`
|
||||
: " (not configured)"}
|
||||
<TooltipContent side={isMobile ? "bottom" : "right"}>
|
||||
{isAll ? "All Models" : formatProviderName(provider)}
|
||||
{isConfigured ? ` (${count})` : " (not configured)"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</Fragment>
|
||||
|
|
@ -927,8 +810,7 @@ export function ModelSelector({
|
|||
const { config, isAutoMode } = item;
|
||||
const isSelected = getSelectedId() === config.id;
|
||||
const isFocused = focusedIndex === index;
|
||||
const hasCitations =
|
||||
"citations_enabled" in config && !!config.citations_enabled;
|
||||
const hasCitations = "citations_enabled" in config && !!config.citations_enabled;
|
||||
|
||||
return (
|
||||
<div
|
||||
|
|
@ -938,19 +820,23 @@ export function ModelSelector({
|
|||
tabIndex={isMobile ? -1 : 0}
|
||||
aria-selected={isSelected}
|
||||
onClick={() => handleSelectItem(item)}
|
||||
onKeyDown={isMobile ? undefined : (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleSelectItem(item);
|
||||
}
|
||||
}}
|
||||
onKeyDown={
|
||||
isMobile
|
||||
? undefined
|
||||
: (e) => {
|
||||
if (e.key === "Enter" || e.key === " ") {
|
||||
e.preventDefault();
|
||||
handleSelectItem(item);
|
||||
}
|
||||
}
|
||||
}
|
||||
onMouseEnter={() => setFocusedIndex(index)}
|
||||
className={cn(
|
||||
"group flex items-center gap-2.5 px-3 py-2 rounded-xl cursor-pointer",
|
||||
"transition-all duration-150 mx-2",
|
||||
"hover:bg-accent/40",
|
||||
isSelected && "bg-primary/6 dark:bg-primary/8",
|
||||
isFocused && "bg-accent/50",
|
||||
isFocused && "bg-accent/50"
|
||||
)}
|
||||
>
|
||||
{/* Provider icon */}
|
||||
|
|
@ -964,9 +850,7 @@ export function ModelSelector({
|
|||
{/* Model info */}
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="flex items-center gap-1.5">
|
||||
<span className="font-medium text-sm truncate">
|
||||
{config.name}
|
||||
</span>
|
||||
<span className="font-medium text-sm truncate">{config.name}</span>
|
||||
{isAutoMode && (
|
||||
<Badge
|
||||
variant="secondary"
|
||||
|
|
@ -978,9 +862,7 @@ export function ModelSelector({
|
|||
</div>
|
||||
<div className="flex items-center gap-1.5 mt-0.5">
|
||||
<span className="text-xs text-muted-foreground truncate">
|
||||
{isAutoMode
|
||||
? "Auto Mode"
|
||||
: (config.model_name as string)}
|
||||
{isAutoMode ? "Auto Mode" : (config.model_name as string)}
|
||||
</span>
|
||||
{!isAutoMode && hasCitations && (
|
||||
<Badge
|
||||
|
|
@ -1005,9 +887,7 @@ export function ModelSelector({
|
|||
<Edit3 className="size-3.5 text-muted-foreground" />
|
||||
</Button>
|
||||
)}
|
||||
{isSelected && (
|
||||
<Check className="size-4 text-primary shrink-0" />
|
||||
)}
|
||||
{isSelected && <Check className="size-4 text-primary shrink-0" />}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
@ -1021,11 +901,7 @@ export function ModelSelector({
|
|||
const userStartIdx = globalItems.length;
|
||||
|
||||
const addHandler =
|
||||
activeTab === "llm"
|
||||
? onAddNewLLM
|
||||
: activeTab === "image"
|
||||
? onAddNewImage
|
||||
: onAddNewVision;
|
||||
activeTab === "llm" ? onAddNewLLM : activeTab === "image" ? onAddNewImage : onAddNewVision;
|
||||
const addLabel =
|
||||
activeTab === "llm"
|
||||
? "Add Model"
|
||||
|
|
@ -1065,7 +941,7 @@ export function ModelSelector({
|
|||
"flex items-center justify-center gap-1.5 text-sm font-medium transition-all duration-200 border-b-[1.5px]",
|
||||
activeTab === value
|
||||
? "border-foreground dark:border-white text-foreground"
|
||||
: "border-transparent text-muted-foreground hover:text-foreground/70",
|
||||
: "border-transparent text-muted-foreground hover:text-foreground/70"
|
||||
)}
|
||||
>
|
||||
<Icon className="size-3.5" />
|
||||
|
|
@ -1076,14 +952,7 @@ export function ModelSelector({
|
|||
</div>
|
||||
|
||||
{/* Two-pane layout */}
|
||||
<div
|
||||
className={cn(
|
||||
"flex",
|
||||
isMobile
|
||||
? "flex-col h-[60vh]"
|
||||
: "flex-row h-[380px]",
|
||||
)}
|
||||
>
|
||||
<div className={cn("flex", isMobile ? "flex-col h-[60vh]" : "flex-row h-[380px]")}>
|
||||
{/* Provider sidebar */}
|
||||
{renderProviderSidebar()}
|
||||
|
||||
|
|
@ -1096,9 +965,7 @@ export function ModelSelector({
|
|||
ref={searchInputRef}
|
||||
placeholder="Search models"
|
||||
value={searchQuery}
|
||||
onChange={(e) =>
|
||||
setSearchQuery(e.target.value)
|
||||
}
|
||||
onChange={(e) => setSearchQuery(e.target.value)}
|
||||
onKeyDown={isMobile ? undefined : handleKeyDown}
|
||||
role="combobox"
|
||||
aria-expanded={true}
|
||||
|
|
@ -1106,7 +973,7 @@ export function ModelSelector({
|
|||
className={cn(
|
||||
"w-full pl-8 pr-3 py-2.5 text-sm bg-transparent",
|
||||
"focus:outline-none",
|
||||
"placeholder:text-muted-foreground",
|
||||
"placeholder:text-muted-foreground"
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
|
@ -1117,13 +984,9 @@ export function ModelSelector({
|
|||
{getProviderIcon(selectedProvider, {
|
||||
className: "size-4",
|
||||
})}
|
||||
<span className="text-sm font-medium">
|
||||
{formatProviderName(selectedProvider)}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{formatProviderName(selectedProvider)}</span>
|
||||
<span className="text-xs text-muted-foreground ml-auto">
|
||||
{configuredProviderSet.has(
|
||||
selectedProvider,
|
||||
)
|
||||
{configuredProviderSet.has(selectedProvider)
|
||||
? `${providerModelCounts[selectedProvider] || 0} models`
|
||||
: "Not configured"}
|
||||
</span>
|
||||
|
|
@ -1144,30 +1007,18 @@ export function ModelSelector({
|
|||
>
|
||||
{currentDisplayItems.length === 0 ? (
|
||||
<div className="flex-1 flex flex-col items-center justify-center gap-3 px-4">
|
||||
{selectedProvider !== "all" &&
|
||||
!configuredProviderSet.has(
|
||||
selectedProvider,
|
||||
) ? (
|
||||
{selectedProvider !== "all" && !configuredProviderSet.has(selectedProvider) ? (
|
||||
<>
|
||||
<div className="opacity-40">
|
||||
{getProviderIcon(
|
||||
selectedProvider,
|
||||
{
|
||||
className:
|
||||
"size-10",
|
||||
},
|
||||
)}
|
||||
{getProviderIcon(selectedProvider, {
|
||||
className: "size-10",
|
||||
})}
|
||||
</div>
|
||||
<p className="text-sm font-medium text-muted-foreground">
|
||||
No{" "}
|
||||
{formatProviderName(
|
||||
selectedProvider,
|
||||
)}{" "}
|
||||
models configured
|
||||
No {formatProviderName(selectedProvider)} models configured
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground/60 text-center">
|
||||
Add a model with this
|
||||
provider to get started
|
||||
Add a model with this provider to get started
|
||||
</p>
|
||||
{addHandler && (
|
||||
<Button
|
||||
|
|
@ -1186,12 +1037,9 @@ export function ModelSelector({
|
|||
) : searchQuery ? (
|
||||
<>
|
||||
<Search className="size-8 text-muted-foreground" />
|
||||
<p className="text-sm text-muted-foreground">
|
||||
No models found
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground">No models found</p>
|
||||
<p className="text-xs text-muted-foreground/60">
|
||||
Try a different search
|
||||
term
|
||||
Try a different search term
|
||||
</p>
|
||||
</>
|
||||
) : (
|
||||
|
|
@ -1212,29 +1060,18 @@ export function ModelSelector({
|
|||
<div className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-semibold text-muted-foreground tracking-wider">
|
||||
Global Models
|
||||
</div>
|
||||
{globalItems.map((item, i) =>
|
||||
renderModelCard(
|
||||
item,
|
||||
globalStartIdx + i,
|
||||
),
|
||||
)}
|
||||
{globalItems.map((item, i) => renderModelCard(item, globalStartIdx + i))}
|
||||
</>
|
||||
)}
|
||||
{globalItems.length > 0 &&
|
||||
userItems.length > 0 && (
|
||||
<div className="my-1.5 mx-4 h-px bg-border/60" />
|
||||
)}
|
||||
{globalItems.length > 0 && userItems.length > 0 && (
|
||||
<div className="my-1.5 mx-4 h-px bg-border/60" />
|
||||
)}
|
||||
{userItems.length > 0 && (
|
||||
<>
|
||||
<div className="flex items-center gap-2 px-3 py-1.5 text-[12px] font-semibold text-muted-foreground tracking-wider">
|
||||
Your Configurations
|
||||
</div>
|
||||
{userItems.map((item, i) =>
|
||||
renderModelCard(
|
||||
item,
|
||||
userStartIdx + i,
|
||||
),
|
||||
)}
|
||||
{userItems.map((item, i) => renderModelCard(item, userStartIdx + i))}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
|
|
@ -1254,9 +1091,7 @@ export function ModelSelector({
|
|||
}}
|
||||
>
|
||||
<Plus className="size-4 text-primary" />
|
||||
<span className="text-sm font-medium">
|
||||
{addLabel}
|
||||
</span>
|
||||
<span className="text-sm font-medium">{addLabel}</span>
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
|
|
@ -1275,18 +1110,13 @@ export function ModelSelector({
|
|||
aria-expanded={open}
|
||||
className={cn(
|
||||
"h-8 gap-2 px-3 text-sm bg-main-panel hover:bg-accent/50 dark:hover:bg-white/[0.06] border border-border/40 select-none",
|
||||
className,
|
||||
className
|
||||
)}
|
||||
>
|
||||
{isLoading ? (
|
||||
<>
|
||||
<Spinner
|
||||
size="sm"
|
||||
className="text-muted-foreground"
|
||||
/>
|
||||
<span className="text-muted-foreground hidden md:inline">
|
||||
Loading
|
||||
</span>
|
||||
<Spinner size="sm" className="text-muted-foreground" />
|
||||
<span className="text-muted-foreground hidden md:inline">Loading</span>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
|
|
@ -1303,9 +1133,7 @@ export function ModelSelector({
|
|||
) : (
|
||||
<>
|
||||
<Bot className="size-4 text-muted-foreground" />
|
||||
<span className="text-muted-foreground hidden md:inline">
|
||||
Select Model
|
||||
</span>
|
||||
<span className="text-muted-foreground hidden md:inline">Select Model</span>
|
||||
</>
|
||||
)}
|
||||
<div className="h-4 w-px bg-border/60 dark:bg-white/10 mx-0.5" />
|
||||
|
|
@ -1352,9 +1180,7 @@ export function ModelSelector({
|
|||
<DrawerHeader className="pb-0">
|
||||
<DrawerTitle>Select Model</DrawerTitle>
|
||||
</DrawerHeader>
|
||||
<div className="flex-1 overflow-hidden">
|
||||
{renderContent()}
|
||||
</div>
|
||||
<div className="flex-1 overflow-hidden">{renderContent()}</div>
|
||||
</DrawerContent>
|
||||
</Drawer>
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue