From 721ddbeef95620134d334dbd6bfba2ec33bbf2cd Mon Sep 17 00:00:00 2001 From: "WIN-JHFT4D3SIVT\\caoxiaozhu" Date: Tue, 7 Apr 2026 10:28:31 +0800 Subject: [PATCH] feat(frontend): add calendar click to switch conversation by date - Add selected date state and conversation mapping in useSidebarPlan - Connect calendar cells to conversation switching logic - Add conversation indicator dot on dates with sessions - Only clickable dates show hand cursor (today + dates with conversations) - Add .selected styling for non-today dates, today keeps blue - Fix hover effect to only apply to non-today dates - Add daily doc for session date mapping feature BREAKING: Calendar click now switches sessions by date --- development-doc/daily/2026-04-07.md | 86 +++++++++++++++++++ frontend/src/pages/chat/chatPage.css | 51 ++++++++++- .../src/pages/chat/composables/useChatView.ts | 1 + .../pages/chat/composables/useSidebarPlan.ts | 32 +++++-- frontend/src/pages/chat/index.vue | 73 ++++++++-------- 5 files changed, 198 insertions(+), 45 deletions(-) create mode 100644 development-doc/daily/2026-04-07.md diff --git a/development-doc/daily/2026-04-07.md b/development-doc/daily/2026-04-07.md new file mode 100644 index 0000000..d5755db --- /dev/null +++ b/development-doc/daily/2026-04-07.md @@ -0,0 +1,86 @@ +# 2026-04-07 工作日志 + +## 今日开发计划 + +### 今日目标 + +- 梳理 chat 页面左侧日历与 conversation session 的关系 +- 明确“按日期点击切换 session”的改造方向 +- 记录当前 session 机制与后续实现方案 + +### 今日计划拆分 + +1. 盘点当前 conversation session 的数据结构与切换逻辑 +2. 确认 session 当前不是按“天”进行切分 +3. 设计日历点击后的 session 切换方案 +4. 将方案记录到 daily,作为后续改造依据 + +--- + +## 今日实际完成 + +- 检查了前端 `conversation store`、`conversation api` 与后端 `conversation router / model` +- 确认当前 conversation session 以 `conversation_id` 为核心,不是按“天”自动切分 +- 确认现有字段主要依赖 `created_at` / `updated_at` 做时间记录,但单个 session 可跨多天持续使用 +- 明确了左侧日历点击切换 session 的推荐改造方式 + +--- + +## 当前结论 + +### session 现状 + +- 当前 session 不是以“天”为单位计算 +- 当前会话列表来源于 `/api/conversations`,按 `updated_at` 倒序展示 +- 点击某一天时,不能直接假定“一天对应一个现成 session” + +### 推荐改造方案 + +采用“**保留现有 conversation 结构 + 前端增加按日期筛选/映射**”的方案: + +1. 保持后端 `Conversation` / `Message` 结构不变 +2. 前端基于 `created_at` 或 `updated_at` 将 conversations 映射到具体日期 +3. 左侧日历某天被点击后,优先切换到该日期最近一次活跃的 session +4. 如果该日期没有 session,则进入新会话态,必要时再创建新的 conversation +5. 会话本质仍是 conversation,不强制把数据库层改成“每天一个 session” + +### 这样处理的原因 + +- 不需要重做现有 conversation 数据模型 +- 不会破坏当前多轮上下文连续性 +- 可以快速给日历交互增加“按日期查看/切换”能力 +- 后续如果要做“每日会话视图”或“按天归档”也更容易扩展 + +--- + +## 建议的后续实现点 + +1. 在前端增加“选中日期”状态 +2. 将 conversations 按日期建立索引映射 +3. 日历点击时,根据日期找到对应 session 并调用 `selectConversation` +4. 如果无匹配 session,则清空当前消息区并进入新会话态 +5. 视需要补充“当天无会话”的空态提示 + +--- + +## 风险与临时决策 + +### 风险 + +- 如果一个 conversation 跨多天活跃,按 `created_at` 还是 `updated_at` 归属日期需要统一规则 +- 如果同一天有多个 session,需要定义点击日历后的优先选择策略 + +### 当前临时决策 + +- 先按 `updated_at` 作为日历映射依据,更符合“最近活跃”的使用直觉 +- 先选择该日期下最近活跃的一条 conversation 作为默认切换目标 +- 暂不改数据库,不引入“每天强制新建 session”的硬规则 + +--- + +## 下一步计划 + +1. 在 chat 页整理日历点击事件接入点 +2. 补充 conversations 与日期映射的前端计算逻辑 +3. 明确空态与多 session 同日时的交互细节 +4. 开始实现“点击日历切换 session”功能 diff --git a/frontend/src/pages/chat/chatPage.css b/frontend/src/pages/chat/chatPage.css index b2c0cbf..60c8d7f 100644 --- a/frontend/src/pages/chat/chatPage.css +++ b/frontend/src/pages/chat/chatPage.css @@ -616,8 +616,8 @@ /* ── Conversation Sidebar ── */ .conv-sidebar { - width: 280px; - min-width: 280px; + width: 320px; + min-width: 320px; background: var(--bg-panel); border-right: 1px solid rgba(0, 245, 212, 0.15); display: flex; @@ -948,18 +948,28 @@ cursor: not-allowed; } -.chat-model { +.chat-model-display { display: flex; align-items: center; gap: 6px; font-family: var(--font-display); font-size: 9px; - letter-spacing: 0.1em; + letter-spacing: 0.06em; color: var(--accent-amber); padding: 3px 10px; border: 1px solid rgba(249, 168, 37, 0.2); border-radius: 20px; background: var(--accent-amber-dim); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; + max-width: 300px; +} + +.chat-model-display span { + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } /* ── Messages ── */ @@ -1775,6 +1785,7 @@ padding: 3px 0; border-radius: 2px; color: rgba(0, 243, 255, 0.4); + position: relative; } .calendar-day.active { @@ -2279,6 +2290,7 @@ border: 1px solid rgba(73, 208, 255, 0.06); background: rgba(255, 255, 255, 0.02); color: rgba(208, 240, 252, 0.68); + transition: none; } .calendar-day.muted { @@ -2304,6 +2316,37 @@ border-color: rgba(125, 211, 252, 0.78); font-weight: 700; box-shadow: 0 0 16px rgba(56, 189, 248, 0.22); + cursor: pointer; +} + +.calendar-day.selected:not(.active) { + border-color: rgba(245, 158, 11, 0.6); + box-shadow: 0 0 12px rgba(245, 158, 11, 0.4); + background: rgba(245, 158, 11, 0.15); +} + +.calendar-day.clickable { + cursor: pointer; +} + +.calendar-day.clickable:hover:not(.active) { + border-color: rgba(0, 243, 255, 0.4); + background: rgba(0, 243, 255, 0.08); +} + +.calendar-day.active:hover { + /* Keep original blue, no hover effect */ +} + +.conv-indicator { + position: absolute; + top: 3px; + right: 3px; + width: 4px; + height: 4px; + border-radius: 50%; + background: var(--accent-cyan); + box-shadow: 0 0 6px rgba(0, 243, 255, 0.6); } .calendar-day.active.busy::after { diff --git a/frontend/src/pages/chat/composables/useChatView.ts b/frontend/src/pages/chat/composables/useChatView.ts index b251cd1..e7fec9b 100644 --- a/frontend/src/pages/chat/composables/useChatView.ts +++ b/frontend/src/pages/chat/composables/useChatView.ts @@ -729,6 +729,7 @@ export function useChatView() { sendMessage, selectConversation, newConversation, + loadConversations, deleteConversation, formatTime, formatConvDate, diff --git a/frontend/src/pages/chat/composables/useSidebarPlan.ts b/frontend/src/pages/chat/composables/useSidebarPlan.ts index 35fd21c..e4e17d8 100644 --- a/frontend/src/pages/chat/composables/useSidebarPlan.ts +++ b/frontend/src/pages/chat/composables/useSidebarPlan.ts @@ -1,6 +1,7 @@ -import { computed, onMounted, ref, watch } from 'vue' +import { computed, onMounted, ref, watch, toRef } from 'vue' import { CornerDownLeft, Database, Sparkles, Sun, ListTodo } from 'lucide-vue-next' import { scheduleCenterApi, type ScheduleCenterDateResponse, type ScheduleCenterDaySummary } from '@/api/scheduleCenter' +import type { Conversation } from '@/api/conversation' export interface SidebarFocusItem { id: string; label: string; title: string; meta: string; tone: 'done' | 'doing' | 'pending' @@ -30,9 +31,20 @@ function formatMonthKey(date: Date) { return `${year}-${month}` } -export function useSidebarPlan(clientTimeRef: { value: Date }, loadDailyDigestFn: () => void) { +export function useSidebarPlan(clientTimeRef: { value: Date }, loadDailyDigestFn: () => void, conversationsRef: Conversation[] = []) { const todayPlanDetail = ref(null) const monthPlanDays = ref([]) + const selectedDate = ref(null) + + // Build a map of date -> has conversation + const conversationDateMap = computed(() => { + const map = new Map() + conversationsRef.forEach((conv) => { + const dateKey = formatDateKey(new Date(conv.updated_at)) + map.set(dateKey, true) + }) + return map + }) const todayDateKey = computed(() => formatDateKey(clientTimeRef.value)) const monthPlanSummaryMap = computed(() => new Map(monthPlanDays.value.map((item) => [item.date, item]))) @@ -42,19 +54,20 @@ export function useSidebarPlan(clientTimeRef: { value: Date }, loadDailyDigestFn const month = clientTimeRef.value.getMonth() const daysInMonth = new Date(year, month + 1, 0).getDate() const firstDayOffset = (new Date(year, month, 1).getDay() + 6) % 7 - const cells: Array<{ key: string; value: number | null; active: boolean; busy: boolean }> = [] + const cells: Array<{ key: string; value: number | null; active: boolean; busy: boolean; selected: boolean; hasConversation: boolean }> = [] for (let index = 0; index < firstDayOffset; index += 1) { - cells.push({ key: `blank-start-${index}`, value: null, active: false, busy: false }) + cells.push({ key: `blank-start-${index}`, value: null, active: false, busy: false, selected: false, hasConversation: false }) } for (let day = 1; day <= daysInMonth; day += 1) { const monthDate = new Date(year, month, day) const dateKey = formatDateKey(monthDate) const summary = monthPlanSummaryMap.value.get(dateKey) const busy = Boolean(summary && (summary.todo_total + summary.task_due_total + summary.goal_total + summary.reminder_total) > 0) - cells.push({ key: dateKey, value: day, active: day === clientTimeRef.value.getDate(), busy }) + const hasConv = conversationDateMap.value.get(dateKey) || false + cells.push({ key: dateKey, value: day, active: day === clientTimeRef.value.getDate(), busy, selected: dateKey === selectedDate.value, hasConversation: hasConv }) } while (cells.length % 7 !== 0) { - cells.push({ key: `blank-end-${cells.length}`, value: null, active: false, busy: false }) + cells.push({ key: `blank-end-${cells.length}`, value: null, active: false, busy: false, selected: false, hasConversation: false }) } return cells }) @@ -201,6 +214,10 @@ export function useSidebarPlan(clientTimeRef: { value: Date }, loadDailyDigestFn void loadSidebarPlanSnapshot(clientTimeRef.value) }) + function selectCalendarDate(dateKey: string) { + selectedDate.value = dateKey + } + onMounted(() => { void loadDailyDigestFn() void loadSidebarPlanSnapshot(new Date()) @@ -211,6 +228,7 @@ export function useSidebarPlan(clientTimeRef: { value: Date }, loadDailyDigestFn calendarCells, calendarYear, calendarMonth, todayPlanCounters, monthReviewStats, sidebarWeekLabels, sidebarStatusHeadline, sidebarStatusBreakdown, sidebarFocusItems, sidebarReviewAchievements, sidebarReviewReflections, - sidebarFeedItems, topbarFeedItems, loadSidebarPlanSnapshot, sidebarCollapsedModules + sidebarFeedItems, topbarFeedItems, loadSidebarPlanSnapshot, sidebarCollapsedModules, + selectedDate, selectCalendarDate, conversationDateMap } } diff --git a/frontend/src/pages/chat/index.vue b/frontend/src/pages/chat/index.vue index dd99650..3cc2d9b 100644 --- a/frontend/src/pages/chat/index.vue +++ b/frontend/src/pages/chat/index.vue @@ -50,6 +50,9 @@ const { systemTelemetry, sessionTelemetry, sendMessage, + selectConversation, + newConversation, + loadConversations, formatTime, autoResize, handleFileSelect, @@ -74,8 +77,9 @@ const { calendarCells, calendarYear, calendarMonth, todayPlanCounters, sidebarWeekLabels, sidebarStatusHeadline, sidebarStatusBreakdown, sidebarFocusItems, sidebarReviewAchievements, sidebarReviewReflections, - sidebarFeedItems, topbarFeedItems, sidebarCollapsedModules -} = useSidebarPlan(clientTime, loadDailyDigest) + sidebarFeedItems, topbarFeedItems, sidebarCollapsedModules, + selectedDate, selectCalendarDate +} = useSidebarPlan(clientTime, loadDailyDigest, store.conversations) // --- Local UI state --- const sidebarCollapsed = ref(false) @@ -111,6 +115,33 @@ function closeKnowledgeHud() { knowledgeHudOpen.value = false } function handleSelectFolder(folder: any) { selectedFolder.value = folder } function handleOpenPreview(doc: any) { previewDoc.value = doc } +function handleCalendarDateSelect(dateKey: string) { + selectCalendarDate(dateKey) + + // Reload conversations to get latest data + loadConversations().then(() => { + // Find conversation that matches the selected date (by updated_at) + const targetDate = new Date(dateKey) + const targetDateStart = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate()) + const targetDateEnd = new Date(targetDate.getFullYear(), targetDate.getMonth(), targetDate.getDate() + 1) + + // Find conversation that falls on the selected date + const conversation = store.conversations.find((conv) => { + const convDate = new Date(conv.updated_at) + return convDate >= targetDateStart && convDate < targetDateEnd + }) + + if (conversation) { + selectConversation(conversation.id) + } else { + // No conversation for this date, create a new one + newConversation() + } + }).catch((err) => { + console.error('[Calendar] Error loading conversations:', err) + }) +} + // --- Message rendering utilities (kept inline for clarity) --- function formatUptime(seconds: number) { const days = Math.floor(seconds / 86400) @@ -269,9 +300,11 @@ function renderMarkdown(content: string) { v-for="cell in calendarCells" :key="cell.key" class="calendar-day" - :class="{ active: cell.active, busy: cell.busy, muted: cell.value === null }" + :class="{ active: cell.active, busy: cell.busy, muted: cell.value === null, selected: cell.selected, clickable: cell.hasConversation || cell.active }" + @click="(cell.hasConversation || cell.active) && handleCalendarDateSelect(cell.key)" > {{ cell.value ?? '' }} + @@ -355,24 +388,11 @@ function renderMarkdown(content: string) {
- -
+
- {{ selectedModel.model }} + {{ selectedModel?.model || selectedModelName || 'Default' }}
@@ -578,21 +598,6 @@ function renderMarkdown(content: string) {
SYSTEM
-
- DATE - {{ formatClientDate(clientTime) }} -
-
- TIME - {{ formatClientClock(clientTime) }} -
-
- WEATHER - - - {{ weatherSummary }} - -
OS {{ systemMeta.systemName }}