feat: restructure into XFlow monorepo with Nuxt 4, Modular Onion Monolith, Android client, and Desktop CLI
This commit is contained in:
@@ -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>
|
||||
Reference in New Issue
Block a user