Files
Avtex/app/components/FlowEditor.vue
T

1875 lines
63 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<div class="h-screen w-full flex bg-slate-950">
<!-- Node Palette -->
<NodePalette :user-role="userRole" />
<!-- Canvas Area -->
<div class="flex-1 flex flex-col relative min-w-0">
<!-- Toolbar -->
<div class="h-10 bg-slate-900 border-b border-slate-700 flex items-center px-3 gap-2 z-20">
<input v-model="canvasName" @change="saveCanvas" :disabled="userRole === 'viewer'" class="text-sm font-semibold text-slate-300 mr-2 bg-transparent border border-transparent hover:border-slate-600 focus:border-blue-500 focus:bg-slate-800 outline-none rounded px-2 py-1 w-48 transition" title="Переименовать карту" />
<!-- Group Selection -->
<div v-if="userRole !== 'viewer'" class="flex items-center gap-1 bg-slate-800 border border-slate-700 rounded px-2 py-1">
<span class="text-[10px] text-slate-500 uppercase font-bold mr-1">Группа:</span>
<select v-model="selectedGroupId" @change="saveCanvas" class="bg-transparent text-xs text-slate-300 outline-none cursor-pointer hover:text-white transition">
<option :value="null">Личная (нет)</option>
<option v-for="g in groupsList" :key="g.id" :value="g.id">
👥 {{ g.name }}
</option>
</select>
</div>
<span v-if="isDirty" class="px-2 py-1 bg-yellow-600 text-yellow-100 rounded text-xs font-medium">
● Несохранено
</span>
<template v-if="userRole !== 'viewer'">
<button @click="addNodeAtCenter" class="px-2 py-1 bg-blue-600 hover:bg-blue-500 text-white rounded text-xs">+ Узел</button>
<button @click="createSmartNodeManual" class="px-2 py-1 bg-indigo-600 hover:bg-indigo-500 text-white rounded text-xs flex items-center gap-1 transition" title="Создать цель S.M.A.R.T">
<span>🎯 +SMART</span>
</button>
<button @click="createGrowChainManual" class="px-2 py-1 bg-teal-650 hover:bg-teal-600 text-white rounded text-xs flex items-center gap-1 transition" title="Создать цепочку G.R.O.W.">
<span>🌱 +GROW</span>
</button>
<button @click="createDiltsChainManual" class="px-2 py-1 bg-violet-600 hover:bg-violet-500 text-white rounded text-xs flex items-center gap-1 transition" title="Создать логические уровни Дилтса">
<span>📐 +Дилтс</span>
</button>
<button @click="createEisenhowerMatrixManual" class="px-2 py-1 bg-amber-600 hover:bg-amber-500 text-white rounded text-xs flex items-center gap-1 transition" title="Создать матрицу Эйзенхауэра">
<span>📊 +Эйзенхауэр</span>
</button>
<button @click="createGoalsTemplateManual" class="px-2 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs flex items-center gap-1 transition" title="Создать карту целей на год (Практическая работа)">
<span>📋 +Цели на Год</span>
</button>
<button @click="showWizardModal = true" class="px-2 py-1 bg-violet-700 hover:bg-violet-650 text-white rounded text-xs flex items-center gap-1 transition" title="Открыть мастер SMART, GROW, Дилтса и Эйзенхауэра с наводящими вопросами">
<span>🧙‍♂️ Конструктор</span>
</button>
<button @click="saveCanvas" class="px-2 py-1 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-xs">Сохранить</button>
<button v-if="userRole === 'owner' || userRole === 'admin'" @click="deleteCanvas" class="p-2 text-slate-400 hover:text-red-400 hover:bg-red-400/10 rounded-lg transition" title="Удалить карту">
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
</svg>
</button>
<button @click="deleteSelected" :disabled="!selectedNode" class="px-2 py-1 bg-red-600 hover:bg-red-500 disabled:bg-slate-700 disabled:text-slate-500 text-white rounded text-xs">Удалить</button>
</template>
<button @click="exportObsidian" class="px-2 py-1 bg-purple-600 hover:bg-purple-500 text-white rounded text-xs">Экспорт</button>
<div class="flex-1"></div>
<button v-if="userRole !== 'viewer'" @click="manualGitSync" :disabled="gitSync.isSyncing.value"
class="px-2 py-1 bg-orange-600 hover:bg-orange-500 disabled:bg-slate-700 disabled:text-slate-500 text-white rounded text-xs flex items-center gap-1">
<span>{{ gitSync.syncStatusIcon.value }}</span>
Git
</button>
<button @click="fitView" class="px-2 py-1 bg-slate-700 hover:bg-slate-600 text-slate-200 rounded text-xs">Расположить</button>
</div>
<!-- Vue Flow Canvas -->
<div class="flex-1 relative w-full h-full" @drop="onDrop" @dragover.prevent>
<VueFlow v-model="elements" :default-zoom="0.8" :min-zoom="0.2" :max-zoom="2" fit-view-on-init
:nodes-draggable="userRole !== 'viewer'"
:nodes-connectable="userRole !== 'viewer'"
:elements-selectable="true"
@node-double-click="onNodeDoubleClick" @node-click="onNodeClick" @pane-click="onPaneClick" @connect="onConnect"
@node-drag-start="onNodeDragStart" @node-drag="onNodeDrag" @node-drag-stop="onNodeDragStop"
class="bg-slate-950 w-full h-full">
<Background pattern-color="#334155" :gap="20" />
<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" @execute="executeNode" />
</template>
<template #node-file="{ data, id }">
<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" @execute="executeNode" />
</template>
<template #node-ai="{ data, id }">
<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" @execute="executeNode" />
</template>
<template #node-git="{ data, id }">
<FlowNodeRenderer :id="id" :data="data" type="git" :selected="selectedNode?.id === id" @update="updateNodeData" />
</template>
</VueFlow>
</div>
</div>
<NodePropertiesPanel
:node="selectedNode"
:users-list="usersList"
:canvases-list="canvasesList"
:is-coaching="isCoaching"
:current-coach-action="currentCoachAction"
@change="commitNodeChange"
@apply-smart-ai="applySmartAI"
@apply-grow-ai="applyGrowAI"
@apply-dilts-ai="applyDiltsAI"
@apply-eisenhower-ai="applyEisenhowerAI"
/>
<!-- Guided Wizard Modal -->
<CoachingWizardModal
ref="wizardModalRef"
:show="showWizardModal"
:is-submitting="isWizardSubmitting"
@close="showWizardModal = false"
@submit-smart-manual="handleSmartManual"
@submit-smart-ai="handleSmartAI"
@submit-grow-manual="handleGrowManual"
@submit-grow-ai="handleGrowAI"
@submit-dilts-manual="handleDiltsManual"
@submit-dilts-ai="handleDiltsAI"
@submit-eisenhower-manual="handleEisenhowerManual"
@submit-eisenhower-ai="handleEisenhowerAI"
/>
<!-- Gantt Chart Timeline View -->
<GanttChartDrawer :elements="elements" @select-node="selectAndCenterNode" />
</div>
</template>
<!-- Force rebuild to fix Vite 404 issue -->
<script setup>
import { VueFlow, useVueFlow } from '@vue-flow/core'
import { Background } from '@vue-flow/background'
import { Controls } from '@vue-flow/controls'
import { MiniMap } from '@vue-flow/minimap'
import '@vue-flow/core/dist/style.css'
import '@vue-flow/core/dist/theme-default.css'
import NodePalette from './editor/NodePalette.vue'
import NodePropertiesPanel from './editor/NodePropertiesPanel.vue'
import CoachingWizardModal from './editor/CoachingWizardModal.vue'
import GanttChartDrawer from './editor/GanttChartDrawer.vue'
import { transformToFlowElements, transformToObsidianCanvas } from '~/utils/canvasTransformer'
const props = defineProps({
canvasId: { type: String, default: 'demo' }
})
const { loadCanvas, updateNodeState: apiUpdateNodeState } = useCanvas()
const gitSync = useGitSync()
const { isDirty, trackChanges, markAsSaved } = useUnsavedChanges()
const auth = useAuth()
const authHeaders = computed(() => ({ Authorization: `Bearer ${auth.token.value}` }))
const elements = ref([])
const selectedNode = ref(null)
const canvasName = ref(props.canvasId)
const selectedGroupId = ref(null)
const groupsList = ref([])
const userRole = ref('viewer')
const { findNode, setTransform } = useVueFlow()
const selectAndCenterNode = (id) => {
const node = findNode(id)
if (node) {
selectedNode.value = node
const x = -node.position.x + window.innerWidth / 2 - 160
const y = -node.position.y + window.innerHeight / 2 - 100
setTransform({ x, y, zoom: 0.85 })
}
}
const nodeTypes = [
{ type: 'text', name: 'Текст', icon: '📝', description: 'Markdown заметка' },
{ type: 'file', name: 'Файл', icon: '📄', description: 'Ссылка на файл' },
{ type: 'task', name: 'Задача', icon: '✅', description: 'Задача с состоянием' },
{ type: 'ai', name: 'AI Узел', icon: '🤖', description: 'LLM запрос' },
{ type: 'webhook', name: 'Webhook', icon: '🔗', description: 'HTTP триггер' },
{ type: 'git', name: 'Git Синхр', icon: '🔄', description: 'Git синхронизация' }
]
// Load canvas from API
const loadData = async () => {
const data = await loadCanvas(props.canvasId)
elements.value = transformToFlowElements(data.canvas)
canvasName.value = data.canvas.name || props.canvasId
selectedGroupId.value = data.canvas.groupId || null
userRole.value = data.role || 'viewer'
// Mark initial state as saved
markAsSaved(data.canvas)
}
// Transformer and drag start are handled in imported components and canvasTransformer utility
const onDrop = (event) => {
const data = JSON.parse(event.dataTransfer.getData('application/vueflow'))
if (!data) return
const rect = event.currentTarget.getBoundingClientRect()
const x = event.clientX - rect.left
const y = event.clientY - rect.top
createNode(data.type, x, y)
}
// Create new node
const createNode = (type, x, y) => {
const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const nodeData = {
id,
type,
position: { x: x - 150, y: y - 100 },
data: {
width: 300,
height: 200,
label: `${type.charAt(0).toUpperCase() + type.slice(1)} Node`
}
}
// Add type-specific defaults
switch (type) {
case 'text':
nodeData.data.text = '# New Note\n\nEnter your text here'
break
case 'file':
nodeData.data.file = 'docs/file.md'
break
case 'task':
nodeData.data.state = 'todo'
nodeData.data.priority = 'medium'
nodeData.data.assigned_to = ''
nodeData.data.alarm = ''
nodeData.data.script_id = ''
break
case 'ai':
nodeData.data.prompt = 'Analyze this content'
break
case 'webhook':
nodeData.data.url = 'https://api.example.com'
nodeData.data.method = 'POST'
break
case 'git':
nodeData.data.repo = 'xflow-maps'
nodeData.data.branch = 'main'
nodeData.data.message = 'Update mind map'
break
}
elements.value.push(nodeData)
}
// Node selection and editing
const onNodeClick = ({ node }) => {
selectedNode.value = node
}
const onPaneClick = () => {
selectedNode.value = null
}
const updateNodeData = ({ id, data }) => {
const node = elements.value.find(el => el.id === id)
if (node) {
node.data = { ...node.data, ...data }
}
}
const commitNodeChange = () => {
if (selectedNode.value) {
// Trigger reactivity
const idx = elements.value.findIndex(el => el.id === selectedNode.value.id)
if (idx !== -1) {
elements.value[idx].data = { ...selectedNode.value.data }
elements.value[idx].style = { ...selectedNode.value.style }
elements.value[idx].type = selectedNode.value.type
elements.value.splice(idx, 1, { ...elements.value[idx] })
}
// Track changes
trackChanges(transformToObsidianCanvas(elements.value))
}
}
// Node actions
const addNodeAtCenter = () => {
createNode('text', 400, 300)
}
const deleteSelected = () => {
if (selectedNode.value) {
elements.value = elements.value.filter(el => el.id !== selectedNode.value.id)
selectedNode.value = null
}
}
const onNodeDragStart = (_event) => {}
const onNodeDrag = (_event) => {}
const onNodeDragStop = (event) => {
const node = event.node
// Update node data position
if (node.data) {
node.data.x = node.position.x
node.data.y = node.position.y
}
// Track changes
trackChanges(transformToObsidianCanvas(elements.value))
console.log('Node drag stopped:', node.id, 'at position:', node.position)
}
const onNodeDoubleClick = (event) => {
const node = event.node
// 1. Cross-Canvas Navigation Logic
const targetId = node.data?.file || node.data?.url || ''
if (targetId && (node.type === 'file' || targetId.includes('.canvas') || targetId.startsWith('/canvas/'))) {
const cleanId = targetId.replace('.canvas', '').replace('/canvas/', '')
navigateTo(`/canvas/${cleanId}`)
return
} else if (node.type === 'file' && !targetId) {
alert('У этого узла еще нет ссылки на другую карту. Выберите карту в панели свойств (справа).')
return
}
// 2. Task Toggle Logic (fallback)
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}`)
// 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) => {
const edge = {
id: `edge-${Date.now()}`,
source: connection.source,
target: connection.target,
sourceHandle: connection.sourceHandle,
targetHandle: connection.targetHandle,
animated: true,
style: { stroke: '#475569', strokeWidth: 2 }
}
elements.value.push(edge)
}
// Canvas actions (moved to enhanced version below with Git sync)
const exportObsidian = () => {
const obsidianCanvas = transformToObsidianCanvas(elements.value)
const blob = new Blob([JSON.stringify(obsidianCanvas, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `${props.canvasId}.canvas`
a.click()
URL.revokeObjectURL(url)
}
const deleteCanvas = async () => {
if (!confirm('Вы уверены, что хотите полностью удалить эту карту? Это действие необратимо.')) return
try {
const res = await fetch(`/api/canvas/${props.canvasId}`, {
method: 'DELETE',
headers: authHeaders.value
})
if (res.ok) {
navigateTo('/canvases')
} else {
const err = await res.json()
alert('Ошибка при удалении: ' + (err.message || 'неизвестная ошибка'))
}
} catch (err) {
console.error('Delete error:', err)
alert('Не удалось удалить карту')
}
}
const fitView = () => {
const padding = 40
const cols = 3
// Read actual unscaled sizes from DOM using offsetWidth/offsetHeight
// These are NOT affected by CSS transforms (zoom)
const domSizes = {}
const domNodes = document.querySelectorAll('.vue-flow__node')
domNodes.forEach(el => {
const id = el.getAttribute('data-id')
if (id) {
domSizes[id] = { width: el.offsetWidth, height: el.offsetHeight }
}
})
const nodes = elements.value.filter(el => el.id && !el.source && !el.target)
// Get real unscaled size from DOM
const getSize = (node) => {
const dom = domSizes[node.id]
if (dom && dom.width > 0 && dom.height > 0) {
return dom
}
return { width: node.data?.width || 320, height: node.data?.height || 200 }
}
// Grid layout: row by row, max height per row
const placed = []
let y = padding
for (let i = 0; i < nodes.length; i += cols) {
const rowNodes = nodes.slice(i, i + cols)
let x = padding
let rowMaxHeight = 0
for (const node of rowNodes) {
const size = getSize(node)
placed.push({ id: node.id, x, y, w: size.width, h: size.height })
x += size.width + padding
rowMaxHeight = Math.max(rowMaxHeight, size.height)
}
y += rowMaxHeight + padding
}
// Apply
elements.value = elements.value.map(el => {
if (el.source && el.target) return el
const p = placed.find(n => n.id === el.id)
if (!p) return el
return {
...el,
position: { x: p.x, y: p.y },
data: { ...el.data, x: p.x, y: p.y }
}
})
}
// Git synchronization functions
const createGitRepo = async () => {
try {
await gitSync.createRepository(props.canvasId, 'personal')
console.log('Git repository created successfully')
} catch (error) {
console.error('Failed to create Git repository:', error)
}
}
const manualGitSync = async () => {
try {
const canvasData = transformToObsidianCanvas(elements.value)
await gitSync.autoSyncCanvas(props.canvasId, canvasData)
} catch (error) {
console.error('Manual Git sync failed:', error)
}
}
// Enhanced save function with Git sync
const saveCanvas = async () => {
try {
console.log('[DEBUG saveCanvas] Starting save...')
const obsidianCanvas = transformToObsidianCanvas(elements.value)
console.log('[DEBUG saveCanvas] Sending PUT request...')
const res = await $fetch(`/api/canvas/${props.canvasId}`, {
method: 'PUT',
body: { ...obsidianCanvas, name: canvasName.value, groupId: selectedGroupId.value },
headers: authHeaders.value
})
console.log('[DEBUG saveCanvas] PUT request successful!', res)
// Mark as saved
markAsSaved(obsidianCanvas)
console.log('[DEBUG saveCanvas] Marked as saved in UI')
// Auto-sync to Git if repository exists
await gitSync.autoSyncCanvas(props.canvasId, obsidianCanvas)
console.log('Canvas saved successfully')
} catch (err) {
console.error('Failed to save canvas:', err)
}
}
// SMART & GROW Integration
const isCoaching = ref(false)
const currentCoachAction = ref('')
// Wizard state
const showWizardModal = ref(false)
const isWizardSubmitting = ref(false)
const wizardModalRef = ref(null)
const createSmartNodeManual = () => {
const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const nodeData = {
id,
type: 'task',
position: { x: 300, y: 250 },
data: {
width: 320,
height: 260,
label: 'Новая SMART цель',
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: '',
text: `# 🎯 Новая Цель\n\n* **[S] Specific:** Что конкретно нужно сделать?\n* **[M] Measurable:** Как мы измерим успех?\n - [ ] Метрика 1\n* **[A] Achievable:** Реалистична ли цель?\n* **[R] Relevant:** Почему это важно?\n* **[T] Time-bound:** Срок сдачи.`
},
style: {
width: '320px',
height: '260px',
background: '#1e1b4b',
border: '1px solid #6366f1',
borderRadius: '8px'
}
}
elements.value.push(nodeData)
trackChanges(transformToObsidianCanvas(elements.value))
}
const createGrowChainManual = () => {
const baseId = `grow-${Date.now()}`
const xStart = 200
const yStart = 250
const spacing = 350
const steps = [
{
suffix: 'g',
type: 'text',
title: '🎯 GROW: Goal (Цель)',
text: `# 🎯 Цель (Goal)\n\n* Какой именно цели я хочу достичь?\n* Какой результат я хочу получить?\n* Почему достижение этой цели важно?`,
bg: '#1e293b',
border: '#3b82f6'
},
{
suffix: 'o',
type: 'text',
title: '🚧 GROW: Options (Варианты)',
text: `# 🚧 Варианты (Options)\n\n* Что именно я могу изменить?\n* Какие ресурсы мне нужны?\n* Кто мне может помочь?\n* А что еще?\n\n- [ ] Вариант 1`,
bg: '#1e293b',
border: '#f97316'
},
{
suffix: 'r',
type: 'text',
title: '🔍 GROW: Reality (Реальность)',
text: `# 🔍 Реальность (Reality)\n\n* Какова текущая ситуация?\n* Какие препятствия я вижу?\n* Что мне мешает начать?\n* Какие у меня уже есть ресурсы?`,
bg: '#1e293b',
border: '#eab308'
},
{
suffix: 'w',
type: 'task',
title: '🏁 GROW: Way Forward (План)',
text: `# 🏁 План действий (Way Forward)\n\n* Какие самые важные действия необходимо предпринять?\n* Что я сделаю в первую очередь? Какие сроки?\n\n- [ ] Первый шаг\n- [ ] Второй шаг`,
bg: '#064e3b',
border: '#10b981'
}
]
const newNodes = steps.map((step, idx) => {
const id = `${baseId}-${step.suffix}`
return {
id,
type: step.type,
position: { x: xStart + (idx * spacing), y: yStart },
data: {
width: 320,
height: 220,
label: step.title,
text: step.text,
...(step.type === 'task' && {
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
})
},
style: {
width: '320px',
height: '220px',
background: step.bg,
border: `1px solid ${step.border}`,
borderRadius: '8px'
}
}
})
const newEdges = []
for (let i = 0; i < newNodes.length - 1; i++) {
newEdges.push({
id: `edge-${baseId}-${i}`,
source: newNodes[i].id,
target: newNodes[i + 1].id,
sourceHandle: 'right',
targetHandle: 'left',
animated: true,
style: { stroke: '#64748b', strokeWidth: 2 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
}
const createDiltsChainManual = () => {
const baseId = `dilts-${Date.now()}`
const xStart = 300
const yStart = 150
const spacingY = 250
const steps = [
{
suffix: 'm',
type: 'text',
title: '🌟 Дилтс: Миссия',
text: `# 🌟 Миссия (Mission)\n\n* **Ради кого?**\n* **Кому еще это важно?**\n\n*(Уровень БЫТЬ)*`,
bg: '#4c1d95',
border: '#8b5cf6'
},
{
suffix: 'i',
type: 'text',
title: '👤 Дилтс: Идентичность',
text: `# 👤 Идентичность (Identity)\n\n* **Кто я?**`,
bg: '#1e1b4b',
border: '#6366f1'
},
{
suffix: 'v',
type: 'text',
title: '💎 Дилтс: Ценности',
text: `# 💎 Ценности (Values)\n\n* **Почему это важно?**`,
bg: '#1e3a8a',
border: '#3b82f6'
},
{
suffix: 's',
type: 'text',
title: '🧠 Дилтс: Навыки',
text: `# 🧠 Навыки (Skills)\n\n* **Как? Каким образом?**`,
bg: '#115e59',
border: '#14b8a6'
},
{
suffix: 'a',
type: 'text',
title: '🎬 Дилтс: Действия',
text: `# 🎬 Действия (Actions)\n\n* **Что делать?**\n\n*(Уровень ДЕЛАТЬ)*`,
bg: '#064e3b',
border: '#10b981'
},
{
suffix: 'e',
type: 'task',
title: '🌍 Дилтс: Окружение',
text: `# 🌍 Окружение (Environment)\n\n* **Где? Когда? С кем?**\n\n*(Уровень ИМЕТЬ)*`,
bg: '#1e293b',
border: '#64748b'
}
]
const newNodes = steps.map((step, idx) => {
const id = `${baseId}-${step.suffix}`
return {
id,
type: step.type,
position: { x: xStart, y: yStart + (idx * spacingY) },
data: {
width: 320,
height: 200,
label: step.title,
text: step.text,
...(step.type === 'task' && {
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
})
},
style: {
width: '320px',
height: '200px',
background: step.bg,
border: `1px solid ${step.border}`,
borderRadius: '8px'
}
}
})
const newEdges = []
for (let i = 0; i < newNodes.length - 1; i++) {
newEdges.push({
id: `edge-${baseId}-${i}`,
source: newNodes[i].id,
target: newNodes[i + 1].id,
sourceHandle: 'bottom',
targetHandle: 'top',
animated: true,
style: { stroke: '#8b5cf6', strokeWidth: 2 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
}
const createEisenhowerMatrixManual = () => {
const baseId = `eisen-${Date.now()}`
const xStart = 300
const yStart = 200
const width = 320
const height = 220
const gap = 40
const quadrants = [
{
suffix: 'q1',
title: '🔥 Важно и Срочно',
text: `# 🔥 Важно и Срочно (Сделать)\n\n*Кризисы, дедлайны, неотложные проблемы*\n\n- [ ] Сделать прямо сейчас`,
bg: '#450a0a',
border: '#ef4444',
dx: 0,
dy: 0
},
{
suffix: 'q2',
title: '⚡ Важно, но Несрочно',
text: `# ⚡ Важно, но Несрочно (Запланировать)\n\n*Стратегическое планирование, обучение, здоровье*\n\n- [ ] Запланировать выполнение`,
bg: '#1e1b4b',
border: '#3b82f6',
dx: width + gap,
dy: 0
},
{
suffix: 'q3',
title: '🧊 Неважно, но Срочно',
text: `# 🧊 Неважно, но Срочно (Делегировать)\n\n*Мелкие поручения, чужие просьбы, входящая рутина*\n\n- [ ] Поручить или делегировать`,
bg: '#451a03',
border: '#f59e0b',
dx: 0,
dy: height + gap
},
{
suffix: 'q4',
title: '💤 Неважно и Несрочно',
text: `# 💤 Неважно и Несрочно (Отложить/Удалить)\n\n*Пожиратели времени, мелкая неважная рутина*\n\n- [ ] Отложить или удалить`,
bg: '#1e293b',
border: '#64748b',
dx: width + gap,
dy: height + gap
}
]
const newNodes = quadrants.map(q => ({
id: `${baseId}-${q.suffix}`,
type: 'task',
position: { x: xStart + q.dx, y: yStart + q.dy },
data: {
width,
height,
label: q.title,
text: q.text,
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
},
style: {
width: `${width}px`,
height: `${height}px`,
background: q.bg,
border: `1px solid ${q.border}`,
borderRadius: '8px'
}
}))
elements.value.push(...newNodes)
trackChanges(transformToObsidianCanvas(elements.value))
}
const createGoalsTemplateManual = () => {
const baseId = `goals-year-${Date.now()}`
const xCenter = 150
const yCenter = 450
const xGoal = 550
const yStart = 150
const spacingY = 150
const centralNode = {
id: `${baseId}-center`,
type: 'text',
position: { x: xCenter, y: yCenter },
data: {
width: 300,
height: 120,
label: '🏆 МОИ ЦЕЛИ НА ГОД',
text: `# 🏆 Мои Цели на Год\n\nПрактическое задание по целеполаганию.\n\nЗаполните и проработайте 5 ключевых целей в узлах справа. Для каждой цели вы можете пройти тест истинности в правой панели.`
},
style: {
width: '300px',
height: '120px',
background: '#1e1b4b',
border: '2px solid #fbbf24',
borderRadius: '12px'
}
}
const newNodes = [centralNode]
const newEdges = []
for (let i = 1; i <= 5; i++) {
const id = `${baseId}-g${i}`
const yPos = yStart + (i - 1) * spacingY
newNodes.push({
id,
type: 'task',
position: { x: xGoal, y: yPos },
data: {
width: 320,
height: 220,
label: `Цель ${i}: [Введите цель]`,
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: '',
text: `# 🎯 Цель ${i}\n\n* **[S] Результат:** Что конкретно нужно сделать?\n* **[R] Необходимость:** Почему это важно?\n* **[A] Реалистичность:** Степень достижения.\n* **[M] Метрики:** Как измерить успех?\n* **[T] Срок:** Сроки достижения.\n\n---\n* **Шаги (План):**\n - [ ] Шаг 1\n* **Ресурсы и помощники:** `
},
style: {
width: '320px',
height: '220px',
background: '#0f172a',
border: '1px solid #3b82f6',
borderRadius: '8px'
}
})
newEdges.push({
id: `edge-${baseId}-g${i}`,
source: centralNode.id,
target: id,
sourceHandle: 'right',
targetHandle: 'left',
animated: true,
style: { stroke: '#fbbf24', strokeWidth: 1.5 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
saveCanvas()
}
const applySmartAI = async () => {
if (!selectedNode.value) return
const nodeText = selectedNode.value.data.text || selectedNode.value.data.label || ''
if (!nodeText.trim()) {
alert('Сначала введите текст цели в узле!')
return
}
isCoaching.value = true
currentCoachAction.value = 'smart'
try {
const res = await $fetch('/api/ai/coach', {
method: 'POST',
body: {
action: 'smart',
text: nodeText
},
headers: authHeaders.value
})
if (res.success) {
selectedNode.value.data.text = res.result
selectedNode.value.data.label = '🎯 ' + (selectedNode.value.data.label || 'Goal').replace(/^🎯\s*/, '')
selectedNode.value.type = 'task'
selectedNode.value.style.background = '#1e1b4b'
selectedNode.value.style.border = '1px solid #6366f1'
commitNodeChange()
await saveCanvas()
}
} catch (err) {
console.error('SMART AI failed:', err)
alert('Не удалось применить SMART: ' + (err.message || 'ошибка сети'))
} finally {
isCoaching.value = false
currentCoachAction.value = ''
}
}
const applyGrowAI = async () => {
if (!selectedNode.value) return
const nodeText = selectedNode.value.data.text || selectedNode.value.data.label || ''
if (!nodeText.trim()) {
alert('Сначала введите описание проблемы или цели в узле!')
return
}
isCoaching.value = true
currentCoachAction.value = 'grow'
try {
const res = await $fetch('/api/ai/coach', {
method: 'POST',
body: {
action: 'grow',
text: nodeText
},
headers: authHeaders.value
})
if (res.success) {
const { goal, reality, options, wayForward } = res.result
const baseId = `grow-ai-${Date.now()}`
const startX = selectedNode.value.position.x
const startY = selectedNode.value.position.y
const spacingX = 350
const growSteps = [
{ suffix: 'g', type: 'text', title: '🎯 Goal (AI)', text: goal, bg: '#1e293b', border: '#3b82f6' },
{ suffix: 'o', type: 'text', title: '🚧 Options (AI)', text: options, bg: '#1e293b', border: '#f97316' },
{ suffix: 'r', type: 'text', title: '🔍 Reality (AI)', text: reality, bg: '#1e293b', border: '#eab308' },
{ suffix: 'w', type: 'task', title: '🏁 Way Forward (AI)', text: wayForward, bg: '#064e3b', border: '#10b981' }
]
const newNodes = growSteps.map((step, idx) => {
const id = `${baseId}-${step.suffix}`
return {
id,
type: step.type,
position: { x: startX + ((idx + 1) * spacingX), y: startY },
data: {
width: 320,
height: 220,
label: step.title,
text: step.text,
...(step.type === 'task' && {
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
})
},
style: {
width: '320px',
height: '220px',
background: step.bg,
border: `1px solid ${step.border}`,
borderRadius: '8px'
}
}
})
const newEdges = []
newEdges.push({
id: `edge-${baseId}-init`,
source: selectedNode.value.id,
target: newNodes[0].id,
sourceHandle: 'right',
targetHandle: 'left',
animated: true,
style: { stroke: '#818cf8', strokeWidth: 2 }
})
for (let i = 0; i < newNodes.length - 1; i++) {
newEdges.push({
id: `edge-${baseId}-${i}`,
source: newNodes[i].id,
target: newNodes[i + 1].id,
sourceHandle: 'right',
targetHandle: 'left',
animated: true,
style: { stroke: '#64748b', strokeWidth: 2 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
await saveCanvas()
}
} catch (err) {
console.error('GROW AI failed:', err)
alert('Не удалось применить GROW: ' + (err.message || 'ошибка сети'))
} finally {
isCoaching.value = false
currentCoachAction.value = ''
}
}
const applyDiltsAI = async () => {
if (!selectedNode.value) return
const nodeText = selectedNode.value.data.text || selectedNode.value.data.label || ''
if (!nodeText.trim()) {
alert('Сначала введите описание проблемы или цели в узле!')
return
}
isCoaching.value = true
currentCoachAction.value = 'dilts'
try {
const res = await $fetch('/api/ai/coach', {
method: 'POST',
body: {
action: 'dilts',
text: nodeText
},
headers: authHeaders.value
})
if (res.success) {
const { mission, identity, values, skills, actions, environment } = res.result
const baseId = `dilts-ai-${Date.now()}`
const startX = selectedNode.value.position.x
const startY = selectedNode.value.position.y
const spacingY = 250
const steps = [
{ suffix: 'm', type: 'text', title: '🌟 Миссия (AI)', text: mission, bg: '#4c1d95', border: '#8b5cf6' },
{ suffix: 'i', type: 'text', title: '👤 Идентичность (AI)', text: identity, bg: '#1e1b4b', border: '#6366f1' },
{ suffix: 'v', type: 'text', title: '💎 Ценности (AI)', text: values, bg: '#1e3a8a', border: '#3b82f6' },
{ suffix: 's', type: 'text', title: '🧠 Навыки (AI)', text: skills, bg: '#115e59', border: '#14b8a6' },
{ suffix: 'a', type: 'text', title: '🎬 Действия (AI)', text: actions, bg: '#064e3b', border: '#10b981' },
{ suffix: 'e', type: 'task', title: '🌍 Окружение (AI)', text: environment, bg: '#1e293b', border: '#64748b' }
]
const newNodes = steps.map((step, idx) => {
const id = `${baseId}-${step.suffix}`
return {
id,
type: step.type,
position: { x: startX + 380, y: startY + (idx * spacingY) },
data: {
width: 320,
height: 200,
label: step.title,
text: step.text,
...(step.type === 'task' && {
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
})
},
style: {
width: '320px',
height: '200px',
background: step.bg,
border: `1px solid ${step.border}`,
borderRadius: '8px'
}
}
})
const newEdges = []
newEdges.push({
id: `edge-${baseId}-init`,
source: selectedNode.value.id,
target: newNodes[0].id,
sourceHandle: 'right',
targetHandle: 'left',
animated: true,
style: { stroke: '#8b5cf6', strokeWidth: 2 }
})
for (let i = 0; i < newNodes.length - 1; i++) {
newEdges.push({
id: `edge-${baseId}-${i}`,
source: newNodes[i].id,
target: newNodes[i + 1].id,
sourceHandle: 'bottom',
targetHandle: 'top',
animated: true,
style: { stroke: '#8b5cf6', strokeWidth: 2 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
await saveCanvas()
}
} catch (err) {
console.error('Dilts AI failed:', err)
alert('Не удалось применить Пирамиду Дилтса: ' + (err.message || 'ошибка сети'))
} finally {
isCoaching.value = false
currentCoachAction.value = ''
}
}
const applyEisenhowerAI = async () => {
if (!selectedNode.value) return
const nodeText = selectedNode.value.data.text || selectedNode.value.data.label || ''
if (!nodeText.trim()) {
alert('Сначала введите список задач или описание проблемы в узле!')
return
}
isCoaching.value = true
currentCoachAction.value = 'eisenhower'
try {
const res = await $fetch('/api/ai/coach', {
method: 'POST',
body: {
action: 'eisenhower',
text: nodeText
},
headers: authHeaders.value
})
if (res.success) {
const { q1, q2, q3, q4 } = res.result
const baseId = `eisen-ai-${Date.now()}`
const startX = selectedNode.value.position.x
const startY = selectedNode.value.position.y
const width = 320
const height = 220
const gap = 40
const quadrants = [
{ suffix: 'q1', title: '🔥 Важно и Срочно (AI)', text: q1, bg: '#450a0a', border: '#ef4444', dx: width + gap, dy: 0 },
{ suffix: 'q2', title: '⚡ Важно, но Несрочно (AI)', text: q2, bg: '#1e1b4b', border: '#3b82f6', dx: width + gap + width + gap, dy: 0 },
{ suffix: 'q3', title: '🧊 Неважно, но Срочно (AI)', text: q3, bg: '#451a03', border: '#f59e0b', dx: width + gap, dy: height + gap },
{ suffix: 'q4', title: '💤 Неважно и Несрочно (AI)', text: q4, bg: '#1e293b', border: '#64748b', dx: width + gap + width + gap, dy: height + gap }
]
const newNodes = quadrants.map(q => ({
id: `${baseId}-${q.suffix}`,
type: 'task',
position: { x: startX + q.dx, y: startY + q.dy },
data: {
width,
height,
label: q.title,
text: q.text,
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
},
style: {
width: `${width}px`,
height: `${height}px`,
background: q.bg,
border: `1px solid ${q.border}`,
borderRadius: '8px'
}
}))
const newEdges = []
newEdges.push({
id: `edge-${baseId}-init`,
source: selectedNode.value.id,
target: newNodes[0].id,
sourceHandle: 'right',
targetHandle: 'left',
animated: true,
style: { stroke: '#ef4444', strokeWidth: 2 }
})
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
await saveCanvas()
}
} catch (err) {
console.error('Eisenhower AI failed:', err)
alert('Не удалось применить Матрицу Эйзенхауэра: ' + (err.message || 'ошибка сети'))
} finally {
isCoaching.value = false
currentCoachAction.value = ''
}
}
const handleSmartManual = (data) => {
const { specific, measurable, achievable, relevant, deadline } = data
const text = `# 🎯 ${specific || 'Цель по SMART'}\n\n` +
`* **[S] Specific:** ${specific || 'Не указано'}\n` +
`* **[M] Measurable:** ${measurable || 'Не указано'}\n` +
(measurable ? measurable.split('\n').map(line => ` - [ ] ${line}`).join('\n') + '\n' : '') +
`* **[A] Achievable:** ${achievable || 'Не указано'}\n` +
`* **[R] Relevant:** ${relevant || 'Не указано'}\n` +
`* **[T] Time-bound:** ${deadline ? new Date(deadline).toLocaleString('ru-RU') : 'Не указано'}`
const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const nodeData = {
id,
type: 'task',
position: { x: 300, y: 250 },
data: {
width: 320,
height: 280,
label: specific || 'Цель по SMART',
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: deadline || '',
script_id: '',
text
},
style: {
width: '320px',
height: '280px',
background: '#1e1b4b',
border: '1px solid #6366f1',
borderRadius: '8px'
}
}
elements.value.push(nodeData)
trackChanges(transformToObsidianCanvas(elements.value))
saveCanvas()
showWizardModal.value = false
}
const handleGrowManual = (data) => {
const { goal, reality, options, wayForward } = data
const baseId = `grow-${Date.now()}`
const xStart = 200
const yStart = 250
const spacing = 350
const steps = [
{
suffix: 'g',
type: 'text',
title: '🎯 GROW: Goal (Цель)',
text: `# 🎯 Цель (Goal)\n\n${goal || 'Не указано'}`,
bg: '#1e293b',
border: '#3b82f6'
},
{
suffix: 'o',
type: 'text',
title: '🚧 GROW: Options (Варианты)',
text: `# 🚧 Варианты (Options)\n\n${options || 'Не указано'}`,
bg: '#1e293b',
border: '#f97316'
},
{
suffix: 'r',
type: 'text',
title: '🔍 GROW: Reality (Реальность)',
text: `# 🔍 Реальность (Reality)\n\n${reality || 'Не указано'}`,
bg: '#1e293b',
border: '#eab308'
},
{
suffix: 'w',
type: 'task',
title: '🏁 GROW: Way Forward (План)',
text: `# 🏁 План действий (Way Forward)\n\n${wayForward || 'Не указано'}`,
bg: '#064e3b',
border: '#10b981'
}
]
const newNodes = steps.map((step, idx) => {
const id = `${baseId}-${step.suffix}`
return {
id,
type: step.type,
position: { x: xStart + (idx * spacing), y: yStart },
data: {
width: 320,
height: 220,
label: step.title,
text: step.text,
...(step.type === 'task' && {
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
})
},
style: {
width: '320px',
height: '220px',
background: step.bg,
border: `1px solid ${step.border}`,
borderRadius: '8px'
}
}
})
const newEdges = []
for (let i = 0; i < newNodes.length - 1; i++) {
newEdges.push({
id: `edge-${baseId}-${i}`,
source: newNodes[i].id,
target: newNodes[i + 1].id,
sourceHandle: 'right',
targetHandle: 'left',
animated: true,
style: { stroke: '#64748b', strokeWidth: 2 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
saveCanvas()
showWizardModal.value = false
}
const handleSmartAI = async (data) => {
const { specific, measurable, achievable, relevant, deadline } = data
isWizardSubmitting.value = true
try {
const combinedPrompt = `Критерии SMART, введенные пользователем:
- Конкретная цель: ${specific}
- Измеримость: ${measurable}
- Достижимость: ${achievable}
- Важность: ${relevant}
- Срок: ${deadline}
Пожалуйста, оптимизируй и красиво оформи эту цель по методологии SMART в единый Markdown-узел. Исправь формулировки на более четкие и профессиональные.`
const res = await $fetch('/api/ai/coach', {
method: 'POST',
body: {
action: 'smart',
text: combinedPrompt
},
headers: authHeaders.value
})
if (res.success) {
const id = `node-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
const nodeData = {
id,
type: 'task',
position: { x: 300, y: 250 },
data: {
width: 320,
height: 280,
label: specific || 'Цель по SMART',
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: deadline || '',
script_id: '',
text: res.result
},
style: {
width: '320px',
height: '280px',
background: '#1e1b4b',
border: '1px solid #6366f1',
borderRadius: '8px'
}
}
elements.value.push(nodeData)
trackChanges(transformToObsidianCanvas(elements.value))
await saveCanvas()
if (wizardModalRef.value) wizardModalRef.value.resetForms()
showWizardModal.value = false
}
} catch (err) {
console.error('Wizard SMART AI failed:', err)
alert('Не удалось оптимизировать через AI: ' + (err.message || 'ошибка сети'))
} finally {
isWizardSubmitting.value = false
}
}
const handleGrowAI = async (data) => {
const { goal, reality, options, wayForward } = data
isWizardSubmitting.value = true
try {
const combinedPrompt = `Критерии GROW коучинга, введенные пользователем:
- Цель (Goal): ${goal}
- Реальность (Reality): ${reality}
- Варианты и препятствия (Options): ${options}
- План действий (Way Forward): ${wayForward}
Пожалуйста, проанализируй эти ответы, дополни их полезными рекомендациями, разбей на структурированные этапы GROW и верни JSON объект для создания 4 узлов.`
const res = await $fetch('/api/ai/coach', {
method: 'POST',
body: {
action: 'grow',
text: combinedPrompt
},
headers: authHeaders.value
})
if (res.success) {
const { goal: aiGoal, reality: aiReality, options: aiOptions, wayForward: aiWayForward } = res.result
const baseId = `grow-ai-${Date.now()}`
const startX = 200
const startY = 250
const spacingX = 350
const growSteps = [
{ suffix: 'g', type: 'text', title: '🎯 Goal (AI)', text: aiGoal, bg: '#1e293b', border: '#3b82f6' },
{ suffix: 'o', type: 'text', title: '🚧 Options (AI)', text: aiOptions, bg: '#1e293b', border: '#f97316' },
{ suffix: 'r', type: 'text', title: '🔍 Reality (AI)', text: aiReality, bg: '#1e293b', border: '#eab308' },
{ suffix: 'w', type: 'task', title: '🏁 Way Forward (AI)', text: aiWayForward, bg: '#064e3b', border: '#10b981' }
]
const newNodes = growSteps.map((step, idx) => {
const id = `${baseId}-${step.suffix}`
return {
id,
type: step.type,
position: { x: startX + (idx * spacingX), y: startY },
data: {
width: 320,
height: 220,
label: step.title,
text: step.text,
...(step.type === 'task' && {
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
})
},
style: {
width: '320px',
height: '220px',
background: step.bg,
border: `1px solid ${step.border}`,
borderRadius: '8px'
}
}
})
const newEdges = []
for (let i = 0; i < newNodes.length - 1; i++) {
newEdges.push({
id: `edge-${baseId}-${i}`,
source: newNodes[i].id,
target: newNodes[i + 1].id,
sourceHandle: 'right',
targetHandle: 'left',
animated: true,
style: { stroke: '#64748b', strokeWidth: 2 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
await saveCanvas()
if (wizardModalRef.value) wizardModalRef.value.resetForms()
showWizardModal.value = false
}
} catch (err) {
console.error('Wizard GROW AI failed:', err)
alert('Не удалось оптимизировать через AI: ' + (err.message || 'ошибка сети'))
} finally {
isWizardSubmitting.value = false
}
}
const handleDiltsManual = (data) => {
const { mission, identity, values, skills, actions, environment } = data
const baseId = `dilts-${Date.now()}`
const xStart = 300
const yStart = 150
const spacingY = 250
const steps = [
{ suffix: 'm', type: 'text', title: '🌟 Дилтс: Миссия', text: `# 🌟 Миссия (Mission)\n\n${mission || 'Не указано'}`, bg: '#4c1d95', border: '#8b5cf6' },
{ suffix: 'i', type: 'text', title: '👤 Дилтс: Идентичность', text: `# 👤 Идентичность (Identity)\n\n${identity || 'Не указано'}`, bg: '#1e1b4b', border: '#6366f1' },
{ suffix: 'v', type: 'text', title: '💎 Дилтс: Ценности', text: `# 💎 Ценности (Values)\n\n${values || 'Не указано'}`, bg: '#1e3a8a', border: '#3b82f6' },
{ suffix: 's', type: 'text', title: '🧠 Навыки (Skills)', text: `# 🧠 Навыки (Skills)\n\n${skills || 'Не указано'}`, bg: '#115e59', border: '#14b8a6' },
{ suffix: 'a', type: 'text', title: '🎬 Дилтс: Действия', text: `# 🎬 Действия (Actions)\n\n${actions || 'Не указано'}`, bg: '#064e3b', border: '#10b981' },
{ suffix: 'e', type: 'task', title: '🌍 Дилтс: Окружение', text: `# 🌍 Окружение (Environment)\n\n${environment || 'Не указано'}`, bg: '#1e293b', border: '#64748b' }
]
const newNodes = steps.map((step, idx) => {
const id = `${baseId}-${step.suffix}`
return {
id,
type: step.type,
position: { x: xStart, y: yStart + (idx * spacingY) },
data: {
width: 320,
height: 200,
label: step.title,
text: step.text,
...(step.type === 'task' && {
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
})
},
style: {
width: '320px',
height: '200px',
background: step.bg,
border: `1px solid ${step.border}`,
borderRadius: '8px'
}
}
})
const newEdges = []
for (let i = 0; i < newNodes.length - 1; i++) {
newEdges.push({
id: `edge-${baseId}-${i}`,
source: newNodes[i].id,
target: newNodes[i + 1].id,
sourceHandle: 'bottom',
targetHandle: 'top',
animated: true,
style: { stroke: '#8b5cf6', strokeWidth: 2 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
saveCanvas()
showWizardModal.value = false
}
const handleDiltsAI = async (data) => {
const { mission, identity, values, skills, actions, environment } = data
isWizardSubmitting.value = true
try {
const combinedPrompt = `Критерии логических уровней Дилтса, введенные пользователем:
- Миссия: ${mission}
- Идентичность (Я): ${identity}
- Ценности: ${values}
- Навыки: ${skills}
- Действия: ${actions}
- Окружение: ${environment}
Пожалуйста, оптимизируй эти ответы, разложи их по уровням Дилтса и верни JSON объект для создания 6 узлов.`
const res = await $fetch('/api/ai/coach', {
method: 'POST',
body: {
action: 'dilts',
text: combinedPrompt
},
headers: authHeaders.value
})
if (res.success) {
const { mission: aiMission, identity: aiIdentity, values: aiValues, skills: aiSkills, actions: aiActions, environment: aiEnvironment } = res.result
const baseId = `dilts-ai-${Date.now()}`
const startX = 300
const startY = 150
const spacingY = 250
const steps = [
{ suffix: 'm', type: 'text', title: '🌟 Миссия (AI)', text: aiMission, bg: '#4c1d95', border: '#8b5cf6' },
{ suffix: 'i', type: 'text', title: '👤 Идентичность (AI)', text: aiIdentity, bg: '#1e1b4b', border: '#6366f1' },
{ suffix: 'v', type: 'text', title: '💎 Ценности (AI)', text: aiValues, bg: '#1e3a8a', border: '#3b82f6' },
{ suffix: 's', type: 'text', title: '🧠 Навыки (AI)', text: aiSkills, bg: '#115e59', border: '#14b8a6' },
{ suffix: 'a', type: 'text', title: '🎬 Действия (AI)', text: aiActions, bg: '#064e3b', border: '#10b981' },
{ suffix: 'e', type: 'task', title: '🌍 Окружение (AI)', text: aiEnvironment, bg: '#1e293b', border: '#64748b' }
]
const newNodes = steps.map((step, idx) => {
const id = `${baseId}-${step.suffix}`
return {
id,
type: step.type,
position: { x: startX, y: startY + (idx * spacingY) },
data: {
width: 320,
height: 200,
label: step.title,
text: step.text,
...(step.type === 'task' && {
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
})
},
style: {
width: '320px',
height: '200px',
background: step.bg,
border: `1px solid ${step.border}`,
borderRadius: '8px'
}
}
})
const newEdges = []
for (let i = 0; i < newNodes.length - 1; i++) {
newEdges.push({
id: `edge-${baseId}-${i}`,
source: newNodes[i].id,
target: newNodes[i + 1].id,
sourceHandle: 'bottom',
targetHandle: 'top',
animated: true,
style: { stroke: '#8b5cf6', strokeWidth: 2 }
})
}
elements.value.push(...newNodes, ...newEdges)
trackChanges(transformToObsidianCanvas(elements.value))
await saveCanvas()
if (wizardModalRef.value) wizardModalRef.value.resetForms()
showWizardModal.value = false
}
} catch (err) {
console.error('Wizard Dilts AI failed:', err)
alert('Не удалось оптимизировать через AI: ' + (err.message || 'ошибка сети'))
} finally {
isWizardSubmitting.value = false
}
}
const handleEisenhowerManual = (data) => {
const { q1, q2, q3, q4 } = data
const baseId = `eisen-${Date.now()}`
const xStart = 300
const yStart = 200
const width = 320
const height = 220
const gap = 40
const quadrants = [
{ suffix: 'q1', title: '🔥 Важно и Срочно', text: `# 🔥 Важно и Срочно (Сделать)\n\n${q1 || 'Не указано'}`, bg: '#450a0a', border: '#ef4444', dx: 0, dy: 0 },
{ suffix: 'q2', title: '⚡ Важно, но Несрочно', text: `# ⚡ Важно, но Несрочно (Запланировать)\n\n${q2 || 'Не указано'}`, bg: '#1e1b4b', border: '#3b82f6', dx: width + gap, dy: 0 },
{ suffix: 'q3', title: '🧊 Неважно, но Срочно', text: `# 🧊 Неважно, но Срочно (Делегировать)\n\n${q3 || 'Не указано'}`, bg: '#451a03', border: '#f59e0b', dx: 0, dy: height + gap },
{ suffix: 'q4', title: '💤 Неважно и Несрочно', text: `# 💤 Неважно и Несрочно (Отложить/Удалить)\n\n${q4 || 'Не указано'}`, bg: '#1e293b', border: '#64748b', dx: width + gap, dy: height + gap }
]
const newNodes = quadrants.map(q => ({
id: `${baseId}-${q.suffix}`,
type: 'task',
position: { x: xStart + q.dx, y: yStart + q.dy },
data: {
width,
height,
label: q.title,
text: q.text,
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
},
style: {
width: `${width}px`,
height: `${height}px`,
background: q.bg,
border: `1px solid ${q.border}`,
borderRadius: '8px'
}
}))
elements.value.push(...newNodes)
trackChanges(transformToObsidianCanvas(elements.value))
saveCanvas()
showWizardModal.value = false
}
const handleEisenhowerAI = async (data) => {
const { q1, q2, q3, q4 } = data
isWizardSubmitting.value = true
try {
const combinedPrompt = `Критерии Матрицы Эйзенхауэра, введенные пользователем:
- Важно и Срочно: ${q1}
- Важно, но Несрочно: ${q2}
- Неважно, но Срочно: ${q3}
- Неважно и Несрочно: ${q4}
Пожалуйста, оптимизируй эти ответы, распредели задачи корректно по квадрантам и верни JSON объект для создания 4 узлов.`
const res = await $fetch('/api/ai/coach', {
method: 'POST',
body: {
action: 'eisenhower',
text: combinedPrompt
},
headers: authHeaders.value
})
if (res.success) {
const { q1: aiQ1, q2: aiQ2, q3: aiQ3, q4: aiQ4 } = res.result
const baseId = `eisen-ai-${Date.now()}`
const startX = 300
const startY = 200
const width = 320
const height = 220
const gap = 40
const quadrants = [
{ suffix: 'q1', title: '🔥 Важно и Срочно (AI)', text: aiQ1, bg: '#450a0a', border: '#ef4444', dx: 0, dy: 0 },
{ suffix: 'q2', title: '⚡ Важно, но Несрочно (AI)', text: aiQ2, bg: '#1e1b4b', border: '#3b82f6', dx: width + gap, dy: 0 },
{ suffix: 'q3', title: '🧊 Неважно, но Срочно (AI)', text: aiQ3, bg: '#451a03', border: '#f59e0b', dx: 0, dy: height + gap },
{ suffix: 'q4', title: '💤 Неважно и Несрочно (AI)', text: aiQ4, bg: '#1e293b', border: '#64748b', dx: width + gap, dy: height + gap }
]
const newNodes = quadrants.map(q => ({
id: `${baseId}-${q.suffix}`,
type: 'task',
position: { x: startX + q.dx, y: startY + q.dy },
data: {
width,
height,
label: q.title,
text: q.text,
state: 'todo',
priority: 'medium',
assigned_to: '',
alarm: '',
script_id: ''
},
style: {
width: `${width}px`,
height: `${height}px`,
background: q.bg,
border: `1px solid ${q.border}`,
borderRadius: '8px'
}
}))
elements.value.push(...newNodes)
trackChanges(transformToObsidianCanvas(elements.value))
await saveCanvas()
if (wizardModalRef.value) wizardModalRef.value.resetForms()
showWizardModal.value = false
}
} catch (err) {
console.error('Wizard Eisenhower failed:', err)
alert('Не удалось оптимизировать через AI: ' + (err.message || 'ошибка сети'))
} finally {
isWizardSubmitting.value = false
}
}
const usersList = ref([])
const canvasesList = ref([])
// Load data on mount
onMounted(async () => {
await loadData()
// Check for existing Git repository
await gitSync.checkRepository(props.canvasId)
// Load users for assignee dropdown
try {
const res = await fetch('/api/users', {
headers: authHeaders.value
})
if (res.ok) {
usersList.value = await res.json()
}
} catch (e) {
console.error('Failed to load users', e)
}
// Load canvases for cross-link dropdown
try {
const res = await fetch('/api/canvas', {
headers: authHeaders.value
})
if (res.ok) {
const data = await res.json()
canvasesList.value = data.canvases || []
}
} catch (e) {
console.error('Failed to load canvases', e)
}
// Load groups for assignment
try {
const data = await $fetch('/api/user/groups', {
headers: authHeaders.value
})
groupsList.value = data.groups || []
} catch (e) {
console.error('Failed to load groups', e)
}
})
</script>
<style scoped>
:deep(.vue-flow__node) {
font-family: ui-sans-serif, system-ui, sans-serif;
height: auto !important;
}
:deep(.vue-flow__edge) {
stroke: #475569;
}
:deep(.vue-flow__controls) {
background: #1e293b;
border: 1px solid #334155;
}
:deep(.vue-flow__controls button) {
background: #334155;
border: none;
color: #e2e8f0;
}
:deep(.vue-flow__controls button:hover) {
background: #475569;
}
</style>