UI: kb manag optimize
This commit is contained in:
@@ -0,0 +1,216 @@
|
|||||||
|
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 { Save, Loader2 } from "lucide-react";
|
||||||
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select";
|
||||||
|
import { Switch } from "@/components/ui/switch";
|
||||||
|
import { useProjectStore } from "@/store/projectStore";
|
||||||
|
import { api } from "@/lib/api";
|
||||||
|
|
||||||
|
export interface KnowledgeBaseFormValues {
|
||||||
|
name: string;
|
||||||
|
description: string;
|
||||||
|
embedding_model: string;
|
||||||
|
chunk_size: number;
|
||||||
|
chunk_overlap: number;
|
||||||
|
top_k: number;
|
||||||
|
is_active: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface KnowledgeBaseFormProps {
|
||||||
|
initialData?: KnowledgeBaseFormValues | null;
|
||||||
|
onSubmit: (data: KnowledgeBaseFormValues) => Promise<void>;
|
||||||
|
onCancel: () => void;
|
||||||
|
isSubmitting?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface EmbeddingModelConfig {
|
||||||
|
id: string;
|
||||||
|
name?: string;
|
||||||
|
provider: string;
|
||||||
|
model: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const defaultFormValues: KnowledgeBaseFormValues = {
|
||||||
|
name: '',
|
||||||
|
description: '',
|
||||||
|
embedding_model: '',
|
||||||
|
chunk_size: 512,
|
||||||
|
chunk_overlap: 50,
|
||||||
|
top_k: 3,
|
||||||
|
is_active: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
export function KnowledgeBaseForm({ initialData, onSubmit, onCancel, isSubmitting = false }: KnowledgeBaseFormProps) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const { currentProject } = useProjectStore();
|
||||||
|
const [form, setForm] = useState<KnowledgeBaseFormValues>(initialData || defaultFormValues);
|
||||||
|
const [embeddingModels, setEmbeddingModels] = useState<EmbeddingModelConfig[]>([]);
|
||||||
|
const [error, setError] = useState('');
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
void fetchEmbeddingModels();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const fetchEmbeddingModels = async () => {
|
||||||
|
try {
|
||||||
|
const data = await api.get<EmbeddingModelConfig[]>('/api/v1/embedding-models');
|
||||||
|
setEmbeddingModels(data);
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to fetch embedding models', err);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const validate = () => {
|
||||||
|
if (!currentProject) {
|
||||||
|
return t('selectProjectBeforeManageKnowledgeBase', 'Please select a project before managing knowledge bases');
|
||||||
|
}
|
||||||
|
if (!form.name.trim()) {
|
||||||
|
return t('knowledgeBaseNameRequired', 'Knowledge base name is required');
|
||||||
|
}
|
||||||
|
if (form.chunk_size < 64 || form.chunk_size > 4096) {
|
||||||
|
return t('knowledgeBaseChunkSizeRange', 'Chunk size must be between 64 and 4096');
|
||||||
|
}
|
||||||
|
if (form.chunk_overlap < 0 || form.chunk_overlap > 512) {
|
||||||
|
return t('knowledgeBaseChunkOverlapRange', 'Chunk overlap must be between 0 and 512');
|
||||||
|
}
|
||||||
|
if (form.chunk_overlap >= form.chunk_size) {
|
||||||
|
return t('knowledgeBaseChunkOverlapTooLarge', 'Chunk overlap must be less than chunk size');
|
||||||
|
}
|
||||||
|
if (form.top_k < 1 || form.top_k > 20) {
|
||||||
|
return t('knowledgeBaseTopKRange', 'Top K must be between 1 and 20');
|
||||||
|
}
|
||||||
|
return '';
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleSubmit = async () => {
|
||||||
|
setError('');
|
||||||
|
const validationMessage = validate();
|
||||||
|
if (validationMessage) {
|
||||||
|
setError(validationMessage);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await onSubmit(form);
|
||||||
|
} catch (err: any) {
|
||||||
|
setError(err.message || t('knowledgeBaseSaveFailed', 'Failed to save knowledge base'));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const selectedEmbeddingModel = embeddingModels.find(m => m.id === form.embedding_model);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-6">
|
||||||
|
{error && <div className="text-sm text-red-600 bg-red-50 border border-red-100 rounded-md p-3">{error}</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={form.name}
|
||||||
|
placeholder={t('knowledgeBaseNamePlaceholder')}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, name: e.target.value }))}
|
||||||
|
disabled={!currentProject || isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 md:col-span-2">
|
||||||
|
<Label htmlFor="knowledge-base-description">{t('description')}</Label>
|
||||||
|
<Input
|
||||||
|
id="knowledge-base-description"
|
||||||
|
value={form.description}
|
||||||
|
placeholder={t('knowledgeBaseDescriptionPlaceholder')}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, description: e.target.value }))}
|
||||||
|
disabled={!currentProject || isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2 md:col-span-2">
|
||||||
|
<Label htmlFor="knowledge-base-embedding-model">{t('knowledgeBaseEmbeddingModel')}</Label>
|
||||||
|
<Select
|
||||||
|
value={form.embedding_model}
|
||||||
|
onValueChange={(val) => setForm((prev) => ({ ...prev, embedding_model: val || '' }))}
|
||||||
|
disabled={!currentProject || isSubmitting}
|
||||||
|
>
|
||||||
|
<SelectTrigger id="knowledge-base-embedding-model">
|
||||||
|
<SelectValue placeholder={t('knowledgeBaseEmbeddingModelPlaceholder')}>
|
||||||
|
{selectedEmbeddingModel
|
||||||
|
? `${selectedEmbeddingModel.name || selectedEmbeddingModel.model} (${selectedEmbeddingModel.provider})`
|
||||||
|
: form.embedding_model || undefined}
|
||||||
|
</SelectValue>
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
{embeddingModels.map((model) => (
|
||||||
|
<SelectItem key={model.id} value={model.id}>
|
||||||
|
{model.name || model.model} ({model.provider})
|
||||||
|
</SelectItem>
|
||||||
|
))}
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="knowledge-base-chunk-size">{t('knowledgeBaseChunkSize')}</Label>
|
||||||
|
<Input
|
||||||
|
id="knowledge-base-chunk-size"
|
||||||
|
type="number"
|
||||||
|
value={form.chunk_size}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, chunk_size: Number(e.target.value) || 0 }))}
|
||||||
|
disabled={!currentProject || isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="knowledge-base-chunk-overlap">{t('knowledgeBaseChunkOverlap')}</Label>
|
||||||
|
<Input
|
||||||
|
id="knowledge-base-chunk-overlap"
|
||||||
|
type="number"
|
||||||
|
value={form.chunk_overlap}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, chunk_overlap: Number(e.target.value) || 0 }))}
|
||||||
|
disabled={!currentProject || isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-2">
|
||||||
|
<Label htmlFor="knowledge-base-top-k">{t('knowledgeBaseTopK')}</Label>
|
||||||
|
<Input
|
||||||
|
id="knowledge-base-top-k"
|
||||||
|
type="number"
|
||||||
|
value={form.top_k}
|
||||||
|
onChange={(e) => setForm((prev) => ({ ...prev, top_k: Number(e.target.value) || 0 }))}
|
||||||
|
disabled={!currentProject || isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-between rounded-lg border border-border px-3 py-2 mt-7 md:col-span-2">
|
||||||
|
<Label htmlFor="knowledge-base-active">{t('activeStatus')}</Label>
|
||||||
|
<Switch
|
||||||
|
id="knowledge-base-active"
|
||||||
|
checked={form.is_active}
|
||||||
|
onCheckedChange={(checked) => setForm((prev) => ({ ...prev, is_active: checked }))}
|
||||||
|
disabled={!currentProject || isSubmitting}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex items-center justify-end gap-2 pt-4 border-t border-border">
|
||||||
|
<Button variant="outline" onClick={onCancel} disabled={isSubmitting}>
|
||||||
|
{t('cancel')}
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleSubmit} disabled={!currentProject || isSubmitting} className="bg-indigo-600 hover:bg-indigo-700 text-primary-foreground">
|
||||||
|
{isSubmitting ? <Loader2 className="h-4 w-4 mr-2 animate-spin" /> : <Save className="h-4 w-4 mr-2" />}
|
||||||
|
{t('save')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
"saveSettings": "Save Settings",
|
"saveSettings": "Save Settings",
|
||||||
"knowledgeBase": "Knowledge Base",
|
"knowledgeBase": "Knowledge Base",
|
||||||
"knowledgeBaseSettings": "Knowledge Base Configuration",
|
"knowledgeBaseSettings": "Knowledge Base Configuration",
|
||||||
"knowledgeBaseSettingsDesc": "Create, edit, reindex, and manage knowledge bases in the current project.",
|
"knowledgeBaseSettingsDesc": "Manage knowledge bases and their documents",
|
||||||
"knowledgeGlobalConfigTitle": "Knowledge Global Configuration",
|
"knowledgeGlobalConfigTitle": "Knowledge Global Configuration",
|
||||||
"knowledgeGlobalConfigDesc": "Configure global API base and key for knowledge service shared across projects.",
|
"knowledgeGlobalConfigDesc": "Configure global API base and key for knowledge service shared across projects.",
|
||||||
"knowledgeGlobalApiBase": "API Base",
|
"knowledgeGlobalApiBase": "API Base",
|
||||||
@@ -169,15 +169,15 @@
|
|||||||
"knowledgeGlobalArkModelRequiredForTest": "For Volcengine Ark, model name is required for connection testing (Model ID or Endpoint ID)",
|
"knowledgeGlobalArkModelRequiredForTest": "For Volcengine Ark, model name is required for connection testing (Model ID or Endpoint ID)",
|
||||||
"saveKnowledgeGlobalConfig": "Save Global Configuration",
|
"saveKnowledgeGlobalConfig": "Save Global Configuration",
|
||||||
"refresh": "Refresh",
|
"refresh": "Refresh",
|
||||||
"knowledgeBaseName": "Knowledge Base Name",
|
"knowledgeBaseName": "Name",
|
||||||
"knowledgeBaseNamePlaceholder": "Enter knowledge base name",
|
"knowledgeBaseNamePlaceholder": "e.g. Sales Q&A",
|
||||||
"knowledgeBaseDescriptionPlaceholder": "Enter knowledge base description (optional)",
|
"knowledgeBaseDescriptionPlaceholder": "Optional description",
|
||||||
"knowledgeBaseEmbeddingModel": "Embedding Model",
|
"knowledgeBaseEmbeddingModel": "Embedding Model",
|
||||||
"knowledgeBaseEmbeddingModelPlaceholder": "Select an embedding model",
|
"knowledgeBaseEmbeddingModelPlaceholder": "Select an embedding model",
|
||||||
"knowledgeBaseChunkSize": "Chunk Size",
|
"knowledgeBaseChunkSize": "Chunk Size",
|
||||||
"knowledgeBaseChunkOverlap": "Chunk Overlap",
|
"knowledgeBaseChunkOverlap": "Chunk Overlap",
|
||||||
"knowledgeBaseTopK": "Top K",
|
"knowledgeBaseTopK": "Top K",
|
||||||
"createKnowledgeBase": "Create Knowledge Base",
|
"createKnowledgeBase": "New Knowledge Base",
|
||||||
"updateKnowledgeBase": "Update Knowledge Base",
|
"updateKnowledgeBase": "Update Knowledge Base",
|
||||||
"knowledgeBaseList": "Knowledge Base List",
|
"knowledgeBaseList": "Knowledge Base List",
|
||||||
"knowledgeBaseMeta": "{{count}} docs · Updated {{updatedAt}}",
|
"knowledgeBaseMeta": "{{count}} docs · Updated {{updatedAt}}",
|
||||||
|
|||||||
@@ -154,7 +154,7 @@
|
|||||||
"saveSettings": "保存设置",
|
"saveSettings": "保存设置",
|
||||||
"knowledgeBase": "知识库",
|
"knowledgeBase": "知识库",
|
||||||
"knowledgeBaseSettings": "知识库配置与建库管理",
|
"knowledgeBaseSettings": "知识库配置与建库管理",
|
||||||
"knowledgeBaseSettingsDesc": "在当前项目下创建、编辑、重建索引并维护知识库配置。",
|
"knowledgeBaseSettingsDesc": "管理知识库及其文档内容",
|
||||||
"knowledgeGlobalConfigTitle": "知识库全局配置",
|
"knowledgeGlobalConfigTitle": "知识库全局配置",
|
||||||
"knowledgeGlobalConfigDesc": "配置知识库服务的全局 API 地址与密钥,所有项目共享。",
|
"knowledgeGlobalConfigDesc": "配置知识库服务的全局 API 地址与密钥,所有项目共享。",
|
||||||
"knowledgeGlobalApiBase": "API Base",
|
"knowledgeGlobalApiBase": "API Base",
|
||||||
@@ -183,14 +183,14 @@
|
|||||||
"saveKnowledgeGlobalConfig": "保存全局配置",
|
"saveKnowledgeGlobalConfig": "保存全局配置",
|
||||||
"refresh": "刷新",
|
"refresh": "刷新",
|
||||||
"knowledgeBaseName": "知识库名称",
|
"knowledgeBaseName": "知识库名称",
|
||||||
"knowledgeBaseNamePlaceholder": "请输入知识库名称",
|
"knowledgeBaseNamePlaceholder": "例如:销售问答库",
|
||||||
"knowledgeBaseDescriptionPlaceholder": "请输入知识库描述(可选)",
|
"knowledgeBaseDescriptionPlaceholder": "选填,知识库描述",
|
||||||
"knowledgeBaseEmbeddingModel": "Embedding 模型",
|
"knowledgeBaseEmbeddingModel": "Embedding 模型",
|
||||||
"knowledgeBaseEmbeddingModelPlaceholder": "请选择一个嵌入模型",
|
"knowledgeBaseEmbeddingModelPlaceholder": "请选择一个嵌入模型",
|
||||||
"knowledgeBaseChunkSize": "Chunk Size",
|
"knowledgeBaseChunkSize": "Chunk Size",
|
||||||
"knowledgeBaseChunkOverlap": "Chunk Overlap",
|
"knowledgeBaseChunkOverlap": "Chunk Overlap",
|
||||||
"knowledgeBaseTopK": "Top K",
|
"knowledgeBaseTopK": "Top K",
|
||||||
"createKnowledgeBase": "创建知识库",
|
"createKnowledgeBase": "新建知识库",
|
||||||
"updateKnowledgeBase": "更新知识库",
|
"updateKnowledgeBase": "更新知识库",
|
||||||
"knowledgeBaseList": "知识库列表",
|
"knowledgeBaseList": "知识库列表",
|
||||||
"knowledgeBaseMeta": "文档 {{count}} 个 · 更新时间 {{updatedAt}}",
|
"knowledgeBaseMeta": "文档 {{count}} 个 · 更新时间 {{updatedAt}}",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user