Skip to content

ghpranav/ghpranav.dev

Repository files navigation

ghpranav.dev terminal icon

ghpranav.dev

A terminal-styled personal site with a fully on-device AI chatbot.
No API keys. No server. No data leaves the browser.

React 19 TypeScript 6 Vite 8 Bun Cloudflare Workers


$ ask
→ initializing on-device LLM...
→ ready. backend: Gemini Nano (Chrome Prompt API, on-device)
  ask anything about my work, projects, or background.
  commands: /exit · /clear · /model · /help

pranav-chat> what are some of your projects?
ai › [the model answers from the system prompt — no network call]

How it works

The ask command auto-detects the best available on-device backend:

Tier Backend Requirements Model size Speed
1 Chrome Prompt API Chrome 138+, flag enabled, desktop ~4GB (once) Fast
2 WebLLM + WebGPU Any WebGPU browser, ≥4GB RAM 800MB–2GB 40–180 tok/s
3 Graceful refusal Anything else (iOS Safari, Firefox, low-end devices)

Tier 1 uses Google's LanguageModel JS API (currently behind chrome://flags/#prompt-api-for-gemini-nano).

Tier 2 activates automatically when Nano isn't available and WebGPU is. The model is chosen by available system RAM: Phi-3.5 mini on ≥8GB, Llama 3.2 1B on 4–8GB. First use always shows a download-consent step before fetching any weights. ask --webllm forces this path even when Nano would otherwise be used.

Stack

UI React 19 + TypeScript + Vite 8, React Compiler
Package manager Bun
On-device LLM (fast path) Chrome Prompt API — Gemini Nano
On-device LLM (fallback) @mlc-ai/web-llm — WebGPU
Hosting Cloudflare Workers (@cloudflare/vite-plugin + wrangler)
Linting ESLint flat config + typescript-eslint + eslint-plugin-react-hooks

Getting started

bun install
bun run dev       # http://localhost:5173
bun run build     # tsc -b && vite build → dist/
bun run preview   # build + wrangler dev (serves locally like prod)
bun run test
bun run lint
bun run deploy    # build + wrangler deploy → Cloudflare

Project layout

src/
├── App.tsx                 # mounts <Terminal />
├── main.tsx                # React root
├── index.css               # global reset + font preload + anti-flash bg
├── types.ts                # discriminated-union TerminalLine + CommandTable
├── commands/
│   ├── index.ts            # assembles CommandTable from individual commands
│   ├── ask.ts              # opens the on-device chat session
│   ├── theme.ts            # switch color theme
│   └── ...                 # help, whoami, projects, skills, contact, etc.
├── components/
│   ├── Terminal.tsx        # the shell — input, history, boot seq, chat mode
│   └── Line.tsx            # pure renderer for every TerminalLine type
├── content/
│   ├── site.ts             # static portfolio content (skills, projects, contacts)
│   └── system-prompt.ts    # BIO + SYSTEM_PROMPT — the LLM's source of truth
├── lib/
│   ├── llm.ts              # backend detection + cascading session factory
│   ├── completion.ts       # tab-completion logic
│   ├── crawlable.ts        # build-time SEO / JSON-LD generator (used by vite.config.ts)
│   └── levenshtein.ts      # "did you mean?" for unknown commands
└── themes/
    ├── index.ts            # assembles and exports all themes
    ├── contrast.ts         # WCAG contrast checker
    └── espresso|gruvbox|nord|paper|tokyo.ts  # one file per theme

Adapting this for yourself

  1. Replace src/content/site.ts with your own skills, projects, and contact links.
  2. Rewrite src/content/system-prompt.ts with your BIO and any rules for the model.
  3. Update the IDENTITY object at the top of site.tscrawlable.ts derives the page title, meta description, and JSON-LD Person from it automatically.
  4. Swap or add themes in src/themes/.

The LLM pipeline, command table, and terminal UI are persona-agnostic.

Cross-origin isolation

WebLLM's multi-threaded WASM runtime needs SharedArrayBuffer, which requires cross-origin isolation. The dev server sets the headers in vite.config.ts; for production they're in public/_headers (Cloudflare Pages / Netlify format).

Deploying elsewhere? Add these to every response:

Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp

Limitations

  • Gemini Nano is small — roughly autocomplete-class. Keep the BIO tight and factual; the system prompt's "I don't have that information" rule covers hallucinations.
  • WebGPU isn't universal. Mobile Safari and most Android browsers hit the graceful-refusal tier.
  • First WebLLM load is slow (800MB–2GB download). Service Worker caching makes subsequent visits instant.
  • Prompt injection. User input is wrapped in <user_question> tags with explicit anti-injection rules in the system prompt — defense-in-depth, not a guarantee. Blast radius is small (text generation only).
  • Chrome auto-updates Nano — the model can change without notice.

Extending the bot

Everything currently fits in the system prompt. If the BIO grows past ~2000 tokens, switch to RAG:

  1. Chunk the BIO into paragraphs.
  2. Embed locally with Transformers.js (Xenova/all-MiniLM-L6-v2).
  3. On each query, retrieve top-3 chunks by cosine similarity.
  4. Inject only those chunks into the prompt.

License

MIT — feel free to learn from it, fork it, or use it as a starting point for your own terminal site. But don't redeploy it under my name.

About

Terminal-style portfolio website that runs a local LLM in your browser. Type `ask` to chat with an on-device LLM (Gemini Nano / WebLLM via WebGPU) — no backend, no API keys, your messages stay local. Built with React + Vite.

Topics

Resources

License

Stars

0 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors