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..63cf5b02 100644 --- a/src/components/Settings/SettingsTab.tsx +++ b/src/components/Settings/SettingsTab.tsx @@ -4,7 +4,6 @@ import appPackage from "../../../package.json"; import { useTheme } from "../../contexts/ThemeContext"; import { useToastContext } from "../../contexts/ToastContext"; import { - getResponseQualityThresholds, type ResponseQualityThresholds, resetResponseQualityThresholds, saveResponseQualityThresholds, @@ -13,21 +12,26 @@ 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 type { ModelInfo } from "../../types/llm"; -import type { - AuditEntry, - DeploymentHealthSummary, - IntegrationConfigRecord, - MemoryKernelPreflightStatus, - SearchApiEmbeddingModelStatus, -} from "../../types/settings"; -import type { CustomVariable } from "../../types/workspace"; -import { Button } from "../shared/Button"; +import { useSettingsOps } from "../../hooks/useSettingsOps"; +import { + formatAuditEvent, + formatBytes, + formatSpeed, + formatVerificationStatus, + getSearchApiEmbeddingBadge, + validateQualityThresholds, +} from "./SettingsTab.helpers"; +import { useSettingsInit } from "./useSettingsInit"; +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, @@ -41,153 +45,22 @@ import { PolicyGatesSection, SettingsHero, } from "./sections/SettingsOverviewSections"; +import { VariablesSection } from "./sections/VariablesSection"; import { formatAppVersion } from "./versionLabel"; import "./SettingsTab.css"; -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", - }, -]; +export { + formatAuditEvent, + formatBytes, + formatSpeed, + formatVerificationStatus, + getSearchApiEmbeddingBadge, + validateQualityThresholds, +}; 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)" }, - { 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; -// 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, @@ -251,105 +124,81 @@ export function SettingsTab() { deleteVariable, } = useCustomVariables(); - const [loadedModel, setLoadedModel] = useState(null); - const [loadedModelInfo, setLoadedModelInfo] = useState( - null, - ); - const [downloadedModels, setDownloadedModels] = useState([]); - const [showOtherModels, setShowOtherModels] = useState(false); - const [kbFolder, setKbFolderState] = useState(null); - const [indexStats, setIndexStats] = useState<{ - total_chunks: number; - total_files: number; - } | null>(null); - const [vectorEnabled, setVectorEnabled] = useState(false); - const [jiraConfigured, setJiraConfigured] = useState(false); - const [jiraForm, setJiraForm] = useState({ - baseUrl: "", - email: "", - apiToken: "", + const init = useSettingsInit({ + getLoadedModel, + getModelInfo, + listModels, + getKbFolder, + getIndexStats, + getVectorConsent, + getContextWindow, + checkJiraConfig, + isEmbeddingDownloaded, + getDeploymentHealthSummary, + listIntegrations, + checkEmbeddingStatus, + refreshSearchApiEmbeddingStatus, + onShowError: showError, }); - const [contextWindowSize, setContextWindowSize] = useState( - null, - ); - const [embeddingDownloaded, setEmbeddingDownloaded] = useState(false); - const [allowUnverifiedLocalModels, setAllowUnverifiedLocalModels] = - useState(false); + + const { + loadedModel, + setLoadedModel, + loadedModelInfo, + setLoadedModelInfo, + downloadedModels, + setDownloadedModels, + kbFolder, + setKbFolder: setKbFolderState, + indexStats, + setIndexStats, + vectorEnabled, + setVectorEnabled, + jiraConfigured, + setJiraConfigured, + contextWindowSize, + setContextWindowSize, + embeddingDownloaded, + setEmbeddingDownloaded, + allowUnverifiedLocalModels, + setAllowUnverifiedLocalModels, + deploymentHealth, + setDeploymentHealth, + integrations, + setIntegrations, + qualityThresholds, + setQualityThresholds, + memoryKernelPreflight, + memoryKernelLoading, + auditEntries, + auditLoading, + auditPage, + setAuditPage, + loadInitialState, + loadAuditEntries, + refreshMemoryKernelStatus, + } = init; + const [generatingEmbeddings, setGeneratingEmbeddings] = useState(false); const [loading, setLoading] = useState(null); const [error, setError] = useState(null); const [backupLoading, setBackupLoading] = useState< "export" | "import" | null >(null); - const [auditEntries, setAuditEntries] = useState([]); - const [auditLoading, setAuditLoading] = useState(false); const [auditExporting, setAuditExporting] = useState(false); const [auditSeverityFilter, setAuditSeverityFilter] = useState< "all" | "info" | "warning" | "error" | "critical" >("all"); const [auditSearchQuery, setAuditSearchQuery] = useState(""); - const [auditPage, setAuditPage] = useState(1); - - // Deployment and integration state - const [deploymentHealth, setDeploymentHealth] = - useState(null); const [deployPreflightChecks, setDeployPreflightChecks] = useState( [], ); const [deployPreflightRunning, setDeployPreflightRunning] = useState(false); - const [integrations, setIntegrations] = useState( - [], - ); - const [qualityThresholds, setQualityThresholds] = - useState(() => getResponseQualityThresholds()); const [qualityThresholdError, setQualityThresholdError] = useState< string | null >(null); - const [memoryKernelPreflight, setMemoryKernelPreflight] = - useState(null); - 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 { - const entries = await invoke("get_audit_entries", { - limit: 200, - }); - setAuditEntries(entries ?? []); - setAuditPage(1); - } catch (err) { - showError(`Failed to load audit logs: ${err}`); - } finally { - setAuditLoading(false); - } - }, [showError]); - - const refreshMemoryKernelStatus = useCallback(async () => { - setMemoryKernelLoading(true); - try { - const status = await invoke( - "get_memory_kernel_preflight_status", - ); - setMemoryKernelPreflight(status); - } catch { - // Non-blocking: show as unavailable rather than failing settings load. - setMemoryKernelPreflight(null); - } finally { - setMemoryKernelLoading(false); - } - }, []); - const filteredAuditEntries = useMemo(() => { const normalized = auditEntries.slice().reverse(); const query = auditSearchQuery.trim().toLowerCase(); @@ -377,11 +226,7 @@ export function SettingsTab() { useEffect(() => { setAuditPage((prev) => Math.min(prev, auditTotalPages)); - }, [auditTotalPages]); - - useEffect(() => { - refreshMemoryKernelStatus(); - }, [refreshMemoryKernelStatus]); + }, [auditTotalPages, setAuditPage]); const pagedAuditEntries = useMemo(() => { const start = (auditPage - 1) * AUDIT_PAGE_SIZE; @@ -389,72 +234,10 @@ export function SettingsTab() { }, [filteredAuditEntries, auditPage]); useEffect(() => { - Promise.resolve(loadInitialState()).catch((err) => - console.error("Settings init failed:", err), - ); Promise.resolve(loadVariables()).catch((err) => console.error("Variables load failed:", err), ); - Promise.resolve(loadAuditEntries()).catch((err) => - console.error("Audit load failed:", err), - ); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [loadVariables, loadAuditEntries]); - - async function loadInitialState() { - try { - const [ - loaded, - modelInfo, - downloaded, - folder, - stats, - consent, - jiraConfigResult, - ctxWindow, - embDownloaded, - allowUnverifiedModels, - deployHealth, - integrationsList, - ] = await Promise.all([ - getLoadedModel(), - getModelInfo().catch(() => null), - listModels(), - getKbFolder(), - getIndexStats().catch(() => null), - getVectorConsent().catch(() => null), - checkJiraConfig().catch(() => false), - getContextWindow().catch(() => null), - isEmbeddingDownloaded().catch(() => false), - invoke("get_allow_unverified_local_models").catch(() => false), - getDeploymentHealthSummary().catch(() => null), - listIntegrations().catch(() => []), - ]); - setLoadedModel(loaded); - setLoadedModelInfo(modelInfo); - setDownloadedModels(downloaded); - setKbFolderState(folder); - setIndexStats(stats); - if (consent) { - setVectorEnabled(consent.enabled); - } - setJiraConfigured(jiraConfigResult); - setContextWindowSize(ctxWindow); - setEmbeddingDownloaded(embDownloaded); - setAllowUnverifiedLocalModels(allowUnverifiedModels); - setDeploymentHealth(deployHealth); - setIntegrations(integrationsList ?? []); - setQualityThresholds(getResponseQualityThresholds()); - - // Check embedding model status - await Promise.all([ - checkEmbeddingStatus(), - refreshSearchApiEmbeddingStatus(), - ]); - } catch (err) { - console.error("Failed to load settings state:", err); - } - } + }, [loadVariables]); async function handleVectorToggle() { const newValue = !vectorEnabled; @@ -466,13 +249,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}`); } @@ -533,17 +318,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( @@ -572,7 +351,6 @@ export function SettingsTab() { return; } - // Load the model const info = await loadCustomModel(selected); setLoadedModel(validation.file_name); setLoadedModelInfo(info); @@ -650,9 +428,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."); @@ -726,85 +502,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); @@ -841,7 +538,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) { @@ -900,6 +596,7 @@ export function SettingsTab() { }, [ runDeploymentPreflight, getDeploymentHealthSummary, + setDeploymentHealth, showSuccess, showError, ]); @@ -914,7 +611,7 @@ export function SettingsTab() { showError(`Failed to update ${integrationType}: ${err}`); } }, - [configureIntegration, listIntegrations, showError], + [configureIntegration, listIntegrations, setIntegrations, showError], ); const updateQualityThreshold = useCallback( @@ -922,7 +619,7 @@ export function SettingsTab() { setQualityThresholds((prev) => ({ ...prev, [key]: value })); setQualityThresholdError(null); }, - [], + [setQualityThresholds], ); const handleSaveQualityThresholds = useCallback(() => { @@ -935,14 +632,14 @@ export function SettingsTab() { setQualityThresholds(saved); setQualityThresholdError(null); showSuccess("Response quality coaching thresholds updated"); - }, [qualityThresholds, showSuccess]); + }, [qualityThresholds, setQualityThresholds, showSuccess]); const handleResetQualityThresholds = useCallback(() => { const defaults = resetResponseQualityThresholds(); setQualityThresholds(defaults); setQualityThresholdError(null); showSuccess("Response quality coaching thresholds reset to defaults"); - }, [showSuccess]); + }, [setQualityThresholds, showSuccess]); const searchApiEmbeddingBadge = getSearchApiEmbeddingBadge( searchApiEmbeddingStatus, @@ -976,806 +673,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 -

-
-
- -