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';
|
role: 'user' | 'assistant' | 'system';
|
||||||
content: string;
|
content: string;
|
||||||
created_at: string;
|
created_at: string;
|
||||||
|
metadata?: Record<string, unknown> | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_BRIEFING_CONFIG: BriefingConfig = {
|
const DEFAULT_BRIEFING_CONFIG: BriefingConfig = {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch } from 'vue'
|
||||||
import { useChatStore } from '@/stores/chat'
|
import { useChatStore } from '@/stores/chat'
|
||||||
import ChatMessage from '@/components/ChatMessage.vue'
|
import ChatMessage from '@/components/ChatMessage.vue'
|
||||||
|
import WeatherCard from '@/components/WeatherCard.vue'
|
||||||
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
|
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
|
||||||
import {
|
import {
|
||||||
getBriefingConfig,
|
getBriefingConfig,
|
||||||
@@ -9,11 +10,28 @@ import {
|
|||||||
getBriefingToday,
|
getBriefingToday,
|
||||||
getBriefingConvMessages,
|
getBriefingConvMessages,
|
||||||
triggerBriefingSlot,
|
triggerBriefingSlot,
|
||||||
|
postRssReaction,
|
||||||
|
deleteRssReaction,
|
||||||
type BriefingConversation,
|
type BriefingConversation,
|
||||||
type BriefingMessage,
|
type BriefingMessage,
|
||||||
} from '@/api/client'
|
} from '@/api/client'
|
||||||
import type { Message } from '@/types/chat'
|
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()
|
const chatStore = useChatStore()
|
||||||
|
|
||||||
// Setup wizard
|
// 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
|
// Manual trigger
|
||||||
const triggering = ref(false)
|
const triggering = ref(false)
|
||||||
async function triggerNow() {
|
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>
|
<p class="briefing-empty-hint">Click "Refresh" to generate a briefing now, or wait for the scheduled slot.</p>
|
||||||
</div>
|
</div>
|
||||||
<div v-else class="briefing-messages">
|
<div v-else class="briefing-messages">
|
||||||
<ChatMessage
|
<template v-for="msg in messages" :key="msg.id">
|
||||||
v-for="msg in messages"
|
<!-- Weather card above the first assistant message with weather metadata -->
|
||||||
:key="msg.id"
|
<WeatherCard
|
||||||
:message="toMsg(msg)"
|
v-if="msg.role === 'assistant' && msgMetadata(msg)?.weather !== undefined"
|
||||||
:is-streaming="false"
|
: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 -->
|
<!-- Live streaming bubble for today -->
|
||||||
<ChatMessage
|
<ChatMessage
|
||||||
v-if="isToday && chatStore.streaming && chatStore.streamingContent"
|
v-if="isToday && chatStore.streaming && chatStore.streamingContent"
|
||||||
@@ -392,4 +461,47 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
.btn-send:disabled { opacity: 0.4; cursor: not-allowed; }
|
.btn-send:disabled { opacity: 0.4; cursor: not-allowed; }
|
||||||
.btn-send:hover:not(:disabled) { opacity: 0.9; }
|
.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>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user