fix(gateway): show /tts audio in Control UI webchat (#61598) (thanks @neeravmakwana)

This commit is contained in:
Neerav Makwana
2026-04-06 08:19:38 -04:00
committed by GitHub
parent 02c092e558
commit 9aaa000da0
8 changed files with 353 additions and 13 deletions

View File

@@ -305,6 +305,20 @@
justify-content: flex-end;
}
/* Embedded audio (e.g. gateway-injected TTS from slash commands) */
.chat-message-audio {
display: flex;
flex-direction: column;
gap: 8px;
margin-bottom: 8px;
max-width: min(420px, 100%);
}
.chat-message-audio-el {
width: 100%;
min-height: 36px;
}
/* Compose input row - horizontal layout */
.chat-compose__row {
display: flex;

View File

@@ -23,6 +23,10 @@ type ImageBlock = {
alt?: string;
};
type AudioClip = {
url: string;
};
function extractImages(message: unknown): ImageBlock[] {
const m = message as Record<string, unknown>;
const content = m.content;
@@ -60,6 +64,32 @@ function extractImages(message: unknown): ImageBlock[] {
return images;
}
function extractAudioClips(message: unknown): AudioClip[] {
const m = message as Record<string, unknown>;
const content = m.content;
const clips: AudioClip[] = [];
if (!Array.isArray(content)) {
return clips;
}
for (const block of content) {
if (typeof block !== "object" || block === null) {
continue;
}
const b = block as Record<string, unknown>;
if (b.type !== "audio") {
continue;
}
const source = b.source as Record<string, unknown> | undefined;
if (source?.type === "base64" && typeof source.data === "string") {
const data = source.data;
const mediaType = (source.media_type as string) || "audio/mpeg";
const url = data.startsWith("data:") ? data : `data:${mediaType};base64,${data}`;
clips.push({ url });
}
}
return clips;
}
export function renderReadingIndicatorGroup(assistant?: AssistantIdentity, basePath?: string) {
return html`
<div class="chat-group assistant">
@@ -580,6 +610,25 @@ function renderMessageImages(images: ImageBlock[]) {
`;
}
function renderMessageAudio(clips: AudioClip[]) {
if (clips.length === 0) {
return nothing;
}
return html`
<div class="chat-message-audio">
${clips.map(
(clip) =>
html`<audio
class="chat-message-audio-el"
controls
preload="metadata"
src=${clip.url}
></audio>`,
)}
</div>
`;
}
/** Render tool cards inside a collapsed `<details>` element. */
function renderCollapsedToolCards(
toolCards: ToolCard[],
@@ -688,6 +737,8 @@ function renderGroupedMessage(
const hasToolCards = toolCards.length > 0;
const images = extractImages(message);
const hasImages = images.length > 0;
const audioClips = extractAudioClips(message);
const hasAudio = audioClips.length > 0;
const extractedText = extractTextCached(message);
const extractedThinking =
@@ -711,7 +762,7 @@ function renderGroupedMessage(
// Suppress empty bubbles when tool cards are the only content and toggle is off
const visibleToolCards = hasToolCards && (opts.showToolCalls ?? true);
if (!markdown && !visibleToolCards && !hasImages) {
if (!markdown && !visibleToolCards && !hasImages && !hasAudio) {
return nothing;
}
@@ -747,7 +798,7 @@ function renderGroupedMessage(
: nothing}
</summary>
<div class="chat-tool-msg-body">
${renderMessageImages(images)}
${renderMessageImages(images)} ${renderMessageAudio(audioClips)}
${reasoningMarkdown
? html`<div class="chat-thinking">
${unsafeHTML(toSanitizedMarkdownHtml(reasoningMarkdown))}
@@ -771,7 +822,7 @@ function renderGroupedMessage(
</details>
`
: html`
${renderMessageImages(images)}
${renderMessageImages(images)} ${renderMessageAudio(audioClips)}
${reasoningMarkdown
? html`<div class="chat-thinking">
${unsafeHTML(toSanitizedMarkdownHtml(reasoningMarkdown))}