project-analyzer: analisis completo con contexto global + dedup

- services/project-analyzer.ts: analiza sesiones + resumenes + HUs existentes
- generateMissingHUs: proposicion inteligente sin duplicados
- createMissingHUs: crea en KAPPA solo las que no existen (comparacion por titulo)
- AiProjectChat: sin limite de HUs en contexto JSON compacto
- DashboardView: card 'Generar HUs faltantes' con resultado
- ai.ts: stripThinkTags() para eliminar bloques de razonamiento
This commit is contained in:
2026-05-28 14:19:13 -05:00
parent e950eb1285
commit eb4fae78b3
3 changed files with 279 additions and 54 deletions
+70 -2
View File
@@ -1,12 +1,14 @@
<script setup lang="ts">
import { computed, watch } from 'vue'
import { computed, watch, ref } from 'vue'
import { useI18n } from 'vue-i18n'
import { useProjectsStore } from '@/stores/projects'
import { useWorkItemsStore } from '@/stores/workitems'
import { getTypeLabel, getTypeColor, getTypeIcon } from '@/services/hierarchy'
import { Activity, FileText, Layers, Clock, Info, AlertTriangle, Plus, Brain } from 'lucide-vue-next'
import { Activity, FileText, Layers, Clock, Info, AlertTriangle, Plus, Brain, Sparkles, Loader2, CheckCircle2, XCircle } from 'lucide-vue-next'
import { Button } from '@/components/ui/button'
import HuDrafts from '@/components/HuDrafts.vue'
import AiProjectChat from '@/components/AiProjectChat.vue'
import { analyzeProject, createMissingHUs } from '@/services/project-analyzer'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'
import { Badge } from '@/components/ui/badge'
import { Skeleton } from '@/components/ui/skeleton'
@@ -27,6 +29,35 @@ const emit = defineEmits<{
'navigate-settings': []
}>()
// ─── Project analysis ────────────────────────────────────
const analyzing = ref(false)
const analysisResult = ref<{ created: number; skipped: number; errors: string[] } | null>(null)
const analysisSummary = ref('')
async function runAnalysis() {
if (!project.value) return
analyzing.value = true
analysisResult.value = null
analysisSummary.value = ''
try {
const result = await analyzeProject(project.value.id, project.value.name || '', workItems.userStories)
analysisSummary.value = result.summary
if (result.hus.length > 0) {
const outcome = await createMissingHUs(project.value.id, result, workItems.userStories)
analysisResult.value = outcome
await workItems.fetchWorkItems(project.value.id)
} else {
analysisResult.value = { created: 0, skipped: 0, errors: [] }
}
} catch (e: any) {
analysisResult.value = { created: 0, skipped: 0, errors: [e.message] }
} finally {
analyzing.value = false
}
}
const project = computed(() => projects.selected)
watch(
@@ -120,6 +151,7 @@ const statusLabel = (status: unknown) => {
<!-- AI Chat -->
<AiProjectChat
:project-id="project.id"
:project-name="project.name ?? ''"
:project-description="project.description ?? ''"
:epic-count="workItems.totalEpics"
@@ -127,6 +159,42 @@ const statusLabel = (status: unknown) => {
@navigate-settings="emit('navigate-settings')"
/>
<!-- Project Analysis -->
<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
</CardTitle>
<Button
size="sm"
:disabled="analyzing"
@click="runAnalysis()"
>
<Loader2 v-if="analyzing" class="size-4 mr-1 animate-spin" />
<Sparkles v-else class="size-4 mr-1" />
{{ analyzing ? 'Analizando...' : 'Generar HUs faltantes' }}
</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.created > 0" class="text-green-600 dark:text-green-400 flex items-center gap-1">
<CheckCircle2 class="size-3" /> {{ analysisResult.created }} HUs creadas
</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.created === 0 && analysisResult.skipped === 0 && analysisResult.errors.length === 0" class="text-muted-foreground">
Todo ya está cubierto. No se requieren nuevas HUs.
</span>
</div>
<div v-if="analysisResult.errors.length > 0" class="text-xs text-destructive space-y-0.5">
<p v-for="e in analysisResult.errors" :key="e"> {{ e }}</p>
</div>
</CardContent>
</Card>
<!-- Loading -->
<template v-if="workItems.loading">
<Skeleton class="h-8 w-1/3" />