Recommended preparation

Try first

Frame your answer before reading the reference

Mid-level15 min first pass

Candidate prompt

Design the composer and current reply for an AI chat app. A user may type Japanese with an IME, attach a file that is still uploading, press Enter twice, or stop a streaming answer and retry. An event from the old Send, Stop, or stream must never change the new active turn. Assign the state owned by the textarea, draft store, upload flow, and current turn. Explain recovery when a Send or Stop response is lost.

Constraints

  • IME Enter cannot send.
  • Only ready attachments may send.
  • An active reply turns Send into Stop.
  • Superseded events are ignored.

Helpful before you start

  • Controlled text input
  • AbortController
  • Streaming event basics

What good looks like in a 15-minute first pass

Must cover

  • IME composition and unready attachments cannot send.
  • Stable identities protect one logical send and active stream.

Strong signals

  • Drafting continues, but Send becomes Stop during an active reply.
  • Reconnect by sequence or snapshot; reject superseded stream events.

Stretch if time

Account drafts, cross-tab ownership, and safe Markdown.

Avoid

Local abort masquerades as server Stop, or stale streams mutate the active turn.

Need a hint? Three decisions to make
  1. Separate persisted draft, transient composition, and attachment readiness state.
  2. Choose stable identities for send retransmission, stream events, and Stop retransmission.
  3. Define terminal retry, sequence resume, and snapshot recovery without crossing turn identity.
Practice this exact case

Guided mock requires a tablet or desktop viewport of at least 768px. It will not start automatically.

Requirements

A 15-minute response path
  1. Frame the interaction boundary
    Limit the case to the composer, upload readiness, and one current assistant turn. History and model execution stay behind abstract contracts. The user may edit a new draft while a reply streams, but Send becomes Stop and cannot start a second response.
  2. Separate input and upload state
    Keep composition status in the mounted textarea, recoverable text in an account-and-conversation draft, and each file in the upload flow. Enter submits only after compositionend; Shift+Enter preserves a newline. A snapshot contains only ready asset IDs. IME Enter, an uploading file, or a second Enter while Send is pending cannot submit.
  3. Create one logical send
    Freeze text, ready attachment IDs, commandId, clientMessageId, and draft revision before clearing that revision. Show the user row immediately. If the Send response is lost, retransmit the same commandId; transport retry is not new intent. Later text belongs to a new editable draft.
  4. Stream and stop by identity
    Bind one assistant placeholder to streamId and accept only its next sequence. Stop creates one stopCommandId and reuses it after response loss. AbortSignal may close the browser reader, but local abort is not authoritative server Stop. Preserve partial text; keep Send unavailable until an event or snapshot confirms complete, stopped, or failed.
  5. Reconcile before the next turn
    Reconnect after lastSequence or fetch getTurnSnapshot. Merge results only when command, message, stream, and sequence identities match. After terminal state, Retry or Regenerate creates a fresh commandId and new streamId. Delayed Send, Stop, or stream results fail the active-turn check. Announce meaningful pending, stopped, failed, and completed changes, not token deltas.
Rejected alternative: overlapping responses
Allowing a second Send while the first response streams makes the Stop target ambiguous and lets answers appear beside the wrong prompt after a race. That user cost is not justified for this scope. Drafting stays available, but Send becomes Stop until the active turn reaches an authoritative terminal state.

Frontend boundary

The browser owns composition-safe input, autosizing, draft recovery, attachment presentation, command coordination, streamed rendering, and focus recovery. The backend is an abstract service contract for send, Stop, event resume, and authoritative snapshots; durable history and model execution are out of scope.

Architecture

Core building blocks
PieceResponsibilityDesign rationale
ComposerViewText, IME events, shortcuts, autosize, and focusKeeps native input semantics at the interaction boundary.
DraftStoreScopes and persists unsent text, excluding transient IME stateAllows navigation recovery without serializing mounted-control state or writing conversation history.
TurnCoordinatorOwns the stored Send and Stop commands, optimistic row, only active stream, and terminal reconciliationRetransmission keeps an intent identity; Retry after terminal creates a new one.
AttachmentCoordinatorTracks local, uploading, ready, and failed attachmentsSend includes only finalized asset IDs.
StreamAdapterParses lowercase SSE fields and exposes typed sequenced eventsTransport parsing stays outside components.
ComposerView (transient IME interaction)
  -> DraftStore (account + conversation + text revision)
  -> AttachmentCoordinator (local file -> finalized asset ID)
  -> TurnCoordinator (stored Send + stored Stop + only active turn)
  -> ChatTurnClient (send + resume + snapshot + authoritative Stop)
  -> StreamAdapter (stream ID + message ID + sequence)
  -> Current-turn reducer (identity gate + terminal state)
  -> MessageList and restrained status announcements
Composer and stream ownership
ComposerView owns transient composition, DraftStore owns recoverable unsent text, AttachmentCoordinator owns readiness, and TurnCoordinator owns the stored command plus only active response. StreamAdapter parses transport records but cannot bypass the reducer's identity and sequence checks.

Worked example: Enter during IME composition followed by cancel

A Japanese input method is composing text when Enter is pressed. The user later submits the completed prompt with a ready attachment, starts another draft, and stops the streaming response. Keyboard shortcuts must respect composition, drafting must remain available, and Stop must not erase accepted partial text.
Scenario walkthrough
EventStore changeVisible UIInvariant
Enter during compositionObserve composition state and do not submit.The IME commits or selects text normally.Shortcut does not corrupt input.
Composition endsUpdate the draft and validated attachment references.Autosize follows measured content within bounds.Draft is one coherent value.
User sendsStore commandId plus clientMessageId and clear only the submitted draft revision.The user row appears; a fresh draft remains editable; Send becomes Stop.One response is active and a lost Send response reuses the stored command.
User stops streamCreate one stopCommandId, optionally abort local reading, and retain partial text while authoritative status reconciles.Stop remains pending, then becomes Retry or Regenerate after terminal confirmation.Local cancellation is not proof of server cancellation or message deletion.
User retries after stoppedAllocate a new commandId; acknowledgement establishes a new streamId.A new assistant reply starts from the preserved prompt or product-defined retry input.Late events from the prior Send, Stop, or stream cannot mutate the new turn.
Boundary diagram separating composer draft and attachment readiness from send commands, turn snapshots, and ordered stream events.
Composer and stream boundaries: one command creates one logical message; stream identity gates every event.
Read diagram as text

Text fallback: The composer owns text, IME state, and attachment readiness. Send freezes a snapshot with a command ID. The turn store accepts snapshot or event data only when conversation, message, stream, and sequence identities match.

Data

Persisted unsent text, transient IME interaction, upload work, a stored send command, the optimistic message, and sequenced assistant output have different lifetimes. The store exposes at most one nonterminal StreamMessage, while a new draft may evolve independently.
type AttachmentPhase = 'local' | 'uploading' | 'ready' | 'error';
type StreamPhase = 'pending' | 'streaming' | 'complete' | 'stopped' | 'failed';

interface ComposerDraft {
  accountId: string;
  conversationId: string;
  text: string;
  revision: number;
  savedAt: number;
  attachmentLocalIds: string[];
}

interface ComposerInteractionState {
  isComposing: boolean;
}

interface AttachmentDraft {
  localId: string;
  phase: AttachmentPhase;
  assetId: string | null;
  error: string | null;
}

interface PendingSend {
  conversationId: string;
  commandId: string;
  clientMessageId: string;
  draftRevision: number;
  phase: 'sending' | 'acknowledged' | 'uncertain';
}

interface StreamMessage {
  conversationId: string;
  commandId: string;
  clientMessageId: string;
  messageId: string;
  streamId: string;
  lastSequence: number;
  text: string;
  phase: StreamPhase;
}

