Skip to content

Hongbin-Ze/Margin

Repository files navigation

Margin

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.


Core Philosophy

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

Tech Stack

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

Quick Start

Requirements

  • Node.js 18+
  • NPM or Yarn

Installation

cd margin
npm install

Configuration

Create a .env.local file and add your Gemini API Key:

GEMINI_API_KEY=your_api_key_here

Run

npm run dev

Visit http://localhost:3000


Project Structure

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

Core Feature Flow

1. Writing & Detection

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"

2. Ambient Card (Thought Observation Card)

When a cognitive fusion sentence is detected:

  1. The sentence is highlighted in amber in the editor
  2. An "Ambient Card" appears in the right companion zone
  3. Card auto-fades after 30 seconds (pauses on hover)
  4. Maximum 2 cards visible simultaneously
  5. 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

3. Paper Crumpling Interaction (Reframe Slider)

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

4. Paper Ball Ejection (Physics Engine)

After completing the crumpling:

  1. Paper ball ejects from the card position
  2. Matter.js simulates parabolic trajectory
  3. Ball lands in the "collection zone" at the bottom
  4. Random ejection angle (-60° to 240°) and force (5-15)
  5. 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
}

5. Jade Anchor & Perspective Card

After completing defusion:

  1. Original sentence changes from amber highlight to jade dashed underline
  2. Hovering the anchor shows a "Perspective Card"
  3. Card displays the AI-reframed thought
  4. Lazy-loads "warm rationalization" text (context-based)

API Endpoints

POST /api/detect

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 }

POST /api/reframe

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" }

POST /api/comfort

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.

Parallel API Pipeline

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

Page Layout

┌──────────────────────────────────────────────────────────┐
│ 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   │                    │                │
└──────────────────────────────────────────────────────────┘

Editor Architecture (Mirror Overlay)

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()

State Management

Core Hooks

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

Data Structures

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';
}

Utility Architecture

Centralized Constants (utils/constants.ts)

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)

Logger System (utils/logger.ts)

Pre-configured loggers:

  • sentenceLogger - Sentence manager
  • cardLogger - Card manager
  • editorLogger - Editor
  • pageLogger - Main page
  • paperBallLogger - Paper ball system
  • journalLogger - Journal manager

Behavior:

  • Development: All logs enabled
  • Production: Only errors logged

Rate Limiting (utils/rateLimit.ts)

  • In-memory rate limiting for API routes
  • Default: 20 requests per minute
  • Auto-cleanup of expired entries every 5 minutes
  • Returns X-RateLimit-Remaining and Retry-After headers

Audio Manager (utils/audioManager.ts)

  • Singleton pattern for audio resource management
  • Prevents memory leaks from repeated audio element creation
  • Methods: play(), preload(), clearAll()

Persistence Strategy

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

Design System

Warm Color Palette

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

Typography

  • Body: Noto Serif SC, 18px, line-height 1.8
  • Effect: Creates physical notebook writing feel

Animations

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

Key Features

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

# Development mode
npm run dev

# Build
npm run build

# Start production server
npm start

# Type check
npx tsc --noEmit

# Lint
npm run lint

Production Readiness

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

License

MIT

About

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors