Spaces:
Sleeping
Sleeping
| 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'); | |