feat(briefing): render WeatherCard and RSS reaction buttons from message metadata

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-25 10:45:16 -04:00
parent a691fc043d
commit 06cb7cc86d
2 changed files with 119 additions and 6 deletions
+1
View File
@@ -353,6 +353,7 @@ export interface BriefingMessage {
role: 'user' | 'assistant' | 'system';
content: string;
created_at: string;
metadata?: Record<string, unknown> | null;
}
const DEFAULT_BRIEFING_CONFIG: BriefingConfig = {
+118 -6
View File
@@ -2,6 +2,7 @@
import { ref, computed, onMounted, watch } from 'vue'
import { useChatStore } from '@/stores/chat'
import ChatMessage from '@/components/ChatMessage.vue'
import WeatherCard from '@/components/WeatherCard.vue'
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
import {
getBriefingConfig,
@@ -9,11 +10,28 @@ import {
getBriefingToday,
getBriefingConvMessages,
triggerBriefingSlot,
postRssReaction,
deleteRssReaction,
type BriefingConversation,
type BriefingMessage,
} from '@/api/client'
import type { Message } from '@/types/chat'
interface MessageMetadata {
weather?: {
location: string
fetched_at: string
current_temp: number
condition: string
today_high: number | null
today_low: number | null
yesterday_high: number | null
yesterday_low: number | null
forecast: { day: string; condition: string; high: number; low: number }[]
} | null
rss_item_ids?: number[]
}
const chatStore = useChatStore()
// Setup wizard
@@ -114,6 +132,27 @@ function onKeydown(e: KeyboardEvent) {
}
}
// RSS reactions: map of rss_item_id -> 'up' | 'down' | null
const reactions = ref<Record<number, 'up' | 'down' | null>>({})
async function handleReaction(itemId: number, reaction: 'up' | 'down') {
const current = reactions.value[itemId]
reactions.value[itemId] = current === reaction ? null : reaction
try {
if (current === reaction) {
await deleteRssReaction(itemId)
} else {
await postRssReaction(itemId, reaction)
}
} catch {
reactions.value[itemId] = current ?? null
}
}
function msgMetadata(msg: BriefingMessage): MessageMetadata | null {
return (msg.metadata as MessageMetadata | null | undefined) ?? null
}
// Manual trigger
const triggering = ref(false)
async function triggerNow() {
@@ -192,12 +231,42 @@ onMounted(async () => {
<p class="briefing-empty-hint">Click "Refresh" to generate a briefing now, or wait for the scheduled slot.</p>
</div>
<div v-else class="briefing-messages">
<ChatMessage
v-for="msg in messages"
:key="msg.id"
:message="toMsg(msg)"
:is-streaming="false"
/>
<template v-for="msg in messages" :key="msg.id">
<!-- Weather card above the first assistant message with weather metadata -->
<WeatherCard
v-if="msg.role === 'assistant' && msgMetadata(msg)?.weather !== undefined"
:weather="msgMetadata(msg)?.weather ?? null"
/>
<ChatMessage
:message="toMsg(msg)"
:is-streaming="false"
/>
<!-- Reaction buttons below assistant messages with rss_item_ids -->
<div
v-if="msg.role === 'assistant' && (msgMetadata(msg)?.rss_item_ids?.length ?? 0) > 0"
class="rss-reactions"
>
<div
v-for="(itemId, index) in msgMetadata(msg)!.rss_item_ids"
:key="itemId"
class="rss-reaction-row"
>
<span class="reaction-label">Story {{ index + 1 }}</span>
<button
class="reaction-btn"
:class="{ active: reactions[itemId] === 'up' }"
@click="handleReaction(itemId, 'up')"
title="Interested"
>👍</button>
<button
class="reaction-btn"
:class="{ active: reactions[itemId] === 'down' }"
@click="handleReaction(itemId, 'down')"
title="Not interested"
>👎</button>
</div>
</div>
</template>
<!-- Live streaming bubble for today -->
<ChatMessage
v-if="isToday && chatStore.streaming && chatStore.streamingContent"
@@ -392,4 +461,47 @@ onMounted(async () => {
}
.btn-send:disabled { opacity: 0.4; cursor: not-allowed; }
.btn-send:hover:not(:disabled) { opacity: 0.9; }
.rss-reactions {
display: flex;
flex-direction: column;
gap: 0.35rem;
padding: 0.5rem 0.75rem;
margin-bottom: 0.25rem;
}
.rss-reaction-row {
display: flex;
align-items: center;
gap: 0.4rem;
}
.reaction-label {
font-size: 0.78rem;
color: var(--color-text-muted);
min-width: 4rem;
}
.reaction-btn {
background: none;
border: 1px solid var(--color-border);
border-radius: 6px;
padding: 0.15rem 0.4rem;
cursor: pointer;
font-size: 0.85rem;
line-height: 1.4;
opacity: 0.55;
transition: opacity 0.15s, border-color 0.15s;
}
.reaction-btn:hover {
opacity: 1;
border-color: var(--color-primary);
}
.reaction-btn.active {
opacity: 1;
border-color: var(--color-primary);
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
}
</style>