From 7bd032ec77b35aa0283fa770f0d150d0f87a833a Mon Sep 17 00:00:00 2001 From: fengxiangdebaozi <10845145+fengxiangdebaozi@user.noreply.gitee.com> Date: Wed, 22 Jul 2026 17:44:34 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=AF=E6=8C=81=E5=B2=97=E4=BD=8D?= =?UTF-8?q?=E4=BC=9A=E5=95=86=E4=BB=BB=E5=8A=A1=E5=92=8C=E6=84=8F=E5=9B=BE?= =?UTF-8?q?=E7=BC=96=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../src/position-roundtable/api/sessions.ts | 22 +- .../pages/PositionRoundtablePage.tsx | 398 +++++++++++++++++- 2 files changed, 408 insertions(+), 12 deletions(-) diff --git a/frontend-web/src/position-roundtable/api/sessions.ts b/frontend-web/src/position-roundtable/api/sessions.ts index 1c5731e3..753edc47 100644 --- a/frontend-web/src/position-roundtable/api/sessions.ts +++ b/frontend-web/src/position-roundtable/api/sessions.ts @@ -128,7 +128,7 @@ function toNode(node: ServerNode): PositionRoundtableNode { } function toSession(session: ServerSession): PositionRoundtableSession { - return { + const result: PositionRoundtableSession = { id: session.id, externalTaskId: session.external_task_id, taskSnapshot: session.task_snapshot ?? {}, @@ -143,8 +143,11 @@ function toSession(session: ServerSession): PositionRoundtableSession { status: session.status, createdAt: session.created_at, updatedAt: session.updated_at, - nodes: session.nodes?.map(toNode), }; + if (Array.isArray(session.nodes)) { + result.nodes = session.nodes.map(toNode); + } + return result; } export async function listPositionSessions(externalTaskId?: string): Promise { @@ -182,6 +185,21 @@ export async function getPositionSession(sessionId: string): Promise, +): Promise { + const result = await readJson( + await apiFetch(`${BASE()}/sessions/${encodeURIComponent(sessionId)}/task`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ task_snapshot: taskSnapshot }), + }), + "update position task", + ); + return toSession(result); +} + export async function savePositionIntent( sessionId: string, input: { intentThreadId?: string | null; intentSnapshot: object | null }, diff --git a/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx b/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx index 602bbc74..a907c2e0 100644 --- a/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx +++ b/frontend-web/src/position-roundtable/pages/PositionRoundtablePage.tsx @@ -19,6 +19,7 @@ import { Layers3, Loader2, MessageSquareText, + PencilLine, Plus, RotateCcw, Search, @@ -86,6 +87,7 @@ import { preparePositionSummary, rejectPositionNode, savePositionIntent, + updatePositionTaskSnapshot, type PositionRoundtableNode, type PositionRoundtableSession, } from "@/position-roundtable/api/sessions"; @@ -116,6 +118,11 @@ type PositionArtifactPreview = { mimeType: string | null; auto?: boolean; }; +type IntentEditDraft = { + objective: string; + constraints: string; + assumptions: string; +}; function sandboxThreadSignature(thread: BaseStream): string { const messages = thread.messages ?? []; @@ -347,6 +354,14 @@ function artifactName(artifact: Record | null | undefined): str return path.split("/").filter(Boolean).at(-1) ?? ""; } +function mergePositionSession( + current: PositionRoundtableSession | null, + next: PositionRoundtableSession, +): PositionRoundtableSession { + if (!current || current.id !== next.id || Array.isArray(next.nodes)) return next; + return { ...current, ...next, nodes: current.nodes }; +} + function parseIntentSummary(snapshot: Record | null | undefined): IntentSummary | null { if (!snapshot || typeof snapshot.objective !== "string" || !Array.isArray(snapshot.constraints)) { return null; @@ -430,6 +445,17 @@ export default function PositionRoundtablePage({ signature: string; } | null>(null); const [flowDetailItem, setFlowDetailItem] = useState(null); + const [taskEditorOpen, setTaskEditorOpen] = useState(false); + const [taskDraft, setTaskDraft] = useState(fallbackTask); + const [taskSaving, setTaskSaving] = useState(false); + const [intentEditorOpen, setIntentEditorOpen] = useState(false); + const [intentDraft, setIntentDraft] = useState({ + objective: "", + constraints: "", + assumptions: "", + }); + const [intentSaving, setIntentSaving] = useState(false); + const [intentCompletedAt, setIntentCompletedAt] = useState(""); const activePosition = POSITIONS.find((item) => item.id === positionId)!; useEffect(() => { artifactsCtxRef.current = artifactsCtx; @@ -482,6 +508,140 @@ export default function PositionRoundtablePage({ }); const hydratedIntentSessionRef = useRef(null); const persistedIntentRef = useRef(null); + const isArchivedSession = positionSession?.status === "archived"; + + const syncSessionUrl = useCallback( + (sessionId: string, nextTaskId: string) => { + const next = new URLSearchParams(searchParamsString); + next.set("taskId", nextTaskId); + next.set("sessionId", sessionId); + next.delete("new"); + setSearchParams(next, { replace: true }); + }, + [searchParamsString, setSearchParams], + ); + + const ensureEditableSession = useCallback( + async (taskSnapshot: PositionTask): Promise => { + if (positionSession) return positionSession; + const created = await createPositionSession({ + externalTaskId: taskSnapshot.id || undefined, + taskSnapshot: { ...taskSnapshot }, + }); + setPositionSession(created); + syncSessionUrl(created.id, taskSnapshot.id); + return created; + }, + [positionSession, syncSessionUrl], + ); + + const openTaskEditor = useCallback(() => { + if (isArchivedSession) return; + setTaskDraft({ ...task }); + setTaskEditorOpen(true); + }, [isArchivedSession, task]); + + const handleSaveTaskEdit = useCallback(async () => { + if (isArchivedSession) { + toast.warning("已归档的会话不能编辑任务信息"); + return; + } + const title = taskDraft.title.trim(); + if (!title) { + toast.warning("请填写任务名称"); + return; + } + const nextTask: PositionTask = { + ...task, + title, + description: taskDraft.description.trim(), + direction: taskDraft.direction.trim() || task.direction, + }; + setTaskSaving(true); + try { + const saved = positionSession + ? await updatePositionTaskSnapshot(positionSession.id, { ...nextTask }) + : await createPositionSession({ + externalTaskId: nextTask.id || undefined, + taskSnapshot: { ...nextTask }, + }); + setPositionSession((current) => mergePositionSession(current, saved)); + syncSessionUrl(saved.id, nextTask.id); + setTaskEditorOpen(false); + toast.success("任务信息已更新"); + } catch (error) { + toast.error(`任务信息保存失败:${error instanceof Error ? error.message : String(error)}`); + } finally { + setTaskSaving(false); + } + }, [isArchivedSession, positionSession, syncSessionUrl, task, taskDraft]); + + const openIntentEditor = useCallback(() => { + if (isArchivedSession) return; + const currentIntent = frozenIntent ?? intent.intentReady; + setIntentDraft({ + objective: currentIntent?.objective ?? "", + constraints: (currentIntent?.constraints ?? []).join("\n"), + assumptions: (currentIntent?.assumptions ?? []).join("\n"), + }); + setIntentEditorOpen(true); + }, [frozenIntent, intent.intentReady, isArchivedSession]); + + const handleSaveIntentEdit = useCallback(async () => { + if (isArchivedSession) { + toast.warning("已归档的会话不能编辑意图理解"); + return; + } + const objective = intentDraft.objective.trim(); + if (!objective) { + toast.warning("请填写任务意图"); + return; + } + const splitLines = (value: string) => + value + .split(/\r?\n/) + .map((item) => item.trim()) + .filter(Boolean); + const nextIntent: IntentSummary = { + objective, + constraints: splitLines(intentDraft.constraints), + assumptions: splitLines(intentDraft.assumptions), + }; + + setIntentSaving(true); + try { + const baseSession = await ensureEditableSession(task); + const intentThreadId = baseSession.intentThreadId ?? intent.intentThreadId; + const saved = await savePositionIntent(baseSession.id, { + intentThreadId, + intentSnapshot: nextIntent, + }); + persistedIntentRef.current = `${saved.id}:${intentThreadId ?? ""}:${JSON.stringify(nextIntent)}`; + setPositionSession((current) => mergePositionSession(current, saved)); + intent.hydrateFromDraft({ + intentThreadId: intentThreadId ?? intent.intentThreadId, + intentReady: nextIntent, + interactiveMessages: intent.interactiveMessages, + timelineEntries: [], + }); + if (!intentCompletedAt) setIntentCompletedAt(timeLabel()); + setIntentEditorOpen(false); + toast.success("意图理解已更新"); + } catch (error) { + toast.error(`意图理解保存失败:${error instanceof Error ? error.message : String(error)}`); + } finally { + setIntentSaving(false); + } + }, [ + ensureEditableSession, + intent, + intentCompletedAt, + intentDraft.assumptions, + intentDraft.constraints, + intentDraft.objective, + isArchivedSession, + task, + ]); const { data: chains = [], isLoading: chainsLoading } = useQuery({ queryKey: ["position-roundtable", "chains"], @@ -550,6 +710,25 @@ export default function PositionRoundtablePage({ const nextNodeKey = activeNode?.nodeKey ?? null; if (activeNodeKey !== nextNodeKey) setActiveNodeKey(nextNodeKey); }, [activeNode?.nodeKey, activeNodeKey]); + useEffect(() => { + if ( + !sessionIdFromUrl || + !positionSession || + Array.isArray(positionSession.nodes) || + positionSession.status === "intent_pending" + ) { + return; + } + let cancelled = false; + void getPositionSession(sessionIdFromUrl) + .then((session) => { + if (!cancelled) setPositionSession(session); + }) + .catch(() => undefined); + return () => { + cancelled = true; + }; + }, [positionSession, sessionIdFromUrl]); const handleNodeUpdate = useCallback((updatedNode: PositionRoundtableNode) => { setPositionSession((current) => { if (!current?.nodes) return current; @@ -597,7 +776,7 @@ export default function PositionRoundtablePage({ }); if (cancelled) return; persistedIntentRef.current = `${saved.id}:${intentThreadId}:${snapshotKey}`; - setPositionSession((current) => (current?.id === saved.id ? { ...current, ...saved } : saved)); + setPositionSession((current) => mergePositionSession(current, saved)); if (!sessionIdFromUrl) { const next = new URLSearchParams(searchParamsString); next.set("taskId", task.id); @@ -756,7 +935,6 @@ export default function PositionRoundtablePage({ }, [sessionIdFromUrl]); const { agent: selectedAgent } = useAgent(selectedAgentId); - const [intentCompletedAt, setIntentCompletedAt] = useState(""); useEffect(() => { if (intent.intentReady && !intentCompletedAt) setIntentCompletedAt(timeLabel()); if (!intent.intentReady && intentCompletedAt) setIntentCompletedAt(""); @@ -857,7 +1035,7 @@ export default function PositionRoundtablePage({ if (!window.confirm("归档后该会话将只读,是否继续?")) return; void archivePositionSession(positionSession.id) .then((session) => { - setPositionSession((current) => (current ? { ...current, ...session } : session)); + setPositionSession((current) => mergePositionSession(current, session)); toast.success("会话已归档,可新建会话继续研讨"); }) .catch((error) => { @@ -1163,11 +1341,19 @@ export default function PositionRoundtablePage({
); @@ -1732,10 +1938,39 @@ function DeliveryOverviewCard({ item }: { item: DeliveryOverviewItem }) { ); } -/** 仅展示岗位会商任务,不在会商页编辑任务。 */ -function TaskInformationCard({ task }: { task: PositionTask }) { +/** 岗位会商内的任务快照展示,可按当前 Session 独立编辑。 */ +function HoverEditButton({ + title, + onClick, +}: { + title: string; + onClick: () => void; +}) { return ( -
+ + ); +} + +function TaskInformationCard({ + task, + editable, + onEdit, +}: { + task: PositionTask; + editable?: boolean; + onEdit?: () => void; +}) { + return ( +
+ {editable && onEdit && }
@@ -1774,6 +2009,72 @@ function TaskInformationCard({ task }: { task: PositionTask }) { ); } +function TaskInformationEditDialog({ + open, + task, + saving, + onOpenChange, + onChange, + onSubmit, +}: { + open: boolean; + task: PositionTask; + saving: boolean; + onOpenChange: (open: boolean) => void; + onChange: (task: PositionTask) => void; + onSubmit: () => void; +}) { + return ( + { + if (!saving) onOpenChange(nextOpen); + }} + > + + + 编辑任务信息 + +
+ +