Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>突触流 Synapse Flow | 智能BCI数据平台</title> | |
| <!-- Vue 3 --> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- ECharts --> | |
| <script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script> | |
| <!-- Marked.js --> | |
| <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script> | |
| <!-- Font Awesome --> | |
| <link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> | |
| <style> | |
| [v-cloak] { display: none; } | |
| .chart-container { height: 300px; width: 100%; } | |
| /* Custom Scrollbar */ | |
| ::-webkit-scrollbar { width: 6px; height: 6px; } | |
| ::-webkit-scrollbar-track { background: #f1f1f1; } | |
| ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 3px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } | |
| </style> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| brand: { | |
| 50: '#f0f9ff', | |
| 100: '#e0f2fe', | |
| 500: '#0ea5e9', | |
| 600: '#0284c7', | |
| 700: '#0369a1', | |
| 900: '#0c4a6e', | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body class="bg-slate-50 text-slate-800 font-sans antialiased"> | |
| <div id="app" v-cloak class="flex h-screen overflow-hidden"> | |
| <!-- Mobile Sidebar Overlay --> | |
| <div v-if="mobileMenuOpen" class="fixed inset-0 bg-black/50 z-40 md:hidden" @click="mobileMenuOpen = false"></div> | |
| <!-- Sidebar --> | |
| <aside :class="{'translate-x-0': mobileMenuOpen, '-translate-x-full': !mobileMenuOpen}" class="fixed md:static inset-y-0 left-0 z-50 w-64 bg-white border-r border-slate-200 transition-transform duration-300 md:translate-x-0 flex flex-col"> | |
| <div class="p-6 border-b border-slate-100 flex items-center gap-3"> | |
| <div class="w-10 h-10 rounded-xl bg-brand-600 flex items-center justify-center text-white text-xl shadow-lg shadow-brand-500/30"> | |
| <i class="fa-solid fa-brain"></i> | |
| </div> | |
| <div> | |
| <h1 class="font-bold text-lg tracking-tight text-slate-900">突触流</h1> | |
| <p class="text-xs text-slate-500 font-medium">Synapse Flow Agent</p> | |
| </div> | |
| </div> | |
| <nav class="flex-1 p-4 space-y-1 overflow-y-auto"> | |
| <a href="#" @click.prevent="currentView = 'dashboard'" :class="{'bg-brand-50 text-brand-700': currentView === 'dashboard', 'text-slate-600 hover:bg-slate-50': currentView !== 'dashboard'}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors"> | |
| <i class="fa-solid fa-chart-line w-5"></i> 实时监控 | |
| </a> | |
| <a href="#" @click.prevent="currentView = 'analysis'" :class="{'bg-brand-50 text-brand-700': currentView === 'analysis', 'text-slate-600 hover:bg-slate-50': currentView !== 'analysis'}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors"> | |
| <i class="fa-solid fa-microchip w-5"></i> 智能分析 | |
| </a> | |
| <a href="#" @click.prevent="currentView = 'history'" :class="{'bg-brand-50 text-brand-700': currentView === 'history', 'text-slate-600 hover:bg-slate-50': currentView !== 'history'}" class="flex items-center gap-3 px-4 py-3 rounded-lg text-sm font-medium transition-colors"> | |
| <i class="fa-solid fa-clock-rotate-left w-5"></i> 历史档案 | |
| </a> | |
| </nav> | |
| <div class="p-4 border-t border-slate-100"> | |
| <div class="bg-slate-900 rounded-xl p-4 text-white"> | |
| <div class="flex items-center justify-between mb-2"> | |
| <span class="text-xs font-semibold text-slate-300">设备状态</span> | |
| <span class="inline-flex items-center gap-1.5 px-2 py-0.5 rounded-full text-[10px] font-medium bg-green-500/20 text-green-400 border border-green-500/20"> | |
| <span class="w-1.5 h-1.5 rounded-full bg-green-400 animate-pulse"></span> 在线 | |
| </span> | |
| </div> | |
| <div class="flex items-end gap-1 h-8 mt-2"> | |
| <div v-for="i in 10" :key="i" class="flex-1 bg-brand-500/40 rounded-sm transition-all duration-300" :style="{height: Math.random() * 100 + '%'}"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="flex-1 flex flex-col h-full overflow-hidden bg-slate-50/50"> | |
| <!-- Header --> | |
| <header class="h-16 bg-white border-b border-slate-200 flex items-center justify-between px-4 md:px-8"> | |
| <button @click="mobileMenuOpen = !mobileMenuOpen" class="md:hidden p-2 text-slate-600 hover:bg-slate-100 rounded-lg"> | |
| <i class="fa-solid fa-bars"></i> | |
| </button> | |
| <div class="flex items-center gap-4 ml-auto"> | |
| <button @click="toggleRecording" :class="isRecording ? 'bg-red-50 text-red-600 border-red-200' : 'bg-slate-50 text-slate-600 border-slate-200 hover:bg-slate-100'" class="px-4 py-2 rounded-lg text-sm font-medium border flex items-center gap-2 transition-all"> | |
| <i :class="isRecording ? 'fa-solid fa-stop animate-pulse' : 'fa-solid fa-play'"></i> | |
| ${ isRecording ? '停止记录' : '开始记录' } | |
| </button> | |
| <div class="w-8 h-8 rounded-full bg-slate-200 flex items-center justify-center text-slate-500 border border-white shadow-sm"> | |
| <i class="fa-solid fa-user"></i> | |
| </div> | |
| </div> | |
| </header> | |
| <!-- Scrollable Area --> | |
| <div class="flex-1 overflow-y-auto p-4 md:p-8"> | |
| <!-- Dashboard View --> | |
| <div v-if="currentView === 'dashboard'" class="space-y-6"> | |
| <div class="grid grid-cols-1 md:grid-cols-4 gap-4"> | |
| <div v-for="(val, key) in metrics" :key="key" class="bg-white p-4 rounded-xl border border-slate-200 shadow-sm"> | |
| <div class="text-xs text-slate-500 font-medium uppercase tracking-wider mb-1">${ key }</div> | |
| <div class="text-2xl font-bold text-slate-800 flex items-baseline gap-2"> | |
| ${ val.toFixed(1) } | |
| <span class="text-xs font-normal text-slate-400">μV</span> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm"> | |
| <div class="flex items-center justify-between mb-6"> | |
| <h3 class="font-bold text-slate-800">实时 EEG 波形监控</h3> | |
| <div class="flex gap-2 text-xs"> | |
| <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-blue-500"></span> Alpha</span> | |
| <span class="flex items-center gap-1"><span class="w-2 h-2 rounded-full bg-indigo-500"></span> Beta</span> | |
| </div> | |
| </div> | |
| <div ref="signalChart" class="chart-container"></div> | |
| </div> | |
| <div class="grid grid-cols-1 md:grid-cols-2 gap-6"> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm"> | |
| <h3 class="font-bold text-slate-800 mb-4">频带能量分布</h3> | |
| <div ref="spectrumChart" class="chart-container" style="height: 250px;"></div> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm flex flex-col"> | |
| <h3 class="font-bold text-slate-800 mb-4">会话控制</h3> | |
| <div class="space-y-4 flex-1"> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">受试者 ID</label> | |
| <input v-model="subjectId" type="text" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none text-sm"> | |
| </div> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">训练类型</label> | |
| <select v-model="sessionType" class="w-full px-3 py-2 border border-slate-300 rounded-lg focus:ring-2 focus:ring-brand-500 focus:border-brand-500 outline-none text-sm"> | |
| <option>Focus Training (专注训练)</option> | |
| <option>Relaxation (放松引导)</option> | |
| <option>Motor Imagery (运动想象)</option> | |
| </select> | |
| </div> | |
| <!-- File Upload Section --> | |
| <div> | |
| <label class="block text-sm font-medium text-slate-700 mb-1">数据源 (可选)</label> | |
| <div class="flex gap-2"> | |
| <button @click="triggerUpload" class="flex-1 px-3 py-2 border border-dashed border-slate-300 rounded-lg text-slate-500 hover:bg-slate-50 hover:text-brand-600 transition-colors text-sm flex items-center justify-center gap-2"> | |
| <i class="fa-solid fa-cloud-arrow-up"></i> | |
| ${ uploadStatus || '上传 EEG/CSV 文件' } | |
| </button> | |
| <input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept=".csv,.txt,.edf,.bdf,.json"> | |
| </div> | |
| </div> | |
| <button @click="analyzeSession" :disabled="isAnalyzing" class="w-full mt-auto bg-brand-600 hover:bg-brand-700 text-white py-2.5 rounded-lg font-medium transition-all shadow-lg shadow-brand-500/30 disabled:opacity-50 disabled:cursor-not-allowed flex justify-center items-center gap-2"> | |
| <i v-if="isAnalyzing" class="fa-solid fa-circle-notch animate-spin"></i> | |
| ${ isAnalyzing ? '正在分析数据...' : '生成分析报告' } | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Analysis View --> | |
| <div v-if="currentView === 'analysis'" class="space-y-6"> | |
| <div v-if="!lastAnalysis" class="text-center py-20 bg-white rounded-xl border border-slate-200 border-dashed"> | |
| <div class="w-16 h-16 bg-slate-50 rounded-full flex items-center justify-center mx-auto mb-4 text-slate-300 text-2xl"> | |
| <i class="fa-solid fa-file-contract"></i> | |
| </div> | |
| <h3 class="text-slate-900 font-medium">暂无分析报告</h3> | |
| <p class="text-slate-500 text-sm mt-1">请在“实时监控”页面生成一次会话分析。</p> | |
| <button @click="currentView = 'dashboard'" class="mt-4 text-brand-600 font-medium text-sm hover:underline">去生成 →</button> | |
| </div> | |
| <div v-else class="grid grid-cols-1 lg:grid-cols-3 gap-6"> | |
| <!-- Left: Report --> | |
| <div class="lg:col-span-2 space-y-6"> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm"> | |
| <div class="flex items-center gap-3 mb-6 pb-4 border-b border-slate-100"> | |
| <div class="p-2 bg-brand-50 text-brand-600 rounded-lg"> | |
| <i class="fa-solid fa-wand-magic-sparkles"></i> | |
| </div> | |
| <div> | |
| <h2 class="font-bold text-slate-900">AI 认知评估报告</h2> | |
| <p class="text-xs text-slate-500">Based on SiliconFlow Intelligence</p> | |
| </div> | |
| </div> | |
| <div class="prose prose-slate prose-sm max-w-none" v-html="parsedSummary"></div> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm"> | |
| <h3 class="font-bold text-slate-800 mb-4">改进建议</h3> | |
| <ul class="space-y-3"> | |
| <li v-for="(rec, idx) in lastAnalysis.recommendations" :key="idx" class="flex items-start gap-3 bg-slate-50 p-3 rounded-lg border border-slate-100"> | |
| <span class="flex-shrink-0 w-6 h-6 bg-brand-100 text-brand-600 rounded-full flex items-center justify-center text-xs font-bold">${ idx + 1 }</span> | |
| <span class="text-sm text-slate-700">${ rec }</span> | |
| </li> | |
| </ul> | |
| </div> | |
| </div> | |
| <!-- Right: Visuals --> | |
| <div class="space-y-6"> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm"> | |
| <h3 class="font-bold text-slate-800 mb-2">认知状态雷达</h3> | |
| <div ref="radarChart" class="chart-container" style="height: 300px;"></div> | |
| </div> | |
| <div class="bg-gradient-to-br from-slate-900 to-slate-800 p-6 rounded-xl text-white shadow-lg"> | |
| <div class="text-xs text-slate-400 uppercase font-medium mb-1">主要状态</div> | |
| <div class="text-2xl font-bold mb-4">${ lastAnalysis.cognitive_state }</div> | |
| <div class="h-1 bg-white/10 rounded-full overflow-hidden"> | |
| <div class="h-full bg-brand-400 w-3/4"></div> | |
| </div> | |
| <div class="mt-2 text-xs text-slate-400 flex justify-between"> | |
| <span>Confidence</span> | |
| <span>88%</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- History View --> | |
| <div v-if="currentView === 'history'" class="bg-white rounded-xl border border-slate-200 shadow-sm overflow-hidden"> | |
| <div class="p-6 border-b border-slate-100 flex justify-between items-center"> | |
| <h3 class="font-bold text-slate-800">历史会话档案</h3> | |
| <button @click="fetchHistory" class="text-sm text-brand-600 hover:text-brand-700 font-medium"> | |
| <i class="fa-solid fa-rotate-right mr-1"></i> 刷新 | |
| </button> | |
| </div> | |
| <div class="overflow-x-auto"> | |
| <table class="w-full text-sm text-left"> | |
| <thead class="bg-slate-50 text-slate-500 font-medium"> | |
| <tr> | |
| <th class="px-6 py-3">ID</th> | |
| <th class="px-6 py-3">时间</th> | |
| <th class="px-6 py-3">受试者</th> | |
| <th class="px-6 py-3">类型</th> | |
| <th class="px-6 py-3">状态评估</th> | |
| <th class="px-6 py-3">操作</th> | |
| </tr> | |
| </thead> | |
| <tbody class="divide-y divide-slate-100"> | |
| <tr v-for="item in historyList" :key="item.id" class="hover:bg-slate-50/50"> | |
| <td class="px-6 py-3 font-mono text-xs text-slate-400">#${ item.id }</td> | |
| <td class="px-6 py-3 text-slate-600">${ item.timestamp }</td> | |
| <td class="px-6 py-3 font-medium text-slate-900">${ item.subject_id }</td> | |
| <td class="px-6 py-3"> | |
| <span class="px-2 py-1 rounded-full bg-blue-50 text-blue-600 text-xs font-medium border border-blue-100">${ item.session_type }</span> | |
| </td> | |
| <td class="px-6 py-3 text-slate-600">${ item.analysis.cognitive_state }</td> | |
| <td class="px-6 py-3"> | |
| <button @click="loadAnalysis(item.analysis)" class="text-brand-600 hover:text-brand-700 font-medium text-xs">查看报告</button> | |
| </td> | |
| </tr> | |
| <tr v-if="historyList.length === 0"> | |
| <td colspan="6" class="px-6 py-8 text-center text-slate-400">暂无历史记录</td> | |
| </tr> | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| const { createApp, ref, onMounted, computed, watch, nextTick } = Vue; | |
| createApp({ | |
| delimiters: ['${', '}'], | |
| setup() { | |
| const currentView = ref('dashboard'); | |
| const mobileMenuOpen = ref(false); | |
| const isRecording = ref(false); | |
| const isAnalyzing = ref(false); | |
| const subjectId = ref('SUB-001'); | |
| const sessionType = ref('Focus Training (专注训练)'); | |
| const metrics = ref({ alpha: 0, beta: 0, theta: 0, delta: 0 }); | |
| const lastAnalysis = ref(null); | |
| const historyList = ref([]); | |
| const uploadStatus = ref(''); | |
| const fileInput = ref(null); | |
| // Chart Refs | |
| const signalChart = ref(null); | |
| const spectrumChart = ref(null); | |
| const radarChart = ref(null); | |
| let signalChartInst = null; | |
| let spectrumChartInst = null; | |
| let radarChartInst = null; | |
| let timer = null; | |
| // Data Buffers | |
| const xData = []; | |
| const yDataAlpha = []; | |
| const yDataBeta = []; | |
| const parsedSummary = computed(() => { | |
| if (!lastAnalysis.value || !lastAnalysis.value.summary) return ''; | |
| return marked.parse(lastAnalysis.value.summary); | |
| }); | |
| const triggerUpload = () => { | |
| fileInput.value.click(); | |
| }; | |
| const handleFileUpload = async (event) => { | |
| const file = event.target.files[0]; | |
| if (!file) return; | |
| uploadStatus.value = '上传中...'; | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const res = await fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| const data = await res.json(); | |
| if (res.ok) { | |
| uploadStatus.value = '上传成功'; | |
| // Use extracted metrics to update dashboard | |
| if (data.extracted_metrics) { | |
| metrics.value = data.extracted_metrics; | |
| // Also update spectrum chart | |
| if (spectrumChartInst) { | |
| spectrumChartInst.setOption({ | |
| series: [{ | |
| data: [ | |
| data.extracted_metrics.alpha, | |
| data.extracted_metrics.beta, | |
| data.extracted_metrics.theta, | |
| data.extracted_metrics.delta | |
| ] | |
| }] | |
| }); | |
| } | |
| } | |
| setTimeout(() => { uploadStatus.value = ''; }, 3000); | |
| } else { | |
| uploadStatus.value = '上传失败'; | |
| alert(data.error || '上传失败'); | |
| } | |
| } catch (e) { | |
| console.error(e); | |
| uploadStatus.value = '出错'; | |
| alert('上传过程中发生错误'); | |
| } | |
| // Reset input | |
| event.target.value = ''; | |
| }; | |
| const initCharts = () => { | |
| if (signalChart.value) { | |
| signalChartInst = echarts.init(signalChart.value); | |
| signalChartInst.setOption({ | |
| grid: { top: 20, right: 20, bottom: 30, left: 40 }, | |
| xAxis: { type: 'category', data: xData, boundaryGap: false }, | |
| yAxis: { type: 'value', min: 0, max: 40 }, | |
| series: [ | |
| { name: 'Alpha', type: 'line', smooth: true, data: yDataAlpha, showSymbol: false, lineStyle: { color: '#3b82f6', width: 2 } }, | |
| { name: 'Beta', type: 'line', smooth: true, data: yDataBeta, showSymbol: false, lineStyle: { color: '#6366f1', width: 2 } } | |
| ], | |
| animation: false | |
| }); | |
| } | |
| if (spectrumChart.value) { | |
| spectrumChartInst = echarts.init(spectrumChart.value); | |
| spectrumChartInst.setOption({ | |
| color: ['#3b82f6', '#6366f1', '#10b981', '#f59e0b'], | |
| tooltip: { trigger: 'axis' }, | |
| xAxis: { type: 'category', data: ['Alpha', 'Beta', 'Theta', 'Delta'] }, | |
| yAxis: { type: 'value' }, | |
| series: [{ | |
| data: [10, 20, 5, 2], | |
| type: 'bar', | |
| itemStyle: { borderRadius: [4, 4, 0, 0] } | |
| }] | |
| }); | |
| } | |
| }; | |
| const updateCharts = (data) => { | |
| // Update Metrics | |
| const latest = data[data.length - 1]; | |
| metrics.value = { | |
| alpha: latest.alpha, | |
| beta: latest.beta, | |
| theta: latest.theta, | |
| delta: latest.delta | |
| }; | |
| // Update Signal Chart | |
| data.forEach(p => { | |
| xData.push(new Date().toLocaleTimeString()); | |
| yDataAlpha.push(p.alpha); | |
| yDataBeta.push(p.beta); | |
| if (xData.length > 50) { | |
| xData.shift(); | |
| yDataAlpha.shift(); | |
| yDataBeta.shift(); | |
| } | |
| }); | |
| if (signalChartInst) { | |
| signalChartInst.setOption({ | |
| xAxis: { data: xData }, | |
| series: [ | |
| { data: yDataAlpha }, | |
| { data: yDataBeta } | |
| ] | |
| }); | |
| } | |
| // Update Spectrum | |
| if (spectrumChartInst) { | |
| spectrumChartInst.setOption({ | |
| series: [{ | |
| data: [latest.alpha, latest.beta, latest.theta, latest.delta] | |
| }] | |
| }); | |
| } | |
| }; | |
| const startMockData = () => { | |
| timer = setInterval(() => { | |
| if (!isRecording.value) return; | |
| fetch('/api/mock/signal') | |
| .then(res => res.json()) | |
| .then(res => { | |
| if(res.status === 'success') { | |
| updateCharts(res.data); | |
| } | |
| }); | |
| }, 1000); | |
| }; | |
| const toggleRecording = () => { | |
| isRecording.value = !isRecording.value; | |
| }; | |
| const analyzeSession = async () => { | |
| isAnalyzing.value = true; | |
| try { | |
| const res = await fetch('/api/analyze', { | |
| method: 'POST', | |
| headers: {'Content-Type': 'application/json'}, | |
| body: JSON.stringify({ | |
| subject_id: subjectId.value, | |
| session_type: sessionType.value | |
| }) | |
| }); | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| lastAnalysis.value = data.result; | |
| currentView.value = 'analysis'; | |
| // Wait for DOM update then init radar | |
| setTimeout(initRadarChart, 100); | |
| } | |
| } catch (e) { | |
| alert('分析失败,请重试'); | |
| } finally { | |
| isAnalyzing.value = false; | |
| } | |
| }; | |
| const initRadarChart = () => { | |
| if (radarChart.value && lastAnalysis.value) { | |
| radarChartInst = echarts.init(radarChart.value); | |
| const radarData = lastAnalysis.value.radar_chart; | |
| radarChartInst.setOption({ | |
| radar: { | |
| indicator: Object.keys(radarData).map(k => ({ name: k, max: 100 })), | |
| splitArea: { | |
| areaStyle: { | |
| color: ['#f8fafc', '#f1f5f9', '#e2e8f0', '#cbd5e1'].reverse() | |
| } | |
| } | |
| }, | |
| series: [{ | |
| type: 'radar', | |
| data: [{ | |
| value: Object.values(radarData), | |
| name: '当前状态', | |
| areaStyle: { color: 'rgba(14, 165, 233, 0.4)' }, | |
| lineStyle: { color: '#0ea5e9' } | |
| }] | |
| }] | |
| }); | |
| } | |
| }; | |
| const fetchHistory = async () => { | |
| const res = await fetch('/api/history'); | |
| const data = await res.json(); | |
| if (data.status === 'success') { | |
| historyList.value = data.history; | |
| } | |
| }; | |
| const loadAnalysis = (analysis) => { | |
| lastAnalysis.value = analysis; | |
| currentView.value = 'analysis'; | |
| setTimeout(initRadarChart, 100); | |
| }; | |
| onMounted(() => { | |
| initCharts(); | |
| startMockData(); | |
| fetchHistory(); | |
| // Responsive charts | |
| window.addEventListener('resize', () => { | |
| signalChartInst && signalChartInst.resize(); | |
| spectrumChartInst && spectrumChartInst.resize(); | |
| radarChartInst && radarChartInst.resize(); | |
| }); | |
| }); | |
| watch(currentView, (newVal) => { | |
| if (newVal === 'dashboard') { | |
| nextTick(() => { | |
| initCharts(); | |
| }); | |
| } else if (newVal === 'analysis') { | |
| nextTick(() => { | |
| initRadarChart(); | |
| }); | |
| } else if (newVal === 'history') { | |
| fetchHistory(); | |
| } | |
| }); | |
| return { | |
| currentView, mobileMenuOpen, isRecording, isAnalyzing, | |
| subjectId, sessionType, metrics, lastAnalysis, historyList, | |
| signalChart, spectrumChart, radarChart, | |
| parsedSummary, uploadStatus, fileInput, | |
| toggleRecording, analyzeSession, fetchHistory, loadAnalysis, | |
| triggerUpload, handleFileUpload | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |