Skip to content

Latest commit

 

History

History
479 lines (304 loc) · 23.3 KB

File metadata and controls

479 lines (304 loc) · 23.3 KB

impl-spec.md — Chrysalis v2

This document specifies the next phase of implementation. It is grounded in the research synthesis from 2026-03-25 and the current state of the codebase. Everything here is buildable against the existing stack (Next.js 16, Dexie, OpenAI) without introducing new infrastructure.

Items are grouped by concern. Each section specifies what to build, where in the codebase it lives, and what the user experience is. UI copy is exact unless marked as example.


1. Cold Start / Empty State Redesign

Problem: The loops page is empty until 3 events cluster. Users see nothing happening and conclude the app is broken or useless. The current empty state ("no loops yet") is passive and offers no evidence that the system noticed anything.

Principle: The cold start is not waiting — it is the system listening. Every log is a thread. The user should see the threads accumulating before they form a loop.


1.1 Loops Page — Three-Phase Empty State

Replace the current single empty state with three phases based on structuredEvents count and loop_type matches.

Phase 0 — No events logged

[animated ∞ symbol — slow 3s pulse, not static]

chrysalis is listening.

log what just happened and the system
will start to find what's underneath it.

The ∞ animation: use a CSS stroke-dashoffset animation on an SVG path, cycling the dash across the symbol over 3s, giving a "tracing" effect. Not a scale pulse — it should read as moving/alive, not breathing.

Phase 1 — 1–2 events logged, no loop yet

[same ∞, still animated]

1 thread captured.

the pattern takes shape at 3.

Count is live: reads from db.structuredEvents.where('userId').equals(USER_ID).count(). Updates without page refresh via Dexie liveQuery.

Phase 2 — 2 events of same loop_type, not yet clustered (frequency < 3)

Surface the loop_type label from the two matching structured events. Render a ghost loop card — structurally identical to a real loop card but:

  • Background: 30% opacity, no border
  • Name: the loop_type label, title-cased, in italic — "Task Avoidance"
  • Frequency badge: shows "×2" in muted color
  • Rewiring bar: absent
  • Pattern tags: absent
  • Overlay text centered on the card: "one more thread and it crystallizes"

This is not fake data. The loop_type label is real extraction output already stored in StructuredEvent. The ghost card reveals what the system already knows is forming.

Implementation:

  • Query: db.structuredEvents.where('userId').equals(USER_ID).toArray() then group by loop_type label (stored as... see §1.2 below)
  • Ghost card is a new component GhostLoopCard — takes {loopType: string, count: number}
  • Render below Phase 1 accumulator text if a loop_type has ≥2 events

Re-analyze button: Hide when there are no real loops. Show only when loops.length > 0.


1.2 Store loop_type on StructuredEvent

Currently loop_type is returned from /api/extract but only used for embedding. It must be persisted on StructuredEvent in Dexie to power the ghost card and the "pattern forming" hint.

Change: Add loopType: string field to StructuredEvent type in lib/db.ts. Store it when saving the structured event in InputScreen.tsx (after extraction returns).

No migration needed — Dexie will add the field to new records. Old records without loopType are ignored in ghost card logic.


1.3 "Pattern Forming" Hint on Confirmation Screen

After runLoopDetection() in ConfirmationScreen.handleConfirm() returns no match, check whether the current event's loopType already exists in at least one other structured event. If yes, and if this is the 2nd occurrence:

Show a quiet line below the confirmation actions:

we're starting to see a pattern here.

Style: --text-3, small, centered, no icon. Fades in with animate-fade-in delay-300.

This is a display-only change. No new state, no new API call. The data is already there.


2. Engagement & Retention

2.1 First-Act Identity Feedback

Current behavior: The IdentityRecap component only renders when interruption rate > 0 AND frequency ≥ 3 AND interruptions ≥ 2. Most first-time users never see it.

Change: After the first acted_on: true reinforcement event (regardless of loop frequency), show a single sentence in the Done screen instead of "new groove.":

you interrupted a pattern today.
that's how grooves change.

