How to Build an AI FAQ Chatbot with Groq, Llama 3.3 & Next.js (Typewriter Effect, Clickable Links)
The Project
I built an AI FAQ chatbot for my portfolio site — a floating chat bubble that answers questions about my skills, projects, experience, and availability. It uses Groq's Llama 3.3 70B under the hood, streams responses word-by-word with a typewriter effect, and renders markdown with clickable links — all inside a compact, mobile-friendly widget.
The goal wasn't just "add a chatbot." It was: make visitors feel like they're talking to me directly, without me having to be online.
Architecture
User types question
↓
Next.js API Route (/api/chat)
↓
Builds system prompt from portfolio data
↓
Groq API (Llama 3.3 70B, 0.3 temperature)
↓
Returns structured response
↓
Typewriter effect renders text word-by-word
↓
Clickable links, bold text, bullet points parsed
System Prompt Engineering
The key to making the chatbot useful was the system prompt. Instead of hardcoding answers, I built a dynamic context from the actual portfolio data:
export function buildPortfolioContext(): string {
return `You are a friendly AI assistant for Muiz Ud Din's portfolio website. ...
## About Muiz
- Name: ${profile.name}
- Title: ${profile.title}
- Location: ${profile.location}
- Email: ${profile.email}
## Skills
${skills.map(...)}
## Projects
${projects.map(...)}
## Testimonials
${testimonials.map(...)}
Guidelines:
- Keep initial answers VERY short — 1-3 sentences
- Always end with a follow-up question
- Never dump all info at once`
}
Every time someone asks a question, the API route builds this context from the live portfolio data — so if I update my skills or add a project, the chatbot automatically knows about it. No code changes needed.
The Typewriter Effect
Instead of showing the full response at once, the chatbot reveals text character by character:
function TypewriterMessage({ text, onDone, scrollRef }) {
const [displayed, setDisplayed] = useState("")
useEffect(() => {
let i = 0
const interval = setInterval(() => {
i++
setDisplayed(text.slice(0, i))
scrollRef.current?.scrollIntoView({ behavior: "smooth" })
if (i >= text.length) {
clearInterval(interval)
onDone()
}
}, 15) // ~15ms per character
return () => clearInterval(interval)
}, [text])
return <BotMessage text={displayed} />
}
This makes the bot feel alive — users see the response being "written" in real-time. Combined with auto-scroll, the chat feels like a conversation, not a search result.
Structured Response Rendering
Raw markdown looks bad in a chat bubble. I built a custom renderer that parses:
- Bold text →
<strong>tags - Bullet points (
-) → Styled list items with • markers - URLs (
https://...) → Clickable<a>tags opening in new tabs - Line breaks → Proper spacing between paragraphs
function renderInline(text: string) {
// Split by bold markers
const parts = text.split(/(\*\*.*?\*\*)/)
return parts.map((part, i) => {
if (part.startsWith("**") && part.endsWith("**")) {
return <strong key={i}>{part.slice(2, -2)}</strong>
}
// Detect and linkify URLs
const urlParts = part.split(/(https?:\/\/[^\s)]+)/g)
return urlParts.map((seg, j) =>
seg.match(urlRegex) ? (
<a key={j} href={seg} target="_blank" rel="noopener noreferrer">
{seg}
</a>
) : seg
)
})
}
Links like https://github.com/MUIZ-UDDIN become purple, underlined, and clickable — exactly what you'd want when someone asks "where can I see your work?"
UX Details That Matter
Outside-click to close — A transparent backdrop overlay closes the chat when you tap anywhere outside it. On mobile, this is essential.
Animated loading dots — While waiting for the API, three bouncing dots appear: "Thinking..."
Error resilience — If the Groq API times out, the bot shows a friendly fallback message and suggests emailing directly. No broken experiences.
Mobile-first — The chat panel uses w-[calc(100vw-3rem)] so it never overflows the viewport, with proper padding on all screen sizes.
Retry Logic
Groq's API can be slow or timeout occasionally. The API route automatically retries up to 2 times with exponential backoff:
async function fetchWithRetry(url: string, options: RequestInit, retries = 2) {
for (let i = 0; i <= retries; i++) {
try {
return await fetch(url, options)
} catch (err) {
if (i === retries) throw err
await new Promise(r => setTimeout(r, 1000 * (i + 1)))
}
}
}
This makes the chatbot reliable even when the API has transient failures.
What I Learned
-
Typewriter effect changes perceived speed — Even if the API takes 3 seconds, the character-by-character reveal makes it feel faster than a 1-second wait with no animation.
-
Dynamic system prompts beat hardcoded Q&A — Building the prompt from live data means the chatbot stays in sync with the portfolio automatically.
-
Custom markdown rendering > raw text — In a chat UI, structured formatting (bold, lists, links) makes responses scannable. Raw text dumps feel overwhelming.
-
Outside-click-to-close is non-negotiable on mobile — Without it, users have to target a small X button, which is frustrating on a phone.
-
Fallback messages build trust — When the API fails, a friendly "reach out via email" message is better than a generic error. Users appreciate being pointed to a real person.
Frequently Asked Questions
Quick answers to common questions about this project.
Use a `useEffect` with `setInterval` that increments a character counter. Each tick adds one more character to the displayed string. Auto-scroll the chat container using a ref and `scrollIntoView({ behavior: 'smooth' })` on each update.
Wrap the API call in a retry function with exponential backoff (2 retries, 1s/2s delays). If all retries fail, return a friendly fallback message like 'I'm having trouble connecting. Please try again or reach out via email.' Never show raw error text to users.
Related Articles
How to Build an Autonomous AI Research Agent with Groq, Tavily & WebSocket Streaming
Tutorial on building an AI research agent with multi-step reasoning — uses Groq LLaMA 3.1 for planning, Tavily for web search, WebSockets for real-time streaming, and Next.js for the dashboard.
How to Build an Enterprise SaaS CRM with FastAPI, React, Twilio & Claude
Complete walkthrough of building a production SaaS CRM with role-based access, real-time analytics, Twilio voice/SMS integration, Gmail contact sync, and WebSocket-powered dashboards deployed on a Hostinger VPS.