Blog
Mert Özen's blog posts on software development, backend, frontend, and DevOps.
Why Are AI Interfaces Different? Streaming, Uncertainty and the Angular + ASP.NET Core Architecture
What sets an AI interface apart from a classic CRUD screen is that the answer arrives in pieces, differs every time and can stop halfway: the same question got two different books, a 6 and 13 second time to first token and 2.5 times different token usage in two runs. In this post we set up the architecture between Angular, ASP.NET Core and the model, show why the API key must never enter the bundle, and define the event vocabulary of /chat/sse.
Streaming Chat in Angular: Reading SSE with fetch, resource stream and Rendering Token by Token with Signals
Since EventSource cannot send a POST body, we read the LLM stream in Angular with fetch, ReadableStream and TextDecoderStream and write the events into signals. In this post we measure a UTF-8 character split at a chunk boundary, a Stop button that reaches the server through AbortController, requestAnimationFrame halving DOM writes, a @for track mistake that recreates the element on every update, and a resource stream that reports resolved while still streaming.
AI Chat State and Error Handling: A State Machine, Partial Answers, Timeouts, 429 and Regenerate
What makes an AI chat reliable is an explicit state machine and telling error types apart: 400 ProblemDetails, 429 with Retry-After, a first-token timeout, an error event mid-stream and a dropped connection. In this post we mark half answers instead of deleting them, separate aborts from errors, show the dev proxy keeping the stream open for 90 seconds after the API process died and a ping-based silence timer catching it in 15, and tie regenerate to the server-side ConversationId history.
Rendering LLM Output Safely: Streaming Markdown, Sanitization, Code Blocks and Citations
Because LLM output is untrusted input, Angular renders it through a three-layer pipeline of marked, DOMPurify and Angular's sanitizer, with no images and a link allowlist. In this post we cut script time to a third by freezing completed blocks, show that a rule trusting empty lines split a list in two and that autolink made a half URL clickable, and render code blocks with a component and [n] citations validated against the source list.
Tool Calls, Approval Flows and Generative UI: Letting the Assistant Choose Components
Showing the assistant's tool calls, tying a sensitive action to a time-limited single-use approval card and letting the model pick an allowed component instead of writing text is done in Angular with typed SSE events and a validating component registry. In this post we show a small model claiming it displayed cards that never appeared, an error text arriving at the UI as props and client validation stopping it, and we look at Hashbrown and its postinstall telemetry.
Connecting Through Standard Protocols: Choosing Between AG-UI, AI SDK UI and Our Own SSE Contract
Choosing between your own SSE contract, AG-UI and AI SDK UI comes down to the backend, the number of clients and the bundle and wire cost you accept. In this post we connect the same assistant to Angular over all three, and measure AG-UI's defaults sending the model's reasoning to the browser in a 368 KB stream, the two protocol libraries adding 500-680 kB to the page, and an abort arriving as RUN_ERROR.
AI UX and Accessibility: Perceived Waiting, aria-live, Keyboard, Feedback and Transparency
A streamed AI answer should reach screen readers as one LiveAnnouncer announcement when the turn ends, not in pieces; focus should stay in the input, and the waiting time and an AI label should be visible. In this post we measure aria-live producing 70 changes for three sentences, the Send/Stop swap dropping focus to body unnoticed by axe-core, fix a contrast issue and wire thumbs up/down feedback to the backend.
Testing a Streaming AI UI: Fake Streams, Vitest Fake Timers, MSW and Playwright
A streaming AI UI is tested deterministically with fake AsyncGenerators that take the model out of the loop, fake timers, MSW responses with ReadableStream bodies and Playwright tests using page.route. In this post we show instanceof DOMException turning a cancellation into an error in the test environment, MSW's sse() handler not fitting a fetch stream, and a ping-less fake stream hitting the 15 s silence timer before the 30 s timeout; 18 tests finish in under 2 seconds.
AI in the Browser and Angular 22's Experimental AI Tooling: Built-in AI APIs and WebMCP
Chrome's built-in AI APIs and Angular 22's experimental WebMCP functions should only be used with feature detection, a time limit and a server path that always works. In this post we show Summarizer returning downloadable, unavailable or nothing across four Chromium 158 setups, produce a review summary on the device first and on the server otherwise, hit NG0203 from inject() after await in a WebMCP tool, and see Angular stay silent in unsupported browsers.