Trae Assistant
fix: resolve Vue template syntax error causing blank page
7c44101
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Echo Mimic Agent - 数字分身工坊</title>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://cdn.jsdelivr.net/npm/echarts@5.4.3/dist/echarts.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
<style>
[v-cloak] { display: none; }
body { background-color: #f8fafc; color: #1e293b; }
.chat-bubble { max-width: 80%; padding: 12px; border-radius: 12px; margin-bottom: 8px; }
.user-bubble { background-color: #3b82f6; color: white; align-self: flex-end; margin-left: auto; }
.ai-bubble { background-color: #ffffff; border: 1px solid #e2e8f0; align-self: flex-start; }
</style>
</head>
<body>
<div id="app" v-cloak class="h-screen flex flex-col md:flex-row overflow-hidden">
<!-- Sidebar (Personas) -->
<div class="w-full md:w-64 bg-white border-r border-gray-200 flex flex-col flex-shrink-0">
<div class="p-4 border-b border-gray-200 bg-blue-50">
<h1 class="text-xl font-bold text-blue-800 flex items-center">
<span class="text-2xl mr-2">🎭</span> Echo Mimic
</h1>
<p class="text-xs text-blue-600 mt-1">数字分身工坊</p>
</div>
<div class="flex-1 overflow-y-auto p-2">
<div v-for="p in savedPersonas" :key="p.id"
@click="selectPersona(p)"
class="p-3 mb-2 rounded-lg cursor-pointer hover:bg-blue-50 transition-colors border border-transparent hover:border-blue-200"
:class="{'bg-blue-100 border-blue-300': currentPersona && currentPersona.id === p.id}">
<div class="font-bold text-gray-800">${ p.name }</div>
<div class="text-xs text-gray-500 truncate">${ p.role }</div>
</div>
<div v-if="savedPersonas.length === 0" class="text-center text-gray-400 mt-10 text-sm">
暂无分身<br>点击 "新建" 创建
</div>
</div>
<div class="p-4 border-t border-gray-200">
<button @click="resetToCreate" class="w-full bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded transition">
+ 新建分身
</button>
</div>
</div>
<!-- Main Content -->
<div class="flex-1 flex flex-col overflow-hidden relative">
<!-- Mobile Header -->
<div class="md:hidden p-4 bg-white border-b border-gray-200 flex justify-between items-center">
<span class="font-bold">Echo Mimic</span>
<button @click="showMobileMenu = !showMobileMenu" class="text-gray-600"></button>
</div>
<!-- Tabs -->
<div class="bg-white border-b border-gray-200 flex px-4 pt-2">
<button v-for="tab in tabs" :key="tab.id"
@click="currentTab = tab.id"
class="px-4 py-2 text-sm font-medium border-b-2 transition-colors mr-4"
:class="currentTab === tab.id ? 'border-blue-500 text-blue-600' : 'border-transparent text-gray-500 hover:text-gray-700'">
${ tab.name }
</button>
</div>
<!-- Tab Content -->
<div class="flex-1 overflow-y-auto p-4 md:p-8 relative">
<!-- Create/Studio Mode -->
<div v-if="currentTab === 'create'" class="max-w-4xl mx-auto grid grid-cols-1 md:grid-cols-2 gap-8">
<div class="space-y-4">
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<h2 class="text-lg font-bold mb-4">🔮 快速生成 (AI Generate)</h2>
<textarea v-model="generationPrompt"
placeholder="描述你想创建的角色,例如:'一个严厉但公正的高中数学老师,喜欢用几何比喻人生'..."
class="w-full p-3 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-blue-500 h-24 mb-3"></textarea>
<button @click="generatePersona" :disabled="isGenerating"
class="w-full bg-indigo-600 hover:bg-indigo-700 text-white py-2 rounded-lg flex justify-center items-center">
<span v-if="isGenerating" class="animate-spin mr-2">⚙️</span>
${ isGenerating ? '生成中...' : 'AI 自动生成' }
</button>
</div>
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<h2 class="text-lg font-bold mb-4">📝 详细配置</h2>
<div class="space-y-3">
<div>
<label class="block text-xs font-medium text-gray-500 uppercase">头像</label>
<div class="flex items-center space-x-3 mt-1">
<div class="w-12 h-12 rounded-full bg-gray-200 overflow-hidden flex items-center justify-center border border-gray-300">
<img v-if="form.avatar_path" :src="form.avatar_path" class="w-full h-full object-cover">
<span v-else class="text-xl">👤</span>
</div>
<input type="file" ref="fileInput" @change="handleFileUpload" class="hidden" accept="image/*">
<button @click="triggerUpload" class="text-sm bg-gray-100 hover:bg-gray-200 text-gray-700 py-1 px-3 rounded transition">
上传图片
</button>
</div>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 uppercase">姓名</label>
<input v-model="form.name" class="w-full p-2 border rounded">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 uppercase">角色/职业</label>
<input v-model="form.role" class="w-full p-2 border rounded">
</div>
<div>
<label class="block text-xs font-medium text-gray-500 uppercase">简介</label>
<textarea v-model="form.bio" class="w-full p-2 border rounded h-20"></textarea>
</div>
<div>
<label class="block text-xs font-medium text-gray-500 uppercase">说话风格</label>
<input v-model="form.speaking_style" class="w-full p-2 border rounded">
</div>
</div>
<div class="mt-4 pt-4 border-t border-gray-100">
<button @click="savePersona" class="w-full bg-green-600 hover:bg-green-700 text-white font-bold py-2 rounded-lg">
💾 保存到库
</button>
</div>
</div>
</div>
<div class="space-y-4">
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<h2 class="text-lg font-bold mb-2">📊 人格五大维度 (Big 5)</h2>
<div id="radarChart" class="w-full h-64"></div>
<div class="grid grid-cols-1 gap-2 mt-4">
<div v-for="(val, key) in form.traits" :key="key" class="flex items-center text-sm">
<span class="w-32 text-gray-600">${ traitLabels[key] || key }</span>
<input type="range" v-model.number="form.traits[key]" min="0" max="100" class="flex-1 mx-2" @input="updateChart">
<span class="w-8 text-right font-mono">${ val }</span>
</div>
</div>
</div>
</div>
</div>
<!-- Chat Mode -->
<div v-if="currentTab === 'chat'" class="h-full flex flex-col max-w-4xl mx-auto bg-white rounded-xl shadow-sm border border-gray-100 overflow-hidden">
<div class="p-4 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
<div class="flex items-center">
<div class="w-8 h-8 rounded-full bg-gray-200 overflow-hidden mr-3 border border-gray-300">
<img v-if="form.avatar_path" :src="form.avatar_path" class="w-full h-full object-cover">
<span v-else class="text-sm flex justify-center items-center h-full">👤</span>
</div>
<div>
<h2 class="font-bold text-lg">${ form.name || '未命名角色' }</h2>
<p class="text-xs text-gray-500">${ form.role || 'Role' }</p>
</div>
</div>
<button @click="clearChat" class="text-xs text-red-500 hover:text-red-700">清空对话</button>
</div>
<div class="flex-1 overflow-y-auto p-4 space-y-4 bg-slate-50" id="chatContainer">
<div v-if="chatHistory.length === 0" class="text-center text-gray-400 mt-10">
<p>👋 开始与 <b>${ form.name }</b> 对话吧!</p>
</div>
<div v-for="(msg, idx) in chatHistory" :key="idx" class="flex flex-col">
<div class="chat-bubble shadow-sm" :class="msg.role === 'user' ? 'user-bubble' : 'ai-bubble'">
<div class="font-bold text-xs opacity-70 mb-1 flex items-center justify-between">
<span>${ msg.role === 'user' ? '我' : form.name }</span>
<span class="text-[10px] ml-2 opacity-50">${ formatTime(msg.timestamp) }</span>
</div>
<div class="prose prose-sm max-w-none" v-html="renderMarkdown(msg.content)"></div>
</div>
</div>
<div v-if="isSending" class="flex items-center text-gray-500 text-sm ml-2">
<span class="animate-pulse">✍️ 正在输入...</span>
</div>
</div>
<div class="p-4 border-t border-gray-200 bg-white">
<div class="flex space-x-2">
<input v-model="inputMessage" @keyup.enter="sendMessage"
placeholder="输入消息..."
class="flex-1 p-3 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500">
<button @click="sendMessage" :disabled="isSending || !inputMessage.trim()"
class="bg-blue-600 text-white px-6 py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 transition">
发送
</button>
</div>
</div>
</div>
<!-- Analytics Tab Content -->
<div v-if="currentTab === 'analytics'" class="max-w-4xl mx-auto p-6 bg-white rounded-xl shadow-sm border border-gray-100">
<h2 class="text-lg font-bold mb-4">📊 数据分析</h2>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mb-8">
<div class="p-4 bg-blue-50 rounded-lg text-center">
<div class="text-3xl font-bold text-blue-600">${ savedPersonas.length }</div>
<div class="text-sm text-gray-600">已创建分身</div>
</div>
<div class="p-4 bg-green-50 rounded-lg text-center">
<div class="text-3xl font-bold text-green-600">${ chatHistory.length }</div>
<div class="text-sm text-gray-600">当前对话数</div>
</div>
<div class="p-4 bg-purple-50 rounded-lg text-center">
<div class="text-3xl font-bold text-purple-600">Mock</div>
<div class="text-sm text-gray-600">运行模式</div>
</div>
</div>
<div class="text-center text-gray-400 py-10">
更多高级分析功能开发中...
</div>
</div>
</div>
</div>
</div>
<script>
const { createApp, ref, reactive, onMounted, nextTick, watch } = Vue;
createApp({
delimiters: ['${', '}'],
setup() {
const tabs = [
{ id: 'create', name: '🏗️ 创造 (Studio)' },
{ id: 'chat', name: '💬 对话 (Chat)' },
{ id: 'analytics', name: '📈 分析 (Analytics)' }
];
const currentTab = ref('create');
const showMobileMenu = ref(false);
// Form Data
const generationPrompt = ref('');
const isGenerating = ref(false);
const form = reactive({
id: null,
name: 'Nova',
role: 'Virtual Assistant',
bio: 'An intelligent digital entity designed to assist with various tasks.',
speaking_style: 'Helpful and precise',
avatar_path: '',
traits: {
Openness: 70,
Conscientiousness: 80,
Extraversion: 50,
Agreeableness: 60,
Neuroticism: 20
}
});
const traitLabels = {
Openness: '开放性 (Openness)',
Conscientiousness: '尽责性 (Conscientiousness)',
Extraversion: '外向性 (Extraversion)',
Agreeableness: '宜人性 (Agreeableness)',
Neuroticism: '神经质 (Neuroticism)'
};
// Chat Data
const inputMessage = ref('');
const chatHistory = ref([]);
const isSending = ref(false);
const savedPersonas = ref([]);
const fileInput = ref(null);
// Charts
let chartInstance = null;
const renderMarkdown = (text) => {
return marked.parse(text);
};
const formatTime = (date) => {
if (!date) return new Date().toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
return new Date(date).toLocaleTimeString([], {hour: '2-digit', minute:'2-digit'});
};
const triggerUpload = () => {
fileInput.value.click();
};
const handleFileUpload = async (event) => {
const file = event.target.files[0];
if (!file) return;
// Simple validation
if (file.size > 16 * 1024 * 1024) {
alert("文件过大 (Max 16MB)");
return;
}
const formData = new FormData();
formData.append('file', file);
try {
const res = await fetch('/api/upload', {
method: 'POST',
body: formData
});
const data = await res.json();
if (data.status === 'success') {
form.avatar_path = data.path;
alert("上传成功");
} else {
alert(data.error || "上传失败");
}
} catch (e) {
console.error(e);
alert("上传出错");
}
};
const initChart = () => {
const el = document.getElementById('radarChart');
if (!el) return;
if (chartInstance) chartInstance.dispose();
chartInstance = echarts.init(el);
const option = {
radar: {
indicator: Object.keys(form.traits).map(k => ({ name: traitLabels[k] || k, max: 100 })),
splitArea: { areaStyle: { color: ['#f1f5f9', '#fff'] } }
},
series: [{
type: 'radar',
data: [{
value: Object.values(form.traits),
name: 'Personality Profile',
areaStyle: { color: 'rgba(59, 130, 246, 0.2)' },
lineStyle: { color: '#3b82f6' },
itemStyle: { color: '#3b82f6' }
}]
}]
};
chartInstance.setOption(option);
};
const updateChart = () => {
if (chartInstance) {
chartInstance.setOption({
series: [{
data: [{ value: Object.values(form.traits) }]
}]
});
}
};
const generatePersona = async () => {
if (!generationPrompt.value.trim()) return;
isGenerating.value = true;
try {
const res = await fetch('/api/generate_persona', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({ description: generationPrompt.value })
});
const data = await res.json();
if (data.status === 'success') {
Object.assign(form, data.data);
form.id = null; // New persona
updateChart();
}
} catch (e) {
console.error(e);
alert('生成失败,请重试');
} finally {
isGenerating.value = false;
}
};
const savePersona = async () => {
try {
const res = await fetch('/api/save_persona', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(form)
});
const data = await res.json();
if (data.status === 'success') {
alert('保存成功!');
fetchPersonas();
}
} catch (e) {
alert('保存失败');
}
};
const fetchPersonas = async () => {
const res = await fetch('/api/personas');
const data = await res.json();
if (data.status === 'success') {
savedPersonas.value = data.data;
}
};
const selectPersona = (p) => {
Object.assign(form, p);
chatHistory.value = []; // Clear chat when switching
currentTab.value = 'chat';
nextTick(() => updateChart());
};
const resetToCreate = () => {
currentTab.value = 'create';
form.id = null;
form.name = 'New Persona';
form.role = '';
form.bio = '';
form.avatar_path = '';
// Reset traits
Object.keys(form.traits).forEach(k => form.traits[k] = 50);
nextTick(() => updateChart());
};
const sendMessage = async () => {
if (!inputMessage.value.trim() || isSending.value) return;
const userMsg = { role: 'user', content: inputMessage.value };
chatHistory.value.push(userMsg);
const msgToSend = inputMessage.value;
inputMessage.value = '';
isSending.value = true;
// Scroll to bottom
nextTick(() => {
const container = document.getElementById('chatContainer');
if (container) container.scrollTop = container.scrollHeight;
});
try {
const res = await fetch('/api/chat', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
message: msgToSend,
history: chatHistory.value,
persona: form
})
});
const data = await res.json();
chatHistory.value.push({ role: 'assistant', content: data.response });
} catch (e) {
chatHistory.value.push({ role: 'assistant', content: '⚠️ Connection Error' });
} finally {
isSending.value = false;
nextTick(() => {
const container = document.getElementById('chatContainer');
if (container) container.scrollTop = container.scrollHeight;
});
}
};
const clearChat = () => {
chatHistory.value = [];
};
// Watch for tab changes to render chart
watch(currentTab, (newTab) => {
if (newTab === 'create') {
nextTick(() => initChart());
}
});
onMounted(() => {
fetchPersonas();
initChart();
window.addEventListener('resize', () => chartInstance && chartInstance.resize());
});
return {
tabs, currentTab, showMobileMenu,
generationPrompt, isGenerating, generatePersona,
form, traitLabels, updateChart,
savePersona, savedPersonas, selectPersona, resetToCreate,
inputMessage, chatHistory, isSending, sendMessage, clearChat,
renderMarkdown, fileInput, triggerUpload, handleFileUpload,
formatTime
};
}
}).mount('#app');
</script>
</body>
</html>