
Most chatbots need a server, an account and an API key that costs money for every question. H3samBot, which I built, needs a browser. It answers questions about photography and IT support from a knowledge base stored as one JSON file, and the matching happens in JavaScript on the page itself. Nothing you type is sent anywhere.
- Open
index.htmland it runs. No build step, nonpm install, no backend. - No API key and no cost, because there’s no AI model call — it’s retrieval, not generation.
- A knowledge base that refreshes itself every six hours from photography and tech news feeds, using GitHub Actions.
- Private by design: your questions never leave the browser.
Try it in thirty seconds
git clone https://github.com/amirh3sam/H3samBot.git
cd H3samBot
# then double-click index.html
Ask it something like best shutter for sports or fix high CPU on Windows. A Status line under the chat tells you if anything is wrong, and the same message goes to the browser console.
Because it’s a static site, any file host can serve it. On GitHub Pages: push the files, open Settings → Pages, choose Deploy from a branch, pick main and the root folder, and it goes live at https://<your-username>.github.io/H3samBot/.
How it finds an answer: TF-IDF and cosine similarity
There’s no language model involved. The bot uses TF-IDF with cosine similarity, a classic information-retrieval method — and understanding it explains both its strengths and its limits.
- Every entry becomes a bag of words. Its question words and answer are lower-cased, stripped of punctuation and split into terms.
- Rare words count for more. That’s the IDF part: a word like “the” appears everywhere and tells you nothing, while “aperture” narrows things down sharply.
- Your question is scored the same way.
- The closest entry wins, measured by the angle between the two word vectors.
Here’s the core of it from bot.js:
function tokenize(t){
return (t||"").toLowerCase().replace(/[^a-z0-9s]/g,' ').split(/s+/).filter(Boolean);
}
// inside tfidfVectors(): a word found in few entries gets a high weight
for (const [w, df] of vocab) idf.set(w, Math.log(1 + N/df));
function cosine(a, b){
let dot = 0, na = 0, nb = 0;
for (const [w, va] of a){ na += va*va; const vb = b.get(w); if (vb) dot += va*vb; }
for (const [, vb] of b){ nb += vb*vb; }
return dot / (Math.sqrt(na) * Math.sqrt(nb) || 1);
}
The practical consequence: it’s very good when your question shares words with an entry, and it has nothing to say when it doesn’t. It can’t reason — but it also can’t invent an answer that isn’t in its knowledge base.
Teaching it something new
Everything the bot knows lives in data/knowledge.json. Add an object to the array and reload the page:
{
"title": "Shutter speed for sports",
"q": "sports shutter speed freeze motion fast action blurry football",
"a": "Start at <b>1/1000s</b> and raise it if motion still blurs. Open the aperture and raise ISO to keep the exposure."
}
| Field | What it’s for |
|---|---|
title |
A short heading shown with the answer |
q |
The words someone might use when asking — this is what gets matched, so write generously |
a |
The answer; HTML such as <b>, <br> and links is allowed |
The q field decides whether an entry is ever found. Put the synonyms in: for an entry about a blurry photo, include blurry, soft, shake, motion and sharp. Entries are missed far more often because of missing words than wrong answers.
Automatic updates with GitHub Actions
A workflow, .github/workflows/update-kb.yml, rebuilds the knowledge base every six hours from sixteen RSS feeds — photography sites such as PetaPixel, DPReview and Fstoppers, and tech sites such as The Verge, Tom’s Hardware and How-To Geek — then commits the new file back to the repository (shortened):
on:
schedule:
- cron: "0 */6 * * *" # every 6 hours
workflow_dispatch: # allow manual runs from the Actions tab
permissions:
contents: write # lets the job push the updated file
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
- run: npm ci || npm i
- run: npm run update:kb
- name: Commit and Push
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add data/knowledge.json
git commit -m "Auto-update knowledge.json" || echo "No changes"
git push
Two details make it work: permissions: contents: write lets the job push, and || echo "No changes" stops the run failing on the days when nothing new was found. You can run the same refresh locally with npm install and npm run update:kb.
Limits worth knowing
- Vocabulary, not understanding. If your words and the entry’s words don’t overlap, it won’t find the answer, even when it’s there.
- Answers are trusted HTML. They’re inserted as HTML so they can be formatted, so keep the knowledge base under your own control and don’t paste in entries from sources you don’t trust.
- English only, as written. The tokenizer keeps only
a–zand0–9, so accented letters and other alphabets are dropped.
Frequently asked questions
Can I connect it to a real AI model later?
Yes, and the design is ready for it. Keep the retrieval exactly as it is, then send the best-matching entries to a model as context along with the question. That’s retrieval-augmented generation (RAG), and the retrieval half is already done.
Does it work offline?
Once the page and knowledge.json have loaded, yes — nothing else is fetched while you chat.
Can I use my own topics?
Replace data/knowledge.json with your own entries in the same shape. Nothing in the matching code is specific to photography or IT.
I also built a chatbot step by step in a three-part series on TikTok. The code for this one: H3samBot on GitHub. Another small, no-build project of mine is the inventory app with Node.js and SQLite.
