feat(journal): conversational LLM-generated daily prep (replaces card)
The structured prep card was data-rich but voiceless. Replaced with an LLM-generated conversational opener — same shape the briefing's compilation slot had — that renders as a normal assistant chat bubble at the top of the day's conversation. Backend (services/journal_prep.py): - Renamed generate_daily_prep -> gather_daily_sections (still pure data fetching, no LLM); kept the old name as a backwards-compat alias. - New _generate_prep_prose: hands the gathered sections to generate_completion with a warm-conversational system prompt; returns prose. Falls back to a plain greeting if the LLM call fails or no model is configured. - ensure_daily_prep_message now persists the prep as role='assistant' with the prose as content. Structured sections stay on msg_metadata for provenance. Auto-upgrades legacy system-role preps in place on next call. Frontend: - Drop the <article class="daily-prep"> structured block from JournalView. The prep is now just the first chat bubble — picks up the existing Illuminated Transcript styling automatically. - Drop dayMessages / prepMessage / prepSections / asArray helpers — no longer needed. - ChatMessage hideMessage filter: comment refined to clarify it only catches LEGACY system-role prep rows. Current preps are assistant-role and render normally. Net effect: open /journal -> first thing you see is a warm assistant bubble that talks about your day -> input bar below to reply. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -43,9 +43,10 @@ function formatMs(ms: number | null | undefined): string {
|
||||
|
||||
const metadata = computed(() => (props.message.metadata ?? {}) as Record<string, unknown>);
|
||||
|
||||
// Hide daily-prep system messages — they're rendered separately as a structured
|
||||
// card in JournalView. Without this filter they render as a flat unstyled block
|
||||
// inside the chat (no .role-system bubble styling exists).
|
||||
// Hide LEGACY system-role daily_prep messages from earlier versions. The
|
||||
// current journal prep is a normal assistant message (renders with proper
|
||||
// bubble styling). Old system-role rows lurking in existing conversations
|
||||
// would render as a flat unstyled block — filter them.
|
||||
const hideMessage = computed(() =>
|
||||
props.message.role === "system" && metadata.value.kind === "daily_prep"
|
||||
);
|
||||
|
||||
@@ -13,7 +13,6 @@ import {
|
||||
triggerJournalPrep,
|
||||
listEvents,
|
||||
type EventEntry,
|
||||
type JournalMessage,
|
||||
} from '@/api/client'
|
||||
|
||||
interface WeatherDay {
|
||||
@@ -53,21 +52,8 @@ const days = ref<string[]>([])
|
||||
const todayDate = ref<string | null>(null)
|
||||
const selectedDay = ref<string | null>(null)
|
||||
const dayConvId = ref<number | null>(null)
|
||||
const dayMessages = ref<JournalMessage[]>([])
|
||||
const isToday = computed(() => selectedDay.value !== null && selectedDay.value === todayDate.value)
|
||||
|
||||
// ── Daily prep card ──────────────────────────────────────────────────────────
|
||||
const prepMessage = computed<JournalMessage | null>(() => {
|
||||
return dayMessages.value.find(
|
||||
(m) => m.role === 'system' && (m.metadata as { kind?: string } | null)?.kind === 'daily_prep',
|
||||
) ?? null
|
||||
})
|
||||
const prepSections = computed<Record<string, unknown>>(() => {
|
||||
const meta = prepMessage.value?.metadata as { sections?: Record<string, unknown> } | null
|
||||
return meta?.sections ?? {}
|
||||
})
|
||||
function asArray(v: unknown): unknown[] { return Array.isArray(v) ? v : [] }
|
||||
|
||||
// ── Weather panel ────────────────────────────────────────────────────────────
|
||||
const weatherData = ref<WeatherData[]>([])
|
||||
const selectedWeatherIdx = ref(0)
|
||||
@@ -153,7 +139,6 @@ async function loadEvents() {
|
||||
// ── Day load + switch ────────────────────────────────────────────────────────
|
||||
async function loadDay(iso: string) {
|
||||
const payload = iso === todayDate.value ? await getJournalToday() : await getJournalDay(iso)
|
||||
dayMessages.value = payload.messages
|
||||
if (payload.conversation) {
|
||||
dayConvId.value = payload.conversation.id
|
||||
await chatStore.fetchConversation(payload.conversation.id)
|
||||
@@ -167,7 +152,6 @@ async function loadAll() {
|
||||
const today = await getJournalToday()
|
||||
todayDate.value = today.day_date
|
||||
selectedDay.value = today.day_date
|
||||
dayMessages.value = today.messages
|
||||
if (today.conversation) {
|
||||
dayConvId.value = today.conversation.id
|
||||
await chatStore.fetchConversation(today.conversation.id)
|
||||
@@ -259,55 +243,8 @@ onMounted(async () => {
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Center: prep card + chat -->
|
||||
<!-- Center: chat (prep is the first assistant message inside) -->
|
||||
<div class="journal-center">
|
||||
<article v-if="prepMessage" class="daily-prep">
|
||||
<header><h2>Today</h2></header>
|
||||
|
||||
<section v-if="asArray(prepSections.tasks).length" class="prep-section">
|
||||
<h3>Tasks</h3>
|
||||
<ul>
|
||||
<li v-for="t in (asArray(prepSections.tasks) as { id: number; title: string; due_date: string | null }[])" :key="t.id">
|
||||
{{ t.title }}<span v-if="t.due_date"> · due {{ t.due_date }}</span>
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section v-if="asArray(prepSections.events).length" class="prep-section">
|
||||
<h3>Calendar</h3>
|
||||
<ul>
|
||||
<li v-for="(e, i) in (asArray(prepSections.events) as { id: number; title: string; start_dt: string }[])" :key="i">
|
||||
{{ e.title }} — {{ e.start_dt }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section v-if="asArray(prepSections.projects).length" class="prep-section">
|
||||
<h3>Active projects</h3>
|
||||
<ul>
|
||||
<li v-for="p in (asArray(prepSections.projects) as { id: number; title: string }[])" :key="p.id">{{ p.title }}</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section v-if="asArray(prepSections.recent_moments).length" class="prep-section">
|
||||
<h3>Recent moments</h3>
|
||||
<ul>
|
||||
<li v-for="m in (asArray(prepSections.recent_moments) as { id: number; content: string; day_date: string }[])" :key="m.id">
|
||||
<em>{{ m.day_date }}</em> — {{ m.content }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
|
||||
<section v-if="asArray(prepSections.open_threads).length" class="prep-section">
|
||||
<h3>Open threads</h3>
|
||||
<ul>
|
||||
<li v-for="m in (asArray(prepSections.open_threads) as { id: number; content: string; day_date: string }[])" :key="m.id">
|
||||
<em>{{ m.day_date }}</em> — {{ m.content }}
|
||||
</li>
|
||||
</ul>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<ChatPanel
|
||||
variant="full"
|
||||
briefingMode
|
||||
@@ -445,31 +382,6 @@ onMounted(async () => {
|
||||
|
||||
.journal-chat-panel { flex: 1; min-height: 0; }
|
||||
|
||||
.daily-prep {
|
||||
border: 1px solid var(--color-border);
|
||||
border-radius: var(--radius-md);
|
||||
padding: 1rem 1.25rem;
|
||||
margin: 1rem 1rem 0.5rem;
|
||||
background: var(--color-bg-card);
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.daily-prep > header h2 {
|
||||
font-family: 'Fraunces', Georgia, serif;
|
||||
font-size: 1.1rem;
|
||||
margin: 0 0 0.5rem 0;
|
||||
}
|
||||
.prep-section { margin-top: 0.75rem; }
|
||||
.prep-section h3 {
|
||||
font-size: 0.72rem;
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.06em;
|
||||
color: var(--color-text-muted);
|
||||
margin: 0 0 0.25rem 0;
|
||||
font-weight: 600;
|
||||
}
|
||||
.prep-section ul { margin: 0; padding-left: 1.1rem; font-size: 0.88rem; color: var(--color-text); }
|
||||
.prep-section li { margin-bottom: 0.15rem; }
|
||||
|
||||
/* ── Right column ─────────────────────────────────────────────────────────── */
|
||||
.journal-right {
|
||||
grid-column: 2;
|
||||
|
||||
Reference in New Issue
Block a user