From b974788a16e92f759de47888c1954c236a278521 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Thu, 28 May 2026 13:20:23 -0500 Subject: [PATCH] fix: parseo JSON robusto, UI upload full-width, selector en header, multiple files - session-analyzer.ts: extractJSON() con 4 estrategias de parseo + log raw - TranscriptionsView: selector proyecto movido al header, upload card full-width - TranscriptionsView: soporte multi-file con cola, banner de progreso - TranscriptionsView: download .md con revokeObjectURL diferido - TranscriptionsView: upload deshabilitado sin proyecto seleccionado - session-analyzer + project-doc exportados como servicios independientes - i18n: keys titleView, statusParsing/Analyzing/Generating, filesLoaded - i18n: fix key title duplicado (title -> titleLabel) --- src/i18n/locales/en.json | 8 +- src/i18n/locales/es.json | 8 +- src/services/session-analyzer.ts | 31 ++++- src/views/TranscriptionsView.vue | 207 ++++++++++++++++++++++--------- 4 files changed, 194 insertions(+), 60 deletions(-) diff --git a/src/i18n/locales/en.json b/src/i18n/locales/en.json index c128399..b497ad5 100644 --- a/src/i18n/locales/en.json +++ b/src/i18n/locales/en.json @@ -249,6 +249,7 @@ }, "transcriptions": { "title": "Transcriptions", + "titleView": "Transcriptions", "subtitle": "Manage project sessions. Upload transcripts, analyze with AI, and maintain an incremental document.", "configureAI": "Configure AI", "aiKeyTitle": "OpenRouter API Key", @@ -276,9 +277,13 @@ "selected": "selected", "createInKappa": "Create {count} in KAPPA", "type": "Type", - "title": "Title", + "titleLabel": "Title", "priority": "Priority", "selectProjectToCreate": "Select a project above to create stories in KAPPA", + "statusParsing": "Processing files...", + "statusAnalyzing": "Analyzing session with AI...", + "statusGenerating": "Generating project document...", + "filesLoaded": "files loaded", "analyzeSession": "Analyze session", "sessionError": "Error analyzing session", "sessionSummary": "Summary", @@ -299,6 +304,7 @@ "updatedAt": "Updated:", "docViewer": "Session document", "selectProjectHint": "Select a project to view its sessions", + "selectProjectFirst": "Select a project to start", "sessionCount": "{count} sessions | {count} session | {count} sessions" }, "projectAi": { diff --git a/src/i18n/locales/es.json b/src/i18n/locales/es.json index 963c329..26893dc 100644 --- a/src/i18n/locales/es.json +++ b/src/i18n/locales/es.json @@ -249,6 +249,7 @@ }, "transcriptions": { "title": "Transcripciones", + "titleView": "Transcripciones", "subtitle": "Gestioná las sesiones del proyecto. Subí transcripciones, analizalas con IA y mantené un documento incremental.", "configureAI": "Configurar IA", "aiKeyTitle": "API Key de OpenRouter", @@ -276,9 +277,13 @@ "selected": "seleccionadas", "createInKappa": "Crear {count} en KAPPA", "type": "Tipo", - "title": "Título", + "titleLabel": "Título", "priority": "Prioridad", "selectProjectToCreate": "Seleccioná un proyecto arriba para crear las HUs en KAPPA", + "statusParsing": "Procesando archivos...", + "statusAnalyzing": "Analizando sesión con IA...", + "statusGenerating": "Generando documento del proyecto...", + "filesLoaded": "archivos cargados", "analyzeSession": "Analizar sesión", "sessionError": "Error al analizar la sesión", "sessionSummary": "Resumen", @@ -299,6 +304,7 @@ "updatedAt": "Actualizado:", "docViewer": "Documento de sesiones", "selectProjectHint": "Seleccioná un proyecto para ver sus sesiones", + "selectProjectFirst": "Seleccioná un proyecto para empezar", "sessionCount": "{count} sesiones | {count} sesión | {count} sesiones" }, "projectAi": { diff --git a/src/services/session-analyzer.ts b/src/services/session-analyzer.ts index 9f378d2..dd25c68 100644 --- a/src/services/session-analyzer.ts +++ b/src/services/session-analyzer.ts @@ -63,12 +63,39 @@ export async function analyzeSession( ) try { - const jsonStr = content.replace(/```json\s*/gi, '').replace(/```\s*$/g, '').trim() + const jsonStr = extractJSON(content) const result: SessionExtraction = JSON.parse(jsonStr) console.log(`[Alpha] Session analysis complete — ${result.pendingTasks.length} tasks, ${result.decisions.length} decisions`) return result } catch (e) { - console.error('[Alpha] Failed to parse session analysis:', content) + console.error('[Alpha] Failed to parse session analysis. Raw response:', content) throw new Error('No se pudo parsear el análisis de la sesión') } } + +/** + * Extrae el primer objeto JSON válido del texto. + * Maneja markdown code blocks, texto antes/después, etc. + */ +function extractJSON(text: string): string { + // 1. Intentar parse directo + try { JSON.parse(text); return text } catch {} + + // 2. Extraer entre ```json y ``` + const jsonBlock = text.match(/```(?:json)?\s*([\s\S]*?)```/) + if (jsonBlock) { + const candidate = jsonBlock[1].trim() + try { JSON.parse(candidate); return candidate } catch {} + } + + // 3. Extraer entre { y } (first { to last }) + const firstBrace = text.indexOf('{') + const lastBrace = text.lastIndexOf('}') + if (firstBrace !== -1 && lastBrace > firstBrace) { + const candidate = text.slice(firstBrace, lastBrace + 1) + try { JSON.parse(candidate); return candidate } catch {} + } + + // 4. Fallback: devolver el texto original (lanzará error en el caller) + return text +} diff --git a/src/views/TranscriptionsView.vue b/src/views/TranscriptionsView.vue index afd5b43..7a8ebd8 100644 --- a/src/views/TranscriptionsView.vue +++ b/src/views/TranscriptionsView.vue @@ -2,9 +2,18 @@ import { ref, computed, watch, nextTick } from 'vue' import { useI18n } from 'vue-i18n' import { useProjectsStore } from '@/stores/projects' -import { useSettingsStore } from '@/stores/settings' +import { useSettingsStore, AVAILABLE_MODELS, PROVIDER_CONFIG, hasProviderApiKey, type AIProvider } from '@/stores/settings' +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuLabel, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu' import { analyzeSession, type SessionExtraction } from '@/services/session-analyzer' import { generateProjectDoc, getProjectDoc } from '@/services/project-doc' +import { parseFile } from '@/services/parse-transcription' import { Card, CardContent, @@ -39,6 +48,8 @@ import { Hash, ChevronLeft, ChevronRight, + ChevronDown, + Check, } from 'lucide-vue-next' const { t } = useI18n() @@ -93,10 +104,10 @@ watch(selectedProjectId, async (id) => { function parseSessionOffsets(md: string) { const offsets: { date: string; title: string; offset: number }[] = [] - const re = /## 📍 Sesión \d+: (.+?) \*\*Archivo fuente:.*?\*\* /gs + const re = /## 📍 Sesión \d+:\s*(.+?)\n\*\*Archivo fuente:/g let match while ((match = re.exec(md)) !== null) { - const title = match[1] + const title = match[1].trim() const dateMatch = title.match(/(\d{4}-\d{2}-\d{2})/) offsets.push({ date: dateMatch?.[1] || '', @@ -165,18 +176,23 @@ const fileQueue = ref([]) const activeFileIndex = ref(-1) async function handleFiles(files: FileList | File[]) { - const { parseFile } = await import('@/services/parse-transcription') + console.log(`[Alpha] handleFiles: ${files.length} file(s)`) const newFiles: QueuedFile[] = [] + parsing.value = true for (const file of Array.from(files)) { + console.log(`[Alpha] Parsing: ${file.name} (${file.size} bytes)`) try { const result = await parseFile(file) + console.log(`[Alpha] Parsed OK: ${result.fileName}, ${result.text.length} chars`) newFiles.push({ fileName: result.fileName, text: result.text, parsed: true }) } catch (e: any) { console.error(`[Alpha] Failed to parse ${file.name}:`, e) } } + parsing.value = false + if (newFiles.length === 0) return fileQueue.value.push(...newFiles) @@ -305,7 +321,18 @@ function downloadDoc() { a.href = url a.download = `${slug}-sesiones.md` a.click() - URL.revokeObjectURL(url) + setTimeout(() => URL.revokeObjectURL(url), 1000) +} + +const allProviders: AIProvider[] = ['openrouter', 'minimax', 'opencode'] +function modelsForProvider(p: AIProvider) { + return AVAILABLE_MODELS.filter(m => m.provider === p) +} +function switchModel(provider: AIProvider, modelId: string) { + if (provider !== settingsStore.provider) { + settingsStore.setActiveProvider(provider) + } + settingsStore.setModel(modelId) } function clearAll() { @@ -325,47 +352,26 @@ function clearAll() {