> ## Documentation Index
> Fetch the complete documentation index at: https://docs.corti.ai/llms.txt
> Use this file to discover all available pages before exploring further.

# Interim & final result handling

> Learn best practices for displaying interim previews and committing final transcript results

The `/transcribe` WebSocket emits two kinds of transcript messages: interim results (`isFinal: false`) that preview in-progress recognition, and final results (`isFinal: true`) that are ready to commit to your document. How you display interims and commit finals in your UI affects perceived latency, accuracy, and the stability of the editing experience.

<Note>
  For enabling interim results and understanding the feature, see [Interim Results](/stt/interim-results). For spacing and casing rules at the insertion boundary, see [Transcript Text Handling](/stt/best-practices-transcribe).
</Note>

***

## The committedText / interimText model

Maintain two pieces of state to separate what is committed from what is still a preview:

* `committedText`: text already inserted into the document and visible to the user.
* `interimText`: the current interim preview, shown outside the document or in a preview layer.

On each transcript message:

* If `isFinal: false`: replace `interimText` with the new segment (after applying boundary spacing). Keep only the newest interim for the current unfinished span.
* If `isFinal: true`: append the final segment to `committedText` and clear `interimText`.

```ts title="committed-text-interim-text-model.ts" expandable theme={null}
interface DictationState {
  /** Text already committed to the document. */
  committedText: string;
  /** Current interim preview, replaced on each new interim. */
  interimText: string;
  /** End time of the most recently finalized transcript. */
  lastFinalEnd: number;
}

/**
 * Process an incoming transcript message.
 * Apply boundary spacing with buildInsertion() before replacing or
 * committing text — see Transcript Text Handling for the rules.
 */
function handleTranscript(
  state: DictationState,
  text: string,
  isFinal: boolean,
  end: number,
): DictationState {
  if (!isFinal) {
    // Replace the active interim with the newest preview.
    return { ...state, interimText: text };
  }

  // Final: commit into the document, clear the interim buffer.
  return {
    committedText: state.committedText + state.interimText + text,
    interimText: "",
    lastFinalEnd: end,
  };
}

/**
 * Clear the interim preview when a command is recognized.
 * Call this BEFORE dispatching the command action, not after.
 */
function clearInterimForCommand(state: DictationState): DictationState {
  return { ...state, interimText: "" };
}
```

<Note>
  Maintain a single active interim span and replace it as newer interim results arrive. Do not accumulate multiple interim results into committed text.
</Note>

***

## Dedicated preview area (recommended starting point)

The simplest approach is to show interim text in a separate element below or beside the editor, toggled via show/hide. The editor's content is only modified when a final transcript is committed.

<Card title="Recommendation">
  \--

  * Show interim text in a dedicated `<p>` or `<span>` outside the editor.
  * Toggle its visibility: show when interim text arrives, hide when the interim is committed or cleared.
  * On `isFinal: true`, commit the final segment into the document using your insertion logic and clear the interim preview.
  * On command recognition, clear the interim preview **before** dispatching the command action. Clearing after dispatch or conditionally on whether the command was handled causes the command phrase to linger.
</Card>

### Clear interim before command dispatch

When a command is recognized, the transcript text for the command phrase may still be in the interim buffer. Clear it the instant the command is detected, before any dispatch or action execution:

```ts theme={null}
function onCommand(command: string) {
  // Clear the interim preview first.
  state = clearInterimForCommand(state);
  // Then dispatch the command.
  dispatchCommand(command);
}
```

<Note>
  The `clearInterimForCommand` function is included in the state model snippet above. If you clear after dispatch or conditionally, the spoken command phrase can appear briefly in the preview before the command takes effect.
</Note>

### Out-of-order messages after stop/flush

After a stop or flush, transcript messages may arrive out of order. A final result may have already been committed for a span, and a late interim for that same span arrives afterward.

To handle this, use `data.start` and `data.end` as identity keys:

* `start` is the primary identity key.
* `end` is the secondary stale-overlap check during stop/flush races.

Skip any interim whose time span overlaps a recognized command (the command handler already cleared the preview) or whose `end` predates a finalized transcript (an out-of-order message from a stop/flush race).

```ts title="stale-interim-guard.ts" expandable theme={null}
interface StaleGuardState {
  /** End time of the most recently finalized transcript. */
  lastFinalEnd: number;
  /** Time spans of recognized commands. */
  commandSpans: Array<{ start: number; end: number }>;
}

/**
 * Returns true if an interim is stale and should be skipped.
 * Uses data.start / data.end as identity keys.
 */
function isStaleInterim(
  state: StaleGuardState,
  interimStart: number,
  interimEnd: number,
): boolean {
  // Skip interims that overlap a recognized command — the command
  // handler already cleared the preview.
  for (const span of state.commandSpans) {
    if (interimStart < span.end && interimEnd > span.start) {
      return true;
    }
  }

  // Skip interims that predate a finalized transcript — an
  // out-of-order message from a stop/flush race.
  if (interimEnd <= state.lastFinalEnd) {
    return true;
  }

  return false;
}
```

