样式修改

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>
{thread.isLoading ? (
<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}
>
{agentDisplayName} · 进行中
@@ -473,7 +473,7 @@ export function PositionNodeChat({
) : (
<TSelect
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}
value={node.nodeKey}
options={switchSelectOptions}
@@ -11,7 +11,8 @@ export interface PositionTask {
title: string;
description: string;
direction: string;
source: "mock";
source: string;
issuedAt: string;
}
export const MOCK_POSITION_TASKS: readonly PositionTask[] = [
@@ -21,7 +22,8 @@ export const MOCK_POSITION_TASKS: readonly PositionTask[] = [
description:
"围绕暑期活动的游泳夏令营做宣传",
direction: "暑期活动",
source: "mock",
source: "本系统创建",
issuedAt: "2026-07-22 17:42:00",
},
] as const;
@@ -51,6 +53,13 @@ export function taskFromPositionSnapshot(
title: value(["title", "taskName", "name"], fallback.title),
description: value(["description", "taskContent", "overview", "content"], fallback.description),
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 { 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<string, unknown> | 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<WorkspaceTab>("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<PositionRoundtableSession[]>([]);
const [historyLoading, setHistoryLoading] = useState(false);
const artifactsCtx = useArtifactsOptional();
const artifactsCtxRef = useRef(artifactsCtx);
const [sandboxArtifact, setSandboxArtifact] = useState<PositionArtifactPreview | null>(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<PositionBuiltinAgentKind | null>(null);
const [builtinAutoStartRequestKey, setBuiltinAutoStartRequestKey] = useState(0);
const builtinAgentOptions = useMemo<PositionBuiltinAgentSwitchOption[]>(
@@ -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({
>
新建会话
</TButton>
{positionSession && positionSession.status !== "archived" && (
<TButton
size="small"
variant="outline"
className="shrink-0 whitespace-nowrap"
icon={<Archive className="size-3.5" />}
onClick={handleArchiveSession}
<DropdownMenu open={historyMenuOpen} onOpenChange={handleHistoryMenuOpenChange}>
<DropdownMenuTrigger asChild>
<TButton
size="small"
variant="outline"
className="shrink-0 whitespace-nowrap"
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]"
>
归档
</TButton>
)}
<DropdownMenuLabel className="px-2.5 py-2 text-xs font-semibold text-slate-700 dark:text-[#d9efff]">
当前任务的历史记录
</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>
@@ -1529,57 +1654,49 @@ export default function PositionRoundtablePage({
<aside
aria-hidden={leftSidebarHidden || undefined}
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
? "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",
)}
>
<TaskInformationCard
<TaskAnalysisPanel
task={task}
editable={!isArchivedSession}
onEdit={openTaskEditor}
collapsed={taskPanelCollapsed}
onCollapsedChange={setTaskPanelCollapsed}
/>
{activePosition.isIntentPosition ? (
isIntelligenceDeliveryOverview ? (
<IntentUnderstandingCard
intent={frozenIntent ?? intent.intentReady}
editable={!isArchivedSession}
onEdit={openIntentEditor}
collapsed={intentPanelCollapsed}
onCollapsedChange={setIntentPanelCollapsed}
/>
) : (
intent={frozenIntent ?? intent.intentReady}
taskEditable={!isArchivedSession}
intentEditable={!isArchivedSession}
onEditTask={openTaskEditor}
onEditIntent={openIntentEditor}
collapsed={false}
onCollapsedChange={setLeftPanelCollapsed}
className="min-h-full"
>
{activePosition.isIntentPosition && !isIntelligenceDeliveryOverview && (
<BusinessChainSelector
chains={chains}
loading={chainsLoading}
value={selectedChainId}
onChange={setSelectedChainId}
onConfigure={openPositionConfig}
className="min-h-0 flex-1"
/>
)
) : (
<div className="contents">
<IntentUnderstandingCard
intent={frozenIntent ?? intent.intentReady}
editable={!isArchivedSession}
onEdit={openIntentEditor}
collapsed={intentPanelCollapsed}
onCollapsedChange={setIntentPanelCollapsed}
/>
)}
{(!activePosition.isIntentPosition || isIntelligenceDeliveryOverview) && (
<BusinessChainFlow
chain={activeChain}
nodes={positionSession?.nodes ?? []}
session={positionSession}
activeNodeKey={activeNodeKey}
onSelectIntent={
activePosition.isIntentPosition ? openIntentFlowDetail : undefined
}
onSelectNode={openNodeFlowDetail}
onSelectSeat={openSeatFlowDetail}
onSelectBuiltin={handleSelectBuiltinStage}
embedded
/>
</div>
)}
)}
</TaskAnalysisPanel>
</aside>
{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({
</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 && (
<TButton
size="small"
@@ -1620,8 +1736,8 @@ export default function PositionRoundtablePage({
summaryDone={summaryDone}
selectedModel={selectedModel}
autoStartRequestKey={builtinAutoStartRequestKey}
builtinAgentOptions={builtinAgentOptions}
onSelectBuiltinAgent={handleSelectBuiltinStage}
builtinAgentOptions={showClosureAgents ? builtinAgentOptions : undefined}
onSelectBuiltinAgent={showClosureAgents ? handleSelectBuiltinStage : undefined}
onSessionUpdate={setPositionSession}
onConversationStart={() => 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({
</div>
</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 && (
<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
selectedAgent={selectedAgent}
selectedAgentId={selectedAgentId}
@@ -1692,7 +1793,7 @@ export default function PositionRoundtablePage({
onSessionUpdate={setPositionSession}
onCollapse={() => setRightPanelOpen(false)}
onPreviewArtifact={handlePreviewArtifact}
onOpenBuiltinChat={handleSelectBuiltinStage}
onOpenBuiltinChat={showClosureAgents ? handleSelectBuiltinStage : undefined}
tab={rightTab}
onTabChange={setRightTab}
/>
@@ -1730,7 +1831,7 @@ export default function PositionRoundtablePage({
onSessionUpdate={setPositionSession}
onCollapse={() => setRightPanelOpen(false)}
onPreviewArtifact={handlePreviewArtifact}
onOpenBuiltinChat={handleSelectBuiltinStage}
onOpenBuiltinChat={showClosureAgents ? handleSelectBuiltinStage : undefined}
tab={rightTab}
onTabChange={setRightTab}
/>
@@ -1738,7 +1839,7 @@ export default function PositionRoundtablePage({
</TDrawer>
)}
{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}>
<ArtifactFileDetail
className="min-h-0 flex-1"
@@ -2635,9 +2736,11 @@ function DeliveryOverviewCard({ item }: { item: DeliveryOverviewItem }) {
function HoverEditButton({
title,
onClick,
revealClassName = "group-hover:pointer-events-auto group-hover:opacity-100",
}: {
title: string;
onClick: () => void;
revealClassName?: string;
}) {
return (
<button
@@ -2645,13 +2748,179 @@ function HoverEditButton({
title={title}
aria-label={title}
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} />
</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({
task,
editable,
@@ -2667,12 +2936,12 @@ function TaskInformationCard({
}) {
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">
<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
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-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">
{editable && onEdit && <HoverEditButton title="编辑任务信息" onClick={onEdit} />}
<button
@@ -2764,6 +3033,24 @@ function TaskInformationEditDialog({
placeholder="请输入任务描述"
/>
</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">
<span className="text-xs font-semibold text-slate-500 dark:text-[#9ebfe2]">任务方向</span>
<TInput
@@ -2792,46 +3079,31 @@ function BusinessChainSelector({
value,
onChange,
onConfigure,
className,
}: {
chains: BusinessChain[];
loading: boolean;
value: string;
onChange: (value: string) => void;
onConfigure: (chainId: string) => void;
className?: string;
}) {
const [expanded, setExpanded] = useState(true);
const [relationOpenId, setRelationOpenId] = useState<string | null>(null);
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">
<div className="flex items-center gap-2">
<span className="flex size-5 items-center justify-center rounded-md bg-primary text-primary-foreground">
<GitBranch className="size-3" strokeWidth={2.5} />
</span>
<h2 className="text-sm font-semibold tracking-wide text-slate-900 dark:text-[#e8edf5]">
业务链
</h2>
<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]">
<section className={cn("group/business-chain flex min-h-0 flex-col", className)}>
<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>
<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]">
{chains.length}
</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>
{expanded && (
<div className="mt-3 max-h-[340px] space-y-2.5 overflow-y-auto pr-1 custom-scrollbar">
<div className="business-chain-scrollbar min-h-0 flex-1 space-y-2.5 overflow-y-auto py-4 pr-1 custom-scrollbar">
{loading ? (
<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" />
@@ -2850,7 +3122,7 @@ function BusinessChainSelector({
<div
key={chain.id}
className={cn(
"overflow-hidden rounded-xl border transition-all duration-200",
"overflow-hidden rounded-[4px] border transition-all duration-200",
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-[#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,
)
}
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" />
<span>{relationOpen ? "收起关系图" : "展开关系图"}</span>
@@ -2945,7 +3217,7 @@ function BusinessChainSelector({
</p>
)}
{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) => {
const content = stage.seats.map((seat) => {
const avatarType = getAvatarTypeFor(
@@ -2980,8 +3252,7 @@ function BusinessChainSelector({
);
})
)}
</div>
)}
</div>
</section>
);
}
@@ -3006,12 +3277,12 @@ function IntentUnderstandingCard({
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
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-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">
{editable && onEdit && <HoverEditButton title="编辑意图理解" onClick={onEdit} />}
<button
@@ -3171,6 +3442,7 @@ function BusinessChainFlow({
onSelectSeat,
onSelectBuiltin,
className,
embedded = false,
}: {
chain: BusinessChain | null;
nodes: PositionRoundtableNode[];
@@ -3181,22 +3453,36 @@ function BusinessChainFlow({
onSelectSeat?: (seat: BusinessChain["seats"][number], stageIndex: number, seatIndex: number) => void;
onSelectBuiltin: (kind: "summary" | "action_plan") => void;
className?: string;
embedded?: boolean;
}) {
const [flowDialogOpen, setFlowDialogOpen] = useState(false);
return (
<section
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,
)}
>
<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">
<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="truncate text-sm font-semibold tracking-wide text-slate-900 dark:text-[#e8edf5]">
{embedded ? (
<span
aria-hidden="true"
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>
</div>
@@ -3359,7 +3645,7 @@ function ChainFlowCanvas({
onPointerCancel={onPanEnd}
onDoubleClick={() => setView({ tx: 0, ty: 0 })}
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",
className,
)}
@@ -3639,7 +3925,7 @@ function RightWorkspace({
onSessionUpdate: (session: PositionRoundtableSession) => void;
onCollapse: () => void;
onPreviewArtifact: (artifact: PositionArtifactPreview) => void;
onOpenBuiltinChat: (kind: ClosureStageKind) => void;
onOpenBuiltinChat?: (kind: ClosureStageKind) => void;
tab: WorkspaceTab;
onTabChange: (tab: WorkspaceTab) => void;
}) {
@@ -4116,7 +4402,7 @@ function AnalysisResults({
session: PositionRoundtableSession | null;
onSessionUpdate: (session: PositionRoundtableSession) => void;
onPreviewArtifact: (artifact: PositionArtifactPreview) => void;
onOpenBuiltinChat: (kind: ClosureStageKind) => void;
onOpenBuiltinChat?: (kind: ClosureStageKind) => void;
}) {
const [positionFilter, setPositionFilter] = useState("all");
const [nodeFilter, setNodeFilter] = useState("all");
@@ -4154,6 +4440,10 @@ function AnalysisResults({
const seat = seatsByAgentId.get(node.agentId);
const agentName = seat?.name ?? node.agentId;
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 =
node.status === "done"
? artifactCount > 0
@@ -4179,18 +4469,20 @@ function AnalysisResults({
: node.status === "rejected"
? "该产物已被驳回,请重新问答并交付。"
: "点击左侧流程图或右侧智能体发起问答。");
items.push({
id: `${node.nodeKey}:delivery:${node.revision}`,
kind: "delivery",
node,
agentName,
productName,
summary,
positionId: node.positionId,
updatedAt: node.updatedAt,
status: node.status,
});
node.artifactManifest.forEach((artifact, index) => {
if (!hasPreviewableArtifact) {
items.push({
id: `${node.nodeKey}:delivery:${node.revision}`,
kind: "delivery",
node,
agentName,
productName,
summary,
positionId: node.positionId,
updatedAt: node.updatedAt,
status: node.status,
});
}
previewableArtifacts.forEach((artifact, index) => {
const path = typeof artifact.path === "string" ? artifact.path : "";
const name =
typeof artifact.name === "string" && artifact.name.trim()
@@ -4401,7 +4693,7 @@ function AnalysisResults({
onChange={(value) => setKindFilter(String(value) as "all" | ResultKind)}
/>
</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="mb-2 flex items-center justify-between gap-2">
<div className="min-w-0">
+17
View File
@@ -599,6 +599,23 @@
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 {
background: #ffffff;
}