Skip to content
TARUN S / PORTFOLIOTAP OR PRESS ANY KEY TO SKIP

Developer

000

AI-Powered Portfolio

TYPE
Full-stack portfolio + AI platform
STACK
20 COMPONENTS
STATUS
LIVE
FEATURED
YES

01 / OVERVIEW

A Next.js portfolio site paired with a standalone Express/NVIDIA-NIM backend that lets recruiters and visitors 'Chat with my AI' — a terminal-styled assistant that answers strictly from a hand-curated profile, with every section of the site itself driven by the same backend so content can be edited in Firestore without a redeploy.

This is my main project: a production-style personal portfolio that treats itself as a real product rather than a static resume page. The frontend is a Next.js 16 (App Router) + React 19 site styled with Tailwind 4, using a distinctive 'camera-bracket' visual language — dashed borders with corner-snap brackets that animate shut around cards (BracketFrame/BracketCard), a custom two-layer cursor reticle that only activates on fine pointers, a terminal-themed hero, and full light/dark theming driven by CSS variables toggled before first paint to avoid a flash. Every section (hero, about, experience, projects, tech stack, contact) is rendered from a single server-side fetch to a companion Express backend's `/api/site` endpoint, which reads from Firestore (one document per hero/about/contact, one document per experience/project/tech-category collection entry) with a 60-second cache and a bundled-JSON fallback so the site never blanks even if Firestore or the backend is unreachable — a three-tier fallback chain (Firestore → seed JSON → bundled frontend /data files) runs at both the backend and frontend layers. The centerpiece is an 'Ask My AI' terminal component that boots with a scripted sequence, performs a real health check against the backend, and then lets visitors chat with an LLM (NVIDIA NIM, OpenAI-compatible endpoint, Llama 3.1 70B by default) whose system prompt injects my entire structured profile as its only source of truth, with hard rules against hallucination, off-topic answers, and prompt injection from the visitor's own message (which is wrapped in `<visitor_message>` tags and treated as data, not instructions). The model can append a single hidden action token (`[[ACTION:RESUME]]` / `[[ACTION:CONTACT]]`) that the backend strips out and the frontend turns into inline action cards (a resume download/preview card or a 'jump to contact form' card), with a client-side regex fallback that infers the same intent from the visitor's own wording whenever the model's token emission is unreliable. A full contact form pipeline handles lead capture (Firestore) and dual transactional emails (a dark-mode-locked, escaped/sanitized HTML auto-reply to the visitor and a notification to me with Reply-To set to the visitor) via Nodemailer/Gmail SMTP, all fire-and-forget so a slow mail server never blocks a form submission. The whole system is built with defensive engineering throughout: every external dependency (Firestore, SMTP, the LLM) is optional at boot and degrades gracefully instead of crashing the server.

02/ STACK & ARCHITECTURE

FRONTEND

  • Next.js 16 (App Router, Server Components)
  • React 19
  • TypeScript
  • Tailwind CSS v4
  • Framer Motion
  • Lucide React
  • react-icons (Simple Icons brand marks)
  • Custom design system (BracketFrame / BracketCard corner-snap components)
  • Custom cursor reticle (useSyncExternalStore-based, SSR-safe)
  • next/font (Space Grotesk, JetBrains Mono, Inter)
  • next/image

BACKEND

  • Node.js
  • Express 4
  • NVIDIA NIM (OpenAI-compatible chat completions API, Llama 3.1 70B default)
  • Firebase Admin SDK / Firestore
  • Nodemailer (Gmail SMTP, App Password auth)
  • Zod (request validation)
  • express-rate-limit
  • dotenv
  • Custom hand-rolled REST client for the LLM

