From 3ae690ac3c52f3046d712b3c8d1ea6a01f60a9a8 Mon Sep 17 00:00:00 2001 From: nic Date: Sun, 3 May 2026 17:07:18 +0200 Subject: [PATCH 1/9] feat(rag-task): Add Rag task to workflow dialog --- src/components/editors/add-extension-dialog.tsx | 1 + src/hooks/use-extensions-store.ts | 13 +++++++++++++ src/lib/api/extensions.ts | 5 +++-- src/pages/workflow-detail.tsx | 6 +++++- 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/src/components/editors/add-extension-dialog.tsx b/src/components/editors/add-extension-dialog.tsx index ca9c388d..14d41ea2 100644 --- a/src/components/editors/add-extension-dialog.tsx +++ b/src/components/editors/add-extension-dialog.tsx @@ -37,6 +37,7 @@ function buildEddiUri(rt: ResourceTypeConfig, id: string, version: number): stri output: "ai.labs.output", propertysetter: "ai.labs.property", mcpcalls: "ai.labs.mcpcalls", + rag: "ai.labs.rag", }; const host = slugToHost[rt.slug] ?? `ai.labs.${rt.slug}`; return `eddi://${host}/${rt.store}/${rt.plural}/${id}?version=${version}`; diff --git a/src/hooks/use-extensions-store.ts b/src/hooks/use-extensions-store.ts index a8228acb..3d934d56 100644 --- a/src/hooks/use-extensions-store.ts +++ b/src/hooks/use-extensions-store.ts @@ -22,6 +22,19 @@ const WELL_KNOWN_TYPES: ExtensionDescriptor[] = [ }, extensions: {}, }, + { + type: "ai.labs.rag", + displayName: "RAG Knowledge Base", + configs: { + uri: { + displayName: "Resource URI", + fieldType: "URI", + isOptional: false, + defaultValue: null, + }, + }, + extensions: {}, + }, ]; /** diff --git a/src/lib/api/extensions.ts b/src/lib/api/extensions.ts index 223664ec..0927b01d 100644 --- a/src/lib/api/extensions.ts +++ b/src/lib/api/extensions.ts @@ -1,4 +1,4 @@ -import { Brain, FileCode, FileText, GitBranch, Globe, MessageSquareText, Plug, Puzzle, Settings } from "lucide-react"; +import { Brain, BookOpenCheck, FileCode, FileText, GitBranch, Globe, MessageSquareText, Plug, Puzzle, Settings } from "lucide-react"; import { api } from "../api-client"; /** Matches EDDI backend ExtensionDescriptor.ConfigValue */ @@ -49,6 +49,7 @@ const iconMap: Record> = { Settings, FileCode, Plug, + BookOpenCheck, }; /** Well-known extension type IDs and their display info */ @@ -69,7 +70,7 @@ export const EXTENSION_TYPE_INFO: Record< "eddi://ai.labs.mcpcalls": { label: "MCP Calls", icon: "Plug", order: 11, color: "text-rose-400" }, "eddi://ai.labs.workflow": { label: "Workflow", icon: "FileText", order: 12, color: "text-indigo-400" }, "eddi://ai.labs.dictionary": { label: "Dictionary", icon: "FileText", order: 13, color: "text-amber-400" }, - "eddi://ai.labs.rag": { label: "RAG", icon: "FileText", order: 14, color: "text-purple-400" }, + "eddi://ai.labs.rag": { label: "Knowledge Bases", icon: "BookOpenCheck", order: 14, color: "text-purple-400" }, }; /** Get a human-readable label for an extension type */ diff --git a/src/pages/workflow-detail.tsx b/src/pages/workflow-detail.tsx index 63238b18..2bd58502 100644 --- a/src/pages/workflow-detail.tsx +++ b/src/pages/workflow-detail.tsx @@ -152,8 +152,12 @@ export function WorkflowDetailPage() { const handleAddExtension = useCallback( (result: AddExtensionResult) => { + // Ensure type has eddi:// prefix (e.g., "eddi://ai.labs.rag") + const type = result.descriptor.type.startsWith("eddi://") + ? result.descriptor.type + : `eddi://${result.descriptor.type}`; const newExt: WorkflowExtension = { - type: result.descriptor.type, + type, extensions: {}, config: result.configUri ? { uri: result.configUri } : {}, }; From 68a4366e248033e357cf97a7f1da0a93cbdaac5b Mon Sep 17 00:00:00 2001 From: nic Date: Tue, 5 May 2026 19:15:43 +0200 Subject: [PATCH 2/9] feat(initial): Cleanup needed --- .../editors/ingestion-source-editor.tsx | 762 ++++++++++++++++++ src/components/editors/rag-editor.tsx | 22 + src/hooks/use-ingestion-sources.ts | 60 ++ src/i18n/locales/ar.json | 3 +- src/i18n/locales/de.json | 3 +- src/i18n/locales/en.json | 3 +- src/i18n/locales/es.json | 3 +- src/i18n/locales/fr.json | 3 +- src/i18n/locales/hi.json | 3 +- src/i18n/locales/ja.json | 3 +- src/i18n/locales/ko.json | 3 +- src/i18n/locales/pt.json | 3 +- src/i18n/locales/th.json | 3 +- src/i18n/locales/zh.json | 3 +- src/lib/api/ingestion-sources.ts | 119 +++ .../__tests__/resource-detail-rag.test.tsx | 105 +++ src/test/mocks/handlers.ts | 105 +++ src/test/setup.ts | 16 + 18 files changed, 1211 insertions(+), 11 deletions(-) create mode 100644 src/components/editors/ingestion-source-editor.tsx create mode 100644 src/hooks/use-ingestion-sources.ts create mode 100644 src/lib/api/ingestion-sources.ts diff --git a/src/components/editors/ingestion-source-editor.tsx b/src/components/editors/ingestion-source-editor.tsx new file mode 100644 index 00000000..4e780cf6 --- /dev/null +++ b/src/components/editors/ingestion-source-editor.tsx @@ -0,0 +1,762 @@ +import { useState, useCallback } from "react"; +import { + Globe, + GlobeLock, + Settings, + Clock, + Scissors, + Plus, + X, + Play, + Loader2, + ChevronDown, +} from "lucide-react"; +import { cn } from "@/lib/utils"; +import { + parseUriId, + type RagIngestionSource, + type WebSourceConfig, + type Scope, + type CrawlSettings, + type IngestionSettings, + type Schedule, +} from "@/lib/api/ingestion-sources"; +import { + useIngestionSources, + useCreateIngestionSource, + useUpdateIngestionSource, + useDeleteIngestionSource, + useTriggerIngestion, +} from "@/hooks/use-ingestion-sources"; + +const EMPTY_SOURCE: RagIngestionSource = { + name: "", + type: "web", + sourceConfig: { startUrl: "" }, + ragConfigUri: "", +}; + +const SOURCE_TYPES = [ + { value: "web", label: "Web Crawler", hint: "Crawl websites with TOC extraction", disabled: false }, + { value: "file", label: "File System", hint: "Coming soon", disabled: true }, + { value: "git", label: "Git Repository", hint: "Coming soon", disabled: true }, + { value: "api", label: "API Endpoint", hint: "Coming soon", disabled: true }, +] as const; + +const CHUNK_STRATEGIES = [ + { value: "recursive", label: "Recursive (recommended)" }, + { value: "paragraph", label: "Paragraph" }, + { value: "sentence", label: "Sentence" }, +] as const; + +const CRON_PRESETS = [ + { label: "Hourly", expr: "0 * * * *" }, + { label: "Daily", expr: "0 2 * * *" }, + { label: "Weekly", expr: "0 2 * * 0" }, + { label: "Monthly", expr: "0 2 1 * *" }, +] as const; + +function Section({ + label, + icon: Icon, + accent, + defaultOpen = true, + children, +}: { + label: string; + icon: React.ElementType; + accent: string; + defaultOpen?: boolean; + children: React.ReactNode; +}) { + const [open, setOpen] = useState(defaultOpen); + return ( +
+ + {open &&
{children}
} +
+ ); +} + +export interface IngestionSourceEditorProps { + initial?: RagIngestionSource; + ragConfigUri: string; + onSave: (source: RagIngestionSource) => void; + onCancel: () => void; + readOnly?: boolean; +} + +export function IngestionSourceEditor({ + initial, + ragConfigUri, + onSave, + onCancel, + readOnly = false, +}: IngestionSourceEditorProps) { + const [source, setSource] = useState( + initial ?? { ...EMPTY_SOURCE, ragConfigUri }, + ); + + const update = useCallback( + (patch: Partial) => setSource((prev) => ({ ...prev, ...patch })), + [], + ); + + const isWeb = source.type === "web"; + const webConfig = source.sourceConfig as WebSourceConfig | undefined; + const scope = webConfig?.scope; + const crawlSettings = webConfig?.crawlSettings; + const ingestSettings = source.ingestionSettings; + const schedule = source.schedule; + + return ( +
+
+
+
+ + update({ name: e.target.value })} + readOnly={readOnly} + placeholder="e.g. Product Documentation" + className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="ingestion-source-name" + /> +
+
+ + update({ description: e.target.value || undefined })} + readOnly={readOnly} + placeholder="Optional description" + className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="ingestion-source-description" + /> +
+
+ +
+ {SOURCE_TYPES.map((st) => ( + + ))} +
+
+
+
+ + {isWeb && ( +
+
+
+ + + update({ sourceConfig: { ...webConfig, startUrl: e.target.value } as WebSourceConfig }) + } + readOnly={readOnly} + placeholder="https://docs.example.com" + className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-start-url" + /> +
+
+ + + update({ sourceConfig: { ...webConfig, tocSelector: e.target.value || undefined } as WebSourceConfig }) + } + readOnly={readOnly} + placeholder="nav.sidebar a[href]" + className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-toc-selector" + /> +

+ CSS selector for navigation links. If empty, only the start URL is crawled. +

+
+ +
+
+ + + Scope Constraints + +
+ +
+
+ + + update({ + sourceConfig: { + ...webConfig, + scope: { ...scope, pathPrefix: e.target.value || "/" } as Scope, + } as WebSourceConfig, + }) + } + readOnly={readOnly} + className="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-path-prefix" + /> +
+
+ + + update({ + sourceConfig: { + ...webConfig, + scope: { ...scope, maxDepth: parseInt(e.target.value, 10) || 0 } as Scope, + } as WebSourceConfig, + }) + } + readOnly={readOnly} + className="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-max-depth" + /> +
+
+ + + update({ + sourceConfig: { + ...webConfig, + scope: { ...scope, maxPages: parseInt(e.target.value, 10) || 1 } as Scope, + } as WebSourceConfig, + }) + } + readOnly={readOnly} + className="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-max-pages" + /> +
+
+
+ + + update({ + sourceConfig: { + ...webConfig, + scope: { ...scope, excludePatterns: patterns.length > 0 ? patterns : undefined } as Scope, + } as WebSourceConfig, + }) + } + readOnly={readOnly} + /> +
+
+
+
+ +
+
+ + + Crawl Settings + +
+
+
+ + + update({ + sourceConfig: { + ...webConfig, + crawlSettings: { ...crawlSettings, requestDelayMs: parseInt(e.target.value, 10) || 0 } as CrawlSettings, + } as WebSourceConfig, + }) + } + readOnly={readOnly} + className="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-request-delay" + /> +
+
+ + + update({ + sourceConfig: { + ...webConfig, + crawlSettings: { ...crawlSettings, timeoutSeconds: parseInt(e.target.value, 10) || 15 } as CrawlSettings, + } as WebSourceConfig, + }) + } + readOnly={readOnly} + className="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-timeout" + /> +
+
+
+ + + update({ + sourceConfig: { + ...webConfig, + crawlSettings: { ...crawlSettings, userAgent: e.target.value || undefined } as CrawlSettings, + } as WebSourceConfig, + }) + } + readOnly={readOnly} + className="h-7 w-full rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-user-agent" + /> +
+
+
+
+
+
+ )} + +
+
+
+ + +
+
+
+
+ + {ingestSettings?.chunkSize ?? 512} chars +
+ update({ ingestionSettings: { ...ingestSettings, chunkSize: parseInt(e.target.value, 10) } as IngestionSettings })} + disabled={readOnly} + className="w-full accent-primary" + data-testid="source-chunk-size" + /> +
+
+
+ + {ingestSettings?.chunkOverlap ?? 64} chars +
+ update({ ingestionSettings: { ...ingestSettings, chunkOverlap: parseInt(e.target.value, 10) } as IngestionSettings })} + disabled={readOnly} + className="w-full accent-primary" + data-testid="source-chunk-overlap" + /> +
+
+
+ +
+ + update({ ingestionSettings: { ...ingestSettings, maxContentLength: parseInt(e.target.value, 10) || 100000 } as IngestionSettings })} + readOnly={readOnly} + className="h-7 w-32 rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-max-content" + /> +
+
+
+
+ +
+
+ +
+ +
+ update({ schedule: { ...schedule, cronExpression: e.target.value || "0 2 * * *" } as Schedule })} + readOnly={readOnly} + placeholder="0 2 * * *" + className="h-8 flex-1 rounded-md border border-input bg-background px-3 font-mono text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + data-testid="source-cron" + /> +
+

Format: minute hour day month weekday

+
+ {CRON_PRESETS.map((preset) => ( + + ))} +
+
+
+
+ + {!readOnly && ( +
+ + +
+ )} +
+ ); +} + +function ExcludePatternEditor({ + patterns, + onChange, + readOnly, +}: { + patterns: string[]; + onChange: (patterns: string[]) => void; + readOnly?: boolean; +}) { + const [input, setInput] = useState(""); + + const add = () => { + const trimmed = input.trim(); + if (!trimmed) return; + onChange([...patterns, trimmed]); + setInput(""); + }; + + return ( +
+ {patterns.length > 0 && ( +
+ {patterns.map((p, i) => ( + + {p} + {!readOnly && ( + + )} + + ))} +
+ )} + {!readOnly && ( +
+ setInput(e.target.value)} + onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); add(); } }} + placeholder="**/legacy/**" + className="h-7 flex-1 rounded border border-input bg-background px-2 text-xs text-foreground focus:outline-none focus:ring-1 focus:ring-ring" + /> + +
+ )} +
+ ); +} + +export function IngestionSourcesPanel({ + resourceId, + version, + readOnly, +}: { + resourceId: string; + version: number; + readOnly?: boolean; +}) { + const ragConfigUri = `eddi://ai.labs.rag/ragstore/rags/${resourceId}?version=${version}`; + const { data: sources, isLoading } = useIngestionSources(ragConfigUri); + const createMutation = useCreateIngestionSource(); + const updateMutation = useUpdateIngestionSource(); + const deleteMutation = useDeleteIngestionSource(); + const triggerMutation = useTriggerIngestion(); + + const [editing, setEditing] = useState<{ source: RagIngestionSource; id?: string; version?: number } | null>(null); + const [triggering, setTriggering] = useState(null); + const [isAdding, setIsAdding] = useState(false); + + const handleSave = (source: RagIngestionSource) => { + if (editing?.id && editing?.version !== undefined) { + updateMutation.mutate({ id: editing.id, version: editing.version, source }); + } else { + createMutation.mutate(source); + } + setEditing(null); + setIsAdding(false); + }; + + const handleTrigger = (id: string, version: number) => { + setTriggering(id); + triggerMutation.mutate({ id, version }, { + onSettled: () => setTriggering(null), + }); + }; + + return ( +
+ {isLoading && ( +
+ + Loading ingestion sources... +
+ )} + + {!isLoading && sources && sources.length === 0 && !isAdding && ( +

+ No ingestion sources configured. Add one to automatically crawl content into this knowledge base. +

+ )} + + {sources && sources.length > 0 && ( +
+ {sources.map((src, idx) => { + const parsed = src.resource ? parseUriId(src.resource) : null; + const srcId = parsed?.id ?? `src-${idx}`; + const srcVersion = parsed?.version ?? 1; + const isWeb = src.type === "web"; + const wc = src.sourceConfig as WebSourceConfig; + return ( +
+
+
+
+ {src.name} + {src.type} +
+ {src.description && ( +

{src.description}

+ )} + {isWeb && wc?.startUrl && ( +

{wc.startUrl}

+ )} +
+
+ {!readOnly && ( + <> + + + + + )} +
+
+
+ ); + })} +
+ )} + + {!readOnly && !isAdding && ( + + )} + + {(isAdding || editing) && ( + { setEditing(null); setIsAdding(false); }} + readOnly={false} + /> + )} +
+ ); +} diff --git a/src/components/editors/rag-editor.tsx b/src/components/editors/rag-editor.tsx index 3ee56e00..a9142cab 100644 --- a/src/components/editors/rag-editor.tsx +++ b/src/components/editors/rag-editor.tsx @@ -15,10 +15,12 @@ import { AlertCircle, Lock, ChevronDown, + Globe, } from "lucide-react"; import { cn } from "@/lib/utils"; import { api } from "@/lib/api-client"; import { SecretKeyPicker } from "@/components/shared/secret-key-picker"; +import { IngestionSourcesPanel } from "@/components/editors/ingestion-source-editor"; // ─── Types ─────────────────────────────────────────────────────────────────── @@ -1006,6 +1008,26 @@ export function RagEditor({ data, onChange, readOnly, resourceId, version = 1 }:

)} + + {/* ══════ Ingestion Sources ══════ */} +
+ {resourceId ? ( + + ) : ( +

+ {t("ragEditor.saveFirstIngestion", "Save this knowledge base first to enable document ingestion.")} +

+ )} +
); } diff --git a/src/hooks/use-ingestion-sources.ts b/src/hooks/use-ingestion-sources.ts new file mode 100644 index 00000000..cae98429 --- /dev/null +++ b/src/hooks/use-ingestion-sources.ts @@ -0,0 +1,60 @@ +import { useQuery, useMutation, useQueryClient } from "@tanstack/react-query"; +import { + listIngestionSources, + createIngestionSource, + updateIngestionSource, + deleteIngestionSource, + triggerIngestion, + type RagIngestionSource, +} from "@/lib/api/ingestion-sources"; + +const ingestionKeys = { + all: (ragConfigUri: string) => ["ingestion-sources", ragConfigUri] as const, + list: (ragConfigUri: string) => ["ingestion-sources", "list", ragConfigUri] as const, +}; + +export function useIngestionSources(ragConfigUri?: string) { + return useQuery({ + queryKey: ingestionKeys.list(ragConfigUri ?? ""), + queryFn: () => listIngestionSources(ragConfigUri!), + enabled: !!ragConfigUri, + }); +} + +export function useCreateIngestionSource() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (source: RagIngestionSource) => createIngestionSource(source), + onSuccess: (_data, source) => { + qc.invalidateQueries({ queryKey: ingestionKeys.all(source.ragConfigUri) }); + }, + }); +} + +export function useUpdateIngestionSource() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (args: { id: string; version: number; source: RagIngestionSource }) => + updateIngestionSource(args.id, args.version, args.source), + onSuccess: (_data, args) => { + qc.invalidateQueries({ queryKey: ingestionKeys.all(args.source.ragConfigUri) }); + }, + }); +} + +export function useDeleteIngestionSource() { + const qc = useQueryClient(); + return useMutation({ + mutationFn: (args: { id: string; version: number; ragConfigUri: string }) => + deleteIngestionSource(args.id, args.version), + onSuccess: (_data, args) => { + qc.invalidateQueries({ queryKey: ingestionKeys.all(args.ragConfigUri) }); + }, + }); +} + +export function useTriggerIngestion() { + return useMutation({ + mutationFn: (args: { id: string; version: number }) => triggerIngestion(args.id, args.version), + }); +} diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index 20b9fd4f..64d39d60 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -1127,7 +1127,8 @@ "ingestText": "استيعاب النص", "ingestionHistory": "حالة الاستيعاب", "saveFirstIngestion": "احفظ قاعدة المعرفة هذه أولاً لتمكين استيعاب المستندات.", - "addParam": "إضافة معلمة" + "addParam": "إضافة معلمة", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index 0eac1de0..fad95f28 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -1127,7 +1127,8 @@ "ingestText": "Text aufnehmen", "ingestionHistory": "Aufnahme-Status", "saveFirstIngestion": "Speichern Sie diese Knowledge Base zuerst, um die Dokumenten-Aufnahme zu aktivieren.", - "addParam": "Parameter hinzufügen" + "addParam": "Parameter hinzufügen", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index f5a7af28..6d75de3f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1200,7 +1200,8 @@ "ingestText": "Ingest Text", "ingestionHistory": "Ingestion Status", "saveFirstIngestion": "Save this knowledge base first to enable document ingestion.", - "addParam": "Add Parameter" + "addParam": "Add Parameter", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index bf30a046..ca4ef029 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1127,7 +1127,8 @@ "ingestText": "Ingerir texto", "ingestionHistory": "Estado de ingesta", "saveFirstIngestion": "Guarde esta base de conocimiento primero para activar la ingesta de documentos.", - "addParam": "Añadir parámetro" + "addParam": "Añadir parámetro", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index 6103d2d9..bc76acb0 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1127,7 +1127,8 @@ "ingestText": "Ingérer le texte", "ingestionHistory": "Statut d'ingestion", "saveFirstIngestion": "Enregistrez d'abord cette base pour autoriser l'ingestion.", - "addParam": "Ajout paramètre" + "addParam": "Ajout paramètre", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index 9cf46c67..5e39f7ac 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -1127,7 +1127,8 @@ "ingestText": "टेक्स्ट सुरक्षित करें", "ingestionHistory": "अंतर्ग्रहण स्थिति", "saveFirstIngestion": "दस्तावेज़ को प्रोसेस करने से पहले इस नॉलेज बेस को सहेजें।", - "addParam": "पैरामीटर जोड़ें" + "addParam": "पैरामीटर जोड़ें", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index b03b52ca..084ea5df 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1127,7 +1127,8 @@ "ingestText": "テキストを取り込む", "ingestionHistory": "取り込みステータス", "saveFirstIngestion": "ドキュメントの取り込みを有効にするには、先にこのナレッジベースを保存してください。", - "addParam": "パラメーターの追加" + "addParam": "パラメーターの追加", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index f91c0b8a..a3528437 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -1127,7 +1127,8 @@ "ingestText": "텍스트 수집", "ingestionHistory": "수집 상태", "saveFirstIngestion": "문서 수집을 활성화하려면 먼저 이 지식 베이스를 저장하세요.", - "addParam": "파라미터 추가" + "addParam": "파라미터 추가", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 7df90d6f..7e4b476f 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -1127,7 +1127,8 @@ "ingestText": "Ingerir Texto", "ingestionHistory": "Status de Ingestão", "saveFirstIngestion": "Salve a base de conhecimento primeiro para ativar a ingestão.", - "addParam": "Adicionar Parâmetro" + "addParam": "Adicionar Parâmetro", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/th.json b/src/i18n/locales/th.json index 4fbd0d1d..ed237fe3 100644 --- a/src/i18n/locales/th.json +++ b/src/i18n/locales/th.json @@ -1127,7 +1127,8 @@ "ingestText": "นำเข้าชนิดข้อความ", "ingestionHistory": "สถานะการประมวล", "saveFirstIngestion": "หลังจากนี้ ค่อยเพิ่มความสมบูรณ์ฐานความรู้ให้กับเอกสารต่อๆไป", - "addParam": "แอดพารามิเตอร์" + "addParam": "แอดพารามิเตอร์", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index 96202913..aed25313 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1127,7 +1127,8 @@ "ingestText": "摄入文本", "ingestionHistory": "摄入状态", "saveFirstIngestion": "使用前请先保存此知识库。", - "addParam": "添加参数" + "addParam": "添加参数", + "ingestionSources": "Ingestion Sources" }, "onboarding": { "welcome": { diff --git a/src/lib/api/ingestion-sources.ts b/src/lib/api/ingestion-sources.ts new file mode 100644 index 00000000..f7859558 --- /dev/null +++ b/src/lib/api/ingestion-sources.ts @@ -0,0 +1,119 @@ +import { api } from "@/lib/api-client"; + +export interface RagIngestionSource { + resource?: string; + name: string; + description?: string; + type: "web" | "file" | "git" | "api"; + sourceConfig: WebSourceConfig | FileSourceConfig | GitSourceConfig | ApiSourceConfig; + ragConfigUri: string; + ingestionSettings?: IngestionSettings; + schedule?: Schedule; +} + +export interface WebSourceConfig { + startUrl: string; + tocSelector?: string; + scope?: Scope; + crawlSettings?: CrawlSettings; +} + +export interface Scope { + sameDomainOnly?: boolean; + pathPrefix?: string; + maxDepth?: number; + maxPages?: number; + excludePatterns?: string[]; +} + +export interface CrawlSettings { + requestDelayMs?: number; + timeoutSeconds?: number; + userAgent?: string; +} + +export interface FileSourceConfig { + basePath: string; + globPattern: string; + recursive: boolean; + encoding?: string; +} + +export interface GitSourceConfig { + repoUrl: string; + branch: string; + path: string; + accessTokenSecret?: string; + filePattern?: string; +} + +export interface ApiSourceConfig { + endpointUrl: string; + method: "GET" | "POST"; + headers?: Record; + pagination?: PaginationConfig; + dataPath?: string; +} + +export interface PaginationConfig { + type: "page" | "cursor" | "offset"; + pageParam?: string; + limitParam?: string; + limit?: number; +} + +export interface IngestionSettings { + chunkStrategy?: "recursive" | "paragraph" | "sentence"; + chunkSize?: number; + chunkOverlap?: number; + contentHashDedup?: boolean; + maxContentLength?: number; +} + +export interface Schedule { + cronExpression?: string; + enabled?: boolean; +} + +const STORE = "ragstore"; +const PLURAL = "ingestion-sources"; + +export function listIngestionSources(ragConfigUri: string): Promise { + const params = new URLSearchParams({ ragConfigUri }); + return api.get(`/${STORE}/${PLURAL}/byRagConfig?${params}`); +} + +export function getIngestionSource(id: string): Promise { + return api.get(`/${STORE}/${PLURAL}/${id}`); +} + +export function createIngestionSource(source: RagIngestionSource): Promise<{ location: string }> { + return api.post<{ location: string }>(`/${STORE}/${PLURAL}`, source); +} + +export function updateIngestionSource( + id: string, + version: number, + source: RagIngestionSource, +): Promise<{ location: string }> { + return api.put<{ location: string }>(`/${STORE}/${PLURAL}/${id}?version=${version}`, source); +} + +export function deleteIngestionSource(id: string, version: number): Promise { + return api.delete(`/${STORE}/${PLURAL}/${id}?version=${version}`); +} + +export function triggerIngestion(id: string, version: number): Promise<{ status: string }> { + return api.post<{ status: string }>(`/${STORE}/${PLURAL}/${id}/trigger?version=${version}`); +} + +export function parseUriId(uri: string): { id: string; version: number } | null { + const match = uri.match(/\/([^/]+)\?version=(\d+)$/); + if (!match) return null; + return { id: match[1]!, version: parseInt(match[2]!, 10) }; +} + +export function parseUriVersion(uri: string): number { + const match = uri.match(/version=(\d+)/); + return match ? parseInt(match[1]!, 10) : 1; +} diff --git a/src/pages/__tests__/resource-detail-rag.test.tsx b/src/pages/__tests__/resource-detail-rag.test.tsx index e898abcf..803572b0 100644 --- a/src/pages/__tests__/resource-detail-rag.test.tsx +++ b/src/pages/__tests__/resource-detail-rag.test.tsx @@ -305,4 +305,109 @@ describe("RAG Knowledge Base Editor", () => { expect(screen.getByTestId("json-view")).toBeInTheDocument(); }); }); + + // ─── Ingestion Sources ───────────────────────────────── + + /** Helper: expand the "Ingestion Sources" section (collapsed by default) */ + async function expandIngestionSources() { + await waitFor(() => { + expect(screen.getByTestId("rag-editor")).toBeInTheDocument(); + }); + const sectionButtons = screen.getAllByRole("button", { expanded: false }); + const sourcesBtn = sectionButtons.find((btn) => + btn.textContent?.toLowerCase().includes("ingestion sources"), + ); + expect(sourcesBtn).toBeDefined(); + fireEvent.click(sourcesBtn!); + await waitFor(() => { + expect(screen.getByTestId("ingestion-sources-panel")).toBeInTheDocument(); + }); + } + + it("renders ingestion sources section when expanded", async () => { + renderPage("rag"); + await expandIngestionSources(); + }); + + it("shows existing ingestion source from mock data", async () => { + renderPage("rag"); + await expandIngestionSources(); + await waitFor(() => { + expect(screen.getByTestId("source-item-0")).toBeInTheDocument(); + }); + expect(screen.getByText("Product Documentation")).toBeInTheDocument(); + }); + + it("shows add ingestion source button when not read-only", async () => { + renderPage("rag"); + await expandIngestionSources(); + await waitFor(() => { + expect(screen.getByTestId("add-ingestion-source-btn")).toBeInTheDocument(); + }); + }); + + it("opens ingestion source editor form on add click", async () => { + renderPage("rag"); + await expandIngestionSources(); + fireEvent.click(screen.getByTestId("add-ingestion-source-btn")); + await waitFor(() => { + expect(screen.getByTestId("ingestion-source-editor")).toBeInTheDocument(); + }); + }); + + it("renders source type selection buttons", async () => { + renderPage("rag"); + await expandIngestionSources(); + fireEvent.click(screen.getByTestId("add-ingestion-source-btn")); + await waitFor(() => { + expect(screen.getByTestId("source-type-web")).toBeInTheDocument(); + expect(screen.getByTestId("source-type-file")).toBeInTheDocument(); + expect(screen.getByTestId("source-type-git")).toBeInTheDocument(); + expect(screen.getByTestId("source-type-api")).toBeInTheDocument(); + }); + }); + + it("web source type is selected by default", async () => { + renderPage("rag"); + await expandIngestionSources(); + fireEvent.click(screen.getByTestId("add-ingestion-source-btn")); + await waitFor(() => { + const webBtn = screen.getByTestId("source-type-web"); + expect(webBtn.className).toContain("ring"); + }); + }); + + it("renders start url input when adding web source", async () => { + renderPage("rag"); + await expandIngestionSources(); + fireEvent.click(screen.getByTestId("add-ingestion-source-btn")); + await waitFor(() => { + expect(screen.getByTestId("source-start-url")).toBeInTheDocument(); + }); + }); + + it("renders cron preset buttons", async () => { + renderPage("rag"); + await expandIngestionSources(); + fireEvent.click(screen.getByTestId("add-ingestion-source-btn")); + await waitFor(() => { + expect(screen.getByTestId("cron-preset-hourly")).toBeInTheDocument(); + expect(screen.getByTestId("cron-preset-daily")).toBeInTheDocument(); + expect(screen.getByTestId("cron-preset-weekly")).toBeInTheDocument(); + expect(screen.getByTestId("cron-preset-monthly")).toBeInTheDocument(); + }); + }); + + it("shows edit and delete buttons on existing sources", async () => { + renderPage("rag"); + await expandIngestionSources(); + await waitFor(() => { + expect(screen.getByTestId("source-item-0")).toBeInTheDocument(); + }); + await waitFor(() => { + expect(screen.getByTestId("source-edit-0")).toBeInTheDocument(); + expect(screen.getByTestId("source-delete-0")).toBeInTheDocument(); + expect(screen.getByTestId("source-trigger-0")).toBeInTheDocument(); + }); + }); }); diff --git a/src/test/mocks/handlers.ts b/src/test/mocks/handlers.ts index bac9559b..8ce78cb4 100644 --- a/src/test/mocks/handlers.ts +++ b/src/test/mocks/handlers.ts @@ -1846,6 +1846,111 @@ export const handlers = [ }); }), + // Ingestion Source mock data (before group store) + http.get("*/ragstore/ingestion-sources/byRagConfig", ({ request }) => { + const url = new URL(request.url); + const ragConfigUri = url.searchParams.get("ragConfigUri") ?? "eddi://ai.labs.rag/ragstore/rags/rag1?version=1"; + return HttpResponse.json([ + { + resource: "eddi://ai.labs.rag/ragstore/ingestion-sources/src1?version=1", + name: "Product Documentation", + description: "Crawls product docs from sidebar", + type: "web", + sourceConfig: { + startUrl: "https://docs.example.com", + tocSelector: "nav.sidebar a[href]", + scope: { + sameDomainOnly: true, + pathPrefix: "/docs/", + maxDepth: 3, + maxPages: 500, + excludePatterns: ["**/legacy/**"], + }, + crawlSettings: { + requestDelayMs: 1000, + timeoutSeconds: 30, + userAgent: "EDDI-Crawler/1.0", + }, + }, + ragConfigUri: ragConfigUri, + ingestionSettings: { + chunkStrategy: "recursive", + chunkSize: 1024, + chunkOverlap: 128, + contentHashDedup: true, + maxContentLength: 50000, + }, + schedule: { + cronExpression: "0 2 * * 0", + enabled: true, + }, + }, + ]); + }), + + http.get("*/ragstore/ingestion-sources/:id", () => { + return HttpResponse.json({ + name: "Product Documentation", + description: "Crawls product docs from sidebar", + type: "web", + sourceConfig: { + startUrl: "https://docs.example.com", + tocSelector: "nav.sidebar a[href]", + scope: { + sameDomainOnly: true, + pathPrefix: "/docs/", + maxDepth: 3, + maxPages: 500, + excludePatterns: ["**/legacy/**"], + }, + crawlSettings: { + requestDelayMs: 1000, + timeoutSeconds: 30, + userAgent: "EDDI-Crawler/1.0", + }, + }, + ragConfigUri: "eddi://ai.labs.rag/ragstore/rags/rag1?version=1", + ingestionSettings: { + chunkStrategy: "recursive", + chunkSize: 1024, + chunkOverlap: 128, + contentHashDedup: true, + maxContentLength: 50000, + }, + schedule: { + cronExpression: "0 2 * * 0", + enabled: true, + }, + }); + }), + + http.post("*/ragstore/ingestion-sources", () => { + return new HttpResponse(null, { + status: 201, + headers: { Location: "/ragstore/ingestion-sources/new-source?version=1" }, + }); + }), + + http.put("*/ragstore/ingestion-sources/:id", () => { + return new HttpResponse(null, { + status: 200, + headers: { Location: "/ragstore/ingestion-sources/updated-source?version=2" }, + }); + }), + + http.delete("*/ragstore/ingestion-sources/:id", () => { + return new HttpResponse(null, { status: 204 }); + }), + + http.post("*/ragstore/ingestion-sources/:id/trigger", ({ request }) => { + const url = new URL(request.url); + const version = url.searchParams.get("version"); + if (!version) { + return HttpResponse.json({ error: "version query parameter is required" }, { status: 400 }); + } + return HttpResponse.json({ status: "accepted" }, { status: 202 }); + }), + // --- Group Store Mock Handlers --- http.get("*/groupstore/groups/descriptors", () => { return HttpResponse.json([ diff --git a/src/test/setup.ts b/src/test/setup.ts index 54076cdc..fcd64d78 100644 --- a/src/test/setup.ts +++ b/src/test/setup.ts @@ -32,6 +32,22 @@ vi.mock("keycloak-js", () => ({ }, })); +// Mock localStorage (jsdom may not always have it in isolated environments) +if (typeof localStorage === "undefined" || localStorage.getItem === undefined) { + const store = new Map(); + Object.defineProperty(globalThis, "localStorage", { + value: { + getItem: (key: string) => store.get(key) ?? null, + setItem: (key: string, value: string) => store.set(key, value), + removeItem: (key: string) => store.delete(key), + clear: () => store.clear(), + get length() { return store.size; }, + key: (i: number) => [...store.keys()][i] ?? null, + }, + configurable: true, + }); +} + // Mock window.matchMedia for theme-provider (JSDOM doesn't implement it) Object.defineProperty(window, "matchMedia", { writable: true, From b084ea4648c5d9697fe38959f9b7fb80c5de1cd0 Mon Sep 17 00:00:00 2001 From: nic Date: Mon, 8 Jun 2026 17:25:11 +0200 Subject: [PATCH 3/9] feat(remove): Remove need for toc selector --- .../editors/ingestion-source-editor.tsx | 20 +------------------ src/hooks/use-extensions-store.ts | 15 +------------- src/lib/api/ingestion-sources.ts | 1 - src/test/mocks/handlers.ts | 2 -- 4 files changed, 2 insertions(+), 36 deletions(-) diff --git a/src/components/editors/ingestion-source-editor.tsx b/src/components/editors/ingestion-source-editor.tsx index 4e780cf6..5c3cbef6 100644 --- a/src/components/editors/ingestion-source-editor.tsx +++ b/src/components/editors/ingestion-source-editor.tsx @@ -208,25 +208,7 @@ export function IngestionSourceEditor({ data-testid="source-start-url" /> -
- - - update({ sourceConfig: { ...webConfig, tocSelector: e.target.value || undefined } as WebSourceConfig }) - } - readOnly={readOnly} - placeholder="nav.sidebar a[href]" - className="h-8 w-full rounded-md border border-input bg-background px-3 text-sm text-foreground focus:outline-none focus:ring-1 focus:ring-ring" - data-testid="source-toc-selector" - /> -

