Build a Streaming Chat Input
Company: OpenAI
Role: Frontend Engineer
Category: Coding & Algorithms
Difficulty: medium
Interview Round: Technical Screen
Quick Answer: This question evaluates frontend engineering skills including streaming data handling, incremental token-by-token rendering, asynchronous state management, and preserving UI responsiveness during long-running API streams.
Part 1: Core Single-Turn Streaming Chat State
Constraints
- 0 <= len(events) <= 100000
- Total length of all prompt, chunk, and error strings <= 1000000
- A submit with an empty or whitespace-only prompt is ignored
- chunk, done, and error events are ignored unless a request is currently loading or streaming
- Duplicate submit events while loading or streaming are ignored
Examples
Input: ([['submit', 'Hi'], ['chunk', 'Hel'], ['chunk', 'lo'], ['done']],)
Expected Output: ['loading|||false', 'streaming|Hel||false', 'streaming|Hello||false', 'success|Hello||true']
Explanation: The response is visible after each chunk, and the form becomes submittable again after done.
Input: ([['submit', 'A'], ['chunk', 'x'], ['submit', 'B'], ['chunk', 'y'], ['done']],)
Expected Output: ['loading|||false', 'streaming|x||false', 'streaming|x||false', 'streaming|xy||false', 'success|xy||true']
Explanation: The second submit is ignored because the first response is still streaming.
Input: ([['submit', 'Question'], ['chunk', 'partial '], ['error', 'network'], ['chunk', 'late']],)
Expected Output: ['loading|||false', 'streaming|partial ||false', 'error|partial |network|true', 'error|partial |network|true']
Explanation: The partial response is preserved on error, and later chunks are ignored.
Input: ([['submit', ' ']],)
Expected Output: ['idle|||true']
Explanation: Whitespace-only prompts do not start a request.
Input: ([],)
Expected Output: []
Explanation: With no events, there are no snapshots.
Hints
- Model the lifecycle with one status value instead of several booleans; this prevents impossible states like loading and error at the same time.
- Only two statuses are considered in-flight: loading and streaming. Most event rules become simple once you define that helper.
Part 2: Multi-Turn Streaming Transcript
Constraints
- 0 <= len(events) <= 100000
- Total length of all text fields <= 1000000
- Only one assistant message may be active at a time
- Empty or whitespace-only prompts are ignored
- If an error occurs, the partial assistant text is kept
Examples
Input: ([['submit', 'Hi'], ['chunk', 'Hello'], ['done'], ['submit', 'Bye'], ['chunk', 'See'], ['chunk', ' ya'], ['done']],)
Expected Output: ['user|sent|Hi', 'assistant|done|Hello', 'user|sent|Bye', 'assistant|done|See ya']
Explanation: Two complete turns are stored in order.
Input: ([['submit', 'A'], ['chunk', 'one'], ['submit', 'B'], ['done']],)
Expected Output: ['user|sent|A', 'assistant|done|one']
Explanation: Prompt B is ignored because assistant A is still streaming.
Input: ([['submit', 'A'], ['chunk', 'partial'], ['error', 'timeout'], ['submit', 'Retry'], ['chunk', 'ok'], ['done']],)
Expected Output: ['user|sent|A', 'assistant|error:timeout|partial', 'user|sent|Retry', 'assistant|done|ok']
Explanation: After the error finalizes the first assistant message, a new turn can begin.
Input: ([['submit', 'Live'], ['chunk', 'still streaming']],)
Expected Output: ['user|sent|Live', 'assistant|streaming|still streaming']
Explanation: An unfinished stream remains marked streaming.
Input: ([],)
Expected Output: []
Explanation: No events produce an empty transcript.
Hints
- When a submit is accepted, append two transcript entries immediately: the user message and an empty assistant message.
- Keep the index of the active assistant message; chunks, done, and error only affect that index.
Part 3: Stop Generating with Cancellation and Stale-Event Handling
Constraints
- 0 <= len(events) <= 100000
- Total length of all text fields <= 1000000
- Accepted submits receive request ids '1', '2', '3', ... in order
- Submitting while a request is active is ignored
- Stale chunk, done, and error events whose request id is not active are ignored
- Calling stop while idle has no effect
Examples
Input: ([['submit', 'A'], ['chunk', '1', 'Hel'], ['stop'], ['chunk', '1', 'lo'], ['done', '1']],)
Expected Output: ['user|-|sent|A', 'assistant|1|stopped|Hel']
Explanation: Stop preserves the partial text. Later events for request 1 are ignored.
Input: ([['submit', 'A'], ['chunk', '1', 'H'], ['stop'], ['submit', 'B'], ['chunk', '1', ' stale'], ['chunk', '2', 'OK'], ['done', '2']],)
Expected Output: ['user|-|sent|A', 'assistant|1|stopped|H', 'user|-|sent|B', 'assistant|2|done|OK']
Explanation: A stale chunk from stopped request 1 cannot affect request 2.
Input: ([['submit', 'A'], ['submit', 'B'], ['chunk', '1', 'X'], ['done', '1']],)
Expected Output: ['user|-|sent|A', 'assistant|1|done|X']
Explanation: The duplicate submit is ignored while request 1 is active.
Input: ([['submit', 'A'], ['chunk', '1', 'partial'], ['error', '1', 'network']],)
Expected Output: ['user|-|sent|A', 'assistant|1|error:network|partial']
Explanation: A real error finalizes the active assistant message as an error.
Input: ([['stop'], ['chunk', '1', 'late'], ['done', '1']],)
Expected Output: []
Explanation: Stopping while idle and stale stream events do nothing.
Hints
- Store both the active request id and the transcript index of its assistant message.
- After every asynchronous boundary in a real UI, you would check whether the request id still matches; this simulation asks you to do the same for every stream event.
Part 4: Long Streaming Responses and Scroll Pinning
Constraints
- 1 <= viewport_height <= 1000000
- 0 <= threshold <= 1000000
- 0 <= initial_content_height <= 1000000
- 0 <= len(event_types) = len(values) <= 100000
- For append events, 0 <= added height <= 1000000
- All scrollTop values are clamped to the valid range [0, max(0, content_height - viewport_height)]
Examples
Input: (100, 10, 200, 100, ['append', 'user_scroll', 'append', 'user_scroll', 'append'], [20, 50, 30, 150, 10])
Expected Output: [120, 50, 50, 150, 160]
Explanation: The viewport starts at bottom, auto-scrolls once, stops auto-scrolling after the user scrolls up, then resumes after the user scrolls back to bottom.
Input: (100, 15, 300, 185, ['append'], [20])
Expected Output: [220]
Explanation: The initial position is within 15 pixels of bottom, so it is considered pinned and auto-scrolls.
Input: (100, 5, 50, 0, ['append', 'append'], [20, 40])
Expected Output: [0, 10]
Explanation: Content shorter than the viewport has scrollTop 0. Once content exceeds the viewport, a pinned view moves to the new bottom.
Input: (100, 10, 200, 100, ['user_scroll', 'append', 'user_scroll', 'append'], [-30, 50, 999, 25])
Expected Output: [0, 0, 150, 175]
Explanation: User scroll positions are clamped. After scrolling to top, appends do not auto-scroll; after scrolling to bottom, they do.
Input: (100, 10, 200, 50, [], [])
Expected Output: []
Explanation: No events produce no scroll positions.
Hints
- Track whether the user is pinned to the bottom. A user is pinned if bottom_scrollTop - current_scrollTop <= threshold.
- For an append event, decide whether to auto-scroll based on whether the user was pinned before the append.
Part 5: Accessible Streaming Announcements with Deterministic Batching
Constraints
- 0 <= len(times) = len(types) = len(texts) <= 100000
- 0 <= times[i] <= 1000000000
- times is sorted in nondecreasing order
- 1 <= interval_ms <= 1000000000
- Total length of all text fields <= 1000000
- Before processing an event at time t, if a scheduled flush time is <= t, flush pending announcement text at the scheduled time
- A done event flushes pending text immediately at its time
- An error event flushes pending text immediately, then announces 'Error: <message>' at the same time
Examples
Input: ([0, 30, 120, 130], ['chunk', 'chunk', 'chunk', 'done'], ['Hel', 'lo', '!', ''], 100)
Expected Output: ['VISUAL|Hello!', 'ANNOUNCE|100|Hello', 'ANNOUNCE|130|!']
Explanation: The first two chunks are batched at time 100. The final chunk is flushed early by done.
Input: ([0, 50, 60], ['chunk', 'chunk', 'done'], ['A', 'B', ''], 100)
Expected Output: ['VISUAL|AB', 'ANNOUNCE|60|AB']
Explanation: Completion before the timer fires flushes the pending text immediately.
Input: ([0, 40, 80], ['chunk', 'chunk', 'error'], ['Par', 'tial', 'network'], 100)
Expected Output: ['VISUAL|Partial', 'ANNOUNCE|80|Partial', 'ANNOUNCE|80|Error: network']
Explanation: On error, partial streamed text is announced first, followed by an error announcement.
Input: ([0, 100, 150], ['chunk', 'chunk', 'done'], ['A', 'B', ''], 100)
Expected Output: ['VISUAL|AB', 'ANNOUNCE|100|A', 'ANNOUNCE|150|B']
Explanation: A scheduled flush at exactly the same time as an event happens before that event is processed.
Input: ([], [], [], 100)
Expected Output: ['VISUAL|']
Explanation: No stream events means no visual text and no announcements.
Hints
- Separate visual state from announced state: visual text appends on every chunk, but announcements use a pending buffer.
- Think of there being at most one scheduled timer. A chunk starts it if none exists; done or error cancels it after flushing.