diff --git a/.changeset/convai-widget-queue-status.md b/.changeset/convai-widget-queue-status.md new file mode 100644 index 00000000..0e57681a --- /dev/null +++ b/.changeset/convai-widget-queue-status.md @@ -0,0 +1,6 @@ +--- +"@elevenlabs/convai-widget-core": minor +"@elevenlabs/convai-widget-embed": patch +--- + +Support concurrency wait-queue (`queue_status`) server events: show a waiting status while all agents are busy, hide the typing indicator and block text/attachment sending while queued, and render a friendly non-error message when the queue wait times out. diff --git a/packages/convai-widget-core/src/contexts/conversation.tsx b/packages/convai-widget-core/src/contexts/conversation.tsx index dd5e3227..34a52b27 100644 --- a/packages/convai-widget-core/src/contexts/conversation.tsx +++ b/packages/convai-widget-core/src/contexts/conversation.tsx @@ -88,6 +88,10 @@ export type TranscriptEntry = type: "mode_toggle"; mode: ConversationMode; conversationIndex: number; + } + | { + type: "queue_timeout"; + conversationIndex: number; }; export function ConversationProvider({ children }: ConversationProviderProps) { @@ -189,6 +193,16 @@ function useConversationSetup() { const conversationTextOnly = signal(null); const isAgentTyping = signal(false); const isExternalAgentMode = signal(false); + const queueStatus = signal(null); + // Unknown statuses mean "not held" so a new backend status cannot lock the UI. + const isHeldInQueue = computed( + () => queueStatus.value === "waiting" || queueStatus.value === "timed_out" + ); + // "timed_out" still counts as held: the server sends it just before + // closing, so the UI keeps showing waiting until the disconnect lands. + const isWaitingForAgent = computed( + () => isHeldInQueue.value && !isDisconnected.value + ); const setAgentTyping = (typing: boolean, durationMs?: number | null) => { clearTypingTimer(); @@ -214,6 +228,8 @@ function useConversationSetup() { transcript, isAgentTyping, isExternalAgentMode, + queueStatus, + isWaitingForAgent, startSession: async ( element: HTMLElement, initialMessage?: string, @@ -259,6 +275,7 @@ function useConversationSetup() { } conversationTextOnly.value = processedConfig.textOnly ?? false; + queueStatus.value = null; transcript.value = [ ...firstMessageEntries(), ...(initialMessage @@ -432,7 +449,27 @@ function useConversationSetup() { setAgentTyping(false); isExternalAgentMode.value = false; }, + // The SDK forwards unhandled server events to onDebug. + // TODO: drop this narrowing once the SDK handles queue_status + // explicitly (planned after the queue protocol is finalized). + onDebug: (props: unknown) => { + const event = props as { + type?: string; + queue_status_event?: { status?: unknown }; + }; + if ( + event?.type === "queue_status" && + typeof event.queue_status_event?.status === "string" + ) { + queueStatus.value = event.queue_status_event.status; + } + }, onDisconnect: details => { + // A queue timeout closes with an error; show friendly copy + // instead of the raw close reason. + const queueTimedOut = + details.reason === "error" && + queueStatus.peek() === "timed_out"; receivedFirstMessageRef.current = false; conversationTextOnly.value = null; streamingMessageIndexRef.current = null; @@ -442,20 +479,25 @@ function useConversationSetup() { isExternalAgentMode.value = false; transcript.value = [ ...transcript.peek(), - details.reason === "error" + queueTimedOut ? { - type: "error", - message: details.message, + type: "queue_timeout", conversationIndex: conversationIndex.peek(), } - : { - type: "disconnection", - role: details.reason === "user" ? "user" : "agent", - conversationIndex: conversationIndex.peek(), - }, + : details.reason === "error" + ? { + type: "error", + message: details.message, + conversationIndex: conversationIndex.peek(), + } + : { + type: "disconnection", + role: details.reason === "user" ? "user" : "agent", + conversationIndex: conversationIndex.peek(), + }, ]; conversationIndex.value++; - if (details.reason === "error") { + if (details.reason === "error" && !queueTimedOut) { error.value = details.message; console.error( "[ConversationalAI] Disconnected due to an error:", @@ -481,21 +523,33 @@ function useConversationSetup() { error.value = null; return id; } catch (e) { - let message = "Could not start a conversation."; - if (e instanceof CloseEvent) { - message = e.reason || message; - } else if (e instanceof Error) { - message = e.message || message; + // A queue timeout can close the connection before startSession + // resolves. + if (queueStatus.peek() === "timed_out") { + transcript.value = [ + ...transcript.value, + { + type: "queue_timeout", + conversationIndex: conversationIndex.peek(), + }, + ]; + } else { + let message = "Could not start a conversation."; + if (e instanceof CloseEvent) { + message = e.reason || message; + } else if (e instanceof Error) { + message = e.message || message; + } + error.value = message; + transcript.value = [ + ...transcript.value, + { + type: "error", + message, + conversationIndex: conversationIndex.peek(), + }, + ]; } - error.value = message; - transcript.value = [ - ...transcript.value, - { - type: "error", - message, - conversationIndex: conversationIndex.peek(), - }, - ]; } finally { lockRef.current = null; } @@ -521,6 +575,8 @@ function useConversationSetup() { conversationRef.current?.sendFeedback(like); }, sendUserMessage: (text: string, options?: SendUserMessageOptions) => { + // The orchestrator discards messages sent while held in the queue. + if (isWaitingForAgent.peek()) return; conversationRef.current?.sendUserMessage(text, options); transcript.value = [ ...transcript.value, @@ -537,6 +593,7 @@ function useConversationSetup() { text?: string; file: TranscriptFileInput & { fileId: string }; }) => { + if (isWaitingForAgent.peek()) return; const trimmed = input.text?.trim() ?? ""; const { fileId, ...fileInput } = input.file; conversationRef.current?.sendMultimodalMessage({ diff --git a/packages/convai-widget-core/src/index.test.ts b/packages/convai-widget-core/src/index.test.ts index e5325993..a37ee5b5 100644 --- a/packages/convai-widget-core/src/index.test.ts +++ b/packages/convai-widget-core/src/index.test.ts @@ -296,6 +296,206 @@ describe("elevenlabs-convai", () => { } ); + describe("concurrency wait queue", () => { + it("shows the waiting status and blocks sending while queued", async () => { + setupWebComponent({ + "agent-id": "queued", + transcript: "true", + "text-input": "true", + }); + + const startButton = page.getByRole("button", { name: "Start a call" }); + await startButton.click(); + const acceptButton = page.getByRole("button", { name: "Accept" }); + await acceptButton.click(); + + await expect + .element(page.getByText("Waiting for an available agent")) + .toBeInTheDocument(); + + // The typing indicator stays hidden while queued. + await expect + .element(page.getByText("Agent is typing ...")) + .not.toBeInTheDocument(); + + // Sending is blocked while held in the queue. + const textInput = page.getByRole("textbox", { + name: "Text message input", + }); + await textInput.fill("Queued message"); + await expect + .element(page.getByRole("button", { name: "Send", exact: true })) + .toBeDisabled(); + await userEvent.keyboard("{Enter}"); + await expect + .element(page.getByText("Queued message")) + .not.toBeInTheDocument(); + + // Hanging up while queued goes through the regular disconnect flow. + const endButton = page.getByRole("button", { name: "End", exact: true }); + await endButton.click(); + await expect + .element(page.getByText("You ended the conversation")) + .toBeInTheDocument(); + await expect + .element(page.getByText("Waiting for an available agent")) + .not.toBeInTheDocument(); + + // No residual gating: a new conversation can start from the textarea. + await textInput.fill("New text message"); + await userEvent.keyboard("{Enter}"); + await expect + .element(page.getByText("New text message")) + .toBeInTheDocument(); + }); + + it("disables rich-content buttons and drops bridged sends while queued", async () => { + const widget = setupWebComponent({ + "agent-id": "queued", + transcript: "true", + "text-input": "true", + "allow-events": "true", + "default-expanded": "true", + }); + + // Start via the textarea so the conversation is text-only and rich + // content renders. + const textInput = page.getByRole("textbox", { + name: "Text message input", + }); + await textInput.fill("Hello"); + await userEvent.keyboard("{Enter}"); + const acceptButton = page.getByRole("button", { name: "Accept" }); + await acceptButton.click(); + + await expect + .element(page.getByText("Waiting for an available agent")) + .toBeInTheDocument(); + + // Rich-content buttons received while queued render disabled. + await expect + .element(page.getByRole("button", { name: "Quick reply" })) + .toBeDisabled(); + + // Programmatic sends via the event bridge are dropped while queued. + widget.dispatchEvent( + new CustomEvent("elevenlabs-agent:user-message", { + detail: { message: "Bridged message" }, + }) + ); + await expect + .element(page.getByText("Bridged message")) + .not.toBeInTheDocument(); + }); + + it("shows the full waiting message on the avatar overlay", async () => { + // The expanded sheet without a transcript shows the avatar overlay, + // which renders the full waiting copy. + setupWebComponent({ "agent-id": "queued_overlay" }); + + const startButton = page.getByRole("button", { name: "Start a call" }); + await startButton.click(); + const acceptButton = page.getByRole("button", { name: "Accept" }); + await acceptButton.click(); + + await expect + .element(page.getByText("All agents are busy right now")) + .toBeInTheDocument(); + }); + + it("keeps the waiting status inside the full trigger", async () => { + // Regression: the long waiting copy must size the trigger, not get + // clipped at the card edge. + setupWebComponent({ "agent-id": "queued", variant: "full" }); + + const startButton = page.getByRole("button", { name: "Start a call" }); + await startButton.click(); + const acceptButton = page.getByRole("button", { name: "Accept" }); + await acceptButton.click(); + + const label = page.getByText("Waiting for an available agent"); + await expect.element(label).toBeInTheDocument(); + + const labelElement = label.element() as HTMLElement; + // The full copy is visible, not ellipsized... + expect(labelElement.scrollWidth).toBeLessThanOrEqual( + labelElement.clientWidth + 1 + ); + // ...and the label stays within the trigger card. + const card = labelElement.closest(".rounded-sheet")!; + const labelRect = labelElement.getBoundingClientRect(); + const cardRect = card.getBoundingClientRect(); + expect(labelRect.right).toBeLessThanOrEqual(cardRect.right); + expect(labelRect.left).toBeGreaterThanOrEqual(cardRect.left); + }); + + it("returns to the regular flow when admitted from the queue", async () => { + setupWebComponent({ + "agent-id": "queue_admit", + transcript: "true", + "text-input": "true", + }); + + const startButton = page.getByRole("button", { name: "Start a call" }); + await startButton.click(); + const acceptButton = page.getByRole("button", { name: "Accept" }); + await acceptButton.click(); + + await expect + .element(page.getByText("Waiting for an available agent")) + .toBeInTheDocument(); + + // Admitted: the waiting status clears and the agent responds. + await expect + .element(page.getByText("Queue cleared response")) + .toBeInTheDocument(); + await expect + .element(page.getByText("Waiting for an available agent")) + .not.toBeInTheDocument(); + + // Sending works again. + const textInput = page.getByRole("textbox", { + name: "Text message input", + }); + await textInput.fill("Hello after queue"); + await userEvent.keyboard("{Enter}"); + await expect + .element(page.getByText("Hello after queue")) + .toBeInTheDocument(); + }); + + it("shows a friendly message when the queue wait times out", async () => { + setupWebComponent({ + "agent-id": "queue_timeout", + transcript: "true", + "text-input": "true", + }); + + const startButton = page.getByRole("button", { name: "Start a call" }); + await startButton.click(); + const acceptButton = page.getByRole("button", { name: "Accept" }); + await acceptButton.click(); + + await expect + .element(page.getByText("Waiting for an available agent")) + .toBeInTheDocument(); + + // Friendly copy instead of the raw close reason or error styling. + await expect + .element( + page.getByText("All agents are still busy. Please try again later.") + ) + .toBeInTheDocument(); + await expect + .element(page.getByText("An error occurred")) + .not.toBeInTheDocument(); + await expect + .element(page.getByText("too many concurrent connections")) + .not.toBeInTheDocument(); + await expect.element(startButton).toBeInTheDocument(); + }); + }); + describe("expansion events", () => { it.each(["document", "widget"])( "should expand and collapse widget when elevenlabs-agent:expand event is dispatched (%s)", diff --git a/packages/convai-widget-core/src/markdown.dev.tsx b/packages/convai-widget-core/src/markdown.dev.tsx index edbfa831..4324f75b 100644 --- a/packages/convai-widget-core/src/markdown.dev.tsx +++ b/packages/convai-widget-core/src/markdown.dev.tsx @@ -258,6 +258,8 @@ function MockConversationProvider({ transcript: mockTranscript, isAgentTyping: signal(false), isExternalAgentMode: signal(false), + queueStatus: signal(null), + isWaitingForAgent: signal(false), startSession: async () => "", endSession: async () => {}, getInputVolume: () => 0, diff --git a/packages/convai-widget-core/src/mocks/browser.ts b/packages/convai-widget-core/src/mocks/browser.ts index 89032f89..edabc533 100644 --- a/packages/convai-widget-core/src/mocks/browser.ts +++ b/packages/convai-widget-core/src/mocks/browser.ts @@ -42,6 +42,18 @@ export const AGENTS = { use_rtc: true, }, fail: BASIC_CONFIG, + // Held in the concurrency wait queue and never admitted. + queued: BASIC_CONFIG, + // Expanded with no transcript so the avatar overlay renders the waiting copy. + queued_overlay: { + ...BASIC_CONFIG, + text_input_enabled: true, + default_expanded: true, + }, + // Admitted from the wait queue after a delay. + queue_admit: BASIC_CONFIG, + // The wait queue times out and the server closes with an error. + queue_timeout: BASIC_CONFIG, end_call_test: { ...BASIC_CONFIG, text_only: true, @@ -375,6 +387,78 @@ export const Worker = setupWorker( client.close(3000, "Test reason"); }); } + if ( + agentId === "queued" || + agentId === "queued_overlay" || + agentId === "queue_admit" || + agentId === "queue_timeout" + ) { + client.send( + JSON.stringify({ + type: "queue_status", + queue_status_event: { status: "waiting" }, + }) + ); + } + if (agentId === "queued") { + // A typing indicator arriving while queued must stay hidden. + client.send(JSON.stringify({ type: "external_agent_connected" })); + client.send( + JSON.stringify({ + type: "agent_typing", + agent_typing_event: { is_typing: true }, + }) + ); + // Rich-content buttons arriving while queued must render disabled. + client.send( + JSON.stringify({ + type: "rich_content", + rich_content: { + rich_content_id: "rc_queued", + component: "buttons", + props: { + buttons: [ + { + type: "message", + label: "Quick reply", + message: "Quick reply", + }, + ], + }, + event_id: 2, + }, + }) + ); + } + if (agentId === "queue_admit") { + await new Promise(resolve => setTimeout(resolve, 1500)); + client.send( + JSON.stringify({ + type: "queue_status", + queue_status_event: { status: "admitted" }, + }) + ); + client.send( + JSON.stringify({ + type: "agent_response", + agent_response_event: { + agent_response: "Queue cleared response", + event_id: 4, + }, + }) + ); + } + if (agentId === "queue_timeout") { + await new Promise(resolve => setTimeout(resolve, 1000)); + client.send( + JSON.stringify({ + type: "queue_status", + queue_status_event: { status: "timed_out" }, + }) + ); + await new Promise(resolve => setTimeout(resolve, 400)); + client.close(3008, "too many concurrent connections"); + } if (agentId === "end_call_test") { client.addEventListener("message", async () => { client.send( diff --git a/packages/convai-widget-core/src/rich-content/ButtonGroup.tsx b/packages/convai-widget-core/src/rich-content/ButtonGroup.tsx index a29a4bd1..f3a06f5c 100644 --- a/packages/convai-widget-core/src/rich-content/ButtonGroup.tsx +++ b/packages/convai-widget-core/src/rich-content/ButtonGroup.tsx @@ -21,9 +21,16 @@ export interface ButtonGroupProps { } export function ButtonGroup({ buttons, richContentId }: ButtonGroupProps) { - const { sendUserMessage, startSession, isDisconnected, status } = - useConversation(); - const disabled = status.value !== "connected" && !isDisconnected.value; + const { + sendUserMessage, + startSession, + isDisconnected, + status, + isWaitingForAgent, + } = useConversation(); + const disabled = + (status.value !== "connected" && !isDisconnected.value) || + isWaitingForAgent.value; if (buttons.length === 0) return null; diff --git a/packages/convai-widget-core/src/types/config.ts b/packages/convai-widget-core/src/types/config.ts index 0d51c5b4..040dfa3b 100644 --- a/packages/convai-widget-core/src/types/config.ts +++ b/packages/convai-widget-core/src/types/config.ts @@ -129,6 +129,9 @@ export const DefaultTextContents = { speaking_status: "Talk to interrupt", connecting_status: "Connecting", chatting_status: "Chatting with AI Agent", + queue_waiting_status: + "All agents are busy right now. We'll connect you automatically as soon as one is available.", + queue_waiting_status_short: "Waiting for an available agent", input_label: "Text message input", input_placeholder: "Send a message...", @@ -139,6 +142,7 @@ export const DefaultTextContents = { agent_ended_conversation: "The agent ended the conversation", conversation_id: "ID", error_occurred: "An error occurred", + queue_timed_out: "All agents are still busy. Please try again later.", copy_id: "Copy ID", initiate_feedback: "How was this conversation?", request_follow_up_feedback: "Tell us more", diff --git a/packages/convai-widget-core/src/utils/display-transcript.ts b/packages/convai-widget-core/src/utils/display-transcript.ts index 409abbc7..ef6442ac 100644 --- a/packages/convai-widget-core/src/utils/display-transcript.ts +++ b/packages/convai-widget-core/src/utils/display-transcript.ts @@ -45,6 +45,10 @@ export type DisplayTranscriptEntry = type: "typing_indicator"; conversationIndex: number; } + | { + type: "queue_timeout"; + conversationIndex: number; + } | { type: "rich_content"; component: string; diff --git a/packages/convai-widget-core/src/widget/AvatarOverlay.tsx b/packages/convai-widget-core/src/widget/AvatarOverlay.tsx index dbd86c9b..8cca74b6 100644 --- a/packages/convai-widget-core/src/widget/AvatarOverlay.tsx +++ b/packages/convai-widget-core/src/widget/AvatarOverlay.tsx @@ -57,7 +57,7 @@ export function AvatarOverlay({ -
+
diff --git a/packages/convai-widget-core/src/widget/FullTrigger.tsx b/packages/convai-widget-core/src/widget/FullTrigger.tsx index ab8c2818..ac58e998 100644 --- a/packages/convai-widget-core/src/widget/FullTrigger.tsx +++ b/packages/convai-widget-core/src/widget/FullTrigger.tsx @@ -26,17 +26,20 @@ export function FullTrigger({ >
-
+
{text.main_label} - +
diff --git a/packages/convai-widget-core/src/widget/Sheet.tsx b/packages/convai-widget-core/src/widget/Sheet.tsx index 5fc82f6c..c6852546 100644 --- a/packages/convai-widget-core/src/widget/Sheet.tsx +++ b/packages/convai-widget-core/src/widget/Sheet.tsx @@ -48,6 +48,7 @@ export function Sheet({ open }: SheetProps) { conversationIndex, isAgentTyping, isExternalAgentMode, + isWaitingForAgent, } = useConversation(); const firstMessage = useFirstMessage(); const { currentContent, currentConfig } = useSheetContent(); @@ -67,7 +68,10 @@ export function Sheet({ open }: SheetProps) { ? firstMessage.value : undefined, firstMessageConversationIndex: conversationIndex.peek(), - showTypingIndicator: isExternalAgentMode.value && isAgentTyping.value, + showTypingIndicator: + isExternalAgentMode.value && + isAgentTyping.value && + !isWaitingForAgent.value, }); }); const showTranscript = useComputed( diff --git a/packages/convai-widget-core/src/widget/SheetActions.tsx b/packages/convai-widget-core/src/widget/SheetActions.tsx index fc2814cb..ca5d830d 100644 --- a/packages/convai-widget-core/src/widget/SheetActions.tsx +++ b/packages/convai-widget-core/src/widget/SheetActions.tsx @@ -53,6 +53,7 @@ export function SheetActions({ startSession, sendUserMessage, sendMultimodalMessage, + isWaitingForAgent, } = useConversation(); const fileError = useSignal(null); @@ -89,7 +90,8 @@ export function SheetActions({ () => !pendingFile.value && !hasReachedLimit.value && - status.value === "connected" + status.value === "connected" && + !isWaitingForAgent.value ); const handleFileSelect = useCallback( @@ -112,7 +114,11 @@ export function SheetActions({ const canSend = useComputed(() => { const hasText = !!userMessage.value.trim(); const hasReadyFile = pendingFile.value?.status === "ready"; - return (hasText || hasReadyFile) && !isUploading.value; + return ( + (hasText || hasReadyFile) && + !isUploading.value && + !isWaitingForAgent.value + ); }); const handleSendMessage = useCallback( diff --git a/packages/convai-widget-core/src/widget/SheetHeader.tsx b/packages/convai-widget-core/src/widget/SheetHeader.tsx index 71ac60c7..073b5f9e 100644 --- a/packages/convai-widget-core/src/widget/SheetHeader.tsx +++ b/packages/convai-widget-core/src/widget/SheetHeader.tsx @@ -51,7 +51,10 @@ export function SheetHeader({
)} - +
diff --git a/packages/convai-widget-core/src/widget/StatusLabel.tsx b/packages/convai-widget-core/src/widget/StatusLabel.tsx index e0b9c913..ebff224c 100644 --- a/packages/convai-widget-core/src/widget/StatusLabel.tsx +++ b/packages/convai-widget-core/src/widget/StatusLabel.tsx @@ -7,38 +7,68 @@ import { useTextContents } from "../contexts/text-contents"; import { useIsConversationTextOnly } from "../contexts/widget-config"; import { useConversationMode } from "../contexts/conversation-mode"; - -function userCurrentLabel() { - const { status, isSpeaking } = useConversation(); +function userCurrentLabel(compact: boolean) { + const { status, isSpeaking, isWaitingForAgent } = useConversation(); const textOnly = useIsConversationTextOnly(); const { isTextMode } = useConversationMode(); const text = useTextContents(); const compute = () => { - if (status.value !== "connected") return {label: text.connecting_status.value, updateImmediately: true}; + // While queued the transport is connected but no agent is present yet, + // so the waiting copy wins over the connected statuses. + if (isWaitingForAgent.value) + return { + label: compact + ? text.queue_waiting_status_short.value + : text.queue_waiting_status.value, + updateImmediately: true, + }; + + if (status.value !== "connected") + return { + label: text.connecting_status.value, + updateImmediately: true, + }; - if (textOnly.value || isTextMode.value) return {label: text.chatting_status.value, updateImmediately: isSpeaking.value}; + if (textOnly.value || isTextMode.value) + return { + label: text.chatting_status.value, + updateImmediately: isSpeaking.value, + }; - if (isSpeaking.value) return {label: text.speaking_status.value, updateImmediately: isSpeaking.value}; + if (isSpeaking.value) + return { + label: text.speaking_status.value, + updateImmediately: isSpeaking.value, + }; + + return { + label: text.listening_status.value, + updateImmediately: isSpeaking.value, + }; + }; + return useComputed(compute); +} - return {label: text.listening_status.value, updateImmediately: isSpeaking.value}; - } - return useComputed(compute) +interface StatusLabelProps extends HTMLAttributes { + /** Render single-line copy for cramped surfaces (header pill, trigger). */ + compact?: boolean; } export function StatusLabel({ className, + compact = false, ...props -}: HTMLAttributes) { - const currentLabel = userCurrentLabel(); - const [label, setLabel] = useState(currentLabel.peek().label); +}: StatusLabelProps) { + const currentLabel = userCurrentLabel(compact); + const [{ label }, setLabel] = useState(() => currentLabel.peek()); useSignalEffect(() => { - const label = currentLabel.value; - if (label.updateImmediately) { - setLabel(label.label); + const next = currentLabel.value; + if (next.updateImmediately) { + setLabel(next); } else { const timeout = setTimeout(() => { - setLabel(label.label); + setLabel(next); }, 500); return () => clearTimeout(timeout); } @@ -53,7 +83,12 @@ export function StatusLabel({ {...props} > -
+
{label}
diff --git a/packages/convai-widget-core/src/widget/TranscriptMessage.tsx b/packages/convai-widget-core/src/widget/TranscriptMessage.tsx index c8cc55d2..4e202451 100644 --- a/packages/convai-widget-core/src/widget/TranscriptMessage.tsx +++ b/packages/convai-widget-core/src/widget/TranscriptMessage.tsx @@ -215,6 +215,24 @@ function ErrorMessage({ ); } +function QueueTimeoutMessage() { + const text = useTextContents(); + const { lastId } = useConversation(); + const config = useWidgetConfig(); + + return ( +
+ {text.queue_timed_out} +
+ {lastId.value && config.value.show_conversation_id && ( + + {text.conversation_id}: {lastId.value} + + )} +
+ ); +} + interface ModeToggleMessageProps { entry: Extract; } @@ -302,6 +320,9 @@ function getMessageComponent(entry: DisplayTranscriptEntry) { if (entry.type === "error") { return ; } + if (entry.type === "queue_timeout") { + return ; + } if (entry.type === "typing_indicator") { return ; }