Spaces:
Sleeping
Sleeping
| <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> | |