echo-emily-backend / web /src /lib /user-data.ts
widgetwalker-username
feat: reorganize into state-of-the-art monorepo structure
3286713
import type { EmotionState } from "@/lib/emily-api";
export interface JournalRecord {
id: string;
text: string;
mood: string | null;
responseText: string;
emotionState: EmotionState | null;
createdAt: string;
triggers: string[];
}
export interface DraftRecord {
id: string;
text: string;
mood: string | null;
createdAt: string;
}
export interface UserSnapshot {
draft: string;
mood: string | null;
entries: JournalRecord[];
drafts: DraftRecord[];
}
export const ACTIVE_USER_KEY = "echo-active-user";
const MAX_ENTRIES = 80;
const MAX_DRAFTS = 40;
export const EMPTY_SNAPSHOT: UserSnapshot = {
draft: "",
mood: null,
entries: [],
drafts: [],
};
const TRIGGER_RULES: Array<{ name: string; keywords: string[] }> = [
{ name: "Work Stress", keywords: ["work", "job", "office", "deadline", "boss"] },
{ name: "Social Media", keywords: ["instagram", "social", "online", "scroll", "twitter"] },
{ name: "Late Nights", keywords: ["late", "night", "sleep", "insomnia", "tired"] },
{ name: "Unresolved Talks", keywords: ["fight", "argument", "talk", "family", "friend"] },
{ name: "Financial Worry", keywords: ["money", "rent", "debt", "finance", "bills"] },
];
function hasWindow(): boolean {
return typeof window !== "undefined";
}
export function getUserStorageKey(userId: string): string {
return `echo-user-${userId}`;
}
export function getActiveUser(): string {
if (!hasWindow()) return "";
return localStorage.getItem(ACTIVE_USER_KEY) ?? "";
}
export function setActiveUser(userId: string): void {
if (!hasWindow()) return;
localStorage.setItem(ACTIVE_USER_KEY, userId);
}
export function clearActiveUser(): void {
if (!hasWindow()) return;
localStorage.removeItem(ACTIVE_USER_KEY);
}
export function getSnapshot(userId: string): UserSnapshot {
if (!hasWindow() || !userId) return { ...EMPTY_SNAPSHOT };
const raw = localStorage.getItem(getUserStorageKey(userId));
if (!raw) return { ...EMPTY_SNAPSHOT };
try {
const parsed = JSON.parse(raw) as UserSnapshot;
return {
draft: parsed.draft ?? "",
mood: parsed.mood ?? null,
entries: parsed.entries ?? [],
drafts: parsed.drafts ?? [],
};
} catch {
return { ...EMPTY_SNAPSHOT };
}
}
export function saveSnapshot(userId: string, snapshot: UserSnapshot): void {
if (!hasWindow() || !userId) return;
localStorage.setItem(getUserStorageKey(userId), JSON.stringify(snapshot));
}
export function appendEntry(snapshot: UserSnapshot, next: JournalRecord): UserSnapshot {
return {
...snapshot,
entries: [next, ...snapshot.entries].slice(0, MAX_ENTRIES),
};
}
export function appendDraft(snapshot: UserSnapshot, text: string): UserSnapshot {
if (!text.trim()) return snapshot;
const nextDraft: DraftRecord = {
id: `draft-${Date.now()}`,
text: text.trim(),
mood: snapshot.mood,
createdAt: new Date().toISOString(),
};
return {
...snapshot,
drafts: [nextDraft, ...snapshot.drafts].slice(0, MAX_DRAFTS),
};
}
export function deleteDraft(snapshot: UserSnapshot, draftId: string): UserSnapshot {
return {
...snapshot,
drafts: snapshot.drafts.filter((d) => d.id !== draftId),
};
}
export function extractTriggers(text: string): string[] {
const normalized = text.toLowerCase();
return TRIGGER_RULES.filter((rule) => rule.keywords.some((k) => normalized.includes(k))).map((rule) => rule.name);
}
export function toMoodLabel(entries: JournalRecord[]): string {
if (!entries.length) return "No data";
const scoreByMood: Record<string, number> = { Anxious: -2, Sad: -1, Neutral: 0, Good: 1, Great: 2 };
const scored = entries.map((entry) => scoreByMood[entry.mood ?? "Neutral"] ?? 0);
const avg = scored.reduce((sum, v) => sum + v, 0) / scored.length;
if (avg >= 1.5) return "Great";
if (avg >= 0.5) return "Good";
if (avg > -0.5) return "Neutral";
if (avg > -1.5) return "Sad";
return "Anxious";
}
export function buildInsights(entries: JournalRecord[]): string[] {
if (!entries.length) return ["No insights yet. Add journal entries to unlock pattern insights."];
const averageValence =
entries.reduce((sum, e) => sum + (e.emotionState?.emotional_valence ?? 0), 0) / entries.length;
const triggerCounts = new Map<string, number>();
for (const entry of entries) {
for (const trigger of entry.triggers) {
triggerCounts.set(trigger, (triggerCounts.get(trigger) ?? 0) + 1);
}
}
const topTrigger = Array.from(triggerCounts.entries()).sort((a, b) => b[1] - a[1])[0]?.[0];
const results = [
averageValence < -0.2
? "Trend: recent valence looks low. Try smaller recovery routines."
: "Trend: recent valence is stable or improving.",
`Mood trend: ${toMoodLabel(entries)}.`,
];
if (topTrigger) results.push(`Top trigger: ${topTrigger}.`);
return results;
}
export function formatLocalDate(iso: string): string {
return new Date(iso).toLocaleString();
}