feat: redesign BriefingView into 3-column layout (weather · chat · news)
- Left column: weather loaded independently via /api/briefing/weather - Center column: chat messages with input bar (unchanged behavior) - Right column: news panel loaded from /api/briefing/news (last 2 days) - Auto-scroll to bottom on mount and after streaming - Background refresh also refreshes news panel - Responsive: stacks to single column on narrow screens - Fix TaskCard.vue to include 'cancelled' in status records Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -20,18 +20,21 @@ const statusCycle: Record<TaskStatus, TaskStatus> = {
|
|||||||
todo: "in_progress",
|
todo: "in_progress",
|
||||||
in_progress: "done",
|
in_progress: "done",
|
||||||
done: "todo",
|
done: "todo",
|
||||||
|
cancelled: "todo",
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusDotClass: Record<TaskStatus, string> = {
|
const statusDotClass: Record<TaskStatus, string> = {
|
||||||
todo: "dot-todo",
|
todo: "dot-todo",
|
||||||
in_progress: "dot-in-progress",
|
in_progress: "dot-in-progress",
|
||||||
done: "dot-done",
|
done: "dot-done",
|
||||||
|
cancelled: "dot-cancelled",
|
||||||
};
|
};
|
||||||
|
|
||||||
const statusTitle: Record<TaskStatus, string> = {
|
const statusTitle: Record<TaskStatus, string> = {
|
||||||
todo: "Todo — click to mark In Progress",
|
todo: "Todo — click to mark In Progress",
|
||||||
in_progress: "In Progress — click to mark Done",
|
in_progress: "In Progress — click to mark Done",
|
||||||
done: "Done — click to mark Todo",
|
done: "Done — click to mark Todo",
|
||||||
|
cancelled: "Cancelled — click to mark Todo",
|
||||||
};
|
};
|
||||||
|
|
||||||
function cycleStatus() {
|
function cycleStatus() {
|
||||||
@@ -152,6 +155,9 @@ function isOverdue(): boolean {
|
|||||||
.dot-done {
|
.dot-done {
|
||||||
background: var(--color-status-done, #22c55e);
|
background: var(--color-status-done, #22c55e);
|
||||||
}
|
}
|
||||||
|
.dot-cancelled {
|
||||||
|
background: var(--color-status-cancelled, #6b7280);
|
||||||
|
}
|
||||||
|
|
||||||
.task-title-compact {
|
.task-title-compact {
|
||||||
font-size: 0.9rem;
|
font-size: 0.9rem;
|
||||||
|
|||||||
+268
-139
@@ -1,11 +1,12 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, watch } from 'vue'
|
import { ref, computed, onMounted, watch, nextTick } from 'vue'
|
||||||
import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
|
import { useBackgroundRefresh } from '@/composables/useBackgroundRefresh'
|
||||||
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 WeatherCard from '@/components/WeatherCard.vue'
|
||||||
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
|
import BriefingSetupWizard from '@/components/BriefingSetupWizard.vue'
|
||||||
import {
|
import {
|
||||||
|
apiGet,
|
||||||
getBriefingConfig,
|
getBriefingConfig,
|
||||||
getBriefingConversations,
|
getBriefingConversations,
|
||||||
getBriefingToday,
|
getBriefingToday,
|
||||||
@@ -13,34 +14,23 @@ import {
|
|||||||
triggerBriefingSlot,
|
triggerBriefingSlot,
|
||||||
postRssReaction,
|
postRssReaction,
|
||||||
deleteRssReaction,
|
deleteRssReaction,
|
||||||
|
getNewsItems,
|
||||||
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'
|
||||||
|
import type { NewsItem } from '@/types/news'
|
||||||
|
|
||||||
interface RssItemMeta {
|
interface WeatherData {
|
||||||
id: number
|
location: string
|
||||||
title: string
|
fetched_at: string
|
||||||
url: string
|
current_temp: number
|
||||||
source: string
|
condition: string
|
||||||
snippet: string
|
today_high: number | null
|
||||||
published_at: string | null
|
today_low: number | null
|
||||||
}
|
yesterday_high: number | null
|
||||||
|
yesterday_low: number | null
|
||||||
interface MessageMetadata {
|
forecast: { day: string; condition: string; high: number; low: number }[]
|
||||||
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[]
|
|
||||||
rss_items?: RssItemMeta[]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const chatStore = useChatStore()
|
const chatStore = useChatStore()
|
||||||
@@ -71,15 +61,53 @@ const isToday = computed(() => selectedConvId.value === todayConvId.value)
|
|||||||
const messages = ref<BriefingMessage[]>([])
|
const messages = ref<BriefingMessage[]>([])
|
||||||
const loadingMessages = ref(false)
|
const loadingMessages = ref(false)
|
||||||
|
|
||||||
|
// Weather panel (left column)
|
||||||
|
const weatherData = ref<WeatherData[]>([])
|
||||||
|
|
||||||
|
async function loadWeather() {
|
||||||
|
try {
|
||||||
|
const data = await apiGet<{ locations: WeatherData[] }>('/api/briefing/weather')
|
||||||
|
weatherData.value = data.locations ?? []
|
||||||
|
} catch { /* silent */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// News panel (right column)
|
||||||
|
const newsItems = ref<NewsItem[]>([])
|
||||||
|
|
||||||
|
async function loadNews() {
|
||||||
|
try {
|
||||||
|
const data = await getNewsItems({ days: 2, limit: 40 })
|
||||||
|
newsItems.value = data.items
|
||||||
|
// Seed reactions from API response
|
||||||
|
for (const item of data.items) {
|
||||||
|
if (reactions.value[item.id] === undefined) {
|
||||||
|
reactions.value[item.id] = item.reaction
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch { /* silent */ }
|
||||||
|
}
|
||||||
|
|
||||||
|
// Scroll to bottom of messages
|
||||||
|
const messagesEl = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
|
function scrollToBottom() {
|
||||||
|
nextTick(() => {
|
||||||
|
if (messagesEl.value) {
|
||||||
|
messagesEl.value.scrollTop = messagesEl.value.scrollHeight
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
async function loadAll() {
|
async function loadAll() {
|
||||||
const [convList, today] = await Promise.all([
|
const [convList, today] = await Promise.all([
|
||||||
getBriefingConversations(),
|
getBriefingConversations(),
|
||||||
getBriefingToday().catch(() => null),
|
getBriefingToday().catch(() => null),
|
||||||
|
loadWeather(),
|
||||||
|
loadNews(),
|
||||||
])
|
])
|
||||||
conversations.value = convList
|
conversations.value = convList
|
||||||
if (today) {
|
if (today) {
|
||||||
todayConvId.value = today.id
|
todayConvId.value = today.id
|
||||||
// Ensure today is in the list
|
|
||||||
if (!convList.find((c) => c.id === today.id)) {
|
if (!convList.find((c) => c.id === today.id)) {
|
||||||
conversations.value = [
|
conversations.value = [
|
||||||
{ id: today.id, title: today.title ?? 'Today', briefing_date: null, message_count: 0, created_at: new Date().toISOString() },
|
{ id: today.id, title: today.title ?? 'Today', briefing_date: null, message_count: 0, created_at: new Date().toISOString() },
|
||||||
@@ -88,9 +116,9 @@ async function loadAll() {
|
|||||||
}
|
}
|
||||||
selectedConvId.value = today.id
|
selectedConvId.value = today.id
|
||||||
messages.value = today.messages
|
messages.value = today.messages
|
||||||
// Load into chatStore so we can stream
|
|
||||||
await chatStore.fetchConversation(today.id)
|
await chatStore.fetchConversation(today.id)
|
||||||
}
|
}
|
||||||
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
|
|
||||||
watch(selectedConvId, async (id) => {
|
watch(selectedConvId, async (id) => {
|
||||||
@@ -98,11 +126,13 @@ watch(selectedConvId, async (id) => {
|
|||||||
if (id === todayConvId.value) {
|
if (id === todayConvId.value) {
|
||||||
await chatStore.fetchConversation(id)
|
await chatStore.fetchConversation(id)
|
||||||
messages.value = (chatStore.currentConversation?.messages ?? []) as unknown as BriefingMessage[]
|
messages.value = (chatStore.currentConversation?.messages ?? []) as unknown as BriefingMessage[]
|
||||||
|
scrollToBottom()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
loadingMessages.value = true
|
loadingMessages.value = true
|
||||||
try {
|
try {
|
||||||
messages.value = await getBriefingConvMessages(id)
|
messages.value = await getBriefingConvMessages(id)
|
||||||
|
scrollToBottom()
|
||||||
} finally {
|
} finally {
|
||||||
loadingMessages.value = false
|
loadingMessages.value = false
|
||||||
}
|
}
|
||||||
@@ -113,6 +143,7 @@ watch(() => chatStore.streaming, async (streaming) => {
|
|||||||
if (!streaming && selectedConvId.value === todayConvId.value && todayConvId.value) {
|
if (!streaming && selectedConvId.value === todayConvId.value && todayConvId.value) {
|
||||||
const today = await getBriefingToday().catch(() => null)
|
const today = await getBriefingToday().catch(() => null)
|
||||||
if (today) messages.value = today.messages
|
if (today) messages.value = today.messages
|
||||||
|
scrollToBottom()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -123,7 +154,6 @@ const sending = ref(false)
|
|||||||
async function send() {
|
async function send() {
|
||||||
const text = input.value.trim()
|
const text = input.value.trim()
|
||||||
if (!text || !todayConvId.value || chatStore.streaming || sending.value) return
|
if (!text || !todayConvId.value || chatStore.streaming || sending.value) return
|
||||||
// Ensure today's conv is loaded in chatStore
|
|
||||||
if (chatStore.currentConversation?.id !== todayConvId.value) {
|
if (chatStore.currentConversation?.id !== todayConvId.value) {
|
||||||
await chatStore.fetchConversation(todayConvId.value)
|
await chatStore.fetchConversation(todayConvId.value)
|
||||||
}
|
}
|
||||||
@@ -160,10 +190,6 @@ async function handleReaction(itemId: number, reaction: 'up' | 'down') {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function msgMetadata(msg: BriefingMessage): MessageMetadata | null {
|
|
||||||
return (msg.metadata as MessageMetadata | null | undefined) ?? null
|
|
||||||
}
|
|
||||||
|
|
||||||
function formatRelativeDate(iso: string | null): string {
|
function formatRelativeDate(iso: string | null): string {
|
||||||
if (!iso) return ''
|
if (!iso) return ''
|
||||||
const d = new Date(iso)
|
const d = new Date(iso)
|
||||||
@@ -180,7 +206,6 @@ async function triggerNow() {
|
|||||||
triggering.value = true
|
triggering.value = true
|
||||||
try {
|
try {
|
||||||
await triggerBriefingSlot('compilation')
|
await triggerBriefingSlot('compilation')
|
||||||
// Reload
|
|
||||||
await loadAll()
|
await loadAll()
|
||||||
} finally {
|
} finally {
|
||||||
triggering.value = false
|
triggering.value = false
|
||||||
@@ -221,6 +246,7 @@ async function _backgroundRefreshMessages() {
|
|||||||
if (fresh.length !== messages.value.length || last?.content !== cur?.content) {
|
if (fresh.length !== messages.value.length || last?.content !== cur?.content) {
|
||||||
messages.value = fresh
|
messages.value = fresh
|
||||||
}
|
}
|
||||||
|
await loadNews()
|
||||||
} catch { /* silent — don't disturb the UI on network hiccup */ }
|
} catch { /* silent — don't disturb the UI on network hiccup */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,16 +267,15 @@ onMounted(async () => {
|
|||||||
<!-- Setup wizard overlay -->
|
<!-- Setup wizard overlay -->
|
||||||
<BriefingSetupWizard v-if="wizardChecked && showWizard" @done="onWizardDone" />
|
<BriefingSetupWizard v-if="wizardChecked && showWizard" @done="onWizardDone" />
|
||||||
|
|
||||||
<!-- Main view (shown after wizard check and only if not showing wizard) -->
|
<!-- Main view -->
|
||||||
<div class="briefing-shell" v-if="wizardChecked && !showWizard">
|
<div class="briefing-shell" v-if="wizardChecked && !showWizard">
|
||||||
<!-- Header -->
|
<!-- Header spans all columns -->
|
||||||
<header class="briefing-header">
|
<header class="briefing-header">
|
||||||
<div class="briefing-header-left">
|
<div class="briefing-header-left">
|
||||||
<h1 class="briefing-title">Briefing</h1>
|
<h1 class="briefing-title">Briefing</h1>
|
||||||
<span class="briefing-today-badge">{{ new Date().toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }) }}</span>
|
<span class="briefing-today-badge">{{ new Date().toLocaleDateString(undefined, { weekday: 'long', month: 'long', day: 'numeric' }) }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="briefing-header-right">
|
<div class="briefing-header-right">
|
||||||
<!-- Conversation history dropdown -->
|
|
||||||
<select v-if="conversations.length" v-model="selectedConvId" class="briefing-conv-select">
|
<select v-if="conversations.length" v-model="selectedConvId" class="briefing-conv-select">
|
||||||
<option v-for="c in conversations" :key="c.id" :value="c.id">{{ convLabel(c) }}</option>
|
<option v-for="c in conversations" :key="c.id" :value="c.id">{{ convLabel(c) }}</option>
|
||||||
</select>
|
</select>
|
||||||
@@ -263,102 +288,115 @@ onMounted(async () => {
|
|||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Messages -->
|
<!-- Left column: Weather -->
|
||||||
<div class="briefing-messages-wrap">
|
<div class="briefing-left">
|
||||||
<div v-if="loadingMessages" class="briefing-loading">Loading…</div>
|
<div class="panel-label">Weather</div>
|
||||||
<template v-else>
|
<template v-if="weatherData.length">
|
||||||
<div v-if="!messages.length" class="briefing-empty">
|
<WeatherCard
|
||||||
<p>No briefing yet for today.</p>
|
v-for="loc in weatherData"
|
||||||
<p class="briefing-empty-hint">Click "Refresh" to generate a briefing now, or wait for the scheduled slot.</p>
|
:key="(loc as WeatherData).location"
|
||||||
</div>
|
:weather="loc"
|
||||||
<div v-else class="briefing-messages">
|
/>
|
||||||
<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"
|
|
||||||
/>
|
|
||||||
<!-- News cards below assistant messages with rss_items -->
|
|
||||||
<div
|
|
||||||
v-if="msg.role === 'assistant' && (msgMetadata(msg)?.rss_items?.length ?? 0) > 0"
|
|
||||||
class="news-cards"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-for="item in msgMetadata(msg)!.rss_items"
|
|
||||||
:key="item.id"
|
|
||||||
class="news-card"
|
|
||||||
>
|
|
||||||
<div class="news-card-meta">
|
|
||||||
<span class="news-source">{{ item.source }}</span>
|
|
||||||
<span v-if="item.published_at" class="news-date">{{ formatRelativeDate(item.published_at) }}</span>
|
|
||||||
</div>
|
|
||||||
<a
|
|
||||||
v-if="item.url"
|
|
||||||
:href="item.url"
|
|
||||||
target="_blank"
|
|
||||||
rel="noopener noreferrer"
|
|
||||||
class="news-title"
|
|
||||||
>{{ item.title }}</a>
|
|
||||||
<p v-else class="news-title news-title--plain">{{ item.title }}</p>
|
|
||||||
<p v-if="item.snippet" class="news-snippet">{{ item.snippet }}</p>
|
|
||||||
<div class="news-reactions">
|
|
||||||
<button
|
|
||||||
class="reaction-btn"
|
|
||||||
:class="{ active: reactions[item.id] === 'up' }"
|
|
||||||
@click="handleReaction(item.id, 'up')"
|
|
||||||
title="Interested"
|
|
||||||
>👍</button>
|
|
||||||
<button
|
|
||||||
class="reaction-btn"
|
|
||||||
:class="{ active: reactions[item.id] === 'down' }"
|
|
||||||
@click="handleReaction(item.id, 'down')"
|
|
||||||
title="Not interested"
|
|
||||||
>👎</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<!-- Live streaming bubble for today -->
|
|
||||||
<ChatMessage
|
|
||||||
v-if="isToday && chatStore.streaming && chatStore.streamingContent"
|
|
||||||
:message="{
|
|
||||||
id: -1,
|
|
||||||
conversation_id: todayConvId ?? -1,
|
|
||||||
role: 'assistant',
|
|
||||||
content: chatStore.streamingContent,
|
|
||||||
context_note_id: null,
|
|
||||||
context_note_title: null,
|
|
||||||
created_at: new Date().toISOString(),
|
|
||||||
}"
|
|
||||||
:is-streaming="true"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
<div v-else class="panel-empty">No weather configured</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Input bar (today only) -->
|
<!-- Center column: Chat -->
|
||||||
<div v-if="isToday" class="briefing-input-bar">
|
<div class="briefing-center">
|
||||||
<textarea
|
<div class="briefing-messages-wrap" ref="messagesEl">
|
||||||
v-model="input"
|
<div v-if="loadingMessages" class="briefing-loading">Loading…</div>
|
||||||
class="briefing-input"
|
<template v-else>
|
||||||
placeholder="Reply to your briefing…"
|
<div v-if="!messages.length" class="briefing-empty">
|
||||||
rows="1"
|
<p>No briefing yet for today.</p>
|
||||||
@keydown="onKeydown"
|
<p class="briefing-empty-hint">Click "Refresh" to generate a briefing now, or wait for the scheduled slot.</p>
|
||||||
></textarea>
|
</div>
|
||||||
<button
|
<div v-else class="briefing-messages">
|
||||||
class="btn-send"
|
<template v-for="msg in messages" :key="msg.id">
|
||||||
@click="send"
|
<ChatMessage
|
||||||
:disabled="!input.trim() || chatStore.streaming || sending"
|
:message="toMsg(msg)"
|
||||||
aria-label="Send"
|
:is-streaming="false"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
<!-- Live streaming bubble for today -->
|
||||||
|
<ChatMessage
|
||||||
|
v-if="isToday && chatStore.streaming && chatStore.streamingContent"
|
||||||
|
:message="{
|
||||||
|
id: -1,
|
||||||
|
conversation_id: todayConvId ?? -1,
|
||||||
|
role: 'assistant',
|
||||||
|
content: chatStore.streamingContent,
|
||||||
|
context_note_id: null,
|
||||||
|
context_note_title: null,
|
||||||
|
created_at: new Date().toISOString(),
|
||||||
|
}"
|
||||||
|
:is-streaming="true"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Input bar (today only) -->
|
||||||
|
<div v-if="isToday" class="briefing-input-bar">
|
||||||
|
<textarea
|
||||||
|
v-model="input"
|
||||||
|
class="briefing-input"
|
||||||
|
placeholder="Reply to your briefing…"
|
||||||
|
rows="1"
|
||||||
|
@keydown="onKeydown"
|
||||||
|
></textarea>
|
||||||
|
<button
|
||||||
|
class="btn-send"
|
||||||
|
@click="send"
|
||||||
|
:disabled="!input.trim() || chatStore.streaming || sending"
|
||||||
|
aria-label="Send"
|
||||||
|
>
|
||||||
|
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
||||||
|
<path d="M2 21l21-9L2 3v7l15 2-15 2v7z"/>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Right column: News -->
|
||||||
|
<div class="briefing-right">
|
||||||
|
<div class="panel-label-row">
|
||||||
|
<div class="panel-label">Today's News</div>
|
||||||
|
<span v-if="newsItems.length" class="news-count">{{ newsItems.length }} items</span>
|
||||||
|
</div>
|
||||||
|
<div v-if="!newsItems.length" class="panel-empty">No articles in the last 2 days</div>
|
||||||
|
<div
|
||||||
|
v-for="item in newsItems"
|
||||||
|
:key="item.id"
|
||||||
|
class="news-card"
|
||||||
>
|
>
|
||||||
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
|
<div class="news-card-meta">
|
||||||
<path d="M2 21l21-9L2 3v7l15 2-15 2v7z"/>
|
<span class="news-source">{{ item.source }}</span>
|
||||||
</svg>
|
<span v-if="item.published_at" class="news-date">{{ formatRelativeDate(item.published_at) }}</span>
|
||||||
</button>
|
</div>
|
||||||
|
<a
|
||||||
|
v-if="item.url"
|
||||||
|
:href="item.url"
|
||||||
|
target="_blank"
|
||||||
|
rel="noopener noreferrer"
|
||||||
|
class="news-title"
|
||||||
|
>{{ item.title }}</a>
|
||||||
|
<p v-else class="news-title news-title--plain">{{ item.title }}</p>
|
||||||
|
<p v-if="item.snippet" class="news-snippet">{{ item.snippet }}</p>
|
||||||
|
<div class="news-reactions">
|
||||||
|
<button
|
||||||
|
class="reaction-btn"
|
||||||
|
:class="{ active: reactions[item.id] === 'up' }"
|
||||||
|
@click="handleReaction(item.id, 'up')"
|
||||||
|
title="Interested"
|
||||||
|
>👍</button>
|
||||||
|
<button
|
||||||
|
class="reaction-btn"
|
||||||
|
:class="{ active: reactions[item.id] === 'down' }"
|
||||||
|
@click="handleReaction(item.id, 'down')"
|
||||||
|
title="Not interested"
|
||||||
|
>👎</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -373,21 +411,20 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
.briefing-shell {
|
.briefing-shell {
|
||||||
display: flex;
|
display: grid;
|
||||||
flex-direction: column;
|
grid-template-columns: 1fr 2fr 1fr;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
max-width: 760px;
|
|
||||||
margin: 0 auto;
|
|
||||||
width: 100%;
|
|
||||||
padding: 0 1rem;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.briefing-header {
|
.briefing-header {
|
||||||
|
grid-column: 1 / -1;
|
||||||
|
grid-row: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 1.25rem 0 1rem;
|
padding: 1.25rem 1rem 1rem;
|
||||||
border-bottom: 1px solid var(--color-border);
|
border-bottom: 1px solid var(--color-border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
gap: 1rem;
|
gap: 1rem;
|
||||||
@@ -448,10 +485,38 @@ onMounted(async () => {
|
|||||||
}
|
}
|
||||||
.btn-trigger:disabled { opacity: 0.5; cursor: not-allowed; }
|
.btn-trigger:disabled { opacity: 0.5; cursor: not-allowed; }
|
||||||
|
|
||||||
|
/* ─── Left column (Weather) ──────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.briefing-left {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
border-right: 1px solid var(--color-border);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.briefing-left :deep(.weather-card) {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* ─── Center column (Chat) ───────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.briefing-center {
|
||||||
|
grid-column: 2;
|
||||||
|
grid-row: 2;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.briefing-messages-wrap {
|
.briefing-messages-wrap {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
overflow-y: auto;
|
overflow-y: auto;
|
||||||
padding: 1rem 0;
|
padding: 1rem;
|
||||||
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.briefing-loading,
|
.briefing-loading,
|
||||||
@@ -478,7 +543,7 @@ onMounted(async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
align-items: flex-end;
|
align-items: flex-end;
|
||||||
padding: 0.75rem 0 1rem;
|
padding: 0.75rem 1rem 1rem;
|
||||||
border-top: 1px solid var(--color-border);
|
border-top: 1px solid var(--color-border);
|
||||||
flex-shrink: 0;
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
@@ -517,12 +582,44 @@ 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; }
|
||||||
|
|
||||||
.news-cards {
|
/* ─── Right column (News) ────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
.briefing-right {
|
||||||
|
grid-column: 3;
|
||||||
|
grid-row: 2;
|
||||||
|
border-left: 1px solid var(--color-border);
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 1rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.5rem;
|
gap: 0.5rem;
|
||||||
margin-top: 0.25rem;
|
}
|
||||||
margin-bottom: 0.25rem;
|
|
||||||
|
.panel-label {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
font-weight: 700;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.06em;
|
||||||
|
color: var(--color-primary);
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-label-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.news-count {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel-empty {
|
||||||
|
font-size: 0.82rem;
|
||||||
|
color: var(--color-text-muted);
|
||||||
|
padding: 0.5rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-card {
|
.news-card {
|
||||||
@@ -533,6 +630,7 @@ onMounted(async () => {
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 0.25rem;
|
gap: 0.25rem;
|
||||||
|
flex-shrink: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.news-card-meta {
|
.news-card-meta {
|
||||||
@@ -604,4 +702,35 @@ a.news-title:hover { text-decoration: underline; color: var(--color-primary); }
|
|||||||
border-color: var(--color-primary);
|
border-color: var(--color-primary);
|
||||||
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
background: color-mix(in srgb, var(--color-primary) 12%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* ─── Responsive ─────────────────────────────────────────────────────────── */
|
||||||
|
|
||||||
|
@media (max-width: 900px) {
|
||||||
|
.briefing-shell {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto auto 1fr auto;
|
||||||
|
}
|
||||||
|
.briefing-header {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 1;
|
||||||
|
}
|
||||||
|
.briefing-left {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 2;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid var(--color-border);
|
||||||
|
max-height: 220px;
|
||||||
|
}
|
||||||
|
.briefing-center {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 3;
|
||||||
|
}
|
||||||
|
.briefing-right {
|
||||||
|
grid-column: 1;
|
||||||
|
grid-row: 4;
|
||||||
|
border-left: none;
|
||||||
|
border-top: 1px solid var(--color-border);
|
||||||
|
max-height: 260px;
|
||||||
|
}
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
Reference in New Issue
Block a user