Quick replies for Cliq. A prototype to experiment with a rule-based approach (limited, action suggestions mostly), no models were harmed in the making of this.
User
History0
No history yet.
Feedback
Optional — leave blank to stay anonymous.
Sign in
Admin access · 4-digit PIN
Rules
—
Loading…
—
Loading…
Test runner
Upload a JSON test suite (or paste it below) and it runs every message through the live matcher, right here — no separate file. Each case's expected actions are compared to what the engine produces.
Accepted shapes: a plain array of messages, or cases with expectations. A case is {"message":"...", "expect":{...}} where expect maps actions to "yes"/"no"/"maybe". Action names understood: call, meeting/schedule, reminder, chat/openChat, slots/availability. Signs also accept ✅/❌/⚠️, true/false, +/-. Cases with no expectation are run as info (shows what fired).
Click to choose a .json file — or drag & drop it here
There's no model here. Every suggestion comes from a deterministic, auditable pipeline over plain keyword rules — nothing is sent off-device. The tradeoff is on purpose: it can't generalise like an LLM, but every match (and every non-match) traces back to a specific line in dataset.json, which is what you want when QA-ing hundreds of phrasings by hand. The whole thing rests on three ideas.
1 · Normalise before matching"HEYYYY!!", "heyyy" and "Hey" should all hit the same rule. Surface variation is stripped away first, so rules only describe intent — not every spelling of it.
2 · A keyword is a lead, not a verdict"We had a meeting" contains "meeting" but isn't scheduling one. "Call it a day" contains "call" without meaning a phone call. So matching is two phases: detect, then validate — a keyword hit can still be thrown out.
3 · Reject before generatingSuggesting the wrong thing is worse than suggesting nothing. Once a rule wins, it still has to survive a false-positive check before any reply is built.
Watch a message flow through
Pick an example or type your own. The funnel below runs the real matcher, stage by stage — the same code the app uses.
The pipeline, step by step
Normalise. Lowercase, strip punctuation, collapse elongated letters (byeee → bye), drop stopwords, lightly stem the rest (ing/ies/ed/s/ly), with a carve-out so boxes/catches aren't mangled.
Structural cases, checked first. A few signals skip keyword scoring entirely: a fenced ```code``` block, a URL, a short bare affirmation (sure, done...) matched as the whole message, a reminder lead ("remember to...", "make sure to..."), and — only as fallbacks if nothing else matched — a whereabouts question, an availability ask ("are you free?"), or a bare time like "4 pm?". @mentions are detected separately and, when the surrounding words are actionable, attach an "Open X's chat" action.
Score every rule (weighted). Each rule offers phrases (its trigger plus keywords); a phrase matches only if every one of its significant tokens appears — order doesn't matter. A rule's base weight is its best phrase's token count squared, so a specific phrase dominates a lone keyword (hop on a call = 3² = 9 vs call = 1). Optional boost regexes add weight (for strong leads like "remind me" that collapse to one token); optional penalty regexes subtract it. The net-highest rule with a positive score wins; ties keep dataset order.
Gate lone ambiguous words. If an action rule matched only through a single ambiguous token (call, meet, reminder, free, ping...), it fires only when the message also carries an intent signal — a question ("can we call?"), a proposal ("let's meet"), or an imperative lead ("remind me tomorrow", "book a meeting"). Multi-word phrases ("hop on a call") are unambiguous and skip the gate. This is what stops "good call", "meeting notes", "friendly reminder" and "call panniten" from launching an action.
Validate the winner. Walking top-down, a candidate is dropped for any of: its exclude idiom list (a hard veto); recap detection (its own words in past tense, or "had a meeting"/"went to", unless a live signal like ?/"you"/"let's" overrides); or a global context filter — an action rule is suppressed when the message negates the action ("don't call"), complains about a UI feature ("the call button is broken"), or quotes/reports speech ("he said 'call me'"). The first survivor wins; if none, the fallbacks run.
Extract entities. Day (tomorrow...) and time (3 PM, half past 3, noon...) are pulled out and combined into {when} for templated replies.
Build the chips. Always exactly 3: the rule's action first (if any), then one reply per stance category (positive/negative/ask — never two from one), then a mood-matched filler tops up any gap. A reply keeps its baked-in emoji only if the incoming message used one.
The recurring gotcha: keyword collapse
Before adding any keyword phrase, run it through stopword removal and stemming in your head. "whats up" reduces to just ["what"] (up is a stopword) — as a keyword that matches any message with the word "what". Same trap: "works for you" → ["work"], "on leave" → ["leave"], "my bad" → ["bad"]. If a phrase collapses to one common word, find a safer multi-token phrasing, or add a targeted exclude regex for the bad pattern rather than dropping the coverage entirely.
Guarding against false positives
The word "call" shows up in "call me an idiot", "call it a day" and "let's hop on a call" — only the last one wants a 📞 action. A keyword alone can't tell them apart, so every action rule carries a negative vocabulary alongside its positive one.
Score, don't boolean-matchMatching isn't "contains a keyword" — every rule earns a weight. The base is its best phrase's token count squared (hop on a call = 9, call = 1), so a specific phrase dominates a lone keyword. The net-highest rule wins.
Ambiguous words need an intent signalPhrases beat keywords, but the shortest real asks ("can we call?", "remind me tomorrow") collapse to a single token too. So a lone ambiguous word (call, meet, reminder, free...) only launches an action alongside a question, proposal, or imperative lead. Declaratives that merely contain the word ("good call", "meeting fatigue", "friendly reminder", "call panniten") stay quiet.
Two kinds of negative: veto vs weightA hard exclude regex (a true idiom like "call it a day") drops the rule outright. A soft penalty only subtracts weight — so a strong positive can outlast it: "hop on a call" (9) survives a −3 penalty, but a bare "call" (1) doesn't. A boost does the reverse for high-signal leads.
An action needs intent, not a noun"meeting notes", "free trial", "the reminder email" all name a thing rather than request one, so no action is offered. "remind me to schedule a meeting" is a reminder, not a meeting — the payload just mentions one. For @mentions, the surrounding verb decides: "ping @A" opens a chat, "thanks @A" doesn't.
Punctuation can flip the intentA trailing ? turns a statement into a question, so the same word needs a different reply. "Done" is an acknowledgement → React ✅; "Done?" is asking whether it's finished → "Yes, all done!" / "Not yet." Same for "Ok" vs "Ok?", and bare confirmations like "Right?" / "Agreed?" / "Makes sense?".
Read the conversational act, not just the wordThe same word plays different roles. A shared set of context filters suppresses an action when the message is negating it ("don't call", "no need to schedule"), using it as a technical term ("API call failed", "call stack"), complaining about the UI ("the reminder feature is broken"), or quoting / reporting someone ("she asked 'can we meet?'"). These run for every action rule at once, so precision improves without a new per-rule exception each time.
📞 Call
Fires · hop on a call · free to talk · call me back
Vetoed · call me an idiot · good call · API call failed · "call me later"
📅 Schedule meeting
Fires · can we meet · schedule a meeting · send a meeting invite
Vetoed · meeting notes · the meeting went well · meet the deadline
⏰ Set reminder
Fires · remind me · don't forget · nudge me later
Vetoed · that reminds me of · the reminder email · remember when
👤 Open user chat
Fires · talk to @A · ping @A · @A can you review?
Vetoed · thanks @A · cc @A · great job @A
📆 Share free slots
Fires · when are you free · free this week · share your availability
One trap produces most false positives: a multi-word phrase quietly collapses to a single common token after stopword removal (on leave→[leave], works for you→[work]), so a lone word fires a rule it shouldn't. These guards all say the same thing in different places — a bare token only counts as intent when it's framed like intent. Each is additive and reversible; none touches genuine recall.
1 · An action word needs an intent signal
AMBIG_ACTION = /^(call|meet|remind|ping|free|sync|
book|schedul|catch|work|available|
discuss|talk|review|…)$/;
// a rule that matched on ONE token:
if (best === 1 && rule.action) {
if (AMBIG_ACTION.test(tok) && !hasSignal)
return; // don't score it
}
hasSignal = a ?, a "can/will we…", a "let's", a "please", or an imperative lead ("remind me", "book a"). A multi-word phrase ("hop on a call") never reaches this — it already scored above 1.
Fires can we call? · let's meet · remind me tomorrow · Silent good call · meeting notes · friendly reminder
"lunch"/"coffee"/"break" collapse to one token and drop into plain statements. They read as an invite only with a ? or an invite verb — unless the whole message is basically just the noun (≤ 2 tokens).
Fires Lunch? · grab coffee? · lunch time · Silent lunch break was long · after coffee we synced
The ^…$ anchor on the first branch is the whole trick: a break nudge fires when "break" is the message, never when it's buried in a sentence.
Fires Break? · snacks break? · break now? · Silent break the build · lunch break was long
4 · A commitment has to be first-person
THIRDPARTY_WILL = /\b([a-z']+)\s+will\s+
(?:take care|handle|work on|…)\b/i;
// "X will take care" — is X the sender?
var m = raw.match(THIRDPARTY_WILL);
if (m && !SENDER_SUBJ.test(m[1])
&& !/\bi'?ll|we'?ll|i will|we will\b/i.test(raw))
return null;
"will take care" is a commitment only when you say it. A third-party subject (he / the team / a name) makes it a description — the ack reply ("Thanks!", "Any rough ETA?") would be the wrong POV.
Fires Will update · I'll take care · we'll check · Silent the team will take care · Guru will handle it
5 · "Review" has to be framed as a request
REVIEW_INTENT =
/\breview\s+(?:this|the|my|pr|code|deck|…)\b
| \b(can|could|please|…)\b[^.?!]*\breview\b
| \b(for|to|pending) review\b
| \b(have|take) a look\b/i;
if (tok === "review" && !REVIEW_INTENT.test(raw))
return;
Bare "review" is an ask only when it's pointed at something ("review this PR", "for review") or led by a request verb. As a plain noun it's a status statement. The thoughts on / feedback paths are separate tokens, so they're untouched.
Fires review this deck · can you review? · Thoughts on this? · Silent the review is Friday · we did the review
Point of view
Some rules key on who is speaking, not just the word — the reply has to answer from the right side of the conversation.
Message
Read as
Reply
I'm on leave today
announcement
acknowledge
Are you on leave? / Leave?
question about you
answer (yes / no)
What time is the standup?
asking the schedule
tell the time — not “Join”
How are you?
check-in question
answer (“Doing well, you?”)
I'm doing good
check-in statement
acknowledge
we won
sharing good news
celebrate them
let me know if free
your availability
share slots
let me know if the room is free
a thing, not you
no suggestion
Mentions
Both the readable form and the raw Cliq wire token are recognized. A chat opens only when a communication verb is aimed at the person.
@User A // readable
{@60040392927} @{60040392927} // raw Cliq ZUID token// name resolves from the message's mentions map, not the text
generateSuggestions(text, DS, lastPicked, mentionsMap) // { "60040392927":"Priya" }
The tricky inputs the guards exist for — click any to trace it in the funnel above. fires shows the matched rule; silent means no suggestion, by design.
What is fillerMood?
Every match returns exactly 3 chips. When a rule doesn't have enough of its own replies to fill all 3 slots, the gap is topped up with a generic filler — but one universal "Got it, thanks!" reads wrong everywhere. fillerMood just picks which pool of fillers to draw from, so the padding matches the rule's tone. Optional; defaults to neutral.
neutral
Reacting to being informed. e.g. "Noted, thanks." / "Sounds good."
warm
Friendlier acknowledgement. e.g. "Thanks for letting me know!"
Bad news / sympathy. e.g. "Here if you need anything."
celebratory
Good news / wins. e.g. "That's fantastic!" / "Wonderful news!"
closure
Wrapping up. e.g. "Take care!" / "Have a great one!"
Dataset format
{
"id": "rule_call",
"trigger": "free for a call",
"keywords": ["ring", "phone", "free to talk"],
"exclude": ["call it a day", "\\bon call\\b"],
"penalty": ["\\bremind me to\\b"],
"fillerMood": "neutral",
"action": { "type": "call", "label": "Call" },
"stances": {
"positive": { "variants": ["Sure, ring me {when} 👍"] },
"negative": { "variants": ["Can't talk right now, sorry."] },
"ask": { "variants": ["What's it about?"] }
}
}
Action types: call, meeting, reminder, open-chat, share_slots, react. Only id, trigger and stances are required; everything else is optional. exclude is a hard veto (regexes that drop the rule); penalty and boost are soft, weighted regexes that subtract or add score — each entry is a pattern string, or a [pattern, weight] pair to override the default. Slots {when}, {day} and {time} in a variant are filled from the extracted entities (or a safe default when the message had none).