feat: integrate Sensei Spook mascot in 4 stances, speech bubbles, and style updates

This commit is contained in:
Ajasra committed 2026-10-28 01:20:01 +08:00
commit e04672db2a
104 files changed
+8262

No files matched your search

+58
View File
@@ -0,0 +1,58 @@
interface DeviceUsage {
count: number;
firstUsedAt: number;
}
// In-memory usage map (or keyed by hashed client token)
const usageStore = new Map<string, DeviceUsage>();
export interface RateLimitResult {
allowed: boolean;
remaining: number;
resetMinutes: number;
}
export function checkRateLimit(
clientId: string,
maxAllowed: number,
cooldownMinutes: number
): RateLimitResult {
if (!maxAllowed || maxAllowed <= 0) {
return { allowed: true, remaining: 999999, resetMinutes: 0 };
}
const now = Date.now();
const cooldownMs = cooldownMinutes * 60 * 1000;
const usage = usageStore.get(clientId);
if (!usage) {
return { allowed: true, remaining: maxAllowed, resetMinutes: 0 };
}
// Check if cooldown period has elapsed since first use
if (now - usage.firstUsedAt > cooldownMs) {
usageStore.delete(clientId);
return { allowed: true, remaining: maxAllowed, resetMinutes: 0 };
}
const remaining = Math.max(0, maxAllowed - usage.count);
const remainingCooldownMs = cooldownMs - (now - usage.firstUsedAt);
const resetMinutes = Math.ceil(remainingCooldownMs / (60 * 1000));
return {
allowed: usage.count < maxAllowed,
remaining,
resetMinutes,
};
}
export function incrementRateLimit(clientId: string): void {
const now = Date.now();
const usage = usageStore.get(clientId);
if (!usage) {
usageStore.set(clientId, { count: 1, firstUsedAt: now });
} else {
usage.count += 1;
}
}