diff --git a/frontend-web/src/position-roundtable/components/PositionNodeChat.tsx b/frontend-web/src/position-roundtable/components/PositionNodeChat.tsx index 83af5f1c..06d05185 100644 --- a/frontend-web/src/position-roundtable/components/PositionNodeChat.tsx +++ b/frontend-web/src/position-roundtable/components/PositionNodeChat.tsx @@ -465,7 +465,7 @@ export function PositionNodeChat({ {thread.isLoading ? (
{agentDisplayName} · 进行中 @@ -473,7 +473,7 @@ export function PositionNodeChat({ ) : ( { + const source = value(["source", "taskSource", "sourceName"], fallback.source); + return source === "mock" ? "本系统创建" : source; + })(), + issuedAt: value( + ["issuedAt", "issued_at", "dispatchTime", "publishedAt", "createdAt", "created_at"], + fallback.issuedAt, + ), }; } diff --git a/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx b/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx index e6b654e5..81b5df8a 100644 --- a/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx +++ b/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx @@ -1,11 +1,10 @@ import type { BaseStream } from "@langchain/langgraph-sdk/react"; -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { type ReactNode, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { useQuery } from "@tanstack/react-query"; import { useNavigate, useSearchParams } from "react-router-dom"; import { AlertTriangle, ArrowLeft, - Archive, Bot, BriefcaseBusiness, CheckCircle2, @@ -16,6 +15,7 @@ import { FileOutput, FileText, GitBranch, + History, Layers3, Loader2, MessageSquareText, @@ -51,6 +51,14 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Textarea } from "@/components/ui/textarea"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { useTheme } from "@/contexts/ThemeContext"; import { LayoutModeSwitcher, ThemeSwitcher } from "@/components/workspace/appearance-controls"; import { getAPIClient } from "@/core/api/api-client"; @@ -84,7 +92,6 @@ import { } from "@/position-roundtable/lib/positions"; import { activatePositionSession, - archivePositionSession, completePositionActionPlan, completePositionSummary, createPositionSession, @@ -375,6 +382,27 @@ function formatResultTime(value: string | undefined, fallback = "待完成"): st }).format(date); } +function historySessionTitle(session: PositionRoundtableSession): string { + for (const key of ["title", "taskName", "task_name"]) { + const value = session.taskSnapshot?.[key]; + if (typeof value === "string" && value.trim()) return value.trim(); + } + return "岗位协同会话"; +} + +function historySessionStatus(status: PositionRoundtableSession["status"]): string { + switch (status) { + case "archived": + return "已归档"; + case "completed": + return "已完成"; + case "active": + return "进行中"; + default: + return "意图识别"; + } +} + function artifactName(artifact: Record | null | undefined): string { if (!artifact) return ""; const name = typeof artifact.name === "string" ? artifact.name.trim() : ""; @@ -463,8 +491,10 @@ export default function PositionRoundtablePage({ const [startingChain, setStartingChain] = useState(false); const [rightPanelOpen, setRightPanelOpen] = useState(true); const [rightTab, setRightTab] = useState("agents"); - const [taskPanelCollapsed, setTaskPanelCollapsed] = useState(false); - const [intentPanelCollapsed, setIntentPanelCollapsed] = useState(false); + const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(false); + const [historyMenuOpen, setHistoryMenuOpen] = useState(false); + const [historySessions, setHistorySessions] = useState([]); + const [historyLoading, setHistoryLoading] = useState(false); const artifactsCtx = useArtifactsOptional(); const artifactsCtxRef = useRef(artifactsCtx); const [sandboxArtifact, setSandboxArtifact] = useState(null); @@ -634,6 +664,8 @@ export default function PositionRoundtablePage({ title, description: taskDraft.description.trim(), direction: taskDraft.direction.trim() || task.direction, + source: taskDraft.source.trim() || task.source, + issuedAt: taskDraft.issuedAt.trim() || task.issuedAt, }; setTaskSaving(true); try { @@ -751,15 +783,14 @@ export default function PositionRoundtablePage({ }, [positionSession?.chainSnapshot, positionSession?.status, selectedChain]); const isIntelligenceDeliveryOverview = activePosition.isIntentPosition && positionSession !== null && positionSession.status !== "intent_pending"; - const showTaskAndIntentPanels = !activePosition.isIntentPosition || isIntelligenceDeliveryOverview; - const leftSidebarHidden = - showTaskAndIntentPanels && taskPanelCollapsed && intentPanelCollapsed; + const leftSidebarHidden = leftPanelCollapsed; const allPositionDeliveriesDone = useMemo( () => allExecutableNodesDone(positionSession?.nodes ?? []), [positionSession?.nodes], ); const summaryDone = snapshotStatus(positionSession?.summarySnapshot) === "done"; const actionPlanDone = snapshotStatus(positionSession?.actionPlanSnapshot) === "done"; + const showClosureAgents = positionId === DEFAULT_POSITION_ID; const [activeBuiltinKind, setActiveBuiltinKind] = useState(null); const [builtinAutoStartRequestKey, setBuiltinAutoStartRequestKey] = useState(0); const builtinAgentOptions = useMemo( @@ -1158,18 +1189,54 @@ export default function PositionRoundtablePage({ setSearchParams(next); }, [intent, searchParams, setSearchParams, task.id]); - const handleArchiveSession = useCallback(() => { - if (!positionSession || positionSession.status === "archived") return; - if (!window.confirm("归档后该会话将只读,是否继续?")) return; - void archivePositionSession(positionSession.id) - .then((session) => { - setPositionSession((current) => mergePositionSession(current, session)); - toast.success("会话已归档,可新建会话继续研讨"); + const loadHistorySessions = useCallback(() => { + setHistoryLoading(true); + void listPositionSessions(task.id) + .then((sessions) => { + setHistorySessions( + [...sessions].sort((left, right) => { + const leftTime = new Date(left.updatedAt ?? left.createdAt ?? 0).getTime(); + const rightTime = new Date(right.updatedAt ?? right.createdAt ?? 0).getTime(); + return rightTime - leftTime; + }), + ); }) .catch((error) => { - toast.error(`归档会话失败:${error instanceof Error ? error.message : String(error)}`); + toast.error(`加载历史记录失败:${error instanceof Error ? error.message : String(error)}`); + }) + .finally(() => { + setHistoryLoading(false); }); - }, [positionSession]); + }, [task.id]); + + const handleHistoryMenuOpenChange = useCallback( + (open: boolean) => { + setHistoryMenuOpen(open); + if (open) loadHistorySessions(); + }, + [loadHistorySessions], + ); + + const handleHistorySessionSelect = useCallback( + (sessionId: string) => { + setHistoryMenuOpen(false); + if (sessionId === sessionIdFromUrl) return; + + const ctx = artifactsCtxRef.current; + if (sandboxArtifactRef.current || ctx?.open) ctx?.setOpen(false); + setSandboxArtifact(null); + setSandboxThreadSnapshot(null); + setActiveNodeKey(null); + setActiveBuiltinKind(null); + setSelectedAgentId(null); + const next = new URLSearchParams(searchParams); + next.set("taskId", task.id); + next.set("sessionId", sessionId); + next.delete("new"); + setSearchParams(next); + }, + [searchParams, sessionIdFromUrl, setSearchParams, task.id], + ); const handlePreviewArtifact = useCallback((artifact: PositionArtifactPreview) => { if (!artifact.threadId || !artifact.path) { @@ -1315,6 +1382,10 @@ export default function PositionRoundtablePage({ }, []); const handleSelectBuiltinStage = useCallback( (kind: ClosureStageKind) => { + if (positionId !== DEFAULT_POSITION_ID) { + toast.info("收口智能体仅在测试分析岗中开放使用。"); + return; + } if (!allPositionDeliveriesDone) { toast.info("岗位产物还在汇集中;全部岗位完成 Markdown 交付后,即可开启方案总结。"); return; @@ -1330,7 +1401,7 @@ export default function PositionRoundtablePage({ setActiveBuiltinKind(kind); setBuiltinAutoStartRequestKey((current) => current + 1); }, - [allPositionDeliveriesDone, closeSandbox, summaryDone], + [allPositionDeliveriesDone, closeSandbox, positionId, summaryDone], ); const handleSelectPositionNode = useCallback( (nextNode: PositionRoundtableNode) => { @@ -1353,8 +1424,7 @@ export default function PositionRoundtablePage({ setSelectedAgentId(null); setActiveNodeKey(null); setRightPanelOpen(nextPositionId === DEFAULT_POSITION_ID); - setTaskPanelCollapsed(false); - setIntentPanelCollapsed(false); + setLeftPanelCollapsed(false); }, [closeSandbox], ); @@ -1490,17 +1560,72 @@ export default function PositionRoundtablePage({ > 新建会话 - {positionSession && positionSession.status !== "archived" && ( - } - onClick={handleArchiveSession} + + + } + > + 历史记录 + + + - 归档 - - )} + + 当前任务的历史记录 + + + {historyLoading ? ( +
+ + 正在加载历史记录… +
+ ) : historySessions.length === 0 ? ( +
+ 暂无历史记录 +
+ ) : ( +
+ {historySessions.map((session) => { + const current = session.id === sessionIdFromUrl; + return ( + handleHistorySessionSelect(session.id)} + className={cn( + "mb-1 flex cursor-pointer items-start rounded-[4px] px-2.5 py-2.5 last:mb-0", + current + ? "bg-primary/10 text-primary dark:bg-[#2b8ffe]/15 dark:text-[#a2e1f9]" + : "text-slate-700 dark:text-[#d8e6f8] dark:focus:bg-[#0b386d] dark:focus:text-white", + )} + > +
+
+ + {desensitize(historySessionTitle(session))} + + {current && ( + + 当前 + + )} +
+
+ {historySessionStatus(session.status)} + {formatResultTime(session.updatedAt ?? session.createdAt, "未记录时间")} +
+
+
+ ); + })} +
+ )} + + 当前岗位 @@ -1529,57 +1654,49 @@ export default function PositionRoundtablePage({ {leftSidebarHidden && ( @@ -1588,8 +1705,7 @@ export default function PositionRoundtablePage({ title="展开左侧任务与意图信息" aria-label="展开左侧任务与意图信息" onClick={() => { - setTaskPanelCollapsed(false); - setIntentPanelCollapsed(false); + setLeftPanelCollapsed(false); }} className="absolute left-3 top-3 z-20 flex h-10 w-[30px] items-center justify-center rounded-r-[4px] border border-l-0 border-[#2b8ffe]/60 bg-[#032650]/95 shadow-[4px_0_12px_rgba(0,0,0,0.18)] transition-colors hover:bg-[#0b386d] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#45c3f3]/70" > @@ -1597,7 +1713,7 @@ export default function PositionRoundtablePage({ )} -
+
{!isIntelligenceDeliveryOverview && !rightPanelOpen && !sandboxOpen && ( setRightPanelOpen(false)} onPreviewArtifact={handlePreviewArtifact} @@ -1641,8 +1757,8 @@ export default function PositionRoundtablePage({ agentName={activeNodeAgentName} positionNodeOptions={currentPositionNodeOptions} onSelectPositionNode={handleSelectPositionNode} - builtinAgentOptions={builtinAgentOptions} - onSelectBuiltinAgent={handleSelectBuiltinStage} + builtinAgentOptions={showClosureAgents ? builtinAgentOptions : undefined} + onSelectBuiltinAgent={showClosureAgents ? handleSelectBuiltinStage : undefined} onNodeUpdate={handleNodeUpdate} onConversationStart={() => setRightPanelOpen(false)} onPreviewArtifact={handlePreviewArtifact} @@ -1657,23 +1773,8 @@ export default function PositionRoundtablePage({
- {isIntelligenceDeliveryOverview && !sandboxOpen && ( - - )} {activePosition.isIntentPosition && !isIntelligenceDeliveryOverview && rightPanelOpen && !sandboxOpen && ( -