代码合并

This commit is contained in:
董宏凯 committed 2026-09-08 09:40:31 +08:00
1 parent b0970de677
commit 8859c13cfb
26 files changed
+1707 -334

No files matched your search

@@ -1,9 +1,12 @@
"use client";
import { ArrowLeft, FileText, MessageSquarePlus, Square } from "lucide-react";
import { ArrowLeft, ClipboardList, FileText, MessageSquarePlus, Square } from "lucide-react";
import type { PagePhase } from "@/report-collaboration/state/selectors";
/** 计划模式覆盖的阶段:第一条消息起到工作流确认执行前。 */
const PLAN_MODE_PHASES = new Set<PagePhase>(["clarifying", "planning", "proposal_ready"]);
const PHASE_BADGES: Record<PagePhase, { label: string; color: string; bg: string }> = {
empty: { label: "新会话", color: "var(--rc-text-secondary)", bg: "transparent" },
clarifying: { label: "澄清需求", color: "var(--rc-waiting)", bg: "rgba(217, 119, 6, 0.1)" },
@@ -51,6 +54,16 @@ export function ReportCollaborationHeader({
<span className="rc-header-title" title={title}>
报告协作 / {title}
</span>
{PLAN_MODE_PHASES.has(phase) ? (
<span
className="rc-chip"
title="规划师正在梳理需求并规划工作流,确认后开始执行"
style={{ background: "rgba(37, 99, 235, 0.1)", color: "var(--rc-accent)" }}
>
<ClipboardList className="size-3" aria-hidden />
计划模式
</span>
) : null}
<span className="rc-chip" style={{ background: badge.bg, color: badge.color }}>
{badge.label}
</span>
@@ -156,8 +156,23 @@ function CanvasInner({ plan, nodeStates, selectedNodeId, onSelectNode, emptyHint
const edges: Edge[] = useMemo(() => {
if (!plan) return [];
return plan.edges.map((edge, index) => {
const targetState = nodeStates[edge.target];
const active = targetState?.status === "running" || targetState?.status === "validating";
// 边语义 = 数据正在流入 target 节点:target 执行中 → 流动动画;
// 失败红虚线 / 等待橙 / 已被替代降灰,其余静态。
const targetStatus = nodeStates[edge.target]?.status;
let className = "rc-edge";
let stroke = "var(--rc-flow-edge)";
if (targetStatus === "running" || targetStatus === "validating") {
className = "rc-edge-active";
stroke = "var(--rc-running)";
} else if (targetStatus === "failed") {
className = "rc-edge-failed";
stroke = "var(--rc-danger)";
} else if (targetStatus === "awaiting_input") {
className = "rc-edge-waiting";
stroke = "var(--rc-waiting)";
} else if (targetStatus === "superseded" || targetStatus === "interrupted" || targetStatus === "cancelled") {
className = "rc-edge-dimmed";
}
return {
id: `${edge.source}->${edge.target}-${index}`,
source: edge.source,
@@ -165,11 +180,8 @@ function CanvasInner({ plan, nodeStates, selectedNodeId, onSelectNode, emptyHint
label: edge.label ?? undefined,
type: "default",
animated: false,
className: active ? "rc-edge-active" : "rc-edge",
style: {
stroke: active ? "var(--rc-running)" : "var(--rc-flow-edge)",
strokeWidth: 2,
},
className,
style: { stroke, strokeWidth: 2 },
};
});
}, [plan, nodeStates]);
@@ -0,0 +1,52 @@
"use client";
import { ClipboardCheck } from "lucide-react";
import type { ReportRequirementSnapshot } from "@/report-collaboration/api/types";
interface RequirementSummaryCardProps {
requirement: ReportRequirementSnapshot;
/** 需求仍可修改的提示(生成方案前);方案已出后为 false。 */
editable?: boolean;
}
function Field({ label, value }: { label: string; value: string | null | undefined }) {
if (!value) return null;
return (
<div className="flex gap-2 text-[12px] leading-5">
<span className="w-16 shrink-0 text-[var(--rc-text-secondary)]">{label}</span>
<span className="min-w-0 flex-1 break-words">{value}</span>
</div>
);
}
/** 计划模式 · 需求摘要卡:结构化展示规划师确认的需求快照。 */
export function RequirementSummaryCard({ requirement, editable = true }: RequirementSummaryCardProps) {
const angles = (requirement.required_angles ?? []).filter(Boolean).join("、");
return (
<div className="rc-card mx-auto w-full max-w-[560px] rounded-xl border border-[var(--rc-border)] bg-[var(--rc-bg-panel)] p-3.5">
<div className="mb-2 flex items-center gap-2">
<ClipboardCheck className="size-4" style={{ color: "var(--rc-accent)" }} aria-hidden />
<span className="text-[13px] font-semibold">需求摘要 · v{requirement.revision}</span>
<span className="rc-chip ml-auto" style={{ background: "rgba(37,99,235,.1)", color: "var(--rc-accent)" }}>
计划模式
</span>
</div>
<div className="flex flex-col gap-1">
<Field label="主题" value={requirement.topic} />
<Field label="目的" value={requirement.objective} />
<Field label="读者" value={requirement.audience} />
<Field label="角度" value={angles || null} />
<Field label="时间范围" value={requirement.time_range} />
<Field label="地域" value={requirement.geography} />
<Field label="篇幅" value={requirement.length_target} />
<Field label="语气" value={requirement.tone} />
</div>
{editable ? (
<p className="mt-2.5 border-t border-[var(--rc-border)] pt-2 text-[11px] leading-4 text-[var(--rc-text-secondary)]">
我正在基于这份需求为你规划工作流,稍候即可在对话中查看方案;想调整可直接在下方补充。
</p>
) : null}
</div>
);
}
@@ -4,7 +4,7 @@ import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import type { Message } from "@langchain/langgraph-sdk";
import { toast } from "sonner";
import { FileText, Loader2, Sparkles } from "lucide-react";
import { FileText, Sparkles } from "lucide-react";
import { ArtifactsProvider } from "@/components/workspace/artifacts";
import { ThreadContext } from "@/components/workspace/messages/context";
@@ -26,6 +26,7 @@ import {
} from "@/report-collaboration/components/ReportPreviewDialog";
import { ReportSandboxOverlay } from "@/report-collaboration/components/ReportSandboxOverlay";
import { ReportSpeakerShell } from "@/report-collaboration/components/ReportSpeakerShell";
import { RequirementSummaryCard } from "@/report-collaboration/components/RequirementSummaryCard";
import { SessionHistoryPanel } from "@/report-collaboration/components/SessionHistoryPanel";
import { useReportPanelLayout } from "@/report-collaboration/hooks/useReportPanelLayout";
import { useReportCollaborationMessages } from "@/report-collaboration/hooks/useReportCollaborationMessages";
@@ -35,6 +36,7 @@ import {
buildCommandConfirmCard,
buildPlanGroupCard,
buildReportAvailableCard,
buildRequirementReadyCard,
readReportCollaborationCard,
} from "@/report-collaboration/state/message-adapter";
import { resolveReportSpeaker } from "@/report-collaboration/state/speaker";
@@ -247,11 +249,23 @@ export function ReportCollaborationSessionPage({ sessionId }: { sessionId: strin
if (next) setFocusedAgentRunId(next);
}, [agentTabs, focusPinned, focusedAgentRunId, tabIds]);
// 虚拟业务卡:方案组(已出方案且 run 未开始)/ 命令确认 / 报告入口。
// 虚拟业务卡:需求摘要(计划模式)/ 方案组(已出方案且 run 未开始)/ 命令确认 / 报告入口。
// 采用方案后卡片保留,「开始协作」按钮随选定态启用;run 开始后消失。
const runStarted = Boolean(state.run && state.run.plan_id);
const extraCards = useMemo<Message[]>(() => {
const cards: Message[] = [];
if (
state.requirement &&
state.plans.length === 0 &&
!runStarted &&
Object.values(state.messagesById).some(
(message) =>
message.role === "ai" &&
(message.metadata as Record<string, unknown> | null | undefined)?.ready_to_plan === true,
)
) {
cards.push(buildRequirementReadyCard(state.requirement));
}
if (
state.plans.length > 0 &&
!runStarted &&
@@ -341,39 +355,16 @@ export function ReportCollaborationSessionPage({ sessionId }: { sessionId: strin
[sessionId, session],
);
// 澄清完成(协调者已写入需求版本)但还没生成过方案:给出显式「发起规划」入口。
const requirementReady = useMemo(
() =>
Object.values(state.messagesById).some(
(message) =>
message.role === "ai" &&
(message.metadata as Record<string, unknown> | null | undefined)?.ready_to_plan === true,
),
[state.messagesById],
);
const showPlanRequestEntry =
state.session?.status === "clarifying" && state.plans.length === 0 && requirementReady;
const [planRequestBusy, setPlanRequestBusy] = useState(false);
const handleRequestPlans = useCallback(async () => {
setPlanRequestBusy(true);
try {
await getReportCollaborationDriver().requestPlans(
sessionId,
state.session?.requirement_revision ?? null,
);
await session.refreshSnapshot();
toast.success("已生成候选方案,可在对话中选择");
} catch (err) {
toast.error(err instanceof Error ? err.message : String(err));
} finally {
setPlanRequestBusy(false);
}
}, [sessionId, session, state.session?.requirement_revision]);
// 方案生成由后端在规划师判定 ready 后自动衔接(同一次请求内规划),
// 前端无手动「生成候选方案」按钮;此处仅保留摘要卡的注入判定。
const businessCardSlot = useCallback(
(message: Message) => {
const card = readReportCollaborationCard(message);
if (!card) return null;
if (card.kind === "requirement_ready") {
return <RequirementSummaryCard requirement={card.requirement} editable={!runStarted} />;
}
if (card.kind === "plan_group") {
return (
<ReportPlanCards
@@ -414,7 +405,7 @@ export function ReportCollaborationSessionPage({ sessionId }: { sessionId: strin
}
return null;
},
[previewPlan, previewPlanId, session],
[previewPlan, previewPlanId, runStarted, session],
);
const speakerShell = useCallback(
@@ -565,43 +556,19 @@ export function ReportCollaborationSessionPage({ sessionId }: { sessionId: strin
: undefined
}
composer={
<div className="flex flex-col">
{showPlanRequestEntry ? (
<div className="mb-1.5 flex justify-center">
<button
type="button"
className="rc-btn"
style={{
background: "var(--rc-accent)",
color: "#fff",
borderColor: "var(--rc-accent)",
}}
disabled={planRequestBusy}
onClick={() => void handleRequestPlans()}
>
{planRequestBusy ? (
<Loader2 className="rc-spin size-3.5" aria-hidden />
) : (
<Sparkles className="size-3.5" aria-hidden />
)}
{planRequestBusy ? "正在生成候选方案…" : "需求已就绪,生成候选方案"}
</button>
</div>
) : null}
<ReportComposer
phase={phase}
busy={session.sendBusy}
target={composerTarget}
onClearTarget={() => setComposerTarget(null)}
onSend={(text) =>
void session.send(text, {
nodeId: composerTarget?.nodeId ?? null,
<ReportComposer
phase={phase}
busy={session.sendBusy}
target={composerTarget}
onClearTarget={() => setComposerTarget(null)}
onSend={(text) =>
void session.send(text, {
nodeId: composerTarget?.nodeId ?? null,
agentRunId: composerTarget?.agentRunId ?? null,
})
}
focusSignal={focusSignal}
/>
</div>
}
/>
}
@@ -21,6 +21,7 @@ import type {
CollaborationCommand,
CollaborationMessage,
ReportPlanCandidate,
ReportRequirementSnapshot,
ReportVersion,
} from "@/report-collaboration/api/types";
@@ -113,6 +114,11 @@ export type ReportCollaborationCardPayload =
selectedPlanId: string | null;
requirementRevision: number;
}
| {
kind: "requirement_ready";
requirement: ReportRequirementSnapshot;
revision: number;
}
| {
kind: "command_confirm";
command: CollaborationCommand;
@@ -163,6 +169,17 @@ export function buildPlanGroupCard(
});
}
/** 需求摘要卡(计划模式:需求已就绪、尚未生成方案)。 */
export function buildRequirementReadyCard(
requirement: ReportRequirementSnapshot,
): Message {
return buildCardMessage(`rc-card-requirement-r${requirement.revision}`, {
kind: "requirement_ready",
requirement,
revision: requirement.revision,
});
}
/** 命令确认卡(command.confirmation.required 之后)。 */
export function buildCommandConfirmCard(command: CollaborationCommand): Message {
return buildCardMessage(`rc-card-confirm-${command.command_id}`, {
@@ -850,6 +850,21 @@
animation: rc-edge-flow 0.5s linear infinite;
}
/* P1.4:失败红虚线 / 等待用户橙色 / 已替代·中断降灰 */
.rc-edge-failed .react-flow__edge-path {
stroke: var(--rc-danger);
stroke-dasharray: 4 4;
}
.rc-edge-waiting .react-flow__edge-path {
stroke: var(--rc-waiting);
}
.rc-edge-dimmed .react-flow__edge-path {
stroke: var(--rc-flow-edge);
opacity: 0.45;
}
@keyframes rc-edge-flow {
from {
stroke-dashoffset: 10;