interface TurnSnapshot extends StreamMessage {
  updatedAt: number;
}

Draft, send-command, and stream ownership

ComposerDraft is keyed by account and conversation; ComposerInteractionState exists only while the textarea is mounted. AttachmentDraft owns upload readiness. PendingSend preserves the immutable send snapshot for acknowledgement loss. StreamMessage is accepted current-turn state, not a copy of the draft. Its terminal phase releases the one-active-response guard without deleting partial assistant text.
Identity reuse rules
SituationIdentity policyResult
Send response is lostRetransmit commandId plus clientMessageIdThe same logical user message converges
Stop response is lostRetransmit the existing stopCommandIdOne Stop intent is applied idempotently
User retries after terminalCreate a new commandId; accept a new streamIdRetry or Regenerate is a new user intent
Old event arrivesCompare command, message, stream, and sequenceA superseded turn cannot change active state

Interface

Expose draft editing independently from the current response. The view receives attachment readiness, a primary action that is Send or Stop, the only active turn, and terminal Retry or Regenerate. Raw transport parsing and history loading remain outside view components.
interface SendCommand {
  commandId: string;
  clientMessageId: string;
  text: string;
  attachmentIds: string[];
}

interface StopCommand {
  stopCommandId: string;
  conversationId: string;
  streamId: string;
}

type TurnEvent =
  | { type: 'message.delta'; conversationId: string; streamId: string; messageId: string; sequence: number; delta: string }
  | { type: 'message.completed'; conversationId: string; streamId: string; messageId: string; sequence: number }
  | { type: 'message.stopped'; conversationId: string; streamId: string; messageId: string; sequence: number }
  | { type: 'message.failed'; conversationId: string; streamId: string; messageId: string; sequence: number; errorCode: string };

interface OpenTurnEventsInput {
  conversationId: string;
  streamId: string;
  afterSequence: number;
  signal: AbortSignal;
}

interface UseChatComposerResult {
  draft: ComposerDraft;
  interaction: ComposerInteractionState;
  attachments: AttachmentDraft[];
  activeTurn: StreamMessage | null;
  primaryAction: 'send' | 'stop';
  updateText(text: string): void;
  send(command: SendCommand): Promise<void>;
  retransmitSend(): Promise<void>;
  stop(command: StopCommand): Promise<void>;
  retryAfterTerminal(command: SendCommand): Promise<void>;
}

interface ChatTurnClient {
  send(input: SendCommand & { conversationId: string; signal: AbortSignal }): Promise<{
    acceptedCommandId: string;
    acceptedClientMessageId: string;
    messageId: string;
    assistantMessageId: string;
    streamId: string;
  }>;
  openEvents(input: OpenTurnEventsInput): AsyncIterable<TurnEvent>;
  stop(input: StopCommand & { signal: AbortSignal }): Promise<{
    acceptedStopCommandId: string;
  }>;
  getTurnSnapshot(input: {
    conversationId: string;
    commandId: string;
    clientMessageId: string;
    streamId: string;
    signal: AbortSignal;
  }): Promise<TurnSnapshot>;
}
event: message.delta
data: {"conversationId":"c_123","streamId":"s_123","messageId":"m_123","sequence":1,"delta":"hello"}

event: message.completed
data: {"conversationId":"c_123","streamId":"s_123","messageId":"m_123","sequence":2}

Integration flow
  1. Restore
    Restore the account-and-conversation draft independently from recent messages.
  2. Send or retransmit
    After composition ends and attachments are ready, call send with stable command and client message IDs. If only its response is lost, retransmit that same commandId.
  3. Open or resume events
    Open with afterSequence equal to lastSequence. Accept a next-sequence event only for the active conversationId, streamId, and messageId.
  4. Stop and reconcile
    Create one stopCommandId per user intent and reuse it after a lost response. Local reading may abort, but resumed events or getTurnSnapshot must still confirm authoritative terminal status.
  5. Retry after terminal
    Only complete, stopped, or failed exposes Retry or Regenerate. That user action creates a new Send command and accepts a new streamId; the old command is never repurposed.