Implementation:

  • In ConfirmationScreen.tsx, after saving ReinforcementEvent with interrupted: true, query total ReinforcementEvent count for this user with interrupted: true.
  • If count === 1 (this is the first ever), pass a prop firstInterruption: true to the Done state.
  • In the done display (currently in app/page.tsx), render the two-line message instead of "new groove." for firstInterruption.

Animation: same fade-up as current done screen, no other changes.


2.2 Intervention Response Buttons — Language Change

Current: "I did it" / "not now"

Change: "I tried" / "not ready"

"I tried" is honest — the user may have attempted the intervention behavior without fully executing it. "not ready" removes the implicit "...to do the right thing" guilt of "not now." Both changes are consistent with the non-judgmental register throughout the app.

This is a two-word change in InterventionScreen.tsx.


2.3 Intervention Quality — Prompt Upgrades

The /api/intervene prompt hasn't been tuned. Intervention quality is the primary retention mechanism. The following changes are to app/api/intervene/route.ts system prompt only — no structural changes.

Add to the prompt:

  1. Self-distancing opener for Reframe type: Begin reframe interventions with a third-person observation: "Someone running this loop is usually trying to protect something." Then pivot to specifics. This reduces emotional reactivity and creates observing distance (Kross self-distancing research).

  2. Implementation intention for Action Override type: End action override interventions with a specific if-then plan, not just a directive. Not "do one application now" but "the next time [trigger] shows up, you'll [specific action] before closing the tab." Specificity creates pre-loaded automaticity.

  3. Values anchor when available: If the loop has a valuesAnchor field (see §3.3), include it: "You said this keeps getting in the way of [values anchor]. That's why this one matters."

  4. Previous log context: The /api/intervene call already accepts history param but it's passed as an empty string in most cases. Populate it with the raw text of the 2 most recent events matching this loop. The model should reference it: "last time this happened, you said..."


2.4 Weekly Closing-the-Loop Notification

Phase 2 / deferred until push notifications are available. Note the trigger mechanism for when it's built:

  • Trigger: ReinforcementEvent count for any loop changes by ≥1 in the past 7 days
  • Content: "You interrupted your [loop name] [N] time(s) this week." — no call to action, no log prompt
  • Never fires if no reinforcement events exist
  • Maximum once per week per loop

This is not a "please log more" notification. It closes the loop on the user's existing behavior.


3. Mindfulness-Informed Logging

3.1 Quick Tap Upgrade — ACT Defusion Framing

Replace the current quick taps with defusion-framed alternatives. The user is guided to observe the pattern rather than re-experience it. These still extract cleanly to the same behavioral fields.

Current → New:

Current New
avoided something felt the pull and didn't do the thing
felt anxious the thought said no
procrastinated opened it, felt something, closed it
got distracted started something I didn't mean to
checked something compulsively checked again even though I knew
snapped at someone said something I already regret

The new labels are written as first-person observations from a slight distance — the user is narrating, not re-living. The extraction prompt handles the rest.

Implementation: Update the quick tap array in InputScreen.tsx. The values change; the UI structure does not.


3.2 Extraction Prompt — Thought Field Upgrade

Change to /api/extract/route.ts system prompt:

For the thought field, change the extraction instruction from "what did the user think" to "what did their mind tell them was true in that moment — about themselves, the situation, or what would happen."

