Trae Assistant
feat: initial commit for deployment
273c668
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>智能数据分析师 Agent</title>
<!-- Tailwind CSS -->
<script src="https://cdn.tailwindcss.com"></script>
<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<!-- ECharts -->
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<!-- Marked -->
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
body {
background-color: #ffffff;
color: #000000;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif;
}
.chat-bubble {
max-width: 80%;
padding: 1rem;
border-radius: 0.5rem;
margin-bottom: 0.5rem;
line-height: 1.6;
}
.user-bubble {
background-color: #f3f4f6;
align-self: flex-end;
margin-left: auto;
border-top-right-radius: 0;
}
.agent-bubble {
background-color: #ffffff;
border: 1px solid #e5e7eb;
align-self: flex-start;
border-top-left-radius: 0;
box-shadow: 0 1px 2px rgba(0,0,0,0.05);
}
.chart-container {
height: 400px;
width: 100%;
}
/* Table styles for dataframe summary */
table {
width: 100%;
border-collapse: collapse;
font-size: 0.875rem;
}
th, td {
border: 1px solid #e5e7eb;
padding: 0.5rem;
text-align: left;
}
th {
background-color: #f9fafb;
font-weight: 600;
}
/* Custom scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #d1d5db;
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: #9ca3af;
}
</style>
</head>
<body class="h-screen flex flex-col overflow-hidden bg-gray-50">
<div id="app" class="flex h-full shadow-lg max-w-[1920px] mx-auto bg-white">
<!-- Sidebar -->
<div class="w-72 border-r border-gray-200 bg-gray-50 flex flex-col p-5">
<h1 class="text-xl font-bold mb-8 flex items-center text-gray-800">
<span class="text-2xl mr-2">📊</span> 智能分析师
</h1>
<div class="mb-8">
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4">数据源</h2>
<div class="space-y-3">
<button @click="loadDemo" :disabled="loading" class="w-full flex items-center justify-center px-4 py-2.5 bg-white border border-gray-300 rounded-lg hover:bg-gray-100 hover:border-gray-400 text-sm font-medium text-gray-700 transition-all shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2 text-gray-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19.428 15.428a2 2 0 00-1.022-.547l-2.384-.477a6 6 0 00-3.86.517l-.318.158a6 6 0 01-3.86.517L6.05 15.21a2 2 0 00-1.806.547M8 4h8l-1 1v5.172a2 2 0 00.586 1.414l5 5c1.26 1.26.367 3.414-1.415 3.414H4.828c-1.782 0-2.674-2.154-1.414-3.414l5-5A2 2 0 009 10.172V5L8 4z" />
</svg>
加载演示数据
</button>
<div class="relative">
<input type="file" @change="uploadFile" ref="fileInput" class="hidden" accept=".csv,.xlsx">
<button @click="triggerUpload" :disabled="loading" class="w-full flex items-center justify-center px-4 py-2.5 bg-blue-600 text-white rounded-lg hover:bg-blue-700 hover:shadow-md text-sm font-medium transition-all shadow-sm">
<svg xmlns="http://www.w3.org/2000/svg" class="h-4 w-4 mr-2" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<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" />
</svg>
上传文件 (CSV/Excel)
</button>
</div>
</div>
</div>
<div v-if="summary" class="flex-1 overflow-y-auto pr-1">
<h2 class="text-xs font-bold text-gray-400 uppercase tracking-wider mb-4">数据集概览</h2>
<div class="bg-white p-4 rounded-lg border border-gray-200 shadow-sm text-sm">
<div class="grid grid-cols-2 gap-2 mb-4">
<div class="bg-gray-50 p-2 rounded text-center">
<div class="text-xs text-gray-500">行数</div>
<div class="font-bold text-gray-800">${ summary.row_count }</div>
</div>
<div class="bg-gray-50 p-2 rounded text-center">
<div class="text-xs text-gray-500">列数</div>
<div class="font-bold text-gray-800">${ summary.columns.length }</div>
</div>
</div>
<div class="mt-4">
<h3 class="font-medium mb-2 text-gray-700 flex items-center">
<span class="w-2 h-2 bg-blue-500 rounded-full mr-2"></span>数值型列
</h3>
<ul class="space-y-1">
<li v-for="col in summary.numeric_columns" :key="col" class="text-xs text-gray-600 bg-gray-50 px-2 py-1 rounded truncate border border-gray-100">${ col }</li>
</ul>
</div>
<div class="mt-4">
<h3 class="font-medium mb-2 text-gray-700 flex items-center">
<span class="w-2 h-2 bg-green-500 rounded-full mr-2"></span>类别型列
</h3>
<ul class="space-y-1">
<li v-for="col in summary.categorical_columns" :key="col" class="text-xs text-gray-600 bg-gray-50 px-2 py-1 rounded truncate border border-gray-100">${ col }</li>
</ul>
</div>
</div>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col min-w-0 bg-white relative">
<!-- Header -->
<div class="h-16 border-b border-gray-200 flex items-center px-8 justify-between bg-white z-10">
<div>
<h2 class="font-bold text-lg text-gray-800">分析仪表盘</h2>
<p class="text-xs text-gray-500">与您的数据对话,发现洞察</p>
</div>
<div class="text-xs px-3 py-1 bg-blue-50 text-blue-600 rounded-full border border-blue-100 font-medium">
Powered by Agentic Analytics
</div>
</div>
<!-- Content Area (Scrollable) -->
<div class="flex-1 overflow-y-auto p-8" id="chat-container">
<!-- Welcome State -->
<div v-if="messages.length === 0" class="h-full flex flex-col items-center justify-center text-gray-400 animate-fade-in">
<div class="w-24 h-24 bg-gray-50 rounded-full flex items-center justify-center mb-6">
<span class="text-4xl">👋</span>
</div>
<h3 class="text-xl font-medium text-gray-700 mb-2">欢迎使用智能分析师</h3>
<p class="text-sm text-gray-500 max-w-md text-center">
数据已准备就绪。您可以尝试上传自己的文件,或者直接开始分析当前的演示数据。
</p>
</div>
<!-- Chat Stream -->
<div v-else class="flex flex-col space-y-8 pb-4">
<div v-for="(msg, index) in messages" :key="index" :class="['flex flex-col', msg.role === 'user' ? 'items-end' : 'items-start']">
<!-- Message Bubble -->
<div :class="['chat-bubble shadow-sm text-sm', msg.role === 'user' ? 'user-bubble text-gray-800' : 'agent-bubble text-gray-700']">
<div v-if="msg.role === 'agent'" class="font-bold text-xs text-blue-600 mb-2 flex items-center">
<span class="w-4 h-4 rounded-full bg-blue-100 flex items-center justify-center mr-1 text-[10px]">AI</span>
智能分析师
</div>
<div v-html="renderMarkdown(msg.content)" class="prose prose-sm max-w-none"></div>
</div>
<!-- Chart (if any) -->
<div v-if="msg.chart" :id="'chart-' + index" class="chart-container mt-3 border border-gray-200 rounded-lg p-4 shadow-sm bg-white"></div>
</div>
<!-- Loading Indicator -->
<div v-if="loading" class="flex items-center space-x-2 text-gray-500 text-sm p-2 bg-gray-50 rounded-lg self-start">
<div class="flex space-x-1">
<div class="w-2 h-2 bg-blue-400 rounded-full animate-bounce"></div>
<div class="w-2 h-2 bg-blue-400 rounded-full animate-bounce" style="animation-delay: 0.1s"></div>
<div class="w-2 h-2 bg-blue-400 rounded-full animate-bounce" style="animation-delay: 0.2s"></div>
</div>
<span class="text-xs font-medium">正在分析数据...</span>
</div>
</div>
</div>
<!-- Input Area -->
<div class="p-6 border-t border-gray-200 bg-white">
<div class="relative max-w-4xl mx-auto">
<input
v-model="inputMessage"
@keyup.enter="sendMessage"
:disabled="!summary || loading"
type="text"
placeholder="输入您的问题,例如:分析销售趋势、展示类别分布..."
class="w-full pl-5 pr-14 py-4 border border-gray-300 rounded-xl focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent disabled:bg-gray-50 disabled:text-gray-400 shadow-sm text-sm transition-all"
>
<button
@click="sendMessage"
:disabled="!summary || loading"
class="absolute right-3 top-3 p-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed transition-colors shadow-sm"
>
<svg xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="2" stroke="currentColor" class="w-5 h-5">
<path stroke-linecap="round" stroke-linejoin="round" d="M6 12L3.269 3.126A59.768 59.768 0 0121.485 12 59.77 59.77 0 013.27 20.876L5.999 12zm0 0h7.5" />
</svg>
</button>
</div>
<div class="text-center mt-3" v-if="summary">
<div class="text-xs text-gray-400 flex justify-center space-x-4">
<button class="hover:text-blue-600 hover:underline transition-colors" @click="setQuery('展示销售趋势')">📊 销售趋势</button>
<button class="hover:text-blue-600 hover:underline transition-colors" @click="setQuery('分析各分类的分布情况')">🥧 分类分布</button>
<button class="hover:text-blue-600 hover:underline transition-colors" @click="setQuery('显示相关性矩阵')">🔥 相关性分析</button>
</div>
</div>
</div>
<!-- Toast Notification -->
<div v-if="toast.show" class="fixed top-5 right-5 z-50 transform transition-all duration-300 ease-in-out" :class="toast.type === 'error' ? 'text-red-800 bg-red-50 border-red-200' : 'text-green-800 bg-green-50 border-green-200'">
<div class="flex items-center p-4 mb-4 rounded-lg shadow border" role="alert">
<svg v-if="toast.type === 'success'" class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"></path></svg>
<svg v-else class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M18 10a8 8 0 11-16 0 8 8 0 0116 0zm-7 4a1 1 0 11-2 0 1 1 0 012 0zm-1-9a1 1 0 00-1 1v4a1 1 0 102 0V6a1 1 0 00-1-1z" clip-rule="evenodd"></path></svg>
<div class="text-sm font-medium">${ toast.message }</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, nextTick, onMounted } = Vue;
createApp({
delimiters: ['${', '}'],
setup() {
const messages = ref([]);
const inputMessage = ref('');
const loading = ref(false);
const summary = ref(null);
const fileInput = ref(null);
const toast = ref({ show: false, message: '', type: 'info' });
const showToast = (msg, type = 'info') => {
toast.value = { show: true, message: msg, type };
setTimeout(() => {
toast.value.show = false;
}, 3000);
};
const renderMarkdown = (text) => {
return marked.parse(text);
};
const scrollToBottom = () => {
nextTick(() => {
const container = document.getElementById('chat-container');
if (container) container.scrollTop = container.scrollHeight;
});
};
const renderChart = (index, chartData) => {
nextTick(() => {
const chartDom = document.getElementById('chart-' + index);
if (!chartDom) return;
const myChart = echarts.init(chartDom);
let option = {};
if (chartData.type === 'line') {
option = {
title: { text: chartData.title },
tooltip: { trigger: 'axis' },
xAxis: { type: 'category', data: chartData.x },
yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
series: [{
data: chartData.y,
type: 'line',
smooth: true,
areaStyle: { opacity: 0.1 },
itemStyle: { color: '#3b82f6' },
lineStyle: { color: '#3b82f6', width: 3 }
}]
};
} else if (chartData.type === 'bar') {
option = {
title: { text: chartData.title },
tooltip: { trigger: 'axis', axisPointer: { type: 'shadow' } },
xAxis: { type: 'category', data: chartData.x, axisLabel: { interval: 0, rotate: 30 } },
yAxis: { type: 'value' },
grid: { left: '3%', right: '4%', bottom: '10%', containLabel: true },
series: [{
data: chartData.y,
type: 'bar',
itemStyle: {
borderRadius: [4, 4, 0, 0],
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#60a5fa' },
{ offset: 1, color: '#2563eb' }
])
}
}]
};
} else if (chartData.type === 'heatmap') {
option = {
title: { text: chartData.title, left: 'center' },
tooltip: { position: 'top' },
grid: { height: '70%', top: '15%' },
xAxis: { type: 'category', data: chartData.x, splitArea: { show: true } },
yAxis: { type: 'category', data: chartData.y, splitArea: { show: true } },
visualMap: {
min: -1, max: 1,
calculable: true,
orient: 'horizontal',
left: 'center',
bottom: '0%',
inRange: { color: ['#ef4444', '#f3f4f6', '#3b82f6'] }
},
series: [{
type: 'heatmap',
data: chartData.z.map((row, i) => row.map((val, j) => [j, i, val || '-'])).flat(),
label: { show: true, formatter: (p) => p.value[2].toFixed(2) },
emphasis: { itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0, 0, 0, 0.5)' } }
}]
};
}
myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize());
});
};
const addMessage = (role, content, chart = null) => {
messages.value.push({ role, content, chart });
scrollToBottom();
if (chart) {
renderChart(messages.value.length - 1, chart);
}
};
const loadDemo = async () => {
loading.value = true;
try {
const res = await fetch('/api/load_demo', { method: 'POST' });
const data = await res.json();
if (data.status === 'success') {
summary.value = data.summary;
showToast('演示数据加载成功', 'success');
if (messages.value.length === 0) {
addMessage('agent', '已为您加载演示零售数据集。包含日期、分类、销售额等信息。');
addMessage('agent', '您可以尝试问我:"展示销售趋势" 或 "分析各分类的利润"。');
}
}
} catch (e) {
console.error(e);
showToast('加载演示数据失败', 'error');
} finally {
loading.value = false;
}
};
const triggerUpload = () => {
if (fileInput.value) {
fileInput.value.click();
} else {
console.error("File input not found");
}
};
const uploadFile = async (event) => {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
loading.value = true;
try {
const res = await fetch('/api/upload', { method: 'POST', body: formData });
const data = await res.json();
if (data.status === 'success') {
summary.value = data.summary;
showToast('文件上传成功', 'success');
addMessage('agent', `文件上传成功!共发现 ${data.summary.row_count} 行和 ${data.summary.columns.length} 列数据。`);
} else {
showToast(data.error || '上传失败', 'error');
}
} catch (e) {
console.error(e);
showToast('上传失败,请检查网络或文件大小', 'error');
} finally {
loading.value = false;
// Reset file input
event.target.value = '';
}
};
const sendMessage = async () => {
if (!inputMessage.value.trim() || loading.value) return;
const msg = inputMessage.value;
addMessage('user', msg);
inputMessage.value = '';
loading.value = true;
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: msg })
});
const data = await res.json();
addMessage('agent', data.text, data.chart);
} catch (e) {
addMessage('agent', '抱歉,分析您的请求时出现错误。');
} finally {
loading.value = false;
}
};
const setQuery = (q) => {
inputMessage.value = q;
sendMessage();
}
// Auto-load demo data on mount
onMounted(() => {
loadDemo();
});
return {
messages,
inputMessage,
loading,
summary,
fileInput,
toast,
renderMarkdown,
loadDemo,
uploadFile,
triggerUpload,
sendMessage,
setQuery
};
}
}).mount('#app');
</script>
</body>
</html>