DEV Community

CBT Tools
CBT Tools

Posted on

I Built 10 Mental Health Detectors in Vanilla JS — Here Is What Rule-Based Gets Right That ML Does Not

The Setup

I built 22 free mental health tools running entirely in the browser — zero backend, zero signup, zero dependencies. Ten of those tools are detectors: they take user input (thoughts, behaviors, predictions) and classify them into clinically-defined CBT categories.

Here is the full set:

# Detector Lines What it classifies
1 Cognitive Distortion Detector 200 10 distortions (all-or-nothing, overgeneralization, catastrophizing...)
2 Core Belief Detector 120 13 terminal beliefs (worthlessness, abandonment, vulnerability...)
3 Safety Behavior Detector 90 9 behavior categories (6 safety + 3 adaptive)
4 Habituation Pattern Detector 90 Per-exposure anxiety-drop trends (4 buckets)
5 Catastrophe Prediction Calibration 90 Prediction-vs-outcome disconfirmation rate
6 CBT Thought Analyzer API 150 10 distortions via HTTP endpoint
7 Procrastination Pattern Detector 180 8 procrastination patterns + CBT interventions
8 Attachment Style Detector 140 4 attachment types (Bartholomew model)
9 Price Discrimination Detector 198 6 big-data price discrimination patterns
10 Choking-Under-Pressure Detector 160 5 pressure-response patterns for athletes

Total: ~1,400 lines of JavaScript for 10 clinically-grounded detectors.


Five Distinct Algorithmic Patterns

The interesting part is not the detectors themselves — it is that they use five fundamentally different algorithmic patterns, and each pattern maps to a specific clinical structure.

Pattern 1: Flat Classification (Cognitive Distortion Detector)

const DISTORTIONS = [
  { keywords: ['always', 'never', 'every'], type: 'all-or-nothing' },
  { keywords: ['should', 'must', 'have to'], type: 'should-statements' },
  // ...8 more
];

function analyze(thought) {
  return DISTORTIONS
    .filter(d => d.keywords.some(k => thought.toLowerCase().includes(k)))
    .map(d => ({ type: d.type, confidence: matchCount / d.keywords.length }));
}
Enter fullscreen mode Exit fullscreen mode

When to use it: the output space is small, flat, and each category has reliable keyword signatures. 10 distortions from Beck's catalog — the entire known output space was defined by CBT research decades ago.

Pattern 2: Semantic Drill-Down (Core Belief Detector)

const SURFACE_TO_INTERMEDIATE = [
  { trigger: 'not good enough', intermediate: 'incompetence' },
  // ...
];
const INTERMEDIATE_TO_CORE = {
  'incompetence': 'worthlessness',
  'abandonment': 'unlovability',
  // 11 more
};

function drill(thought) {
  const intermediate = matchSurface(thought);
  return INTERMEDIATE_TO_CORE[intermediate]; // terminal belief
}
Enter fullscreen mode Exit fullscreen mode

When to use it: the classification is two-hop — surface thought → intermediate belief → terminal core belief. The downward arrow technique from CBT is literally a drill. You cannot flatten this into a single keyword lookup without losing the clinical chain that IS the intervention.

Pattern 3: Behavior Classification (Safety Behavior Detector)

const SAFETY_BEHAVIORS = [
  { keywords: ['check', 'verify', 'confirm'], type: 'checking' },
  { keywords: ['avoid', 'skip', 'put off'], type: 'avoidance' },
  // ...
];

function classifyBehavior(action) {
  // classifies ACTIONS, not thoughts
  return SAFETY_BEHAVIORS.find(b => b.keywords.some(k => action.includes(k)));
}
Enter fullscreen mode Exit fullscreen mode

When to use it: you are classifying behaviors (what the user did) rather than cognitions (what the user thought). The same keyword ('avoid') means different things in different contexts. Separating the detector by input type (thought vs behavior) prevents category confusion.

Pattern 4: Time-Series Trend Detection (Habituation Pattern Detector)

function detectTrend(exposures) {
  if (exposures.length < 4) return 'insufficient-data';
  const firstHalf = avg(exposures.slice(0, n/2).startingAnxiety);
  const secondHalf = avg(exposures.slice(n/2).startingAnxiety);
  if (secondHalf < firstHalf * 0.7) return 'decreasing'; // ERP working
  if (secondHalf > firstHalf * 1.1) return 'increasing'; // needs adjustment
  return 'stable';
}
Enter fullscreen mode Exit fullscreen mode

