@@ -51,6 +51,8 @@ let animationFrame: number | null = null;
5151
5252// Speech Recognition
5353let recognition : SpeechRecognition | null = null ;
54+ let committedResultCount = 0 ; // How many results have been committed as entries
55+ let interimTranscript = "" ; // Current interim (unstable) text
5456
5557// ============================================================================
5658// MCP App Setup
@@ -155,22 +157,44 @@ function startSpeechRecognition(): boolean {
155157
156158 recognition . onstart = ( ) => {
157159 log . info ( "Speech recognition started" ) ;
160+ committedResultCount = 0 ;
161+ interimTranscript = "" ;
158162 } ;
159163
160164 recognition . onresult = ( event ) => {
161165 const e = event as SpeechRecognitionEvent ;
162- for ( let i = e . resultIndex ; i < e . results . length ; i ++ ) {
166+
167+ // Process results, committing newly-finalized ones
168+ interimTranscript = "" ;
169+
170+ for ( let i = 0 ; i < e . results . length ; i ++ ) {
163171 const result = e . results [ i ] ;
164172 const transcript = result [ 0 ] . transcript ;
165173
166174 if ( result . isFinal ) {
167- addTranscriptEntry ( transcript , true ) ;
168- updateSendButton ( ) ;
169- updateModelContext ( ) ;
175+ // Only commit if this result hasn't been committed yet
176+ if ( i >= committedResultCount ) {
177+ const text = transcript . trim ( ) ;
178+ if ( text ) {
179+ clearInterimTranscript ( ) ;
180+ addTranscriptEntry ( text , true ) ;
181+ updateSendButton ( ) ;
182+ updateModelContext ( ) ;
183+ }
184+ committedResultCount = i + 1 ;
185+ }
170186 } else {
171- updateInterimTranscript ( transcript ) ;
187+ // Accumulate all interim text
188+ interimTranscript += transcript ;
172189 }
173190 }
191+
192+ // Show interim text if any
193+ if ( interimTranscript . trim ( ) ) {
194+ updateInterimTranscript ( "" , interimTranscript ) ;
195+ } else {
196+ clearInterimTranscript ( ) ;
197+ }
174198 } ;
175199
176200 recognition . onerror = ( event ) => {
@@ -204,6 +228,9 @@ function startSpeechRecognition(): boolean {
204228}
205229
206230function stopSpeechRecognition ( ) {
231+ // Commit any accumulated final transcript before stopping
232+ commitFinalTranscript ( ) ;
233+
207234 if ( recognition ) {
208235 try {
209236 recognition . stop ( ) ;
@@ -214,6 +241,19 @@ function stopSpeechRecognition() {
214241 }
215242}
216243
244+ function commitFinalTranscript ( ) {
245+ // Commit any remaining interim text when stopping
246+ const textToCommit = interimTranscript . trim ( ) ;
247+ if ( textToCommit ) {
248+ clearInterimTranscript ( ) ;
249+ addTranscriptEntry ( textToCommit , true ) ;
250+ updateSendButton ( ) ;
251+ updateModelContext ( ) ;
252+ }
253+ committedResultCount = 0 ;
254+ interimTranscript = "" ;
255+ }
256+
217257// ============================================================================
218258// UI Helpers
219259// ============================================================================
@@ -271,7 +311,7 @@ function addTranscriptEntry(text: string, isFinal: boolean) {
271311 transcriptEl . appendChild ( entry ) ;
272312}
273313
274- function updateInterimTranscript ( text : string ) {
314+ function updateInterimTranscript ( finalText : string , interimText : string ) {
275315 clearTranscriptPlaceholder ( ) ;
276316
277317 let interim = transcriptEl . querySelector (
@@ -284,7 +324,21 @@ function updateInterimTranscript(text: string) {
284324 }
285325
286326 const timestamp = new Date ( ) . toLocaleTimeString ( ) ;
287- interim . innerHTML = `<div class="timestamp">${ timestamp } </div>${ escapeHtml ( text ) } ` ;
327+
328+ // Show final text (stable) in normal style, interim text (unstable) in delta style
329+ const finalHtml = escapeHtml ( finalText ) ;
330+ const interimHtml = interimText
331+ ? `<span class="interim-delta">${ escapeHtml ( interimText ) } </span>`
332+ : "" ;
333+
334+ interim . innerHTML = `<div class="timestamp">${ timestamp } </div>${ finalHtml } ${ interimHtml } ` ;
335+ }
336+
337+ function clearInterimTranscript ( ) {
338+ const interim = transcriptEl . querySelector ( ".transcript-entry.interim" ) ;
339+ if ( interim ) {
340+ interim . remove ( ) ;
341+ }
288342}
289343
290344function escapeHtml ( text : string ) : string {
0 commit comments