Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
180 changes: 73 additions & 107 deletions speech-to-text/nextjs/realtime/example/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"use client";

import { useState, useCallback, useEffect } from "react";
import { useScribe, CommitStrategy } from "@elevenlabs/react";
import { useCallback, useState } from "react";
import { CommitStrategy, useScribe } from "@elevenlabs/react";
import { LiveWaveform } from "@/components/ui/live-waveform";

export default function Home() {
Expand All @@ -18,43 +18,60 @@ export default function Home() {
setPartialTranscript(data.text || "");
},
onCommittedTranscript: data => {
if (data.text && data.text.trim()) {
if (data.text?.trim()) {
setCommittedHistory(prev => [data.text, ...prev]);
}
setPartialTranscript("");
},
onError: err => {
console.error("Scribe error:", err);
setPartialTranscript("");
setError("Connection error occurred. Please try again.");
},
});

useEffect(() => {
if (scribe.status === "disconnected" || scribe.status === "error") {
onDisconnect: () => {
setPartialTranscript("");
}
}, [scribe.status]);
},
});

// Check both connected and transcribing states to properly show active status
const isActive =
scribe.status === "connected" || scribe.status === "transcribing";
const isConnecting = scribe.status === "connecting";

const statusLabel = (() => {
switch (scribe.status) {
case "connected":
return "Connected";
case "transcribing":
return "Transcribing";
case "connecting":
return "Connecting";
case "error":
return "Error";
default:
return "Disconnected";
}
})();

const handleStart = useCallback(async () => {
try {
setError(null);
setPartialTranscript("");

// Fetch a fresh single-use token from our API
const response = await fetch("/api/scribe-token");
if (!response.ok) {
throw new Error("Failed to get transcription token");
const data = (await response.json()) as {
token?: string;
error?: string;
};
if (!response.ok || typeof data.token !== "string") {
throw new Error(
typeof data.error === "string"
? data.error
: "Failed to get transcription token"
);
}
const { token } = await response.json();

// Connect with microphone access
await scribe.connect({
token,
token: data.token,
microphone: {
echoCancellation: true,
noiseSuppression: true,
Expand All @@ -64,7 +81,9 @@ export default function Home() {
} catch (err) {
console.error("Failed to start transcription:", err);
setError(
"Failed to start transcription. Please check your permissions and try again."
err instanceof Error
? err.message
: "Failed to start transcription. Please check your permissions and try again."
);
}
}, [scribe]);
Expand All @@ -77,15 +96,11 @@ export default function Home() {
const handleToggle = () => {
if (isActive) {
handleStop();
} else {
handleStart();
} else if (!isConnecting) {
void handleStart();
}
};

const handleClearHistory = () => {
setCommittedHistory([]);
};

return (
<main className="min-h-screen bg-white text-neutral-900">
<div className="mx-auto w-full max-w-2xl px-6 py-12 sm:py-16">
Expand All @@ -98,106 +113,57 @@ export default function Home() {
</p>
</header>

<div className="mt-8 space-y-6">
{/* Controls */}
<div className="flex items-center justify-between">
<div className="flex items-center space-x-4">
<button
onClick={handleToggle}
disabled={isConnecting}
className={`rounded-md px-4 py-2 text-sm font-medium transition-colors ${
isActive
? "bg-red-500 text-white hover:bg-red-600"
: isConnecting
? "bg-neutral-200 text-neutral-400 cursor-not-allowed"
: "bg-neutral-900 text-white hover:bg-neutral-800"
}`}
>
{isConnecting ? "Connecting..." : isActive ? "Stop" : "Start"}
</button>
{committedHistory.length > 0 && (
<button
onClick={handleClearHistory}
className="rounded-md px-4 py-2 text-sm font-medium text-neutral-600 hover:text-neutral-900 transition-colors"
>
Clear History
</button>
)}
</div>
<div className="text-xs text-neutral-400">
{isActive ? (
<span className="flex items-center">
<span className="mr-1.5 h-2 w-2 rounded-full bg-green-500"></span>
{scribe.status === "transcribing"
? "Transcribing"
: "Connected"}
</span>
) : (
<span>Disconnected</span>
)}
</div>
<div className="mt-10 space-y-6">
<div className="flex flex-wrap items-center gap-3">
<button
type="button"
onClick={handleToggle}
disabled={isConnecting}
className="rounded-md bg-neutral-900 px-4 py-2 text-sm font-medium text-white hover:bg-neutral-800 disabled:opacity-50"
>
{isConnecting ? "Connecting…" : isActive ? "Stop" : "Start"}
</button>
<span className="text-xs text-neutral-400">
Status: {statusLabel}
</span>
</div>

{/* Error message */}
{error && (
<div className="rounded-md bg-red-50 border border-red-200 px-4 py-3 text-sm text-red-700">
{error ? (
<p className="text-sm text-red-600" role="alert">
{error}
</div>
)}

{/* Live waveform */}
<div className="h-16">
<LiveWaveform
active={isActive}
processing={isActive}
barColor="rgb(115 115 115)"
fadeEdges={true}
fadeWidth={24}
height={64}
/>
</div>
</p>
) : null}

<LiveWaveform
active={isActive}
processing={isActive}
barColor="rgb(115 115 115)"
fadeEdges
fadeWidth={24}
height={64}
/>

{/* Partial transcript */}
{(isActive || partialTranscript) && (
<div className="space-y-2">
<h2 className="text-xs text-neutral-400 uppercase tracking-wide">
Live Transcript
</h2>
<div className="min-h-[3rem] rounded-md border border-neutral-200 px-4 py-3">
<p className="text-sm text-neutral-600">
{partialTranscript || (
<span className="text-neutral-400">Listening...</span>
)}
</p>
</div>
<div className="space-y-1">
<p className="text-xs text-neutral-400">Live transcript</p>
<p className="min-h-[1.5rem] text-sm italic text-neutral-500">
{partialTranscript || "Listening…"}
</p>
</div>
)}

{/* Committed transcript history */}
{committedHistory.length > 0 && (
<div className="space-y-2">
<h2 className="text-xs text-neutral-400 uppercase tracking-wide">
History
</h2>
<div className="space-y-2 max-h-96 overflow-y-auto">
<p className="text-xs text-neutral-400">History</p>
<div className="max-h-96 space-y-2 overflow-y-auto">
{committedHistory.map((text, index) => (
<div
key={index}
className="rounded-md border border-neutral-200 px-4 py-3 text-sm"
>
<p key={`${index}-${text}`} className="text-sm text-neutral-800">
{text}
</div>
</p>
))}
</div>
</div>
)}

{/* Instructions when not started */}
{!isActive && !isConnecting && committedHistory.length === 0 && (
<div className="text-center py-12 text-sm text-neutral-500">
Click "Start" to begin transcribing audio from your microphone.
</div>
)}
</div>
</div>
</main>
Expand Down
Loading