### Cursor movement during active dictation

If the user moves the cursor or changes selection while dictating, treat it as a new insertion context:

* Clear the interim span.
* Send a `flush` message so remaining buffered audio returns transcripts promptly, then continue dictation in the new location. The API responds with `type: "flushed"` when done returning text from audio received before the flush message was sent.

If there is an active selection, decide explicitly whether to replace the selection with the inserted transcript (common) or collapse the selection to an insertion point before inserting.

***

## Inline at caret position (advanced)

For a more polished experience, interim text can be shown as an overlay at the caret's pixel coordinates, appearing inline with the document text without modifying the editor's content. This approach is more complex and requires care to avoid common pitfalls.

<Note>
  This section describes editor-agnostic principles and editor-type-specific considerations. A full implementation depends on your editor framework.
</Note>

### Editor-agnostic principles

These apply regardless of editor type (textarea, contenteditable, Tiptap, Lexical, ProseMirror, native controls):

* **Overlay, not DOM injection.** Do not pollute the editor's document state or undo history with temporary text. Render the interim as a sibling element positioned over the editor. The editor's content is only modified when a final transcript is committed.
* **Mirror the insert target.** The overlay position and the actual insert must resolve to the same logical location. If your adapter clamps to end-of-content when focus is outside the editor, the overlay must use the same clamped position.
* **Stale interim guards.** Track the command's `start`/`end` time span and the most recently finalized transcript's `end` time. Skip any interim whose time span overlaps a command (the command handler already cleared the preview) or whose `end` predates a finalized transcript. These guards use transcript message fields (`data.start`, `data.end`), not DOM APIs.
* **Clear interim before command dispatch.** Clear the preview the instant a command is recognized, before any dispatch or action execution. Clearing after dispatch or conditionally on whether the command was handled causes the command phrase to linger.
* **Performance.** Throttle caret measurements (coalesce bursts via `requestAnimationFrame` or equivalent). Cache style reads (padding, line-height) instead of reading them on every measurement. Skip measurement when the selection or focus change is outside the editor.

### Caret coordinate resolution by editor type

The overlay needs pixel coordinates for the caret position. The approach depends on the editor:

* **Textarea / input.** The caret's pixel position is not directly accessible. Use a mirror-div technique: a hidden `<div>` with identical font, padding, and width; render the text up to the caret offset and measure the resulting span's bounding rect.
* **Contenteditable (raw DOM).** Create a collapsed `Range` at the caret point and call `getClientRects()`. Do not use `getBoundingClientRect()`; it returns zeros for collapsed ranges. At `<br>` or wrapping boundaries, `getClientRects()` returns multiple rects; pick the one with the smallest `left` (the start of the next line where new text will land).
* **Rich editors (Tiptap, Lexical, ProseMirror).** Use the editor's coordinate API directly (e.g. Tiptap's `view.coordsAtPos()`, ProseMirror's `coordsAtPos()`). These return reliable pixel coordinates without Range API workarounds.
* **Native controls (Windows, macOS).** Use the platform's accessibility API or caret-rect query. Coordinate systems differ from web.

### Contenteditable-specific implementation notes

The following issues are specific to contenteditable and do not apply to textarea or rich editor frameworks:

* **Structural trailing `<br>`.** Every contenteditable has one (leftover from the empty editor's `<div><br></div>`). It must be excluded from trailing-break counting because it never moves the caret to a new line; every insert goes before it.
* **Half-leading compensation.** The overlay span inherits the editor's `line-height` (e.g. 22.75px) but the caret rect is the font's em-box (e.g. 17px). The line-box is taller by half-leading on each side, so anchoring at the caret's top places the overlay's glyphs below the caret's. Lift the overlay by `(lineHeight - caretRect.height) / 2`.
* **Empty text node anchoring after trailing `<br>`.** Chrome paints a caret anchored after an element node (`<br>`) one visual line too high, a well-known contenteditable quirk. Anchor the caret in an empty text node after the `<br>` instead; the empty node contributes nothing to `textContent`, so text-offset-based models are unaffected.

<br />

<Note>
  [Contact us](mailto:help@corti.ai) if you need further assistance working with interim results.
</Note>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.