完善skills;测试用例生成页面功能初步实现
This commit is contained in:
@@ -94,7 +94,7 @@ export default function NewChatPage() {
|
||||
开始对话前,请先创建至少一个知识库。
|
||||
</p>
|
||||
<Link
|
||||
href="/dashboard/knowledge"
|
||||
href="/dashboard/knowledge/document"
|
||||
className="inline-flex items-center justify-center rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 bg-primary text-primary-foreground hover:bg-primary/90 h-10 px-4 py-2"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
|
||||
@@ -1,14 +1,17 @@
|
||||
"use client";
|
||||
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||
import { useDropzone } from "react-dropzone";
|
||||
import {
|
||||
ChevronRight,
|
||||
Download,
|
||||
FileJson,
|
||||
FileText,
|
||||
History,
|
||||
Loader2,
|
||||
Save,
|
||||
Sparkles,
|
||||
Trash2,
|
||||
Upload,
|
||||
} from "lucide-react";
|
||||
import DashboardLayout from "@/components/layout/dashboard-layout";
|
||||
@@ -23,6 +26,23 @@ import {
|
||||
} from "@/components/ui/card";
|
||||
import { Input } from "@/components/ui/input";
|
||||
import { Label } from "@/components/ui/label";
|
||||
import { Switch } from "@/components/ui/switch";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogDescription,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@/components/ui/dialog";
|
||||
import {
|
||||
Table,
|
||||
TableBody,
|
||||
TableCell,
|
||||
TableHead,
|
||||
TableHeader,
|
||||
TableRow,
|
||||
} from "@/components/ui/table";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
import {
|
||||
downloadJson,
|
||||
@@ -35,11 +55,15 @@ import {
|
||||
} from "@/lib/document-mock";
|
||||
import {
|
||||
createSrsJob,
|
||||
deleteSrsJob,
|
||||
getSrsJobResult,
|
||||
getSrsJobStatus,
|
||||
listSrsHistory,
|
||||
saveSrsRequirements,
|
||||
SrsHistoryItem,
|
||||
toExtractionResult,
|
||||
} from "@/lib/srs-tools-api";
|
||||
import { buildSectionTree, rebuildRawOutput, SectionTreeNode } from "@/lib/srs-json";
|
||||
import { cn } from "@/lib/utils";
|
||||
|
||||
const formatDateTime = (value: string) => {
|
||||
@@ -69,6 +93,89 @@ const wait = (ms: number) =>
|
||||
|
||||
const EXTRACTION_JOB_KEY = "doc_processing_extraction_job_id";
|
||||
|
||||
const normalizeRequirementType = (value: unknown): RequirementItem["requirementType"] => {
|
||||
if (
|
||||
value === "functional" ||
|
||||
value === "interface" ||
|
||||
value === "performance" ||
|
||||
value === "security" ||
|
||||
value === "reliability" ||
|
||||
value === "other"
|
||||
) {
|
||||
return value;
|
||||
}
|
||||
if (value === "接口需求") {
|
||||
return "interface";
|
||||
}
|
||||
if (value === "性能需求") {
|
||||
return "performance";
|
||||
}
|
||||
if (value === "安全需求") {
|
||||
return "security";
|
||||
}
|
||||
if (value === "可靠性需求") {
|
||||
return "reliability";
|
||||
}
|
||||
if (value === "其他需求") {
|
||||
return "other";
|
||||
}
|
||||
return "functional";
|
||||
};
|
||||
|
||||
const hasInterfaceMetadata = (item: RequirementItem) => {
|
||||
const candidates = [item.interfaceName, item.interfaceType, item.dataSource, item.dataDestination];
|
||||
return candidates.some((field) => {
|
||||
const value = (field || "").trim();
|
||||
return Boolean(value) && !["未知", "unknown", "n/a", "-", "--", "无"].includes(value.toLowerCase());
|
||||
});
|
||||
};
|
||||
|
||||
const normalizeRequirement = (item: RequirementItem, index: number): RequirementItem => {
|
||||
const normalizedType = normalizeRequirementType(item.requirementType);
|
||||
const requirementType =
|
||||
normalizedType === "interface" || hasInterfaceMetadata(item) ? "interface" : normalizedType;
|
||||
|
||||
return {
|
||||
...item,
|
||||
priority: item.priority || "中",
|
||||
requirementType,
|
||||
sectionNumber: item.sectionNumber || "",
|
||||
sectionTitle: item.sectionTitle || "未归类章节",
|
||||
sortOrder: typeof item.sortOrder === "number" ? item.sortOrder : index,
|
||||
interfaceName: requirementType === "interface" ? item.interfaceName || "" : "",
|
||||
interfaceType: requirementType === "interface" ? item.interfaceType || "" : "",
|
||||
dataSource: requirementType === "interface" ? item.dataSource || "" : "",
|
||||
dataDestination: requirementType === "interface" ? item.dataDestination || "" : "",
|
||||
};
|
||||
};
|
||||
|
||||
const normalizeExtractionResult = (
|
||||
extraction: RequirementExtractionResult
|
||||
): RequirementExtractionResult => {
|
||||
const requirements = extraction.requirements.map((item, index) =>
|
||||
normalizeRequirement(item, index)
|
||||
);
|
||||
return {
|
||||
...extraction,
|
||||
requirements,
|
||||
rawOutput: rebuildRawOutput(
|
||||
extraction.rawOutput as Record<string, unknown> | undefined,
|
||||
requirements,
|
||||
extraction.documentName
|
||||
),
|
||||
};
|
||||
};
|
||||
|
||||
const collectSectionKeys = (nodes: SectionTreeNode[]): string[] => {
|
||||
const keys: string[] = [];
|
||||
const walk = (node: SectionTreeNode) => {
|
||||
keys.push(node.key);
|
||||
node.children.forEach((child) => walk(child));
|
||||
};
|
||||
nodes.forEach((node) => walk(node));
|
||||
return keys;
|
||||
};
|
||||
|
||||
export default function RequirementExtractionPage() {
|
||||
const [documentFile, setDocumentFile] = useState<File | null>(null);
|
||||
const [extraction, setExtraction] = useState<RequirementExtractionResult | null>(
|
||||
@@ -81,9 +188,35 @@ export default function RequirementExtractionPage() {
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [activeJobId, setActiveJobId] = useState<number | null>(null);
|
||||
const [isImportingJson, setIsImportingJson] = useState(false);
|
||||
const [historyItems, setHistoryItems] = useState<SrsHistoryItem[]>([]);
|
||||
const [isHistoryLoading, setIsHistoryLoading] = useState(false);
|
||||
const [expandedSectionKeys, setExpandedSectionKeys] = useState<string[]>([]);
|
||||
const [isHistoryOpen, setIsHistoryOpen] = useState(false);
|
||||
const jsonInputRef = useRef<HTMLInputElement>(null);
|
||||
const { toast } = useToast();
|
||||
|
||||
const setExtractionAndPersist = useCallback((value: RequirementExtractionResult | null) => {
|
||||
if (!value) {
|
||||
setExtraction(null);
|
||||
return;
|
||||
}
|
||||
const normalized = normalizeExtractionResult(value);
|
||||
setExtraction(normalized);
|
||||
saveExtractionDraft(normalized);
|
||||
}, []);
|
||||
|
||||
const loadHistory = useCallback(async () => {
|
||||
setIsHistoryLoading(true);
|
||||
try {
|
||||
const items = await listSrsHistory();
|
||||
setHistoryItems(items);
|
||||
} catch {
|
||||
setHistoryItems([]);
|
||||
} finally {
|
||||
setIsHistoryLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const storedJobId = window.localStorage.getItem(EXTRACTION_JOB_KEY);
|
||||
if (storedJobId) {
|
||||
@@ -95,12 +228,15 @@ export default function RequirementExtractionPage() {
|
||||
|
||||
const draft = loadExtractionDraft();
|
||||
if (!draft) {
|
||||
void loadHistory();
|
||||
return;
|
||||
}
|
||||
|
||||
setExtraction(draft);
|
||||
const normalized = normalizeExtractionResult(draft);
|
||||
setExtraction(normalized);
|
||||
setSelectedRequirementId(draft.requirements[0]?.id ?? null);
|
||||
}, []);
|
||||
void loadHistory();
|
||||
}, [loadHistory]);
|
||||
|
||||
const onDrop = useCallback((acceptedFiles: File[]) => {
|
||||
const first = acceptedFiles[0];
|
||||
@@ -125,24 +261,74 @@ export default function RequirementExtractionPage() {
|
||||
(item) => item.id === selectedRequirementId
|
||||
);
|
||||
|
||||
const sectionTree = useMemo(() => {
|
||||
if (!extraction) {
|
||||
return [];
|
||||
}
|
||||
return buildSectionTree(
|
||||
extraction.rawOutput as Record<string, unknown> | undefined,
|
||||
extraction.requirements
|
||||
);
|
||||
}, [extraction]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!sectionTree.length) {
|
||||
setExpandedSectionKeys([]);
|
||||
return;
|
||||
}
|
||||
setExpandedSectionKeys((prev) => {
|
||||
if (prev.length > 0) {
|
||||
return prev;
|
||||
}
|
||||
return collectSectionKeys(sectionTree).slice(0, 2);
|
||||
});
|
||||
}, [sectionTree]);
|
||||
|
||||
const updateRequirement = (
|
||||
requirementId: string,
|
||||
updater: (item: RequirementItem) => RequirementItem
|
||||
) => {
|
||||
let nextSelected = selectedRequirementId;
|
||||
setExtraction((prev) => {
|
||||
if (!prev) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const index = prev.requirements.findIndex((item) => item.id === requirementId);
|
||||
if (index < 0) {
|
||||
return prev;
|
||||
}
|
||||
|
||||
const current = prev.requirements[index];
|
||||
const updated = normalizeRequirement(updater(current), index);
|
||||
const requirements = [...prev.requirements];
|
||||
requirements[index] = {
|
||||
...updated,
|
||||
sortOrder: index,
|
||||
};
|
||||
|
||||
const next: RequirementExtractionResult = {
|
||||
...prev,
|
||||
requirements: prev.requirements.map((item) =>
|
||||
item.id === requirementId ? updater(item) : item
|
||||
requirements,
|
||||
rawOutput: rebuildRawOutput(
|
||||
prev.rawOutput as Record<string, unknown> | undefined,
|
||||
requirements,
|
||||
prev.documentName
|
||||
),
|
||||
};
|
||||
|
||||
saveExtractionDraft(next);
|
||||
|
||||
if (selectedRequirementId === requirementId) {
|
||||
nextSelected = updated.id;
|
||||
}
|
||||
|
||||
return next;
|
||||
});
|
||||
|
||||
if (nextSelected) {
|
||||
setSelectedRequirementId(nextSelected);
|
||||
}
|
||||
};
|
||||
|
||||
const handleExtract = async () => {
|
||||
@@ -167,7 +353,7 @@ export default function RequirementExtractionPage() {
|
||||
const status = await getSrsJobStatus(job.job_id);
|
||||
if (status.status === "completed") {
|
||||
const rawResult = await getSrsJobResult(job.job_id);
|
||||
finalResult = toExtractionResult(rawResult);
|
||||
finalResult = normalizeExtractionResult(toExtractionResult(rawResult));
|
||||
break;
|
||||
}
|
||||
if (status.status === "failed") {
|
||||
@@ -180,9 +366,9 @@ export default function RequirementExtractionPage() {
|
||||
throw new Error("提取任务超时,请稍后重试");
|
||||
}
|
||||
|
||||
setExtraction(finalResult);
|
||||
setExtractionAndPersist(finalResult);
|
||||
setSelectedRequirementId(finalResult.requirements[0]?.id ?? null);
|
||||
saveExtractionDraft(finalResult);
|
||||
await loadHistory();
|
||||
toast({
|
||||
title: "提取完成",
|
||||
description: `已生成 ${finalResult.requirements.length} 条需求项。`,
|
||||
@@ -210,11 +396,10 @@ export default function RequirementExtractionPage() {
|
||||
setIsImportingJson(true);
|
||||
try {
|
||||
const content = await file.text();
|
||||
const parsed = parseRequirementJson(content);
|
||||
setExtraction(parsed);
|
||||
const parsed = normalizeExtractionResult(parseRequirementJson(content));
|
||||
setExtractionAndPersist(parsed);
|
||||
setSelectedRequirementId(parsed.requirements[0]?.id ?? null);
|
||||
setActiveJobId(null);
|
||||
saveExtractionDraft(parsed);
|
||||
window.localStorage.removeItem(EXTRACTION_JOB_KEY);
|
||||
toast({
|
||||
title: "导入成功",
|
||||
@@ -238,9 +423,11 @@ export default function RequirementExtractionPage() {
|
||||
return;
|
||||
}
|
||||
|
||||
const normalized = normalizeExtractionResult(extraction);
|
||||
|
||||
const persist = async () => {
|
||||
if (!activeJobId) {
|
||||
saveExtractionDraft(extraction);
|
||||
setExtractionAndPersist(normalized);
|
||||
toast({
|
||||
title: "保存成功",
|
||||
description: "当前需求编辑结果已保存到本地。",
|
||||
@@ -250,10 +437,10 @@ export default function RequirementExtractionPage() {
|
||||
|
||||
setIsSaving(true);
|
||||
try {
|
||||
const saved = await saveSrsRequirements(activeJobId, extraction);
|
||||
const next = toExtractionResult(saved);
|
||||
setExtraction(next);
|
||||
saveExtractionDraft(next);
|
||||
const saved = await saveSrsRequirements(activeJobId, normalized);
|
||||
const next = normalizeExtractionResult(toExtractionResult(saved));
|
||||
setExtractionAndPersist(next);
|
||||
await loadHistory();
|
||||
toast({
|
||||
title: "保存成功",
|
||||
description: "修改内容已保存到服务端。",
|
||||
@@ -279,13 +466,125 @@ export default function RequirementExtractionPage() {
|
||||
}
|
||||
|
||||
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
|
||||
downloadJson(`requirements-${timestamp}.json`, extraction);
|
||||
const payload = rebuildRawOutput(
|
||||
extraction.rawOutput as Record<string, unknown> | undefined,
|
||||
extraction.requirements,
|
||||
extraction.documentName
|
||||
);
|
||||
downloadJson(`requirements-${timestamp}.json`, payload);
|
||||
toast({
|
||||
title: "导出成功",
|
||||
description: "JSON 文件已下载。",
|
||||
});
|
||||
};
|
||||
|
||||
const toggleSection = (sectionKey: string) => {
|
||||
setExpandedSectionKeys((prev) =>
|
||||
prev.includes(sectionKey)
|
||||
? prev.filter((key) => key !== sectionKey)
|
||||
: [...prev, sectionKey]
|
||||
);
|
||||
};
|
||||
|
||||
const handleLoadHistoryItem = async (jobId: number) => {
|
||||
try {
|
||||
const result = await getSrsJobResult(jobId);
|
||||
const normalized = normalizeExtractionResult(toExtractionResult(result));
|
||||
setExtractionAndPersist(normalized);
|
||||
setSelectedRequirementId(normalized.requirements[0]?.id ?? null);
|
||||
setActiveJobId(jobId);
|
||||
window.localStorage.setItem(EXTRACTION_JOB_KEY, String(jobId));
|
||||
setIsHistoryOpen(false);
|
||||
toast({
|
||||
title: "加载成功",
|
||||
description: `已加载 ${normalized.documentName}`,
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "加载历史文件失败";
|
||||
toast({
|
||||
title: "加载失败",
|
||||
description: message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteHistoryItem = async (jobId: number) => {
|
||||
try {
|
||||
await deleteSrsJob(jobId);
|
||||
if (activeJobId === jobId) {
|
||||
setActiveJobId(null);
|
||||
window.localStorage.removeItem(EXTRACTION_JOB_KEY);
|
||||
setExtraction(null);
|
||||
setSelectedRequirementId(null);
|
||||
}
|
||||
await loadHistory();
|
||||
toast({
|
||||
title: "删除成功",
|
||||
description: "历史文件已删除。",
|
||||
});
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : "删除历史文件失败";
|
||||
toast({
|
||||
title: "删除失败",
|
||||
description: message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const renderSectionNode = (node: SectionTreeNode) => {
|
||||
const expanded = expandedSectionKeys.includes(node.key);
|
||||
const sectionLabel =
|
||||
`${node.sectionNumber ? `${node.sectionNumber} ` : ""}${node.sectionTitle}`.trim() ||
|
||||
"未归类章节";
|
||||
return (
|
||||
<div key={node.key} className="space-y-2">
|
||||
<button
|
||||
className="flex w-full items-center gap-2 rounded-md px-2 py-1 text-left hover:bg-muted"
|
||||
onClick={() => toggleSection(node.key)}
|
||||
>
|
||||
<ChevronRight
|
||||
className={cn("h-4 w-4 text-muted-foreground transition-transform", expanded && "rotate-90")}
|
||||
/>
|
||||
<div className="flex flex-1 items-center justify-between gap-2">
|
||||
<p className="text-sm font-medium">{sectionLabel}</p>
|
||||
<Badge variant="outline">{node.requirements.length}</Badge>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{expanded && (
|
||||
<div className="space-y-2 border-l pl-4">
|
||||
{node.requirements.map((item) => {
|
||||
const active = item.id === selectedRequirementId;
|
||||
return (
|
||||
<button
|
||||
key={`${node.key}-${item.id}`}
|
||||
onClick={() => setSelectedRequirementId(item.id)}
|
||||
className={cn(
|
||||
"w-full rounded-lg border p-3 text-left transition-colors",
|
||||
active ? "border-primary bg-primary/5" : "hover:border-primary/40"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="font-medium text-sm line-clamp-1">{item.id}</p>
|
||||
<Badge variant={priorityVariant[item.priority]}>{item.priority}</Badge>
|
||||
</div>
|
||||
<p className="mt-1 text-xs text-muted-foreground line-clamp-2">
|
||||
{item.description}
|
||||
</p>
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">{item.id}</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
|
||||
{node.children.map((child) => renderSectionNode(child))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-6">
|
||||
@@ -359,6 +658,78 @@ export default function RequirementExtractionPage() {
|
||||
<Download className="mr-2 h-4 w-4" />
|
||||
导出 JSON
|
||||
</Button>
|
||||
|
||||
<Dialog open={isHistoryOpen} onOpenChange={setIsHistoryOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline" onClick={() => void loadHistory()}>
|
||||
<History className="mr-2 h-4 w-4" />
|
||||
历史文件
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-3xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>历史提取文件</DialogTitle>
|
||||
<DialogDescription>
|
||||
仅展示通过“开始提取”生成的历史文件,可加载、覆盖保存或删除。
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>文档名称</TableHead>
|
||||
<TableHead>需求数</TableHead>
|
||||
<TableHead>提取时间</TableHead>
|
||||
<TableHead className="text-right">操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isHistoryLoading && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
||||
正在加载历史文件...
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{!isHistoryLoading && historyItems.length === 0 && (
|
||||
<TableRow>
|
||||
<TableCell colSpan={4} className="text-center text-muted-foreground">
|
||||
暂无历史文件
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{!isHistoryLoading &&
|
||||
historyItems.map((item) => (
|
||||
<TableRow key={item.jobId}>
|
||||
<TableCell>{item.documentName}</TableCell>
|
||||
<TableCell>{item.totalRequirements}</TableCell>
|
||||
<TableCell>{formatDateTime(item.generatedAt)}</TableCell>
|
||||
<TableCell className="text-right">
|
||||
<div className="flex justify-end gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => void handleLoadHistoryItem(item.jobId)}
|
||||
>
|
||||
查看
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="destructive"
|
||||
onClick={() => void handleDeleteHistoryItem(item.jobId)}
|
||||
>
|
||||
<Trash2 className="mr-1 h-3.5 w-3.5" />
|
||||
删除
|
||||
</Button>
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<input
|
||||
ref={jsonInputRef}
|
||||
type="file"
|
||||
@@ -380,8 +751,8 @@ export default function RequirementExtractionPage() {
|
||||
<div className="grid gap-6 lg:grid-cols-[320px_1fr]">
|
||||
<Card className="min-h-[500px]">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">需求项列表</CardTitle>
|
||||
<CardDescription>点击左侧条目可在右侧编辑详情</CardDescription>
|
||||
<CardTitle className="text-lg">需求项树</CardTitle>
|
||||
<CardDescription>按章节层级折叠展开,快速定位需求所属章节</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!extraction && (
|
||||
@@ -391,34 +762,12 @@ export default function RequirementExtractionPage() {
|
||||
)}
|
||||
{extraction && (
|
||||
<div className="space-y-3 max-h-[520px] overflow-y-auto pr-1">
|
||||
{extraction.requirements.map((item) => {
|
||||
const active = item.id === selectedRequirementId;
|
||||
return (
|
||||
<button
|
||||
key={item.id}
|
||||
onClick={() => setSelectedRequirementId(item.id)}
|
||||
className={cn(
|
||||
"w-full rounded-lg border p-3 text-left transition-colors",
|
||||
active
|
||||
? "border-primary bg-primary/5"
|
||||
: "hover:border-primary/40"
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<p className="font-medium text-sm line-clamp-1">{item.title}</p>
|
||||
<Badge variant={priorityVariant[item.priority]}>
|
||||
{item.priority}
|
||||
</Badge>
|
||||
</div>
|
||||
<p className="mt-2 text-xs text-muted-foreground line-clamp-2">
|
||||
{item.description}
|
||||
</p>
|
||||
<p className="mt-2 text-[11px] text-muted-foreground">
|
||||
{item.id} · {item.sourceField}
|
||||
</p>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
{sectionTree.length === 0 && (
|
||||
<div className="rounded-lg border border-dashed p-6 text-sm text-muted-foreground text-center">
|
||||
需求项为空
|
||||
</div>
|
||||
)}
|
||||
{sectionTree.map((node) => renderSectionNode(node))}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
@@ -427,7 +776,7 @@ export default function RequirementExtractionPage() {
|
||||
<Card className="min-h-[500px]">
|
||||
<CardHeader>
|
||||
<CardTitle className="text-lg">需求详情编辑</CardTitle>
|
||||
<CardDescription>字段级编辑后可保存到本地并导出 JSON</CardDescription>
|
||||
<CardDescription>支持接口需求开关与字段级编辑,保存后可覆盖历史记录</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{!selectedRequirement && (
|
||||
@@ -452,20 +801,6 @@ export default function RequirementExtractionPage() {
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="req-title">需求标题</Label>
|
||||
<Input
|
||||
id="req-title"
|
||||
value={selectedRequirement.title}
|
||||
onChange={(event) =>
|
||||
updateRequirement(selectedRequirement.id, (item) => ({
|
||||
...item,
|
||||
title: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="req-priority">优先级</Label>
|
||||
<select
|
||||
@@ -486,17 +821,28 @@ export default function RequirementExtractionPage() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="req-source">来源字段</Label>
|
||||
<Input
|
||||
id="req-source"
|
||||
value={selectedRequirement.sourceField}
|
||||
onChange={(event) =>
|
||||
updateRequirement(selectedRequirement.id, (item) => ({
|
||||
...item,
|
||||
sourceField: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
<div className="flex items-center justify-between rounded-md border p-3">
|
||||
<div>
|
||||
<Label htmlFor="is-interface">是否接口需求</Label>
|
||||
<p className="text-xs text-muted-foreground mt-1">
|
||||
开启后可编辑接口名称、接口类型、数据来源和数据目的地。
|
||||
</p>
|
||||
</div>
|
||||
<Switch
|
||||
id="is-interface"
|
||||
checked={selectedRequirement.requirementType === "interface"}
|
||||
onCheckedChange={(checked) =>
|
||||
updateRequirement(selectedRequirement.id, (item) => ({
|
||||
...item,
|
||||
requirementType: checked ? "interface" : "functional",
|
||||
interfaceName: checked ? item.interfaceName || "" : "",
|
||||
interfaceType: checked ? item.interfaceType || "" : "",
|
||||
dataSource: checked ? item.dataSource || "" : "",
|
||||
dataDestination: checked ? item.dataDestination || "" : "",
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
@@ -528,6 +874,66 @@ export default function RequirementExtractionPage() {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
{selectedRequirement.requirementType === "interface" && (
|
||||
<>
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="req-interface-name">接口名称</Label>
|
||||
<Input
|
||||
id="req-interface-name"
|
||||
value={selectedRequirement.interfaceName || ""}
|
||||
onChange={(event) =>
|
||||
updateRequirement(selectedRequirement.id, (item) => ({
|
||||
...item,
|
||||
interfaceName: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="req-interface-type">接口类型</Label>
|
||||
<Input
|
||||
id="req-interface-type"
|
||||
value={selectedRequirement.interfaceType || ""}
|
||||
onChange={(event) =>
|
||||
updateRequirement(selectedRequirement.id, (item) => ({
|
||||
...item,
|
||||
interfaceType: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="req-data-source">数据来源</Label>
|
||||
<Input
|
||||
id="req-data-source"
|
||||
value={selectedRequirement.dataSource || ""}
|
||||
onChange={(event) =>
|
||||
updateRequirement(selectedRequirement.id, (item) => ({
|
||||
...item,
|
||||
dataSource: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="req-data-destination">数据目的地</Label>
|
||||
<Input
|
||||
id="req-data-destination"
|
||||
value={selectedRequirement.dataDestination || ""}
|
||||
onChange={(event) =>
|
||||
updateRequirement(selectedRequirement.id, (item) => ({
|
||||
...item,
|
||||
dataDestination: event.target.value,
|
||||
}))
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,74 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { Braces, FolderCog, Wrench } from "lucide-react";
|
||||
import DashboardLayout from "@/components/layout/dashboard-layout";
|
||||
|
||||
const plannedFeatures = [
|
||||
{
|
||||
title: "仓库接入",
|
||||
description: "后续接入代码仓库、分支与目录范围管理能力。",
|
||||
icon: FolderCog,
|
||||
},
|
||||
{
|
||||
title: "代码索引",
|
||||
description: "后续支持代码解析、结构化索引与语义检索。",
|
||||
icon: Braces,
|
||||
},
|
||||
{
|
||||
title: "能力扩展",
|
||||
description: "后续补充问答、关联分析与工程化工具链集成。",
|
||||
icon: Wrench,
|
||||
},
|
||||
];
|
||||
|
||||
export default function CodeKnowledgeBasePage() {
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-3xl font-bold tracking-tight">代码知识库</h2>
|
||||
<p className="max-w-3xl text-muted-foreground">
|
||||
当前页面先提供前端结构,后续再逐步补充代码接入、索引、检索和问答能力。
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6 md:grid-cols-3">
|
||||
{plannedFeatures.map((item) => (
|
||||
<section
|
||||
key={item.title}
|
||||
className="space-y-4 rounded-lg border bg-card p-6"
|
||||
>
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-md bg-primary/10 text-primary">
|
||||
<item.icon className="h-5 w-5" />
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">{item.title}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{item.description}
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
))}
|
||||
</div>
|
||||
|
||||
<section className="space-y-4 rounded-lg border border-dashed bg-card p-6">
|
||||
<div className="space-y-2">
|
||||
<h3 className="text-lg font-semibold">当前状态</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
已完成页面入口、导航层级和基础占位内容,暂未接入后端接口和业务流程。
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex flex-wrap gap-3">
|
||||
<Link
|
||||
href="/dashboard/knowledge/document"
|
||||
className="inline-flex items-center justify-center rounded-md border border-input bg-background px-4 py-2 text-sm font-medium hover:bg-accent hover:text-accent-foreground"
|
||||
>
|
||||
返回文档知识库
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { FileIcon, defaultStyles } from "react-file-icon";
|
||||
import {
|
||||
ArrowRight,
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Search,
|
||||
Settings,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import DashboardLayout from "@/components/layout/dashboard-layout";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
interface KnowledgeBase {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
documents: Document[];
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
interface Document {
|
||||
id: number;
|
||||
file_name: string;
|
||||
file_path: string;
|
||||
file_size: number;
|
||||
content_type: string;
|
||||
knowledge_base_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
processing_tasks: any[];
|
||||
}
|
||||
|
||||
export default function DocumentKnowledgeBasePage() {
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
||||
const [collapsedDocumentSections, setCollapsedDocumentSections] = useState<
|
||||
Record<number, boolean>
|
||||
>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
void fetchKnowledgeBases();
|
||||
}, []);
|
||||
|
||||
const fetchKnowledgeBases = async () => {
|
||||
try {
|
||||
const data = await api.get("/api/knowledge-base");
|
||||
setKnowledgeBases(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch knowledge bases:", error);
|
||||
if (error instanceof ApiError) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm("确定要删除这个知识库吗?")) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
await api.delete(`/api/knowledge-base/${id}`);
|
||||
setKnowledgeBases((prev) => prev.filter((kb) => kb.id !== id));
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "知识库删除成功",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to delete knowledge base:", error);
|
||||
if (error instanceof ApiError) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDocumentsSection = (knowledgeBaseId: number) => {
|
||||
setCollapsedDocumentSections((prev) => ({
|
||||
...prev,
|
||||
[knowledgeBaseId]: !prev[knowledgeBaseId],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-8">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">文档知识库</h2>
|
||||
<p className="text-muted-foreground">管理你的知识库与文档</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/knowledge/new"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建知识库
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
{knowledgeBases.map((kb) => {
|
||||
const isDocumentsCollapsed =
|
||||
collapsedDocumentSections[kb.id] ?? true;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={kb.id}
|
||||
className="space-y-4 rounded-lg border bg-card p-6"
|
||||
>
|
||||
<div className="flex items-start justify-between">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{kb.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{kb.description || "暂无描述"}
|
||||
</p>
|
||||
<p className="mt-1 text-sm text-muted-foreground">
|
||||
{kb.documents.length} 个文档 ·{" "}
|
||||
{new Date(kb.created_at).toLocaleDateString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Link
|
||||
href={`/dashboard/knowledge/${kb.id}`}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-secondary"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href={`/dashboard/test-retrieval/${kb.id}`}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-secondary"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => void handleDelete(kb.id)}
|
||||
className="inline-flex h-8 w-8 items-center justify-center rounded-md bg-destructive/10 hover:bg-destructive/20"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{kb.documents.length > 0 && (
|
||||
<div className="border-t pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleDocumentsSection(kb.id)}
|
||||
className="mb-2 flex w-full items-center justify-between rounded-md px-1 py-1 text-left hover:bg-accent/50"
|
||||
>
|
||||
<h4 className="text-sm font-medium">文档</h4>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<span>{isDocumentsCollapsed ? "展开" : "收起"}</span>
|
||||
<ChevronRight
|
||||
className={`h-4 w-4 transition-transform ${
|
||||
isDocumentsCollapsed ? "" : "rotate-90"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{!isDocumentsCollapsed && (
|
||||
<div className="flex max-h-[400px] flex-wrap gap-2 overflow-y-auto">
|
||||
{kb.documents.slice(0, 9).map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex h-[150px] w-[150px] cursor-pointer flex-col items-center justify-center gap-2 rounded-lg border bg-card p-2 transition-colors hover:bg-accent/50"
|
||||
>
|
||||
<div className="mb-2 h-8 w-8">
|
||||
{doc.content_type.toLowerCase().includes("pdf") ? (
|
||||
<FileIcon extension="pdf" {...defaultStyles.pdf} />
|
||||
) : doc.content_type.toLowerCase().includes("doc") ? (
|
||||
<FileIcon extension="doc" {...defaultStyles.docx} />
|
||||
) : doc.content_type.toLowerCase().includes("txt") ? (
|
||||
<FileIcon extension="txt" {...defaultStyles.txt} />
|
||||
) : doc.content_type.toLowerCase().includes("md") ? (
|
||||
<FileIcon extension="md" {...defaultStyles.md} />
|
||||
) : (
|
||||
<FileIcon
|
||||
extension={doc.file_name.split(".").pop() || ""}
|
||||
color="#E2E8F0"
|
||||
labelColor="#94A3B8"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="max-w-[100px] text-center text-sm font-medium">
|
||||
<div className="line-clamp-2 overflow-hidden text-ellipsis">
|
||||
{doc.file_name}
|
||||
</div>
|
||||
</div>
|
||||
<span className="mt-1 text-xs text-muted-foreground">
|
||||
{new Date(doc.created_at).toLocaleDateString("zh-CN")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{kb.documents.length > 9 && (
|
||||
<Link
|
||||
href={`/dashboard/knowledge/${kb.id}`}
|
||||
className="flex h-[150px] w-[150px] cursor-pointer flex-col items-center justify-center rounded-lg border bg-card p-2 transition-colors hover:bg-accent/50"
|
||||
>
|
||||
<div className="mb-2 flex h-8 w-8 items-center justify-center">
|
||||
<ArrowRight className="h-6 w-6" />
|
||||
</div>
|
||||
<span className="text-center text-sm font-medium">
|
||||
查看全部文档
|
||||
</span>
|
||||
<span className="mt-1 text-xs text-muted-foreground">
|
||||
共 {kb.documents.length} 个
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{!loading && knowledgeBases.length === 0 && (
|
||||
<div className="py-12 text-center">
|
||||
<p className="text-muted-foreground">暂无知识库,请先创建一个。</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="space-y-4">
|
||||
<div className="mx-auto h-8 w-8 animate-spin rounded-full border-4 border-primary/30 border-t-primary" />
|
||||
<p className="animate-pulse text-muted-foreground">
|
||||
正在加载知识库...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
}
|
||||
@@ -1,259 +1,5 @@
|
||||
"use client";
|
||||
import { redirect } from "next/navigation";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import Link from "next/link";
|
||||
import { FileIcon, defaultStyles } from "react-file-icon";
|
||||
import {
|
||||
ArrowRight,
|
||||
ChevronRight,
|
||||
Plus,
|
||||
Search,
|
||||
Settings,
|
||||
Trash2,
|
||||
} from "lucide-react";
|
||||
import DashboardLayout from "@/components/layout/dashboard-layout";
|
||||
import { api, ApiError } from "@/lib/api";
|
||||
import { useToast } from "@/components/ui/use-toast";
|
||||
|
||||
interface KnowledgeBase {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
documents: Document[];
|
||||
created_at: string;
|
||||
}
|
||||
interface Document {
|
||||
id: number;
|
||||
file_name: string;
|
||||
file_path: string;
|
||||
file_size: number;
|
||||
content_type: string;
|
||||
knowledge_base_id: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
processing_tasks: any[];
|
||||
}
|
||||
|
||||
export default function KnowledgeBasePage() {
|
||||
const [knowledgeBases, setKnowledgeBases] = useState<KnowledgeBase[]>([]);
|
||||
const [collapsedDocumentSections, setCollapsedDocumentSections] = useState<
|
||||
Record<number, boolean>
|
||||
>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const { toast } = useToast();
|
||||
|
||||
useEffect(() => {
|
||||
fetchKnowledgeBases();
|
||||
}, []);
|
||||
|
||||
const fetchKnowledgeBases = async () => {
|
||||
try {
|
||||
const data = await api.get("/api/knowledge-base");
|
||||
setKnowledgeBases(data);
|
||||
} catch (error) {
|
||||
console.error("Failed to fetch knowledge bases:", error);
|
||||
if (error instanceof ApiError) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
if (!confirm("确定要删除这个知识库吗?"))
|
||||
return;
|
||||
try {
|
||||
await api.delete(`/api/knowledge-base/${id}`);
|
||||
setKnowledgeBases((prev) => prev.filter((kb) => kb.id !== id));
|
||||
toast({
|
||||
title: "成功",
|
||||
description: "知识库删除成功",
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Failed to delete knowledge base:", error);
|
||||
if (error instanceof ApiError) {
|
||||
toast({
|
||||
title: "错误",
|
||||
description: error.message,
|
||||
variant: "destructive",
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const toggleDocumentsSection = (knowledgeBaseId: number) => {
|
||||
setCollapsedDocumentSections((prev) => ({
|
||||
...prev,
|
||||
[knowledgeBaseId]: !prev[knowledgeBaseId],
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<DashboardLayout>
|
||||
<div className="space-y-8">
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-3xl font-bold tracking-tight">
|
||||
知识库
|
||||
</h2>
|
||||
<p className="text-muted-foreground">
|
||||
管理你的知识库与文档
|
||||
</p>
|
||||
</div>
|
||||
<Link
|
||||
href="/dashboard/knowledge/new"
|
||||
className="inline-flex items-center justify-center rounded-md bg-primary px-4 py-2 text-sm font-medium text-primary-foreground hover:bg-primary/90"
|
||||
>
|
||||
<Plus className="mr-2 h-4 w-4" />
|
||||
新建知识库
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
<div className="grid gap-6">
|
||||
{knowledgeBases.map((kb) => {
|
||||
const isDocumentsCollapsed =
|
||||
collapsedDocumentSections[kb.id] ?? true;
|
||||
|
||||
return (
|
||||
<div
|
||||
key={kb.id}
|
||||
className="rounded-lg border bg-card p-6 space-y-4"
|
||||
>
|
||||
<div className="flex justify-between items-start">
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{kb.name}</h3>
|
||||
<p className="text-sm text-muted-foreground">
|
||||
{kb.description || "暂无描述"}
|
||||
</p>
|
||||
<p className="text-sm text-muted-foreground mt-1">
|
||||
{kb.documents.length} 个文档 •{" "}
|
||||
{new Date(kb.created_at).toLocaleDateString("zh-CN")}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex space-x-2">
|
||||
<Link
|
||||
href={`/dashboard/knowledge/${kb.id}`}
|
||||
className="inline-flex items-center justify-center rounded-md bg-secondary w-8 h-8"
|
||||
>
|
||||
<Settings className="h-4 w-4" />
|
||||
</Link>
|
||||
<Link
|
||||
href={`/dashboard/test-retrieval/${kb.id}`}
|
||||
className="inline-flex items-center justify-center rounded-md bg-secondary w-8 h-8"
|
||||
>
|
||||
<Search className="h-4 w-4" />
|
||||
</Link>
|
||||
<button
|
||||
onClick={() => handleDelete(kb.id)}
|
||||
className="inline-flex items-center justify-center rounded-md bg-destructive/10 hover:bg-destructive/20 w-8 h-8"
|
||||
>
|
||||
<Trash2 className="h-4 w-4 text-destructive" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{kb.documents.length > 0 && (
|
||||
<div className="border-t pt-4">
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => toggleDocumentsSection(kb.id)}
|
||||
className="mb-2 flex w-full items-center justify-between rounded-md px-1 py-1 text-left hover:bg-accent/50"
|
||||
>
|
||||
<h4 className="text-sm font-medium">文档</h4>
|
||||
<div className="flex items-center gap-1 text-xs text-muted-foreground">
|
||||
<span>{isDocumentsCollapsed ? "展开" : "收起"}</span>
|
||||
<ChevronRight
|
||||
className={`h-4 w-4 transition-transform ${
|
||||
isDocumentsCollapsed ? "" : "rotate-90"
|
||||
}`}
|
||||
/>
|
||||
</div>
|
||||
</button>
|
||||
|
||||
{!isDocumentsCollapsed && (
|
||||
<div className="flex flex-wrap gap-2 max-h-[400px] overflow-y-auto">
|
||||
{kb.documents.slice(0, 9).map((doc) => (
|
||||
<div
|
||||
key={doc.id}
|
||||
className="flex flex-col items-center gap-2 p-2 rounded-lg border bg-card hover:bg-accent/50 cursor-pointer transition-colors w-[150px] h-[150px] justify-center"
|
||||
>
|
||||
<div className="w-8 h-8 mb-2">
|
||||
{doc.content_type.toLowerCase().includes("pdf") ? (
|
||||
<FileIcon extension="pdf" {...defaultStyles.pdf} />
|
||||
) : doc.content_type.toLowerCase().includes("doc") ? (
|
||||
<FileIcon extension="doc" {...defaultStyles.docx} />
|
||||
) : doc.content_type.toLowerCase().includes("txt") ? (
|
||||
<FileIcon extension="txt" {...defaultStyles.txt} />
|
||||
) : doc.content_type.toLowerCase().includes("md") ? (
|
||||
<FileIcon extension="md" {...defaultStyles.md} />
|
||||
) : (
|
||||
<FileIcon
|
||||
extension={doc.file_name.split(".").pop() || ""}
|
||||
color="#E2E8F0"
|
||||
labelColor="#94A3B8"
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="text-sm font-medium text-center max-w-[100px]">
|
||||
<div className="line-clamp-2 overflow-hidden text-ellipsis">
|
||||
{doc.file_name}
|
||||
</div>
|
||||
</div>
|
||||
<span className="text-xs text-muted-foreground mt-1">
|
||||
{new Date(doc.created_at).toLocaleDateString("zh-CN")}
|
||||
</span>
|
||||
</div>
|
||||
))}
|
||||
{kb.documents.length > 9 && (
|
||||
<Link
|
||||
href={`/dashboard/knowledge/${kb.id}`}
|
||||
className="flex flex-col items-center p-2 rounded-lg border bg-card hover:bg-accent/50 cursor-pointer transition-colors w-[150px] h-[150px] justify-center"
|
||||
>
|
||||
<div className="w-8 h-8 mb-2 flex items-center justify-center">
|
||||
<ArrowRight className="w-6 h-6" />
|
||||
</div>
|
||||
<span className="text-sm font-medium text-center">
|
||||
查看全部文档
|
||||
</span>
|
||||
<span className="text-xs text-muted-foreground mt-1">
|
||||
共 {kb.documents.length} 个
|
||||
</span>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
|
||||
{!loading && knowledgeBases.length === 0 && (
|
||||
<div className="text-center py-12">
|
||||
<p className="text-muted-foreground">
|
||||
暂无知识库,请先创建一个。
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{loading && (
|
||||
<div className="flex items-center justify-center py-12">
|
||||
<div className="space-y-4">
|
||||
<div className="w-8 h-8 border-4 border-primary/30 border-t-primary rounded-full animate-spin mx-auto"></div>
|
||||
<p className="text-muted-foreground animate-pulse">
|
||||
正在加载知识库...
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</DashboardLayout>
|
||||
);
|
||||
export default function KnowledgePage() {
|
||||
redirect("/dashboard/knowledge/document");
|
||||
}
|
||||
|
||||
@@ -99,7 +99,7 @@ export default function DashboardPage() {
|
||||
</div>
|
||||
</div>
|
||||
<a
|
||||
href="/dashboard/knowledge"
|
||||
href="/dashboard/knowledge/document"
|
||||
className="mt-6 flex items-center text-blue-600 dark:text-blue-400 hover:text-blue-700 dark:hover:text-blue-300 text-sm font-medium"
|
||||
>
|
||||
查看全部知识库
|
||||
@@ -152,7 +152,7 @@ export default function DashboardPage() {
|
||||
</a>
|
||||
|
||||
<a
|
||||
href="/dashboard/knowledge"
|
||||
href="/dashboard/knowledge/document"
|
||||
className="flex flex-col items-center justify-center rounded-2xl border border-slate-200 dark:border-slate-700 bg-white dark:bg-slate-800 p-8 shadow-sm hover:shadow-md transition-all hover:border-indigo-500 dark:hover:border-indigo-500"
|
||||
>
|
||||
<div className="rounded-full bg-indigo-100 dark:bg-indigo-900/30 p-4 mb-4">
|
||||
@@ -222,7 +222,7 @@ export default function DashboardPage() {
|
||||
向知识库上传 PDF、DOCX、MD 或 TXT 文件。系统会自动完成处理与索引,支持 AI 检索。
|
||||
</p>
|
||||
<a
|
||||
href="/dashboard/knowledge"
|
||||
href="/dashboard/knowledge/document"
|
||||
className="mt-4 inline-flex items-center text-indigo-600 dark:text-indigo-400 hover:text-indigo-700 dark:hover:text-indigo-300 text-sm font-medium"
|
||||
>
|
||||
去上传
|
||||
|
||||
Reference in New Issue
Block a user