From 066047f3d1d9d6401c5625ded7b1947561dc7699 Mon Sep 17 00:00:00 2001 From: Ricardo Gonzalez Date: Thu, 28 May 2026 13:38:19 -0500 Subject: [PATCH] dual storage: sesiones en BD + markdown como output - db.ts: v3 con tablas sessions, session_summaries, project_state - transcriptions-db.ts: CRUD para sesiones, summaries, project state - project-doc.ts: generateMasterDoc() desde BD (no desde datos en memoria) - session-analyzer.ts: +sessionDate en prompt y extraction - TranscriptionsView: flujo parse -> guardar BD -> IA -> project_state -> MD - docs/arquitectura_transcripciones.md: documentacion oficial del patron --- docs/arquitectura_transcripciones.md | 180 +++++++++++++++++++++++++++ src/services/db.ts | 40 +++++- src/services/project-doc.ts | 151 ++++++++++------------ src/services/session-analyzer.ts | 23 ++-- src/services/transcriptions-db.ts | 61 +++++++++ src/views/TranscriptionsView.vue | 134 ++++++++++++++++---- 6 files changed, 467 insertions(+), 122 deletions(-) create mode 100644 docs/arquitectura_transcripciones.md create mode 100644 src/services/transcriptions-db.ts diff --git a/docs/arquitectura_transcripciones.md b/docs/arquitectura_transcripciones.md new file mode 100644 index 0000000..428697d --- /dev/null +++ b/docs/arquitectura_transcripciones.md @@ -0,0 +1,180 @@ +# Sistema de Transcripciones — Arquitectura + +> Documentación oficial del pipeline transcripciones en Alpha. +> Este patrón se hereda a RUMBO. + +--- + +## Filosofía + +``` +BD (Dexie/SQLite) = Fuente de verdad +Markdown = Output generado (nunca se edita manualmente) +``` + +El markdown es un **artefacto derivado**. Siempre se regenera desde la BD. +Si necesitás modificar algo, se actualiza la BD y se regenera el `.md`. + +--- + +## Estructura de Datos + +``` +┌─────────────────────────────────────────────────────────────┐ +│ DATABASE (Dexie/IndexedDB) │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ sessions │ +│ ├── id: number ← PK autoincrement │ +│ ├── projectId: number ← FK a proyectos │ +│ ├── date: string ← Fecha de la sesión │ +│ ├── title: string ← Título (ej: "Kickoff") │ +│ ├── fileName: string ← Archivo original subido │ +│ ├── fileType: string ← docx|vtt|txt|md │ +│ ├── fileSize: number ← bytes │ +│ ├── rawText: string ← Texto completo parseado │ +│ └── processedAt: string ← ISO timestamp │ +│ │ +│ session_summaries │ +│ ├── sessionId: number ← PK = FK → sessions.id │ +│ ├── summary: string ← Resumen ejecutivo │ +│ ├── objectives: string ← JSON array │ +│ ├── tasks: string ← JSON array │ +│ ├── commitments: string ← JSON array │ +│ ├── decisions: string ← JSON array │ +│ ├── keyPoints: string ← JSON array │ +│ └── modelUsed: string ← Modelo IA usado │ +│ │ +│ project_state │ +│ ├── projectId: number ← PK │ +│ ├── summary: string ← Resumen consolidado │ +│ ├── objectives: string ← JSON array (unificado) │ +│ ├── tasks: string ← JSON array (consolidado) │ +│ ├── commitments: string ← JSON array (consolidado) │ +│ └── updatedAt: string ← ISO timestamp │ +│ │ +└─────────────────────────────────────────────────────────────┘ + +┌─────────────────────────────────────────────────────────────┐ +│ OUTPUT (Markdown) │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ 📄 master-[proyecto].md │ +│ │ +│ ├── BLOQUE 1 — Consolidado Global │ +│ │ ├── Resumen ejecutivo (desde project_state) │ +│ │ ├── Objetivos │ +│ │ ├── Tareas pendientes (con checkbox) │ +│ │ └── Compromisos (con estados) │ +│ │ │ +│ └── BLOQUE 2 — Detalle por Sesión │ +│ ├── 📍 Sesión YYYY-MM-DD — Título │ +│ │ ├── Resumen │ +│ │ ├── Objetivos │ +│ │ ├── Tareas │ +│ │ ├── Compromisos │ +│ │ └── Transcripción completa │ +│ └── ... │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +--- + +## Flujo Completo + +``` +┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ +│ SUBIR │ → │ PARSEAR │ → │ GUARDAR │ → │ EXTRAER │ +│ .docx │ │ .vtt │ │ EN BD │ │ CON IA │ +│ .vtt │ │ .txt │ │ sessions │ │ │ +│ .txt │ │ .md │ │ │ │ │ +│ .md │ │ │ │ │ │ │ +└──────────┘ └──────────┘ └──────────┘ └──────────┘ + │ + ▼ + ┌──────────┐ + │ GUARDAR │ + │ RESULT │ + │ EN BD │ + │summaries │ + └──────────┘ + │ + ▼ + ┌──────────┐ + │ ACTUALIZ │ + │ project │ + │ _state │ + └──────────┘ + │ + ▼ + ┌──────────┐ + │ GENERAR │ + │ .md │ + └──────────┘ +``` + +### Paso a paso + +| Paso | Acción | Tabla afectada | +|------|--------|----------------| +| 1 | Usuario sube archivo(s) | — | +| 2 | Parsear a texto plano (mammoth para .docx, regex para .vtt) | — | +| 3 | Guardar transcripción cruda en BD | `sessions` (INSERT) | +| 4 | Enviar a IA para extracción estructurada | — | +| 5 | Guardar resultado de IA en BD | `session_summaries` (INSERT) | +| 6 | Recalcular estado consolidado del proyecto | `project_state` (UPSERT) | +| 7 | Regenerar documento markdown | — (derivado de BD) | + +--- + +## Por qué dual storage (BD + Markdown) + +| Problema | Solución con BD | +|----------|----------------| +| Buscar dentro de transcripciones viejas | `SELECT * FROM sessions WHERE rawText LIKE '%keyword%'` | +| Marcar tarea como completada | `UPDATE project_state SET tasks = ...` sin re-procesar nada | +| Regenerar Bloque 1 al agregar sesión nueva | Read de BD, no re-procesar sesiones anteriores | +| Versiones históricas | Cada sesión es un registro independiente | +| Compartir con alguien que no tiene la app | Exportar `.md` — es legible en cualquier editor | +| Backup | La BD se puede respaldar; el `.md` es un snapshot legible | + +### Por qué el markdown NO es fuente de verdad + +- **No se edita manualmente.** Si alguien modifica el `.md`, esos cambios se pierden al regenerar. +- **Es un snapshot.** Siempre se puede regenerar desde la BD. +- **Es portable.** Se puede compartir, subir a GitHub, etc. + +--- + +## Comparación con la implementación anterior + +| Aspecto | Antes (v1) | Ahora (v2) | +|---------|------------|------------| +| Storage | Solo markdown en `project_docs` | BD estructurada + markdown generado | +| Transcripciones | Solo en memoria | Persistidas en `sessions` | +| Búsqueda | Imposible | Query SQL por texto | +| Estado consolidado | Recalculado cada vez desde las sesiones | Cacheado en `project_state` | +| Editar tarea | Había que re-procesar la sesión | UPDATE directo a `project_state` | +| Sesiones individuales | Incrustadas en markdown | Registros independientes | + +--- + +## Consideraciones para RUMBO + +En RUMBO (Tauri + Turso/libSQL), el schema es idéntico pero cambia el engine: + +``` +Alpha (browser): Dexie + IndexedDB → Markdown descargable +RUMBO (desktop): Turso + libSQL → Markdown exportable + auto-sync +``` + +Las tablas, tipos y flujo son los mismos. Solo cambia: +- `db.sessions.put()` → `INSERT OR REPLACE INTO sessions` +- El markdown se escribe a archivo en `~/RUMBO/projects/[id]/master.md` + +--- + +*Documentado: 2026-05-28* +*Validado en: Alpha* +*Destino: RUMBO* diff --git a/src/services/db.ts b/src/services/db.ts index 17cea66..3194c84 100644 --- a/src/services/db.ts +++ b/src/services/db.ts @@ -13,14 +13,52 @@ export interface ProjectDocRecord { markdown: string } +export interface SessionRecord { + id?: number + projectId: number + date: string + title: string + fileName: string + fileType: string + fileSize: number + rawText: string + processedAt: string +} + +export interface SessionSummaryRecord { + sessionId: number + summary: string + objectives: string // JSON array + tasks: string // JSON array + commitments: string // JSON array + decisions: string // JSON array + keyPoints: string // JSON array + modelUsed: string +} + +export interface ProjectStateRecord { + projectId: number + summary: string + objectives: string // JSON array (unificado) + tasks: string // JSON array (consolidado) + commitments: string // JSON array (consolidado) + updatedAt: string +} + const db = new Dexie('alpha-core') as Dexie & { settings: Dexie.Table project_docs: Dexie.Table + sessions: Dexie.Table + session_summaries: Dexie.Table + project_state: Dexie.Table } -db.version(2).stores({ +db.version(3).stores({ settings: '&key', project_docs: '&projectId, projectName, updatedAt', + sessions: '++id, projectId, date', + session_summaries: '&sessionId', + project_state: '&projectId', }) export default db diff --git a/src/services/project-doc.ts b/src/services/project-doc.ts index 195ad2e..7cb787a 100644 --- a/src/services/project-doc.ts +++ b/src/services/project-doc.ts @@ -1,66 +1,56 @@ -import db from '@/services/db' -import type { SessionExtraction } from '@/services/session-analyzer' +import { getSessionsByProject, getSessionSummary, getProjectState, type SessionRecord, type ProjectStateRecord } from '@/services/transcriptions-db' -export interface ProjectDoc { - projectId: number - projectName: string - updatedAt: string - sessionCount: number - markdown: string +/** + * Genera el markdown del proyecto completamente desde la BD. + * No recibe datos de sesión directos — los obtiene de las tablas. + */ +export async function generateMasterDoc(projectId: number, projectName: string): Promise { + const state = await getProjectState(projectId) + const sessions = await getSessionsByProject(projectId) + + const block1 = buildBlock1(state, projectName, sessions.length) + const block2 = await buildBlock2(sessions) + + return `${block1}\n\n${block2}\n` } -export async function getProjectDoc(projectId: number): Promise { - const doc = await db.table('project_docs').get(projectId) - return (doc as ProjectDoc) || null -} - -export async function generateProjectDoc( - projectId: number, +function buildBlock1( + state: ProjectStateRecord | undefined, projectName: string, - extraction: SessionExtraction, - transcriptionText: string, - fileName: string, - previousDoc?: ProjectDoc | null, -): Promise { + sessionCount: number, +): string { const now = new Date().toISOString().slice(0, 16).replace('T', ' ') - const sessionCount = (previousDoc?.sessionCount || 0) + 1 - // ─── Block 1: Summary (replaced) ─────────────────────── + const objectives = safeParse(state?.objectives, []) + const tasks = safeParse<{ description: string; origin: string; priority: string; status?: string }[]>(state?.tasks, []) + const commitments = safeParse<{ description: string; responsible: string; dueDate: string; status: string }[]>(state?.commitments, []) + const summary = state?.summary || 'Sin resumen disponible' - const objectivesMd = extraction.objectives.map(o => - o.isNew ? `- [ ] ${o.text} 🆕` : `- [ ] ${o.text}` - ).join('\n') - - const tasksMd = extraction.pendingTasks.map((t, i) => - `| ${i + 1} | [ ] ${t.description} | ${t.origin} | ${now.slice(0, 10)} | ${t.priority} |` - ).join('\n') - - const commitmentsMd = extraction.commitments.map(c => + const objectivesMd = objectives.map(o => `- [ ] ${o}`).join('\n') + const tasksMd = tasks.map((t, i) => `| ${i + 1} | [${t.status === 'completada' ? 'x' : ' '}] ${t.description} | ${t.origin} | ${t.priority} |`).join('\n') + const commitmentsMd = commitments.map(c => `| ${c.description} | ${c.responsible} | ${c.dueDate} | ${c.status === 'Cumplido' ? '✅' : '⏳'} | — |` ).join('\n') - - const completedMd = extraction.completedTasks.map(t => `- [x] ${t}`).join('\n') - - const milestonesMd = extraction.commitments + const milestonesMd = commitments .filter(c => c.dueDate && c.status !== 'Cumplido') - .map(c => `- **${c.dueDate}**: ${c.description}`) + .map(c => `- **${c.dueDate}**: ${c.description} (_${c.responsible}_)`) .join('\n') - const block1 = `# 📋 ${projectName} — Resumen Ejecutivo + return `# 📋 ${projectName} — Resumen Ejecutivo > ⚠️ Última actualización: ${now} --- ## 🎯 Resumen Ejecutivo -${extraction.summary} +${summary} ## 🎯 Objetivos ${objectivesMd || '_Sin objetivos registrados_'} ## 📝 Tareas Pendientes -| # | Tarea | Origen | Fecha creación | Prioridad | -|---|-------|--------|----------------|-----------| +| # | Tarea | Origen | Prioridad | +|---|-------|--------|-----------| ${tasksMd || '_Sin tareas pendientes_'} ## ✅ Compromisos @@ -68,76 +58,65 @@ ${tasksMd || '_Sin tareas pendientes_'} |------------|-------------|--------------|--------|-------| ${commitmentsMd || '_Sin compromisos_'} -## ✅ Tareas Completadas -${completedMd || '_Sin tareas completadas en esta sesión_'} - ## 📅 Próximos Hitos ${milestonesMd || '_Sin hitos próximos_'} ## 📊 Métricas de Seguimiento - Sesiones registradas: ${sessionCount} -- Tareas pendientes: ${extraction.pendingTasks.length} -- Compromisos cumplidos: ${extraction.commitments.filter(c => c.status === 'Cumplido').length}/${extraction.commitments.length} -- Decisiones tomadas: ${extraction.decisions.length} +- Tareas pendientes: ${tasks.filter(t => t.status !== 'completada').length} +- Compromisos cumplidos: ${commitments.filter(c => c.status === 'Cumplido').length}/${commitments.length}` +} ---- +async function buildBlock2(sessions: SessionRecord[]): Promise { + if (sessions.length === 0) return '## 📜 Registro de Sesiones\n\n_Sin sesiones registradas_' -### Bloque 2: Registro de Sesiones + const parts: string[] = ['---\n\n## 📜 Registro Completo de Sesiones'] ---- + for (const session of sessions) { + const summary = await getSessionSummary(session.id!) + const objectives = safeParse<{ text: string; isNew: boolean }[]>(summary?.objectives, []) + const tasks = safeParse<{ description: string; origin: string; priority: string }[]>(summary?.tasks, []) + const commitments = safeParse<{ description: string; responsible: string; dueDate: string; status: string }[]>(summary?.commitments, []) + const decisions = safeParse(summary?.decisions, []) + const keyPoints = safeParse(summary?.keyPoints, []) -## 📜 Registro Completo de Sesiones -` + const entry = `--- - // ─── Block 2: Session entry (appended) ───────────────── +## 📍 Sesión: ${session.date} — ${session.title} - const decisionsMd = extraction.decisions.map(d => `- ${d}`).join('\n') - const keyPointsMd = extraction.keyPoints.map(k => `- ${k}`).join('\n') - - const sessionEntry = `--- - -## 📍 Sesión ${sessionCount}: ${extraction.sessionTitle} - -**Archivo fuente:** \`${fileName}\` -**Fecha:** ${now} +**Archivo fuente:** \`${session.fileName}\` ### Resumen de la sesión -${extraction.summary} +${summary?.summary || '_Sin resumen disponible_'} + +### Objetivos de esta sesión +${objectives.map(o => `- ${o.isNew ? '🆕 ' : ''}${o.text}`).join('\n') || '_Ninguno_'} ### Tareas identificadas en esta sesión -${extraction.pendingTasks.map(t => `- [ ] ${t.description} (_${t.priority}_)`).join('\n') || '_Ninguna_'} +${tasks.map(t => `- [ ] ${t.description} (_${t.priority}_)`).join('\n') || '_Ninguna_'} + +### Compromisos de esta sesión +${commitments.map(c => `- ${c.description} → ${c.responsible} (${c.dueDate}) [${c.status}]`).join('\n') || '_Ninguno_'} ### Decisiones tomadas -${decisionsMd || '_Ninguna_'} +${decisions.map(d => `- ${d}`).join('\n') || '_Ninguna_'} ### Puntos clave -${keyPointsMd || '_Ninguno_'} +${keyPoints.map(k => `- ${k}`).join('\n') || '_Ninguno_'} ### Transcripción completa \`\`\` -${transcriptionText} -\`\`\` -` +${session.rawText} +\`\`\`` - // ─── Assemble document ───────────────────────────────── - - const previousSessions = previousDoc - ? previousDoc.markdown.split('---\n\n## 📜 Registro Completo de Sesiones')[1] || '' - : '' - - const markdown = `${block1}${previousSessions}\n${sessionEntry}\n` - - const doc: ProjectDoc = { - projectId, - projectName, - updatedAt: now, - sessionCount, - markdown, + parts.push(entry) } - // Save to Dexie - await db.table('project_docs').put(doc) - - return doc + return parts.join('\n') +} + +function safeParse(json: string | undefined | null, fallback: T): T { + if (!json) return fallback + try { return JSON.parse(json) as T } catch { return fallback } } diff --git a/src/services/session-analyzer.ts b/src/services/session-analyzer.ts index dd25c68..78c0ed6 100644 --- a/src/services/session-analyzer.ts +++ b/src/services/session-analyzer.ts @@ -1,6 +1,7 @@ import { callAI } from '@/services/ai' export interface SessionExtraction { + sessionDate: string // YYYY-MM-DD sessionTitle: string summary: string objectives: { text: string; isNew: boolean }[] @@ -14,19 +15,21 @@ export interface SessionExtraction { const SESSION_SYSTEM_PROMPT = `Eres un asistente de gestión de proyectos. Analizás transcripciones de reuniones y extraés información estructurada. Reglas: -1. Identificá el título de la sesión basado en el contenido y fecha -2. Extraé un resumen ejecutivo de 2-3 oraciones -3. Listá objetivos mencionados, marcando cuáles son NUEVOS vs existentes -4. Extraé tareas pendientes con su origen y prioridad (Alta/Media/Baja) -5. Identificá compromisos con responsable, fecha límite y estado -6. Listá decisiones tomadas durante la sesión -7. Detectá tareas completadas (si hay evidencia) -8. Incluí puntos clave, bloqueos o descubrimientos -9. No inventes información que no esté en la transcripción -10. Respondé SOLO con JSON válido +1. Identificá la fecha de la sesión (si no está explícita, usá la fecha actual) +2. Identificá el título de la sesión basado en el contenido +3. Extraé un resumen ejecutivo de 2-3 oraciones +4. Listá objetivos mencionados, marcando cuáles son NUEVOS vs existentes +5. Extraé tareas pendientes con su origen y prioridad (Alta/Media/Baja) +6. Identificá compromisos con responsable, fecha límite y estado +7. Listá decisiones tomadas durante la sesión +8. Detectá tareas completadas (si hay evidencia) +9. Incluí puntos clave, bloqueos o descubrimientos +10. No inventes información que no esté en la transcripción +11. Respondé SOLO con JSON válido Formato de respuesta JSON: { + "sessionDate": "YYYY-MM-DD", "sessionTitle": "Título descriptivo de la sesión", "summary": "Resumen ejecutivo de 2-3 oraciones", "objectives": [ diff --git a/src/services/transcriptions-db.ts b/src/services/transcriptions-db.ts new file mode 100644 index 0000000..9cb64a3 --- /dev/null +++ b/src/services/transcriptions-db.ts @@ -0,0 +1,61 @@ +import db, { type SessionRecord, type SessionSummaryRecord, type ProjectStateRecord } from '@/services/db' +export type { SessionRecord, SessionSummaryRecord, ProjectStateRecord } + +// ─── Sessions ───────────────────────────────────────────── + +export async function saveSession(s: Omit): Promise { + return db.sessions.add(s) +} + +export async function getSessionsByProject(projectId: number): Promise { + return db.sessions.where('projectId').equals(projectId).reverse().sortBy('date') +} + +export async function getSession(id: number): Promise { + return db.sessions.get(id) +} + +// ─── Session Summaries ──────────────────────────────────── + +export async function saveSessionSummary(s: SessionSummaryRecord) { + await db.session_summaries.put(s) +} + +export async function getSessionSummary(sessionId: number): Promise { + return db.session_summaries.get(sessionId) +} + +export async function getSummariesByProject(projectId: number): Promise { + const sessions = await getSessionsByProject(projectId) + const ids = sessions.map(s => s.id!).filter(Boolean) + const summaries: SessionSummaryRecord[] = [] + for (const id of ids) { + const s = await getSessionSummary(id) + if (s) summaries.push(s) + } + return summaries +} + +// ─── Project State ──────────────────────────────────────── + +export async function saveProjectState(s: ProjectStateRecord) { + await db.project_state.put(s) +} + +export async function getProjectState(projectId: number): Promise { + return db.project_state.get(projectId) +} + +// ─── Helpers ────────────────────────────────────────────── + +export async function deleteProjectData(projectId: number) { + const sessions = await getSessionsByProject(projectId) + const ids = sessions.map(s => s.id!).filter(Boolean) + await db.sessions.where('projectId').equals(projectId).delete() + await db.session_summaries.bulkDelete(ids) + await db.project_state.delete(projectId) +} + +export async function getSessionCount(projectId: number): Promise { + return db.sessions.where('projectId').equals(projectId).count() +} diff --git a/src/views/TranscriptionsView.vue b/src/views/TranscriptionsView.vue index 7a8ebd8..aa1fe09 100644 --- a/src/views/TranscriptionsView.vue +++ b/src/views/TranscriptionsView.vue @@ -12,8 +12,9 @@ import { DropdownMenuTrigger, } from '@/components/ui/dropdown-menu' import { analyzeSession, type SessionExtraction } from '@/services/session-analyzer' -import { generateProjectDoc, getProjectDoc } from '@/services/project-doc' +import { generateMasterDoc } from '@/services/project-doc' import { parseFile } from '@/services/parse-transcription' +import { saveSession, saveSessionSummary, saveProjectState, getSessionCount } from '@/services/transcriptions-db' import { Card, CardContent, @@ -93,12 +94,12 @@ const selectedProject = computed(() => watch(selectedProjectId, async (id) => { if (!id) return clearAll() - const doc = await getProjectDoc(id) - if (doc) { - docMarkdown.value = doc.markdown - docSessionCount.value = doc.sessionCount - docUpdatedAt.value = doc.updatedAt - parseSessionOffsets(doc.markdown) + const count = await getSessionCount(id) + docSessionCount.value = count + if (count > 0) { + const md = await generateMasterDoc(id, selectedProject.value?.name || '') + docMarkdown.value = md + parseSessionOffsets(md) } }, { immediate: false }) @@ -273,7 +274,48 @@ async function analyzeAsSession() { parsedText.value, selectedProject.value?.name || '', ) + + // 1. Guardar transcripción en BD + const now = new Date().toISOString() + const sessionId = await saveSession({ + projectId: selectedProjectId.value, + date: result.sessionDate || now.slice(0, 10), + title: result.sessionTitle, + fileName: parsedFileName.value, + fileType: parsedFileName.value.split('.').pop() || 'txt', + fileSize: parsedText.value.length, + rawText: parsedText.value, + processedAt: now, + }) + + // 2. Guardar resumen de IA en BD + await saveSessionSummary({ + sessionId, + summary: result.summary, + objectives: JSON.stringify(result.objectives), + tasks: JSON.stringify(result.pendingTasks), + commitments: JSON.stringify(result.commitments), + decisions: JSON.stringify(result.decisions), + keyPoints: JSON.stringify(result.keyPoints), + modelUsed: settingsStore.modelId, + }) + + // 3. Consolidar y actualizar project_state + await consolidateProjectState(selectedProjectId.value) + + // 4. Regenerar documento MD + const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '') + docMarkdown.value = md + docSessionCount.value = await getSessionCount(selectedProjectId.value) + docUpdatedAt.value = now.slice(0, 16).replace('T', ' ') + parseSessionOffsets(md) + docGenerated.value = true + + // 5. Mostrar resultado en UI sessionResult.value = result + parsedText.value = '' + parsedFileName.value = '' + } catch (e: any) { sessionError.value = e.message } finally { @@ -281,29 +323,71 @@ async function analyzeAsSession() { } } +async function consolidateProjectState(projectId: number) { + // Reconstruye project_state desde todas las summaries del proyecto + const { getSummariesByProject, getProjectState } = await import('@/services/transcriptions-db') + const summaries = await getSummariesByProject(projectId) + const prev = await getProjectState(projectId) + + const allObjectives: string[] = [] + const allTasks: { description: string; origin: string; priority: string; status?: string }[] = [] + const allCommitments: { description: string; responsible: string; dueDate: string; status: string }[] = [] + let consolidatedSummary = '' + + for (const s of summaries) { + const objs = safeParseJSON<{ text: string; isNew: boolean }[]>(s.objectives, []) + objs.forEach(o => { if (!allObjectives.includes(o.text)) allObjectives.push(o.text) }) + + const tasks = safeParseJSON<{ description: string; origin: string; priority: string }[]>(s.tasks, []) + tasks.forEach(t => { + const exists = allTasks.find(ex => ex.description === t.description) + if (!exists) allTasks.push({ ...t, status: 'pendiente' }) + }) + + const cmts = safeParseJSON<{ description: string; responsible: string; dueDate: string; status: string }[]>(s.commitments, []) + cmts.forEach(c => { + const exists = allCommitments.find(ex => ex.description === c.description) + if (!exists) allCommitments.push(c) + }) + + if (!consolidatedSummary) consolidatedSummary = s.summary + } + + // Preservar estados de tareas existentes (por si se marcaron como completadas) + if (prev) { + const prevTasks = safeParseJSON<{ description: string; status?: string }[]>(prev.tasks, []) + for (const pt of prevTasks) { + const match = allTasks.find(t => t.description === pt.description) + if (match && pt.status === 'completada') match.status = 'completada' + } + } + + await saveProjectState({ + projectId, + summary: consolidatedSummary, + objectives: JSON.stringify(allObjectives), + tasks: JSON.stringify(allTasks), + commitments: JSON.stringify(allCommitments), + updatedAt: new Date().toISOString(), + }) +} + +function safeParseJSON(json: string | undefined | null, fallback: T): T { + if (!json) return fallback + try { return JSON.parse(json) as T } catch { return fallback } +} + async function generateDoc() { - if (!sessionResult.value || !selectedProjectId.value || !parsedText.value) return + if (!selectedProjectId.value) return docGenerating.value = true - const prevDoc = await getProjectDoc(selectedProjectId.value) - try { - const doc = await generateProjectDoc( - selectedProjectId.value, - selectedProject.value?.name || '', - sessionResult.value, - parsedText.value, - parsedFileName.value, - prevDoc, - ) - docMarkdown.value = doc.markdown - docSessionCount.value = doc.sessionCount - docUpdatedAt.value = doc.updatedAt - parseSessionOffsets(doc.markdown) + const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '') + docMarkdown.value = md + docSessionCount.value = await getSessionCount(selectedProjectId.value) + docUpdatedAt.value = new Date().toISOString().slice(0, 16).replace('T', ' ') + parseSessionOffsets(md) docGenerated.value = true - parsedText.value = '' - parsedFileName.value = '' - sessionResult.value = null } catch (e: any) { sessionError.value = e.message } finally {