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