From 50031a6d758024935c0f74ecbe99669e36541a14 Mon Sep 17 00:00:00 2001 From: Saagar Patel Date: Tue, 21 Apr 2026 16:18:20 +0200 Subject: [PATCH 1/5] refactor(components): move SettingsTab helpers into sibling file Extract formatBytes, formatSpeed, formatVerificationStatus, getSearchApiEmbeddingBadge, validateQualityThresholds, and formatAuditEvent into SettingsTab.helpers.ts. SettingsTab.tsx re-exports them so SettingsTab.helpers.test.ts keeps passing without import changes. --- .../Settings/SettingsTab.helpers.ts | 99 +++++++++++++++ src/components/Settings/SettingsTab.tsx | 117 +++--------------- 2 files changed, 116 insertions(+), 100 deletions(-) create mode 100644 src/components/Settings/SettingsTab.helpers.ts diff --git a/src/components/Settings/SettingsTab.helpers.ts b/src/components/Settings/SettingsTab.helpers.ts new file mode 100644 index 00000000..a259edaf --- /dev/null +++ b/src/components/Settings/SettingsTab.helpers.ts @@ -0,0 +1,99 @@ +import type { ResponseQualityThresholds } from "../../features/analytics/qualityThresholds"; +import type { SearchApiEmbeddingModelStatus } from "../../types/settings"; + +export function formatBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const k = 1024; + const sizes = ["B", "KB", "MB", "GB"]; + const i = Math.floor(Math.log(bytes) / Math.log(k)); + return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`; +} + +export function formatSpeed(bps: number): string { + if (bps === 0) return ""; + return `${formatBytes(bps)}/s`; +} + +export function formatVerificationStatus( + status: string | null | undefined, +): string { + if (status === "verified") return "Verified"; + if (status === "unverified") return "Unverified"; + return "Unknown"; +} + +export function getSearchApiEmbeddingBadge( + status: SearchApiEmbeddingModelStatus | null, + installError: string | null, +): { label: string; className: string; detail: string } { + if (installError) { + return { + label: "Unavailable", + className: "error", + detail: installError, + }; + } + + if (!status) { + return { + label: "Checking", + className: "downloaded", + detail: + "Checking whether the managed search API embedding model is installed.", + }; + } + + if (!status.installed) { + return { + label: "Not Installed", + className: "not-downloaded", + detail: + "Install this managed model to keep search-api embeddings explicit, pinned, and offline at runtime.", + }; + } + + if (!status.ready) { + return { + label: "Needs Repair", + className: "error", + detail: + status.error ?? + "The managed search API embedding model is installed but not ready.", + }; + } + + return { + label: "Ready", + className: "loaded", + detail: `Pinned revision ${status.revision}. Loaded from local disk only at runtime.`, + }; +} + +export function validateQualityThresholds( + thresholds: ResponseQualityThresholds, +): string | null { + if (thresholds.editRatioWatch >= thresholds.editRatioAction) { + return "Edit ratio watch threshold must be lower than action threshold."; + } + if (thresholds.timeToDraftWatchMs >= thresholds.timeToDraftActionMs) { + return "Time-to-draft watch threshold must be lower than action threshold."; + } + if (thresholds.copyPerSaveWatch <= thresholds.copyPerSaveAction) { + return "Copy-per-save watch threshold must be higher than action threshold."; + } + if (thresholds.editedSaveRateWatch >= thresholds.editedSaveRateAction) { + return "Edited save rate watch threshold must be lower than action threshold."; + } + return null; +} + +export function formatAuditEvent( + event: string | Record, +): string { + if (typeof event === "string") return event; + if (typeof event === "object" && event !== null) { + const key = Object.keys(event)[0]; + return key ? `${key}: ${event[key]}` : JSON.stringify(event); + } + return String(event); +} diff --git a/src/components/Settings/SettingsTab.tsx b/src/components/Settings/SettingsTab.tsx index cf0ae783..7836bb94 100644 --- a/src/components/Settings/SettingsTab.tsx +++ b/src/components/Settings/SettingsTab.tsx @@ -24,10 +24,17 @@ import type { DeploymentHealthSummary, IntegrationConfigRecord, MemoryKernelPreflightStatus, - SearchApiEmbeddingModelStatus, } from "../../types/settings"; import type { CustomVariable } from "../../types/workspace"; import { Button } from "../shared/Button"; +import { + formatAuditEvent, + formatBytes, + formatSpeed, + formatVerificationStatus, + getSearchApiEmbeddingBadge, + validateQualityThresholds, +} from "./SettingsTab.helpers"; import { AuditLogsSection, BackupSection, @@ -44,6 +51,15 @@ import { import { formatAppVersion } from "./versionLabel"; import "./SettingsTab.css"; +export { + formatAuditEvent, + formatBytes, + formatSpeed, + formatVerificationStatus, + getSearchApiEmbeddingBadge, + validateQualityThresholds, +}; + const RECOMMENDED_MODELS: ModelInfo[] = [ { id: "llama-3.1-8b-instruct", @@ -78,17 +94,6 @@ const OTHER_SUPPORTED_MODELS: ModelInfo[] = [ const APP_VERSION = appPackage.version; -// Audit event types can be either a plain string (unit variants like "key_generated") -// or an object (data variants like { custom: "value" }). Normalize for display. -function formatAuditEvent(event: string | Record): string { - if (typeof event === "string") return event; - if (typeof event === "object" && event !== null) { - const key = Object.keys(event)[0]; - return key ? `${key}: ${event[key]}` : JSON.stringify(event); - } - return String(event); -} - const CONTEXT_WINDOW_OPTIONS = [ { value: null, label: "Model Default" }, { value: 2048, label: "2K (2,048 tokens)" }, @@ -100,94 +105,6 @@ const CONTEXT_WINDOW_OPTIONS = [ const AUDIT_PAGE_SIZE = 50; -// Helper to format bytes for display -export function formatBytes(bytes: number): string { - if (bytes === 0) return "0 B"; - const k = 1024; - const sizes = ["B", "KB", "MB", "GB"]; - const i = Math.floor(Math.log(bytes) / Math.log(k)); - return `${(bytes / k ** i).toFixed(1)} ${sizes[i]}`; -} - -// Helper to format download speed -export function formatSpeed(bps: number): string { - if (bps === 0) return ""; - return `${formatBytes(bps)}/s`; -} - -export function formatVerificationStatus( - status: string | null | undefined, -): string { - if (status === "verified") return "Verified"; - if (status === "unverified") return "Unverified"; - return "Unknown"; -} - -export function getSearchApiEmbeddingBadge( - status: SearchApiEmbeddingModelStatus | null, - installError: string | null, -): { label: string; className: string; detail: string } { - if (installError) { - return { - label: "Unavailable", - className: "error", - detail: installError, - }; - } - - if (!status) { - return { - label: "Checking", - className: "downloaded", - detail: - "Checking whether the managed search API embedding model is installed.", - }; - } - - if (!status.installed) { - return { - label: "Not Installed", - className: "not-downloaded", - detail: - "Install this managed model to keep search-api embeddings explicit, pinned, and offline at runtime.", - }; - } - - if (!status.ready) { - return { - label: "Needs Repair", - className: "error", - detail: - status.error ?? - "The managed search API embedding model is installed but not ready.", - }; - } - - return { - label: "Ready", - className: "loaded", - detail: `Pinned revision ${status.revision}. Loaded from local disk only at runtime.`, - }; -} - -export function validateQualityThresholds( - thresholds: ResponseQualityThresholds, -): string | null { - if (thresholds.editRatioWatch >= thresholds.editRatioAction) { - return "Edit ratio watch threshold must be lower than action threshold."; - } - if (thresholds.timeToDraftWatchMs >= thresholds.timeToDraftActionMs) { - return "Time-to-draft watch threshold must be lower than action threshold."; - } - if (thresholds.copyPerSaveWatch <= thresholds.copyPerSaveAction) { - return "Copy-per-save watch threshold must be higher than action threshold."; - } - if (thresholds.editedSaveRateWatch >= thresholds.editedSaveRateAction) { - return "Edited save rate watch threshold must be lower than action threshold."; - } - return null; -} - export function SettingsTab() { const { loadModel, From 570f565b8e744f737a030bf8e2511cd5c60ab58a Mon Sep 17 00:00:00 2001 From: Saagar Patel Date: Tue, 21 Apr 2026 16:24:15 +0200 Subject: [PATCH 2/5] refactor(components): split SettingsTab into per-section files Extract Language Model, Context Window, Semantic Search, Knowledge Base, Advanced Search, Template Variables, and Jira sections into dedicated sibling files under sections/. The SettingsTab shell now orchestrates these section components along with the previously extracted ops and overview sections. Each section owns the props it needs; variable-form state lives inside VariablesSection, jira-form state lives inside JiraSection, and show-other-models toggle lives inside ModelSection. --- src/components/Settings/SettingsTab.tsx | 1070 ++--------------- .../sections/AdvancedSearchSection.tsx | 32 + .../sections/ContextWindowSection.tsx | 54 + .../Settings/sections/JiraSection.tsx | 127 ++ .../Settings/sections/KbSection.tsx | 66 + .../Settings/sections/ModelSection.tsx | 331 +++++ .../sections/SemanticSearchSection.tsx | 219 ++++ .../Settings/sections/VariablesSection.tsx | 217 ++++ 8 files changed, 1164 insertions(+), 952 deletions(-) create mode 100644 src/components/Settings/sections/AdvancedSearchSection.tsx create mode 100644 src/components/Settings/sections/ContextWindowSection.tsx create mode 100644 src/components/Settings/sections/JiraSection.tsx create mode 100644 src/components/Settings/sections/KbSection.tsx create mode 100644 src/components/Settings/sections/ModelSection.tsx create mode 100644 src/components/Settings/sections/SemanticSearchSection.tsx create mode 100644 src/components/Settings/sections/VariablesSection.tsx diff --git a/src/components/Settings/SettingsTab.tsx b/src/components/Settings/SettingsTab.tsx index 7836bb94..c6221941 100644 --- a/src/components/Settings/SettingsTab.tsx +++ b/src/components/Settings/SettingsTab.tsx @@ -13,11 +13,11 @@ import { resolveRevampFlags } from "../../features/revamp/flags"; import { useCustomVariables } from "../../hooks/useCustomVariables"; import { useDownload } from "../../hooks/useDownload"; import { useEmbedding } from "../../hooks/useEmbedding"; -import { useSettingsOps } from "../../hooks/useSettingsOps"; import { useJira } from "../../hooks/useJira"; import { useKb } from "../../hooks/useKb"; import { useLlm } from "../../hooks/useLlm"; import { useSearchApiEmbedding } from "../../hooks/useSearchApiEmbedding"; +import { useSettingsOps } from "../../hooks/useSettingsOps"; import type { ModelInfo } from "../../types/llm"; import type { AuditEntry, @@ -25,8 +25,6 @@ import type { IntegrationConfigRecord, MemoryKernelPreflightStatus, } from "../../types/settings"; -import type { CustomVariable } from "../../types/workspace"; -import { Button } from "../shared/Button"; import { formatAuditEvent, formatBytes, @@ -35,6 +33,12 @@ import { getSearchApiEmbeddingBadge, validateQualityThresholds, } from "./SettingsTab.helpers"; +import { AdvancedSearchSection } from "./sections/AdvancedSearchSection"; +import { ContextWindowSection } from "./sections/ContextWindowSection"; +import { JiraSection } from "./sections/JiraSection"; +import { KbSection } from "./sections/KbSection"; +import { ModelSection } from "./sections/ModelSection"; +import { SemanticSearchSection } from "./sections/SemanticSearchSection"; import { AuditLogsSection, BackupSection, @@ -48,6 +52,7 @@ import { PolicyGatesSection, SettingsHero, } from "./sections/SettingsOverviewSections"; +import { VariablesSection } from "./sections/VariablesSection"; import { formatAppVersion } from "./versionLabel"; import "./SettingsTab.css"; @@ -60,49 +65,7 @@ export { validateQualityThresholds, }; -const RECOMMENDED_MODELS: ModelInfo[] = [ - { - id: "llama-3.1-8b-instruct", - name: "Llama 3.1 8B Instruct", - size: "4.9 GB", - description: "Recommended: higher quality and more reliable grounding", - }, -]; - -// Still supported, but intentionally hidden behind progressive disclosure to keep -// operators focused on a single default model path. -const OTHER_SUPPORTED_MODELS: ModelInfo[] = [ - { - id: "llama-3.2-1b-instruct", - name: "Llama 3.2 1B Instruct", - size: "1.3 GB", - description: "Fast, lightweight model for quick responses", - }, - { - id: "llama-3.2-3b-instruct", - name: "Llama 3.2 3B Instruct", - size: "2.0 GB", - description: "Balanced performance and quality", - }, - { - id: "phi-3-mini-4k-instruct", - name: "Phi-3 Mini 4K", - size: "2.4 GB", - description: "Microsoft model, good for reasoning", - }, -]; - const APP_VERSION = appPackage.version; - -const CONTEXT_WINDOW_OPTIONS = [ - { value: null, label: "Model Default" }, - { value: 2048, label: "2K (2,048 tokens)" }, - { value: 4096, label: "4K (4,096 tokens)" }, - { value: 8192, label: "8K (8,192 tokens)" }, - { value: 16384, label: "16K (16,384 tokens)" }, - { value: 32768, label: "32K (32,768 tokens)" }, -]; - const AUDIT_PAGE_SIZE = 50; export function SettingsTab() { @@ -173,7 +136,6 @@ export function SettingsTab() { null, ); const [downloadedModels, setDownloadedModels] = useState([]); - const [showOtherModels, setShowOtherModels] = useState(false); const [kbFolder, setKbFolderState] = useState(null); const [indexStats, setIndexStats] = useState<{ total_chunks: number; @@ -181,11 +143,6 @@ export function SettingsTab() { } | null>(null); const [vectorEnabled, setVectorEnabled] = useState(false); const [jiraConfigured, setJiraConfigured] = useState(false); - const [jiraForm, setJiraForm] = useState({ - baseUrl: "", - email: "", - apiToken: "", - }); const [contextWindowSize, setContextWindowSize] = useState( null, ); @@ -207,7 +164,6 @@ export function SettingsTab() { const [auditSearchQuery, setAuditSearchQuery] = useState(""); const [auditPage, setAuditPage] = useState(1); - // Deployment and integration state const [deploymentHealth, setDeploymentHealth] = useState(null); const [deployPreflightChecks, setDeployPreflightChecks] = useState( @@ -227,16 +183,6 @@ export function SettingsTab() { const [memoryKernelLoading, setMemoryKernelLoading] = useState(false); const revampFlags = useMemo(() => resolveRevampFlags(), []); - // Custom variables state - const [editingVariable, setEditingVariable] = useState( - null, - ); - const [variableForm, setVariableForm] = useState({ name: "", value: "" }); - const [showVariableForm, setShowVariableForm] = useState(false); - const [variableFormError, setVariableFormError] = useState( - null, - ); - const loadAuditEntries = useCallback(async () => { setAuditLoading(true); try { @@ -260,7 +206,6 @@ export function SettingsTab() { ); setMemoryKernelPreflight(status); } catch { - // Non-blocking: show as unavailable rather than failing settings load. setMemoryKernelPreflight(null); } finally { setMemoryKernelLoading(false); @@ -363,7 +308,6 @@ export function SettingsTab() { setIntegrations(integrationsList ?? []); setQualityThresholds(getResponseQualityThresholds()); - // Check embedding model status await Promise.all([ checkEmbeddingStatus(), refreshSearchApiEmbeddingStatus(), @@ -383,13 +327,15 @@ export function SettingsTab() { } } - async function handleJiraConnect(e: React.FormEvent) { - e.preventDefault(); + async function handleJiraConnect( + baseUrl: string, + email: string, + apiToken: string, + ) { setError(null); try { - await configureJira(jiraForm.baseUrl, jiraForm.email, jiraForm.apiToken); + await configureJira(baseUrl, email, apiToken); setJiraConfigured(true); - setJiraForm({ baseUrl: "", email: "", apiToken: "" }); } catch (err) { setError(`Failed to connect to Jira: ${err}`); } @@ -450,17 +396,11 @@ export function SettingsTab() { const { open } = await import("@tauri-apps/plugin-dialog"); const selected = await open({ multiple: false, - filters: [ - { - name: "GGUF Model", - extensions: ["gguf"], - }, - ], + filters: [{ name: "GGUF Model", extensions: ["gguf"] }], title: "Select GGUF Model File", }); if (selected && typeof selected === "string") { - // Validate the file first const validation = await validateGgufFile(selected); if (!validation.is_valid) { setError( @@ -489,7 +429,6 @@ export function SettingsTab() { return; } - // Load the model const info = await loadCustomModel(selected); setLoadedModel(validation.file_name); setLoadedModelInfo(info); @@ -567,9 +506,7 @@ export function SettingsTab() { async function handleLoadEmbeddingModel() { setError(null); try { - // Engine is initialized at startup; this is idempotent await initEmbeddingEngine(); - // Get model path const path = await getEmbeddingModelPath("nomic-embed-text"); if (!path) { showError("Embedding model file not found. Try re-downloading."); @@ -643,85 +580,6 @@ export function SettingsTab() { } } - // Custom variable handlers - const handleEditVariable = useCallback((variable: CustomVariable) => { - setEditingVariable(variable); - setVariableForm({ name: variable.name, value: variable.value }); - setShowVariableForm(true); - setVariableFormError(null); - }, []); - - const handleAddVariable = useCallback(() => { - setEditingVariable(null); - setVariableForm({ name: "", value: "" }); - setShowVariableForm(true); - setVariableFormError(null); - }, []); - - const handleCancelVariableForm = useCallback(() => { - setShowVariableForm(false); - setEditingVariable(null); - setVariableForm({ name: "", value: "" }); - setVariableFormError(null); - }, []); - - const handleSaveVariable = useCallback(async () => { - const name = variableForm.name.trim(); - const value = variableForm.value.trim(); - - // Validate name format (alphanumeric and underscores only) - if (!/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) { - setVariableFormError( - "Name must start with a letter or underscore and contain only letters, numbers, and underscores", - ); - return; - } - - if (!value) { - setVariableFormError("Value is required"); - return; - } - - // Check for duplicate name (except when editing the same variable) - const isDuplicate = customVariables.some( - (v) => - v.name.toLowerCase() === name.toLowerCase() && - v.id !== editingVariable?.id, - ); - if (isDuplicate) { - setVariableFormError("A variable with this name already exists"); - return; - } - - const success = await saveVariable(name, value, editingVariable?.id); - if (success) { - showSuccess(editingVariable ? "Variable updated" : "Variable created"); - handleCancelVariableForm(); - } else { - setVariableFormError("Failed to save variable"); - } - }, [ - variableForm, - editingVariable, - customVariables, - saveVariable, - showSuccess, - handleCancelVariableForm, - ]); - - const handleDeleteVariable = useCallback( - async (variableId: string) => { - const success = await deleteVariable(variableId); - if (success) { - showSuccess("Variable deleted"); - } else { - showError("Failed to delete variable"); - } - }, - [deleteVariable, showSuccess, showError], - ); - - // Backup handlers const handleExportBackup = useCallback(async () => { setBackupLoading("export"); setError(null); @@ -758,7 +616,6 @@ export function SettingsTab() { showSuccess( `Imported ${result.drafts_imported} drafts, ${result.templates_imported} templates, ${result.variables_imported} variables, ${result.trees_imported} trees`, ); - // Reload data loadInitialState(); loadVariables(); } catch (err) { @@ -893,806 +750,115 @@ export function SettingsTab() { -
-

Language Model

-

- Select and load a language model for generating responses. -

- - {loadedModel && ( -
- - Currently loaded: {loadedModel} - {loadedModelInfo?.verification_status && ( - - {formatVerificationStatus( - loadedModelInfo.verification_status, - )} - - )} - - -
- )} - -
-

Recommended

-

- For consistent results across operators, AssistSupport recommends a - single default model. -

-
-
- {RECOMMENDED_MODELS.map((model) => { - const isDownloaded = downloadedModels.includes(model.id); - const isLoaded = loadedModel === model.id; - const isLoadingThis = loading === model.id; - const isDownloadingThis = - isDownloading && downloadProgress?.model_id === model.id; - - return ( -
-
-

{model.name}

-

{model.description}

- {model.size} -
-
- {isDownloadingThis ? ( -
-
-
- - {Math.round(downloadProgress?.percent || 0)}% - -
-
- - {formatBytes(downloadProgress?.downloaded_bytes || 0)} - {downloadProgress?.total_bytes - ? ` / ${formatBytes(downloadProgress.total_bytes)}` - : ""} - - - {formatSpeed(downloadProgress?.speed_bps || 0)} - -
- -
- ) : isDownloaded ? ( - - ) : ( - - )} -
-
- ); - })} -
- -
- - {showOtherModels && ( - <> -

- These models are supported for experimentation, but may be less - reliable for production ticket responses. -

-
- {OTHER_SUPPORTED_MODELS.map((model) => { - const isDownloaded = downloadedModels.includes(model.id); - const isLoaded = loadedModel === model.id; - const isLoadingThis = loading === model.id; - const isDownloadingThis = - isDownloading && downloadProgress?.model_id === model.id; - - return ( -
-
-

{model.name}

-

{model.description}

- {model.size} -
-
- {isDownloadingThis ? ( -
-
-
- - {Math.round(downloadProgress?.percent || 0)}% - -
-
- - {formatBytes( - downloadProgress?.downloaded_bytes || 0, - )} - {downloadProgress?.total_bytes - ? ` / ${formatBytes(downloadProgress.total_bytes)}` - : ""} - - - {formatSpeed(downloadProgress?.speed_bps || 0)} - -
- -
- ) : isDownloaded ? ( - - ) : ( - - )} -
-
- ); - })} -
- - )} -
- -
-

Custom Model

-

- Load a GGUF-format model from your computer. Verified models load - normally. Unverified files are blocked unless you enable the - advanced override below. -

- -

- Keep this off unless you trust the GGUF file source. If you turn it - on, AssistSupport still warns and asks for confirmation before - loading an unverified file. -

- -
- -
-

AI Status & Guarantees

-

- AssistSupport runs AI locally and can operate fully offline. These - signals help operators trust what the AI is doing. -

-
-
-

Local Guarantees

-
    -
  • - Offline-first: no cloud AI calls -
  • -
  • - Copy gating: citations required (override - logs locally) -
  • -
  • - Prompts hidden: operators cannot edit system - prompts -
  • -
-
-
-

Runtime Status

-
    -
  • - Chat model:{" "} - {loadedModel ? loadedModel : "Not loaded"} -
  • -
  • - Embeddings:{" "} - {isEmbeddingLoaded ? "Loaded" : "Not loaded"} -
  • -
  • - Search API embedding:{" "} - {searchApiEmbeddingStatus?.ready - ? "Ready" - : searchApiEmbeddingStatus?.installed - ? "Installed but not ready" - : "Not installed"} -
  • -
  • - KB folder: {kbFolder ? kbFolder : "Not set"} -
  • -
  • - MemoryKernel:{" "} - {memoryKernelPreflight - ? memoryKernelPreflight.status - : "Unavailable"} - {memoryKernelPreflight?.service_contract_version - ? ` (svc ${memoryKernelPreflight.service_contract_version})` - : ""} -
  • -
-
- -
-
-
-
-
- -
-

Context Window

-

- Configure the maximum context length for LLM generation. Larger values - allow more content but use more memory. -

-
- - {!loadedModel && ( -

- Load a model to configure context window. -

- )} -

- Higher values require more RAM. The "Model Default" option uses the - model's training context (capped at 8K). -

-
-
- -
-

Semantic Search Models

-

- AssistSupport uses two separate local models for semantic search: one - for the desktop knowledge base and one for the Python search API. Both - are managed explicitly here and kept offline at runtime. -

- -
-
-

Desktop Embedding Model

-

- Used for local knowledge-base embeddings and vector search. Uses{" "} - nomic-embed-text (768 dimensions, about 550 MB). -

-
- {isDownloading && - downloadProgress?.model_id === "nomic-embed-text" ? ( -
-
-
- - {Math.round(downloadProgress?.percent || 0)}% - -
-
- - {formatBytes(downloadProgress?.downloaded_bytes || 0)} - {downloadProgress?.total_bytes - ? ` / ${formatBytes(downloadProgress.total_bytes)}` - : ""} - - - {formatSpeed(downloadProgress?.speed_bps || 0)} - -
- -
- ) : !embeddingDownloaded ? ( -
- - Not Downloaded - - -
- ) : !isEmbeddingLoaded ? ( -
- Downloaded - -
- ) : ( -
- Loaded -
- - {embeddingModelInfo?.name || "nomic-embed-text"} - - - {embeddingModelInfo?.embedding_dim || 768} dimensions - -
- -
- )} - - {vectorEnabled && isEmbeddingLoaded && ( -
- -

- Creates vector embeddings for all indexed documents. -

-
- )} -
-
- -
-

Search API Embedding Model

-

- Used by the local Python hybrid search API. This managed install - is pinned to a specific Hugging Face revision and loaded from - local disk only. -

-
- - {searchApiEmbeddingBadge.label} - -
- - {searchApiEmbeddingStatus?.model_name ?? - "sentence-transformers/all-MiniLM-L6-v2"} - - - {searchApiEmbeddingStatus?.local_path - ? "Managed local install" - : "No managed install detected"} - -
- -
-

- {searchApiEmbeddingBadge.detail} -

-
- -
-
-
-
- -
-

Knowledge Base

-

- Configure the folder containing your knowledge base documents. -

- -
-
-
- {kbFolder ? ( - {kbFolder} - ) : ( - No folder selected - )} -
- -
- - {kbFolder && ( -
-
- Files indexed - - {indexStats?.total_files ?? "—"} - -
-
- Total chunks - - {indexStats?.total_chunks ?? "—"} - -
- -
- )} -
-
- -
-

Advanced Search

-

- Enable AI-powered semantic search for better knowledge base results. -

-
- -

- Creates embeddings of your documents for semantic search. All - processing happens locally on your machine. -

-
-
+ { + void handleLoadModel(modelId); + }} + onUnloadModel={() => { + void handleUnloadModel(); + }} + onDownloadModel={(modelId) => { + void handleDownloadModel(modelId); + }} + onCancelDownload={cancelDownload} + onLoadCustomModel={() => { + void handleLoadCustomModel(); + }} + onAllowUnverifiedLocalModelsChange={(enabled) => { + void handleSetAllowUnverifiedLocalModels(enabled); + }} + onRefreshMemoryKernel={() => { + void refreshMemoryKernelStatus(); + }} + /> -
-

Template Variables

-

- Define custom variables to use in response templates. Use as{" "} - {`{{variable_name}}`} in your prompts. -

+ { + void handleContextWindowChange(value); + }} + /> -
- {customVariables.length === 0 ? ( -

No custom variables defined yet.

- ) : ( -
- {customVariables.map((variable) => ( -
-
- {`{{${variable.name}}}`} - {variable.value} -
-
- - -
-
- ))} -
- )} + { + void handleDownloadEmbeddingModel(); + }} + onLoadEmbeddingModel={() => { + void handleLoadEmbeddingModel(); + }} + onUnloadEmbeddingModel={() => { + void handleUnloadEmbeddingModel(); + }} + onGenerateEmbeddings={() => { + void handleGenerateEmbeddings(); + }} + onInstallSearchApiEmbeddingModel={() => { + void handleInstallSearchApiEmbeddingModel(); + }} + onRefreshSearchApiEmbeddingStatus={() => { + void refreshSearchApiEmbeddingStatus(); + }} + /> - -
+ { + void handleSelectKbFolder(); + }} + onRebuildIndex={() => { + void handleRebuildIndex(); + }} + /> - {showVariableForm && ( -
-
e.stopPropagation()} - > -

{editingVariable ? "Edit Variable" : "Add Variable"}

- {variableFormError && ( -
{variableFormError}
- )} -
- - - setVariableForm((f) => ({ ...f, name: e.target.value })) - } - autoFocus - /> -

- Letters, numbers, and underscores only -

-
-
- -