Análisis IA en dos fases: épicas primero, luego HUs vinculadas con epic_development
- project-analyzer.ts: dividido en analyzeProjectEpics() y analyzeProjectHUs() Fase 1: genera solo épicas con linkedHuTitles Fase 2: genera HUs dentro de épicas con epicName - saveEpicDrafts / saveHUDrafts separados para cada tipo - DashboardView: dos botones '1. Generar Épicas' y '2. Generar HUs' - pushDraft épica: al crear en KAPPA, actualiza metadata de HUs vinculadas con epicDevelopment - pushDraft HU: envía epic_development + payload completo (feature, sprint, asignado_a, etc.) - project_gap prompt: instrucciones separadas para FASE 1 (épicas) y FASE 2 (HUs)
This commit is contained in:
@@ -2,7 +2,6 @@ import { callAI } from '@/services/ai'
|
||||
import { getPrompt } from '@/services/prompts-db'
|
||||
import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db'
|
||||
import { saveDraft, createDraftId } from '@/services/hu-drafts-db'
|
||||
import { generateAndSavePlan } from '@/services/qa-analyzer'
|
||||
import type { EnrichedUserStory } from '@/stores/workitems'
|
||||
|
||||
export interface AnalysisHU {
|
||||
@@ -10,6 +9,11 @@ export interface AnalysisHU {
|
||||
description: string
|
||||
acceptance_criteria: string[]
|
||||
priority: string
|
||||
story_points?: number
|
||||
type?: string
|
||||
feature?: string
|
||||
sprint?: number
|
||||
epicName?: string // nombre de la épica a la que pertenece
|
||||
}
|
||||
|
||||
export interface AnalysisEpic {
|
||||
@@ -20,18 +24,18 @@ export interface AnalysisEpic {
|
||||
estimatedEnd?: string
|
||||
}
|
||||
|
||||
interface AnalysisResult {
|
||||
hus: AnalysisHU[]
|
||||
interface AnalysisEpicsResult {
|
||||
epics: AnalysisEpic[]
|
||||
summary: string
|
||||
rationale: string // explicación de por qué estas épicas
|
||||
}
|
||||
|
||||
interface AnalysisHUsResult {
|
||||
hus: AnalysisHU[]
|
||||
summary: string
|
||||
}
|
||||
|
||||
export async function analyzeProject(
|
||||
projectId: number,
|
||||
projectName: string,
|
||||
existingHUs: EnrichedUserStory[],
|
||||
signal?: AbortSignal,
|
||||
): Promise<AnalysisResult> {
|
||||
async function buildProjectContext(projectId: number, projectName: string, existingHUs: EnrichedUserStory[]) {
|
||||
const sessions = await getSessionsByProject(projectId)
|
||||
const state = await getProjectState(projectId)
|
||||
|
||||
@@ -48,9 +52,9 @@ export async function analyzeProject(
|
||||
})
|
||||
}
|
||||
|
||||
const context = {
|
||||
return {
|
||||
projectName,
|
||||
existingHUs: existingHUs.map(h => ({ t: h._cleanTitle || h.title, s: h.status, p: h.priority })),
|
||||
existingHUs: existingHUs.map(h => ({ t: h._cleanTitle || h.title, s: h.status, p: h.priority, e: h._assignedName || '' })),
|
||||
sessions: sessionsWithSummaries.slice(-10).reverse(),
|
||||
projectState: state ? {
|
||||
summary: state.summary?.slice(0, 500),
|
||||
@@ -58,39 +62,150 @@ export async function analyzeProject(
|
||||
tasks: (safeParse<{ status: string }[]>(state.tasks, [])).filter(t => t.status !== 'completada').slice(0, 30),
|
||||
} : null,
|
||||
}
|
||||
}
|
||||
|
||||
const userContent = `Contexto completo del proyecto en JSON:\n${JSON.stringify(context, null, 0)}`
|
||||
// ─── FASE 1: Generar solo Épicas ──────────────────────
|
||||
|
||||
console.log(`[Alpha] Project analysis — ${projectId}, ${existingHUs.length} HUs existentes, ${sessions.length} sesiones`)
|
||||
export async function analyzeProjectEpics(
|
||||
projectId: number,
|
||||
projectName: string,
|
||||
existingHUs: EnrichedUserStory[],
|
||||
existingEpics: string[], // épicas que ya están en KAPPA
|
||||
signal?: AbortSignal,
|
||||
): Promise<AnalysisEpicsResult> {
|
||||
const context = await buildProjectContext(projectId, projectName, existingHUs)
|
||||
|
||||
const userContent = `Contexto del proyecto:\n${JSON.stringify(context, null, 0)}\n\nÉpicas ya existentes en KAPPA: ${existingEpics.join(', ') || 'Ninguna'}`
|
||||
|
||||
console.log(`[Alpha] Phase 1 — Analyzing epics for ${projectName}, ${existingEpics.length} existentes`)
|
||||
|
||||
const systemPrompt = await getPrompt('project_gap')
|
||||
// Pasamos una instrucción adicional para que solo genere épicas en esta fase
|
||||
const phasePrompt = `[FASE 1: SOLO ÉPICAS]
|
||||
Analizá TODO el contexto del proyecto.
|
||||
Identificá las épicas necesarias. Una épica agrupa funcionalidades de un mismo tema (ej: "Módulo de Pagos", "Scrapers", "Dashboard").
|
||||
Revisá si las épicas ya existentes en KAPPA cubren las necesidades.
|
||||
Si una épica ya existe en KAPPA, NO la generes de nuevo.
|
||||
Si el acta de inicio, sesiones o documentación mencionan funcionalidades que no están cubiertas por ninguna épica existente, proponé nuevas épicas.
|
||||
Para cada épica, listá los títulos tentativos de las HUs que pertenecerían a ella (linkedHuTitles).
|
||||
|
||||
${systemPrompt}
|
||||
|
||||
IMPORTANTE: Respondé SOLO con épicas en esta fase. NO generes HUs aún.`
|
||||
|
||||
const content = await callAI(
|
||||
[{ role: 'system', content: systemPrompt }, { role: 'user', content: userContent }],
|
||||
[{ role: 'system', content: phasePrompt }, { role: 'user', content: userContent }],
|
||||
0.3, 8192, signal,
|
||||
)
|
||||
|
||||
try {
|
||||
const jsonStr = extractJSON(content)
|
||||
const result: AnalysisResult = JSON.parse(jsonStr)
|
||||
console.log(`[Alpha] Analysis result: ${result.hus.length} HUs, ${result.epics?.length || 0} épicas`)
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
const result: AnalysisEpicsResult = {
|
||||
epics: parsed.epics || parsed.epic || [],
|
||||
summary: parsed.summary || '',
|
||||
rationale: parsed.rationale || parsed.summary || '',
|
||||
}
|
||||
console.log(`[Alpha] Phase 1 complete: ${result.epics.length} épicas propuestas`)
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error('[Alpha] Failed to parse analysis. Raw:', content)
|
||||
throw new Error('No se pudo procesar el análisis del proyecto')
|
||||
console.error('[Alpha] Failed to parse epics analysis. Raw:', content)
|
||||
throw new Error('No se pudieron generar las épicas')
|
||||
}
|
||||
}
|
||||
|
||||
export async function saveAsDrafts(
|
||||
// ─── FASE 2: Generar HUs dentro de las épicas confirmadas ──
|
||||
|
||||
export async function analyzeProjectHUs(
|
||||
projectId: number,
|
||||
analysis: AnalysisResult,
|
||||
projectName: string,
|
||||
existingHUs: EnrichedUserStory[],
|
||||
confirmedEpics: AnalysisEpic[], // épicas que el usuario aceptó y ya están o serán enviadas a KAPPA
|
||||
signal?: AbortSignal,
|
||||
): Promise<AnalysisHUsResult> {
|
||||
const context = await buildProjectContext(projectId, projectName, existingHUs)
|
||||
|
||||
const epicsDetail = confirmedEpics.map(e =>
|
||||
`- ${e.name}: ${e.description?.slice(0, 200)} (HUs sugeridas: ${e.linkedHuTitles.join(', ')})`
|
||||
).join('\n')
|
||||
|
||||
const userContent = `Contexto del proyecto:\n${JSON.stringify(context, null, 0)}\n\nÉpicas confirmadas:\n${epicsDetail}`
|
||||
|
||||
console.log(`[Alpha] Phase 2 — Generating HUs for ${confirmedEpics.length} epics in ${projectName}`)
|
||||
|
||||
const systemPrompt = await getPrompt('project_gap')
|
||||
const phasePrompt = `[FASE 2: GENERAR HUs]
|
||||
Las siguientes épicas ya están definidas. Tu tarea es generar las HUs (feature, task, US, bug, spike, etc.) que pertenecen a cada épica.
|
||||
Cada HU debe estar vinculada a UNA épica existente.
|
||||
Usá el campo "epicName" para indicar a qué épica pertenece cada HU.
|
||||
No generes HUs duplicadas con las que ya existen en KAPPA.
|
||||
Incluí para cada HU: título, descripción, criterios de aceptación, prioridad, story points, tipo, feature, sprint estimado.
|
||||
|
||||
${systemPrompt}
|
||||
|
||||
IMPORTANTE: Respondé SOLO con HUs en esta fase. NO generes épicas nuevas.`
|
||||
|
||||
const content = await callAI(
|
||||
[{ role: 'system', content: phasePrompt }, { role: 'user', content: userContent }],
|
||||
0.3, 8192, signal,
|
||||
)
|
||||
|
||||
try {
|
||||
const jsonStr = extractJSON(content)
|
||||
const parsed = JSON.parse(jsonStr)
|
||||
const result: AnalysisHUsResult = {
|
||||
hus: parsed.hus || [],
|
||||
summary: parsed.summary || '',
|
||||
}
|
||||
console.log(`[Alpha] Phase 2 complete: ${result.hus.length} HUs generadas`)
|
||||
return result
|
||||
} catch (e) {
|
||||
console.error('[Alpha] Failed to parse HUs analysis. Raw:', content)
|
||||
throw new Error('No se pudieron generar las HUs')
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Guardar drafts ───────────────────────────────────
|
||||
|
||||
export async function saveEpicDrafts(
|
||||
projectId: number,
|
||||
epics: AnalysisEpic[],
|
||||
existingHUs: EnrichedUserStory[],
|
||||
sourceSessionId?: number,
|
||||
): Promise<{ saved: number; skipped: number }> {
|
||||
let saved = 0
|
||||
let skipped = 0
|
||||
|
||||
// Guardar HUs
|
||||
for (const hu of analysis.hus || []) {
|
||||
for (const epic of epics) {
|
||||
const normalizedName = epic.name.toLowerCase().trim()
|
||||
const isDuplicate = existingHUs.some(ex => (ex._cleanTitle || ex.title).toLowerCase().trim() === normalizedName)
|
||||
if (isDuplicate) { skipped++; continue }
|
||||
|
||||
await saveDraft({
|
||||
id: createDraftId(), projectId, title: epic.name,
|
||||
description: epic.description, acceptanceCriteria: '',
|
||||
priority: 'Media', type: 'E',
|
||||
metadata: JSON.stringify({
|
||||
linkedHuTitles: epic.linkedHuTitles,
|
||||
estimatedStart: epic.estimatedStart,
|
||||
estimatedEnd: epic.estimatedEnd,
|
||||
}),
|
||||
syncStatus: 'draft', createdAt: new Date().toISOString(),
|
||||
})
|
||||
saved++
|
||||
}
|
||||
|
||||
return { saved, skipped }
|
||||
}
|
||||
|
||||
export async function saveHUDrafts(
|
||||
projectId: number,
|
||||
hus: AnalysisHU[],
|
||||
existingHUs: EnrichedUserStory[],
|
||||
): Promise<{ saved: number; skipped: number }> {
|
||||
let saved = 0
|
||||
let skipped = 0
|
||||
|
||||
for (const hu of hus) {
|
||||
const normalizedTitle = hu.title.toLowerCase().trim()
|
||||
const isDuplicate = existingHUs.some(ex => {
|
||||
const et = (ex._cleanTitle || ex.title).toLowerCase().trim()
|
||||
@@ -102,27 +217,14 @@ export async function saveAsDrafts(
|
||||
await saveDraft({
|
||||
id: draftId, projectId, title: hu.title,
|
||||
description: hu.description, acceptanceCriteria: hu.acceptance_criteria.join('\n'),
|
||||
priority: hu.priority, type: 'U', metadata: '{}',
|
||||
sourceSessionId, syncStatus: 'draft', createdAt: new Date().toISOString(),
|
||||
})
|
||||
// QA plan: fire-and-forget para no bloquear el guardado
|
||||
generateAndSavePlan(projectId, draftId, hu.title, hu.description, hu.acceptance_criteria.join('\n'))
|
||||
.catch(e => console.error(`[Alpha] QA auto-gen failed for ${hu.title}:`, e))
|
||||
saved++
|
||||
}
|
||||
|
||||
// Guardar épicas
|
||||
for (const epic of analysis.epics || []) {
|
||||
const normalizedName = epic.name.toLowerCase().trim()
|
||||
const isDuplicate = existingHUs.some(ex => (ex._cleanTitle || ex.title).toLowerCase().trim() === normalizedName)
|
||||
if (isDuplicate) { skipped++; continue }
|
||||
|
||||
await saveDraft({
|
||||
id: createDraftId(), projectId, title: epic.name,
|
||||
description: epic.description, acceptanceCriteria: '',
|
||||
priority: 'Media', type: 'E',
|
||||
metadata: JSON.stringify({ linkedHuTitles: epic.linkedHuTitles, estimatedStart: epic.estimatedStart, estimatedEnd: epic.estimatedEnd }),
|
||||
sourceSessionId, syncStatus: 'draft', createdAt: new Date().toISOString(),
|
||||
priority: hu.priority, type: hu.type === 'task' ? 'T' : hu.type === 'bug' ? 'B' : hu.type === 'feature' ? 'F' : 'U',
|
||||
metadata: JSON.stringify({
|
||||
epicName: hu.epicName,
|
||||
feature: hu.feature,
|
||||
sprint: hu.sprint,
|
||||
storyPoints: hu.story_points,
|
||||
}),
|
||||
syncStatus: 'draft', createdAt: new Date().toISOString(),
|
||||
})
|
||||
saved++
|
||||
}
|
||||
|
||||
+39
-12
@@ -40,38 +40,65 @@ Responde SOLO con JSON válido en este formato:
|
||||
},
|
||||
project_gap: {
|
||||
label: 'Análisis de brechas del proyecto',
|
||||
content: `Eres un analista funcional experto. Tu tarea es analizar TODO el contexto de un proyecto y generar las Épicas e Historias de Usuario (HUs) que faltan.
|
||||
content: `Eres un analista funcional experto en metodologías ágiles.
|
||||
Trabajás en DOS FASES separadas. Cada fase tiene sus propias instrucciones.
|
||||
|
||||
Reglas:
|
||||
Reglas generales:
|
||||
1. Analizá TODA la información disponible: sesiones, resúmenes, estado del proyecto, HUs existentes
|
||||
2. Identificá requisitos, funcionalidades, mejoras o bugs que NO estén cubiertos
|
||||
3. Agrupá HUs relacionadas en Épicas. Cada épica agrupa funcionalidades de un mismo tema
|
||||
4. Cada HU debe tener: título claro, descripción, criterios de aceptación verificables
|
||||
5. No generes duplicados. Compará con la lista existente
|
||||
6. Priorizá según urgencia implícita (Alta/Media/Baja)
|
||||
7. Si todo ya está cubierto, devolvé arreglos vacíos
|
||||
8. Respondé SOLO con JSON válido
|
||||
3. No generes duplicados. Compará con TODO lo que ya existe
|
||||
4. Respondé SOLO con JSON válido
|
||||
5. Si todo ya está cubierto, devolvé arreglos vacíos
|
||||
|
||||
Formato de respuesta:
|
||||
--- FASE 1: Generar Épicas ---
|
||||
Instrucciones específicas:
|
||||
- Identificá las épicas necesarias agrupando funcionalidades por tema
|
||||
- Revisá si las épicas ya existentes cubren las necesidades
|
||||
- Si una épica ya existe en KAPPA, NO la generes de nuevo
|
||||
- Para cada épica, listá los títulos tentativos de las HUs que pertenecerían a ella (linkedHuTitles)
|
||||
- Incluí un campo "rationale" explicando por qué proponés cada épica
|
||||
- NO generes HUs en esta fase
|
||||
|
||||
Formato de respuesta FASE 1:
|
||||
{
|
||||
"epics": [
|
||||
{
|
||||
"name": "Nombre de la Épica",
|
||||
"name": "Nombre de la Épica (ej: Módulo de Pagos)",
|
||||
"description": "Descripción de la épica",
|
||||
"linkedHuTitles": ["Título HU 1", "Título HU 2"],
|
||||
"estimatedStart": "YYYY-MM-DD",
|
||||
"estimatedEnd": "YYYY-MM-DD"
|
||||
}
|
||||
],
|
||||
"summary": "Resumen del análisis de épicas",
|
||||
"rationale": "Explicación de por qué estas épicas y no otras"
|
||||
}
|
||||
|
||||
--- FASE 2: Generar HUs dentro de Épicas ---
|
||||
Instrucciones específicas:
|
||||
- Las épicas ya están definidas. Generá las HUs que pertenecen a cada una.
|
||||
- Cada HU debe tener un campo "epicName" indicando a qué épica pertenece
|
||||
- Tipos de HU: feature, task, US (historia de usuario), bug, spike
|
||||
- Incluí: título, descripción, criterios de aceptación, prioridad, story points, tipo, feature, sprint estimado
|
||||
- No generes HUs duplicadas con las existentes
|
||||
- NO generes épicas en esta fase
|
||||
|
||||
Formato de respuesta FASE 2:
|
||||
{
|
||||
"hus": [
|
||||
{
|
||||
"title": "Título de la HU",
|
||||
"description": "Descripción detallada",
|
||||
"acceptance_criteria": ["Criterio 1", "Criterio 2"],
|
||||
"priority": "Alta|Media|Baja"
|
||||
"priority": "Alta|Media|Baja",
|
||||
"story_points": 3,
|
||||
"type": "feature|task|bug|spike",
|
||||
"feature": "Nombre de la feature",
|
||||
"sprint": 12,
|
||||
"epicName": "Nombre de la épica a la que pertenece"
|
||||
}
|
||||
],
|
||||
"summary": "Resumen del análisis"
|
||||
"summary": "Resumen de las HUs generadas"
|
||||
}`,
|
||||
},
|
||||
session: {
|
||||
|
||||
@@ -177,6 +177,14 @@ export const useWorkItemsStore = defineStore('workitems', () => {
|
||||
|
||||
// 4. Actualizar UI con datos frescos de KAPPA
|
||||
userStories.value = stories.map(hu => enrichHU(hu, id))
|
||||
if (userStories.value.length > 0) {
|
||||
console.log('[Alpha DEBUG] 1ra HU desc:', {
|
||||
id: userStories.value[0].id,
|
||||
title: userStories.value[0].title?.slice(0, 40),
|
||||
hasDescription: !!userStories.value[0].description,
|
||||
descLength: userStories.value[0].description?.length,
|
||||
})
|
||||
}
|
||||
|
||||
epics.value = epicData.map(epic => ({
|
||||
...epic,
|
||||
|
||||
+160
-45
@@ -11,7 +11,7 @@ import { Button } from '@/components/ui/button'
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog'
|
||||
import HuDrafts from '@/components/HuDrafts.vue'
|
||||
import AiProjectChat from '@/components/AiProjectChat.vue'
|
||||
import { analyzeProject, saveAsDrafts } from '@/services/project-analyzer'
|
||||
import { analyzeProjectEpics, analyzeProjectHUs, saveEpicDrafts, saveHUDrafts } from '@/services/project-analyzer'
|
||||
import { getDrafts, deleteDraft, type HuDraftRecord } from '@/services/hu-drafts-db'
|
||||
import { kappa } from '@/services/kappa-api'
|
||||
import { generateAndSavePlan, getQAPlans, type HUQAPlan } from '@/services/qa-analyzer'
|
||||
@@ -289,7 +289,7 @@ async function pushDraft(d: HuDraftRecord) {
|
||||
let endpoint: string, body: string
|
||||
|
||||
if (d.type === 'E') {
|
||||
// Push épica
|
||||
// Push épica primero — KAPPA asigna el ID
|
||||
const meta = JSON.parse(d.metadata || '{}')
|
||||
const linkedHuIds: number[] = []
|
||||
for (const huTitle of meta.linkedHuTitles || []) {
|
||||
@@ -308,24 +308,51 @@ async function pushDraft(d: HuDraftRecord) {
|
||||
status: false,
|
||||
})
|
||||
} else {
|
||||
// Push HU
|
||||
// Push HU con epic_development si está vinculada a una épica
|
||||
const meta = JSON.parse(d.metadata || '{}')
|
||||
const epicDevId = meta.epicDevelopment || null
|
||||
endpoint = '/api/userstorys/create/'
|
||||
body = JSON.stringify({
|
||||
initiative: String(d.projectId), title: d.title,
|
||||
initiative: String(d.projectId),
|
||||
title: d.title,
|
||||
description: d.description ? `<p>${d.description.replace(/\n/g, '</p><p>')}</p>` : '',
|
||||
criterios_aceptacion: d.acceptanceCriteria ? `<p>${d.acceptanceCriteria.replace(/\n/g, '</p><p>')}</p>` : '',
|
||||
story_points: String(d.story_points ?? ''),
|
||||
story_points: d.story_points ?? null,
|
||||
priority: d.priority === 'Alta' ? '1' : d.priority === 'Baja' ? '3' : '2',
|
||||
sprint: '', asignado_a: [], client_taker: null,
|
||||
characterization_hu: '', has_impairment: false,
|
||||
epic_development: null, feature: '',
|
||||
initial_date: null, end_date: null,
|
||||
sprint: meta.sprint ?? '',
|
||||
asignado_a: meta.asignado_a ?? [],
|
||||
client_taker: meta.clientTaker ?? null,
|
||||
characterization_hu: meta.characterizationHu ?? '',
|
||||
has_impairment: false,
|
||||
epic_development: epicDevId,
|
||||
feature: meta.feature ?? '',
|
||||
initial_date: null,
|
||||
end_date: meta.endDate ?? null,
|
||||
status: meta.status ?? 1,
|
||||
is_planned: false,
|
||||
})
|
||||
}
|
||||
|
||||
const res = await fetch(endpoint, { method: 'POST', headers, body })
|
||||
if (res.ok) {
|
||||
if (d.type !== 'E') {
|
||||
if (d.type === 'E') {
|
||||
// Épica creada → capturar ID y vincular HUs pendientes
|
||||
const created = await res.json()
|
||||
const epicKappaId = created.id
|
||||
const meta = JSON.parse(d.metadata || '{}')
|
||||
for (const huTitle of meta.linkedHuTitles || []) {
|
||||
const linkedDraft = drafts.value.find(x =>
|
||||
x.type !== 'E' && x.syncStatus === 'draft' &&
|
||||
x.title.toLowerCase().trim() === huTitle.toLowerCase().trim()
|
||||
)
|
||||
if (linkedDraft) {
|
||||
const linkedMeta = JSON.parse(linkedDraft.metadata || '{}')
|
||||
linkedMeta.epicDevelopment = String(epicKappaId)
|
||||
linkedDraft.metadata = JSON.stringify(linkedMeta)
|
||||
await dbSaveDraft(linkedDraft)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const created = await res.json()
|
||||
d.kappaId = created.id || undefined
|
||||
}
|
||||
@@ -365,44 +392,118 @@ async function discardDraft(id: string) {
|
||||
await loadDrafts()
|
||||
}
|
||||
|
||||
// ─── Project analysis ────────────────────────────────────
|
||||
// ─── Project analysis — Two-phase ─────────────────────────
|
||||
const phase = ref<'idle' | 'epics' | 'hus' | 'done'>('idle')
|
||||
const analyzing = ref(false)
|
||||
const analysisAbort = ref<AbortController | null>(null)
|
||||
const analysisMessage = ref('')
|
||||
const analysisResult = ref<{ saved: number; skipped: number } | null>(null)
|
||||
const analysisSummary = ref('')
|
||||
|
||||
function cancelAnalysis() {
|
||||
analysisAbort.value?.abort()
|
||||
analyzing.value = false
|
||||
analysisAbort.value = null
|
||||
analysisSummary.value = 'Análisis cancelado'
|
||||
analysisMessage.value = ''
|
||||
phase.value = 'idle'
|
||||
}
|
||||
|
||||
async function runAnalysis() {
|
||||
// Obtener épicas que ya existen en KAPPA
|
||||
function getExistingEpicNames(): string[] {
|
||||
return workItems.epics.map(e => (e._cleanName || e.name || e.title || '').toLowerCase().trim()).filter(Boolean)
|
||||
}
|
||||
|
||||
// FASE 1: Generar épicas
|
||||
async function runPhaseEpics() {
|
||||
if (!project.value) return
|
||||
phase.value = 'epics'
|
||||
analyzing.value = true
|
||||
analysisAbort.value = new AbortController()
|
||||
analysisResult.value = null
|
||||
analysisSummary.value = ''
|
||||
analysisMessage.value = 'Analizando contexto y generando épicas...'
|
||||
|
||||
try {
|
||||
const result = await analyzeProject(project.value.id, project.value.name || '', workItems.userStories, analysisAbort.value?.signal)
|
||||
analysisSummary.value = result.summary
|
||||
const existingEpics = getExistingEpicNames()
|
||||
const result = await analyzeProjectEpics(
|
||||
project.value.id, project.value.name || '',
|
||||
workItems.userStories, existingEpics,
|
||||
analysisAbort.value?.signal,
|
||||
)
|
||||
analysisMessage.value = result.rationale
|
||||
|
||||
if (result.hus.length > 0) {
|
||||
const outcome = await saveAsDrafts(project.value.id, result, workItems.userStories)
|
||||
if (result.epics.length > 0) {
|
||||
const outcome = await saveEpicDrafts(project.value.id, result.epics, workItems.userStories)
|
||||
analysisResult.value = outcome
|
||||
await loadDrafts()
|
||||
} else {
|
||||
analysisResult.value = { saved: 0, skipped: 0 }
|
||||
analysisMessage.value = 'No se identificaron nuevas épicas necesarias.'
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.name === 'AbortError' || e.message?.includes('aborted')) {
|
||||
analysisSummary.value = 'Análisis cancelado'
|
||||
analysisMessage.value = 'Análisis cancelado'
|
||||
} else {
|
||||
console.error('[Alpha] Analysis error:', e)
|
||||
analysisSummary.value = `Error: ${e.message}`
|
||||
console.error('[Alpha] Phase 1 error:', e)
|
||||
analysisMessage.value = `Error: ${e.message}`
|
||||
}
|
||||
analysisResult.value = { saved: 0, skipped: 0 }
|
||||
phase.value = 'idle'
|
||||
} finally {
|
||||
analyzing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
// FASE 2: Generar HUs dentro de las épicas
|
||||
async function runPhaseHUs() {
|
||||
if (!project.value) return
|
||||
phase.value = 'hus'
|
||||
analyzing.value = true
|
||||
analysisAbort.value = new AbortController()
|
||||
analysisResult.value = null
|
||||
analysisMessage.value = 'Generando HUs dentro de las épicas...'
|
||||
|
||||
try {
|
||||
// Obtener las épicas confirmadas desde los drafts de tipo épica
|
||||
const epicDrafts = drafts.value.filter(d => d.type === 'E')
|
||||
const confirmedEpics = epicDrafts.map(d => ({
|
||||
name: d.title,
|
||||
description: d.description,
|
||||
linkedHuTitles: (() => { try { return JSON.parse(d.metadata || '{}').linkedHuTitles || [] } catch { return [] } })(),
|
||||
}))
|
||||
|
||||
if (confirmedEpics.length === 0) {
|
||||
analysisMessage.value = 'No hay épicas en borrador. Primero generá y enviá las épicas a KAPPA.'
|
||||
analysisResult.value = { saved: 0, skipped: 0 }
|
||||
phase.value = 'idle'
|
||||
analyzing.value = false
|
||||
return
|
||||
}
|
||||
|
||||
const result = await analyzeProjectHUs(
|
||||
project.value.id, project.value.name || '',
|
||||
workItems.userStories, confirmedEpics,
|
||||
analysisAbort.value?.signal,
|
||||
)
|
||||
analysisMessage.value = result.summary
|
||||
|
||||
if (result.hus.length > 0) {
|
||||
const outcome = await saveHUDrafts(project.value.id, result.hus, workItems.userStories)
|
||||
analysisResult.value = outcome
|
||||
await loadDrafts()
|
||||
phase.value = 'done'
|
||||
} else {
|
||||
analysisResult.value = { saved: 0, skipped: 0 }
|
||||
analysisMessage.value = 'No se identificaron HUs nuevas. Todo parece cubierto.'
|
||||
phase.value = 'done'
|
||||
}
|
||||
} catch (e: any) {
|
||||
if (e.name === 'AbortError' || e.message?.includes('aborted')) {
|
||||
analysisMessage.value = 'Análisis cancelado'
|
||||
} else {
|
||||
console.error('[Alpha] Phase 2 error:', e)
|
||||
analysisMessage.value = `Error: ${e.message}`
|
||||
}
|
||||
analysisResult.value = { saved: 0, skipped: 0 }
|
||||
phase.value = 'idle'
|
||||
} finally {
|
||||
analyzing.value = false
|
||||
}
|
||||
@@ -529,45 +630,59 @@ const statusLabel = (status: unknown) => {
|
||||
@navigate-settings="emit('navigate-settings')"
|
||||
/>
|
||||
|
||||
<!-- Project Analysis -->
|
||||
<!-- Project Analysis — Two-phase -->
|
||||
<Card id="dashboard-analysis" class="border-dashed">
|
||||
<CardHeader class="pb-3 flex flex-row items-center justify-between">
|
||||
<CardTitle class="text-sm font-medium flex items-center gap-2">
|
||||
<Sparkles class="size-4" />
|
||||
Análisis completo del proyecto
|
||||
Análisis del proyecto
|
||||
</CardTitle>
|
||||
<div class="flex gap-2">
|
||||
<div v-if="!analyzing" class="flex gap-2">
|
||||
<Button
|
||||
v-if="analyzing"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="cancelAnalysis()"
|
||||
:disabled="analyzing || phase === 'done'"
|
||||
@click="runPhaseEpics()"
|
||||
>
|
||||
Cancelar
|
||||
<Sparkles class="size-3 mr-1" />
|
||||
1. Generar Épicas
|
||||
</Button>
|
||||
<Button
|
||||
size="sm"
|
||||
:disabled="analyzing"
|
||||
@click="runAnalysis()"
|
||||
:disabled="analyzing || phase === 'idle'"
|
||||
@click="runPhaseHUs()"
|
||||
>
|
||||
<Loader2 v-if="analyzing" class="size-4 mr-1 animate-spin" />
|
||||
<Sparkles v-else class="size-4 mr-1" />
|
||||
{{ analyzing ? 'Analizando...' : 'Generar HUs faltantes' }}
|
||||
<Sparkles class="size-3 mr-1" />
|
||||
2. Generar HUs
|
||||
</Button>
|
||||
</div>
|
||||
<Button
|
||||
v-if="analyzing"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
@click="cancelAnalysis()"
|
||||
>
|
||||
Cancelar
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent v-if="analysisResult" class="space-y-2 text-sm">
|
||||
<p class="text-muted-foreground">{{ analysisSummary }}</p>
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<span v-if="analysisResult.saved > 0" class="text-green-600 dark:text-green-400 flex items-center gap-1">
|
||||
<CheckCircle2 class="size-3" /> {{ analysisResult.saved }} borradores guardados
|
||||
</span>
|
||||
<span v-if="analysisResult.skipped > 0" class="text-amber-600 dark:text-amber-400 flex items-center gap-1">
|
||||
{{ analysisResult.skipped }} duplicadas saltadas
|
||||
</span>
|
||||
<span v-if="analysisResult.saved === 0 && analysisResult.skipped === 0" class="text-muted-foreground">
|
||||
Todo ya está cubierto. No se requieren nuevas HUs.
|
||||
</span>
|
||||
<CardContent class="space-y-3">
|
||||
<div v-if="analyzing" class="flex items-center gap-2 text-sm text-muted-foreground">
|
||||
<Loader2 class="size-4 animate-spin" />
|
||||
{{ analysisMessage }}
|
||||
</div>
|
||||
<div v-if="analysisResult" class="space-y-2 text-sm">
|
||||
<p class="text-muted-foreground">{{ analysisMessage }}</p>
|
||||
<div class="flex items-center gap-3 text-xs">
|
||||
<span v-if="analysisResult.saved > 0" class="text-green-600 dark:text-green-400 flex items-center gap-1">
|
||||
<CheckCircle2 class="size-3" /> {{ analysisResult.saved }} borradores guardados
|
||||
</span>
|
||||
<span v-if="analysisResult.skipped > 0" class="text-amber-600 dark:text-amber-400 flex items-center gap-1">
|
||||
{{ analysisResult.skipped }} duplicadas saltadas
|
||||
</span>
|
||||
<span v-if="analysisResult.saved === 0 && analysisResult.skipped === 0" class="text-muted-foreground">
|
||||
{{ phase === 'epics' ? 'No se requieren nuevas épicas.' : phase === 'hus' ? 'No se requieren nuevas HUs.' : '' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
Reference in New Issue
Block a user