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:
parent
75fee86110
commit
4188384f1b
7 changed files with 820 additions and 9 deletions
|
|
@ -162,6 +162,7 @@ export interface Phase {
|
|||
status: string
|
||||
task_id: string | null
|
||||
revise_count: number
|
||||
revise_comment: string | null
|
||||
created_at: string
|
||||
updated_at: string
|
||||
task?: Task | null
|
||||
|
|
@ -264,4 +265,6 @@ export const api = {
|
|||
post<Phase>(`/phases/${phaseId}/reject`, { reason }),
|
||||
revisePhase: (phaseId: number, comment: string) =>
|
||||
post<{ phase: Phase; new_task: Task }>(`/phases/${phaseId}/revise`, { comment }),
|
||||
startPhase: (projectId: string) =>
|
||||
post<{ status: string; phase_id: number; task_id: string }>(`/projects/${projectId}/phases/start`, {}),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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">➤ {{ ph.revise_comment }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue