EduSparkBot
feat: upgrade backend and frontend with upload feature and enhanced charts
a8a5d2d
const { createApp, ref, onMounted, nextTick } = Vue;
createApp({
delimiters: ['${', '}'],
setup() {
// State
const currentTab = ref('dashboard');
const showMobileMenu = ref(false);
const chatInput = ref('');
const chatHistory = ref([
{ role: 'assistant', content: '你好!我是你的 AI 学习导师。你想学点什么?' }
]);
const isTyping = ref(false);
const chatContainer = ref(null);
const generatorTopic = ref('');
const generatorLevel = ref('Beginner');
const isGenerating = ref(false);
const generatedCourse = ref(null);
const courses = ref([]);
let chartInstance = null;
let activityChartInstance = null;
const fileInput = ref(null);
const toasts = ref([]);
// Methods
const showToast = (type, title, message) => {
const id = Date.now();
toasts.value.push({ id, type, title, message });
setTimeout(() => {
toasts.value = toasts.value.filter(t => t.id !== id);
}, 3000);
};
const triggerUpload = () => {
if (fileInput.value) {
fileInput.value.click();
} else {
console.error("File input not found");
}
};
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
const formData = new FormData();
formData.append('file', file);
try {
showToast('info', '上传中...', `正在上传 ${file.name}`);
const res = await fetch('/api/upload', {
method: 'POST',
body: formData
});
if (res.ok) {
const data = await res.json();
showToast('success', '上传成功', data.message);
} else {
const err = await res.json();
showToast('error', '上传失败', err.error || '文件可能过大或格式不支持');
}
} catch (e) {
showToast('error', '网络错误', '无法连接到服务器');
console.error(e);
} finally {
event.target.value = ''; // Reset input
}
};
const setTab = (tab) => {
currentTab.value = tab;
showMobileMenu.value = false;
if (tab === 'dashboard') {
nextTick(() => initChart());
}
};
const renderMarkdown = (text) => {
return marked.parse(text);
};
const scrollToBottom = () => {
if (chatContainer.value) {
setTimeout(() => {
chatContainer.value.scrollTop = chatContainer.value.scrollHeight;
}, 100);
}
};
const initChart = async () => {
const radarDom = document.getElementById('skill-radar');
const activityDom = document.getElementById('activity-chart');
if (!radarDom) return;
if (chartInstance) chartInstance.dispose();
if (activityChartInstance) activityChartInstance.dispose();
chartInstance = echarts.init(radarDom);
if (activityDom) activityChartInstance = echarts.init(activityDom);
try {
const res = await fetch('/api/stats');
const data = await res.json();
// Radar Chart
const radarOption = {
radar: {
indicator: data.skills,
radius: '65%'
},
series: [{
type: 'radar',
data: [{
value: data.skills.map(s => s.value),
name: '当前能力',
areaStyle: { color: 'rgba(79, 70, 229, 0.2)' },
lineStyle: { color: '#4f46e5' },
itemStyle: { color: '#4f46e5' }
}]
}]
};
chartInstance.setOption(radarOption);
// Activity Chart (Line)
if (activityChartInstance && data.activity) {
const activityOption = {
tooltip: { trigger: 'axis' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', data: data.activity.labels },
yAxis: { type: 'value' },
series: [{
data: data.activity.data,
type: 'line',
smooth: true,
itemStyle: { color: '#10b981' },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(16, 185, 129, 0.5)' },
{ offset: 1, color: 'rgba(16, 185, 129, 0.0)' }
])
}
}]
};
activityChartInstance.setOption(activityOption);
}
} catch (e) {
console.error("Chart Error", e);
}
window.addEventListener('resize', () => {
chartInstance && chartInstance.resize();
activityChartInstance && activityChartInstance.resize();
});
};
const sendMessage = async () => {
if (!chatInput.value.trim()) return;
const msg = chatInput.value;
chatHistory.value.push({ role: 'user', content: msg });
chatInput.value = '';
scrollToBottom();
isTyping.value = true;
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message: msg,
history: chatHistory.value.slice(-5) // Send last 5 context
})
});
const data = await res.json();
chatHistory.value.push({ role: 'assistant', content: data.response });
} catch (e) {
chatHistory.value.push({ role: 'assistant', content: "**网络错误**: 无法连接到 AI 服务。" });
} finally {
isTyping.value = false;
scrollToBottom();
}
};
const clearChat = () => {
chatHistory.value = [{ role: 'assistant', content: '对话已清空。我们可以重新开始!' }];
};
const generateCourse = async () => {
if (!generatorTopic.value) return;
isGenerating.value = true;
generatedCourse.value = null;
try {
const res = await fetch('/api/generate_course', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
topic: generatorTopic.value,
level: generatorLevel.value
})
});
const data = await res.json();
generatedCourse.value = data;
// Refresh library silently
loadCourses();
} catch (e) {
alert("生成失败,请重试");
} finally {
isGenerating.value = false;
}
};
const loadCourses = async () => {
try {
const res = await fetch('/api/courses');
courses.value = await res.json();
} catch (e) {
console.error(e);
}
};
// Lifecycle
onMounted(() => {
initChart();
loadCourses();
});
return {
currentTab,
showMobileMenu,
chatInput,
chatHistory,
isTyping,
chatContainer,
generatorTopic,
generatorLevel,
isGenerating,
generatedCourse,
courses,
setTab,
renderMarkdown,
sendMessage,
clearChat,
generateCourse,
loadCourses,
fileInput,
toasts,
showToast,
triggerUpload,
handleFileUpload
};
}
}).mount('#app');