import { Bot, ExternalLink, History, Maximize2, Minus, SquarePen, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Link, useLocation } from "react-router"; import { toast } from "sonner"; import { agentMockEnabled, isRunActive, useAgentRun, useCreateAgentRun } from "~/api/agent.ts"; import { useDownloadTask } from "~/api/downloads.ts"; import { useLibraryDetail } from "~/api/library.ts"; import { useRecord } from "~/api/records.ts"; import { useSite } from "~/api/sites.ts"; import { StreamingCursor } from "~/components/agentui/streaming-response.tsx"; import { Button } from "~/components/ui/button.tsx"; import { Popover, PopoverContent, PopoverTrigger } from "~/components/ui/popover.tsx"; import { Sheet, SheetClose, SheetContent, SheetDescription, SheetHeader, SheetTitle } from "~/components/ui/sheet.tsx"; import { errorMessage } from "~/lib/api.ts"; import { capabilityMeta, isDenied, RunStatusBadge } from "./agent-conversation.tsx"; import { DRAFT_DRAWER_GOAL, writeDraft } from "./agent-nav.ts"; import { AgentRunPanel, GoalComposer } from "./agent-run-panel.tsx"; import { AgentSessionList, drawerListMemory } from "./agent-session-list.tsx"; import { type AgentContext, agentShortcutLabel, clearAgentContext, contextPrefix, minimizeAgentDrawer, newAgentConversation, noteAgentReturnPath, restoreAgentDrawer, setAgentDrawerOpen, shortGoalTitle, showAgentRunInDrawer, syncAgentContextToPath, takeReturnFocus, toggleAgentDrawer, useAgentDrawer, } from "./agent-store.ts"; import { AgentRunOpenContext } from "./agent-subagent-tree.tsx"; import { quickGoalsFor } from "./quick-goals.ts"; import { useAgentStream } from "./use-agent-stream.ts"; /** 悬浮按钮等"抽屉外但属于抽屉"的控件打这个属性:点它不算点外面(否则先被关掉又被重新打开) */ export const AGENT_LAUNCHER_ATTR = "data-agent-launcher"; /** * 上下文的显示名称:页面按钮给的 label(「作品:沙丘2」)去掉类别;按路由推断的只有 ID,按 ID 查名称。 * 查不到时退回类别名(「这部作品」),不显示 ID。 */ export function useContextName(ctx: AgentContext | null): string | null { const path = !!ctx?.fromPath; const entity = useLibraryDetail(path ? ctx?.entityId : undefined); const site = useSite(path ? ctx?.siteId : undefined); const task = useDownloadTask(path ? ctx?.downloadTaskId : undefined); const record = useRecord(path ? ctx?.recordId : undefined); if (!ctx) return null; if (!path) return ctx.label ? ctx.label.replace(/^(作品|资源|下载|整理记录|站点|任务|后台任务)[::]\s*/, "") : null; if (ctx.entityId) return entity.data?.entity.title ?? "这部作品"; if (ctx.siteId) return site.data?.name ?? ctx.siteId; if (ctx.downloadTaskId) return task.data?.name ?? "这个下载任务"; if (ctx.recordId) return record.data?.media?.title ?? "这条整理记录"; if (ctx.taskId) return "这个后台任务"; return null; } /** 历史弹层:最近 20 条,等你处理置顶,子会话收在父会话下;点一条在抽屉里打开 */ function HistoryPopover({ activeId }: { activeId: string | null }) { const [open, setOpen] = useState(false); const mock = agentMockEnabled(); return ( { setOpen(false); showAgentRunInDrawer(id); }} className="min-h-0 flex-1" /> ); } /** * 智能体抽屉(AGUI-3):任意页面可打开,带当前页面上下文(作品 / 站点 / 下载任务等)。 * 头部只有「历史」弹层和「新对话」;没有 Tab。运行中可最小化到右下角小窗,流式输出继续。 * 挂载:在 console layout 里渲染一次 。页面内打开:openAgentDrawer({ entityId, label, ... })。 */ export function AgentDrawer() { const { open, minimized, context, runId } = useAgentDrawer(); const create = useCreateAgentRun(); const { pathname, search } = useLocation(); const content = useRef(null); // 记下最近的非智能体页面:会话页「在侧边打开」回到这里 useEffect(() => noteAgentReturnPath(pathname, search), [pathname, search]); useEffect(() => syncAgentContextToPath(pathname), [pathname]); const name = useContextName(context); const mock = agentMockEnabled(); // 目标前缀带名称和 ID:名称给人看,ID 给模型用 const prefix = contextPrefix(context && name ? { ...context, label: name } : context); return ( <> { const ta = content.current?.querySelector("textarea:not([disabled])"); if (!ta) return; e.preventDefault(); ta.focus(); ta.setSelectionRange(ta.value.length, ta.value.length); }} // 关闭后焦点回到打开前的位置(快捷键打开没有触发元素,缺省会落到 body) onCloseAutoFocus={(e) => { const el = takeReturnFocus(); if (!el) return; e.preventDefault(); el.focus(); }} onInteractOutside={(e) => { if ((e.target as Element | null)?.closest?.(`[${AGENT_LAUNCHER_ATTR}]`)) e.preventDefault(); }} > 智能体 {`${agentShortcutLabel()} 打开或收起`} {runId ? ( <> ) : null} {/* 关闭钮放进标题栏这一行,与其它按钮对齐(Sheet 自带的是绝对定位,偏下 5px) */}
{runId ? ( ) : (
create.mutate( { goal: `${prefix}${goal}`, ...opts }, { onSuccess: (run) => { writeDraft(DRAFT_DRAWER_GOAL, ""); showAgentRunInDrawer(run.id); }, onError: (e) => toast.error(errorMessage(e)), }, ) } />
)}
{minimized && runId ? : null} ); } /** 最小化后的小窗:保持事件流订阅,显示状态与最新一句输出 */ function AgentMiniDock({ runId }: { runId: string }) { const run = useAgentRun(runId); const active = isRunActive(run.data?.run.status); const live = useAgentStream(runId, active || !run.data); const last = live.items[live.items.length - 1]; const streaming = active && !live.finished; const line = last?.kind === "text" ? last.text.trim().split("\n").pop() : last?.kind === "tool" ? `${isDenied(last.call) ? "被拒绝" : last.call.status === "running" ? "调用" : "完成"} · ${capabilityMeta(last.call.capabilityId)?.title ?? last.call.capabilityId}` : last?.kind === "thinking" ? "思考中…" : last?.kind === "end" ? last.ok ? "已完成,展开查看结果" : last.message : "等待输出…"; const status = run.data ? live.finished && isRunActive(run.data.run.status) ? "running" : run.data.run.status : null; return (
{status ? : null} {run.data ? shortGoalTitle(run.data.run.goal) : ""}
); } /** 顶栏按钮(窄屏):与快捷键、悬浮按钮同一套切换(页面内按钮用 openAgentDrawer 传更完整的上下文) */ export function AgentDrawerTrigger({ className }: { className?: string }) { const { pathname } = useLocation(); return ( ); }