109 lines
6.8 KiB
TypeScript
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>;
|
|
}
|