UI-facing contract

Send accepts text, finalized attachment IDs, commandId, clientMessageId, and AbortSignal at the client boundary. AbortSignal ending a browser request says nothing about whether the server accepted Send or Stop. The coordinator therefore retains the original command, resumes ordered events, or requests an authoritative snapshot. Components see a single primary action and terminal recovery; they never parse SSE or choose which late event wins.

Optimizations

Scenario: a Stop response is lost
  1. Keep the composer responsive
    The assistant reply is streaming from streamId s1. Send is rendered as Stop, yet the user may edit a separate draft. Recent history is paged independently, so neither a long transcript nor current-turn recovery blocks typing.
  2. Store the Stop intent
    The user activates Stop. The coordinator creates stopCommandId x1, targets s1, and may abort the local reader for immediate feedback. The partial assistant text remains visible with a stopping status.
  3. Recover uncertain authority
    The Stop response is lost. Retrying reuses x1; it does not invent another cancellation intent. Local abort still is not authoritative Stop, so the client reconnects with afterSequence or asks getTurnSnapshot for s1.
  4. Reject the late result
    A delayed delta or Send acknowledgement from s1 is accepted only while its command, message, stream, and sequence match the active turn. Once a terminal snapshot is stored, any later s1 event is ignored without erasing preserved text.
  5. Create a new retry turn
    After stopped or failed is authoritative, Retry or Regenerate creates a new commandId and receives streamId s2. A late Stop result for x1 may confirm old history, but it cannot change s2 or the new draft.

Accessibility behavior

Give the textarea a persistent label and describe Enter versus modified Enter without overriding platform composition. Each attachment exposes its name, phase, error, Retry, and Remove controls. Do not announce token deltas; use one concise polite announcement when Send becomes Stop and when the turn completes, stops, or fails. Keep focus in the composer after ordinary Send, and move it only for a recovery action the user requested.

Draft confidentiality and safe rendering

Namespace persisted drafts and attachment handles by accountId plus conversationId. On account switch, detach the previous account's in-memory draft before loading the next namespace. On logout, delete drafts and local handles unless a product-approved retention policy applies; retained drafts need a bounded TTL and a visible clear-drafts control. Browser storage is not a confidentiality boundary, so draft text must not enter telemetry, and sensitive modes may use memory-only drafts. Cross-tab coordination elects one current-turn owner while letting each tab keep transient composition state.
Treat assistant content as untrusted even when it arrives from the product API. Parse only supported Markdown, sanitize the result, allowlist link protocols, and render code as inert text; never pass streamed or completed assistant HTML directly to innerHTML. Apply that boundary to partial updates because unfinished Markdown is still untrusted input.

Performance and measurement

Page older history independently from current-turn state. Add list virtualization or coalesce visual delta commits only after profiling representative long conversations and low-end devices; accepted sequence state still updates correctly even when paints are batched. Test IME composition, duplicate Enter, attachment races, lost Send and Stop responses, reconnect gaps, account changes, hostile Markdown, keyboard recovery, and narrow layouts. Compare duplicate-message rate, stale-event suppression, stop latency, draft loss, input responsiveness, and recovery completion by device and network cohort.
Timeline where stop and retry replace the active stream and a late event from the old stream is ignored.
Stop and retry timeline: stable command identities converge while stale stream events cannot mutate the new turn.
Read diagram as text

Text fallback: Stop targets stream one with a stable stop command. Retry creates stream two after reconciliation. A delayed delta from stream one fails the active-stream check and is ignored.

Use the Question Library for baseline coverage, then move into a Study Plan before targeted Company Prep.