FixIt — AI-Powered DIY Guide Generator

Inspiration

Every weekend, millions of people stare at a broken shelf, a leaky faucet, or a bare wall — armed with ambition but paralyzed by uncertainty. YouTube tutorials are 20 minutes long for a 3-minute fix. Forum threads from 2009 assume you own tools you've never heard of. And calling a professional for something small feels like admitting defeat.

I wanted to build something that felt like texting a knowledgeable friend: describe your problem, list what you have, get a clear plan in seconds. No ads, no fluff, no "smash that like button." Just your guide, built for your exact situation.

The name FixIt came from that same spirit — direct, confident, actionable.


How I Built It

Architecture

FixIt is a Next.js 15 app running on the App Router, with all AI logic handled server-side through API routes. The stack:

Layer Technology
Frontend Next.js 15, React, Tailwind CSS v4
AI (guides) Claude Opus (claude-opus-4-6)
AI (classification) Claude Haiku (claude-haiku-4-5-20251001)
PDF export Puppeteer (headless Chromium)
i18n Custom React Context + localStorage

The Streaming Pipeline

The biggest architectural decision was making guide generation feel alive rather than showing a spinner for 15 seconds. I implemented a full streaming pipeline:

  1. The /api/generate route calls client.messages.stream() from the Anthropic SDK
  2. It pipes the raw text chunks directly into a ReadableStream<Uint8Array> response
  3. The client reads chunks with ReadableStream.getReader() and accumulates text
  4. On every chunk, a custom parser (tryParsePartialGuide) extracts whatever JSON fields are already complete

The partial JSON parser was the trickiest part. A naïve JSON.parse on incomplete text fails immediately, so I wrote a brace-matching algorithm that's aware of string escaping:

$$ \text{depth}(i) = \sum_{k=0}^{i} \delta(c_k), \quad \delta(c) = \begin{cases} +1 & c = \texttt{{} \text{ or } \texttt{[} \ -1 & c = \texttt{}} \text{ or } \texttt{]} \ 0 & \text{otherwise} \end{cases} $$

A complete JSON object or array is found when $\text{depth}(i) = 0$ for some $i > 0$ — but only when the parser isn't currently inside a string literal (tracking escaped quotes \" and toggle state).

Illustration System

Rather than generating SVG images with AI (which produced inconsistent, ugly results), I built a two-stage classification pipeline:

  1. 16 hand-crafted SVG icons covering every common DIY action (drill, screw, hammer, measure, cut, assemble…) — each at viewBox="0 0 240 160", IKEA-instruction style
  2. Haiku classifies each step into one of those 16 categories (max_tokens: 20, single word response)

This approach costs roughly $0.000025 per step instead of generating a full SVG, and the quality went from "unrecognizable blob" to "clean professional illustration."

PDF Generation

Puppeteer renders the guide as a styled HTML page in a headless browser, then exports it as a pixel-perfect PDF. The template is a self-contained HTML string with inlined CSS — no external dependencies that could fail in a serverless environment.


Challenges

Streaming Partial JSON

The hardest technical challenge was reliably extracting structured data from a half-finished JSON string. Claude streams character by character, so at any moment you might have:

{"titre": "Install a Floating Shelf", "etapes": [{"numero": 1, "titre": "Mark the

The parser needs to extract titre (complete) while ignoring the incomplete etapes array. The solution uses recursive brace/bracket matching with string-escape awareness — if a { appears inside "...", it doesn't open a new depth level.

Preventing JSON Markdown Wrapping

Claude sometimes wraps its JSON output in markdown fences:

```json
{ ... }
```

A simple regex strips these before parsing:

text.replace(/^```(?:json)?\s*/i, "").replace(/\s*```$/, "")

But this only works at the end of the stream. During streaming, fences are accumulated harmlessly and stripped only at the final parse step.

Streaming + Nginx Buffering

ReadableStream responses work beautifully in development but require the explicit header "X-Accel-Buffering": "no" to prevent nginx from buffering chunks into a single response — defeating the entire purpose of streaming.

TypeScript and Union Literal Types

The i18n system hit a subtle TypeScript wall. Defining translations as as const makes every string a literal type:

type T = typeof translations.en
// tagline: "Your AI-powered DIY guide generator"

The French locale has different literal strings, so typeof translations.fr is incompatible. The fix: use a union via the key type:

type T = typeof translations[Language]
// tagline: "Your AI-powered DIY guide generator" | "Votre guide bricolage généré par IA"

This lets both locales satisfy the same interface without type errors.


What I Learned

Streaming UX changes everything. A 12-second wait with a spinner feels broken. The same 12 seconds watching your guide materialize section by section feels fast. The actual latency is identical — perception is the product.

Small models for small tasks. Using Haiku for a one-word classification call (instead of Opus) cuts cost and latency by ~95% for that step, with no quality loss. The right model for the right job matters enormously.

Pre-designed > AI-generated for illustrations. AI-generated SVGs were inconsistent, hallucinated invalid attributes, and often unrecognizable. 16 hand-crafted icons + smart classification produced a better, faster, cheaper result. Sometimes the best use of AI is routing, not generating.

Locale-aware prompting. Simply appending "Generate ALL content in French" to the prompt was sufficient for Claude to produce completely localized guides — including culturally appropriate tool names, units, and phrasing. The model handles localization better than most manual translation pipelines.

Built With

Share this project:

Updates