Initial commit: Inner OS v1
Full-stack inner development operating system built on Next.js 16 + Prisma 7 + PostgreSQL + OpenAI + Clerk. Generates personalized daily mental training tasks, collects reflection, updates user state, and provides internal reward feedback. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
+43
@@ -0,0 +1,43 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.*
|
||||
.yarn/*
|
||||
!.yarn/patches
|
||||
!.yarn/plugins
|
||||
!.yarn/releases
|
||||
!.yarn/versions
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# next.js
|
||||
/.next/
|
||||
/out/
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
*.pem
|
||||
|
||||
# debug
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
.pnpm-debug.log*
|
||||
|
||||
# env files (can opt-in for committing if needed)
|
||||
.env*
|
||||
|
||||
# vercel
|
||||
.vercel
|
||||
|
||||
# typescript
|
||||
*.tsbuildinfo
|
||||
next-env.d.ts
|
||||
|
||||
/src/generated/prisma
|
||||
@@ -0,0 +1,5 @@
|
||||
<!-- BEGIN:nextjs-agent-rules -->
|
||||
# This is NOT the Next.js you know
|
||||
|
||||
This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` before writing any code. Heed deprecation notices.
|
||||
<!-- END:nextjs-agent-rules -->
|
||||
@@ -0,0 +1,28 @@
|
||||
# Inner OS — Claude Code Configuration
|
||||
|
||||
## Project overview
|
||||
Inner OS v1 is an AI-driven inner development operating system. Next.js monolith + Prisma + PostgreSQL + OpenAI. Generates one personalized daily mental training task, explains why it's chosen, collects reflection, updates user state, and provides internal (non-competitive) reward feedback.
|
||||
|
||||
## Skill routing
|
||||
|
||||
When the user's request matches an available skill, invoke it via the Skill tool. When in doubt, invoke the skill.
|
||||
|
||||
Key routing rules:
|
||||
- Product ideas/brainstorming → invoke /office-hours
|
||||
- Strategy/scope → invoke /plan-ceo-review
|
||||
- Architecture → invoke /plan-eng-review
|
||||
- Design system/plan review → invoke /design-consultation or /plan-design-review
|
||||
- Full review pipeline → invoke /autoplan
|
||||
- Bugs/errors → invoke /investigate
|
||||
- QA/testing site behavior → invoke /qa or /qa-only
|
||||
- Code review/diff check → invoke /review
|
||||
- Visual polish → invoke /design-review
|
||||
- Ship/deploy/PR → invoke /ship or /land-and-deploy
|
||||
- Save progress → invoke /context-save
|
||||
- Resume context → invoke /context-restore
|
||||
|
||||
## Design System
|
||||
Always read DESIGN.md before making any visual or UI decisions.
|
||||
All font choices, colors, spacing, and aesthetic direction are defined there.
|
||||
Do not deviate without explicit user approval.
|
||||
In QA mode, flag any code that doesn't match DESIGN.md.
|
||||
@@ -0,0 +1,128 @@
|
||||
# Design System — Inner OS
|
||||
|
||||
## Product Context
|
||||
- **What this is:** AI-driven inner development operating system — generates one personalized daily mental training task, explains why it's chosen, collects post-session reflection, updates user state, and delivers internal reward feedback.
|
||||
- **Who it's for:** Solo inner development practitioners who want structured mental training without a human coach.
|
||||
- **Space/industry:** Mental wellness / inner development (adjacent to Headspace, Calm, Waking Up)
|
||||
- **Project type:** Web app (mobile-first, journal-width constrained)
|
||||
|
||||
## Aesthetic Direction
|
||||
- **Direction:** Editorial/Organic hybrid — typographic warmth with natural earth tones
|
||||
- **Decoration level:** Intentional — subtle warmth cues (cream background, soft rounded corners) without decorative blobs or gradients. The "decoration" IS the typography and whitespace.
|
||||
- **Mood:** Calm companion. The UI should feel like a well-printed private journal, not a SaaS dashboard. Paper texture, ink-color text, breathing room. The app recedes; the practice is center stage.
|
||||
- **Memorable thing:** "It feels like a private journal, not an app."
|
||||
- **Reference sites:** None (greenfield design system)
|
||||
|
||||
## Typography
|
||||
- **Display/Headings:** Fraunces (serif, wght 300-700, opsz 16-48) — Literary, warm serif with variable optical size. Feels like a book, not a browser. Soft curves and humanist proportions serve the private journal metaphor.
|
||||
- **Body:** DM Sans (sans-serif, wght 400-500-700) — Clean geometric sans with humanist warmth. High x-height for readability at 16px.
|
||||
- **UI/Labels:** DM Sans (500 weight) — Same family, slightly bolder.
|
||||
- **Data/Tables:** DM Sans (tabular-nums enabled) — Built-in tabular figures for progress charts and metrics.
|
||||
- **Code:** JetBrains Mono (developer tooling only, never user-facing)
|
||||
- **Loading:** Google Fonts CDN with `font-display: swap`
|
||||
- **Scale:**
|
||||
|
||||
| Level | Size / Line-height | Use |
|
||||
|-------|-------------------|-----|
|
||||
| xs | 12px / 1.5 (18px) | Captions, timestamps |
|
||||
| sm | 14px / 1.5 (21px) | Secondary text, labels |
|
||||
| base | 16px / 1.6 (25.6px) | Body text |
|
||||
| md | 18px / 1.5 (27px) | Card headings, emphasized body |
|
||||
| lg | 22px / 1.4 (30.8px) | Section headings |
|
||||
| xl | 28px / 1.3 (36.4px) | Page titles |
|
||||
| 2xl | 36px / 1.2 (43.2px) | Hero/day heading |
|
||||
|
||||
## Color
|
||||
- **Approach:** Restrained — one accent (amber), one secondary (sage), warm neutrals. Color is rare and meaningful — it signals action, progress, or reward, never decoration.
|
||||
|
||||
### Light Mode
|
||||
| Token | Hex | Role |
|
||||
|-------|-----|------|
|
||||
| Primary/Accent | `#C8834A` (Amber) | CTAs, active states, streak indicators |
|
||||
| Secondary | `#7A9A8B` (Sage) | Success states, progress fill, calm indicators |
|
||||
| Background | `#FAF7F2` (Cream) | Page background |
|
||||
| Surface | `#F3EFE8` (Sand) | Cards, elevated surfaces |
|
||||
| Border | `#E6E0D4` (Stone) | Dividers, input borders |
|
||||
| Text Primary | `#2C2416` (Ink) | Body text, headings |
|
||||
| Text Secondary | `#6B5E4A` (Muted ink) | Captions, secondary labels |
|
||||
| Text Tertiary | `#9B8E7A` (Faded ink) | Placeholders, disabled |
|
||||
|
||||
### Semantic
|
||||
| Token | Hex | Use |
|
||||
|-------|-----|-----|
|
||||
| Success | `#7A9A8B` (Sage) | Completion confirmations, progress |
|
||||
| Warning | `#C8834A` (Amber) | Streak-at-risk, attention needed |
|
||||
| Error | `#C45A4A` | Session expired, validation errors |
|
||||
| Info | `#5A8AB5` | Neutral status, tips |
|
||||
|
||||
### Dark Mode
|
||||
- **Strategy:** Redesign surfaces, reduce saturation 15%. Warm dark background instead of cool dark. Amber and sage stay — they read warm against dark.
|
||||
- **Tokens:**
|
||||
|
||||
| Token | Hex | Role |
|
||||
|-------|-----|------|
|
||||
| Background | `#1E1A14` (Night ink) | Page background |
|
||||
| Surface | `#292418` (Dark sand) | Cards, elevated surfaces |
|
||||
| Border | `#3D3528` (Dark stone) | Dividers, input borders |
|
||||
| Text Primary | `#F3EFE8` (Warm sand) | Body text, headings |
|
||||
| Text Secondary | `#B8A892` (Muted cream) | Captions, secondary labels |
|
||||
| Text Tertiary | `#8A7D6B` | Placeholders, disabled |
|
||||
|
||||
## Spacing
|
||||
- **Base unit:** 8px
|
||||
- **Density:** Comfortable
|
||||
|
||||
| Token | Value | Use |
|
||||
|-------|-------|-----|
|
||||
| 2xs | 4px | Icon-text gap, inline spacing |
|
||||
| xs | 8px | Tight gaps, chip padding |
|
||||
| sm | 12px | Card padding (compact) |
|
||||
| md | 16px | Standard padding, card gaps |
|
||||
| lg | 24px | Section gap, card padding (comfortable) |
|
||||
| xl | 32px | Page-level spacing |
|
||||
| 2xl | 48px | Major section divisions |
|
||||
| 3xl | 64px | Top/bottom page breathing room |
|
||||
|
||||
## Shape
|
||||
- **Border radius (hierarchical):**
|
||||
|
||||
| Token | Value | Use |
|
||||
|-------|-------|-----|
|
||||
| sm | 6px | Inputs, buttons |
|
||||
| md | 12px | Cards, modals |
|
||||
| lg | 16px | Large surfaces |
|
||||
| full | 9999px | Pills, badges, stage indicators |
|
||||
|
||||
## Layout
|
||||
- **Approach:** Hybrid — single-column journal-width (640px max, centered) for reading/reflection pages; grid-disciplined for progress charts
|
||||
- **Grid:** 1 column (mobile), centered 640px (desktop)
|
||||
- **Max content width:** 640px
|
||||
- **Navigation:** Bottom tab bar, 4 tabs (Today, Progress, Rewards, Settings), fixed position, 56px height, 44px minimum touch targets
|
||||
|
||||
## Motion
|
||||
- **Approach:** Intentional — transitions that aid comprehension without calling attention
|
||||
- **Easing:** enter(ease-out), exit(ease-in), move(ease-in-out)
|
||||
- **Duration:**
|
||||
- Micro: 150ms (button hover)
|
||||
- Short: 200ms (page transitions, fade + 4px Y shift)
|
||||
- Medium: 400ms (reward animations, scale + fade)
|
||||
- Long: 600ms (progress bar fill)
|
||||
|
||||
## Accessibility
|
||||
- **Baseline:** WCAG AA
|
||||
- **Touch targets:** 44px minimum
|
||||
- **Contrast:** All body text meets 4.5:1 against background
|
||||
- **Keyboard:** All interactive elements keyboard-navigable
|
||||
- **Focus:** Visible focus ring (amber outline, 2px) on all interactive elements
|
||||
- **Screen readers:** ARIA landmarks on all pages (banner, main, navigation, complementary)
|
||||
|
||||
## Decisions Log
|
||||
| Date | Decision | Rationale |
|
||||
|------|----------|-----------|
|
||||
| 2026-05-20 | Initial design system created | Created by /design-consultation based on CEO plan, eng review, and 10 design review decisions |
|
||||
| 2026-05-20 | Practice journal aesthetic | User's memorable thing: "It feels like a private journal, not an app" |
|
||||
| 2026-05-20 | Fraunces + DM Sans typography | Literary serif for journal feel; clean sans for readability |
|
||||
| 2026-05-20 | Cream/Amber/Sage/Ink palette | Warm, grounded, non-clinical colors that serve the journal metaphor |
|
||||
| 2026-05-20 | Journal-width max 640px centered | Matches reading column width; mobile-first, not responsive-gimmick |
|
||||
| 2026-05-20 | Bottom tab bar navigation | Standard mobile pattern, no learning curve, 4 primary destinations |
|
||||
| 2026-05-20 | Calm companion emotional tone | Warm but not saccharine, grounded but not clinical |
|
||||
@@ -0,0 +1,36 @@
|
||||
This is a [Next.js](https://nextjs.org) project bootstrapped with [`create-next-app`](https://nextjs.org/docs/app/api-reference/cli/create-next-app).
|
||||
|
||||
## Getting Started
|
||||
|
||||
First, run the development server:
|
||||
|
||||
```bash
|
||||
npm run dev
|
||||
# or
|
||||
yarn dev
|
||||
# or
|
||||
pnpm dev
|
||||
# or
|
||||
bun dev
|
||||
```
|
||||
|
||||
Open [http://localhost:3000](http://localhost:3000) with your browser to see the result.
|
||||
|
||||
You can start editing the page by modifying `app/page.tsx`. The page auto-updates as you edit the file.
|
||||
|
||||
This project uses [`next/font`](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) to automatically optimize and load [Geist](https://vercel.com/font), a new font family for Vercel.
|
||||
|
||||
## Learn More
|
||||
|
||||
To learn more about Next.js, take a look at the following resources:
|
||||
|
||||
- [Next.js Documentation](https://nextjs.org/docs) - learn about Next.js features and API.
|
||||
- [Learn Next.js](https://nextjs.org/learn) - an interactive Next.js tutorial.
|
||||
|
||||
You can check out [the Next.js GitHub repository](https://github.com/vercel/next.js) - your feedback and contributions are welcome!
|
||||
|
||||
## Deploy on Vercel
|
||||
|
||||
The easiest way to deploy your Next.js app is to use the [Vercel Platform](https://vercel.com/new?utm_medium=default-template&filter=next.js&utm_source=create-next-app&utm_campaign=create-next-app-readme) from the creators of Next.js.
|
||||
|
||||
Check out our [Next.js deployment documentation](https://nextjs.org/docs/app/building-your-application/deploying) for more details.
|
||||
@@ -0,0 +1,43 @@
|
||||
# Inner OS — Deferred Work
|
||||
|
||||
## From /plan-eng-review (2026-05-20)
|
||||
|
||||
### T1: Define state capture mechanism
|
||||
**What:** Specify the UX and formulas for capturing attention_level, emotion_stability, and stress_level.
|
||||
**Why:** These are inputs to task generation, but no data entry path exists. Session reflection has focus_score and emotion_score (1-5 user self-report), but no mapping to the state model.
|
||||
**Context:** The session reflection form needs clear self-report questions ("How focused were you? 1-5", "How emotionally balanced? 1-5"). Map formulas: attention_delta = (focus_score - 3) * 5, emotion_delta = (emotion_score - 3) * 5. For the empty-reflection case (user writes nothing), default deltas to 0 (no change).
|
||||
**Effort:** S (human: ~3h / CC: ~15min)
|
||||
**Priority:** P1
|
||||
**Depends on:** Session reflection form design
|
||||
|
||||
### T2: Add LLM output content safety
|
||||
**What:** Add a lightweight safety layer on LLM-generated tasks before showing them to users.
|
||||
**Why:** GPT-4o-mini could generate harmful suggestions (extreme practices, dangerous advice). A single bad output is a reputational event.
|
||||
**Context:** Implement a keyword blocklist (self-harm, extreme behaviors, medical advice) and an optional LLM self-review prompt ("Does this task contain harmful or dangerous suggestions? Reply yes/no."). If flagged, fall back to a template task.
|
||||
**Effort:** S (human: ~2h / CC: ~10min)
|
||||
**Priority:** P1
|
||||
**Depends on:** Task generation pipeline
|
||||
|
||||
### T3: Define stage advancement rules per level
|
||||
**What:** Define concrete thresholds for each stage transition (0→1, 1→2, 2→3, 3→4, 4→5).
|
||||
**Why:** The LLM prompt needs current stage to set task difficulty. Without per-level rules, advancement is arbitrary.
|
||||
**Context:** Proposed rules:
|
||||
- Stage 0→1: 7 completed sessions (any consistency)
|
||||
- Stage 1→2: 14 completed sessions + consistency >= 60
|
||||
- Stage 2→3: 30 completed sessions + consistency >= 80 + stability >= 60
|
||||
- Stage 3→4: 60 completed sessions + consistency >= 90 + stability >= 70
|
||||
- Stage 4→5: 120 completed sessions + consistency >= 95 + stability >= 80
|
||||
**Effort:** S (human: ~1h / CC: ~10min)
|
||||
**Priority:** P2
|
||||
**Depends on:** —
|
||||
|
||||
### T4: Add Clerk user deletion webhook handling
|
||||
**What:** Handle Clerk user.deleted webhook to cascade-delete user data, plus a periodic cleanup job for orphaned rows.
|
||||
**Why:** Clerk webhooks are at-least-once (not exactly-once). If delivery fails, UserState/Sessions/Tasks/RewardEvents become orphaned with no owner.
|
||||
**Context:** Add `onDelete: Cascade` on User→UserState Prisma relation. Implement Clerk webhook handler at `/api/webhooks/clerk`. Add a weekly cleanup job that deletes rows where user_id doesn't exist in Clerk (query Clerk API).
|
||||
**Effort:** M (human: ~4h / CC: ~15min)
|
||||
**Priority:** P1
|
||||
**Depends on:** Clerk integration
|
||||
|
||||
## From /plan-ceo-review (2026-05-20)
|
||||
- (all scope items accepted as "Build now" — nothing deferred from CEO review)
|
||||
@@ -0,0 +1,983 @@
|
||||
{
|
||||
"lockfileVersion": 1,
|
||||
"configVersion": 0,
|
||||
"workspaces": {
|
||||
"": {
|
||||
"name": "inner-os",
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^7.3.7",
|
||||
"@prisma/adapter-pg": "^7.8.0",
|
||||
"@prisma/client": "^7.8.0",
|
||||
"next": "16.2.6",
|
||||
"openai": "^6.38.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20.19.41",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"dotenv": "^17.4.2",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"prisma": "^7.8.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5",
|
||||
},
|
||||
},
|
||||
},
|
||||
"packages": {
|
||||
"@alloc/quick-lru": ["@alloc/quick-lru@5.2.0", "", {}, ""],
|
||||
|
||||
"@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, ""],
|
||||
|
||||
"@babel/compat-data": ["@babel/compat-data@7.29.3", "", {}, ""],
|
||||
|
||||
"@babel/core": ["@babel/core@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-compilation-targets": "^7.28.6", "@babel/helper-module-transforms": "^7.28.6", "@babel/helpers": "^7.28.6", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/traverse": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, ""],
|
||||
|
||||
"@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, ""],
|
||||
|
||||
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, ""],
|
||||
|
||||
"@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, ""],
|
||||
|
||||
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, ""],
|
||||
|
||||
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, ""],
|
||||
|
||||
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, ""],
|
||||
|
||||
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, ""],
|
||||
|
||||
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, ""],
|
||||
|
||||
"@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, ""],
|
||||
|
||||
"@babel/parser": ["@babel/parser@7.29.3", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": { "parser": "bin/babel-parser.js" } }, ""],
|
||||
|
||||
"@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, ""],
|
||||
|
||||
"@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, ""],
|
||||
|
||||
"@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, ""],
|
||||
|
||||
"@clerk/backend": ["@clerk/backend@3.4.11", "", { "dependencies": { "@clerk/shared": "^4.12.2", "standardwebhooks": "^1.0.0", "tslib": "2.8.1" } }, "sha512-WT+4FrcMMofxe+irQYUkawoRWaHHXT9/wiRYhRbSb30cOPjN95ViydqPhAyp8ZWAHInHg4mILynQQRJH14SKZQ=="],
|
||||
|
||||
"@clerk/nextjs": ["@clerk/nextjs@7.3.7", "", { "dependencies": { "@clerk/backend": "^3.4.11", "@clerk/react": "^6.6.6", "@clerk/shared": "^4.12.2", "server-only": "0.0.1", "tslib": "2.8.1" }, "peerDependencies": { "next": "^15.2.8 || ^15.3.8 || ^15.4.10 || ^15.5.9 || ^15.6.0-0 || ^16.0.10 || ^16.1.0-0", "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-CLG63zKPHditk52Z/+c6BJ47V1Q68ACjeps0xHDRW3X/Yv/FZdM+IUKu9Egb5PJ/TkRFxsI1nU5SOY2B8o/WxA=="],
|
||||
|
||||
"@clerk/react": ["@clerk/react@6.6.6", "", { "dependencies": { "@clerk/shared": "^4.12.2", "tslib": "2.8.1" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-MVHLDZeGobSbGSZgAdb4G1BbB8ZU5XAmBBdIVLXiPOLEst2TYM5bP137WRA76y9GlmVmKYdKzph/TUi87P/JOA=="],
|
||||
|
||||
"@clerk/shared": ["@clerk/shared@4.12.2", "", { "dependencies": { "@tanstack/query-core": "^5.100.6", "dequal": "2.0.3", "glob-to-regexp": "0.4.1", "js-cookie": "3.0.5", "std-env": "^3.9.0" }, "peerDependencies": { "react": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0", "react-dom": "^18.0.0 || ~19.0.3 || ~19.1.4 || ~19.2.3 || ~19.3.0-0" } }, "sha512-jDkip8tKTzYz/cPKMCsjOoACH3Xh37zcbCrssMRTYOq3GZypIpZ6WAs4m4G82URL0WY+yz5frrHVjRrHyAb6LA=="],
|
||||
|
||||
"@electric-sql/pglite": ["@electric-sql/pglite@0.4.1", "", {}, "sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q=="],
|
||||
|
||||
"@electric-sql/pglite-socket": ["@electric-sql/pglite-socket@0.1.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" }, "bin": { "pglite-server": "dist/scripts/server.js" } }, "sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw=="],
|
||||
|
||||
"@electric-sql/pglite-tools": ["@electric-sql/pglite-tools@0.3.1", "", { "peerDependencies": { "@electric-sql/pglite": "0.4.1" } }, "sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA=="],
|
||||
|
||||
"@eslint-community/eslint-utils": ["@eslint-community/eslint-utils@4.9.1", "", { "dependencies": { "eslint-visitor-keys": "^3.4.3" }, "peerDependencies": { "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" } }, ""],
|
||||
|
||||
"@eslint-community/regexpp": ["@eslint-community/regexpp@4.12.2", "", {}, ""],
|
||||
|
||||
"@eslint/config-array": ["@eslint/config-array@0.21.2", "", { "dependencies": { "@eslint/object-schema": "^2.1.7", "debug": "^4.3.1", "minimatch": "^3.1.5" } }, ""],
|
||||
|
||||
"@eslint/config-helpers": ["@eslint/config-helpers@0.4.2", "", { "dependencies": { "@eslint/core": "^0.17.0" } }, ""],
|
||||
|
||||
"@eslint/core": ["@eslint/core@0.17.0", "", { "dependencies": { "@types/json-schema": "^7.0.15" } }, ""],
|
||||
|
||||
"@eslint/eslintrc": ["@eslint/eslintrc@3.3.5", "", { "dependencies": { "ajv": "^6.14.0", "debug": "^4.3.2", "espree": "^10.0.1", "globals": "^14.0.0", "ignore": "^5.2.0", "import-fresh": "^3.2.1", "js-yaml": "^4.1.1", "minimatch": "^3.1.5", "strip-json-comments": "^3.1.1" } }, ""],
|
||||
|
||||
"@eslint/js": ["@eslint/js@9.39.4", "", {}, ""],
|
||||
|
||||
"@eslint/object-schema": ["@eslint/object-schema@2.1.7", "", {}, ""],
|
||||
|
||||
"@eslint/plugin-kit": ["@eslint/plugin-kit@0.4.1", "", { "dependencies": { "@eslint/core": "^0.17.0", "levn": "^0.4.1" } }, ""],
|
||||
|
||||
"@hono/node-server": ["@hono/node-server@1.19.11", "", { "peerDependencies": { "hono": "^4" } }, "sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g=="],
|
||||
|
||||
"@humanfs/core": ["@humanfs/core@0.19.2", "", { "dependencies": { "@humanfs/types": "^0.15.0" } }, ""],
|
||||
|
||||
"@humanfs/node": ["@humanfs/node@0.16.8", "", { "dependencies": { "@humanfs/core": "^0.19.2", "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" } }, ""],
|
||||
|
||||
"@humanfs/types": ["@humanfs/types@0.15.0", "", {}, ""],
|
||||
|
||||
"@humanwhocodes/module-importer": ["@humanwhocodes/module-importer@1.0.1", "", {}, ""],
|
||||
|
||||
"@humanwhocodes/retry": ["@humanwhocodes/retry@0.4.3", "", {}, ""],
|
||||
|
||||
"@img/colour": ["@img/colour@1.1.0", "", {}, ""],
|
||||
|
||||
"@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, ""],
|
||||
|
||||
"@img/sharp-libvips-darwin-arm64": ["@img/sharp-libvips-darwin-arm64@1.2.4", "", { "os": "darwin", "cpu": "arm64" }, ""],
|
||||
|
||||
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, ""],
|
||||
|
||||
"@jridgewell/remapping": ["@jridgewell/remapping@2.3.5", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, ""],
|
||||
|
||||
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, ""],
|
||||
|
||||
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, ""],
|
||||
|
||||
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, ""],
|
||||
|
||||
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||
|
||||
"@next/env": ["@next/env@16.2.6", "", {}, ""],
|
||||
|
||||
"@next/eslint-plugin-next": ["@next/eslint-plugin-next@16.2.6", "", { "dependencies": { "fast-glob": "3.3.1" } }, ""],
|
||||
|
||||
"@next/swc-darwin-arm64": ["@next/swc-darwin-arm64@16.2.6", "", { "os": "darwin", "cpu": "arm64" }, ""],
|
||||
|
||||
"@next/swc-darwin-x64": ["@next/swc-darwin-x64@16.2.6", "", { "os": "darwin", "cpu": "x64" }, "sha512-v/YLBHIY132Ced3puBJ7YJKw1lqsCrgcNo2aRJlCEyQrrCeRJlvGlnmxhPxNQI3KE3N1DN5r9TPNPvka3nq5RQ=="],
|
||||
|
||||
"@next/swc-linux-arm64-gnu": ["@next/swc-linux-arm64-gnu@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-RPOvqlYBbcQjkz9VQQDZ2T2bARIjXZV1KFlt+V2Mr6SW/e4I9fcKsaA0hdyf2FHoTlsV2xnBd5Y912rP/1Ce6w=="],
|
||||
|
||||
"@next/swc-linux-arm64-musl": ["@next/swc-linux-arm64-musl@16.2.6", "", { "os": "linux", "cpu": "arm64" }, "sha512-URUTu1+dMkxJsPFgm+OeEvq9wf5sujw0EvgYy80TDGHTSLTnIHeqb0Eu8A3sC95IRgjejQL+kC4mw+4yPxiAXA=="],
|
||||
|
||||
"@next/swc-linux-x64-gnu": ["@next/swc-linux-x64-gnu@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-DOj182mPV8G3UkrayLoREM5YEYI+Dk5wv7Ox9xl1fFibAELEsFD0lDPfHIeILlutMMfdyhlzYPELG3peuKaurw=="],
|
||||
|
||||
"@next/swc-linux-x64-musl": ["@next/swc-linux-x64-musl@16.2.6", "", { "os": "linux", "cpu": "x64" }, "sha512-HKQ5SP/V/ub73UvF7n/zeJlxk2kLmtL7Wzrg4WfmkjmNos5onJ2tKu7yZOPdL18A6Svfn3max29ym+ry7NkK4g=="],
|
||||
|
||||
"@next/swc-win32-arm64-msvc": ["@next/swc-win32-arm64-msvc@16.2.6", "", { "os": "win32", "cpu": "arm64" }, "sha512-LZXpTlPyS5v7HhSmnvsLGP3iIYgYOBnc8r8ArlT55sGHV89bR2HlDdBjWQ+PY6SJMmk8TuVGFuxalnP3k/0Dwg=="],
|
||||
|
||||
"@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.2.6", "", { "os": "win32", "cpu": "x64" }, "sha512-F0+4i0h9J6C4eE3EAPWsoCk7UW/dbzOjyzxY0qnDUOYFu6FFmdZ6l97/XdV3/Nz3VYyO7UWjyEJUXkGqcoXfMA=="],
|
||||
|
||||
"@nodelib/fs.scandir": ["@nodelib/fs.scandir@2.1.5", "", { "dependencies": { "@nodelib/fs.stat": "2.0.5", "run-parallel": "^1.1.9" } }, ""],
|
||||
|
||||
"@nodelib/fs.stat": ["@nodelib/fs.stat@2.0.5", "", {}, ""],
|
||||
|
||||
"@nodelib/fs.walk": ["@nodelib/fs.walk@1.2.8", "", { "dependencies": { "@nodelib/fs.scandir": "2.1.5", "fastq": "^1.6.0" } }, ""],
|
||||
|
||||
"@nolyfill/is-core-module": ["@nolyfill/is-core-module@1.0.39", "", {}, ""],
|
||||
|
||||
"@prisma/adapter-pg": ["@prisma/adapter-pg@7.8.0", "", { "dependencies": { "@prisma/driver-adapter-utils": "7.8.0", "@types/pg": "^8.16.0", "pg": "^8.16.3", "postgres-array": "3.0.4" } }, "sha512-ygb3UkerK3v8MDpXVgCISdRNDozpxh6+JVJgiIGbSr5KBgz10LLf5ejUskPGoXlsIjxsOu6nuy1JVQr2EKGSlg=="],
|
||||
|
||||
"@prisma/client": ["@prisma/client@7.8.0", "", { "dependencies": { "@prisma/client-runtime-utils": "7.8.0" }, "peerDependencies": { "prisma": "*", "typescript": ">=5.4.0" } }, "sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw=="],
|
||||
|
||||
"@prisma/client-runtime-utils": ["@prisma/client-runtime-utils@7.8.0", "", {}, "sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw=="],
|
||||
|
||||
"@prisma/config": ["@prisma/config@7.8.0", "", { "dependencies": { "c12": "3.3.4", "deepmerge-ts": "7.1.5", "effect": "3.20.0", "empathic": "2.0.0" } }, "sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw=="],
|
||||
|
||||
"@prisma/debug": ["@prisma/debug@7.8.0", "", {}, "sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA=="],
|
||||
|
||||
"@prisma/dev": ["@prisma/dev@0.24.3", "", { "dependencies": { "@electric-sql/pglite": "0.4.1", "@electric-sql/pglite-socket": "0.1.1", "@electric-sql/pglite-tools": "0.3.1", "@hono/node-server": "1.19.11", "@prisma/get-platform": "7.2.0", "@prisma/query-plan-executor": "7.2.0", "@prisma/streams-local": "0.1.2", "foreground-child": "3.3.1", "get-port-please": "3.2.0", "hono": "^4.12.8", "http-status-codes": "2.3.0", "pathe": "2.0.3", "proper-lockfile": "4.1.2", "remeda": "2.33.4", "std-env": "3.10.0", "valibot": "1.2.0", "zeptomatch": "2.1.0" } }, "sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg=="],
|
||||
|
||||
"@prisma/driver-adapter-utils": ["@prisma/driver-adapter-utils@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A=="],
|
||||
|
||||
"@prisma/engines": ["@prisma/engines@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0", "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "@prisma/fetch-engine": "7.8.0", "@prisma/get-platform": "7.8.0" } }, "sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw=="],
|
||||
|
||||
"@prisma/engines-version": ["@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "", {}, "sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA=="],
|
||||
|
||||
"@prisma/fetch-engine": ["@prisma/fetch-engine@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0", "@prisma/engines-version": "7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a", "@prisma/get-platform": "7.8.0" } }, "sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ=="],
|
||||
|
||||
"@prisma/get-platform": ["@prisma/get-platform@7.2.0", "", { "dependencies": { "@prisma/debug": "7.2.0" } }, "sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA=="],
|
||||
|
||||
"@prisma/query-plan-executor": ["@prisma/query-plan-executor@7.2.0", "", {}, "sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ=="],
|
||||
|
||||
"@prisma/streams-local": ["@prisma/streams-local@0.1.2", "", { "dependencies": { "ajv": "^8.12.0", "better-result": "^2.7.0", "env-paths": "^3.0.0", "proper-lockfile": "^4.1.2" } }, "sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg=="],
|
||||
|
||||
"@prisma/studio-core": ["@prisma/studio-core@0.27.3", "", { "dependencies": { "@radix-ui/react-toggle": "1.1.10", "chart.js": "4.5.1" }, "peerDependencies": { "@types/react": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" } }, "sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw=="],
|
||||
|
||||
"@radix-ui/primitive": ["@radix-ui/primitive@1.1.3", "", {}, "sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg=="],
|
||||
|
||||
"@radix-ui/react-compose-refs": ["@radix-ui/react-compose-refs@1.1.2", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg=="],
|
||||
|
||||
"@radix-ui/react-primitive": ["@radix-ui/react-primitive@2.1.3", "", { "dependencies": { "@radix-ui/react-slot": "1.2.3" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ=="],
|
||||
|
||||
"@radix-ui/react-slot": ["@radix-ui/react-slot@1.2.3", "", { "dependencies": { "@radix-ui/react-compose-refs": "1.1.2" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A=="],
|
||||
|
||||
"@radix-ui/react-toggle": ["@radix-ui/react-toggle@1.1.10", "", { "dependencies": { "@radix-ui/primitive": "1.1.3", "@radix-ui/react-primitive": "2.1.3", "@radix-ui/react-use-controllable-state": "1.2.2" }, "peerDependencies": { "@types/react": "*", "@types/react-dom": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ=="],
|
||||
|
||||
"@radix-ui/react-use-controllable-state": ["@radix-ui/react-use-controllable-state@1.2.2", "", { "dependencies": { "@radix-ui/react-use-effect-event": "0.0.2", "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg=="],
|
||||
|
||||
"@radix-ui/react-use-effect-event": ["@radix-ui/react-use-effect-event@0.0.2", "", { "dependencies": { "@radix-ui/react-use-layout-effect": "1.1.1" }, "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA=="],
|
||||
|
||||
"@radix-ui/react-use-layout-effect": ["@radix-ui/react-use-layout-effect@1.1.1", "", { "peerDependencies": { "@types/react": "*", "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" } }, "sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ=="],
|
||||
|
||||
"@rtsao/scc": ["@rtsao/scc@1.1.0", "", {}, ""],
|
||||
|
||||
"@stablelib/base64": ["@stablelib/base64@1.0.1", "", {}, "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ=="],
|
||||
|
||||
"@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="],
|
||||
|
||||
"@swc/helpers": ["@swc/helpers@0.5.15", "", { "dependencies": { "tslib": "^2.8.0" } }, ""],
|
||||
|
||||
"@tailwindcss/node": ["@tailwindcss/node@4.3.0", "", { "dependencies": { "@jridgewell/remapping": "^2.3.5", "enhanced-resolve": "^5.21.0", "jiti": "^2.6.1", "lightningcss": "1.32.0", "magic-string": "^0.30.21", "source-map-js": "^1.2.1", "tailwindcss": "4.3.0" } }, ""],
|
||||
|
||||
"@tailwindcss/oxide": ["@tailwindcss/oxide@4.3.0", "", { "optionalDependencies": { "@tailwindcss/oxide-darwin-arm64": "4.3.0" } }, ""],
|
||||
|
||||
"@tailwindcss/oxide-darwin-arm64": ["@tailwindcss/oxide-darwin-arm64@4.3.0", "", { "os": "darwin", "cpu": "arm64" }, ""],
|
||||
|
||||
"@tailwindcss/postcss": ["@tailwindcss/postcss@4.3.0", "", { "dependencies": { "@alloc/quick-lru": "^5.2.0", "@tailwindcss/node": "4.3.0", "@tailwindcss/oxide": "4.3.0", "postcss": "^8.5.10", "tailwindcss": "4.3.0" } }, ""],
|
||||
|
||||
"@tanstack/query-core": ["@tanstack/query-core@5.100.11", "", {}, "sha512-lmE0994apShXPj8CUxgx4ch5yUJhE9k/+tVwihBvPOyerACWdBocfFg24t8+0RhtlTd7tEgchDkhlCxNssvDxw=="],
|
||||
|
||||
"@types/estree": ["@types/estree@1.0.9", "", {}, ""],
|
||||
|
||||
"@types/json-schema": ["@types/json-schema@7.0.15", "", {}, ""],
|
||||
|
||||
"@types/json5": ["@types/json5@0.0.29", "", {}, ""],
|
||||
|
||||
"@types/node": ["@types/node@20.19.41", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ=="],
|
||||
|
||||
"@types/pg": ["@types/pg@8.20.0", "", { "dependencies": { "@types/node": "*", "pg-protocol": "*", "pg-types": "^2.2.0" } }, "sha512-bEPFOaMAHTEP1EzpvHTbmwR8UsFyHSKsRisLIHVMXnpNefSbGA1bD6CVy+qKjGSqmZqNqBDV2azOBo8TgkcVow=="],
|
||||
|
||||
"@types/react": ["@types/react@19.2.15", "", { "dependencies": { "csstype": "^3.2.2" } }, ""],
|
||||
|
||||
"@types/react-dom": ["@types/react-dom@19.2.3", "", { "peerDependencies": { "@types/react": "^19.2.0" } }, ""],
|
||||
|
||||
"@typescript-eslint/eslint-plugin": ["@typescript-eslint/eslint-plugin@8.59.4", "", { "dependencies": { "@eslint-community/regexpp": "^4.12.2", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/type-utils": "8.59.4", "@typescript-eslint/utils": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "@typescript-eslint/parser": "^8.59.4", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, ""],
|
||||
|
||||
"@typescript-eslint/parser": ["@typescript-eslint/parser@8.59.4", "", { "dependencies": { "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, ""],
|
||||
|
||||
"@typescript-eslint/project-service": ["@typescript-eslint/project-service@8.59.4", "", { "dependencies": { "@typescript-eslint/tsconfig-utils": "^8.59.4", "@typescript-eslint/types": "^8.59.4", "debug": "^4.4.3" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, ""],
|
||||
|
||||
"@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.59.4", "", { "dependencies": { "@typescript-eslint/types": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4" } }, ""],
|
||||
|
||||
"@typescript-eslint/tsconfig-utils": ["@typescript-eslint/tsconfig-utils@8.59.4", "", { "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, ""],
|
||||
|
||||
"@typescript-eslint/type-utils": ["@typescript-eslint/type-utils@8.59.4", "", { "dependencies": { "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/utils": "8.59.4", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, ""],
|
||||
|
||||
"@typescript-eslint/types": ["@typescript-eslint/types@8.59.4", "", {}, ""],
|
||||
|
||||
"@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.59.4", "", { "dependencies": { "@typescript-eslint/project-service": "8.59.4", "@typescript-eslint/tsconfig-utils": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/visitor-keys": "8.59.4", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", "tinyglobby": "^0.2.15", "ts-api-utils": "^2.5.0" }, "peerDependencies": { "typescript": ">=4.8.4 <6.1.0" } }, ""],
|
||||
|
||||
"@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.4", "@typescript-eslint/types": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, ""],
|
||||
|
||||
"@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.59.4", "", { "dependencies": { "@typescript-eslint/types": "8.59.4", "eslint-visitor-keys": "^5.0.0" } }, ""],
|
||||
|
||||
"@unrs/resolver-binding-darwin-arm64": ["@unrs/resolver-binding-darwin-arm64@1.12.2", "", { "os": "darwin", "cpu": "arm64" }, ""],
|
||||
|
||||
"acorn": ["acorn@8.16.0", "", { "bin": "bin/acorn" }, ""],
|
||||
|
||||
"acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, ""],
|
||||
|
||||
"ajv": ["ajv@6.15.0", "", { "dependencies": { "fast-deep-equal": "^3.1.1", "fast-json-stable-stringify": "^2.0.0", "json-schema-traverse": "^0.4.1", "uri-js": "^4.2.2" } }, ""],
|
||||
|
||||
"ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, ""],
|
||||
|
||||
"argparse": ["argparse@2.0.1", "", {}, ""],
|
||||
|
||||
"aria-query": ["aria-query@5.3.2", "", {}, ""],
|
||||
|
||||
"array-buffer-byte-length": ["array-buffer-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "is-array-buffer": "^3.0.5" } }, ""],
|
||||
|
||||
"array-includes": ["array-includes@3.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.0", "es-object-atoms": "^1.1.1", "get-intrinsic": "^1.3.0", "is-string": "^1.1.1", "math-intrinsics": "^1.1.0" } }, ""],
|
||||
|
||||
"array.prototype.findlast": ["array.prototype.findlast@1.2.5", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "es-shim-unscopables": "^1.0.2" } }, ""],
|
||||
|
||||
"array.prototype.findlastindex": ["array.prototype.findlastindex@1.2.6", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-shim-unscopables": "^1.1.0" } }, ""],
|
||||
|
||||
"array.prototype.flat": ["array.prototype.flat@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, ""],
|
||||
|
||||
"array.prototype.flatmap": ["array.prototype.flatmap@1.3.3", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-shim-unscopables": "^1.0.2" } }, ""],
|
||||
|
||||
"array.prototype.tosorted": ["array.prototype.tosorted@1.1.4", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3", "es-errors": "^1.3.0", "es-shim-unscopables": "^1.0.2" } }, ""],
|
||||
|
||||
"arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, ""],
|
||||
|
||||
"ast-types-flow": ["ast-types-flow@0.0.8", "", {}, ""],
|
||||
|
||||
"async-function": ["async-function@1.0.0", "", {}, ""],
|
||||
|
||||
"available-typed-arrays": ["available-typed-arrays@1.0.7", "", { "dependencies": { "possible-typed-array-names": "^1.0.0" } }, ""],
|
||||
|
||||
"aws-ssl-profiles": ["aws-ssl-profiles@1.1.2", "", {}, "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g=="],
|
||||
|
||||
"axe-core": ["axe-core@4.11.4", "", {}, ""],
|
||||
|
||||
"axobject-query": ["axobject-query@4.1.0", "", {}, ""],
|
||||
|
||||
"balanced-match": ["balanced-match@1.0.2", "", {}, ""],
|
||||
|
||||
"baseline-browser-mapping": ["baseline-browser-mapping@2.10.31", "", { "bin": "dist/cli.cjs" }, ""],
|
||||
|
||||
"better-result": ["better-result@2.9.2", "", {}, "sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q=="],
|
||||
|
||||
"brace-expansion": ["brace-expansion@1.1.14", "", { "dependencies": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" } }, ""],
|
||||
|
||||
"braces": ["braces@3.0.3", "", { "dependencies": { "fill-range": "^7.1.1" } }, ""],
|
||||
|
||||
"browserslist": ["browserslist@4.28.2", "", { "dependencies": { "baseline-browser-mapping": "^2.10.12", "caniuse-lite": "^1.0.30001782", "electron-to-chromium": "^1.5.328", "node-releases": "^2.0.36", "update-browserslist-db": "^1.2.3" }, "bin": "cli.js" }, ""],
|
||||
|
||||
"c12": ["c12@3.3.4", "", { "dependencies": { "chokidar": "^5.0.0", "confbox": "^0.2.4", "defu": "^6.1.6", "dotenv": "^17.3.1", "exsolve": "^1.0.8", "giget": "^3.2.0", "jiti": "^2.6.1", "ohash": "^2.0.11", "pathe": "^2.0.3", "perfect-debounce": "^2.1.0", "pkg-types": "^2.3.0", "rc9": "^3.0.1" }, "peerDependencies": { "magicast": "*" }, "optionalPeers": ["magicast"] }, "sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA=="],
|
||||
|
||||
"call-bind": ["call-bind@1.0.9", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "get-intrinsic": "^1.3.0", "set-function-length": "^1.2.2" } }, ""],
|
||||
|
||||
"call-bind-apply-helpers": ["call-bind-apply-helpers@1.0.2", "", { "dependencies": { "es-errors": "^1.3.0", "function-bind": "^1.1.2" } }, ""],
|
||||
|
||||
"call-bound": ["call-bound@1.0.4", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "get-intrinsic": "^1.3.0" } }, ""],
|
||||
|
||||
"callsites": ["callsites@3.1.0", "", {}, ""],
|
||||
|
||||
"caniuse-lite": ["caniuse-lite@1.0.30001793", "", {}, ""],
|
||||
|
||||
"chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, ""],
|
||||
|
||||
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
|
||||
|
||||
"chokidar": ["chokidar@5.0.0", "", { "dependencies": { "readdirp": "^5.0.0" } }, "sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw=="],
|
||||
|
||||
"client-only": ["client-only@0.0.1", "", {}, ""],
|
||||
|
||||
"color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, ""],
|
||||
|
||||
"color-name": ["color-name@1.1.4", "", {}, ""],
|
||||
|
||||
"concat-map": ["concat-map@0.0.1", "", {}, ""],
|
||||
|
||||
"confbox": ["confbox@0.2.4", "", {}, "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ=="],
|
||||
|
||||
"convert-source-map": ["convert-source-map@2.0.0", "", {}, ""],
|
||||
|
||||
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, ""],
|
||||
|
||||
"csstype": ["csstype@3.2.3", "", {}, ""],
|
||||
|
||||
"damerau-levenshtein": ["damerau-levenshtein@1.0.8", "", {}, ""],
|
||||
|
||||
"data-view-buffer": ["data-view-buffer@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, ""],
|
||||
|
||||
"data-view-byte-length": ["data-view-byte-length@1.0.2", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-data-view": "^1.0.2" } }, ""],
|
||||
|
||||
"data-view-byte-offset": ["data-view-byte-offset@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-data-view": "^1.0.1" } }, ""],
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, ""],
|
||||
|
||||
"deep-is": ["deep-is@0.1.4", "", {}, ""],
|
||||
|
||||
"deepmerge-ts": ["deepmerge-ts@7.1.5", "", {}, "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw=="],
|
||||
|
||||
"define-data-property": ["define-data-property@1.1.4", "", { "dependencies": { "es-define-property": "^1.0.0", "es-errors": "^1.3.0", "gopd": "^1.0.1" } }, ""],
|
||||
|
||||
"define-properties": ["define-properties@1.2.1", "", { "dependencies": { "define-data-property": "^1.0.1", "has-property-descriptors": "^1.0.0", "object-keys": "^1.1.1" } }, ""],
|
||||
|
||||
"defu": ["defu@6.1.7", "", {}, "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ=="],
|
||||
|
||||
"denque": ["denque@2.1.0", "", {}, "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw=="],
|
||||
|
||||
"dequal": ["dequal@2.0.3", "", {}, "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="],
|
||||
|
||||
"destr": ["destr@2.0.5", "", {}, "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, ""],
|
||||
|
||||
"doctrine": ["doctrine@2.1.0", "", { "dependencies": { "esutils": "^2.0.2" } }, ""],
|
||||
|
||||
"dotenv": ["dotenv@17.4.2", "", {}, "sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw=="],
|
||||
|
||||
"dunder-proto": ["dunder-proto@1.0.1", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.1", "es-errors": "^1.3.0", "gopd": "^1.2.0" } }, ""],
|
||||
|
||||
"effect": ["effect@3.20.0", "", { "dependencies": { "@standard-schema/spec": "^1.0.0", "fast-check": "^3.23.1" } }, "sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw=="],
|
||||
|
||||
"electron-to-chromium": ["electron-to-chromium@1.5.360", "", {}, ""],
|
||||
|
||||
"emoji-regex": ["emoji-regex@9.2.2", "", {}, ""],
|
||||
|
||||
"empathic": ["empathic@2.0.0", "", {}, "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA=="],
|
||||
|
||||
"enhanced-resolve": ["enhanced-resolve@5.21.5", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.3.3" } }, ""],
|
||||
|
||||
"env-paths": ["env-paths@3.0.0", "", {}, "sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A=="],
|
||||
|
||||
"es-abstract": ["es-abstract@1.24.2", "", { "dependencies": { "array-buffer-byte-length": "^1.0.2", "arraybuffer.prototype.slice": "^1.0.4", "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "data-view-buffer": "^1.0.2", "data-view-byte-length": "^1.0.2", "data-view-byte-offset": "^1.0.1", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "es-set-tostringtag": "^2.1.0", "es-to-primitive": "^1.3.0", "function.prototype.name": "^1.1.8", "get-intrinsic": "^1.3.0", "get-proto": "^1.0.1", "get-symbol-description": "^1.1.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "internal-slot": "^1.1.0", "is-array-buffer": "^3.0.5", "is-callable": "^1.2.7", "is-data-view": "^1.0.2", "is-negative-zero": "^2.0.3", "is-regex": "^1.2.1", "is-set": "^2.0.3", "is-shared-array-buffer": "^1.0.4", "is-string": "^1.1.1", "is-typed-array": "^1.1.15", "is-weakref": "^1.1.1", "math-intrinsics": "^1.1.0", "object-inspect": "^1.13.4", "object-keys": "^1.1.1", "object.assign": "^4.1.7", "own-keys": "^1.0.1", "regexp.prototype.flags": "^1.5.4", "safe-array-concat": "^1.1.3", "safe-push-apply": "^1.0.0", "safe-regex-test": "^1.1.0", "set-proto": "^1.0.0", "stop-iteration-iterator": "^1.1.0", "string.prototype.trim": "^1.2.10", "string.prototype.trimend": "^1.0.9", "string.prototype.trimstart": "^1.0.8", "typed-array-buffer": "^1.0.3", "typed-array-byte-length": "^1.0.3", "typed-array-byte-offset": "^1.0.4", "typed-array-length": "^1.0.7", "unbox-primitive": "^1.1.0", "which-typed-array": "^1.1.19" } }, ""],
|
||||
|
||||
"es-define-property": ["es-define-property@1.0.1", "", {}, ""],
|
||||
|
||||
"es-errors": ["es-errors@1.3.0", "", {}, ""],
|
||||
|
||||
"es-iterator-helpers": ["es-iterator-helpers@1.3.2", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-abstract": "^1.24.2", "es-errors": "^1.3.0", "es-set-tostringtag": "^2.1.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.3.0", "globalthis": "^1.0.4", "gopd": "^1.2.0", "has-property-descriptors": "^1.0.2", "has-proto": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "iterator.prototype": "^1.1.5", "math-intrinsics": "^1.1.0" } }, ""],
|
||||
|
||||
"es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, ""],
|
||||
|
||||
"es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, ""],
|
||||
|
||||
"es-shim-unscopables": ["es-shim-unscopables@1.1.0", "", { "dependencies": { "hasown": "^2.0.2" } }, ""],
|
||||
|
||||
"es-to-primitive": ["es-to-primitive@1.3.0", "", { "dependencies": { "is-callable": "^1.2.7", "is-date-object": "^1.0.5", "is-symbol": "^1.0.4" } }, ""],
|
||||
|
||||
"escalade": ["escalade@3.2.0", "", {}, ""],
|
||||
|
||||
"escape-string-regexp": ["escape-string-regexp@4.0.0", "", {}, ""],
|
||||
|
||||
"eslint": ["eslint@9.39.4", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.1", "@eslint/config-array": "^0.21.2", "@eslint/config-helpers": "^0.4.2", "@eslint/core": "^0.17.0", "@eslint/eslintrc": "^3.3.5", "@eslint/js": "9.39.4", "@eslint/plugin-kit": "^0.4.1", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", "@types/estree": "^1.0.6", "ajv": "^6.14.0", "chalk": "^4.0.0", "cross-spawn": "^7.0.6", "debug": "^4.3.2", "escape-string-regexp": "^4.0.0", "eslint-scope": "^8.4.0", "eslint-visitor-keys": "^4.2.1", "espree": "^10.4.0", "esquery": "^1.5.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", "file-entry-cache": "^8.0.0", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", "lodash.merge": "^4.6.2", "minimatch": "^3.1.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, "peerDependencies": { "jiti": "*" }, "bin": "bin/eslint.js" }, ""],
|
||||
|
||||
"eslint-config-next": ["eslint-config-next@16.2.6", "", { "dependencies": { "@next/eslint-plugin-next": "16.2.6", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.32.0", "eslint-plugin-jsx-a11y": "^6.10.0", "eslint-plugin-react": "^7.37.0", "eslint-plugin-react-hooks": "^7.0.0", "globals": "16.4.0", "typescript-eslint": "^8.46.0" }, "peerDependencies": { "eslint": ">=9.0.0", "typescript": ">=3.3.1" } }, ""],
|
||||
|
||||
"eslint-import-resolver-node": ["eslint-import-resolver-node@0.3.10", "", { "dependencies": { "debug": "^3.2.7", "is-core-module": "^2.16.1", "resolve": "^2.0.0-next.6" } }, ""],
|
||||
|
||||
"eslint-import-resolver-typescript": ["eslint-import-resolver-typescript@3.10.1", "", { "dependencies": { "@nolyfill/is-core-module": "1.0.39", "debug": "^4.4.0", "get-tsconfig": "^4.10.0", "is-bun-module": "^2.0.0", "stable-hash": "^0.0.5", "tinyglobby": "^0.2.13", "unrs-resolver": "^1.6.2" }, "peerDependencies": { "eslint": "*", "eslint-plugin-import": "*", "eslint-plugin-import-x": "*" }, "optionalPeers": ["eslint-plugin-import-x"] }, ""],
|
||||
|
||||
"eslint-module-utils": ["eslint-module-utils@2.12.1", "", { "dependencies": { "debug": "^3.2.7" } }, ""],
|
||||
|
||||
"eslint-plugin-import": ["eslint-plugin-import@2.32.0", "", { "dependencies": { "@rtsao/scc": "^1.1.0", "array-includes": "^3.1.9", "array.prototype.findlastindex": "^1.2.6", "array.prototype.flat": "^1.3.3", "array.prototype.flatmap": "^1.3.3", "debug": "^3.2.7", "doctrine": "^2.1.0", "eslint-import-resolver-node": "^0.3.9", "eslint-module-utils": "^2.12.1", "hasown": "^2.0.2", "is-core-module": "^2.16.1", "is-glob": "^4.0.3", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "object.groupby": "^1.0.3", "object.values": "^1.2.1", "semver": "^6.3.1", "string.prototype.trimend": "^1.0.9", "tsconfig-paths": "^3.15.0" }, "peerDependencies": { "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8 || ^9" } }, ""],
|
||||
|
||||
"eslint-plugin-jsx-a11y": ["eslint-plugin-jsx-a11y@6.10.2", "", { "dependencies": { "aria-query": "^5.3.2", "array-includes": "^3.1.8", "array.prototype.flatmap": "^1.3.2", "ast-types-flow": "^0.0.8", "axe-core": "^4.10.0", "axobject-query": "^4.1.0", "damerau-levenshtein": "^1.0.8", "emoji-regex": "^9.2.2", "hasown": "^2.0.2", "jsx-ast-utils": "^3.3.5", "language-tags": "^1.0.9", "minimatch": "^3.1.2", "object.fromentries": "^2.0.8", "safe-regex-test": "^1.0.3", "string.prototype.includes": "^2.0.1" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9" } }, ""],
|
||||
|
||||
"eslint-plugin-react": ["eslint-plugin-react@7.37.5", "", { "dependencies": { "array-includes": "^3.1.8", "array.prototype.findlast": "^1.2.5", "array.prototype.flatmap": "^1.3.3", "array.prototype.tosorted": "^1.1.4", "doctrine": "^2.1.0", "es-iterator-helpers": "^1.2.1", "estraverse": "^5.3.0", "hasown": "^2.0.2", "jsx-ast-utils": "^2.4.1 || ^3.0.0", "minimatch": "^3.1.2", "object.entries": "^1.1.9", "object.fromentries": "^2.0.8", "object.values": "^1.2.1", "prop-types": "^15.8.1", "resolve": "^2.0.0-next.5", "semver": "^6.3.1", "string.prototype.matchall": "^4.0.12", "string.prototype.repeat": "^1.0.0" }, "peerDependencies": { "eslint": "^3 || ^4 || ^5 || ^6 || ^7 || ^8 || ^9.7" } }, ""],
|
||||
|
||||
"eslint-plugin-react-hooks": ["eslint-plugin-react-hooks@7.1.1", "", { "dependencies": { "@babel/core": "^7.24.4", "@babel/parser": "^7.24.4", "hermes-parser": "^0.25.1", "zod": "^3.25.0 || ^4.0.0", "zod-validation-error": "^3.5.0 || ^4.0.0" }, "peerDependencies": { "eslint": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-0 || ^9.0.0 || ^10.0.0" } }, ""],
|
||||
|
||||
"eslint-scope": ["eslint-scope@8.4.0", "", { "dependencies": { "esrecurse": "^4.3.0", "estraverse": "^5.2.0" } }, ""],
|
||||
|
||||
"eslint-visitor-keys": ["eslint-visitor-keys@4.2.1", "", {}, ""],
|
||||
|
||||
"espree": ["espree@10.4.0", "", { "dependencies": { "acorn": "^8.15.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^4.2.1" } }, ""],
|
||||
|
||||
"esquery": ["esquery@1.7.0", "", { "dependencies": { "estraverse": "^5.1.0" } }, ""],
|
||||
|
||||
"esrecurse": ["esrecurse@4.3.0", "", { "dependencies": { "estraverse": "^5.2.0" } }, ""],
|
||||
|
||||
"estraverse": ["estraverse@5.3.0", "", {}, ""],
|
||||
|
||||
"esutils": ["esutils@2.0.3", "", {}, ""],
|
||||
|
||||
"exsolve": ["exsolve@1.0.8", "", {}, "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA=="],
|
||||
|
||||
"fast-check": ["fast-check@3.23.2", "", { "dependencies": { "pure-rand": "^6.1.0" } }, "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A=="],
|
||||
|
||||
"fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, ""],
|
||||
|
||||
"fast-glob": ["fast-glob@3.3.1", "", { "dependencies": { "@nodelib/fs.stat": "^2.0.2", "@nodelib/fs.walk": "^1.2.3", "glob-parent": "^5.1.2", "merge2": "^1.3.0", "micromatch": "^4.0.4" } }, ""],
|
||||
|
||||
"fast-json-stable-stringify": ["fast-json-stable-stringify@2.1.0", "", {}, ""],
|
||||
|
||||
"fast-levenshtein": ["fast-levenshtein@2.0.6", "", {}, ""],
|
||||
|
||||
"fast-sha256": ["fast-sha256@1.3.0", "", {}, "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ=="],
|
||||
|
||||
"fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="],
|
||||
|
||||
"fastq": ["fastq@1.20.1", "", { "dependencies": { "reusify": "^1.0.4" } }, ""],
|
||||
|
||||
"fdir": ["fdir@6.5.0", "", { "peerDependencies": { "picomatch": "^3 || ^4" } }, ""],
|
||||
|
||||
"file-entry-cache": ["file-entry-cache@8.0.0", "", { "dependencies": { "flat-cache": "^4.0.0" } }, ""],
|
||||
|
||||
"fill-range": ["fill-range@7.1.1", "", { "dependencies": { "to-regex-range": "^5.0.1" } }, ""],
|
||||
|
||||
"find-up": ["find-up@5.0.0", "", { "dependencies": { "locate-path": "^6.0.0", "path-exists": "^4.0.0" } }, ""],
|
||||
|
||||
"flat-cache": ["flat-cache@4.0.1", "", { "dependencies": { "flatted": "^3.2.9", "keyv": "^4.5.4" } }, ""],
|
||||
|
||||
"flatted": ["flatted@3.4.2", "", {}, ""],
|
||||
|
||||
"for-each": ["for-each@0.3.5", "", { "dependencies": { "is-callable": "^1.2.7" } }, ""],
|
||||
|
||||
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
|
||||
|
||||
"function-bind": ["function-bind@1.1.2", "", {}, ""],
|
||||
|
||||
"function.prototype.name": ["function.prototype.name@1.1.8", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "functions-have-names": "^1.2.3", "hasown": "^2.0.2", "is-callable": "^1.2.7" } }, ""],
|
||||
|
||||
"functions-have-names": ["functions-have-names@1.2.3", "", {}, ""],
|
||||
|
||||
"generate-function": ["generate-function@2.3.1", "", { "dependencies": { "is-property": "^1.0.2" } }, "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ=="],
|
||||
|
||||
"generator-function": ["generator-function@2.0.1", "", {}, ""],
|
||||
|
||||
"gensync": ["gensync@1.0.0-beta.2", "", {}, ""],
|
||||
|
||||
"get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, ""],
|
||||
|
||||
"get-port-please": ["get-port-please@3.2.0", "", {}, "sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A=="],
|
||||
|
||||
"get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, ""],
|
||||
|
||||
"get-symbol-description": ["get-symbol-description@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6" } }, ""],
|
||||
|
||||
"get-tsconfig": ["get-tsconfig@4.14.0", "", { "dependencies": { "resolve-pkg-maps": "^1.0.0" } }, ""],
|
||||
|
||||
"giget": ["giget@3.2.0", "", { "bin": "dist/cli.mjs" }, "sha512-GvHTWcykIR/fP8cj8dMpuMMkvaeJfPvYnhq0oW+chSeIr+ldX21ifU2Ms6KBoyKZQZmVaUAAhQ2EZ68KJF8a7A=="],
|
||||
|
||||
"glob-parent": ["glob-parent@6.0.2", "", { "dependencies": { "is-glob": "^4.0.3" } }, ""],
|
||||
|
||||
"glob-to-regexp": ["glob-to-regexp@0.4.1", "", {}, "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw=="],
|
||||
|
||||
"globals": ["globals@16.4.0", "", {}, ""],
|
||||
|
||||
"globalthis": ["globalthis@1.0.4", "", { "dependencies": { "define-properties": "^1.2.1", "gopd": "^1.0.1" } }, ""],
|
||||
|
||||
"gopd": ["gopd@1.2.0", "", {}, ""],
|
||||
|
||||
"graceful-fs": ["graceful-fs@4.2.11", "", {}, ""],
|
||||
|
||||
"grammex": ["grammex@3.1.12", "", {}, "sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ=="],
|
||||
|
||||
"graphmatch": ["graphmatch@1.1.1", "", {}, "sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg=="],
|
||||
|
||||
"has-bigints": ["has-bigints@1.1.0", "", {}, ""],
|
||||
|
||||
"has-flag": ["has-flag@4.0.0", "", {}, ""],
|
||||
|
||||
"has-property-descriptors": ["has-property-descriptors@1.0.2", "", { "dependencies": { "es-define-property": "^1.0.0" } }, ""],
|
||||
|
||||
"has-proto": ["has-proto@1.2.0", "", { "dependencies": { "dunder-proto": "^1.0.0" } }, ""],
|
||||
|
||||
"has-symbols": ["has-symbols@1.1.0", "", {}, ""],
|
||||
|
||||
"has-tostringtag": ["has-tostringtag@1.0.2", "", { "dependencies": { "has-symbols": "^1.0.3" } }, ""],
|
||||
|
||||
"hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, ""],
|
||||
|
||||
"hermes-estree": ["hermes-estree@0.25.1", "", {}, ""],
|
||||
|
||||
"hermes-parser": ["hermes-parser@0.25.1", "", { "dependencies": { "hermes-estree": "0.25.1" } }, ""],
|
||||
|
||||
"hono": ["hono@4.12.21", "", {}, "sha512-uV63apnb0kyPtAUwoWgaGh9HyIFcv8lgmzPZSiTBQAFOFGIzka5EZ1dZocmGnn0XdX0+XTqJ6Tqv7selMuGLRQ=="],
|
||||
|
||||
"http-status-codes": ["http-status-codes@2.3.0", "", {}, "sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA=="],
|
||||
|
||||
"iconv-lite": ["iconv-lite@0.7.2", "", { "dependencies": { "safer-buffer": ">= 2.1.2 < 3.0.0" } }, "sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw=="],
|
||||
|
||||
"ignore": ["ignore@5.3.2", "", {}, ""],
|
||||
|
||||
"import-fresh": ["import-fresh@3.3.1", "", { "dependencies": { "parent-module": "^1.0.0", "resolve-from": "^4.0.0" } }, ""],
|
||||
|
||||
"imurmurhash": ["imurmurhash@0.1.4", "", {}, ""],
|
||||
|
||||
"internal-slot": ["internal-slot@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "hasown": "^2.0.2", "side-channel": "^1.1.0" } }, ""],
|
||||
|
||||
"is-array-buffer": ["is-array-buffer@3.0.5", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, ""],
|
||||
|
||||
"is-async-function": ["is-async-function@2.1.1", "", { "dependencies": { "async-function": "^1.0.0", "call-bound": "^1.0.3", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, ""],
|
||||
|
||||
"is-bigint": ["is-bigint@1.1.0", "", { "dependencies": { "has-bigints": "^1.0.2" } }, ""],
|
||||
|
||||
"is-boolean-object": ["is-boolean-object@1.2.2", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, ""],
|
||||
|
||||
"is-bun-module": ["is-bun-module@2.0.0", "", { "dependencies": { "semver": "^7.7.1" } }, ""],
|
||||
|
||||
"is-callable": ["is-callable@1.2.7", "", {}, ""],
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, ""],
|
||||
|
||||
"is-data-view": ["is-data-view@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "get-intrinsic": "^1.2.6", "is-typed-array": "^1.1.13" } }, ""],
|
||||
|
||||
"is-date-object": ["is-date-object@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "has-tostringtag": "^1.0.2" } }, ""],
|
||||
|
||||
"is-extglob": ["is-extglob@2.1.1", "", {}, ""],
|
||||
|
||||
"is-finalizationregistry": ["is-finalizationregistry@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, ""],
|
||||
|
||||
"is-generator-function": ["is-generator-function@1.1.2", "", { "dependencies": { "call-bound": "^1.0.4", "generator-function": "^2.0.0", "get-proto": "^1.0.1", "has-tostringtag": "^1.0.2", "safe-regex-test": "^1.1.0" } }, ""],
|
||||
|
||||
"is-glob": ["is-glob@4.0.3", "", { "dependencies": { "is-extglob": "^2.1.1" } }, ""],
|
||||
|
||||
"is-map": ["is-map@2.0.3", "", {}, ""],
|
||||
|
||||
"is-negative-zero": ["is-negative-zero@2.0.3", "", {}, ""],
|
||||
|
||||
"is-number": ["is-number@7.0.0", "", {}, ""],
|
||||
|
||||
"is-number-object": ["is-number-object@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, ""],
|
||||
|
||||
"is-property": ["is-property@1.0.2", "", {}, "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="],
|
||||
|
||||
"is-regex": ["is-regex@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, ""],
|
||||
|
||||
"is-set": ["is-set@2.0.3", "", {}, ""],
|
||||
|
||||
"is-shared-array-buffer": ["is-shared-array-buffer@1.0.4", "", { "dependencies": { "call-bound": "^1.0.3" } }, ""],
|
||||
|
||||
"is-string": ["is-string@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3", "has-tostringtag": "^1.0.2" } }, ""],
|
||||
|
||||
"is-symbol": ["is-symbol@1.1.1", "", { "dependencies": { "call-bound": "^1.0.2", "has-symbols": "^1.1.0", "safe-regex-test": "^1.1.0" } }, ""],
|
||||
|
||||
"is-typed-array": ["is-typed-array@1.1.15", "", { "dependencies": { "which-typed-array": "^1.1.16" } }, ""],
|
||||
|
||||
"is-weakmap": ["is-weakmap@2.0.2", "", {}, ""],
|
||||
|
||||
"is-weakref": ["is-weakref@1.1.1", "", { "dependencies": { "call-bound": "^1.0.3" } }, ""],
|
||||
|
||||
"is-weakset": ["is-weakset@2.0.4", "", { "dependencies": { "call-bound": "^1.0.3", "get-intrinsic": "^1.2.6" } }, ""],
|
||||
|
||||
"isarray": ["isarray@2.0.5", "", {}, ""],
|
||||
|
||||
"isexe": ["isexe@2.0.0", "", {}, ""],
|
||||
|
||||
"iterator.prototype": ["iterator.prototype@1.1.5", "", { "dependencies": { "define-data-property": "^1.1.4", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "get-proto": "^1.0.0", "has-symbols": "^1.1.0", "set-function-name": "^2.0.2" } }, ""],
|
||||
|
||||
"jiti": ["jiti@2.7.0", "", { "bin": "lib/jiti-cli.mjs" }, ""],
|
||||
|
||||
"js-cookie": ["js-cookie@3.0.5", "", {}, "sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw=="],
|
||||
|
||||
"js-tokens": ["js-tokens@4.0.0", "", {}, ""],
|
||||
|
||||
"js-yaml": ["js-yaml@4.1.1", "", { "dependencies": { "argparse": "^2.0.1" }, "bin": "bin/js-yaml.js" }, ""],
|
||||
|
||||
"jsesc": ["jsesc@3.1.0", "", { "bin": "bin/jsesc" }, ""],
|
||||
|
||||
"json-buffer": ["json-buffer@3.0.1", "", {}, ""],
|
||||
|
||||
"json-schema-traverse": ["json-schema-traverse@0.4.1", "", {}, ""],
|
||||
|
||||
"json-stable-stringify-without-jsonify": ["json-stable-stringify-without-jsonify@1.0.1", "", {}, ""],
|
||||
|
||||
"json5": ["json5@1.0.2", "", { "dependencies": { "minimist": "^1.2.0" }, "bin": "lib/cli.js" }, ""],
|
||||
|
||||
"jsx-ast-utils": ["jsx-ast-utils@3.3.5", "", { "dependencies": { "array-includes": "^3.1.6", "array.prototype.flat": "^1.3.1", "object.assign": "^4.1.4", "object.values": "^1.1.6" } }, ""],
|
||||
|
||||
"keyv": ["keyv@4.5.4", "", { "dependencies": { "json-buffer": "3.0.1" } }, ""],
|
||||
|
||||
"language-subtag-registry": ["language-subtag-registry@0.3.23", "", {}, ""],
|
||||
|
||||
"language-tags": ["language-tags@1.0.9", "", { "dependencies": { "language-subtag-registry": "^0.3.20" } }, ""],
|
||||
|
||||
"levn": ["levn@0.4.1", "", { "dependencies": { "prelude-ls": "^1.2.1", "type-check": "~0.4.0" } }, ""],
|
||||
|
||||
"lightningcss": ["lightningcss@1.32.0", "", { "dependencies": { "detect-libc": "^2.0.3" }, "optionalDependencies": { "lightningcss-darwin-arm64": "1.32.0" } }, ""],
|
||||
|
||||
"lightningcss-darwin-arm64": ["lightningcss-darwin-arm64@1.32.0", "", { "os": "darwin", "cpu": "arm64" }, ""],
|
||||
|
||||
"locate-path": ["locate-path@6.0.0", "", { "dependencies": { "p-locate": "^5.0.0" } }, ""],
|
||||
|
||||
"lodash.merge": ["lodash.merge@4.6.2", "", {}, ""],
|
||||
|
||||
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
|
||||
|
||||
"loose-envify": ["loose-envify@1.4.0", "", { "dependencies": { "js-tokens": "^3.0.0 || ^4.0.0" }, "bin": "cli.js" }, ""],
|
||||
|
||||
"lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, ""],
|
||||
|
||||
"lru.min": ["lru.min@1.1.4", "", {}, "sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA=="],
|
||||
|
||||
"magic-string": ["magic-string@0.30.21", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.5" } }, ""],
|
||||
|
||||
"math-intrinsics": ["math-intrinsics@1.1.0", "", {}, ""],
|
||||
|
||||
"merge2": ["merge2@1.4.1", "", {}, ""],
|
||||
|
||||
"micromatch": ["micromatch@4.0.8", "", { "dependencies": { "braces": "^3.0.3", "picomatch": "^2.3.1" } }, ""],
|
||||
|
||||
"minimatch": ["minimatch@3.1.5", "", { "dependencies": { "brace-expansion": "^1.1.7" } }, ""],
|
||||
|
||||
"minimist": ["minimist@1.2.8", "", {}, ""],
|
||||
|
||||
"ms": ["ms@2.1.3", "", {}, ""],
|
||||
|
||||
"mysql2": ["mysql2@3.15.3", "", { "dependencies": { "aws-ssl-profiles": "^1.1.1", "denque": "^2.1.0", "generate-function": "^2.3.1", "iconv-lite": "^0.7.0", "long": "^5.2.1", "lru.min": "^1.0.0", "named-placeholders": "^1.1.3", "seq-queue": "^0.0.5", "sqlstring": "^2.3.2" } }, "sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg=="],
|
||||
|
||||
"named-placeholders": ["named-placeholders@1.1.6", "", { "dependencies": { "lru.min": "^1.1.0" } }, "sha512-Tz09sEL2EEuv5fFowm419c1+a/jSMiBjI9gHxVLrVdbUkkNUUfjsVYs9pVZu5oCon/kmRh9TfLEObFtkVxmY0w=="],
|
||||
|
||||
"nanoid": ["nanoid@3.3.12", "", { "bin": "bin/nanoid.cjs" }, ""],
|
||||
|
||||
"napi-postinstall": ["napi-postinstall@0.3.4", "", { "bin": "lib/cli.js" }, ""],
|
||||
|
||||
"natural-compare": ["natural-compare@1.4.0", "", {}, ""],
|
||||
|
||||
"next": ["next@16.2.6", "", { "dependencies": { "@next/env": "16.2.6", "@swc/helpers": "0.5.15", "baseline-browser-mapping": "^2.9.19", "caniuse-lite": "^1.0.30001579", "postcss": "8.4.31", "styled-jsx": "5.1.6" }, "optionalDependencies": { "@next/swc-darwin-arm64": "16.2.6", "@next/swc-darwin-x64": "16.2.6", "@next/swc-linux-arm64-gnu": "16.2.6", "@next/swc-linux-arm64-musl": "16.2.6", "@next/swc-linux-x64-gnu": "16.2.6", "@next/swc-linux-x64-musl": "16.2.6", "@next/swc-win32-arm64-msvc": "16.2.6", "@next/swc-win32-x64-msvc": "16.2.6", "sharp": "^0.34.5" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.51.1", "babel-plugin-react-compiler": "*", "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "optionalPeers": ["@opentelemetry/api", "@playwright/test", "babel-plugin-react-compiler", "sass"], "bin": "dist/bin/next" }, ""],
|
||||
|
||||
"node-exports-info": ["node-exports-info@1.6.0", "", { "dependencies": { "array.prototype.flatmap": "^1.3.3", "es-errors": "^1.3.0", "object.entries": "^1.1.9", "semver": "^6.3.1" } }, ""],
|
||||
|
||||
"node-releases": ["node-releases@2.0.44", "", {}, ""],
|
||||
|
||||
"object-assign": ["object-assign@4.1.1", "", {}, ""],
|
||||
|
||||
"object-inspect": ["object-inspect@1.13.4", "", {}, ""],
|
||||
|
||||
"object-keys": ["object-keys@1.1.1", "", {}, ""],
|
||||
|
||||
"object.assign": ["object.assign@4.1.7", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0", "has-symbols": "^1.1.0", "object-keys": "^1.1.1" } }, ""],
|
||||
|
||||
"object.entries": ["object.entries@1.1.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.4", "define-properties": "^1.2.1", "es-object-atoms": "^1.1.1" } }, ""],
|
||||
|
||||
"object.fromentries": ["object.fromentries@2.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2", "es-object-atoms": "^1.0.0" } }, ""],
|
||||
|
||||
"object.groupby": ["object.groupby@1.0.3", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.2" } }, ""],
|
||||
|
||||
"object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, ""],
|
||||
|
||||
"ohash": ["ohash@2.0.11", "", {}, "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ=="],
|
||||
|
||||
"openai": ["openai@6.38.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws"], "bin": "bin/cli" }, "sha512-AoMplt2UalrpgUDMh3L09QWjNRlgJPipclQvA6sYAaeF6nHNBMgmikAZGmcYLn8on4d9sQY9Q8bOLfrBS7Lc8g=="],
|
||||
|
||||
"optionator": ["optionator@0.9.4", "", { "dependencies": { "deep-is": "^0.1.3", "fast-levenshtein": "^2.0.6", "levn": "^0.4.1", "prelude-ls": "^1.2.1", "type-check": "^0.4.0", "word-wrap": "^1.2.5" } }, ""],
|
||||
|
||||
"own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, ""],
|
||||
|
||||
"p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, ""],
|
||||
|
||||
"p-locate": ["p-locate@5.0.0", "", { "dependencies": { "p-limit": "^3.0.2" } }, ""],
|
||||
|
||||
"parent-module": ["parent-module@1.0.1", "", { "dependencies": { "callsites": "^3.0.0" } }, ""],
|
||||
|
||||
"path-exists": ["path-exists@4.0.0", "", {}, ""],
|
||||
|
||||
"path-key": ["path-key@3.1.1", "", {}, ""],
|
||||
|
||||
"path-parse": ["path-parse@1.0.7", "", {}, ""],
|
||||
|
||||
"pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="],
|
||||
|
||||
"perfect-debounce": ["perfect-debounce@2.1.0", "", {}, "sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g=="],
|
||||
|
||||
"pg": ["pg@8.21.0", "", { "dependencies": { "pg-connection-string": "^2.13.0", "pg-pool": "^3.14.0", "pg-protocol": "^1.14.0", "pg-types": "2.2.0", "pgpass": "1.0.5" }, "optionalDependencies": { "pg-cloudflare": "^1.4.0" }, "peerDependencies": { "pg-native": ">=3.0.1" }, "optionalPeers": ["pg-native"] }, "sha512-AUP1EYJuHraQGsVoCQVIcM7TEJVGtDzxWtGFZd8rds9d+CCXlU5Js1rYgfLNvxy9iJrpHjGrRjoi/3BT9fRyiA=="],
|
||||
|
||||
"pg-cloudflare": ["pg-cloudflare@1.4.0", "", {}, "sha512-Vo7z/6rrQYxpNRylp4Tlob2elzbh+N/MOQbxFVWCxS7oEx6jF53GTJFxK2WWpKuBRkmiin4Mt+xofFDjx09R0A=="],
|
||||
|
||||
"pg-connection-string": ["pg-connection-string@2.13.0", "", {}, "sha512-EMnU9E2fSULdsbErBbMaXJvFeD9B4+nPcM3f+4lsiCR0BHLPrLVjv3DbyM2hgQQviKJaTWIRRTjKjWlHg3p2ig=="],
|
||||
|
||||
"pg-int8": ["pg-int8@1.0.1", "", {}, "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw=="],
|
||||
|
||||
"pg-pool": ["pg-pool@3.14.0", "", { "peerDependencies": { "pg": ">=8.0" } }, "sha512-gKtPkFdQPU3DksooVLi9LsjZxrsBUZIpa+7aVx+LV5pNh0KzP4Zleud2po+ConrxbuXGBJ6Hfer6hdgpIBpBaw=="],
|
||||
|
||||
"pg-protocol": ["pg-protocol@1.14.0", "", {}, "sha512-n5taZ1kO3s9ngDTVxsEznOqCyToTgz0FLuPq0B33COy5pPpuWJpY3/2oRBVETuOgzdqRXfWpM9HIhp2LBBT1BA=="],
|
||||
|
||||
"pg-types": ["pg-types@2.2.0", "", { "dependencies": { "pg-int8": "1.0.1", "postgres-array": "~2.0.0", "postgres-bytea": "~1.0.0", "postgres-date": "~1.0.4", "postgres-interval": "^1.1.0" } }, "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA=="],
|
||||
|
||||
"pgpass": ["pgpass@1.0.5", "", { "dependencies": { "split2": "^4.1.0" } }, "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug=="],
|
||||
|
||||
"picocolors": ["picocolors@1.1.1", "", {}, ""],
|
||||
|
||||
"picomatch": ["picomatch@4.0.4", "", {}, ""],
|
||||
|
||||
"pkg-types": ["pkg-types@2.3.1", "", { "dependencies": { "confbox": "^0.2.4", "exsolve": "^1.0.8", "pathe": "^2.0.3" } }, "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg=="],
|
||||
|
||||
"possible-typed-array-names": ["possible-typed-array-names@1.1.0", "", {}, ""],
|
||||
|
||||
"postcss": ["postcss@8.5.15", "", { "dependencies": { "nanoid": "^3.3.12", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" } }, ""],
|
||||
|
||||
"postgres": ["postgres@3.4.7", "", {}, "sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw=="],
|
||||
|
||||
"postgres-array": ["postgres-array@3.0.4", "", {}, "sha512-nAUSGfSDGOaOAEGwqsRY27GPOea7CNipJPOA7lPbdEpx5Kg3qzdP0AaWC5MlhTWV9s4hFX39nomVZ+C4tnGOJQ=="],
|
||||
|
||||
"postgres-bytea": ["postgres-bytea@1.0.1", "", {}, "sha512-5+5HqXnsZPE65IJZSMkZtURARZelel2oXUEO8rH83VS/hxH5vv1uHquPg5wZs8yMAfdv971IU+kcPUczi7NVBQ=="],
|
||||
|
||||
"postgres-date": ["postgres-date@1.0.7", "", {}, "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q=="],
|
||||
|
||||
"postgres-interval": ["postgres-interval@1.2.0", "", { "dependencies": { "xtend": "^4.0.0" } }, "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ=="],
|
||||
|
||||
"prelude-ls": ["prelude-ls@1.2.1", "", {}, ""],
|
||||
|
||||
"prisma": ["prisma@7.8.0", "", { "dependencies": { "@prisma/config": "7.8.0", "@prisma/dev": "0.24.3", "@prisma/engines": "7.8.0", "@prisma/studio-core": "0.27.3", "mysql2": "3.15.3", "postgres": "3.4.7" }, "peerDependencies": { "better-sqlite3": ">=9.0.0", "typescript": ">=5.4.0" }, "optionalPeers": ["better-sqlite3"], "bin": "build/index.js" }, "sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw=="],
|
||||
|
||||
"prop-types": ["prop-types@15.8.1", "", { "dependencies": { "loose-envify": "^1.4.0", "object-assign": "^4.1.1", "react-is": "^16.13.1" } }, ""],
|
||||
|
||||
"proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="],
|
||||
|
||||
"punycode": ["punycode@2.3.1", "", {}, ""],
|
||||
|
||||
"pure-rand": ["pure-rand@6.1.0", "", {}, "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA=="],
|
||||
|
||||
"queue-microtask": ["queue-microtask@1.2.3", "", {}, ""],
|
||||
|
||||
"rc9": ["rc9@3.0.1", "", { "dependencies": { "defu": "^6.1.6", "destr": "^2.0.5" } }, "sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ=="],
|
||||
|
||||
"react": ["react@19.2.4", "", {}, ""],
|
||||
|
||||
"react-dom": ["react-dom@19.2.4", "", { "dependencies": { "scheduler": "^0.27.0" }, "peerDependencies": { "react": "^19.2.4" } }, ""],
|
||||
|
||||
"react-is": ["react-is@16.13.1", "", {}, ""],
|
||||
|
||||
"readdirp": ["readdirp@5.0.0", "", {}, "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ=="],
|
||||
|
||||
"reflect.getprototypeof": ["reflect.getprototypeof@1.0.10", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.9", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.7", "get-proto": "^1.0.1", "which-builtin-type": "^1.2.1" } }, ""],
|
||||
|
||||
"regexp.prototype.flags": ["regexp.prototype.flags@1.5.4", "", { "dependencies": { "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-errors": "^1.3.0", "get-proto": "^1.0.1", "gopd": "^1.2.0", "set-function-name": "^2.0.2" } }, ""],
|
||||
|
||||
"remeda": ["remeda@2.33.4", "", {}, "sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ=="],
|
||||
|
||||
"require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="],
|
||||
|
||||
"resolve": ["resolve@2.0.0-next.7", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.2", "node-exports-info": "^1.6.0", "object-keys": "^1.1.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": "bin/resolve" }, ""],
|
||||
|
||||
"resolve-from": ["resolve-from@4.0.0", "", {}, ""],
|
||||
|
||||
"resolve-pkg-maps": ["resolve-pkg-maps@1.0.0", "", {}, ""],
|
||||
|
||||
"retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="],
|
||||
|
||||
"reusify": ["reusify@1.1.0", "", {}, ""],
|
||||
|
||||
"run-parallel": ["run-parallel@1.2.0", "", { "dependencies": { "queue-microtask": "^1.2.2" } }, ""],
|
||||
|
||||
"safe-array-concat": ["safe-array-concat@1.1.4", "", { "dependencies": { "call-bind": "^1.0.9", "call-bound": "^1.0.4", "get-intrinsic": "^1.3.0", "has-symbols": "^1.1.0", "isarray": "^2.0.5" } }, ""],
|
||||
|
||||
"safe-push-apply": ["safe-push-apply@1.0.0", "", { "dependencies": { "es-errors": "^1.3.0", "isarray": "^2.0.5" } }, ""],
|
||||
|
||||
"safe-regex-test": ["safe-regex-test@1.1.0", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "is-regex": "^1.2.1" } }, ""],
|
||||
|
||||
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
|
||||
|
||||
"scheduler": ["scheduler@0.27.0", "", {}, ""],
|
||||
|
||||
"semver": ["semver@6.3.1", "", { "bin": "bin/semver.js" }, ""],
|
||||
|
||||
"seq-queue": ["seq-queue@0.0.5", "", {}, "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="],
|
||||
|
||||
"server-only": ["server-only@0.0.1", "", {}, "sha512-qepMx2JxAa5jjfzxG79yPPq+8BuFToHd1hm7kI+Z4zAq1ftQiP7HcxMhDDItrbtwVeLg/cY2JnKnrcFkmiswNA=="],
|
||||
|
||||
"set-function-length": ["set-function-length@1.2.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "function-bind": "^1.1.2", "get-intrinsic": "^1.2.4", "gopd": "^1.0.1", "has-property-descriptors": "^1.0.2" } }, ""],
|
||||
|
||||
"set-function-name": ["set-function-name@2.0.2", "", { "dependencies": { "define-data-property": "^1.1.4", "es-errors": "^1.3.0", "functions-have-names": "^1.2.3", "has-property-descriptors": "^1.0.2" } }, ""],
|
||||
|
||||
"set-proto": ["set-proto@1.0.0", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0" } }, ""],
|
||||
|
||||
"sharp": ["sharp@0.34.5", "", { "dependencies": { "@img/colour": "^1.0.0", "detect-libc": "^2.1.2", "semver": "^7.7.3" }, "optionalDependencies": { "@img/sharp-darwin-arm64": "0.34.5", "@img/sharp-libvips-darwin-arm64": "1.2.4" } }, ""],
|
||||
|
||||
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, ""],
|
||||
|
||||
"shebang-regex": ["shebang-regex@3.0.0", "", {}, ""],
|
||||
|
||||
"side-channel": ["side-channel@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.3", "side-channel-list": "^1.0.0", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" } }, ""],
|
||||
|
||||
"side-channel-list": ["side-channel-list@1.0.1", "", { "dependencies": { "es-errors": "^1.3.0", "object-inspect": "^1.13.4" } }, ""],
|
||||
|
||||
"side-channel-map": ["side-channel-map@1.0.1", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3" } }, ""],
|
||||
|
||||
"side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, ""],
|
||||
|
||||
"signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||
|
||||
"source-map-js": ["source-map-js@1.2.1", "", {}, ""],
|
||||
|
||||
"split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="],
|
||||
|
||||
"sqlstring": ["sqlstring@2.3.3", "", {}, "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg=="],
|
||||
|
||||
"stable-hash": ["stable-hash@0.0.5", "", {}, ""],
|
||||
|
||||
"standardwebhooks": ["standardwebhooks@1.0.0", "", { "dependencies": { "@stablelib/base64": "^1.0.0", "fast-sha256": "^1.3.0" } }, "sha512-BbHGOQK9olHPMvQNHWul6MYlrRTAOKn03rOe4A8O3CLWhNf4YHBqq2HJKKC+sfqpxiBY52pNeesD6jIiLDz8jg=="],
|
||||
|
||||
"std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="],
|
||||
|
||||
"stop-iteration-iterator": ["stop-iteration-iterator@1.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "internal-slot": "^1.1.0" } }, ""],
|
||||
|
||||
"string.prototype.includes": ["string.prototype.includes@2.0.1", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-abstract": "^1.23.3" } }, ""],
|
||||
|
||||
"string.prototype.matchall": ["string.prototype.matchall@4.0.12", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-abstract": "^1.23.6", "es-errors": "^1.3.0", "es-object-atoms": "^1.0.0", "get-intrinsic": "^1.2.6", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "internal-slot": "^1.1.0", "regexp.prototype.flags": "^1.5.3", "set-function-name": "^2.0.2", "side-channel": "^1.1.0" } }, ""],
|
||||
|
||||
"string.prototype.repeat": ["string.prototype.repeat@1.0.0", "", { "dependencies": { "define-properties": "^1.1.3", "es-abstract": "^1.17.5" } }, ""],
|
||||
|
||||
"string.prototype.trim": ["string.prototype.trim@1.2.10", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-data-property": "^1.1.4", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-object-atoms": "^1.0.0", "has-property-descriptors": "^1.0.2" } }, ""],
|
||||
|
||||
"string.prototype.trimend": ["string.prototype.trimend@1.0.9", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.2", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, ""],
|
||||
|
||||
"string.prototype.trimstart": ["string.prototype.trimstart@1.0.8", "", { "dependencies": { "call-bind": "^1.0.7", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, ""],
|
||||
|
||||
"strip-bom": ["strip-bom@3.0.0", "", {}, ""],
|
||||
|
||||
"strip-json-comments": ["strip-json-comments@3.1.1", "", {}, ""],
|
||||
|
||||
"styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, ""],
|
||||
|
||||
"supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, ""],
|
||||
|
||||
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, ""],
|
||||
|
||||
"tailwindcss": ["tailwindcss@4.3.0", "", {}, ""],
|
||||
|
||||
"tapable": ["tapable@2.3.3", "", {}, ""],
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.16", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, ""],
|
||||
|
||||
"to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, ""],
|
||||
|
||||
"ts-api-utils": ["ts-api-utils@2.5.0", "", { "peerDependencies": { "typescript": ">=4.8.4" } }, ""],
|
||||
|
||||
"tsconfig-paths": ["tsconfig-paths@3.15.0", "", { "dependencies": { "@types/json5": "^0.0.29", "json5": "^1.0.2", "minimist": "^1.2.6", "strip-bom": "^3.0.0" } }, ""],
|
||||
|
||||
"tslib": ["tslib@2.8.1", "", {}, ""],
|
||||
|
||||
"type-check": ["type-check@0.4.0", "", { "dependencies": { "prelude-ls": "^1.2.1" } }, ""],
|
||||
|
||||
"typed-array-buffer": ["typed-array-buffer@1.0.3", "", { "dependencies": { "call-bound": "^1.0.3", "es-errors": "^1.3.0", "is-typed-array": "^1.1.14" } }, ""],
|
||||
|
||||
"typed-array-byte-length": ["typed-array-byte-length@1.0.3", "", { "dependencies": { "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.14" } }, ""],
|
||||
|
||||
"typed-array-byte-offset": ["typed-array-byte-offset@1.0.4", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "for-each": "^0.3.3", "gopd": "^1.2.0", "has-proto": "^1.2.0", "is-typed-array": "^1.1.15", "reflect.getprototypeof": "^1.0.9" } }, ""],
|
||||
|
||||
"typed-array-length": ["typed-array-length@1.0.7", "", { "dependencies": { "call-bind": "^1.0.7", "for-each": "^0.3.3", "gopd": "^1.0.1", "is-typed-array": "^1.1.13", "possible-typed-array-names": "^1.0.0", "reflect.getprototypeof": "^1.0.6" } }, ""],
|
||||
|
||||
"typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, ""],
|
||||
|
||||
"typescript-eslint": ["typescript-eslint@8.59.4", "", { "dependencies": { "@typescript-eslint/eslint-plugin": "8.59.4", "@typescript-eslint/parser": "8.59.4", "@typescript-eslint/typescript-estree": "8.59.4", "@typescript-eslint/utils": "8.59.4" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, ""],
|
||||
|
||||
"unbox-primitive": ["unbox-primitive@1.1.0", "", { "dependencies": { "call-bound": "^1.0.3", "has-bigints": "^1.0.2", "has-symbols": "^1.1.0", "which-boxed-primitive": "^1.1.1" } }, ""],
|
||||
|
||||
"undici-types": ["undici-types@6.21.0", "", {}, ""],
|
||||
|
||||
"unrs-resolver": ["unrs-resolver@1.12.2", "", { "dependencies": { "napi-postinstall": "^0.3.4" }, "optionalDependencies": { "@unrs/resolver-binding-darwin-arm64": "1.12.2" } }, ""],
|
||||
|
||||
"update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": "cli.js" }, ""],
|
||||
|
||||
"uri-js": ["uri-js@4.4.1", "", { "dependencies": { "punycode": "^2.1.0" } }, ""],
|
||||
|
||||
"valibot": ["valibot@1.2.0", "", { "peerDependencies": { "typescript": ">=5" } }, "sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg=="],
|
||||
|
||||
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "bin/node-which" } }, ""],
|
||||
|
||||
"which-boxed-primitive": ["which-boxed-primitive@1.1.1", "", { "dependencies": { "is-bigint": "^1.1.0", "is-boolean-object": "^1.2.1", "is-number-object": "^1.1.1", "is-string": "^1.1.1", "is-symbol": "^1.1.1" } }, ""],
|
||||
|
||||
"which-builtin-type": ["which-builtin-type@1.2.1", "", { "dependencies": { "call-bound": "^1.0.2", "function.prototype.name": "^1.1.6", "has-tostringtag": "^1.0.2", "is-async-function": "^2.0.0", "is-date-object": "^1.1.0", "is-finalizationregistry": "^1.1.0", "is-generator-function": "^1.0.10", "is-regex": "^1.2.1", "is-weakref": "^1.0.2", "isarray": "^2.0.5", "which-boxed-primitive": "^1.1.0", "which-collection": "^1.0.2", "which-typed-array": "^1.1.16" } }, ""],
|
||||
|
||||
"which-collection": ["which-collection@1.0.2", "", { "dependencies": { "is-map": "^2.0.3", "is-set": "^2.0.3", "is-weakmap": "^2.0.2", "is-weakset": "^2.0.3" } }, ""],
|
||||
|
||||
"which-typed-array": ["which-typed-array@1.1.20", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, ""],
|
||||
|
||||
"word-wrap": ["word-wrap@1.2.5", "", {}, ""],
|
||||
|
||||
"xtend": ["xtend@4.0.2", "", {}, "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ=="],
|
||||
|
||||
"yallist": ["yallist@3.1.1", "", {}, ""],
|
||||
|
||||
"yocto-queue": ["yocto-queue@0.1.0", "", {}, ""],
|
||||
|
||||
"zeptomatch": ["zeptomatch@2.1.0", "", { "dependencies": { "grammex": "^3.1.11", "graphmatch": "^1.1.0" } }, "sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA=="],
|
||||
|
||||
"zod": ["zod@4.4.3", "", {}, ""],
|
||||
|
||||
"zod-validation-error": ["zod-validation-error@4.0.2", "", { "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" } }, ""],
|
||||
|
||||
"@babel/core/json5": ["json5@2.2.3", "", { "bin": "lib/cli.js" }, ""],
|
||||
|
||||
"@eslint-community/eslint-utils/eslint-visitor-keys": ["eslint-visitor-keys@3.4.3", "", {}, ""],
|
||||
|
||||
"@eslint/eslintrc/globals": ["globals@14.0.0", "", {}, ""],
|
||||
|
||||
"@prisma/engines/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="],
|
||||
|
||||
"@prisma/fetch-engine/@prisma/get-platform": ["@prisma/get-platform@7.8.0", "", { "dependencies": { "@prisma/debug": "7.8.0" } }, "sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw=="],
|
||||
|
||||
"@prisma/get-platform/@prisma/debug": ["@prisma/debug@7.2.0", "", {}, "sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw=="],
|
||||
|
||||
"@prisma/streams-local/ajv": ["ajv@8.20.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA=="],
|
||||
|
||||
"@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, ""],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch": ["minimatch@10.2.5", "", { "dependencies": { "brace-expansion": "^5.0.5" } }, ""],
|
||||
|
||||
"@typescript-eslint/typescript-estree/semver": ["semver@7.8.0", "", { "bin": "bin/semver.js" }, ""],
|
||||
|
||||
"@typescript-eslint/visitor-keys/eslint-visitor-keys": ["eslint-visitor-keys@5.0.1", "", {}, ""],
|
||||
|
||||
"eslint-import-resolver-node/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, ""],
|
||||
|
||||
"eslint-module-utils/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, ""],
|
||||
|
||||
"eslint-plugin-import/debug": ["debug@3.2.7", "", { "dependencies": { "ms": "^2.1.1" } }, ""],
|
||||
|
||||
"fast-glob/glob-parent": ["glob-parent@5.1.2", "", { "dependencies": { "is-glob": "^4.0.1" } }, ""],
|
||||
|
||||
"is-bun-module/semver": ["semver@7.8.0", "", { "bin": "bin/semver.js" }, ""],
|
||||
|
||||
"micromatch/picomatch": ["picomatch@2.3.2", "", {}, ""],
|
||||
|
||||
"next/postcss": ["postcss@8.4.31", "", { "dependencies": { "nanoid": "^3.3.6", "picocolors": "^1.0.0", "source-map-js": "^1.0.2" } }, ""],
|
||||
|
||||
"pg-types/postgres-array": ["postgres-array@2.0.0", "", {}, "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA=="],
|
||||
|
||||
"proper-lockfile/signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="],
|
||||
|
||||
"sharp/semver": ["semver@7.8.0", "", { "bin": "bin/semver.js" }, ""],
|
||||
|
||||
"@prisma/streams-local/ajv/json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion": ["brace-expansion@5.0.6", "", { "dependencies": { "balanced-match": "^4.0.2" } }, ""],
|
||||
|
||||
"@typescript-eslint/typescript-estree/minimatch/brace-expansion/balanced-match": ["balanced-match@4.0.4", "", {}, ""],
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
[
|
||||
{
|
||||
"type": "attention_training",
|
||||
"difficulty": 1,
|
||||
"duration": 3,
|
||||
"description": "Breath counting: Count each exhale from 1 to 10, then start over. If your mind wanders, gently return to 1.",
|
||||
"reason": "Foundational attention exercise. Builds the skill of noticing when attention drifts and bringing it back without judgment."
|
||||
},
|
||||
{
|
||||
"type": "attention_training",
|
||||
"difficulty": 2,
|
||||
"duration": 5,
|
||||
"description": "Breath at the nostrils: Focus on the sensation of air passing the tip of your nose. Notice temperature, texture, and rhythm without changing anything.",
|
||||
"reason": "Narrower focus point than breath counting. Trains precision of attention by anchoring to a subtle physical sensation."
|
||||
},
|
||||
{
|
||||
"type": "attention_training",
|
||||
"difficulty": 3,
|
||||
"duration": 10,
|
||||
"description": "Open awareness: Sit quietly and let your attention rest on whatever arises — sounds, body sensations, thoughts. Don't cling or reject. Just notice.",
|
||||
"reason": "Advanced attention exercise. Builds meta-awareness — the ability to observe the mind's movements without being carried away by them."
|
||||
},
|
||||
{
|
||||
"type": "emotion_awareness",
|
||||
"difficulty": 1,
|
||||
"duration": 3,
|
||||
"description": "Name the weather: Close your eyes and ask 'What's the emotional weather right now?' Label it simply — sunny, cloudy, stormy, still. Don't explain why. Just name it.",
|
||||
"reason": "Builds the vocabulary and habit of checking in with emotional state. Naming emotions reduces their intensity and creates space for choice."
|
||||
},
|
||||
{
|
||||
"type": "emotion_awareness",
|
||||
"difficulty": 2,
|
||||
"duration": 5,
|
||||
"description": "Body scan: Slowly move attention from the top of your head to your toes. At each body part, notice any sensation — tightness, warmth, buzzing, emptiness. Don't try to change anything.",
|
||||
"reason": "Emotions live in the body before they reach conscious awareness. Body scanning builds the sensory vocabulary that makes emotional signals legible."
|
||||
},
|
||||
{
|
||||
"type": "emotion_awareness",
|
||||
"difficulty": 3,
|
||||
"duration": 7,
|
||||
"description": "Emotion tracking: Set a timer for 7 minutes. Every 60 seconds, pause and write one word describing your current emotional state. Notice how emotions shift moment to moment.",
|
||||
"reason": "Reveals the impermanent nature of emotions. Watching emotions change undermines the belief that 'this feeling will last forever.'"
|
||||
},
|
||||
{
|
||||
"type": "stress_regulation",
|
||||
"difficulty": 1,
|
||||
"duration": 3,
|
||||
"description": "4-4-6 breathing: Inhale for 4 counts, hold for 4, exhale for 6. The long exhale activates the parasympathetic nervous system.",
|
||||
"reason": "Physiological stress reduction through breath pattern. The extended exhale is one of the fastest ways to signal safety to the nervous system."
|
||||
},
|
||||
{
|
||||
"type": "stress_regulation",
|
||||
"difficulty": 2,
|
||||
"duration": 5,
|
||||
"description": "Progressive relaxation: Tense each muscle group for 5 seconds, then release for 10. Start with feet, work up to face. Notice the contrast between tension and release.",
|
||||
"reason": "Teaches the difference between tension and relaxation at the body level. Many people don't realize they're holding tension until they deliberately release it."
|
||||
},
|
||||
{
|
||||
"type": "stress_regulation",
|
||||
"difficulty": 3,
|
||||
"duration": 8,
|
||||
"description": "Grounding through senses: Name 5 things you can see, 4 things you can touch, 3 things you can hear, 2 things you can smell, 1 thing you can taste.",
|
||||
"reason": "Full sensory grounding anchors you in the present moment. Effective for acute stress by redirecting attention from internal worry to external reality."
|
||||
},
|
||||
{
|
||||
"type": "cognitive_clarity",
|
||||
"difficulty": 1,
|
||||
"duration": 3,
|
||||
"description": "Thought labeling: Sit quietly and watch thoughts arise. Label each one silently — 'planning,' 'worrying,' 'remembering,' 'judging.' Don't engage the thought. Just label and release.",
|
||||
"reason": "Creates distance between you and your thoughts. Labeling activates the prefrontal cortex and reduces amygdala reactivity."
|
||||
},
|
||||
{
|
||||
"type": "cognitive_clarity",
|
||||
"difficulty": 2,
|
||||
"duration": 5,
|
||||
"description": "Reframing practice: Think of a mildly frustrating situation today. Write down your automatic thought, then write three alternative interpretations that are equally plausible.",
|
||||
"reason": "Cognitive reframing is the core skill of cognitive flexibility. Practicing with mild frustrations builds the muscle for harder situations."
|
||||
},
|
||||
{
|
||||
"type": "cognitive_clarity",
|
||||
"difficulty": 3,
|
||||
"duration": 10,
|
||||
"description": "Inner dialogue observation: For 10 minutes, write down every thought that passes through your mind as if transcribing a conversation. Don't edit or judge. Then read it back. Notice which 'voice' is speaking — the critic? The planner? The child?",
|
||||
"reason": "Reveals the multiplicity of the inner world. Recognizing your inner voices is the first step toward choosing which ones to amplify."
|
||||
},
|
||||
{
|
||||
"type": "integration",
|
||||
"difficulty": 1,
|
||||
"duration": 3,
|
||||
"description": "One-minute pause: Three times today, pause for 60 seconds. Take three conscious breaths. Notice what's happening in your body and mind. Then continue.",
|
||||
"reason": "Integrates practice into daily life. Short, frequent pauses build the habit of mindful awareness without requiring dedicated sitting time."
|
||||
},
|
||||
{
|
||||
"type": "integration",
|
||||
"difficulty": 2,
|
||||
"duration": 5,
|
||||
"description": "Mindful routine: Pick one daily activity (brushing teeth, washing dishes, walking to your desk). Do it with full attention today. Notice every sensation, movement, and sound.",
|
||||
"reason": "Turns everyday activities into practice opportunities. The goal isn't more practice time — it's weaving practice into time you already have."
|
||||
},
|
||||
{
|
||||
"type": "integration",
|
||||
"difficulty": 3,
|
||||
"duration": 10,
|
||||
"description": "Difficult conversation practice: Before a conversation that might be challenging, sit for 5 minutes and set an intention. During the conversation, notice your reactions without acting on them. After, reflect for 5 minutes on what you noticed.",
|
||||
"reason": "The ultimate integration — bringing awareness into interpersonal dynamics. This is where practice meets real life."
|
||||
}
|
||||
]
|
||||
@@ -0,0 +1,18 @@
|
||||
import { defineConfig, globalIgnores } from "eslint/config";
|
||||
import nextVitals from "eslint-config-next/core-web-vitals";
|
||||
import nextTs from "eslint-config-next/typescript";
|
||||
|
||||
const eslintConfig = defineConfig([
|
||||
...nextVitals,
|
||||
...nextTs,
|
||||
// Override default ignores of eslint-config-next.
|
||||
globalIgnores([
|
||||
// Default ignores of eslint-config-next:
|
||||
".next/**",
|
||||
"out/**",
|
||||
"build/**",
|
||||
"next-env.d.ts",
|
||||
]),
|
||||
]);
|
||||
|
||||
export default eslintConfig;
|
||||
@@ -0,0 +1,7 @@
|
||||
import type { NextConfig } from "next";
|
||||
|
||||
const nextConfig: NextConfig = {
|
||||
/* config options here */
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"name": "inner-os",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"start": "next start",
|
||||
"lint": "eslint"
|
||||
},
|
||||
"dependencies": {
|
||||
"@clerk/nextjs": "^7.3.7",
|
||||
"@prisma/adapter-pg": "^7.8.0",
|
||||
"@prisma/client": "^7.8.0",
|
||||
"next": "16.2.6",
|
||||
"openai": "^6.38.0",
|
||||
"react": "19.2.4",
|
||||
"react-dom": "19.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/postcss": "^4",
|
||||
"@types/node": "^20.19.41",
|
||||
"@types/react": "^19",
|
||||
"@types/react-dom": "^19",
|
||||
"dotenv": "^17.4.2",
|
||||
"eslint": "^9",
|
||||
"eslint-config-next": "16.2.6",
|
||||
"prisma": "^7.8.0",
|
||||
"tailwindcss": "^4",
|
||||
"typescript": "^5"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,7 @@
|
||||
const config = {
|
||||
plugins: {
|
||||
"@tailwindcss/postcss": {},
|
||||
},
|
||||
};
|
||||
|
||||
export default config;
|
||||
@@ -0,0 +1,14 @@
|
||||
// This file was generated by Prisma, and assumes you have installed the following:
|
||||
// npm install --save-dev prisma dotenv
|
||||
import "dotenv/config";
|
||||
import { defineConfig } from "prisma/config";
|
||||
|
||||
export default defineConfig({
|
||||
schema: "prisma/schema.prisma",
|
||||
migrations: {
|
||||
path: "prisma/migrations",
|
||||
},
|
||||
datasource: {
|
||||
url: process.env["DATABASE_URL"],
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,126 @@
|
||||
generator client {
|
||||
provider = "prisma-client"
|
||||
output = "../src/generated/prisma"
|
||||
}
|
||||
|
||||
datasource db {
|
||||
provider = "postgresql"
|
||||
}
|
||||
|
||||
enum TaskType {
|
||||
attention_training
|
||||
emotion_awareness
|
||||
stress_regulation
|
||||
cognitive_clarity
|
||||
integration
|
||||
}
|
||||
|
||||
enum TaskStatus {
|
||||
pending
|
||||
completed
|
||||
expired
|
||||
}
|
||||
|
||||
enum RewardEventType {
|
||||
daily_completion
|
||||
streak_3
|
||||
streak_7
|
||||
streak_14
|
||||
streak_30
|
||||
stage_up
|
||||
first_session
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id // Clerk user ID
|
||||
email String @unique
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
state UserState? @relation("UserState")
|
||||
sessions Session[]
|
||||
tasks Task[]
|
||||
rewardEvents RewardEvent[]
|
||||
weeklySummary WeeklySummary?
|
||||
|
||||
@@map("users")
|
||||
}
|
||||
|
||||
model UserState {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique @map("user_id")
|
||||
attentionLevel Int @default(50) @map("attention_level")
|
||||
emotionStability Int @default(50) @map("emotion_stability")
|
||||
stressLevel Int @default(50) @map("stress_level")
|
||||
practiceStage Int @default(0) @map("practice_stage")
|
||||
consistencyScore Int @default(0) @map("consistency_score")
|
||||
streakDays Int @default(0) @map("streak_days")
|
||||
totalSessionsCompleted Int @default(0) @map("total_sessions_completed")
|
||||
onboardingCompleted Boolean @default(false) @map("onboarding_completed")
|
||||
lastSessionDate DateTime? @map("last_session_date")
|
||||
updatedAt DateTime @updatedAt @map("updated_at")
|
||||
|
||||
user User @relation("UserState", fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("user_states")
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
taskType TaskType @map("task_type")
|
||||
taskDescription String @map("task_description")
|
||||
durationSeconds Int @default(300) @map("duration_seconds")
|
||||
completed Boolean @default(false)
|
||||
focusScore Int? @map("focus_score")
|
||||
emotionScore Int? @map("emotion_score")
|
||||
notes String?
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, createdAt])
|
||||
@@map("sessions")
|
||||
}
|
||||
|
||||
model Task {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
type TaskType
|
||||
difficulty Int @default(1)
|
||||
duration Int @default(5)
|
||||
description String
|
||||
reasonJson String @default("{}") @map("reason_json")
|
||||
status TaskStatus @default(pending)
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, createdAt])
|
||||
@@index([userId, status])
|
||||
@@map("tasks")
|
||||
}
|
||||
|
||||
model RewardEvent {
|
||||
id String @id @default(uuid())
|
||||
userId String @map("user_id")
|
||||
type RewardEventType
|
||||
message String
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId, createdAt])
|
||||
@@map("reward_events")
|
||||
}
|
||||
|
||||
model WeeklySummary {
|
||||
id String @id @default(uuid())
|
||||
userId String @unique @map("user_id")
|
||||
summary String?
|
||||
weekStart DateTime @map("week_start")
|
||||
createdAt DateTime @default(now()) @map("created_at")
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@map("weekly_summaries")
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
<svg fill="none" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><path d="M14.5 13.5V5.41a1 1 0 0 0-.3-.7L9.8.29A1 1 0 0 0 9.08 0H1.5v13.5A2.5 2.5 0 0 0 4 16h8a2.5 2.5 0 0 0 2.5-2.5m-1.5 0v-7H8v-5H3v12a1 1 0 0 0 1 1h8a1 1 0 0 0 1-1M9.5 5V2.12L12.38 5zM5.13 5h-.62v1.25h2.12V5zm-.62 3h7.12v1.25H4.5zm.62 3h-.62v1.25h7.12V11z" clip-rule="evenodd" fill="#666" fill-rule="evenodd"/></svg>
|
||||
|
After Width: | Height: | Size: 391 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><g clip-path="url(#a)"><path fill-rule="evenodd" clip-rule="evenodd" d="M10.27 14.1a6.5 6.5 0 0 0 3.67-3.45q-1.24.21-2.7.34-.31 1.83-.97 3.1M8 16A8 8 0 1 0 8 0a8 8 0 0 0 0 16m.48-1.52a7 7 0 0 1-.96 0H7.5a4 4 0 0 1-.84-1.32q-.38-.89-.63-2.08a40 40 0 0 0 3.92 0q-.25 1.2-.63 2.08a4 4 0 0 1-.84 1.31zm2.94-4.76q1.66-.15 2.95-.43a7 7 0 0 0 0-2.58q-1.3-.27-2.95-.43a18 18 0 0 1 0 3.44m-1.27-3.54a17 17 0 0 1 0 3.64 39 39 0 0 1-4.3 0 17 17 0 0 1 0-3.64 39 39 0 0 1 4.3 0m1.1-1.17q1.45.13 2.69.34a6.5 6.5 0 0 0-3.67-3.44q.65 1.26.98 3.1M8.48 1.5l.01.02q.41.37.84 1.31.38.89.63 2.08a40 40 0 0 0-3.92 0q.25-1.2.63-2.08a4 4 0 0 1 .85-1.32 7 7 0 0 1 .96 0m-2.75.4a6.5 6.5 0 0 0-3.67 3.44 29 29 0 0 1 2.7-.34q.31-1.83.97-3.1M4.58 6.28q-1.66.16-2.95.43a7 7 0 0 0 0 2.58q1.3.27 2.95.43a18 18 0 0 1 0-3.44m.17 4.71q-1.45-.12-2.69-.34a6.5 6.5 0 0 0 3.67 3.44q-.65-1.27-.98-3.1" fill="#666"/></g><defs><clipPath id="a"><path fill="#fff" d="M0 0h16v16H0z"/></clipPath></defs></svg>
|
||||
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 394 80"><path fill="#000" d="M262 0h68.5v12.7h-27.2v66.6h-13.6V12.7H262V0ZM149 0v12.7H94v20.4h44.3v12.6H94v21h55v12.6H80.5V0h68.7zm34.3 0h-17.8l63.8 79.4h17.9l-32-39.7 32-39.6h-17.9l-23 28.6-23-28.6zm18.3 56.7-9-11-27.1 33.7h17.8l18.3-22.7z"/><path fill="#000" d="M81 79.3 17 0H0v79.3h13.6V17l50.2 62.3H81Zm252.6-.4c-1 0-1.8-.4-2.5-1s-1.1-1.6-1.1-2.6.3-1.8 1-2.5 1.6-1 2.6-1 1.8.3 2.5 1a3.4 3.4 0 0 1 .6 4.3 3.7 3.7 0 0 1-3 1.8zm23.2-33.5h6v23.3c0 2.1-.4 4-1.3 5.5a9.1 9.1 0 0 1-3.8 3.5c-1.6.8-3.5 1.3-5.7 1.3-2 0-3.7-.4-5.3-1s-2.8-1.8-3.7-3.2c-.9-1.3-1.4-3-1.4-5h6c.1.8.3 1.6.7 2.2s1 1.2 1.6 1.5c.7.4 1.5.5 2.4.5 1 0 1.8-.2 2.4-.6a4 4 0 0 0 1.6-1.8c.3-.8.5-1.8.5-3V45.5zm30.9 9.1a4.4 4.4 0 0 0-2-3.3 7.5 7.5 0 0 0-4.3-1.1c-1.3 0-2.4.2-3.3.5-.9.4-1.6 1-2 1.6a3.5 3.5 0 0 0-.3 4c.3.5.7.9 1.3 1.2l1.8 1 2 .5 3.2.8c1.3.3 2.5.7 3.7 1.2a13 13 0 0 1 3.2 1.8 8.1 8.1 0 0 1 3 6.5c0 2-.5 3.7-1.5 5.1a10 10 0 0 1-4.4 3.5c-1.8.8-4.1 1.2-6.8 1.2-2.6 0-4.9-.4-6.8-1.2-2-.8-3.4-2-4.5-3.5a10 10 0 0 1-1.7-5.6h6a5 5 0 0 0 3.5 4.6c1 .4 2.2.6 3.4.6 1.3 0 2.5-.2 3.5-.6 1-.4 1.8-1 2.4-1.7a4 4 0 0 0 .8-2.4c0-.9-.2-1.6-.7-2.2a11 11 0 0 0-2.1-1.4l-3.2-1-3.8-1c-2.8-.7-5-1.7-6.6-3.2a7.2 7.2 0 0 1-2.4-5.7 8 8 0 0 1 1.7-5 10 10 0 0 1 4.3-3.5c2-.8 4-1.2 6.4-1.2 2.3 0 4.4.4 6.2 1.2 1.8.8 3.2 2 4.3 3.4 1 1.4 1.5 3 1.5 5h-5.8z"/></svg>
|
||||
|
After Width: | Height: | Size: 1.3 KiB |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1155 1000"><path d="m577.3 0 577.4 1000H0z" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 128 B |
@@ -0,0 +1 @@
|
||||
<svg fill="none" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill-rule="evenodd" clip-rule="evenodd" d="M1.5 2.5h13v10a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1zM0 1h16v11.5a2.5 2.5 0 0 1-2.5 2.5h-11A2.5 2.5 0 0 1 0 12.5zm3.75 4.5a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5M7 4.75a.75.75 0 1 1-1.5 0 .75.75 0 0 1 1.5 0m1.75.75a.75.75 0 1 0 0-1.5.75.75 0 0 0 0 1.5" fill="#666"/></svg>
|
||||
|
After Width: | Height: | Size: 385 B |
@@ -0,0 +1,40 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import OpenAI from "openai";
|
||||
import { generateAllWeeklySummaries } from "@/lib/engines/summary-generator";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
// Verify cron secret
|
||||
const authHeader = req.headers.get("authorization");
|
||||
const expectedSecret = process.env.CRON_SECRET;
|
||||
|
||||
if (!expectedSecret || expectedSecret === "change-me-to-a-random-string") {
|
||||
return NextResponse.json(
|
||||
{ error: "CRON_SECRET not configured" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
|
||||
if (!authHeader || authHeader !== `Bearer ${expectedSecret}`) {
|
||||
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
|
||||
try {
|
||||
const results = await generateAllWeeklySummaries(openai);
|
||||
const succeeded = results.filter((r) => r.success).length;
|
||||
const failed = results.filter((r) => !r.success).length;
|
||||
|
||||
return NextResponse.json({
|
||||
processed: results.length,
|
||||
succeeded,
|
||||
failed,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Weekly summary cron failed:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Cron job failed" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,25 @@
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET(req: Request) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const { searchParams } = new URL(req.url);
|
||||
const limit = Math.min(parseInt(searchParams.get("limit") || "20", 10), 50);
|
||||
|
||||
const rewards = await prisma.rewardEvent.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: limit,
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
message: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ rewards });
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import OpenAI from "openai";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { processReflection } from "@/lib/engines/reflection-analyzer";
|
||||
import { computeStateUpdate } from "@/lib/engines/progress-tracker";
|
||||
import { generateRewards, persistRewards } from "@/lib/engines/reward-generator";
|
||||
|
||||
export async function POST(req: Request) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const body = await req.json().catch(() => null);
|
||||
if (!body || typeof body !== "object") {
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { taskId, notes, durationSeconds } = body as {
|
||||
taskId?: string;
|
||||
notes?: string;
|
||||
durationSeconds?: number;
|
||||
};
|
||||
|
||||
if (!taskId) {
|
||||
return NextResponse.json({ error: "taskId is required" }, { status: 400 });
|
||||
}
|
||||
|
||||
// Verify the task belongs to this user and is pending
|
||||
const task = await prisma.task.findFirst({
|
||||
where: { id: taskId, userId, status: "pending" },
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json(
|
||||
{ error: "Task not found or already completed" },
|
||||
{ status: 404 }
|
||||
);
|
||||
}
|
||||
|
||||
// Get current state
|
||||
const currentState = await prisma.userState.findUnique({
|
||||
where: { userId },
|
||||
});
|
||||
|
||||
if (!currentState) {
|
||||
return NextResponse.json(
|
||||
{ error: "User state not found. Complete onboarding first." },
|
||||
{ status: 400 }
|
||||
);
|
||||
}
|
||||
|
||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
|
||||
// Process reflection through LLM #2
|
||||
const reflection = await processReflection(
|
||||
{
|
||||
notes: notes || "",
|
||||
durationSeconds: durationSeconds || task.duration * 60,
|
||||
taskType: task.type,
|
||||
taskDescription: task.description,
|
||||
},
|
||||
{
|
||||
attentionLevel: currentState.attentionLevel,
|
||||
emotionStability: currentState.emotionStability,
|
||||
stressLevel: currentState.stressLevel,
|
||||
},
|
||||
openai
|
||||
);
|
||||
|
||||
// Get recent session dates for consistency computation
|
||||
const recentSessions = await prisma.session.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 14,
|
||||
select: { createdAt: true },
|
||||
});
|
||||
|
||||
// Compute state update
|
||||
const stateUpdate = computeStateUpdate(
|
||||
currentState,
|
||||
{
|
||||
attentionDelta: reflection.attentionDelta,
|
||||
emotionDelta: reflection.emotionDelta,
|
||||
stressDelta: reflection.stressDelta,
|
||||
},
|
||||
recentSessions.map((s) => s.createdAt)
|
||||
);
|
||||
|
||||
// Generate rewards
|
||||
const rewards = generateRewards({
|
||||
isFirstSession: currentState.totalSessionsCompleted === 0,
|
||||
streakMilestone: stateUpdate.streakMilestone,
|
||||
streakDays: stateUpdate.streakDays,
|
||||
stageAdvanced: stateUpdate.stageAdvanced,
|
||||
newStage: stateUpdate.practiceStage,
|
||||
totalSessionsCompleted: stateUpdate.totalSessionsCompleted,
|
||||
});
|
||||
|
||||
// Perform all DB writes in a transaction
|
||||
await prisma.$transaction([
|
||||
// Mark task as completed
|
||||
prisma.task.update({
|
||||
where: { id: taskId },
|
||||
data: { status: "completed" },
|
||||
}),
|
||||
|
||||
// Create session record
|
||||
prisma.session.create({
|
||||
data: {
|
||||
userId,
|
||||
taskType: task.type,
|
||||
taskDescription: task.description,
|
||||
durationSeconds: durationSeconds || task.duration * 60,
|
||||
completed: true,
|
||||
focusScore: reflection.focusScore,
|
||||
emotionScore: reflection.emotionScore,
|
||||
notes: notes || null,
|
||||
},
|
||||
}),
|
||||
|
||||
// Update user state
|
||||
prisma.userState.update({
|
||||
where: { userId },
|
||||
data: {
|
||||
attentionLevel: stateUpdate.attentionLevel,
|
||||
emotionStability: stateUpdate.emotionStability,
|
||||
stressLevel: stateUpdate.stressLevel,
|
||||
practiceStage: stateUpdate.practiceStage,
|
||||
consistencyScore: stateUpdate.consistencyScore,
|
||||
streakDays: stateUpdate.streakDays,
|
||||
totalSessionsCompleted: stateUpdate.totalSessionsCompleted,
|
||||
lastSessionDate: new Date(),
|
||||
},
|
||||
}),
|
||||
|
||||
// Persist rewards
|
||||
...rewards.map((r) =>
|
||||
prisma.rewardEvent.create({
|
||||
data: {
|
||||
userId,
|
||||
type: r.type,
|
||||
message: r.message,
|
||||
},
|
||||
})
|
||||
),
|
||||
]);
|
||||
|
||||
return NextResponse.json({
|
||||
reflection: {
|
||||
focusScore: reflection.focusScore,
|
||||
emotionScore: reflection.emotionScore,
|
||||
insight: reflection.insight,
|
||||
},
|
||||
state: {
|
||||
attentionLevel: stateUpdate.attentionLevel,
|
||||
emotionStability: stateUpdate.emotionStability,
|
||||
stressLevel: stateUpdate.stressLevel,
|
||||
practiceStage: stateUpdate.practiceStage,
|
||||
consistencyScore: stateUpdate.consistencyScore,
|
||||
streakDays: stateUpdate.streakDays,
|
||||
totalSessionsCompleted: stateUpdate.totalSessionsCompleted,
|
||||
},
|
||||
rewards,
|
||||
stageAdvanced: stateUpdate.stageAdvanced,
|
||||
streakMilestone: stateUpdate.streakMilestone,
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import OpenAI from "openai";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const summary = await prisma.weeklySummary.findUnique({
|
||||
where: { userId },
|
||||
select: {
|
||||
summary: true,
|
||||
weekStart: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!summary) {
|
||||
return NextResponse.json({ summary: null });
|
||||
}
|
||||
|
||||
return NextResponse.json({ summary });
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const today = new Date();
|
||||
today.setHours(0, 0, 0, 0);
|
||||
|
||||
const task = await prisma.task.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
status: "pending",
|
||||
createdAt: { gte: today },
|
||||
},
|
||||
orderBy: { createdAt: "desc" },
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
difficulty: true,
|
||||
duration: true,
|
||||
description: true,
|
||||
reasonJson: true,
|
||||
status: true,
|
||||
createdAt: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!task) {
|
||||
return NextResponse.json({ task: null });
|
||||
}
|
||||
|
||||
let reason = "";
|
||||
try {
|
||||
reason = JSON.parse(task.reasonJson).reason || "";
|
||||
} catch {
|
||||
// reasonJson is unparseable, leave empty
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
task: {
|
||||
...task,
|
||||
reason,
|
||||
reasonJson: undefined,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import OpenAI from "openai";
|
||||
import { generateDailyTask } from "@/lib/engines/task-selector";
|
||||
|
||||
export async function POST() {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
||||
|
||||
try {
|
||||
const result = await generateDailyTask(userId, openai);
|
||||
|
||||
return NextResponse.json({
|
||||
task: result.task,
|
||||
fromTemplate: result.fromTemplate,
|
||||
safe: result.safety.safe,
|
||||
});
|
||||
} catch (error) {
|
||||
console.error("Task generation failed:", error);
|
||||
return NextResponse.json(
|
||||
{ error: "Failed to generate task" },
|
||||
{ status: 500 }
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function POST() {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
// Mark onboarding complete
|
||||
const state = await prisma.userState.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
onboardingCompleted: true,
|
||||
},
|
||||
update: {
|
||||
onboardingCompleted: true,
|
||||
},
|
||||
select: {
|
||||
attentionLevel: true,
|
||||
emotionStability: true,
|
||||
stressLevel: true,
|
||||
practiceStage: true,
|
||||
consistencyScore: true,
|
||||
streakDays: true,
|
||||
totalSessionsCompleted: true,
|
||||
onboardingCompleted: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ state });
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
import { auth } from "@clerk/nextjs/server";
|
||||
import { NextResponse } from "next/server";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
export async function GET() {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const state = await prisma.userState.findUnique({
|
||||
where: { userId },
|
||||
select: {
|
||||
attentionLevel: true,
|
||||
emotionStability: true,
|
||||
stressLevel: true,
|
||||
practiceStage: true,
|
||||
consistencyScore: true,
|
||||
streakDays: true,
|
||||
totalSessionsCompleted: true,
|
||||
onboardingCompleted: true,
|
||||
lastSessionDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
if (!state) {
|
||||
return NextResponse.json({
|
||||
state: null,
|
||||
hasStarted: false,
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.json({
|
||||
state,
|
||||
hasStarted: true,
|
||||
});
|
||||
}
|
||||
|
||||
export async function PATCH(req: Request) {
|
||||
const { userId } = await auth();
|
||||
if (!userId) return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
|
||||
|
||||
const body = await req.json().catch(() => null);
|
||||
if (!body || typeof body !== "object") {
|
||||
return NextResponse.json({ error: "Invalid request body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const { onboardingCompleted } = body as { onboardingCompleted?: boolean };
|
||||
|
||||
const state = await prisma.userState.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
onboardingCompleted: onboardingCompleted ?? false,
|
||||
},
|
||||
update: {
|
||||
...(onboardingCompleted !== undefined ? { onboardingCompleted } : {}),
|
||||
},
|
||||
select: {
|
||||
attentionLevel: true,
|
||||
emotionStability: true,
|
||||
stressLevel: true,
|
||||
practiceStage: true,
|
||||
consistencyScore: true,
|
||||
streakDays: true,
|
||||
totalSessionsCompleted: true,
|
||||
onboardingCompleted: true,
|
||||
lastSessionDate: true,
|
||||
},
|
||||
});
|
||||
|
||||
return NextResponse.json({ state });
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@@ -0,0 +1,46 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
@theme inline {
|
||||
--color-cream: #FAF7F2;
|
||||
--color-sand: #F3EFE8;
|
||||
--color-stone: #E6E0D4;
|
||||
--color-amber: #C8834A;
|
||||
--color-sage: #7A9A8B;
|
||||
--color-ink: #2C2416;
|
||||
--color-muted: #6B5E4A;
|
||||
--color-faded: #9B8E7A;
|
||||
--color-success: #7A9A8B;
|
||||
--color-warning: #C8834A;
|
||||
--color-error: #C45A4A;
|
||||
--color-info: #5A8AB5;
|
||||
--font-display: var(--font-fraunces), Georgia, serif;
|
||||
--font-body: var(--font-dm-sans), system-ui, sans-serif;
|
||||
}
|
||||
|
||||
:root {
|
||||
--background: #FAF7F2;
|
||||
--foreground: #2C2416;
|
||||
--surface: #F3EFE8;
|
||||
--border: #E6E0D4;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: dark) {
|
||||
:root {
|
||||
--background: #1E1A14;
|
||||
--foreground: #F3EFE8;
|
||||
--surface: #292418;
|
||||
--border: #3D3528;
|
||||
--color-muted: #B8A892;
|
||||
--color-faded: #8A7D6B;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
background: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: var(--font-body);
|
||||
font-size: 16px;
|
||||
line-height: 1.6;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
import type { Metadata } from "next";
|
||||
import { ClerkProvider } from "@clerk/nextjs";
|
||||
import { DM_Sans, Fraunces } from "next/font/google";
|
||||
import TabBar from "@/components/TabBar";
|
||||
import "./globals.css";
|
||||
|
||||
const fraunces = Fraunces({
|
||||
variable: "--font-fraunces",
|
||||
subsets: ["latin"],
|
||||
axes: ["opsz", "SOFT", "WONK"],
|
||||
});
|
||||
|
||||
const dmSans = DM_Sans({
|
||||
variable: "--font-dm-sans",
|
||||
subsets: ["latin"],
|
||||
weight: ["400", "500", "700"],
|
||||
});
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: "Inner OS",
|
||||
description: "Your private inner development practice.",
|
||||
};
|
||||
|
||||
export default function RootLayout({
|
||||
children,
|
||||
}: Readonly<{
|
||||
children: React.ReactNode;
|
||||
}>) {
|
||||
return (
|
||||
<ClerkProvider>
|
||||
<html
|
||||
lang="en"
|
||||
className={`${fraunces.variable} ${dmSans.variable} h-full antialiased`}
|
||||
>
|
||||
<body className="min-h-full flex flex-col pb-16 bg-cream dark:bg-[#1E1A14] text-ink dark:text-[#F3EFE8]">
|
||||
<main className="flex-1 max-w-2xl mx-auto w-full px-5 pt-8 pb-4">
|
||||
{children}
|
||||
</main>
|
||||
<TabBar />
|
||||
</body>
|
||||
</html>
|
||||
</ClerkProvider>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,265 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import { useRouter } from "next/navigation";
|
||||
import StatBar from "@/components/StatBar";
|
||||
|
||||
interface StateData {
|
||||
attentionLevel: number;
|
||||
emotionStability: number;
|
||||
stressLevel: number;
|
||||
practiceStage: number;
|
||||
consistencyScore: number;
|
||||
streakDays: number;
|
||||
totalSessionsCompleted: number;
|
||||
onboardingCompleted: boolean;
|
||||
}
|
||||
|
||||
interface TaskData {
|
||||
id: string;
|
||||
type: string;
|
||||
difficulty: number;
|
||||
duration: number;
|
||||
description: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export default function HomePage() {
|
||||
const { user, isLoaded: userLoaded } = useUser();
|
||||
const router = useRouter();
|
||||
const [state, setState] = useState<StateData | null>(null);
|
||||
const [task, setTask] = useState<TaskData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [generating, setGenerating] = useState(false);
|
||||
const [genError, setGenError] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userLoaded) return;
|
||||
Promise.all([
|
||||
fetch("/api/user/state").then((r) => r.json()),
|
||||
fetch("/api/task/daily").then((r) => r.json()),
|
||||
])
|
||||
.then(([stateJson, taskJson]) => {
|
||||
setState(stateJson.state);
|
||||
setTask(taskJson.task || null);
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [userLoaded]);
|
||||
|
||||
const handleGenerate = async () => {
|
||||
setGenerating(true);
|
||||
setGenError(null);
|
||||
try {
|
||||
const res = await fetch("/api/task/generate", { method: "POST" });
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
throw new Error(json.error || "Failed to generate task");
|
||||
}
|
||||
const json = await res.json();
|
||||
setTask(json.task);
|
||||
} catch (e) {
|
||||
setGenError(e instanceof Error ? e.message : "Something went wrong");
|
||||
} finally {
|
||||
setGenerating(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!userLoaded || loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-pulse text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
// Redirect to settings for onboarding
|
||||
if (state && !state.onboardingCompleted) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Greeting firstName={user.firstName} />
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-4 text-center">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-amber/10 border-2 border-amber flex items-center justify-center">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#C8834A"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="10" />
|
||||
<path d="M12 8v4l2 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<div>
|
||||
<h2 className="text-base font-medium text-ink dark:text-[#F3EFE8]">
|
||||
One step left
|
||||
</h2>
|
||||
<p className="text-sm text-muted mt-1 max-w-xs mx-auto">
|
||||
Complete onboarding in Settings to start your practice.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => router.push("/settings")}
|
||||
className="py-2.5 px-5 rounded-lg bg-amber text-white font-medium text-sm transition-all hover:bg-amber/90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2"
|
||||
>
|
||||
Go to Settings
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const metrics = state
|
||||
? [
|
||||
{ label: "Attention", value: state.attentionLevel, color: "amber" as const },
|
||||
{ label: "Emotion", value: state.emotionStability, color: "sage" as const },
|
||||
{ label: "Stress", value: state.stressLevel, color: "warning" as const },
|
||||
{ label: "Consistency", value: state.consistencyScore, color: "info" as const },
|
||||
]
|
||||
: [];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<Greeting firstName={user.firstName} />
|
||||
|
||||
{/* State overview */}
|
||||
{state && (
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528]">
|
||||
<p className="text-xs text-muted uppercase tracking-wider font-medium">
|
||||
Streak
|
||||
</p>
|
||||
<p className="text-3xl font-bold tabular-nums text-amber font-[family-name:var(--font-fraunces)]">
|
||||
{state.streakDays}
|
||||
</p>
|
||||
<p className="text-xs text-muted mt-0.5">days</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528]">
|
||||
<p className="text-xs text-muted uppercase tracking-wider font-medium">
|
||||
Sessions
|
||||
</p>
|
||||
<p className="text-3xl font-bold tabular-nums text-sage font-[family-name:var(--font-fraunces)]">
|
||||
{state.totalSessionsCompleted}
|
||||
</p>
|
||||
<p className="text-xs text-muted mt-0.5">total</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Core metrics */}
|
||||
{state && (
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-4">
|
||||
<h2 className="text-sm font-medium text-ink dark:text-[#F3EFE8]">
|
||||
Core metrics
|
||||
</h2>
|
||||
{metrics.map((m) => (
|
||||
<StatBar
|
||||
key={m.label}
|
||||
label={m.label}
|
||||
value={m.value}
|
||||
color={m.color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Today's task */}
|
||||
<div className="space-y-3">
|
||||
<h2 className="text-sm font-medium text-ink dark:text-[#F3EFE8]">
|
||||
Today’s practice
|
||||
</h2>
|
||||
|
||||
{task ? (
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-xs font-medium uppercase tracking-wider px-2 py-0.5 rounded-full bg-amber/10 text-amber">
|
||||
{formatTaskType(task.type)}
|
||||
</span>
|
||||
<span className="text-xs text-muted">
|
||||
{formatDifficulty(task.difficulty)} · {task.duration}m
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-medium text-ink dark:text-[#F3EFE8] font-[family-name:var(--font-fraunces)] italic">
|
||||
{formatTaskType(task.type)} practice
|
||||
</h3>
|
||||
<p className="text-sm text-muted mt-2 leading-relaxed">
|
||||
{task.description}
|
||||
</p>
|
||||
</div>
|
||||
{task.reason && (
|
||||
<div className="text-sm text-muted italic border-l-2 border-amber/30 pl-3">
|
||||
{task.reason}
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
onClick={() => router.push(`/practice?taskId=${task.id}`)}
|
||||
className="w-full py-3 px-6 rounded-lg bg-ink dark:bg-[#F3EFE8] text-cream dark:text-[#1E1A14] font-medium text-base transition-all hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2"
|
||||
>
|
||||
Begin practice
|
||||
</button>
|
||||
</div>
|
||||
) : (
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-4 text-center">
|
||||
<p className="text-sm text-muted">
|
||||
No task for today yet. Generate one to start your practice.
|
||||
</p>
|
||||
{genError && (
|
||||
<p className="text-sm text-error">{genError}</p>
|
||||
)}
|
||||
<button
|
||||
onClick={handleGenerate}
|
||||
disabled={generating}
|
||||
className="w-full py-3 px-6 rounded-lg bg-amber text-white font-medium text-base transition-all hover:bg-amber/90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2 disabled:opacity-50"
|
||||
>
|
||||
{generating ? "Generating..." : "Generate today's task"}
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function Greeting({ firstName }: { firstName: string | null }) {
|
||||
const hour = new Date().getHours();
|
||||
const greeting = hour < 12 ? "Good morning" : hour < 18 ? "Good afternoon" : "Good evening";
|
||||
const name = firstName || "there";
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl italic text-ink dark:text-[#F3EFE8]">
|
||||
{greeting}, {name}
|
||||
</h1>
|
||||
<p className="text-sm text-muted">Your practice is waiting.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTaskType(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
attention_training: "Attention",
|
||||
emotion_awareness: "Emotion",
|
||||
stress_regulation: "Stress",
|
||||
cognitive_clarity: "Clarity",
|
||||
integration: "Integration",
|
||||
};
|
||||
return labels[type] ?? type;
|
||||
}
|
||||
|
||||
function formatDifficulty(difficulty: number): string {
|
||||
const labels: Record<number, string> = {
|
||||
1: "Beginner",
|
||||
2: "Intermediate",
|
||||
3: "Advanced",
|
||||
};
|
||||
return labels[difficulty] ?? String(difficulty);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
"use client";
|
||||
|
||||
import { Suspense, useEffect, useState, useCallback } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
|
||||
interface TaskData {
|
||||
id: string;
|
||||
type: string;
|
||||
difficulty: number;
|
||||
duration: number;
|
||||
description: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export default function PracticePage() {
|
||||
return (
|
||||
<Suspense fallback={<LoadingFallback />}>
|
||||
<PracticeContent />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function LoadingFallback() {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-pulse text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PracticeContent() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
const taskId = searchParams.get("id");
|
||||
|
||||
const [task, setTask] = useState<TaskData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [phase, setPhase] = useState<"ready" | "active" | "done">("ready");
|
||||
const [secondsRemaining, setSecondsRemaining] = useState(0);
|
||||
const [elapsedSeconds, setElapsedSeconds] = useState(0);
|
||||
|
||||
useEffect(() => {
|
||||
if (!taskId) {
|
||||
router.push("/");
|
||||
return;
|
||||
}
|
||||
|
||||
fetch("/api/task/daily")
|
||||
.then((r) => r.json())
|
||||
.then((json) => {
|
||||
if (json.task && json.task.id === taskId) {
|
||||
setTask(json.task);
|
||||
setSecondsRemaining(json.task.duration * 60);
|
||||
} else {
|
||||
router.push("/");
|
||||
}
|
||||
})
|
||||
.catch(() => router.push("/"))
|
||||
.finally(() => setLoading(false));
|
||||
}, [taskId, router]);
|
||||
|
||||
// Timer
|
||||
useEffect(() => {
|
||||
if (phase !== "active") return;
|
||||
if (secondsRemaining <= 0) {
|
||||
setPhase("done");
|
||||
return;
|
||||
}
|
||||
const interval = setInterval(() => {
|
||||
setSecondsRemaining((s) => s - 1);
|
||||
setElapsedSeconds((s) => s + 1);
|
||||
}, 1000);
|
||||
return () => clearInterval(interval);
|
||||
}, [phase, secondsRemaining]);
|
||||
|
||||
const handleStart = () => setPhase("active");
|
||||
|
||||
const handleFinish = () => {
|
||||
router.push(
|
||||
`/reflect?taskId=${taskId}&duration=${elapsedSeconds}&type=${task?.type}&description=${encodeURIComponent(task?.description || "")}`
|
||||
);
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-pulse text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!task) return null;
|
||||
|
||||
const minutes = Math.floor(secondsRemaining / 60);
|
||||
const secs = secondsRemaining % 60;
|
||||
const progressPct =
|
||||
task.duration > 0
|
||||
? Math.min(100, Math.round((elapsedSeconds / (task.duration * 60)) * 100))
|
||||
: 0;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
{/* Back */}
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
className="text-sm text-muted hover:text-ink dark:hover:text-[#F3EFE8] transition-colors"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
|
||||
{/* Task display */}
|
||||
<div className="space-y-6">
|
||||
<div className="space-y-1">
|
||||
<span className="px-2 py-0.5 rounded-full text-xs font-medium bg-amber/10 text-amber">
|
||||
{formatTaskType(task.type)}
|
||||
</span>
|
||||
<h1 className="font-[family-name:var(--font-fraunces)] text-2xl italic text-ink dark:text-[#F3EFE8] mt-3">
|
||||
Your practice
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-4">
|
||||
<p className="text-lg leading-relaxed">{task.description}</p>
|
||||
<p className="text-sm text-muted italic leading-relaxed border-t border-stone dark:border-[#3D3528] pt-3">
|
||||
Why this task: {task.reason}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Timer */}
|
||||
<div className="flex flex-col items-center gap-6 py-4">
|
||||
{phase === "ready" && (
|
||||
<button
|
||||
onClick={handleStart}
|
||||
className="w-32 h-32 rounded-full bg-amber text-white font-medium text-lg flex items-center justify-center transition-all hover:scale-105 active:scale-95 focus:outline-none focus:ring-4 focus:ring-amber/30"
|
||||
>
|
||||
Start
|
||||
</button>
|
||||
)}
|
||||
|
||||
{phase === "active" && (
|
||||
<div className="space-y-6 flex flex-col items-center">
|
||||
<div className="relative w-32 h-32 rounded-full bg-sand dark:bg-[#292418] border-4 border-stone dark:border-[#3D3528] flex items-center justify-center">
|
||||
{/* Progress ring */}
|
||||
<svg
|
||||
className="absolute inset-0 w-full h-full -rotate-90"
|
||||
viewBox="0 0 128 128"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle
|
||||
cx="64"
|
||||
cy="64"
|
||||
r="58"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
strokeWidth="4"
|
||||
className="text-amber"
|
||||
strokeLinecap="round"
|
||||
strokeDasharray={`${2 * Math.PI * 58}`}
|
||||
strokeDashoffset={`${2 * Math.PI * 58 * (1 - progressPct / 100)}`}
|
||||
/>
|
||||
</svg>
|
||||
<span className="font-[family-name:var(--font-fraunces)] text-3xl tabular-nums text-ink dark:text-[#F3EFE8]">
|
||||
{String(minutes).padStart(2, "0")}:{String(secs).padStart(2, "0")}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleFinish}
|
||||
className="text-sm text-muted hover:text-ink dark:hover:text-[#F3EFE8] transition-colors underline underline-offset-4"
|
||||
>
|
||||
Finish early
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{phase === "done" && (
|
||||
<div className="space-y-6 flex flex-col items-center">
|
||||
<div className="w-32 h-32 rounded-full bg-sage/10 border-2 border-sage flex items-center justify-center">
|
||||
<svg
|
||||
width="40"
|
||||
height="40"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#7A9A8B"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm text-muted">Practice complete</p>
|
||||
<button
|
||||
onClick={handleFinish}
|
||||
className="py-3 px-8 rounded-lg bg-ink dark:bg-[#F3EFE8] text-cream dark:text-[#1E1A14] font-medium text-base transition-all hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2"
|
||||
>
|
||||
Reflect on your practice
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTaskType(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
attention_training: "Attention",
|
||||
emotion_awareness: "Emotion",
|
||||
stress_regulation: "Stress",
|
||||
cognitive_clarity: "Clarity",
|
||||
integration: "Integration",
|
||||
};
|
||||
return labels[type] ?? type;
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
import StatBar from "@/components/StatBar";
|
||||
import StageBadge from "@/components/StageBadge";
|
||||
|
||||
interface StateData {
|
||||
attentionLevel: number;
|
||||
emotionStability: number;
|
||||
stressLevel: number;
|
||||
practiceStage: number;
|
||||
consistencyScore: number;
|
||||
streakDays: number;
|
||||
totalSessionsCompleted: number;
|
||||
}
|
||||
|
||||
export default function ProgressPage() {
|
||||
const { isLoaded } = useUser();
|
||||
const [state, setState] = useState<StateData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoaded) return;
|
||||
fetch("/api/user/state")
|
||||
.then((r) => r.json())
|
||||
.then((json) => setState(json.state))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [isLoaded]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-pulse text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!state) {
|
||||
return (
|
||||
<div className="flex flex-col items-center justify-center min-h-[60vh] gap-2">
|
||||
<p className="text-muted text-sm">Complete onboarding to see your progress.</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const metrics = [
|
||||
{ label: "Attention", value: state.attentionLevel, color: "amber" as const },
|
||||
{ label: "Emotional Stability", value: state.emotionStability, color: "sage" as const },
|
||||
{ label: "Stress Level", value: state.stressLevel, color: "warning" as const },
|
||||
{ label: "Consistency", value: state.consistencyScore, color: "info" as const },
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1">
|
||||
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl italic text-ink dark:text-[#F3EFE8]">
|
||||
Your progress
|
||||
</h1>
|
||||
<p className="text-sm text-muted">
|
||||
Stage <StageBadge stage={state.practiceStage} />
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* Stats grid */}
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div className="p-4 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528]">
|
||||
<p className="text-xs text-muted uppercase tracking-wider font-medium">
|
||||
Streak
|
||||
</p>
|
||||
<p className="text-3xl font-bold tabular-nums text-amber font-[family-name:var(--font-fraunces)]">
|
||||
{state.streakDays}
|
||||
</p>
|
||||
<p className="text-xs text-muted mt-0.5">days</p>
|
||||
</div>
|
||||
<div className="p-4 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528]">
|
||||
<p className="text-xs text-muted uppercase tracking-wider font-medium">
|
||||
Sessions
|
||||
</p>
|
||||
<p className="text-3xl font-bold tabular-nums text-sage font-[family-name:var(--font-fraunces)]">
|
||||
{state.totalSessionsCompleted}
|
||||
</p>
|
||||
<p className="text-xs text-muted mt-0.5">total</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Metric bars */}
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-5">
|
||||
<h2 className="text-sm font-medium text-ink dark:text-[#F3EFE8]">
|
||||
Core metrics
|
||||
</h2>
|
||||
{metrics.map((m) => (
|
||||
<StatBar
|
||||
key={m.label}
|
||||
label={m.label}
|
||||
value={m.value}
|
||||
color={m.color}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
|
||||
{/* Stage info */}
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-2">
|
||||
<h2 className="text-sm font-medium text-ink dark:text-[#F3EFE8]">
|
||||
Stage progress
|
||||
</h2>
|
||||
<div className="flex gap-1.5">
|
||||
{[0, 1, 2, 3, 4].map((s) => (
|
||||
<div
|
||||
key={s}
|
||||
className={`flex-1 h-2 rounded-full ${
|
||||
s <= state.practiceStage
|
||||
? "bg-amber"
|
||||
: "bg-stone dark:bg-[#3D3528]"
|
||||
}`}
|
||||
/>
|
||||
))}
|
||||
</div>
|
||||
<div className="flex justify-between text-[10px] text-faded">
|
||||
<span>Beginner</span>
|
||||
<span>Foundation</span>
|
||||
<span>Growing</span>
|
||||
<span>Established</span>
|
||||
<span>Advanced</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,198 @@
|
||||
"use client";
|
||||
|
||||
import { useState, Suspense } from "react";
|
||||
import { useSearchParams, useRouter } from "next/navigation";
|
||||
|
||||
function ReflectForm() {
|
||||
const searchParams = useSearchParams();
|
||||
const router = useRouter();
|
||||
|
||||
const taskId = searchParams.get("taskId") || "";
|
||||
const duration = parseInt(searchParams.get("duration") || "0", 10);
|
||||
const taskType = searchParams.get("type") || "";
|
||||
const taskDescription = searchParams.get("description") || "";
|
||||
|
||||
const [notes, setNotes] = useState("");
|
||||
const [submitting, setSubmitting] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [result, setResult] = useState<{
|
||||
focusScore: number;
|
||||
emotionScore: number;
|
||||
insight: string;
|
||||
} | null>(null);
|
||||
|
||||
const handleSubmit = async () => {
|
||||
if (!taskId) return;
|
||||
setSubmitting(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/session/complete", {
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
taskId,
|
||||
notes,
|
||||
durationSeconds: duration,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!res.ok) {
|
||||
const json = await res.json();
|
||||
throw new Error(json.error || "Failed to save reflection");
|
||||
}
|
||||
|
||||
const json = await res.json();
|
||||
setResult(json.reflection);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : "Something went wrong");
|
||||
} finally {
|
||||
setSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!taskId) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
className="text-muted text-sm underline"
|
||||
>
|
||||
No session to reflect on. Go home.
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
// After successful submission
|
||||
if (result) {
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-2 text-center">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-sage/10 border-2 border-sage flex items-center justify-center">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#7A9A8B"
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
</div>
|
||||
<h1 className="font-[family-name:var(--font-fraunces)] text-2xl italic text-ink dark:text-[#F3EFE8]">
|
||||
Practice complete
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-4">
|
||||
<p className="text-base leading-relaxed italic text-muted">
|
||||
“{result.insight}”
|
||||
</p>
|
||||
<div className="flex gap-4 text-xs text-muted pt-3 border-t border-stone dark:border-[#3D3528]">
|
||||
<span>
|
||||
<span className="text-ink dark:text-[#F3EFE8] font-bold">
|
||||
{result.focusScore}/5
|
||||
</span>{" "}
|
||||
focus
|
||||
</span>
|
||||
<span>
|
||||
<span className="text-ink dark:text-[#F3EFE8] font-bold">
|
||||
{result.emotionScore}/5
|
||||
</span>{" "}
|
||||
emotional awareness
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => router.push("/")}
|
||||
className="w-full py-3 px-6 rounded-lg bg-ink dark:bg-[#F3EFE8] text-cream dark:text-[#1E1A14] font-medium text-base transition-all hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2"
|
||||
>
|
||||
Back home
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1">
|
||||
<button
|
||||
onClick={() => router.back()}
|
||||
className="text-sm text-muted hover:text-ink dark:hover:text-[#F3EFE8] transition-colors"
|
||||
>
|
||||
← Back
|
||||
</button>
|
||||
<h1 className="font-[family-name:var(--font-fraunces)] text-2xl italic text-ink dark:text-[#F3EFE8] mt-3">
|
||||
Reflect on your practice
|
||||
</h1>
|
||||
<p className="text-sm text-muted">
|
||||
{formatTaskType(taskType)} · {Math.floor(duration / 60)}m{" "}
|
||||
{duration % 60}s
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="space-y-4">
|
||||
<label
|
||||
htmlFor="reflection-notes"
|
||||
className="block text-sm font-medium text-muted"
|
||||
>
|
||||
What did you notice during your practice?
|
||||
</label>
|
||||
<textarea
|
||||
id="reflection-notes"
|
||||
value={notes}
|
||||
onChange={(e) => setNotes(e.target.value)}
|
||||
rows={5}
|
||||
placeholder="Were you focused or distracted? What emotions came up? Did anything surprise you?"
|
||||
className="w-full p-4 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] text-ink dark:text-[#F3EFE8] text-base placeholder:text-faded resize-none focus:outline-none focus:ring-2 focus:ring-amber"
|
||||
autoFocus
|
||||
/>
|
||||
|
||||
{error && (
|
||||
<div className="p-3 rounded-lg bg-error/10 border border-error/20 text-error text-sm">
|
||||
{error}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={submitting}
|
||||
className="w-full py-3 px-6 rounded-lg bg-ink dark:bg-[#F3EFE8] text-cream dark:text-[#1E1A14] font-medium text-base transition-all hover:opacity-90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2 disabled:opacity-50"
|
||||
>
|
||||
{submitting ? "Saving..." : "Complete practice"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export default function ReflectPage() {
|
||||
return (
|
||||
<Suspense
|
||||
fallback={
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-pulse text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
}
|
||||
>
|
||||
<ReflectForm />
|
||||
</Suspense>
|
||||
);
|
||||
}
|
||||
|
||||
function formatTaskType(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
attention_training: "Attention",
|
||||
emotion_awareness: "Emotion",
|
||||
stress_regulation: "Stress",
|
||||
cognitive_clarity: "Clarity",
|
||||
integration: "Integration",
|
||||
};
|
||||
return labels[type] ?? type;
|
||||
}
|
||||
@@ -0,0 +1,171 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
interface Reward {
|
||||
id: string;
|
||||
type: string;
|
||||
message: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function RewardsPage() {
|
||||
const { isLoaded } = useUser();
|
||||
const [rewards, setRewards] = useState<Reward[]>([]);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoaded) return;
|
||||
fetch("/api/rewards?limit=50")
|
||||
.then((r) => r.json())
|
||||
.then((json) => setRewards(json.rewards || []))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [isLoaded]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-pulse text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1">
|
||||
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl italic text-ink dark:text-[#F3EFE8]">
|
||||
Rewards
|
||||
</h1>
|
||||
<p className="text-sm text-muted">
|
||||
{rewards.length === 0
|
||||
? "Your rewards will appear here as you practice."
|
||||
: `${rewards.length} reward${rewards.length === 1 ? "" : "s"} earned`}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{rewards.length === 0 ? (
|
||||
<div className="text-center py-12">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] flex items-center justify-center">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#C8834A"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm text-muted mt-4">
|
||||
Complete your first practice to earn a reward.
|
||||
</p>
|
||||
</div>
|
||||
) : (
|
||||
<div className="space-y-3">
|
||||
{rewards.map((reward) => (
|
||||
<div
|
||||
key={reward.id}
|
||||
className="p-4 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] flex gap-3"
|
||||
>
|
||||
<div className="mt-0.5 flex-shrink-0">
|
||||
<RewardIcon type={reward.type} />
|
||||
</div>
|
||||
<div className="space-y-1">
|
||||
<p className="text-sm font-medium text-ink dark:text-[#F3EFE8]">
|
||||
{formatRewardType(reward.type)}
|
||||
</p>
|
||||
<p className="text-sm text-muted leading-relaxed">
|
||||
{reward.message}
|
||||
</p>
|
||||
<p className="text-xs text-faded">
|
||||
{new Date(reward.createdAt).toLocaleDateString("en-US", {
|
||||
month: "short",
|
||||
day: "numeric",
|
||||
})}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RewardIcon({ type }: { type: string }) {
|
||||
const stroke = type === "first_session" || type === "stage_up" ? "#C8834A" : "#7A9A8B";
|
||||
|
||||
switch (type) {
|
||||
case "first_session":
|
||||
case "stage_up":
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={stroke}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||
</svg>
|
||||
);
|
||||
case "streak_3":
|
||||
case "streak_7":
|
||||
case "streak_14":
|
||||
case "streak_30":
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={stroke}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M17.5 19H9a7 7 0 1 1 6.71-9h1.79a4.5 4.5 0 1 1 0 9Z" />
|
||||
</svg>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={stroke}
|
||||
strokeWidth="2"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline points="20 6 9 17 4 12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function formatRewardType(type: string): string {
|
||||
const labels: Record<string, string> = {
|
||||
daily_completion: "Daily Practice",
|
||||
streak_3: "3-Day Streak",
|
||||
streak_7: "7-Day Streak",
|
||||
streak_14: "14-Day Streak",
|
||||
streak_30: "30-Day Streak",
|
||||
stage_up: "Stage Advance",
|
||||
first_session: "First Practice",
|
||||
};
|
||||
return labels[type] ?? type;
|
||||
}
|
||||
@@ -0,0 +1,145 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser, SignOutButton } from "@clerk/nextjs";
|
||||
|
||||
interface StateData {
|
||||
attentionLevel: number;
|
||||
emotionStability: number;
|
||||
stressLevel: number;
|
||||
practiceStage: number;
|
||||
consistencyScore: number;
|
||||
streakDays: number;
|
||||
totalSessionsCompleted: number;
|
||||
onboardingCompleted: boolean;
|
||||
}
|
||||
|
||||
export default function SettingsPage() {
|
||||
const { user, isLoaded: userLoaded } = useUser();
|
||||
const [state, setState] = useState<StateData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
const [completing, setCompleting] = useState(false);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!userLoaded) return;
|
||||
fetch("/api/user/state")
|
||||
.then((r) => r.json())
|
||||
.then((json) => setState(json.state))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [userLoaded]);
|
||||
|
||||
const handleOnboarding = async () => {
|
||||
setCompleting(true);
|
||||
setMessage(null);
|
||||
try {
|
||||
const res = await fetch("/api/user/onboarding", { method: "POST" });
|
||||
if (!res.ok) throw new Error("Failed");
|
||||
const json = await res.json();
|
||||
setState(json.state);
|
||||
setMessage("You're all set. Head home to start your practice.");
|
||||
} catch {
|
||||
setMessage("Something went wrong. Try again.");
|
||||
} finally {
|
||||
setCompleting(false);
|
||||
}
|
||||
};
|
||||
|
||||
if (!userLoaded || loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-pulse text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
const needsOnboarding = state && !state.onboardingCompleted;
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1">
|
||||
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl italic text-ink dark:text-[#F3EFE8]">
|
||||
Settings
|
||||
</h1>
|
||||
</div>
|
||||
|
||||
{/* Onboarding */}
|
||||
{needsOnboarding && (
|
||||
<div className="p-5 rounded-lg bg-amber/5 border border-amber/20 space-y-4">
|
||||
<div>
|
||||
<h2 className="text-base font-medium text-ink dark:text-[#F3EFE8]">
|
||||
Welcome to Inner OS
|
||||
</h2>
|
||||
<p className="text-sm text-muted mt-1">
|
||||
Inner OS generates one personalized mental training task each day,
|
||||
guided by your practice history and inner development stage. Your
|
||||
reflections shape what comes next. Everything is private — just you
|
||||
and your practice.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={handleOnboarding}
|
||||
disabled={completing}
|
||||
className="py-2.5 px-5 rounded-lg bg-amber text-white font-medium text-sm transition-all hover:bg-amber/90 active:scale-[0.98] focus:outline-none focus:ring-2 focus:ring-amber focus:ring-offset-2 disabled:opacity-50"
|
||||
>
|
||||
{completing ? "Setting up..." : "Get started"}
|
||||
</button>
|
||||
{message && (
|
||||
<p className="text-sm text-sage">{message}</p>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Profile */}
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-3">
|
||||
<h2 className="text-sm font-medium text-ink dark:text-[#F3EFE8]">
|
||||
Profile
|
||||
</h2>
|
||||
<div className="space-y-1 text-sm">
|
||||
<p className="text-muted">
|
||||
Signed in as{" "}
|
||||
<span className="text-ink dark:text-[#F3EFE8] font-medium">
|
||||
{user.primaryEmailAddress?.emailAddress || user.firstName || "User"}
|
||||
</span>
|
||||
</p>
|
||||
{state?.onboardingCompleted && (
|
||||
<p className="text-muted">
|
||||
Stage {state.practiceStage} · {state.totalSessionsCompleted} sessions
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* About */}
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] space-y-2">
|
||||
<h2 className="text-sm font-medium text-ink dark:text-[#F3EFE8]">
|
||||
About Inner OS
|
||||
</h2>
|
||||
<div className="text-sm text-muted space-y-2">
|
||||
<p>
|
||||
Inner OS is a private inner development practice. You receive one
|
||||
personalized mental training task each day, tailored to your current
|
||||
state and development stage.
|
||||
</p>
|
||||
<p>
|
||||
Your reflections are private. Your data is yours. There are no
|
||||
leaderboards, no social features, no competition — just you and the
|
||||
work of becoming more aware, more stable, and more yourself.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Sign out */}
|
||||
<div className="pt-2">
|
||||
<SignOutButton>
|
||||
<button className="w-full py-3 px-6 rounded-lg border border-stone dark:border-[#3D3528] text-muted hover:text-error transition-colors font-medium text-sm">
|
||||
Sign out
|
||||
</button>
|
||||
</SignOutButton>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
"use client";
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useUser } from "@clerk/nextjs";
|
||||
|
||||
interface SummaryData {
|
||||
summary: string;
|
||||
weekStart: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
export default function SummaryPage() {
|
||||
const { isLoaded } = useUser();
|
||||
const [summary, setSummary] = useState<SummaryData | null>(null);
|
||||
const [loading, setLoading] = useState(true);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoaded) return;
|
||||
fetch("/api/summary/weekly")
|
||||
.then((r) => r.json())
|
||||
.then((json) => setSummary(json.summary))
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
}, [isLoaded]);
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="flex items-center justify-center min-h-[60vh]">
|
||||
<div className="animate-pulse text-muted text-sm">Loading...</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-8">
|
||||
<div className="space-y-1">
|
||||
<h1 className="font-[family-name:var(--font-fraunces)] text-3xl italic text-ink dark:text-[#F3EFE8]">
|
||||
Weekly summary
|
||||
</h1>
|
||||
<p className="text-sm text-muted">
|
||||
{summary
|
||||
? `Week of ${new Date(summary.weekStart).toLocaleDateString("en-US", {
|
||||
month: "long",
|
||||
day: "numeric",
|
||||
})}`
|
||||
: "Generated each week from your practice history"}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{summary ? (
|
||||
<div className="p-5 rounded-lg bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528]">
|
||||
<div
|
||||
className="prose prose-sm dark:prose-invert max-w-none space-y-3 text-ink dark:text-[#F3EFE8] leading-relaxed"
|
||||
dangerouslySetInnerHTML={{ __html: summary.summary.replace(/\n\n/g, "</p><p>").replace(/\n/g, "<br/>") }}
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-12 space-y-4">
|
||||
<div className="w-16 h-16 mx-auto rounded-full bg-sand dark:bg-[#292418] border border-stone dark:border-[#3D3528] flex items-center justify-center">
|
||||
<svg
|
||||
width="28"
|
||||
height="28"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="#7A9A8B"
|
||||
strokeWidth="1.5"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
||||
</svg>
|
||||
</div>
|
||||
<p className="text-sm text-muted max-w-xs mx-auto">
|
||||
Your first weekly summary will be generated after you complete at least one practice session. Summaries run automatically each week.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
const stageLabels: Record<number, { label: string; color: string }> = {
|
||||
0: { label: "Beginner", color: "text-muted bg-stone" },
|
||||
1: { label: "Foundation", color: "text-amber bg-amber/10" },
|
||||
2: { label: "Growing", color: "text-sage bg-sage/10" },
|
||||
3: { label: "Established", color: "text-info bg-info/10" },
|
||||
4: { label: "Advanced", color: "text-ink bg-amber/15" },
|
||||
};
|
||||
|
||||
export default function StageBadge({ stage }: { stage: number }) {
|
||||
const info = stageLabels[stage] ?? stageLabels[0];
|
||||
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center gap-1 px-2.5 py-0.5 rounded-full text-xs font-medium ${info.color}`}
|
||||
>
|
||||
<span className="w-1.5 h-1.5 rounded-full bg-current opacity-60" />
|
||||
{info.label}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
interface StatBarProps {
|
||||
label: string;
|
||||
value: number;
|
||||
max?: number;
|
||||
color?: "amber" | "sage" | "info" | "warning";
|
||||
}
|
||||
|
||||
const colorMap = {
|
||||
amber: "bg-amber",
|
||||
sage: "bg-sage",
|
||||
info: "bg-info",
|
||||
warning: "bg-warning",
|
||||
};
|
||||
|
||||
export default function StatBar({
|
||||
label,
|
||||
value,
|
||||
max = 100,
|
||||
color = "amber",
|
||||
}: StatBarProps) {
|
||||
const pct = Math.max(0, Math.min(100, Math.round((value / max) * 100)));
|
||||
|
||||
return (
|
||||
<div className="space-y-1">
|
||||
<div className="flex justify-between text-xs">
|
||||
<span className="text-muted font-medium">{label}</span>
|
||||
<span className="text-ink font-bold tabular-nums">{Math.round(value)}</span>
|
||||
</div>
|
||||
<div
|
||||
className="h-1.5 rounded-full bg-stone overflow-hidden"
|
||||
role="progressbar"
|
||||
aria-valuenow={Math.round(value)}
|
||||
aria-valuemin={0}
|
||||
aria-valuemax={max}
|
||||
aria-label={label}
|
||||
>
|
||||
<div
|
||||
className={`h-full rounded-full transition-all duration-500 ${colorMap[color]}`}
|
||||
style={{ width: `${pct}%` }}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,149 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { usePathname } from "next/navigation";
|
||||
|
||||
const tabs = [
|
||||
{ href: "/", label: "Today", icon: SunIcon },
|
||||
{ href: "/progress", label: "Progress", icon: ChartIcon },
|
||||
{ href: "/rewards", label: "Rewards", icon: StarIcon },
|
||||
{ href: "/summary", label: "Summary", icon: BookIcon },
|
||||
{ href: "/settings", label: "Settings", icon: GearIcon },
|
||||
];
|
||||
|
||||
export default function TabBar() {
|
||||
const pathname = usePathname();
|
||||
|
||||
return (
|
||||
<nav
|
||||
className="fixed bottom-0 left-0 right-0 z-50 border-t border-stone bg-cream dark:bg-[#1E1A14] dark:border-[#3D3528] safe-area-bottom"
|
||||
role="navigation"
|
||||
aria-label="Main navigation"
|
||||
>
|
||||
<ul className="flex items-center justify-around max-w-2xl mx-auto">
|
||||
{tabs.map((tab) => {
|
||||
const active =
|
||||
tab.href === "/"
|
||||
? pathname === "/"
|
||||
: pathname.startsWith(tab.href);
|
||||
return (
|
||||
<li key={tab.href} className="flex-1">
|
||||
<Link
|
||||
href={tab.href}
|
||||
className={`flex flex-col items-center gap-0.5 py-2.5 px-1 text-xs font-medium transition-colors ${
|
||||
active
|
||||
? "text-amber"
|
||||
: "text-faded hover:text-muted"
|
||||
}`}
|
||||
aria-current={active ? "page" : undefined}
|
||||
>
|
||||
<tab.icon active={active} />
|
||||
{tab.label}
|
||||
</Link>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
);
|
||||
}
|
||||
|
||||
function SunIcon({ active }: { active: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={active ? "#C8834A" : "#9B8E7A"}
|
||||
strokeWidth={active ? 2 : 1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="5" />
|
||||
<line x1="12" y1="1" x2="12" y2="3" />
|
||||
<line x1="12" y1="21" x2="12" y2="23" />
|
||||
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
|
||||
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
|
||||
<line x1="1" y1="12" x2="3" y2="12" />
|
||||
<line x1="21" y1="12" x2="23" y2="12" />
|
||||
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
|
||||
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function ChartIcon({ active }: { active: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={active ? "#C8834A" : "#9B8E7A"}
|
||||
strokeWidth={active ? 2 : 1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polyline points="22 12 18 12 15 21 9 3 6 12 2 12" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function StarIcon({ active }: { active: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill={active ? "#C8834A" : "none"}
|
||||
stroke={active ? "#C8834A" : "#9B8E7A"}
|
||||
strokeWidth={active ? 2 : 1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<polygon points="12 2 15.09 8.26 22 9.27 17 14.14 18.18 21.02 12 17.77 5.82 21.02 7 14.14 2 9.27 8.91 8.26 12 2" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function BookIcon({ active }: { active: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={active ? "#C8834A" : "#9B8E7A"}
|
||||
strokeWidth={active ? 2 : 1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20" />
|
||||
<path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
|
||||
function GearIcon({ active }: { active: boolean }) {
|
||||
return (
|
||||
<svg
|
||||
width="20"
|
||||
height="20"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke={active ? "#C8834A" : "#9B8E7A"}
|
||||
strokeWidth={active ? 2 : 1.5}
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<circle cx="12" cy="12" r="3" />
|
||||
<path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 0 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 0 1-2.83-2.83l.06-.06A1.65 1.65 0 0 0 4.68 15a1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 0 1 2.83-2.83l.06.06A1.65 1.65 0 0 0 9 4.68a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 0 1 2.83 2.83l-.06.06A1.65 1.65 0 0 0 19.4 9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z" />
|
||||
</svg>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
import type { UserState } from "@/generated/prisma/client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// T3: Stage advancement rules
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Practice stages:
|
||||
* 0 — Beginner (just started, < 5 sessions)
|
||||
* 1 — Foundation (5-14 sessions, building habit)
|
||||
* 2 — Growing (15-29 sessions, consistent practice)
|
||||
* 3 — Established (30-59 sessions, deepening)
|
||||
* 4 — Advanced (60+ sessions, integration focus)
|
||||
*/
|
||||
|
||||
const STAGE_THRESHOLDS: Record<number, { sessions: number; consistency: number }> = {
|
||||
0: { sessions: 0, consistency: 0 },
|
||||
1: { sessions: 5, consistency: 20 },
|
||||
2: { sessions: 15, consistency: 35 },
|
||||
3: { sessions: 30, consistency: 50 },
|
||||
4: { sessions: 60, consistency: 65 },
|
||||
};
|
||||
|
||||
export function computeStage(
|
||||
totalSessions: number,
|
||||
consistencyScore: number
|
||||
): { stage: number; advanced: boolean } {
|
||||
let stage = 0;
|
||||
for (const [s, threshold] of Object.entries(STAGE_THRESHOLDS)) {
|
||||
const stageNum = parseInt(s);
|
||||
if (
|
||||
totalSessions >= threshold.sessions &&
|
||||
consistencyScore >= threshold.consistency
|
||||
) {
|
||||
stage = Math.max(stage, stageNum);
|
||||
}
|
||||
}
|
||||
return { stage, advanced: stage > 0 };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Streak computation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface StreakResult {
|
||||
streakDays: number;
|
||||
streakBroken: boolean;
|
||||
lastSessionDate: Date | null;
|
||||
}
|
||||
|
||||
export function computeStreak(
|
||||
lastSessionDate: Date | null,
|
||||
previousStreak: number,
|
||||
now: Date = new Date()
|
||||
): StreakResult {
|
||||
if (!lastSessionDate) {
|
||||
return { streakDays: 0, streakBroken: false, lastSessionDate: null };
|
||||
}
|
||||
|
||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
const lastDate = new Date(
|
||||
lastSessionDate.getFullYear(),
|
||||
lastSessionDate.getMonth(),
|
||||
lastSessionDate.getDate()
|
||||
);
|
||||
|
||||
const diffDays = Math.floor(
|
||||
(today.getTime() - lastDate.getTime()) / (1000 * 60 * 60 * 24)
|
||||
);
|
||||
|
||||
if (diffDays === 0) {
|
||||
// Already completed today — maintain current streak
|
||||
return { streakDays: previousStreak, streakBroken: false, lastSessionDate };
|
||||
}
|
||||
|
||||
if (diffDays === 1) {
|
||||
// Yesterday — streak continues
|
||||
return { streakDays: previousStreak + 1, streakBroken: false, lastSessionDate };
|
||||
}
|
||||
|
||||
// More than 1 day gap — streak broken
|
||||
return { streakDays: 1, streakBroken: true, lastSessionDate };
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Consistency score
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Consistency score (0-100) reflects how regularly the user practices.
|
||||
* - Each day with a session in the last 14 days contributes ~7.14 points
|
||||
* - Capped at 100
|
||||
*/
|
||||
export function computeConsistency(
|
||||
sessionDates: Date[],
|
||||
lookbackDays: number = 14
|
||||
): number {
|
||||
const now = new Date();
|
||||
const cutoff = new Date(now.getTime() - lookbackDays * 24 * 60 * 60 * 1000);
|
||||
|
||||
const recentDates = new Set(
|
||||
sessionDates
|
||||
.filter((d) => d >= cutoff)
|
||||
.map((d) => {
|
||||
const local = new Date(d);
|
||||
return `${local.getFullYear()}-${local.getMonth()}-${local.getDate()}`;
|
||||
})
|
||||
);
|
||||
|
||||
return Math.min(Math.round((recentDates.size / lookbackDays) * 100), 100);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Full state update after session completion
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface StateUpdate {
|
||||
attentionLevel: number;
|
||||
emotionStability: number;
|
||||
stressLevel: number;
|
||||
practiceStage: number;
|
||||
consistencyScore: number;
|
||||
streakDays: number;
|
||||
totalSessionsCompleted: number;
|
||||
stageAdvanced: boolean;
|
||||
streakMilestone: boolean;
|
||||
}
|
||||
|
||||
export function computeStateUpdate(
|
||||
currentState: UserState,
|
||||
deltas: {
|
||||
attentionDelta: number;
|
||||
emotionDelta: number;
|
||||
stressDelta: number;
|
||||
},
|
||||
recentSessionDates: Date[],
|
||||
now: Date = new Date()
|
||||
): StateUpdate {
|
||||
// Apply deltas with clamping
|
||||
const attentionLevel = Math.max(
|
||||
0,
|
||||
Math.min(100, currentState.attentionLevel + deltas.attentionDelta)
|
||||
);
|
||||
const emotionStability = Math.max(
|
||||
0,
|
||||
Math.min(100, currentState.emotionStability + deltas.emotionDelta)
|
||||
);
|
||||
const stressLevel = Math.max(
|
||||
0,
|
||||
Math.min(100, currentState.stressLevel + deltas.stressDelta)
|
||||
);
|
||||
|
||||
// Compute streak
|
||||
const streak = computeStreak(
|
||||
currentState.lastSessionDate,
|
||||
currentState.streakDays,
|
||||
now
|
||||
);
|
||||
|
||||
// Compute consistency
|
||||
const consistencyScore = computeConsistency(recentSessionDates);
|
||||
|
||||
// Compute stage
|
||||
const totalSessionsCompleted = currentState.totalSessionsCompleted + 1;
|
||||
const newStage = computeStage(totalSessionsCompleted, consistencyScore);
|
||||
const stageAdvanced = newStage.stage > currentState.practiceStage;
|
||||
|
||||
// Check streak milestones (3, 7, 14, 30)
|
||||
const streakMilestone =
|
||||
streak.streakDays > 0 &&
|
||||
[3, 7, 14, 30].includes(streak.streakDays) &&
|
||||
streak.streakDays > currentState.streakDays;
|
||||
|
||||
return {
|
||||
attentionLevel,
|
||||
emotionStability,
|
||||
stressLevel,
|
||||
practiceStage: newStage.stage,
|
||||
consistencyScore,
|
||||
streakDays: streak.streakDays,
|
||||
totalSessionsCompleted,
|
||||
stageAdvanced,
|
||||
streakMilestone,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
import OpenAI from "openai";
|
||||
import { keywordCheck, llmSafetyCheck, type SafetyResult } from "@/lib/safety";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface ReflectionInput {
|
||||
notes: string;
|
||||
durationSeconds: number;
|
||||
taskType: string;
|
||||
taskDescription: string;
|
||||
}
|
||||
|
||||
interface ReflectionOutput {
|
||||
focusScore: number; // 1-5 scale
|
||||
emotionScore: number; // 1-5 scale
|
||||
insight: string; // short reflection on what the user noticed
|
||||
safety: SafetyResult;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// T1: State update formulas
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* attention_delta = (focus_score - 3) * 5
|
||||
* Range: -10 (focus_score=1) to +10 (focus_score=5)
|
||||
* A score of 3 means no change (neutral baseline)
|
||||
*/
|
||||
export function computeAttentionDelta(focusScore: number): number {
|
||||
return (focusScore - 3) * 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* emotion_delta = (emotion_score - 3) * 5
|
||||
* Range: -10 (emotion_score=1) to +10 (emotion_score=5)
|
||||
* A score of 3 means no change (neutral baseline)
|
||||
*/
|
||||
export function computeEmotionDelta(emotionScore: number): number {
|
||||
return (emotionScore - 3) * 5;
|
||||
}
|
||||
|
||||
/**
|
||||
* Stress moves inversely — higher focus and emotional awareness reduce stress.
|
||||
* stress_delta = -((focus_score + emotion_score) / 2 - 3) * 4
|
||||
* Range: -8 to +8, biased toward stress reduction
|
||||
*/
|
||||
export function computeStressDelta(focusScore: number, emotionScore: number): number {
|
||||
const avg = (focusScore + emotionScore) / 2;
|
||||
return -Math.round((avg - 3) * 4);
|
||||
}
|
||||
|
||||
/** Clamp a value between min and max */
|
||||
function clamp(value: number, min: number, max: number): number {
|
||||
return Math.max(min, Math.min(max, value));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LLM reflection analysis (LLM #2)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const REFLECTION_PROMPT = `You are a compassionate inner development coach analyzing a user's practice reflection. Your job is to extract structured feedback from their free-text reflection.
|
||||
|
||||
You will receive:
|
||||
- The user's reflection notes (what they wrote after completing their mental training task)
|
||||
- The task type and description (what they were practicing)
|
||||
- How long they practiced (in seconds)
|
||||
|
||||
Respond with a JSON object containing:
|
||||
- focus_score: 1-5 rating of how focused/attentive they seemed during practice (1 = completely distracted, 5 = deeply focused)
|
||||
- emotion_score: 1-5 rating of emotional engagement/insight from their reflection (1 = disconnected/numb, 5 = deeply connected/insightful)
|
||||
- insight: One sentence distilling the most meaningful thing they noticed or learned (second person "you")
|
||||
|
||||
Score calibration:
|
||||
- A reflection like "it was fine" with no detail → focus_score 2-3, emotion_score 2-3
|
||||
- A reflection with specific observations and genuine curiosity → focus_score 4-5, emotion_score 4-5
|
||||
- A reflection mentioning frustration or struggle → lower focus but don't punish emotion if they're honest about it
|
||||
- A completely empty or one-word reflection → focus_score 1, emotion_score 1
|
||||
- A reflection that shows they really tried but found it hard → focus_score 3-4, emotion_score 3-4 (effort counts)
|
||||
|
||||
Be generous but honest. These scores guide their development path.`;
|
||||
|
||||
export async function analyzeReflection(
|
||||
input: ReflectionInput,
|
||||
openai: OpenAI
|
||||
): Promise<ReflectionOutput> {
|
||||
// Safety check on user input
|
||||
const keywordResult = keywordCheck(input.notes);
|
||||
if (!keywordResult.safe) {
|
||||
return {
|
||||
focusScore: 3,
|
||||
emotionScore: 3,
|
||||
insight: "Thank you for sharing.",
|
||||
safety: keywordResult,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: REFLECTION_PROMPT },
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
reflection: input.notes || "(no reflection written)",
|
||||
task_type: input.taskType,
|
||||
task_description: input.taskDescription,
|
||||
duration_seconds: input.durationSeconds,
|
||||
}),
|
||||
},
|
||||
],
|
||||
response_format: {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: "reflection",
|
||||
strict: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
focus_score: { type: "number", minimum: 1, maximum: 5 },
|
||||
emotion_score: { type: "number", minimum: 1, maximum: 5 },
|
||||
insight: { type: "string" },
|
||||
},
|
||||
required: ["focus_score", "emotion_score", "insight"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
temperature: 0.5,
|
||||
max_tokens: 200,
|
||||
});
|
||||
|
||||
const raw = response.choices[0]?.message?.content;
|
||||
if (!raw) throw new Error("Empty LLM response");
|
||||
|
||||
const parsed = JSON.parse(raw);
|
||||
|
||||
// Validate and clamp scores
|
||||
const focusScore = clamp(Math.round(parsed.focus_score), 1, 5);
|
||||
const emotionScore = clamp(Math.round(parsed.emotion_score), 1, 5);
|
||||
const insight =
|
||||
typeof parsed.insight === "string" && parsed.insight.length > 0
|
||||
? parsed.insight
|
||||
: "You completed your practice today.";
|
||||
|
||||
// LLM safety check on generated insight
|
||||
const llmSafety = await llmSafetyCheck(insight, openai as any);
|
||||
|
||||
return {
|
||||
focusScore,
|
||||
emotionScore,
|
||||
insight: llmSafety.safe ? insight : "You completed your practice today.",
|
||||
safety: llmSafety,
|
||||
};
|
||||
} catch {
|
||||
// Fallback: neutral scores with generic insight
|
||||
return {
|
||||
focusScore: 3,
|
||||
emotionScore: 3,
|
||||
insight: "You showed up today. That's what matters.",
|
||||
safety: { safe: true },
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Full reflection processing — analyzes, computes deltas, updates state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface ProcessedReflection {
|
||||
focusScore: number;
|
||||
emotionScore: number;
|
||||
insight: string;
|
||||
attentionDelta: number;
|
||||
emotionDelta: number;
|
||||
stressDelta: number;
|
||||
safety: SafetyResult;
|
||||
}
|
||||
|
||||
export async function processReflection(
|
||||
input: ReflectionInput,
|
||||
currentState: {
|
||||
attentionLevel: number;
|
||||
emotionStability: number;
|
||||
stressLevel: number;
|
||||
},
|
||||
openai: OpenAI
|
||||
): Promise<ProcessedReflection> {
|
||||
const analysis = await analyzeReflection(input, openai);
|
||||
|
||||
return {
|
||||
focusScore: analysis.focusScore,
|
||||
emotionScore: analysis.emotionScore,
|
||||
insight: analysis.insight,
|
||||
attentionDelta: computeAttentionDelta(analysis.focusScore),
|
||||
emotionDelta: computeEmotionDelta(analysis.emotionScore),
|
||||
stressDelta: computeStressDelta(analysis.focusScore, analysis.emotionScore),
|
||||
safety: analysis.safety,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,153 @@
|
||||
import type { RewardEventType } from "@/generated/prisma/client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface RewardEvent {
|
||||
type: RewardEventType;
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface RewardContext {
|
||||
isFirstSession: boolean;
|
||||
streakMilestone: boolean;
|
||||
streakDays: number;
|
||||
stageAdvanced: boolean;
|
||||
newStage: number;
|
||||
totalSessionsCompleted: number;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reward messages
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const FIRST_SESSION_MESSAGES = [
|
||||
"Your first practice is complete. This is where the path begins — not with a destination, but with a single step.",
|
||||
"Day one. You didn't just think about it — you did it. That's the difference that makes all the difference.",
|
||||
"Welcome to your practice. The door is open now. Walk through it again tomorrow.",
|
||||
];
|
||||
|
||||
const STREAK_3_MESSAGES = [
|
||||
"Three days in a row. A pattern is forming. Your mind is starting to expect this — that's the habit taking root.",
|
||||
"Day three. The first two days were willpower. Today is the beginning of momentum.",
|
||||
];
|
||||
|
||||
const STREAK_7_MESSAGES = [
|
||||
"Seven days. A full week of showing up for yourself. Most people never make it this far. You did.",
|
||||
"One week of daily practice. Your brain is literally rewiring. Neuroplasticity isn't a metaphor — it's what's happening right now.",
|
||||
];
|
||||
|
||||
const STREAK_14_MESSAGES = [
|
||||
"Two weeks. Research says it takes 21 days to form a habit — you're two-thirds there, and it already feels different, doesn't it?",
|
||||
"Fourteen consecutive days. You're not 'trying to meditate' anymore. You're someone who practices. The identity has shifted.",
|
||||
];
|
||||
|
||||
const STREAK_30_MESSAGES = [
|
||||
"Thirty days. A month of daily practice. Whatever you were before you started — anxious, scattered, reactive — you're becoming someone else now. Someone who can watch their own mind and choose.",
|
||||
"One month. This isn't a streak anymore. It's a practice. It's part of who you are. The question is no longer 'will I practice today?' but 'when?'",
|
||||
];
|
||||
|
||||
const STAGE_UP_MESSAGES: Record<number, string[]> = {
|
||||
1: [
|
||||
"You've reached the Foundation stage. The basics are in place. Now we build depth — longer sessions, subtler awareness.",
|
||||
"Stage up: Foundation. You've moved from trying to training. The practice is becoming familiar territory.",
|
||||
],
|
||||
2: [
|
||||
"Growing stage reached. Your consistency is solid and your awareness is sharpening. This is where practice starts to change how you move through the world.",
|
||||
"Stage up: Growing. You're past the awkward beginner phase. Your sessions have texture and depth now.",
|
||||
],
|
||||
3: [
|
||||
"Established. Fifteen sessions and strong consistency. Your inner world is becoming legible to you. This is emotional literacy in action.",
|
||||
"Stage up: Established. Your practice has roots. What started as a daily exercise is becoming a way of being.",
|
||||
],
|
||||
4: [
|
||||
"Advanced. This is rare territory. Most people never develop this level of inner awareness. Your practice now informs everything you do.",
|
||||
"Stage up: Advanced. You're not just practicing awareness — you're living it. The boundary between 'practice' and 'life' is dissolving.",
|
||||
],
|
||||
};
|
||||
|
||||
const GENERIC_COMPLETION_MESSAGES = [
|
||||
"Done. You showed up, you practiced, you reflected. That's the whole game — and you're playing it.",
|
||||
"Practice complete. Notice how you feel right now compared to before. That shift is the point.",
|
||||
"Another session in the books. Not every practice will feel profound — but every practice counts.",
|
||||
"You did the work today. Not tomorrow, not 'when things calm down' — today. That's discipline, and discipline is self-love.",
|
||||
"Session finished. Your future self — the one with more clarity, more calm, more choice — thanks you.",
|
||||
];
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Reward generation
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function pick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
export function generateRewards(ctx: RewardContext): RewardEvent[] {
|
||||
const rewards: RewardEvent[] = [];
|
||||
|
||||
// First session — highest priority reward
|
||||
if (ctx.isFirstSession) {
|
||||
rewards.push({
|
||||
type: "first_session",
|
||||
message: pick(FIRST_SESSION_MESSAGES),
|
||||
});
|
||||
// Also give a daily completion reward
|
||||
rewards.push({
|
||||
type: "daily_completion",
|
||||
message: pick(GENERIC_COMPLETION_MESSAGES),
|
||||
});
|
||||
return rewards;
|
||||
}
|
||||
|
||||
// Always give daily completion
|
||||
rewards.push({
|
||||
type: "daily_completion",
|
||||
message: pick(GENERIC_COMPLETION_MESSAGES),
|
||||
});
|
||||
|
||||
// Streak milestones (only one per session, highest applicable)
|
||||
if (ctx.streakMilestone) {
|
||||
if (ctx.streakDays === 30) {
|
||||
rewards.push({ type: "streak_30", message: pick(STREAK_30_MESSAGES) });
|
||||
} else if (ctx.streakDays === 14) {
|
||||
rewards.push({ type: "streak_14", message: pick(STREAK_14_MESSAGES) });
|
||||
} else if (ctx.streakDays === 7) {
|
||||
rewards.push({ type: "streak_7", message: pick(STREAK_7_MESSAGES) });
|
||||
} else if (ctx.streakDays === 3) {
|
||||
rewards.push({ type: "streak_3", message: pick(STREAK_3_MESSAGES) });
|
||||
}
|
||||
}
|
||||
|
||||
// Stage advancement
|
||||
if (ctx.stageAdvanced && ctx.newStage > 0) {
|
||||
const messages = STAGE_UP_MESSAGES[ctx.newStage];
|
||||
if (messages) {
|
||||
rewards.push({ type: "stage_up", message: pick(messages) });
|
||||
}
|
||||
}
|
||||
|
||||
return rewards;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Persist rewards to database
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function persistRewards(
|
||||
userId: string,
|
||||
rewards: RewardEvent[],
|
||||
prisma: { rewardEvent: { create: (args: { data: any }) => Promise<any> } }
|
||||
): Promise<void> {
|
||||
await Promise.all(
|
||||
rewards.map((r) =>
|
||||
prisma.rewardEvent.create({
|
||||
data: {
|
||||
userId,
|
||||
type: r.type,
|
||||
message: r.message,
|
||||
},
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,191 @@
|
||||
import OpenAI from "openai";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { keywordCheck, llmSafetyCheck, type SafetyResult } from "@/lib/safety";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
interface WeekData {
|
||||
sessions: {
|
||||
taskType: string;
|
||||
taskDescription: string;
|
||||
focusScore: number | null;
|
||||
emotionScore: number | null;
|
||||
notes: string | null;
|
||||
durationSeconds: number;
|
||||
createdAt: Date;
|
||||
}[];
|
||||
state: {
|
||||
attentionLevel: number;
|
||||
emotionStability: number;
|
||||
stressLevel: number;
|
||||
practiceStage: number;
|
||||
consistencyScore: number;
|
||||
streakDays: number;
|
||||
totalSessionsCompleted: number;
|
||||
};
|
||||
}
|
||||
|
||||
interface GeneratedSummary {
|
||||
summary: string;
|
||||
safety: SafetyResult;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LLM summary generation (LLM #3)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SUMMARY_PROMPT = `You are a wise, warm inner development coach writing a weekly practice summary. Your tone is grounded, encouraging, and specific — never generic or saccharine.
|
||||
|
||||
You will receive data about the user's practice this week: sessions completed, task types, focus/emotion scores, reflections, and their current state metrics (attention, emotional stability, stress, consistency, streak, stage).
|
||||
|
||||
Write a 3-4 paragraph summary that:
|
||||
|
||||
1. Opens by acknowledging their commitment this week — be specific about what they practiced and how many sessions
|
||||
2. Highlights patterns you notice: which types of practice led to better focus? Did their emotional awareness deepen? Were there difficult days they pushed through?
|
||||
3. Notes changes in their state metrics from what you can infer — are they trending in a good direction?
|
||||
4. Closes with gentle encouragement for the week ahead, grounded in what they've actually demonstrated (not empty motivation)
|
||||
|
||||
Voice rules:
|
||||
- Be specific. Reference actual task types and patterns. "Your two attention training sessions showed stronger focus than your stress regulation work" is good. "Great job this week!" is not.
|
||||
- Be honest. If it was a light week with only 1-2 sessions, acknowledge that without judgment.
|
||||
- Never diagnose, never claim medical benefits, never use self-help clichés.
|
||||
- Write in second person ("you").
|
||||
- Keep it to 3-4 paragraphs, 200-300 words total.`;
|
||||
|
||||
export async function generateWeeklySummary(
|
||||
userId: string,
|
||||
openai: OpenAI
|
||||
): Promise<GeneratedSummary> {
|
||||
// Gather week data
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
|
||||
const [sessions, userState] = await Promise.all([
|
||||
prisma.session.findMany({
|
||||
where: {
|
||||
userId,
|
||||
createdAt: { gte: weekAgo },
|
||||
completed: true,
|
||||
},
|
||||
orderBy: { createdAt: "asc" },
|
||||
select: {
|
||||
taskType: true,
|
||||
taskDescription: true,
|
||||
focusScore: true,
|
||||
emotionScore: true,
|
||||
notes: true,
|
||||
durationSeconds: true,
|
||||
createdAt: true,
|
||||
},
|
||||
}),
|
||||
prisma.userState.findUnique({ where: { userId } }),
|
||||
]);
|
||||
|
||||
if (!userState || sessions.length === 0) {
|
||||
return {
|
||||
summary:
|
||||
"No completed practice sessions this week. Every week is a fresh start — your practice is here when you're ready.",
|
||||
safety: { safe: true },
|
||||
};
|
||||
}
|
||||
|
||||
const weekData: WeekData = {
|
||||
sessions,
|
||||
state: {
|
||||
attentionLevel: userState.attentionLevel,
|
||||
emotionStability: userState.emotionStability,
|
||||
stressLevel: userState.stressLevel,
|
||||
practiceStage: userState.practiceStage,
|
||||
consistencyScore: userState.consistencyScore,
|
||||
streakDays: userState.streakDays,
|
||||
totalSessionsCompleted: userState.totalSessionsCompleted,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: SUMMARY_PROMPT },
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify(weekData),
|
||||
},
|
||||
],
|
||||
temperature: 0.7,
|
||||
max_tokens: 500,
|
||||
});
|
||||
|
||||
const summary = response.choices[0]?.message?.content?.trim();
|
||||
if (!summary || summary.length < 50) throw new Error("Summary too short");
|
||||
|
||||
// Safety check
|
||||
const keywordResult = keywordCheck(summary);
|
||||
if (!keywordResult.safe) {
|
||||
return { summary: "Your practice summary for this week.", safety: keywordResult };
|
||||
}
|
||||
|
||||
const llmSafety = await llmSafetyCheck(summary, openai as any);
|
||||
|
||||
// Persist the summary (upsert — one per user per week)
|
||||
const weekStart = new Date(weekAgo);
|
||||
weekStart.setHours(0, 0, 0, 0);
|
||||
|
||||
await prisma.weeklySummary.upsert({
|
||||
where: { userId },
|
||||
create: {
|
||||
userId,
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
weekStart,
|
||||
},
|
||||
update: {
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
weekStart,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
summary: llmSafety.safe ? summary : "Your practice summary for this week.",
|
||||
safety: llmSafety,
|
||||
};
|
||||
} catch {
|
||||
const fallback = `You completed ${sessions.length} practice session${sessions.length === 1 ? "" : "s"} this week. Showing up consistently is the foundation everything else is built on. Your current streak is ${userState.streakDays} day${userState.streakDays === 1 ? "" : "s"}. See you next week.`;
|
||||
return { summary: fallback, safety: { safe: true } };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Cron handler — generates summaries for all active users
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function generateAllWeeklySummaries(
|
||||
openai: OpenAI
|
||||
): Promise<{ userId: string; success: boolean }[]> {
|
||||
// Find users who had sessions in the last 7 days
|
||||
const weekAgo = new Date();
|
||||
weekAgo.setDate(weekAgo.getDate() - 7);
|
||||
|
||||
const activeUsers = await prisma.session.findMany({
|
||||
where: {
|
||||
createdAt: { gte: weekAgo },
|
||||
completed: true,
|
||||
},
|
||||
select: { userId: true },
|
||||
distinct: ["userId"],
|
||||
});
|
||||
|
||||
const results: { userId: string; success: boolean }[] = [];
|
||||
|
||||
for (const { userId } of activeUsers) {
|
||||
try {
|
||||
await generateWeeklySummary(userId, openai);
|
||||
results.push({ userId, success: true });
|
||||
} catch {
|
||||
results.push({ userId, success: false });
|
||||
}
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
@@ -0,0 +1,326 @@
|
||||
import OpenAI from "openai";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { keywordCheck, llmSafetyCheck, type SafetyResult } from "@/lib/safety";
|
||||
import type { TaskType } from "@/generated/prisma/client";
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Types
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export interface TaskTemplate {
|
||||
type: TaskType;
|
||||
difficulty: number;
|
||||
duration: number;
|
||||
description: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
interface TaskSelectionInput {
|
||||
userId: string;
|
||||
userState: {
|
||||
attentionLevel: number;
|
||||
emotionStability: number;
|
||||
stressLevel: number;
|
||||
practiceStage: number;
|
||||
streakDays: number;
|
||||
};
|
||||
recentTaskTypes: TaskType[];
|
||||
}
|
||||
|
||||
interface GeneratedTask {
|
||||
type: TaskType;
|
||||
difficulty: number;
|
||||
duration: number;
|
||||
description: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Template fallback (T5)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let _templates: TaskTemplate[] | null = null;
|
||||
|
||||
async function loadTemplates(): Promise<TaskTemplate[]> {
|
||||
if (_templates) return _templates;
|
||||
// Templates are bundled at build time from data/task-templates.json
|
||||
_templates = (await import("@/../data/task-templates.json")).default as TaskTemplate[];
|
||||
return _templates!;
|
||||
}
|
||||
|
||||
function pickTemplate(
|
||||
templates: TaskTemplate[],
|
||||
recentTaskTypes: TaskType[],
|
||||
userState: TaskSelectionInput["userState"]
|
||||
): GeneratedTask {
|
||||
const stage = userState.practiceStage;
|
||||
|
||||
// Map stage to preferred difficulty
|
||||
let targetDifficulty: number;
|
||||
if (stage === 0) targetDifficulty = 1;
|
||||
else if (stage <= 2) targetDifficulty = Math.min(stage + 1, 2);
|
||||
else targetDifficulty = Math.min(Math.ceil(stage / 2), 3);
|
||||
|
||||
// Prefer types not recently used
|
||||
const recentSet = new Set(recentTaskTypes.slice(0, 3));
|
||||
const scored = templates.map((t) => ({
|
||||
...t,
|
||||
score:
|
||||
(recentSet.has(t.type) ? 0 : 2) +
|
||||
(t.difficulty === targetDifficulty ? 2 : t.difficulty === targetDifficulty + 1 ? 1 : 0),
|
||||
}));
|
||||
|
||||
scored.sort((a, b) => b.score - a.score);
|
||||
const top = scored.filter((t) => t.score >= scored[0].score - 1);
|
||||
|
||||
// Pick randomly from top candidates
|
||||
const picked = top[Math.floor(Math.random() * top.length)];
|
||||
|
||||
return {
|
||||
type: picked.type,
|
||||
difficulty: picked.difficulty,
|
||||
duration: picked.duration,
|
||||
description: picked.description,
|
||||
reason: picked.reason,
|
||||
};
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// LLM task generation (LLM #1 — bundled TaskSelector + Explainer)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const TASK_PROMPT = `You are a wise, grounded inner development coach. Your job is to select ONE daily mental training task for the user and explain why it's the right task for them right now.
|
||||
|
||||
You will receive the user's current state (attention, emotion stability, stress level, practice stage, streak) and their recent task types. Based on this, you must:
|
||||
|
||||
1. Choose the most appropriate task TYPE from: attention_training, emotion_awareness, stress_regulation, cognitive_clarity, integration
|
||||
2. Set a DIFFICULTY from 1 (beginner) to 3 (advanced)
|
||||
3. Set a DURATION in minutes (1-15)
|
||||
4. Write a clear, actionable DESCRIPTION of the exercise (2-4 sentences, second person "you")
|
||||
5. Write a personalized REASON explaining why THIS task was chosen for THIS user TODAY (2-3 sentences, warm and encouraging but not saccharine)
|
||||
|
||||
Principles:
|
||||
- If stress_level is high (>65), prioritize stress_regulation
|
||||
- If attention_level is low (<35), prioritize attention_training
|
||||
- If the user has done the same type 2+ days in a row, vary it
|
||||
- Match difficulty to practice_stage (0-1 → diff 1, 2-3 → diff 2, 4+ → diff 3)
|
||||
- Every few days, include an integration task to connect practice to daily life
|
||||
- Be specific and concrete — never generic platitudes`
|
||||
|
||||
interface LLMTaskResponse {
|
||||
type: TaskType;
|
||||
difficulty: number;
|
||||
duration: number;
|
||||
description: string;
|
||||
reason: string;
|
||||
}
|
||||
|
||||
export async function generateTask(
|
||||
input: TaskSelectionInput,
|
||||
openai: OpenAI
|
||||
): Promise<GeneratedTask & { safety: SafetyResult }> {
|
||||
const templates = await loadTemplates();
|
||||
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: TASK_PROMPT },
|
||||
{
|
||||
role: "user",
|
||||
content: JSON.stringify({
|
||||
attention_level: input.userState.attentionLevel,
|
||||
emotion_stability: input.userState.emotionStability,
|
||||
stress_level: input.userState.stressLevel,
|
||||
practice_stage: input.userState.practiceStage,
|
||||
streak_days: input.userState.streakDays,
|
||||
recent_task_types: input.recentTaskTypes,
|
||||
}),
|
||||
},
|
||||
],
|
||||
response_format: {
|
||||
type: "json_schema",
|
||||
json_schema: {
|
||||
name: "task",
|
||||
strict: true,
|
||||
schema: {
|
||||
type: "object",
|
||||
properties: {
|
||||
type: {
|
||||
type: "string",
|
||||
enum: [
|
||||
"attention_training",
|
||||
"emotion_awareness",
|
||||
"stress_regulation",
|
||||
"cognitive_clarity",
|
||||
"integration",
|
||||
],
|
||||
},
|
||||
difficulty: { type: "number", minimum: 1, maximum: 3 },
|
||||
duration: { type: "number", minimum: 1, maximum: 15 },
|
||||
description: { type: "string" },
|
||||
reason: { type: "string" },
|
||||
},
|
||||
required: ["type", "difficulty", "duration", "description", "reason"],
|
||||
additionalProperties: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
temperature: 0.7,
|
||||
max_tokens: 400,
|
||||
});
|
||||
|
||||
const raw = response.choices[0]?.message?.content;
|
||||
if (!raw) throw new Error("Empty LLM response");
|
||||
|
||||
const parsed: LLMTaskResponse = JSON.parse(raw);
|
||||
|
||||
// Validate parsed values
|
||||
const validTypes: TaskType[] = [
|
||||
"attention_training",
|
||||
"emotion_awareness",
|
||||
"stress_regulation",
|
||||
"cognitive_clarity",
|
||||
"integration",
|
||||
];
|
||||
if (!validTypes.includes(parsed.type)) throw new Error(`Invalid task type: ${parsed.type}`);
|
||||
if (parsed.difficulty < 1 || parsed.difficulty > 3)
|
||||
throw new Error(`Invalid difficulty: ${parsed.difficulty}`);
|
||||
if (parsed.duration < 1 || parsed.duration > 15)
|
||||
throw new Error(`Invalid duration: ${parsed.duration}`);
|
||||
if (!parsed.description || parsed.description.length < 20)
|
||||
throw new Error("Description too short");
|
||||
if (!parsed.reason || parsed.reason.length < 20) throw new Error("Reason too short");
|
||||
|
||||
// T2: Content safety
|
||||
const keywordResult = keywordCheck(parsed.description + " " + parsed.reason);
|
||||
if (!keywordResult.safe) {
|
||||
// Fall back to template on safety failure
|
||||
const fallback = pickTemplate(templates, input.recentTaskTypes, input.userState);
|
||||
return { ...fallback, safety: keywordResult };
|
||||
}
|
||||
|
||||
const llmSafety = await llmSafetyCheck(
|
||||
parsed.description + " " + parsed.reason,
|
||||
openai as any
|
||||
);
|
||||
|
||||
return {
|
||||
type: parsed.type,
|
||||
difficulty: parsed.difficulty,
|
||||
duration: parsed.duration,
|
||||
description: parsed.description,
|
||||
reason: parsed.reason,
|
||||
safety: llmSafety,
|
||||
};
|
||||
} catch {
|
||||
// T5: Fall back to template on any LLM failure
|
||||
const fallback = pickTemplate(templates, input.recentTaskTypes, input.userState);
|
||||
return { ...fallback, safety: { safe: true } };
|
||||
}
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Daily task generation + persistence
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export async function generateDailyTask(
|
||||
userId: string,
|
||||
openai: OpenAI
|
||||
): Promise<{
|
||||
task: GeneratedTask & { id: string };
|
||||
safety: SafetyResult;
|
||||
fromTemplate: boolean;
|
||||
}> {
|
||||
// Gather user state
|
||||
const [userState, recentSessions, recentTasks] = await Promise.all([
|
||||
prisma.userState.findUnique({ where: { userId } }),
|
||||
prisma.session.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 7,
|
||||
select: { taskType: true },
|
||||
}),
|
||||
prisma.task.findMany({
|
||||
where: { userId },
|
||||
orderBy: { createdAt: "desc" },
|
||||
take: 3,
|
||||
select: { type: true },
|
||||
}),
|
||||
]);
|
||||
|
||||
if (!userState) {
|
||||
// No state yet — use template
|
||||
const templates = await loadTemplates();
|
||||
const result = pickTemplate(templates, [], {
|
||||
attentionLevel: 50,
|
||||
emotionStability: 50,
|
||||
stressLevel: 50,
|
||||
practiceStage: 0,
|
||||
streakDays: 0,
|
||||
});
|
||||
|
||||
const task = await prisma.task.create({
|
||||
data: {
|
||||
userId,
|
||||
type: result.type,
|
||||
difficulty: result.difficulty,
|
||||
duration: result.duration,
|
||||
description: result.description,
|
||||
reasonJson: JSON.stringify({ reason: result.reason, source: "template" }),
|
||||
status: "pending",
|
||||
},
|
||||
});
|
||||
|
||||
return { task: { ...result, id: task.id }, safety: { safe: true }, fromTemplate: true };
|
||||
}
|
||||
|
||||
const recentTaskTypes = [
|
||||
...recentSessions.map((s) => s.taskType),
|
||||
...recentTasks.map((t) => t.type),
|
||||
];
|
||||
|
||||
const result = await generateTask(
|
||||
{
|
||||
userId,
|
||||
userState: {
|
||||
attentionLevel: userState.attentionLevel,
|
||||
emotionStability: userState.emotionStability,
|
||||
stressLevel: userState.stressLevel,
|
||||
practiceStage: userState.practiceStage,
|
||||
streakDays: userState.streakDays,
|
||||
},
|
||||
recentTaskTypes,
|
||||
},
|
||||
openai
|
||||
);
|
||||
|
||||
// Persist the generated task
|
||||
const persisted = await prisma.task.create({
|
||||
data: {
|
||||
userId,
|
||||
type: result.type,
|
||||
difficulty: result.difficulty,
|
||||
duration: result.duration,
|
||||
description: result.description,
|
||||
reasonJson: JSON.stringify({
|
||||
reason: result.reason,
|
||||
source: result.safety.safe ? "llm" : "template",
|
||||
}),
|
||||
status: "pending",
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
task: {
|
||||
id: persisted.id,
|
||||
type: result.type,
|
||||
difficulty: result.difficulty,
|
||||
duration: result.duration,
|
||||
description: result.description,
|
||||
reason: result.reason,
|
||||
},
|
||||
safety: result.safety,
|
||||
fromTemplate: false,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
import { PrismaClient } from "@/generated/prisma/client";
|
||||
import { PrismaPg } from "@prisma/adapter-pg";
|
||||
|
||||
const globalForPrisma = globalThis as unknown as {
|
||||
prisma: PrismaClient | undefined;
|
||||
};
|
||||
|
||||
function createPrismaClient(): PrismaClient {
|
||||
const adapter = new PrismaPg({
|
||||
connectionString: process.env.DATABASE_URL!,
|
||||
});
|
||||
return new PrismaClient({ adapter });
|
||||
}
|
||||
|
||||
export const prisma = globalForPrisma.prisma ?? createPrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
globalForPrisma.prisma = prisma;
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
const BLOCKED_KEYWORDS = [
|
||||
"self-harm",
|
||||
"self harm",
|
||||
"suicide",
|
||||
"kill yourself",
|
||||
"end your life",
|
||||
"hurt yourself",
|
||||
"starve yourself",
|
||||
"extreme fasting",
|
||||
"dangerous dose",
|
||||
"overdose",
|
||||
"illegal substance",
|
||||
"prescription drug",
|
||||
"medical advice",
|
||||
"diagnose",
|
||||
"cure your",
|
||||
"treatment for",
|
||||
];
|
||||
|
||||
const SELF_REVIEW_PROMPT = `Does this task contain harmful or dangerous suggestions (self-harm, extreme behaviors, medical advice, or illegal activities)? Reply with ONLY "yes" or "no".`;
|
||||
|
||||
export interface SafetyResult {
|
||||
safe: boolean;
|
||||
reason?: string;
|
||||
}
|
||||
|
||||
export function keywordCheck(text: string): SafetyResult {
|
||||
const lower = text.toLowerCase();
|
||||
for (const keyword of BLOCKED_KEYWORDS) {
|
||||
if (lower.includes(keyword)) {
|
||||
return {
|
||||
safe: false,
|
||||
reason: `Blocked keyword: "${keyword}"`,
|
||||
};
|
||||
}
|
||||
}
|
||||
return { safe: true };
|
||||
}
|
||||
|
||||
export async function llmSafetyCheck(
|
||||
text: string,
|
||||
openai: { chat: { completions: { create: (args: any) => Promise<any> } } }
|
||||
): Promise<SafetyResult> {
|
||||
try {
|
||||
const response = await openai.chat.completions.create({
|
||||
model: "gpt-4o-mini",
|
||||
messages: [
|
||||
{ role: "system", content: SELF_REVIEW_PROMPT },
|
||||
{ role: "user", content: text },
|
||||
],
|
||||
max_tokens: 5,
|
||||
temperature: 0,
|
||||
});
|
||||
|
||||
const answer = response.choices[0]?.message?.content?.trim().toLowerCase();
|
||||
if (answer === "yes") {
|
||||
return { safe: false, reason: "LLM flagged content as harmful" };
|
||||
}
|
||||
return { safe: true };
|
||||
} catch {
|
||||
// If self-review LLM call fails, fall through to keyword check result
|
||||
return { safe: true };
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
import { clerkMiddleware, createRouteMatcher } from "@clerk/nextjs/server";
|
||||
|
||||
const isPublicRoute = createRouteMatcher([
|
||||
"/api/cron/weekly-summaries",
|
||||
"/sign-in(.*)",
|
||||
"/sign-up(.*)",
|
||||
]);
|
||||
|
||||
export default clerkMiddleware(async (auth, req) => {
|
||||
if (!isPublicRoute(req)) {
|
||||
await auth.protect();
|
||||
}
|
||||
});
|
||||
|
||||
export const config = {
|
||||
matcher: [
|
||||
// Skip Next.js internals and static files
|
||||
"/((?!_next/static|_next/image|favicon.ico).*)",
|
||||
"/",
|
||||
"/(api|trpc)(.*)",
|
||||
],
|
||||
};
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2017",
|
||||
"lib": ["dom", "dom.iterable", "esnext"],
|
||||
"allowJs": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"noEmit": true,
|
||||
"esModuleInterop": true,
|
||||
"module": "esnext",
|
||||
"moduleResolution": "bundler",
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"jsx": "react-jsx",
|
||||
"incremental": true,
|
||||
"plugins": [
|
||||
{
|
||||
"name": "next"
|
||||
}
|
||||
],
|
||||
"paths": {
|
||||
"@/*": ["./src/*"]
|
||||
}
|
||||
},
|
||||
"include": [
|
||||
"next-env.d.ts",
|
||||
"**/*.ts",
|
||||
"**/*.tsx",
|
||||
".next/types/**/*.ts",
|
||||
".next/dev/types/**/*.ts",
|
||||
"**/*.mts"
|
||||
],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
Reference in New Issue
Block a user