feat: add project-aware agent workspace
This commit is contained in:
@@ -0,0 +1,108 @@
|
||||
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>;
|
||||
}
|
||||
Reference in New Issue
Block a user