同步会话实时响应时的样式
This commit is contained in:
@@ -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 (
|
||||||
|
|||||||
@@ -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",
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user