feat(memory): complete M.2-M.5 memory upgrade phases with tests
- M.2: ForgettingCurve, MemoryDecay, MemoryReinforcement (selective forgetting) - M.3: DailyDigestGenerator, ReminderScheduler, ProactiveInformer (proactive reminders) - M.4: MemoryExtractor with LLM-based memory extraction from conversations - M.5: MemoryRecallInjector with token budget control for prompt injection - All phases include comprehensive unit tests (109 tests passing) - Updated checklist.md to mark all tasks complete
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { computed, onMounted, onUnmounted, ref } from 'vue'
|
||||
import { computed, onMounted, onUnmounted, ref, watch } from 'vue'
|
||||
import {
|
||||
ChevronRight,
|
||||
Cloud,
|
||||
@@ -27,8 +27,12 @@ import KnowledgeHUDPreview from '@/components/chat/KnowledgeHUDPreview.vue'
|
||||
import OrchestrationPanel from '@/components/chat/OrchestrationPanel.vue'
|
||||
import TelemetrySparkline from '@/components/chat/TelemetrySparkline.vue'
|
||||
import NavShortcutRow from '@/components/navigation/NavShortcutRow.vue'
|
||||
import DailyDigestCard from '@/components/memory/DailyDigestCard.vue'
|
||||
import ReminderToast from '@/components/memory/ReminderToast.vue'
|
||||
import { useChatView } from '@/pages/chat/composables/useChatView'
|
||||
import { useKnowledgeView } from '@/pages/knowledge/composables/useKnowledgeView'
|
||||
import { getRecentDigests, getDueReminders, snoozeReminder, dismissReminder } from '@/api/memory'
|
||||
import { scheduleCenterApi, type ScheduleCenterDateResponse, type ScheduleCenterDaySummary } from '@/api/scheduleCenter'
|
||||
|
||||
const {
|
||||
store,
|
||||
@@ -81,15 +85,125 @@ const selectedFolder = ref<any>(null)
|
||||
const previewDoc = ref<any>(null)
|
||||
let clientTimeTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
interface SidebarFocusItem {
|
||||
id: string
|
||||
label: string
|
||||
title: string
|
||||
meta: string
|
||||
tone: 'done' | 'doing' | 'pending'
|
||||
}
|
||||
|
||||
interface SidebarNewsItem {
|
||||
id: string
|
||||
title: string
|
||||
meta: string
|
||||
}
|
||||
|
||||
// Daily Digest state
|
||||
const dailyDigest = ref<any>(null)
|
||||
const digestLoading = ref(false)
|
||||
const recentDigests = ref<any[]>([])
|
||||
const todayPlanDetail = ref<ScheduleCenterDateResponse | null>(null)
|
||||
const monthPlanDays = ref<ScheduleCenterDaySummary[]>([])
|
||||
|
||||
// Active reminder state
|
||||
const activeReminder = ref<any>(null)
|
||||
const reminderVisible = ref(false)
|
||||
let reminderPollTimer: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
const {
|
||||
showNewFolderDialog, newFolderName, createFolder, openNewFolderDialog,
|
||||
triggerUpload, handleUpload, uploadInput, uploadError, uploadSuccess
|
||||
} = useKnowledgeView()
|
||||
|
||||
// Load daily digest
|
||||
async function loadDailyDigest() {
|
||||
digestLoading.value = true
|
||||
try {
|
||||
const today = new Date().toISOString().split('T')[0]
|
||||
const response = await getRecentDigests(6)
|
||||
const items = response.data?.items ?? []
|
||||
recentDigests.value = items
|
||||
dailyDigest.value = items.find((item: any) => item.date === today) ?? null
|
||||
} catch (err) {
|
||||
console.warn('Failed to load daily digest:', err)
|
||||
recentDigests.value = []
|
||||
dailyDigest.value = null
|
||||
} finally {
|
||||
digestLoading.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// Poll for due reminders
|
||||
async function pollDueReminders() {
|
||||
try {
|
||||
const response = await getDueReminders()
|
||||
if (response.data?.items?.length > 0) {
|
||||
activeReminder.value = response.data.items[0]
|
||||
reminderVisible.value = true
|
||||
} else {
|
||||
reminderVisible.value = false
|
||||
}
|
||||
} catch (err) {
|
||||
console.warn('Failed to poll due reminders:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleSnooze(id: string, minutes: number) {
|
||||
try {
|
||||
await snoozeReminder(id, minutes)
|
||||
reminderVisible.value = false
|
||||
// Reschedule next poll
|
||||
setTimeout(pollDueReminders, minutes * 60 * 1000)
|
||||
} catch (err) {
|
||||
console.warn('Failed to snooze reminder:', err)
|
||||
}
|
||||
}
|
||||
|
||||
async function handleDismiss(id: string) {
|
||||
try {
|
||||
await dismissReminder(id)
|
||||
reminderVisible.value = false
|
||||
} catch (err) {
|
||||
console.warn('Failed to dismiss reminder:', err)
|
||||
}
|
||||
}
|
||||
|
||||
function updateClientTime() {
|
||||
clientTime.value = new Date()
|
||||
}
|
||||
|
||||
function formatDateKey(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
return `${year}-${month}-${day}`
|
||||
}
|
||||
|
||||
function formatMonthKey(date: Date) {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
return `${year}-${month}`
|
||||
}
|
||||
|
||||
async function loadSidebarPlanSnapshot(date = new Date()) {
|
||||
const dateKey = formatDateKey(date)
|
||||
const monthKey = formatMonthKey(date)
|
||||
|
||||
try {
|
||||
const [todayResponse, monthResponse] = await Promise.all([
|
||||
scheduleCenterApi.date(dateKey),
|
||||
scheduleCenterApi.month(monthKey),
|
||||
])
|
||||
todayPlanDetail.value = todayResponse.data
|
||||
monthPlanDays.value = monthResponse.data.days
|
||||
} catch (err) {
|
||||
console.warn('Failed to load sidebar plan snapshot:', err)
|
||||
todayPlanDetail.value = null
|
||||
monthPlanDays.value = []
|
||||
}
|
||||
}
|
||||
|
||||
function openOrchestrationDrawer() {
|
||||
orchestrationDrawerOpen.value = true
|
||||
}
|
||||
@@ -163,6 +277,203 @@ const weatherIcon = computed(() => {
|
||||
return Cloud
|
||||
})
|
||||
|
||||
const todayDateKey = computed(() => formatDateKey(clientTime.value))
|
||||
const monthPlanSummaryMap = computed(() => new Map(monthPlanDays.value.map((item) => [item.date, item])))
|
||||
const calendarWeekLabels = ['一', '二', '三', '四', '五', '六', '日']
|
||||
|
||||
const calendarCells = computed(() => {
|
||||
const year = clientTime.value.getFullYear()
|
||||
const month = clientTime.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 }> = []
|
||||
|
||||
for (let index = 0; index < firstDayOffset; index += 1) {
|
||||
cells.push({ key: `blank-start-${index}`, value: null, active: false, busy: 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 === clientTime.value.getDate(),
|
||||
busy,
|
||||
})
|
||||
}
|
||||
|
||||
while (cells.length % 7 !== 0) {
|
||||
cells.push({ key: `blank-end-${cells.length}`, value: null, active: false, busy: false })
|
||||
}
|
||||
|
||||
return cells
|
||||
})
|
||||
|
||||
const todayPlanCounters = computed(() => {
|
||||
const detail = todayPlanDetail.value
|
||||
if (!detail) {
|
||||
return {
|
||||
done: 0,
|
||||
doing: 0,
|
||||
pending: 0,
|
||||
total: 0,
|
||||
completion: 0,
|
||||
}
|
||||
}
|
||||
|
||||
const todoDone = detail.todos.filter((item) => item.is_completed).length
|
||||
const todoPending = detail.todos.filter((item) => !item.is_completed).length
|
||||
const taskDone = detail.tasks.filter((item) => item.status === 'done').length
|
||||
const taskDoing = detail.tasks.filter((item) => item.status === 'in_progress').length
|
||||
const taskPending = detail.tasks.filter((item) => item.status === 'todo').length
|
||||
const goalDone = detail.goals.filter((item) => item.status === 'done').length
|
||||
const goalPending = detail.goals.filter((item) => item.status !== 'done').length
|
||||
const reminderDone = detail.reminders.filter((item) => item.status === 'done' || item.is_dismissed).length
|
||||
const reminderPending = detail.reminders.filter((item) => item.status !== 'done' && !item.is_dismissed).length
|
||||
|
||||
const done = todoDone + taskDone + goalDone + reminderDone
|
||||
const doing = taskDoing
|
||||
const pending = todoPending + taskPending + goalPending + reminderPending
|
||||
const total = done + doing + pending
|
||||
|
||||
return {
|
||||
done,
|
||||
doing,
|
||||
pending,
|
||||
total,
|
||||
completion: total > 0 ? Math.round((done / total) * 100) : 0,
|
||||
}
|
||||
})
|
||||
|
||||
const todayPlanBreakdown = computed(() => ([
|
||||
{ key: 'done', label: '已完成', value: todayPlanCounters.value.done, tone: 'done' },
|
||||
{ key: 'doing', label: '进行中', value: todayPlanCounters.value.doing, tone: 'doing' },
|
||||
{ key: 'pending', label: '未开始', value: todayPlanCounters.value.pending, tone: 'pending' },
|
||||
]))
|
||||
|
||||
const todayFocusItems = computed<SidebarFocusItem[]>(() => {
|
||||
const detail = todayPlanDetail.value
|
||||
if (!detail) return []
|
||||
|
||||
const goalItems = detail.goals
|
||||
.filter((goal) => goal.status !== 'done')
|
||||
.map((goal) => ({
|
||||
id: `goal-${goal.id}`,
|
||||
label: '目标',
|
||||
title: goal.title,
|
||||
meta: goal.note || '今日目标推进',
|
||||
tone: 'doing' as const,
|
||||
}))
|
||||
|
||||
const taskItems = detail.tasks
|
||||
.filter((task) => task.status !== 'done' && task.status !== 'cancelled')
|
||||
.sort((a, b) => {
|
||||
const priorityRank = { urgent: 0, high: 1, medium: 2, low: 3 }
|
||||
return priorityRank[a.priority] - priorityRank[b.priority]
|
||||
})
|
||||
.map((task) => ({
|
||||
id: `task-${task.id}`,
|
||||
label: task.priority === 'urgent' || task.priority === 'high' ? '高优任务' : '任务',
|
||||
title: task.title,
|
||||
meta: task.status === 'in_progress' ? '处理中' : '待启动',
|
||||
tone: task.status === 'in_progress' ? 'doing' as const : 'pending' as const,
|
||||
}))
|
||||
|
||||
const reminderItems = detail.reminders
|
||||
.filter((reminder) => reminder.status !== 'done' && !reminder.is_dismissed)
|
||||
.map((reminder) => ({
|
||||
id: `reminder-${reminder.id}`,
|
||||
label: '提醒',
|
||||
title: reminder.title,
|
||||
meta: reminder.reminder_at.slice(11, 16),
|
||||
tone: 'pending' as const,
|
||||
}))
|
||||
|
||||
const todoItems = detail.todos
|
||||
.filter((todo) => !todo.is_completed)
|
||||
.map((todo) => ({
|
||||
id: `todo-${todo.id}`,
|
||||
label: '待办',
|
||||
title: todo.title,
|
||||
meta: todo.source === 'manual' ? '手动记录' : '系统同步',
|
||||
tone: 'pending' as const,
|
||||
}))
|
||||
|
||||
return [...goalItems, ...taskItems, ...reminderItems, ...todoItems].slice(0, 5)
|
||||
})
|
||||
|
||||
const monthReviewStats = computed(() => monthPlanDays.value.reduce(
|
||||
(acc, item) => {
|
||||
acc.todoTotal += item.todo_total
|
||||
acc.todoCompleted += item.todo_completed
|
||||
acc.taskTotal += item.task_due_total
|
||||
acc.reminderTotal += item.reminder_total
|
||||
acc.goalTotal += item.goal_total
|
||||
acc.highPriorityTotal += item.high_priority_total
|
||||
if (item.todo_total + item.task_due_total + item.reminder_total + item.goal_total > 0) {
|
||||
acc.activeDays += 1
|
||||
}
|
||||
return acc
|
||||
},
|
||||
{
|
||||
todoTotal: 0,
|
||||
todoCompleted: 0,
|
||||
taskTotal: 0,
|
||||
reminderTotal: 0,
|
||||
goalTotal: 0,
|
||||
highPriorityTotal: 0,
|
||||
activeDays: 0,
|
||||
},
|
||||
))
|
||||
|
||||
const monthReviewAchievements = computed(() => {
|
||||
const stats = monthReviewStats.value
|
||||
const items = [
|
||||
stats.todoCompleted > 0 ? `累计完成 ${stats.todoCompleted} 项待办,执行节奏已形成闭环。` : '',
|
||||
stats.activeDays > 0 ? `本月已有 ${stats.activeDays} 天产生有效计划记录,日程连续性稳定。` : '',
|
||||
stats.highPriorityTotal > 0 ? `高优事项共 ${stats.highPriorityTotal} 项进入跟进,重点任务没有脱离视野。` : '',
|
||||
].filter(Boolean)
|
||||
|
||||
if (items.length > 0) return items.slice(0, 3)
|
||||
return ['本月计划数据还在积累中,可以从今日重点开始逐步建立复盘样本。']
|
||||
})
|
||||
|
||||
const monthReviewReflections = computed(() => {
|
||||
const stats = monthReviewStats.value
|
||||
const pendingTodoCount = Math.max(stats.todoTotal - stats.todoCompleted, 0)
|
||||
const items = [
|
||||
pendingTodoCount > 0 ? `仍有 ${pendingTodoCount} 项待办未完成,建议拆成更短的收尾窗口。` : '',
|
||||
stats.highPriorityTotal >= 8 ? '高优事项密度偏高,最好提前锁定 1 到 2 个绝对优先级。' : '',
|
||||
stats.reminderTotal >= Math.max(6, stats.activeDays) ? '提醒数量较多,说明执行中断点偏多,适合增加固定回顾时段。' : '',
|
||||
].filter(Boolean)
|
||||
|
||||
if (items.length > 0) return items.slice(0, 3)
|
||||
return ['本月节奏相对平稳,下一步可以把重点事项再收敛到更清晰的主线。']
|
||||
})
|
||||
|
||||
const sidebarNewsItems = computed<SidebarNewsItem[]>(() => {
|
||||
const digestFeed = recentDigests.value.flatMap((digest: any, digestIndex: number) => {
|
||||
const dateLabel = typeof digest.date === 'string' ? digest.date.slice(5) : '近期'
|
||||
const points = Array.isArray(digest.keyPoints) ? digest.keyPoints : []
|
||||
return points.slice(0, 2).map((point: any, pointIndex: number) => ({
|
||||
id: `digest-${digestIndex}-${pointIndex}`,
|
||||
title: typeof point?.content === 'string' ? point.content : String(point ?? ''),
|
||||
meta: point?.source || dateLabel,
|
||||
}))
|
||||
})
|
||||
|
||||
if (digestFeed.length > 0) return digestFeed.slice(0, 4)
|
||||
|
||||
return [
|
||||
{ id: 'fallback-1', title: 'AI 研发节奏继续升温,模型与工作流一体化成为主流议题。', meta: 'Industry' },
|
||||
{ id: 'fallback-2', title: '本地知识库与计划系统的联动体验,正在成为效率工具的新竞争点。', meta: 'Product' },
|
||||
{ id: 'fallback-3', title: '建议接入真实 RSS 源后替换当前占位卡片,以获得即时资讯流。', meta: 'System' },
|
||||
]
|
||||
})
|
||||
|
||||
async function loadWeather(latitude: number, longitude: number) {
|
||||
try {
|
||||
const response = await fetch(
|
||||
@@ -184,6 +495,16 @@ onMounted(() => {
|
||||
updateClientTime()
|
||||
clientTimeTimer = setInterval(updateClientTime, 1000)
|
||||
|
||||
// Load daily digest
|
||||
void loadDailyDigest()
|
||||
void loadSidebarPlanSnapshot(new Date())
|
||||
|
||||
// Start polling for due reminders (every 60 seconds)
|
||||
void pollDueReminders()
|
||||
reminderPollTimer = setInterval(() => {
|
||||
void pollDueReminders()
|
||||
}, 60000)
|
||||
|
||||
if (!navigator.geolocation) {
|
||||
weatherCode.value = null
|
||||
weatherSummary.value = 'Weather unavailable'
|
||||
@@ -204,6 +525,13 @@ onMounted(() => {
|
||||
|
||||
onUnmounted(() => {
|
||||
if (clientTimeTimer) clearInterval(clientTimeTimer)
|
||||
if (reminderPollTimer) clearInterval(reminderPollTimer)
|
||||
})
|
||||
|
||||
watch(todayDateKey, (next, previous) => {
|
||||
if (next === previous) return
|
||||
void loadDailyDigest()
|
||||
void loadSidebarPlanSnapshot(clientTime.value)
|
||||
})
|
||||
|
||||
function formatUptime(seconds: number) {
|
||||
@@ -343,51 +671,104 @@ function renderMarkdown(content: string) {
|
||||
<template>
|
||||
<div class="chat-view">
|
||||
<!-- Conversation list sidebar -->
|
||||
<aside class="conv-sidebar">
|
||||
<div class="conv-sidebar-header">
|
||||
<div class="section-label">// SESSIONS</div>
|
||||
</div>
|
||||
|
||||
<div class="conv-list">
|
||||
<div
|
||||
v-for="conv in store.conversations"
|
||||
:key="conv.id"
|
||||
class="conv-item"
|
||||
:class="{ active: conv.id === store.currentConversationId }"
|
||||
@click="selectConversation(conv.id)"
|
||||
>
|
||||
<div class="conv-item-icon">
|
||||
<MessageCircle :size="12" />
|
||||
<aside class="conv-sidebar jarvis-sidebar">
|
||||
<!-- Jarvis Date & Calendar -->
|
||||
<div class="jarvis-panel jarvis-date-panel">
|
||||
<div class="jarvis-date-row">
|
||||
<div class="jarvis-date-num">{{ clientTime.getDate().toString().padStart(2, '0') }}</div>
|
||||
<div class="jarvis-date-meta">
|
||||
<div class="jarvis-month">{{ clientTime.toLocaleString('en-US', { month: 'short' }).toUpperCase() }} / {{ clientTime.getFullYear() }}</div>
|
||||
<div class="jarvis-time">{{ clientTime.toLocaleTimeString('en-US', { hour12: false }) }}</div>
|
||||
</div>
|
||||
<div class="conv-item-body">
|
||||
<div class="conv-title">{{ conv.title || 'New Conversation' }}</div>
|
||||
<div class="conv-date">{{ formatConvDate(conv.updated_at || conv.created_at) }}</div>
|
||||
</div>
|
||||
<div class="jarvis-calendar">
|
||||
<div class="calendar-header">
|
||||
<span>M</span><span>T</span><span>W</span><span>T</span><span>F</span><span>S</span><span>S</span>
|
||||
</div>
|
||||
<div class="calendar-grid">
|
||||
<span v-for="d in 28" :key="d" class="calendar-day" :class="{ active: d === clientTime.getDate() }">{{ d }}</span>
|
||||
</div>
|
||||
<button class="conv-delete" @click="deleteConversation(conv.id, $event)">
|
||||
<Trash2 :size="12" />
|
||||
</button>
|
||||
<div class="conv-active-line"></div>
|
||||
</div>
|
||||
|
||||
<div v-if="conversationsError" class="conv-empty">
|
||||
<div class="empty-icon"><MessageCircle :size="24" /></div>
|
||||
<div class="empty-text">{{ conversationsError }}</div>
|
||||
<div class="empty-hint">请刷新页面或重新登录后重试</div>
|
||||
</div>
|
||||
|
||||
<div v-else-if="store.conversations.length === 0" class="conv-empty">
|
||||
<div class="empty-icon"><MessageCircle :size="24" /></div>
|
||||
<div class="empty-text">No sessions yet</div>
|
||||
<div class="empty-hint">Send a message to create one</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conv-sidebar-footer">
|
||||
<button class="new-chat-btn muted" @click="newConversation">
|
||||
<span class="btn-line"></span>
|
||||
<MessageCircle :size="14" />
|
||||
<span>NEW SESSION</span>
|
||||
<div class="jarvis-section-label">// COMMAND_CENTER</div>
|
||||
<div class="jarvis-commander-grid">
|
||||
<button class="commander-card intel" @click="newConversation">
|
||||
<div class="commander-glow"></div>
|
||||
<div class="commander-scan"></div>
|
||||
<div class="commander-icon-box">
|
||||
<Sparkles :size="18" />
|
||||
</div>
|
||||
<div class="commander-info">
|
||||
<div class="commander-title">智能指挥官</div>
|
||||
<div class="commander-status">SYSTEM_ACTIVE</div>
|
||||
</div>
|
||||
<div class="commander-corner top-r"></div>
|
||||
<div class="commander-corner bottom-l"></div>
|
||||
</button>
|
||||
|
||||
<button class="commander-card schedule" @click="selectConversation('schedule-mode')">
|
||||
<div class="commander-glow"></div>
|
||||
<div class="commander-scan"></div>
|
||||
<div class="commander-icon-box">
|
||||
<Database :size="18" />
|
||||
</div>
|
||||
<div class="commander-info">
|
||||
<div class="commander-title">日程指挥官</div>
|
||||
<div class="commander-status">SYNCING_TIME</div>
|
||||
</div>
|
||||
<div class="commander-corner top-r"></div>
|
||||
<div class="commander-corner bottom-l"></div>
|
||||
</button>
|
||||
|
||||
<button class="commander-card code" @click="selectConversation('code-mode')">
|
||||
<div class="commander-glow"></div>
|
||||
<div class="commander-scan"></div>
|
||||
<div class="commander-icon-box">
|
||||
<CornerDownLeft :size="18" />
|
||||
</div>
|
||||
<div class="commander-info">
|
||||
<div class="commander-title">代码指挥官</div>
|
||||
<div class="commander-status">KERNEL_READY</div>
|
||||
</div>
|
||||
<div class="commander-corner top-r"></div>
|
||||
<div class="commander-corner bottom-l"></div>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Project Status -->
|
||||
<div class="jarvis-panel jarvis-status-panel">
|
||||
<div class="jarvis-section-title">PROJECT_STATUS_REPORT</div>
|
||||
<div class="jarvis-progress-item">
|
||||
<div class="jarvis-progress-label"><span>TODAY_PLAN [1/1]</span><span>100%</span></div>
|
||||
<div class="jarvis-progress-bar"><div class="jarvis-progress-fill" style="width: 100%"></div></div>
|
||||
</div>
|
||||
<div class="jarvis-progress-item mt-3">
|
||||
<div class="jarvis-progress-label"><span>MONTHLY_PLAN [57/114]</span><span>50%</span></div>
|
||||
<div class="jarvis-progress-bar"><div class="jarvis-progress-fill" style="width: 50%"></div></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Key Objectives -->
|
||||
<div class="jarvis-panel jarvis-objectives-panel mb-2">
|
||||
<div class="jarvis-section-title">KEY_OBJECTIVES</div>
|
||||
<ul class="jarvis-plan-list">
|
||||
<li class="jarvis-plan-item"><span class="num">01</span> 洽谈8个大客户</li>
|
||||
<li class="jarvis-plan-item"><span class="num">02</span> 架构优化指导</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- RSS Feed -->
|
||||
<div class="jarvis-panel jarvis-rss-panel">
|
||||
<div class="jarvis-section-title">RSS_INTEL_FEED</div>
|
||||
<div class="jarvis-rss-list">
|
||||
<div class="rss-item">>> AI 产业报告:大模型算力需求增长 300%...</div>
|
||||
<div class="rss-item">>> GitHub 热榜:Jarvis 开源架构受到关注...</div>
|
||||
<div class="rss-item">>> 系统通知:神经引擎已完成 V5.1 固件升级...</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="conv-sidebar-footer" style="display: none;">
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
@@ -445,6 +826,13 @@ function renderMarkdown(content: string) {
|
||||
<div class="welcome-hint">把目标给我,我先帮您收束重点,再往下推进。</div>
|
||||
</div>
|
||||
|
||||
<!-- Daily Digest Card -->
|
||||
<DailyDigestCard
|
||||
v-if="dailyDigest"
|
||||
:digest="dailyDigest"
|
||||
:loading="digestLoading"
|
||||
/>
|
||||
|
||||
<!-- Message bubbles -->
|
||||
<div
|
||||
v-for="(msg, i) in store.messages"
|
||||
@@ -792,6 +1180,14 @@ function renderMarkdown(content: string) {
|
||||
<!-- Global Dialogs for Knowledge -->
|
||||
<input ref="uploadInput" type="file" class="hidden-upload" accept=".pdf,.md,.txt,.doc,.docx,.csv,.xlsx" @change="handleUpload" />
|
||||
|
||||
<!-- Reminder Toast -->
|
||||
<ReminderToast
|
||||
:reminder="activeReminder"
|
||||
:visible="reminderVisible"
|
||||
@snooze="handleSnooze"
|
||||
@dismiss="handleDismiss"
|
||||
/>
|
||||
|
||||
<div v-if="showNewFolderDialog" class="knowledge-hud-preview" @click.self="showNewFolderDialog = false">
|
||||
<div class="hud-dialog-jarvis">
|
||||
<div class="dialog-header-tech">
|
||||
@@ -2351,4 +2747,318 @@ function renderMarkdown(content: string) {
|
||||
gap: 8px;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
/* ── JARVIS SIDEBAR STYLES ── */
|
||||
.jarvis-sidebar {
|
||||
background: rgba(4, 10, 20, 0.95) !important;
|
||||
padding: 10px !important;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.jarvis-panel {
|
||||
background: rgba(0, 20, 40, 0.5);
|
||||
border: 1px solid rgba(0, 243, 255, 0.15);
|
||||
padding: 12px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
clip-path: polygon(10px 0, 100% 0, 100% calc(100% - 10px), calc(100% - 10px) 100%, 0 100%, 0 10px);
|
||||
}
|
||||
|
||||
.jarvis-panel::before {
|
||||
content: '';
|
||||
position: absolute;
|
||||
top: 0; left: 0; width: 6px; height: 6px;
|
||||
border-left: 1px solid #00f3ff; border-top: 1px solid #00f3ff;
|
||||
}
|
||||
|
||||
.jarvis-date-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.jarvis-date-num {
|
||||
font-family: var(--font-display);
|
||||
font-size: 32px;
|
||||
font-weight: 800;
|
||||
color: #00f3ff;
|
||||
text-shadow: 0 0 10px rgba(0, 243, 255, 0.5);
|
||||
}
|
||||
|
||||
.jarvis-month {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 10px;
|
||||
opacity: 0.6;
|
||||
letter-spacing: 0.1em;
|
||||
}
|
||||
|
||||
.jarvis-time {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 14px;
|
||||
color: #c8f7ff;
|
||||
}
|
||||
|
||||
.jarvis-calendar {
|
||||
border-top: 1px solid rgba(0, 243, 255, 0.1);
|
||||
padding-top: 8px;
|
||||
}
|
||||
|
||||
.calendar-header {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
opacity: 0.4;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.calendar-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.calendar-day {
|
||||
padding: 3px 0;
|
||||
border-radius: 2px;
|
||||
color: rgba(0, 243, 255, 0.4);
|
||||
}
|
||||
|
||||
.calendar-day.active {
|
||||
background: #00f3ff;
|
||||
color: #000;
|
||||
font-weight: 800;
|
||||
box-shadow: 0 0 10px #00f3ff;
|
||||
}
|
||||
|
||||
.jarvis-section-label {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
letter-spacing: 0.2em;
|
||||
color: rgba(0, 243, 255, 0.6);
|
||||
margin-bottom: 8px;
|
||||
padding-left: 4px;
|
||||
}
|
||||
|
||||
.jarvis-conv-list {
|
||||
padding: 0 !important;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.jarvis-session-item {
|
||||
background: rgba(0, 243, 255, 0.03) !important;
|
||||
border-left: 2px solid transparent !important;
|
||||
border-bottom: 1px solid rgba(0, 243, 255, 0.05) !important;
|
||||
margin-bottom: 4px !important;
|
||||
border-radius: 0 !important;
|
||||
transition: all 0.3s;
|
||||
}
|
||||
|
||||
.jarvis-session-item:hover {
|
||||
background: rgba(0, 243, 255, 0.08) !important;
|
||||
padding-left: 14px !important;
|
||||
}
|
||||
|
||||
.jarvis-session-item.active {
|
||||
background: rgba(0, 243, 255, 0.12) !important;
|
||||
border-left-color: #00f3ff !important;
|
||||
box-shadow: inset 4px 0 15px rgba(0, 243, 255, 0.05);
|
||||
}
|
||||
|
||||
.jarvis-text-glow {
|
||||
text-shadow: 0 0 8px rgba(0, 243, 255, 0.4);
|
||||
}
|
||||
|
||||
.jarvis-section-title {
|
||||
font-family: var(--font-display);
|
||||
font-size: 10px;
|
||||
letter-spacing: 0.15em;
|
||||
color: #00f3ff;
|
||||
margin-bottom: 10px;
|
||||
border-bottom: 1px solid rgba(0, 243, 255, 0.1);
|
||||
padding-bottom: 4px;
|
||||
}
|
||||
|
||||
.jarvis-progress-item {
|
||||
margin-bottom: 6px;
|
||||
}
|
||||
|
||||
.jarvis-progress-label {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
font-size: 9px;
|
||||
font-family: var(--font-mono);
|
||||
opacity: 0.7;
|
||||
margin-bottom: 4px;
|
||||
}
|
||||
|
||||
.jarvis-progress-bar {
|
||||
height: 3px;
|
||||
background: rgba(0, 243, 255, 0.1);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.jarvis-progress-fill {
|
||||
height: 100%;
|
||||
background: #00f3ff;
|
||||
box-shadow: 0 0 10px #00f3ff;
|
||||
}
|
||||
|
||||
.jarvis-plan-list {
|
||||
list-style: none;
|
||||
font-size: 10px;
|
||||
}
|
||||
|
||||
.jarvis-plan-item {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
padding: 4px 0;
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.jarvis-plan-item .num {
|
||||
color: #00f3ff;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.jarvis-new-btn {
|
||||
background: rgba(0, 243, 255, 0.1) !important;
|
||||
border: 1px solid rgba(0, 243, 255, 0.3) !important;
|
||||
color: #00f3ff !important;
|
||||
font-family: var(--font-display);
|
||||
height: 36px;
|
||||
box-shadow: 0 0 15px rgba(0, 243, 255, 0.05);
|
||||
}
|
||||
|
||||
.jarvis-new-btn:hover {
|
||||
background: rgba(0, 243, 255, 0.2) !important;
|
||||
box-shadow: 0 0 20px rgba(0, 243, 255, 0.15);
|
||||
}
|
||||
|
||||
.conv-sidebar-footer {
|
||||
border-top: 1px solid rgba(0, 243, 255, 0.1) !important;
|
||||
}
|
||||
|
||||
/* ── COMMANDER CARDS ── */
|
||||
.jarvis-commander-grid {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 10px;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
|
||||
.commander-card {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 14px;
|
||||
padding: 12px 16px;
|
||||
background: rgba(0, 243, 255, 0.02);
|
||||
border: 1px solid rgba(0, 243, 255, 0.1);
|
||||
color: #00f3ff;
|
||||
text-align: left;
|
||||
cursor: pointer;
|
||||
transition: all 0.4s cubic-bezier(0.16, 1, 0.3, 1);
|
||||
overflow: hidden;
|
||||
border-radius: 2px;
|
||||
}
|
||||
|
||||
.commander-card:hover {
|
||||
background: rgba(0, 243, 255, 0.08);
|
||||
border-color: rgba(0, 243, 255, 0.4);
|
||||
box-shadow: 0 0 20px rgba(0, 243, 255, 0.1);
|
||||
transform: translateY(-2px);
|
||||
}
|
||||
|
||||
/* Internal Glow */
|
||||
.commander-glow {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
background: radial-gradient(circle at center, rgba(0, 243, 255, 0.15), transparent 70%);
|
||||
opacity: 0;
|
||||
transition: opacity 0.4s;
|
||||
}
|
||||
.commander-card:hover .commander-glow { opacity: 1; }
|
||||
|
||||
/* Scanning Line */
|
||||
.commander-scan {
|
||||
position: absolute;
|
||||
top: 0; left: -100%; width: 100%; height: 100%;
|
||||
background: linear-gradient(90deg, transparent, rgba(0, 243, 255, 0.1), transparent);
|
||||
transition: none;
|
||||
}
|
||||
.commander-card:hover .commander-scan {
|
||||
animation: commander-scan-move 1.5s infinite;
|
||||
}
|
||||
|
||||
@keyframes commander-scan-move {
|
||||
0% { left: -100%; }
|
||||
100% { left: 100%; }
|
||||
}
|
||||
|
||||
/* Icon Box */
|
||||
.commander-icon-box {
|
||||
width: 38px; height: 38px;
|
||||
display: flex; align-items: center; justify-content: center;
|
||||
background: rgba(0, 243, 255, 0.05);
|
||||
border: 1px solid rgba(0, 243, 255, 0.2);
|
||||
position: relative;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.commander-info { position: relative; z-index: 1; flex: 1; }
|
||||
.commander-title { font-size: 14px; font-weight: 700; letter-spacing: 0.02em; margin-bottom: 2px; }
|
||||
.commander-status { font-family: var(--font-mono); font-size: 8px; opacity: 0.5; letter-spacing: 0.15em; }
|
||||
|
||||
/* Corner Accents */
|
||||
.commander-corner {
|
||||
position: absolute; width: 4px; height: 4px;
|
||||
border: 1px solid #00f3ff;
|
||||
}
|
||||
.commander-corner.top-r { top: 0; right: 0; border-left: none; border-bottom: none; }
|
||||
.commander-corner.bottom-l { bottom: 0; left: 0; border-right: none; border-top: none; }
|
||||
|
||||
/* Theme Colors */
|
||||
.commander-card.schedule { color: #fbbf24; border-color: rgba(245, 158, 11, 0.1); }
|
||||
.commander-card.schedule:hover { border-color: rgba(245, 158, 11, 0.4); background: rgba(245, 158, 11, 0.08); }
|
||||
.commander-card.schedule .commander-icon-box { background: rgba(245, 158, 11, 0.05); border-color: rgba(245, 158, 11, 0.2); }
|
||||
.commander-card.schedule .commander-corner { border-color: #fbbf24; }
|
||||
.commander-card.schedule .commander-glow { background: radial-gradient(circle at center, rgba(245, 158, 11, 0.15), transparent 70%); }
|
||||
|
||||
.commander-card.code { color: #a78bfa; border-color: rgba(167, 139, 250, 0.1); }
|
||||
.commander-card.code:hover { border-color: rgba(167, 139, 250, 0.4); background: rgba(167, 139, 250, 0.08); }
|
||||
.commander-card.code .commander-icon-box { background: rgba(167, 139, 250, 0.05); border-color: rgba(167, 139, 250, 0.2); }
|
||||
.commander-card.code .commander-corner { border-color: #a78bfa; }
|
||||
.commander-card.code .commander-glow { background: radial-gradient(circle at center, rgba(167, 139, 250, 0.15), transparent 70%); }
|
||||
|
||||
/* ── RSS FEED ── */
|
||||
.jarvis-rss-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
}
|
||||
|
||||
.rss-item {
|
||||
font-family: var(--font-mono);
|
||||
font-size: 9px;
|
||||
line-height: 1.4;
|
||||
color: rgba(200, 247, 255, 0.7);
|
||||
padding: 4px;
|
||||
border-left: 1px solid rgba(0, 243, 255, 0.2);
|
||||
transition: color 0.2s;
|
||||
}
|
||||
|
||||
.rss-item:hover {
|
||||
color: #00f3ff;
|
||||
background: rgba(0, 243, 255, 0.05);
|
||||
}
|
||||
|
||||
.limited-height {
|
||||
max-height: 120px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user