diff options
Diffstat (limited to 'webapp/lib/handle-realtime-event.ts')
| -rw-r--r-- | webapp/lib/handle-realtime-event.ts | 231 |
1 files changed, 231 insertions, 0 deletions
diff --git a/webapp/lib/handle-realtime-event.ts b/webapp/lib/handle-realtime-event.ts new file mode 100644 index 0000000..fb3d0bc --- /dev/null +++ b/webapp/lib/handle-realtime-event.ts @@ -0,0 +1,231 @@ +import { Item } from "@/components/types"; + +export default function handleRealtimeEvent( + ev: any, + setItems: React.Dispatch<React.SetStateAction<Item[]>> +) { + // Helper function to create a new item with default fields + function createNewItem(base: Partial<Item>): Item { + return { + object: "realtime.item", + timestamp: new Date().toLocaleTimeString(), + ...base, + } as Item; + } + + // Helper function to update an existing item if found by id, or add a new one if not. + // We can also pass partial updates to reduce repetitive code. + function updateOrAddItem(id: string, updates: Partial<Item>): void { + setItems((prev) => { + const idx = prev.findIndex((m) => m.id === id); + if (idx >= 0) { + const updated = [...prev]; + updated[idx] = { ...updated[idx], ...updates }; + return updated; + } else { + return [...prev, createNewItem({ id, ...updates })]; + } + }); + } + + const { type } = ev; + + switch (type) { + case "session.created": { + // Starting a new session, clear all items + setItems([]); + break; + } + + case "input_audio_buffer.speech_started": { + // Create a user message item with running status and placeholder content + const { item_id } = ev; + setItems((prev) => [ + ...prev, + createNewItem({ + id: item_id, + type: "message", + role: "user", + content: [{ type: "text", text: "..." }], + status: "running", + }), + ]); + break; + } + + case "conversation.item.created": { + const { item } = ev; + if (item.type === "message") { + // A completed message from user or assistant + const updatedContent = + item.content && item.content.length > 0 ? item.content : []; + setItems((prev) => { + const idx = prev.findIndex((m) => m.id === item.id); + if (idx >= 0) { + const updated = [...prev]; + updated[idx] = { + ...updated[idx], + ...item, + content: updatedContent, + status: "completed", + timestamp: + updated[idx].timestamp || new Date().toLocaleTimeString(), + }; + return updated; + } else { + return [ + ...prev, + createNewItem({ + ...item, + content: updatedContent, + status: "completed", + }), + ]; + } + }); + } + // NOTE: We no longer handle function_call items here. + // The handling of function_call items has been moved to the "response.output_item.done" event. + else if (item.type === "function_call_output") { + // Function call output item created + // Add the output item and mark the corresponding function_call as completed + // Also display in transcript as tool message with the response + setItems((prev) => { + const newItems = [ + ...prev, + createNewItem({ + ...item, + role: "tool", + content: [ + { + type: "text", + text: `Function call response: ${item.output}`, + }, + ], + status: "completed", + }), + ]; + + return newItems.map((m) => + m.call_id === item.call_id && m.type === "function_call" + ? { ...m, status: "completed" } + : m + ); + }); + } + break; + } + + case "conversation.item.input_audio_transcription.completed": { + // Update the user message with the final transcript + const { item_id, transcript } = ev; + setItems((prev) => + prev.map((m) => + m.id === item_id && m.type === "message" && m.role === "user" + ? { + ...m, + content: [{ type: "text", text: transcript }], + status: "completed", + } + : m + ) + ); + break; + } + + case "response.content_part.added": { + const { item_id, part, output_index } = ev; + // Append new content to the assistant message if output_index == 0 + if (part.type === "text" && output_index === 0) { + setItems((prev) => { + const idx = prev.findIndex((m) => m.id === item_id); + if (idx >= 0) { + const updated = [...prev]; + const existingContent = updated[idx].content || []; + updated[idx] = { + ...updated[idx], + content: [ + ...existingContent, + { type: part.type, text: part.text }, + ], + }; + return updated; + } else { + // If the item doesn't exist yet, create it as a running assistant message + return [ + ...prev, + createNewItem({ + id: item_id, + type: "message", + role: "assistant", + content: [{ type: part.type, text: part.text }], + status: "running", + }), + ]; + } + }); + } + break; + } + + case "response.audio_transcript.delta": { + // Streaming transcript text (assistant) + const { item_id, delta, output_index } = ev; + if (output_index === 0 && delta) { + setItems((prev) => { + const idx = prev.findIndex((m) => m.id === item_id); + if (idx >= 0) { + const updated = [...prev]; + const existingContent = updated[idx].content || []; + updated[idx] = { + ...updated[idx], + content: [...existingContent, { type: "text", text: delta }], + }; + return updated; + } else { + return [ + ...prev, + createNewItem({ + id: item_id, + type: "message", + role: "assistant", + content: [{ type: "text", text: delta }], + status: "running", + }), + ]; + } + }); + } + break; + } + + case "response.output_item.done": { + const { item } = ev; + if (item.type === "function_call") { + // A new function call item + // Display it in the transcript as an assistant message indicating a function is being requested + console.log("function_call", item); + setItems((prev) => [ + ...prev, + createNewItem({ + ...item, + role: "assistant", + content: [ + { + type: "text", + text: `${item.name}(${JSON.stringify( + JSON.parse(item.arguments) + )})`, + }, + ], + status: "running", + }), + ]); + } + break; + } + + default: + break; + } +} |
