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 }));
}
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
}
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)));
}
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';
}
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
}
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
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
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.
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.
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:
- Toolkit hub: alexcoledev.github.io/cbt-toolkit
- Source code: github.com/alexcoledev/cbt-toolkit
- API (for the 3 detectors I turned into AI agents): LIVE on RapidAPI — CBT Thought Analyzer, Procrastination Pattern Detector, Attachment Style Detector
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)