- CSS selector for navigation links. If empty, only the start URL is crawled. -

-
+
diff --git a/src/hooks/use-extensions-store.ts b/src/hooks/use-extensions-store.ts index 3d934d56..b95d9d29 100644 --- a/src/hooks/use-extensions-store.ts +++ b/src/hooks/use-extensions-store.ts @@ -21,20 +21,7 @@ const WELL_KNOWN_TYPES: ExtensionDescriptor[] = [ }, }, extensions: {}, - }, - { - type: "ai.labs.rag", - displayName: "RAG Knowledge Base", - configs: { - uri: { - displayName: "Resource URI", - fieldType: "URI", - isOptional: false, - defaultValue: null, - }, - }, - extensions: {}, - }, + } ]; /** diff --git a/src/lib/api/ingestion-sources.ts b/src/lib/api/ingestion-sources.ts index f7859558..05bd8144 100644 --- a/src/lib/api/ingestion-sources.ts +++ b/src/lib/api/ingestion-sources.ts @@ -13,7 +13,6 @@ export interface RagIngestionSource { export interface WebSourceConfig { startUrl: string; - tocSelector?: string; scope?: Scope; crawlSettings?: CrawlSettings; } diff --git a/src/test/mocks/handlers.ts b/src/test/mocks/handlers.ts index 8ce78cb4..9fa87f2f 100644 --- a/src/test/mocks/handlers.ts +++ b/src/test/mocks/handlers.ts @@ -1858,7 +1858,6 @@ export const handlers = [ type: "web", sourceConfig: { startUrl: "https://docs.example.com", - tocSelector: "nav.sidebar a[href]", scope: { sameDomainOnly: true, pathPrefix: "/docs/", @@ -1895,7 +1894,6 @@ export const handlers = [ type: "web", sourceConfig: { startUrl: "https://docs.example.com", - tocSelector: "nav.sidebar a[href]", scope: { sameDomainOnly: true, pathPrefix: "/docs/", From 8f3a3588eea4f3cfe43509795be7bed19a2c31fd Mon Sep 17 00:00:00 2001 From: nic Date: Tue, 9 Jun 2026 12:06:26 +0200 Subject: [PATCH 4/9] fix(update): Update source config --- src/hooks/use-ingestion-sources.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/hooks/use-ingestion-sources.ts b/src/hooks/use-ingestion-sources.ts index cae98429..78346607 100644 --- a/src/hooks/use-ingestion-sources.ts +++ b/src/hooks/use-ingestion-sources.ts @@ -10,7 +10,7 @@ import { const ingestionKeys = { all: (ragConfigUri: string) => ["ingestion-sources", ragConfigUri] as const, - list: (ragConfigUri: string) => ["ingestion-sources", "list", ragConfigUri] as const, + list: (ragConfigUri: string) => ["ingestion-sources", ragConfigUri, "list"] as const, }; export function useIngestionSources(ragConfigUri?: string) { From a408e1be45e183a86b75fdcf53a0d56c4fb454dd Mon Sep 17 00:00:00 2001 From: nic Date: Tue, 9 Jun 2026 12:10:16 +0200 Subject: [PATCH 5/9] feat(ui): Trigger feedback --- src/components/editors/ingestion-source-editor.tsx | 6 ++++++ src/i18n/locales/ar.json | 4 +++- src/i18n/locales/de.json | 4 +++- src/i18n/locales/en.json | 4 +++- src/i18n/locales/es.json | 4 +++- src/i18n/locales/fr.json | 4 +++- src/i18n/locales/hi.json | 4 +++- src/i18n/locales/ja.json | 4 +++- src/i18n/locales/ko.json | 4 +++- src/i18n/locales/pt.json | 4 +++- src/i18n/locales/th.json | 4 +++- src/i18n/locales/zh.json | 4 +++- 12 files changed, 39 insertions(+), 11 deletions(-) diff --git a/src/components/editors/ingestion-source-editor.tsx b/src/components/editors/ingestion-source-editor.tsx index 5c3cbef6..7dc884eb 100644 --- a/src/components/editors/ingestion-source-editor.tsx +++ b/src/components/editors/ingestion-source-editor.tsx @@ -1,4 +1,7 @@ import { useState, useCallback } from "react"; +import { useTranslation } from "react-i18next"; +import { toast } from "sonner"; +import { getErrorMessage } from "@/lib/api-client"; import { Globe, GlobeLock, @@ -614,6 +617,7 @@ export function IngestionSourcesPanel({ const deleteMutation = useDeleteIngestionSource(); const triggerMutation = useTriggerIngestion(); + const { t } = useTranslation(); const [editing, setEditing] = useState<{ source: RagIngestionSource; id?: string; version?: number } | null>(null); const [triggering, setTriggering] = useState(null); const [isAdding, setIsAdding] = useState(false); @@ -631,6 +635,8 @@ export function IngestionSourcesPanel({ const handleTrigger = (id: string, version: number) => { setTriggering(id); triggerMutation.mutate({ id, version }, { + onSuccess: () => toast.success(t("ragEditor.triggerSuccess", "Ingestion triggered")), + onError: (err) => toast.error(t("ragEditor.triggerError", "Failed to trigger ingestion"), { description: getErrorMessage(err) }), onSettled: () => setTriggering(null), }); }; diff --git a/src/i18n/locales/ar.json b/src/i18n/locales/ar.json index 64d39d60..04f6fe07 100644 --- a/src/i18n/locales/ar.json +++ b/src/i18n/locales/ar.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "حالة الاستيعاب", "saveFirstIngestion": "احفظ قاعدة المعرفة هذه أولاً لتمكين استيعاب المستندات.", "addParam": "إضافة معلمة", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/de.json b/src/i18n/locales/de.json index fad95f28..742e37b1 100644 --- a/src/i18n/locales/de.json +++ b/src/i18n/locales/de.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "Aufnahme-Status", "saveFirstIngestion": "Speichern Sie diese Knowledge Base zuerst, um die Dokumenten-Aufnahme zu aktivieren.", "addParam": "Parameter hinzufügen", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index 6d75de3f..97a6ba2f 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -1201,7 +1201,9 @@ "ingestionHistory": "Ingestion Status", "saveFirstIngestion": "Save this knowledge base first to enable document ingestion.", "addParam": "Add Parameter", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index ca4ef029..15e9c014 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "Estado de ingesta", "saveFirstIngestion": "Guarde esta base de conocimiento primero para activar la ingesta de documentos.", "addParam": "Añadir parámetro", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/fr.json b/src/i18n/locales/fr.json index bc76acb0..8c0d26eb 100644 --- a/src/i18n/locales/fr.json +++ b/src/i18n/locales/fr.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "Statut d'ingestion", "saveFirstIngestion": "Enregistrez d'abord cette base pour autoriser l'ingestion.", "addParam": "Ajout paramètre", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/hi.json b/src/i18n/locales/hi.json index 5e39f7ac..5c492b4b 100644 --- a/src/i18n/locales/hi.json +++ b/src/i18n/locales/hi.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "अंतर्ग्रहण स्थिति", "saveFirstIngestion": "दस्तावेज़ को प्रोसेस करने से पहले इस नॉलेज बेस को सहेजें।", "addParam": "पैरामीटर जोड़ें", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/ja.json b/src/i18n/locales/ja.json index 084ea5df..bd2c29c5 100644 --- a/src/i18n/locales/ja.json +++ b/src/i18n/locales/ja.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "取り込みステータス", "saveFirstIngestion": "ドキュメントの取り込みを有効にするには、先にこのナレッジベースを保存してください。", "addParam": "パラメーターの追加", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/ko.json b/src/i18n/locales/ko.json index a3528437..fc3ee4d3 100644 --- a/src/i18n/locales/ko.json +++ b/src/i18n/locales/ko.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "수집 상태", "saveFirstIngestion": "문서 수집을 활성화하려면 먼저 이 지식 베이스를 저장하세요.", "addParam": "파라미터 추가", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/pt.json b/src/i18n/locales/pt.json index 7e4b476f..f0fc32c6 100644 --- a/src/i18n/locales/pt.json +++ b/src/i18n/locales/pt.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "Status de Ingestão", "saveFirstIngestion": "Salve a base de conhecimento primeiro para ativar a ingestão.", "addParam": "Adicionar Parâmetro", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/th.json b/src/i18n/locales/th.json index ed237fe3..d098f98a 100644 --- a/src/i18n/locales/th.json +++ b/src/i18n/locales/th.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "สถานะการประมวล", "saveFirstIngestion": "หลังจากนี้ ค่อยเพิ่มความสมบูรณ์ฐานความรู้ให้กับเอกสารต่อๆไป", "addParam": "แอดพารามิเตอร์", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { diff --git a/src/i18n/locales/zh.json b/src/i18n/locales/zh.json index aed25313..8da5e3a5 100644 --- a/src/i18n/locales/zh.json +++ b/src/i18n/locales/zh.json @@ -1128,7 +1128,9 @@ "ingestionHistory": "摄入状态", "saveFirstIngestion": "使用前请先保存此知识库。", "addParam": "添加参数", - "ingestionSources": "Ingestion Sources" + "ingestionSources": "Ingestion Sources", + "triggerSuccess": "Ingestion triggered", + "triggerError": "Failed to trigger ingestion" }, "onboarding": { "welcome": { From fff5430c660274f0757d4512482fd9d1b4fb886a Mon Sep 17 00:00:00 2001 From: nic Date: Fri, 12 Jun 2026 16:34:42 +0200 Subject: [PATCH 6/9] feat(handle): Errors --- .../editors/ingestion-source-editor.tsx | 50 +++++++++-- src/components/editors/rag-editor.tsx | 11 ++- src/i18n/locales/ar.json | 10 ++- src/i18n/locales/de.json | 10 ++- src/i18n/locales/en.json | 10 ++- src/i18n/locales/es.json | 10 ++- src/i18n/locales/fr.json | 10 ++- src/i18n/locales/hi.json | 10 ++- src/i18n/locales/ja.json | 10 ++- src/i18n/locales/ko.json | 10 ++- src/i18n/locales/pt.json | 10 ++- src/i18n/locales/th.json | 10 ++- src/i18n/locales/zh.json | 10 ++- .../__tests__/resource-detail-rag.test.tsx | 82 +++++++++++++++++++ 14 files changed, 231 insertions(+), 22 deletions(-) diff --git a/src/components/editors/ingestion-source-editor.tsx b/src/components/editors/ingestion-source-editor.tsx index 7dc884eb..57969a82 100644 --- a/src/components/editors/ingestion-source-editor.tsx +++ b/src/components/editors/ingestion-source-editor.tsx @@ -103,6 +103,7 @@ export interface IngestionSourceEditorProps { onSave: (source: RagIngestionSource) => void; onCancel: () => void; readOnly?: boolean; + isSaving?: boolean; } export function IngestionSourceEditor({ @@ -111,6 +112,7 @@ export function IngestionSourceEditor({ onSave, onCancel, readOnly = false, + isSaving = false, }: IngestionSourceEditorProps) { const [source, setSource] = useState( initial ?? { ...EMPTY_SOURCE, ragConfigUri }, @@ -530,11 +532,11 @@ export function IngestionSourceEditor({
)} @@ -624,12 +626,45 @@ export function IngestionSourcesPanel({ const handleSave = (source: RagIngestionSource) => { if (editing?.id && editing?.version !== undefined) { - updateMutation.mutate({ id: editing.id, version: editing.version, source }); + updateMutation.mutate( + { id: editing.id, version: editing.version, source }, + { + onSuccess: () => { + toast.success(t("ragEditor.sourceUpdated", "Ingestion source updated")); + setEditing(null); + setIsAdding(false); + }, + onError: (err) => { + toast.error(t("ragEditor.sourceUpdateError", "Failed to update ingestion source"), { description: getErrorMessage(err) }); + }, + }, + ); } else { - createMutation.mutate(source); + createMutation.mutate(source, { + onSuccess: () => { + toast.success(t("ragEditor.sourceCreated", "Ingestion source created")); + setIsAdding(false); + setEditing(null); + }, + onError: (err) => { + toast.error(t("ragEditor.sourceCreateError", "Failed to create ingestion source"), { description: getErrorMessage(err) }); + }, + }); } - setEditing(null); - setIsAdding(false); + }; + + const handleDelete = (srcId: string, srcVersion: number) => { + deleteMutation.mutate( + { id: srcId, version: srcVersion, ragConfigUri }, + { + onSuccess: () => { + toast.success(t("ragEditor.sourceDeleted", "Ingestion source deleted")); + }, + onError: (err) => { + toast.error(t("ragEditor.sourceDeleteError", "Failed to delete ingestion source"), { description: getErrorMessage(err) }); + }, + }, + ); }; const handleTrigger = (id: string, version: number) => { @@ -707,7 +742,7 @@ export function IngestionSourcesPanel({