1863 lines
62 KiB
Vue
1863 lines
62 KiB
Vue
<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="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>
|