Back
AI / Tooling

DAEMON.

A streaming AI terminal embedded in my portfolio. Visitors can ask it anything about my work, background, and availability — and get answers in real time, in character.

Next.js 15TypeScriptClaude APIOllamaFramer MotionStreaming (ReadableStream)

Problem

Most portfolio contact flows are passive — a form, an email, a LinkedIn link. Visitors read static copy and decide whether to reach out. I wanted something that could answer questions about my work on demand, handle varied phrasing, and feel like a real product rather than a novelty widget.

The challenge: an AI component that actually works (not a fake typewriter), is scoped to accurate information, and doesn't break the visual tone of the portfolio.

Architecture

The terminal is a DaemonTerminal client component with three screen states: boot (startup animation), idle (cycling taglines, glitch title), and chat(streaming conversation). State transitions are handled by Framer Motion's AnimatePresence.

The /api/chat route handler on the server receives the full conversation history on each request, prepends the system prompt, and proxies to the model. The response is a ReadableStream — the client reads tokens as they arrive and appends them to the display buffer.

A separate /api/weather route handles the Oslo weather shortcut, fetching from Open-Meteo (no API key required) and returning a formatted one-liner.

Key decisions

Streaming over request/response

Immediate feedback matters in a terminal interface. I used ReadableStream with a custom reader loop so each token renders as it arrives — no skeleton loaders, no loading spinner. The perceived latency is near-zero even when the model takes 3–4 seconds to complete.

Ollama as local fallback

The primary model runs via the Claude API. On local dev I run Ollama so I can iterate without API costs. The route handler accepts the same message format from both, keeping the frontend agnostic to which model is responding.

System prompt as brand layer

DAEMON's personality — terse, local, knowledgeable about my work — is defined in the system prompt on the server, never exposed client-side. The prompt contains my CV, project list, and response rules. This means the terminal answers accurately about my work without hallucinating.

Inactivity return to idle

After 30 seconds of no input, the terminal resets to idle state and clears conversation history. This keeps the experience clean for the next visitor and avoids leaking conversation context across sessions.

Glitch as signal, not noise

The title glitch effect fires every 7–14 seconds and swaps exactly one character for a code-flavoured glyph. Subtle enough to reward attention, not so frequent it becomes distracting. Framer Motion AnimatePresence handles the tagline transitions.

What I'd change

The current implementation holds full conversation history in React state — fine for a demo, but it means history is lost on refresh and grows unbounded. A next version would persist sessions to localStorage with a TTL, and add a token budget guard on the server to prevent runaway costs on long conversations.

I'd also add a rate limiter on the API route (currently open) and proper error boundaries around the streaming reader loop.