This produces defused thought content: "I'm going to fail anyway" rather than "thought about failure." The defused form is more therapeutically useful (it's what the brain actually said) and it serves the Reframe intervention type better.

No schema change. Confidence semantics unchanged.


3.3 Values Anchor — Per-Loop Prompt

After a loop reaches ≥3 events and fires its first intervention, add a one-time prompt to the Done screen:

one thing:

what does this pattern keep getting in the way of?

Below: a single text input (no label, placeholder: "e.g. being someone I trust"). Submit button: "save it". Dismissible with a small "skip" link.

Data model change: Add valuesAnchor: string | null field to Loop in lib/db.ts. Store the response via updateLoop().

Usage: The values anchor is passed to /api/intervene and /api/identity as context. Used in intervention copy (see §2.3) and identity recap.

This prompt fires once per loop, ever. Track with a valuesAnchorAsked: boolean flag on Loop.


3.4 Emotion Label Surface — Pre-Confirmation Beat

When the extraction returns an emotion field with confidence ≥ 0.70, show the emotion label for ~1.5s before the full confirmation screen renders. Not a separate screen — an overlay or pre-render state on the confirmation screen.

[emotion label, large serif, centered]

anxiety / shame

The label fades in (0.3s) then the full confirmation screen fades in behind it (after 1.5s, or on tap). The label remains visible as the first field in the confirmation.

Why: Naming an emotion reduces amygdala activation (Lieberman 2007 fMRI). This is a therapeutic function, not decoration. The 1.5s is not a delay — it is the intervention.

Implementation: In ConfirmationScreen.tsx, add a showEmotionBeat state. If emotion.confidence >= 0.70, render the emotion label first with a 1.5s auto-advance timer. The timer is cancellable on tap (skip the beat, go straight to confirmation). If no high-confidence emotion, no beat.


4. Additional Neuroplasticity Features

4.1 Embodied Cue — Pre-Intervention Line

Before the intervention card renders on the intervention screen, show a single line:

notice where you feel it.

Style: --text-3, small, centered, displayed for 1s before the intervention card fades in. Not skippable (it's 1 second). No button.

Implementation: Add a showEmbodiedCue state in InterventionScreen.tsx. On mount, show the cue line for 1000ms, then set state to show the full intervention card. The cue fades out as the card fades in.

This adds 1 second to the intervention flow. It is not a grounding pause (that's already there). It is a somatic focusing technique — directing attention to physical sensation before a cognitive prompt lands. The research rationale is state-dependent encoding: bodily attention at the moment of a new association improves retention of the association.


4.2 Spaced Repetition Check-In (24h)

After an intervention is marked acted_on: true, schedule a check-in for 24 hours later.

Implementation (browser-based, no push notifications):

  • On acted_on: true, store checkInAt: Date.now() + 86400000 on the Intervention record.
  • On app open (in app/page.tsx useEffect), query for any intervention where checkInAt <= Date.now() and checkInShown is not true.
  • If found, before rendering the input screen, show the check-in screen (new CheckInScreen component).

Check-in screen:

yesterday, you tried something.

[loop name]

what happened after?

Free text input. Submit → stored as a JournalEntry (see §5) linked to the intervention. Dismissible with "skip". After submit or skip, mark checkInShown: true on the intervention and proceed to normal input screen.

This is the spaced repetition mechanism. The check-in fires once, 24h post-intervention. It does not re-generate a new intervention — it closes the narrative on the first one.

Data model changes:

  • Intervention: add checkInAt: number | null, checkInShown: boolean
  • JournalEntry (see §5): add interventionId: string | null as optional link

5. Journaling Feature

5.1 Data Model — JournalEntry

Add to lib/db.ts:

interface JournalEntry {
  id: string;
  userId: string;
  createdAt: number;
  prompt: string;               // the question that was asked
  response: string;             // free-text user response
  entryType: "end_of_day" | "check_in" | "values_anchor";
  linkedLoopId?: string;        // optional
  linkedInterventionId?: string; // optional
}

Add to Dexie schema: journalEntries: "++id, userId, createdAt, entryType".

No extraction. No embedding. No loop contribution. Journal entries are narrative, not signal.


5.2 End-of-Day Review

A light evening reflection mode, separate from the main logging flow. Does not replace in-the-moment logging — it complements it.

Entry point:

  • A "reflect" tab added to nav (between "loops" and "settings"), or accessible from the loops page via a quiet link: "end of day →"
  • Route: /app/reflect/page.tsx

Flow:

The reflect page loads the 3 most recent RawEvent entries from today (same calendar day, user's timezone). For each, shows the raw text in a quiet block:

today you logged:

"opened LinkedIn, felt like shit, closed it"
"didn't apply again, just sat there"
"felt the pull and closed the tab"

Below, a single prompt — one of four, rotated daily (day index mod 4):

  1. "looking at today — what do you notice?"
  2. "what was hardest today, and what does that say?"
  3. "what did you try to protect yourself from today?"
  4. "if tomorrow looked slightly different, what would that feel like?"

These are Pennebaker-style meaning-making prompts, not descriptive prompts.

Free text area (large, no character limit, no extract button). Submit saves a JournalEntry with entryType: "end_of_day". Done state: "saved." — no further action.

No AI in this flow. The reflection is entirely between the user and themselves. Adding extraction here would be a category error — this is for narrative construction, not signal capture.

When to show: The page is always accessible. No push notification gating for MVP. The nav entry point is enough.


5.3 Journal History (deferred)

A /reflect/history page listing past journal entries by date, readable but not editable. Deferred until storage is expanded beyond IndexedDB. Not in current scope.


6. UI Warmth & Visual Design

6.1 Empty State Warmth

The current empty state is stark because it uses the standard --text-3 color on a dark background with no surrounding context. Changes:

  • The ∞ symbol: increase from current size to 5rem. Add the SVG trace animation described in §1.1. Color: --accent (honey amber in dark mode) at 60% opacity.
  • The "chrysalis is listening" text: --text-1 (not --text-3). Lora italic. Slightly larger than current.
  • The supporting text ("log what just happened..."): --text-3, normal weight, IBM Plex Mono, max-width constrained to 28ch for comfortable reading.
  • Spacing: significantly more vertical padding around the empty state block — it should feel like it has room to breathe, not like a footnote.

6.2 Softer Flow Transitions

Problem: The current stage transitions (input → confirming → grounding → intervening → done) are abrupt cuts or fast fades. The flow feels like a form wizard, not a continuous experience.

Changes:

  • All stage transitions: change from crossfade to a slide-up + fade where the incoming stage rises from below while the outgoing stage fades and rises slightly. Duration: 350ms. Easing: cubic-bezier(0.16, 1, 0.3, 1) (expo-out — fast start, gentle land).
  • The grounding → intervention transition specifically: keep a longer hold (200ms black/empty gap) so the breath genuinely completes before the intervention card arrives. Currently the card can appear before the user has actually exhaled.
  • Done screen: the current "logged." text appears cleanly. Keep it. But add a very subtle upward drift (4px over 800ms) so it feels like it's floating rather than just appearing.

Implementation: Add a new transition wrapper component StageTransition that wraps each stage in a div with CSS transition classes. Use a key prop on each stage so React unmounts/remounts properly, triggering enter animations.

/* new in globals.css */
.stage-enter {
  opacity: 0;
  transform: translateY(12px);
}
.stage-enter-active {
  opacity: 1;
  transform: translateY(0);
  transition: opacity 350ms cubic-bezier(0.16, 1, 0.3, 1),
              transform 350ms cubic-bezier(0.16, 1, 0.3, 1);
}

6.3 Organic Loop Chain Visualization

Current: The chain visualization in the expanded loop card shows trigger → emotion → behavior → loop as text with arrows. It reads like a database dump.

New approach: Render the chain as a connected node diagram — not a technical graph, but an organic, slightly curved horizontal flow.

Each node:

  • A rounded pill (border-radius: 999px)
  • Background: subtle, per-field color (trigger: blue-tinted, emotion: accent/warm, behavior: violet-tinted, reward: green-tinted) at 15% opacity, with a 1px border at 40% opacity
  • Text: the field value, Lora italic, small
  • Label above the pill: the field name in --text-3 uppercase tiny

Connection between nodes:

  • A faint curved SVG path connecting pill centers (not a straight character)
  • Color: --text-3 at 30% opacity
  • The path curves slightly downward in the middle (bezier), giving a gravity/flow feeling rather than a mechanical arrow

The final node wraps back with a curved line to the first — a loop. The closing arc is drawn in --accent color at 40% opacity (amber in dark mode), subtly suggesting the cycle.

Implementation:

  • New component: LoopChainViz in components/LoopChainViz.tsx
  • Takes {trigger, emotion, behavior, reward} string values
  • Renders pills inline with an SVG overlay for the connecting paths
  • SVG paths calculated from pill positions via getBoundingClientRect() in a useEffect after render
  • Falls back to the existing text chain if any field is null

6.4 Intervention as Message, Not Panel

Current: The intervention is displayed in a glass-panel card with a type label, content block, rewiring progress bar, and two buttons. It looks like a feature panel.

New design: The intervention should feel like a message — as if someone who knows you sent it.

Changes:

  • Remove the type label ("reframe", "action override") from the visible UI. It reads as technical classification. The type is still stored in the DB; it just doesn't need to be shown to the user.
  • Remove the glass panel border and background entirely. The intervention content sits directly on the page background, distinguished by typography alone.
  • The intervention text: Lora italic, 1.25rem, --text-1, line-height 1.6. No label. No card chrome.
  • The loop name pill: move it above the intervention text, small, muted, like a "re:" header on an email — "re: task avoidance".
  • The rewiring progress: move below the text, display as a single quiet line rather than a labeled bar:
    [●●●○○] rewiring in progress
    
    Five dots, filled proportional to interruption rate. Color: --accent for filled dots. No percentage number.
  • The response buttons: "I tried" and "not ready" (per §2.2). Style: "I tried" is a quiet primary button — not large, not flashy. "not ready" is plain text with no button styling, just --text-3 color and underline on hover. The asymmetry signals that "I tried" is the intended path without demanding it.
  • Spacing: generous. The intervention text needs air. Minimum 32px above and below the text block.

The goal: the intervention should feel like a message from a thoughtful person, not a notification from an app. Typography and whitespace do this. Chrome and panels undo it.


6.5 General Warmth — Small Fixes

  • Confirmation screen fields: The colored field labels (trigger, felt, did, thought, reward) currently have a slightly clinical tag appearance. Round the corners more (border-radius: 6pxborder-radius: 12px) and reduce the label font to 0.6rem. Make them feel like soft categorizations, not database fields.
  • Grounding screen: The current copy is "take a breath / you're still here / let's look at this." The third line ("let's look at this") is the weakest — it signals incoming information rather than continuing the grounding. Change to: "something's here." This maintains the pause register and creates gentle curiosity rather than a gear-shift into analysis mode.
  • Nav: "chrysalis" brand link — reduce opacity to 70% instead of full --text-1. The nav should recede, not compete. Active link: use a 2px bottom border in --accent color rather than the current opacity change.
  • Buttons: btn-primary — reduce from current padding to slightly smaller (py-2.5 px-6). The current buttons feel large against the quiet typography. The intervention buttons especially (see §6.4) should feel proportional to the text, not dominant over it.

7. Scope Boundaries

The following are deliberately out of scope for this implementation phase:

  • Cloud/backend storage (Supabase, Turso, etc.) — Dexie continues to handle all persistence. Journaling adds a new Dexie table, not an external service.
  • Push notifications — The weekly recap and check-in timing mechanisms are built but gated on browser notification permission. No native mobile.
  • Multi-user / auth — still local-user only.
  • The /app/reflect/history page — deferred until storage decision.
  • Ambient detection (Phase 2 in original spec) — unchanged, still deferred.
  • Streak mechanics — explicitly excluded. See §2 rationale.

8. Implementation Order

Recommended sequence based on impact vs. effort:

  1. §1.2 — Store loopType on StructuredEvent (prerequisite for §1.1 and §1.3)
  2. §1.1 — Three-phase empty state (highest visibility gap, pure display change)
  3. §6.4 — Intervention as message (high emotional impact, moderate effort)
  4. §2.2 — Button language change (30 seconds, meaningful)
  5. §6.2 — Softer transitions (affects every interaction, moderate effort)
  6. §3.4 — Emotion label beat (therapeutic value, low effort)
  7. §4.1 — Embodied cue line (trivial, high theoretical value)
  8. §6.3 — Organic chain visualization (visual polish, moderate effort)
  9. §1.3 — Pattern forming hint on confirmation screen
  10. §2.1 — First-act identity feedback
  11. §3.1 — Quick tap defusion framing
  12. §3.2 — Thought extraction prompt upgrade
  13. §3.3 — Values anchor prompt + data model
  14. §2.3 — Intervention prompt upgrades (intervention quality)
  15. §5.1 + §5.2 — JournalEntry model + end-of-day reflect page
  16. §4.2 — 24h check-in (spaced repetition)
  17. §6.1 + §6.5 — Empty state warmth + small UI fixes (can be done alongside anything)