import { Bot, ChevronDown, Maximize2, SquarePen, X } from "lucide-react"; import { useEffect, useRef, useState } from "react"; import { Link, useLocation } from "react-router"; import { toast } from "sonner"; import { agentMockEnabled, 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 { 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 { 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, newAgentConversation, noteAgentReturnPath, setAgentDrawerOpen, shortGoalTitle, showAgentRunInDrawer, syncAgentContextToPath, takeReturnFocus, toggleAgentDrawer, useAgentDrawer, } from "./agent-store.ts"; import { AgentRunOpenContext } from "./agent-subagents.tsx"; import { quickGoalsFor } from "./quick-goals.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; } /** 抽屉标题 = 最近会话切换:显示当前会话的短标题,点开是和会话页同一份会话列表,点一条在抽屉里打开 */ function RecentSwitcher({ runId }: { runId: string | null }) { const [open, setOpen] = useState(false); const run = useAgentRun(runId ?? undefined); const mock = agentMockEnabled(); const title = runId ? (run.data ? shortGoalTitle(run.data.run.goal, 28) : "会话") : "新对话"; return ( { setOpen(false); showAgentRunInDrawer(id); }} className="min-h-0 flex-1" /> ); } /** * 智能体抽屉:任意页面可打开,带当前页面上下文(作品 / 站点 / 下载任务等)。内容和会话页是同一个组件 * (新对话 GoalComposer、会话 AgentRunPanel),这里是紧凑版。 * 头部:标题即最近会话切换、新对话、在页面打开、关闭。关上抽屉会话照常在后台跑,再打开接着看。 * 挂载:在 console layout 里渲染一次 。页面内打开:openAgentDrawer({ entityId, label, ... })。 */ export function AgentDrawer() { const { open, context, runId } = useAgentDrawer(); const create = useCreateAgentRun(); const { pathname, search } = useLocation(); const content = useRef(null); // 记下最近的非智能体页面:会话页「在侧边打开」回到这里 useEffect(() => noteAgentReturnPath(pathname, search), [pathname, search]); useEffect(() => syncAgentContextToPath(pathname, search), [pathname, search]); 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)), }, ) } /> )}
); } /** 顶栏胶囊里的智能体按钮(没有侧栏的窄屏):与快捷键、侧栏入口同一套切换(页面内按钮用 openAgentDrawer 传更完整的上下文) */ export function AgentDrawerTrigger({ className }: { className?: string }) { const { pathname, search } = useLocation(); return ( ); }