mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-15 20:59:40 +02:00
Phase 2 of the Companion Brain. Adds live-reactive projections that aggregate data across all 5 pilot modules into high-level views: - DaySnapshot: today's tasks (total/completed/overdue/due), calendar events (upcoming/next), drink intake (water/coffee/total with goals), nutrition (meals/calories/protein with goals), places visited - Streaks: consecutive-day tracking for water goal, task completion, and meal logging with active/at_risk/broken status (90-day lookback) - Context Document: ~500 token markdown generator combining DaySnapshot + Streaks for LLM system prompts Also wires startEventStore() into the app layout so domain events from Phase 1 are persisted to IndexedDB on every module mutation. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
107 lines
3.6 KiB
TypeScript
107 lines
3.6 KiB
TypeScript
/**
|
|
* Context Document Generator — Produces a ~500 token text snapshot
|
|
* of the user's current state for use as an LLM system prompt.
|
|
*
|
|
* Combines DaySnapshot + Streaks into a structured markdown string
|
|
* that any LLM tier (local Gemma or cloud) can reason over.
|
|
*/
|
|
|
|
import type { DaySnapshot, StreakInfo } from './types';
|
|
|
|
function formatTime(iso: string): string {
|
|
try {
|
|
return new Date(iso).toLocaleTimeString('de-DE', { hour: '2-digit', minute: '2-digit' });
|
|
} catch {
|
|
return iso.slice(11, 16);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Generate a concise user context document.
|
|
*
|
|
* @param day - Today's snapshot
|
|
* @param streaks - Current streak info
|
|
* @returns Markdown string (~300-500 tokens)
|
|
*/
|
|
export function generateContextDocument(day: DaySnapshot, streaks: StreakInfo[]): string {
|
|
const lines: string[] = [];
|
|
|
|
lines.push(`## Nutzer-Kontext (${day.date})\n`);
|
|
|
|
// ── Today ───────────────────────────────────────
|
|
lines.push('### Heute');
|
|
|
|
// Tasks
|
|
const taskLine = `- ${day.tasks.total} offene Tasks`;
|
|
const extras: string[] = [];
|
|
if (day.tasks.completed > 0) extras.push(`${day.tasks.completed} erledigt`);
|
|
if (day.tasks.overdue > 0) extras.push(`${day.tasks.overdue} ueberfaellig`);
|
|
if (day.tasks.dueToday.length > 0) extras.push(`${day.tasks.dueToday.length} heute faellig`);
|
|
lines.push(extras.length > 0 ? `${taskLine} (${extras.join(', ')})` : taskLine);
|
|
|
|
if (day.tasks.dueToday.length > 0) {
|
|
for (const t of day.tasks.dueToday.slice(0, 5)) {
|
|
lines.push(` - "${t.title}"${t.priority === 'high' ? ' (hohe Prioritaet)' : ''}`);
|
|
}
|
|
}
|
|
|
|
// Events
|
|
if (day.events.total > 0) {
|
|
lines.push(`- ${day.events.total} Termine`);
|
|
if (day.events.nextEvent) {
|
|
const e = day.events.nextEvent;
|
|
lines.push(` - Naechster: "${e.title}" um ${formatTime(e.startTime)}`);
|
|
}
|
|
for (const e of day.events.upcoming.slice(1, 4)) {
|
|
lines.push(` - "${e.title}" ${formatTime(e.startTime)}-${formatTime(e.endTime)}`);
|
|
}
|
|
} else {
|
|
lines.push('- Keine Termine');
|
|
}
|
|
|
|
// Drinks
|
|
lines.push(
|
|
`- Wasser: ${day.drinks.water.ml}ml / ${day.drinks.water.goal}ml (${day.drinks.water.percent}%)`
|
|
);
|
|
if (day.drinks.coffee.count > 0) {
|
|
lines.push(`- Kaffee: ${day.drinks.coffee.count}x (${day.drinks.coffee.ml}ml)`);
|
|
}
|
|
|
|
// Nutrition
|
|
lines.push(
|
|
`- Ernaehrung: ${day.nutrition.meals} Mahlzeiten, ${day.nutrition.calories.actual} / ${day.nutrition.calories.goal} kcal (${day.nutrition.calories.percent}%)`
|
|
);
|
|
if (day.nutrition.protein) {
|
|
lines.push(` - Protein: ${day.nutrition.protein.actual}g / ${day.nutrition.protein.goal}g`);
|
|
}
|
|
|
|
// Places
|
|
if (day.places.visitedToday > 0) {
|
|
lines.push(`- ${day.places.visitedToday} Orte besucht`);
|
|
}
|
|
if (day.places.tracking) {
|
|
lines.push('- Standort-Tracking aktiv');
|
|
}
|
|
|
|
// ── Streaks ─────────────────────────────────────
|
|
const activeStreaks = streaks.filter((s) => s.status === 'active');
|
|
const atRisk = streaks.filter((s) => s.status === 'at_risk');
|
|
const broken = streaks.filter((s) => s.status === 'broken' && s.currentStreak === 0);
|
|
|
|
if (activeStreaks.length > 0 || atRisk.length > 0) {
|
|
lines.push('\n### Streaks');
|
|
for (const s of activeStreaks) {
|
|
lines.push(`- ${s.label}: ${s.currentStreak} Tage (aktiv)`);
|
|
}
|
|
for (const s of atRisk) {
|
|
lines.push(`- ${s.label}: ${s.currentStreak} Tage (GEFAEHRDET — heute noch nicht aktiv)`);
|
|
}
|
|
for (const s of broken) {
|
|
if (s.longestStreak > 0) {
|
|
lines.push(`- ${s.label}: unterbrochen (Rekord: ${s.longestStreak} Tage)`);
|
|
}
|
|
}
|
|
}
|
|
|
|
return lines.join('\n');
|
|
}
|