An ACT-based Cognitive Defusion Diary Application
Margin is a professional journaling tool designed to help users achieve "cognitive defusion." It uses AI to detect "cognitive fusion" sentences in real-time while users write, guiding them to create distance from negative thoughts in a gentle, non-intrusive way.
| Principle | Description |
|---|---|
| Silent Companionship | Never interrupt the user's writing flow; exist as a silent observer |
| Ambient Intervention | Interventions appear as sidebar cards and highlights, never modal dialogs |
| Atmosphere Over Precision | Visual design emphasizes the warm, safe feeling of a "handwritten diary" |
| Margin Metaphor | Provide "space" between thoughts and self |
| Category | Technology |
|---|---|
| Framework | Next.js 14 (App Router) |
| Language | TypeScript (strict mode) |
| Styling | Tailwind CSS + Custom Warm Palette |
| AI | Google Gemini 3 Flash Preview |
| Physics | Matter.js (2D paper ball simulation) |
| Font | Noto Serif SC (Chinese serif) |
| Persistence | localStorage |
- Node.js 18+
- NPM or Yarn
cd margin
npm installCreate a .env.local file and add your Gemini API Key:
GEMINI_API_KEY=your_api_key_herenpm run devVisit http://localhost:3000
margin/
├── app/
│ ├── api/
│ │ ├── detect/route.ts # Cognitive fusion detection API
│ │ ├── reframe/route.ts # Cognitive defusion reframing API
│ │ └── comfort/route.ts # Warm rationalization API
│ ├── globals.css # Global styles & animations
│ ├── icon.png # Favicon (paper ball)
│ ├── layout.tsx # Root layout
│ └── page.tsx # Main page (254 lines)
├── components/
│ ├── Editor.tsx # Dual-layer editor (highlight + anchor)
│ ├── AmbientCard.tsx # Thought observation card (170 lines)
│ ├── ReframeSlider.tsx # Paper crumpling slider (247 lines)
│ ├── PerspectiveCard.tsx # Perspective card (hover display)
│ ├── PhysicsPaperBalls.tsx # Physics paper ball engine (201 lines)
│ ├── CompanionZone.tsx # Right companion zone container
│ ├── JournalList.tsx # Journal list
│ ├── FallingPaperBall.tsx # Falling paper ball animation
│ ├── PaperBallCollection.tsx # Static paper ball display
│ ├── DeleteConfirmDialog.tsx # Delete confirmation dialog
│ └── SaveIndicator.tsx # Save status indicator
├── hooks/
│ ├── useSentenceManager.ts # Sentence parsing & AI detection (666 lines, CORE)
│ ├── useCardManager.ts # Card queue management (173 lines)
│ ├── useJournalManager.ts # Journal CRUD (190 lines)
│ ├── usePerspectiveCard.ts # Perspective card display (75 lines)
│ ├── usePaperBallManager.ts # Paper ball state management (284 lines)
│ ├── useCardTimer.ts # Card 30-second timer (75 lines)
│ └── useLocalStorage.ts # localStorage wrapper (16 lines)
├── lib/
│ └── apiUtils.ts # API utilities (timeout, sanitization)
├── utils/
│ ├── constants.ts # Centralized constants (71 lines)
│ ├── logger.ts # Logging utility (90 lines)
│ ├── rateLimit.ts # Rate limiting (145 lines)
│ └── audioManager.ts # Audio singleton (90 lines)
├── public/
│ ├── paper-flat.png # Flat paper image
│ ├── paper-crumpled.png # Half-crumpled paper
│ ├── paper-ball.png # Paper ball
│ └── paper-crush.mp3 # Crumpling sound effect
└── tailwind.config.ts # Warm color palette config
When users write in the editor, the system parses sentences in real-time. Upon detecting sentence-ending punctuation (。!?.!?), the Gemini API is called to determine if it's a "cognitive fusion" sentence.
Cognitive Fusion Types:
| Type | Example |
|---|---|
| Self-denial | "I'm a failure" |
| Absolutizing | "I'll never do it right" |
| Over-generalization | "Nobody likes me" |
| Catastrophizing | "It's all over" |
| Self-attack | "I'm so stupid" |
| "Should" thinking | "I shouldn't make mistakes" |
| Comparative denial | "I'm just worse than others" |
| Fatalistic | "I'm destined to fail" |
Non-triggering Cases:
- Normal emotions: "I feel tired today"
- Factual descriptions: "Today's interview didn't go well"
- Descriptions of others: "They don't understand me"
- Specific difficulties: "This problem is hard"
When a cognitive fusion sentence is detected:
- The sentence is highlighted in amber in the editor
- An "Ambient Card" appears in the right companion zone
- Card auto-fades after 30 seconds (pauses on hover)
- Maximum 2 cards visible simultaneously
- Opacity gradually decreases: 60% → 30% before timeout
Card Content:
"I noticed this thought..."
"{Original sentence}"
"There might be another way to hold this."
▸ See it differently
After clicking the guide text, an interactive slider appears:
| Progress | Visual State | Effect |
|---|---|---|
| 0-34% | Flat paper | Drag begins |
| 34-67% | Half-crumpled | Paper starts crinkling |
| 67% | Paper ball | 🔊 Sound effect plays |
| 95%+ | Complete | ✅ Defusion exercise complete |
If released before completion:
- Shake animation hint
- 5-second auto-recovery to zero
After completing the crumpling:
- Paper ball ejects from the card position
- Matter.js simulates parabolic trajectory
- Ball lands in the "collection zone" at the bottom
- Random ejection angle (-60° to 240°) and force (5-15)
- Historical paper balls are persisted (max 15)
Physics Properties:
{
restitution: 0.6, // Bounciness
friction: 0.4, // Surface friction
frictionAir: 0.005, // Air resistance
density: 0.001 // Mass density
}After completing defusion:
- Original sentence changes from amber highlight to jade dashed underline
- Hovering the anchor shows a "Perspective Card"
- Card displays the AI-reframed thought
- Lazy-loads "warm rationalization" text (context-based)
Detects if a sentence is a cognitive fusion sentence.
| Aspect | Detail |
|---|---|
| Timeout | 8 seconds |
| Rate Limit | 20 requests/minute |
| Input Sanitization | ✅ Prompt injection protection |
Request:
{ "sentence": "I'll never do anything right" }Response:
{ "isFusion": true }Reframes sentences using cognitive defusion techniques.
Reframing Techniques:
- Observer perspective: "I notice I'm having a thought that..."
- Personifying the brain: "My brain is telling me..."
- Thanking the brain: "Thank you, brain, for reminding me..."
- Thought as visitor: "A thought came to visit me..."
- Story narration: "My brain is telling that old story again..."
- Present-moment anchoring: "In this moment, a thought has appeared..."
Request:
{ "sentence": "I'm useless" }Response:
{ "reframed": "I notice I'm having a thought that I'm useless" }Generates warm rationalization text based on journal context.
Request:
{
"journalContent": "Got criticized by my boss again today...",
"originalSentence": "I'm so useless",
"reframedSentence": "I notice I'm having a thought that I'm useless"
}Response:
{ "comfort": "It's natural to have such thoughts under this kind of pressure" }Personality: Warm neighbor character style (like Animal Crossing)
Optimization: Only receives journal content before the fusion sentence (not after) to reduce prompt size and speed up generation.
To minimize perceived latency, the three API calls run as a parallel pipeline rather than sequentially:
User writes → Detect (3s) → Card appears immediately
→ Reframe (background, 5s)
→ Comfort (background, after reframe)
- Before: Card appeared after detect + reframe completed (~8s)
- After: Card appears after detect only (~3s), reframe and comfort load in background
┌──────────────────────────────────────────────────────────┐
│ Left (w-60) │ Center (flex-1) │ Right (w-80) │
│ ───────────── │ ───────────── │ ───────────── │
│ JournalList │ Editor │ CompanionZone │
│ Journal list │ Dual-layer editor │ Companion zone │
│ │ - Input layer │ - AmbientCard │
│ - New journal │ - Highlight overlay│ - Perspective │
│ - Switch journal │ │ - Paper balls │
│ - Delete journal │ │ │
└──────────────────────────────────────────────────────────┘
To solve CJK (Chinese/Japanese/Korean) input method cursor jumping issues, a dual-layer architecture is used:
┌────────────────────────────────────┐
│ Visual Layer (pointer-events: none)│ ← Highlights, anchors
├────────────────────────────────────┤
│ Input Layer (transparent textarea) │ ← Text input, focus handling
└────────────────────────────────────┘
- Both layers scroll synchronously
- Visual layer uses
<span>elements for highlights and anchors - Hover detection via
getBoundingClientRect()
| Hook | Responsibility | Lines |
|---|---|---|
useSentenceManager |
Sentence parsing, AI detection calls, caching, card state | 607 |
useCardManager |
Card queue control (max 2), position calculation, lifecycle | 175 |
useJournalManager |
Journal CRUD, auto-save | 190 |
usePerspectiveCard |
Anchor hover-triggered perspective card display | 75 |
usePaperBallManager |
Paper ball collision physics, position management | 284 |
useCardTimer |
Single card 30-second countdown | 75 |
Sentence:
interface Sentence {
id: string; // UUID
text: string; // Sentence content
startIndex: number; // Character start position
endIndex: number; // Character end position
detectionStatus: 'pending' | 'detecting' | 'completed' | 'failed';
isFusion: boolean | null; // Is it a fusion sentence?
reframedText: string | null; // Reframed sentence
comfortText: string | null; // Warm rationalization text
cardStatus: 'none' | 'shown' | 'completed' | 'dismissed' | 'timeout';
}VisibleCard:
interface VisibleCard {
sentenceId: string;
sentence: Sentence;
position: { top: number }; // Sidebar vertical position
showAt: number; // Display timestamp
status: 'entering' | 'visible' | 'exiting';
}| Category | Constants |
|---|---|
| Storage Keys | SESSION_ID, PAPER_BALLS, JOURNALS, COMPLETED_SENTENCES |
| Sentence Manager | SENTENCE_MATCH_OFFSET (500), MIN_SENTENCE_LENGTH (3) |
| Paper Ball | MAX_PAPER_BALLS_DISPLAY (15), PAPER_BALL_RADIUS (20) |
| API | API_TIMEOUT_MS (8000) |
| Slider | PAPER_BALL_THRESHOLD (0.67), SLIDER_COMPLETE_THRESHOLD (0.95) |
| Animation | CARD_ANIMATION_MS (600), PERSPECTIVE_CARD_ANIMATION_MS (280) |
| Card Manager | MAX_VISIBLE_CARDS (2), EDITOR_LINE_HEIGHT (33) |
Pre-configured loggers:
sentenceLogger- Sentence managercardLogger- Card managereditorLogger- EditorpageLogger- Main pagepaperBallLogger- Paper ball systemjournalLogger- Journal manager
Behavior:
- Development: All logs enabled
- Production: Only errors logged
- In-memory rate limiting for API routes
- Default: 20 requests per minute
- Auto-cleanup of expired entries every 5 minutes
- Returns
X-RateLimit-RemainingandRetry-Afterheaders
- Singleton pattern for audio resource management
- Prevents memory leaks from repeated audio element creation
- Methods:
play(),preload(),clearAll()
| Key | Content | Scope |
|---|---|---|
margin_session_id |
Session ID | Global |
margin_journals_{sessionId} |
Journal metadata & content | Session |
margin_journal_completed_sentences |
Completed defusion sentence records | Per-journal |
margin_paper_balls |
Paper ball history (max 15) | Global |
| Variable | Color | Usage |
|---|---|---|
warm-50 |
#FDFBF9 |
Main background |
journal-bg |
#F5EDE4 |
Journal list (almond) |
companion-bg |
#F8F0EC |
Companion zone (rose beige) |
accent-warm |
#E8A87C |
Accent color, cursor |
| Amber highlight | amber-100/200 |
Fusion sentence highlight |
| Jade | emerald-300 |
Completed anchor underline |
- Body: Noto Serif SC, 18px, line-height 1.8
- Effect: Creates physical notebook writing feel
| Animation | Duration | Usage |
|---|---|---|
slideInFromRight |
600ms | Card entry |
slideOutToRight |
600ms | Card exit |
shake |
500ms | Slider incomplete shake |
fade-in |
300ms | General fade-in |
scale-in |
200ms | Dialog scale |
| Feature | Description |
|---|---|
| Non-intrusive Intervention | All interactions in sidebar, never interrupts writing |
| Real-time AI Detection | Triggered on sentence completion, 8-second timeout protection |
| Parallel Pipeline | Detect → show card immediately → reframe & comfort in background |
| Visual Metaphor | Paper crumpling → paper ball → collection, makes cognitive defusion tangible |
| Physics Simulation | Matter.js-powered realistic paper ball bouncing |
| Journal Isolation | Each journal's anchor state stored independently |
| CJK-friendly | Dual-layer editor solves input method cursor issues |
| Context-aware | Warm text generated based on journal content before the fusion sentence |
| Rate Limited | 20 req/min per client to prevent abuse |
| Input Sanitized | Basic prompt injection protection |
# Development mode
npm run dev
# Build
npm run build
# Start production server
npm start
# Type check
npx tsc --noEmit
# Lint
npm run lint✅ Verified:
- Build successful with no type errors
- All console.log replaced with centralized logger
- All hardcoded values extracted to constants
- Rate limiting on all API endpoints
- Timeout protection on all external API calls
- Memory leak prevention (audio singleton, physics cleanup)
- Input sanitization for AI prompts
MIT