QA plans al sugerir HU + cancel analisis + datepicker shadcn
- project-analyzer: saveAsDrafts genera QA plan por cada HU sugerida - DashboardView: cancelAnalysis con AbortController + mensaje limpio - HuDrafts: DatePicker con Calendar + Popover (shadcn-vue) - HuDrafts: formulario dinámico segun tipo (Epic vs HU/Feature/etc) - components/ui: Popover + Calendar creados - qa_plans: tabla separada (cubre drafts + user_stories existentes)
This commit is contained in:
@@ -5,6 +5,7 @@
|
|||||||
"": {
|
"": {
|
||||||
"name": "kappa-hub",
|
"name": "kappa-hub",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@internationalized/date": "^3.12.2",
|
||||||
"@lucide/vue": "^1.16.0",
|
"@lucide/vue": "^1.16.0",
|
||||||
"@tabler/icons-vue": "^3.44.0",
|
"@tabler/icons-vue": "^3.44.0",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
@@ -243,7 +244,7 @@
|
|||||||
|
|
||||||
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
"@img/sharp-win32-x64": ["@img/sharp-win32-x64@0.34.5", "", { "os": "win32", "cpu": "x64" }, "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw=="],
|
||||||
|
|
||||||
"@internationalized/date": ["@internationalized/date@3.12.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="],
|
"@internationalized/date": ["@internationalized/date@3.12.2", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-FY1Y+H64NDs+HAF6omlnWxm3mEpfgaCSWtL5l551ZZfImA+kGjPFgrnJrGjH6lfmLL0g8Z/mBu1R3kufeCp6Jw=="],
|
||||||
|
|
||||||
"@internationalized/number": ["@internationalized/number@3.6.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ=="],
|
"@internationalized/number": ["@internationalized/number@3.6.6", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-iFgmQaXHE0vytNfpLZWOC2mEJCBRzcUxt53Xf/yCXG93lRvqas237i3r7X4RKMwO3txiyZD4mQjKAByFv6UGSQ=="],
|
||||||
|
|
||||||
@@ -1309,6 +1310,8 @@
|
|||||||
|
|
||||||
"recast-x/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
"recast-x/source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="],
|
||||||
|
|
||||||
|
"reka-ui/@internationalized/date": ["@internationalized/date@3.12.1", "", { "dependencies": { "@swc/helpers": "^0.5.0" } }, "sha512-6IedsVWXyq4P9Tj+TxuU8WGWM70hYLl12nbYU8jkikVpa6WXapFazPUcHUMDMoWftIDE2ILDkFFte6W2nFCkRQ=="],
|
||||||
|
|
||||||
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
"restore-cursor/onetime": ["onetime@7.0.0", "", { "dependencies": { "mimic-function": "^5.0.0" } }, "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ=="],
|
||||||
|
|
||||||
"restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
"restore-cursor/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="],
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"tauri": "npx tauri"
|
"tauri": "npx tauri"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
|
"@internationalized/date": "^3.12.2",
|
||||||
"@lucide/vue": "^1.16.0",
|
"@lucide/vue": "^1.16.0",
|
||||||
"@tabler/icons-vue": "^3.44.0",
|
"@tabler/icons-vue": "^3.44.0",
|
||||||
"@tailwindcss/vite": "^4.3.0",
|
"@tailwindcss/vite": "^4.3.0",
|
||||||
|
|||||||
+61
-19
@@ -10,7 +10,9 @@ import { Input } from '@/components/ui/input'
|
|||||||
import { Badge } from '@/components/ui/badge'
|
import { Badge } from '@/components/ui/badge'
|
||||||
import { Skeleton } from '@/components/ui/skeleton'
|
import { Skeleton } from '@/components/ui/skeleton'
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'
|
||||||
import { Plus, Send, Pencil, CheckCircle2, Clock } from 'lucide-vue-next'
|
import { Popover } from '@/components/ui/popover'
|
||||||
|
import { Calendar } from '@/components/ui/calendar'
|
||||||
|
import { Plus, Send, Pencil, CheckCircle2, Clock, Calendar as CalendarIcon } from 'lucide-vue-next'
|
||||||
|
|
||||||
const { t } = useI18n()
|
const { t } = useI18n()
|
||||||
|
|
||||||
@@ -136,6 +138,23 @@ async function pushAllToKappa() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function parseDate(s: string): Date | undefined {
|
||||||
|
if (!s) return undefined
|
||||||
|
const d = new Date(s + 'T00:00:00')
|
||||||
|
return isNaN(d.getTime()) ? undefined : d
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDate(d: Date | undefined): string {
|
||||||
|
if (!d) return ''
|
||||||
|
return `${d.getFullYear()}-${String(d.getMonth() + 1).padStart(2, '0')}-${String(d.getDate()).padStart(2, '0')}`
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateEpicDate(value: string | number | null, idx: 0 | 1) {
|
||||||
|
const parts = (form.value.code || '').split('|')
|
||||||
|
parts[idx] = String(value ?? '')
|
||||||
|
form.value.code = parts.join('|') || null
|
||||||
|
}
|
||||||
|
|
||||||
onMounted(loadDrafts)
|
onMounted(loadDrafts)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
@@ -154,24 +173,47 @@ onMounted(loadDrafts)
|
|||||||
<!-- Form -->
|
<!-- Form -->
|
||||||
<Card v-if="showForm" class="mb-4">
|
<Card v-if="showForm" class="mb-4">
|
||||||
<CardContent class="p-4 space-y-3">
|
<CardContent class="p-4 space-y-3">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<Select v-model="form.item_type">
|
||||||
<Select v-model="form.item_type">
|
<SelectTrigger class="w-full">
|
||||||
<SelectTrigger class="w-full">
|
<SelectValue :placeholder="t('users.role')" />
|
||||||
<SelectValue :placeholder="t('users.role')" />
|
</SelectTrigger>
|
||||||
</SelectTrigger>
|
<SelectContent>
|
||||||
<SelectContent>
|
<SelectItem v-for="opt in itemTypeOptions" :key="opt.value" :value="opt.value">
|
||||||
<SelectItem v-for="opt in itemTypeOptions" :key="opt.value" :value="opt.value">
|
{{ opt.label }}
|
||||||
{{ opt.label }}
|
</SelectItem>
|
||||||
</SelectItem>
|
</SelectContent>
|
||||||
</SelectContent>
|
</Select>
|
||||||
</Select>
|
<Input :model-value="form.title" @update:model-value="form.title = String($event ?? '')" :placeholder="t('dashboard.title')" />
|
||||||
<Input :model-value="form.story_points ?? ''" @update:model-value="form.story_points = $event ? Number($event) : null" type="number" placeholder="SP" step="0.5" min="0" />
|
<Input :model-value="form.description ?? ''" @update:model-value="form.description = String($event ?? '') || null" :placeholder="t('dashboard.description')" />
|
||||||
</div>
|
<template v-if="form.item_type === 'E'">
|
||||||
<Input v-model="form.title" :placeholder="t('dashboard.title')" />
|
<div class="grid grid-cols-2 gap-3">
|
||||||
<div class="grid grid-cols-2 gap-3">
|
<Popover>
|
||||||
<Input :model-value="form.sprint ?? ''" @update:model-value="form.sprint = String($event ?? '') || null" placeholder="Sprint" />
|
<template #trigger>
|
||||||
<Input :model-value="form.assigned_to ?? ''" @update:model-value="form.assigned_to = String($event ?? '') || null" placeholder="Asignado a" />
|
<Button variant="outline" class="w-full justify-start text-sm font-normal">
|
||||||
</div>
|
<CalendarIcon class="size-4 mr-2" />
|
||||||
|
<span>{{ (form.code || '').split('|')[0] || 'Inicio estimado' }}</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<Calendar :model-value="parseDate((form.code || '').split('|')[0])" @update:model-value="updateEpicDate(formatDate($event), 0)" />
|
||||||
|
</Popover>
|
||||||
|
<Popover>
|
||||||
|
<template #trigger>
|
||||||
|
<Button variant="outline" class="w-full justify-start text-sm font-normal">
|
||||||
|
<CalendarIcon class="size-4 mr-2" />
|
||||||
|
<span>{{ (form.code || '').split('|')[1] || 'Fin estimado' }}</span>
|
||||||
|
</Button>
|
||||||
|
</template>
|
||||||
|
<Calendar :model-value="parseDate((form.code || '').split('|')[1])" @update:model-value="updateEpicDate(formatDate($event), 1)" />
|
||||||
|
</Popover>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div class="grid grid-cols-3 gap-3">
|
||||||
|
<Input :model-value="form.story_points ?? ''" @update:model-value="form.story_points = $event ? Number($event) : null" type="number" placeholder="SP" step="0.5" min="0" />
|
||||||
|
<Input :model-value="form.sprint ?? ''" @update:model-value="form.sprint = String($event ?? '') || null" placeholder="Sprint" />
|
||||||
|
<Input :model-value="form.assigned_to ?? ''" @update:model-value="form.assigned_to = String($event ?? '') || null" placeholder="Asignado a" />
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<div class="flex gap-2">
|
<div class="flex gap-2">
|
||||||
<Button size="sm" @click="saveDraft">Guardar</Button>
|
<Button size="sm" @click="saveDraft">Guardar</Button>
|
||||||
<Button size="sm" variant="outline" @click="showForm = false">Cancelar</Button>
|
<Button size="sm" variant="outline" @click="showForm = false">Cancelar</Button>
|
||||||
|
|||||||
@@ -0,0 +1,71 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import { type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import { Button } from '@/components/ui/button'
|
||||||
|
import { ChevronLeft, ChevronRight } from 'lucide-vue-next'
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
modelValue?: Date
|
||||||
|
class?: HTMLAttributes['class']
|
||||||
|
}>()
|
||||||
|
|
||||||
|
const emit = defineEmits<{ 'update:modelValue': [value: Date | undefined] }>()
|
||||||
|
|
||||||
|
const viewDate = ref(new Date())
|
||||||
|
|
||||||
|
const monthNames = ['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre']
|
||||||
|
const dayNames = ['Do','Lu','Ma','Mi','Ju','Vi','Sa']
|
||||||
|
|
||||||
|
const daysInMonth = computed(() => new Date(viewDate.value.getFullYear(), viewDate.value.getMonth() + 1, 0).getDate())
|
||||||
|
const firstDay = computed(() => new Date(viewDate.value.getFullYear(), viewDate.value.getMonth(), 1).getDay())
|
||||||
|
|
||||||
|
const today = new Date()
|
||||||
|
const selectedDate = ref(props.modelValue)
|
||||||
|
|
||||||
|
function prevMonth() { viewDate.value = new Date(viewDate.value.getFullYear(), viewDate.value.getMonth() - 1, 1) }
|
||||||
|
function nextMonth() { viewDate.value = new Date(viewDate.value.getFullYear(), viewDate.value.getMonth() + 1, 1) }
|
||||||
|
|
||||||
|
function selectDay(d: number) {
|
||||||
|
const date = new Date(viewDate.value.getFullYear(), viewDate.value.getMonth(), d)
|
||||||
|
date.setHours(0, 0, 0, 0)
|
||||||
|
selectedDate.value = date
|
||||||
|
emit('update:modelValue', date)
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSelected(d: number) {
|
||||||
|
if (!selectedDate.value) return false
|
||||||
|
return selectedDate.value.getFullYear() === viewDate.value.getFullYear() &&
|
||||||
|
selectedDate.value.getMonth() === viewDate.value.getMonth() &&
|
||||||
|
selectedDate.value.getDate() === d
|
||||||
|
}
|
||||||
|
|
||||||
|
function isToday(d: number) {
|
||||||
|
return today.getFullYear() === viewDate.value.getFullYear() &&
|
||||||
|
today.getMonth() === viewDate.value.getMonth() &&
|
||||||
|
today.getDate() === d
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div :class="cn('p-3', props.class)">
|
||||||
|
<div class="flex items-center justify-between mb-2">
|
||||||
|
<Button variant="ghost" size="icon" class="size-7" @click="prevMonth"><ChevronLeft class="size-4" /></Button>
|
||||||
|
<span class="text-sm font-medium">{{ monthNames[viewDate.getMonth()] }} {{ viewDate.getFullYear() }}</span>
|
||||||
|
<Button variant="ghost" size="icon" class="size-7" @click="nextMonth"><ChevronRight class="size-4" /></Button>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-0 text-center text-xs mb-1">
|
||||||
|
<span v-for="d in dayNames" :key="d" class="text-muted-foreground py-1">{{ d }}</span>
|
||||||
|
</div>
|
||||||
|
<div class="grid grid-cols-7 gap-0 text-center text-sm">
|
||||||
|
<span v-for="_ in firstDay" :key="'e'+_" />
|
||||||
|
<button
|
||||||
|
v-for="d in daysInMonth"
|
||||||
|
:key="d"
|
||||||
|
class="py-1.5 rounded hover:bg-muted transition-colors relative"
|
||||||
|
:class="[isSelected(d) ? 'bg-primary text-primary-foreground hover:bg-primary' : '', isToday(d) && !isSelected(d) ? 'bg-accent text-accent-foreground' : '']"
|
||||||
|
@click="selectDay(d)"
|
||||||
|
>{{ d }}</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Calendar } from './Calendar.vue'
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { type HTMLAttributes } from 'vue'
|
||||||
|
import { cn } from '@/lib/utils'
|
||||||
|
import {
|
||||||
|
PopoverContent,
|
||||||
|
PopoverRoot,
|
||||||
|
PopoverTrigger,
|
||||||
|
type PopoverContentProps,
|
||||||
|
} from 'reka-ui'
|
||||||
|
|
||||||
|
const props = defineProps<PopoverContentProps & { class?: HTMLAttributes['class'] }>()
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<PopoverRoot>
|
||||||
|
<PopoverTrigger as-child>
|
||||||
|
<slot name="trigger" />
|
||||||
|
</PopoverTrigger>
|
||||||
|
<PopoverContent v-bind="props" :class="cn('z-50 w-72 rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-none', props.class)">
|
||||||
|
<slot />
|
||||||
|
</PopoverContent>
|
||||||
|
</PopoverRoot>
|
||||||
|
</template>
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
export { default as Popover } from './Popover.vue'
|
||||||
@@ -320,9 +320,9 @@
|
|||||||
},
|
},
|
||||||
"hierarchy": {
|
"hierarchy": {
|
||||||
"epic": "Epic",
|
"epic": "Epic",
|
||||||
"hu": "HU",
|
|
||||||
"feature": "Feature",
|
"feature": "Feature",
|
||||||
"task": "Task",
|
"task": "Task",
|
||||||
|
"hu": "HU",
|
||||||
"bug": "Bug"
|
"bug": "Bug"
|
||||||
},
|
},
|
||||||
"workitems": {
|
"workitems": {
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { callAI } from '@/services/ai'
|
import { callAI } from '@/services/ai'
|
||||||
import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db'
|
import { getSessionsByProject, getSessionSummary, getProjectState } from '@/services/transcriptions-db'
|
||||||
import { saveDraft, createDraftId } from '@/services/hu-drafts-db'
|
import { saveDraft, createDraftId } from '@/services/hu-drafts-db'
|
||||||
|
import { generateAndSavePlan } from '@/services/qa-analyzer'
|
||||||
import type { EnrichedUserStory } from '@/stores/workitems'
|
import type { EnrichedUserStory } from '@/stores/workitems'
|
||||||
|
|
||||||
export interface AnalysisHU {
|
export interface AnalysisHU {
|
||||||
@@ -129,12 +130,16 @@ export async function saveAsDrafts(
|
|||||||
})
|
})
|
||||||
if (isDuplicate) { skipped++; continue }
|
if (isDuplicate) { skipped++; continue }
|
||||||
|
|
||||||
|
const draftId = createDraftId()
|
||||||
await saveDraft({
|
await saveDraft({
|
||||||
id: createDraftId(), projectId, title: hu.title,
|
id: draftId, projectId, title: hu.title,
|
||||||
description: hu.description, acceptanceCriteria: hu.acceptance_criteria.join('\n'),
|
description: hu.description, acceptanceCriteria: hu.acceptance_criteria.join('\n'),
|
||||||
priority: hu.priority, type: 'U', metadata: '{}',
|
priority: hu.priority, type: 'U', metadata: '{}',
|
||||||
sourceSessionId, syncStatus: 'draft', createdAt: new Date().toISOString(),
|
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++
|
saved++
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+35
-20
@@ -108,10 +108,6 @@ async function pushDraft(d: HuDraftRecord) {
|
|||||||
})
|
})
|
||||||
if (res.ok) {
|
if (res.ok) {
|
||||||
d.syncStatus = 'pushed'; await dbSaveDraft(d)
|
d.syncStatus = 'pushed'; await dbSaveDraft(d)
|
||||||
// Auto-generar QA plan tras push exitoso de épica
|
|
||||||
generateAndSavePlan(d.projectId, d.id, d.title, d.description || '', '').catch(e =>
|
|
||||||
console.error(`[Alpha] QA plan auto-gen failed for epic ${d.title}:`, e)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
d.syncStatus = 'draft'; await dbSaveDraft(d)
|
d.syncStatus = 'draft'; await dbSaveDraft(d)
|
||||||
}
|
}
|
||||||
@@ -136,10 +132,6 @@ async function pushDraft(d: HuDraftRecord) {
|
|||||||
d.kappaId = created.id || undefined
|
d.kappaId = created.id || undefined
|
||||||
d.syncStatus = 'pushed'
|
d.syncStatus = 'pushed'
|
||||||
await dbSaveDraft(d)
|
await dbSaveDraft(d)
|
||||||
// Auto-generar QA plan tras push exitoso
|
|
||||||
generateAndSavePlan(d.projectId, d.id, d.title, d.description || '', d.acceptanceCriteria).catch(e =>
|
|
||||||
console.error(`[Alpha] QA plan auto-gen failed for ${d.title}:`, e)
|
|
||||||
)
|
|
||||||
} else {
|
} else {
|
||||||
d.syncStatus = 'draft'; await dbSaveDraft(d)
|
d.syncStatus = 'draft'; await dbSaveDraft(d)
|
||||||
}
|
}
|
||||||
@@ -168,17 +160,26 @@ async function discardDraft(id: string) {
|
|||||||
|
|
||||||
// ─── Project analysis ────────────────────────────────────
|
// ─── Project analysis ────────────────────────────────────
|
||||||
const analyzing = ref(false)
|
const analyzing = ref(false)
|
||||||
|
const analysisAbort = ref<AbortController | null>(null)
|
||||||
const analysisResult = ref<{ saved: number; skipped: number } | null>(null)
|
const analysisResult = ref<{ saved: number; skipped: number } | null>(null)
|
||||||
const analysisSummary = ref('')
|
const analysisSummary = ref('')
|
||||||
|
|
||||||
|
function cancelAnalysis() {
|
||||||
|
analysisAbort.value?.abort()
|
||||||
|
analyzing.value = false
|
||||||
|
analysisAbort.value = null
|
||||||
|
analysisSummary.value = 'Análisis cancelado'
|
||||||
|
}
|
||||||
|
|
||||||
async function runAnalysis() {
|
async function runAnalysis() {
|
||||||
if (!project.value) return
|
if (!project.value) return
|
||||||
analyzing.value = true
|
analyzing.value = true
|
||||||
|
analysisAbort.value = new AbortController()
|
||||||
analysisResult.value = null
|
analysisResult.value = null
|
||||||
analysisSummary.value = ''
|
analysisSummary.value = ''
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const result = await analyzeProject(project.value.id, project.value.name || '', workItems.userStories)
|
const result = await analyzeProject(project.value.id, project.value.name || '', workItems.userStories, analysisAbort.value?.signal)
|
||||||
analysisSummary.value = result.summary
|
analysisSummary.value = result.summary
|
||||||
|
|
||||||
if (result.hus.length > 0) {
|
if (result.hus.length > 0) {
|
||||||
@@ -188,8 +189,12 @@ async function runAnalysis() {
|
|||||||
analysisResult.value = { saved: 0, skipped: 0 }
|
analysisResult.value = { saved: 0, skipped: 0 }
|
||||||
}
|
}
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
console.error('[Alpha] Analysis error:', e)
|
if (e.name === 'AbortError' || e.message?.includes('aborted')) {
|
||||||
analysisSummary.value = `Error: ${e.message}`
|
analysisSummary.value = 'Análisis cancelado'
|
||||||
|
} else {
|
||||||
|
console.error('[Alpha] Analysis error:', e)
|
||||||
|
analysisSummary.value = `Error: ${e.message}`
|
||||||
|
}
|
||||||
analysisResult.value = { saved: 0, skipped: 0 }
|
analysisResult.value = { saved: 0, skipped: 0 }
|
||||||
} finally {
|
} finally {
|
||||||
analyzing.value = false
|
analyzing.value = false
|
||||||
@@ -313,15 +318,25 @@ const statusLabel = (status: unknown) => {
|
|||||||
<Sparkles class="size-4" />
|
<Sparkles class="size-4" />
|
||||||
Análisis completo del proyecto
|
Análisis completo del proyecto
|
||||||
</CardTitle>
|
</CardTitle>
|
||||||
<Button
|
<div class="flex gap-2">
|
||||||
size="sm"
|
<Button
|
||||||
:disabled="analyzing"
|
v-if="analyzing"
|
||||||
@click="runAnalysis()"
|
size="sm"
|
||||||
>
|
variant="outline"
|
||||||
<Loader2 v-if="analyzing" class="size-4 mr-1 animate-spin" />
|
@click="cancelAnalysis()"
|
||||||
<Sparkles v-else class="size-4 mr-1" />
|
>
|
||||||
{{ analyzing ? 'Analizando...' : 'Generar HUs faltantes' }}
|
Cancelar
|
||||||
</Button>
|
</Button>
|
||||||
|
<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>
|
||||||
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent v-if="analysisResult" class="space-y-2 text-sm">
|
<CardContent v-if="analysisResult" class="space-y-2 text-sm">
|
||||||
<p class="text-muted-foreground">{{ analysisSummary }}</p>
|
<p class="text-muted-foreground">{{ analysisSummary }}</p>
|
||||||
|
|||||||
Reference in New Issue
Block a user