fix[examples/transcript]: fix transitional deltas (#275)

* fix transcript deltas

* Update mcp-app.css
This commit is contained in:
Olivier Chafik 2026-01-15 12:06:02 +00:00 • committed by GitHub
commit b94982cc35
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 68 additions and 9 deletions

View file

@ -66,9 +66,14 @@
}
.transcript-entry.interim {
opacity: 0.6;
font-style: italic;
border-left: 3px solid var(--color-primary);
font-style: normal;
}
/* Unstable/interim text within entry - dimmed and italic */
.transcript-entry.interim .interim-delta {
opacity: 0.5;
font-style: italic;
}
.transcript-entry.sent {

View file

@ -51,6 +51,8 @@ let animationFrame: number | null = null;
// Speech Recognition
let recognition: SpeechRecognition | null = null;
let committedResultCount = 0; // How many results have been committed as entries
let interimTranscript = ""; // Current interim (unstable) text
// ============================================================================
// MCP App Setup
@ -155,22 +157,44 @@ function startSpeechRecognition(): boolean {
recognition.onstart = () => {
log.info("Speech recognition started");
committedResultCount = 0;
interimTranscript = "";
};
recognition.onresult = (event) => {
const e = event as SpeechRecognitionEvent;
for (let i = e.resultIndex; i < e.results.length; i++) {
// Process results, committing newly-finalized ones
interimTranscript = "";
for (let i = 0; i < e.results.length; i++) {
const result = e.results[i];
const transcript = result[0].transcript;
if (result.isFinal) {
addTranscriptEntry(transcript, true);
updateSendButton();
updateModelContext();
// Only commit if this result hasn't been committed yet
if (i >= committedResultCount) {
const text = transcript.trim();
if (text) {
clearInterimTranscript();
addTranscriptEntry(text, true);
updateSendButton();
updateModelContext();
}
committedResultCount = i + 1;
}
} else {
updateInterimTranscript(transcript);
// Accumulate all interim text
interimTranscript += transcript;
}
}
// Show interim text if any
if (interimTranscript.trim()) {
updateInterimTranscript("", interimTranscript);
} else {
clearInterimTranscript();
}
};
recognition.onerror = (event) => {
@ -204,6 +228,9 @@ function startSpeechRecognition(): boolean {
}
function stopSpeechRecognition() {
// Commit any accumulated final transcript before stopping
commitFinalTranscript();
if (recognition) {
try {
recognition.stop();
@ -214,6 +241,19 @@ function stopSpeechRecognition() {
}
}
function commitFinalTranscript() {
// Commit any remaining interim text when stopping
const textToCommit = interimTranscript.trim();
if (textToCommit) {
clearInterimTranscript();
addTranscriptEntry(textToCommit, true);
updateSendButton();
updateModelContext();
}
committedResultCount = 0;
interimTranscript = "";
}
// ============================================================================
// UI Helpers
// ============================================================================
@ -271,7 +311,7 @@ function addTranscriptEntry(text: string, isFinal: boolean) {
transcriptEl.appendChild(entry);
}
function updateInterimTranscript(text: string) {
function updateInterimTranscript(finalText: string, interimText: string) {
clearTranscriptPlaceholder();
let interim = transcriptEl.querySelector(
@ -284,7 +324,21 @@ function updateInterimTranscript(text: string) {
}
const timestamp = new Date().toLocaleTimeString();
interim.innerHTML = `<div class="timestamp">${timestamp}</div>${escapeHtml(text)}`;
// Show final text (stable) in normal style, interim text (unstable) in delta style
const finalHtml = escapeHtml(finalText);
const interimHtml = interimText
? `<span class="interim-delta">${escapeHtml(interimText)}</span>`
: "";
interim.innerHTML = `<div class="timestamp">${timestamp}</div>${finalHtml}${interimHtml}`;
}
function clearInterimTranscript() {
const interim = transcriptEl.querySelector(".transcript-entry.interim");
if (interim) {
interim.remove();
}
}
function escapeHtml(text: string): string {