同步会话实时响应时的样式

This commit is contained in:
xintaofei
2026-03-25 00:27:44 +08:00
parent 421857f249
commit 0fc829a618
2 changed files with 50 additions and 30 deletions

View File

@@ -2379,7 +2379,7 @@ export const ContentPartsRenderer = memo(function ContentPartsRenderer({
role, role,
}: ContentPartsRendererProps) { }: ContentPartsRendererProps) {
return ( return (
<div className="space-y-2"> <div className="space-y-4">
{parts.map((part, i) => { {parts.map((part, i) => {
if (part.type === "text") { if (part.type === "text") {
return ( return (

View File

@@ -161,26 +161,41 @@ function formatLivePlanEntries(
return `Plan updated:\n${lines.join("\n")}` return `Plan updated:\n${lines.join("\n")}`
} }
function buildStreamingTurnFromLiveMessage( function buildStreamingTurnsFromLiveMessage(
conversationId: number, conversationId: number,
liveMessage: LiveMessage liveMessage: LiveMessage
): MessageTurn | null { ): MessageTurn[] {
const blocks: MessageTurn["blocks"] = [] // Split streaming content into multiple turns matching the historical
// pattern: each "round" (text/thinking + tool calls + tool results) is a
// separate turn. A new turn starts when a text/thinking/plan block appears
// after completed tool calls in the current group.
const groups: MessageTurn["blocks"][] = [[]]
let currentGroupHasCompletedTool = false
for (const block of liveMessage.content) { for (const block of liveMessage.content) {
const isContentBlock =
block.type === "text" || block.type === "thinking" || block.type === "plan"
if (isContentBlock && currentGroupHasCompletedTool) {
groups.push([])
currentGroupHasCompletedTool = false
}
const currentBlocks = groups[groups.length - 1]
switch (block.type) { switch (block.type) {
case "text": case "text":
if (block.text.length > 0) { if (block.text.length > 0) {
blocks.push({ type: "text", text: block.text }) currentBlocks.push({ type: "text", text: block.text })
} }
break break
case "thinking": case "thinking":
if (block.text.length > 0) { if (block.text.length > 0) {
blocks.push({ type: "thinking", text: block.text }) currentBlocks.push({ type: "thinking", text: block.text })
} }
break break
case "plan": { case "plan": {
blocks.push({ currentBlocks.push({
type: "thinking", type: "thinking",
text: formatLivePlanEntries(block.entries), text: formatLivePlanEntries(block.entries),
}) })
@@ -192,7 +207,7 @@ function buildStreamingTurnFromLiveMessage(
kind: block.info.kind, kind: block.info.kind,
rawInput: block.info.raw_input, rawInput: block.info.raw_input,
}) })
blocks.push({ currentBlocks.push({
type: "tool_use", type: "tool_use",
tool_use_id: block.info.tool_call_id, tool_use_id: block.info.tool_call_id,
tool_name: toolName, tool_name: toolName,
@@ -201,26 +216,31 @@ function buildStreamingTurnFromLiveMessage(
const isFinalState = const isFinalState =
block.info.status === "completed" || block.info.status === "failed" block.info.status === "completed" || block.info.status === "failed"
if (isFinalState) { if (isFinalState) {
blocks.push({ currentBlocks.push({
type: "tool_result", type: "tool_result",
tool_use_id: block.info.tool_call_id, tool_use_id: block.info.tool_call_id,
output_preview: block.info.raw_output ?? block.info.content, output_preview: block.info.raw_output ?? block.info.content,
is_error: block.info.status === "failed", is_error: block.info.status === "failed",
}) })
currentGroupHasCompletedTool = true
} }
break break
} }
} }
} }
if (blocks.length === 0) return null const timestamp = new Date(liveMessage.startedAt).toISOString()
return groups
return { .filter((blocks) => blocks.length > 0)
id: `live-${conversationId}-${liveMessage.id}`, .map((blocks, i) => ({
role: "assistant", id:
blocks, i === 0
timestamp: new Date(liveMessage.startedAt).toISOString(), ? `live-${conversationId}-${liveMessage.id}`
} : `live-${conversationId}-${liveMessage.id}-${i}`,
role: "assistant" as const,
blocks,
timestamp,
}))
} }
function upsertExternalIdIndex( function upsertExternalIdIndex(
@@ -315,17 +335,17 @@ function reducer(
const current = state.byConversationId.get(action.conversationId) const current = state.byConversationId.get(action.conversationId)
if (!current) return state if (!current) return state
// Convert liveMessage to a completed MessageTurn // Convert liveMessage to completed MessageTurns (split into rounds)
const streamingTurn = current.liveMessage const streamingTurns = current.liveMessage
? buildStreamingTurnFromLiveMessage( ? buildStreamingTurnsFromLiveMessage(
current.conversationId, current.conversationId,
current.liveMessage current.liveMessage
) )
: null : []
// Promote: optimisticTurns + streamingTurn → localTurns // Promote: optimisticTurns + streamingTurns → localTurns
const promoted = [...current.localTurns, ...current.optimisticTurns] const promoted = [...current.localTurns, ...current.optimisticTurns]
if (streamingTurn) promoted.push(streamingTurn) promoted.push(...streamingTurns)
return updateSessionInState(state, action.conversationId, () => ({ return updateSessionInState(state, action.conversationId, () => ({
...current, ...current,
@@ -609,18 +629,18 @@ export function ConversationRuntimeProvider({
phase: "optimistic", phase: "optimistic",
})) }))
// Phase 4: Streaming turn (live agent response) // Phase 4: Streaming turns (live agent response, split into rounds)
const streamingMessage = session.liveMessage const streamingMessage = session.liveMessage
const streamingTurn = streamingMessage const streamingTurns = streamingMessage
? buildStreamingTurnFromLiveMessage(conversationId, streamingMessage) ? buildStreamingTurnsFromLiveMessage(conversationId, streamingMessage)
: null : []
const result = [...persisted, ...local, ...optimistic] const result = [...persisted, ...local, ...optimistic]
if (streamingTurn) { for (const [i, turn] of streamingTurns.entries()) {
result.push({ result.push({
key: `streaming-${conversationId}-${streamingMessage?.id ?? "unknown"}`, key: `streaming-${conversationId}-${streamingMessage?.id ?? "unknown"}-${i}`,
turn: streamingTurn, turn,
phase: "streaming", phase: "streaming",
}) })
} }