DEV Community

Yash Dharme
Yash Dharme

Posted on

Reply Desk: a review-reply assistant for small shops, where customers' words never leave the counter

Hacktoberfest Open-Source AI Challenge Week 2: Side Hustle Submission 💸

This is a submission for the Hacktoberfest Open-Source AI Challenge: Week 2 — Side Hustle.

What I Built

Reply Desk is a review-reply assistant for small businesses: the chai stall, the salon, the neighbourhood clinic, the coaching centre, the home baker.

These businesses live and die on Google and Zomato reviews. Replying to every review helps, and nobody behind the counter has time to do it. Worse, the reviews that actually matter ("found a hair in my food", "the staff was rude") get buried under twenty "nice chai!" ones.

Reply Desk does three things:

  1. It puts urgent reviews first. Food-safety, health, money and staff-behaviour complaints are flagged "Today" and moved to the top, the moment you paste them.
  2. It reads the mood of each review: positive, mixed or negative.
  3. It drafts a reply in the owner's voice, signed with their name. Unhappy customers are pointed to the owner's own contact, like a WhatsApp number, instead of an argument in public.

You read it, tweak it, copy it, post it. If the mood is wrong, tap the right one and it redrafts.

All of this runs on an open-weight model, Gemma 3 1B, inside the browser tab. No account, no API key, no server, no upload.

Who would pay for it

A café owner who would never pay for "AI". They would pay ₹199–299 a month for "every review answered in two minutes, and you'll never miss a complaint again". The same goes for a small agency that manages listings for ten local shops.

The pricing only works because of the open model, which is a later section.

Demo

It's a static web app with no backend, so you run it on your own machine:

git clone https://github.com/Yash11778/reply-desk
cd reply-desk
npm install
npm run dev
Enter fullscreen mode Exit fullscreen mode

Open the local URL, click "Fill in an example", then "Draft replies". With WebGPU it's quick. Without it, it falls back to the CPU and takes roughly 10–20 seconds a reply, but it works. The first run downloads the model once (~0.8–1 GB), and after that it's cached.

This is real, unedited output from a test run on a laptop with no GPU at all (CPU only, inside the browser), for a made-up café called "Chai Point Corner" run by "Priya":

Review: Found a hair in my poha. Disgusting. Want my money back.
Flag: Today: Something found in the food · Mood: negative
Reply: I sincerely apologize for the unpleasant experience you had with our poha. I'm truly sorry that your experience was so disappointing. Please contact me directly at 98765 43210 so we can discuss this further and make things right. I value your feedback and want to ensure you're satisfied.
Priya

Review: Waited 25 minutes for two sandwiches and the guy at the counter was rude when I asked.
Flag: Today: Staff behaviour · Mood: negative
Reply: Thank you for your feedback. I sincerely apologize for the delay and the experience you had with our staff. I want to assure you that I'll investigate this matter immediately. Please contact me directly at 98765 43210 so we can discuss this further.
Priya

Review: Nice ambience, friendly owner aunty. Sandwich was soggy though.
Mood: mixed
Reply: I'm sorry to hear you had a disappointing experience. Thank you for your feedback. I'll certainly take your comments about the sandwich and will investigate further to understand what went wrong.
Priya

They aren't Shakespeare, and they don't need to be. They're polite, specific, signed, and they never invent a discount or a fake phone number. That last part took work (see below). The third one is also a fair example of why the owner reads before posting: it skipped thanking her for the "friendly owner aunty" bit, and a human would add that back in five seconds.

Code

GitHub logo Yash11778 / reply-desk

Draft replies to customer reviews with an open-weigh model (Gemma) that runs in your browser. Nothing isuploaded.

Reply Desk

Paste your Google or Zomato reviews. An open-weight model running in your browser tab sorts them, flags the ones you must deal with today, and drafts a reply to each one in your own voice. Your customers' words never leave your computer.

Built for the Hacktoberfest Open-Source AI Challenge, Week 2: Side Hustle.

Who it's for

Small businesses that live on reviews: cafés, salons, clinics, coaching centres, home bakers. Replying to every review is good for them and nobody has time for it. The ones that matter most, like "found a hair in my food" or "staff was rude", get buried under "nice chai!".

How it works

  1. Set up once: business name, what it is, your name, and where unhappy customers should reach you. It's saved in your browser.
  2. Paste reviews, with a blank line between each.
  3. Urgent first: a plain keyword check flags food-safety, health, money…

MIT licensed. TypeScript and Vite, no framework, about 9 kB of app code plus the model runtime.

How It Works

paste reviews ──► split on blank lines
              ──► keyword check: urgent?  (instant, no model)
              ──► complaint words?  no ──► positive (no model)
                                    yes ─► Gemma: mixed or negative?
              ──► Gemma: draft a reply with the prompt for that mood
              ──► clean-up: strip chatter, placeholders, double sign-offs
Enter fullscreen mode Exit fullscreen mode

The model runs in a Web Worker through Transformers.js, using onnx-community/gemma-3-1b-it-ONNX. That's 4-bit weights on WebGPU, and 8-bit on CPU.

Lesson 1: give a 1B model one job, not three

My first prompt asked Gemma to classify the mood, flag urgency and write the reply, in a fixed format. It ignored the format and wrote things like **First line: POSITIVE**. It also wrote this, for a real-looking complaint:

Please contact Priya directly at [Priya's Phone Number or Email]

Two changes fixed it:

  • One prompt per job. Classification is its own short prompt, and the reply is another.
  • One prompt per mood. "If the review is bad, apologise" made it apologise to happy customers. So the code picks the mood first, and each mood gets its own one-line instruction:
const TASK: Record<Mood, (b: Business) => string> = {
  positive: () => 'Thank them for their review.',
  mixed: () => "Thank them for the good part, and say plainly that you'll look at the thing they didn't like.",
  negative: (b) => `Apologise once, without excuses, and ask them to contact you on ${b.contact} so you can make it right.`,
};
Enter fullscreen mode Exit fullscreen mode

The owner's real contact detail is passed in, and the prompt says "Do not invent phone numbers, emails, discounts or promises." A small clean-up pass then strips anything in [square brackets], "Okay, here's a reply:" preambles, markdown, and a doubled sign-off.

Lesson 2: don't use the model where a word list is better

Missing a food-safety complaint is the one mistake that really matters, so urgency is a plain keyword list, not the model: hair, cockroach, sick, food poisoning, stale, refund, rude, and so on. It's predictable and instant, and it lets the urgent cards appear at the top before the model has even loaded.

The same goes for clearly happy reviews. The CPU (8-bit) build liked to hedge and call "Best masala chai in the area!!" "mixed". Now a review with no complaint words at all (but, though, cold, slow, rude, not…) is positive without asking the model. Gemma only decides the hard case: mixed or negative.

Lesson 3: "runs in the browser" has fine print

Getting the no-GPU path working took three fixes, all found by running it rather than reading docs:

  1. The 4-bit build crashed on CPU: Could not find an implementation for GatherBlockQuantized. onnxruntime-web's WASM backend doesn't have that kernel, so the CPU path loads the 8-bit build instead.
  2. The 8-bit build then 404'd on model_int8.onnx_data. The model repo's config says its weights live in an external data file, which is true for the other builds but not for int8, which is one self-contained file. Passing use_external_data_format: { 'model_int8.onnx': 0, … } fixed it.
  3. It ran on one CPU core. WASM threads need SharedArrayBuffer, which needs the page to be cross-origin isolated. Two headers in vite.config.ts (Cross-Origin-Opener-Policy: same-origin, Cross-Origin-Embedder-Policy: credentialless) let it use every core. credentialless matters here, because it keeps the Hugging Face CDN download working without that server sending extra headers.

Why Open Matters Here

It's what makes the business work.

  • Privacy is the product. Reviews carry customers' names, phone numbers, and complaints about named staff. "Your customers' words never leave this computer" is a promise I can actually keep, because there's no server to leave to.
  • Zero cost per reply. A hosted model charges per token, forever. Here the shop's own laptop does the work, so a ₹199/month price has no API bill eating it. A hosted model would also force me to meter usage, and "unlimited replies" is a much easier sell to a shop owner.
  • No lock-in. No API key to leak, no provider changing prices or deprecating the model under me. If a better small open model comes out, swapping it is one line (MODEL in worker.ts).
  • It still works offline once the model is cached, for example in a shop with patchy internet.

What's Next

  • Hindi, Marathi and Hinglish replies, since many reviews here are written that way. That needs a bigger or fine-tuned model.
  • Per-business tone examples: paste three replies you've written, and it matches them.
  • A weekly summary: "4 people mentioned slow service this week".

Prize Categories

Best Use of Gemma. Gemma 3 1B is the only model in the app. It runs on-device through Transformers.js and does both the mood reading and every reply.


Built with AI assistance (Claude Code) for scaffolding, testing and debugging. All sample reviews are invented for the demo.

Top comments (0)