From 38b880b5f2e2781de5518078d30c279e43161d6b Mon Sep 17 00:00:00 2001 From: John Cortright Date: Tue, 14 Jul 2026 22:15:12 -0700 Subject: [PATCH] chore: remove unrelated diff noise (lockfile, comments, indentation) --- .gitignore | 2 - admin/app/jobs/embed_file_job.ts | 4 + admin/app/models/kb_ingest_state.ts | 24 +-- .../components/chat/KnowledgeBaseModal.tsx | 29 ++++ admin/inertia/lib/api.ts | 15 +- admin/package-lock.json | 148 ++++++++++++++++-- 6 files changed, 197 insertions(+), 25 deletions(-) diff --git a/.gitignore b/.gitignore index 4bf6304..4273fe3 100644 --- a/.gitignore +++ b/.gitignore @@ -41,5 +41,3 @@ admin/public/assets # Admin specific development files admin/storage -nomad_admin_config.json -nomad_admin_hostconfig.json diff --git a/admin/app/jobs/embed_file_job.ts b/admin/app/jobs/embed_file_job.ts index 7bae3fb..cdb775b 100644 --- a/admin/app/jobs/embed_file_job.ts +++ b/admin/app/jobs/embed_file_job.ts @@ -18,6 +18,10 @@ export interface EmbedFileJobParams { batchOffset?: number // Current batch offset (for ZIM files) totalArticles?: number // Total articles in ZIM (for progress tracking) isFinalBatch?: boolean // Whether this is the last batch (prevents premature deletion) + // Running total of chunks embedded across prior batches in this dispatch chain. + // Carried forward so the final batch can persist an accurate `chunks_embedded` + // count via KbIngestState.markIndexed (see #933 -- without this, only the last + // batch's chunk count was stored while Qdrant held the full set). chunksSoFar?: number collection?: string } diff --git a/admin/app/models/kb_ingest_state.ts b/admin/app/models/kb_ingest_state.ts index 76da782..dac1028 100644 --- a/admin/app/models/kb_ingest_state.ts +++ b/admin/app/models/kb_ingest_state.ts @@ -41,20 +41,20 @@ export default class KbIngestState extends BaseModel { declare updated_at: DateTime static async getOrCreate(filePath: string, collection?: string): Promise { - return this.firstOrCreate( - { file_path: filePath }, - { file_path: filePath, state: 'pending_decision', chunks_embedded: 0, collection: collection ?? null } - ) - } + return this.firstOrCreate( + { file_path: filePath }, + { file_path: filePath, state: 'pending_decision', chunks_embedded: 0, collection: collection ?? null } + ) + } static async markIndexed(filePath: string, chunksEmbedded: number, collection?: string): Promise { - const row = await this.getOrCreate(filePath, collection) - row.state = 'indexed' - row.chunks_embedded = chunksEmbedded - row.last_error = null - if (collection) row.collection = collection - await row.save() - } + const row = await this.getOrCreate(filePath, collection) + row.state = 'indexed' + row.chunks_embedded = chunksEmbedded + row.last_error = null + if (collection) row.collection = collection + await row.save() + } static async markFailed(filePath: string, errorMessage: string): Promise { const row = await this.getOrCreate(filePath) diff --git a/admin/inertia/components/chat/KnowledgeBaseModal.tsx b/admin/inertia/components/chat/KnowledgeBaseModal.tsx index 77ac969..942fb0a 100644 --- a/admin/inertia/components/chat/KnowledgeBaseModal.tsx +++ b/admin/inertia/components/chat/KnowledgeBaseModal.tsx @@ -36,6 +36,9 @@ interface KnowledgeBaseModalProps { onClose: () => void } +// File extensions the in-browser viewer can render. Must stay in sync with +// `RagService.VIEWABLE_TEXT_EXTENSIONS` -- anything outside this set falls back +// to Download. const VIEWABLE_EXTENSIONS = new Set(['md', 'txt', 'csv', 'json', 'yaml', 'yml', 'toml', 'xml', 'html']) function isViewableExtension(filename: string): boolean { @@ -69,6 +72,13 @@ function renderSortHeader( ) } +/** + * Compact label for the per-row ingestion state. Files that exist in Qdrant + * with no `kb_ingest_state` row (`state === null`) are legacy/pre-RFC-883 + * installs whose chunks are real, so we display them as "Indexed" rather than + * surfacing the absent-row detail. Admin-docs group has no pill (the "Managed + * by NOMAD" message in the action column carries the same signal). + */ function renderStatePill(record: KbFileGroup): React.ReactNode { if (record.bucket === 'admin_docs') return null const effective: KbIngestStateValue = record.state ?? 'indexed' @@ -107,6 +117,13 @@ type RowAction = | { kind: 'index'; label: string; force: boolean; variant: 'primary'; icon: DynamicIconName } | { kind: 'reembed'; label: string; force: true; variant: 'secondary'; icon: DynamicIconName } +/** + * Pick the single adaptive per-row action button. Returns null when no action + * makes sense for the current state (e.g. healthy indexed file with no + * warnings -- bulk Re-embed All covers that case). `hasWarnings` lets us + * surface a Re-embed affordance specifically when a file *looks* indexed but + * has zero chunks or a stalled-mid-ingestion warning attached. + */ function pickRowAction(record: KbFileGroup, hasWarnings: boolean): RowAction | null { if (record.bucket === 'admin_docs') return null const effective: KbIngestStateValue = record.state ?? 'indexed' @@ -171,6 +188,10 @@ export default function KnowledgeBaseModal({ aiAssistantName = "AI Assistant", o return Array.from(new Set([...KB_COLLECTIONS, ...knownCollections])).sort() }, [knownCollections]) + // Per-file conditional warnings (RFC #883 section 6). `ok: false` means the + // computation itself failed (Qdrant/DB/FS) -- distinct from `ok: true` with + // an empty map, which means everything is healthy. We surface the failure + // explicitly so a silent backend failure doesn't masquerade as health. const { data: warningsResult } = useQuery({ queryKey: ['kbFileWarnings'], queryFn: () => api.getKbFileWarnings(), @@ -179,6 +200,9 @@ export default function KnowledgeBaseModal({ aiAssistantName = "AI Assistant", o const fileWarnings = warningsResult?.warnings ?? {} const warningsUnavailable = warningsResult !== undefined && warningsResult.ok === false + // Global auto-index policy. KVStore returns `null` for an unset key, which + // we treat as 'Always' for backward compatibility with installs that predate + // this UI. The user can opt into Manual mode from the toggle below. const { data: ingestPolicySetting } = useQuery({ queryKey: ['ingestPolicy'], queryFn: () => api.getSetting('rag.defaultIngestPolicy'), @@ -1036,8 +1060,13 @@ function FileViewerModal({ source, onClose }: { source: string; onClose: () => v staleTime: 60_000, }) + // Title falls back to the trailing path segment so the modal still has a + // useful header while the fetch is in-flight or if it failed. const fallbackName = source.split(/[/\\]/).at(-1) ?? source const title = data?.fileName ?? fallbackName + // `catchInternal` swallows errors and resolves to undefined, surfacing a + // toast -- so the "couldn't load" branch is gated on a finished-but-empty + // fetch rather than on react-query's `isError`. const showError = isFetched && !data return ( diff --git a/admin/inertia/lib/api.ts b/admin/inertia/lib/api.ts index e9c0fc8..c9baa8a 100644 --- a/admin/inertia/lib/api.ts +++ b/admin/inertia/lib/api.ts @@ -273,6 +273,14 @@ class API { })() } + /** + * Ask the backend to send Ollama `keep_alive: 0` to every currently-loaded + * chat model except `targetModel` (and the embedding model, which is always + * exempt server-side). Fire-and-forget -- the chat UI doesn't await this + * before creating a new session, since unload is housekeeping. + * + * Pass `null` to unload every chat model. + */ async unloadChatModels(targetModel: string | null) { return catchInternal(async () => { const response = await this.client.post<{ unloaded: string[] }>( @@ -307,6 +315,7 @@ class API { onChunk: (content: string, thinking: string, done: boolean) => void, signal?: AbortSignal ): Promise { + // Axios doesn't support ReadableStream in browser, so need to use fetch const response = await fetch('/api/ollama/chat', { method: 'POST', headers: { 'Content-Type': 'application/json' }, @@ -336,7 +345,7 @@ class API { let data: any try { data = JSON.parse(line.slice(6)) - } catch { continue } + } catch { continue /* skip malformed chunks */ } if (data.error) throw new Error('The model encountered an error. Please try again.') @@ -856,11 +865,13 @@ class API { const response = await this.client.post('/benchmark/submit', { benchmark_id, anonymous }) return response.data } catch (error: any) { + // For 409 Conflict errors, throw a specific error that the UI can handle if (error.response?.status === 409) { const err = new Error(error.response?.data?.error || 'This benchmark has already been submitted to the repository') ; (err as any).status = 409 throw err } + // For other errors, extract the message and throw const errorMessage = error.response?.data?.error || error.message || 'Failed to submit benchmark' throw new Error(errorMessage) } @@ -934,6 +945,8 @@ class API { })() } + // Wikipedia selector methods + async getWikipediaState(): Promise { return catchInternal(async () => { const response = await this.client.get('/zim/wikipedia') diff --git a/admin/package-lock.json b/admin/package-lock.json index 2c08c3c..0a18db6 100644 --- a/admin/package-lock.json +++ b/admin/package-lock.json @@ -2230,6 +2230,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2246,6 +2249,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2262,6 +2268,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2278,6 +2287,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2294,6 +2306,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2310,6 +2325,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2326,6 +2344,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2342,6 +2363,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "LGPL-3.0-or-later", "optional": true, "os": [ @@ -2358,6 +2382,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2380,6 +2407,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2402,6 +2432,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2424,6 +2457,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2446,6 +2482,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2468,6 +2507,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2490,6 +2532,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -2512,6 +2557,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "Apache-2.0", "optional": true, "os": [ @@ -3387,6 +3435,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3403,6 +3454,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -3419,6 +3473,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3435,6 +3492,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -3451,6 +3511,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4136,6 +4199,9 @@ "cpu": [ "arm" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4149,6 +4215,9 @@ "cpu": [ "arm" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4162,6 +4231,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4175,6 +4247,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4188,6 +4263,9 @@ "cpu": [ "loong64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4201,6 +4279,9 @@ "cpu": [ "loong64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4214,6 +4295,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4227,6 +4311,9 @@ "cpu": [ "ppc64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4240,6 +4327,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4253,6 +4343,9 @@ "cpu": [ "riscv64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4266,6 +4359,9 @@ "cpu": [ "s390x" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4279,6 +4375,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4292,6 +4391,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4488,7 +4590,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4505,7 +4606,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4522,7 +4622,6 @@ "cpu": [ "arm" ], - "dev": true, "license": "Apache-2.0", "optional": true, "os": [ @@ -4539,7 +4638,9 @@ "cpu": [ "arm64" ], - "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4556,7 +4657,9 @@ "cpu": [ "arm64" ], - "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4573,7 +4676,9 @@ "cpu": [ "x64" ], - "dev": true, + "libc": [ + "glibc" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4590,7 +4695,9 @@ "cpu": [ "x64" ], - "dev": true, + "libc": [ + "musl" + ], "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4607,7 +4714,6 @@ "cpu": [ "arm64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4624,7 +4730,6 @@ "cpu": [ "ia32" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4641,7 +4746,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "Apache-2.0 AND MIT", "optional": true, "os": [ @@ -4834,6 +4938,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4850,6 +4957,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -4866,6 +4976,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MIT", "optional": true, "os": [ @@ -4882,6 +4995,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MIT", "optional": true, "os": [ @@ -11124,6 +11240,9 @@ "cpu": [ "arm64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11144,6 +11263,9 @@ "cpu": [ "arm64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11164,6 +11286,9 @@ "cpu": [ "x64" ], + "libc": [ + "glibc" + ], "license": "MPL-2.0", "optional": true, "os": [ @@ -11184,6 +11309,9 @@ "cpu": [ "x64" ], + "libc": [ + "musl" + ], "license": "MPL-2.0", "optional": true, "os": [