Files
cobble/src/pages/admin/bots/[bot_id]/(partials)/speak-message-button.tsx
T
tbenandClaude Opus 5.5 163d9a434c Add dictation and streamed read-aloud to the chat
- A mic in the composer records, transcribes with the bot's
  transcription model (api/admin/bots/transcribe) and writes the text
  into the input at the cursor to edit before sending. Escape cancels;
  recordings aren't stored.
- Every bot message has a read-aloud button that plays
  GET api/admin/bots/speak-message, streamed from the provider as the
  audio is made, so it starts in a couple of seconds. One message plays
  at a time; only the user's own sessions can be read.
- Both buttons stay visible but disabled while the bot has no model for
  them (bot_voice page prop).
- Transcription and speech no longer pick free models automatically:
  they're not available until a model is picked, and Settings says so.
  Transcription lists speech-to-text models only, so the chat-model
  transcript path and its missing-audio check are gone.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-19 21:21:04 +01:00

51 lines
1.8 KiB
TypeScript

import { useContext } from "react";
import Button from "@moduletrace/twui/layout/Button";
import { Square, Volume2 } from "lucide-react";
import { SpeakMessageContext } from "../(data)/speak-message-context";
const UnavailableTitle =
"Reading aloud needs a speech model. A super admin can pick one in Settings, under Models.";
type Props = {
message_id: number;
};
/**
* Reads a bot message aloud with its speech model, or stops it. Its title
* says why it last failed. Disabled, saying so, while speech has no model.
*/
export default function SpeakMessageButton({ message_id }: Props) {
const speak = useContext(SpeakMessageContext);
const active = speak.message_id == message_id;
const playing = active && speak.state == "playing";
const error = speak.error?.message_id == message_id ? speak.error.text : undefined;
return (
<Button
title={
!speak.enabled
? UnavailableTitle
: playing
? "Stop"
: error || "Read aloud"
}
aria-label={playing ? "Stop reading" : "Read aloud"}
aria-invalid={error ? true : undefined}
type="button"
disabled={!speak.enabled}
variant="ghost"
color="text"
loading={active && speak.state == "loading"}
loadingIconSize="smaller"
onClick={() => speak.toggle({ message_id })}
className="shrink-0 p-1! -mx-1 text-ink-secondary dark:text-ink-secondary hover:text-ink rounded-default-xs focus-visible:ring-2 focus-visible:ring-ink"
>
{playing ? (
<Square size={12} aria-hidden fill="currentColor" />
) : (
<Volume2 size={14} aria-hidden />
)}
</Button>
);
}