init. project

This commit is contained in:
2026-04-13 11:34:23 +08:00
commit c7c0659a85
202 changed files with 31196 additions and 0 deletions

View File

@@ -0,0 +1,308 @@
"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>
);
}