The idea
My portfolio site is a fairly standard one — it shows who I am, what I've built, and where I've worked. But it always felt passive. Someone lands on it, reads through the timeline, and leaves. No conversation, no depth beyond what fits on a page.
I wanted to change that. What if a visitor could just ask? "Has MJ worked with AI before?" "What was his biggest project?" "Does he have team leadership experience?" Instead of hunting through paragraphs, they'd get a direct, conversational answer, as if they were talking to me.
That's the idea: an "Ask MJ" chat widget, powered by AI, that answers in first person as me, grounded in real content about my career.
The architecture: RAG in plain English
I chose RAG. It sounds complex but the concept is simple:
Store your knowledge — take a document about yourself (in my case, a Markdown file with my full work history, skills, projects, and even performance review highlights and hobbies :) and break it into chunks. Embed each chunk as a vector(a mathematical representation of its meaning) and store it in a database.
At query time, retrieve — when someone asks a question, embed the question the same way, then find the chunks in the database whose vectors are closest in meaning. Those are the most relevant pieces of context.
Generate — pass the retrieved context plus the user's question to a language model, and let it compose a reply.
The result is a chat that only answers from your content, not from the general internet. It can't hallucinate facts about you because it's constrained to what you've written.
Technology choices
Supabase + pgvector — the memory
I already use Supabase for other projects and it was a natural fit. What makes it perfect here is pgvector, a Postgres extension that lets you store and query vector embeddings natively inside your database. No separate vector database service to set up, no extra infrastructure. One SQL migration, one similarity-search function, done.
create extension if not exists vector;
create table documents (
id bigserial primary key,
content text not null,
embedding vector(1536)
);
The similarity search function ranks chunks by cosine distance, the closer two vectors are, the more semantically related they are. It's elegant and fast.
OpenAI — the embedder
For turning text into vectors I used OpenAI's text-embedding-3-small model. It produces 1536-dimensional embeddings and is both affordable and high quality. Every chunk of my content gets embedded once (during the ingest step), and every user question gets embedded at query time before the similarity search.
Groq + Llama — the voice
For the actual chat generation I chose Groq running Llama 3.3 70B. Groq's inference hardware is genuinely fast. Responses start streaming back almost immediately, which is important for a good chat UX. Llama 3.3 70B is a capable open model that follows nuanced system prompts well.
The system prompt was where I spent real attention. It's not enough to say "you are MJ." The model needs explicit rules: always speak in first person, never say "he" or "MJ has", you ARE this person, not an AI assistant describing someone else. That specificity is what keeps the voice consistent.
Next.js App Router — the glue
The whole thing runs inside my existing Next.js site with App Router. The chat API is a single route handler (/api/chat) that handles the full pipeline, embed the query, retrieve from Supabase, build the prompt, stream Groq's response back to the browser as plain text. The client reads the stream token by token and appends to the UI in real time.
No separate backend. No microservices. One route file.
Building with Claude Code
Here's what made this especially interesting: I built it using Claude Code — Anthropic's AI coding agent that runs in the terminal alongside your editor.
The workflow was collaborative in a way that felt genuinely different from using a regular code editor. I'd describe what I wanted, Claude would propose an implementation plan, we'd discuss the approach, and then it would write the code. When something broke, it diagnosed and fixed the issue in context, without losing track of what we were building.
What we built, step by step
The full feature came together in these pieces:
Content file (
mj-content.md) — a structured Markdown document with my full career history, AI experience, product stories, languages, and volunteering work. The source of truth for everything the chat knows.Ingest script (
ingest.ts) — a one-time Node script that reads the content file, splits it on---section boundaries into natural chunks, embeds each chunk via OpenAI, clears the old rows, and upserts everything into Supabase. Re-runnable whenever the content updates. One command:yarn ingest.API route (
/api/chat) — the server-side pipeline: embed the query → similarity search in Supabase → build a first-person system prompt with the retrieved context → stream Groq Llama's response back as a plain textReadableStream.ChatWidget component — a
'use client'React component with a floating button, a slide-up panel, streaming message rendering, animated typing indicator, error handling, Escape-to-close, body scroll lock on mobile, and full-screen layout on small screens.
The result
The chat works. Someone can visit mjdashtaki.com, click "Ask about MJ from MJ's AI Twin", and ask something like "Has MJ led any teams?" or "What was his biggest technical challenge?" and get a warm, first-person answer grounded in real facts about my career.
It's not a gimmick. It makes the portfolio interactive in a way a static page can't be. Recruiters, collaborators, or curious engineers can get real signal fast, without reading everything.
The whole thing — from zero to deployed — was built in a single focused session, using a stack of four packages added to an existing Next.js project. That's what I find exciting about where tooling is right now: the distance from idea to working product has collapsed.
What's next
A few things I'd like to add:
- Suggested questions — surface a few starter prompts ("What's MJ's strongest skill?", "Is he available for freelance?") to help visitors who don't know where to begin.
But for a v1, I'm happy with it. It speaks as me, it knows my story, and it loads fast.
If you want to build something similar on your own portfolio, the stack is: Supabase pgvector + OpenAI embeddings + Groq Llama + Next.js API route. The ingest script and route handler are each under 60 lines.
Top comments (0)