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:
jackyu66git
2026-05-20 12:11:27 +08:00
co-authored by Claude Opus 4.6
commit cef29d40bd
49 changed files with 4723 additions and 0 deletions
+43
View File
@@ -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
+5
View File
@@ -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 -->
+28
View File
@@ -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.
+128
View File
@@ -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 |
+36
View File
@@ -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.
+43
View File
@@ -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)
+983
View File
@@ -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", "", {}, ""],
}
}
+107
View File
@@ -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."
}
]
+18
View File
@@ -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;
+7
View File
@@ -0,0 +1,7 @@
import type { NextConfig } from "next";
const nextConfig: NextConfig = {
/* config options here */
};
export default nextConfig;
+32
View File
@@ -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"
}
}
+7
View File
@@ -0,0 +1,7 @@
const config = {
plugins: {
"@tailwindcss/postcss": {},
},
};
export default config;
+14
View File
@@ -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"],
},
});
+126
View File
@@ -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")
}
+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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

+1
View File
@@ -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 }
);
}
}
+25
View File
@@ -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 });
}
+167
View File
@@ -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,
});
}
+24
View File
@@ -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 });
}
+49
View File
@@ -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,
},
});
}
+27
View File
@@ -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 }
);
}
}
+32
View File
@@ -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 });
}
+71
View File
@@ -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

+46
View File
@@ -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;
}
+44
View File
@@ -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>
);
}
+265
View File
@@ -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&rsquo;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);
}
+216
View File
@@ -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;
}
+130
View File
@@ -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>
);
}
+198
View File
@@ -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">
&ldquo;{result.insight}&rdquo;
</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;
}
+171
View File
@@ -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;
}
+145
View File
@@ -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>
);
}
+82
View File
@@ -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>
);
}
+20
View File
@@ -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>
);
}
+44
View File
@@ -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>
);
}
+149
View File
@@ -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>
);
}
+185
View File
@@ -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,
};
}
+202
View File
@@ -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,
};
}
+153
View File
@@ -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,
},
})
)
);
}
+191
View File
@@ -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;
}
+326
View File
@@ -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,
};
}
+19
View File
@@ -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;
}
+64
View File
@@ -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 };
}
}
+22
View File
@@ -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)(.*)",
],
};
+34
View File
@@ -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"]
}