import { useEffect } from "react"; import { useSearchParams } from "react-router"; import { type AgentRunGroup, runGroup, useAgentRun } from "~/api/agent.ts"; // 智能体页面的导航状态(纯函数 + 模块级记忆,单测覆盖): // - 分栏「对话 / 自动巡检」:URL 的 ?tab= 优先,其次当前会话所在分栏,再次上次看的分栏; // - 会话链接、列表页链接的拼法(保留演示开关); // - 页面会话列表的筛选与各分栏滚动位置(列表页 ↔ 会话页之间保留,刷新页面才清)。 export const TAB_PARAM = "tab"; /** URL 里的分栏;没写或写错时为 null */ export function groupFromSearch(search: string | URLSearchParams): AgentRunGroup | null { const v = (typeof search === "string" ? new URLSearchParams(search) : search).get(TAB_PARAM); return v === "auto" || v === "conversation" ? v : null; } /** 分栏取值:URL 显式指定 > 当前会话所在分栏 > 上次看的分栏 > 对话 */ export function resolveGroup(o: { fromUrl: AgentRunGroup | null; activeRun?: AgentRunGroup | null; remembered?: AgentRunGroup | null; }): AgentRunGroup { return o.fromUrl ?? o.activeRun ?? o.remembered ?? "conversation"; } function withQuery(path: string, q: Record): string { const p = new URLSearchParams(); for (const [k, v] of Object.entries(q)) if (v) p.set(k, v); const s = p.toString(); return s ? `${path}?${s}` : path; } /** 会话页链接:分栏由会话本身决定,不带 tab */ export function agentRunHref(id: string, mock = false): string { return withQuery(`/console/agent/${id}`, { mock: mock ? "1" : null }); } /** 列表页链接:「对话」是缺省,不写进 URL */ export function agentListHref(group: AgentRunGroup = "conversation", mock = false): string { return withQuery("/console/agent", { [TAB_PARAM]: group === "auto" ? "auto" : null, mock: mock ? "1" : null }); } /** 页面会话列表的记忆:上次看的分栏、搜索与筛选、各分栏的滚动位置 */ export type ListMemory = { group: AgentRunGroup; q: string; view: "all" | "pending" | "failed"; scroll: Record; }; export const newListMemory = (): ListMemory => ({ group: "conversation", q: "", view: "all", scroll: { conversation: 0, auto: 0 }, }); export const pageListMemory: ListMemory = newListMemory(); /** * 当前项要不要滚进可见区:完全看不见才滚,只滚到刚好露出(nearest),返回新的 scrollTop;不用滚时为 null。 * top / height 是当前项相对滚动内容顶部的位置。 */ export function revealScrollTop( item: { top: number; height: number }, viewport: { scrollTop: number; height: number }, ): number | null { const bottom = item.top + item.height; const viewBottom = viewport.scrollTop + viewport.height; if (item.top >= viewport.scrollTop && bottom <= viewBottom) return null; if (item.top < viewport.scrollTop) return Math.max(0, item.top); return Math.max(0, bottom - viewport.height); } /** 会话所在分栏(子会话按父会话);快照没到之前为 null */ export function useRunGroup(runId: string | undefined): AgentRunGroup | null { const run = useAgentRun(runId); const r = run.data?.run; const parentId = r?.trigger === "subagent" ? (r.parentRunId ?? undefined) : undefined; const parent = useAgentRun(parentId); if (!r) return null; if (parentId && !parent.data) return null; return runGroup(r, parent.data?.run ?? null); } /** * 页面会话列表的分栏:URL 的 ?tab= 优先;会话页再按当前会话所在分栏,快照没到时沿用上次的分栏(避免闪一下)。 * 列表页没有 tab 就是「对话」,URL 与选中项一致。切换分栏只改 URL(replace,不进历史)。 */ export function usePageListGroup(activeRun?: AgentRunGroup | null): [AgentRunGroup, (g: AgentRunGroup) => void] { const [params, setParams] = useSearchParams(); const onRunPage = activeRun !== undefined; const group = resolveGroup({ fromUrl: groupFromSearch(params), activeRun: onRunPage ? activeRun : null, remembered: onRunPage ? pageListMemory.group : null, }); useEffect(() => { pageListMemory.group = group; }, [group]); const change = (g: AgentRunGroup) => setParams( (prev) => { const next = new URLSearchParams(prev); next.set(TAB_PARAM, g); return next; }, { replace: true, preventScrollReset: true }, ); return [group, change]; }