feat: knowledge base first OK
This commit is contained in:
@@ -31,6 +31,7 @@ interface Message {
|
||||
total_tokens: number;
|
||||
};
|
||||
artifacts?: MessageArtifact[];
|
||||
kbCitations?: KnowledgeCitation[];
|
||||
}
|
||||
|
||||
interface MessageViz {
|
||||
@@ -51,6 +52,14 @@ interface MessageArtifact {
|
||||
preview_url?: string;
|
||||
}
|
||||
|
||||
interface KnowledgeCitation {
|
||||
doc_id: string;
|
||||
title: string;
|
||||
score: number;
|
||||
chunk: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
interface ArtifactPreviewTarget {
|
||||
name: string;
|
||||
mimeType: string;
|
||||
@@ -105,6 +114,11 @@ interface Skill {
|
||||
type: string;
|
||||
}
|
||||
|
||||
interface KnowledgeBaseOption {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const dedupeSkillsById = (skills: Skill[]): Skill[] => {
|
||||
const map = new Map<string, Skill>();
|
||||
for (const skill of skills) {
|
||||
@@ -120,6 +134,7 @@ interface SessionData {
|
||||
metadata?: {
|
||||
active_data_file?: DataFileContext | null;
|
||||
selected_data_source?: string | null;
|
||||
selected_knowledge_base_id?: string | null;
|
||||
[key: string]: any;
|
||||
};
|
||||
messages: Array<{
|
||||
@@ -179,12 +194,34 @@ const normalizeArtifacts = (raw: unknown): MessageArtifact[] => {
|
||||
}, []);
|
||||
};
|
||||
|
||||
const normalizeKnowledgeCitations = (raw: unknown): KnowledgeCitation[] => {
|
||||
if (!Array.isArray(raw)) return [];
|
||||
return raw.reduce<KnowledgeCitation[]>((acc, item) => {
|
||||
if (!item || typeof item !== "object") return acc;
|
||||
const source = item as Record<string, unknown>;
|
||||
const title = typeof source.title === "string" ? source.title : "";
|
||||
const chunk = typeof source.chunk === "string" ? source.chunk : "";
|
||||
const score = typeof source.score === "number" ? source.score : Number(source.score || 0);
|
||||
if (!title || !chunk) return acc;
|
||||
acc.push({
|
||||
doc_id: typeof source.doc_id === "string" ? source.doc_id : "",
|
||||
title,
|
||||
score: Number.isFinite(score) ? score : 0,
|
||||
chunk,
|
||||
metadata: source.metadata && typeof source.metadata === "object" ? source.metadata as Record<string, unknown> : undefined,
|
||||
});
|
||||
return acc;
|
||||
}, []);
|
||||
};
|
||||
|
||||
export function ChatInterface() {
|
||||
const { t } = useTranslation();
|
||||
const [messagesBySession, setMessagesBySession] = useState<Record<string, Message[]>>({});
|
||||
const [input, setInput] = useState("");
|
||||
const [selectedDataSource, setSelectedDataSource] = useState<string>("");
|
||||
const [selectedKnowledgeBaseId, setSelectedKnowledgeBaseId] = useState<string>("");
|
||||
const [availableSkills, setAvailableSkills] = useState<Skill[]>([]);
|
||||
const [availableKnowledgeBases, setAvailableKnowledgeBases] = useState<KnowledgeBaseOption[]>([]);
|
||||
const [selectedSkillIds, setSelectedSkillIds] = useState<string[]>([]);
|
||||
const [isMenuOpen, setIsMenuOpen] = useState(false);
|
||||
const [artifactPreview, setArtifactPreview] = useState<ArtifactPreviewTarget | null>(null);
|
||||
@@ -443,6 +480,10 @@ export function ChatInterface() {
|
||||
useEffect(() => {
|
||||
if (currentProject) {
|
||||
fetchDataSources();
|
||||
fetchKnowledgeBases();
|
||||
} else {
|
||||
setAvailableKnowledgeBases([]);
|
||||
setSelectedKnowledgeBaseId("");
|
||||
}
|
||||
}, [currentProject]);
|
||||
|
||||
@@ -461,9 +502,25 @@ export function ChatInterface() {
|
||||
}
|
||||
};
|
||||
|
||||
const fetchKnowledgeBases = async () => {
|
||||
if (!currentProject) return;
|
||||
try {
|
||||
const data = await api.get<Array<{ id: string; name: string }>>(`/api/v1/knowledge-bases?project_id=${currentProject.id}`);
|
||||
const projectKnowledgeBases = (data || []).map((item) => ({ id: item.id, name: item.name }));
|
||||
setAvailableKnowledgeBases(projectKnowledgeBases);
|
||||
if (selectedKnowledgeBaseId && !projectKnowledgeBases.find((item) => item.id === selectedKnowledgeBaseId)) {
|
||||
setSelectedKnowledgeBaseId("");
|
||||
void syncSessionContext({ selected_knowledge_base_id: null });
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch knowledge bases", e);
|
||||
}
|
||||
};
|
||||
|
||||
const syncSessionContext = async (payload: {
|
||||
active_data_file?: DataFileContext | null;
|
||||
selected_data_source?: string | null;
|
||||
selected_knowledge_base_id?: string | null;
|
||||
}) => {
|
||||
try {
|
||||
await api.put(`/nanobot/sessions/${encodeURIComponent(activeSessionKey)}/context-file`, payload);
|
||||
@@ -472,6 +529,16 @@ export function ChatInterface() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectKnowledgeBase = async (knowledgeBaseId: string) => {
|
||||
setSelectedKnowledgeBaseId(knowledgeBaseId);
|
||||
await syncSessionContext({ selected_knowledge_base_id: knowledgeBaseId });
|
||||
};
|
||||
|
||||
const handleClearKnowledgeBase = async () => {
|
||||
setSelectedKnowledgeBaseId("");
|
||||
await syncSessionContext({ selected_knowledge_base_id: null });
|
||||
};
|
||||
|
||||
const handleSelectDataSource = async (sourceId: string) => {
|
||||
setSelectedDataSource(sourceId);
|
||||
await syncSessionContext({ selected_data_source: sourceId });
|
||||
@@ -516,6 +583,7 @@ export function ChatInterface() {
|
||||
reasoningContent: typeof m.reasoning_content === "string" ? m.reasoning_content : undefined,
|
||||
usage: m.usage,
|
||||
artifacts: normalizeArtifacts(m.artifacts),
|
||||
kbCitations: normalizeKnowledgeCitations(m.kb_citations),
|
||||
};
|
||||
});
|
||||
setMessagesForSession(activeSessionKey, formattedMessages);
|
||||
@@ -524,14 +592,17 @@ export function ChatInterface() {
|
||||
}
|
||||
const restoredFile = data.metadata?.active_data_file || null;
|
||||
const restoredSource = data.metadata?.selected_data_source || "";
|
||||
const restoredKnowledgeBaseId = data.metadata?.selected_knowledge_base_id || "";
|
||||
setActiveDataFile(restoredFile);
|
||||
setSelectedDataSource(restoredSource);
|
||||
setSelectedKnowledgeBaseId(restoredKnowledgeBaseId);
|
||||
setAttachedFile(null);
|
||||
} catch (e) {
|
||||
console.error("Failed to fetch session messages", e);
|
||||
setMessagesForSession(activeSessionKey, []);
|
||||
setActiveDataFile(null);
|
||||
setSelectedDataSource("");
|
||||
setSelectedKnowledgeBaseId("");
|
||||
setAttachedFile(null);
|
||||
} finally {
|
||||
setIsLoadingForSession(activeSessionKey, false);
|
||||
@@ -631,6 +702,7 @@ export function ChatInterface() {
|
||||
};
|
||||
|
||||
const selectedDataSourceName = availableDataSources.find(ds => ds.id === selectedDataSource)?.name || "";
|
||||
const selectedKnowledgeBaseName = availableKnowledgeBases.find((item) => item.id === selectedKnowledgeBaseId)?.name || "";
|
||||
const selectedSkills = availableSkills.filter(skill => selectedSkillIds.includes(skill.id));
|
||||
const isThinkingCollapsed = (messageId: string) => collapsedThinkingByMessage[messageId] ?? true;
|
||||
const toggleThinkingCollapsed = (messageId: string) => {
|
||||
@@ -650,7 +722,7 @@ export function ChatInterface() {
|
||||
};
|
||||
|
||||
const renderActiveSelections = () => {
|
||||
if (!selectedDataSource && selectedSkills.length === 0) return null;
|
||||
if (!selectedDataSource && !selectedKnowledgeBaseId && selectedSkills.length === 0) return null;
|
||||
return (
|
||||
<div className="px-2 pt-2">
|
||||
<div className="flex flex-wrap gap-2">
|
||||
@@ -660,6 +732,12 @@ export function ChatInterface() {
|
||||
{`${t('dataSource')}:${selectedDataSourceName}`}
|
||||
</div>
|
||||
) : null}
|
||||
{selectedKnowledgeBaseId ? (
|
||||
<div className="px-3 py-1.5 rounded-full text-xs border flex items-center gap-1.5 bg-violet-50 text-violet-700 border-violet-200">
|
||||
<Database className="h-3.5 w-3.5" />
|
||||
{`${t('knowledgeBase')}:${selectedKnowledgeBaseName || selectedKnowledgeBaseId}`}
|
||||
</div>
|
||||
) : null}
|
||||
{selectedSkills.map((skill) => (
|
||||
<div
|
||||
key={skill.id}
|
||||
@@ -812,6 +890,7 @@ export function ChatInterface() {
|
||||
prefer_sql_chart: preferSqlChart,
|
||||
file_url: fileUrl,
|
||||
route_mode: "auto",
|
||||
knowledge_base_id: selectedKnowledgeBaseId || undefined,
|
||||
}),
|
||||
signal: controller.signal,
|
||||
});
|
||||
@@ -917,6 +996,7 @@ export function ChatInterface() {
|
||||
completion_tokens: number;
|
||||
total_tokens: number;
|
||||
};
|
||||
kb_citations?: unknown;
|
||||
};
|
||||
|
||||
if (payload.type === "delta" && payload.content) {
|
||||
@@ -963,9 +1043,10 @@ export function ChatInterface() {
|
||||
flushAssistant(true);
|
||||
pushProgressLog(t('answerGenerationCompleted'));
|
||||
const messageArtifacts = normalizeArtifacts(payload.artifacts);
|
||||
const messageCitations = normalizeKnowledgeCitations(payload.kb_citations);
|
||||
setMessagesForSession(targetSessionKey, (prev) =>
|
||||
prev.map((msg) =>
|
||||
msg.id === assistantId ? { ...msg, content: typeof payload.content === "string" ? payload.content : msg.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz, usage: payload.usage, artifacts: messageArtifacts.length > 0 ? messageArtifacts : msg.artifacts } : msg
|
||||
msg.id === assistantId ? { ...msg, content: typeof payload.content === "string" ? payload.content : msg.content || "", awaitingFirstToken: false, viz: streamedViz ?? msg.viz, usage: payload.usage, artifacts: messageArtifacts.length > 0 ? messageArtifacts : msg.artifacts, kbCitations: messageCitations.length > 0 ? messageCitations : msg.kbCitations } : msg
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -1146,6 +1227,52 @@ export function ChatInterface() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-border">
|
||||
<div className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2 px-2 flex items-center gap-1.5">
|
||||
<Database className="h-3 w-3" />
|
||||
{t('knowledgeBase')}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{availableKnowledgeBases.length > 0 ? (
|
||||
availableKnowledgeBases.map((kb) => (
|
||||
<button
|
||||
key={kb.id}
|
||||
onClick={() => {
|
||||
void handleSelectKnowledgeBase(kb.id);
|
||||
}}
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm transition-all duration-200",
|
||||
selectedKnowledgeBaseId === kb.id
|
||||
? "bg-background text-foreground shadow-sm ring-1 ring-border"
|
||||
: "text-muted-foreground hover:bg-background hover:shadow-sm"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Database className={cn("h-4 w-4", selectedKnowledgeBaseId === kb.id ? "text-violet-500" : "text-muted-foreground")} />
|
||||
<span className="font-medium">{kb.name}</span>
|
||||
</div>
|
||||
{selectedKnowledgeBaseId === kb.id && <CheckCircle2 className="h-4 w-4 text-violet-500" />}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="px-3 py-3 text-xs text-muted-foreground">
|
||||
{t('noKnowledgeBases')}
|
||||
</div>
|
||||
)}
|
||||
{selectedKnowledgeBaseId ? (
|
||||
<div className="mt-2 pt-2 border-t border-border">
|
||||
<button
|
||||
onClick={() => {
|
||||
void handleClearKnowledgeBase();
|
||||
}}
|
||||
className="w-full py-1.5 text-[11px] text-muted-foreground hover:text-muted-foreground transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{t('clearSelected')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Skills */}
|
||||
@@ -1501,6 +1628,22 @@ export function ChatInterface() {
|
||||
))}
|
||||
</div>
|
||||
) : null}
|
||||
{msg.kbCitations && msg.kbCitations.length > 0 ? (
|
||||
<div className="mt-4 rounded-xl border border-violet-200 bg-violet-50/60 p-3">
|
||||
<div className="text-xs font-semibold text-violet-700 uppercase tracking-wider mb-2">{t('knowledgeCitations')}</div>
|
||||
<div className="space-y-2">
|
||||
{msg.kbCitations.map((citation, citationIndex) => (
|
||||
<div key={`${msg.id}-citation-${citationIndex}`} className="rounded-lg border border-violet-200 bg-white/80 px-3 py-2">
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<div className="text-sm font-medium text-violet-900 truncate">{citation.title}</div>
|
||||
<div className="text-[11px] text-violet-700 shrink-0">{t('matchScore', { score: citation.score.toFixed(3) })}</div>
|
||||
</div>
|
||||
<div className="mt-1 text-xs text-violet-800 line-clamp-3 whitespace-pre-wrap break-words">{citation.chunk}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
{msg.viz ? (
|
||||
<div className="mt-3 pt-3 border-t border-border">
|
||||
<InlineVisualizationCard viz={msg.viz} />
|
||||
@@ -1581,6 +1724,52 @@ export function ChatInterface() {
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 pt-3 border-t border-border">
|
||||
<div className="text-[11px] font-semibold text-muted-foreground uppercase tracking-wider mb-2 px-2 flex items-center gap-1.5">
|
||||
<Database className="h-3 w-3" />
|
||||
{t('knowledgeBase')}
|
||||
</div>
|
||||
<div className="space-y-0.5">
|
||||
{availableKnowledgeBases.length > 0 ? (
|
||||
availableKnowledgeBases.map((kb) => (
|
||||
<button
|
||||
key={kb.id}
|
||||
onClick={() => {
|
||||
void handleSelectKnowledgeBase(kb.id);
|
||||
}}
|
||||
className={cn(
|
||||
"w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm transition-all duration-200",
|
||||
selectedKnowledgeBaseId === kb.id
|
||||
? "bg-background text-foreground shadow-sm ring-1 ring-border"
|
||||
: "text-muted-foreground hover:bg-background hover:shadow-sm"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center gap-2.5">
|
||||
<Database className={cn("h-4 w-4", selectedKnowledgeBaseId === kb.id ? "text-violet-500" : "text-muted-foreground")} />
|
||||
<span className="font-medium">{kb.name}</span>
|
||||
</div>
|
||||
{selectedKnowledgeBaseId === kb.id && <CheckCircle2 className="h-4 w-4 text-violet-500" />}
|
||||
</button>
|
||||
))
|
||||
) : (
|
||||
<div className="px-3 py-3 text-xs text-muted-foreground">
|
||||
{t('noKnowledgeBases')}
|
||||
</div>
|
||||
)}
|
||||
{selectedKnowledgeBaseId ? (
|
||||
<div className="mt-2 pt-2 border-t border-border">
|
||||
<button
|
||||
onClick={() => {
|
||||
void handleClearKnowledgeBase();
|
||||
}}
|
||||
className="w-full py-1.5 text-[11px] text-muted-foreground hover:text-muted-foreground transition-colors flex items-center justify-center gap-1"
|
||||
>
|
||||
{t('clearSelected')}
|
||||
</button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Right Column: Skills */}
|
||||
|
||||
@@ -139,6 +139,104 @@
|
||||
"leaveBlankIfNotModifying": "Leave blank if not modifying",
|
||||
"confirmNewPassword": "Confirm New Password",
|
||||
"saveSettings": "Save Settings",
|
||||
"knowledgeBase": "Knowledge Base",
|
||||
"knowledgeBaseSettings": "Knowledge Base Configuration",
|
||||
"knowledgeBaseSettingsDesc": "Create, edit, reindex, and manage knowledge bases in the current project.",
|
||||
"knowledgeGlobalConfigTitle": "Knowledge Global Configuration",
|
||||
"knowledgeGlobalConfigDesc": "Configure global API base and key for knowledge service shared across projects.",
|
||||
"knowledgeGlobalApiBase": "API Base",
|
||||
"knowledgeGlobalApiBasePlaceholder": "e.g. https://api.siliconflow.cn/v1 (without /embeddings)",
|
||||
"knowledgeGlobalApiKey": "API Key",
|
||||
"knowledgeGlobalApiKeyPlaceholder": "Leave blank to keep the current key",
|
||||
"knowledgeGlobalApiKeyMasked": "Saved key: {{masked}}",
|
||||
"knowledgeGlobalApiKeyEmpty": "No API key configured",
|
||||
"knowledgeGlobalDefaultEmbeddingModel": "Default Embedding Model",
|
||||
"knowledgeGlobalDefaultEmbeddingModelPlaceholder": "e.g. text-embedding-3-small",
|
||||
"knowledgeGlobalModelNameHint": "API Base should be the provider base URL (without /embeddings), and model name must be explicit for testing and indexing.",
|
||||
"knowledgeGlobalModelNameTooLong": "Default embedding model name cannot exceed 200 characters",
|
||||
"knowledgeGlobalConfigLoadFailed": "Failed to load knowledge global configuration",
|
||||
"knowledgeGlobalConfigSaveFailed": "Failed to save knowledge global configuration",
|
||||
"knowledgeGlobalConfigSaved": "Knowledge global configuration saved successfully",
|
||||
"knowledgeGlobalConfigApiBaseInvalid": "API Base must start with http:// or https://",
|
||||
"knowledgeGlobalConfigApiBaseShouldBeBaseUrl": "API Base must be a base URL and should not include /embeddings",
|
||||
"testKnowledgeGlobalConnection": "Test Connection",
|
||||
"knowledgeGlobalConnectionTestPassed": "Connection test passed",
|
||||
"knowledgeGlobalConnectionTestFailed": "Connection test failed",
|
||||
"knowledgeGlobalConnectionModelResult": "Model: {{model}}",
|
||||
"knowledgeGlobalConnectionDimensionResult": "Embedding dimension: {{dim}}",
|
||||
"knowledgeGlobalConnectionAvailableModelsResult": "Available model examples: {{models}}",
|
||||
"knowledgeGlobalModelNameRequiredForTest": "Model name is required for connection testing",
|
||||
"knowledgeGlobalArkModelRequiredForTest": "For Volcengine Ark, model name is required for connection testing (Model ID or Endpoint ID)",
|
||||
"saveKnowledgeGlobalConfig": "Save Global Configuration",
|
||||
"refresh": "Refresh",
|
||||
"knowledgeBaseName": "Knowledge Base Name",
|
||||
"knowledgeBaseNamePlaceholder": "Enter knowledge base name",
|
||||
"knowledgeBaseDescriptionPlaceholder": "Enter knowledge base description (optional)",
|
||||
"knowledgeBaseEmbeddingModel": "Embedding Model",
|
||||
"knowledgeBaseEmbeddingModelPlaceholder": "e.g. text-embedding-3-large",
|
||||
"knowledgeBaseChunkSize": "Chunk Size",
|
||||
"knowledgeBaseChunkOverlap": "Chunk Overlap",
|
||||
"knowledgeBaseTopK": "Top K",
|
||||
"createKnowledgeBase": "Create Knowledge Base",
|
||||
"updateKnowledgeBase": "Update Knowledge Base",
|
||||
"knowledgeBaseList": "Knowledge Base List",
|
||||
"knowledgeBaseMeta": "{{count}} docs · Updated {{updatedAt}}",
|
||||
"manageKnowledgeDocuments": "Manage Documents",
|
||||
"knowledgeDocumentManagerTitle": "Document Management ({{name}})",
|
||||
"knowledgeDocumentManagerTitleEmpty": "Knowledge Document Management",
|
||||
"selectKnowledgeBaseToManageDocuments": "Select a knowledge base above to manage documents",
|
||||
"knowledgeDocumentTitle": "Document Title",
|
||||
"knowledgeDocumentTitlePlaceholder": "e.g. Refund Policy",
|
||||
"knowledgeDocumentContent": "Document Content",
|
||||
"knowledgeDocumentContentPlaceholder": "Enter document content",
|
||||
"knowledgeDocumentMetadata": "Document Metadata (Optional JSON)",
|
||||
"knowledgeDocumentMetadataPlaceholder": "e.g. {\"source\":\"manual\",\"lang\":\"en\"}",
|
||||
"knowledgeDocumentMeta": "Updated {{updatedAt}}",
|
||||
"knowledgeDocumentTitleRequired": "Please enter a document title",
|
||||
"knowledgeDocumentContentRequired": "Please enter document content",
|
||||
"knowledgeDocumentMetadataInvalid": "Document metadata must be valid JSON",
|
||||
"createKnowledgeDocument": "Create Document",
|
||||
"updateKnowledgeDocument": "Update Document",
|
||||
"editKnowledgeDocument": "Edit Document",
|
||||
"deleteKnowledgeDocument": "Delete Document",
|
||||
"confirmDeleteKnowledgeDocument": "Are you sure you want to delete this document?",
|
||||
"knowledgeDocumentCreated": "Document created successfully",
|
||||
"knowledgeDocumentUpdated": "Document updated successfully",
|
||||
"knowledgeDocumentDeleted": "Document deleted successfully",
|
||||
"knowledgeDocumentLoadFailed": "Failed to load documents",
|
||||
"knowledgeDocumentSaveFailed": "Failed to save document",
|
||||
"knowledgeDocumentDeleteFailed": "Failed to delete document",
|
||||
"noKnowledgeDocuments": "No documents in this knowledge base",
|
||||
"knowledgeDocumentUploadTitle": "Upload Documents to Knowledge Base",
|
||||
"knowledgeDocumentUploadHint": "Supports txt, md, json, yaml, xml, html, csv, xls, xlsx. Max 5MB per file.",
|
||||
"knowledgeDocumentUploadSelected": "{{count}} file(s) selected",
|
||||
"knowledgeDocumentUploadNone": "No files selected",
|
||||
"knowledgeDocumentUploadAction": "Upload and Add",
|
||||
"knowledgeDocumentUploadEmpty": "Please select files to upload",
|
||||
"knowledgeDocumentUploadSuccess": "{{count}} file(s) uploaded successfully",
|
||||
"knowledgeDocumentUploadFailed": "Failed to upload documents",
|
||||
"knowledgeCitations": "Knowledge Citations",
|
||||
"matchScore": "Score: {{score}}",
|
||||
"editKnowledgeBase": "Edit Knowledge Base",
|
||||
"deleteKnowledgeBase": "Delete Knowledge Base",
|
||||
"reindexKnowledgeBase": "Reindex",
|
||||
"refreshKnowledgeBaseList": "Refresh Knowledge Bases",
|
||||
"knowledgeBaseLoadFailed": "Failed to load knowledge bases",
|
||||
"knowledgeBaseNameRequired": "Please enter a knowledge base name",
|
||||
"knowledgeBaseChunkSizeRange": "Chunk Size must be between 64 and 4096",
|
||||
"knowledgeBaseChunkOverlapRange": "Chunk Overlap must be between 0 and 512",
|
||||
"knowledgeBaseChunkOverlapTooLarge": "Chunk Overlap must be smaller than Chunk Size",
|
||||
"knowledgeBaseTopKRange": "Top K must be between 1 and 20",
|
||||
"knowledgeBaseCreated": "Knowledge base created successfully",
|
||||
"knowledgeBaseUpdated": "Knowledge base updated successfully",
|
||||
"knowledgeBaseSaveFailed": "Failed to save knowledge base",
|
||||
"confirmDeleteKnowledgeBase": "Are you sure you want to delete this knowledge base?",
|
||||
"knowledgeBaseDeleted": "Knowledge base deleted successfully",
|
||||
"knowledgeBaseDeleteFailed": "Failed to delete knowledge base",
|
||||
"knowledgeBaseReindexSuccess": "Knowledge base reindexed successfully",
|
||||
"knowledgeBaseReindexFailed": "Failed to reindex knowledge base",
|
||||
"selectProjectBeforeManageKnowledgeBase": "Please select a project before managing knowledge bases",
|
||||
"noKnowledgeBases": "No knowledge bases in this project. Create one in Settings first.",
|
||||
"confirmDeleteUser": "Are you sure you want to delete this user?",
|
||||
"newUserMustHavePassword": "New users must have a password",
|
||||
"anErrorOccurred": "An error occurred",
|
||||
|
||||
@@ -152,6 +152,104 @@
|
||||
"leaveBlankIfNotModifying": "如不修改请留空",
|
||||
"confirmNewPassword": "确认新密码",
|
||||
"saveSettings": "保存设置",
|
||||
"knowledgeBase": "知识库",
|
||||
"knowledgeBaseSettings": "知识库配置与建库管理",
|
||||
"knowledgeBaseSettingsDesc": "在当前项目下创建、编辑、重建索引并维护知识库配置。",
|
||||
"knowledgeGlobalConfigTitle": "知识库全局配置",
|
||||
"knowledgeGlobalConfigDesc": "配置知识库服务的全局 API 地址与密钥,所有项目共享。",
|
||||
"knowledgeGlobalApiBase": "API Base",
|
||||
"knowledgeGlobalApiBasePlaceholder": "例如:https://api.siliconflow.cn/v1(不要填写 /embeddings)",
|
||||
"knowledgeGlobalApiKey": "API Key",
|
||||
"knowledgeGlobalApiKeyPlaceholder": "留空表示保持当前密钥不变",
|
||||
"knowledgeGlobalApiKeyMasked": "当前已保存密钥:{{masked}}",
|
||||
"knowledgeGlobalApiKeyEmpty": "当前未配置 API Key",
|
||||
"knowledgeGlobalDefaultEmbeddingModel": "默认向量模型名称",
|
||||
"knowledgeGlobalDefaultEmbeddingModelPlaceholder": "例如:text-embedding-3-small",
|
||||
"knowledgeGlobalModelNameHint": "API Base 请填写模型服务基地址(不含 /embeddings),模型名称需显式填写用于测试与建库。",
|
||||
"knowledgeGlobalModelNameTooLong": "默认向量模型名称长度不能超过 200 个字符",
|
||||
"knowledgeGlobalConfigLoadFailed": "加载知识库全局配置失败",
|
||||
"knowledgeGlobalConfigSaveFailed": "保存知识库全局配置失败",
|
||||
"knowledgeGlobalConfigSaved": "知识库全局配置保存成功",
|
||||
"knowledgeGlobalConfigApiBaseInvalid": "API Base 需以 http:// 或 https:// 开头",
|
||||
"knowledgeGlobalConfigApiBaseShouldBeBaseUrl": "API Base 需填写基地址,不要包含 /embeddings",
|
||||
"testKnowledgeGlobalConnection": "测试连接",
|
||||
"knowledgeGlobalConnectionTestPassed": "测试连接成功",
|
||||
"knowledgeGlobalConnectionTestFailed": "测试连接失败",
|
||||
"knowledgeGlobalConnectionModelResult": "模型:{{model}}",
|
||||
"knowledgeGlobalConnectionDimensionResult": "向量维度:{{dim}}",
|
||||
"knowledgeGlobalConnectionAvailableModelsResult": "可用模型示例:{{models}}",
|
||||
"knowledgeGlobalModelNameRequiredForTest": "测试连接必须填写向量模型名称",
|
||||
"knowledgeGlobalArkModelRequiredForTest": "火山方舟测试连接需填写向量模型名称(Model ID 或 Endpoint ID)",
|
||||
"saveKnowledgeGlobalConfig": "保存全局配置",
|
||||
"refresh": "刷新",
|
||||
"knowledgeBaseName": "知识库名称",
|
||||
"knowledgeBaseNamePlaceholder": "请输入知识库名称",
|
||||
"knowledgeBaseDescriptionPlaceholder": "请输入知识库描述(可选)",
|
||||
"knowledgeBaseEmbeddingModel": "Embedding 模型",
|
||||
"knowledgeBaseEmbeddingModelPlaceholder": "例如:text-embedding-3-large",
|
||||
"knowledgeBaseChunkSize": "Chunk Size",
|
||||
"knowledgeBaseChunkOverlap": "Chunk Overlap",
|
||||
"knowledgeBaseTopK": "Top K",
|
||||
"createKnowledgeBase": "创建知识库",
|
||||
"updateKnowledgeBase": "更新知识库",
|
||||
"knowledgeBaseList": "知识库列表",
|
||||
"knowledgeBaseMeta": "文档 {{count}} 个 · 更新时间 {{updatedAt}}",
|
||||
"manageKnowledgeDocuments": "管理文档",
|
||||
"knowledgeDocumentManagerTitle": "文档管理({{name}})",
|
||||
"knowledgeDocumentManagerTitleEmpty": "知识库文档管理",
|
||||
"selectKnowledgeBaseToManageDocuments": "请先从上方知识库列表选择一个知识库后再管理文档",
|
||||
"knowledgeDocumentTitle": "文档标题",
|
||||
"knowledgeDocumentTitlePlaceholder": "例如:退款政策说明",
|
||||
"knowledgeDocumentContent": "文档内容",
|
||||
"knowledgeDocumentContentPlaceholder": "请输入文档正文内容",
|
||||
"knowledgeDocumentMetadata": "文档元数据(JSON,可选)",
|
||||
"knowledgeDocumentMetadataPlaceholder": "例如:{\"source\":\"manual\",\"lang\":\"zh\"}",
|
||||
"knowledgeDocumentMeta": "更新于 {{updatedAt}}",
|
||||
"knowledgeDocumentTitleRequired": "请输入文档标题",
|
||||
"knowledgeDocumentContentRequired": "请输入文档内容",
|
||||
"knowledgeDocumentMetadataInvalid": "文档元数据必须是合法的 JSON",
|
||||
"createKnowledgeDocument": "新增文档",
|
||||
"updateKnowledgeDocument": "更新文档",
|
||||
"editKnowledgeDocument": "编辑文档",
|
||||
"deleteKnowledgeDocument": "删除文档",
|
||||
"confirmDeleteKnowledgeDocument": "确定删除该文档吗?",
|
||||
"knowledgeDocumentCreated": "文档创建成功",
|
||||
"knowledgeDocumentUpdated": "文档更新成功",
|
||||
"knowledgeDocumentDeleted": "文档删除成功",
|
||||
"knowledgeDocumentLoadFailed": "加载文档失败",
|
||||
"knowledgeDocumentSaveFailed": "保存文档失败",
|
||||
"knowledgeDocumentDeleteFailed": "删除文档失败",
|
||||
"noKnowledgeDocuments": "当前知识库还没有文档",
|
||||
"knowledgeDocumentUploadTitle": "上传文档到知识库",
|
||||
"knowledgeDocumentUploadHint": "支持 txt、md、json、yaml、xml、html、csv、xls、xlsx,单文件不超过 5MB。",
|
||||
"knowledgeDocumentUploadSelected": "已选择 {{count}} 个文件",
|
||||
"knowledgeDocumentUploadNone": "尚未选择文件",
|
||||
"knowledgeDocumentUploadAction": "上传并入库",
|
||||
"knowledgeDocumentUploadEmpty": "请先选择要上传的文件",
|
||||
"knowledgeDocumentUploadSuccess": "已成功上传 {{count}} 个文件",
|
||||
"knowledgeDocumentUploadFailed": "上传文档失败",
|
||||
"knowledgeCitations": "知识库引用片段",
|
||||
"matchScore": "匹配分:{{score}}",
|
||||
"editKnowledgeBase": "编辑知识库",
|
||||
"deleteKnowledgeBase": "删除知识库",
|
||||
"reindexKnowledgeBase": "重建索引",
|
||||
"refreshKnowledgeBaseList": "刷新知识库列表",
|
||||
"knowledgeBaseLoadFailed": "加载知识库失败",
|
||||
"knowledgeBaseNameRequired": "请输入知识库名称",
|
||||
"knowledgeBaseChunkSizeRange": "Chunk Size 需在 64 到 4096 之间",
|
||||
"knowledgeBaseChunkOverlapRange": "Chunk Overlap 需在 0 到 512 之间",
|
||||
"knowledgeBaseChunkOverlapTooLarge": "Chunk Overlap 需小于 Chunk Size",
|
||||
"knowledgeBaseTopKRange": "Top K 需在 1 到 20 之间",
|
||||
"knowledgeBaseCreated": "知识库创建成功",
|
||||
"knowledgeBaseUpdated": "知识库更新成功",
|
||||
"knowledgeBaseSaveFailed": "保存知识库失败",
|
||||
"confirmDeleteKnowledgeBase": "确定要删除这个知识库吗?",
|
||||
"knowledgeBaseDeleted": "知识库删除成功",
|
||||
"knowledgeBaseDeleteFailed": "删除知识库失败",
|
||||
"knowledgeBaseReindexSuccess": "知识库重建索引成功",
|
||||
"knowledgeBaseReindexFailed": "知识库重建索引失败",
|
||||
"selectProjectBeforeManageKnowledgeBase": "请先选择一个项目,再进行知识库管理",
|
||||
"noKnowledgeBases": "当前项目暂无知识库,请先在设置中创建",
|
||||
"confirmDeleteUser": "确认删除该用户吗?",
|
||||
"newUserMustHavePassword": "新建用户必须填写密码",
|
||||
"anErrorOccurred": "发生错误",
|
||||
|
||||
@@ -4,17 +4,119 @@ import { Button } from "@/components/ui/button";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, CardFooter } from "@/components/ui/card";
|
||||
import { Save, Loader2 } from "lucide-react";
|
||||
import { Save, Loader2, Database, RefreshCw, Pencil, Trash2, FileText, Plus } from "lucide-react";
|
||||
import { api } from "@/lib/api";
|
||||
import { useAuthStore } from "@/store/authStore";
|
||||
import { useProjectStore } from "@/store/projectStore";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
|
||||
interface KnowledgeBase {
|
||||
id: string;
|
||||
name: string;
|
||||
description?: string;
|
||||
project_id?: number | null;
|
||||
embedding_model?: string | null;
|
||||
chunk_size: number;
|
||||
chunk_overlap: number;
|
||||
top_k: number;
|
||||
is_active: boolean;
|
||||
updated_at: string;
|
||||
documents?: Array<{ id: string }>;
|
||||
}
|
||||
|
||||
interface KnowledgeBaseForm {
|
||||
name: string;
|
||||
description: string;
|
||||
embedding_model: string;
|
||||
chunk_size: number;
|
||||
chunk_overlap: number;
|
||||
top_k: number;
|
||||
is_active: boolean;
|
||||
}
|
||||
|
||||
interface KnowledgeDocument {
|
||||
id: string;
|
||||
title: string;
|
||||
content: string;
|
||||
metadata?: Record<string, unknown>;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
interface KnowledgeGlobalConfig {
|
||||
api_base?: string | null;
|
||||
api_key?: string | null;
|
||||
api_key_masked?: string | null;
|
||||
has_api_key: boolean;
|
||||
default_embedding_model?: string | null;
|
||||
}
|
||||
|
||||
interface KnowledgeConnectionTestResult {
|
||||
success: boolean;
|
||||
message: string;
|
||||
model_name?: string | null;
|
||||
embedding_dimension?: number | null;
|
||||
resolved_api_base?: string | null;
|
||||
available_models?: string[];
|
||||
}
|
||||
|
||||
const defaultKnowledgeBaseForm: KnowledgeBaseForm = {
|
||||
name: '',
|
||||
description: '',
|
||||
embedding_model: '',
|
||||
chunk_size: 512,
|
||||
chunk_overlap: 50,
|
||||
top_k: 3,
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
const defaultKnowledgeDocumentForm = {
|
||||
title: '',
|
||||
content: '',
|
||||
metadata: '',
|
||||
};
|
||||
|
||||
export function Settings() {
|
||||
const { t } = useTranslation();
|
||||
const { user, updateUser } = useAuthStore();
|
||||
const { currentProject } = useProjectStore();
|
||||
const [email, setEmail] = useState('');
|
||||
const [password, setPassword] = useState('');
|
||||
const [confirmPassword, setConfirmPassword] = useState('');
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isLoadingKnowledgeBases, setIsLoadingKnowledgeBases] = useState(false);
|
||||
const [isSavingKnowledgeBase, setIsSavingKnowledgeBase] = useState(false);
|
||||
const [deletingKnowledgeBaseId, setDeletingKnowledgeBaseId] = useState('');
|
||||
const [reindexingKnowledgeBaseId, setReindexingKnowledgeBaseId] = useState('');
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
||||
const [editingKnowledgeBaseId, setEditingKnowledgeBaseId] = useState('');
|
||||
const [knowledgeBaseForm, setKnowledgeBaseForm] = useState<KnowledgeBaseForm>(defaultKnowledgeBaseForm);
|
||||
const [knowledgeGlobalConfig, setKnowledgeGlobalConfig] = useState<KnowledgeGlobalConfig>({
|
||||
api_base: '',
|
||||
api_key: null,
|
||||
api_key_masked: null,
|
||||
has_api_key: false,
|
||||
default_embedding_model: '',
|
||||
});
|
||||
const [knowledgeGlobalForm, setKnowledgeGlobalForm] = useState({
|
||||
api_base: '',
|
||||
api_key: '',
|
||||
default_embedding_model: '',
|
||||
});
|
||||
const [isLoadingKnowledgeGlobalConfig, setIsLoadingKnowledgeGlobalConfig] = useState(false);
|
||||
const [isSavingKnowledgeGlobalConfig, setIsSavingKnowledgeGlobalConfig] = useState(false);
|
||||
const [isTestingKnowledgeGlobalConnection, setIsTestingKnowledgeGlobalConnection] = useState(false);
|
||||
const [knowledgeConnectionTestResult, setKnowledgeConnectionTestResult] = useState<KnowledgeConnectionTestResult | null>(null);
|
||||
const [selectedKnowledgeBaseId, setSelectedKnowledgeBaseId] = useState('');
|
||||
const [knowledgeDocuments, setKnowledgeDocuments] = useState<KnowledgeDocument[]>([]);
|
||||
const [isLoadingKnowledgeDocuments, setIsLoadingKnowledgeDocuments] = useState(false);
|
||||
const [isSavingKnowledgeDocument, setIsSavingKnowledgeDocument] = useState(false);
|
||||
const [deletingKnowledgeDocumentId, setDeletingKnowledgeDocumentId] = useState('');
|
||||
const [editingKnowledgeDocumentId, setEditingKnowledgeDocumentId] = useState('');
|
||||
const [knowledgeDocumentForm, setKnowledgeDocumentForm] = useState(defaultKnowledgeDocumentForm);
|
||||
const [uploadingKnowledgeDocuments, setUploadingKnowledgeDocuments] = useState(false);
|
||||
const [knowledgeUploadFiles, setKnowledgeUploadFiles] = useState<File[]>([]);
|
||||
const [error, setError] = useState('');
|
||||
const [success, setSuccess] = useState('');
|
||||
|
||||
@@ -23,8 +125,430 @@ export function Settings() {
|
||||
setEmail(user.email || '');
|
||||
}
|
||||
}, [user]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchKnowledgeBases();
|
||||
}, [currentProject?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
void fetchKnowledgeGlobalConfig();
|
||||
}, []);
|
||||
|
||||
const isPasswordMismatch = password !== '' && confirmPassword !== '' && password !== confirmPassword;
|
||||
|
||||
const fetchKnowledgeGlobalConfig = async () => {
|
||||
setIsLoadingKnowledgeGlobalConfig(true);
|
||||
try {
|
||||
const data = await api.get<KnowledgeGlobalConfig>('/api/v1/knowledge-bases/global-config');
|
||||
setKnowledgeGlobalConfig(data);
|
||||
setKnowledgeGlobalForm({
|
||||
api_base: data.api_base || '',
|
||||
api_key: '',
|
||||
default_embedding_model: data.default_embedding_model || '',
|
||||
});
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeGlobalConfigLoadFailed'));
|
||||
} finally {
|
||||
setIsLoadingKnowledgeGlobalConfig(false);
|
||||
}
|
||||
};
|
||||
|
||||
const validateKnowledgeGlobalConfig = () => {
|
||||
const normalizedApiBase = knowledgeGlobalForm.api_base.trim();
|
||||
if (!normalizedApiBase) {
|
||||
return '';
|
||||
}
|
||||
if (!(normalizedApiBase.startsWith('http://') || normalizedApiBase.startsWith('https://'))) {
|
||||
return t('knowledgeGlobalConfigApiBaseInvalid');
|
||||
}
|
||||
if (normalizedApiBase.toLowerCase().endsWith('/embeddings')) {
|
||||
return t('knowledgeGlobalConfigApiBaseShouldBeBaseUrl');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const validateKnowledgeGlobalModelName = () => {
|
||||
const normalizedModelName = knowledgeGlobalForm.default_embedding_model.trim();
|
||||
if (!normalizedModelName) {
|
||||
return '';
|
||||
}
|
||||
if (normalizedModelName.length > 200) {
|
||||
return t('knowledgeGlobalModelNameTooLong');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleSaveKnowledgeGlobalConfig = async () => {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
const validationMessage = validateKnowledgeGlobalConfig();
|
||||
if (validationMessage) {
|
||||
setError(validationMessage);
|
||||
return;
|
||||
}
|
||||
const modelValidationMessage = validateKnowledgeGlobalModelName();
|
||||
if (modelValidationMessage) {
|
||||
setError(modelValidationMessage);
|
||||
return;
|
||||
}
|
||||
setIsSavingKnowledgeGlobalConfig(true);
|
||||
try {
|
||||
const payload: Record<string, string | null> = {
|
||||
api_base: knowledgeGlobalForm.api_base.trim() || null,
|
||||
default_embedding_model: knowledgeGlobalForm.default_embedding_model.trim() || null,
|
||||
};
|
||||
const normalizedApiKey = knowledgeGlobalForm.api_key.trim();
|
||||
if (normalizedApiKey) {
|
||||
payload.api_key = normalizedApiKey;
|
||||
}
|
||||
const data = await api.put<KnowledgeGlobalConfig>('/api/v1/knowledge-bases/global-config', payload);
|
||||
setKnowledgeGlobalConfig(data);
|
||||
setKnowledgeGlobalForm({
|
||||
api_base: data.api_base || '',
|
||||
api_key: '',
|
||||
default_embedding_model: data.default_embedding_model || '',
|
||||
});
|
||||
setKnowledgeConnectionTestResult(null);
|
||||
setSuccess(t('knowledgeGlobalConfigSaved'));
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeGlobalConfigSaveFailed'));
|
||||
} finally {
|
||||
setIsSavingKnowledgeGlobalConfig(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleTestKnowledgeGlobalConnection = async () => {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setKnowledgeConnectionTestResult(null);
|
||||
const validationMessage = validateKnowledgeGlobalConfig();
|
||||
if (validationMessage) {
|
||||
setError(validationMessage);
|
||||
return;
|
||||
}
|
||||
const modelValidationMessage = validateKnowledgeGlobalModelName();
|
||||
if (modelValidationMessage) {
|
||||
setError(modelValidationMessage);
|
||||
return;
|
||||
}
|
||||
const normalizedModelName = knowledgeGlobalForm.default_embedding_model.trim();
|
||||
if (!normalizedModelName) {
|
||||
setError(t('knowledgeGlobalModelNameRequiredForTest'));
|
||||
return;
|
||||
}
|
||||
setIsTestingKnowledgeGlobalConnection(true);
|
||||
try {
|
||||
const payload: Record<string, string> = {};
|
||||
const normalizedApiBase = knowledgeGlobalForm.api_base.trim();
|
||||
const normalizedApiKey = knowledgeGlobalForm.api_key.trim();
|
||||
if (normalizedApiBase) payload.api_base = normalizedApiBase;
|
||||
if (normalizedApiKey) payload.api_key = normalizedApiKey;
|
||||
if (normalizedModelName) payload.model_name = normalizedModelName;
|
||||
const result = await api.post<KnowledgeConnectionTestResult>('/api/v1/knowledge-bases/global-config/test-connection', payload);
|
||||
setKnowledgeConnectionTestResult(result);
|
||||
setSuccess(t('knowledgeGlobalConnectionTestPassed'));
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeGlobalConnectionTestFailed'));
|
||||
} finally {
|
||||
setIsTestingKnowledgeGlobalConnection(false);
|
||||
}
|
||||
};
|
||||
|
||||
const fetchKnowledgeBases = async () => {
|
||||
if (!currentProject) {
|
||||
setKnowledgeBases([]);
|
||||
return;
|
||||
}
|
||||
setIsLoadingKnowledgeBases(true);
|
||||
try {
|
||||
const data = await api.get<KnowledgeBase[]>(`/api/v1/knowledge-bases?project_id=${currentProject.id}`);
|
||||
setKnowledgeBases(data);
|
||||
if (editingKnowledgeBaseId && !data.find((item) => item.id === editingKnowledgeBaseId)) {
|
||||
setEditingKnowledgeBaseId('');
|
||||
setKnowledgeBaseForm(defaultKnowledgeBaseForm);
|
||||
}
|
||||
if (selectedKnowledgeBaseId && !data.find((item) => item.id === selectedKnowledgeBaseId)) {
|
||||
setSelectedKnowledgeBaseId('');
|
||||
setKnowledgeDocuments([]);
|
||||
setEditingKnowledgeDocumentId('');
|
||||
setKnowledgeDocumentForm(defaultKnowledgeDocumentForm);
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeBaseLoadFailed'));
|
||||
} finally {
|
||||
setIsLoadingKnowledgeBases(false);
|
||||
}
|
||||
};
|
||||
|
||||
const resetKnowledgeBaseForm = () => {
|
||||
setEditingKnowledgeBaseId('');
|
||||
setKnowledgeBaseForm(defaultKnowledgeBaseForm);
|
||||
};
|
||||
|
||||
const validateKnowledgeBaseForm = () => {
|
||||
if (!currentProject) {
|
||||
return t('selectProjectBeforeManageKnowledgeBase');
|
||||
}
|
||||
if (!knowledgeBaseForm.name.trim()) {
|
||||
return t('knowledgeBaseNameRequired');
|
||||
}
|
||||
if (knowledgeBaseForm.chunk_size < 64 || knowledgeBaseForm.chunk_size > 4096) {
|
||||
return t('knowledgeBaseChunkSizeRange');
|
||||
}
|
||||
if (knowledgeBaseForm.chunk_overlap < 0 || knowledgeBaseForm.chunk_overlap > 512) {
|
||||
return t('knowledgeBaseChunkOverlapRange');
|
||||
}
|
||||
if (knowledgeBaseForm.chunk_overlap >= knowledgeBaseForm.chunk_size) {
|
||||
return t('knowledgeBaseChunkOverlapTooLarge');
|
||||
}
|
||||
if (knowledgeBaseForm.top_k < 1 || knowledgeBaseForm.top_k > 20) {
|
||||
return t('knowledgeBaseTopKRange');
|
||||
}
|
||||
return '';
|
||||
};
|
||||
|
||||
const handleSaveKnowledgeBase = async () => {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
const validationMessage = validateKnowledgeBaseForm();
|
||||
if (validationMessage) {
|
||||
setError(validationMessage);
|
||||
return;
|
||||
}
|
||||
if (!currentProject) return;
|
||||
setIsSavingKnowledgeBase(true);
|
||||
try {
|
||||
const payload = {
|
||||
name: knowledgeBaseForm.name.trim(),
|
||||
description: knowledgeBaseForm.description.trim() || null,
|
||||
embedding_model: knowledgeBaseForm.embedding_model.trim() || null,
|
||||
chunk_size: knowledgeBaseForm.chunk_size,
|
||||
chunk_overlap: knowledgeBaseForm.chunk_overlap,
|
||||
top_k: knowledgeBaseForm.top_k,
|
||||
is_active: knowledgeBaseForm.is_active,
|
||||
project_id: currentProject.id,
|
||||
};
|
||||
if (editingKnowledgeBaseId) {
|
||||
await api.put(`/api/v1/knowledge-bases/${editingKnowledgeBaseId}`, payload);
|
||||
setSuccess(t('knowledgeBaseUpdated'));
|
||||
} else {
|
||||
await api.post('/api/v1/knowledge-bases', payload);
|
||||
setSuccess(t('knowledgeBaseCreated'));
|
||||
}
|
||||
await fetchKnowledgeBases();
|
||||
resetKnowledgeBaseForm();
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeBaseSaveFailed'));
|
||||
} finally {
|
||||
setIsSavingKnowledgeBase(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditKnowledgeBase = (item: KnowledgeBase) => {
|
||||
setEditingKnowledgeBaseId(item.id);
|
||||
setKnowledgeBaseForm({
|
||||
name: item.name || '',
|
||||
description: item.description || '',
|
||||
embedding_model: item.embedding_model || '',
|
||||
chunk_size: item.chunk_size,
|
||||
chunk_overlap: item.chunk_overlap,
|
||||
top_k: item.top_k,
|
||||
is_active: item.is_active,
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteKnowledgeBase = async (id: string) => {
|
||||
if (!window.confirm(t('confirmDeleteKnowledgeBase'))) {
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setDeletingKnowledgeBaseId(id);
|
||||
try {
|
||||
await api.delete(`/api/v1/knowledge-bases/${id}`);
|
||||
setSuccess(t('knowledgeBaseDeleted'));
|
||||
if (editingKnowledgeBaseId === id) {
|
||||
resetKnowledgeBaseForm();
|
||||
}
|
||||
if (selectedKnowledgeBaseId === id) {
|
||||
setSelectedKnowledgeBaseId('');
|
||||
setKnowledgeDocuments([]);
|
||||
setEditingKnowledgeDocumentId('');
|
||||
setKnowledgeDocumentForm(defaultKnowledgeDocumentForm);
|
||||
}
|
||||
await fetchKnowledgeBases();
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeBaseDeleteFailed'));
|
||||
} finally {
|
||||
setDeletingKnowledgeBaseId('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleReindexKnowledgeBase = async (id: string) => {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setReindexingKnowledgeBaseId(id);
|
||||
try {
|
||||
await api.post(`/api/v1/knowledge-bases/${id}/reindex`, {});
|
||||
setSuccess(t('knowledgeBaseReindexSuccess'));
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeBaseReindexFailed'));
|
||||
} finally {
|
||||
setReindexingKnowledgeBaseId('');
|
||||
}
|
||||
};
|
||||
|
||||
const resetKnowledgeDocumentForm = () => {
|
||||
setEditingKnowledgeDocumentId('');
|
||||
setKnowledgeDocumentForm(defaultKnowledgeDocumentForm);
|
||||
};
|
||||
|
||||
const fetchKnowledgeDocuments = async (kbId: string) => {
|
||||
if (!kbId) {
|
||||
setKnowledgeDocuments([]);
|
||||
return;
|
||||
}
|
||||
setIsLoadingKnowledgeDocuments(true);
|
||||
try {
|
||||
const data = await api.get<KnowledgeDocument[]>(`/api/v1/knowledge-bases/${kbId}/documents`);
|
||||
setKnowledgeDocuments(data);
|
||||
if (editingKnowledgeDocumentId && !data.find((item) => item.id === editingKnowledgeDocumentId)) {
|
||||
resetKnowledgeDocumentForm();
|
||||
}
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeDocumentLoadFailed'));
|
||||
} finally {
|
||||
setIsLoadingKnowledgeDocuments(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenKnowledgeDocuments = async (kbId: string) => {
|
||||
if (selectedKnowledgeBaseId === kbId) {
|
||||
setSelectedKnowledgeBaseId('');
|
||||
setKnowledgeDocuments([]);
|
||||
resetKnowledgeDocumentForm();
|
||||
return;
|
||||
}
|
||||
setSelectedKnowledgeBaseId(kbId);
|
||||
resetKnowledgeDocumentForm();
|
||||
await fetchKnowledgeDocuments(kbId);
|
||||
};
|
||||
|
||||
const validateKnowledgeDocumentForm = () => {
|
||||
if (!selectedKnowledgeBaseId) {
|
||||
return t('selectKnowledgeBaseToManageDocuments');
|
||||
}
|
||||
if (!knowledgeDocumentForm.title.trim()) {
|
||||
return t('knowledgeDocumentTitleRequired');
|
||||
}
|
||||
if (!knowledgeDocumentForm.content.trim()) {
|
||||
return t('knowledgeDocumentContentRequired');
|
||||
}
|
||||
const metadataText = knowledgeDocumentForm.metadata.trim();
|
||||
if (!metadataText) {
|
||||
return '';
|
||||
}
|
||||
try {
|
||||
JSON.parse(metadataText);
|
||||
return '';
|
||||
} catch {
|
||||
return t('knowledgeDocumentMetadataInvalid');
|
||||
}
|
||||
};
|
||||
|
||||
const handleSaveKnowledgeDocument = async () => {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
const validationMessage = validateKnowledgeDocumentForm();
|
||||
if (validationMessage) {
|
||||
setError(validationMessage);
|
||||
return;
|
||||
}
|
||||
if (!selectedKnowledgeBaseId) return;
|
||||
setIsSavingKnowledgeDocument(true);
|
||||
try {
|
||||
const metadataText = knowledgeDocumentForm.metadata.trim();
|
||||
const payload = {
|
||||
title: knowledgeDocumentForm.title.trim(),
|
||||
content: knowledgeDocumentForm.content.trim(),
|
||||
metadata: metadataText ? JSON.parse(metadataText) : {},
|
||||
};
|
||||
if (editingKnowledgeDocumentId) {
|
||||
await api.put(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/${editingKnowledgeDocumentId}`, payload);
|
||||
setSuccess(t('knowledgeDocumentUpdated'));
|
||||
} else {
|
||||
await api.post(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents`, payload);
|
||||
setSuccess(t('knowledgeDocumentCreated'));
|
||||
}
|
||||
await fetchKnowledgeDocuments(selectedKnowledgeBaseId);
|
||||
await fetchKnowledgeBases();
|
||||
resetKnowledgeDocumentForm();
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeDocumentSaveFailed'));
|
||||
} finally {
|
||||
setIsSavingKnowledgeDocument(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEditKnowledgeDocument = (item: KnowledgeDocument) => {
|
||||
setEditingKnowledgeDocumentId(item.id);
|
||||
setKnowledgeDocumentForm({
|
||||
title: item.title || '',
|
||||
content: item.content || '',
|
||||
metadata: item.metadata && Object.keys(item.metadata).length > 0 ? JSON.stringify(item.metadata, null, 2) : '',
|
||||
});
|
||||
};
|
||||
|
||||
const handleDeleteKnowledgeDocument = async (docId: string) => {
|
||||
if (!selectedKnowledgeBaseId) return;
|
||||
if (!window.confirm(t('confirmDeleteKnowledgeDocument'))) {
|
||||
return;
|
||||
}
|
||||
setError('');
|
||||
setSuccess('');
|
||||
setDeletingKnowledgeDocumentId(docId);
|
||||
try {
|
||||
await api.delete(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/${docId}`);
|
||||
if (editingKnowledgeDocumentId === docId) {
|
||||
resetKnowledgeDocumentForm();
|
||||
}
|
||||
setSuccess(t('knowledgeDocumentDeleted'));
|
||||
await fetchKnowledgeDocuments(selectedKnowledgeBaseId);
|
||||
await fetchKnowledgeBases();
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeDocumentDeleteFailed'));
|
||||
} finally {
|
||||
setDeletingKnowledgeDocumentId('');
|
||||
}
|
||||
};
|
||||
|
||||
const handleUploadKnowledgeDocuments = async () => {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
if (!selectedKnowledgeBaseId) {
|
||||
setError(t('selectKnowledgeBaseToManageDocuments'));
|
||||
return;
|
||||
}
|
||||
if (knowledgeUploadFiles.length === 0) {
|
||||
setError(t('knowledgeDocumentUploadEmpty'));
|
||||
return;
|
||||
}
|
||||
setUploadingKnowledgeDocuments(true);
|
||||
try {
|
||||
const formData = new FormData();
|
||||
knowledgeUploadFiles.forEach((file) => formData.append('files', file));
|
||||
await api.post(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/upload`, formData);
|
||||
setSuccess(t('knowledgeDocumentUploadSuccess', { count: knowledgeUploadFiles.length }));
|
||||
setKnowledgeUploadFiles([]);
|
||||
await fetchKnowledgeDocuments(selectedKnowledgeBaseId);
|
||||
await fetchKnowledgeBases();
|
||||
} catch (err: any) {
|
||||
setError(err.message || t('knowledgeDocumentUploadFailed'));
|
||||
} finally {
|
||||
setUploadingKnowledgeDocuments(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
setError('');
|
||||
setSuccess('');
|
||||
@@ -65,17 +589,19 @@ export function Settings() {
|
||||
}
|
||||
};
|
||||
|
||||
const selectedKnowledgeBase = knowledgeBases.find((item) => item.id === selectedKnowledgeBaseId) || null;
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex flex-col h-full bg-muted/50/30 overflow-hidden">
|
||||
<div className="h-14 px-6 flex items-center justify-between border-b border-border bg-background">
|
||||
<div className="flex items-center gap-2 text-foreground/80 font-medium">
|
||||
<Save className="h-5 w-5 text-indigo-500" />
|
||||
个人设置
|
||||
{t('personalSettings')}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 p-6 overflow-auto">
|
||||
<div className="grid gap-6 max-w-2xl mx-auto">
|
||||
<div className="grid gap-6 max-w-4xl mx-auto">
|
||||
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded-md p-3">{error}</div>}
|
||||
{success && <div className="text-sm text-emerald-600 bg-emerald-50 border border-emerald-100 rounded-md p-3">{success}</div>}
|
||||
|
||||
@@ -138,7 +664,400 @@ export function Settings() {
|
||||
<CardFooter className="bg-muted/50/50 border-t border-border pt-6">
|
||||
<Button onClick={handleSave} className="ml-auto bg-indigo-600 hover:bg-indigo-700 text-primary-foreground" disabled={isSaving || isPasswordMismatch}>
|
||||
{isSaving ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
||||
保存设置
|
||||
{t('saveSettings')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
<Card className="border-border shadow-sm">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-xl flex items-center gap-2">
|
||||
<Database className="h-5 w-5 text-indigo-500" />
|
||||
{t('knowledgeBaseSettings')}
|
||||
</CardTitle>
|
||||
<CardDescription>{t('knowledgeBaseSettingsDesc')}</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-6">
|
||||
<div className="rounded-lg border border-border p-4 space-y-4">
|
||||
<div className="space-y-1">
|
||||
<div className="text-sm font-medium text-foreground">{t('knowledgeGlobalConfigTitle')}</div>
|
||||
<div className="text-xs text-muted-foreground">{t('knowledgeGlobalConfigDesc')}</div>
|
||||
</div>
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="knowledge-global-api-base">{t('knowledgeGlobalApiBase')}</Label>
|
||||
<Input
|
||||
id="knowledge-global-api-base"
|
||||
value={knowledgeGlobalForm.api_base}
|
||||
placeholder={t('knowledgeGlobalApiBasePlaceholder')}
|
||||
onChange={(e) => setKnowledgeGlobalForm((prev) => ({ ...prev, api_base: e.target.value }))}
|
||||
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="knowledge-global-api-key">{t('knowledgeGlobalApiKey')}</Label>
|
||||
<Input
|
||||
id="knowledge-global-api-key"
|
||||
type="password"
|
||||
value={knowledgeGlobalForm.api_key}
|
||||
placeholder={t('knowledgeGlobalApiKeyPlaceholder')}
|
||||
onChange={(e) => setKnowledgeGlobalForm((prev) => ({ ...prev, api_key: e.target.value }))}
|
||||
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{knowledgeGlobalConfig.has_api_key
|
||||
? t('knowledgeGlobalApiKeyMasked', { masked: knowledgeGlobalConfig.api_key_masked || '******' })
|
||||
: t('knowledgeGlobalApiKeyEmpty')}
|
||||
</div>
|
||||
</div>
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="knowledge-global-default-embedding-model">{t('knowledgeGlobalDefaultEmbeddingModel')}</Label>
|
||||
<Input
|
||||
id="knowledge-global-default-embedding-model"
|
||||
value={knowledgeGlobalForm.default_embedding_model}
|
||||
placeholder={t('knowledgeGlobalDefaultEmbeddingModelPlaceholder')}
|
||||
onChange={(e) => setKnowledgeGlobalForm((prev) => ({ ...prev, default_embedding_model: e.target.value }))}
|
||||
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig || isTestingKnowledgeGlobalConnection}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('knowledgeGlobalModelNameHint')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{knowledgeConnectionTestResult ? (
|
||||
<div className="rounded-md border border-emerald-200 bg-emerald-50 p-3 text-xs text-emerald-700 space-y-1">
|
||||
<div>{knowledgeConnectionTestResult.message}</div>
|
||||
{knowledgeConnectionTestResult.model_name ? (
|
||||
<div>{t('knowledgeGlobalConnectionModelResult', { model: knowledgeConnectionTestResult.model_name })}</div>
|
||||
) : null}
|
||||
{typeof knowledgeConnectionTestResult.embedding_dimension === 'number' ? (
|
||||
<div>{t('knowledgeGlobalConnectionDimensionResult', { dim: knowledgeConnectionTestResult.embedding_dimension })}</div>
|
||||
) : null}
|
||||
{knowledgeConnectionTestResult.available_models && knowledgeConnectionTestResult.available_models.length > 0 ? (
|
||||
<div>{t('knowledgeGlobalConnectionAvailableModelsResult', { models: knowledgeConnectionTestResult.available_models.slice(0, 5).join(', ') })}</div>
|
||||
) : null}
|
||||
</div>
|
||||
) : null}
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleTestKnowledgeGlobalConnection}
|
||||
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig || isTestingKnowledgeGlobalConnection}
|
||||
>
|
||||
{isTestingKnowledgeGlobalConnection ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Database className="h-4 w-4 mr-2" />}
|
||||
{t('testKnowledgeGlobalConnection')}
|
||||
</Button>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => void fetchKnowledgeGlobalConfig()}
|
||||
disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig || isTestingKnowledgeGlobalConnection}
|
||||
>
|
||||
{isLoadingKnowledgeGlobalConfig ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-2" />}
|
||||
{t('refresh')}
|
||||
</Button>
|
||||
<Button onClick={handleSaveKnowledgeGlobalConfig} disabled={isLoadingKnowledgeGlobalConfig || isSavingKnowledgeGlobalConfig || isTestingKnowledgeGlobalConnection}>
|
||||
{isSavingKnowledgeGlobalConfig ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
||||
{t('saveKnowledgeGlobalConfig')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{!currentProject ? (
|
||||
<div className="text-sm text-amber-700 bg-amber-50 border border-amber-200 rounded-md p-3">
|
||||
{t('selectProjectBeforeManageKnowledgeBase')}
|
||||
</div>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="knowledge-base-name">{t('knowledgeBaseName')}</Label>
|
||||
<Input
|
||||
id="knowledge-base-name"
|
||||
value={knowledgeBaseForm.name}
|
||||
placeholder={t('knowledgeBaseNamePlaceholder')}
|
||||
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||
disabled={!currentProject}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="knowledge-base-description">{t('description')}</Label>
|
||||
<Input
|
||||
id="knowledge-base-description"
|
||||
value={knowledgeBaseForm.description}
|
||||
placeholder={t('knowledgeBaseDescriptionPlaceholder')}
|
||||
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, description: e.target.value }))}
|
||||
disabled={!currentProject}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2 md:col-span-2">
|
||||
<Label htmlFor="knowledge-base-embedding-model">{t('knowledgeBaseEmbeddingModel')}</Label>
|
||||
<Input
|
||||
id="knowledge-base-embedding-model"
|
||||
value={knowledgeBaseForm.embedding_model}
|
||||
placeholder={t('knowledgeBaseEmbeddingModelPlaceholder')}
|
||||
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, embedding_model: e.target.value }))}
|
||||
disabled={!currentProject}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="knowledge-base-chunk-size">{t('knowledgeBaseChunkSize')}</Label>
|
||||
<Input
|
||||
id="knowledge-base-chunk-size"
|
||||
type="number"
|
||||
value={knowledgeBaseForm.chunk_size}
|
||||
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, chunk_size: Number(e.target.value) || 0 }))}
|
||||
disabled={!currentProject}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="knowledge-base-chunk-overlap">{t('knowledgeBaseChunkOverlap')}</Label>
|
||||
<Input
|
||||
id="knowledge-base-chunk-overlap"
|
||||
type="number"
|
||||
value={knowledgeBaseForm.chunk_overlap}
|
||||
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, chunk_overlap: Number(e.target.value) || 0 }))}
|
||||
disabled={!currentProject}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="knowledge-base-top-k">{t('knowledgeBaseTopK')}</Label>
|
||||
<Input
|
||||
id="knowledge-base-top-k"
|
||||
type="number"
|
||||
value={knowledgeBaseForm.top_k}
|
||||
onChange={(e) => setKnowledgeBaseForm((prev) => ({ ...prev, top_k: Number(e.target.value) || 0 }))}
|
||||
disabled={!currentProject}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-between rounded-lg border border-border px-3 py-2 mt-7">
|
||||
<Label htmlFor="knowledge-base-active">{t('activeStatus')}</Label>
|
||||
<Switch
|
||||
id="knowledge-base-active"
|
||||
checked={knowledgeBaseForm.is_active}
|
||||
onCheckedChange={(checked) => setKnowledgeBaseForm((prev) => ({ ...prev, is_active: checked }))}
|
||||
disabled={!currentProject}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{editingKnowledgeBaseId ? (
|
||||
<Button variant="outline" onClick={resetKnowledgeBaseForm} disabled={isSavingKnowledgeBase}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button onClick={handleSaveKnowledgeBase} disabled={!currentProject || isSavingKnowledgeBase}>
|
||||
{isSavingKnowledgeBase ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
||||
{editingKnowledgeBaseId ? t('updateKnowledgeBase') : t('createKnowledgeBase')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4 space-y-3">
|
||||
<div className="font-medium text-sm text-foreground/80">{t('knowledgeBaseList')}</div>
|
||||
{isLoadingKnowledgeBases ? (
|
||||
<div className="h-20 flex items-center justify-center text-muted-foreground">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : knowledgeBases.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground rounded-md border border-dashed border-border p-4">
|
||||
{t('noKnowledgeBases')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{knowledgeBases.map((item) => (
|
||||
<div key={item.id} className="rounded-lg border border-border p-3 flex items-start justify-between gap-3">
|
||||
<div className="space-y-1 min-w-0">
|
||||
<div className="font-medium text-sm text-foreground truncate">{item.name}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('knowledgeBaseMeta', {
|
||||
count: item.documents?.length || 0,
|
||||
updatedAt: new Date(item.updated_at).toLocaleString(),
|
||||
})}
|
||||
</div>
|
||||
{item.description ? (
|
||||
<div className="text-xs text-muted-foreground break-words">{item.description}</div>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
void handleOpenKnowledgeDocuments(item.id);
|
||||
}}
|
||||
title={t('manageKnowledgeDocuments')}
|
||||
>
|
||||
{selectedKnowledgeBaseId === item.id ? <Plus className="h-4 w-4 text-indigo-500" /> : <FileText className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button variant="ghost" size="icon" onClick={() => handleEditKnowledgeBase(item)} title={t('editKnowledgeBase')}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
void handleReindexKnowledgeBase(item.id);
|
||||
}}
|
||||
disabled={reindexingKnowledgeBaseId === item.id}
|
||||
title={t('reindexKnowledgeBase')}
|
||||
>
|
||||
{reindexingKnowledgeBaseId === item.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <RefreshCw className="h-4 w-4" />}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
void handleDeleteKnowledgeBase(item.id);
|
||||
}}
|
||||
disabled={deletingKnowledgeBaseId === item.id}
|
||||
title={t('deleteKnowledgeBase')}
|
||||
>
|
||||
{deletingKnowledgeBaseId === item.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4 text-red-500" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="border-t border-border pt-4 space-y-3">
|
||||
<div className="font-medium text-sm text-foreground/80">
|
||||
{selectedKnowledgeBase
|
||||
? t('knowledgeDocumentManagerTitle', { name: selectedKnowledgeBase.name })
|
||||
: t('knowledgeDocumentManagerTitleEmpty')}
|
||||
</div>
|
||||
{!selectedKnowledgeBase ? (
|
||||
<div className="text-sm text-muted-foreground rounded-md border border-dashed border-border p-4">
|
||||
{t('selectKnowledgeBaseToManageDocuments')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-4">
|
||||
<div className="rounded-lg border border-border p-3 space-y-3">
|
||||
<div className="text-sm font-medium text-foreground">{t('knowledgeDocumentUploadTitle')}</div>
|
||||
<Input
|
||||
type="file"
|
||||
multiple
|
||||
onChange={(e) => setKnowledgeUploadFiles(Array.from(e.target.files || []))}
|
||||
disabled={uploadingKnowledgeDocuments}
|
||||
/>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('knowledgeDocumentUploadHint')}
|
||||
</div>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{knowledgeUploadFiles.length > 0
|
||||
? t('knowledgeDocumentUploadSelected', { count: knowledgeUploadFiles.length })
|
||||
: t('knowledgeDocumentUploadNone')}
|
||||
</div>
|
||||
<Button onClick={handleUploadKnowledgeDocuments} disabled={uploadingKnowledgeDocuments || knowledgeUploadFiles.length === 0}>
|
||||
{uploadingKnowledgeDocuments ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Plus className="h-4 w-4 mr-2" />}
|
||||
{t('knowledgeDocumentUploadAction')}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-3">
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="knowledge-doc-title">{t('knowledgeDocumentTitle')}</Label>
|
||||
<Input
|
||||
id="knowledge-doc-title"
|
||||
value={knowledgeDocumentForm.title}
|
||||
placeholder={t('knowledgeDocumentTitlePlaceholder')}
|
||||
onChange={(e) => setKnowledgeDocumentForm((prev) => ({ ...prev, title: e.target.value }))}
|
||||
disabled={isSavingKnowledgeDocument}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="knowledge-doc-content">{t('knowledgeDocumentContent')}</Label>
|
||||
<Textarea
|
||||
id="knowledge-doc-content"
|
||||
value={knowledgeDocumentForm.content}
|
||||
placeholder={t('knowledgeDocumentContentPlaceholder')}
|
||||
onChange={(e) => setKnowledgeDocumentForm((prev) => ({ ...prev, content: e.target.value }))}
|
||||
disabled={isSavingKnowledgeDocument}
|
||||
rows={5}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="knowledge-doc-metadata">{t('knowledgeDocumentMetadata')}</Label>
|
||||
<Textarea
|
||||
id="knowledge-doc-metadata"
|
||||
value={knowledgeDocumentForm.metadata}
|
||||
placeholder={t('knowledgeDocumentMetadataPlaceholder')}
|
||||
onChange={(e) => setKnowledgeDocumentForm((prev) => ({ ...prev, metadata: e.target.value }))}
|
||||
disabled={isSavingKnowledgeDocument}
|
||||
rows={3}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center justify-end gap-2">
|
||||
{editingKnowledgeDocumentId ? (
|
||||
<Button variant="outline" onClick={resetKnowledgeDocumentForm} disabled={isSavingKnowledgeDocument}>
|
||||
{t('cancel')}
|
||||
</Button>
|
||||
) : null}
|
||||
<Button onClick={handleSaveKnowledgeDocument} disabled={isSavingKnowledgeDocument}>
|
||||
{isSavingKnowledgeDocument ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
||||
{editingKnowledgeDocumentId ? t('updateKnowledgeDocument') : t('createKnowledgeDocument')}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{isLoadingKnowledgeDocuments ? (
|
||||
<div className="h-20 flex items-center justify-center text-muted-foreground">
|
||||
<Loader2 className="h-5 w-5 animate-spin" />
|
||||
</div>
|
||||
) : knowledgeDocuments.length === 0 ? (
|
||||
<div className="text-sm text-muted-foreground rounded-md border border-dashed border-border p-4">
|
||||
{t('noKnowledgeDocuments')}
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-2">
|
||||
{knowledgeDocuments.map((doc) => (
|
||||
<div key={doc.id} className="rounded-lg border border-border p-3 flex items-start justify-between gap-3">
|
||||
<div className="space-y-1 min-w-0">
|
||||
<div className="font-medium text-sm text-foreground truncate">{doc.title}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{t('knowledgeDocumentMeta', {
|
||||
updatedAt: new Date(doc.updated_at).toLocaleString(),
|
||||
})}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground break-words">{doc.content.slice(0, 120)}</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-1 shrink-0">
|
||||
<Button variant="ghost" size="icon" onClick={() => handleEditKnowledgeDocument(doc)} title={t('editKnowledgeDocument')}>
|
||||
<Pencil className="h-4 w-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => {
|
||||
void handleDeleteKnowledgeDocument(doc.id);
|
||||
}}
|
||||
disabled={deletingKnowledgeDocumentId === doc.id}
|
||||
title={t('deleteKnowledgeDocument')}
|
||||
>
|
||||
{deletingKnowledgeDocumentId === doc.id ? <Loader2 className="h-4 w-4 animate-spin" /> : <Trash2 className="h-4 w-4 text-red-500" />}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</CardContent>
|
||||
<CardFooter className="bg-muted/50/50 border-t border-border pt-6">
|
||||
<Button variant="outline" onClick={() => void fetchKnowledgeBases()} disabled={!currentProject || isLoadingKnowledgeBases} className="ml-auto">
|
||||
{isLoadingKnowledgeBases ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <RefreshCw className="h-4 w-4 mr-2" />}
|
||||
{t('refreshKnowledgeBaseList')}
|
||||
</Button>
|
||||
</CardFooter>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user