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:
@@ -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
@@ -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
@@ -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 }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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": [
|
||||||
|
|||||||
@@ -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()
|
||||||
|
}
|
||||||
@@ -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 {
|
||||||
|
|||||||
Reference in New Issue
Block a user