Skip to content

hutfut/josh-bot

Repository files navigation

josh-bot

CI

An interactive portfolio site built as a parody AI product page. A chatbot answers questions about my experience, skills, and opinions through selectable AI voice personalities, each prompt-engineered with a distinct tone over a shared knowledge base.

Architecture

Responses are generated server-side via the Anthropic Claude API and streamed to the client in real time. The system prompt is composed from three layers:

  1. Knowledge base: factual context about my career, skills, and background (maintained as a TypeScript module)
  2. Voice personality: per-voice prompt that controls tone and style
  3. Persona context: adjusts content emphasis based on the selected audience (recruiter, engineer, general)

Follow-up suggestions are extracted from LLM output to guide conversation contextually.

Available Voices

Voice Personality
The Butler Formal, dry, begrudgingly helpful
The Engineer Blunt, direct, minimal embellishment
The Spokesperson Corporate PR tone, buzzword-heavy
The Hype Man Unreasonably enthusiastic

Voice selection uses a two-phase UX: prominent voice cards on first load (before the conversation starts), collapsing to a compact sticky bar once the user sends their first message. Voices can be switched mid-conversation without resetting messages or session state.

Prompt Injection Defense

Multiple independent layers to reduce the attack surface for prompt injection and jailbreaking.

Server-side sessions: Conversation history is stored server-side, keyed by session token. The client sends a session ID and the current message, never the history array. Prevents fabricated conversation history.

Sandwich defense prompt: Security directives placed at the start and end of the system prompt. Personality and factual context sit in the middle.

Input sanitization: Messages scanned against regex patterns for known injection techniques (instruction overrides, role/delimiter injection, prompt extraction, etc.). Unicode-normalized (NFKC) to handle homoglyph attacks. Flagged inputs are logged.

Canary tokens & output filtering: A canary identifier is embedded in the system prompt. Streamed output is checked against the canary and known prompt fragments in real time. Detected leaks kill the stream.

Input allowlisting: voiceId and persona validated against Set-based allowlists. Invalid values fall back to defaults.

Server-side session caps: The per-session message limit is enforced in a session store

Rate limiting: Two-tier per-IP (10/min, 100/day) via Upstash Redis, with in-memory fallback for local dev.

Tech Stack

Layer Technology
Framework SvelteKit, Svelte 5 (runes API)
Language TypeScript
Styling Tailwind CSS v4
LLM Anthropic Claude API (streaming)
Rate Limiting Upstash Redis: two-tier per-IP (10/min, 100/day)
Sessions In-memory server-side store (Redis-swappable)
Analytics PostHog (product analytics, event tracking)
CI/CD GitHub Actions (type check, build)
Deployment Vercel, serverless, Node 22.x runtime

Client-side product analytics via PostHog track voice selection, persona choice, conversation depth, and follow-up pill clicks. No PII is collected; only behavioral metadata.

No database. All content is version-controlled TypeScript. Rate limiting and session state are the only stateful components.

Local Development

git clone https://github.com/hutfut/josh-bot.git
cd josh-bot
npm install
npm run dev        # offline mode: skips LLM and rate limiting
npm run dev:live   # live mode: uses real LLM and rate limiter (requires API key)

Environment Variables

Variable Required Description
ANTHROPIC_API_KEY Yes (live) Enables chat functionality
UPSTASH_REDIS_REST_URL No Production rate limiting
UPSTASH_REDIS_REST_TOKEN No Production rate limiting
PUBLIC_POSTHOG_KEY No PostHog project API key (enables client-side analytics)
POSTHOG_PERSONAL_API_KEY No PostHog personal API key with Query Read permission (powers /analytics page)
POSTHOG_PROJECT_ID No PostHog project ID (powers /analytics page)
SKIP_EXTERNAL No Set to true to bypass LLM and rate limiting (set via .env.offline, loaded by npm run dev)

Rate limiting falls back to an in-memory implementation when Upstash credentials are not provided. Session storage is in-memory in all environments.

About

a portfolio site parodying ai chatbot startups

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors