Spaces:
Sleeping
Sleeping
| <html lang="zh-CN"> | |
| <head> | |
| <meta charset="UTF-8"> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| <title>电商运营助手 AI - 专业版</title> | |
| <!-- Vue 3 --> | |
| <script src="https://unpkg.com/vue@3/dist/vue.global.js"></script> | |
| <!-- Tailwind CSS --> | |
| <script src="https://cdn.tailwindcss.com"></script> | |
| <!-- Markdown-it --> | |
| <script src="https://unpkg.com/markdown-it/dist/markdown-it.min.js"></script> | |
| <!-- Chart.js --> | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> | |
| <style> | |
| [v-cloak] { display: none ; } | |
| /* Markdown 样式覆盖 */ | |
| .prose h3 { color: #1e3a8a; font-weight: 700; margin-top: 1.5em; margin-bottom: 0.5em; } | |
| .prose h4 { color: #334155; font-weight: 600; margin-top: 1.2em; } | |
| .prose ul { list-style-type: disc; padding-left: 1.5em; margin: 1em 0; } | |
| .prose blockquote { border-left: 4px solid #3b82f6; padding-left: 1em; color: #64748b; font-style: italic; background: #f8fafc; padding: 0.5em 1em; border-radius: 0 4px 4px 0; } | |
| .prose table { width: 100%; border-collapse: collapse; margin: 1em 0; font-size: 0.9em; } | |
| .prose th, .prose td { border: 1px solid #e2e8f0; padding: 8px 12px; } | |
| .prose th { background: #f1f5f9; text-align: left; } | |
| /* 滚动条美化 */ | |
| ::-webkit-scrollbar { width: 8px; height: 8px; } | |
| ::-webkit-scrollbar-track { background: #f1f5f9; } | |
| ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 4px; } | |
| ::-webkit-scrollbar-thumb:hover { background: #94a3b8; } | |
| .chart-container { | |
| background: white; | |
| border-radius: 8px; | |
| padding: 16px; | |
| box-shadow: 0 1px 3px rgba(0,0,0,0.1); | |
| margin: 16px 0; | |
| border: 1px solid #e2e8f0; | |
| } | |
| </style> | |
| <script> | |
| tailwind.config = { | |
| theme: { | |
| extend: { | |
| colors: { | |
| brand: { | |
| 50: '#eff6ff', | |
| 100: '#dbeafe', | |
| 500: '#3b82f6', | |
| 600: '#2563eb', | |
| 900: '#1e3a8a', | |
| } | |
| } | |
| } | |
| } | |
| } | |
| </script> | |
| </head> | |
| <body class="bg-slate-50 text-slate-800 h-screen flex overflow-hidden"> | |
| <div id="app" v-cloak class="flex w-full h-full"> | |
| <!-- Sidebar --> | |
| <aside class="w-64 bg-white border-r border-slate-200 flex flex-col z-10 hidden md:flex"> | |
| <div class="h-16 flex items-center px-6 border-b border-slate-100"> | |
| <div class="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center mr-3 shadow-sm"> | |
| <span class="text-white font-bold text-lg">E</span> | |
| </div> | |
| <h1 class="font-bold text-slate-800 text-lg tracking-tight">运营助手 AI</h1> | |
| </div> | |
| <nav class="flex-1 p-4 space-y-1 overflow-y-auto"> | |
| <div class="text-xs font-semibold text-slate-400 uppercase tracking-wider mb-2 px-2">功能模块</div> | |
| <a href="#" @click.prevent="resetChat" | |
| class="flex items-center px-3 py-2.5 text-sm font-medium rounded-lg text-slate-600 hover:bg-red-50 hover:text-red-600 transition-colors mb-2"> | |
| <span class="mr-3 text-lg">🆕</span> 新对话 | |
| </a> | |
| <a href="#" @click.prevent="currentTab = 'chat'" | |
| :class="['flex items-center px-3 py-2.5 text-sm font-medium rounded-lg transition-colors', currentTab === 'chat' ? 'bg-brand-50 text-brand-600' : 'text-slate-600 hover:bg-slate-50']"> | |
| <span class="mr-3 text-lg">💬</span> 智能诊断 | |
| </a> | |
| <a href="#" @click.prevent="currentTab = 'upload'" | |
| :class="['flex items-center px-3 py-2.5 text-sm font-medium rounded-lg transition-colors', currentTab === 'upload' ? 'bg-brand-50 text-brand-600' : 'text-slate-600 hover:bg-slate-50']"> | |
| <span class="mr-3 text-lg">📂</span> 数据清洗 | |
| </a> | |
| <a href="#" @click.prevent="currentTab = 'dashboard'" | |
| :class="['flex items-center px-3 py-2.5 text-sm font-medium rounded-lg transition-colors', currentTab === 'dashboard' ? 'bg-brand-50 text-brand-600' : 'text-slate-600 hover:bg-slate-50']"> | |
| <span class="mr-3 text-lg">📊</span> 仪表盘 (Demo) | |
| </a> | |
| </nav> | |
| <div class="p-4 border-t border-slate-100"> | |
| <div class="bg-slate-50 rounded-lg p-3 text-xs text-slate-500"> | |
| <div class="flex items-center justify-between mb-1"> | |
| <span>API 状态</span> | |
| <span class="flex items-center text-green-600"><span class="w-2 h-2 rounded-full bg-green-500 mr-1"></span> 正常</span> | |
| </div> | |
| <div class="text-slate-400">v2.0.0 (Pro)</div> | |
| </div> | |
| </div> | |
| </aside> | |
| <!-- Mobile Menu Overlay --> | |
| <div v-show="mobileMenuOpen" class="fixed inset-0 z-40 md:hidden" role="dialog" aria-modal="true"> | |
| <!-- Background backdrop --> | |
| <div class="fixed inset-0 bg-slate-600 bg-opacity-75 transition-opacity" @click="mobileMenuOpen = false"></div> | |
| <!-- Menu panel --> | |
| <div class="relative flex-1 flex flex-col max-w-xs w-full bg-white h-full pt-5 pb-4 transition-transform transform"> | |
| <div class="absolute top-0 right-0 -mr-12 pt-2"> | |
| <button @click="mobileMenuOpen = false" class="ml-1 flex items-center justify-center h-10 w-10 rounded-full focus:outline-none focus:ring-2 focus:ring-inset focus:ring-white"> | |
| <span class="sr-only">Close sidebar</span> | |
| <svg class="h-6 w-6 text-white" fill="none" viewBox="0 0 24 24" stroke="currentColor" aria-hidden="true"> | |
| <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" /> | |
| </svg> | |
| </button> | |
| </div> | |
| <div class="flex-shrink-0 flex items-center px-4 mb-6"> | |
| <div class="w-8 h-8 bg-brand-600 rounded-lg flex items-center justify-center mr-3 shadow-sm"> | |
| <span class="text-white font-bold text-lg">E</span> | |
| </div> | |
| <h1 class="font-bold text-slate-800 text-lg">运营助手 AI</h1> | |
| </div> | |
| <div class="mt-5 flex-1 h-0 overflow-y-auto"> | |
| <nav class="px-2 space-y-1"> | |
| <a href="#" @click.prevent="resetChat(); mobileMenuOpen = false" | |
| class="group flex items-center px-2 py-2 text-base font-medium rounded-md text-slate-600 hover:bg-red-50 hover:text-red-600"> | |
| <span class="mr-4 text-xl">🆕</span> 新对话 | |
| </a> | |
| <a href="#" @click.prevent="currentTab = 'chat'; mobileMenuOpen = false" | |
| :class="[currentTab === 'chat' ? 'bg-brand-50 text-brand-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']"> | |
| <span class="mr-4 text-xl">💬</span> 智能诊断 | |
| </a> | |
| <a href="#" @click.prevent="currentTab = 'upload'; mobileMenuOpen = false" | |
| :class="[currentTab === 'upload' ? 'bg-brand-50 text-brand-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']"> | |
| <span class="mr-4 text-xl">📂</span> 数据清洗 | |
| </a> | |
| <a href="#" @click.prevent="currentTab = 'dashboard'; mobileMenuOpen = false" | |
| :class="[currentTab === 'dashboard' ? 'bg-brand-50 text-brand-600' : 'text-slate-600 hover:bg-slate-50 hover:text-slate-900', 'group flex items-center px-2 py-2 text-base font-medium rounded-md']"> | |
| <span class="mr-4 text-xl">📊</span> 仪表盘 (Demo) | |
| </a> | |
| </nav> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Main Content --> | |
| <main class="flex-1 flex flex-col min-w-0 bg-slate-50 relative"> | |
| <!-- Mobile Header --> | |
| <header class="h-14 bg-white border-b border-slate-200 md:hidden flex items-center justify-between px-4 z-20"> | |
| <span class="font-bold text-slate-800">电商运营助手</span> | |
| <button @click="mobileMenuOpen = !mobileMenuOpen" class="text-slate-500"> | |
| <svg class="w-6 h-6" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 6h16M4 12h16M4 18h16"></path></svg> | |
| </button> | |
| </header> | |
| <!-- Chat View --> | |
| <div v-show="currentTab === 'chat'" class="flex-1 flex flex-col overflow-hidden"> | |
| <!-- Chat History / Report Area --> | |
| <div ref="chatContainer" class="flex-1 overflow-y-auto p-4 md:p-8 scroll-smooth"> | |
| <div class="max-w-3xl mx-auto"> | |
| <!-- Welcome Card --> | |
| <div v-if="!messages.length && !loading" class="bg-white rounded-2xl shadow-sm border border-slate-200 p-8 text-center mt-10 animate-fade-in-up"> | |
| <div class="w-16 h-16 bg-brand-100 text-brand-600 rounded-full flex items-center justify-center mx-auto mb-6 text-3xl">🤖</div> | |
| <h2 class="text-2xl font-bold text-slate-800 mb-2">准备好优化您的店铺了吗?</h2> | |
| <p class="text-slate-500 mb-8 max-w-md mx-auto">输入您的品类、产品或遇到的运营难题,我将为您生成深度诊断报告。</p> | |
| <div class="grid grid-cols-1 sm:grid-cols-2 gap-3 text-left"> | |
| <button @click="quickStart('女装连衣裙 转化率低')" class="p-3 border border-slate-200 rounded-xl hover:border-brand-300 hover:bg-brand-50 transition-all group"> | |
| <div class="font-medium text-slate-700 group-hover:text-brand-700">📉 转化率低</div> | |
| <div class="text-xs text-slate-400 mt-1">诊断详情页与价格策略</div> | |
| </button> | |
| <button @click="quickStart('宠物零食 新品推广')" class="p-3 border border-slate-200 rounded-xl hover:border-brand-300 hover:bg-brand-50 transition-all group"> | |
| <div class="font-medium text-slate-700 group-hover:text-brand-700">🚀 新品冷启动</div> | |
| <div class="text-xs text-slate-400 mt-1">制定全周期推广计划</div> | |
| </button> | |
| <button @click="quickStart('户外露营装备 竞品分析')" class="p-3 border border-slate-200 rounded-xl hover:border-brand-300 hover:bg-brand-50 transition-all group"> | |
| <div class="font-medium text-slate-700 group-hover:text-brand-700">🆚 竞品分析</div> | |
| <div class="text-xs text-slate-400 mt-1">挖掘差异化卖点</div> | |
| </button> | |
| <button @click="quickStart('智能家居 详情页优化')" class="p-3 border border-slate-200 rounded-xl hover:border-brand-300 hover:bg-brand-50 transition-all group"> | |
| <div class="font-medium text-slate-700 group-hover:text-brand-700">🎨 视觉升级</div> | |
| <div class="text-xs text-slate-400 mt-1">提升页面吸引力</div> | |
| </button> | |
| </div> | |
| </div> | |
| <!-- Messages --> | |
| <div v-for="(msg, index) in messages" :key="index" class="mb-8"> | |
| <!-- User Message --> | |
| <div v-if="msg.role === 'user'" class="flex justify-end mb-4"> | |
| <div class="bg-slate-800 text-white px-5 py-3 rounded-2xl rounded-tr-sm max-w-[85%] shadow-md"> | |
| ${ msg.content } | |
| </div> | |
| </div> | |
| <!-- AI Message --> | |
| <div v-if="msg.role === 'assistant'" class="flex gap-4"> | |
| <div class="w-10 h-10 rounded-full bg-brand-600 flex-shrink-0 flex items-center justify-center text-white shadow-sm mt-1">AI</div> | |
| <div class="flex-1 bg-white rounded-2xl rounded-tl-sm border border-slate-200 p-6 shadow-sm min-w-0"> | |
| <!-- Markdown Content --> | |
| <div class="prose prose-slate max-w-none text-slate-600" v-html="renderMarkdown(msg.content)"></div> | |
| <!-- Charts Container (Dynamic) --> | |
| <div :id="'charts-' + index" class="mt-4 grid grid-cols-1 md:grid-cols-2 gap-4"></div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Loading Indicator --> | |
| <div v-if="loading" class="flex gap-4 mb-8"> | |
| <div class="w-10 h-10 rounded-full bg-brand-600 flex-shrink-0 flex items-center justify-center text-white shadow-sm animate-pulse">AI</div> | |
| <div class="flex items-center space-x-2 text-slate-400 text-sm py-3"> | |
| <span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce"></span> | |
| <span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></span> | |
| <span class="w-2 h-2 bg-slate-400 rounded-full animate-bounce" style="animation-delay: 0.4s"></span> | |
| <span>正在思考策略...</span> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Input Area --> | |
| <div class="p-4 bg-white border-t border-slate-200"> | |
| <div class="max-w-3xl mx-auto relative"> | |
| <textarea | |
| v-model="query" | |
| @keydown.enter.prevent="submitQuery" | |
| placeholder="输入您的问题..." | |
| rows="1" | |
| class="w-full pl-4 pr-12 py-3 bg-slate-50 border border-slate-200 rounded-xl focus:outline-none focus:ring-2 focus:ring-brand-500 focus:bg-white transition-all resize-none shadow-sm" | |
| style="min-height: 50px; max-height: 150px;" | |
| :disabled="loading" | |
| ></textarea> | |
| <button | |
| @click="submitQuery" | |
| :disabled="loading || !query.trim()" | |
| class="absolute right-2 bottom-2 p-2 bg-brand-600 text-white rounded-lg hover:bg-brand-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors" | |
| > | |
| <svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 12h14M12 5l7 7-7 7"></path></svg> | |
| </button> | |
| </div> | |
| <div class="text-center text-xs text-slate-400 mt-2">AI生成内容仅供参考,请结合实际业务情况决策</div> | |
| </div> | |
| </div> | |
| <!-- Upload View --> | |
| <div v-show="currentTab === 'upload'" class="flex-1 p-8 overflow-y-auto"> | |
| <div class="max-w-4xl mx-auto"> | |
| <h2 class="text-2xl font-bold text-slate-800 mb-6">数据文件上传与清洗</h2> | |
| <div class="bg-white rounded-xl border border-dashed border-slate-300 p-10 text-center hover:bg-slate-50 transition-colors cursor-pointer" | |
| @click="triggerUpload" | |
| @drop.prevent="handleDrop" | |
| @dragover.prevent> | |
| <div class="w-16 h-16 bg-blue-50 text-blue-500 rounded-full flex items-center justify-center mx-auto mb-4"> | |
| <svg class="w-8 h-8" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path></svg> | |
| </div> | |
| <h3 class="text-lg font-medium text-slate-700 mb-1">点击或拖拽文件到此处</h3> | |
| <p class="text-slate-500 text-sm mb-4">支持 CSV, Excel (xlsx), JSON 格式,最大 10MB</p> | |
| <input type="file" ref="fileInput" class="hidden" @change="handleFileSelect" accept=".csv,.xlsx,.json"> | |
| <button class="px-4 py-2 bg-white border border-slate-300 rounded-lg text-slate-700 text-sm font-medium hover:bg-slate-50"> | |
| 选择文件 | |
| </button> | |
| </div> | |
| <!-- Upload Progress/Result --> | |
| <div v-if="uploadStatus" class="mt-6 bg-white rounded-xl border border-slate-200 p-6 shadow-sm"> | |
| <div class="flex items-center justify-between mb-4"> | |
| <div class="flex items-center"> | |
| <div :class="['w-10 h-10 rounded-lg flex items-center justify-center mr-3', uploadStatus.status === 'success' ? 'bg-green-100 text-green-600' : 'bg-blue-100 text-blue-600']"> | |
| <span v-if="uploadStatus.status === 'success'">✓</span> | |
| <span v-else>...</span> | |
| </div> | |
| <div> | |
| <div class="font-medium text-slate-800">${ uploadStatus.filename }</div> | |
| <div class="text-xs text-slate-500">${ (uploadStatus.size / 1024).toFixed(1) } KB</div> | |
| </div> | |
| </div> | |
| <span :class="['text-sm font-medium', uploadStatus.status === 'success' ? 'text-green-600' : 'text-blue-600']"> | |
| ${ uploadStatus.message } | |
| </span> | |
| </div> | |
| <div v-if="uploadStatus.analysis" class="bg-slate-50 rounded-lg p-4 text-sm text-slate-600"> | |
| <p class="font-medium text-slate-800 mb-2">自动分析结果:</p> | |
| <p>${ uploadStatus.analysis.summary }</p> | |
| <div class="mt-2 grid grid-cols-3 gap-2 text-xs"> | |
| <div class="bg-white p-2 rounded border border-slate-200">行数: ${ uploadStatus.analysis.rows }</div> | |
| <div class="bg-white p-2 rounded border border-slate-200 col-span-2">列: ${ uploadStatus.analysis.columns.join(', ') }</div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Dashboard View (Demo) --> | |
| <div v-show="currentTab === 'dashboard'" class="flex-1 p-8 overflow-y-auto"> | |
| <div class="max-w-5xl mx-auto"> | |
| <h2 class="text-2xl font-bold text-slate-800 mb-6">运营仪表盘 (Demo)</h2> | |
| <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8"> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm"> | |
| <div class="text-slate-500 text-sm mb-1">今日GMV</div> | |
| <div class="text-2xl font-bold text-slate-800">¥ 124,592</div> | |
| <div class="text-green-500 text-xs mt-2">↑ 12.5% 环比</div> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm"> | |
| <div class="text-slate-500 text-sm mb-1">转化率</div> | |
| <div class="text-2xl font-bold text-slate-800">3.2%</div> | |
| <div class="text-red-500 text-xs mt-2">↓ 0.4% 环比</div> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm"> | |
| <div class="text-slate-500 text-sm mb-1">客单价</div> | |
| <div class="text-2xl font-bold text-slate-800">¥ 285</div> | |
| <div class="text-green-500 text-xs mt-2">↑ 5.2% 环比</div> | |
| </div> | |
| </div> | |
| <div class="bg-white p-6 rounded-xl border border-slate-200 shadow-sm h-96 flex items-center justify-center text-slate-400"> | |
| <p>此处可集成更多可视化图表 (ECharts / Chart.js)</p> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| <script> | |
| const { createApp, ref, onMounted, nextTick } = Vue; | |
| createApp({ | |
| delimiters: ['${', '}'], // 防止 Jinja2 冲突 | |
| setup() { | |
| const currentTab = ref('chat'); | |
| const query = ref(''); | |
| const loading = ref(false); | |
| const messages = ref([]); | |
| const chatContainer = ref(null); | |
| const fileInput = ref(null); | |
| const uploadStatus = ref(null); | |
| const mobileMenuOpen = ref(false); | |
| // Markdown parser | |
| const md = window.markdownit({ | |
| html: true, | |
| linkify: true, | |
| typographer: true | |
| }); | |
| const scrollToBottom = async () => { | |
| await nextTick(); | |
| if (chatContainer.value) { | |
| chatContainer.value.scrollTop = chatContainer.value.scrollHeight; | |
| } | |
| }; | |
| const renderMarkdown = (text) => { | |
| // 移除 json:chart 代码块,避免在文本中显示,图表会单独渲染 | |
| const cleanText = text.replace(/```json:chart[\s\S]*?```/g, ''); | |
| return md.render(cleanText); | |
| }; | |
| const renderCharts = (messageIndex, text) => { | |
| const regex = /```json:chart([\s\S]*?)```/g; | |
| let match; | |
| const chartDataList = []; | |
| while ((match = regex.exec(text)) !== null) { | |
| try { | |
| const jsonData = JSON.parse(match[1]); | |
| chartDataList.push(jsonData); | |
| } catch (e) { | |
| console.error("JSON parse error:", e); | |
| } | |
| } | |
| if (chartDataList.length > 0) { | |
| nextTick(() => { | |
| const container = document.getElementById('charts-' + messageIndex); | |
| if (!container) return; | |
| // 清空旧图表以防重绘 | |
| container.innerHTML = ''; | |
| chartDataList.forEach((data, i) => { | |
| const canvasId = `chart-${messageIndex}-${i}`; | |
| const wrapper = document.createElement('div'); | |
| wrapper.className = 'chart-container'; | |
| wrapper.innerHTML = `<canvas id="${canvasId}"></canvas>`; | |
| container.appendChild(wrapper); | |
| const ctx = document.getElementById(canvasId).getContext('2d'); | |
| new Chart(ctx, { | |
| type: data.type || 'bar', | |
| data: { | |
| labels: data.labels, | |
| datasets: data.datasets.map(ds => ({ | |
| ...ds, | |
| backgroundColor: ds.label.includes('Top') ? 'rgba(59, 130, 246, 0.2)' : 'rgba(37, 99, 235, 0.6)', | |
| borderColor: '#2563eb', | |
| borderWidth: 1 | |
| })) | |
| }, | |
| options: { | |
| responsive: true, | |
| plugins: { | |
| title: { display: true, text: data.title }, | |
| legend: { position: 'bottom' } | |
| } | |
| } | |
| }); | |
| }); | |
| }); | |
| } | |
| }; | |
| const quickStart = (text) => { | |
| query.value = text; | |
| submitQuery(); | |
| }; | |
| const resetChat = () => { | |
| if (messages.value.length > 0 && !confirm('确定要开始新对话吗?当前记录将被清空。')) { | |
| return; | |
| } | |
| messages.value = []; | |
| currentTab.value = 'chat'; | |
| uploadStatus.value = null; | |
| }; | |
| const submitQuery = async () => { | |
| if (!query.value.trim() || loading.value) return; | |
| const userQ = query.value; | |
| query.value = ''; | |
| loading.value = true; | |
| // 构建历史记录 (仅包含之前的对话,不包含当前正在发送的) | |
| const history = messages.value.map(msg => ({ | |
| role: msg.role, | |
| content: msg.content | |
| })); | |
| // Add User Message | |
| messages.value.push({ role: 'user', content: userQ }); | |
| scrollToBottom(); | |
| // Placeholder for AI Message | |
| const aiMsgIndex = messages.value.length; | |
| messages.value.push({ role: 'assistant', content: '' }); | |
| try { | |
| const response = await fetch('/api/chat', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' }, | |
| body: JSON.stringify({ | |
| query: userQ, | |
| history: history, | |
| stream: true | |
| }) | |
| }); | |
| const reader = response.body.getReader(); | |
| const decoder = new TextDecoder(); | |
| let fullContent = ''; | |
| while (true) { | |
| const { done, value } = await reader.read(); | |
| if (done) break; | |
| const chunk = decoder.decode(value, { stream: true }); | |
| fullContent += chunk; | |
| // Update content | |
| messages.value[aiMsgIndex].content = fullContent; | |
| // 尝试渲染图表 (在流式传输过程中也可以尝试,但最好在完整块到达后) | |
| // 为了简化,每次更新都尝试解析最新的图表数据 | |
| renderCharts(aiMsgIndex, fullContent); | |
| scrollToBottom(); | |
| } | |
| } catch (e) { | |
| messages.value[aiMsgIndex].content += `\n\n**Error**: ${e.message}`; | |
| } finally { | |
| loading.value = false; | |
| } | |
| }; | |
| // File Upload Logic | |
| const triggerUpload = () => { | |
| fileInput.value.click(); | |
| }; | |
| const handleFileSelect = (event) => { | |
| const file = event.target.files[0]; | |
| if (file) uploadFile(file); | |
| }; | |
| const handleDrop = (event) => { | |
| const file = event.dataTransfer.files[0]; | |
| if (file) uploadFile(file); | |
| }; | |
| const uploadFile = async (file) => { | |
| uploadStatus.value = { | |
| filename: file.name, | |
| size: file.size, | |
| status: 'uploading', | |
| message: '正在上传并分析...' | |
| }; | |
| const formData = new FormData(); | |
| formData.append('file', file); | |
| try { | |
| const res = await fetch('/api/upload', { | |
| method: 'POST', | |
| body: formData | |
| }); | |
| if (!res.ok) throw new Error('Upload failed'); | |
| const data = await res.json(); | |
| uploadStatus.value = { | |
| ...uploadStatus.value, | |
| status: 'success', | |
| message: '分析完成', | |
| analysis: data.analysis | |
| }; | |
| } catch (e) { | |
| uploadStatus.value = { | |
| ...uploadStatus.value, | |
| status: 'error', | |
| message: '上传失败: ' + e.message | |
| }; | |
| } | |
| }; | |
| return { | |
| currentTab, | |
| query, | |
| loading, | |
| messages, | |
| chatContainer, | |
| fileInput, | |
| uploadStatus, | |
| mobileMenuOpen, | |
| resetChat, | |
| submitQuery, | |
| quickStart, | |
| renderMarkdown, | |
| triggerUpload, | |
| handleFileSelect, | |
| handleDrop | |
| }; | |
| } | |
| }).mount('#app'); | |
| </script> | |
| </body> | |
| </html> | |