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:
- The
/api/generateroute callsclient.messages.stream()from the Anthropic SDK - It pipes the raw text chunks directly into a
ReadableStream<Uint8Array>response - The client reads chunks with
ReadableStream.getReader()and accumulates text - 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:
- 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 - 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
- anthropic
- nextjs
- typescript
Log in or sign up for Devpost to join the conversation.