import { Check, ChevronDown, FolderInput, MoreHorizontal, Pencil, RefreshCw, Search, Trash2, X } from "lucide-react"; import { useEffect, useRef, useState, type CSSProperties, type RefObject } from "react"; import { Button } from "@/shared/ui/button"; import { Input } from "@/shared/ui/input"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/shared/ui/dialog"; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuTrigger } from "@/shared/ui/dropdown-menu"; import { Separator } from "@/shared/ui/separator"; import { cn } from "@/shared/ui/cn"; import { StatusBadge } from "@/shared/ui/status"; import { copySelectedText } from "@/shared/ai-elements/paragraph-copy"; import type { AgentChatSessionSummary, AgentTopic } from "../api/client"; type Props = { open: boolean; onOpenChange: (open: boolean) => void; triggerRef: RefObject; scopeLabel: string; activeSessionId?: string | null; loading: boolean; loadingSessionId: string | null; sessions: AgentChatSessionSummary[]; topics?: AgentTopic[]; topicId?: string; onTopicChange?: (id: string | undefined) => void; onMoveSessionToTopic?: (sessionId: string, topicId: string | null) => Promise; query?: string; onQueryChange?: (value: string) => void; hasMore?: boolean; loadingMore?: boolean; error?: string; pendingUpdate?: boolean; onLoadMore?: () => Promise | void; onRefresh?: () => Promise | void; onSelectSession: (sessionId: string) => void; onRenameSession?: (sessionId: string, title: string) => Promise | void; onDeleteSession?: (sessionId: string) => Promise | void; }; export function AgentHistoryPanel({ open, onOpenChange, triggerRef, scopeLabel, activeSessionId, loading, loadingSessionId, sessions, query = "", onQueryChange, hasMore, loadingMore, error, pendingUpdate, onLoadMore, onRefresh, topics = [], topicId, onTopicChange, onMoveSessionToTopic, onSelectSession, onRenameSession, onDeleteSession }: Props) { const [search, setSearch] = useState(query); const [movingId, setMovingId] = useState(null); const [editingId, setEditingId] = useState(null); const [deletingId, setDeletingId] = useState(null); const [title, setTitle] = useState(""); const [busy, setBusy] = useState(null); const [actionError, setActionError] = useState(null); const searchRef = useRef(null); const [viewport, setViewport] = useState<{ height: number; top: number } | null>(null); useEffect(() => { if (!open) return; const visual = window.visualViewport; const update = () => { const next = { height: visual?.height ?? window.innerHeight, top: visual?.offsetTop ?? 0 }; setViewport((current) => current?.height === next.height && current.top === next.top ? current : next); }; update(); visual?.addEventListener("resize", update); visual?.addEventListener("scroll", update); window.addEventListener("resize", update); return () => { visual?.removeEventListener("resize", update); visual?.removeEventListener("scroll", update); window.removeEventListener("resize", update); }; }, [open]); useEffect(() => { if (!open || search === query) return; const timer = setTimeout(() => onQueryChange?.(search.trim()), 250); return () => clearTimeout(timer); }, [onQueryChange, open, query, search]); const perform = async (id: string, action: () => Promise | void) => { setBusy(id); setActionError(null); try { await action(); setEditingId(null); setDeletingId(null); } catch (reason) { setActionError(reason instanceof Error ? reason.message : "操作失败,请重试"); } finally { setBusy(null); } }; let previousGroup = ""; return { event.preventDefault(); searchRef.current?.focus(); }} onCloseAutoFocus={(event) => { event.preventDefault(); triggerRef.current?.focus(); }}>
历史对话 {scopeLabel}
{onTopicChange || pendingUpdate ?
{onTopicChange ? onTopicChange(id === "all" ? undefined : id)}> 全部主题 未分类 {topics.map((topic) => {topic.name})} : null} {pendingUpdate ? : null}
: null} {error || actionError ?

{actionError ?? error}

: null}
{loading && !sessions.length ?

正在加载历史对话…

: !sessions.length ?

{error ? "历史记录暂不可用,请刷新重试" : query ? "没有找到匹配的会话" : "暂无历史记录"}

: null} {sessions.map((session) => { const group = historyGroup(session.updatedAt); const showGroup = group !== previousGroup; previousGroup = group; const active = session.id === activeSessionId; const editing = session.id === editingId; const deleting = session.id === deletingId; const itemBusy = busy === session.id || loadingSessionId === session.id; return
{showGroup ?

{group}

: null}
{editing ?
{ event.preventDefault(); if (title.trim()) void perform(session.id, () => onRenameSession?.(session.id, title.trim())); }} onKeyDown={(event) => { if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); setEditingId(null); } }}> setTitle(event.target.value)} className="min-w-0 flex-1" />
: movingId === session.id ?

移入主题

{[{ id: "", name: "未分类" }, ...topics].map((topic) => )}
: deleting ?

删除“{session.title}”?

删除后无法从历史记录中恢复

: <> {session.purpose === "task_collaboration" ? 任务协作 : null} {active ? 当前 : null} {session.needsInput ? 需要确认 : null} {session.isStreaming || session.runStatus === "running" ? 处理中 : null} {session.runStatus === "error" ? 处理失败 : null} { setEditingId(session.id); setDeletingId(null); setTitle(session.title); }}> { setMovingId(session.id); setEditingId(null); setDeletingId(null); }}> { setDeletingId(session.id); setEditingId(null); }}> }
; })} {hasMore ?
: null}
; } function historyGroup(value: number) { const now = new Date(); const day = new Date(now.getFullYear(), now.getMonth(), now.getDate()).getTime(); const date = new Date(value); const start = new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime(); const yesterday = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 1).getTime(); const week = new Date(now.getFullYear(), now.getMonth(), now.getDate() - 7).getTime(); return start >= day ? "今天" : start === yesterday ? "昨天" : start >= week ? "近 7 天" : "更早"; } function formatUpdatedAt(value: number) { return new Intl.DateTimeFormat("zh-CN", { year: "numeric", month: "2-digit", day: "2-digit", hour: "2-digit", minute: "2-digit" }).format(value); }