import { action, type BuiltInLLMMessage, computed, type Default, handler, ifElse, llmDialog, NAME, navigateTo, pattern, safeDateNow, Stream, toSchema, UI, type VNode, wish, Writable, } from "commonfabric"; import Note from "./note.tsx"; import Suggestion from "../system/suggestion.tsx"; import { type MentionablePiece, type NotePiece } from "./schemas.tsx"; // ===== Pure utility functions ===== const toLocalISODate = (d: Date): string => { const y = d.getFullYear(); const m = String(d.getMonth() + 1).padStart(2, "0"); const day = String(d.getDate()).padStart(2, "0"); return `${y}-${m}-${day}`; }; const getTodayDate = (): string => toLocalISODate(new Date(safeDateNow())); const journalTitle = (dateStr: string): string => { const d = new Date(dateStr + "T00:00:00"); const dayOfWeek = d.toLocaleDateString("en-US", { weekday: "long" }); return `${dateStr} - ${dayOfWeek}`; }; const DEFAULT_TEMPLATE = `# {{date}} - {{dayOfWeek}} ## Tasks - [ ] ## Notes ## Reflection `; function applyTemplate(templateStr: string, dateStr: string): string { const d = new Date(dateStr + "T00:00:00"); const dayOfWeek = d.toLocaleDateString("en-US", { weekday: "long" }); const monthName = d.toLocaleDateString("en-US", { month: "long" }); const year = d.getFullYear().toString(); return templateStr .replace(/\{\{date\}\}/g, dateStr) .replace(/\{\{dayOfWeek\}\}/g, dayOfWeek) .replace(/\{\{month\}\}/g, monthName) .replace(/\{\{year\}\}/g, year); } // ===== Module-scope handlers ===== /** * Handle calendar day selection: navigate to existing note or create a new one. */ const handleCalendarChange = handler< { detail: { value: string; oldValue: string } }, { entries: Writable; template: Writable; selectedDate: Writable; addPiece: Stream<{ piece: MentionablePiece }>; } >(({ detail }, { entries, template, selectedDate, addPiece }) => { const dateStr = detail.value; selectedDate.set(dateStr); // Check if a note already exists for this date const all = entries.get(); for (const entry of all) { if (entry?.title === journalTitle(dateStr)) { return navigateTo(entry as any); } } // Create a new daily note const noteTitle = journalTitle(dateStr); const t = template.get(); const content = applyTemplate(t || DEFAULT_TEMPLATE, dateStr); const note = Note({ title: noteTitle, content, }); entries.push(note as any); addPiece.send({ piece: note as any }); return navigateTo(note as any); }); /** * Handle "Go to Today" — navigate to today's note, creating it if needed. */ const handleGoToToday = handler< void, { entries: Writable; template: Writable; selectedDate: Writable; addPiece: Stream<{ piece: MentionablePiece }>; } >((_event, { entries, template, selectedDate, addPiece }) => { const dateStr = getTodayDate(); selectedDate.set(dateStr); const all = entries.get(); for (const entry of all) { if (entry?.title === journalTitle(dateStr)) { return navigateTo(entry as any); } } const d = new Date(dateStr + "T00:00:00"); const dayOfWeek = d.toLocaleDateString("en-US", { weekday: "long" }); const noteTitle = `${dateStr} - ${dayOfWeek}`; const t = template.get(); const content = applyTemplate(t || DEFAULT_TEMPLATE, dateStr); const note = Note({ title: noteTitle, content, }); entries.push(note as any); addPiece.send({ piece: note as any }); return navigateTo(note as any); }); // ===== Weekly rollup ===== type WeeklyRollup = { headline: string; themes: Array<{ name: string; detail: string }>; accomplishments: string[]; openThreads: string[]; mood: string; }; const triggerRollup = handler< unknown, { addMessage: Stream } >((_, { addMessage }) => { addMessage.send({ role: "user", content: [{ type: "text" as const, text: "Analyze my daily notes from this past week and produce a weekly rollup.", }], }); }); // ===== Input / Output types ===== interface DailyJournalInput { title?: Writable>; entries?: Writable>; template?: Writable>; } export interface DailyJournalOutput { [NAME]: string; [UI]: VNode; title: string; entries: NotePiece[]; template: string; isJournal: boolean; summary: string; mentionable: NotePiece[]; goToToday: Stream; weeklyRollup: WeeklyRollup | undefined; } // ===== Pattern ===== export default pattern( ({ title, entries, template }) => { // Access default-app for addPiece (global piece registration) const { addPiece } = wish<{ addPiece: Stream<{ piece: MentionablePiece }>; }>({ query: "#default" }).result!; // UI state const showSettings = new Writable(false); const selectedDate = new Writable(getTodayDate()); // Dates that already have a journal entry (for calendar markers) const datesWithNotes = computed(() => { const dates: string[] = []; for (const entry of entries.get()) { const t = entry?.title; if (t && /^\d{4}-\d{2}-\d{2}/.test(t)) { dates.push(t.slice(0, 10)); } } return dates; }); // Sorted entries — most recent first const sortedEntries = computed(() => { const arr: NotePiece[] = []; for (const entry of entries.get()) { if (entry) arr.push(entry); } return arr.sort((a, b) => (b.title ?? "").localeCompare(a.title ?? "")); }); // Short summary for search/LLM const summary = computed(() => { const count = entries.get().length; const sorted = sortedEntries; const latest = sorted.length > 0 ? (sorted[0].title ?? "").slice(0, 10) || "none" : "none"; return `Daily Journal — ${count} entries, last: ${latest}`; }); // Settings visibility helpers const mainDisplay = computed(() => showSettings.get() ? "none" : "flex"); const settingsDisplay = computed(() => showSettings.get() ? "flex" : "none" ); const toggleSettings = action(() => { showSettings.set(!showSettings.get()); }); const goToToday = handleGoToToday({ entries, template, selectedDate, addPiece, }); // ===== Weekly Rollup (LLM-powered) ===== // Gather last 7 days of note content for the system prompt const recentNotesContext = computed(() => { const now = safeDateNow(); const today = new Date(now); const sevenDaysAgo = new Date(now); sevenDaysAgo.setDate(today.getDate() - 7); const cutoff = toLocalISODate(sevenDaysAgo); const recent: string[] = []; for (const entry of entries.get()) { const t = entry?.title; if (!t || !/^\d{4}-\d{2}-\d{2}/.test(t)) continue; const dateStr = t.slice(0, 10); if (dateStr >= cutoff) { const noteTitle = entry.title ?? dateStr; const noteContent = entry.content ?? ""; recent.push( `\n${noteContent}\n`, ); } } recent.sort().reverse(); return recent; }); const hasRecentNotes = computed(() => recentNotesContext.length > 0); const rollupSystemPrompt = computed(() => { const notes = recentNotesContext; const noteCount = notes.length; const notesXml = notes.join("\n\n"); return `You are a personal journal assistant producing a weekly rollup from daily notes. You will receive ${noteCount} daily note(s) from the past 7 days. Even if there is only one note, produce a complete rollup based on whatever content is available. Work with what you have — a single day's notes are still worth summarizing. Produce a structured result with: - headline: A punchy one-sentence summary of the period - themes: 2-4 topics or areas of focus you identified (each with name and brief detail). If there's only one note, extract themes from that note. - accomplishments: Tasks completed, decisions made, or progress noted. Extract from checkbox items, explicit mentions, or implied completions. - openThreads: Unresolved items, questions, or things to follow up on. Extract from unchecked tasks, open questions, or forward-looking statements. - mood: A brief read on energy/tone from the writing style and content. Be concise and specific. Reference actual content from the notes rather than being generic. ${notesXml} `; }); const rollupMessages = new Writable([]); const rollupParams = { system: rollupSystemPrompt, messages: rollupMessages, tools: {}, model: "anthropic:claude-haiku-4-5" as const, builtinTools: false, resultSchema: toSchema(), }; const { addMessage: rollupAddMessage, pending: rollupPending, result: rollupResult, } = llmDialog(rollupParams); const weeklyRollup = computed(() => rollupResult as WeeklyRollup | undefined ); const hasRollup = computed(() => !!weeklyRollup); // Suggestion context derived from the weekly rollup const suggestionSituation = computed(() => { const rollup = weeklyRollup; if (!rollup) return "Suggest something useful based on my daily journal."; const threads: string[] = []; if (rollup.openThreads) { for (const t of rollup.openThreads) threads.push(t); } const themeNames: string[] = []; if (rollup.themes) { for (const t of rollup.themes) themeNames.push(t.name); } const mood = rollup.mood || "unknown"; return `Based on my weekly journal rollup, suggest a next step or useful pattern. Active themes: ${ themeNames.join(", ") }. Open threads: ${threads.join(", ")}. Mood: ${mood}.`; }); const suggestionContext = computed(() => { const rollup = weeklyRollup; if (!rollup) { return { weeklyHeadline: "", themes: [], accomplishments: [], openThreads: [], }; } return { weeklyHeadline: rollup.headline || "", themes: rollup.themes, accomplishments: rollup.accomplishments, openThreads: rollup.openThreads, }; }); return { [NAME]: computed(() => `📅 ${title.get()}`), [UI]: ( {/* Main view — two column layout */}
{/* Column 1: Calendar + Entries */}
{/* Go to Today */} Go to Today's Note {/* Mini calendar */} {/* Entries list */}

