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
This commit is contained in:
2026-05-28 13:38:19 -05:00
parent b974788a16
commit 066047f3d1
6 changed files with 467 additions and 122 deletions
+180
View File
@@ -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*
+39 -1
View File
@@ -13,14 +13,52 @@ export interface ProjectDocRecord {
markdown: string 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 & { const db = new Dexie('alpha-core') as Dexie & {
settings: Dexie.Table<SettingEntry, string> settings: Dexie.Table<SettingEntry, string>
project_docs: Dexie.Table<ProjectDocRecord, number> project_docs: Dexie.Table<ProjectDocRecord, number>
sessions: Dexie.Table<SessionRecord, number>
session_summaries: Dexie.Table<SessionSummaryRecord, number>
project_state: Dexie.Table<ProjectStateRecord, number>
} }
db.version(2).stores({ db.version(3).stores({
settings: '&key', settings: '&key',
project_docs: '&projectId, projectName, updatedAt', project_docs: '&projectId, projectName, updatedAt',
sessions: '++id, projectId, date',
session_summaries: '&sessionId',
project_state: '&projectId',
}) })
export default db export default db
+65 -86
View File
@@ -1,66 +1,56 @@
import db from '@/services/db' import { getSessionsByProject, getSessionSummary, getProjectState, type SessionRecord, type ProjectStateRecord } from '@/services/transcriptions-db'
import type { SessionExtraction } from '@/services/session-analyzer'
export interface ProjectDoc { /**
projectId: number * Genera el markdown del proyecto completamente desde la BD.
projectName: string * No recibe datos de sesión directos — los obtiene de las tablas.
updatedAt: string */
sessionCount: number export async function generateMasterDoc(projectId: number, projectName: string): Promise<string> {
markdown: string 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<ProjectDoc | null> { function buildBlock1(
const doc = await db.table('project_docs').get(projectId) state: ProjectStateRecord | undefined,
return (doc as ProjectDoc) || null
}
export async function generateProjectDoc(
projectId: number,
projectName: string, projectName: string,
extraction: SessionExtraction, sessionCount: number,
transcriptionText: string, ): string {
fileName: string,
previousDoc?: ProjectDoc | null,
): Promise<ProjectDoc> {
const now = new Date().toISOString().slice(0, 16).replace('T', ' ') const now = new Date().toISOString().slice(0, 16).replace('T', ' ')
const sessionCount = (previousDoc?.sessionCount || 0) + 1
// ─── Block 1: Summary (replaced) ─────────────────────── const objectives = safeParse<string[]>(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 => const objectivesMd = objectives.map(o => `- [ ] ${o}`).join('\n')
o.isNew ? `- [ ] ${o.text} 🆕` : `- [ ] ${o.text}` const tasksMd = tasks.map((t, i) => `| ${i + 1} | [${t.status === 'completada' ? 'x' : ' '}] ${t.description} | ${t.origin} | ${t.priority} |`).join('\n')
).join('\n') const commitmentsMd = commitments.map(c =>
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 =>
`| ${c.description} | ${c.responsible} | ${c.dueDate} | ${c.status === 'Cumplido' ? '✅' : '⏳'} | — |` `| ${c.description} | ${c.responsible} | ${c.dueDate} | ${c.status === 'Cumplido' ? '✅' : '⏳'} | — |`
).join('\n') ).join('\n')
const milestonesMd = commitments
const completedMd = extraction.completedTasks.map(t => `- [x] ${t}`).join('\n')
const milestonesMd = extraction.commitments
.filter(c => c.dueDate && c.status !== 'Cumplido') .filter(c => c.dueDate && c.status !== 'Cumplido')
.map(c => `- **${c.dueDate}**: ${c.description}`) .map(c => `- **${c.dueDate}**: ${c.description} (_${c.responsible}_)`)
.join('\n') .join('\n')
const block1 = `# 📋 ${projectName} — Resumen Ejecutivo return `# 📋 ${projectName} — Resumen Ejecutivo
> ⚠️ Última actualización: ${now} > ⚠️ Última actualización: ${now}
--- ---
## 🎯 Resumen Ejecutivo ## 🎯 Resumen Ejecutivo
${extraction.summary} ${summary}
## 🎯 Objetivos ## 🎯 Objetivos
${objectivesMd || '_Sin objetivos registrados_'} ${objectivesMd || '_Sin objetivos registrados_'}
## 📝 Tareas Pendientes ## 📝 Tareas Pendientes
| # | Tarea | Origen | Fecha creación | Prioridad | | # | Tarea | Origen | Prioridad |
|---|-------|--------|----------------|-----------| |---|-------|--------|-----------|
${tasksMd || '_Sin tareas pendientes_'} ${tasksMd || '_Sin tareas pendientes_'}
## ✅ Compromisos ## ✅ Compromisos
@@ -68,76 +58,65 @@ ${tasksMd || '_Sin tareas pendientes_'}
|------------|-------------|--------------|--------|-------| |------------|-------------|--------------|--------|-------|
${commitmentsMd || '_Sin compromisos_'} ${commitmentsMd || '_Sin compromisos_'}
## ✅ Tareas Completadas
${completedMd || '_Sin tareas completadas en esta sesión_'}
## 📅 Próximos Hitos ## 📅 Próximos Hitos
${milestonesMd || '_Sin hitos próximos_'} ${milestonesMd || '_Sin hitos próximos_'}
## 📊 Métricas de Seguimiento ## 📊 Métricas de Seguimiento
- Sesiones registradas: ${sessionCount} - Sesiones registradas: ${sessionCount}
- Tareas pendientes: ${extraction.pendingTasks.length} - Tareas pendientes: ${tasks.filter(t => t.status !== 'completada').length}
- Compromisos cumplidos: ${extraction.commitments.filter(c => c.status === 'Cumplido').length}/${extraction.commitments.length} - Compromisos cumplidos: ${commitments.filter(c => c.status === 'Cumplido').length}/${commitments.length}`
- Decisiones tomadas: ${extraction.decisions.length} }
--- async function buildBlock2(sessions: SessionRecord[]): Promise<string> {
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<string[]>(summary?.decisions, [])
const keyPoints = safeParse<string[]>(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') **Archivo fuente:** \`${session.fileName}\`
const keyPointsMd = extraction.keyPoints.map(k => `- ${k}`).join('\n')
const sessionEntry = `---
## 📍 Sesión ${sessionCount}: ${extraction.sessionTitle}
**Archivo fuente:** \`${fileName}\`
**Fecha:** ${now}
### Resumen de la sesión ### 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 ### 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 ### Decisiones tomadas
${decisionsMd || '_Ninguna_'} ${decisions.map(d => `- ${d}`).join('\n') || '_Ninguna_'}
### Puntos clave ### Puntos clave
${keyPointsMd || '_Ninguno_'} ${keyPoints.map(k => `- ${k}`).join('\n') || '_Ninguno_'}
### Transcripción completa ### Transcripción completa
\`\`\` \`\`\`
${transcriptionText} ${session.rawText}
\`\`\` \`\`\``
`
// ─── Assemble document ───────────────────────────────── parts.push(entry)
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,
} }
// Save to Dexie return parts.join('\n')
await db.table('project_docs').put(doc) }
return doc function safeParse<T>(json: string | undefined | null, fallback: T): T {
if (!json) return fallback
try { return JSON.parse(json) as T } catch { return fallback }
} }
+13 -10
View File
@@ -1,6 +1,7 @@
import { callAI } from '@/services/ai' import { callAI } from '@/services/ai'
export interface SessionExtraction { export interface SessionExtraction {
sessionDate: string // YYYY-MM-DD
sessionTitle: string sessionTitle: string
summary: string summary: string
objectives: { text: string; isNew: boolean }[] 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. const SESSION_SYSTEM_PROMPT = `Eres un asistente de gestión de proyectos. Analizás transcripciones de reuniones y extraés información estructurada.
Reglas: Reglas:
1. Identificá el título de la sesión basado en el contenido y fecha 1. Identificá la fecha de la sesión (si no está explícita, usá la fecha actual)
2. Extraé un resumen ejecutivo de 2-3 oraciones 2. Identificá el título de la sesión basado en el contenido
3. Listá objetivos mencionados, marcando cuáles son NUEVOS vs existentes 3. Extraé un resumen ejecutivo de 2-3 oraciones
4. Extraé tareas pendientes con su origen y prioridad (Alta/Media/Baja) 4. Listá objetivos mencionados, marcando cuáles son NUEVOS vs existentes
5. Identificá compromisos con responsable, fecha límite y estado 5. Extraé tareas pendientes con su origen y prioridad (Alta/Media/Baja)
6. Listá decisiones tomadas durante la sesión 6. Identificá compromisos con responsable, fecha límite y estado
7. Detectá tareas completadas (si hay evidencia) 7. Listá decisiones tomadas durante la sesión
8. Incluí puntos clave, bloqueos o descubrimientos 8. Detectá tareas completadas (si hay evidencia)
9. No inventes información que no esté en la transcripción 9. Incluí puntos clave, bloqueos o descubrimientos
10. Respondé SOLO con JSON válido 10. No inventes información que no esté en la transcripción
11. Respondé SOLO con JSON válido
Formato de respuesta JSON: Formato de respuesta JSON:
{ {
"sessionDate": "YYYY-MM-DD",
"sessionTitle": "Título descriptivo de la sesión", "sessionTitle": "Título descriptivo de la sesión",
"summary": "Resumen ejecutivo de 2-3 oraciones", "summary": "Resumen ejecutivo de 2-3 oraciones",
"objectives": [ "objectives": [
+61
View File
@@ -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<SessionRecord, 'id'>): Promise<number> {
return db.sessions.add(s)
}
export async function getSessionsByProject(projectId: number): Promise<SessionRecord[]> {
return db.sessions.where('projectId').equals(projectId).reverse().sortBy('date')
}
export async function getSession(id: number): Promise<SessionRecord | undefined> {
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<SessionSummaryRecord | undefined> {
return db.session_summaries.get(sessionId)
}
export async function getSummariesByProject(projectId: number): Promise<SessionSummaryRecord[]> {
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<ProjectStateRecord | undefined> {
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<number> {
return db.sessions.where('projectId').equals(projectId).count()
}
+109 -25
View File
@@ -12,8 +12,9 @@ import {
DropdownMenuTrigger, DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu' } from '@/components/ui/dropdown-menu'
import { analyzeSession, type SessionExtraction } from '@/services/session-analyzer' 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 { parseFile } from '@/services/parse-transcription'
import { saveSession, saveSessionSummary, saveProjectState, getSessionCount } from '@/services/transcriptions-db'
import { import {
Card, Card,
CardContent, CardContent,
@@ -93,12 +94,12 @@ const selectedProject = computed(() =>
watch(selectedProjectId, async (id) => { watch(selectedProjectId, async (id) => {
if (!id) return if (!id) return
clearAll() clearAll()
const doc = await getProjectDoc(id) const count = await getSessionCount(id)
if (doc) { docSessionCount.value = count
docMarkdown.value = doc.markdown if (count > 0) {
docSessionCount.value = doc.sessionCount const md = await generateMasterDoc(id, selectedProject.value?.name || '')
docUpdatedAt.value = doc.updatedAt docMarkdown.value = md
parseSessionOffsets(doc.markdown) parseSessionOffsets(md)
} }
}, { immediate: false }) }, { immediate: false })
@@ -273,7 +274,48 @@ async function analyzeAsSession() {
parsedText.value, parsedText.value,
selectedProject.value?.name || '', 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 sessionResult.value = result
parsedText.value = ''
parsedFileName.value = ''
} catch (e: any) { } catch (e: any) {
sessionError.value = e.message sessionError.value = e.message
} finally { } 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<T>(json: string | undefined | null, fallback: T): T {
if (!json) return fallback
try { return JSON.parse(json) as T } catch { return fallback }
}
async function generateDoc() { async function generateDoc() {
if (!sessionResult.value || !selectedProjectId.value || !parsedText.value) return if (!selectedProjectId.value) return
docGenerating.value = true docGenerating.value = true
const prevDoc = await getProjectDoc(selectedProjectId.value)
try { try {
const doc = await generateProjectDoc( const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '')
selectedProjectId.value, docMarkdown.value = md
selectedProject.value?.name || '', docSessionCount.value = await getSessionCount(selectedProjectId.value)
sessionResult.value, docUpdatedAt.value = new Date().toISOString().slice(0, 16).replace('T', ' ')
parsedText.value, parseSessionOffsets(md)
parsedFileName.value,
prevDoc,
)
docMarkdown.value = doc.markdown
docSessionCount.value = doc.sessionCount
docUpdatedAt.value = doc.updatedAt
parseSessionOffsets(doc.markdown)
docGenerated.value = true docGenerated.value = true
parsedText.value = ''
parsedFileName.value = ''
sessionResult.value = null
} catch (e: any) { } catch (e: any) {
sessionError.value = e.message sessionError.value = e.message
} finally { } finally {