feat: AI context, Webhooks, Kanban drag-drop, and UI fixes
This commit is contained in:
@@ -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) => {
|
||||
|
||||
@@ -21,6 +21,17 @@
|
||||
<div v-if="type === 'task' && data.priority" :class="priorityBadgeClass">
|
||||
{{ priorityLabel }}
|
||||
</div>
|
||||
<!-- Кнопка запуска для спец-узлов -->
|
||||
<button
|
||||
v-if="['ai', 'webhook'].includes(type)"
|
||||
@click.stop="handleExecute"
|
||||
:disabled="isExecuting"
|
||||
class="ml-auto p-1.5 hover:bg-slate-700 rounded-md transition-colors group relative"
|
||||
title="Запустить выполнение"
|
||||
>
|
||||
<span v-if="isExecuting" class="block animate-spin text-blue-400">🌀</span>
|
||||
<span v-else class="block group-hover:scale-110 transition-transform">▶️</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Content preview for measurement -->
|
||||
@@ -69,7 +80,19 @@ const props = defineProps({
|
||||
selected: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['update'])
|
||||
const emit = defineEmits(['update', 'execute'])
|
||||
const isExecuting = ref(false)
|
||||
|
||||
const handleExecute = async () => {
|
||||
if (isExecuting.value) return
|
||||
isExecuting.value = true
|
||||
try {
|
||||
emit('execute', { id: props.id, type: props.type })
|
||||
} finally {
|
||||
// Состояние загрузки сбрасывается внешним событием или по таймауту для безопасности
|
||||
setTimeout(() => { isExecuting.value = false }, 10000)
|
||||
}
|
||||
}
|
||||
|
||||
// Root element ref
|
||||
const rootEl = ref(null)
|
||||
|
||||
Reference in New Issue
Block a user