import { useBool } from "@common/hooks/useBool"; import { useQuery } from "@common/hooks/useAsyncState"; import { useInputState } from "@common/hooks/useInputState"; import { useUpdate } from "@common/hooks/useUpdate"; import clsx from "clsx"; import { useMemo, useRef } from "preact/hooks"; import styles from "../../assets/settings-modal.module.css"; import { useAppState } from "../../contexts/state"; import LLM from "../../utils/llm"; export const ConnectionSettings = () => { const { connection, model, dispatch } = useAppState(); const [url, setUrl] = useInputState(connection?.url ?? ""); const [apiKey, setApiKey] = useInputState(connection?.apiKey ?? ""); const [selectedModel, setSelectedModel] = useInputState(model?.id ?? ""); const [update, triggerFetch] = useUpdate(); const showPassword = useBool(false); const urlRef = useRef(url); const apiKeyRef = useRef(apiKey); urlRef.current = url; apiKeyRef.current = apiKey; const connectionToFetch = useMemo(() => { const currentUrl = urlRef.current; const currentApiKey = apiKeyRef.current; if (!currentUrl || !currentApiKey) return null; return { url: currentUrl, apiKey: currentApiKey }; }, [update]); const fetchModels = useMemo(() => async (conn: LLM.Connection | null) => { if (!conn) return []; const r = await LLM.getModels(conn); return r.data; }, []); const modelsData = useQuery(fetchModels, connectionToFetch); const isLoadingModels = connectionToFetch != null && modelsData == undefined; const [modelFilter, setModelFilter] = useInputState(""); const groupedModels = useMemo(() => { const sorted = (modelsData ?? []).sort((a, b) => { const aWeight = Number(a.supported_parameters.includes('tools')) * 2 + Number(a.supported_parameters.includes('reasoning')); const bWeight = Number(b.supported_parameters.includes('tools')) * 2 + Number(b.supported_parameters.includes('reasoning')); if (aWeight !== bWeight) return bWeight - aWeight; if (a.context_length !== b.context_length) return b.context_length - a.context_length; return a.id.localeCompare(b.id); }); const groups = Map.groupBy(sorted, m => m.context_length); return Array.from(groups.entries()) .sort((a, b) => b[0] - a[0]) .map(([context, models]) => ({ context, models })); }, [modelsData]); const filteredGroupedModels = useMemo(() => { if (!modelFilter) return groupedModels; const query = modelFilter.toLowerCase(); const fuzzyMatch = (target: string) => { const t = target.toLowerCase(); let qi = 0; for (let ti = 0; ti < t.length && qi < query.length; ti++) { if (t[ti] === query[qi]) qi++; } return qi === query.length; }; return groupedModels .map(({ context, models }) => ({ context, models: models.filter(m => m.id === selectedModel || fuzzyMatch(m.id)), })) .filter(({ models }) => models.length > 0); }, [groupedModels, modelFilter, selectedModel]); const handleBlur = () => { if (url && apiKey) { dispatch({ type: "SET_CONNECTION", connection: { url, apiKey } }); triggerFetch(); } }; const handleKeyDown = (e: KeyboardEvent) => { if (e.key === "Enter" && url && apiKey) { dispatch({ type: "SET_CONNECTION", connection: { url, apiKey } }); triggerFetch(); } }; const handleModelChange = (e: Event) => { setSelectedModel(e); const target = e.target as HTMLSelectElement; const selectedModelInfo = modelsData?.find(m => m.id === target.value) ?? null; dispatch({ type: "SET_MODEL", model: selectedModelInfo }); }; const connectionToTest = url && apiKey ? { url, apiKey } : null; return (
{connectionToTest ? ( isLoadingModels ? (

Loading models...

) : groupedModels.length > 0 ? ( <> ) : (

No models available

) ) : (

Enter connection details to load models

)}
); };