feat: AI context, Webhooks, Kanban drag-drop, and UI fixes

This commit is contained in:
2026-04-29 17:43:18 +05:00
parent 89b3340253
commit bc2f99408d
29 changed files with 991 additions and 52 deletions
+67 -20
View File
@@ -55,19 +55,19 @@
<Controls />
<MiniMap pannable zoomable class="bg-slate-800 border-slate-700" />
<template #node-text="{ data, id }">
<FlowNodeRenderer :id="id" :data="data" type="text" :selected="selectedNode?.id === id" @update="updateNodeData" />
<FlowNodeRenderer :id="id" :data="data" type="text" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
</template>
<template #node-file="{ data, id }">
<FlowNodeRenderer :id="id" :data="data" type="file" :selected="selectedNode?.id === id" @update="updateNodeData" />
<FlowNodeRenderer :id="id" :data="data" type="file" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
</template>
<template #node-task="{ data, id }">
<FlowNodeRenderer :id="id" :data="data" type="task" :selected="selectedNode?.id === id" @update="updateNodeData" />
<FlowNodeRenderer :id="id" :data="data" type="task" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
</template>
<template #node-ai="{ data, id }">
<FlowNodeRenderer :id="id" :data="data" type="ai" :selected="selectedNode?.id === id" @update="updateNodeData" />
<FlowNodeRenderer :id="id" :data="data" type="ai" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
</template>
<template #node-webhook="{ data, id }">
<FlowNodeRenderer :id="id" :data="data" type="webhook" :selected="selectedNode?.id === id" @update="updateNodeData" />
<FlowNodeRenderer :id="id" :data="data" type="webhook" :selected="selectedNode?.id === id" @update="updateNodeData" @execute="executeNode" />
</template>
<template #node-git="{ data, id }">
<FlowNodeRenderer :id="id" :data="data" type="git" :selected="selectedNode?.id === id" @update="updateNodeData" />
@@ -134,6 +134,21 @@
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Дедлайн (Срок)</label>
<input v-model="selectedNode.data.alarm" @blur="commitNodeChange" type="datetime-local" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
</div>
<div v-if="selectedNode.type === 'webhook'">
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Webhook URL</label>
<input v-model="selectedNode.data.url" @blur="commitNodeChange" placeholder="https://api.example.com/..." class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
</div>
<div v-if="selectedNode.type === 'webhook'">
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Метод</label>
<select v-model="selectedNode.data.method" @change="commitNodeChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200">
<option value="GET">GET</option>
<option value="POST">POST</option>
</select>
</div>
<div v-if="selectedNode.type === 'ai'">
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">AI Промпт (Инструкция)</label>
<textarea v-model="selectedNode.data.prompt" @blur="commitNodeChange" rows="3" placeholder="Что должен сделать ИИ? (например: Отредактируй этот текст...)" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none resize-none font-mono"></textarea>
</div>
<div v-if="selectedNode.data.script_id !== undefined">
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Скрипт</label>
<input v-model="selectedNode.data.script_id" @blur="commitNodeChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none font-mono" />
@@ -168,6 +183,7 @@
</div>
</template>
<!-- Force rebuild to fix Vite 404 issue -->
<script setup>
import { VueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
@@ -429,34 +445,65 @@ const onNodeDoubleClick = (event) => {
const node = event.node
// 1. Cross-Canvas Navigation Logic
// Check if it's a file node or has a URL/file reference to another canvas
const targetId = node.data?.file || node.data?.url || ''
if (targetId && (node.type === 'file' || targetId.includes('.canvas') || targetId.startsWith('/canvas/'))) {
// Clean up ID (e.g. "project.canvas" -> "project", "/canvas/project" -> "project")
const cleanId = targetId.replace('.canvas', '').replace('/canvas/', '')
console.log('[Navigation] Opening cross-linked canvas:', cleanId)
navigateTo(`/canvas/${cleanId}`)
return // Stop further processing
return
} else if (node.type === 'file' && !targetId) {
alert('У этого узла еще нет ссылки на другую карту. Выберите карту в панели свойств (справа).')
return
}
// 2. Task Toggle Logic (fallback)
const currentState = node.data?.state || 'todo'
// Quick toggle workflow: todo -> in_progress -> review -> completed -> todo
const nextStateMap = {
'todo': 'in_progress',
'in_progress': 'review',
'review': 'completed',
'completed': 'todo',
'blocked': 'todo'
if (node.type === 'task') {
const currentState = node.data?.state || 'todo'
const nextStateMap = {
'todo': 'in_progress',
'in_progress': 'review',
'review': 'completed',
'completed': 'todo',
'blocked': 'todo'
}
const nextState = nextStateMap[currentState] || 'todo'
updateNodeData({ id: node.id, data: { state: nextState } })
}
}
const executeNode = async ({ id, type }) => {
console.log(`[FlowEditor] Executing node ${id} of type ${type}`)
const nextState = nextStateMap[currentState] || 'todo'
updateNodeData({ id: node.id, data: { state: nextState } })
// Collect context from incoming nodes
const incomingEdges = elements.value.filter(el => el.target === id)
const contextNodes = incomingEdges.map(edge => {
const sourceNode = elements.value.find(el => el.id === edge.source)
return {
id: sourceNode?.id,
type: sourceNode?.type,
text: sourceNode?.data?.text || sourceNode?.data?.label || ''
}
}).filter(n => n.text)
try {
const res = await $fetch(`/api/nodes/${id}/execute`, {
method: 'POST',
body: {
canvasId: props.canvasId,
context: contextNodes
},
headers: authHeaders.value
})
if (res.success) {
console.log(`[FlowEditor] Execution successful:`, res.result)
// Перезагружаем данные, чтобы увидеть результат
await loadData()
}
} catch (err) {
console.error(`[FlowEditor] Execution failed:`, err)
alert('Ошибка при выполнении узла: ' + (err.message || 'Неизвестная ошибка'))
}
}
const onConnect = (connection) => {