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
+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,
};
}