Trae Assistant
feat: enhance chat ux with new chat button and siliconflow integration
946410b
<!DOCTYPE html>
<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 !important; }
/* 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>