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
+39 -1
View File
@@ -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<SettingEntry, string>
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',
project_docs: '&projectId, projectName, updatedAt',
sessions: '++id, projectId, date',
session_summaries: '&sessionId',
project_state: '&projectId',
})
export default db
+65 -86
View File
@@ -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<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> {
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<ProjectDoc> {
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<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 =>
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<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')
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<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'
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": [
+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,
} 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<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() {
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 {