feat: integrate coaching frameworks, Eisenhower matrix, Gantt chart, and Goal Validation test
This commit is contained in:
@@ -5,6 +5,9 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const { initAuth } = useAuth()
|
||||
initAuth()
|
||||
|
||||
useHead({
|
||||
title: 'XFlow',
|
||||
meta: [
|
||||
|
||||
+1345
-222
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,266 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<div v-if="show" class="fixed inset-0 z-50 flex items-center justify-center bg-slate-950/80 backdrop-blur-sm p-4 font-sans">
|
||||
<div class="bg-slate-900 border border-slate-700 rounded-2xl w-full max-w-2xl max-h-[90vh] flex flex-col shadow-2xl overflow-hidden">
|
||||
<!-- Modal Header -->
|
||||
<div class="px-6 py-4 border-b border-slate-800 flex justify-between items-center bg-slate-900/50">
|
||||
<h2 class="text-base font-bold text-slate-200 flex items-center gap-2">
|
||||
<span>🧙♂️</span> Коуч-Ассистент: Конструктор SMART & GROW
|
||||
</h2>
|
||||
<button @click="$emit('close')" class="text-slate-400 hover:text-white transition">✕</button>
|
||||
</div>
|
||||
|
||||
<!-- Tabs -->
|
||||
<div class="flex border-b border-slate-800 bg-slate-950/40">
|
||||
<button
|
||||
@click="wizardType = 'smart'"
|
||||
:class="['flex-1 py-3 text-xs font-bold uppercase tracking-wider transition border-b-2', wizardType === 'smart' ? 'text-indigo-400 border-indigo-500 bg-indigo-950/10' : 'text-slate-400 border-transparent hover:bg-slate-800/40']"
|
||||
>
|
||||
🎯 SMART Цель
|
||||
</button>
|
||||
<button
|
||||
@click="wizardType = 'grow'"
|
||||
:class="['flex-1 py-3 text-xs font-bold uppercase tracking-wider transition border-b-2', wizardType === 'grow' ? 'text-teal-400 border-teal-500 bg-teal-950/10' : 'text-slate-400 border-transparent hover:bg-slate-800/40']"
|
||||
>
|
||||
🌱 GROW Сессия
|
||||
</button>
|
||||
<button
|
||||
@click="wizardType = 'dilts'"
|
||||
:class="['flex-1 py-3 text-xs font-bold uppercase tracking-wider transition border-b-2', wizardType === 'dilts' ? 'text-violet-400 border-violet-500 bg-violet-950/10' : 'text-slate-400 border-transparent hover:bg-slate-800/40']"
|
||||
>
|
||||
📐 Пирамида Дилтса
|
||||
</button>
|
||||
<button
|
||||
@click="wizardType = 'eisenhower'"
|
||||
:class="['flex-1 py-3 text-xs font-bold uppercase tracking-wider transition border-b-2', wizardType === 'eisenhower' ? 'text-amber-400 border-amber-500 bg-amber-950/10' : 'text-slate-400 border-transparent hover:bg-slate-800/40']"
|
||||
>
|
||||
📊 Матрица Эйзенхауэра
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Modal Body (Forms) -->
|
||||
<div class="p-6 overflow-y-auto flex-1 space-y-4">
|
||||
<!-- S.M.A.R.T Form -->
|
||||
<div v-if="wizardType === 'smart'" class="space-y-4">
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">S (Specific) — Какая конкретная задача?</label>
|
||||
<textarea v-model="smartForm.specific" rows="2" placeholder="Что именно вы хотите сделать? (например: Разработать модуль экспорта canvas)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">M (Measurable) — Как вы измерите успех?</label>
|
||||
<textarea v-model="smartForm.measurable" rows="2" placeholder="Укажите критерии готовности (каждую метрику с новой строки)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">A (Achievable) — За счет чего цель достижима?</label>
|
||||
<textarea v-model="smartForm.achievable" rows="2" placeholder="Какими ресурсами и навыками вы располагаете?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">R (Relevant) — Почему эта цель важна?</label>
|
||||
<textarea v-model="smartForm.relevant" rows="2" placeholder="Какую пользу принесет выполнение этой цели общему проекту?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">T (Time-bound) — Каков дедлайн?</label>
|
||||
<input v-model="smartForm.deadline" type="datetime-local" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-indigo-500 focus:ring-1 focus:ring-indigo-500 outline-none" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- G.R.O.W Form -->
|
||||
<div v-if="wizardType === 'grow'" class="space-y-4">
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">G (Goal) — Цель</label>
|
||||
<textarea v-model="growForm.goal" rows="3" placeholder="Какой именно цели я хочу достичь? Какой результат я хочу получить? Почему достижение этой цели важно?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-teal-500 focus:ring-1 focus:ring-teal-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">O (Options) — Варианты</label>
|
||||
<textarea v-model="growForm.options" rows="4" placeholder="Что именно я могу изменить? Какие ресурсы мне нужны? Кто мне может помочь? А что еще?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-teal-500 focus:ring-1 focus:ring-teal-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">R (Reality) — Реальность</label>
|
||||
<textarea v-model="growForm.reality" rows="4" placeholder="Какова текущая ситуация? Какие препятствия я вижу? Что мне мешает начать? Какие у меня уже есть ресурсы?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-teal-500 focus:ring-1 focus:ring-teal-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">W (Way Forward) — План</label>
|
||||
<textarea v-model="growForm.wayForward" rows="3" placeholder="Какие самые важные действия необходимо предпринять? Что я сделаю в первую очередь? Какие сроки?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-teal-500 focus:ring-1 focus:ring-teal-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Dilts Form -->
|
||||
<div v-if="wizardType === 'dilts'" class="space-y-4">
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">🌟 Миссия — Ради кого? Кому еще это важно? (БЫТЬ)</label>
|
||||
<textarea v-model="diltsForm.mission" rows="2" placeholder="Ради кого? Кому еще это важно? (БЫТЬ)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">👤 Я — Кто я?</label>
|
||||
<textarea v-model="diltsForm.identity" rows="2" placeholder="Кто я?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">💎 Ценности — Почему это важно?</label>
|
||||
<textarea v-model="diltsForm.values" rows="2" placeholder="Почему это важно?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">🧠 Навыки — Как? Каким образом?</label>
|
||||
<textarea v-model="diltsForm.skills" rows="2" placeholder="Как? Каким образом?" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">🎬 Действия — Что делать? (ДЕЛАТЬ)</label>
|
||||
<textarea v-model="diltsForm.actions" rows="2" placeholder="Что делать? (ДЕЛАТЬ)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">🌍 Окружение — Где? Когда? С кем? (ИМЕТЬ)</label>
|
||||
<textarea v-model="diltsForm.environment" rows="2" placeholder="Где? Когда? С кем? (ИМЕТЬ)" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-violet-500 focus:ring-1 focus:ring-violet-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Eisenhower Form -->
|
||||
<div v-if="wizardType === 'eisenhower'" class="space-y-4">
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">🔥 Квадрант I: Важно и Срочно (Сделать сейчас)</label>
|
||||
<textarea v-model="eisenhowerForm.q1" rows="2" placeholder="Кризисы, горящие дедлайны, важные неотложные задачи" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">⚡ Квадрант II: Важно, но Несрочно (Запланировать)</label>
|
||||
<textarea v-model="eisenhowerForm.q2" rows="2" placeholder="Долгосрочное планирование, здоровье, обучение, стратегические цели" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">🧊 Квадрант III: Неважно, но Срочно (Делегировать)</label>
|
||||
<textarea v-model="eisenhowerForm.q3" rows="2" placeholder="Входящие рутинные звонки/письма, мелкие поручения, задачи, которые можно перепоручить" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-xs font-bold text-slate-400 block mb-1">💤 Квадрант IV: Неважно и Несрочно (Отложить/Удалить)</label>
|
||||
<textarea v-model="eisenhowerForm.q4" rows="2" placeholder="Пожиратели времени, мелкие развлечения, неважная рутина" class="w-full bg-slate-950 border border-slate-800 rounded-lg p-2.5 text-xs text-slate-200 focus:border-amber-500 focus:ring-1 focus:ring-amber-500 outline-none resize-none"></textarea>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Modal Footer -->
|
||||
<div class="px-6 py-4 border-t border-slate-800 bg-slate-900/50 flex justify-end gap-3">
|
||||
<button @click="$emit('close')" class="px-4 py-2 border border-slate-700 hover:bg-slate-800 text-slate-300 rounded-lg text-xs transition">
|
||||
Отмена
|
||||
</button>
|
||||
<button
|
||||
@click="submitManual"
|
||||
:disabled="isSubmitting"
|
||||
class="px-4 py-2 bg-slate-800 hover:bg-slate-700 border border-slate-700 text-white rounded-lg text-xs transition"
|
||||
>
|
||||
Создать вручную
|
||||
</button>
|
||||
<button
|
||||
@click="submitAI"
|
||||
:disabled="isSubmitting"
|
||||
class="px-4 py-2 bg-indigo-600 hover:bg-indigo-500 disabled:opacity-50 text-white rounded-lg text-xs font-bold transition flex items-center gap-1.5"
|
||||
>
|
||||
<span v-if="isSubmitting" class="animate-spin text-xs">🌀</span>
|
||||
<span v-else>✨</span>
|
||||
Оптимизировать через AI
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
show: { type: Boolean, default: false },
|
||||
isSubmitting: { type: Boolean, default: false }
|
||||
})
|
||||
|
||||
const emit = defineEmits([
|
||||
'close',
|
||||
'submit-smart-manual',
|
||||
'submit-smart-ai',
|
||||
'submit-grow-manual',
|
||||
'submit-grow-ai',
|
||||
'submit-dilts-manual',
|
||||
'submit-dilts-ai',
|
||||
'submit-eisenhower-manual',
|
||||
'submit-eisenhower-ai'
|
||||
])
|
||||
|
||||
const wizardType = ref('smart')
|
||||
|
||||
const smartForm = ref({
|
||||
specific: '',
|
||||
measurable: '',
|
||||
achievable: '',
|
||||
relevant: '',
|
||||
deadline: ''
|
||||
})
|
||||
|
||||
const growForm = ref({
|
||||
goal: '',
|
||||
reality: '',
|
||||
options: '',
|
||||
wayForward: ''
|
||||
})
|
||||
|
||||
const diltsForm = ref({
|
||||
mission: '',
|
||||
identity: '',
|
||||
values: '',
|
||||
skills: '',
|
||||
actions: '',
|
||||
environment: ''
|
||||
})
|
||||
|
||||
const eisenhowerForm = ref({
|
||||
q1: '',
|
||||
q2: '',
|
||||
q3: '',
|
||||
q4: ''
|
||||
})
|
||||
|
||||
const resetForms = () => {
|
||||
smartForm.value = { specific: '', measurable: '', achievable: '', relevant: '', deadline: '' }
|
||||
growForm.value = { goal: '', reality: '', options: '', wayForward: '' }
|
||||
diltsForm.value = { mission: '', identity: '', values: '', skills: '', actions: '', environment: '' }
|
||||
eisenhowerForm.value = { q1: '', q2: '', q3: '', q4: '' }
|
||||
}
|
||||
|
||||
const submitManual = () => {
|
||||
if (wizardType.value === 'smart') {
|
||||
emit('submit-smart-manual', { ...smartForm.value })
|
||||
} else if (wizardType.value === 'grow') {
|
||||
emit('submit-grow-manual', { ...growForm.value })
|
||||
} else if (wizardType.value === 'dilts') {
|
||||
emit('submit-dilts-manual', { ...diltsForm.value })
|
||||
} else if (wizardType.value === 'eisenhower') {
|
||||
emit('submit-eisenhower-manual', { ...eisenhowerForm.value })
|
||||
}
|
||||
resetForms()
|
||||
}
|
||||
|
||||
const submitAI = () => {
|
||||
if (wizardType.value === 'smart') {
|
||||
if (!smartForm.value.specific.trim()) {
|
||||
alert('Пожалуйста, введите хотя бы описание цели!')
|
||||
return
|
||||
}
|
||||
emit('submit-smart-ai', { ...smartForm.value })
|
||||
} else if (wizardType.value === 'grow') {
|
||||
if (!growForm.value.goal.trim()) {
|
||||
alert('Пожалуйста, введите хотя бы цель (Goal)!')
|
||||
return
|
||||
}
|
||||
emit('submit-grow-ai', { ...growForm.value })
|
||||
} else if (wizardType.value === 'dilts') {
|
||||
if (!diltsForm.value.mission.trim()) {
|
||||
alert('Пожалуйста, введите хотя бы описание миссии!')
|
||||
return
|
||||
}
|
||||
emit('submit-dilts-ai', { ...diltsForm.value })
|
||||
} else if (wizardType.value === 'eisenhower') {
|
||||
if (!eisenhowerForm.value.q1.trim() && !eisenhowerForm.value.q2.trim() && !eisenhowerForm.value.q3.trim() && !eisenhowerForm.value.q4.trim()) {
|
||||
alert('Пожалуйста, введите задачи хотя бы в один из квадрантов!')
|
||||
return
|
||||
}
|
||||
emit('submit-eisenhower-ai', { ...eisenhowerForm.value })
|
||||
}
|
||||
}
|
||||
|
||||
// Expose reset in case parent wants to reset from outside
|
||||
defineExpose({ resetForms })
|
||||
</script>
|
||||
@@ -0,0 +1,318 @@
|
||||
<template>
|
||||
<div
|
||||
class="fixed bottom-0 left-0 right-0 z-30 transition-all duration-300 ease-in-out font-sans flex flex-col"
|
||||
:class="[isOpen ? 'h-80' : 'h-10']"
|
||||
>
|
||||
<!-- Handle / Toggle Bar -->
|
||||
<div
|
||||
@click="isOpen = !isOpen"
|
||||
class="h-10 bg-slate-900 border-t border-slate-700/80 hover:bg-slate-800 cursor-pointer flex items-center px-4 justify-between select-none transition"
|
||||
>
|
||||
<div class="flex items-center gap-2 text-slate-200 font-bold text-xs">
|
||||
<span>📅</span>
|
||||
<span>Диаграмма Ганта</span>
|
||||
<span class="px-1.5 py-0.5 bg-slate-800 text-[10px] text-slate-400 rounded-full border border-slate-700 font-medium">
|
||||
Задач: {{ tasks.length }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-slate-400 text-xs">
|
||||
{{ isOpen ? '▼ Свернуть' : '▲ Развернуть' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline Body -->
|
||||
<div
|
||||
v-if="isOpen"
|
||||
class="flex-1 bg-slate-950/95 backdrop-blur-md border-t border-slate-800 flex overflow-hidden min-w-0"
|
||||
>
|
||||
<!-- Task Titles Sidebar (Left) -->
|
||||
<div class="w-64 border-r border-slate-800 flex flex-col shrink-0">
|
||||
<div class="h-8 border-b border-slate-800 bg-slate-900/40 px-3 flex items-center text-[10px] font-bold text-slate-500 uppercase tracking-wider">
|
||||
Задача
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto divide-y divide-slate-900/60 scrollbar-thin">
|
||||
<div
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
@click="$emit('select-node', task.id)"
|
||||
class="h-10 px-3 flex items-center text-xs text-slate-300 hover:text-white hover:bg-slate-800/30 cursor-pointer transition select-none truncate font-medium"
|
||||
title="Кликните для фокусировки на холсте"
|
||||
>
|
||||
<span class="mr-1.5">{{ getStatusIcon(task.data.state) }}</span>
|
||||
<span class="truncate">{{ task.data.label || 'Без названия' }}</span>
|
||||
</div>
|
||||
<div v-if="tasks.length === 0" class="p-6 text-center text-xs text-slate-600">
|
||||
Нет задач на холсте
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Timeline Chart (Right) -->
|
||||
<div class="flex-1 flex flex-col overflow-hidden min-w-0">
|
||||
<!-- Date Columns Header -->
|
||||
<div
|
||||
class="h-8 border-b border-slate-800 bg-slate-900/40 flex overflow-x-auto overflow-y-hidden select-none scrollbar-none"
|
||||
ref="timelineHeader"
|
||||
@scroll="syncScrollLeft"
|
||||
>
|
||||
<div
|
||||
v-for="(day, idx) in timelineDays"
|
||||
:key="idx"
|
||||
class="h-full border-r border-slate-900/40 shrink-0 flex flex-col items-center justify-center text-[9px] font-semibold text-slate-400"
|
||||
:style="{ width: `${dayWidth}px` }"
|
||||
>
|
||||
<span>{{ day.dayOfWeek }}</span>
|
||||
<span class="text-slate-500 font-normal">{{ day.dateStr }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Task Bars Body -->
|
||||
<div
|
||||
class="flex-1 overflow-auto divide-y divide-slate-900/60 scrollbar-thin"
|
||||
ref="timelineBody"
|
||||
@scroll="syncScrollRight"
|
||||
>
|
||||
<div
|
||||
v-for="task in tasks"
|
||||
:key="task.id"
|
||||
class="h-10 relative flex items-center bg-slate-950/20 hover:bg-slate-900/30 transition group"
|
||||
>
|
||||
<!-- Timeline Grid Background Lines -->
|
||||
<div class="absolute inset-0 flex pointer-events-none">
|
||||
<div
|
||||
v-for="idx in timelineDays.length"
|
||||
:key="idx"
|
||||
class="h-full border-r border-slate-900/40 shrink-0"
|
||||
:style="{ width: `${dayWidth}px` }"
|
||||
></div>
|
||||
</div>
|
||||
|
||||
<!-- Task Gantt Bar -->
|
||||
<div
|
||||
v-if="hasValidDates(task)"
|
||||
@click="$emit('select-node', task.id)"
|
||||
class="absolute h-5 rounded-md border text-[10px] font-bold text-white px-2 flex items-center truncate cursor-pointer transition shadow-md hover:scale-[1.01] z-10"
|
||||
:class="getBarClasses(task)"
|
||||
:style="getBarStyles(task)"
|
||||
>
|
||||
<span class="truncate">{{ task.data.label }}</span>
|
||||
<!-- Tooltip on hover -->
|
||||
<div class="absolute bottom-6 left-0 hidden group-hover:block bg-slate-900 border border-slate-700 text-[10px] rounded p-2 text-slate-300 w-48 shadow-xl pointer-events-none whitespace-normal leading-relaxed z-20">
|
||||
<div class="font-bold text-white truncate mb-1">{{ task.data.label }}</div>
|
||||
<div>Статус: <span class="font-medium text-slate-200">{{ getStatusLabel(task.data.state) }}</span></div>
|
||||
<div>Приоритет: <span class="font-medium text-slate-200">{{ getPriorityLabel(task.data.priority) }}</span></div>
|
||||
<div v-if="task.data.assigned_to">Исполнитель: <span class="font-medium text-slate-200">{{ task.data.assigned_to }}</span></div>
|
||||
<div class="text-[9px] text-slate-500 mt-1 border-t border-slate-800 pt-1">
|
||||
С: {{ formatDate(task.data.start_date) }}<br/>
|
||||
По: {{ formatDate(task.data.alarm) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div v-if="tasks.length === 0" class="h-full flex items-center justify-center text-xs text-slate-600">
|
||||
Создайте задачи и задайте им даты начала/дедлайны
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
elements: { type: Array, default: () => [] }
|
||||
})
|
||||
|
||||
defineEmits(['select-node'])
|
||||
|
||||
const isOpen = ref(false)
|
||||
const dayWidth = 60 // Width of one day column in pixels
|
||||
|
||||
const timelineHeader = ref(null)
|
||||
const timelineBody = ref(null)
|
||||
|
||||
// Scroll synchronization between header and body
|
||||
const syncScrollLeft = (e) => {
|
||||
if (timelineBody.value) {
|
||||
timelineBody.value.scrollLeft = e.target.scrollLeft
|
||||
}
|
||||
}
|
||||
const syncScrollRight = (e) => {
|
||||
if (timelineHeader.value) {
|
||||
timelineHeader.value.scrollLeft = e.target.scrollLeft
|
||||
}
|
||||
}
|
||||
|
||||
// Filter tasks
|
||||
const tasks = computed(() => {
|
||||
return props.elements.filter(el => el.type === 'task')
|
||||
})
|
||||
|
||||
// Calculate overall min/max dates
|
||||
const dateRange = computed(() => {
|
||||
let minDate = new Date()
|
||||
let maxDate = new Date(Date.now() + 14 * 24 * 60 * 60 * 1000) // Default 14 days from now
|
||||
|
||||
const taskDates = []
|
||||
tasks.value.forEach(t => {
|
||||
if (t.data.start_date) taskDates.push(new Date(t.data.start_date))
|
||||
if (t.data.alarm) taskDates.push(new Date(t.data.alarm))
|
||||
})
|
||||
|
||||
if (taskDates.length > 0) {
|
||||
minDate = new Date(Math.min(...taskDates))
|
||||
maxDate = new Date(Math.max(...taskDates))
|
||||
}
|
||||
|
||||
// Add 2 days padding to sides
|
||||
minDate.setDate(minDate.getDate() - 2)
|
||||
maxDate.setDate(maxDate.getDate() + 5)
|
||||
|
||||
// Cap timeline length at 30 days to prevent rendering overflow
|
||||
const diffTime = Math.abs(maxDate - minDate)
|
||||
const diffDays = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
|
||||
if (diffDays > 30) {
|
||||
maxDate = new Date(minDate.getTime() + 30 * 24 * 60 * 60 * 1000)
|
||||
}
|
||||
|
||||
// Reset to midnight
|
||||
minDate.setHours(0, 0, 0, 0)
|
||||
maxDate.setHours(23, 59, 59, 999)
|
||||
|
||||
return { minDate, maxDate }
|
||||
})
|
||||
|
||||
// Generate list of days for timeline header
|
||||
const timelineDays = computed(() => {
|
||||
const days = []
|
||||
const current = new Date(dateRange.value.minDate)
|
||||
const end = dateRange.value.maxDate
|
||||
const ruDays = ['Вс', 'Пн', 'Вт', 'Ср', 'Чт', 'Пт', 'Сб']
|
||||
|
||||
while (current <= end) {
|
||||
days.push({
|
||||
date: new Date(current),
|
||||
dateStr: `${current.getDate().toString().padStart(2, '0')}.${(current.getMonth() + 1).toString().padStart(2, '0')}`,
|
||||
dayOfWeek: ruDays[current.getDay()]
|
||||
})
|
||||
current.setDate(current.getDate() + 1)
|
||||
}
|
||||
return days
|
||||
})
|
||||
|
||||
// Check if task has valid start and end date
|
||||
const hasValidDates = (task) => {
|
||||
return !!(task.data.start_date || task.data.alarm)
|
||||
}
|
||||
|
||||
// Format single date for tooltips
|
||||
const formatDate = (dateStr) => {
|
||||
if (!dateStr) return 'Не указано'
|
||||
const date = new Date(dateStr)
|
||||
return date.toLocaleString('ru-RU', { day: '2-digit', month: '2-digit', hour: '2-digit', minute: '2-digit' })
|
||||
}
|
||||
|
||||
// Calculate absolute left offset and width in percent or pixels for Gantt Bar
|
||||
const getBarStyles = (task) => {
|
||||
const minTime = dateRange.value.minDate.getTime()
|
||||
const maxTime = dateRange.value.maxDate.getTime()
|
||||
const totalDuration = maxTime - minTime
|
||||
|
||||
let start = task.data.start_date ? new Date(task.data.start_date).getTime() : Date.now()
|
||||
let end = task.data.alarm ? new Date(task.data.alarm).getTime() : start + 2 * 24 * 60 * 60 * 1000 // 2 days default
|
||||
|
||||
if (start > end) {
|
||||
// Correct order if user reversed dates
|
||||
const tmp = start
|
||||
start = end
|
||||
end = tmp
|
||||
}
|
||||
|
||||
// Boundary checks
|
||||
start = Math.max(start, minTime)
|
||||
end = Math.min(end, maxTime)
|
||||
|
||||
const leftPercent = ((start - minTime) / totalDuration) * 100
|
||||
const widthPercent = ((end - start) / totalDuration) * 100
|
||||
|
||||
// Calculate actual pixel offsets based on timeline width
|
||||
const totalDays = timelineDays.value.length
|
||||
const totalPixels = totalDays * dayWidth
|
||||
|
||||
const leftPx = (leftPercent / 100) * totalPixels
|
||||
const widthPx = Math.max((widthPercent / 100) * totalPixels, 20) // min 20px width
|
||||
|
||||
return {
|
||||
left: `${leftPx}px`,
|
||||
width: `${widthPx}px`
|
||||
}
|
||||
}
|
||||
|
||||
// Style colors based on task status
|
||||
const getBarClasses = (task) => {
|
||||
const state = task.data.state || 'todo'
|
||||
switch (state) {
|
||||
case 'completed':
|
||||
return 'bg-emerald-600/30 border-emerald-500 hover:bg-emerald-500/40 text-emerald-300'
|
||||
case 'in_progress':
|
||||
return 'bg-amber-600/30 border-amber-500 hover:bg-amber-500/40 text-amber-300'
|
||||
case 'review':
|
||||
return 'bg-indigo-600/30 border-indigo-500 hover:bg-indigo-500/40 text-indigo-300'
|
||||
case 'blocked':
|
||||
return 'bg-red-600/30 border-red-500 hover:bg-red-500/40 text-red-300'
|
||||
default:
|
||||
return 'bg-blue-600/30 border-blue-500 hover:bg-blue-500/40 text-blue-300'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusIcon = (state) => {
|
||||
switch (state) {
|
||||
case 'completed': return '✅'
|
||||
case 'in_progress': return '⏳'
|
||||
case 'review': return '👀'
|
||||
case 'blocked': return '⛔'
|
||||
default: return '📋'
|
||||
}
|
||||
}
|
||||
|
||||
const getStatusLabel = (state) => {
|
||||
switch (state) {
|
||||
case 'completed': return 'Завершено'
|
||||
case 'in_progress': return 'В работе'
|
||||
case 'review': return 'На проверке'
|
||||
case 'blocked': return 'Заблокировано'
|
||||
default: return 'К выполнению'
|
||||
}
|
||||
}
|
||||
|
||||
const getPriorityLabel = (priority) => {
|
||||
switch (priority) {
|
||||
case 'high': return 'Высокий'
|
||||
case 'low': return 'Низкий'
|
||||
default: return 'Средний'
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.scrollbar-thin::-webkit-scrollbar {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar-track {
|
||||
background: transparent;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb {
|
||||
background: #334155;
|
||||
border-radius: 3px;
|
||||
}
|
||||
.scrollbar-thin::-webkit-scrollbar-thumb:hover {
|
||||
background: #475569;
|
||||
}
|
||||
.scrollbar-none::-webkit-scrollbar {
|
||||
display: none;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,38 @@
|
||||
<template>
|
||||
<div v-if="userRole !== 'viewer'" class="w-56 bg-slate-900 border-r border-slate-700 flex flex-col shrink-0 z-20 font-sans">
|
||||
<div class="p-3 border-b border-slate-700">
|
||||
<h3 class="text-sm font-bold text-slate-200">Узлы</h3>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-2 space-y-1">
|
||||
<div v-for="nt in nodeTypes" :key="nt.type" draggable="true" @dragstart="onDragStart($event, nt)"
|
||||
class="p-2 bg-slate-800 rounded cursor-move hover:bg-slate-700 border border-slate-600 text-xs select-none">
|
||||
<div class="flex items-center gap-2">
|
||||
<span>{{ nt.icon }}</span>
|
||||
<div>
|
||||
<div class="font-medium text-slate-200">{{ nt.name }}</div>
|
||||
<div class="text-[10px] text-slate-500">{{ nt.description }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
const props = defineProps({
|
||||
userRole: { type: String, default: 'viewer' }
|
||||
})
|
||||
|
||||
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 синхронизация' }
|
||||
]
|
||||
|
||||
const onDragStart = (event, nodeType) => {
|
||||
event.dataTransfer.setData('application/vueflow', JSON.stringify(nodeType))
|
||||
}
|
||||
</script>
|
||||
@@ -0,0 +1,291 @@
|
||||
<template>
|
||||
<div class="w-60 bg-slate-900 border-l border-slate-700 flex flex-col shrink-0 z-20 font-sans">
|
||||
<div class="p-3 border-b border-slate-700">
|
||||
<h3 class="text-sm font-bold text-slate-300">Свойства</h3>
|
||||
</div>
|
||||
<div class="flex-1 overflow-y-auto p-3">
|
||||
<div v-if="node" class="space-y-3">
|
||||
<!-- SMART & GROW AI Coaching tools -->
|
||||
<div v-if="['text', 'task'].includes(node.type)" class="p-2.5 bg-slate-950/60 border border-indigo-500/30 rounded-lg space-y-2 mb-2">
|
||||
<div class="text-[10px] uppercase font-bold text-indigo-400 flex items-center gap-1">
|
||||
<span>✨</span> AI Коуч-Ассистент
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-1.5">
|
||||
<button
|
||||
@click="$emit('apply-smart-ai')"
|
||||
:disabled="isCoaching"
|
||||
class="py-1.5 px-2 bg-indigo-600 hover:bg-indigo-500 disabled:bg-slate-800 disabled:text-slate-600 text-white rounded text-[10px] font-bold flex flex-col items-center justify-center gap-1 transition text-center"
|
||||
title="Преобразовать цель в формат SMART при помощи ИИ"
|
||||
>
|
||||
<span v-if="isCoaching && currentCoachAction === 'smart'" class="animate-spin text-xs">🌀</span>
|
||||
<span v-else>🎯</span>
|
||||
<span>SMART</span>
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('apply-grow-ai')"
|
||||
:disabled="isCoaching"
|
||||
class="py-1.5 px-2 bg-teal-650 hover:bg-teal-600 disabled:bg-slate-800 disabled:text-slate-600 text-white rounded text-[10px] font-bold flex flex-col items-center justify-center gap-1 transition text-center"
|
||||
title="Построить цепочку GROW для проблемы при помощи ИИ"
|
||||
>
|
||||
<span v-if="isCoaching && currentCoachAction === 'grow'" class="animate-spin text-xs">🌀</span>
|
||||
<span v-else>🌱</span>
|
||||
<span>GROW</span>
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('apply-dilts-ai')"
|
||||
:disabled="isCoaching"
|
||||
class="py-1.5 px-2 bg-violet-600 hover:bg-violet-500 disabled:bg-slate-800 disabled:text-slate-600 text-white rounded text-[10px] font-bold flex flex-col items-center justify-center gap-1 transition text-center"
|
||||
title="Разложить проблему по логическим уровням Дилтса при помощи ИИ"
|
||||
>
|
||||
<span v-if="isCoaching && currentCoachAction === 'dilts'" class="animate-spin text-xs">🌀</span>
|
||||
<span v-else>📐</span>
|
||||
<span>Дилтс</span>
|
||||
</button>
|
||||
<button
|
||||
@click="$emit('apply-eisenhower-ai')"
|
||||
:disabled="isCoaching"
|
||||
class="py-1.5 px-2 bg-amber-600 hover:bg-amber-500 disabled:bg-slate-800 disabled:text-slate-600 text-white rounded text-[10px] font-bold flex flex-col items-center justify-center gap-1 transition text-center"
|
||||
title="Разложить задачи по матрице Эйзенхауэра при помощи ИИ"
|
||||
>
|
||||
<span v-if="isCoaching && currentCoachAction === 'eisenhower'" class="animate-spin text-xs">🌀</span>
|
||||
<span v-else>📊</span>
|
||||
<span>Эйзенхауэр</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Goal Test Section -->
|
||||
<div v-if="['text', 'task'].includes(node.type)" class="p-2.5 bg-slate-950/60 border border-emerald-500/30 rounded-lg space-y-2 mb-2">
|
||||
<div class="text-[10px] uppercase font-bold text-emerald-400 flex items-center justify-between cursor-pointer select-none" @click="showGoalTest = !showGoalTest">
|
||||
<span class="flex items-center gap-1">⚖️ Тест истинности цели</span>
|
||||
<span>{{ showGoalTest ? '▼' : '▲' }}</span>
|
||||
</div>
|
||||
<div v-if="showGoalTest" class="space-y-2 pt-1.5 border-t border-slate-800">
|
||||
<div v-for="(q, key) in goalTestQuestions" :key="key" class="space-y-1">
|
||||
<div class="flex justify-between items-center text-[9px] text-slate-400 font-medium leading-tight">
|
||||
<span :title="q.full" class="cursor-help border-b border-dashed border-slate-600">{{ q.short }}</span>
|
||||
<span class="text-emerald-400 font-bold shrink-0 ml-1">{{ goalTestScores[key] }}/10</span>
|
||||
</div>
|
||||
<input v-model.number="goalTestScores[key]" type="range" min="1" max="10" class="w-full h-1 bg-slate-800 rounded-lg appearance-none cursor-pointer accent-emerald-500" />
|
||||
</div>
|
||||
|
||||
<div class="pt-2 border-t border-slate-800 flex flex-col gap-1.5">
|
||||
<div class="flex justify-between items-center text-xs">
|
||||
<span class="text-slate-400 font-semibold">Итоговый балл:</span>
|
||||
<span class="font-bold" :class="goalTestTotalScore >= 60 ? 'text-emerald-400' : 'text-red-400'">
|
||||
{{ goalTestTotalScore }} / 80
|
||||
</span>
|
||||
</div>
|
||||
<div class="text-[9px] p-1.5 rounded leading-normal border" :class="goalTestTotalScore >= 60 ? 'bg-emerald-950/40 text-emerald-300 border-emerald-800/30' : 'bg-red-950/40 text-red-300 border-red-800/30'">
|
||||
{{ goalTestTotalScore >= 60 ? '🟢 Цель истинно Ваша! Отличная мотивация.' : '🔴 Внимание! Возможны проблемы с мотивацией (менее 60 баллов).' }}
|
||||
</div>
|
||||
<button @click="writeGoalTestToNode" class="w-full py-1.5 bg-emerald-600 hover:bg-emerald-500 text-white rounded text-[10px] font-bold transition">
|
||||
✍️ Записать результат в узел
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Тип узла</label>
|
||||
<select v-model="node.type" @change="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200">
|
||||
<option v-for="nt in nodeTypes" :key="nt.type" :value="nt.type">
|
||||
{{ nt.icon }} {{ nt.name }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Название</label>
|
||||
<input v-model="node.data.label" @blur="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
|
||||
</div>
|
||||
<div v-if="node.data.text !== undefined">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Текст</label>
|
||||
<textarea v-model="node.data.text" @blur="handleChange" rows="4" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none resize-none font-mono"></textarea>
|
||||
</div>
|
||||
<div v-if="node.data.file !== undefined">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Файл</label>
|
||||
<input v-model="node.data.file" @blur="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
|
||||
</div>
|
||||
<div v-if="node.type === 'task'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Статус Задачи</label>
|
||||
<select v-model="node.data.state" @change="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200">
|
||||
<option value="todo">📋 К выполнению (To Do)</option>
|
||||
<option value="in_progress">⏳ В работе (In Progress)</option>
|
||||
<option value="review">👀 На проверке (Review)</option>
|
||||
<option value="completed">✅ Завершено (Done)</option>
|
||||
<option value="blocked">⛔ Заблокировано (Blocked)</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="node.type === 'task'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Приоритет</label>
|
||||
<select v-model="node.data.priority" @change="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200">
|
||||
<option value="low">🧊 Низкий</option>
|
||||
<option value="medium">⚡ Средний</option>
|
||||
<option value="high">🔥 Высокий</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="node.type === 'task'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Исполнитель</label>
|
||||
<select v-model="node.data.assigned_to" @change="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200">
|
||||
<option value="">Не назначено</option>
|
||||
<option v-for="u in usersList" :key="u.username" :value="u.username">
|
||||
{{ u.displayName || u.username }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="node.type === 'task'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Дата начала</label>
|
||||
<input v-model="node.data.start_date" @blur="handleChange" type="datetime-local" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
|
||||
</div>
|
||||
<div v-if="node.type === 'task'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Дедлайн (Срок)</label>
|
||||
<input v-model="node.data.alarm" @blur="handleChange" type="datetime-local" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
|
||||
</div>
|
||||
<div v-if="node.type === 'webhook'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Webhook URL</label>
|
||||
<input v-model="node.data.url" @blur="handleChange" placeholder="https://api.example.com/..." class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none" />
|
||||
</div>
|
||||
<div v-if="node.type === 'webhook'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Метод</label>
|
||||
<select v-model="node.data.method" @change="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200">
|
||||
<option value="GET">GET</option>
|
||||
<option value="POST">POST</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="node.type === 'ai'">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">AI Промпт (Инструкция)</label>
|
||||
<textarea v-model="node.data.prompt" @blur="handleChange" rows="3" placeholder="Что должен сделать ИИ? (например: Отредактируй этот текст...)" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none resize-none font-mono"></textarea>
|
||||
</div>
|
||||
<div v-if="node.data.script_id !== undefined">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Скрипт</label>
|
||||
<input v-model="node.data.script_id" @blur="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none font-mono" />
|
||||
</div>
|
||||
<div v-if="node.type === 'file' || node.data.file !== undefined">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Ссылка на Карту</label>
|
||||
<select v-model="node.data.file" @change="handleChange" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none">
|
||||
<option value="">Не выбрано</option>
|
||||
<option v-for="c in canvasesList" :key="c.id" :value="c.id">
|
||||
🗺️ {{ c.name || c.id }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
<div v-if="node.data.prompt !== undefined">
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">AI Запрос</label>
|
||||
<textarea v-model="node.data.prompt" @blur="handleChange" rows="3" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200 focus:border-blue-500 outline-none resize-none font-mono"></textarea>
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Ширина</label>
|
||||
<input v-model.number="node.data.width" @blur="handleChange" type="number" min="100" max="800" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200" />
|
||||
</div>
|
||||
<div>
|
||||
<label class="text-[10px] uppercase tracking-wider text-slate-500 block mb-1">Высота</label>
|
||||
<input v-model.number="node.data.height" @blur="handleChange" type="number" min="50" max="600" class="w-full bg-slate-800 border border-slate-600 rounded px-2 py-1 text-xs text-slate-200" />
|
||||
</div>
|
||||
</div>
|
||||
<div v-else class="text-xs text-slate-500 text-center mt-8">
|
||||
Выберите узел для редактирования
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, computed, watch } from 'vue'
|
||||
|
||||
const props = defineProps({
|
||||
node: { type: Object, default: null },
|
||||
usersList: { type: Array, default: () => [] },
|
||||
canvasesList: { type: Array, default: () => [] },
|
||||
isCoaching: { type: Boolean, default: false },
|
||||
currentCoachAction: { type: String, default: '' }
|
||||
})
|
||||
|
||||
const emit = defineEmits(['change', 'apply-smart-ai', 'apply-grow-ai', 'apply-dilts-ai', 'apply-eisenhower-ai'])
|
||||
|
||||
const nodeTypes = [
|
||||
{ type: 'text', name: 'Текст', icon: '📝' },
|
||||
{ type: 'file', name: 'Файл', icon: '📄' },
|
||||
{ type: 'task', name: 'Задача', icon: '✅' },
|
||||
{ type: 'ai', name: 'AI Узел', icon: '🤖' },
|
||||
{ type: 'webhook', name: 'Webhook', icon: '🔗' },
|
||||
{ type: 'git', name: 'Git Синхр', icon: '🔄' }
|
||||
]
|
||||
|
||||
const handleChange = () => {
|
||||
emit('change')
|
||||
}
|
||||
|
||||
// Goal Validation Test logic
|
||||
const showGoalTest = ref(false)
|
||||
const goalTestScores = ref({
|
||||
q1: 5, q2: 5, q3: 5, q4: 5, q5: 5, q6: 5, q7: 5, q8: 5
|
||||
})
|
||||
|
||||
const goalTestQuestions = {
|
||||
q1: { short: '1. Видение успеха', full: 'Насколько легко и с удовольствием я вижу себя, достигнувшим цель?' },
|
||||
q2: { short: '2. Страсть к процессу', full: 'Эта цель вызывает у меня страсть сама по себе? Мне нравится процесс ее достижения? (Без учета внешних наград)' },
|
||||
q3: { short: '3. Доп. польза', full: 'Достижение этой цели принесет мне дополнительную пользу в жизни/других сферах?' },
|
||||
q4: { short: '4. Баланс жизни', full: 'Смогу ли я достигнуть эту цель, не обделяя другие сферы жизни (время, силы, здоровье)?' },
|
||||
q5: { short: '5. Радость сразу', full: 'Я смогу получать радость и пользу от достижения цели сразу, по ходу процесса?' },
|
||||
q6: { short: '6. Зона контроля', full: 'Действия по достижению этой цели находятся в рамках моей личной собственной деятельности?' },
|
||||
q7: { short: '7. Поддержка окружения', full: 'Мое окружение будет поддерживать меня и мои усилия?' },
|
||||
q8: { short: '8. Ресурсы и автономия', full: 'У меня есть достаточно ресурсов для достижения? Достижима ли она без посторонней помощи?' }
|
||||
}
|
||||
|
||||
const goalTestTotalScore = computed(() => {
|
||||
return Object.values(goalTestScores.value).reduce((a, b) => a + b, 0)
|
||||
})
|
||||
|
||||
// Watch selected node and try to parse existing test scores if they exist
|
||||
watch(() => props.node?.id, () => {
|
||||
showGoalTest.value = false
|
||||
// Reset scores to default
|
||||
goalTestScores.value = { q1: 5, q2: 5, q3: 5, q4: 5, q5: 5, q6: 5, q7: 5, q8: 5 }
|
||||
|
||||
// Try to parse existing score from node text if present
|
||||
if (props.node?.data?.text) {
|
||||
const text = props.node.data.text
|
||||
const match = text.match(/Результат теста цели: (\d+)\/80/)
|
||||
if (match) {
|
||||
const qMatches = {
|
||||
q1: text.match(/Видение успеха:\s*(\d+)/),
|
||||
q2: text.match(/Страсть к процессу:\s*(\d+)/),
|
||||
q3: text.match(/Доп. польза:\s*(\d+)/) || text.match(/Дополнительная польза:\s*(\d+)/),
|
||||
q4: text.match(/Баланс жизни:\s*(\d+)/),
|
||||
q5: text.match(/Радость сразу:\s*(\d+)/),
|
||||
q6: text.match(/Зона контроля:\s*(\d+)/) || text.match(/Личная зона контроля:\s*(\d+)/),
|
||||
q7: text.match(/Поддержка окружения:\s*(\d+)/),
|
||||
q8: text.match(/Ресурсы и автономия:\s*(\d+)/) || text.match(/Хватает ресурсов:\s*(\d+)/)
|
||||
}
|
||||
for (const [key, m] of Object.entries(qMatches)) {
|
||||
if (m) goalTestScores.value[key] = parseInt(m[1])
|
||||
}
|
||||
showGoalTest.value = true
|
||||
}
|
||||
}
|
||||
}, { immediate: true })
|
||||
|
||||
const writeGoalTestToNode = () => {
|
||||
if (!props.node) return
|
||||
|
||||
const text = props.node.data.text || props.node.data.label || ''
|
||||
|
||||
// Strip previous test results if present
|
||||
let cleanText = text.split('\n\n---\n### ⚖️ Результат теста цели:')[0]
|
||||
cleanText = cleanText.split('\n---\n### ⚖️ Результат теста цели:')[0]
|
||||
|
||||
const verdict = goalTestTotalScore.value >= 60 ? 'Ваша цель' : 'Чужая цель / Риск потери мотивации'
|
||||
const testMarkdown = `\n\n---\n### ⚖️ Результат теста цели: ${goalTestTotalScore.value}/80 (${verdict})\n` +
|
||||
`* **Видение успеха:** ${goalTestScores.value.q1}/10\n` +
|
||||
`* **Страсть к процессу:** ${goalTestScores.value.q2}/10\n` +
|
||||
`* **Доп. польза:** ${goalTestScores.value.q3}/10\n` +
|
||||
`* **Баланс жизни:** ${goalTestScores.value.q4}/10\n` +
|
||||
`* **Радость сразу:** ${goalTestScores.value.q5}/10\n` +
|
||||
`* **Зона контроля:** ${goalTestScores.value.q6}/10\n` +
|
||||
`* **Поддержка окружения:** ${goalTestScores.value.q7}/10\n` +
|
||||
`* **Ресурсы и автономия:** ${goalTestScores.value.q8}/10`
|
||||
|
||||
props.node.data.text = cleanText + testMarkdown
|
||||
handleChange()
|
||||
}
|
||||
</script>
|
||||
@@ -12,9 +12,11 @@ interface User {
|
||||
export const useAuth = () => {
|
||||
const token = useCookie<string | null>('xflow_token', {
|
||||
maxAge: 60 * 60 * 24 * 7,
|
||||
sameSite: 'strict'
|
||||
sameSite: 'lax'
|
||||
})
|
||||
|
||||
const isInitializing = useState('auth_initializing', () => true)
|
||||
|
||||
const user = computed<User | null>(() => {
|
||||
if (!token.value) return null
|
||||
try {
|
||||
@@ -55,8 +57,10 @@ export const useAuth = () => {
|
||||
}
|
||||
|
||||
const initAuth = () => {
|
||||
if (process.server) return
|
||||
|
||||
const auth = getAuth()
|
||||
onAuthStateChanged(auth, async (firebaseUser) => {
|
||||
const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
|
||||
// If user is authenticated in Firebase but we lost our backend token, try to restore
|
||||
if (firebaseUser && !token.value) {
|
||||
try {
|
||||
@@ -68,13 +72,15 @@ export const useAuth = () => {
|
||||
token.value = res.token
|
||||
|
||||
if (useRoute().path === '/login') {
|
||||
navigateTo('/')
|
||||
await navigateTo('/')
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Failed to restore backend auth from Firebase', e)
|
||||
}
|
||||
}
|
||||
isInitializing.value = false
|
||||
})
|
||||
return unsubscribe
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -82,6 +88,7 @@ export const useAuth = () => {
|
||||
user,
|
||||
isAuthenticated,
|
||||
isAdmin,
|
||||
isInitializing,
|
||||
googleSignIn,
|
||||
logout,
|
||||
initAuth
|
||||
|
||||
+10
-4
@@ -1,6 +1,13 @@
|
||||
<template>
|
||||
<div class="min-h-screen flex items-center justify-center p-4">
|
||||
<div class="w-full max-w-sm bg-slate-800 border border-slate-700 rounded-xl p-6 shadow-xl text-center">
|
||||
<!-- Splash / Loader while checking Firebase session -->
|
||||
<div v-if="isInitializing" class="text-center animate-pulse">
|
||||
<div class="text-6xl mb-4">🚀</div>
|
||||
<div class="text-slate-400 font-medium">Проверка авторизации...</div>
|
||||
</div>
|
||||
|
||||
<!-- Login UI -->
|
||||
<div v-else class="w-full max-w-sm bg-slate-800 border border-slate-700 rounded-xl p-6 shadow-xl text-center">
|
||||
<div class="mb-6">
|
||||
<div class="text-5xl mb-4">🚀</div>
|
||||
<h1 class="text-2xl font-bold mb-2">XFlow</h1>
|
||||
@@ -16,7 +23,7 @@
|
||||
<path fill="#4285F4" d="M22.56 12.25c0-.78-.07-1.53-.2-2.25H12v4.26h5.92a5.06 5.06 0 0 1-2.2 3.32v2.77h3.57c2.08-1.92 3.28-4.74 3.28-8.1z"/>
|
||||
<path fill="#34A853" d="M12 23c2.97 0 5.46-.98 7.28-2.66l-3.57-2.77c-.98.66-2.23 1.06-3.71 1.06-2.86 0-5.29-1.93-6.16-4.53H2.18v2.84C3.99 20.53 7.7 23 12 23z"/>
|
||||
<path fill="#FBBC05" d="M5.84 14.09c-.22-.66-.35-1.36-.35-2.09s.13-1.43.35-2.09V7.07H2.18C1.43 8.55 1 10.22 1 12s.43 3.45 1.18 4.93l2.85-2.22.81-.62z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.99 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
<path fill="#EA4335" d="M12 5.38c1.62 0 3.06.56 4.21 1.64l3.15-3.15C17.45 2.09 14.97 1 12 1 7.7 1 3.47 2.18 7.07l3.66 2.84c.87-2.6 3.3-4.53 6.16-4.53z"/>
|
||||
</svg>
|
||||
{{ loading ? 'Вход...' : 'Войти через Google' }}
|
||||
</button>
|
||||
@@ -33,7 +40,7 @@ definePageMeta({
|
||||
layout: 'default'
|
||||
})
|
||||
|
||||
const { googleSignIn, initAuth, isAuthenticated } = useAuth()
|
||||
const { googleSignIn, initAuth, isAuthenticated, isInitializing } = useAuth()
|
||||
const loading = ref(false)
|
||||
|
||||
const signInWithGoogle = async () => {
|
||||
@@ -52,7 +59,6 @@ onMounted(() => {
|
||||
if (isAuthenticated.value) {
|
||||
navigateTo('/')
|
||||
}
|
||||
initAuth()
|
||||
})
|
||||
|
||||
watch(isAuthenticated, (val) => {
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { requireAuth } from '~/server/utils/auth'
|
||||
import { generateAIResponse } from '~/server/utils/ai'
|
||||
|
||||
export default defineEventHandler(async (event) => {
|
||||
const user = requireAuth(event)
|
||||
const body = await readBody(event)
|
||||
const { action, text, model } = body
|
||||
|
||||
if (!action || !text) {
|
||||
throw createError({ statusCode: 400, message: 'Action and text are required' })
|
||||
}
|
||||
|
||||
console.log(`[AI Coach] action=${action} userId=${user.id}`)
|
||||
|
||||
try {
|
||||
if (action === 'smart') {
|
||||
const systemPrompt = `You are an expert executive coach. Your job is to transform any vague goal or statement into a high-quality, fully detailed S.M.A.R.T. goal. Keep your response in Russian.
|
||||
Use Markdown headers for each element: Specific (Конкретная), Measurable (Измеримая), Achievable (Достижимая), Relevant (Значимая), Time-bound (Ограниченная во времени).
|
||||
For Measurable, include a clear checklist with markdown checkboxes (- [ ]) for metrics. At the end, write a brief, inspiring summary.`
|
||||
|
||||
const prompt = `Пожалуйста, преобразуй следующую цель в формат S.M.A.R.T.
|
||||
Исходная цель: "${text}"`
|
||||
|
||||
const result = await generateAIResponse(prompt, systemPrompt, model)
|
||||
return { success: true, result }
|
||||
|
||||
} else if (action === 'grow') {
|
||||
const systemPrompt = `You are a professional business coach. Your job is to perform a structured coaching analysis of the user's problem or goal using the G.R.O.W. framework. Keep all content in Russian.
|
||||
You must return the response strictly as a JSON object with the following keys:
|
||||
{
|
||||
"goal": "Markdown text. Description of the reformulated Goal (G) starting with # 🎯 Цель (Goal).\\n\\nAnswer these questions:\\n- Какой именно цели я хочу достичь?\\n- Какой результат я хочу получить?\\n- Почему достижение этой цели важно?",
|
||||
"options": "Markdown text. Options & Obstacles (O) starting with # 🚧 Варианты (Options).\\n\\nAnswer these questions:\\n- Что именно я могу изменить?\\n- Какие ресурсы мне нужны?\\n- Кто мне может помочь?\\n- А что еще?\\nInclude a checkbox list (- [ ]).",
|
||||
"reality": "Markdown text. Description of the current Situation/Reality (R) starting with # 🔍 Реальность (Reality).\\n\\nAnswer these questions:\\n- Какова текущая ситуация?\\n- Какие препятствия я вижу?\\n- Что мне мешает начать?\\n- Какие у меня уже есть ресурсы?",
|
||||
"wayForward": "Markdown text. Action Plan / Way Forward (W) starting with # 🏁 План действий (Way Forward).\\n\\nAnswer these questions:\\n- Какие самые важные действия необходимо предпринять?\\n- Что я сделаю в первую очередь? Какие сроки?\\nInclude a checkbox list (- [ ])."
|
||||
}
|
||||
Do NOT include any markdown code blocks (like \`\`\`json) or any extra text before or after the JSON. Return only the JSON object.`
|
||||
|
||||
const prompt = `Проведи GROW-коучинг анализ для следующей проблемы/цели: "${text}"`
|
||||
|
||||
const aiResponse = await generateAIResponse(prompt, systemPrompt, model)
|
||||
|
||||
// Robust JSON extraction
|
||||
let jsonText = aiResponse.trim()
|
||||
if (jsonText.startsWith('```json')) {
|
||||
jsonText = jsonText.substring(7)
|
||||
} else if (jsonText.startsWith('```')) {
|
||||
jsonText = jsonText.substring(3)
|
||||
}
|
||||
if (jsonText.endsWith('```')) {
|
||||
jsonText = jsonText.substring(0, jsonText.length - 3)
|
||||
}
|
||||
jsonText = jsonText.trim()
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
return { success: true, result: parsed }
|
||||
} catch (parseErr) {
|
||||
console.error('Failed to parse AI G.R.O.W. JSON response:', aiResponse)
|
||||
// Fallback: if JSON parsing fails, wrap the text into fields
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
goal: `# 🎯 Цель (Goal)\n\n${text}`,
|
||||
reality: `# 🔍 Реальность (Reality)\n\nНе удалось автоматически разбить ответ AI на JSON. Вот оригинальный ответ:\n\n${aiResponse}`,
|
||||
options: `# 🚧 Варианты\n\n- Рассмотреть альтернативные пути решения вручную`,
|
||||
wayForward: `# 🏁 План действий\n\n- [ ] Детализировать шаги вручную`
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (action === 'dilts') {
|
||||
const systemPrompt = `You are a professional life and business coach. Your job is to perform a structured analysis of the user's goal or situation using Robert Dilts' Logical Levels (Пирамида логических уровней Дилтса). Keep all content in Russian.
|
||||
You must return the response strictly as a JSON object with the following keys:
|
||||
{
|
||||
"mission": "Markdown text starting with # 🌟 Миссия (Mission).\\n\\nAnswer these questions:\\n- Ради кого?\\n- Кому еще это важно?\\n(Уровень БЫТЬ)",
|
||||
"identity": "Markdown text starting with # 👤 Идентичность (Identity).\\n\\nAnswer these questions:\\n- Кто я?",
|
||||
"values": "Markdown text starting with # 💎 Ценности (Values).\\n\\nAnswer these questions:\\n- Почему это важно?",
|
||||
"skills": "Markdown text starting with # 🧠 Навыки (Skills).\\n\\nAnswer these questions:\\n- Как? Каким образом?",
|
||||
"actions": "Markdown text starting with # 🎬 Действия (Actions).\\n\\nAnswer these questions:\\n- Что делать? (Уровень ДЕЛАТЬ)",
|
||||
"environment": "Markdown text starting with # 🌍 Окружение (Environment).\\n\\nAnswer these questions:\\n- Где? Когда? С кем? (Уровень ИМЕТЬ)"
|
||||
}
|
||||
Do NOT include any markdown code blocks (like \`\`\`json) or any extra text before or after the JSON. Return only the JSON object.`
|
||||
|
||||
const prompt = `Проведи коучинговый анализ по логическим уровням Дилтса для следующего запроса: "${text}"`
|
||||
|
||||
const aiResponse = await generateAIResponse(prompt, systemPrompt, model)
|
||||
|
||||
// Robust JSON extraction
|
||||
let jsonText = aiResponse.trim()
|
||||
if (jsonText.startsWith('```json')) {
|
||||
jsonText = jsonText.substring(7)
|
||||
} else if (jsonText.startsWith('```')) {
|
||||
jsonText = jsonText.substring(3)
|
||||
}
|
||||
if (jsonText.endsWith('```')) {
|
||||
jsonText = jsonText.substring(0, jsonText.length - 3)
|
||||
}
|
||||
jsonText = jsonText.trim()
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
return { success: true, result: parsed }
|
||||
} catch (parseErr) {
|
||||
console.error('Failed to parse AI Dilts JSON response:', aiResponse)
|
||||
// Fallback
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
mission: `# 🌟 Миссия (Mission)\n\nРади кого? Кому еще это важно? (Уровень БЫТЬ)\n\n${text}`,
|
||||
identity: `# 👤 Идентичность (Identity)\n\nКто я?`,
|
||||
values: `# 💎 Ценности (Values)\n\nПочему это важно?`,
|
||||
skills: `# 🧠 Навыки (Skills)\n\nКак? Каким образом?`,
|
||||
actions: `# 🎬 Действия (Actions)\n\nЧто делать? (Уровень ДЕЛАТЬ)\n\nНе удалось автоматически разбить ответ AI на JSON:\n\n${aiResponse}`,
|
||||
environment: `# 🌍 Окружение (Environment)\n\nГде? Когда? С кем? (Уровень ИМЕТЬ)`
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (action === 'eisenhower') {
|
||||
const systemPrompt = `You are a professional productivity expert. Your job is to analyze the user's tasks or list of ideas and prioritize them using the Eisenhower Matrix (Матрица Эйзенхауэра). Keep all content in Russian.
|
||||
You must return the response strictly as a JSON object with the following keys:
|
||||
{
|
||||
"q1": "Markdown text starting with # 🔥 Важно и Срочно (Do First).\\n\\nIdentify tasks that are both urgent and important. Focus on crises, deadlines, and crucial problems.",
|
||||
"q2": "Markdown text starting with # ⚡ Важно, но Несрочно (Schedule).\\n\\nIdentify tasks that are important but not urgent. Focus on long-term planning, personal growth, and relationships.",
|
||||
"q3": "Markdown text starting with # 🧊 Неважно, но Срочно (Delegate).\\n\\nIdentify tasks that are urgent but not important. Focus on interruptions, minor meetings, and tasks that can be delegated.",
|
||||
"q4": "Markdown text starting with # 💤 Неважно и Несрочно (Eliminate).\\n\\nIdentify tasks that are neither urgent nor important. Focus on time-wasting activities, distractions, and low-value tasks."
|
||||
}
|
||||
Do NOT include any markdown code blocks (like \`\`\`json) or any extra text before or after the JSON. Return only the JSON object.`
|
||||
|
||||
const prompt = `Распредели следующие задачи/цели по квадрантам Матрицы Эйзенхауэра: "${text}"`
|
||||
|
||||
const aiResponse = await generateAIResponse(prompt, systemPrompt, model)
|
||||
|
||||
// Robust JSON extraction
|
||||
let jsonText = aiResponse.trim()
|
||||
if (jsonText.startsWith('```json')) {
|
||||
jsonText = jsonText.substring(7)
|
||||
} else if (jsonText.startsWith('```')) {
|
||||
jsonText = jsonText.substring(3)
|
||||
}
|
||||
if (jsonText.endsWith('```')) {
|
||||
jsonText = jsonText.substring(0, jsonText.length - 3)
|
||||
}
|
||||
jsonText = jsonText.trim()
|
||||
|
||||
try {
|
||||
const parsed = JSON.parse(jsonText)
|
||||
return { success: true, result: parsed }
|
||||
} catch (parseErr) {
|
||||
console.error('Failed to parse AI Eisenhower JSON response:', aiResponse)
|
||||
// Fallback
|
||||
return {
|
||||
success: true,
|
||||
result: {
|
||||
q1: `# 🔥 Важно и Срочно (Do First)\n\n- [ ] Срочные приоритетные задачи\n\n${text}`,
|
||||
q2: `# ⚡ Важно, но Несрочно (Schedule)\n\n- [ ] Задачи планирования`,
|
||||
q3: `# 🧊 Неважно, но Срочно (Delegate)\n\n- [ ] Задачи для делегирования\n\nНе удалось разобрать ответ ИИ:\n\n${aiResponse}`,
|
||||
q4: `# 💤 Неважно и Несрочно (Eliminate)\n\n- [ ] Задачи, которые можно отложить`
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
throw createError({ statusCode: 400, message: `Unsupported action: ${action}` })
|
||||
}
|
||||
} catch (err: any) {
|
||||
console.error('[AI Coach Error]:', err)
|
||||
throw createError({
|
||||
statusCode: 500,
|
||||
message: err.message || 'AI coach generation failed'
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,97 @@
|
||||
// Transform Obsidian canvas to Vue Flow elements
|
||||
export const transformToFlowElements = (canvasData: any) => {
|
||||
const nodes = (canvasData.nodes || []).map((node: any) => ({
|
||||
id: node.id,
|
||||
type: node.type === 'file' ? 'file' : node.type === 'ai' ? 'ai' : node.type === 'git' ? 'git' : node.type === 'task' ? 'task' : node.type === 'webhook' ? 'webhook' : 'text',
|
||||
position: { x: Number(node.x) || 0, y: Number(node.y) || 0 },
|
||||
data: {
|
||||
...node,
|
||||
width: node.width || 300,
|
||||
height: node.height || 200,
|
||||
label: node.label || node.text?.slice(0, 20) || node.file || 'Node',
|
||||
// Ensure specific fields exist so the properties panel shows them
|
||||
...(node.type === 'task' && {
|
||||
text: node.text ?? '',
|
||||
state: node.state ?? 'todo',
|
||||
priority: node.priority ?? 'medium',
|
||||
assigned_to: node.assigned_to ?? '',
|
||||
start_date: node.start_date ?? '',
|
||||
alarm: node.alarm ?? '',
|
||||
script_id: node.script_id ?? ''
|
||||
}),
|
||||
...(node.type === 'text' && { text: node.text ?? '' }),
|
||||
...(node.type === 'file' && { file: node.file ?? '' }),
|
||||
...(node.type === 'ai' && { prompt: node.prompt ?? '' }),
|
||||
...(node.type === 'webhook' && {
|
||||
script_id: node.script_id ?? '',
|
||||
url: node.url ?? '',
|
||||
method: node.method ?? 'POST'
|
||||
}),
|
||||
...(node.type === 'git' && {
|
||||
repo: node.repo ?? '',
|
||||
branch: node.branch ?? 'main',
|
||||
message: node.message ?? ''
|
||||
})
|
||||
},
|
||||
style: {
|
||||
width: `${node.width || 300}px`,
|
||||
height: `${node.height || 200}px`,
|
||||
background: node.style?.background || '#1e293b',
|
||||
border: `1px solid ${node.style?.border || '#334155'}`,
|
||||
borderRadius: '8px'
|
||||
}
|
||||
}))
|
||||
|
||||
const edges = (canvasData.edges || []).map((edge: any) => ({
|
||||
id: edge.id,
|
||||
source: edge.fromNode,
|
||||
target: edge.toNode,
|
||||
sourceHandle: edge.fromSide,
|
||||
targetHandle: edge.toSide,
|
||||
animated: true,
|
||||
style: { stroke: '#475569', strokeWidth: 2 }
|
||||
}))
|
||||
|
||||
return [...nodes, ...edges]
|
||||
}
|
||||
|
||||
// Transform Vue Flow elements back to Obsidian canvas
|
||||
export const transformToObsidianCanvas = (flowElements: any[]) => {
|
||||
const nodes = flowElements.filter(el => el.position !== undefined).map(node => ({
|
||||
id: node.id,
|
||||
type: node.type === 'file' ? 'file' : node.type === 'task' ? 'task' : node.type === 'ai' ? 'ai' : node.type === 'webhook' ? 'webhook' : node.type === 'git' ? 'git' : 'text',
|
||||
x: Math.round(Number(node.position.x) || 0),
|
||||
y: Math.round(Number(node.position.y) || 0),
|
||||
width: node.data.width || 300,
|
||||
height: node.data.height || 200,
|
||||
text: node.data.text,
|
||||
label: node.data.label,
|
||||
file: node.data.file,
|
||||
state: node.data.state,
|
||||
priority: node.data.priority,
|
||||
assigned_to: node.data.assigned_to,
|
||||
start_date: node.data.start_date,
|
||||
alarm: node.data.alarm,
|
||||
script_id: node.data.script_id,
|
||||
prompt: node.data.prompt,
|
||||
url: node.data.url,
|
||||
method: node.data.method,
|
||||
repo: node.data.repo,
|
||||
branch: node.data.branch,
|
||||
message: node.data.message,
|
||||
style: {
|
||||
background: node.style?.background || '#1e293b',
|
||||
border: node.style?.border?.replace('1px solid ', '') || '#334155'
|
||||
}
|
||||
}))
|
||||
|
||||
const edges = flowElements.filter(el => el.source && el.target).map(edge => ({
|
||||
id: edge.id,
|
||||
fromNode: edge.source,
|
||||
fromSide: edge.sourceHandle || 'right',
|
||||
toNode: edge.target,
|
||||
toSide: edge.targetHandle || 'left'
|
||||
}))
|
||||
|
||||
return { nodes, edges }
|
||||
}
|
||||
Reference in New Issue
Block a user