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:
2026-04-05 14:09:51 +08:00
parent 9bfa0dcc11
commit 11160ec4d2
22 changed files with 4117 additions and 186 deletions

View File

@@ -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>