My Portfolio Talks Back Now: How I Built It


An overview of the AI assistant on my portfolio, the decisions behind it, and what I’m planning to improve next.
Every portfolio reaches the same quiet moment. A visitor reads through the projects, understands the experience, and then has one last question the page cannot answer: How does this person actually think? Or more simply, Would they be a good fit for my team?
I wanted my portfolio to answer that question itself, so I built Alekwu, an AI assistant that can talk through my work, my stack, and the thinking behind it.
The AI assistant on my site can now hold a real conversation about my work, my stack, and, half jokingly, why a team should hire Jeffrey. It is live, it streams responses, and it is fully deployed. This article explains how I built it and the thinking behind the key decisions.
What I set out to build
My goal was never to “just add a chatbot.” A chatbot is a widget. I wanted something better: a product experience that represents me when I am not there and behaves like software I would be proud to put my name on.
That mindset shaped every decision. I was not trying to create a flashy demo. I was building something useful for recruiters, hiring managers, and engineers who want a clearer sense of how I think and work.
The architecture that makes it work
One important detail drives the whole system: the model does not keep memory between requests, it is stateless between requests.
I do not see that as a limitation. I see it as a design constraint. Every time a user sends a message, my application sends the model the relevant conversation history in a fresh context. The model generates a response, and then the request ends. My application is responsible for preserving state, not the model.
That separation matters. The model handles reasoning, while my code handles memory, identity, and user experience. Once you understand that division, the architecture becomes much easier to manage.
How it is wired
The browser does not communicate with the model provider directly. Instead, it sends requests to my own Next.js API route. That route securely stores the API key in a server-side environment variable and streams the response back to the browser.
This approach is worth the extra setup. Calling the provider directly from the client might be faster to prototype, but it exposes secrets and creates avoidable security risk. Using a server route adds only a small amount of code and gives me a much safer, production-ready setup.
What users actually experience
The first working version was functional, but it felt flat. A user would ask a question, wait in silence, and then receive the full response all at once. It worked, but it did not feel responsive.
So I switched to streaming.
Now the answer appears as soon as the model starts generating it. The total response time is similar, but the experience feels much faster because the user sees immediate progress. That first bit of visible output matters. It tells people the system is alive and working.
I also made two small but important improvements. First, responses are rendered as Markdown so lists, links, and code display properly. Second, users can stop generation whenever they want. That matters because control builds trust.
Managing cost and context
Because each turn includes conversation history, token usage can grow quickly in a long chat. That affects both cost and performance, so I use a sliding window to keep the most recent messages in context while dropping older ones from the request.
There is no perfect context window size. It depends on the product, the expected conversation length, and the budget. I chose a balance that keeps the assistant useful without letting cost or latency get out of hand.
Handling errors cleanly
I designed the system to assume things will fail occasionally. API timeouts happen. Rate limits happen. Safety filters sometimes block output. That is normal for systems like this, so those failures should never surface as raw technical errors.
Instead, the server catches those issues and returns a clear human-readable message. If a request fails, the user can retry without retyping everything. The goal is simple: no stack traces, no confusing failures, and no broken experience.
What I am building next
The current version works well, but there are still meaningful improvements to make.
Right now, refreshing the page clears the thread. I want to save conversations locally so users who come back on the same browser can pick up where they left off. I also want to ground the assistant more directly in my resume and project write-ups using retrieval, so its answers come from source material rather than inference.
I am also considering summarizing older turns instead of dropping them completely. That would preserve the important context while keeping the conversation efficient.
Final thoughts
Building this assistant confirmed something I already believed: the model is only one part of the product. The real value comes from everything around it — the memory, the security, the speed, the error handling, and the overall experience.
That is what makes the project worth showing. It is not just an AI feature. It is a thoughtful product built around a real use case.
If you are building in this space, or hiring for it, I would be happy to talk.
Was this helpful?
Enjoyed this article? Share it with your network