2026-03-21 10:13:35 +08:00
|
|
|
|
<script setup lang="ts">
|
2026-04-08 00:13:06 +08:00
|
|
|
|
import { ref, toRef } from 'vue'
|
2026-03-29 20:31:13 +08:00
|
|
|
|
import {
|
2026-04-11 08:48:37 +08:00
|
|
|
|
Database,
|
2026-03-29 20:31:13 +08:00
|
|
|
|
ChevronRight,
|
|
|
|
|
|
Send,
|
|
|
|
|
|
Sparkles,
|
|
|
|
|
|
CornerDownLeft,
|
|
|
|
|
|
Paperclip,
|
|
|
|
|
|
Smile,
|
|
|
|
|
|
X,
|
|
|
|
|
|
} from 'lucide-vue-next'
|
2026-03-21 10:13:35 +08:00
|
|
|
|
import EmojiPicker from '@/components/chat/EmojiPicker.vue'
|
|
|
|
|
|
import FileMessage from '@/components/chat/FileMessage.vue'
|
2026-03-29 20:31:13 +08:00
|
|
|
|
import KnowledgeHudPanel from '@/components/chat/KnowledgeHudPanel.vue'
|
|
|
|
|
|
import KnowledgeSlidePanel from '@/components/chat/KnowledgeSlidePanel.vue'
|
|
|
|
|
|
import KnowledgeHUDPreview from '@/components/chat/KnowledgeHUDPreview.vue'
|
2026-04-11 08:48:37 +08:00
|
|
|
|
import KnowledgeRAGPanel from '@/components/chat/KnowledgeRAGPanel.vue'
|
2026-03-22 13:48:16 +08:00
|
|
|
|
import OrchestrationPanel from '@/components/chat/OrchestrationPanel.vue'
|
2026-04-06 23:48:52 +08:00
|
|
|
|
import KanbanPanel from '@/components/chat/KanbanPanel.vue'
|
|
|
|
|
|
import KanbanDetail from '@/components/chat/KanbanDetail.vue'
|
2026-03-29 20:31:13 +08:00
|
|
|
|
import TelemetrySparkline from '@/components/chat/TelemetrySparkline.vue'
|
|
|
|
|
|
import NavShortcutRow from '@/components/navigation/NavShortcutRow.vue'
|
2026-04-05 14:09:51 +08:00
|
|
|
|
import DailyDigestCard from '@/components/memory/DailyDigestCard.vue'
|
|
|
|
|
|
import ReminderToast from '@/components/memory/ReminderToast.vue'
|
2026-04-11 08:48:37 +08:00
|
|
|
|
import type { TaskQuadrant } from '@/api/task'
|
|
|
|
|
|
import { documentApi } from '@/api/document'
|
2026-03-21 22:13:12 +08:00
|
|
|
|
import { useChatView } from '@/pages/chat/composables/useChatView'
|
2026-03-29 20:31:13 +08:00
|
|
|
|
import { useKnowledgeView } from '@/pages/knowledge/composables/useKnowledgeView'
|
2026-04-05 20:45:16 +08:00
|
|
|
|
import { useDailyDigest } from '@/pages/chat/composables/useDailyDigest'
|
|
|
|
|
|
import { useClientTime, formatNetworkRate } from '@/pages/chat/composables/useClientTime'
|
2026-04-08 00:13:06 +08:00
|
|
|
|
import { useSidebarPlan, formatDateKey } from '@/pages/chat/composables/useSidebarPlan'
|
2026-04-11 08:48:37 +08:00
|
|
|
|
import TempleModal from '@/pages/temple/index.vue'
|
2026-03-21 22:13:12 +08:00
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
// --- Chat view (core messaging logic) ---
|
2026-03-21 22:13:12 +08:00
|
|
|
|
const {
|
|
|
|
|
|
store,
|
|
|
|
|
|
inputMessage,
|
|
|
|
|
|
isSending,
|
|
|
|
|
|
chatContainer,
|
|
|
|
|
|
inputRef,
|
|
|
|
|
|
isTyping,
|
|
|
|
|
|
fileInputRef,
|
|
|
|
|
|
showEmojiPicker,
|
2026-03-22 13:48:16 +08:00
|
|
|
|
selectedModelName,
|
|
|
|
|
|
selectedModel,
|
2026-04-11 08:48:37 +08:00
|
|
|
|
selectedRuntime,
|
2026-03-22 13:48:16 +08:00
|
|
|
|
orchestrationStatus,
|
|
|
|
|
|
orchestrationInsight,
|
|
|
|
|
|
activeAgent,
|
|
|
|
|
|
visitedAgents,
|
|
|
|
|
|
orchestrationEventFeed,
|
2026-03-29 20:31:13 +08:00
|
|
|
|
systemMeta,
|
2026-03-22 13:48:16 +08:00
|
|
|
|
systemTelemetry,
|
|
|
|
|
|
sessionTelemetry,
|
2026-03-21 22:13:12 +08:00
|
|
|
|
sendMessage,
|
2026-04-07 10:28:31 +08:00
|
|
|
|
selectConversation,
|
|
|
|
|
|
newConversation,
|
|
|
|
|
|
loadConversations,
|
2026-03-21 22:13:12 +08:00
|
|
|
|
formatTime,
|
|
|
|
|
|
autoResize,
|
|
|
|
|
|
handleFileSelect,
|
|
|
|
|
|
insertEmoji,
|
|
|
|
|
|
openFilePicker,
|
|
|
|
|
|
} = useChatView()
|
2026-03-22 13:48:16 +08:00
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
// --- Knowledge view ---
|
2026-03-29 20:31:13 +08:00
|
|
|
|
const {
|
|
|
|
|
|
showNewFolderDialog, newFolderName, createFolder, openNewFolderDialog,
|
2026-04-05 14:56:45 +08:00
|
|
|
|
triggerUpload, handleUpload, uploadInput
|
2026-03-29 20:31:13 +08:00
|
|
|
|
} = useKnowledgeView()
|
|
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
// --- Client time & weather ---
|
2026-04-11 08:48:37 +08:00
|
|
|
|
const { clientTime, city, weatherIcon, weatherSummary } = useClientTime()
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
// --- Daily digest & reminders ---
|
|
|
|
|
|
const { dailyDigest, digestLoading, activeReminder, reminderVisible, loadDailyDigest, handleSnooze, handleDismiss } = useDailyDigest()
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
// --- Sidebar plan (calendar, focus, review) ---
|
|
|
|
|
|
const {
|
2026-04-11 08:48:37 +08:00
|
|
|
|
calendarCells, issueStatusCounters,
|
2026-04-05 20:45:16 +08:00
|
|
|
|
sidebarWeekLabels, sidebarStatusHeadline, sidebarStatusBreakdown,
|
2026-04-11 08:48:37 +08:00
|
|
|
|
sidebarFocusItems, issueStatusQuadrants, issueCommanderSummary, sidebarReviewAchievements, sidebarReviewReflections,
|
2026-04-07 10:28:31 +08:00
|
|
|
|
sidebarFeedItems, topbarFeedItems, sidebarCollapsedModules,
|
2026-04-11 08:48:37 +08:00
|
|
|
|
selectCalendarDate, loadSidebarPlanSnapshot
|
2026-04-08 00:13:06 +08:00
|
|
|
|
} = useSidebarPlan(clientTime, loadDailyDigest, toRef(store, 'conversations'))
|
2026-04-05 20:45:16 +08:00
|
|
|
|
|
|
|
|
|
|
// --- Local UI state ---
|
|
|
|
|
|
const sidebarCollapsed = ref(false)
|
|
|
|
|
|
const orchestrationDrawerOpen = ref(false)
|
2026-04-06 23:48:52 +08:00
|
|
|
|
const kanbanDrawerOpen = ref(false)
|
|
|
|
|
|
const kanbanDetailOpen = ref(false)
|
2026-04-11 08:48:37 +08:00
|
|
|
|
const kanbanDetailState = ref<{ mode: 'create' | 'edit'; taskId?: string | null; quadrant?: TaskQuadrant | null } | null>(null)
|
2026-04-05 20:45:16 +08:00
|
|
|
|
const knowledgeHudOpen = ref(false)
|
2026-04-11 08:48:37 +08:00
|
|
|
|
const knowledgeRAGOpen = ref(false)
|
|
|
|
|
|
const ragPanelRef = ref<any>(null)
|
2026-04-05 20:45:16 +08:00
|
|
|
|
const selectedFolder = ref<any>(null)
|
|
|
|
|
|
const previewDoc = ref<any>(null)
|
2026-04-11 08:48:37 +08:00
|
|
|
|
const templeVisible = ref(false)
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
function openOrchestrationDrawer() { orchestrationDrawerOpen.value = true }
|
|
|
|
|
|
function closeOrchestrationDrawer() { orchestrationDrawerOpen.value = false }
|
2026-04-06 23:48:52 +08:00
|
|
|
|
function openKanbanDrawer() { kanbanDrawerOpen.value = true }
|
|
|
|
|
|
function closeKanbanDrawer() { kanbanDrawerOpen.value = false }
|
2026-04-11 08:48:37 +08:00
|
|
|
|
function openKanbanCreate(quadrantId: string) {
|
|
|
|
|
|
kanbanDetailState.value = { mode: 'create', quadrant: quadrantId as TaskQuadrant }
|
|
|
|
|
|
kanbanDetailOpen.value = true
|
|
|
|
|
|
}
|
|
|
|
|
|
function openKanbanTask(taskId: string) {
|
|
|
|
|
|
kanbanDetailState.value = { mode: 'edit', taskId }
|
2026-04-06 23:48:52 +08:00
|
|
|
|
kanbanDetailOpen.value = true
|
|
|
|
|
|
}
|
2026-04-11 08:48:37 +08:00
|
|
|
|
function closeKanbanDetail() {
|
|
|
|
|
|
kanbanDetailOpen.value = false
|
|
|
|
|
|
kanbanDetailState.value = null
|
|
|
|
|
|
refreshTodayStatus()
|
|
|
|
|
|
}
|
|
|
|
|
|
function refreshTodayStatus() {
|
|
|
|
|
|
void loadSidebarPlanSnapshot(clientTime.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
async function handleKanbanSaved() {
|
|
|
|
|
|
await loadSidebarPlanSnapshot(clientTime.value)
|
|
|
|
|
|
}
|
|
|
|
|
|
async function handleKanbanDeleted() {
|
|
|
|
|
|
await loadSidebarPlanSnapshot(clientTime.value)
|
|
|
|
|
|
closeKanbanDetail()
|
|
|
|
|
|
}
|
2026-03-29 20:31:13 +08:00
|
|
|
|
function openKnowledgeHud() {
|
|
|
|
|
|
selectedFolder.value = null
|
|
|
|
|
|
previewDoc.value = null
|
|
|
|
|
|
knowledgeHudOpen.value = true
|
|
|
|
|
|
}
|
2026-04-05 20:45:16 +08:00
|
|
|
|
function closeKnowledgeHud() { knowledgeHudOpen.value = false }
|
2026-04-11 08:48:37 +08:00
|
|
|
|
function openKnowledgeRAG() { knowledgeRAGOpen.value = true }
|
|
|
|
|
|
function closeKnowledgeRAG() {
|
|
|
|
|
|
knowledgeRAGOpen.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// RAG chat mode - send message to knowledge base instead of chat
|
|
|
|
|
|
async function sendRAGMessage() {
|
|
|
|
|
|
if (!inputMessage.value.trim() || isSending.value) return
|
|
|
|
|
|
|
|
|
|
|
|
const userQuery = inputMessage.value.trim()
|
|
|
|
|
|
inputMessage.value = ''
|
|
|
|
|
|
isSending.value = true
|
|
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
|
// Add user message to RAG panel
|
|
|
|
|
|
ragPanelRef.value?.addMessage({
|
|
|
|
|
|
id: Date.now().toString(),
|
|
|
|
|
|
role: 'user',
|
|
|
|
|
|
content: userQuery,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Call RAG API
|
|
|
|
|
|
const response = await documentApi.ragChat({
|
|
|
|
|
|
query: userQuery,
|
|
|
|
|
|
top_k: 5,
|
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
|
|
// Add assistant response to RAG panel
|
|
|
|
|
|
ragPanelRef.value?.addMessage({
|
|
|
|
|
|
id: (Date.now() + 1).toString(),
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
content: response.data.answer,
|
|
|
|
|
|
sources: response.data.sources || [],
|
|
|
|
|
|
})
|
|
|
|
|
|
} catch (error) {
|
|
|
|
|
|
// Fallback response
|
|
|
|
|
|
ragPanelRef.value?.addMessage({
|
|
|
|
|
|
id: (Date.now() + 1).toString(),
|
|
|
|
|
|
role: 'assistant',
|
|
|
|
|
|
content: '抱歉,搜索知识库时出现问题。请稍后重试。',
|
|
|
|
|
|
sources: [],
|
|
|
|
|
|
})
|
|
|
|
|
|
} finally {
|
|
|
|
|
|
isSending.value = false
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2026-04-05 20:45:16 +08:00
|
|
|
|
function handleSelectFolder(folder: any) { selectedFolder.value = folder }
|
|
|
|
|
|
function handleOpenPreview(doc: any) { previewDoc.value = doc }
|
2026-03-29 20:31:13 +08:00
|
|
|
|
|
2026-04-07 10:28:31 +08:00
|
|
|
|
function handleCalendarDateSelect(dateKey: string) {
|
|
|
|
|
|
selectCalendarDate(dateKey)
|
|
|
|
|
|
|
|
|
|
|
|
loadConversations().then(() => {
|
2026-04-08 00:13:06 +08:00
|
|
|
|
const conversations = store.conversations
|
|
|
|
|
|
const conversation = conversations.find((conv: { id: string; updated_at: string }) => formatDateKey(new Date(conv.updated_at)) === dateKey)
|
2026-04-07 10:28:31 +08:00
|
|
|
|
|
|
|
|
|
|
if (conversation) {
|
|
|
|
|
|
selectConversation(conversation.id)
|
2026-04-08 00:13:06 +08:00
|
|
|
|
return
|
2026-04-07 10:28:31 +08:00
|
|
|
|
}
|
2026-04-08 00:13:06 +08:00
|
|
|
|
|
|
|
|
|
|
newConversation()
|
2026-04-07 10:28:31 +08:00
|
|
|
|
}).catch((err) => {
|
|
|
|
|
|
console.error('[Calendar] Error loading conversations:', err)
|
|
|
|
|
|
})
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
// --- Message rendering utilities (kept inline for clarity) ---
|
2026-03-29 20:31:13 +08:00
|
|
|
|
function formatUptime(seconds: number) {
|
|
|
|
|
|
const days = Math.floor(seconds / 86400)
|
|
|
|
|
|
const hours = Math.floor((seconds % 86400) / 3600)
|
|
|
|
|
|
const minutes = Math.floor((seconds % 3600) / 60)
|
|
|
|
|
|
if (days > 0) return `${days}d ${hours}h ${minutes}m`
|
|
|
|
|
|
if (hours > 0) return `${hours}h ${minutes}m`
|
|
|
|
|
|
return `${minutes}m`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentIp() {
|
|
|
|
|
|
if (typeof window === 'undefined') return '--'
|
|
|
|
|
|
return window.location.hostname || '--'
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function currentPort() {
|
|
|
|
|
|
if (typeof window === 'undefined') return '--'
|
|
|
|
|
|
return window.location.port || (window.location.protocol === 'https:' ? '443' : '80')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatMemoryMb(value: number | null) {
|
|
|
|
|
|
if (value === null) return '--'
|
|
|
|
|
|
if (value >= 1024) return `${(value / 1024).toFixed(1)} GB`
|
|
|
|
|
|
return `${Math.round(value)} MB`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function formatDiskGb(value: number) {
|
|
|
|
|
|
if (value >= 1024) return `${(value / 1024).toFixed(1)} TB`
|
|
|
|
|
|
return `${value.toFixed(1)} GB`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-03-22 13:48:16 +08:00
|
|
|
|
function extractThinkParts(content: string) {
|
|
|
|
|
|
const thinkPattern = /<think>([\s\S]*?)<\/think>/gi
|
|
|
|
|
|
const thinkBlocks = Array.from(content.matchAll(thinkPattern))
|
|
|
|
|
|
.map((match) => match[1]?.trim())
|
|
|
|
|
|
.filter((block): block is string => Boolean(block))
|
|
|
|
|
|
const visibleContent = content.replace(thinkPattern, '').trim()
|
2026-04-05 20:45:16 +08:00
|
|
|
|
return { hasThink: thinkBlocks.length > 0, thinkBlocks, visibleContent }
|
2026-03-22 13:48:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function escapeHtml(content: string) {
|
|
|
|
|
|
return content
|
|
|
|
|
|
.replaceAll('&', '&')
|
|
|
|
|
|
.replaceAll('<', '<')
|
|
|
|
|
|
.replaceAll('>', '>')
|
|
|
|
|
|
.replaceAll('"', '"')
|
|
|
|
|
|
.replaceAll("'", ''')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderInlineMarkdown(content: string) {
|
|
|
|
|
|
return content
|
|
|
|
|
|
.replace(/`([^`]+)`/g, '<code>$1</code>')
|
|
|
|
|
|
.replace(/\*\*([^*]+)\*\*/g, '<strong>$1</strong>')
|
|
|
|
|
|
.replace(/\*([^*]+)\*/g, '<em>$1</em>')
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function isMarkdownTable(lines: string[]) {
|
|
|
|
|
|
return lines.length >= 2
|
|
|
|
|
|
&& lines[0].includes('|')
|
|
|
|
|
|
&& lines[1].includes('|')
|
|
|
|
|
|
&& lines[1].split('|').filter(Boolean).every((cell) => /^\s*:?-{3,}:?\s*$/.test(cell))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function splitTableRow(line: string) {
|
2026-04-05 20:45:16 +08:00
|
|
|
|
return line.trim().replace(/^\|/, '').replace(/\|$/, '').split('|').map((cell) => cell.trim())
|
2026-03-22 13:48:16 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function renderMarkdown(content: string) {
|
2026-04-05 20:45:16 +08:00
|
|
|
|
const normalizedContent = content.replace(/\\r\\n/g, '\n').replace(/\\n/g, '\n').replace(/\r\n/g, '\n')
|
2026-03-22 13:48:16 +08:00
|
|
|
|
const normalized = escapeHtml(normalizedContent)
|
|
|
|
|
|
const blocks = normalized.split(/\n\n+/)
|
|
|
|
|
|
|
|
|
|
|
|
return blocks.map((block) => {
|
|
|
|
|
|
const trimmed = block.trim()
|
|
|
|
|
|
if (!trimmed) return ''
|
|
|
|
|
|
|
|
|
|
|
|
if (/^```[\s\S]*```$/.test(trimmed)) {
|
|
|
|
|
|
const code = trimmed.replace(/^```\w*\n?/, '').replace(/```$/, '').trimEnd()
|
|
|
|
|
|
return `<pre class="md-pre"><code>${code}</code></pre>`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
const lines = trimmed.split('\n')
|
|
|
|
|
|
|
|
|
|
|
|
if (isMarkdownTable(lines)) {
|
|
|
|
|
|
const [headerLine, , ...bodyLines] = lines
|
|
|
|
|
|
const headers = splitTableRow(headerLine)
|
|
|
|
|
|
const thead = `<thead><tr>${headers.map((cell) => `<th>${renderInlineMarkdown(cell)}</th>`).join('')}</tr></thead>`
|
|
|
|
|
|
const tbody = bodyLines.length
|
|
|
|
|
|
? `<tbody>${bodyLines.map((line) => `<tr>${splitTableRow(line).map((cell) => `<td>${renderInlineMarkdown(cell)}</td>`).join('')}</tr>`).join('')}</tbody>`
|
|
|
|
|
|
: ''
|
|
|
|
|
|
return `<div class="md-table-wrap"><table class="md-table">${thead}${tbody}</table></div>`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (lines.every((line) => /^\s*[-*]\s+/.test(line))) {
|
2026-04-05 20:45:16 +08:00
|
|
|
|
const items = lines.map((line) => `<li>${renderInlineMarkdown(line.replace(/^\s*[-*]\s+/, '').trim())}</li>`).join('')
|
2026-03-22 13:48:16 +08:00
|
|
|
|
return `<ul class="md-list">${items}</ul>`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (lines.every((line) => /^\s*\d+\.\s+/.test(line))) {
|
2026-04-05 20:45:16 +08:00
|
|
|
|
const items = lines.map((line) => `<li>${renderInlineMarkdown(line.replace(/^\s*\d+\.\s+/, '').trim())}</li>`).join('')
|
2026-03-22 13:48:16 +08:00
|
|
|
|
return `<ol class="md-list">${items}</ol>`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (/^#{1,3}\s+/.test(trimmed)) {
|
|
|
|
|
|
const level = Math.min((trimmed.match(/^#+/)?.[0].length || 1) + 1, 6)
|
|
|
|
|
|
const text = trimmed.replace(/^#{1,3}\s+/, '')
|
|
|
|
|
|
return `<h${level} class="md-heading">${renderInlineMarkdown(text)}</h${level}>`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return `<p>${renderInlineMarkdown(trimmed).replace(/\n/g, '<br>')}</p>`
|
|
|
|
|
|
}).join('')
|
|
|
|
|
|
}
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<template>
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<div class="chat-view" :class="{ 'blur-when-modal': kanbanDetailOpen }">
|
2026-03-21 10:13:35 +08:00
|
|
|
|
<!-- Conversation list sidebar -->
|
2026-04-05 20:45:16 +08:00
|
|
|
|
<aside class="conv-sidebar jarvis-sidebar" :class="{ collapsed: sidebarCollapsed }">
|
|
|
|
|
|
<div v-if="sidebarCollapsed" class="jarvis-sidebar-icon-rail">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-for="module in sidebarCollapsedModules"
|
|
|
|
|
|
:key="module.id"
|
|
|
|
|
|
class="jarvis-sidebar-icon-btn"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
:title="module.label"
|
|
|
|
|
|
:aria-label="module.label"
|
2026-04-06 23:48:52 +08:00
|
|
|
|
@click="module.id === 'kanban' ? openKanbanDrawer() : (sidebarCollapsed = false)"
|
2026-04-05 20:45:16 +08:00
|
|
|
|
>
|
|
|
|
|
|
<component :is="module.icon" :size="18" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<div v-else class="jarvis-sidebar-scroll">
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<div class="section-label">// DAILY STATUS</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<div class="jarvis-panel jarvis-date-panel">
|
|
|
|
|
|
<div class="jarvis-date-row">
|
|
|
|
|
|
<div class="jarvis-date-meta">
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<div class="jarvis-month">{{ clientTime.toLocaleString('en-US', { month: 'long', year: 'numeric' }) }}</div>
|
2026-04-06 22:21:54 +08:00
|
|
|
|
<div class="jarvis-time">{{ clientTime.toLocaleTimeString('en-US', { hour12: true }) }}</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
</div>
|
2026-04-06 22:18:44 +08:00
|
|
|
|
<div class="jarvis-location">
|
|
|
|
|
|
<div class="location-info">
|
|
|
|
|
|
<span class="location-name">{{ city }}</span>
|
|
|
|
|
|
<span class="location-weather-text">{{ weatherSummary }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<i :class="['wi', weatherIcon, 'location-icon']"></i>
|
|
|
|
|
|
</div>
|
2026-04-05 14:09:51 +08:00
|
|
|
|
</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
|
|
|
|
|
|
<div class="jarvis-calendar">
|
|
|
|
|
|
<div class="calendar-header">
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<span v-for="item in sidebarWeekLabels" :key="item.label" :class="{ 'is-weekend': item.isWeekend }">{{ item.label }}</span>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="calendar-grid">
|
|
|
|
|
|
<span
|
|
|
|
|
|
v-for="cell in calendarCells"
|
|
|
|
|
|
:key="cell.key"
|
|
|
|
|
|
class="calendar-day"
|
2026-04-11 08:48:37 +08:00
|
|
|
|
:class="{ active: cell.active, muted: cell.value === null, selected: cell.selected, clickable: cell.active || cell.hasConversation }"
|
2026-04-07 11:18:07 +08:00
|
|
|
|
@click="(cell.active || cell.hasConversation) && handleCalendarDateSelect(cell.key)"
|
2026-04-05 14:56:45 +08:00
|
|
|
|
>
|
|
|
|
|
|
{{ cell.value ?? '' }}
|
2026-04-07 10:28:31 +08:00
|
|
|
|
<span v-if="cell.hasConversation" class="conv-indicator"></span>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
2026-04-05 14:09:51 +08:00
|
|
|
|
</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
|
2026-04-05 14:09:51 +08:00
|
|
|
|
</div>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<div class="jarvis-panel jarvis-plan-panel" @click="openKanbanDrawer" style="cursor: pointer;">
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<div class="jarvis-section-title">ISSUE STATUS</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<div class="jarvis-status-shell">
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<div class="jarvis-progress-ring" :style="{ '--completion': `${issueStatusCounters.completion}%` }">
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<div class="jarvis-progress-core">
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<strong>{{ issueStatusCounters.completion }}%</strong>
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<span>COMPLETION</span>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-05 14:09:51 +08:00
|
|
|
|
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<div class="jarvis-status-copy">
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<div v-if="sidebarStatusHeadline" class="jarvis-status-headline">
|
2026-04-05 14:56:45 +08:00
|
|
|
|
{{ sidebarStatusHeadline }}
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<ul class="jarvis-status-list">
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<li v-for="item in sidebarStatusBreakdown" :key="item.key" class="jarvis-status-item" :class="{ 'is-total': item.key === 'total' }">
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<span class="status-dot" :class="item.tone"></span>
|
|
|
|
|
|
<span class="status-label">{{ item.label }}</span>
|
|
|
|
|
|
<strong class="status-value">{{ item.value }}</strong>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-04-05 14:09:51 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
<div class="jarvis-panel jarvis-focus-panel">
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<div class="jarvis-section-title">TODAY'S FOCUS</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<ul v-if="sidebarFocusItems.length > 0" class="jarvis-focus-list">
|
|
|
|
|
|
<li v-for="(item, index) in sidebarFocusItems" :key="item.id" class="jarvis-focus-item" :class="`is-${item.tone}`">
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<span class="focus-order" :class="{ 'is-done': item.tone === 'done' }">
|
|
|
|
|
|
<span v-if="item.tone === 'done'" class="focus-check">✓</span>
|
|
|
|
|
|
<span v-else>{{ String(index + 1).padStart(2, '0') }}</span>
|
|
|
|
|
|
</span>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<div class="focus-copy">
|
|
|
|
|
|
<div class="focus-label">{{ item.label }}</div>
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<div class="focus-title" :class="{ 'is-done': item.tone === 'done' }">{{ item.title }}</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<div class="focus-meta">{{ item.meta }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
<div v-else class="jarvis-empty-state">暂无今日重点,等待日程中心返回数据。</div>
|
|
|
|
|
|
</div>
|
2026-03-22 13:48:16 +08:00
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
<div class="jarvis-panel jarvis-review-panel">
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<div class="jarvis-section-title">MONTHLY REVIEW</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<div class="jarvis-review-group">
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<div class="jarvis-review-subtitle">ACHIEVEMENTS</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<ul class="jarvis-review-list">
|
|
|
|
|
|
<li v-for="item in sidebarReviewAchievements" :key="item" class="jarvis-review-item">{{ item }}</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="jarvis-review-group">
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<div class="jarvis-review-subtitle">REFLECTIONS</div>
|
2026-04-05 14:56:45 +08:00
|
|
|
|
<ul class="jarvis-review-list reflection">
|
|
|
|
|
|
<li v-for="item in sidebarReviewReflections" :key="item" class="jarvis-review-item">{{ item }}</li>
|
|
|
|
|
|
</ul>
|
|
|
|
|
|
</div>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</div>
|
2026-03-22 13:48:16 +08:00
|
|
|
|
|
|
|
|
|
|
</div>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</aside>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Chat area -->
|
|
|
|
|
|
<section class="chat-area">
|
2026-03-22 13:48:16 +08:00
|
|
|
|
<div class="chat-shell">
|
|
|
|
|
|
<div class="chat-main">
|
|
|
|
|
|
<!-- Top bar -->
|
|
|
|
|
|
<div class="chat-topbar">
|
2026-03-29 20:31:13 +08:00
|
|
|
|
<div class="chat-shortcuts">
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<NavShortcutRow
|
|
|
|
|
|
@select-folder="handleSelectFolder"
|
|
|
|
|
|
@open-knowledge-hud="openKnowledgeHud"
|
2026-03-29 20:31:13 +08:00
|
|
|
|
/>
|
2026-03-22 13:48:16 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
<div class="chat-model-panel">
|
|
|
|
|
|
<label class="chat-model-label" for="chat-model-select">
|
|
|
|
|
|
<Sparkles :size="12" />
|
2026-04-07 10:28:31 +08:00
|
|
|
|
<span>MODEL</span>
|
2026-03-22 13:48:16 +08:00
|
|
|
|
</label>
|
2026-04-07 10:28:31 +08:00
|
|
|
|
<div class="chat-model-display">
|
2026-03-22 13:48:16 +08:00
|
|
|
|
<Sparkles :size="12" />
|
2026-04-07 10:28:31 +08:00
|
|
|
|
<span>{{ selectedModel?.model || selectedModelName || 'Default' }}</span>
|
2026-03-22 13:48:16 +08:00
|
|
|
|
</div>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</div>
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<div class="chat-model-panel">
|
|
|
|
|
|
<label class="chat-model-label" for="chat-runtime-select">
|
|
|
|
|
|
<Sparkles :size="12" />
|
|
|
|
|
|
<span>RUNTIME</span>
|
|
|
|
|
|
</label>
|
|
|
|
|
|
<select id="chat-runtime-select" v-model="selectedRuntime" class="chat-runtime-select">
|
|
|
|
|
|
<option value="jarvis">Jarvis</option>
|
|
|
|
|
|
<option value="hermes">Hermes</option>
|
|
|
|
|
|
</select>
|
|
|
|
|
|
</div>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
<div class="chat-intel-strip" v-if="sidebarFeedItems.length > 0">
|
|
|
|
|
|
<div class="chat-intel-label">RSS NEWS</div>
|
|
|
|
|
|
<div class="chat-intel-marquee">
|
|
|
|
|
|
<div class="chat-intel-track">
|
|
|
|
|
|
<span v-for="(item, index) in topbarFeedItems" :key="`${item.id}-${index}`" class="chat-intel-item">
|
|
|
|
|
|
<span class="chat-intel-item-meta">{{ item.meta }}</span>
|
|
|
|
|
|
<span class="chat-intel-item-title">{{ item.title }}</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-22 13:48:16 +08:00
|
|
|
|
<!-- Messages -->
|
|
|
|
|
|
<div ref="chatContainer" class="messages-area">
|
|
|
|
|
|
<!-- Welcome screen -->
|
|
|
|
|
|
<div v-if="store.messages.length === 0" class="welcome-screen">
|
|
|
|
|
|
<div class="welcome-icon">
|
|
|
|
|
|
<div class="welcome-ring r1"></div>
|
|
|
|
|
|
<div class="welcome-ring r2"></div>
|
|
|
|
|
|
<div class="welcome-ring r3"></div>
|
|
|
|
|
|
<div class="welcome-core">
|
|
|
|
|
|
<Sparkles :size="28" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="welcome-title">JARVIS</div>
|
2026-03-22 22:42:47 +08:00
|
|
|
|
<div class="welcome-sub">Strategic Thinking Partner</div>
|
|
|
|
|
|
<div class="welcome-hint">把目标给我,我先帮您收束重点,再往下推进。</div>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</div>
|
2026-03-22 13:48:16 +08:00
|
|
|
|
|
2026-04-05 14:09:51 +08:00
|
|
|
|
<!-- Daily Digest Card -->
|
|
|
|
|
|
<DailyDigestCard
|
|
|
|
|
|
v-if="dailyDigest"
|
|
|
|
|
|
:digest="dailyDigest"
|
|
|
|
|
|
:loading="digestLoading"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-03-22 13:48:16 +08:00
|
|
|
|
<!-- Message bubbles -->
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(msg, i) in store.messages"
|
|
|
|
|
|
:key="msg.id"
|
|
|
|
|
|
class="message-row"
|
|
|
|
|
|
:class="msg.role"
|
|
|
|
|
|
:style="{ animationDelay: `${i * 30}ms` }"
|
|
|
|
|
|
>
|
|
|
|
|
|
<div class="msg-avatar">
|
|
|
|
|
|
<span v-if="msg.role === 'user'">{{ '>' }}</span>
|
|
|
|
|
|
<span v-else class="ai-icon">J</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="msg-content">
|
|
|
|
|
|
<div class="msg-meta">
|
|
|
|
|
|
<span class="msg-role">{{ msg.role === 'user' ? 'YOU' : 'JARVIS' }}</span>
|
|
|
|
|
|
<span v-if="msg.role === 'assistant' && msg.model" class="msg-model">{{ msg.model }}</span>
|
|
|
|
|
|
<span class="msg-time">{{ formatTime(msg.created_at) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<template v-if="msg.role === 'assistant' && extractThinkParts(msg.content).hasThink">
|
|
|
|
|
|
<details class="think-panel">
|
|
|
|
|
|
<summary class="think-summary">
|
|
|
|
|
|
<span class="think-chip">
|
|
|
|
|
|
<span class="think-chip-dot"></span>
|
|
|
|
|
|
<span>THINK</span>
|
|
|
|
|
|
</span>
|
|
|
|
|
|
</summary>
|
|
|
|
|
|
<div class="think-content">
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-for="(block, index) in extractThinkParts(msg.content).thinkBlocks"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
class="think-block"
|
|
|
|
|
|
v-html="renderMarkdown(block)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</details>
|
|
|
|
|
|
<div
|
|
|
|
|
|
v-if="extractThinkParts(msg.content).visibleContent"
|
|
|
|
|
|
class="msg-bubble markdown-body"
|
|
|
|
|
|
v-html="renderMarkdown(extractThinkParts(msg.content).visibleContent)"
|
|
|
|
|
|
></div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<div v-else class="msg-bubble markdown-body" v-html="renderMarkdown(msg.content)"></div>
|
|
|
|
|
|
<div v-if="msg.role === 'user' && msg.attachments?.length" class="msg-attachments">
|
|
|
|
|
|
<FileMessage
|
|
|
|
|
|
v-for="att in msg.attachments"
|
|
|
|
|
|
:key="att.id"
|
|
|
|
|
|
:filename="att.name"
|
|
|
|
|
|
:file-type="att.type"
|
|
|
|
|
|
:file-size="att.size"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</div>
|
2026-03-22 13:48:16 +08:00
|
|
|
|
|
|
|
|
|
|
<div v-if="isTyping" class="message-row assistant thinking-row">
|
|
|
|
|
|
<div class="msg-avatar">
|
|
|
|
|
|
<span class="ai-icon">J</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="msg-content">
|
|
|
|
|
|
<div class="msg-meta">
|
|
|
|
|
|
<span class="msg-role">JARVIS</span>
|
|
|
|
|
|
<span class="msg-model">thinking</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="msg-bubble thinking-bubble">
|
|
|
|
|
|
<div class="thinking-hud">
|
|
|
|
|
|
<div class="thinking-core">
|
|
|
|
|
|
<span class="thinking-ring ring-1"></span>
|
|
|
|
|
|
<span class="thinking-ring ring-2"></span>
|
|
|
|
|
|
<span class="thinking-ring ring-3"></span>
|
|
|
|
|
|
<span class="thinking-dot"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="thinking-copy">
|
|
|
|
|
|
<div class="thinking-title">JARVIS THINKING</div>
|
|
|
|
|
|
<div class="thinking-subtitle">正在分析请求并准备响应</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="typing-inline" aria-hidden="true">
|
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
|
<span class="dot"></span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
<!-- Top buttons above input -->
|
|
|
|
|
|
<div class="top-buttons-row">
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<button class="top-action-btn" @click="templeVisible = true" title="Temple">
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<span class="btn-icon temple-icon">◈</span>
|
2026-04-05 20:45:16 +08:00
|
|
|
|
</button>
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<button class="top-action-btn" @click="openKnowledgeRAG()" title="Knowledge">
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<span class="btn-icon knowledge-icon">◉</span>
|
2026-04-05 20:45:16 +08:00
|
|
|
|
</button>
|
2026-04-06 21:33:45 +08:00
|
|
|
|
<button class="top-action-btn" @click="$router.push('/war-room')" title="War Room">
|
|
|
|
|
|
<span class="btn-icon war-icon">⬡</span>
|
2026-04-05 20:45:16 +08:00
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-22 13:48:16 +08:00
|
|
|
|
<!-- Input area -->
|
|
|
|
|
|
<div class="input-area">
|
|
|
|
|
|
<div class="input-frame">
|
|
|
|
|
|
<div class="input-corners tl"></div>
|
|
|
|
|
|
<div class="input-corners tr"></div>
|
|
|
|
|
|
<div class="input-corners bl"></div>
|
|
|
|
|
|
<div class="input-corners br"></div>
|
|
|
|
|
|
<textarea
|
|
|
|
|
|
ref="inputRef"
|
|
|
|
|
|
v-model="inputMessage"
|
2026-04-11 08:48:37 +08:00
|
|
|
|
:placeholder="knowledgeRAGOpen ? '输入问题搜索知识库...' : '输入指令,按 Enter 发送...'"
|
2026-03-22 13:48:16 +08:00
|
|
|
|
:disabled="isSending"
|
|
|
|
|
|
rows="1"
|
2026-04-11 08:48:37 +08:00
|
|
|
|
@keydown.enter.exact.prevent="knowledgeRAGOpen ? sendRAGMessage() : sendMessage()"
|
2026-03-22 13:48:16 +08:00
|
|
|
|
@input="autoResize"
|
|
|
|
|
|
></textarea>
|
|
|
|
|
|
<input
|
|
|
|
|
|
ref="fileInputRef"
|
|
|
|
|
|
type="file"
|
|
|
|
|
|
multiple
|
|
|
|
|
|
accept=".jpg,.jpeg,.png,.gif,.webp,.pdf,.doc,.docx,.xls,.xlsx,.ppt,.pptx,.txt"
|
|
|
|
|
|
style="display: none"
|
|
|
|
|
|
@change="handleFileSelect"
|
|
|
|
|
|
/>
|
|
|
|
|
|
<button class="attach-btn" @click="openFilePicker" title="上传文件">
|
|
|
|
|
|
<Paperclip :size="15" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<div class="emoji-wrapper">
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="emoji-btn"
|
|
|
|
|
|
:class="{ active: showEmojiPicker }"
|
|
|
|
|
|
@click="showEmojiPicker = !showEmojiPicker"
|
|
|
|
|
|
title="表情包"
|
|
|
|
|
|
>
|
|
|
|
|
|
<Smile :size="15" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
<EmojiPicker
|
|
|
|
|
|
:visible="showEmojiPicker"
|
|
|
|
|
|
@select="insertEmoji"
|
|
|
|
|
|
@close="showEmojiPicker = false"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button
|
|
|
|
|
|
class="send-btn"
|
|
|
|
|
|
:class="{ active: inputMessage.trim() }"
|
|
|
|
|
|
:disabled="!inputMessage.trim() || isSending"
|
2026-04-11 08:48:37 +08:00
|
|
|
|
@click="knowledgeRAGOpen ? sendRAGMessage() : sendMessage()"
|
2026-03-22 13:48:16 +08:00
|
|
|
|
>
|
|
|
|
|
|
<Send :size="15" />
|
|
|
|
|
|
<CornerDownLeft :size="12" class="enter-hint" />
|
|
|
|
|
|
</button>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</div>
|
2026-03-22 13:48:16 +08:00
|
|
|
|
<div class="input-hints">
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<template v-if="knowledgeRAGOpen">
|
|
|
|
|
|
<span class="hint-item rag-hint">知识库搜索模式</span>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
<template v-else>
|
|
|
|
|
|
<span class="hint-item">ENTER 发送</span>
|
|
|
|
|
|
<span class="hint-sep">|</span>
|
|
|
|
|
|
<span class="hint-item">SHIFT+ENTER 换行</span>
|
|
|
|
|
|
</template>
|
2026-03-21 10:13:35 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-03-29 20:31:13 +08:00
|
|
|
|
<aside class="runtime-sidebar">
|
|
|
|
|
|
<div class="sidebar-runtime-panel" :class="`is-${orchestrationStatus}`">
|
|
|
|
|
|
<div class="section-label">// RUNTIME STATUS</div>
|
|
|
|
|
|
<div class="runtime-meta-panel runtime-meta-panel-merged">
|
|
|
|
|
|
<div class="runtime-panel-title">SYSTEM</div>
|
|
|
|
|
|
<div class="runtime-meta-item">
|
|
|
|
|
|
<span class="runtime-meta-key">OS</span>
|
|
|
|
|
|
<span class="runtime-meta-value">{{ systemMeta.systemName }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-meta-item">
|
|
|
|
|
|
<span class="runtime-meta-key">VERSION</span>
|
|
|
|
|
|
<span class="runtime-meta-value">{{ systemMeta.systemVersion }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-meta-item">
|
|
|
|
|
|
<span class="runtime-meta-key">IP</span>
|
|
|
|
|
|
<span class="runtime-meta-value">{{ currentIp() }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-meta-item">
|
|
|
|
|
|
<span class="runtime-meta-key">PORT</span>
|
|
|
|
|
|
<span class="runtime-meta-value">{{ currentPort() }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-meta-item">
|
|
|
|
|
|
<span class="runtime-meta-key">UPTIME</span>
|
|
|
|
|
|
<span class="runtime-meta-value">{{ formatUptime(systemMeta.uptimeSeconds) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-grid">
|
|
|
|
|
|
<div class="runtime-card cpu-card">
|
|
|
|
|
|
<div class="runtime-topline">
|
|
|
|
|
|
<span class="runtime-label">CPU</span>
|
|
|
|
|
|
<span class="runtime-value">{{ systemTelemetry.cpu.online && systemTelemetry.cpu.current !== null ? `${systemTelemetry.cpu.current}%` : 'OFFLINE' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<TelemetrySparkline :points="systemTelemetry.cpu.series" stroke="#22d3ee" fill="rgba(34, 211, 238, 0.16)" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-card gpu-card">
|
|
|
|
|
|
<div class="runtime-topline">
|
|
|
|
|
|
<span class="runtime-label">GPU</span>
|
|
|
|
|
|
<span class="runtime-value">{{ systemMeta.gpuUtilPercent !== null ? `${Math.round(systemMeta.gpuUtilPercent)}%` : 'N/A' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="gpu-info-stats">
|
|
|
|
|
|
<div class="gpu-info-row">
|
|
|
|
|
|
<span class="gpu-info-key">DEVICE</span>
|
|
|
|
|
|
<span class="gpu-info-value gpu-info-copy">{{ systemMeta.gpuName || 'GPU unavailable' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="gpu-info-row">
|
|
|
|
|
|
<span class="gpu-info-key">VRAM</span>
|
|
|
|
|
|
<span class="gpu-info-value">{{ formatMemoryMb(systemMeta.gpuMemoryUsedMb) }} / {{ formatMemoryMb(systemMeta.gpuMemoryTotalMb) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<TelemetrySparkline
|
|
|
|
|
|
:points="systemTelemetry.gpu.series"
|
|
|
|
|
|
stroke="#f472b6"
|
|
|
|
|
|
fill="rgba(244, 114, 182, 0.16)"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-card mem-card">
|
|
|
|
|
|
<div class="runtime-topline">
|
|
|
|
|
|
<span class="runtime-label">MEM</span>
|
|
|
|
|
|
<span class="runtime-value">{{ systemTelemetry.memory.online && systemTelemetry.memory.current !== null ? `${systemTelemetry.memory.current}%` : 'OFFLINE' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<TelemetrySparkline :points="systemTelemetry.memory.series" stroke="#a78bfa" fill="rgba(167, 139, 250, 0.14)" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-card disk-card">
|
|
|
|
|
|
<div class="runtime-topline">
|
|
|
|
|
|
<span class="runtime-label">DISK</span>
|
|
|
|
|
|
<span class="runtime-value">{{ systemTelemetry.disk.online && systemTelemetry.disk.current !== null ? `${systemTelemetry.disk.current}%` : 'OFFLINE' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="gpu-info-stats">
|
|
|
|
|
|
<div class="gpu-info-row">
|
|
|
|
|
|
<span class="gpu-info-key">CAPACITY</span>
|
|
|
|
|
|
<span class="gpu-info-value">{{ `${formatDiskGb(systemMeta.diskUsedGb)} / ${formatDiskGb(systemMeta.diskTotalGb)}` }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<TelemetrySparkline :points="systemTelemetry.disk.series" stroke="#4ade80" fill="rgba(74, 222, 128, 0.14)" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-card network-card">
|
|
|
|
|
|
<div class="runtime-topline">
|
|
|
|
|
|
<span class="runtime-label">NETWORK</span>
|
|
|
|
|
|
<span class="runtime-value">{{ systemTelemetry.network.upload.online ? 'LIVE' : 'OFFLINE' }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="network-metric-head">
|
|
|
|
|
|
<div class="network-legend">
|
|
|
|
|
|
<span class="network-direction up">↑</span>
|
|
|
|
|
|
<span class="network-speed up">{{ formatNetworkRate(systemTelemetry.network.upload.current, systemTelemetry.network.upload.online) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="network-legend">
|
|
|
|
|
|
<span class="network-direction down">↓</span>
|
|
|
|
|
|
<span class="network-speed down">{{ formatNetworkRate(systemTelemetry.network.download.current, systemTelemetry.network.download.online) }}</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="network-overlay-chart">
|
|
|
|
|
|
<TelemetrySparkline :points="systemTelemetry.network.upload.series" stroke="#2563eb" fill="rgba(37, 99, 235, 0.10)" />
|
|
|
|
|
|
<TelemetrySparkline :points="systemTelemetry.network.download.series" stroke="#38bdf8" fill="rgba(56, 189, 248, 0.06)" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="runtime-sidebar-footer">
|
|
|
|
|
|
<button class="event-feed runtime-feed runtime-feed-launch" type="button" @click="openOrchestrationDrawer">
|
|
|
|
|
|
<div class="feed-hero">
|
|
|
|
|
|
<div class="feed-title">Recent Events</div>
|
|
|
|
|
|
<div class="feed-hero-meta">
|
|
|
|
|
|
<div class="runtime-log-count">{{ orchestrationEventFeed.length }}</div>
|
|
|
|
|
|
<ChevronRight :size="16" class="feed-launch-arrow" />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
<div class="knowledge-hud-shell" :class="{ open: knowledgeHudOpen }">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="knowledgeHudOpen"
|
|
|
|
|
|
class="knowledge-hud-backdrop"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="Close knowledge HUD"
|
|
|
|
|
|
@click="closeKnowledgeHud"
|
|
|
|
|
|
></button>
|
|
|
|
|
|
<section class="knowledge-hud" :class="{ open: knowledgeHudOpen }">
|
|
|
|
|
|
<div class="knowledge-hud-frame">
|
|
|
|
|
|
<div class="knowledge-hud-chrome">
|
|
|
|
|
|
<div class="knowledge-hud-title">
|
|
|
|
|
|
<Database :size="14" />
|
|
|
|
|
|
<span>KNOWLEDGE ARCHIVE HUD</span>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<button class="knowledge-hud-close" type="button" aria-label="Close knowledge HUD" @click="closeKnowledgeHud">
|
|
|
|
|
|
<X :size="16" />
|
|
|
|
|
|
</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="knowledge-hud-body">
|
|
|
|
|
|
<KnowledgeHudPanel />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<!-- Knowledge RAG Panel (Slide-up from bottom) -->
|
|
|
|
|
|
<KnowledgeRAGPanel
|
|
|
|
|
|
v-if="knowledgeRAGOpen"
|
|
|
|
|
|
ref="ragPanelRef"
|
|
|
|
|
|
:is-chat-loading="isSending"
|
|
|
|
|
|
@close="closeKnowledgeRAG"
|
|
|
|
|
|
@send="sendRAGMessage"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-03-29 20:31:13 +08:00
|
|
|
|
<div class="agent-drawer-shell" :class="{ open: orchestrationDrawerOpen }">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="orchestrationDrawerOpen"
|
|
|
|
|
|
class="agent-drawer-backdrop"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="Close agent drawer"
|
|
|
|
|
|
@click="closeOrchestrationDrawer"
|
|
|
|
|
|
></button>
|
|
|
|
|
|
<aside class="agent-drawer" :class="{ open: orchestrationDrawerOpen }">
|
|
|
|
|
|
<OrchestrationPanel
|
|
|
|
|
|
:visible="true"
|
|
|
|
|
|
:status="orchestrationStatus"
|
|
|
|
|
|
:insight="orchestrationInsight"
|
|
|
|
|
|
:active-agent="activeAgent"
|
|
|
|
|
|
:visited-agents="visitedAgents"
|
|
|
|
|
|
:events="orchestrationEventFeed"
|
|
|
|
|
|
:system-telemetry="{
|
|
|
|
|
|
cpu: systemTelemetry.cpu,
|
|
|
|
|
|
memory: systemTelemetry.memory,
|
|
|
|
|
|
disk: systemTelemetry.disk,
|
|
|
|
|
|
}"
|
|
|
|
|
|
:session-telemetry="sessionTelemetry"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
2026-04-06 23:48:52 +08:00
|
|
|
|
<!-- Kanban Drawer (四象限任务管理) -->
|
|
|
|
|
|
<div class="kanban-drawer-shell" :class="{ open: kanbanDrawerOpen }">
|
|
|
|
|
|
<button
|
|
|
|
|
|
v-if="kanbanDrawerOpen"
|
|
|
|
|
|
class="kanban-drawer-backdrop"
|
|
|
|
|
|
type="button"
|
|
|
|
|
|
aria-label="Close kanban"
|
|
|
|
|
|
@click="closeKanbanDrawer"
|
|
|
|
|
|
></button>
|
|
|
|
|
|
<aside class="kanban-drawer" :class="{ open: kanbanDrawerOpen }">
|
|
|
|
|
|
<KanbanPanel
|
|
|
|
|
|
:visible="kanbanDrawerOpen"
|
2026-04-11 08:48:37 +08:00
|
|
|
|
:quadrants="issueStatusQuadrants"
|
|
|
|
|
|
:commander-summary="issueCommanderSummary"
|
2026-04-06 23:48:52 +08:00
|
|
|
|
@close="closeKanbanDrawer"
|
2026-04-11 08:48:37 +08:00
|
|
|
|
@create-task="openKanbanCreate"
|
|
|
|
|
|
@open-task="openKanbanTask"
|
2026-04-06 23:48:52 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</aside>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Kanban Detail Modal (Teleported to body to avoid blur) -->
|
|
|
|
|
|
<Teleport to="body">
|
|
|
|
|
|
<KanbanDetail
|
2026-04-11 08:48:37 +08:00
|
|
|
|
v-if="kanbanDetailState"
|
2026-04-06 23:48:52 +08:00
|
|
|
|
:visible="kanbanDetailOpen"
|
2026-04-11 08:48:37 +08:00
|
|
|
|
:mode="kanbanDetailState.mode"
|
|
|
|
|
|
:task-id="kanbanDetailState.taskId"
|
|
|
|
|
|
:default-quadrant="kanbanDetailState.quadrant"
|
2026-04-06 23:48:52 +08:00
|
|
|
|
@close="closeKanbanDetail"
|
2026-04-11 08:48:37 +08:00
|
|
|
|
@saved="handleKanbanSaved"
|
|
|
|
|
|
@deleted="handleKanbanDeleted"
|
2026-04-06 23:48:52 +08:00
|
|
|
|
/>
|
|
|
|
|
|
</Teleport>
|
|
|
|
|
|
|
2026-03-29 20:31:13 +08:00
|
|
|
|
<!-- Knowledge Side Panel (Phase 02) -->
|
|
|
|
|
|
<Transition name="slide">
|
|
|
|
|
|
<KnowledgeSlidePanel
|
|
|
|
|
|
v-if="selectedFolder"
|
|
|
|
|
|
:folder="selectedFolder"
|
|
|
|
|
|
@close="selectedFolder = null"
|
|
|
|
|
|
@open-preview="handleOpenPreview"
|
|
|
|
|
|
@trigger-new-folder="openNewFolderDialog"
|
|
|
|
|
|
@trigger-upload="triggerUpload"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Knowledge HUD Preview (Phase 03) -->
|
|
|
|
|
|
<Transition name="fade">
|
|
|
|
|
|
<KnowledgeHUDPreview
|
|
|
|
|
|
v-if="previewDoc"
|
|
|
|
|
|
:doc="previewDoc"
|
|
|
|
|
|
@close="previewDoc = null"
|
|
|
|
|
|
/>
|
|
|
|
|
|
</Transition>
|
|
|
|
|
|
|
|
|
|
|
|
<!-- Global Dialogs for Knowledge -->
|
|
|
|
|
|
<input ref="uploadInput" type="file" class="hidden-upload" accept=".pdf,.md,.txt,.doc,.docx,.csv,.xlsx" @change="handleUpload" />
|
|
|
|
|
|
|
2026-04-05 14:09:51 +08:00
|
|
|
|
<!-- Reminder Toast -->
|
|
|
|
|
|
<ReminderToast
|
|
|
|
|
|
:reminder="activeReminder"
|
|
|
|
|
|
:visible="reminderVisible"
|
|
|
|
|
|
@snooze="handleSnooze"
|
|
|
|
|
|
@dismiss="handleDismiss"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-04-11 08:48:37 +08:00
|
|
|
|
<!-- Temple Modal (智慧神殿) -->
|
|
|
|
|
|
<TempleModal
|
|
|
|
|
|
:visible="templeVisible"
|
|
|
|
|
|
@close="templeVisible = false"
|
|
|
|
|
|
/>
|
|
|
|
|
|
|
2026-03-29 20:31:13 +08:00
|
|
|
|
<div v-if="showNewFolderDialog" class="knowledge-hud-preview" @click.self="showNewFolderDialog = false">
|
|
|
|
|
|
<div class="hud-dialog-jarvis">
|
|
|
|
|
|
<div class="dialog-header-tech">
|
|
|
|
|
|
<span class="sub-kicker">INIT_NODE_PROCEDURE</span>
|
|
|
|
|
|
<button class="close-jarvis-btn small" @click="showNewFolderDialog = false"><X :size="14" /></button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="dialog-body-tech">
|
|
|
|
|
|
<p class="dialog-label">ENTER_VIRTUAL_ADDRESS_NAME</p>
|
|
|
|
|
|
<input v-model="newFolderName" class="jarvis-input" placeholder="SECTOR_NAME..." @keyup.enter="createFolder" autofocus />
|
|
|
|
|
|
</div>
|
|
|
|
|
|
<div class="dialog-actions-tech">
|
|
|
|
|
|
<button class="action-btn-jarvis small" @click="showNewFolderDialog = false">ABORT</button>
|
|
|
|
|
|
<button class="action-btn-jarvis amber small" @click="createFolder">INITIALIZE</button>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</section>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
2026-04-05 20:45:16 +08:00
|
|
|
|
<style scoped src="./chatPage.css">
|
|
|
|
|
|
</style>
|