From cc5ada1f3df2601654af29091d078f8035ca2c2e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E8=91=A3=E5=AE=8F=E5=87=AF?= Date: Tue, 9 Jun 2026 15:58:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(roundtable):=20=E7=AC=AC=E4=B8=80=E6=AD=A5?= =?UTF-8?q?=E8=AF=86=E5=88=AB=E5=87=BA=E7=9A=84=E6=84=8F=E5=9B=BE=E6=94=AF?= =?UTF-8?q?=E6=8C=81=E6=89=8B=E5=8A=A8=E7=BC=96=E8=BE=91=E5=B9=B6=E4=BF=9D?= =?UTF-8?q?=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 「当前意图摘要」卡新增编辑态:核心目标(必填)、约束、关键假设(可增删列表), 保存后由 useStep1Intent.updateIntentReady 覆盖权威 intentReady,下游(Step 2 种子消息 buildInitialIntent、侧栏摘要卡、step2Locked)随之生效,并落草稿持久化。 持久化用「计数 + effect」触发 handleManualSave,避免 enqueueSave 闭包在 setState flush 前读到旧意图的竞态(与 step2/step3 计数触发器同构)。聊天气泡内的绿色意图卡 保持为历史记录,右侧「当前意图摘要」为可编辑的当前值。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../hooks/useStep1Intent.ts | 13 + .../pages/RoundtablePlanningPage.tsx | 234 ++++++++++++++++-- 2 files changed, 231 insertions(+), 16 deletions(-) diff --git a/frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts b/frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts index 89ec494e..251214f1 100644 --- a/frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts +++ b/frontend-web/src/roundtable-planning/hooks/useStep1Intent.ts @@ -123,6 +123,12 @@ export interface UseStep1IntentReturn { clarifications: ClarificationPayload[], overrideByKey?: Record, ) => void; + /** + * 用户在「当前意图摘要」卡里手动编辑后保存:直接覆盖 `intentReady`。下游(Step 2 seed + * `buildInitialIntent`、左右侧栏摘要卡、step2Locked 判定)都读 `intentReady`,改这里即全局生效。 + * 持久化由调用方(页面)在更新后触发草稿保存。 + */ + updateIntentReady: (next: IntentSummary) => void; /** Abort 当前 intent 流。气泡尾部追加「(已暂停)」并 toast。 */ stopIntentStream: () => void; /** 从草稿加载:批量 setState + 标记 init 已完成,跳过自动 runStep1Init。 */ @@ -568,6 +574,12 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe setInteractiveMessages((prev) => [...prev, msg]); }, []); + const updateIntentReady = useCallback((next: IntentSummary) => { + // 用户手动改完意图 → 覆盖为权威值,并清掉「待澄清」态(已确认意图,无需再答问)。 + setIntentReady(next); + setPendingIntentClarificationMsgId(null); + }, []); + return { intentThreadId, intentReady, @@ -582,6 +594,7 @@ export function useStep1Intent(options: UseStep1IntentOptions): UseStep1IntentRe runStep1Init, sendIntentMessage, submitIntentClarifications, + updateIntentReady, stopIntentStream, hydrateFromDraft, appendLocalMessage, diff --git a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx index 1ee2c3f7..1d75ce14 100644 --- a/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx +++ b/frontend-web/src/roundtable-planning/pages/RoundtablePlanningPage.tsx @@ -41,12 +41,14 @@ import { ListOrdered, Loader2, Pencil, + Plus, Save, Settings, Sparkles, Trash2, Upload, Users, + X, } from "lucide-react"; import { useTheme } from "@/contexts/ThemeContext"; @@ -86,6 +88,7 @@ import { } from "@/roundtable-planning/lib/roundtable-constants"; import type { SeatSelectionResult } from "@/roundtable-planning/lib/business-chain"; import type { OrchestrationPlan } from "@/roundtable-planning/lib/dag-types"; +import type { IntentSummary } from "@/roundtable-planning/api/intent"; import { getJobByDraft, startJob } from "@/roundtable-planning/api/roundtable-jobs"; export default function RoundtablePlanningPage() { @@ -469,6 +472,26 @@ export default function RoundtablePlanningPage() { loadedFromDraftRefHolder.current = drafts.loadedFromDraftRef.current; }); + // ─── 用户手动编辑「当前意图摘要」并保存 ─────────────────────── + // 先 updateIntentReady 覆盖意图,再 bump 计数触发一次草稿保存。**必须用计数 + effect** + // 而非直接调 handleManualSave:enqueueSave 闭包捕获的 getStep1Snapshot 读的是本次渲染的 + // step1.intentReady,同步调用会在 setState flush 前存进旧意图(踩坑);放到 effect 里,等 + // 重渲染后再保存,getStep1Snapshot 才读到新值(与 step2/step3 的计数触发器同构)。 + const [intentEditTick, setIntentEditTick] = useState(0); + const handleSaveIntent = useCallback( + (next: IntentSummary) => { + step1.updateIntentReady(next); + setIntentEditTick((t) => t + 1); + }, + [step1], + ); + useEffect(() => { + if (intentEditTick === 0) return; + drafts.handleManualSave(); + // 仅在 tick 递增时保存;handleManualSave 取最新闭包(effect 跑在重渲染之后)。 + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [intentEditTick]); + // ─── 从侧边栏「多智能体会商」记录进入 ─────────────────────── // 侧边栏点击某条草稿时:不在本页 → 把 id 存进 sessionStorage 再跳转,挂载时消费; // 已在本页 → 直接派 `roundtable:open-draft` 事件。两条路径都走 handleLoadDraft。 @@ -1006,6 +1029,7 @@ export default function RoundtablePlanningPage() { intentReady={step1.intentReady} timelineEntries={step1.timelineEntries} triggerToast={triggerToast} + onSaveIntent={handleSaveIntent} /> @@ -1663,6 +1687,148 @@ function ModelSelectorCard({ ); } +/** + * 「当前意图摘要」编辑态:目标(单行/多行文本)、约束、关键假设(均为可增删的列表)。 + * 受控本地草稿,保存时过滤空行、要求目标非空,再上抛 {@link IntentSummary}。 + */ +function IntentSummaryEditor({ + initial, + title, + onSave, + onCancel, +}: { + initial: IntentSummary; + title: string; + onSave: (next: IntentSummary) => void; + onCancel: () => void; +}) { + const [objective, setObjective] = useState(initial.objective ?? ""); + const [constraints, setConstraints] = useState( + initial.constraints?.length ? [...initial.constraints] : [""], + ); + const [assumptions, setAssumptions] = useState( + initial.assumptions?.length ? [...initial.assumptions] : [], + ); + + const objectiveInvalid = objective.trim().length === 0; + + const updateRow = ( + setter: React.Dispatch>, + idx: number, + value: string, + ) => setter((prev) => prev.map((v, i) => (i === idx ? value : v))); + const removeRow = (setter: React.Dispatch>, idx: number) => + setter((prev) => prev.filter((_, i) => i !== idx)); + const addRow = (setter: React.Dispatch>) => + setter((prev) => [...prev, ""]); + + const handleSave = () => { + if (objectiveInvalid) return; + onSave({ + objective: objective.trim(), + constraints: constraints.map((c) => c.trim()).filter(Boolean), + assumptions: assumptions.map((a) => a.trim()).filter(Boolean), + }); + }; + + const rowList = ( + label: string, + rows: string[], + setter: React.Dispatch>, + placeholder: string, + ) => ( +
+ {label} +
+ {rows.map((row, i) => ( +
+ updateRow(setter, i, e.target.value)} + placeholder={placeholder} + maxLength={200} + className="min-w-0 flex-1 rounded-lg border border-slate-200 bg-white px-2.5 py-1.5 text-[11.5px] text-slate-700 focus:border-primary focus:outline-none focus:ring-1 focus:ring-[var(--brand-200)]/50" + /> + +
+ ))} + +
+
+ ); + + return ( +
+
+
+ +

+ 编辑{title} +

+
+
+ + +
+
+ +
+
+ 核心目标 +