- Add continuous guided light-body-scan meditation (16 segments, 97% coverage) - Edge TTS API endpoint with Xiaoxiao neural voice at -40% rate - Pre-generated chime WAVs and guidance MP3s in public/audio/ - Mute toggle in practice page, sequenced non-overlapping audio - Fix middleware to serve /audio and /api/audio/tts without auth - All LLM output localized to Chinese; fix blank home/settings pages - DeepSeek API compat: json_object, increased max_tokens - Analytics, cron reminders, onboarding flow fixes Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
154 lines
6.0 KiB
TypeScript
154 lines
6.0 KiB
TypeScript
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 = [
|
|
"第一次练习完成了。这条路始于足下,不在于终点。",
|
|
"第一天。你不是只想了想,你做到了。这就是一切的区别。",
|
|
"欢迎开始你的练习。门已经打开了。明天,再走进来。",
|
|
];
|
|
|
|
const STREAK_3_MESSAGES = [
|
|
"连续三天。一个模式正在形成。你的心智开始期待这一刻——习惯正在扎根。",
|
|
"第三天。前两次靠的是意志力。今天,是势头的开始。",
|
|
];
|
|
|
|
const STREAK_7_MESSAGES = [
|
|
"七天。一整周都在为自己出现。大多数人从未走到这么远。你做到了。",
|
|
"一周的每日练习。你的大脑正在实实在在地重塑。神经可塑性不是比喻——它正在发生。",
|
|
];
|
|
|
|
const STREAK_14_MESSAGES = [
|
|
"两周。研究表明形成习惯需要21天——你已经走了三分之二,感觉已经不一样了,不是吗?",
|
|
"连续十四天。你不再在「尝试冥想」。你是一个在练习的人。身份已经转变。",
|
|
];
|
|
|
|
const STREAK_30_MESSAGES = [
|
|
"三十天。一个月的每日练习。无论你开始前是什么样子——焦虑、散乱、反应——你正在变成另一个人。一个能观察自己的心并做出选择的人。",
|
|
"一个月。这不再是一个连续记录。这是一项练习。它是你的一部分。问题不再是「我今天要不要练习?」,而是「什么时候?」",
|
|
];
|
|
|
|
const STAGE_UP_MESSAGES: Record<number, string[]> = {
|
|
1: [
|
|
"你已经进入基础阶段。基本功已经到位。现在开始深入——更长的练习,更细微的觉察。",
|
|
"阶段提升:基础。你从尝试进入了训练。练习正在成为熟悉的领域。",
|
|
],
|
|
2: [
|
|
"成长阶段已到。你的坚持是稳固的,你的觉知正在敏锐。这就是练习开始改变你如何在这个世界上行走的地方。",
|
|
"阶段提升:成长。你度过了笨拙的初学者阶段。你的练习现在有了质感和深度。",
|
|
],
|
|
3: [
|
|
"稳固阶段。十五次练习和强劲的坚持度。你的内心世界正在变得清晰可读。这就是情绪素养在行动。",
|
|
"阶段提升:稳固。你的练习扎下了根。从每天的练习开始,逐渐成为一种存在方式。",
|
|
],
|
|
4: [
|
|
"进阶。这是稀有的领域。大多数人从未发展出这个层次的内心觉知。你的练习现在渗透到你做的每一件事中。",
|
|
"阶段提升:进阶。你不只是在练习觉知——你在活出它。「练习」和「生活」之间的界限正在消融。",
|
|
],
|
|
};
|
|
|
|
const GENERIC_COMPLETION_MESSAGES = [
|
|
"完成。你出现了,你练习了,你反思了。这就是全部——而你在做着。",
|
|
"练习完成。留意你现在的感受和之前的区别。那个转变就是意义所在。",
|
|
"又一次练习记录在册。不是每一次练习都会感觉深刻——但每一次练习都很重要。",
|
|
"你今天完成了功课。不是明天,不是「等事情平静下来的时候」——是今天。这就是自律,自律就是自爱。",
|
|
"练习结束。你未来的自己——那个更清晰、更平静、更有选择权的自己——在感谢你。",
|
|
];
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// 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,
|
|
},
|
|
})
|
|
)
|
|
);
|
|
}
|