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:
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user