Entries

Settings
{sortedEntries.map((entry: any) => ( ))}
{/* Column 2: Weekly Rollup */}
{ifElse( hasRecentNotes,

Weekly Rollup

Refresh
{ifElse( hasRollup,

{weeklyRollup?.headline}

Themes {weeklyRollup?.themes.map( (theme: { name: string; detail: string }) => (
{theme.name} {" — "} {theme.detail}
), )}
{ifElse( computed( () => (weeklyRollup?.accomplishments?.length ?? 0) > 0, ), Done
    {weeklyRollup?.accomplishments.map( (item: string) =>
  • {item}
  • , )}
, , )} {ifElse( computed( () => (weeklyRollup?.openThreads?.length ?? 0) > 0, ), Open Threads
    {weeklyRollup?.openThreads.map( (item: string) =>
  • {item}
  • , )}
, , )}

{weeklyRollup?.mood}

,
{ifElse( rollupPending, Summarizing your week..., , )}
, )}
, , )} {/* Suggestion based on weekly context */} {ifElse( hasRollup, Suggested , , )}
{/* Settings view */}

Daily Note Template

Available variables: {"{{date}}"}, {"{{dayOfWeek}}"}, {"{{month}}"}, {"{{year}}"}

Done
), title, entries, template, isJournal: true, summary, mentionable: entries, goToToday, weeklyRollup, }; }, );