样式修改

This commit is contained in:
董宏凯 committed 2026-07-23 12:38:48 +08:00
1 parent 08ce3dda73
commit 9f788e37de
4 files changed
+468 -150

No files matched your search

@@ -465,7 +465,7 @@ export function PositionNodeChat({
</span> </span>
{thread.isLoading ? ( {thread.isLoading ? (
<div <div
className="flex h-9 shrink-0 items-center rounded-xl border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 shadow-sm dark:border-[#2b8ffe]/35 dark:bg-[#082d5f] dark:text-[#e8edf5]" className="flex h-9 shrink-0 items-center rounded-[4px] border border-slate-200 bg-white px-3 text-xs font-semibold text-slate-700 shadow-sm dark:border-[#2b8ffe]/35 dark:bg-[#082d5f] dark:text-[#e8edf5]"
style={switchSelectStyle} style={switchSelectStyle}
> >
{agentDisplayName} · 进行中 {agentDisplayName} · 进行中
@@ -473,7 +473,7 @@ export function PositionNodeChat({
) : ( ) : (
<TSelect <TSelect
aria-label="同岗位智能体切换" aria-label="同岗位智能体切换"
className="shrink-0 [&_.t-input]:!rounded-xl [&_.t-input]:!border-slate-200 [&_.t-input]:!bg-white [&_.t-input]:!text-xs [&_.t-input]:!font-semibold [&_.t-input]:!text-slate-700 [&_.t-input]:!shadow-sm dark:[&_.t-input]:!border-[#2b8ffe]/35 dark:[&_.t-input]:!bg-[#082d5f] dark:[&_.t-input]:!text-[#e8edf5]" className="shrink-0 [&_.t-input]:!rounded-[4px] [&_.t-input]:!border-slate-200 [&_.t-input]:!bg-white [&_.t-input]:!text-xs [&_.t-input]:!font-semibold [&_.t-input]:!text-slate-700 [&_.t-input]:!shadow-sm dark:[&_.t-input]:!border-[#2b8ffe]/35 dark:[&_.t-input]:!bg-[#082d5f] dark:[&_.t-input]:!text-[#e8edf5]"
style={switchSelectStyle} style={switchSelectStyle}
value={node.nodeKey} value={node.nodeKey}
options={switchSelectOptions} options={switchSelectOptions}
@@ -11,7 +11,8 @@ export interface PositionTask {
title: string; title: string;
description: string; description: string;
direction: string; direction: string;
source: "mock"; source: string;
issuedAt: string;
} }
export const MOCK_POSITION_TASKS: readonly PositionTask[] = [ export const MOCK_POSITION_TASKS: readonly PositionTask[] = [
@@ -21,7 +22,8 @@ export const MOCK_POSITION_TASKS: readonly PositionTask[] = [
description: description:
"围绕暑期活动的游泳夏令营做宣传", "围绕暑期活动的游泳夏令营做宣传",
direction: "暑期活动", direction: "暑期活动",
source: "mock", source: "本系统创建",
issuedAt: "2026-07-22 17:42:00",
}, },
] as const; ] as const;
@@ -51,6 +53,13 @@ export function taskFromPositionSnapshot(
title: value(["title", "taskName", "name"], fallback.title), title: value(["title", "taskName", "name"], fallback.title),
description: value(["description", "taskContent", "overview", "content"], fallback.description), description: value(["description", "taskContent", "overview", "content"], fallback.description),
direction: value(["direction", "taskDirection", "category"], fallback.direction), direction: value(["direction", "taskDirection", "category"], fallback.direction),
source: "mock", source: (() => {
const source = value(["source", "taskSource", "sourceName"], fallback.source);
return source === "mock" ? "本系统创建" : source;
})(),
issuedAt: value(
["issuedAt", "issued_at", "dispatchTime", "publishedAt", "createdAt", "created_at"],
fallback.issuedAt,
),
}; };
} }
@@ -1,11 +1,10 @@
import type { BaseStream } from "@langchain/langgraph-sdk/react"; 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 { useQuery } from "@tanstack/react-query";
import { useNavigate, useSearchParams } from "react-router-dom"; import { useNavigate, useSearchParams } from "react-router-dom";
import { import {
AlertTriangle, AlertTriangle,
ArrowLeft, ArrowLeft,
Archive,
Bot, Bot,
BriefcaseBusiness, BriefcaseBusiness,
CheckCircle2, CheckCircle2,
@@ -16,6 +15,7 @@ import {
FileOutput, FileOutput,
FileText, FileText,
GitBranch, GitBranch,
History,
Layers3, Layers3,
Loader2, Loader2,
MessageSquareText, MessageSquareText,
@@ -51,6 +51,14 @@ import {
DialogTitle, DialogTitle,
} from "@/components/ui/dialog"; } from "@/components/ui/dialog";
import { Textarea } from "@/components/ui/textarea"; import { Textarea } from "@/components/ui/textarea";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { useTheme } from "@/contexts/ThemeContext"; import { useTheme } from "@/contexts/ThemeContext";
import { LayoutModeSwitcher, ThemeSwitcher } from "@/components/workspace/appearance-controls"; import { LayoutModeSwitcher, ThemeSwitcher } from "@/components/workspace/appearance-controls";
import { getAPIClient } from "@/core/api/api-client"; import { getAPIClient } from "@/core/api/api-client";
@@ -84,7 +92,6 @@ import {
} from "@/position-roundtable/lib/positions"; } from "@/position-roundtable/lib/positions";
import { import {
activatePositionSession, activatePositionSession,
archivePositionSession,
completePositionActionPlan, completePositionActionPlan,
completePositionSummary, completePositionSummary,
createPositionSession, createPositionSession,
@@ -375,6 +382,27 @@ function formatResultTime(value: string | undefined, fallback = "待完成"): st
}).format(date); }).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<string, unknown> | null | undefined): string { function artifactName(artifact: Record<string, unknown> | null | undefined): string {
if (!artifact) return ""; if (!artifact) return "";
const name = typeof artifact.name === "string" ? artifact.name.trim() : ""; const name = typeof artifact.name === "string" ? artifact.name.trim() : "";
@@ -463,8 +491,10 @@ export default function PositionRoundtablePage({
const [startingChain, setStartingChain] = useState(false); const [startingChain, setStartingChain] = useState(false);
const [rightPanelOpen, setRightPanelOpen] = useState(true); const [rightPanelOpen, setRightPanelOpen] = useState(true);
const [rightTab, setRightTab] = useState<WorkspaceTab>("agents"); const [rightTab, setRightTab] = useState<WorkspaceTab>("agents");
const [taskPanelCollapsed, setTaskPanelCollapsed] = useState(false); const [leftPanelCollapsed, setLeftPanelCollapsed] = useState(false);
const [intentPanelCollapsed, setIntentPanelCollapsed] = useState(false); const [historyMenuOpen, setHistoryMenuOpen] = useState(false);
const [historySessions, setHistorySessions] = useState<PositionRoundtableSession[]>([]);
const [historyLoading, setHistoryLoading] = useState(false);
const artifactsCtx = useArtifactsOptional(); const artifactsCtx = useArtifactsOptional();
const artifactsCtxRef = useRef(artifactsCtx); const artifactsCtxRef = useRef(artifactsCtx);
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null); const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(null);
@@ -634,6 +664,8 @@ export default function PositionRoundtablePage({
title, title,
description: taskDraft.description.trim(), description: taskDraft.description.trim(),
direction: taskDraft.direction.trim() || task.direction, direction: taskDraft.direction.trim() || task.direction,
source: taskDraft.source.trim() || task.source,
issuedAt: taskDraft.issuedAt.trim() || task.issuedAt,
}; };
setTaskSaving(true); setTaskSaving(true);
try { try {
@@ -751,15 +783,14 @@ export default function PositionRoundtablePage({
}, [positionSession?.chainSnapshot, positionSession?.status, selectedChain]); }, [positionSession?.chainSnapshot, positionSession?.status, selectedChain]);
const isIntelligenceDeliveryOverview = const isIntelligenceDeliveryOverview =
activePosition.isIntentPosition && positionSession !== null && positionSession.status !== "intent_pending"; activePosition.isIntentPosition && positionSession !== null && positionSession.status !== "intent_pending";
const showTaskAndIntentPanels = !activePosition.isIntentPosition || isIntelligenceDeliveryOverview; const leftSidebarHidden = leftPanelCollapsed;
const leftSidebarHidden =
showTaskAndIntentPanels && taskPanelCollapsed && intentPanelCollapsed;
const allPositionDeliveriesDone = useMemo( const allPositionDeliveriesDone = useMemo(
() => allExecutableNodesDone(positionSession?.nodes ?? []), () => allExecutableNodesDone(positionSession?.nodes ?? []),
[positionSession?.nodes], [positionSession?.nodes],
); );
const summaryDone = snapshotStatus(positionSession?.summarySnapshot) === "done"; const summaryDone = snapshotStatus(positionSession?.summarySnapshot) === "done";
const actionPlanDone = snapshotStatus(positionSession?.actionPlanSnapshot) === "done"; const actionPlanDone = snapshotStatus(positionSession?.actionPlanSnapshot) === "done";
const showClosureAgents = positionId === DEFAULT_POSITION_ID;
const [activeBuiltinKind, setActiveBuiltinKind] = useState<PositionBuiltinAgentKind | null>(null); const [activeBuiltinKind, setActiveBuiltinKind] = useState<PositionBuiltinAgentKind | null>(null);
const [builtinAutoStartRequestKey, setBuiltinAutoStartRequestKey] = useState(0); const [builtinAutoStartRequestKey, setBuiltinAutoStartRequestKey] = useState(0);
const builtinAgentOptions = useMemo<PositionBuiltinAgentSwitchOption[]>( const builtinAgentOptions = useMemo<PositionBuiltinAgentSwitchOption[]>(
@@ -1158,18 +1189,54 @@ export default function PositionRoundtablePage({
setSearchParams(next); setSearchParams(next);
}, [intent, searchParams, setSearchParams, task.id]); }, [intent, searchParams, setSearchParams, task.id]);
const handleArchiveSession = useCallback(() => { const loadHistorySessions = useCallback(() => {
if (!positionSession || positionSession.status === "archived") return; setHistoryLoading(true);
if (!window.confirm("归档后该会话将只读,是否继续?")) return; void listPositionSessions(task.id)
void archivePositionSession(positionSession.id) .then((sessions) => {
.then((session) => { setHistorySessions(
setPositionSession((current) => mergePositionSession(current, session)); [...sessions].sort((left, right) => {
toast.success("会话已归档,可新建会话继续研讨"); 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) => { .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) => { const handlePreviewArtifact = useCallback((artifact: PositionArtifactPreview) => {
if (!artifact.threadId || !artifact.path) { if (!artifact.threadId || !artifact.path) {
@@ -1315,6 +1382,10 @@ export default function PositionRoundtablePage({
}, []); }, []);
const handleSelectBuiltinStage = useCallback( const handleSelectBuiltinStage = useCallback(
(kind: ClosureStageKind) => { (kind: ClosureStageKind) => {
if (positionId !== DEFAULT_POSITION_ID) {
toast.info("收口智能体仅在测试分析岗中开放使用。");
return;
}
if (!allPositionDeliveriesDone) { if (!allPositionDeliveriesDone) {
toast.info("岗位产物还在汇集中;全部岗位完成 Markdown 交付后,即可开启方案总结。"); toast.info("岗位产物还在汇集中;全部岗位完成 Markdown 交付后,即可开启方案总结。");
return; return;
@@ -1330,7 +1401,7 @@ export default function PositionRoundtablePage({
setActiveBuiltinKind(kind); setActiveBuiltinKind(kind);
setBuiltinAutoStartRequestKey((current) => current + 1); setBuiltinAutoStartRequestKey((current) => current + 1);
}, },
[allPositionDeliveriesDone, closeSandbox, summaryDone], [allPositionDeliveriesDone, closeSandbox, positionId, summaryDone],
); );
const handleSelectPositionNode = useCallback( const handleSelectPositionNode = useCallback(
(nextNode: PositionRoundtableNode) => { (nextNode: PositionRoundtableNode) => {
@@ -1353,8 +1424,7 @@ export default function PositionRoundtablePage({
setSelectedAgentId(null); setSelectedAgentId(null);
setActiveNodeKey(null); setActiveNodeKey(null);
setRightPanelOpen(nextPositionId === DEFAULT_POSITION_ID); setRightPanelOpen(nextPositionId === DEFAULT_POSITION_ID);
setTaskPanelCollapsed(false); setLeftPanelCollapsed(false);
setIntentPanelCollapsed(false);
}, },
[closeSandbox], [closeSandbox],
); );
@@ -1490,17 +1560,72 @@ export default function PositionRoundtablePage({
> >
新建会话 新建会话
</TButton> </TButton>
{positionSession && positionSession.status !== "archived" && ( <DropdownMenu open={historyMenuOpen} onOpenChange={handleHistoryMenuOpenChange}>
<TButton <DropdownMenuTrigger asChild>
size="small" <TButton
variant="outline" size="small"
className="shrink-0 whitespace-nowrap" variant="outline"
icon={<Archive className="size-3.5" />} className="shrink-0 whitespace-nowrap"
onClick={handleArchiveSession} icon={<History className="size-3.5" />}
>
历史记录
</TButton>
</DropdownMenuTrigger>
<DropdownMenuContent
align="end"
className="w-[320px] rounded-[4px] border-slate-200 p-1.5 shadow-lg dark:border-[#2b8ffe]/50 dark:bg-[#032650]"
> >
归档 <DropdownMenuLabel className="px-2.5 py-2 text-xs font-semibold text-slate-700 dark:text-[#d9efff]">
</TButton> 当前任务的历史记录
)} </DropdownMenuLabel>
<DropdownMenuSeparator className="bg-slate-200 dark:bg-[#2b8ffe]/30" />
{historyLoading ? (
<div className="flex h-20 items-center justify-center gap-2 text-xs text-muted-foreground dark:text-[#8aa0c4]">
<Loader2 className="size-3.5 animate-spin" />
正在加载历史记录…
</div>
) : historySessions.length === 0 ? (
<div className="px-2.5 py-5 text-center text-xs text-muted-foreground dark:text-[#8aa0c4]">
暂无历史记录
</div>
) : (
<div className="max-h-80 overflow-y-auto py-0.5">
{historySessions.map((session) => {
const current = session.id === sessionIdFromUrl;
return (
<DropdownMenuItem
key={session.id}
onSelect={() => 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",
)}
>
<div className="min-w-0 flex-1">
<div className="flex min-w-0 items-center gap-2">
<span className="truncate text-xs font-semibold">
{desensitize(historySessionTitle(session))}
</span>
{current && (
<span className="shrink-0 rounded-[4px] bg-primary/10 px-1.5 py-0.5 text-[10px] font-semibold text-primary dark:bg-[#2b8ffe]/15 dark:text-[#a2e1f9]">
当前
</span>
)}
</div>
<div className="mt-1 flex items-center justify-between gap-2 text-[10px] text-muted-foreground dark:text-[#8aa0c4]">
<span>{historySessionStatus(session.status)}</span>
<span>{formatResultTime(session.updatedAt ?? session.createdAt, "未记录时间")}</span>
</div>
</div>
</DropdownMenuItem>
);
})}
</div>
)}
</DropdownMenuContent>
</DropdownMenu>
<span className="hidden shrink-0 whitespace-nowrap text-xs font-medium text-muted-foreground sm:inline"> <span className="hidden shrink-0 whitespace-nowrap text-xs font-medium text-muted-foreground sm:inline">
当前岗位 当前岗位
</span> </span>
@@ -1529,57 +1654,49 @@ export default function PositionRoundtablePage({
<aside <aside
aria-hidden={leftSidebarHidden || undefined} aria-hidden={leftSidebarHidden || undefined}
className={cn( className={cn(
"custom-scrollbar relative flex min-h-0 shrink-0 flex-col gap-3 overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm transition-[width,opacity,transform,padding,border-width] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none dark:rounded-none dark:border-0 dark:bg-transparent dark:text-[#c5cfe0] dark:shadow-none", "custom-scrollbar relative flex min-h-0 shrink-0 flex-col gap-3 overflow-y-auto rounded-2xl border border-slate-200 bg-white shadow-sm transition-[width,opacity,transform,padding,border-width] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none dark:rounded-none dark:border-0 dark:bg-transparent dark:text-[#c5cfe0] dark:shadow-none",
leftSidebarHidden leftSidebarHidden
? "w-0 -translate-x-4 !border-0 !p-0 opacity-0 pointer-events-none" ? "w-0 translate-x-4 !border-0 !p-0 opacity-0 pointer-events-none"
: "w-[360px] translate-x-0 p-3 opacity-100 dark:p-0", : "w-[360px] translate-x-0 p-3 opacity-100 dark:p-0",
)} )}
> >
<TaskInformationCard <TaskAnalysisPanel
task={task} task={task}
editable={!isArchivedSession} intent={frozenIntent ?? intent.intentReady}
onEdit={openTaskEditor} taskEditable={!isArchivedSession}
collapsed={taskPanelCollapsed} intentEditable={!isArchivedSession}
onCollapsedChange={setTaskPanelCollapsed} onEditTask={openTaskEditor}
/> onEditIntent={openIntentEditor}
collapsed={false}
{activePosition.isIntentPosition ? ( onCollapsedChange={setLeftPanelCollapsed}
isIntelligenceDeliveryOverview ? ( className="min-h-full"
<IntentUnderstandingCard >
intent={frozenIntent ?? intent.intentReady} {activePosition.isIntentPosition && !isIntelligenceDeliveryOverview && (
editable={!isArchivedSession}
onEdit={openIntentEditor}
collapsed={intentPanelCollapsed}
onCollapsedChange={setIntentPanelCollapsed}
/>
) : (
<BusinessChainSelector <BusinessChainSelector
chains={chains} chains={chains}
loading={chainsLoading} loading={chainsLoading}
value={selectedChainId} value={selectedChainId}
onChange={setSelectedChainId} onChange={setSelectedChainId}
onConfigure={openPositionConfig} onConfigure={openPositionConfig}
className="min-h-0 flex-1"
/> />
) )}
) : ( {(!activePosition.isIntentPosition || isIntelligenceDeliveryOverview) && (
<div className="contents">
<IntentUnderstandingCard
intent={frozenIntent ?? intent.intentReady}
editable={!isArchivedSession}
onEdit={openIntentEditor}
collapsed={intentPanelCollapsed}
onCollapsedChange={setIntentPanelCollapsed}
/>
<BusinessChainFlow <BusinessChainFlow
chain={activeChain} chain={activeChain}
nodes={positionSession?.nodes ?? []} nodes={positionSession?.nodes ?? []}
session={positionSession} session={positionSession}
activeNodeKey={activeNodeKey} activeNodeKey={activeNodeKey}
onSelectIntent={
activePosition.isIntentPosition ? openIntentFlowDetail : undefined
}
onSelectNode={openNodeFlowDetail} onSelectNode={openNodeFlowDetail}
onSelectSeat={openSeatFlowDetail}
onSelectBuiltin={handleSelectBuiltinStage} onSelectBuiltin={handleSelectBuiltinStage}
embedded
/> />
</div> )}
)} </TaskAnalysisPanel>
</aside> </aside>
{leftSidebarHidden && ( {leftSidebarHidden && (
@@ -1588,8 +1705,7 @@ export default function PositionRoundtablePage({
title="展开左侧任务与意图信息" title="展开左侧任务与意图信息"
aria-label="展开左侧任务与意图信息" aria-label="展开左侧任务与意图信息"
onClick={() => { onClick={() => {
setTaskPanelCollapsed(false); setLeftPanelCollapsed(false);
setIntentPanelCollapsed(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" 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({
</button> </button>
)} )}
<section className="relative flex min-w-[420px] flex-1 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none dark:backdrop-blur-md"> <section className="relative flex min-w-[420px] flex-1 flex-col overflow-hidden rounded-[4px] border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none dark:backdrop-blur-md">
{!isIntelligenceDeliveryOverview && !rightPanelOpen && !sandboxOpen && ( {!isIntelligenceDeliveryOverview && !rightPanelOpen && !sandboxOpen && (
<TButton <TButton
size="small" size="small"
@@ -1620,8 +1736,8 @@ export default function PositionRoundtablePage({
summaryDone={summaryDone} summaryDone={summaryDone}
selectedModel={selectedModel} selectedModel={selectedModel}
autoStartRequestKey={builtinAutoStartRequestKey} autoStartRequestKey={builtinAutoStartRequestKey}
builtinAgentOptions={builtinAgentOptions} builtinAgentOptions={showClosureAgents ? builtinAgentOptions : undefined}
onSelectBuiltinAgent={handleSelectBuiltinStage} onSelectBuiltinAgent={showClosureAgents ? handleSelectBuiltinStage : undefined}
onSessionUpdate={setPositionSession} onSessionUpdate={setPositionSession}
onConversationStart={() => setRightPanelOpen(false)} onConversationStart={() => setRightPanelOpen(false)}
onPreviewArtifact={handlePreviewArtifact} onPreviewArtifact={handlePreviewArtifact}
@@ -1641,8 +1757,8 @@ export default function PositionRoundtablePage({
agentName={activeNodeAgentName} agentName={activeNodeAgentName}
positionNodeOptions={currentPositionNodeOptions} positionNodeOptions={currentPositionNodeOptions}
onSelectPositionNode={handleSelectPositionNode} onSelectPositionNode={handleSelectPositionNode}
builtinAgentOptions={builtinAgentOptions} builtinAgentOptions={showClosureAgents ? builtinAgentOptions : undefined}
onSelectBuiltinAgent={handleSelectBuiltinStage} onSelectBuiltinAgent={showClosureAgents ? handleSelectBuiltinStage : undefined}
onNodeUpdate={handleNodeUpdate} onNodeUpdate={handleNodeUpdate}
onConversationStart={() => setRightPanelOpen(false)} onConversationStart={() => setRightPanelOpen(false)}
onPreviewArtifact={handlePreviewArtifact} onPreviewArtifact={handlePreviewArtifact}
@@ -1657,23 +1773,8 @@ export default function PositionRoundtablePage({
</div> </div>
</section> </section>
{isIntelligenceDeliveryOverview && !sandboxOpen && (
<aside className="flex w-[390px] shrink-0 overflow-hidden">
<BusinessChainFlow
chain={activeChain}
nodes={positionSession?.nodes ?? []}
session={positionSession}
activeNodeKey={activeNodeKey}
onSelectIntent={openIntentFlowDetail}
onSelectNode={openNodeFlowDetail}
onSelectSeat={openSeatFlowDetail}
onSelectBuiltin={handleSelectBuiltinStage}
className="h-full w-full"
/>
</aside>
)}
{activePosition.isIntentPosition && !isIntelligenceDeliveryOverview && rightPanelOpen && !sandboxOpen && ( {activePosition.isIntentPosition && !isIntelligenceDeliveryOverview && rightPanelOpen && !sandboxOpen && (
<aside className="flex w-[400px] shrink-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:text-[#c5cfe0] dark:shadow-none dark:backdrop-blur-md"> <aside className="flex w-[400px] shrink-0 flex-col overflow-hidden rounded-[4px] border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:text-[#c5cfe0] dark:shadow-none dark:backdrop-blur-md">
<RightWorkspace <RightWorkspace
selectedAgent={selectedAgent} selectedAgent={selectedAgent}
selectedAgentId={selectedAgentId} selectedAgentId={selectedAgentId}
@@ -1692,7 +1793,7 @@ export default function PositionRoundtablePage({
onSessionUpdate={setPositionSession} onSessionUpdate={setPositionSession}
onCollapse={() => setRightPanelOpen(false)} onCollapse={() => setRightPanelOpen(false)}
onPreviewArtifact={handlePreviewArtifact} onPreviewArtifact={handlePreviewArtifact}
onOpenBuiltinChat={handleSelectBuiltinStage} onOpenBuiltinChat={showClosureAgents ? handleSelectBuiltinStage : undefined}
tab={rightTab} tab={rightTab}
onTabChange={setRightTab} onTabChange={setRightTab}
/> />
@@ -1730,7 +1831,7 @@ export default function PositionRoundtablePage({
onSessionUpdate={setPositionSession} onSessionUpdate={setPositionSession}
onCollapse={() => setRightPanelOpen(false)} onCollapse={() => setRightPanelOpen(false)}
onPreviewArtifact={handlePreviewArtifact} onPreviewArtifact={handlePreviewArtifact}
onOpenBuiltinChat={handleSelectBuiltinStage} onOpenBuiltinChat={showClosureAgents ? handleSelectBuiltinStage : undefined}
tab={rightTab} tab={rightTab}
onTabChange={setRightTab} onTabChange={setRightTab}
/> />
@@ -1738,7 +1839,7 @@ export default function PositionRoundtablePage({
</TDrawer> </TDrawer>
)} )}
{sandboxOpen && sandboxArtifact && artifactsCtx && ( {sandboxOpen && sandboxArtifact && artifactsCtx && (
<aside className="flex w-[min(48vw,720px)] min-w-[440px] shrink-0 flex-col overflow-hidden rounded-2xl border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none"> <aside className="flex w-[min(48vw,720px)] min-w-[440px] shrink-0 flex-col overflow-hidden rounded-[4px] border border-slate-200 bg-white shadow-sm dark:border-[#2b8ffe]/45 dark:bg-[#032650]/80 dark:shadow-none">
<ThreadContext.Provider value={sandboxThreadContextValue}> <ThreadContext.Provider value={sandboxThreadContextValue}>
<ArtifactFileDetail <ArtifactFileDetail
className="min-h-0 flex-1" className="min-h-0 flex-1"
@@ -2635,9 +2736,11 @@ function DeliveryOverviewCard({ item }: { item: DeliveryOverviewItem }) {
function HoverEditButton({ function HoverEditButton({
title, title,
onClick, onClick,
revealClassName = "group-hover:pointer-events-auto group-hover:opacity-100",
}: { }: {
title: string; title: string;
onClick: () => void; onClick: () => void;
revealClassName?: string;
}) { }) {
return ( return (
<button <button
@@ -2645,13 +2748,179 @@ function HoverEditButton({
title={title} title={title}
aria-label={title} aria-label={title}
onClick={onClick} onClick={onClick}
className="pointer-events-none flex size-7 shrink-0 items-center justify-center rounded-[4px] text-[#45c3f3] opacity-0 transition-[opacity,color,background-color] duration-150 group-hover:pointer-events-auto group-hover:opacity-100 hover:bg-[#45c3f3]/10 hover:text-[#a2e1f9] focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#45c3f3]/65 motion-reduce:transition-none" className={cn(
"pointer-events-none flex size-7 shrink-0 items-center justify-center rounded-[4px] text-[#45c3f3] opacity-0 transition-[opacity,color,background-color] duration-150 hover:bg-[#45c3f3]/10 hover:text-[#a2e1f9] focus-visible:pointer-events-auto focus-visible:opacity-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#45c3f3]/65 motion-reduce:transition-none",
revealClassName,
)}
> >
<PencilLine className="size-3.5" strokeWidth={2.4} /> <PencilLine className="size-3.5" strokeWidth={2.4} />
</button> </button>
); );
} }
function TaskAnalysisPanel({
task,
intent,
taskEditable,
intentEditable,
onEditTask,
onEditIntent,
collapsed,
onCollapsedChange,
className,
children,
}: {
task: PositionTask;
intent: ReturnType<typeof useStep1Intent>["intentReady"];
taskEditable?: boolean;
intentEditable?: boolean;
onEditTask?: () => void;
onEditIntent?: () => void;
collapsed: boolean;
onCollapsedChange: (collapsed: boolean) => void;
className?: string;
children?: ReactNode;
}) {
return (
<section
className={cn(
"group relative flex shrink-0 flex-col overflow-hidden rounded-[4px] border border-[#ECE7E4] bg-white shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[rgba(43,143,254,0.6)] dark:bg-[#032650] dark:shadow-none",
className,
)}
>
<div className="relative flex h-10 items-center border-b border-slate-200 !bg-[linear-gradient(89.65deg,#0062d9_0.03%,#0062d900_99.97%)] px-4 dark:border-b-[#2b8ffe]/60 dark:after:absolute dark:after:inset-x-0 dark:after:bottom-[-1px] dark:after:h-px dark:after:bg-[#45c3f3]/85 dark:after:content-['']">
<span
aria-hidden="true"
className="mr-3 h-5 w-1 shrink-0 rounded-l-[2px] rounded-br-[2px] bg-[linear-gradient(167.47deg,#a2e1f9_0%,#45c3f3_100%)]"
/>
<h2 className="text-base font-[550] tracking-wide text-white">任务分析</h2>
<button
type="button"
title={collapsed ? "展开任务分析" : "收起任务分析"}
aria-label={collapsed ? "展开任务分析" : "收起任务分析"}
aria-expanded={!collapsed}
onClick={() => onCollapsedChange(!collapsed)}
className="relative z-10 ml-auto flex size-7 shrink-0 items-center justify-center rounded-[4px] transition-colors hover:bg-[#45c3f3]/10 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#45c3f3]/65"
>
<img src={expandLogo} alt="" className="size-4" />
</button>
</div>
<div
className={cn(
"grid min-h-0 flex-1 transition-[grid-template-rows,opacity] duration-300 ease-[cubic-bezier(0.22,1,0.36,1)] motion-reduce:transition-none",
collapsed ? "grid-rows-[0fr] opacity-0" : "grid-rows-[1fr] opacity-100",
)}
>
<div
className={cn(
"min-h-0",
children
? "flex flex-col"
: "custom-scrollbar overflow-y-auto",
)}
>
<div className={cn("space-y-4 p-4", children && "shrink-0")}>
<article className="group/task-details relative overflow-hidden rounded-[4px] bg-[rgba(0,98,217,0.1)] px-4 py-5">
{taskEditable && onEditTask && (
<div className="absolute right-2 top-2">
<HoverEditButton
title="编辑任务信息"
onClick={onEditTask}
revealClassName="group-hover/task-details:pointer-events-auto group-hover/task-details:opacity-100"
/>
</div>
)}
<h3
title={desensitize(task.title)}
className="pr-8 text-xl font-[550] leading-8 text-white"
>
<span className="line-clamp-1">{desensitize(task.title)}</span>
</h3>
<p
title={desensitize(task.description)}
className="mt-3 line-clamp-3 whitespace-pre-wrap text-sm leading-6 text-white"
>
{desensitize(task.description)}
</p>
<div className="mt-4 space-y-2 border-t border-[rgba(85,166,254,0.16)] pt-3 text-sm text-slate-600 dark:text-[#c5d7ee]">
<p className="flex min-w-0 items-center gap-2">
<FileText className="size-4 shrink-0 text-[#45c3f3]" strokeWidth={1.8} />
<span className="shrink-0 text-slate-500 dark:text-[#9ebfe2]">任务来源:</span>
<span className="truncate">{desensitize(task.source)}</span>
</p>
<p className="flex min-w-0 items-center gap-2">
<Clock3 className="size-4 shrink-0 text-[#45c3f3]" strokeWidth={1.8} />
<span className="shrink-0 text-slate-500 dark:text-[#9ebfe2]">下发时间:</span>
<span className="truncate">{task.issuedAt || "未设置"}</span>
</p>
</div>
</article>
<section className="group/intent-analysis">
<div className="relative flex h-10 items-center border-b border-slate-200 dark:border-[#2b8ffe]/60">
<span
aria-hidden="true"
className="mr-3 h-6 w-1 shrink-0 rounded-l-[2px] rounded-br-[2px] bg-[linear-gradient(167.47deg,#a2e1f9_0%,#45c3f3_100%)]"
/>
<h3 className="text-lg font-[550] tracking-wide text-slate-900 dark:text-white">意图分析</h3>
{intentEditable && onEditIntent && (
<div className="ml-auto">
<HoverEditButton
title="编辑意图理解"
onClick={onEditIntent}
revealClassName="group-hover/intent-analysis:pointer-events-auto group-hover/intent-analysis:opacity-100"
/>
</div>
)}
</div>
{intent ? (
<div className="space-y-3 py-4">
<div>
<p className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务意图</p>
<p className="mt-1 text-sm leading-6 text-slate-700 dark:text-[#f2f7ff]">
{desensitize(intent.objective)}
</p>
</div>
<div>
<p className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">关键约束</p>
<ul className="mt-1 list-disc space-y-1 pl-4 text-sm leading-6 text-slate-600 marker:text-slate-400 dark:text-[#d8e6f8] dark:marker:text-[#83b6ed]">
{intent.constraints.map((constraint, index) => (
<li key={`${constraint}-${index}`}>{desensitize(constraint)}</li>
))}
</ul>
</div>
{intent.assumptions && intent.assumptions.length > 0 && (
<div>
<p className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">关键假设</p>
<ul className="mt-1 list-disc space-y-1 pl-4 text-sm leading-6 text-slate-600 marker:text-slate-400 dark:text-[#d8e6f8] dark:marker:text-[#83b6ed]">
{intent.assumptions.map((assumption, index) => (
<li key={`${assumption}-${index}`}>{desensitize(assumption)}</li>
))}
</ul>
</div>
)}
</div>
) : (
<p className="py-4 text-sm leading-6 text-muted-foreground dark:text-[#d8e6f8]">
情报分析岗完成意图识别后,将在这里展示任务意图与关键约束。
</p>
)}
</section>
</div>
{children && (
<div className="flex min-h-0 flex-col px-4 pb-4">
{children}
</div>
)}
</div>
</div>
</section>
);
}
function TaskInformationCard({ function TaskInformationCard({
task, task,
editable, editable,
@@ -2667,12 +2936,12 @@ function TaskInformationCard({
}) { }) {
return ( return (
<section className="group relative shrink-0 overflow-hidden rounded-[4px] border border-[#ECE7E4] bg-white shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[rgba(43,143,254,0.6)] dark:bg-[#032650] dark:shadow-none"> <section className="group relative shrink-0 overflow-hidden rounded-[4px] border border-[#ECE7E4] bg-white shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[rgba(43,143,254,0.6)] dark:bg-[#032650] dark:shadow-none">
<div className="relative flex h-10 items-center border-b border-slate-200 bg-primary/10 px-4 dark:border-b-[#2b8ffe]/60 dark:bg-[linear-gradient(89.65deg,#0062d9_0.03%,rgba(0,98,217,0.78)_52%,rgba(0,98,217,0)_100%)] dark:after:absolute dark:after:inset-x-0 dark:after:bottom-[-1px] dark:after:h-px dark:after:bg-[#45c3f3]/85 dark:after:content-['']"> <div className="relative flex h-10 items-center border-b border-slate-200 !bg-[linear-gradient(89.65deg,#0062d9_0.03%,#0062d900_99.97%)] px-4 dark:border-b-[#2b8ffe]/60 dark:after:absolute dark:after:inset-x-0 dark:after:bottom-[-1px] dark:after:h-px dark:after:bg-[#45c3f3]/85 dark:after:content-['']">
<span <span
aria-hidden="true" aria-hidden="true"
className="mr-3 h-5 w-1 shrink-0 rounded-l-[2px] rounded-br-[2px] bg-[linear-gradient(167.47deg,#a2e1f9_0%,#45c3f3_100%)]" className="mr-3 h-5 w-1 shrink-0 rounded-l-[2px] rounded-br-[2px] bg-[linear-gradient(167.47deg,#a2e1f9_0%,#45c3f3_100%)]"
/> />
<h2 className="text-base font-semibold tracking-wide text-slate-900 dark:text-white">任务信息</h2> <h2 className="text-base font-[550] tracking-wide text-white">任务信息</h2>
<div className="relative z-10 ml-auto flex items-center gap-2"> <div className="relative z-10 ml-auto flex items-center gap-2">
{editable && onEdit && <HoverEditButton title="编辑任务信息" onClick={onEdit} />} {editable && onEdit && <HoverEditButton title="编辑任务信息" onClick={onEdit} />}
<button <button
@@ -2764,6 +3033,24 @@ function TaskInformationEditDialog({
placeholder="请输入任务描述" placeholder="请输入任务描述"
/> />
</label> </label>
<div className="grid grid-cols-2 gap-3">
<label className="block space-y-1.5">
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务来源</span>
<TInput
value={task.source}
onChange={(value) => onChange({ ...task, source: String(value) })}
placeholder="例如:本系统创建"
/>
</label>
<label className="block space-y-1.5">
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">下发时间</span>
<TInput
value={task.issuedAt}
onChange={(value) => onChange({ ...task, issuedAt: String(value) })}
placeholder="例如:2026-07-22 17:42:00"
/>
</label>
</div>
<label className="block space-y-1.5"> <label className="block space-y-1.5">
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务方向</span> <span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务方向</span>
<TInput <TInput
@@ -2792,46 +3079,31 @@ function BusinessChainSelector({
value, value,
onChange, onChange,
onConfigure, onConfigure,
className,
}: { }: {
chains: BusinessChain[]; chains: BusinessChain[];
loading: boolean; loading: boolean;
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;
onConfigure: (chainId: string) => void; onConfigure: (chainId: string) => void;
className?: string;
}) { }) {
const [expanded, setExpanded] = useState(true);
const [relationOpenId, setRelationOpenId] = useState<string | null>(null); const [relationOpenId, setRelationOpenId] = useState<string | null>(null);
return ( return (
<section className="rounded-2xl border border-[#ECE7E4] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/55 dark:shadow-none"> <section className={cn("group/business-chain flex min-h-0 flex-col", className)}>
<div className="flex items-center gap-2"> <div className="relative flex h-10 items-center border-b border-slate-200 dark:border-[#2b8ffe]/60">
<span className="flex size-5 items-center justify-center rounded-md bg-primary text-primary-foreground"> <span
<GitBranch className="size-3" strokeWidth={2.5} /> aria-hidden="true"
</span> className="mr-3 h-6 w-1 shrink-0 rounded-l-[2px] rounded-br-[2px] bg-[linear-gradient(167.47deg,#a2e1f9_0%,#45c3f3_100%)]"
<h2 className="text-sm font-semibold tracking-wide text-slate-900 dark:text-[#e8edf5]"> />
业务链 <h3 className="text-lg font-[550] tracking-wide text-slate-900 dark:text-white">业务链</h3>
</h2> <span className="ml-auto flex size-6 items-center justify-center rounded-[4px] bg-slate-100 text-xs font-semibold tabular-nums text-slate-500 dark:bg-[#0b386d] dark:text-[#83c1ff]">
<span className="ml-auto flex size-7 items-center justify-center rounded-full bg-neutral-100 text-sm font-bold tabular-nums text-neutral-500 dark:bg-[#0b386d] dark:text-[#83c1ff]">
{chains.length} {chains.length}
</span> </span>
<button
type="button"
title={expanded ? "收起业务链" : "展开业务链"}
aria-expanded={expanded}
onClick={() => setExpanded((current) => !current)}
className="flex size-7 items-center justify-center rounded-full border border-[#E6C9C4] bg-white text-neutral-400 transition-colors hover:border-primary/40 hover:text-primary dark:border-[#2b8ffe]/60 dark:bg-[#042b59] dark:text-[#83c1ff]"
>
<ChevronDown
className={cn(
"size-3.5 transition-transform duration-300",
expanded && "rotate-180",
)}
/>
</button>
</div> </div>
{expanded && ( <div className="business-chain-scrollbar min-h-0 flex-1 space-y-2.5 overflow-y-auto py-4 pr-1 custom-scrollbar">
<div className="mt-3 max-h-[340px] space-y-2.5 overflow-y-auto pr-1 custom-scrollbar">
{loading ? ( {loading ? (
<div className="flex h-20 items-center justify-center gap-2 text-xs text-slate-400 dark:text-[#8aa0c4]"> <div className="flex h-20 items-center justify-center gap-2 text-xs text-slate-400 dark:text-[#8aa0c4]">
<Loader2 className="size-4 animate-spin" /> <Loader2 className="size-4 animate-spin" />
@@ -2850,7 +3122,7 @@ function BusinessChainSelector({
<div <div
key={chain.id} key={chain.id}
className={cn( className={cn(
"overflow-hidden rounded-xl border transition-all duration-200", "overflow-hidden rounded-[4px] border transition-all duration-200",
selected selected
? "border-primary/30 bg-primary/5 ring-1 ring-primary/20 shadow-sm dark:border-[#2b8ffe] dark:bg-[#0b4fae]/35 dark:ring-[#2b8ffe]/30" ? "border-primary/30 bg-primary/5 ring-1 ring-primary/20 shadow-sm dark:border-[#2b8ffe] dark:bg-[#0b4fae]/35 dark:ring-[#2b8ffe]/30"
: "border-[#ECE7E4] bg-neutral-50/50 hover:border-neutral-300 dark:border-[#2b8ffe]/25 dark:bg-[#032650]/45 dark:hover:border-[#2b8ffe]/60", : "border-[#ECE7E4] bg-neutral-50/50 hover:border-neutral-300 dark:border-[#2b8ffe]/25 dark:bg-[#032650]/45 dark:hover:border-[#2b8ffe]/60",
@@ -2918,7 +3190,7 @@ function BusinessChainSelector({
current === chain.id ? null : chain.id, current === chain.id ? null : chain.id,
) )
} }
className="flex items-center justify-center gap-1 rounded-full border border-primary/15 bg-primary/5 px-3 py-1 text-[10.5px] font-semibold text-primary transition-colors hover:bg-primary/10 dark:border-[#2b8ffe]/40 dark:bg-[#2b8ffe]/10 dark:text-[#83c1ff]" className="flex items-center justify-center gap-1 rounded-[4px] border border-primary/15 bg-primary/5 px-3 py-1 text-[10.5px] font-semibold text-primary transition-colors hover:bg-primary/10 dark:border-[#2b8ffe]/40 dark:bg-[#2b8ffe]/10 dark:text-[#83c1ff]"
> >
<Layers3 className="size-3" /> <Layers3 className="size-3" />
<span>{relationOpen ? "收起关系图" : "展开关系图"}</span> <span>{relationOpen ? "收起关系图" : "展开关系图"}</span>
@@ -2945,7 +3217,7 @@ function BusinessChainSelector({
</p> </p>
)} )}
{relationOpen && ( {relationOpen && (
<div className="mt-2 flex flex-wrap items-center justify-center gap-1 rounded-xl border border-dashed border-[#E8D0CC]/70 bg-[#FAF6F5] p-2.5 dark:border-[#2b8ffe]/30 dark:bg-[#032650]/75"> <div className="mt-2 flex flex-wrap items-center justify-center gap-1 rounded-[4px] border border-dashed border-[#E8D0CC]/70 bg-[#FAF6F5] p-2.5 dark:border-[#2b8ffe]/30 dark:bg-[#032650]/75">
{stageGroups.flatMap((stage, stageIndex) => { {stageGroups.flatMap((stage, stageIndex) => {
const content = stage.seats.map((seat) => { const content = stage.seats.map((seat) => {
const avatarType = getAvatarTypeFor( const avatarType = getAvatarTypeFor(
@@ -2980,8 +3252,7 @@ function BusinessChainSelector({
); );
}) })
)} )}
</div> </div>
)}
</section> </section>
); );
} }
@@ -3006,12 +3277,12 @@ function IntentUnderstandingCard({
collapsed ? "flex-none" : "flex-1", collapsed ? "flex-none" : "flex-1",
)} )}
> >
<div className="relative flex h-10 shrink-0 items-center border-b border-slate-200 bg-primary/10 px-4 dark:border-b-[#2b8ffe]/60 dark:bg-[linear-gradient(89.65deg,#0062d9_0.03%,rgba(0,98,217,0.78)_52%,rgba(0,98,217,0)_100%)] dark:after:absolute dark:after:inset-x-0 dark:after:bottom-[-1px] dark:after:h-px dark:after:bg-[#45c3f3]/85 dark:after:content-['']"> <div className="relative flex h-10 shrink-0 items-center border-b border-slate-200 !bg-[linear-gradient(89.65deg,#0062d9_0.03%,#0062d900_99.97%)] px-4 dark:border-b-[#2b8ffe]/60 dark:after:absolute dark:after:inset-x-0 dark:after:bottom-[-1px] dark:after:h-px dark:after:bg-[#45c3f3]/85 dark:after:content-['']">
<span <span
aria-hidden="true" aria-hidden="true"
className="mr-3 h-5 w-1 shrink-0 rounded-l-[2px] rounded-br-[2px] bg-[linear-gradient(167.47deg,#a2e1f9_0%,#45c3f3_100%)]" className="mr-3 h-5 w-1 shrink-0 rounded-l-[2px] rounded-br-[2px] bg-[linear-gradient(167.47deg,#a2e1f9_0%,#45c3f3_100%)]"
/> />
<h2 className="text-base font-semibold tracking-wide text-slate-900 dark:text-white">意图理解</h2> <h2 className="text-base font-[550] tracking-wide text-white">意图理解</h2>
<div className="relative z-10 ml-auto flex items-center gap-2"> <div className="relative z-10 ml-auto flex items-center gap-2">
{editable && onEdit && <HoverEditButton title="编辑意图理解" onClick={onEdit} />} {editable && onEdit && <HoverEditButton title="编辑意图理解" onClick={onEdit} />}
<button <button
@@ -3171,6 +3442,7 @@ function BusinessChainFlow({
onSelectSeat, onSelectSeat,
onSelectBuiltin, onSelectBuiltin,
className, className,
embedded = false,
}: { }: {
chain: BusinessChain | null; chain: BusinessChain | null;
nodes: PositionRoundtableNode[]; nodes: PositionRoundtableNode[];
@@ -3181,22 +3453,36 @@ function BusinessChainFlow({
onSelectSeat?: (seat: BusinessChain["seats"][number], stageIndex: number, seatIndex: number) => void; onSelectSeat?: (seat: BusinessChain["seats"][number], stageIndex: number, seatIndex: number) => void;
onSelectBuiltin: (kind: "summary" | "action_plan") => void; onSelectBuiltin: (kind: "summary" | "action_plan") => void;
className?: string; className?: string;
embedded?: boolean;
}) { }) {
const [flowDialogOpen, setFlowDialogOpen] = useState(false); const [flowDialogOpen, setFlowDialogOpen] = useState(false);
return ( return (
<section <section
className={cn( className={cn(
"flex h-[540px] shrink-0 flex-col overflow-hidden rounded-2xl border border-[#ECE7E4] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/55 dark:shadow-none", "flex h-[540px] shrink-0 flex-col overflow-hidden rounded-[4px] border border-[#ECE7E4] bg-white p-4 shadow-[0_4px_20px_rgba(0,0,0,0.02)] dark:border-[#2b8ffe]/30 dark:bg-[#042b59]/55 dark:shadow-none",
embedded && "h-[540px] rounded-none border-0 bg-transparent p-0 shadow-none",
className, className,
)} )}
> >
<div className="mb-3 flex items-center justify-between gap-2"> <div
className={cn(
"mb-3 flex items-center justify-between gap-2",
embedded && "relative mb-4 h-10 shrink-0 border-b border-slate-200 dark:border-[#2b8ffe]/60",
)}
>
<div className="flex min-w-0 items-center gap-2"> <div className="flex min-w-0 items-center gap-2">
<span className="flex size-5 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground"> {embedded ? (
<Layers3 className="size-3" strokeWidth={2.5} /> <span
</span> aria-hidden="true"
<h2 className="truncate text-sm font-semibold tracking-wide text-slate-900 dark:text-[#e8edf5]"> className="h-6 w-1 shrink-0 rounded-l-[2px] rounded-br-[2px] bg-[linear-gradient(167.47deg,#a2e1f9_0%,#45c3f3_100%)]"
/>
) : (
<span className="flex size-5 shrink-0 items-center justify-center rounded-md bg-primary text-primary-foreground">
<Layers3 className="size-3" strokeWidth={2.5} />
</span>
)}
<h2 className={cn("truncate font-semibold tracking-wide text-slate-900 dark:text-[#e8edf5]", embedded ? "text-lg font-[550] dark:text-white" : "text-sm")}>
业务链条 业务链条
</h2> </h2>
</div> </div>
@@ -3359,7 +3645,7 @@ function ChainFlowCanvas({
onPointerCancel={onPanEnd} onPointerCancel={onPanEnd}
onDoubleClick={() => setView({ tx: 0, ty: 0 })} onDoubleClick={() => setView({ tx: 0, ty: 0 })}
className={cn( className={cn(
"overflow-hidden rounded-xl border border-slate-100 bg-slate-50/70 touch-none dark:border-[#2b8ffe]/25 dark:bg-[#032650]/75", "overflow-hidden rounded-[4px] border border-slate-100 bg-slate-50/70 touch-none dark:border-[#2b8ffe]/25 dark:bg-[#032650]/75",
panning ? "cursor-grabbing" : "cursor-grab", panning ? "cursor-grabbing" : "cursor-grab",
className, className,
)} )}
@@ -3639,7 +3925,7 @@ function RightWorkspace({
onSessionUpdate: (session: PositionRoundtableSession) => void; onSessionUpdate: (session: PositionRoundtableSession) => void;
onCollapse: () => void; onCollapse: () => void;
onPreviewArtifact: (artifact: PositionArtifactPreview) => void; onPreviewArtifact: (artifact: PositionArtifactPreview) => void;
onOpenBuiltinChat: (kind: ClosureStageKind) => void; onOpenBuiltinChat?: (kind: ClosureStageKind) => void;
tab: WorkspaceTab; tab: WorkspaceTab;
onTabChange: (tab: WorkspaceTab) => void; onTabChange: (tab: WorkspaceTab) => void;
}) { }) {
@@ -4116,7 +4402,7 @@ function AnalysisResults({
session: PositionRoundtableSession | null; session: PositionRoundtableSession | null;
onSessionUpdate: (session: PositionRoundtableSession) => void; onSessionUpdate: (session: PositionRoundtableSession) => void;
onPreviewArtifact: (artifact: PositionArtifactPreview) => void; onPreviewArtifact: (artifact: PositionArtifactPreview) => void;
onOpenBuiltinChat: (kind: ClosureStageKind) => void; onOpenBuiltinChat?: (kind: ClosureStageKind) => void;
}) { }) {
const [positionFilter, setPositionFilter] = useState("all"); const [positionFilter, setPositionFilter] = useState("all");
const [nodeFilter, setNodeFilter] = useState("all"); const [nodeFilter, setNodeFilter] = useState("all");
@@ -4154,6 +4440,10 @@ function AnalysisResults({
const seat = seatsByAgentId.get(node.agentId); const seat = seatsByAgentId.get(node.agentId);
const agentName = seat?.name ?? node.agentId; const agentName = seat?.name ?? node.agentId;
const artifactCount = node.artifactManifest.length; const artifactCount = node.artifactManifest.length;
const previewableArtifacts = node.artifactManifest.filter(
(artifact) => typeof artifact.path === "string" && artifact.path.trim().length > 0,
);
const hasPreviewableArtifact = previewableArtifacts.length > 0;
const productName = const productName =
node.status === "done" node.status === "done"
? artifactCount > 0 ? artifactCount > 0
@@ -4179,18 +4469,20 @@ function AnalysisResults({
: node.status === "rejected" : node.status === "rejected"
? "该产物已被驳回,请重新问答并交付。" ? "该产物已被驳回,请重新问答并交付。"
: "点击左侧流程图或右侧智能体发起问答。"); : "点击左侧流程图或右侧智能体发起问答。");
items.push({ if (!hasPreviewableArtifact) {
id: `${node.nodeKey}:delivery:${node.revision}`, items.push({
kind: "delivery", id: `${node.nodeKey}:delivery:${node.revision}`,
node, kind: "delivery",
agentName, node,
productName, agentName,
summary, productName,
positionId: node.positionId, summary,
updatedAt: node.updatedAt, positionId: node.positionId,
status: node.status, updatedAt: node.updatedAt,
}); status: node.status,
node.artifactManifest.forEach((artifact, index) => { });
}
previewableArtifacts.forEach((artifact, index) => {
const path = typeof artifact.path === "string" ? artifact.path : ""; const path = typeof artifact.path === "string" ? artifact.path : "";
const name = const name =
typeof artifact.name === "string" && artifact.name.trim() typeof artifact.name === "string" && artifact.name.trim()
@@ -4401,7 +4693,7 @@ function AnalysisResults({
onChange={(value) => setKindFilter(String(value) as "all" | ResultKind)} onChange={(value) => setKindFilter(String(value) as "all" | ResultKind)}
/> />
</div> </div>
{nodes.length > 0 && ( {onOpenBuiltinChat && nodes.length > 0 && (
<div className="rounded-xl border border-slate-200 bg-slate-50/70 p-3 dark:border-[#2b8ffe]/30 dark:bg-[#032650]/65"> <div className="rounded-xl border border-slate-200 bg-slate-50/70 p-3 dark:border-[#2b8ffe]/30 dark:bg-[#032650]/65">
<div className="mb-2 flex items-center justify-between gap-2"> <div className="mb-2 flex items-center justify-between gap-2">
<div className="min-w-0"> <div className="min-w-0">
+17
View File
@@ -599,6 +599,23 @@
padding-box; padding-box;
} }
/* 岗位会商左侧业务链:列表较窄,避免滚动条抢占卡片内容宽度。 */
.business-chain-scrollbar {
scrollbar-width: thin;
scrollbar-color: #2b8ffe transparent;
}
.business-chain-scrollbar::-webkit-scrollbar {
width: 6px;
height: 6px;
}
.business-chain-scrollbar::-webkit-scrollbar-thumb {
border: 0;
background: linear-gradient(180deg, rgb(85 166 254 / 0.82), rgb(0 98 217 / 0.9))
padding-box;
}
.workspace-shell { .workspace-shell {
background: #ffffff; background: #ffffff;
} }