ADHD comes with a specific flavor of painful thoughts: "They hate me", "I'm lazy", "Everyone is further along than me". These aren't random — they map to known cognitive distortions, and one of them (rejection sensitive dysphoria, or RSD) is almost unique to ADHD. I built a detector that catches them in 70 lines of vanilla JS. No model. No API. No sentiment library.
The problem: ADHD thoughts have a signature
If you have ADHD, you know the pattern. A small thing happens — a colleague doesn't smile back, a task takes longer than expected — and within seconds your brain produces a thought like:
- "They hate me."
- "I'm so lazy, I can never do anything right."
- "Everyone is further along than me."
- "They're going to realize I'm a fraud."
These aren't generic negative thoughts. They have a signature. Three of them are classic cognitive distortions (mind-reading, labeling, unfair comparison), and one — the sudden, intense "they hate me" / "they're going to find out" spike — is the cognitive fingerprint of rejection sensitive dysphoria (RSD), a phenomenon that clinicians estimate affects 60–70% of people with ADHD and is rarely seen outside of it.
A generic "is this thought negative?" detector would miss the distinction. What you want is a detector that knows the ADHD-specific taxonomy: which distortion, which RSD trigger, which comparison trap. And you want it to run in the browser, on your phone, with zero latency and zero data leaving your device.
The detector: 7 keyword branches, one compound conditional
Here's the entire distortion detector, lifted verbatim from the free CBT for ADHD tool:
function dwIdentifyDistortion(thought) {
var t = thought.toLowerCase();
// 1. RSD triggers — mind-reading the worst
if (t.indexOf('they hate') > -1 || t.indexOf('hates me') > -1
|| t.indexOf('they think') > -1 || t.indexOf('everyone thinks') > -1
|| t.indexOf('they\'re going to realize') > -1 || t.indexOf('find out') > -1)
return 'mind-reading (assuming others\' thoughts)';
// 2. Catastrophizing
if (t.indexOf('ruined') > -1 || t.indexOf('destroyed') > -1
|| t.indexOf('over') > -1 || t.indexOf('disaster') > -1
|| t.indexOf('can\'t handle') > -1 || t.indexOf('too much') > -1)
return 'catastrophizing';
// 3. All-or-nothing
if (t.indexOf('always') > -1 || t.indexOf('never') > -1
|| t.indexOf('everyone') > -1 || t.indexOf('no one') > -1
|| t.indexOf('nobody') > -1 || t.indexOf('everything') > -1
|| t.indexOf('nothing') > -1)
return 'all-or-nothing / overgeneralization';
// 4. THE COMPOUND CONDITIONAL — identity attack needs a subject + predicate
if (t.indexOf('i am') > -1 && (t.indexOf('lazy') > -1
|| t.indexOf('broken') > -1 || t.indexOf('stupid') > -1
|| t.indexOf('failure') > -1 || t.indexOf('fraud') > -1
|| t.indexOf('fake') > -1 || t.indexOf('burden') > -1
|| t.indexOf('useless') > -1 || t.indexOf('worthless') > -1))
return 'labeling / global identity attack';
// 5. Should statements
if (t.indexOf('should') > -1 || t.indexOf('must') > -1
|| t.indexOf('ought') > -1 || t.indexOf('supposed to') > -1)
return 'should statement';
// 6. Personalization
if (t.indexOf('my fault') > -1 || t.indexOf('i caused') > -1
|| t.indexOf('because of me') > -1 || t.indexOf('i let') > -1)
return 'personalization';
// 7. ADHD comparison trap — neurodivergent peer comparison
if (t.indexOf('further along') > -1 || t.indexOf('behind') > -1
|| t.indexOf('should have') > -1)
return 'unfair comparison / backward-looking';
return 'unexamined emotional thought';
}
Seven if statements. indexOf on a lowercased string. That's the whole classifier. Let me walk through why each design choice matters.
Branch 1: the RSD signature
The first branch isn't generic mind-reading. It targets the specific phrases that show up in RSD episodes:
-
"they hate"/"hates me"— the instant rejection perception -
"they're going to realize"/"find out"— the impending-exposure panic
A generic mind-reading detector might catch "they think I'm wrong". But RSD has a narrower, more intense signature: the thought isn't "they disagree with me" — it's "they have discovered I am fundamentally unacceptable." The keywords "find out" and "going to realize" capture that exposure-panic quality that plain mind-reading misses.
This is the domain adaptation: same indexOf architecture as a generic distortion detector, but the keyword set is swapped to the ADHD/RSD lexicon.
Branch 4: the compound conditional (the genuinely different part)
This is the one line that makes this detector architecturally distinct from a flat keyword classifier:
if (t.indexOf('i am') > -1 && (t.indexOf('lazy') > -1 || t.indexOf('broken') > -1 || ...))
Two levels. The thought must contain "i am" AND one of the identity-attack predicates ("lazy", "broken", "fraud", "fake", "worthless", ...).
Why the &&? Because "lazy" alone is ambiguous. "The lazy approach worked" is not a distortion. "My dog is lazy" is not a distortion. But "I am lazy" — first person + identity predicate — is a global labeling attack, and it's the single most common ADHD shame thought.
The compound conditional encodes a linguistic constraint: identity attacks require a first-person subject. A flat indexOf('lazy') would over-match. The && gate prevents false positives without any NLP, any POS tagging, any dependency parse. It's a 1-token stand-in for "is this a self-referential statement."
None of my prior detectors use this pattern. The cognitive distortion detector uses flat single-keyword matching. The core belief detector uses a two-hop drill but not a compound gate. This subject && predicate structure is a small but real algorithmic upgrade: it trades one extra indexOf call for a meaningful precision gain on the highest-stakes category (identity attacks).
Branch 7: the ADHD comparison trap
if (t.indexOf('further along') > -1 || t.indexOf('behind') > -1 || t.indexOf('should have') > -1)
return 'unfair comparison / backward-looking';
"Everyone is further along than me." "I'm so behind." "I should have figured this out by now."
This is the neurodivergent comparison pattern — measuring yourself against neurotypical peers who don't share your executive function baseline. It's not in the standard CBT distortion list (it's a hybrid of unfair comparison + should statement + overgeneralization), but it's so characteristic of ADHD that it gets its own branch and its own label. The taxonomy is domain-swapped, not just keyword-swapped.
The insights layer: split-half trend detection
The detector runs on every entry. But the value is in the pattern over time. Here's the trend logic (also from the live tool):
var chrono = dwEntries.slice().sort(function(a,b){
return new Date(a.when) - new Date(b.when);
});
var trendStr = 'stable';
if (chrono.length >= 4) {
var half = Math.floor(chrono.length / 2);
var firstAvg = chrono.slice(0, half).reduce(function(s,e){return s+e.belief;},0) / half;
var secondAvg = chrono.slice(half).reduce(function(s,e){return s+e.belief;},0) / (chrono.length - half);
if (secondAvg < firstAvg - 5)
trendStr = 'decreasing (beliefs loosening — the pause is working!)';
else if (secondAvg > firstAvg + 5)
trendStr = 'increasing (review triggers — consider a therapist)';
}
Split-half comparison: sort chronologically, split at the midpoint, compare the average belief-in-thought of the first half vs the second half. A drop of more than 5 points = beliefs are loosening (the intervention is working). A rise of more than 5 = escalating (review triggers, consider professional support). Needs at least 4 entries to trigger (two per half).
Why split-half and not a moving average or linear regression? Because the user is logging flare-ups — episodic, not continuous. A moving average smooths over the very spikes you're trying to detect. Split-half asks a simpler, more robust question: "is the second half of your log better or worse than the first half?" It's the non-parametric version of "are you improving," and it works on 4 entries with zero statistics library.
Why rule-based, not ML?
For this domain, rule-based beats a fine-tuned classifier on every axis that matters:
| Axis | Rule-based (this) | Fine-tuned classifier |
|---|---|---|
| Precision on RSD | The keywords "find out" + "going to realize" are RSD signatures by construction |
Would need labeled RSD examples — RSD is underrecognized, labels are scarce |
| Latency | <1ms (7 indexOf calls) |
100–500ms (tokenize + inference) |
| Cost | $0 forever | Inference cost per call |
| Privacy | Thoughts never leave the browser | API call sends raw thoughts to a server |
| Explainability | The matched branch IS the explanation — "you said 'I am lazy', that's a labeling attack" | "The model assigned 0.87 to class 'labeling'" — opaque |
| Determinism | Same thought → same distortion, every time | Stochastic — same thought can return different labels |
The privacy point is not theoretical. These are the most vulnerable thoughts a person can have — "I'm a fraud", "they're going to find out", "I'm broken". Sending them to an API is a design failure. The detector runs client-side, in the same localStorage-backed tool that saves the thought record. Nothing leaves the device.
The precision point is the real differentiator. RSD is a long-tail phenomenon — it's clinically distinct but underrepresented in training data. A classifier trained on general "negative thought" datasets would lump RSD triggers into generic mind-reading and miss the exposure-panic quality. The rule-based detector encodes the domain expert's knowledge directly: "find out" and "going to realize" are RSD signatures because a clinician said so, not because they appeared 500 times in a labeled dataset. Domain adaptation via knowledge-base swap, not via fine-tuning. That's the rule-based equivalent of transfer learning — and it's 10,000× cheaper.
The compound conditional as a design pattern
The subject && predicate pattern generalizes beyond ADHD:
// Depression: negative triad needs self + world + future
if (t.indexOf('i am') > -1 && (t.indexOf('worthless') > -1 || ...)) // self
if (t.indexOf('world') > -1 && (t.indexOf('terrible') > -1 || ...)) // world
if (t.indexOf('future') > -1 && (t.indexOf('hopeless') > -1 || ...)) // future
// Social anxiety: self + scrutiny
if (t.indexOf('they') > -1 && (t.indexOf('judging') > -1 || t.indexOf('staring') > -1))
Any domain where the distortion requires a grammatical structure (not just a keyword) benefits from the compound gate. It's a cheap precision upgrade — one extra indexOf — that encodes a linguistic constraint without any NLP. The cost is one boolean &&. The benefit is eliminating false positives on the highest-stakes category.
The full pipeline
- User logs a flare-up: trigger, emotion, intensity 0–100, the thought, belief in the thought 0–100, what the emotion made them do.
-
dwIdentifyDistortion(thought)→ classifies the thought into one of 7 ADHD-specific distortions (or "unexamined"). -
dwReframeThought(thought, emotion)→ generates a domain-specific CBT reframe (e.g., "I am lazy" → "Difficulty starting is an executive function challenge, not a character flaw. What's one small step?"). -
dwRender()→ displays the entry + reframe, and aggregates insights: avg intensity, avg belief, most common emotion, most common response, and the split-half belief trend. -
localStorage→ saves everything client-side. JSON export for backup.
70 lines for the detector + reframe + insights. No dependencies. No backend. No signup. No data leaves the device.
Try it
The full interactive tool — emotion log, distortion detection, CBT reframes, trend insights, localStorage save — is live and free:
→ CBT for ADHD: Emotional Regulation, RSD & Shame (Free Tool)
It's part of a CBT toolkit hub of 26+ free interactive mental health tools, all vanilla JS, all client-side, all zero-dependency. There's also a standalone cognitive distortion checker if you just want to paste a thought and see which distortions apply.
The takeaway: ADHD thoughts aren't random negativity — they have a detectable signature, and one branch of that signature (RSD) is nearly pathognomonic. A 70-line keyword classifier with one compound conditional catches that signature with better precision, privacy, and cost than a fine-tuned model — because the domain knowledge is in the keyword set, not in a weight matrix. Sometimes the simplest solution is the one that respects the user's privacy and gets the right answer.
Top comments (0)