Rajnish Noonia

Tag: React

  • LiveLens: Two Languages, One Browser Tab, Zero Servers — Now with In-Browser AI Chat

    A real-time trading blotter and a webcam object detector have more in common than they look like they should. Both are event-driven: a stream of updates arrives continuously, something needs to render the current state without falling behind, and something else needs to watch that stream for patterns worth flagging – a position that’s moved too far, a count that’s spiked. I’ve spent most of the last decade building the first kind of system for capital markets. LiveLens is what happens when you point the same architectural instincts at a webcam instead of a market feed, and run the whole thing client-side, in a single browser tab, with no backend at all.

    Since the first version the app has grown quite a bit. What started as an object detection demo with a Python analytics layer now also includes a full AI chat interface that runs either entirely in the browser (no API key, no server) or connects to Claude, OpenAI, or Gemini via their streaming APIs. All in the same tab.

    What it does

    Grant camera access and LiveLens detects objects in the live feed, tracks each one’s identity across frames, and continuously computes rolling statistics: per-class counts, average dwell time, flagging anything that looks like a genuine anomaly rather than normal frame-to-frame noise. None of the video, and none of the analysis, ever leaves the tab.

    Flip to the Chat tab and you get a conversational AI interface that lets you run language models completely offline in the browser, or switch over to a cloud provider if you need more horsepower. The two modes share the same UI, same settings panel, same streaming experience. It’s one interface with a source selector, not two separate features bolted together.

    Three layers, three tools, on purpose

    It would be tempting to reach for one language and use it for everything. I split LiveLens into three layers instead, each running the tool that’s actually good at that job:

    • Perception: TensorFlow.js runs a MobileNet-based object detector (coco-ssd) directly in the browser, WebGL-accelerated.
    • Identity: a small TypeScript tracker turns a stream of unlabelled per-frame detections into persistent tracked objects.
    • Statistics: a genuine Python module, running in-browser via Pyodide, does the counting, aggregation, and anomaly detection with pandas and numpy.
    • Chat: Transformers.js v3 runs ONNX-quantised language models directly in the browser via WebGPU or WASM, no API key required. Optionally routes to Claude, OpenAI or Gemini instead.

    Why the neural net runs in JavaScript, not Python

    This is worth being upfront about, because “Python in the browser” is the headline feature and it would be easy to assume the model runs there too. It doesn’t, deliberately.

    In-browser neural-net inference is a mature, well-supported path in the JavaScript ecosystem. TensorFlow.js and transformers.js both have WebGL/WebGPU-accelerated runtimes, wide model support, and years of production hardening. Pyodide, which compiles the CPython interpreter and the scientific Python stack to WebAssembly, doesn’t have an equivalent neural-net execution engine yet. Asking it to run the detector would mean either a much slower pure-Python inference path or shipping a second runtime just to reinvent what TensorFlow.js already does well.

    So the detector runs where the ecosystem is strongest, and Python gets used for what it’s actually strong at: data manipulation and statistics. That division of labour is the whole point of the architecture, not an incidental detail.

    The tracker: turning detections into objects

    coco-ssd gives you a fresh, unlabelled set of bounding boxes on every frame. It has no concept of “the same coffee cup as last frame” – as far as the model is concerned, every detection is a new observation with no history.

    To compute dwell time, or to feed any kind of coherent time series into the analytics layer, that identity has to come from somewhere. LiveLens uses a lightweight IoU-based tracker: on each frame, every new detection is matched to the closest existing track of the same class by bounding-box overlap, using greedy matching against an intersection-over-union threshold.

    function intersectionOverUnion(a, b) {
    const [ax, ay, aw, ah] = a;
    const [bx, by, bw, bh] = b;
    const x1 = Math.max(ax, bx);
    const y1 = Math.max(ay, by);
    const x2 = Math.min(ax + aw, bx + bw);
    const y2 = Math.min(ay + ah, by + bh);
    const interArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
    const unionArea = aw * ah + bw * bh - interArea;
    return unionArea <= 0 ? 0 : interArea / unionArea;
    }

    Tracks that go unmatched for more than 1.5 seconds are dropped. Long enough to bridge a missed frame or brief occlusion without inventing a new identity for the same physical object, short enough that a track doesn’t linger indefinitely after something actually leaves the frame. It’s a simplified relative of the greedy-matching idea behind SORT-style trackers, without a Kalman filter. Accurate enough for a browser demo, not for a production tracking system.

    Pyodide: real pandas, real numpy, zero server

    This is the part that made the project worth writing up. Pyodide loads the full CPython interpreter, plus numpy and pandas, compiled to WebAssembly, and runs it in the same tab as the React app:

    def analyze(window_json: str) -> str:
    objects = json.loads(window_json)
    df = pd.DataFrame(objects)
    counts = df.groupby("class")["id"].nunique().to_dict()
    df["dwell"] = df["last_seen"] - df["first_seen"]
    dwell_ms = df.groupby("class")["dwell"].mean().to_dict()
    anomalies = []
    for cls, count in counts.items():
    hist = _history[cls]
    if len(hist) >= 5:
    mean, std = np.mean(hist), np.std(hist)
    z = (count - mean) / std if std > 0 else 0
    if abs(z) >= 2.5:
    anomalies.append({"class": cls, "count": count, "z": round(z, 2)})
    hist.append(count)
    return json.dumps({"counts": counts, "dwell_ms": dwell_ms, "anomalies": anomalies})

    _history is module-level state inside the Pyodide runtime, so it persists across calls for the life of the tab without React having to manage a rolling buffer itself. React just calls analyze() on an interval with the current tracked-object snapshot and gets back a JSON summary, exactly as if it were talking to a REST endpoint. The difference is there’s no network hop, no serialization over the wire beyond a function call boundary, and no server to deploy or pay for.

    The anomaly check itself is deliberately simple: a rolling per-class count history, a z-score against the mean and standard deviation of the last ~60 windows, and a threshold. It won’t catch anything subtle, but it’s the same basic shape as the alerting logic behind far more sophisticated systems, computed live, over a real stream, with real statistics.

    The AI Chat layer: running language models in the browser

    The newer addition to LiveLens is a chat tab that lets you talk to a language model without leaving the browser. There’s no proxy, no serverless function, nothing phoning home. The model weights download once, get cached by the browser, and run locally on WebGPU or fall back to WASM if WebGPU isn’t available.

    Under the hood it’s Transformers.js v3 running ONNX-quantised models in a Web Worker so the inference doesn’t block the UI thread. The worker loads the pipeline, receives messages, streams tokens back, and the main thread just renders whatever it receives. It’s a clean separation: the camera detection and Python analytics keep running even while the language model is thinking.

    Which models are available offline

    The model picker offers two groups, general purpose and coding, so you can pick based on what you’re actually doing:

    • SmolLM2 135M / 360M / 1.7B: HuggingFace’s small but surprisingly capable series, good for quick questions and demos. The 135M model loads in seconds even on slower connections.
    • Qwen 2.5 0.5B / 1.5B: strong multilingual reasoning from Alibaba, fits comfortably in browser memory.
    • Llama 3.2 1B: Meta’s smallest Llama, good general-purpose model, 2GB download.
    • Phi 3.5 Mini: Microsoft’s model that punches above its weight for reasoning and code, 2.2GB.
    • Qwen2.5-Coder 0.5B / 1.5B / 3B: dedicated code models that are genuinely useful for explaining snippets, debugging, and answering tech questions without sending your code to a third-party server.

    Switching to a coding model automatically swaps the system prompt to something more appropriate. It makes a noticeable difference in the quality of responses for technical questions. The system prompt is fully editable anyway, so you can tune it however you want.

    Cloud API fallback: Claude, OpenAI, Gemini

    The offline models are genuinely useful but they have obvious limits. A 1B parameter model is not going to match GPT-4o on complex reasoning tasks. So the same chat interface also supports switching to a cloud provider, using the exact same streaming UX.

    All three providers stream tokens over server-sent events. There’s a readSSE async generator that handles the data: line parsing, [DONE] termination, and abort signals, and each provider has its own thin wrapper on top that handles the auth header and extracts the right field from the delta object:

    • Claude: API key stored in localStorage, anthropic-dangerous-direct-browser-access: true header to allow direct browser calls, extracts content_block_delta.text_delta from the SSE stream.
    • OpenAI: API key in localStorage, standard chat/completions stream, extracts choices[0].delta.content.
    • Gemini: uses OAuth 2.0 via Google Identity Services rather than an API key, because Gemini supports it and it’s a nicer UX than asking users to generate and paste service account credentials. You provide an OAuth client ID, click Sign in with Google, and it handles the token flow. Roles get mapped to Gemini’s convention (assistant becomes model) before the request goes out.

    API keys and the client ID are stored in localStorage only. They never leave the browser except in the Authorization header of the request to the respective API. No telemetry, no backend logging, nothing persisted anywhere else.

    Settings UX: floating panels, not a modal

    The settings live behind two icon buttons in the chat topbar: a gear icon for model and provider config, and a pencil icon for the system prompt. Clicking either one opens a floating panel that overlays the messages area without pushing any content around or blocking interaction with the rest of the page. The panel is non-modal, so you can still scroll through the conversation and type in the input while it’s open.

    Keeping them separate felt right. Model selection is something you do once per session. The system prompt is something you might actually want to tweak mid-conversation, so giving it its own dedicated panel makes it feel less buried.

    Interrupt and send

    One thing that bothered me about a lot of chat demos is that the Send button disappears while the model is responding and you can’t send anything new until it’s done. If you realise mid-response that your question was badly phrased, or you want to follow up immediately, you have to wait.

    In LiveLens you can send a new message at any point. If generation is in progress, the current response gets aborted, the partial text gets saved into the conversation history, and the new request starts immediately. The button label changes to “Interrupt & Send” during generation so it’s clear what’s going to happen. There’s also a separate Stop button if you just want to halt the current response without sending anything new.

    For offline models the abort signal goes to the Web Worker which terminates the generation loop. For API sources it cancels the fetch via AbortController. Either way the partial text is already in state when the abort fires so nothing gets lost.

    Context management

    A small token usage indicator in the input bar shows roughly what percentage of the model’s context window is consumed. It turns amber at 80%. At that point you probably want to either clear the context or be a bit more concise. “Clear context” is one click and it’s right there next to the usage pill, not buried in a settings menu somewhere.

    The app also remembers the last selected tab, model, and provider across reloads. The offline model restarts loading automatically on page load if that was what you had selected. It’s already cached locally after the first download so it’s fast.

    Why this matters beyond a toy demo

    This is the same shape as a real-time trading blotter watching market data for anomalous price moves, or an operations dashboard flagging unusual order flow: a continuous event stream, an identity/state layer sitting between raw events and anything downstream, and a statistics layer watching for deviation from a rolling baseline. The domain changed from financial instruments to webcam detections. The architecture didn’t. Being able to build that shape entirely client-side, with a real statistical computing stack and no backend at all, says something about how far browser runtimes have come.

    The chat layer adds something different: a language model that runs in the same tab as the rest of the app, that can be pointed at what the detection pipeline is seeing, and that doesn’t require an account or API key to use. Whether that’s useful in production depends on the use case, but the fact that it’s a viable option at all is worth noting. A 1B parameter model that runs locally in a browser tab and streams tokens in real time without a server would have been a strange claim to make even two or three years ago.

    Practical implications

    Detection quality depends on lighting and the model’s speed/accuracy trade-off. lite_mobilenet_v2 is tuned to keep up with live video, not to win accuracy benchmarks. Expect it to miss small or partially occluded objects.

    WebGL matters. TensorFlow.js prefers a WebGL backend; without it the app falls back to WASM, which is noticeably slower. Most modern browsers and GPUs handle this fine, but it’s worth knowing if performance looks off on an older machine.

    Pyodide’s first load has a real cost. Fetching the CPython interpreter plus numpy and pandas as WASM packages is a few megabytes of one-time download, cached by the browser afterwards. The analytics panel shows “Loading Python…” for exactly this reason – it’s not a bug, it’s an honest reflection of what’s happening.

    The tracker is intentionally simple. Greedy IoU matching with no motion prediction means fast-moving objects or heavy occlusion can split one physical object into two tracked IDs. A production system would add a motion model; a demo doesn’t need one to make the point.

    Offline model quality scales with size. The 135M SmolLM2 is genuinely fast but don’t expect it to write production code or reason through anything complicated. The 3B Qwen-Coder is a lot more capable but it’s a 3GB download, so pick based on what you actually need. For anything serious the API providers are the more pragmatic choice anyway. The offline path is there for when you want zero data leaving the machine.

  • Building a TypeScript-First Frontend SDK: An Architectural Journey

    Over the past few years I have spent much of my time on one recurring problem: how do you let many teams build many data-intensive web applications without each of them reinventing the same foundations? This post walks through the architectural journey of designing a TypeScript-first frontend SDK to answer that question. It focuses on the patterns and trade-offs rather than any particular implementation, because the ideas travel well beyond the project that prompted them.

    Application layer Individual applications built on the platform Data grid apps Dashboards Workflow tools Admin tools Others… Framework layer Native React and Angular — thin bindings over the core Angular bindings Components · Directives · DI bridge React bindings Components · Hooks · DI bridge Core Framework-agnostic TypeScript — no UI framework dependency MVVMViewModel · State IoC / DIContainer · Discovery Data gridConfig · Streaming FormsRules · Validation ShellConfig-driven LayoutPanels · Persist ThemingTokens · Dark mode MessagingEvents · Notifications VisualisationCharts · Maps StreamingObservables ObservabilityLogging · Tracing · APM SecurityAuth · Permissions AI-ready contextContext files · Agent rules Scaffolding CLI + delivery pipeline CI/CD · Containers · AWS · dev / test / prod environments
    A layered SDK: framework-agnostic core, thin React/Angular bindings, and applications on top

    The Starting Point: Why a Component Library Is Not Enough

    The usual answer to “we need consistency across teams” is a component library: styled buttons, inputs and grids published as a package. In my experience that solves perhaps a fifth of the real problem. It says nothing about where state lives, how view logic is tested without rendering, how the same feature behaves identically in two UI frameworks, how dependent form fields validate each other, or how any of it stays coherent across years of changing teams.

    So the goal shifted from “shared components” to “shared architecture”: a TypeScript-first, framework-agnostic SDK in which React and Angular are equal, native rendering targets. The choice of view framework becomes a team preference, not an architectural fork.

    Principle 1: MVVM Keeps the View Thin

    The most important decision was to adopt Model–View–ViewModel as the backbone. The view is a template that binds to properties and commands. The ViewModel is a plain TypeScript class that owns state and behaviour. The model is data and services. Nothing else is allowed to blur those lines.

    Three benefits follow. Testability: a ViewModel has no JSX and no decorators, so business logic is tested as ordinary unit tests instead of by mounting components and poking the DOM. Reuse across frameworks: the same ViewModel drives a React view and an Angular view, so behaviour is written once. Clarity: engineers always know where logic belongs, which removes a whole category of code-review debate.

    Principle 2: Dependency Injection Owned by the Core

    MVVM only works well when ViewModels can ask for what they need without knowing where it comes from. Neither framework’s native mechanism fits: Angular’s injector only exists in Angular, and React context is not really DI. The answer was an IoC container that lives in the core, independent of any UI framework, with thin bridges into each one – a hook-based resolver for React and an injector bridge for Angular.

    Services are registered against interfaces, and can be discovered by capability rather than concrete type. This is where SOLID stops being a slide and becomes the path of least resistance: new behaviour arrives as a new registration, not an edit to existing classes, and every dependency can be swapped for a test double.

    Principle 3: Layers With Enforced Boundaries

    The SDK is organised as a monorepo with three layers:

    • Core: pure TypeScript – MVVM primitives, the container, state, messaging, streaming, configuration, theming tokens and platform services. No UI framework dependency.
    • Framework bindings: thin React and Angular packages that render the core’s concepts natively and bridge the container into each framework.
    • Applications: products built on a shared, configuration-driven shell, deployable to the browser or to desktop containers.

    Build-time boundary rules stop applications from reaching into the core directly and reject circular dependencies. Architecture that is only documented erodes; architecture that fails the build stays intact.

    Principle 4: Solve Cross-Cutting Concerns Once

    Most of the value of a platform is in the unglamorous capabilities that every application needs and no team wants to build. Rather than leave them to each project, the SDK provides them as shared services:

    • Configuration-driven shell and layout: navigation, panels and workspaces declared in configuration, with dockable panels whose arrangement is persisted per user.
    • State persistence: any screen can opt in to having column widths, filters, sort order and layout restored on reload.
    • Data grid abstraction: grids configured declaratively, with streaming updates delivered through observables, so teams write configuration and a ViewModel rather than grid plumbing.
    • Forms with a rule engine: field dependencies, visibility and validation expressed as rules; client and server validation surface through one pipeline.
    • Design tokens: semantic colours, spacing and typography exposed as typed constants and CSS variables, with dark mode as a first-class theme.
    • Observability and security: structured logging, tracing and application performance monitoring wired in by default, alongside authentication and permission handling.

    Principle 5: Start Every Project From Working, Not Blank

    A scaffolding CLI turns the SDK into a starting point rather than a set of instructions. A few commands produce an application that already has the shell, theming, authentication, observability, CI/CD pipelines, container configuration and cloud deployment templates for AWS across dev, test and production environments. The decisions that usually consume a project’s first sprint are already made, and every project starts in the same shape – which makes moving between them much easier for engineers.

    Principle 6: Design for AI-Assisted Development

    A newer lesson: an SDK now has two audiences, human developers and AI coding agents. Each package carries a concise context file describing its patterns, naming conventions and rules, and a routing structure points an agent at only the context relevant to its task. Generated projects inherit these files automatically.

    Treating that context as a maintained deliverable – updated alongside code and checked in CI – keeps agents inside the architecture instead of inventing their own. The result is AI assistance that accelerates teams without quietly eroding standards.

    Principle 7: Governance That Scales

    • Enforced module boundaries at build time
    • Semantic versioning with deprecation windows, so nothing disappears without warning
    • Living documentation: component catalogues and visual regression tests in the pipeline
    • Context files as a contract for both humans and agents

    What I Took Away

    The measurable outcome was that applications which once took months could be delivered in weeks, with a consistent look, feel and behaviour. But the more lasting lessons were architectural:

    • Separate behaviour from rendering early; it is very hard to retrofit.
    • Own your dependency injection in the core if you want framework independence.
    • Encode rules in the build, not in wiki pages.
    • Invest in the boring cross-cutting services – that is where teams lose the most time.
    • Treat AI context as part of the product, not an afterthought.

    A good platform is measured by how rarely the teams building on it need to think about it.