Files
next-tjwater-frontend/src/features/agent/components/agent-topic-dialog.tsx
T
jiang 2d8506fea0
Generic Container CI/CD / test-build-publish (push) Successful in 2m9s
Frontend CI/CD / build-test-publish-and-deploy (push) Successful in 2m39s
feat: add project-aware agent workspace
2026-10-22 18:52:18 +08:00

109 lines
6.8 KiB
TypeScript

import { Folder, FolderPlus, Pencil, Trash2, X } from "lucide-react";
import { useId, useRef, useState, type RefObject } from "react";
import { Button } from "@/shared/ui/button";
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/shared/ui/dialog";
import { Input } from "@/shared/ui/input";
import type { AgentTopic } from "../api/client";
type TopicView = { kind: "list" | "create" } | { kind: "rename" | "delete"; topic: AgentTopic };
type Props = {
open: boolean;
initialMode: "create" | "manage";
onOpenChange: (open: boolean) => void;
triggerRef: RefObject<HTMLButtonElement | null>;
topics: AgentTopic[];
onCreate: (name: string) => Promise<unknown>;
onRename: (id: string, name: string) => Promise<unknown>;
onDelete: (id: string) => Promise<unknown>;
};
export function AgentTopicDialog({ open, initialMode, onOpenChange, triggerRef, topics, onCreate, onRename, onDelete }: Props) {
const [view, setView] = useState<TopicView>({ kind: initialMode === "create" ? "create" : "list" });
const [name, setName] = useState("");
const [busy, setBusy] = useState(false);
const [error, setError] = useState<string>();
const fieldId = useId();
const inputRef = useRef<HTMLInputElement>(null);
const newTopicRef = useRef<HTMLButtonElement>(null);
const isForm = view.kind === "create" || view.kind === "rename";
const title = view.kind === "create" ? "新建主题" : view.kind === "rename" ? "重命名主题" : view.kind === "delete" ? "删除主题" : "管理主题";
const description = view.kind === "create" ? "将相关事件、问题和对话整理到同一主题。"
: view.kind === "rename" ? "修改名称不会影响主题中的现有会话。"
: view.kind === "delete" ? "仅删除分类,其中的会话会保留在未分类中。"
: `当前项目共有 ${topics.length} 个主题,用于整理相关事件与问题。`;
const changeView = (next: TopicView) => {
setError(undefined);
setName(next.kind === "rename" ? next.topic.name : "");
setView(next);
};
const perform = async (action: () => Promise<unknown>, done: () => void) => {
setBusy(true);
setError(undefined);
try { await action(); done(); }
catch (reason) { setError(reason instanceof Error ? reason.message : "主题操作失败,请重试"); }
finally { setBusy(false); }
};
const cancel = () => initialMode === "create" ? onOpenChange(false) : changeView({ kind: "list" });
const footerClass = "gap-2 px-6 pb-6 sm:gap-0";
return <Dialog open={open} onOpenChange={(next) => { if (!busy) onOpenChange(next); }}>
<DialogContent data-agent-theme="chat" overlayClassName="agent-history-overlay" showCloseButton={false}
className="agent-topic-dialog z-[111] flex max-h-[min(80dvh,40rem)] w-[calc(100vw-2rem)] max-w-md flex-col gap-0 overflow-hidden bg-card p-0"
onOpenAutoFocus={(event) => { event.preventDefault(); (inputRef.current ?? newTopicRef.current)?.focus(); }}
onCloseAutoFocus={(event) => { event.preventDefault(); triggerRef.current?.focus(); }}>
<DialogHeader className="p-6 pb-4 pr-14 text-left">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<Button variant="ghost" size="icon-sm" className="absolute right-4 top-4" aria-label="关闭主题窗口" disabled={busy} onClick={() => onOpenChange(false)}>
<X aria-hidden="true" />
</Button>
{isForm ? <form className="flex min-h-0 flex-col" onSubmit={(event) => {
event.preventDefault();
if (!name.trim() || busy) return;
void perform(() => view.kind === "rename" ? onRename(view.topic.id, name.trim()) : onCreate(name.trim()),
() => view.kind === "create" ? onOpenChange(false) : changeView({ kind: "list" }));
}}>
<div className="min-h-0 space-y-2 overflow-y-auto px-6 pb-6">
<label htmlFor={fieldId} className="text-sm font-medium">主题名称</label>
<Input ref={inputRef} id={fieldId} autoFocus maxLength={80} value={name} disabled={busy}
aria-describedby={`${fieldId}-hint`} onChange={(event) => setName(event.target.value)} placeholder="例如:城南压力异常排查" />
<p id={`${fieldId}-hint`} className="text-xs text-muted-foreground">名称最多 80 个字符。</p>
{error ? <p role="alert" className="text-sm text-destructive">{error}</p> : null}
</div>
<DialogFooter className={footerClass}>
<Button type="button" variant="outline" disabled={busy} onClick={cancel}>取消</Button>
<Button type="submit" loading={busy} disabled={!name.trim()}>{view.kind === "rename" ? "保存主题" : "创建主题"}</Button>
</DialogFooter>
</form> : view.kind === "delete" ? <>
<div className="min-h-0 space-y-3 overflow-y-auto px-6 pb-6">
<p className="break-words text-sm">确认删除“{view.topic.name}”?</p>
{error ? <p role="alert" className="text-sm text-destructive">{error}</p> : null}
</div>
<DialogFooter className={footerClass}>
<Button type="button" autoFocus variant="outline" disabled={busy} onClick={() => changeView({ kind: "list" })}>取消</Button>
<Button variant="destructive" loading={busy} onClick={() => void perform(() => onDelete(view.topic.id), () => changeView({ kind: "list" }))}>确认删除主题</Button>
</DialogFooter>
</> : <>
<div role="list" aria-label="主题列表" className="min-h-0 overflow-y-auto px-6 pb-4">
{topics.length === 0 ? <p className="py-6 text-center text-sm text-muted-foreground">还没有主题,可以新建一个来整理对话。</p> : topics.map((topic) => (
<div role="listitem" key={topic.id} className="flex items-center gap-3 border-b border-border/40 py-3 last:border-0">
<Folder aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" />
<span className="min-w-0 flex-1 break-words text-sm font-medium">{topic.name}</span>
<div className="flex shrink-0 items-center gap-2">
<Button variant="ghost" size="icon-sm" aria-label={`重命名主题:${topic.name}`} onClick={() => changeView({ kind: "rename", topic })}><Pencil aria-hidden="true" /></Button>
<Button variant="ghost" size="icon-sm" aria-label={`删除主题:${topic.name}`} onClick={() => changeView({ kind: "delete", topic })}><Trash2 aria-hidden="true" /></Button>
</div>
</div>
))}
</div>
<DialogFooter className={footerClass}>
<Button ref={newTopicRef} autoFocus variant="outline" onClick={() => changeView({ kind: "create" })}><FolderPlus aria-hidden="true" />新建主题</Button>
<Button onClick={() => onOpenChange(false)}>完成</Button>
</DialogFooter>
</>}
</DialogContent>
</Dialog>;
}