ai-chatbot/components/agents/AgentProjectListSheet.tsx
Your Name 8d2066a3bf
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled
feat: expand agent workflow and project management
2026-03-19 18:33:42 +08:00

213 lines
8.1 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 { useEffect, useState } from "react";
import { formatDistanceToNow } from "date-fns";
import { zhCN } from "date-fns/locale";
import { FolderOpen, Loader2, Pencil, Plus, Trash2 } from "lucide-react";
import { toast } from "sonner";
import { agentProjectService } from "@/services/agent-projects";
import { Button } from "@/components/ui/button";
import { ScrollArea } from "@/components/ui/scroll-area";
import {
Sheet,
SheetContent,
SheetDescription,
SheetHeader,
SheetTitle,
} from "@/components/ui/sheet";
import { cn } from "@/lib/utils";
import { buildImageProxyUrl } from "@/lib/image-delivery";
import type { AgentGenerationProject } from "@/types/agent";
interface AgentProjectListSheetProps {
open: boolean;
onOpenChange: (open: boolean) => void;
currentProjectId?: string | null;
onCreateProject: () => Promise<void> | void;
onSelectProject: (projectId: string) => Promise<void> | void;
onDeleteProject: (projectId: string) => Promise<void> | void;
onRenameProject: (projectId: string, currentTitle?: string | null) => Promise<void> | void;
}
export function AgentProjectListSheet({
open,
onOpenChange,
currentProjectId,
onCreateProject,
onSelectProject,
onDeleteProject,
onRenameProject,
}: AgentProjectListSheetProps) {
const [projects, setProjects] = useState<AgentGenerationProject[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [pendingDeleteId, setPendingDeleteId] = useState<string | null>(null);
const [pendingRenameId, setPendingRenameId] = useState<string | null>(null);
const loadProjects = async () => {
setIsLoading(true);
try {
const result = await agentProjectService.getProjects();
if (!result.success || !result.data) {
throw new Error(result.error || "加载失败");
}
setProjects(result.data);
} catch (error) {
toast.error(error instanceof Error ? error.message : "加载生图项目失败");
} finally {
setIsLoading(false);
}
};
useEffect(() => {
if (!open) return;
void loadProjects();
}, [open, currentProjectId]);
return (
<Sheet open={open} onOpenChange={onOpenChange}>
<SheetContent
side="right"
className="w-[380px] border-white/10 bg-[#0b0d0f] p-0 text-white sm:max-w-[380px]"
>
<SheetHeader className="border-b border-white/10 px-5 py-4 text-left">
<SheetTitle className="text-white">生图项目列表</SheetTitle>
<SheetDescription className="text-white/45">
这里保存独立于无限画布的生图项目,可随时继续生成。
</SheetDescription>
</SheetHeader>
<div className="border-b border-white/10 px-5 py-4">
<Button
type="button"
onClick={async () => {
await onCreateProject();
await loadProjects();
onOpenChange(false);
}}
className="h-10 w-full gap-2 bg-emerald-500 text-white hover:bg-emerald-400"
>
<Plus className="h-4 w-4" />
新建生图项目
</Button>
</div>
<ScrollArea className="h-[calc(100vh-150px)]">
<div className="space-y-2 p-4">
{isLoading ? (
<div className="flex justify-center py-12">
<Loader2 className="h-6 w-6 animate-spin text-white/50" />
</div>
) : projects.length > 0 ? (
projects.map((project) => (
<button
key={project.id}
type="button"
onClick={async () => {
await onSelectProject(project.id);
onOpenChange(false);
}}
className={cn(
"group flex w-full items-center gap-3 rounded-2xl border p-3 text-left transition-colors",
project.id === currentProjectId
? "border-emerald-400/30 bg-emerald-500/[0.08]"
: "border-white/10 bg-white/[0.03] hover:bg-white/[0.06]"
)}
>
<div className="h-14 w-14 shrink-0 overflow-hidden rounded-xl border border-white/10 bg-black/30">
{project.thumbnail ? (
<img
src={buildImageProxyUrl(project.thumbnail, {
width: 160,
quality: 82,
format: "webp",
})}
alt={project.title}
className="h-full w-full object-cover"
/>
) : (
<div className="flex h-full w-full items-center justify-center">
<FolderOpen className="h-5 w-5 text-white/25" />
</div>
)}
</div>
<div className="min-w-0 flex-1">
<div
className={cn(
"truncate text-sm font-medium",
project.id === currentProjectId ? "text-emerald-200" : "text-white"
)}
>
{project.title || "未命名生图项目"}
</div>
<div className="mt-1 text-xs text-white/45">
{formatDistanceToNow(new Date(project.updatedAt), {
addSuffix: true,
locale: zhCN,
})}
</div>
</div>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-white/45 opacity-0 transition-opacity hover:text-sky-300 group-hover:opacity-100"
disabled={pendingRenameId === project.id}
onClick={async (event) => {
event.stopPropagation();
setPendingRenameId(project.id);
try {
await onRenameProject(project.id, project.title || "");
await loadProjects();
} finally {
setPendingRenameId(null);
}
}}
>
{pendingRenameId === project.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Pencil className="h-4 w-4" />
)}
</Button>
<Button
type="button"
variant="ghost"
size="icon"
className="h-8 w-8 shrink-0 text-white/45 opacity-0 transition-opacity hover:text-rose-300 group-hover:opacity-100"
disabled={pendingDeleteId === project.id || pendingRenameId === project.id}
onClick={async (event) => {
event.stopPropagation();
if (!window.confirm("确定删除这个生图项目吗?")) return;
setPendingDeleteId(project.id);
try {
await onDeleteProject(project.id);
await loadProjects();
} finally {
setPendingDeleteId(null);
}
}}
>
{pendingDeleteId === project.id ? (
<Loader2 className="h-4 w-4 animate-spin" />
) : (
<Trash2 className="h-4 w-4" />
)}
</Button>
</button>
))
) : (
<div className="rounded-2xl border border-dashed border-white/10 bg-white/[0.02] px-4 py-12 text-center">
<div className="text-sm text-white/70">还没有生图项目</div>
<div className="mt-2 text-xs text-white/40">点击上方按钮创建一个独立项目。</div>
</div>
)}
</div>
</ScrollArea>
</SheetContent>
</Sheet>
);
}