What actually happens when a visitor clicks Ask Belle?
This course starts with the user action, not the architecture diagram. You open the chat, type a question, and the page first reads config, then decides whether to stay in frontend logic or call a backend for a grounded answer.
One click, five actors
Think of Ask Belle as a cue in a live performance. After the visitor presses the button, the message moves through the widget, backend, knowledge source, and model, then comes back to the page.
chatbot:
enabled: true
title: Ask Belle
subtitle: Research, teaching, publications, and projects
button_label: Ask Belle
intro: Ask about Belle's research agenda, publications, teaching experience, or design projects.
placeholder: Ask about research, publications, teaching, projects, or collaboration...
api_url: https://chatbot-backend-omega-lilac.vercel.app/api/chat
knowledge_base_url: /assets/data/belle-site-knowledge.json
suggested_prompts:
- What is Belle's research agenda?
- Which publications should I start with?
This block tells the site the assistant exists. If `enabled` were false, the widget would disappear entirely.
The next few lines define the visitor-facing surface. Title, subtitle, button label, intro, and placeholder all shape how the assistant feels before it says anything.
`api_url` is the bridge to the private part. This is the address of the backend that can safely call the model.
`knowledge_base_url` points to the controlled memory. Instead of searching the whole web, Ask Belle starts from a curated file of Belle's own site content.
The suggested prompts act like guardrails. They teach visitors what kinds of questions are on-topic.
Static site does not mean “no AI.” It means “split the job.”
The most important architecture decision fits in one sentence: the chat UI can live in the static site, but the model call cannot. Once that is clear, most of the later implementation choices become straightforward.
Quiz 1: What happens if you put `OPENAI_API_KEY` directly in the frontend script?
Pick the answer that best matches the Ask Belle architecture.
Before you code
- Write a one-sentence scope definition for the assistant before connecting a model.
- Decide which information must stay in the frontend and which responsibilities must move to the backend.
- Prepare a minimal config block with at least a button label, API URL, and knowledge base URL.
Do this in your repo
- Write down 4 topics your site assistant is allowed to answer.
- Write one fallback line for “that detail is not available on the published site.”
- Choose a fallback mode name such as site guide mode.
What usually goes wrong
- Defining the assistant goal as “answer anything about me.”
- Chasing model output quality before setting boundaries.
- Mixing secrets and backend responsibilities into a static frontend.
Who is on stage? Ask Belle only has a handful of real “actors.”
If you want AI to help you change this feature, you first need to say clearly which part is template markup, which part is browser code, which part is backend logic, and which part is just content.
The file cast
_config.ymlTells the site that this assistant exists, what it is called, and where it connects._includes/chatbot-widget.htmlInjects the launcher, panel, messages, and composer into the page.assets/js/chatbot.jsControls open, close, send, fallback, scroll, and citations.assets/css/main.scssDefines panel positioning, layering above the nav, and mobile layout behavior.chatbot-backend/api/chat.jsRestricts origins, loads the knowledge source, calls the model, and returns a grounded answer.Group chat between components
<section
class="site-chatbot__panel"
id="site-chatbot-panel"
data-chatbot-panel
hidden
role="dialog"
aria-modal="true"
aria-labelledby="site-chatbot-title"
inert
>
This is the chat panel itself. It starts hidden, because the site loads normally before the visitor opens the assistant.
`role="dialog"` tells assistive technology what kind of thing this is. It is not just a random `<div>` floating on the page.
`aria-modal="true"` says “when open, treat this like the active conversation surface.” That matters for accessibility and focus management.
`inert` means the panel should not behave as interactive content until it is truly opened. This prevents phantom focus and click bugs.
Good AI work starts with file boundaries, not prompt poetry.
If you tell AI “fix the chatbot,” it will likely make a mess. You want to be able to say “change the widget markup, but do not touch the backend.” That precision comes from having clear module boundaries.
Quiz 2: You want to change the “New chat” label. Which layer should you touch first?
Map the cast
- Write a one-line responsibility statement for each relevant file.
- Mark which file controls UI, which controls behavior, and which controls model boundaries.
- Make sure you can tell AI in one sentence which layer to change and which layer to leave alone.
Do this in your repo
- Pick 5 relevant files and write a plain-English summary for each.
- Pair two of them as “visible shell” and “behavior layer.”
- Write one precise AI instruction such as “change only the widget markup.”
What usually goes wrong
- Throwing every change into one giant “fix chatbot” instruction.
- Not knowing what config, markup, script, and backend each own.
- Debugging only with global search instead of by layer.
The time sink is not “sending a message.” It is making the browser behave like a mature component.
The hardest frontend part of Ask Belle is not the API request. It is , , focus management, click-outside behavior, and all the edge cases that show up in real viewport sizes.
if (!responsePayload || !responsePayload.message) {
responsePayload = await buildFallbackResponse(config, state, text, { setupMode: !config.apiUrl });
}
typingNode.remove();
appendMessage(messages, 'assistant', responsePayload.message, responsePayload.citations || [], { scroll: 'none' });
scrollTurnIntoView(messages, userMessageNode);
state.conversation.push({ role: 'assistant', content: responsePayload.message });
} catch (error) {
if (!error || !error.isExpectedChatbotFailure) {
console.error('Chatbot request failed.', error);
}
typingNode.remove();
const fallback = await buildFallbackResponse(config, state, text, { degradedMode: true });
appendMessage(messages, 'assistant', fallback.message, fallback.citations || [], { scroll: 'none' });
scrollTurnIntoView(messages, userMessageNode);
If the live response is missing, the browser does not panic. It switches to a fallback answer built from the local knowledge source.
The typing indicator gets removed before the answer is inserted. That keeps the conversation visually clean.
The message is appended with `scroll: 'none'` on purpose. The code does not blindly jump to the bottom.
`scrollTurnIntoView(messages, userMessageNode)` is the real UX trick. It repositions the message area so the visitor can still see the question they just asked.
Even inside the error path, the user still gets help. This is what makes Ask Belle resilient instead of brittle.
Where the browser earns its keep
Open cleanly
Launcher opens panel, locks focus, and does not block the rest of the site when closed.
Fail softly
If the backend is down, the assistant becomes a site guide instead of a dead button.
Scroll intentionally
The user should see “what I asked” and “how it starts answering,” not just the bottom edge of a bubble.
function scrollTurnIntoView(container, anchorNode) {
if (!container || !anchorNode) return;
const alignTurnStart = () => {
const containerStyle = window.getComputedStyle(container);
const paddingTop = Number.parseFloat(containerStyle.paddingTop) || 0;
const containerBox = container.getBoundingClientRect();
const anchorBox = anchorNode.getBoundingClientRect();
const anchorTop = anchorBox.top - containerBox.top + container.scrollTop;
const top = Math.max(anchorTop - paddingTop - 12, 0);
container.scrollTop = top;
};
This function asks one simple question: “Where should the message area move so the current turn starts in view?”
It measures both the container and the user’s message box. That avoids the classic mistake of calculating against the wrong frame of reference.
The final line sets the scroll position on purpose, not by accident. That tiny `- 12` creates breathing room so the question is not glued to the top edge.
Quiz 3: Why should Ask Belle not always scroll to the very bottom after answering?
Browser behavior checklist
- Support open, close, Esc, click-outside, and reset.
- Automatically fall back when live AI fails.
- Return to the user’s question area after the answer, not the bottom of the message list.
Do this in your repo
- Intentionally fail the API request once and confirm the UI does not die.
- Test whether the launcher, panel, and other page controls conflict in a narrow viewport.
- Record a 20-second screen capture and verify the post-answer scroll position feels right.
What usually goes wrong
- Testing only “does it return text?” instead of scroll and focus behavior.
- Turning fallback into an error banner instead of a continued helping path.
- Defaulting to the bottom of the thread and breaking the reading starting point.
The backend is not just a model relay
Ask Belle’s backend does three critical jobs: it only accepts the right origins, it speaks only from the site’s , and it gives the model explicit instructions not to invent details.
const configuredOrigin = String(process.env.ALLOWED_ORIGIN || '').trim();
const allowedOrigins = new Set([...DEV_ORIGINS]);
if (configuredOrigin) {
allowedOrigins.add(configuredOrigin);
}
return origin && allowedOrigins.has(origin) ? origin : '';
}
async function loadKnowledgeBase() {
if (cachedKnowledgeBase) return cachedKnowledgeBase;
The backend first decides who is allowed to talk to it. It starts from local dev origins, then adds the production site domain.
If the incoming origin is not on the list, it gets no friendly pass. The function returns an empty string instead of pretending the request is trusted.
Then the backend loads the knowledge file once and caches it. That keeps replies grounded without re-downloading the same content every single time.
function detectQueryIntent(text) {
const query = String(text);
if (isContactQuery(query)) return 'contact';
if (/publication|paper|article|journal|chapter|citation/i.test(query)) return 'publications';
if (/teach|course|mentor|class|instruction/i.test(query)) return 'teaching';
if (/project|build|tool|design|develop|app|prototype/i.test(query)) return 'projects';
if (/service|review|editorial|workshop|talk|speaker|serve/i.test(query)) return 'service';
if (/research|agenda|strand|focus|study|interests/i.test(query)) return 'research';
return '';
}
This is a cheap, practical intent guesser. It is not a full classifier; it just asks which bucket the question sounds like.
The important part is not elegance. It is control. Once the backend knows a question smells like research or contact, it can boost the matching pages from the knowledge file.
Notice how specific the keyword buckets are. Each bucket is tuned to the site itself, so the assistant stays narrow instead of pretending it knows everything.
'You are Ask Belle, a site assistant for Belle Li\'s academic portfolio.',
'Write like a concise, grounded site guide for a visitor, not like a generic assistant or marketing copy.',
'Answer using only the provided website context.',
'Respond in the same language as the user when practical.',
'If the answer is not supported by the provided context, say that the detail is not available on the published site.',
'Start with the direct answer, then add only the most useful supporting details.',
'Default to exactly 2 short sentences for overview questions unless the user explicitly asks for more detail.',
'Do not invent publications, projects, dates, or roles.',
The model is explicitly told what personality to avoid. Not generic, not marketing, not “helpful assistant voice.”
It is also told what truth source to obey. Only the provided website context counts.
The last lines are anti-hallucination guardrails. If the site does not say it, the answer should not invent it.
Grounded AI is not just a prompt. It is a pipeline.
Origin limits, knowledge matching, instruction shaping, and controlled error handling all matter. Remove any one of them, and a trustworthy site assistant turns into a random chat box.
Quiz 4: If a user asks for a detail that is not published on the site, what is the healthiest behavior?
Backend safety checklist
- Only allow your own site origin.
- Use a controlled knowledge file rather than open web search.
- Explicitly tell the model to answer only from the provided context.
Do this in your repo
- Add one backend instruction that says “if the site does not state it, do not invent it.”
- Create a question that is not in the knowledge base and test whether the answer stays honest.
- Check that at least 3 environment variables are truly read from the backend rather than hardcoded.
What usually goes wrong
- Turning a grounded assistant into an open-ended improv chatbot.
- Skipping origin checks so any page can hit your endpoint.
- Writing only a prompt, with no knowledge matching or controlled failure path.
If you want to build your own version, follow this order
This section is not Belle’s retrospective. It is your playbook. Assume you already have a static site and now want to add an AI assistant that answers only from published site content.
Your build order
Write the scope first
List the sections it is allowed to answer about, plus one fallback line for out-of-scope details.
Curate a tiny knowledge file
Start with title, url, summary, and keywords. You do not need a giant RAG system on day one.
Build the widget before live AI
Make open, close, send, reset, and fallback work before connecting the model.
Deploy backend separately
Keep the model key, origin checks, payload limits, and timeout behavior contained in the backend.
Run the polish checklist
Test the live path, fallback path, mobile layout, cache invalidation, and focus behavior.
.site-chatbot__panel {
position: fixed;
right: var(--chatbot-edge-gap);
bottom: var(--chatbot-edge-gap);
top: var(--chatbot-safe-top);
width: min(25rem, calc(100vw - (2 * var(--chatbot-edge-gap))));
display: grid;
grid-template-rows: auto auto minmax(0, 1fr) auto auto;
overflow: hidden;
border: 1px solid var(--chatbot-border);
border-radius: 1.35rem;
background:
radial-gradient(circle at top right, rgba(224, 90, 43, 0.08), transparent 30%),
linear-gradient(180deg, rgba(255, 250, 247, 0.98) 0%, rgba(255, 255, 255, 0.98) 100%);
This CSS is why the panel behaves like an overlay, not a broken card in the document flow. `position: fixed` pins it to the viewport.
`top: var(--chatbot-safe-top)` is the anti-masthead move. It reserves space so the panel does not slide under the site navigation.
The grid rows define the internal anatomy. Header, contacts, scrolling messages, composer, and footer each get their own lane.
This is a reminder that shipping AI features still means shipping layout engineering.
Starter config
title,subtitle,button_labelapi_urlpoints to a separate backendknowledge_base_urlpoints to a controlled JSON file- 3–4 suggested prompts hint at the allowed scope
Backend env vars
OPENAI_API_KEYOPENAI_MODELALLOWED_ORIGINKNOWLEDGE_BASE_URL
Smoke test
- click outside closes
- Esc closes
- fallback still answers
- scroll returns to the user question
Quiz 5: You want this feature on your own portfolio. What is the healthiest build order?
Ask Belle works because it is narrow on purpose.
This is not a chatbot that tries to answer everything. It is a bounded assistant built around Belle’s published site content, which is exactly why it has a chance to be both useful and trustworthy. When you build your own version, start narrow before you try to make it powerful.
Launch readiness
- Frontend config, backend env vars, and the knowledge file are all wired together.
- Both the live path and the fallback path have been tested.
- Mobile layout, narrow viewport behavior, and hard-refresh behavior have all been checked.
Do this in your repo
- Write a launch checklist with frontend, backend, billing, and UX columns.
- Ask one friend to test it and observe only how they ask their first question.
- Take the most confusing moment from that test and turn it into a clearer prompt or UI cue.
What usually goes wrong
- Shipping as soon as the feature “works” without testing cache, credits, CORS, and narrow viewports.
- Never observing whether real users ask out-of-scope questions.
- Defining “done” as demo-capable instead of production-acceptable behavior.