From 22e1891a570198d49b78f002a00a23a7d68d6f67 Mon Sep 17 00:00:00 2001 From: qixinbo Date: Sun, 29 Mar 2026 14:44:32 +0800 Subject: [PATCH] chore: layout optimize --- backend/app/api/embedding_models.py | 96 ++ backend/app/schemas/embedding_model.py | 28 + backend/app/services/embedding_model_store.py | 77 ++ backend/app/services/knowledge_index.py | 25 +- backend/main.py | 3 +- frontend/src/App.tsx | 18 + frontend/src/components/Sidebar.tsx | 22 + frontend/src/pages/EmbeddingModels.tsx | 308 ++++++ frontend/src/pages/KnowledgeBases.tsx | 742 ++++++++++++++ frontend/src/pages/Settings.tsx | 922 +----------------- 10 files changed, 1316 insertions(+), 925 deletions(-) create mode 100644 backend/app/api/embedding_models.py create mode 100644 backend/app/schemas/embedding_model.py create mode 100644 backend/app/services/embedding_model_store.py create mode 100644 frontend/src/pages/EmbeddingModels.tsx create mode 100644 frontend/src/pages/KnowledgeBases.tsx diff --git a/backend/app/api/embedding_models.py b/backend/app/api/embedding_models.py new file mode 100644 index 0000000..3e96136 --- /dev/null +++ b/backend/app/api/embedding_models.py @@ -0,0 +1,96 @@ +from typing import List, Optional +from fastapi import APIRouter, HTTPException, Depends +from openai import OpenAI + +from app.schemas.embedding_model import ( + EmbeddingModelConfig, + EmbeddingModelConfigCreate, + EmbeddingModelConfigUpdate, + EmbeddingModelConnectionTestRequest +) +from app.services.embedding_model_store import embedding_model_store +from app.services.openai_compat import normalize_openai_base_url +from app.api.llm import get_admin_user, get_current_user, CurrentUser + +router = APIRouter() + +def _mask_api_key(value: Optional[str]) -> Optional[str]: + if not value: + return None + if len(value) <= 8: + return "*" * len(value) + return f"{value[:4]}{'*' * (len(value) - 8)}{value[-4:]}" + +@router.get("/embedding-models", response_model=List[EmbeddingModelConfig]) +def list_embedding_models(current_user: CurrentUser = Depends(get_current_user)): + models = embedding_model_store.list_models() + for m in models: + if not current_user.is_admin: + m["api_key"] = None + return models + +@router.post("/embedding-models", response_model=EmbeddingModelConfig) +def create_embedding_model(payload: EmbeddingModelConfigCreate, _: CurrentUser = Depends(get_admin_user)): + return embedding_model_store.create_model(payload.model_dump()) + +@router.get("/embedding-models/{model_id}", response_model=EmbeddingModelConfig) +def get_embedding_model(model_id: str, current_user: CurrentUser = Depends(get_current_user)): + model = embedding_model_store.get_model(model_id) + if not model: + raise HTTPException(status_code=404, detail="Embedding model not found") + if not current_user.is_admin: + model["api_key"] = None + return model + +@router.put("/embedding-models/{model_id}", response_model=EmbeddingModelConfig) +def update_embedding_model(model_id: str, payload: EmbeddingModelConfigUpdate, _: CurrentUser = Depends(get_admin_user)): + model = embedding_model_store.update_model(model_id, payload.model_dump(exclude_unset=True)) + if not model: + raise HTTPException(status_code=404, detail="Embedding model not found") + return model + +@router.delete("/embedding-models/{model_id}") +def delete_embedding_model(model_id: str, _: CurrentUser = Depends(get_admin_user)): + deleted = embedding_model_store.delete_model(model_id) + if not deleted: + raise HTTPException(status_code=404, detail="Embedding model not found") + return {"status": "success"} + +@router.post("/embedding-models/test") +def test_embedding_model_connection(payload: EmbeddingModelConnectionTestRequest, _: CurrentUser = Depends(get_admin_user)): + api_base = normalize_openai_base_url(payload.api_base or "") + api_key = payload.api_key + model_name = (payload.model or "").strip() + + if not api_base: + raise HTTPException(status_code=400, detail="API Base is required") + if not api_key: + raise HTTPException(status_code=400, detail="API Key is required") + if not model_name: + raise HTTPException(status_code=400, detail="Model name is required") + + try: + client = OpenAI( + api_key=api_key, + base_url=api_base, + ) + embedding_resp = client.embeddings.create( + model=model_name, + input="connection test", + ) + except Exception as exc: + raise HTTPException(status_code=400, detail=f"Embedding call failed: {exc}") + + dimension = None + if getattr(embedding_resp, "data", None): + first = embedding_resp.data[0] + vector = getattr(first, "embedding", None) + if isinstance(vector, list): + dimension = len(vector) + + return { + "success": True, + "message": "Connection successful", + "model_name": model_name, + "embedding_dimension": dimension, + } diff --git a/backend/app/schemas/embedding_model.py b/backend/app/schemas/embedding_model.py new file mode 100644 index 0000000..7b74d00 --- /dev/null +++ b/backend/app/schemas/embedding_model.py @@ -0,0 +1,28 @@ +from typing import Optional +from pydantic import BaseModel, Field + +class EmbeddingModelConfigBase(BaseModel): + name: str = Field(..., description="Display name for the model configuration") + provider: str = Field("openai", description="Provider type (e.g. openai)") + model: str = Field(..., description="Model name (e.g. text-embedding-3-small)") + api_base: Optional[str] = None + api_key: Optional[str] = None + +class EmbeddingModelConfigCreate(EmbeddingModelConfigBase): + pass + +class EmbeddingModelConfigUpdate(BaseModel): + name: Optional[str] = None + provider: Optional[str] = None + model: Optional[str] = None + api_base: Optional[str] = None + api_key: Optional[str] = None + +class EmbeddingModelConfig(EmbeddingModelConfigBase): + id: str + +class EmbeddingModelConnectionTestRequest(BaseModel): + provider: str = Field("openai") + model: str = Field(...) + api_base: Optional[str] = None + api_key: Optional[str] = None diff --git a/backend/app/services/embedding_model_store.py b/backend/app/services/embedding_model_store.py new file mode 100644 index 0000000..9ffc436 --- /dev/null +++ b/backend/app/services/embedding_model_store.py @@ -0,0 +1,77 @@ +import json +import threading +import uuid +from pathlib import Path +from typing import Any, Dict, List, Optional + +from app.core.data_root import get_data_root + +class EmbeddingModelStore: + def __init__(self) -> None: + self._lock = threading.RLock() + + @staticmethod + def _file_path() -> Path: + return get_data_root() / "embedding_models.json" + + def _read(self) -> List[Dict[str, Any]]: + file_path = self._file_path() + if not file_path.exists(): + return [] + try: + with file_path.open("r", encoding="utf-8") as f: + data = json.load(f) + except (OSError, json.JSONDecodeError): + return [] + if not isinstance(data, list): + return [] + return data + + def _write(self, data: List[Dict[str, Any]]) -> None: + file_path = self._file_path() + file_path.parent.mkdir(parents=True, exist_ok=True) + with file_path.open("w", encoding="utf-8") as f: + json.dump(data, f, indent=2, ensure_ascii=False) + + def list_models(self) -> List[Dict[str, Any]]: + with self._lock: + return self._read() + + def get_model(self, model_id: str) -> Optional[Dict[str, Any]]: + with self._lock: + data = self._read() + for item in data: + if item.get("id") == model_id: + return item + return None + + def create_model(self, payload: Dict[str, Any]) -> Dict[str, Any]: + with self._lock: + data = self._read() + new_model = payload.copy() + new_model["id"] = uuid.uuid4().hex + data.append(new_model) + self._write(data) + return new_model + + def update_model(self, model_id: str, payload: Dict[str, Any]) -> Optional[Dict[str, Any]]: + with self._lock: + data = self._read() + for item in data: + if item.get("id") == model_id: + item.update(payload) + self._write(data) + return item + return None + + def delete_model(self, model_id: str) -> bool: + with self._lock: + data = self._read() + initial_len = len(data) + data = [item for item in data if item.get("id") != model_id] + if len(data) < initial_len: + self._write(data) + return True + return False + +embedding_model_store = EmbeddingModelStore() diff --git a/backend/app/services/knowledge_index.py b/backend/app/services/knowledge_index.py index 7d24e89..5943e6a 100644 --- a/backend/app/services/knowledge_index.py +++ b/backend/app/services/knowledge_index.py @@ -124,10 +124,27 @@ class KnowledgeIndexService: @staticmethod def _build_embed_model(kb: Dict[str, Any]) -> Any: - global_config = knowledge_global_config_store.get() - api_base = global_config.get("api_base") - api_key = global_config.get("api_key") - model_name = kb.get("embedding_model") or global_config.get("default_embedding_model") + from app.services.embedding_model_store import embedding_model_store + models = embedding_model_store.list_models() + if not models: + return None + + target_model = None + kb_model_val = kb.get("embedding_model") + if kb_model_val: + # Try matching by ID first, then by model name + target_model = next((m for m in models if m.get("id") == kb_model_val), None) + if not target_model: + target_model = next((m for m in models if m.get("model") == kb_model_val), None) + + if not target_model: + # Fallback to the first model + target_model = models[0] + + api_base = target_model.get("api_base") + api_key = target_model.get("api_key") + model_name = target_model.get("model") + if not api_base or not api_key or not model_name: return None api_base = _normalize_embedding_api_base(api_base) diff --git a/backend/main.py b/backend/main.py index 5986e60..e958e1c 100644 --- a/backend/main.py +++ b/backend/main.py @@ -16,7 +16,7 @@ import re import os from datetime import datetime -from app.api import upload, llm, skills, users, datasources, projects, semantic, mcp, subagents, knowledge +from app.api import upload, llm, skills, users, datasources, projects, semantic, mcp, subagents, knowledge, embedding_models from app.connectors.postgres import postgres_connector from app.connectors.clickhouse import clickhouse_connector from app.core.artifacts import extract_artifacts @@ -71,6 +71,7 @@ app.include_router(semantic.router, prefix="/api/v1") app.include_router(mcp.router, prefix="/api/v1") app.include_router(subagents.router, prefix="/api/v1") app.include_router(knowledge.router, prefix="/api/v1") +app.include_router(embedding_models.router, prefix="/api/v1") STREAM_DELTA_CHUNK_SIZE = 48 PREVIEWABLE_TEXT_EXTENSIONS = { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5056d18..efa5070 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -9,6 +9,8 @@ import { Users } from "./pages/Users"; import { Projects } from "./pages/Projects"; import { Login } from "./pages/Login"; import { ModelConfigs } from "./pages/ModelConfigs"; +import { EmbeddingModels } from "./pages/EmbeddingModels"; +import { KnowledgeBases } from "./pages/KnowledgeBases"; import { DataSources } from "./pages/DataSources"; import { Modeling } from "./pages/Modeling"; import { Subagents } from "./pages/Subagents"; @@ -127,6 +129,22 @@ function App() { } /> + + + + + + + } /> + + + + + + + } /> diff --git a/frontend/src/components/Sidebar.tsx b/frontend/src/components/Sidebar.tsx index f8a8c2d..30952d9 100644 --- a/frontend/src/components/Sidebar.tsx +++ b/frontend/src/components/Sidebar.tsx @@ -919,6 +919,17 @@ function SidebarBody() { {t('dataSourceManagement')} + + + + + + + + +
+
+ {isLoading ? ( +
+ +
+ ) : ( + + + + {t('modelName', 'Model Name')} + {t('provider', 'Provider')} + {t('modelIdentifier', 'Model Identifier')} + {t('actions', 'Actions')} + + + + {filteredConfigs.length === 0 ? ( + + {t('noModelData', 'No model data')} + + ) : ( + filteredConfigs.map((item) => ( + + + {item.name || item.model} + + {item.provider} + {item.model} + + {isAdmin && ( + <> + + + + )} + + + )) + )} + +
+ )} +
+
+ + + +
+ + {editingId ? t('editModel', 'Edit Model') : t('addModel', 'Add Model')} + +
+ {error &&
{error}
} + +
+
+ + setForm((p) => ({ ...p, name: e.target.value }))} placeholder={t('egTextEmbedding3Small', 'e.g. text-embedding-3-small')} /> +
+
+ + +
+
+ +
+
+ + setForm((p) => ({ ...p, model: e.target.value }))} placeholder="text-embedding-3-small" required /> +
+
+ + setForm((p) => ({ ...p, api_base: e.target.value }))} placeholder={t('egApiDomain', 'e.g. https://api.openai.com/v1')} /> +
+
+ +
+ +
+ setForm((p) => ({ ...p, api_key: e.target.value }))} + className="pr-10" + placeholder={t('leaveBlankIfNotModifying', 'Leave blank if not modifying')} + /> + +
+
+
+ + +
+ + +
+
+
+
+
+ + ); +} diff --git a/frontend/src/pages/KnowledgeBases.tsx b/frontend/src/pages/KnowledgeBases.tsx new file mode 100644 index 0000000..851989d --- /dev/null +++ b/frontend/src/pages/KnowledgeBases.tsx @@ -0,0 +1,742 @@ +import { useState, useEffect } from 'react'; +import { useTranslation } from 'react-i18next'; +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, Database, RefreshCw, Pencil, Trash2, FileText, Plus } from "lucide-react"; +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"; +import { api } from "@/lib/api"; +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; + created_at: string; + updated_at: string; +} + +interface EmbeddingModelConfig { + id: string; + name?: string; + provider: string; + model: 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 KnowledgeBases() { + const { t } = useTranslation(); + const { currentProject } = useProjectStore(); + const [isLoadingKnowledgeBases, setIsLoadingKnowledgeBases] = useState(false); + const [isSavingKnowledgeBase, setIsSavingKnowledgeBase] = useState(false); + const [deletingKnowledgeBaseId, setDeletingKnowledgeBaseId] = useState(''); + const [reindexingKnowledgeBaseId, setReindexingKnowledgeBaseId] = useState(''); + const [knowledgeBases, setKnowledgeBases] = useState([]); + const [editingKnowledgeBaseId, setEditingKnowledgeBaseId] = useState(''); + const [knowledgeBaseForm, setKnowledgeBaseForm] = useState(defaultKnowledgeBaseForm); + const [selectedKnowledgeBaseId, setSelectedKnowledgeBaseId] = useState(''); + const [knowledgeDocuments, setKnowledgeDocuments] = useState([]); + 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([]); + const [embeddingModels, setEmbeddingModels] = useState([]); + const [error, setError] = useState(''); + const [success, setSuccess] = useState(''); + + useEffect(() => { + void fetchKnowledgeBases(); + }, [currentProject?.id]); + + useEffect(() => { + void fetchEmbeddingModels(); + }, []); + + const fetchEmbeddingModels = async () => { + try { + const data = await api.get('/api/v1/embedding-models'); + setEmbeddingModels(data); + } catch (err) { + console.error('Failed to fetch embedding models', err); + } + }; + + const fetchKnowledgeBases = async () => { + if (!currentProject) { + setKnowledgeBases([]); + return; + } + setIsLoadingKnowledgeBases(true); + try { + const data = await api.get(`/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', 'Failed to load knowledge bases')); + } finally { + setIsLoadingKnowledgeBases(false); + } + }; + + const resetKnowledgeBaseForm = () => { + setEditingKnowledgeBaseId(''); + setKnowledgeBaseForm(defaultKnowledgeBaseForm); + }; + + const validateKnowledgeBaseForm = () => { + if (!currentProject) { + return t('selectProjectBeforeManageKnowledgeBase', 'Please select a project before managing knowledge bases'); + } + if (!knowledgeBaseForm.name.trim()) { + return t('knowledgeBaseNameRequired', 'Knowledge base name is required'); + } + if (knowledgeBaseForm.chunk_size < 64 || knowledgeBaseForm.chunk_size > 4096) { + return t('knowledgeBaseChunkSizeRange', 'Chunk size must be between 64 and 4096'); + } + if (knowledgeBaseForm.chunk_overlap < 0 || knowledgeBaseForm.chunk_overlap > 512) { + return t('knowledgeBaseChunkOverlapRange', 'Chunk overlap must be between 0 and 512'); + } + if (knowledgeBaseForm.chunk_overlap >= knowledgeBaseForm.chunk_size) { + return t('knowledgeBaseChunkOverlapTooLarge', 'Chunk overlap must be less than chunk size'); + } + if (knowledgeBaseForm.top_k < 1 || knowledgeBaseForm.top_k > 20) { + return t('knowledgeBaseTopKRange', 'Top K must be between 1 and 20'); + } + 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', 'Knowledge base updated successfully')); + } else { + await api.post('/api/v1/knowledge-bases', payload); + setSuccess(t('knowledgeBaseCreated', 'Knowledge base created successfully')); + } + await fetchKnowledgeBases(); + resetKnowledgeBaseForm(); + } catch (err: any) { + setError(err.message || t('knowledgeBaseSaveFailed', 'Failed to save knowledge base')); + } 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', 'Are you sure to delete this knowledge base?'))) { + return; + } + setError(''); + setSuccess(''); + setDeletingKnowledgeBaseId(id); + try { + await api.delete(`/api/v1/knowledge-bases/${id}`); + setSuccess(t('knowledgeBaseDeleted', 'Knowledge base deleted successfully')); + if (editingKnowledgeBaseId === id) { + resetKnowledgeBaseForm(); + } + if (selectedKnowledgeBaseId === id) { + setSelectedKnowledgeBaseId(''); + setKnowledgeDocuments([]); + setEditingKnowledgeDocumentId(''); + setKnowledgeDocumentForm(defaultKnowledgeDocumentForm); + } + await fetchKnowledgeBases(); + } catch (err: any) { + setError(err.message || t('knowledgeBaseDeleteFailed', 'Failed to delete knowledge base')); + } finally { + setDeletingKnowledgeBaseId(''); + } + }; + + const handleReindexKnowledgeBase = async (id: string) => { + setError(''); + setSuccess(''); + setReindexingKnowledgeBaseId(id); + try { + await api.post(`/api/v1/knowledge-bases/${id}/reindex`, {}); + setSuccess(t('knowledgeBaseReindexSuccess', 'Knowledge base reindexed successfully')); + } catch (err: any) { + setError(err.message || t('knowledgeBaseReindexFailed', 'Failed to reindex knowledge base')); + } finally { + setReindexingKnowledgeBaseId(''); + } + }; + + const resetKnowledgeDocumentForm = () => { + setEditingKnowledgeDocumentId(''); + setKnowledgeDocumentForm(defaultKnowledgeDocumentForm); + }; + + const fetchKnowledgeDocuments = async (kbId: string) => { + if (!kbId) { + setKnowledgeDocuments([]); + return; + } + setIsLoadingKnowledgeDocuments(true); + try { + const data = await api.get(`/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', 'Failed to load knowledge documents')); + } 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', 'Please select a knowledge base to manage documents'); + } + if (!knowledgeDocumentForm.title.trim()) { + return t('knowledgeDocumentTitleRequired', 'Document title is required'); + } + if (!knowledgeDocumentForm.content.trim()) { + return t('knowledgeDocumentContentRequired', 'Document content is required'); + } + const metadataText = knowledgeDocumentForm.metadata.trim(); + if (!metadataText) { + return ''; + } + try { + JSON.parse(metadataText); + return ''; + } catch { + return t('knowledgeDocumentMetadataInvalid', 'Document metadata must be valid JSON'); + } + }; + + 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', 'Knowledge document updated successfully')); + } else { + await api.post(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents`, payload); + setSuccess(t('knowledgeDocumentCreated', 'Knowledge document created successfully')); + } + await fetchKnowledgeDocuments(selectedKnowledgeBaseId); + await fetchKnowledgeBases(); + resetKnowledgeDocumentForm(); + } catch (err: any) { + setError(err.message || t('knowledgeDocumentSaveFailed', 'Failed to save knowledge document')); + } 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', 'Are you sure to delete this document?'))) { + return; + } + setError(''); + setSuccess(''); + setDeletingKnowledgeDocumentId(docId); + try { + await api.delete(`/api/v1/knowledge-bases/${selectedKnowledgeBaseId}/documents/${docId}`); + if (editingKnowledgeDocumentId === docId) { + resetKnowledgeDocumentForm(); + } + setSuccess(t('knowledgeDocumentDeleted', 'Knowledge document deleted successfully')); + await fetchKnowledgeDocuments(selectedKnowledgeBaseId); + await fetchKnowledgeBases(); + } catch (err: any) { + setError(err.message || t('knowledgeDocumentDeleteFailed', 'Failed to delete knowledge document')); + } finally { + setDeletingKnowledgeDocumentId(''); + } + }; + + const handleUploadKnowledgeDocuments = async () => { + setError(''); + setSuccess(''); + if (!selectedKnowledgeBaseId) { + setError(t('selectKnowledgeBaseToManageDocuments', 'Please select a knowledge base to manage documents')); + return; + } + if (knowledgeUploadFiles.length === 0) { + setError(t('knowledgeDocumentUploadEmpty', 'Please select files to upload')); + 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', 'Failed to upload knowledge documents')); + } finally { + setUploadingKnowledgeDocuments(false); + } + }; + + const selectedKnowledgeBase = knowledgeBases.find((item) => item.id === selectedKnowledgeBaseId) || null; + + return ( +
+
+
+ + {t('knowledgeBaseManagement', 'Knowledge Base Management')} +
+
+ +
+
+ {error &&
{error}
} + {success &&
{success}
} + + + + + + {t('knowledgeBaseSettings')} + + {t('knowledgeBaseSettingsDesc')} + + + {!currentProject ? ( +
+ {t('selectProjectBeforeManageKnowledgeBase')} +
+ ) : null} + +
+
+ + setKnowledgeBaseForm((prev) => ({ ...prev, name: e.target.value }))} + disabled={!currentProject} + /> +
+ +
+ + setKnowledgeBaseForm((prev) => ({ ...prev, description: e.target.value }))} + disabled={!currentProject} + /> +
+ +
+ + +
+ +
+ + setKnowledgeBaseForm((prev) => ({ ...prev, chunk_size: Number(e.target.value) || 0 }))} + disabled={!currentProject} + /> +
+ +
+ + setKnowledgeBaseForm((prev) => ({ ...prev, chunk_overlap: Number(e.target.value) || 0 }))} + disabled={!currentProject} + /> +
+ +
+ + setKnowledgeBaseForm((prev) => ({ ...prev, top_k: Number(e.target.value) || 0 }))} + disabled={!currentProject} + /> +
+ +
+ + setKnowledgeBaseForm((prev) => ({ ...prev, is_active: checked }))} + disabled={!currentProject} + /> +
+
+ +
+ {editingKnowledgeBaseId ? ( + + ) : null} + +
+ +
+
{t('knowledgeBaseList')}
+ {isLoadingKnowledgeBases ? ( +
+ +
+ ) : knowledgeBases.length === 0 ? ( +
+ {t('noKnowledgeBases')} +
+ ) : ( +
+ {knowledgeBases.map((item) => ( +
+
+
{item.name}
+
+ {t('knowledgeBaseMeta', { + count: item.documents?.length || 0, + updatedAt: new Date(item.updated_at).toLocaleString(), + })} +
+ {item.description ? ( +
{item.description}
+ ) : null} +
+
+ + + + +
+
+ ))} +
+ )} +
+ +
+
+ {selectedKnowledgeBase + ? t('knowledgeDocumentManagerTitle', { name: selectedKnowledgeBase.name }) + : t('knowledgeDocumentManagerTitleEmpty')} +
+ {!selectedKnowledgeBase ? ( +
+ {t('selectKnowledgeBaseToManageDocuments')} +
+ ) : ( +
+
+
{t('knowledgeDocumentUploadTitle')}
+ setKnowledgeUploadFiles(Array.from(e.target.files || []))} + disabled={uploadingKnowledgeDocuments} + /> +
+ {t('knowledgeDocumentUploadHint')} +
+
+
+ {knowledgeUploadFiles.length > 0 + ? t('knowledgeDocumentUploadSelected', { count: knowledgeUploadFiles.length }) + : t('knowledgeDocumentUploadNone')} +
+ +
+
+ +
+
+ + setKnowledgeDocumentForm((prev) => ({ ...prev, title: e.target.value }))} + disabled={isSavingKnowledgeDocument} + /> +
+
+ +