diff --git a/apps/ai-game-creator-shell/src/App.tsx b/apps/ai-game-creator-shell/src/App.tsx index 8e51a380f..f4498546d 100644 --- a/apps/ai-game-creator-shell/src/App.tsx +++ b/apps/ai-game-creator-shell/src/App.tsx @@ -883,6 +883,14 @@ export function App({ // 直连回合的思考过程(流式):整段替换;回合结束/开始新回合/清空对话时一并清掉。 const [directCodexTransientReasoning, setDirectCodexTransientReasoning] = useState(''); + /** 实时回合里"某个工具首次出现时,已生成正文的长度"——用它把正文与工具交替排列。 */ + const [directToolTextOffsets, setDirectToolTextOffsets] = useState< + Record> + >({}); + const directToolTextOffsetsRef = useRef>>( + {}, + ); + const directAccumulatedTextLengthRef = useRef(0); const [ directCodexTransientReplyUpdatedAt, setDirectCodexTransientReplyUpdatedAt, @@ -984,6 +992,9 @@ export function App({ setDirectCodexProgressUpdatedAt(null); setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; directCodexTransientReplyRef.current = ''; setDirectCodexTransientReplyUpdatedAt(null); } @@ -1083,6 +1094,9 @@ export function App({ setDirectCodexProgressUpdatedAt(Date.now()); setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; directCodexTransientReplyRef.current = ''; setDirectCodexTransientReplyUpdatedAt(null); setProjectSupervisorRuntimeError(''); @@ -1117,6 +1131,9 @@ export function App({ activeDirectCodexTurnRef.current = null; setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; directCodexTransientReplyRef.current = ''; setDirectCodexTransientReplyUpdatedAt(null); return true; @@ -2188,7 +2205,29 @@ export function App({ ); } // 工具调用增量:字段可选,老事件(undefined)走原路径,行为不变。 + if (typeof payload.accumulatedText === 'string') { + directAccumulatedTextLengthRef.current = payload.accumulatedText.length; + } if (payload.toolCalls?.length) { + // 每个工具只记第一次出现时的正文长度:这就是它在对话流里的位置。 + const turnOffsets = + directToolTextOffsetsRef.current[payload.turnId] ?? {}; + let offsetsChanged = false; + for (const call of payload.toolCalls) { + const callId = (call as { id?: string }).id; + if (!callId || turnOffsets[callId] !== undefined) { + continue; + } + turnOffsets[callId] = directAccumulatedTextLengthRef.current; + offsetsChanged = true; + } + if (offsetsChanged) { + directToolTextOffsetsRef.current = { + ...directToolTextOffsetsRef.current, + [payload.turnId]: { ...turnOffsets }, + }; + setDirectToolTextOffsets(directToolTextOffsetsRef.current); + } applyDirectToolCalls( payload.toolCalls.map((call) => ({ ...call, @@ -2212,6 +2251,9 @@ export function App({ setDirectCodexProgressUpdatedAt(updatedAt); setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; setDirectCodexTransientReplyUpdatedAt(null); return; } @@ -6860,6 +6902,9 @@ export function App({ setDirectCodexProgress('正在等待陶泥儿开始'); setDirectCodexTransientReply(''); setDirectCodexTransientReasoning(''); + directToolTextOffsetsRef.current = {}; + setDirectToolTextOffsets({}); + directAccumulatedTextLengthRef.current = 0; setDirectCodexProgressUpdatedAt(Date.now()); directCodexTransientReplyRef.current = ''; setDirectCodexTransientReplyUpdatedAt(null); @@ -12594,6 +12639,7 @@ export function App({ if (projectSupervisorOnly) { return ( >; showDesignReasoning?: boolean; designReasoning?: string; designReasoningEntries?: DesignReasoningEntry[]; @@ -227,6 +229,7 @@ export function ProjectSupervisorView({ showProfessionalCollaboration = true, transientReply, transientReasoning = '', + directToolTextOffsets = {}, showDesignReasoning = false, designReasoning = '', designReasoningEntries = [], @@ -371,6 +374,69 @@ export function ProjectSupervisorView({ return `${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`; }; + /** + * 把实时正文与工具块按"工具出现时正文长度"交替排列: + * 文本段 → 该位置的工具块 → 下一段文本……同一切分点的工具合并成一个块。 + * 没有切分点(历史回合)时退化为"正文在前、工具块在后",与老项目兼容。 + */ + const renderLiveStream = (liveText: string, calls: typeof liveToolCalls) => { + const offsets = directToolTextOffsets[liveToolCallTurnId] ?? {}; + const groups = new Map(); + for (const call of calls) { + const offset = offsets[call.id]; + const key = Number.isFinite(offset) ? Number(offset) : liveText.length; + const bucket = groups.get(key) ?? []; + bucket.push(call); + groups.set(key, bucket); + } + if (groups.size === 0) { + return null; + } + const positions = [...groups.keys()].sort((left, right) => left - right); + const parts: ReactNode[] = []; + let cursor = 0; + for (const position of positions) { + const segment = liveText.slice(cursor, Math.max(cursor, position)); + if (segment.length > 0) { + parts.push( +
+ +
, + ); + } + const bucket = groups.get(position) ?? []; + parts.push( + , + ); + cursor = Math.max(cursor, position); + } + const tail = liveText.slice(cursor); + if (tail.length > 0) { + parts.push( +
+ +
, + ); + } + return parts; + }; + /** 整轮会话的结束时间与耗时(进行中时用 tick 驱的 now,所以秒数会实时跳动)。 */ const renderTurnUsage = (turnId: string) => { if (!turnId) { @@ -573,7 +639,10 @@ export function ProjectSupervisorView({ ) : null} {/* 直连回合的流式正文:恢复为对话区里的普通 assistant 消息(不再放进状态卡片), 这样"边生成边显示"和"状态卡片只放状态"两件事同时成立。 */} - {directCodex && transientReply ? ( + {directCodex && transientReply && liveToolCalls.length > 0 + ? renderLiveStream(transientReply, liveToolCalls) + : null} + {directCodex && transientReply && liveToolCalls.length === 0 ? (
) : null} - {liveToolCalls.length > 0 ? ( + {/* 有正文时,工具块的顺序由 renderLiveStream 决定,这里不再重复渲染 */} + {liveToolCalls.length > 0 && !(directCodex && transientReply) ? (