When to use it: the signal is longitudinal — you need to compare across time points, not classify a single input. Habituation (anxiety dropping over repeated exposures) is inherently a time-series phenomenon. A single exposure tells you nothing; the trend across 4+ exposures tells you everything.

Pattern 5: Prediction Calibration (Catastrophe Prediction Detector)

function calibrationRate(predictions) {
  const disconfirmed = predictions.filter(p => !p.outcomeHappened);
  return disconfirmed.length / predictions.length;
  // >= 0.6: trust + escalate experiments
  // >= 0.3: accumulate more data
  // else: run first experiment
}
Enter fullscreen mode Exit fullscreen mode

When to use it: the signal is cross-sectional counting — you are comparing predictions to outcomes across many entries, not classifying a single entry. The perfectionist predicts catastrophe; the calibration rate measures how often the catastrophe did NOT happen. This is the computational backbone of behavioral experiments.


Why Rule-Based Beats ML Here (The Unpopular Take)

I keep getting asked: "why not use an LLM for this?" Here is the honest answer, with the tradeoffs laid out:

1. The output space is small and already known

CBT research spent decades defining the 10 cognitive distortions, 13 core beliefs, 9 safety behaviors. These are closed sets. An ML model would learn to approximate a lookup table that I can write down in 20 lines. The model adds complexity without adding coverage.

2. Determinism is a feature, not a bug

The downward arrow technique should produce the same core belief for the same input every time. If a user drills "I am not good enough" on Monday and gets "worthlessness", they should get "worthlessness" on Tuesday too. Nondeterminism — which is inherent in LMs — is a bug in a clinical technique, not a feature.

3. Explainability IS the intervention

When the Core Belief Detector returns "worthlessness", it renders the full chain:

"I am not good enough" → (incompetence) → (worthlessness)

That chain is the therapeutic intervention. The user sees how their surface thought connects to their core belief. An ML model that returns "worthlessness" without the chain has given the label but not the insight. The chain is the point.

4. Privacy is non-negotiable

Mental health thoughts are the most sensitive data a person can produce. Sending them to an API is a privacy violation. Rule-based detectors run entirely client-side — the thoughts never leave the browser. This is not a nice-to-have; it is an ethical requirement.

5. Zero cost, zero latency

No API calls, no model loading, no rate limits. The detector runs in <1ms. For a tool that someone might use 50 times during a panic attack, latency matters.

When ML WOULD be better

I am not anti-ML. ML would be better if:

  • The output space were open (e.g., free-form reframing suggestions — my rule-based reframer is limited to 5 keyword branches)
  • The input were ambiguous (e.g., detecting sarcasm or indirect language — my keyword matching misses "I guess I will just deal with it" as dismissal)
  • The domain were not yet formalized (e.g., predicting which CBT technique will work for a specific user — no closed-form catalog exists)

The honest framing is: rule-based for classification of known categories, ML for generation in open-ended spaces. I use the right tool for each sub-problem.


The Architecture That Emerged

After building 10 detectors, the architecture is boringly consistent:

1. Define the output categories (from clinical literature)
2. Define keyword signatures for each category
3. Match input against signatures
4. Compute confidence (match count / total keywords)
5. Generate intervention (rule-based reframe or technique suggestion)
6. Log to localStorage for trend detection
Enter fullscreen mode Exit fullscreen mode

Steps 1-5 are <100 lines each. Step 6 is a 40-line localStorage wrapper shared across all tools. The entire toolkit is 1,400 lines of detection logic + 40 lines of storage + 200 lines of shared UI.

No framework. No backend. No build step. No dependencies. No API. No ML.


What I Would Do Differently

  1. Start with the meta-analysis. I built 10 detectors before writing this synthesis. I should have written the design patterns article after detector #3 — it would have saved me from re-discovering the same patterns in detectors #4-10.

  2. Formalize the patterns earlier. The five patterns (flat, drill, behavior, time-series, calibration) were implicit in my code but I never named them. Naming them would have made the code more consistent and the articles easier to write.

  3. Test against clinical cases earlier. I validated against my own thought records, not against published case studies. A test suite of "thought → expected distortion" pairs from CBT textbooks would have caught edge cases sooner.


The Tools

All 10 detectors are free, open-source, and run in your browser:

No signup. No tracking. No cost. Your thoughts stay in your browser.


If you found this useful, I write about building mental health tools with vanilla JS. Follow for more — I am currently turning the toolkit into AI agents on an app store and documenting the architecture.

Top comments (0)