Files
rag_agent/rag-web-ui/frontend/src/app/dashboard/consistency-analysis/page.tsx
2026-04-13 11:34:23 +08:00

309 lines
10 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useCallback, useEffect, useState } from "react";
import { useDropzone } from "react-dropzone";
import {
AlertTriangle,
CheckCircle2,
Download,
FileCode,
FileText,
Loader2,
Play,
Upload,
} from "lucide-react";
import DashboardLayout from "@/components/layout/dashboard-layout";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@/components/ui/card";
import { Progress } from "@/components/ui/progress";
import { useToast } from "@/components/ui/use-toast";
import {
ConsistencyReport,
downloadJson,
loadConsistencyDraft,
mockAnalyzeConsistency,
saveConsistencyDraft,
} from "@/lib/document-mock";
import { cn } from "@/lib/utils";
const severityVariant: Record<string, "default" | "secondary" | "destructive"> =
{
: "destructive",
: "default",
: "secondary",
};
export default function ConsistencyAnalysisPage() {
const [requirementFile, setRequirementFile] = useState<File | null>(null);
const [codeFile, setCodeFile] = useState<File | null>(null);
const [report, setReport] = useState<ConsistencyReport | null>(null);
const [isAnalyzing, setIsAnalyzing] = useState(false);
const { toast } = useToast();
useEffect(() => {
const draft = loadConsistencyDraft();
if (!draft) {
return;
}
setReport(draft);
}, []);
const requirementDropzone = useDropzone({
onDrop: useCallback((acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (!file) {
return;
}
setRequirementFile(file);
}, []),
maxFiles: 1,
accept: {
"application/pdf": [".pdf"],
"application/msword": [".doc"],
"application/vnd.openxmlformats-officedocument.wordprocessingml.document": [
".docx",
],
"application/json": [".json"],
"text/plain": [".txt", ".md"],
},
});
const codeDropzone = useDropzone({
onDrop: useCallback((acceptedFiles: File[]) => {
const file = acceptedFiles[0];
if (!file) {
return;
}
setCodeFile(file);
}, []),
maxFiles: 1,
accept: {
"text/plain": [
".py",
".js",
".ts",
".tsx",
".java",
".go",
".cs",
".cpp",
".c",
".md",
".json",
],
},
});
const handleAnalyze = async () => {
if (!requirementFile || !codeFile) {
toast({
title: "缺少输入文件",
description: "请同时上传需求文档和代码文件。",
variant: "destructive",
});
return;
}
setIsAnalyzing(true);
try {
const result = await mockAnalyzeConsistency(requirementFile, codeFile);
setReport(result);
saveConsistencyDraft(result);
toast({
title: "分析完成",
description: `发现 ${result.issues.length} 项待处理问题。`,
});
} catch {
toast({
title: "分析失败",
description: "前端模拟分析失败,请重试。",
variant: "destructive",
});
} finally {
setIsAnalyzing(false);
}
};
const handleExportReport = () => {
if (!report) {
return;
}
const timestamp = new Date().toISOString().replace(/[:.]/g, "-");
downloadJson(`consistency-report-${timestamp}.json`, report);
toast({
title: "导出成功",
description: "一致性分析报告已下载。",
});
};
return (
<DashboardLayout>
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-2xl"></CardTitle>
<CardDescription>
-
</CardDescription>
</CardHeader>
<CardContent className="grid gap-4 lg:grid-cols-2">
<div
{...requirementDropzone.getRootProps()}
className={cn(
"rounded-lg border-2 border-dashed p-6 transition-colors",
requirementDropzone.isDragActive
? "border-primary bg-primary/5"
: "border-border hover:border-primary/40"
)}
>
<input {...requirementDropzone.getInputProps()} />
<div className="flex items-start gap-3">
<FileText className="h-5 w-5 mt-0.5 text-muted-foreground" />
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground mt-1">
WordPDFJSONTXT/MD
</p>
{requirementFile && (
<p className="text-xs mt-3 inline-flex items-center gap-1 rounded-full border px-3 py-1">
<Upload className="h-3.5 w-3.5" />
{requirementFile.name}
</p>
)}
</div>
</div>
</div>
<div
{...codeDropzone.getRootProps()}
className={cn(
"rounded-lg border-2 border-dashed p-6 transition-colors",
codeDropzone.isDragActive
? "border-primary bg-primary/5"
: "border-border hover:border-primary/40"
)}
>
<input {...codeDropzone.getInputProps()} />
<div className="flex items-start gap-3">
<FileCode className="h-5 w-5 mt-0.5 text-muted-foreground" />
<div>
<p className="text-sm font-medium"></p>
<p className="text-xs text-muted-foreground mt-1">
py/js/ts/tsx/java/go/cs/cpp/c
</p>
{codeFile && (
<p className="text-xs mt-3 inline-flex items-center gap-1 rounded-full border px-3 py-1">
<Upload className="h-3.5 w-3.5" />
{codeFile.name}
</p>
)}
</div>
</div>
</div>
<div className="lg:col-span-2 flex flex-wrap gap-3">
<Button onClick={handleAnalyze} disabled={isAnalyzing}>
{isAnalyzing ? (
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
) : (
<Play className="mr-2 h-4 w-4" />
)}
</Button>
<Button variant="outline" onClick={handleExportReport} disabled={!report}>
<Download className="mr-2 h-4 w-4" />
JSON
</Button>
</div>
</CardContent>
</Card>
{report && (
<>
<div className="grid gap-4 md:grid-cols-3">
<Card>
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground"></p>
<p className="mt-2 text-3xl font-bold">{report.consistencyScore}%</p>
<Progress value={report.consistencyScore} className="mt-3" />
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground"></p>
<p className="mt-2 text-3xl font-bold">{report.coverage}%</p>
<Progress value={report.coverage} className="mt-3" />
</CardContent>
</Card>
<Card>
<CardContent className="pt-6">
<p className="text-sm text-muted-foreground"></p>
<p className="mt-2 text-3xl font-bold">{report.issues.length}</p>
<p className="mt-3 text-xs text-muted-foreground inline-flex items-center gap-1">
<AlertTriangle className="h-3.5 w-3.5" />
</p>
</CardContent>
</Card>
</div>
<Card>
<CardHeader>
<CardTitle className="text-lg"></CardTitle>
<CardDescription>
: {report.requirementFileName} vs {report.codeFileName}
</CardDescription>
</CardHeader>
<CardContent>
<div className="space-y-3 max-h-[520px] overflow-y-auto pr-1">
{report.issues.map((issue) => (
<div
key={issue.id}
className="rounded-lg border p-4 space-y-3 bg-card"
>
<div className="flex flex-wrap items-center justify-between gap-3">
<div className="flex items-center gap-2">
<CheckCircle2 className="h-4 w-4 text-primary" />
<p className="text-sm font-medium">
{issue.id} · {issue.type}
</p>
</div>
<Badge variant={severityVariant[issue.severity]}>
{issue.severity}
</Badge>
</div>
<p className="text-sm text-muted-foreground">{issue.summary}</p>
<div className="grid gap-3 md:grid-cols-2">
<div className="rounded-md border p-2 text-xs text-muted-foreground">
: {issue.requirementRef}
</div>
<div className="rounded-md border p-2 text-xs text-muted-foreground">
: {issue.codeRef}
</div>
</div>
<div className="rounded-md border border-primary/30 bg-primary/5 p-3 text-sm">
: {issue.suggestion}
</div>
</div>
))}
</div>
</CardContent>
</Card>
</>
)}
</div>
</DashboardLayout>
);
}