代码合并
This commit is contained in:
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;
|
||||
|
||||
Reference in new issue
Block a user