kin: KIN-059 Workflow new_project с выбором команды. При создании нового проекта через GUI или CLI директор описывает проект свободным текстом и выбирает галочками какие этапы research нужны: ☐ Business analyst (бизнес-модель, аудитория, монетизация) ☐ Market researcher (конкуренты, ниша, отзывы, сильные/слабые стороны) ☐ Legal researcher (юрисдикция, лицензии, KYC/AML, GDPR) ☐ Tech researcher (API, ограничения, стоимость, альтернативы) ☐ UX designer (анализ UX конкурентов, user journey, wireframes) ☐ Marketer (стратегия продвижения, SEO, conversion-паттерны) ☐ Architect (blueprint на основе одобренных research'ей) — всегда последний Architect включается автоматически если выбран хотя бы один researcher. Каждый выбранный этап — отдельная задача на review. Директор одобряет, отклоняет, или просит доисследовать (Revise). Следующий этап только после approve предыдущего. GUI: форма 'New Project' с описанием + чекбоксы ролей + кнопка 'Start Research'. CLI: kin new-project 'описание' --roles 'business,market,tech,architect'

This commit is contained in:
Gros Frumos 2026-03-16 09:30:00 +02:00
parent 75fee86110
commit 4188384f1b
7 changed files with 820 additions and 9 deletions

View file

@ -1,5 +1,5 @@
<script setup lang="ts">
import { ref, onMounted, computed, watch } from 'vue'
import { ref, onMounted, onUnmounted, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { api, type ProjectDetail, type AuditResult, type Phase } from '../api'
import Badge from '../components/Badge.vue'
@ -28,12 +28,32 @@ const rejectPhaseId = ref<number | null>(null)
const rejectReason = ref('')
const rejectError = ref('')
const rejectSaving = ref(false)
const startPhaseSaving = ref(false)
const approvePhaseSaving = ref(false)
let phasePollTimer: ReturnType<typeof setInterval> | null = null
function checkAndPollPhases() {
const hasRunning = phases.value.some(ph => ph.task?.status === 'in_progress')
if (hasRunning && !phasePollTimer) {
phasePollTimer = setInterval(async () => {
phases.value = await api.getPhases(props.id).catch(() => phases.value)
if (!phases.value.some(ph => ph.task?.status === 'in_progress')) {
clearInterval(phasePollTimer!)
phasePollTimer = null
}
}, 5000)
} else if (!hasRunning && phasePollTimer) {
clearInterval(phasePollTimer)
phasePollTimer = null
}
}
async function loadPhases() {
phasesLoading.value = true
phaseError.value = ''
try {
phases.value = await api.getPhases(props.id)
checkAndPollPhases()
} catch (e: any) {
phaseError.value = e.message
} finally {
@ -42,11 +62,27 @@ async function loadPhases() {
}
async function approvePhase(phaseId: number) {
approvePhaseSaving.value = true
try {
await api.approvePhase(phaseId)
await loadPhases()
} catch (e: any) {
phaseError.value = e.message
} finally {
approvePhaseSaving.value = false
}
}
async function startPhase() {
startPhaseSaving.value = true
phaseError.value = ''
try {
await api.startPhase(props.id)
await loadPhases()
} catch (e: any) {
phaseError.value = e.message
} finally {
startPhaseSaving.value = false
}
}
@ -242,6 +278,10 @@ onMounted(async () => {
await loadPhases()
})
onUnmounted(() => {
if (phasePollTimer) { clearInterval(phasePollTimer); phasePollTimer = null }
})
const taskCategories = computed(() => {
if (!project.value) return []
const cats = new Set(project.value.tasks.map(t => t.category).filter(Boolean) as string[])
@ -584,23 +624,37 @@ async function addDecision() {
class="text-xs text-blue-400 hover:text-blue-300 no-underline">
{{ ph.task_id }}
</router-link>
<template v-if="ph.status === 'active'">
<button @click="approvePhase(ph.id)"
class="px-2 py-0.5 text-xs bg-green-900/40 text-green-400 border border-green-800 rounded hover:bg-green-900">
Approve
<!-- Running indicator -->
<span v-if="ph.task?.status === 'in_progress'"
class="inline-block w-2 h-2 bg-blue-500 rounded-full animate-pulse"
title="Agent running..."></span>
<!-- Start Research button: phase ready but task not started yet -->
<button
v-if="(ph.status === 'active' || ph.status === 'revising') && ph.task?.status === 'pending'"
@click="startPhase"
:disabled="startPhaseSaving"
class="px-2 py-0.5 text-xs bg-blue-900/40 text-blue-400 border border-blue-800 rounded hover:bg-blue-900 disabled:opacity-50">
{{ startPhaseSaving ? 'Starting...' : (ph.status === 'revising' ? 'Re-run' : 'Start Research') }}
</button>
<!-- Approve/Revise/Reject: only when agent submitted work for review (decision #78) -->
<template v-if="(ph.status === 'active' || ph.status === 'revising') && ph.task?.status === 'review'">
<button @click="approvePhase(ph.id)" :disabled="approvePhaseSaving"
class="px-2 py-0.5 text-xs bg-green-900/40 text-green-400 border border-green-800 rounded hover:bg-green-900 disabled:opacity-50">
{{ approvePhaseSaving ? '...' : 'Approve' }}
</button>
<button @click="openRevise(ph.id)"
class="px-2 py-0.5 text-xs bg-yellow-900/40 text-yellow-400 border border-yellow-800 rounded hover:bg-yellow-900">
<button @click="openRevise(ph.id)" :disabled="approvePhaseSaving"
class="px-2 py-0.5 text-xs bg-yellow-900/40 text-yellow-400 border border-yellow-800 rounded hover:bg-yellow-900 disabled:opacity-50">
Revise
</button>
<button @click="openReject(ph.id)"
class="px-2 py-0.5 text-xs bg-red-900/40 text-red-400 border border-red-800 rounded hover:bg-red-900">
<button @click="openReject(ph.id)" :disabled="approvePhaseSaving"
class="px-2 py-0.5 text-xs bg-red-900/40 text-red-400 border border-red-800 rounded hover:bg-red-900 disabled:opacity-50">
Reject
</button>
</template>
</div>
</div>
<div v-if="ph.task?.title" class="mt-1 text-xs text-gray-500 ml-7">{{ ph.task.title }}</div>
<div v-if="ph.revise_comment" class="mt-1 text-xs text-yellow-600 ml-7">&#x27A4; {{ ph.revise_comment }}</div>
</div>
</div>