Trae Assistant
Fix Vue template syntax error in variable insertion helper
5a1c60c
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LogicStream - 智能业务决策编排引擎</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>
<link href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css" rel="stylesheet">
<style>
body { font-family: 'Inter', system-ui, -apple-system, sans-serif; background-color: #f3f4f6; color: #1f2937; }
.fade-enter-active, .fade-leave-active { transition: opacity 0.3s ease; }
.fade-enter-from, .fade-leave-to { opacity: 0; }
/* Custom scrollbar */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: #f1f1f1; }
::-webkit-scrollbar-thumb { background: #c1c1c1; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #a8a8a8; }
</style>
</head>
<body>
<div id="app" class="h-screen flex flex-col overflow-hidden">
<!-- Header -->
<header class="bg-white border-b border-gray-200 h-16 flex items-center justify-between px-6 shadow-sm z-10">
<div class="flex items-center space-x-3">
<div class="w-8 h-8 bg-blue-600 rounded-lg flex items-center justify-center text-white font-bold">L</div>
<h1 class="text-xl font-bold text-gray-800">LogicStream <span class="text-xs font-normal text-gray-500 ml-2">智能业务编排</span></h1>
</div>
<div class="flex items-center space-x-4">
<button @click="currentView = 'dashboard'" :class="{'text-blue-600 font-semibold': currentView === 'dashboard', 'text-gray-500': currentView !== 'dashboard'}" class="hover:text-blue-600 transition">概览</button>
<button @click="currentView = 'designer'" :class="{'text-blue-600 font-semibold': currentView === 'designer', 'text-gray-500': currentView !== 'designer'}" class="hover:text-blue-600 transition">工作流设计</button>
<div class="h-4 w-px bg-gray-300"></div>
<button class="text-gray-500 hover:text-blue-600"><i class="fas fa-user-circle text-xl"></i></button>
</div>
</header>
<!-- Main Content -->
<div class="flex-1 flex overflow-hidden">
<!-- Sidebar (Workflow List) -->
<aside class="w-64 bg-white border-r border-gray-200 flex flex-col" v-if="currentView === 'designer'">
<div class="p-4 border-b border-gray-200 flex justify-between items-center">
<h2 class="font-semibold text-gray-700">我的工作流</h2>
<button @click="createNewWorkflow" class="text-blue-600 hover:bg-blue-50 p-1 rounded"><i class="fas fa-plus"></i></button>
</div>
<div class="flex-1 overflow-y-auto p-2 space-y-2">
<div v-for="wf in workflows" :key="wf.id"
@click="selectWorkflow(wf)"
:class="{'bg-blue-50 border-blue-200': currentWorkflow && currentWorkflow.id === wf.id, 'hover:bg-gray-50 border-transparent': !currentWorkflow || currentWorkflow.id !== wf.id}"
class="p-3 rounded-lg border cursor-pointer transition group relative">
<div class="font-medium text-sm text-gray-800 truncate">${ wf.name }</div>
<div class="text-xs text-gray-500 truncate mt-1">${ wf.description || '无描述' }</div>
<button @click.stop="deleteWorkflow(wf.id)" class="absolute right-2 top-3 text-gray-400 hover:text-red-500 opacity-0 group-hover:opacity-100 transition"><i class="fas fa-trash-alt"></i></button>
</div>
</div>
</aside>
<!-- Workspace -->
<main class="flex-1 bg-gray-50 relative flex flex-col overflow-hidden">
<!-- Dashboard View -->
<div v-if="currentView === 'dashboard'" class="p-8 overflow-y-auto h-full">
<div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-8">
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<div class="text-gray-500 text-sm mb-1">总工作流数</div>
<div class="text-3xl font-bold text-gray-800">${ workflows.length }</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<div class="text-gray-500 text-sm mb-1">今日运行次数</div>
<div class="text-3xl font-bold text-blue-600">12</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100">
<div class="text-gray-500 text-sm mb-1">平均成功率</div>
<div class="text-3xl font-bold text-green-600">98.5%</div>
</div>
</div>
<div class="bg-white p-6 rounded-xl shadow-sm border border-gray-100 mb-8 h-80">
<h3 class="text-lg font-semibold mb-4 text-gray-700">调用趋势 (Mock Data)</h3>
<div id="chart-container" class="w-full h-full"></div>
</div>
</div>
<!-- Designer View -->
<div v-else-if="currentView === 'designer' && currentWorkflow" class="flex flex-1 overflow-hidden">
<!-- Canvas Area -->
<div class="flex-1 flex flex-col h-full">
<!-- Toolbar -->
<div class="bg-white border-b border-gray-200 p-3 flex justify-between items-center">
<div class="flex items-center space-x-2">
<input v-model="currentWorkflow.name" class="font-bold text-lg bg-transparent border-b border-transparent hover:border-gray-300 focus:border-blue-500 focus:outline-none px-1" />
<span class="text-xs text-gray-400 px-2 py-1 bg-gray-100 rounded">上次保存: ${ formatTime(currentWorkflow.updated_at) }</span>
</div>
<div class="flex space-x-2">
<button @click="saveWorkflow" class="px-3 py-1.5 bg-white border border-gray-300 text-gray-700 rounded hover:bg-gray-50 text-sm"><i class="fas fa-save mr-1"></i> 保存</button>
<button @click="runWorkflow" class="px-3 py-1.5 bg-blue-600 text-white rounded hover:bg-blue-700 text-sm shadow-sm flex items-center">
<i class="fas fa-play mr-1" :class="{'fa-spin': isRunning}"></i> ${ isRunning ? '运行中...' : '运行' }
</button>
</div>
</div>
<!-- Workflow Steps Editor -->
<div class="flex-1 overflow-y-auto p-6 space-y-4">
<div v-if="!currentWorkflow.steps || currentWorkflow.steps.length === 0" class="text-center py-20 text-gray-400 border-2 border-dashed border-gray-300 rounded-xl">
<p>暂无步骤,点击下方按钮添加</p>
</div>
<div v-for="(step, index) in currentWorkflow.steps" :key="step.id" class="bg-white rounded-lg shadow-sm border border-gray-200 p-4 relative transition hover:shadow-md">
<div class="absolute -left-3 top-1/2 transform -translate-y-1/2 w-6 h-6 bg-blue-100 text-blue-600 rounded-full flex items-center justify-center text-xs font-bold border border-blue-200 z-10">
${ index + 1 }
</div>
<div class="flex justify-between items-start mb-3">
<div class="flex items-center space-x-2">
<span class="px-2 py-0.5 rounded text-xs font-medium uppercase tracking-wide"
:class="{
'bg-green-100 text-green-700': step.type === 'input',
'bg-purple-100 text-purple-700': step.type === 'llm'
}">
${ step.type }
</span>
<input v-model="step.name" class="font-semibold text-gray-700 bg-transparent focus:outline-none border-b border-transparent focus:border-blue-300" placeholder="步骤名称" />
<span class="text-xs text-gray-400 font-mono">ID: ${ step.id }</span>
</div>
<button @click="removeStep(index)" class="text-gray-400 hover:text-red-500"><i class="fas fa-times"></i></button>
</div>
<!-- Step Content -->
<div v-if="step.type === 'input'" class="space-y-2">
<label class="block text-xs font-medium text-gray-500">输入内容 (作为初始上下文)</label>
<textarea v-model="step.content" rows="2" class="w-full text-sm p-2 border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none"></textarea>
</div>
<div v-if="step.type === 'llm'" class="space-y-2">
<label class="block text-xs font-medium text-gray-500">提示词 (Prompt)</label>
<div class="relative">
<textarea v-model="step.prompt" rows="3" class="w-full text-sm p-2 border border-gray-300 rounded focus:ring-1 focus:ring-blue-500 focus:border-blue-500 outline-none resize-none font-mono text-gray-600"></textarea>
<div class="text-xs text-gray-400 mt-1">可用变量: <span v-for="(s, i) in currentWorkflow.steps" :key="s.id" v-show="i < index" class="mr-1 bg-gray-100 px-1 rounded cursor-pointer hover:bg-gray-200" @click="insertVar(step, s.id)">${ getVarLabel(s.id) }</span></div>
</div>
</div>
<!-- Result Preview -->
<div v-if="runResults && runResults[step.id]" class="mt-3 pt-3 border-t border-gray-100 bg-gray-50 -mx-4 -mb-4 px-4 py-3 rounded-b-lg">
<div class="text-xs font-bold text-gray-500 mb-1 flex justify-between">
<span>运行结果</span>
<span class="text-green-600"><i class="fas fa-check-circle"></i> 完成</span>
</div>
<pre class="text-xs text-gray-700 whitespace-pre-wrap overflow-x-auto max-h-40 font-mono bg-white p-2 border border-gray-200 rounded">${ runResults[step.id] }</pre>
</div>
</div>
<!-- Add Step Buttons -->
<div class="flex justify-center space-x-3 pt-4 pb-10">
<button @click="addStep('input')" class="flex items-center space-x-1 px-4 py-2 bg-white border border-gray-300 shadow-sm rounded-full text-sm text-gray-600 hover:bg-gray-50 hover:text-blue-600 transition">
<i class="fas fa-keyboard"></i> <span>添加输入</span>
</button>
<button @click="addStep('llm')" class="flex items-center space-x-1 px-4 py-2 bg-white border border-gray-300 shadow-sm rounded-full text-sm text-gray-600 hover:bg-gray-50 hover:text-purple-600 transition">
<i class="fas fa-robot"></i> <span>添加 AI 分析</span>
</button>
</div>
</div>
</div>
<!-- Right Panel: Logs -->
<div class="w-80 bg-white border-l border-gray-200 flex flex-col h-full shadow-lg z-20" v-show="showLogs">
<div class="p-3 border-b border-gray-200 bg-gray-50 flex justify-between items-center">
<h3 class="font-semibold text-gray-700">运行日志</h3>
<button @click="logs = []" class="text-xs text-gray-500 hover:text-gray-800">清空</button>
</div>
<div class="flex-1 overflow-y-auto p-3 space-y-2 font-mono text-xs">
<div v-if="logs.length === 0" class="text-gray-400 text-center py-10">等待运行...</div>
<div v-for="(log, idx) in logs" :key="idx" class="p-2 rounded bg-gray-50 border-l-2"
:class="{'border-blue-500': log.level === 'INFO', 'border-green-500': log.level === 'SUCCESS', 'border-red-500': log.level === 'ERROR'}">
<div class="text-gray-400 text-[10px] mb-0.5">${ log.timestamp.split('T')[1].split('.')[0] }</div>
<div class="text-gray-800 break-words">${ log.message }</div>
</div>
</div>
</div>
</div>
<div v-else class="flex-1 flex items-center justify-center text-gray-400">
<div class="text-center">
<i class="fas fa-project-diagram text-6xl mb-4 text-gray-200"></i>
<p>请选择或创建一个工作流</p>
</div>
</div>
</main>
</div>
</div>
<script>
{% raw %}
const { createApp, ref, onMounted, nextTick } = Vue;
createApp({
delimiters: ['${', '}'],
setup() {
const currentView = ref('dashboard');
const workflows = ref([]);
const currentWorkflow = ref(null);
const isRunning = ref(false);
const logs = ref([]);
const showLogs = ref(true);
const runResults = ref({});
const fetchWorkflows = async () => {
try {
const res = await fetch('/api/workflows');
workflows.value = await res.json();
} catch (e) {
console.error("Failed to fetch workflows", e);
}
};
const createNewWorkflow = async () => {
const newWf = {
name: "未命名工作流 " + (workflows.value.length + 1),
description: "新建的业务逻辑流",
steps: []
};
try {
const res = await fetch('/api/workflows', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(newWf)
});
const data = await res.json();
await fetchWorkflows();
// Select the new one
const created = workflows.value.find(w => w.id === data.id);
if (created) selectWorkflow(created);
} catch (e) {
alert("创建失败: " + e.message);
}
};
const selectWorkflow = (wf) => {
// Deep copy to avoid direct mutation issues before save
currentWorkflow.value = JSON.parse(JSON.stringify(wf));
// Ensure steps is array
if (typeof currentWorkflow.value.steps === 'string') {
try {
currentWorkflow.value.steps = JSON.parse(currentWorkflow.value.steps);
} catch(e) {
currentWorkflow.value.steps = [];
}
}
if (!currentWorkflow.value.steps) currentWorkflow.value.steps = [];
currentView.value = 'designer';
runResults.value = {};
logs.value = [];
};
const saveWorkflow = async () => {
if (!currentWorkflow.value) return;
try {
await fetch(`/api/workflows/${currentWorkflow.value.id}`, {
method: 'PUT',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(currentWorkflow.value)
});
await fetchWorkflows(); // Refresh list
alert("保存成功");
} catch (e) {
alert("保存失败: " + e.message);
}
};
const deleteWorkflow = async (id) => {
if (!confirm("确定要删除吗?")) return;
try {
await fetch(`/api/workflows/${id}`, { method: 'DELETE' });
await fetchWorkflows();
if (currentWorkflow.value && currentWorkflow.value.id === id) {
currentWorkflow.value = null;
}
} catch (e) {
alert("删除失败");
}
};
const addStep = (type) => {
if (!currentWorkflow.value) return;
const id = `step_${currentWorkflow.value.steps.length + 1}`;
currentWorkflow.value.steps.push({
id: id,
type: type,
name: type === 'input' ? '输入数据' : 'AI 处理',
content: '',
prompt: '',
output: ''
});
};
const removeStep = (index) => {
currentWorkflow.value.steps.splice(index, 1);
};
const runWorkflow = async () => {
if (!currentWorkflow.value) return;
isRunning.value = true;
logs.value = [];
runResults.value = {};
// Optimistic UI: save first
await saveWorkflow();
try {
const res = await fetch('/api/run_workflow', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify({
workflow_id: currentWorkflow.value.id,
steps: currentWorkflow.value.steps
})
});
const data = await res.json();
logs.value = data.logs;
runResults.value = data.results;
if (data.status === 'success') {
// Update local steps with output
// Not strictly necessary as results are in runResults, but good for persistence logic later
} else {
logs.value.push({timestamp: new Date().toISOString(), level: 'ERROR', message: "Run returned error status"});
}
} catch (e) {
logs.value.push({timestamp: new Date().toISOString(), level: 'CRITICAL', message: "Network error: " + e.message});
} finally {
isRunning.value = false;
}
};
const insertVar = (step, varName) => {
step.prompt += `{{${varName}.output}}`;
};
const getVarLabel = (id) => {
return `{{${id}.output}}`;
};
const formatTime = (t) => {
if (!t) return '';
return new Date(t).toLocaleString('zh-CN');
};
// Chart Init
const initChart = () => {
const chartDom = document.getElementById('chart-container');
if (!chartDom) return;
const myChart = echarts.init(chartDom);
const option = {
tooltip: { trigger: 'axis' },
grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true },
xAxis: { type: 'category', boundaryGap: false, data: ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun'] },
yAxis: { type: 'value' },
series: [
{
name: 'Executions',
type: 'line',
stack: 'Total',
smooth: true,
areaStyle: { color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [{offset: 0, color: '#3B82F6'}, {offset: 1, color: '#EFF6FF'}]) },
itemStyle: { color: '#3B82F6' },
data: [120, 132, 101, 134, 90, 230, 210]
}
]
};
myChart.setOption(option);
window.addEventListener('resize', () => myChart.resize());
};
onMounted(() => {
fetchWorkflows();
// Delay chart init slightly to ensure DOM is ready if starting on dashboard
setTimeout(initChart, 500);
});
return {
currentView,
workflows,
currentWorkflow,
isRunning,
logs,
showLogs,
runResults,
fetchWorkflows,
createNewWorkflow,
selectWorkflow,
saveWorkflow,
deleteWorkflow,
addStep,
removeStep,
runWorkflow,
insertVar,
getVarLabel,
formatTime
};
}
}).mount('#app');
{% endraw %}
</script>
</body>
</html>