03 / WHAT WAS ACTUALLY HARD

  1. 01

    Grounded AI chat with hard anti-hallucination guardrails

    The 'Chat with my AI' terminal talks to a standalone Express backend that injects my entire structured profile (identity, skills, projects, experience, FAQ) into the LLM's system prompt as its ONLY source of truth, with explicit rules: answer only from the profile, refuse anything off-topic (coding help, general knowledge, current events), never invent contact details or metrics, and treat the visitor's message as untrusted data wrapped in `<visitor_message>` tags so it can't override the system prompt via injection. Reasoning-model `<think>` blocks are stripped server-side before any reply reaches the visitor.

  2. 02

    Action-token UI: resume and contact cards inline in chat

    The model can emit at most one hidden action token on the last line of a reply (`[[ACTION:RESUME]]` or `[[ACTION:CONTACT]]`), parsed and stripped server-side via regex so the frontend gets clean text plus a typed `action` field. When the model stays silent (measured to happen often on short conversations), a client-side intent-inference layer reads the visitor's own wording with deliberately narrow regexes (e.g. matching 'resume'/'cv' but not bare 'job') to render the same resume-download/contact-form cards anyway, so the feature degrades gracefully rather than silently failing.

  3. 03

    One-call, cache-backed, triple-fallback content pipeline

    Every section of the site is populated by a single server-side fetch to `/api/site`, which reads six Firestore sources in parallel (hero/about/contact as single docs, experience/projects/techStack as one-document-per-entry collections) with a 60-second in-memory cache. If Firestore is unreachable, the backend falls back to bundled seed JSON; if the backend itself is unreachable, the Next.js server falls back to its own bundled `/data` files — so editing a Firestore doc updates the live site with no redeploy, and the site never renders blank end-to-end.

  4. 04

    Multi-path visitor-facing contact pipeline with dark-mode-locked HTML email

    The contact form validates client-side (mirroring the backend's Zod schema) then POSTs to a rate-limited (3/10min/IP) endpoint that persists the lead to Firestore before responding, then fires two transactional emails asynchronously: a branded auto-reply to the visitor and an internal notification to me with Reply-To set to the visitor's address. Both emails are hand-built as nested HTML tables with fully inlined styles for Outlook/Gmail compatibility, HTML-escaped and CR/LF-stripped against header injection, and hardened against automatic dark-mode re-coloring via `color-scheme: only dark`, a `prefers-color-scheme` re-assertion block, and `[data-ogsc]`/`[data-ogsb]` attribute overrides for Outlook.com's dark-mode rewriting.

  5. 05

    Corner-snap 'bracket' design system used consistently across the site

    A single shared component pair (`BracketFrame`/`BracketCard`) draws four camera-viewfinder-style L-shaped corner marks around dashed-border panels, animated so each corner flies in from just outside its own corner before children stagger in — reused identically across the About statement block, the stats row, and the resume/project card stacks so the motif never drifts into a second, subtly different implementation.

  6. 06

    SSR-safe interactive layer: cursor reticle, theme toggle, resume overlay

    A custom two-part cursor (an unsprung center dot plus a spring-lagged bracket ring) replaces the native cursor only on confirmed fine-pointer, non-reduced-motion devices, and only after mount — so a JS failure can never leave the page without a visible pointer. Theme (light/dark) and the fullscreen resume overlay are both implemented as module-level external stores subscribed to via `useSyncExternalStore` rather than effect-driven state, avoiding hydration mismatches and extra renders while letting unrelated components (navbar, hero, AI chat) all trigger the same shared state without prop drilling or a Context provider (which would need its own client wrapper since `page.tsx` is a Server Component).

  7. 07

    Boot-sequence terminal with a real backend health check

    The AI chat panel opens with a scripted 'booting' animation, but the final connection line is never claimed as successful until an actual `/api/health` round-trip resolves — so the UI can never lie about backend availability. A live status dot and periodic re-pings keep the 'AI online/offline' indicator accurate throughout the session, and arrow-key input history recall is implemented client-side independent of the conversation history sent to the model.

  8. 08

    Defense-in-depth backend: optional-at-boot dependencies and strict validation

    Firestore and SMTP are both optional at process start — with no credentials configured, the server still boots, the AI chat falls back to seeded profile data, and the contact form still saves leads without emailing anyone, logged clearly to the console rather than crashing. All chat and contact payloads are validated through Zod schemas (message length caps, history capped at 8 turns, name/email/message bounds) before touching any downstream service, CORS is a strict origin allowlist rather than a wildcard, and both chat (15/min) and contact (3/10min) routes are independently rate-limited per IP.

  9. 09

    Hand-maintained seed pipeline as the single source of truth for the AI's knowledge

    The chatbot's entire 'brain' is a single hand-authored `profile.json`, deliberately decoupled from the website's own `/data` content files, pushed to Firestore via a seeder script that runs structural Zod validation and refuses to write if required fields (name, email, at least one real skill, at least one real project) are still empty — printing exactly what's missing rather than silently seeding a broken profile. `_`-prefixed keys (an internal TODO checklist) are stripped before the write so they can never leak into a model prompt.