DEV Community

473185670
473185670

Posted on

How I Built a Catastrophic Thought Reframer in 15 Lines of Vanilla JavaScript — No AI, No API, No LLM

When someone is having a panic attack, they think they're dying. Their heart races, they feel chest tightness, and the thought hits: "I'm having a heart attack."

CBT (cognitive behavioral therapy) works by reframing that thought with a medical fact: panic raises your heart rate and blood pressure, but it cannot block blood flow to your heart. No one has ever died from a panic attack.

But a therapist isn't always available at 2 AM during an attack. So I built a tool that generates the correct CBT reframe instantly — and the core of it is 15 lines of vanilla JavaScript.

No AI. No API call. No LLM. No backend. The "intelligence" is therapist-authored reframes matched by keyword.

Here's how it works and why rule-based generation beats AI for this specific problem.


The problem: 4 catastrophic misinterpretations

Panic disorder is maintained by catastrophic misinterpretation of normal body sensations. The research (Clark, 1986) identifies a small, stable set of misinterpretations:

Catastrophic thought The medical reality
"I'm having a heart attack" Panic raises HR and BP but doesn't block blood flow. Heart attacks involve blocked flow.
"I'm going to faint" Fainting requires LOW blood pressure. Panic raises BP. Physiologically impossible.
"I'm losing my mind / losing control" Derealization is a known, harmless adrenaline effect. You never lose control of actions.
"I'm choking / can't breathe" Chest tightness is muscle tension + shallow breathing. Your brainstem controls breathing automatically.

Four categories. Stable vocabulary. High stakes (medical accuracy matters). This is the key insight: the domain is so constrained that you don't need a language model. You need a lookup table.


The 15-line reframer

Here's the entire function. It takes a catastrophic thought as input and returns a targeted, medically-accurate CBT reframe:

function reframeThought(thought) {
  var t = thought.toLowerCase();
  if (t.indexOf('heart') > -1 || t.indexOf('die') > -1 || t.indexOf('death') > -1) {
    return "A racing heart during panic is adrenaline — the fight-or-flight response. Panic attacks <strong>cannot cause heart attacks</strong>. Heart attacks involve blocked blood flow; panic raises heart rate but does not block flow. The sensation peaks within 10 minutes and passes completely. No one has ever died from a panic attack.";
  }
  if (t.indexOf('faint') > -1 || t.indexOf('pass out') > -1 || t.indexOf('collapse') > -1) {
    return "You <strong>cannot faint from a panic attack</strong>. Fainting happens when blood pressure drops too low. Panic does the opposite — it raises blood pressure. The dizziness is from shallow breathing, not low BP. Fainting is physiologically impossible during panic.";
  }
  if (t.indexOf('crazy') > -1 || t.indexOf('control') > -1 || t.indexOf('lose') > -1 || t.indexOf('unreal') > -1) {
    return "Feeling unreal or detached (derealization) is a <strong>known, harmless effect of adrenaline</strong>. It is distressing but not dangerous and always passes. You remain in control of your actions throughout. The feeling is the brain on adrenaline, not a mental breakdown.";
  }
  if (t.indexOf('choke') > -1 || t.indexOf('suffocat') > -1 || t.indexOf('breath') > -1 || t.indexOf('air') > -1) {
    return "Chest tightness and 'can't breathe' during panic come from <strong>muscle tension and shallow breathing</strong>, not a blocked airway. Your breathing is controlled automatically by the brainstem and will not stop. Slow your exhale — this is uncomfortable but not dangerous.";
  }
  return "These physical sensations are <strong>adrenaline — the fight-or-flight response</strong>. They are uncomfortable but not dangerous. The attack will peak within 10 minutes and pass within 20-30. Your body cannot stay in panic indefinitely; the parasympathetic nervous system will bring you back.";
}
Enter fullscreen mode Exit fullscreen mode

That's it. Five if branches, each matching 3-5 keywords, each returning a specific reframe. The fallback handles any thought that doesn't match a known category.

15 lines of logic. Zero dependencies. Runs in microseconds.


Why this beats GPT-4 for this specific problem

I considered using an LLM. Here's why I didn't:

1. Medical accuracy is non-negotiable

The reframe for "I'm going to faint" contains a specific medical fact: fainting requires low blood pressure, panic raises blood pressure, therefore fainting is physiologically impossible.

An LLM might generate: "While fainting can be scary, it's generally not dangerous and panic attacks usually pass on their own."

That's wrong and dangerous. It implies fainting CAN happen during panic (it can't), and it misses the specific mechanism (BP direction) that makes the reframe convincing. The rule-based version is therapist-authored and medically precise. Every time.

2. Zero latency — during an attack, "now" matters

When someone is panicking, a 2-second API call feels like an eternity. The rule-based reframer returns the answer in the same frame. There's no loading state, no network request, no "thinking..." spinner.

3. Zero privacy risk

Panic thoughts are deeply personal. "I thought I was dying" shouldn't be sent to OpenAI's servers. The rule-based version runs entirely client-side. Nothing leaves the browser.

4. Zero cost at scale

10,000 users having panic attacks at 2 AM = 10,000 API calls with an LLM = real money. The rule-based version = 10,000 indexOf() calls = free.

5. Explainability

If a user asks "why did it give me that reframe?", the answer is transparent: your thought contained the word "heart", which matched the cardiac branch, which returns the heart-attack reframe. With an LLM, the answer is "the model's weights produced this token sequence" — useless for debugging or improving.

6. Precision over recall

An LLM might try to reframe a thought that ISN'T a catastrophic misinterpretation ("I feel anxious about my exam") and generate something irrelevant or harmful. The rule-based version only fires when a catastrophic keyword is present. It does nothing for non-panic thoughts — which is the correct behavior for a panic tool.


When would I use an LLM instead?

The rule-based approach works because panic attacks have ~4 catastrophic misinterpretations with stable vocabulary. If I were building a general-purpose CBT chatbot that handles depression, anxiety, OCD, relationship issues, and open-ended therapeutic conversation — that's an open domain with infinite vocabulary. Rule-based can't cover it, and an LLM is the right tool.

The decision framework:

Is the domain constrained (stable, small set of input categories)?
  → YES: Are the outputs high-stakes (medical, legal, financial)?
    → YES: Rule-based with expert-authored outputs (this article)
    → NO: Either works; rule-based is simpler
  → NO: LLM (open domain, infinite vocabulary)
Enter fullscreen mode Exit fullscreen mode

Knowing which side of this line your problem falls on is a real engineering skill. Most developers default to "add an LLM" without checking if the domain is constrained enough for a lookup table.


The full pipeline (100 lines total)

The reframer is 15 lines, but it sits inside a complete panic diary tool. The full pipeline:

User logs an attack
  → enters catastrophic thought ("I'm having a heart attack")
  → rates belief in the thought (0-100)
  → clicks Save
    → reframeThought() generates the CBT reframe (15 lines)
    → entry saved to localStorage (3 lines)
    → renderHistory() shows all past attacks + reframes (20 lines)
    → renderInsights() computes: avg belief, most common symptom,
      peak anxiety trend, adaptive next-step guidance (30 lines)
Enter fullscreen mode Exit fullscreen mode

The insights function is where the long-term value lives. After 5+ attacks, it compares your belief-in-thought across entries:

  • Belief > 60: "Your belief in catastrophic thoughts is still high. Re-read the medical facts. Try interoceptive exposure."
  • Belief 30-60: "Your belief is dropping. This is CBT working. Keep logging."
  • Belief < 30: "Excellent progress. You've internalized that panic sensations are harmless adrenaline. Continue situation exposure."

This is the CBT mechanism encoded in JavaScript: repeated exposure to the reframe reduces belief in the catastrophic thought, which reduces fear of the sensations, which reduces panic frequency. The tool measures this directly via the belief trend.


The architecture decision in one sentence

When your problem has 4 categories, stable vocabulary, and medical accuracy requirements, a 15-line keyword matcher with expert-authored outputs is more accurate, faster, cheaper, safer, and more explainable than any LLM.

The full panic diary tool (with the 12-symptom checklist, belief rating, history, insights, JSON export, and localStorage persistence) is ~100 lines of vanilla JS in a single HTML file. No framework. No build step. No npm install. No backend. No signup. No API key.

It's live here: CBT for Panic Attacks — Free Interactive Panic Diary


What I'd do differently at scale

  1. Hybrid approach for edge cases: Keep the rule-based reframer for the 4 known categories (heart, fainting, control, choking). Add an LLM fallback ONLY for thoughts that don't match any branch — with a clear "this is AI-generated, not therapist-authored" label and a medical review queue.

  2. Multilingual keyword matching: The current version is English-only. At scale, you'd need keyword sets per language. Still rule-based — just more branches.

  3. A/B test the reframes: The authored reframes are based on CBT manuals, but they could be tested. "No one has ever died from a panic attack" vs "Panic attacks have never caused a death in recorded medical history" — which reduces belief more? This is a copywriting problem, not an ML problem.

  4. Log unmatched thoughts: Track thoughts that hit the fallback branch. If 30% of inputs don't match any category, the domain is less constrained than I assumed — time to add branches or reconsider the LLM.


The broader pattern

This is the same pattern I used for a cognitive distortion detector (11 distortions, ~110 keywords, keyword-pattern matching instead of ML). And the same pattern behind 23 free CBT tools I built for conditions from anxiety to OCD to burnout — all vanilla JS, all localStorage, all zero-dependency.

The meta-pattern: mental health CBT is a constrained domain. The DSM-5 lists a finite set of disorders. Each disorder has a finite set of cognitive distortions. Each distortion has a finite set of reframing techniques. The vocabulary is stable (patients use the same words: "heart attack", "faint", "losing my mind", "can't breathe"). This makes it ideal for rule-based tools that are faster, cheaper, safer, and more accurate than AI — if you know the domain well enough to write the rules.

The $7 CBT Thought Record Notion template I sell is the structured version of this same approach: a therapist-designed template, not an AI chatbot.


The takeaway: before you reach for an LLM, check if your problem has a small, stable set of input categories and high-stakes outputs. If it does, 15 lines of keyword matching with expert-authored responses might be the better engineering choice. It was for me.

Top comments (0)