第二次生成结果后台挂起
This commit is contained in:
1 parent
19399482e1
commit
90bced3ed2
12 files changed
+465
-45
No files matched your search
@@ -31,6 +31,8 @@ export interface DraftStep1Snapshot {
|
||||
}
|
||||
|
||||
export interface DraftStep2Snapshot {
|
||||
activeRunId?: string | null;
|
||||
runs?: DraftStep2RunSnapshot[];
|
||||
/**
|
||||
* The seats actually selected for this session. Restored on load so the
|
||||
* Step 2/3 「参与角色」panel shows the chosen agents instead of the default 6.
|
||||
@@ -57,6 +59,27 @@ export interface DraftStep2Snapshot {
|
||||
chain?: { id: string; title: string } | null;
|
||||
}
|
||||
|
||||
export interface DraftStep2RunSnapshot {
|
||||
id: string;
|
||||
title: string;
|
||||
createdAt: string;
|
||||
source: "initial" | "recommend" | "chain" | "manual";
|
||||
selectedAgents?: Array<{ agent_id: string; name: string }>;
|
||||
threadIds: ThreadIdMap | null;
|
||||
coordinatorName: string | null;
|
||||
step2RoundtableDialogues: unknown[];
|
||||
lastLeaderContent: string;
|
||||
hasConsensus: boolean;
|
||||
consensusPercentage: number;
|
||||
budgetLimit: number;
|
||||
seatStubMessages?: Record<string, unknown[]>;
|
||||
orchestrationMode?: "recommend" | "chain";
|
||||
chain?: { id: string; title: string } | null;
|
||||
step3?: DraftStep3Snapshot | null;
|
||||
jobId?: string | null;
|
||||
jobStatus?: JobStatus | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Step 3「结果绘制」snapshot — the generated visualization report, stored inline
|
||||
* so a reloaded draft can show it without depending on the ephemeral report
|
||||
|
||||
@@ -67,6 +67,7 @@ function mapJob(raw: Record<string, unknown>): RoundtableJob {
|
||||
chain: (raw.chain as RoundtableJob["chain"]) ?? null,
|
||||
error: (raw.error as string | null) ?? null,
|
||||
dialogues: Array.isArray(raw.dialogues) ? (raw.dialogues as JobDialogue[]) : [],
|
||||
step3: (raw.step3 as RoundtableJob["step3"]) ?? null,
|
||||
createdAt: (raw.createdAt as string | null) ?? null,
|
||||
updatedAt: (raw.updatedAt as string | null) ?? null,
|
||||
};
|
||||
|
||||
@@ -19,7 +19,6 @@
|
||||
import { useCallback, useEffect, useRef, useState } from "react";
|
||||
|
||||
import type { Step1Snapshot } from "@/roundtable-planning/hooks/useStep1Intent";
|
||||
import type { Step2Snapshot } from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||
import {
|
||||
createDraft,
|
||||
deleteDraft,
|
||||
@@ -27,6 +26,7 @@ import {
|
||||
getDraft,
|
||||
listDrafts,
|
||||
updateDraft,
|
||||
type DraftStep2Snapshot,
|
||||
type DraftStep3Snapshot,
|
||||
} from "@/roundtable-planning/api/drafts";
|
||||
import type { JobStatus } from "@/roundtable-planning/lib/job-types";
|
||||
@@ -51,13 +51,13 @@ export interface UseDraftPersistenceOptions {
|
||||
/** 读 Step 1 当前快照(由 useStep1Intent 提供的 getter)。 */
|
||||
getStep1Snapshot: () => Step1Snapshot;
|
||||
/** 读 Step 2 当前快照(由 useStep2Orchestration 提供的 getter)。 */
|
||||
getStep2Snapshot: () => Step2Snapshot;
|
||||
getStep2Snapshot: () => DraftStep2Snapshot;
|
||||
/** 读 Step 3「结果绘制」当前快照(无则 null)。 */
|
||||
getStep3Snapshot?: () => DraftStep3Snapshot | null;
|
||||
/** 加载草稿时把 Step 1 状态打回去。 */
|
||||
hydrateStep1: (snapshot: Step1Snapshot) => void;
|
||||
/** 加载草稿时把 Step 2 状态打回去。 */
|
||||
hydrateStep2: (snapshot: Step2Snapshot) => void;
|
||||
hydrateStep2: (snapshot: DraftStep2Snapshot, step3: DraftStep3Snapshot | null) => void;
|
||||
/** 加载草稿时把 Step 3 结果打回去(无则传 null 清空)。 */
|
||||
hydrateStep3?: (snapshot: DraftStep3Snapshot | null) => void;
|
||||
/** 加载草稿前先中断所有 Step 1 / Step 2 in-flight。 */
|
||||
@@ -187,7 +187,7 @@ export function useDraftPersistence(
|
||||
}, [refreshList]);
|
||||
|
||||
/** 当前会话是否有值得保存的内容。 */
|
||||
const hasSavableContent = useCallback((step1: Step1Snapshot, step2: Step2Snapshot): boolean => {
|
||||
const hasSavableContent = useCallback((step1: Step1Snapshot, step2: DraftStep2Snapshot): boolean => {
|
||||
const hasUserMessage = step1.interactiveMessages.some((m) => m.sender === "user");
|
||||
return hasUserMessage || step2.step2RoundtableDialogues.length > 0;
|
||||
}, []);
|
||||
@@ -350,7 +350,12 @@ export function useDraftPersistence(
|
||||
suppressStep2AutoSaveRef.current = true;
|
||||
|
||||
if (draft.step1) hydrateStep1(draft.step1 as Step1Snapshot);
|
||||
if (draft.step2) hydrateStep2(draft.step2 as Step2Snapshot);
|
||||
if (draft.step2) {
|
||||
hydrateStep2(
|
||||
draft.step2 as DraftStep2Snapshot,
|
||||
(draft.step3 as DraftStep3Snapshot | null) ?? null,
|
||||
);
|
||||
}
|
||||
// 恢复 Step 3 报告(无则传 null 清掉上一条草稿残留),进入「结果绘制」即可看回。
|
||||
hydrateStep3?.((draft.step3 as DraftStep3Snapshot | null) ?? null);
|
||||
setStep2InitGate(draft.furthestStep >= 2);
|
||||
|
||||
@@ -336,6 +336,7 @@ export function useStep2Orchestration(
|
||||
const [selectedAgents, setSelectedAgents] = useState<SelectedAgent[]>(
|
||||
DEFAULT_SELECTED_AGENTS,
|
||||
);
|
||||
const [initNonce, setInitNonce] = useState(0);
|
||||
const [threadIds, setThreadIds] = useState<ThreadIdMap | null>(null);
|
||||
const [coordinatorName, setCoordinatorName] = useState<string | null>(null);
|
||||
const [isInitializing, setIsInitializing] = useState<boolean>(false);
|
||||
@@ -1538,7 +1539,7 @@ export function useStep2Orchestration(
|
||||
// 只关心 currentStep 切换 —— buildInitialIntent / runOrchestration / 等
|
||||
// 都是 useCallback / ref 包装,变化不应触发重新 init。
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [currentStep]);
|
||||
}, [currentStep, initNonce]);
|
||||
|
||||
// 挂起 / 恢复:与上面的 init effect 独立。`isRoundTableRunning` toggle 时:
|
||||
// - false → stopOrchestration
|
||||
@@ -1738,6 +1739,7 @@ export function useStep2Orchestration(
|
||||
|
||||
const resetInitGate = useCallback(() => {
|
||||
step2InitRef.current = false;
|
||||
setInitNonce((n) => n + 1);
|
||||
}, []);
|
||||
|
||||
const hydrateFromDraft = useCallback(
|
||||
|
||||
@@ -71,6 +71,8 @@ export interface RoundtableJob {
|
||||
error: string | null;
|
||||
/** 研讨对话记录(leader 说明 + 各席位交付),跑完 / 进行中都可看真实内容。 */
|
||||
dialogues: JobDialogue[];
|
||||
/** Step3 报告快照(done 时有):{ html, generatedAt, model, summary }。 */
|
||||
step3: { html: string; generatedAt?: string; model?: string; summary?: string } | null;
|
||||
createdAt: string | null;
|
||||
updatedAt: string | null;
|
||||
}
|
||||
|
||||
@@ -60,12 +60,19 @@ import { DispatchChainJobModal } from "@/roundtable-planning/components/Dispatch
|
||||
import { Step1Panel } from "@/roundtable-planning/components/Step1Panel";
|
||||
import { Step2Panel } from "@/roundtable-planning/components/Step2Panel";
|
||||
import { Step3Panel } from "@/roundtable-planning/components/Step3Panel";
|
||||
import type { DraftStep3Snapshot } from "@/roundtable-planning/api/drafts";
|
||||
import type {
|
||||
DraftStep2RunSnapshot,
|
||||
DraftStep2Snapshot,
|
||||
DraftStep3Snapshot,
|
||||
} from "@/roundtable-planning/api/drafts";
|
||||
import {
|
||||
useStep1Intent,
|
||||
type InteractiveMessage,
|
||||
} from "@/roundtable-planning/hooks/useStep1Intent";
|
||||
import { useStep2Orchestration } from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||
import {
|
||||
useStep2Orchestration,
|
||||
type Step2Snapshot,
|
||||
} from "@/roundtable-planning/hooks/useStep2Orchestration";
|
||||
import { useDraftPersistence } from "@/roundtable-planning/hooks/useDraftPersistence";
|
||||
import {
|
||||
BUILTIN_AGENT_META,
|
||||
@@ -101,11 +108,15 @@ export default function RoundtablePlanningPage() {
|
||||
// chain 模式来源链条(仅展示/持久化用)。
|
||||
const [chainMeta, setChainMeta] = useState<{ id: string; title: string } | null>(null);
|
||||
const [step2SidebarExpanded, setStep2SidebarExpanded] = useState(true);
|
||||
const [step2Runs, setStep2Runs] = useState<DraftStep2RunSnapshot[]>([]);
|
||||
const [activeStep2RunId, setActiveStep2RunId] = useState<string | null>(null);
|
||||
|
||||
// Step 3「结果绘制」结果 —— 持久化进草稿 step3。绘制完成由 onReportSaved 写入;
|
||||
// 加载草稿时 hydrateStep3 恢复;新建任务清空。step3SavedTick 递增触发一次草稿自动保存。
|
||||
const [step3Report, setStep3Report] = useState<DraftStep3Snapshot | null>(null);
|
||||
const [step3SavedTick, setStep3SavedTick] = useState<number>(0);
|
||||
const step2RunsRef = useRef<DraftStep2RunSnapshot[]>([]);
|
||||
const activeStep2RunIdRef = useRef<string | null>(null);
|
||||
|
||||
// 角色面板(LeftSidebar)。原页面把它当成两步共用的展示数据,这里继续放在
|
||||
// 主页 —— Step 1 / Step 2 / Step 3 都会读 / 写它,任何 hook 单独持有都不合适。
|
||||
@@ -207,6 +218,173 @@ export default function RoundtablePlanningPage() {
|
||||
[step2.step2RoundtableDialogues],
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
step2RunsRef.current = step2Runs;
|
||||
}, [step2Runs]);
|
||||
|
||||
useEffect(() => {
|
||||
activeStep2RunIdRef.current = activeStep2RunId;
|
||||
}, [activeStep2RunId]);
|
||||
|
||||
const buildCurrentStep2Snapshot = useCallback(
|
||||
(): DraftStep2Snapshot => ({
|
||||
selectedAgents: step2.selectedAgents,
|
||||
threadIds: step2.threadIds,
|
||||
coordinatorName: step2.coordinatorName,
|
||||
step2RoundtableDialogues: step2.step2RoundtableDialogues,
|
||||
lastLeaderContent: step2.lastLeaderContent,
|
||||
hasConsensus: step2.hasConsensus,
|
||||
consensusPercentage: step2.consensusPercentage,
|
||||
budgetLimit: step2.budgetLimit,
|
||||
seatStubMessages: step2.seatStubMessages,
|
||||
orchestrationMode,
|
||||
chain: chainMeta,
|
||||
}),
|
||||
[
|
||||
chainMeta,
|
||||
orchestrationMode,
|
||||
step2.budgetLimit,
|
||||
step2.consensusPercentage,
|
||||
step2.coordinatorName,
|
||||
step2.hasConsensus,
|
||||
step2.lastLeaderContent,
|
||||
step2.seatStubMessages,
|
||||
step2.selectedAgents,
|
||||
step2.step2RoundtableDialogues,
|
||||
step2.threadIds,
|
||||
],
|
||||
);
|
||||
|
||||
const snapshotToRun = useCallback(
|
||||
(
|
||||
snapshot: DraftStep2Snapshot,
|
||||
fallback: Partial<Pick<DraftStep2RunSnapshot, "id" | "title" | "createdAt" | "source">> = {},
|
||||
): DraftStep2RunSnapshot => ({
|
||||
id: fallback.id ?? `step2-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`,
|
||||
title: fallback.title ?? "第一次生成结果",
|
||||
createdAt: fallback.createdAt ?? new Date().toISOString(),
|
||||
source: fallback.source ?? snapshot.orchestrationMode ?? "initial",
|
||||
selectedAgents: snapshot.selectedAgents,
|
||||
threadIds: snapshot.threadIds,
|
||||
coordinatorName: snapshot.coordinatorName,
|
||||
step2RoundtableDialogues: snapshot.step2RoundtableDialogues ?? [],
|
||||
lastLeaderContent: snapshot.lastLeaderContent ?? "",
|
||||
hasConsensus: Boolean(snapshot.hasConsensus),
|
||||
consensusPercentage: snapshot.consensusPercentage ?? 0,
|
||||
budgetLimit: snapshot.budgetLimit ?? 0,
|
||||
seatStubMessages: snapshot.seatStubMessages ?? {},
|
||||
orchestrationMode: snapshot.orchestrationMode,
|
||||
chain: snapshot.chain ?? null,
|
||||
step3: step3Report,
|
||||
}),
|
||||
[step3Report],
|
||||
);
|
||||
|
||||
const mergeActiveRun = useCallback(
|
||||
(
|
||||
runs: DraftStep2RunSnapshot[],
|
||||
activeRunId: string | null,
|
||||
snapshot: DraftStep2Snapshot,
|
||||
report: DraftStep3Snapshot | null,
|
||||
): DraftStep2RunSnapshot[] => {
|
||||
if (!activeRunId) return runs;
|
||||
const idx = runs.findIndex((run) => run.id === activeRunId);
|
||||
if (idx === -1) return runs;
|
||||
const current = runs[idx];
|
||||
const next = snapshotToRun(snapshot, {
|
||||
id: current.id,
|
||||
title: current.title,
|
||||
createdAt: current.createdAt,
|
||||
source: current.source,
|
||||
});
|
||||
next.step3 = report;
|
||||
return runs.map((run, i) => (i === idx ? next : run));
|
||||
},
|
||||
[snapshotToRun],
|
||||
);
|
||||
|
||||
const getStep2SnapshotWithRuns = useCallback((): DraftStep2Snapshot => {
|
||||
const snapshot = buildCurrentStep2Snapshot();
|
||||
const runs = mergeActiveRun(
|
||||
step2RunsRef.current,
|
||||
activeStep2RunIdRef.current,
|
||||
snapshot,
|
||||
step3Report,
|
||||
);
|
||||
return {
|
||||
...snapshot,
|
||||
runs,
|
||||
activeRunId: activeStep2RunIdRef.current,
|
||||
};
|
||||
}, [buildCurrentStep2Snapshot, mergeActiveRun, step3Report]);
|
||||
|
||||
const normalizeStep2Runs = useCallback(
|
||||
(
|
||||
snapshot: DraftStep2Snapshot,
|
||||
fallbackStep3: DraftStep3Snapshot | null,
|
||||
): { runs: DraftStep2RunSnapshot[]; activeRunId: string | null } => {
|
||||
if (snapshot.runs?.length) {
|
||||
const activeRunId = snapshot.activeRunId ?? snapshot.runs[0]?.id ?? null;
|
||||
const runs = snapshot.runs.map((run) =>
|
||||
run.id === activeRunId && !run.step3 && fallbackStep3
|
||||
? { ...run, step3: fallbackStep3 }
|
||||
: run,
|
||||
);
|
||||
return {
|
||||
runs,
|
||||
activeRunId,
|
||||
};
|
||||
}
|
||||
|
||||
const hasLegacyContent =
|
||||
(snapshot.step2RoundtableDialogues?.length ?? 0) > 0 ||
|
||||
Boolean(snapshot.threadIds) ||
|
||||
Boolean(snapshot.lastLeaderContent);
|
||||
|
||||
if (!hasLegacyContent) return { runs: [], activeRunId: null };
|
||||
|
||||
const legacy = snapshotToRun(snapshot, {
|
||||
id: "legacy-run",
|
||||
title: "第一次生成结果",
|
||||
source: snapshot.orchestrationMode ?? "initial",
|
||||
});
|
||||
legacy.step3 = fallbackStep3;
|
||||
return { runs: [legacy], activeRunId: legacy.id };
|
||||
},
|
||||
[snapshotToRun],
|
||||
);
|
||||
|
||||
const hydrateStep2Run = useCallback(
|
||||
(run: DraftStep2RunSnapshot) => {
|
||||
setOrchestrationMode(run.orchestrationMode ?? "recommend");
|
||||
setChainMeta(run.chain ?? null);
|
||||
setStep3Report(run.step3 ?? null);
|
||||
step2.hydrateFromDraft(run as unknown as Step2Snapshot);
|
||||
},
|
||||
[step2],
|
||||
);
|
||||
|
||||
const handleSelectStep2Run = useCallback(
|
||||
(runId: string) => {
|
||||
if (runId === activeStep2RunIdRef.current) return;
|
||||
step2.stopOrchestration();
|
||||
const currentSnapshot = buildCurrentStep2Snapshot();
|
||||
const syncedRuns = mergeActiveRun(
|
||||
step2RunsRef.current,
|
||||
activeStep2RunIdRef.current,
|
||||
currentSnapshot,
|
||||
step3Report,
|
||||
);
|
||||
const target = syncedRuns.find((run) => run.id === runId);
|
||||
if (!target) return;
|
||||
setStep2Runs(syncedRuns);
|
||||
setActiveStep2RunId(runId);
|
||||
hydrateStep2Run(target);
|
||||
setCurrentStep(2);
|
||||
},
|
||||
[buildCurrentStep2Snapshot, hydrateStep2Run, mergeActiveRun, step2, step3Report],
|
||||
);
|
||||
|
||||
// 草稿 hook —— 同时依赖 step1 + step2 的 snapshot / hydrate。
|
||||
const drafts = useDraftPersistence({
|
||||
currentStep,
|
||||
@@ -223,26 +401,23 @@ export default function RoundtablePlanningPage() {
|
||||
interactiveMessages: step1.interactiveMessages,
|
||||
timelineEntries: step1.timelineEntries,
|
||||
}),
|
||||
getStep2Snapshot: () => ({
|
||||
selectedAgents: step2.selectedAgents,
|
||||
threadIds: step2.threadIds,
|
||||
coordinatorName: step2.coordinatorName,
|
||||
step2RoundtableDialogues: step2.step2RoundtableDialogues,
|
||||
lastLeaderContent: step2.lastLeaderContent,
|
||||
hasConsensus: step2.hasConsensus,
|
||||
consensusPercentage: step2.consensusPercentage,
|
||||
budgetLimit: step2.budgetLimit,
|
||||
seatStubMessages: step2.seatStubMessages,
|
||||
orchestrationMode,
|
||||
chain: chainMeta,
|
||||
}),
|
||||
getStep2Snapshot: getStep2SnapshotWithRuns,
|
||||
hydrateStep1: step1.hydrateFromDraft,
|
||||
// 包一层:先把页面持有的编排模式 / 链条恢复出来(老草稿无字段 → recommend),
|
||||
// 再交给 step2 hook 恢复其余 Step 2 状态。
|
||||
hydrateStep2: (snapshot) => {
|
||||
hydrateStep2: (snapshot, savedStep3) => {
|
||||
const normalized = normalizeStep2Runs(snapshot, savedStep3);
|
||||
setStep2Runs(normalized.runs);
|
||||
setActiveStep2RunId(normalized.activeRunId);
|
||||
const active =
|
||||
normalized.runs.find((run) => run.id === normalized.activeRunId) ?? null;
|
||||
if (active) {
|
||||
hydrateStep2Run(active);
|
||||
return;
|
||||
}
|
||||
setOrchestrationMode(snapshot.orchestrationMode ?? "recommend");
|
||||
setChainMeta(snapshot.chain ?? null);
|
||||
step2.hydrateFromDraft(snapshot);
|
||||
step2.hydrateFromDraft(snapshot as unknown as Step2Snapshot);
|
||||
},
|
||||
cancelAllStreams: () => {
|
||||
step2.abortAllInFlight();
|
||||
@@ -309,6 +484,38 @@ export default function RoundtablePlanningPage() {
|
||||
// 内置 agent 走 BUILTIN_AGENT_META;非内置用候选池传过来的 name。
|
||||
name: BUILTIN_AGENT_META[agent_id]?.displayName ?? name ?? agent_id,
|
||||
}));
|
||||
step2.stopOrchestration();
|
||||
const currentSnapshot = buildCurrentStep2Snapshot();
|
||||
const syncedRuns = mergeActiveRun(
|
||||
step2RunsRef.current,
|
||||
activeStep2RunIdRef.current,
|
||||
currentSnapshot,
|
||||
step3Report,
|
||||
);
|
||||
const nextRunNumber = syncedRuns.length + 1;
|
||||
const newRunId = `step2-run-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
||||
const newRun: DraftStep2RunSnapshot = {
|
||||
id: newRunId,
|
||||
title: `第 ${nextRunNumber} 次生成结果`,
|
||||
createdAt: new Date().toISOString(),
|
||||
source: result.mode,
|
||||
selectedAgents: normalized,
|
||||
threadIds: null,
|
||||
coordinatorName: null,
|
||||
step2RoundtableDialogues: [],
|
||||
lastLeaderContent: "",
|
||||
hasConsensus: false,
|
||||
consensusPercentage: 0,
|
||||
budgetLimit: step2.budgetLimit,
|
||||
seatStubMessages: {},
|
||||
orchestrationMode: result.mode,
|
||||
chain: result.mode === "chain" ? result.chain ?? null : null,
|
||||
step3: null,
|
||||
};
|
||||
setStep2Runs([...syncedRuns, newRun]);
|
||||
setActiveStep2RunId(newRunId);
|
||||
setStep3Report(null);
|
||||
step2.resetStep2State();
|
||||
step2.setSelectedAgents(normalized);
|
||||
step2.resetInitGate();
|
||||
// result.mode 决定 Step 2 编排:recommend = 总控自由派活;chain = 按链条顺序派活。
|
||||
@@ -322,7 +529,7 @@ export default function RoundtablePlanningPage() {
|
||||
: "成功确认!已切换至第二阶段圆桌研讨系统",
|
||||
);
|
||||
},
|
||||
[step2, triggerToast],
|
||||
[buildCurrentStep2Snapshot, mergeActiveRun, step2, step3Report, triggerToast],
|
||||
);
|
||||
|
||||
// 把页面重置回「第一步全新任务」——新建任务 + 后台挂起后都复用。
|
||||
@@ -331,6 +538,8 @@ export default function RoundtablePlanningPage() {
|
||||
const resetToStep1 = useCallback(() => {
|
||||
step2.stopOrchestration();
|
||||
step2.resetStep2State();
|
||||
setStep2Runs([]);
|
||||
setActiveStep2RunId(null);
|
||||
setOrchestrationMode("recommend");
|
||||
setChainMeta(null);
|
||||
setStep3Report(null);
|
||||
@@ -603,6 +812,9 @@ export default function RoundtablePlanningPage() {
|
||||
intentReady={step1.intentReady}
|
||||
sidebarExpanded={step2SidebarExpanded}
|
||||
setSidebarExpanded={setStep2SidebarExpanded}
|
||||
step2Runs={step2Runs}
|
||||
activeStep2RunId={activeStep2RunId}
|
||||
onSelectStep2Run={handleSelectStep2Run}
|
||||
/>
|
||||
|
||||
{/* MIDDLE VIEWPORT */}
|
||||
@@ -700,6 +912,11 @@ export default function RoundtablePlanningPage() {
|
||||
savedReport={step3Report}
|
||||
onReportSaved={(snap) => {
|
||||
setStep3Report(snap);
|
||||
setStep2Runs((prev) =>
|
||||
prev.map((run) =>
|
||||
run.id === activeStep2RunIdRef.current ? { ...run, step3: snap } : run,
|
||||
),
|
||||
);
|
||||
setStep3SavedTick((t) => t + 1);
|
||||
}}
|
||||
onReset={handleStartNewTask}
|
||||
@@ -1027,6 +1244,9 @@ interface LeftSidebarProps {
|
||||
intentReady: ReturnType<typeof useStep1Intent>["intentReady"];
|
||||
sidebarExpanded: boolean;
|
||||
setSidebarExpanded: React.Dispatch<React.SetStateAction<boolean>>;
|
||||
step2Runs: DraftStep2RunSnapshot[];
|
||||
activeStep2RunId: string | null;
|
||||
onSelectStep2Run: (runId: string) => void;
|
||||
}
|
||||
|
||||
function Step2SandboxSidebarSync({
|
||||
@@ -1063,6 +1283,9 @@ function LeftSidebar({
|
||||
intentReady,
|
||||
sidebarExpanded,
|
||||
setSidebarExpanded,
|
||||
step2Runs,
|
||||
activeStep2RunId,
|
||||
onSelectStep2Run,
|
||||
}: LeftSidebarProps) {
|
||||
const isStep2 = currentStep === 2;
|
||||
|
||||
@@ -1116,6 +1339,11 @@ function LeftSidebar({
|
||||
</div>
|
||||
|
||||
<HumanParticipationCard />
|
||||
<Step2RunSwitcher
|
||||
runs={step2Runs}
|
||||
activeRunId={activeStep2RunId}
|
||||
onSelect={onSelectStep2Run}
|
||||
/>
|
||||
<TaskOverviewCard currentStep={currentStep} />
|
||||
</>
|
||||
) : (
|
||||
@@ -1170,6 +1398,94 @@ function LeftSidebar({
|
||||
);
|
||||
}
|
||||
|
||||
function Step2RunSwitcher({
|
||||
runs,
|
||||
activeRunId,
|
||||
onSelect,
|
||||
}: {
|
||||
runs: DraftStep2RunSnapshot[];
|
||||
activeRunId: string | null;
|
||||
onSelect: (runId: string) => void;
|
||||
}) {
|
||||
if (runs.length === 0) return null;
|
||||
|
||||
const formatTime = (value: string) => {
|
||||
const date = new Date(value);
|
||||
if (Number.isNaN(date.getTime())) return "";
|
||||
return date.toLocaleString("zh-CN", {
|
||||
month: "2-digit",
|
||||
day: "2-digit",
|
||||
hour: "2-digit",
|
||||
minute: "2-digit",
|
||||
hour12: false,
|
||||
});
|
||||
};
|
||||
|
||||
const sourceLabel = (run: DraftStep2RunSnapshot) => {
|
||||
if (run.source === "chain") return "业务链条";
|
||||
if (run.source === "recommend") return "智能体推荐";
|
||||
return "历史结果";
|
||||
};
|
||||
|
||||
const statusLabel = (run: DraftStep2RunSnapshot) => {
|
||||
if (run.step3) return "已生成报告";
|
||||
if (run.hasConsensus) return "已达成共识";
|
||||
if ((run.step2RoundtableDialogues?.length ?? 0) > 0) return "研讨记录";
|
||||
return "待开始";
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="bg-white border border-slate-200/50 rounded-2xl p-4 shadow-[0_2px_8px_rgba(0,0,0,0.01)]">
|
||||
<div className="flex items-center gap-2 border-b border-slate-100 pb-2.5 mb-3">
|
||||
<ListOrdered className="w-4 h-4 text-primary" strokeWidth={2.5} />
|
||||
<h2 className="text-xs font-bold text-slate-900 uppercase tracking-widest">生成结果</h2>
|
||||
</div>
|
||||
<div className="space-y-2">
|
||||
{runs.map((run) => {
|
||||
const active = run.id === activeRunId;
|
||||
return (
|
||||
<button
|
||||
key={run.id}
|
||||
type="button"
|
||||
onClick={() => onSelect(run.id)}
|
||||
className={cn(
|
||||
"w-full rounded-xl border p-2.5 text-left transition-colors",
|
||||
active
|
||||
? "border-primary/40 bg-primary/5 text-slate-900"
|
||||
: "border-slate-100 bg-slate-50/60 text-slate-600 hover:border-primary/20 hover:bg-white",
|
||||
)}
|
||||
>
|
||||
<div className="flex items-center justify-between gap-2">
|
||||
<span className="truncate text-xs font-extrabold">{run.title}</span>
|
||||
{active ? (
|
||||
<CheckCircle className="h-3.5 w-3.5 shrink-0 text-primary" />
|
||||
) : null}
|
||||
</div>
|
||||
<div className="mt-1.5 flex items-center justify-between gap-2 text-[10px] font-semibold">
|
||||
<span className="truncate text-slate-400">{sourceLabel(run)}</span>
|
||||
<span className="shrink-0 text-slate-400">{formatTime(run.createdAt)}</span>
|
||||
</div>
|
||||
<div className="mt-2 flex items-center gap-1.5">
|
||||
<span
|
||||
className={cn(
|
||||
"h-1.5 w-1.5 rounded-full",
|
||||
run.step3
|
||||
? "bg-emerald-500"
|
||||
: run.hasConsensus
|
||||
? "bg-primary"
|
||||
: "bg-slate-300",
|
||||
)}
|
||||
/>
|
||||
<span className="text-[10px] font-bold text-slate-500">{statusLabel(run)}</span>
|
||||
</div>
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function TaskOverviewCard({ currentStep }: { currentStep: number }) {
|
||||
return (
|
||||
<div className="bg-white border border-slate-200/50 rounded-2xl p-4 shadow-[0_2px_8px_rgba(0,0,0,0.01)]">
|
||||
|
||||
Reference in new issue
Block a user