feat(会商第三步): 恢复普通会商完整成果并隔离8BF报告模式
- 为普通多智能体会商恢复报告完成后的结构化流程图、最终方案展示、经典结果绘制和大屏多页,并保持三个生成流独立并行。 - 8BF 入口显式采用 report-only 模式:只生成和编辑 Markdown 总结报告,不再启动或露出流程图、结果绘制及大屏功能。 - 结构化流程图增加抽取、校验、暂停、失败重试和歧义选择交互;报告完成后状态文案准确切换,自动滚动定位到抽取与校验进度。 - 为总结报告、经典 HTML 绘制和大屏虚拟 write_file 补齐完成结果及历史恢复处理,避免并行分支或旧草稿让完成产物误显示为生成中。 - 最终方案展示中的已落盘文件直接按完成态预览,并恢复 MD、HTML 等文件类型徽标,不再被提交按钮覆盖。
This commit is contained in:
1 parent
2254841822
commit
fbf9031e25
8 files changed
+469
-76
No files matched your search
@@ -273,6 +273,7 @@ export default function EightBfRoundtablePage({ compactChrome = false }: { compa
|
|||||||
hideHeaderSaveDraft
|
hideHeaderSaveDraft
|
||||||
hideHeaderSettings
|
hideHeaderSettings
|
||||||
newTaskLabel="新建会话"
|
newTaskLabel="新建会话"
|
||||||
|
step3Mode="report-only"
|
||||||
hideDefaultStep1RightSidebar
|
hideDefaultStep1RightSidebar
|
||||||
step2HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 多智能体会商`}
|
step2HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 多智能体会商`}
|
||||||
step3HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 方案总结`}
|
step3HeaderTitle={`${configuredChain?.title || "8BF 业务链"} · 方案总结`}
|
||||||
|
|||||||
@@ -61,6 +61,8 @@ interface SeatDelivery {
|
|||||||
interface HighFidelityReportProps {
|
interface HighFidelityReportProps {
|
||||||
useStep1Chrome?: boolean;
|
useStep1Chrome?: boolean;
|
||||||
headerTitle?: string;
|
headerTitle?: string;
|
||||||
|
/** `report-only` is used by 8BF; normal roundtables retain every Step 3 view. */
|
||||||
|
step3Mode?: "full" | "report-only";
|
||||||
threadIds: ThreadIdMap | null;
|
threadIds: ThreadIdMap | null;
|
||||||
coordinatorName: string | null;
|
coordinatorName: string | null;
|
||||||
/** The most recent leader-agent text — rendered as the final consensus. */
|
/** The most recent leader-agent text — rendered as the final consensus. */
|
||||||
@@ -121,9 +123,26 @@ function formatGeneratedAt(d: Date): string {
|
|||||||
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** 最终成果区打开的是已落盘文件,不走实时流;独立给工具栏一个稳定的文件类型徽标。 */
|
||||||
|
function artifactTypeLabel(fileName: string): string {
|
||||||
|
const normalized = fileName.trim().toLowerCase();
|
||||||
|
if (/\.(md|markdown)$/.test(normalized)) return "MD";
|
||||||
|
if (/\.html?$/.test(normalized)) return "HTML";
|
||||||
|
if (/\.(mmd|mermaid)$/.test(normalized)) return "MMD";
|
||||||
|
if (/\.json$/.test(normalized)) return "JSON";
|
||||||
|
if (/\.csv$/.test(normalized)) return "CSV";
|
||||||
|
if (/\.ya?ml$/.test(normalized)) return "YAML";
|
||||||
|
if (/\.tsx?$/.test(normalized)) return "TS";
|
||||||
|
if (/\.js$/.test(normalized)) return "JS";
|
||||||
|
if (/\.py$/.test(normalized)) return "PY";
|
||||||
|
if (/\.txt$/.test(normalized)) return "TXT";
|
||||||
|
return "FILE";
|
||||||
|
}
|
||||||
|
|
||||||
export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
||||||
useStep1Chrome = false,
|
useStep1Chrome = false,
|
||||||
headerTitle,
|
headerTitle,
|
||||||
|
step3Mode = "full",
|
||||||
threadIds,
|
threadIds,
|
||||||
coordinatorName,
|
coordinatorName,
|
||||||
consensusText,
|
consensusText,
|
||||||
@@ -156,7 +175,9 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
return `圆桌总控结论-${short}`;
|
return `圆桌总控结论-${short}`;
|
||||||
}, [intentReady?.objective]);
|
}, [intentReady?.objective]);
|
||||||
|
|
||||||
// 第 3 步只保留「总结报告」工作台;旧的最终方案展示 / HTML 绘制入口不再开放。
|
const hasFullStep3Outputs = step3Mode === "full";
|
||||||
|
// 第 3 步主页为总结报告。普通会商可继续打开最终方案、经典可视化和大屏多页;
|
||||||
|
// 8BF 明确停在报告工作台,不启动或露出这些后续交付。
|
||||||
const [summaryMode, setSummaryMode] = useState(true);
|
const [summaryMode, setSummaryMode] = useState(true);
|
||||||
const [drawMode, setDrawMode] = useState(false);
|
const [drawMode, setDrawMode] = useState(false);
|
||||||
// 文件沙箱(2026-06-13 改内联):点文件卡片在右侧滑入沙箱面板、左侧席位栏收起
|
// 文件沙箱(2026-06-13 改内联):点文件卡片在右侧滑入沙箱面板、左侧席位栏收起
|
||||||
@@ -398,6 +419,9 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
const fileSandboxVirtualUrl = fileSandboxTarget
|
const fileSandboxVirtualUrl = fileSandboxTarget
|
||||||
? `write-file:${fileSandboxStubPath}?message_id=hfr-file-stub&tool_call_id=tc-hfr-0`
|
? `write-file:${fileSandboxStubPath}?message_id=hfr-file-stub&tool_call_id=tc-hfr-0`
|
||||||
: "";
|
: "";
|
||||||
|
const fileSandboxTypeLabel = fileSandboxTarget
|
||||||
|
? artifactTypeLabel(fileSandboxTarget.file.name)
|
||||||
|
: "FILE";
|
||||||
const fileSandboxThreadStub = useMemo(() => {
|
const fileSandboxThreadStub = useMemo(() => {
|
||||||
const messages =
|
const messages =
|
||||||
fileSandboxTarget && fileSandboxContent != null
|
fileSandboxTarget && fileSandboxContent != null
|
||||||
@@ -414,6 +438,14 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
// 最终产物仅借用 write-file 虚拟 URL 来复用预览器;内容已经通过下载接口完整读回,
|
||||||
|
// 不是流式写入。因此要同时提供 tool result,避免 ArtifactFileDetail 误显示「正在生成」。
|
||||||
|
{
|
||||||
|
id: "hfr-file-tool-result",
|
||||||
|
type: "tool",
|
||||||
|
content: "",
|
||||||
|
tool_call_id: "tc-hfr-0",
|
||||||
|
},
|
||||||
]
|
]
|
||||||
: [];
|
: [];
|
||||||
return {
|
return {
|
||||||
@@ -434,7 +466,7 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
if (summaryMode) {
|
if (summaryMode || !hasFullStep3Outputs) {
|
||||||
return (
|
return (
|
||||||
<Step3SummaryMode
|
<Step3SummaryMode
|
||||||
useStep1Chrome={useStep1Chrome}
|
useStep1Chrome={useStep1Chrome}
|
||||||
@@ -448,16 +480,19 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
intentReady={intentReady}
|
intentReady={intentReady}
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
availableModels={availableModels}
|
availableModels={availableModels}
|
||||||
|
modelsLoading={modelsLoading}
|
||||||
setSelectedModel={setSelectedModel}
|
setSelectedModel={setSelectedModel}
|
||||||
coordinatorPrompt={coordinatorPrompt}
|
coordinatorPrompt={coordinatorPrompt}
|
||||||
savedSummary={savedSummary}
|
savedSummary={savedSummary}
|
||||||
summary={summaryFlow}
|
summary={summaryFlow}
|
||||||
|
enableFlow={hasFullStep3Outputs}
|
||||||
|
onViewStatic={hasFullStep3Outputs ? () => setSummaryMode(false) : undefined}
|
||||||
taskArtifactSubmission={taskArtifactSubmission}
|
taskArtifactSubmission={taskArtifactSubmission}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (drawMode) {
|
if (drawMode && hasFullStep3Outputs) {
|
||||||
return (
|
return (
|
||||||
<Step3ReportMode
|
<Step3ReportMode
|
||||||
threadIds={threadIds}
|
threadIds={threadIds}
|
||||||
@@ -1043,21 +1078,26 @@ export const HighFidelityReport: React.FC<HighFidelityReportProps> = ({
|
|||||||
allowVirtualMarkdownEdit
|
allowVirtualMarkdownEdit
|
||||||
headerLeadingActionBare
|
headerLeadingActionBare
|
||||||
headerLeadingAction={(
|
headerLeadingAction={(
|
||||||
<TaskArtifactSubmitButton
|
<div className="flex items-center gap-1.5">
|
||||||
compact
|
<span className="bg-background text-muted-foreground inline-flex h-7 shrink-0 items-center rounded-full border border-black/6 px-2.5 text-[10px] font-semibold tracking-[0.16em] dark:border-white/8">
|
||||||
controller={taskArtifactSubmission}
|
{fileSandboxTypeLabel}
|
||||||
target={taskArtifactSubmission?.targetFor({
|
</span>
|
||||||
threadId: fileSandboxTarget.threadId,
|
<TaskArtifactSubmitButton
|
||||||
path: fileSandboxTarget.file.path,
|
compact
|
||||||
name: fileSandboxTarget.file.name,
|
controller={taskArtifactSubmission}
|
||||||
mimeType: fileSandboxTarget.file.mimeType,
|
target={taskArtifactSubmission?.targetFor({
|
||||||
}) ?? {
|
threadId: fileSandboxTarget.threadId,
|
||||||
threadId: fileSandboxTarget.threadId,
|
path: fileSandboxTarget.file.path,
|
||||||
path: fileSandboxTarget.file.path,
|
name: fileSandboxTarget.file.name,
|
||||||
name: fileSandboxTarget.file.name,
|
mimeType: fileSandboxTarget.file.mimeType,
|
||||||
mimeType: fileSandboxTarget.file.mimeType,
|
}) ?? {
|
||||||
}}
|
threadId: fileSandboxTarget.threadId,
|
||||||
/>
|
path: fileSandboxTarget.file.path,
|
||||||
|
name: fileSandboxTarget.file.name,
|
||||||
|
mimeType: fileSandboxTarget.file.mimeType,
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</ThreadContext.Provider>
|
</ThreadContext.Provider>
|
||||||
|
|||||||
@@ -26,6 +26,8 @@ export interface Step3PanelProps {
|
|||||||
/** 8BF 第三步对话区与第一、二步共用深蓝顶栏;默认不改变原会商。 */
|
/** 8BF 第三步对话区与第一、二步共用深蓝顶栏;默认不改变原会商。 */
|
||||||
useStep1Chrome?: boolean;
|
useStep1Chrome?: boolean;
|
||||||
headerTitle?: string;
|
headerTitle?: string;
|
||||||
|
/** 普通会商提供完整交付;8BF 仅保留 Markdown 总结报告。 */
|
||||||
|
step3Mode?: "full" | "report-only";
|
||||||
threadIds: ThreadIdMap | null;
|
threadIds: ThreadIdMap | null;
|
||||||
coordinatorName: string | null;
|
coordinatorName: string | null;
|
||||||
consensusText: string;
|
consensusText: string;
|
||||||
@@ -65,6 +67,7 @@ export function Step3Panel(props: Step3PanelProps) {
|
|||||||
const {
|
const {
|
||||||
useStep1Chrome = false,
|
useStep1Chrome = false,
|
||||||
headerTitle,
|
headerTitle,
|
||||||
|
step3Mode = "full",
|
||||||
threadIds,
|
threadIds,
|
||||||
coordinatorName,
|
coordinatorName,
|
||||||
consensusText,
|
consensusText,
|
||||||
@@ -115,6 +118,7 @@ export function Step3Panel(props: Step3PanelProps) {
|
|||||||
<HighFidelityReport
|
<HighFidelityReport
|
||||||
useStep1Chrome={useStep1Chrome}
|
useStep1Chrome={useStep1Chrome}
|
||||||
headerTitle={headerTitle}
|
headerTitle={headerTitle}
|
||||||
|
step3Mode={step3Mode}
|
||||||
threadIds={threadIds}
|
threadIds={threadIds}
|
||||||
coordinatorName={coordinatorName}
|
coordinatorName={coordinatorName}
|
||||||
consensusText={consensusText}
|
consensusText={consensusText}
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { createPortal } from "react-dom";
|
|||||||
import ReactMarkdown from "react-markdown";
|
import ReactMarkdown from "react-markdown";
|
||||||
import remarkGfm from "remark-gfm";
|
import remarkGfm from "remark-gfm";
|
||||||
import {
|
import {
|
||||||
|
ArrowRight,
|
||||||
CheckCircle,
|
CheckCircle,
|
||||||
Download,
|
Download,
|
||||||
Eye,
|
Eye,
|
||||||
@@ -26,6 +27,7 @@ import {
|
|||||||
RefreshCw,
|
RefreshCw,
|
||||||
SendHorizontal,
|
SendHorizontal,
|
||||||
Sparkles,
|
Sparkles,
|
||||||
|
Workflow,
|
||||||
X,
|
X,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
|
||||||
@@ -42,6 +44,12 @@ import {
|
|||||||
} from "@/roundtable-planning/api/multi-agent";
|
} from "@/roundtable-planning/api/multi-agent";
|
||||||
import { ArtifactPreviewModal } from "@/roundtable-planning/components/ArtifactPreviewModal";
|
import { ArtifactPreviewModal } from "@/roundtable-planning/components/ArtifactPreviewModal";
|
||||||
import { ConsensusDocxDownloadButton } from "@/roundtable-planning/components/ConsensusDocxDownloadButton";
|
import { ConsensusDocxDownloadButton } from "@/roundtable-planning/components/ConsensusDocxDownloadButton";
|
||||||
|
import { FlowGraphPanel } from "@/roundtable-planning/components/FlowGraphPanel";
|
||||||
|
import {
|
||||||
|
detectFlowIntent,
|
||||||
|
extractFlowGraph,
|
||||||
|
FLOW_FENCE_OPEN,
|
||||||
|
} from "@/roundtable-planning/lib/flow-graph";
|
||||||
import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToBottomButton";
|
import { ScrollToBottomButton } from "@/roundtable-planning/components/ScrollToBottomButton";
|
||||||
import { Step2Message } from "@/roundtable-planning/components/Step2Panel";
|
import { Step2Message } from "@/roundtable-planning/components/Step2Panel";
|
||||||
import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout";
|
import { Step2SandboxLayout } from "@/roundtable-planning/components/Step2SandboxLayout";
|
||||||
@@ -74,6 +82,7 @@ interface Step3SummaryModeProps {
|
|||||||
intentReady: ReportIntent | null;
|
intentReady: ReportIntent | null;
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
availableModels: ReadonlyArray<ModelOption>;
|
availableModels: ReadonlyArray<ModelOption>;
|
||||||
|
modelsLoading: boolean;
|
||||||
setSelectedModel: (model: string) => void;
|
setSelectedModel: (model: string) => void;
|
||||||
/** 业务链写给总控的编排提示 —— 注入总结智能体作为最高优先级口径。 */
|
/** 业务链写给总控的编排提示 —— 注入总结智能体作为最高优先级口径。 */
|
||||||
coordinatorPrompt?: string | null;
|
coordinatorPrompt?: string | null;
|
||||||
@@ -81,6 +90,10 @@ interface Step3SummaryModeProps {
|
|||||||
savedSummary?: DraftStep3SummarySnapshot | null;
|
savedSummary?: DraftStep3SummarySnapshot | null;
|
||||||
/** 页面层常驻的总结生成流(2026-06-13 提升:切走不断流,进入本视图续看)。 */
|
/** 页面层常驻的总结生成流(2026-06-13 提升:切走不断流,进入本视图续看)。 */
|
||||||
summary: UseStep3SummaryReturn;
|
summary: UseStep3SummaryReturn;
|
||||||
|
/** 普通会商保留流程图交付;8BF 为 false。 */
|
||||||
|
enableFlow?: boolean;
|
||||||
|
/** 普通会商进入完整的最终方案展示;8BF 不提供该入口。 */
|
||||||
|
onViewStatic?: () => void;
|
||||||
taskArtifactSubmission?: TaskArtifactSubmissionController;
|
taskArtifactSubmission?: TaskArtifactSubmissionController;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,10 +111,13 @@ export function Step3SummaryMode({
|
|||||||
intentReady,
|
intentReady,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
availableModels,
|
availableModels,
|
||||||
|
modelsLoading,
|
||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
coordinatorPrompt,
|
coordinatorPrompt,
|
||||||
savedSummary,
|
savedSummary,
|
||||||
summary,
|
summary,
|
||||||
|
enableFlow = false,
|
||||||
|
onViewStatic,
|
||||||
taskArtifactSubmission,
|
taskArtifactSubmission,
|
||||||
}: Step3SummaryModeProps) {
|
}: Step3SummaryModeProps) {
|
||||||
void coordinatorPrompt; // 选项已随 hook 提升到页面层,这里仅保留 prop 兼容
|
void coordinatorPrompt; // 选项已随 hook 提升到页面层,这里仅保留 prop 兼容
|
||||||
@@ -135,6 +151,36 @@ export function Step3SummaryMode({
|
|||||||
|
|
||||||
const { open: artifactsOpen } = useArtifacts();
|
const { open: artifactsOpen } = useArtifacts();
|
||||||
|
|
||||||
|
// 普通会商的结构化流程图:优先使用后端抽取+校验的结果,兼容旧草稿里 flow-json。
|
||||||
|
const flowGraph = useMemo(() => {
|
||||||
|
if (!enableFlow) return null;
|
||||||
|
if (summary.flowPhase === "done" && summary.flowGraph) {
|
||||||
|
return { graph: summary.flowGraph, streaming: false };
|
||||||
|
}
|
||||||
|
if (summary.flowPhase === "extracting" || summary.flowPhase === "validating") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
for (let index = dialogues.length - 1; index >= 0; index -= 1) {
|
||||||
|
const dialogue = dialogues[index];
|
||||||
|
if (dialogue.avatarType === "human") continue;
|
||||||
|
const graph = extractFlowGraph(dialogue.content ?? "");
|
||||||
|
if (graph && (dialogue.content ?? "").includes(FLOW_FENCE_OPEN)) {
|
||||||
|
return { graph, streaming: dialogue.streaming === true };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}, [dialogues, enableFlow, summary.flowGraph, summary.flowPhase]);
|
||||||
|
|
||||||
|
const [flowView, setFlowView] = useState(false);
|
||||||
|
const flowAutoOpenedRef = useRef(false);
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enableFlow || !flowGraph || flowGraph.graph.nodes.length === 0) return;
|
||||||
|
if (flowAutoOpenedRef.current) return;
|
||||||
|
flowAutoOpenedRef.current = true;
|
||||||
|
// 自动生成的图优先打开;若用户已在报告沙箱内,保留当前阅读位置并露出切换入口。
|
||||||
|
if (summary.autoFlow || !artifactsOpen) setFlowView(true);
|
||||||
|
}, [artifactsOpen, enableFlow, flowGraph, summary.autoFlow]);
|
||||||
|
|
||||||
// 沙箱里已写出的 Markdown 总结报告。
|
// 沙箱里已写出的 Markdown 总结报告。
|
||||||
const mdArtifact = useMemo(() => {
|
const mdArtifact = useMemo(() => {
|
||||||
for (const [threadId, msgs] of Object.entries(seatStubMessages)) {
|
for (const [threadId, msgs] of Object.entries(seatStubMessages)) {
|
||||||
@@ -156,16 +202,22 @@ export function Step3SummaryMode({
|
|||||||
return null;
|
return null;
|
||||||
}, [seatStubMessages]);
|
}, [seatStubMessages]);
|
||||||
|
|
||||||
const handleAsk = (message: string, files?: File[]) => ask(message, files);
|
const handleAsk = (message: string, files?: File[]) => {
|
||||||
|
if (enableFlow && detectFlowIntent(message)) {
|
||||||
|
flowAutoOpenedRef.current = true;
|
||||||
|
setFlowView(true);
|
||||||
|
}
|
||||||
|
ask(message, files);
|
||||||
|
};
|
||||||
|
|
||||||
// 报告生成过程(思考、文字说明、文件写入步骤)交由 Step2Message 原样渲染,
|
// 报告生成过程(思考、文字说明、文件写入步骤)交由 Step2Message 原样渲染,
|
||||||
// 保持与第二步一致的流式过程可见性。
|
// 保持与第二步一致的流式过程可见性。
|
||||||
const displayDialogues = dialogues;
|
const displayDialogues = dialogues;
|
||||||
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
const [leftCollapsed, setLeftCollapsed] = useState(false);
|
||||||
// 右侧沙箱露出时自动收起左栏,关闭时复原——边沿触发,手动切换不回弹。
|
// 右侧沙箱或流程图露出时自动收起左栏,关闭时复原——边沿触发,手动切换不回弹。
|
||||||
// 初值固定为 false:这样即使「进入/续看时沙箱已是打开态」(rightPanelVisible 一上来就为 true),
|
// 初值固定为 false:这样即使「进入/续看时沙箱已是打开态」(rightPanelVisible 一上来就为 true),
|
||||||
// 首个 effect 也能识别 false→true 的边沿并收起左栏(修此前已打开时左栏不收起的问题)。
|
// 首个 effect 也能识别 false→true 的边沿并收起左栏(修此前已打开时左栏不收起的问题)。
|
||||||
const rightPanelVisible = artifactsOpen;
|
const rightPanelVisible = artifactsOpen || (!!flowGraph && flowView);
|
||||||
const prevRightPanelVisibleRef = useRef(false);
|
const prevRightPanelVisibleRef = useRef(false);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (rightPanelVisible !== prevRightPanelVisibleRef.current) {
|
if (rightPanelVisible !== prevRightPanelVisibleRef.current) {
|
||||||
@@ -195,16 +247,23 @@ export function Step3SummaryMode({
|
|||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
const isFlowRunning =
|
||||||
|
enableFlow &&
|
||||||
|
(summary.flowPhase === "extracting" || summary.flowPhase === "validating");
|
||||||
const isStreaming =
|
const isStreaming =
|
||||||
phase === "creating" ||
|
phase === "creating" ||
|
||||||
phase === "streaming" ||
|
phase === "streaming" ||
|
||||||
|
isFlowRunning ||
|
||||||
dialogues.some((d) => d.streaming === true);
|
dialogues.some((d) => d.streaming === true);
|
||||||
const activeSeatMessages = activeArtifactThreadId
|
const activeSeatMessages = activeArtifactThreadId
|
||||||
? seatStubMessages[activeArtifactThreadId]
|
? seatStubMessages[activeArtifactThreadId]
|
||||||
: undefined;
|
: undefined;
|
||||||
|
|
||||||
const statusLabel =
|
const statusLabel = isFlowRunning
|
||||||
phase === "creating"
|
? summary.flowPhase === "extracting"
|
||||||
|
? "结构化流程图抽取中…"
|
||||||
|
: "流程图校验 / 修复中…"
|
||||||
|
: phase === "creating"
|
||||||
? "正在初始化总结线程…"
|
? "正在初始化总结线程…"
|
||||||
: phase === "streaming"
|
: phase === "streaming"
|
||||||
? "方案总结报告智能体撰写中…"
|
? "方案总结报告智能体撰写中…"
|
||||||
@@ -225,7 +284,7 @@ export function Step3SummaryMode({
|
|||||||
<Sparkles className="w-5 h-5 text-primary" />
|
<Sparkles className="w-5 h-5 text-primary" />
|
||||||
<h3 className="text-sm font-black text-slate-900 tracking-tight">方案总结 · 报告</h3>
|
<h3 className="text-sm font-black text-slate-900 tracking-tight">方案总结 · 报告</h3>
|
||||||
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold text-primary bg-[var(--brand-200)]/60 border border-red-200/50 px-2 py-0.5 rounded-full">
|
<span className="inline-flex items-center gap-1.5 text-[10px] font-bold text-primary bg-[var(--brand-200)]/60 border border-red-200/50 px-2 py-0.5 rounded-full">
|
||||||
{(phase === "creating" || phase === "streaming") && (
|
{isStreaming && (
|
||||||
<Loader2 className="w-3 h-3 animate-spin" />
|
<Loader2 className="w-3 h-3 animate-spin" />
|
||||||
)}
|
)}
|
||||||
{phase === "paused" && <Pause className="w-3 h-3 text-amber-600" />}
|
{phase === "paused" && <Pause className="w-3 h-3 text-amber-600" />}
|
||||||
@@ -237,12 +296,22 @@ export function Step3SummaryMode({
|
|||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => redraw()}
|
onClick={() => redraw()}
|
||||||
disabled={phase === "creating" || phase === "streaming"}
|
disabled={busy}
|
||||||
className="bg-white border border-slate-200 text-slate-700 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed text-[10.5px] font-bold px-3 py-1.5 rounded-xl flex items-center gap-1 transition"
|
className="bg-white border border-slate-200 text-slate-700 hover:bg-slate-50 disabled:opacity-50 disabled:cursor-not-allowed text-[10.5px] font-bold px-3 py-1.5 rounded-xl flex items-center gap-1 transition"
|
||||||
>
|
>
|
||||||
<RefreshCw className="w-3.5 h-3.5 text-slate-500" />
|
<RefreshCw className="w-3.5 h-3.5 text-slate-500" />
|
||||||
重新生成
|
重新生成
|
||||||
</button>
|
</button>
|
||||||
|
{onViewStatic && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onViewStatic}
|
||||||
|
className="bg-white border border-slate-200 text-slate-700 hover:bg-slate-50 text-[10.5px] font-bold px-3 py-1.5 rounded-xl flex items-center gap-1 transition"
|
||||||
|
>
|
||||||
|
最终方案展示
|
||||||
|
<ArrowRight className="w-3.5 h-3.5 text-slate-500" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -347,6 +416,24 @@ export function Step3SummaryMode({
|
|||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
seatMessages={activeSeatMessages}
|
seatMessages={activeSeatMessages}
|
||||||
taskArtifactSubmission={taskArtifactSubmission}
|
taskArtifactSubmission={taskArtifactSubmission}
|
||||||
|
sidePanel={
|
||||||
|
enableFlow && flowGraph && flowView ? (
|
||||||
|
<FlowGraphPanel
|
||||||
|
data={flowGraph.graph}
|
||||||
|
streaming={flowGraph.streaming}
|
||||||
|
onClose={() => setFlowView(false)}
|
||||||
|
onSwitchToReport={
|
||||||
|
mdArtifact
|
||||||
|
? () => {
|
||||||
|
setFlowView(false);
|
||||||
|
openArtifactPreview(mdArtifact.threadId, mdArtifact.path);
|
||||||
|
}
|
||||||
|
: undefined
|
||||||
|
}
|
||||||
|
onChange={summary.setFlowGraph}
|
||||||
|
/>
|
||||||
|
) : undefined
|
||||||
|
}
|
||||||
>
|
>
|
||||||
<AgentProgressPanel
|
<AgentProgressPanel
|
||||||
useStep1Chrome={useStep1Chrome}
|
useStep1Chrome={useStep1Chrome}
|
||||||
@@ -356,6 +443,11 @@ export function Step3SummaryMode({
|
|||||||
error={error}
|
error={error}
|
||||||
busy={busy}
|
busy={busy}
|
||||||
mdAvailable={!!mdArtifact}
|
mdAvailable={!!mdArtifact}
|
||||||
|
enableFlow={enableFlow}
|
||||||
|
flowPhase={summary.flowPhase}
|
||||||
|
onRegenerateFlow={summary.regenerateFlow}
|
||||||
|
pendingFlowClarification={summary.pendingFlowClarification}
|
||||||
|
onResolveFlowClarification={summary.resolveFlowClarification}
|
||||||
onRetry={redraw}
|
onRetry={redraw}
|
||||||
onAsk={handleAsk}
|
onAsk={handleAsk}
|
||||||
onPause={pause}
|
onPause={pause}
|
||||||
@@ -363,11 +455,31 @@ export function Step3SummaryMode({
|
|||||||
onArtifactPreview={openArtifactPreview}
|
onArtifactPreview={openArtifactPreview}
|
||||||
selectedModel={selectedModel}
|
selectedModel={selectedModel}
|
||||||
availableModels={availableModels}
|
availableModels={availableModels}
|
||||||
|
modelsLoading={modelsLoading}
|
||||||
setSelectedModel={setSelectedModel}
|
setSelectedModel={setSelectedModel}
|
||||||
taskArtifactSubmission={taskArtifactSubmission}
|
taskArtifactSubmission={taskArtifactSubmission}
|
||||||
/>
|
/>
|
||||||
</Step2SandboxLayout>
|
</Step2SandboxLayout>
|
||||||
|
|
||||||
|
{enableFlow && flowGraph && !flowView && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={() => setFlowView(true)}
|
||||||
|
title={artifactsOpen ? "切换到流程图画布" : "打开流程图画布"}
|
||||||
|
className="absolute bottom-4 right-4 z-20 inline-flex items-center gap-1.5 rounded-full border border-primary/30 bg-white px-3.5 py-2 text-[11px] font-bold text-primary shadow-lg transition hover:bg-primary/5"
|
||||||
|
>
|
||||||
|
{flowGraph.streaming ? (
|
||||||
|
<Loader2 className="h-3.5 w-3.5 animate-spin" />
|
||||||
|
) : (
|
||||||
|
<Workflow className="h-3.5 w-3.5" />
|
||||||
|
)}
|
||||||
|
流程图
|
||||||
|
<span className="rounded-full bg-primary/10 px-1.5 font-mono text-[10px]">
|
||||||
|
{flowGraph.graph.nodes.length}
|
||||||
|
</span>
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -481,6 +593,11 @@ function AgentProgressPanel({
|
|||||||
error,
|
error,
|
||||||
busy,
|
busy,
|
||||||
mdAvailable,
|
mdAvailable,
|
||||||
|
enableFlow,
|
||||||
|
flowPhase,
|
||||||
|
onRegenerateFlow,
|
||||||
|
pendingFlowClarification,
|
||||||
|
onResolveFlowClarification,
|
||||||
onRetry,
|
onRetry,
|
||||||
onAsk,
|
onAsk,
|
||||||
onPause,
|
onPause,
|
||||||
@@ -488,6 +605,7 @@ function AgentProgressPanel({
|
|||||||
onArtifactPreview,
|
onArtifactPreview,
|
||||||
selectedModel,
|
selectedModel,
|
||||||
availableModels,
|
availableModels,
|
||||||
|
modelsLoading,
|
||||||
setSelectedModel,
|
setSelectedModel,
|
||||||
taskArtifactSubmission,
|
taskArtifactSubmission,
|
||||||
}: {
|
}: {
|
||||||
@@ -499,6 +617,12 @@ function AgentProgressPanel({
|
|||||||
busy: boolean;
|
busy: boolean;
|
||||||
/** 沙箱里是否已有 Markdown 总结报告。 */
|
/** 沙箱里是否已有 Markdown 总结报告。 */
|
||||||
mdAvailable: boolean;
|
mdAvailable: boolean;
|
||||||
|
/** 普通会商才会生成、展示和编辑流程图。 */
|
||||||
|
enableFlow: boolean;
|
||||||
|
flowPhase: UseStep3SummaryReturn["flowPhase"];
|
||||||
|
onRegenerateFlow: () => void;
|
||||||
|
pendingFlowClarification: string | null;
|
||||||
|
onResolveFlowClarification: UseStep3SummaryReturn["resolveFlowClarification"];
|
||||||
onRetry: () => void;
|
onRetry: () => void;
|
||||||
onAsk: (message: string, files?: File[]) => void;
|
onAsk: (message: string, files?: File[]) => void;
|
||||||
onPause: () => void;
|
onPause: () => void;
|
||||||
@@ -506,21 +630,46 @@ function AgentProgressPanel({
|
|||||||
onArtifactPreview: UseStep3SummaryReturn["openArtifactPreview"];
|
onArtifactPreview: UseStep3SummaryReturn["openArtifactPreview"];
|
||||||
selectedModel: string;
|
selectedModel: string;
|
||||||
availableModels: ReadonlyArray<ModelOption>;
|
availableModels: ReadonlyArray<ModelOption>;
|
||||||
|
modelsLoading: boolean;
|
||||||
setSelectedModel: (model: string) => void;
|
setSelectedModel: (model: string) => void;
|
||||||
taskArtifactSubmission?: TaskArtifactSubmissionController;
|
taskArtifactSubmission?: TaskArtifactSubmissionController;
|
||||||
}) {
|
}) {
|
||||||
|
// 进度面板独立根据 props 判断流程图阶段,不能引用外层 Step3SummaryMode 的局部变量。
|
||||||
|
const isFlowRunning =
|
||||||
|
enableFlow && (flowPhase === "extracting" || flowPhase === "validating");
|
||||||
const scrollRef = useRef<HTMLDivElement>(null);
|
const scrollRef = useRef<HTMLDivElement>(null);
|
||||||
const { atBottom, scrollToBottom } = useAutoScroll(scrollRef, [dialogues]);
|
const contentRef = useRef<HTMLDivElement>(null);
|
||||||
|
const { atBottom, scrollToBottom, forceScrollToBottom } = useAutoScroll(
|
||||||
|
scrollRef,
|
||||||
|
[dialogues, flowPhase],
|
||||||
|
{ contentRef },
|
||||||
|
);
|
||||||
// 新一轮气泡出现(报告生成或用户续问)→ 强制滚到底,
|
// 新一轮气泡出现(报告生成或用户续问)→ 强制滚到底,
|
||||||
// 让用户看到新阶段。仅在气泡**条数**增加时触发,
|
// 让用户看到新阶段。仅在气泡**条数**增加时触发,
|
||||||
// 流式增量内容仍由 useAutoScroll 的「在底部才跟随」逻辑处理,不抢用户向上翻阅。
|
// 流式增量内容仍由 useAutoScroll 的「在底部才跟随」逻辑处理,不抢用户向上翻阅。
|
||||||
const prevDialogueCountRef = useRef(dialogues.length);
|
const prevDialogueCountRef = useRef(dialogues.length);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (dialogues.length > prevDialogueCountRef.current) {
|
if (dialogues.length > prevDialogueCountRef.current) {
|
||||||
scrollToBottom();
|
forceScrollToBottom();
|
||||||
}
|
}
|
||||||
prevDialogueCountRef.current = dialogues.length;
|
prevDialogueCountRef.current = dialogues.length;
|
||||||
}, [dialogues.length, scrollToBottom]);
|
}, [dialogues.length, forceScrollToBottom]);
|
||||||
|
// 结构化抽取可能复用前一张「调度中」气泡,导致消息条数不变;只依赖上面的
|
||||||
|
// dialogues.length 时就不会触发定位。阶段切换后等 DOM 提交一帧再强制下滑,
|
||||||
|
// 保证用户能立刻看到抽取/校验进度;后续增量仍由 useAutoScroll 按用户是否上翻决定。
|
||||||
|
useEffect(() => {
|
||||||
|
if (!enableFlow || (flowPhase !== "extracting" && flowPhase !== "validating")) return;
|
||||||
|
let followUpFrameId: number | null = null;
|
||||||
|
const frameId = window.requestAnimationFrame(() => {
|
||||||
|
forceScrollToBottom();
|
||||||
|
// 校验卡及其中的流式内容可能在第一帧后才完成布局,再确认一次最终高度。
|
||||||
|
followUpFrameId = window.requestAnimationFrame(forceScrollToBottom);
|
||||||
|
});
|
||||||
|
return () => {
|
||||||
|
window.cancelAnimationFrame(frameId);
|
||||||
|
if (followUpFrameId !== null) window.cancelAnimationFrame(followUpFrameId);
|
||||||
|
};
|
||||||
|
}, [enableFlow, flowPhase, forceScrollToBottom]);
|
||||||
// 「回答速度指示器」—— 与主页面共用全局开关 streamSpeedMeterEnabled。
|
// 「回答速度指示器」—— 与主页面共用全局开关 streamSpeedMeterEnabled。
|
||||||
const [localSettings] = useLocalSettings();
|
const [localSettings] = useLocalSettings();
|
||||||
const speedMeterEnabled = localSettings.display.streamSpeedMeterEnabled;
|
const speedMeterEnabled = localSettings.display.streamSpeedMeterEnabled;
|
||||||
@@ -598,10 +747,12 @@ function AgentProgressPanel({
|
|||||||
id="step3-summary-model-select"
|
id="step3-summary-model-select"
|
||||||
value={selectedModel}
|
value={selectedModel}
|
||||||
onChange={(e) => setSelectedModel(e.target.value)}
|
onChange={(e) => setSelectedModel(e.target.value)}
|
||||||
disabled={busy || availableModels.length === 0}
|
disabled={busy || modelsLoading || availableModels.length === 0}
|
||||||
title={
|
title={
|
||||||
availableModels.length === 0
|
modelsLoading
|
||||||
? "未在 config.yaml 中配置任何模型"
|
? "正在加载模型列表…"
|
||||||
|
: availableModels.length === 0
|
||||||
|
? "未在 config.yaml 中配置任何模型"
|
||||||
: busy
|
: busy
|
||||||
? "撰写中不可切换;完成后或重新生成时生效"
|
? "撰写中不可切换;完成后或重新生成时生效"
|
||||||
: "切换撰写使用的推理模型(下次生成生效)"
|
: "切换撰写使用的推理模型(下次生成生效)"
|
||||||
@@ -609,7 +760,7 @@ function AgentProgressPanel({
|
|||||||
className="shrink-0 max-w-[180px] bg-slate-50/50 border border-slate-200 rounded-xl px-2.5 py-1.5 text-[11px] font-bold text-slate-700 cursor-pointer focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-secondary dark:text-foreground dark:border-border"
|
className="shrink-0 max-w-[180px] bg-slate-50/50 border border-slate-200 rounded-xl px-2.5 py-1.5 text-[11px] font-bold text-slate-700 cursor-pointer focus:outline-none focus:border-primary focus:ring-1 focus:ring-[var(--brand-200)]/50 disabled:opacity-50 disabled:cursor-not-allowed dark:bg-secondary dark:text-foreground dark:border-border"
|
||||||
>
|
>
|
||||||
{availableModels.length === 0 && (
|
{availableModels.length === 0 && (
|
||||||
<option value="">未配置模型</option>
|
<option value="">{modelsLoading ? "加载中…" : "未配置模型"}</option>
|
||||||
)}
|
)}
|
||||||
{availableModels.map((m) => (
|
{availableModels.map((m) => (
|
||||||
<option key={m.name} value={m.name}>
|
<option key={m.name} value={m.name}>
|
||||||
@@ -639,10 +790,11 @@ function AgentProgressPanel({
|
|||||||
<div
|
<div
|
||||||
ref={scrollRef}
|
ref={scrollRef}
|
||||||
className={cn(
|
className={cn(
|
||||||
"size-full overflow-y-auto overflow-x-hidden p-4 space-y-4",
|
"size-full overflow-y-auto overflow-x-hidden",
|
||||||
useStep1Chrome ? "eightbf-roundtable-step3-log" : "bg-white",
|
useStep1Chrome ? "eightbf-roundtable-step3-log" : "bg-white",
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
|
<div ref={contentRef} className="flex min-h-full flex-col space-y-4 p-4">
|
||||||
{phase === "creating" && dialogues.length === 0 && (
|
{phase === "creating" && dialogues.length === 0 && (
|
||||||
<div className="flex items-center gap-2 text-xs text-slate-500">
|
<div className="flex items-center gap-2 text-xs text-slate-500">
|
||||||
<Loader2 className="w-3.5 h-3.5 animate-spin text-primary" />
|
<Loader2 className="w-3.5 h-3.5 animate-spin text-primary" />
|
||||||
@@ -663,10 +815,68 @@ function AgentProgressPanel({
|
|||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
{busy && (
|
{busy && !isFlowRunning && (
|
||||||
<div className="inline-flex items-center gap-2 rounded-full border bg-white px-2.5 py-1 text-[11px] text-slate-500">
|
<div className="inline-flex items-center gap-2 rounded-full border bg-white px-2.5 py-1 text-[11px] text-slate-500">
|
||||||
<span className="bg-primary size-1.5 animate-pulse rounded-full" />
|
<span className="bg-primary size-1.5 animate-pulse rounded-full" />
|
||||||
{mdAvailable ? "报告正文实时输出,右侧沙箱同步预览 Markdown" : "正在撰写 Markdown 总结报告"}
|
{mdAvailable
|
||||||
|
? "报告正文实时输出,右侧沙箱同步预览 Markdown"
|
||||||
|
: "正在撰写 Markdown 总结报告"}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{enableFlow && (flowPhase === "extracting" || flowPhase === "validating") && (
|
||||||
|
<div className="inline-flex items-center gap-2 rounded-full border border-blue-200 bg-blue-50 px-2.5 py-1 text-[11px] font-bold text-blue-600">
|
||||||
|
<Loader2 className="w-3 h-3 animate-spin" />
|
||||||
|
{flowPhase === "extracting"
|
||||||
|
? "报告已完成,正在抽取结构化流程图"
|
||||||
|
: "报告已完成,正在校验 / 修复流程图"}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onPause}
|
||||||
|
title="暂停结构化流程图抽取,已完成的报告会保留"
|
||||||
|
className="-mr-1 rounded-full border border-blue-200 bg-white px-1.5 py-0.5 text-[10px] font-bold text-blue-700 transition hover:bg-blue-100"
|
||||||
|
>
|
||||||
|
<Pause className="mr-0.5 inline-block h-3 w-3" />
|
||||||
|
暂停
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{enableFlow && flowPhase === "error" && (
|
||||||
|
<div className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800">
|
||||||
|
<div className="font-bold mb-1">流程图生成失败</div>
|
||||||
|
<div className="leading-relaxed">可切换模型后重新生成流程图。</div>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={onRegenerateFlow}
|
||||||
|
className="mt-2 inline-flex items-center gap-1 text-[11px] font-bold text-amber-800 underline hover:text-amber-900"
|
||||||
|
>
|
||||||
|
<RefreshCw className="w-3 h-3" />
|
||||||
|
重新生成流程图
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{enableFlow && pendingFlowClarification && (
|
||||||
|
<div className="rounded-xl border border-amber-200 bg-amber-50 p-3 text-xs text-amber-800">
|
||||||
|
<div className="mb-2 font-bold">请确认下一步要做什么:</div>
|
||||||
|
<div className="flex flex-wrap gap-1.5">
|
||||||
|
{(
|
||||||
|
[
|
||||||
|
["regenerate_report", "重新生成报告"],
|
||||||
|
["modify_flow", "修改流程图"],
|
||||||
|
["regenerate_flow", "重新生成流程图"],
|
||||||
|
["qa", "只是提问"],
|
||||||
|
] as const
|
||||||
|
).map(([intent, label]) => (
|
||||||
|
<button
|
||||||
|
key={intent}
|
||||||
|
type="button"
|
||||||
|
onClick={() => onResolveFlowClarification(intent)}
|
||||||
|
className="rounded-lg border border-amber-300 bg-white px-2.5 py-1.5 text-[11px] font-bold text-amber-800 transition hover:bg-amber-100"
|
||||||
|
>
|
||||||
|
{label}
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -696,10 +906,13 @@ function AgentProgressPanel({
|
|||||||
<div className="flex items-center gap-2 text-xs font-bold text-emerald-600">
|
<div className="flex items-center gap-2 text-xs font-bold text-emerald-600">
|
||||||
<CheckCircle className="w-4 h-4" />
|
<CheckCircle className="w-4 h-4" />
|
||||||
{mdAvailable
|
{mdAvailable
|
||||||
? "已生成方案总结报告,右侧可预览 / 下载 .md;可在下方继续修改报告或提问。"
|
? enableFlow
|
||||||
|
? "已生成方案总结报告与流程图,右侧可预览 / 下载 .md 或切换流程图。"
|
||||||
|
: "已生成方案总结报告,右侧可预览 / 下载 .md;可在下方继续修改报告或提问。"
|
||||||
: "已生成方案总结,可在下方继续修改报告或提问。"}
|
: "已生成方案总结,可在下方继续修改报告或提问。"}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!atBottom && (
|
{!atBottom && (
|
||||||
<ScrollToBottomButton
|
<ScrollToBottomButton
|
||||||
@@ -783,7 +996,9 @@ function AgentProgressPanel({
|
|||||||
placeholder={
|
placeholder={
|
||||||
busy
|
busy
|
||||||
? "生成中也可随时插话干预:发送将打断当前轮并据此继续…"
|
? "生成中也可随时插话干预:发送将打断当前轮并据此继续…"
|
||||||
: "继续对话:改报告(如「执行路线写细些」)或提问"
|
: enableFlow
|
||||||
|
? "继续对话:改报告、修改流程图或提问"
|
||||||
|
: "继续对话:改报告(如「执行路线写细些」)或提问"
|
||||||
}
|
}
|
||||||
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none"
|
className="min-h-8 max-h-56 flex-1 resize-none bg-transparent py-1.5 text-xs leading-relaxed text-slate-800 placeholder:text-slate-400 focus:outline-none"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -20,9 +20,10 @@ import {
|
|||||||
type StreamPhase,
|
type StreamPhase,
|
||||||
} from "@/roundtable-planning/api/multi-agent";
|
} from "@/roundtable-planning/api/multi-agent";
|
||||||
import type { ReportIntent } from "@/roundtable-planning/hooks/useStep3Report";
|
import type { ReportIntent } from "@/roundtable-planning/hooks/useStep3Report";
|
||||||
import type {
|
import {
|
||||||
Step2Dialogue,
|
completeAllStubWriteFiles,
|
||||||
StubAiMessage,
|
type Step2Dialogue,
|
||||||
|
type StubAiMessage,
|
||||||
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||||
import {
|
import {
|
||||||
buildDashboardPrompt,
|
buildDashboardPrompt,
|
||||||
@@ -234,6 +235,16 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
return { messageId, toolCallId };
|
return { messageId, toolCallId };
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
/** 前端已完成模板渲染后,为虚拟 HTML 文件补齐 tool result,解除沙箱「正在生成」状态。 */
|
||||||
|
const completeDashStubWriteFiles = useCallback((threadId: string) => {
|
||||||
|
const current = stubRef.current[threadId] ?? [];
|
||||||
|
const nextMessages = completeAllStubWriteFiles(current);
|
||||||
|
if (nextMessages === current) return;
|
||||||
|
const nextMap = { ...stubRef.current, [threadId]: nextMessages };
|
||||||
|
stubRef.current = nextMap;
|
||||||
|
setSeatStubMessages(nextMap);
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 预览开关(并行流防抢沙箱):只有用户当前所在视图的流允许**自动**打开沙箱。
|
// 预览开关(并行流防抢沙箱):只有用户当前所在视图的流允许**自动**打开沙箱。
|
||||||
const previewEnabledRef = useRef(false);
|
const previewEnabledRef = useRef(false);
|
||||||
const lastAutoOpenRef = useRef<{ threadId: string; messageId: string; toolCallId: string } | null>(
|
const lastAutoOpenRef = useRef<{ threadId: string; messageId: string; toolCallId: string } | null>(
|
||||||
@@ -274,8 +285,9 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
const current = htmlRef.current;
|
const current = htmlRef.current;
|
||||||
if (!current) return;
|
if (!current) return;
|
||||||
const { messageId, toolCallId } = upsertDashStub(tid, current);
|
const { messageId, toolCallId } = upsertDashStub(tid, current);
|
||||||
|
completeDashStubWriteFiles(tid);
|
||||||
openDashboardArtifact(tid, messageId, toolCallId, { force: true }); // 用户显式点击不受预览开关限制
|
openDashboardArtifact(tid, messageId, toolCallId, { force: true }); // 用户显式点击不受预览开关限制
|
||||||
}, [activeArtifactThreadId, upsertDashStub, openDashboardArtifact]);
|
}, [activeArtifactThreadId, upsertDashStub, completeDashStubWriteFiles, openDashboardArtifact]);
|
||||||
|
|
||||||
const abort = useCallback(() => {
|
const abort = useCallback(() => {
|
||||||
aborterRef.current?.abort();
|
aborterRef.current?.abort();
|
||||||
@@ -345,7 +357,10 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
const data = extractReportData(res.content);
|
const data = extractReportData(res.content);
|
||||||
if (!data) {
|
if (!data) {
|
||||||
// 解析失败:把 stub 还原成上一版可用的大屏(若有),别让沙箱停在生 JSON 上。
|
// 解析失败:把 stub 还原成上一版可用的大屏(若有),别让沙箱停在生 JSON 上。
|
||||||
if (htmlRef.current) upsertDashStub(threadId, htmlRef.current);
|
if (htmlRef.current) {
|
||||||
|
upsertDashStub(threadId, htmlRef.current);
|
||||||
|
completeDashStubWriteFiles(threadId);
|
||||||
|
}
|
||||||
finalizeDialogue(
|
finalizeDialogue(
|
||||||
bubbleId,
|
bubbleId,
|
||||||
(res.content || "").trim() ||
|
(res.content || "").trim() ||
|
||||||
@@ -362,6 +377,7 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
setData(data); // 同步结构化数据 → 应用内 React 大屏可直接渲染
|
setData(data); // 同步结构化数据 → 应用内 React 大屏可直接渲染
|
||||||
dataRef.current = data; // 记下当前数据,供下一轮修改时喂回模型做最小改动
|
dataRef.current = data; // 记下当前数据,供下一轮修改时喂回模型做最小改动
|
||||||
const { messageId, toolCallId } = upsertDashStub(threadId, rendered);
|
const { messageId, toolCallId } = upsertDashStub(threadId, rendered);
|
||||||
|
completeDashStubWriteFiles(threadId);
|
||||||
openDashboardArtifact(threadId, messageId, toolCallId);
|
openDashboardArtifact(threadId, messageId, toolCallId);
|
||||||
|
|
||||||
finalizeDialogue(bubbleId, res.content, "大屏绘制完成");
|
finalizeDialogue(bubbleId, res.content, "大屏绘制完成");
|
||||||
@@ -378,6 +394,7 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
setDialogueConfidence,
|
setDialogueConfidence,
|
||||||
finalizeDialogue,
|
finalizeDialogue,
|
||||||
upsertDashStub,
|
upsertDashStub,
|
||||||
|
completeDashStubWriteFiles,
|
||||||
openDashboardArtifact,
|
openDashboardArtifact,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -483,6 +500,7 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
setDashThreadId(synthThreadId);
|
setDashThreadId(synthThreadId);
|
||||||
setActiveArtifactThreadId(synthThreadId);
|
setActiveArtifactThreadId(synthThreadId);
|
||||||
const { messageId, toolCallId } = upsertDashStub(synthThreadId, savedHtml);
|
const { messageId, toolCallId } = upsertDashStub(synthThreadId, savedHtml);
|
||||||
|
completeDashStubWriteFiles(synthThreadId);
|
||||||
// 记录预览锚点(不打开):进入大屏视图时若沙箱已开着,可立即接管显示本流产物。
|
// 记录预览锚点(不打开):进入大屏视图时若沙箱已开着,可立即接管显示本流产物。
|
||||||
lastAutoOpenRef.current = { threadId: synthThreadId, messageId, toolCallId };
|
lastAutoOpenRef.current = { threadId: synthThreadId, messageId, toolCallId };
|
||||||
// 历史完整对话(文字 + 步骤条) + stub —— 新版草稿存了就整段回显,像第二步一样。
|
// 历史完整对话(文字 + 步骤条) + stub —— 新版草稿存了就整段回显,像第二步一样。
|
||||||
@@ -495,7 +513,12 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
: null;
|
: null;
|
||||||
if (savedStubs && Object.keys(savedStubs).length) {
|
if (savedStubs && Object.keys(savedStubs).length) {
|
||||||
// 合并历史 stub(各气泡步骤卡的真线程预览),保留合成 stub(恢复后大屏预览锚点)。
|
// 合并历史 stub(各气泡步骤卡的真线程预览),保留合成 stub(恢复后大屏预览锚点)。
|
||||||
const merged = { ...stubRef.current, ...savedStubs };
|
const merged: Record<string, StubAiMessage[]> = Object.fromEntries(
|
||||||
|
Object.entries({ ...stubRef.current, ...savedStubs }).map(([threadId, messages]) => [
|
||||||
|
threadId,
|
||||||
|
completeAllStubWriteFiles(messages as StubAiMessage[]),
|
||||||
|
]),
|
||||||
|
);
|
||||||
stubRef.current = merged;
|
stubRef.current = merged;
|
||||||
setSeatStubMessages(merged);
|
setSeatStubMessages(merged);
|
||||||
}
|
}
|
||||||
@@ -522,7 +545,7 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
setPhase("done");
|
setPhase("done");
|
||||||
// 不自动开沙箱(对齐 06-12「进第三步默认关闭」策略),用户点按钮随时可开。
|
// 不自动开沙箱(对齐 06-12「进第三步默认关闭」策略),用户点按钮随时可开。
|
||||||
},
|
},
|
||||||
[abort, upsertDashStub],
|
[abort, upsertDashStub, completeDashStubWriteFiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
/** 从 dashboardJson(草稿/后台作业存的结构化数据)恢复:按当前主题渲染。 */
|
/** 从 dashboardJson(草稿/后台作业存的结构化数据)恢复:按当前主题渲染。 */
|
||||||
@@ -556,6 +579,7 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
setData(parsed);
|
setData(parsed);
|
||||||
dataRef.current = parsed; // 续轮修改的基线
|
dataRef.current = parsed; // 续轮修改的基线
|
||||||
const { messageId, toolCallId } = upsertDashStub(synthThreadId, rendered);
|
const { messageId, toolCallId } = upsertDashStub(synthThreadId, rendered);
|
||||||
|
completeDashStubWriteFiles(synthThreadId);
|
||||||
lastAutoOpenRef.current = { threadId: synthThreadId, messageId, toolCallId };
|
lastAutoOpenRef.current = { threadId: synthThreadId, messageId, toolCallId };
|
||||||
} else {
|
} else {
|
||||||
htmlRef.current = null;
|
htmlRef.current = null;
|
||||||
@@ -567,7 +591,7 @@ export function useStep3Dashboard(options: UseStep3DashboardOptions): UseStep3Da
|
|||||||
setError(null);
|
setError(null);
|
||||||
setPhase(parsed ? "done" : "idle");
|
setPhase(parsed ? "done" : "idle");
|
||||||
},
|
},
|
||||||
[abort, themeMode, upsertDashStub],
|
[abort, themeMode, upsertDashStub, completeDashStubWriteFiles],
|
||||||
);
|
);
|
||||||
|
|
||||||
// 恢复路径辅助:savedReport 由组件/页面按字段调用对应 hydrate;这里只消除未用告警。
|
// 恢复路径辅助:savedReport 由组件/页面按字段调用对应 hydrate;这里只消除未用告警。
|
||||||
|
|||||||
@@ -478,6 +478,36 @@ export function useStep3Report(
|
|||||||
[],
|
[],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 自定义 SSE 的 stub 不会天然带回 LangGraph 的 ToolMessage。经典绘制正常收尾时,
|
||||||
|
* 为每个 write_file 补一条等价的完成消息,让通用沙箱能结束「正在生成」状态并自动预览。
|
||||||
|
*/
|
||||||
|
const completeStubWriteFiles = useCallback((threadId: string) => {
|
||||||
|
const current = stubRef.current[threadId] ?? [];
|
||||||
|
const aiMessage = current.find((message) => message.type === "ai");
|
||||||
|
if (!aiMessage) return;
|
||||||
|
|
||||||
|
const completedIds = new Set(
|
||||||
|
current
|
||||||
|
.filter((message) => message.type === "tool" && message.tool_call_id)
|
||||||
|
.map((message) => message.tool_call_id!),
|
||||||
|
);
|
||||||
|
const toolResults: StubAiMessage[] = aiMessage.tool_calls
|
||||||
|
.filter((toolCall) => toolCall.name === "write_file" && !completedIds.has(toolCall.id))
|
||||||
|
.map((toolCall) => ({
|
||||||
|
id: `roundtable-report-tool-result-${threadId}-${toolCall.id}`,
|
||||||
|
type: "tool",
|
||||||
|
content: "",
|
||||||
|
tool_calls: [],
|
||||||
|
tool_call_id: toolCall.id,
|
||||||
|
}));
|
||||||
|
if (toolResults.length === 0) return;
|
||||||
|
|
||||||
|
const nextMap = { ...stubRef.current, [threadId]: [...current, ...toolResults] };
|
||||||
|
stubRef.current = nextMap;
|
||||||
|
setSeatStubMessages(nextMap);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const buildWriteFileVirtualUrl = useCallback(
|
const buildWriteFileVirtualUrl = useCallback(
|
||||||
(path: string, messageId: string, toolCallId: string) =>
|
(path: string, messageId: string, toolCallId: string) =>
|
||||||
new URL(
|
new URL(
|
||||||
@@ -825,6 +855,9 @@ export function useStep3Report(
|
|||||||
? await reconcileStubFromRealFile(threadId, aborter.signal)
|
? await reconcileStubFromRealFile(threadId, aborter.signal)
|
||||||
: "";
|
: "";
|
||||||
if (aborter.signal.aborted) return;
|
if (aborter.signal.aborted) return;
|
||||||
|
// 该轮已正常收尾:解除当前 HTML 虚拟预览的写入态。不能等其它并行的总结/大屏流,
|
||||||
|
// 它们各自有独立线程和 stub,互不影响。
|
||||||
|
completeStubWriteFiles(threadId);
|
||||||
finalizeDialogue(bubbleId, closing, "绘制完成");
|
finalizeDialogue(bubbleId, closing, "绘制完成");
|
||||||
setPhase("done");
|
setPhase("done");
|
||||||
setReportTheme(theme);
|
setReportTheme(theme);
|
||||||
@@ -844,6 +877,7 @@ export function useStep3Report(
|
|||||||
selectedModel,
|
selectedModel,
|
||||||
openRealArtifactFallback,
|
openRealArtifactFallback,
|
||||||
reconcileStubFromRealFile,
|
reconcileStubFromRealFile,
|
||||||
|
completeStubWriteFiles,
|
||||||
finalizeDialogue,
|
finalizeDialogue,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
@@ -1043,6 +1077,13 @@ export function useStep3Report(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: `roundtable-report-tool-result-${synthThreadId}-${toolCallId}`,
|
||||||
|
type: "tool",
|
||||||
|
content: "",
|
||||||
|
tool_calls: [],
|
||||||
|
tool_call_id: toolCallId,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
};
|
};
|
||||||
// 历史完整对话(中间文字 + 步骤条) + stub —— 新版草稿存了就整段回显,像第二步一样。
|
// 历史完整对话(中间文字 + 步骤条) + stub —— 新版草稿存了就整段回显,像第二步一样。
|
||||||
|
|||||||
@@ -34,9 +34,11 @@ import type {
|
|||||||
ReportIntent,
|
ReportIntent,
|
||||||
ReportSeatDelivery,
|
ReportSeatDelivery,
|
||||||
} from "@/roundtable-planning/hooks/useStep3Report";
|
} from "@/roundtable-planning/hooks/useStep3Report";
|
||||||
import type {
|
import {
|
||||||
Step2Dialogue,
|
completeAllStubWriteFiles,
|
||||||
StubAiMessage,
|
completeStubWriteFile,
|
||||||
|
type Step2Dialogue,
|
||||||
|
type StubAiMessage,
|
||||||
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||||
import { detectFlowIntent, extractFlowGraph, hasFlowFence, serializeFlowGraph, type FlowGraphData } from "@/roundtable-planning/lib/flow-graph";
|
import { detectFlowIntent, extractFlowGraph, hasFlowFence, serializeFlowGraph, type FlowGraphData } from "@/roundtable-planning/lib/flow-graph";
|
||||||
import {
|
import {
|
||||||
@@ -127,7 +129,7 @@ export interface UseStep3SummaryReturn {
|
|||||||
dialogues: Step2Dialogue[];
|
dialogues: Step2Dialogue[];
|
||||||
seatStubMessages: Record<string, StubAiMessage[]>;
|
seatStubMessages: Record<string, StubAiMessage[]>;
|
||||||
activeArtifactThreadId: string | null;
|
activeArtifactThreadId: string | null;
|
||||||
/** 是否正在生成中(撰写/初始化/上传)—— 驱动暂停按钮与发送态。 */
|
/** 是否正在生成中(报告撰写或流程图抽取/校验)—— 驱动暂停按钮与发送态。 */
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
/** 启动总结(幂等:进行中重复调用会被忽略)。 */
|
/** 启动总结(幂等:进行中重复调用会被忽略)。 */
|
||||||
start: () => void;
|
start: () => void;
|
||||||
@@ -821,6 +823,34 @@ export function useStep3Summary(
|
|||||||
[scheduleStubMessagesFlush],
|
[scheduleStubMessagesFlush],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** 将 summary special 流的真实 tool 返回同步到虚拟 Markdown 文件。 */
|
||||||
|
const completeSummaryStubWriteFile = useCallback(
|
||||||
|
(threadId: string, toolCallId: string) => {
|
||||||
|
const current = stubRef.current[threadId] ?? [];
|
||||||
|
const nextMessages = completeStubWriteFile(current, toolCallId);
|
||||||
|
if (nextMessages === current) return;
|
||||||
|
const nextMap = { ...stubRef.current, [threadId]: nextMessages };
|
||||||
|
stubRef.current = nextMap;
|
||||||
|
pendingStubMessagesRef.current = nextMap;
|
||||||
|
scheduleStubMessagesFlush();
|
||||||
|
},
|
||||||
|
[scheduleStubMessagesFlush],
|
||||||
|
);
|
||||||
|
|
||||||
|
/** summary 一轮已完成时补齐尚未透传的 write_file tool 返回。 */
|
||||||
|
const completeSummaryStubWriteFiles = useCallback(
|
||||||
|
(threadId: string) => {
|
||||||
|
const current = stubRef.current[threadId] ?? [];
|
||||||
|
const nextMessages = completeAllStubWriteFiles(current);
|
||||||
|
if (nextMessages === current) return;
|
||||||
|
const nextMap = { ...stubRef.current, [threadId]: nextMessages };
|
||||||
|
stubRef.current = nextMap;
|
||||||
|
pendingStubMessagesRef.current = nextMap;
|
||||||
|
scheduleStubMessagesFlush();
|
||||||
|
},
|
||||||
|
[scheduleStubMessagesFlush],
|
||||||
|
);
|
||||||
|
|
||||||
const buildWriteFileVirtualUrl = useCallback(
|
const buildWriteFileVirtualUrl = useCallback(
|
||||||
(path: string, messageId: string, toolCallId: string) =>
|
(path: string, messageId: string, toolCallId: string) =>
|
||||||
new URL(
|
new URL(
|
||||||
@@ -955,6 +985,11 @@ export function useStep3Summary(
|
|||||||
(frame: unknown, threadId: string) => {
|
(frame: unknown, threadId: string) => {
|
||||||
if (!Array.isArray(frame) || frame.length === 0) return;
|
if (!Array.isArray(frame) || frame.length === 0) return;
|
||||||
const chunk = frame[0] as Record<string, unknown> | undefined;
|
const chunk = frame[0] as Record<string, unknown> | undefined;
|
||||||
|
if (String(chunk?.type ?? "").toLowerCase() === "tool") {
|
||||||
|
const toolCallId = typeof chunk?.tool_call_id === "string" ? chunk.tool_call_id : "";
|
||||||
|
if (toolCallId) completeSummaryStubWriteFile(threadId, toolCallId);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const toolCalls = chunk?.tool_calls;
|
const toolCalls = chunk?.tool_calls;
|
||||||
if (!Array.isArray(toolCalls)) return;
|
if (!Array.isArray(toolCalls)) return;
|
||||||
for (const tc of toolCalls) {
|
for (const tc of toolCalls) {
|
||||||
@@ -970,7 +1005,7 @@ export function useStep3Summary(
|
|||||||
upsertStub(threadId, name, a as Record<string, unknown>);
|
upsertStub(threadId, name, a as Record<string, unknown>);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[upsertStub],
|
[completeSummaryStubWriteFile, upsertStub],
|
||||||
);
|
);
|
||||||
|
|
||||||
const openArtifactPreview = useCallback(
|
const openArtifactPreview = useCallback(
|
||||||
@@ -1162,6 +1197,9 @@ export function useStep3Summary(
|
|||||||
flowActiveRef.current = false;
|
flowActiveRef.current = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 报告线程本轮已收尾。即使后端没有单独透传 tool 帧,也只结束本线程的预览写入态;
|
||||||
|
// 后续自动流程图/大屏等并行分支完全不参与这个判断。
|
||||||
|
completeSummaryStubWriteFiles(threadId);
|
||||||
finalizeDialogue(bubbleId, res.content, "总结完成");
|
finalizeDialogue(bubbleId, res.content, "总结完成");
|
||||||
setPhase("done");
|
setPhase("done");
|
||||||
|
|
||||||
@@ -1187,6 +1225,7 @@ export function useStep3Summary(
|
|||||||
handleArtifactToolPhase,
|
handleArtifactToolPhase,
|
||||||
handleUpstreamFrame,
|
handleUpstreamFrame,
|
||||||
reconcileStubFromRealFile,
|
reconcileStubFromRealFile,
|
||||||
|
completeSummaryStubWriteFiles,
|
||||||
finalizeDialogue,
|
finalizeDialogue,
|
||||||
seedStubWithCurrentMd,
|
seedStubWithCurrentMd,
|
||||||
],
|
],
|
||||||
@@ -1939,6 +1978,13 @@ export function useStep3Summary(
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
id: `roundtable-stub-tool-result-${messageId}-${toolCallId}`,
|
||||||
|
type: "tool",
|
||||||
|
content: "",
|
||||||
|
tool_calls: [],
|
||||||
|
tool_call_id: toolCallId,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
}
|
}
|
||||||
: {};
|
: {};
|
||||||
@@ -1953,7 +1999,15 @@ export function useStep3Summary(
|
|||||||
// 合并:合成 stub 保住「恢复后预览」锚点,历史 stub 保住各气泡步骤卡的真线程预览。
|
// 合并:合成 stub 保住「恢复后预览」锚点,历史 stub 保住各气泡步骤卡的真线程预览。
|
||||||
const effectiveStub =
|
const effectiveStub =
|
||||||
savedStubs && Object.keys(savedStubs).length ? { ...stub, ...savedStubs } : stub;
|
savedStubs && Object.keys(savedStubs).length ? { ...stub, ...savedStubs } : stub;
|
||||||
replaceStubMessages(effectiveStub);
|
// 恢复的是静态完成记录;兼容旧草稿没有保存 tool result 的场景。
|
||||||
|
replaceStubMessages(
|
||||||
|
Object.fromEntries(
|
||||||
|
Object.entries(effectiveStub).map(([threadId, messages]) => [
|
||||||
|
threadId,
|
||||||
|
completeAllStubWriteFiles(messages),
|
||||||
|
]),
|
||||||
|
),
|
||||||
|
);
|
||||||
currentMdRef.current = snap.md;
|
currentMdRef.current = snap.md;
|
||||||
liveThreadRef.current = realThreadId;
|
liveThreadRef.current = realThreadId;
|
||||||
// 快照收尾语带 flow-json → 上次主产物是流程图,恢复后续问沿用流程图语境。
|
// 快照收尾语带 flow-json → 上次主产物是流程图,恢复后续问沿用流程图语境。
|
||||||
@@ -2015,7 +2069,11 @@ export function useStep3Summary(
|
|||||||
[abort, buildWriteFileVirtualUrl, replaceStubMessages],
|
[abort, buildWriteFileVirtualUrl, replaceStubMessages],
|
||||||
);
|
);
|
||||||
|
|
||||||
const busy = phase === "creating" || phase === "streaming";
|
const busy =
|
||||||
|
phase === "creating" ||
|
||||||
|
phase === "streaming" ||
|
||||||
|
flowPhase === "extracting" ||
|
||||||
|
flowPhase === "validating";
|
||||||
|
|
||||||
return {
|
return {
|
||||||
phase,
|
phase,
|
||||||
|
|||||||
@@ -350,6 +350,11 @@ export default function RoundtablePlanningPage({
|
|||||||
hideHeaderSettings = false,
|
hideHeaderSettings = false,
|
||||||
/** 可选的外部任务过程产物提交适配器(8BF 使用,通用圆桌保持不显示)。 */
|
/** 可选的外部任务过程产物提交适配器(8BF 使用,通用圆桌保持不显示)。 */
|
||||||
taskArtifactSubmissionConfig,
|
taskArtifactSubmissionConfig,
|
||||||
|
/**
|
||||||
|
* 第三步的交付范围。普通会商保留报告后的流程图、最终方案/席位列表与可视化大屏;
|
||||||
|
* 8BF 只交付 Markdown 总结报告,避免两个入口互相改变行为。
|
||||||
|
*/
|
||||||
|
step3Mode = "full",
|
||||||
}: {
|
}: {
|
||||||
compactChrome?: boolean;
|
compactChrome?: boolean;
|
||||||
forceDarkAppearance?: boolean;
|
forceDarkAppearance?: boolean;
|
||||||
@@ -396,6 +401,7 @@ export default function RoundtablePlanningPage({
|
|||||||
newTaskLabel?: string;
|
newTaskLabel?: string;
|
||||||
hideHeaderSettings?: boolean;
|
hideHeaderSettings?: boolean;
|
||||||
taskArtifactSubmissionConfig?: TaskArtifactSubmissionConfig;
|
taskArtifactSubmissionConfig?: TaskArtifactSubmissionConfig;
|
||||||
|
step3Mode?: "full" | "report-only";
|
||||||
}) {
|
}) {
|
||||||
const { theme } = useTheme();
|
const { theme } = useTheme();
|
||||||
// 全工作区跨页的后台作业完成通知器(Provider 挂在 PageLayout 常驻层)。
|
// 全工作区跨页的后台作业完成通知器(Provider 挂在 PageLayout 常驻层)。
|
||||||
@@ -403,6 +409,7 @@ export default function RoundtablePlanningPage({
|
|||||||
// 保留 ThemeContext 的主题切换能力;独立业务页可只覆盖自身的视觉模式,
|
// 保留 ThemeContext 的主题切换能力;独立业务页可只覆盖自身的视觉模式,
|
||||||
// 不触碰全局 html 主题,后续移除该 prop 即可恢复随系统主题切换。
|
// 不触碰全局 html 主题,后续移除该 prop 即可恢复随系统主题切换。
|
||||||
const isDark = forceDarkAppearance || theme === "dark";
|
const isDark = forceDarkAppearance || theme === "dark";
|
||||||
|
const hasFullStep3Outputs = step3Mode === "full";
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
const location = useLocation();
|
const location = useLocation();
|
||||||
const taskContextRef = useRef<Record<string, unknown> | null>(taskContext ?? null);
|
const taskContextRef = useRef<Record<string, unknown> | null>(taskContext ?? null);
|
||||||
@@ -1634,9 +1641,9 @@ export default function RoundtablePlanningPage({
|
|||||||
.filter((g) => g.length > 0),
|
.filter((g) => g.length > 0),
|
||||||
[orchestrationPlan],
|
[orchestrationPlan],
|
||||||
);
|
);
|
||||||
// 第三步的交付统一为 Markdown 总结报告。流程图不再作为本页的后续自动产物,
|
// 普通会商:Markdown 报告完成后自动续跑结构化流程图;8BF:只完成报告即结束。
|
||||||
// 避免报告写完后额外触发一次结构化抽取请求。
|
// 这个开关由入口显式传入,而不是从业务链/任务状态推断,防止两类会商串行为。
|
||||||
const autoSummaryFlow = false;
|
const autoSummaryFlow = hasFullStep3Outputs;
|
||||||
|
|
||||||
const summaryFlow = useStep3Summary({
|
const summaryFlow = useStep3Summary({
|
||||||
intentReady: step1.intentReady,
|
intentReady: step1.intentReady,
|
||||||
@@ -1646,7 +1653,7 @@ export default function RoundtablePlanningPage({
|
|||||||
coordinatorPrompt: orchestrationPlan?.coordinatorPrompt ?? null,
|
coordinatorPrompt: orchestrationPlan?.coordinatorPrompt ?? null,
|
||||||
chainLevels,
|
chainLevels,
|
||||||
autoFlow: autoSummaryFlow,
|
autoFlow: autoSummaryFlow,
|
||||||
flowEnabled: false,
|
flowEnabled: hasFullStep3Outputs,
|
||||||
savedSummary: step3Report?.summaryReport ?? null,
|
savedSummary: step3Report?.summaryReport ?? null,
|
||||||
onSummarySaved: handleSummarySaved,
|
onSummarySaved: handleSummarySaved,
|
||||||
ingestAvailable,
|
ingestAvailable,
|
||||||
@@ -1753,28 +1760,30 @@ export default function RoundtablePlanningPage({
|
|||||||
} else {
|
} else {
|
||||||
summaryFlow.start();
|
summaryFlow.start();
|
||||||
}
|
}
|
||||||
// ② 结果绘制(经典模式一)。旧版草稿可能把大屏 HTML 存在 html 槽位(带 marker),
|
if (hasFullStep3Outputs) {
|
||||||
// 那不是经典产物 → 经典照常启动。错峰 ~300ms,削掉与总结的同刻 start 尖峰。
|
// ② 结果绘制(经典模式一)。旧版草稿可能把大屏 HTML 存在 html 槽位(带 marker),
|
||||||
if (rep?.html && !isDashboardHtml(rep.html)) {
|
// 那不是经典产物 → 经典照常启动。错峰 ~300ms,削掉与总结的同刻 start 尖峰。
|
||||||
reportFlow.hydrateSaved(rep);
|
if (rep?.html && !isDashboardHtml(rep.html)) {
|
||||||
} else {
|
reportFlow.hydrateSaved(rep);
|
||||||
staggerStart(() => reportFlow.start(), STEP3_STAGGER_MS); // 仍并行绘制,只是起跑错开
|
} else {
|
||||||
}
|
staggerStart(() => reportFlow.start(), STEP3_STAGGER_MS); // 仍并行绘制,只是起跑错开
|
||||||
// ③ 大屏多页(模式二):优先 dashboardJson(按当前主题渲染);旧版 marker html 兜底。
|
}
|
||||||
// 带上大屏生成过程的完整对话(文字 + 步骤条),恢复时整段回显。错峰 ~600ms。
|
// ③ 大屏多页(模式二):优先 dashboardJson(按当前主题渲染);旧版 marker html 兜底。
|
||||||
const dashSaved = {
|
// 带上大屏生成过程的完整对话(文字 + 步骤条),恢复时整段回显。错峰 ~600ms。
|
||||||
dialogues: rep?.dashboardDialogues,
|
const dashSaved = {
|
||||||
seatStubMessages: rep?.dashboardSeatStubMessages,
|
dialogues: rep?.dashboardDialogues,
|
||||||
};
|
seatStubMessages: rep?.dashboardSeatStubMessages,
|
||||||
if (rep?.dashboardJson) {
|
};
|
||||||
dashboardFlow.hydrateFromJson(rep.dashboardJson, dashSaved);
|
if (rep?.dashboardJson) {
|
||||||
} else if (rep?.html && isDashboardHtml(rep.html)) {
|
dashboardFlow.hydrateFromJson(rep.dashboardJson, dashSaved);
|
||||||
dashboardFlow.hydrateSaved(rep.html, dashSaved);
|
} else if (rep?.html && isDashboardHtml(rep.html)) {
|
||||||
} else {
|
dashboardFlow.hydrateSaved(rep.html, dashSaved);
|
||||||
staggerStart(() => dashboardFlow.start(), STEP3_STAGGER_MS * 2);
|
} else {
|
||||||
|
staggerStart(() => dashboardFlow.start(), STEP3_STAGGER_MS * 2);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, [summaryFlow, reportFlow, dashboardFlow, clearStep3StartTimers]);
|
}, [summaryFlow, reportFlow, dashboardFlow, clearStep3StartTimers, hasFullStep3Outputs]);
|
||||||
|
|
||||||
// 切换草稿:先停掉旧草稿的在途 Step3 流(防串台)。声明在 hydrate effect **之前**,
|
// 切换草稿:先停掉旧草稿的在途 Step3 流(防串台)。声明在 hydrate effect **之前**,
|
||||||
// 保证同一提交内先 abort 旧流、再由下方 effect hydrate 新草稿,顺序不颠倒。
|
// 保证同一提交内先 abort 旧流、再由下方 effect hydrate 新草稿,顺序不颠倒。
|
||||||
@@ -2608,6 +2617,7 @@ export default function RoundtablePlanningPage({
|
|||||||
<Step3Panel
|
<Step3Panel
|
||||||
useStep1Chrome={rootClassName?.includes("eightbf-roundtable") ?? false}
|
useStep1Chrome={rootClassName?.includes("eightbf-roundtable") ?? false}
|
||||||
headerTitle={step3HeaderTitle}
|
headerTitle={step3HeaderTitle}
|
||||||
|
step3Mode={step3Mode}
|
||||||
threadIds={step2.threadIds}
|
threadIds={step2.threadIds}
|
||||||
coordinatorName={step2.coordinatorName}
|
coordinatorName={step2.coordinatorName}
|
||||||
consensusText={step2.lastLeaderContent}
|
consensusText={step2.lastLeaderContent}
|
||||||
|
|||||||
Reference in new issue
Block a user