112 lines
3.4 KiB
TypeScript
112 lines
3.4 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect, useCallback } from "react";
|
|
import { Input } from "@/components/ui/input";
|
|
import { Button } from "@/components/ui/button";
|
|
import { cn } from "@/lib/utils";
|
|
import { listProjects, type ProjectRead } from "@/lib/api/client";
|
|
import { Copy, Search, FileText } from "lucide-react";
|
|
import { toast } from "sonner";
|
|
|
|
export function HistoricalReferences() {
|
|
const [projects, setProjects] = useState<ProjectRead[]>([]);
|
|
const [search, setSearch] = useState("");
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
const load = useCallback(async () => {
|
|
setLoading(true);
|
|
try {
|
|
const data = await listProjects();
|
|
setProjects(data);
|
|
} catch (e) {
|
|
toast.error("加载历史项目失败");
|
|
setProjects([]);
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
}, []);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
}, [load]);
|
|
|
|
const filtered = search.trim()
|
|
? projects.filter(
|
|
(p) =>
|
|
p.raw_requirement.toLowerCase().includes(search.toLowerCase()) ||
|
|
(p.ai_solution_md || "").toLowerCase().includes(search.toLowerCase())
|
|
)
|
|
: projects.slice(0, 10);
|
|
|
|
const copySnippet = (text: string, label: string) => {
|
|
if (!text) return;
|
|
navigator.clipboard.writeText(text);
|
|
toast.success(`已复制 ${label}`);
|
|
};
|
|
|
|
return (
|
|
<div className="border-t p-2">
|
|
<p className="text-xs font-medium text-muted-foreground px-2 mb-2 flex items-center gap-1">
|
|
<FileText className="h-3.5 w-3.5" />
|
|
历史参考
|
|
</p>
|
|
<div className="relative mb-2">
|
|
<Search className="absolute left-2 top-1/2 h-3.5 w-3.5 -translate-y-1/2 text-muted-foreground" />
|
|
<Input
|
|
placeholder="搜索项目..."
|
|
value={search}
|
|
onChange={(e) => setSearch(e.target.value)}
|
|
className="h-8 pl-7 text-xs"
|
|
/>
|
|
</div>
|
|
<div className="max-h-48 overflow-y-auto space-y-1">
|
|
{loading ? (
|
|
<p className="text-xs text-muted-foreground px-2">加载中...</p>
|
|
) : filtered.length === 0 ? (
|
|
<p className="text-xs text-muted-foreground px-2">暂无项目</p>
|
|
) : (
|
|
filtered.map((p) => (
|
|
<div
|
|
key={p.id}
|
|
className={cn(
|
|
"rounded border bg-background/50 p-2 text-xs space-y-1"
|
|
)}
|
|
>
|
|
<p className="font-medium text-foreground line-clamp-1">
|
|
项目 #{p.id}
|
|
</p>
|
|
<p className="text-muted-foreground line-clamp-2">
|
|
{p.raw_requirement.slice(0, 80)}…
|
|
</p>
|
|
<div className="flex gap-1 pt-1">
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 px-1.5 text-xs"
|
|
onClick={() =>
|
|
copySnippet(p.raw_requirement, "原始需求")
|
|
}
|
|
>
|
|
<Copy className="h-3 w-3 mr-0.5" />
|
|
需求
|
|
</Button>
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
className="h-6 px-1.5 text-xs"
|
|
onClick={() =>
|
|
copySnippet(p.ai_solution_md || "", "方案")
|
|
}
|
|
>
|
|
<Copy className="h-3 w-3 mr-0.5" />
|
|
方案
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
))
|
|
)}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|