dedup sesiones + calendario desde BD + scroll por fecha
- analyzeAsSession: detecta duplicados por UTC timestamp antes de guardar - TranscriptionsView: sessionDatesList reemplaza sessionOffsets - session dates se leen directo de tabla sessions (no desde markdown) - scrollToSession busca por fecha en markdown con regex escapada - calendario usa BD como fuente de verdad
This commit is contained in:
@@ -14,7 +14,7 @@ import {
|
|||||||
import { analyzeSession, type SessionExtraction } from '@/services/session-analyzer'
|
import { analyzeSession, type SessionExtraction } from '@/services/session-analyzer'
|
||||||
import { generateMasterDoc } 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 { saveSession, saveSessionSummary, saveProjectState, getSessionCount, getSessionsByProject } from '@/services/transcriptions-db'
|
||||||
import { parseTeamsUTC, toColombiaTime } from '@/services/timezone'
|
import { parseTeamsUTC, toColombiaTime } from '@/services/timezone'
|
||||||
import {
|
import {
|
||||||
Card,
|
Card,
|
||||||
@@ -77,7 +77,7 @@ const docGenerated = ref(false)
|
|||||||
const docMarkdown = ref('')
|
const docMarkdown = ref('')
|
||||||
const docSessionCount = ref(0)
|
const docSessionCount = ref(0)
|
||||||
const docUpdatedAt = ref('')
|
const docUpdatedAt = ref('')
|
||||||
const sessionOffsets = ref<{ date: string; title: string; offset: number }[]>([])
|
const sessionDatesList = ref<{ date: string; title: string; fileName: string }[]>([])
|
||||||
const viewerRef = ref<HTMLTextAreaElement | null>(null)
|
const viewerRef = ref<HTMLTextAreaElement | null>(null)
|
||||||
|
|
||||||
// ─── Calendar state ──────────────────────────────────────
|
// ─── Calendar state ──────────────────────────────────────
|
||||||
@@ -96,45 +96,43 @@ const selectedProject = computed(() =>
|
|||||||
watch(selectedProjectId, async (id) => {
|
watch(selectedProjectId, async (id) => {
|
||||||
if (!id) return
|
if (!id) return
|
||||||
clearAll()
|
clearAll()
|
||||||
const count = await getSessionCount(id)
|
await loadSessionDates(id)
|
||||||
|
const count = sessionDatesList.value.length
|
||||||
docSessionCount.value = count
|
docSessionCount.value = count
|
||||||
if (count > 0) {
|
if (count > 0) {
|
||||||
const md = await generateMasterDoc(id, selectedProject.value?.name || '')
|
const md = await generateMasterDoc(id, selectedProject.value?.name || '')
|
||||||
docMarkdown.value = md
|
docMarkdown.value = md
|
||||||
parseSessionOffsets(md)
|
|
||||||
}
|
}
|
||||||
}, { immediate: false })
|
}, { immediate: false })
|
||||||
|
|
||||||
function parseSessionOffsets(md: string) {
|
async function loadSessionDates(projectId: number) {
|
||||||
const offsets: { date: string; title: string; offset: number }[] = []
|
const sessions = await getSessionsByProject(projectId)
|
||||||
const re = /## 📍 Sesión \d+:\s*(.+?)\n\*\*Archivo fuente:/g
|
sessionDatesList.value = sessions.map(s => ({
|
||||||
let match
|
date: s.date,
|
||||||
while ((match = re.exec(md)) !== null) {
|
title: s.title,
|
||||||
const title = match[1].trim()
|
fileName: s.fileName,
|
||||||
const dateMatch = title.match(/(\d{4}-\d{2}-\d{2})/)
|
}))
|
||||||
offsets.push({
|
|
||||||
date: dateMatch?.[1] || '',
|
|
||||||
title,
|
|
||||||
offset: match.index,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
sessionOffsets.value = offsets
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function scrollToSession(offset: number) {
|
function scrollToSession(date: string) {
|
||||||
if (viewerRef.value) {
|
if (!viewerRef.value || !docMarkdown.value) return
|
||||||
const textarea = viewerRef.value
|
|
||||||
const md = docMarkdown.value
|
const md = docMarkdown.value
|
||||||
// Calculate line number from offset
|
// Buscar la sesión por fecha en el markdown: ## 📍 Sesión: YYYY-MM-DD
|
||||||
const lineNum = md.slice(0, offset).split('\n').length
|
const re = new RegExp(`## 📍 Sesión: ${escapeRegex(date)}`)
|
||||||
const lineHeight = 20 // approximate
|
const match = re.exec(md)
|
||||||
textarea.scrollTop = (lineNum - 5) * lineHeight
|
if (!match) return
|
||||||
textarea.focus()
|
const lineNum = md.slice(0, match.index).split('\n').length
|
||||||
|
const lineHeight = 20
|
||||||
|
viewerRef.value.scrollTop = (lineNum - 5) * lineHeight
|
||||||
|
viewerRef.value.focus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function escapeRegex(s: string) {
|
||||||
|
return s.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||||
}
|
}
|
||||||
|
|
||||||
function sessionDates(): string[] {
|
function sessionDates(): string[] {
|
||||||
return sessionOffsets.value.map(s => s.date).filter(Boolean)
|
return sessionDatesList.value.map(s => s.date).filter(Boolean)
|
||||||
}
|
}
|
||||||
|
|
||||||
// ─── Calendar helpers ────────────────────────────────────
|
// ─── Calendar helpers ────────────────────────────────────
|
||||||
@@ -149,14 +147,13 @@ function firstDayOfMonth(m: number, y: number) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function isSessionDate(d: number) {
|
function isSessionDate(d: number) {
|
||||||
const ds = sessionDates()
|
|
||||||
const dateStr = `${calYear.value}-${String(calMonth.value + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
const dateStr = `${calYear.value}-${String(calMonth.value + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||||
return ds.includes(dateStr)
|
return sessionDatesList.value.some(s => s.date === dateStr)
|
||||||
}
|
}
|
||||||
|
|
||||||
function getSessionForDate(d: number) {
|
function getSessionForDate(d: number) {
|
||||||
const dateStr = `${calYear.value}-${String(calMonth.value + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
const dateStr = `${calYear.value}-${String(calMonth.value + 1).padStart(2, '0')}-${String(d).padStart(2, '0')}`
|
||||||
return sessionOffsets.value.find(s => s.date === dateStr)
|
return sessionDatesList.value.find(s => s.date === dateStr) || null
|
||||||
}
|
}
|
||||||
|
|
||||||
function prevMonth() {
|
function prevMonth() {
|
||||||
@@ -283,7 +280,20 @@ async function analyzeAsSession() {
|
|||||||
sessionTeamsInfo.value = teamsDate ? { utc: teamsDate.utc, timeStr: teamsDate.timeStr } : null
|
sessionTeamsInfo.value = teamsDate ? { utc: teamsDate.utc, timeStr: teamsDate.timeStr } : null
|
||||||
const sessionDate = teamsDate?.dateStr || result.sessionDate || now.slice(0, 10)
|
const sessionDate = teamsDate?.dateStr || result.sessionDate || now.slice(0, 10)
|
||||||
|
|
||||||
// 1b. Guardar transcripción en BD
|
// 1b. Deduplicación: verificar si ya existe una sesión con el mismo UTC timestamp
|
||||||
|
const existing = await getSessionsByProject(selectedProjectId.value)
|
||||||
|
const isDuplicate = existing.some(s => {
|
||||||
|
// Comparar por UTC timestamp en filename o por fecha + título similar
|
||||||
|
const existingUTC = parseTeamsUTC(s.fileName)
|
||||||
|
return teamsDate !== null && existingUTC !== null && teamsDate.utc === existingUTC.utc
|
||||||
|
})
|
||||||
|
if (isDuplicate) {
|
||||||
|
sessionError.value = `Esta sesión ya fue registrada (UTC ${teamsDate?.utc}). Se descarta.`
|
||||||
|
sessionLoading.value = false
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1c. Guardar transcripción en BD
|
||||||
const sessionId = await saveSession({
|
const sessionId = await saveSession({
|
||||||
projectId: selectedProjectId.value,
|
projectId: selectedProjectId.value,
|
||||||
date: sessionDate,
|
date: sessionDate,
|
||||||
@@ -313,9 +323,9 @@ async function analyzeAsSession() {
|
|||||||
// 4. Regenerar documento MD
|
// 4. Regenerar documento MD
|
||||||
const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '')
|
const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '')
|
||||||
docMarkdown.value = md
|
docMarkdown.value = md
|
||||||
docSessionCount.value = await getSessionCount(selectedProjectId.value)
|
await loadSessionDates(selectedProjectId.value)
|
||||||
|
docSessionCount.value = sessionDatesList.value.length
|
||||||
docUpdatedAt.value = now.slice(0, 16).replace('T', ' ')
|
docUpdatedAt.value = now.slice(0, 16).replace('T', ' ')
|
||||||
parseSessionOffsets(md)
|
|
||||||
docGenerated.value = true
|
docGenerated.value = true
|
||||||
|
|
||||||
// 5. Mostrar resultado en UI
|
// 5. Mostrar resultado en UI
|
||||||
@@ -391,9 +401,9 @@ async function generateDoc() {
|
|||||||
try {
|
try {
|
||||||
const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '')
|
const md = await generateMasterDoc(selectedProjectId.value, selectedProject.value?.name || '')
|
||||||
docMarkdown.value = md
|
docMarkdown.value = md
|
||||||
docSessionCount.value = await getSessionCount(selectedProjectId.value)
|
await loadSessionDates(selectedProjectId.value)
|
||||||
|
docSessionCount.value = sessionDatesList.value.length
|
||||||
docUpdatedAt.value = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
docUpdatedAt.value = new Date().toISOString().slice(0, 16).replace('T', ' ')
|
||||||
parseSessionOffsets(md)
|
|
||||||
docGenerated.value = true
|
docGenerated.value = true
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
sessionError.value = e.message
|
sessionError.value = e.message
|
||||||
@@ -667,7 +677,7 @@ function clearAll() {
|
|||||||
</div>
|
</div>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent class="pt-0">
|
<CardContent class="pt-0">
|
||||||
<div v-if="sessionOffsets.length === 0" class="text-xs text-muted-foreground text-center py-2">
|
<div v-if="sessionDatesList.length === 0" class="text-xs text-muted-foreground text-center py-2">
|
||||||
{{ t('transcriptions.noSessions') }}
|
{{ t('transcriptions.noSessions') }}
|
||||||
</div>
|
</div>
|
||||||
<template v-else>
|
<template v-else>
|
||||||
@@ -681,7 +691,7 @@ function clearAll() {
|
|||||||
:key="d"
|
:key="d"
|
||||||
class="py-0.5 rounded hover:bg-muted transition-colors relative"
|
class="py-0.5 rounded hover:bg-muted transition-colors relative"
|
||||||
:class="isSessionDate(d) ? 'bg-primary/10 text-primary font-bold' : 'text-muted-foreground'"
|
:class="isSessionDate(d) ? 'bg-primary/10 text-primary font-bold' : 'text-muted-foreground'"
|
||||||
@click="() => { const s = getSessionForDate(d); if (s) scrollToSession(s.offset) }"
|
@click="() => { const s = getSessionForDate(d); if (s) scrollToSession(s.date) }"
|
||||||
:title="getSessionForDate(d)?.title || ''"
|
:title="getSessionForDate(d)?.title || ''"
|
||||||
>
|
>
|
||||||
{{ d }}
|
{{ d }}
|
||||||
|
|||||||
Reference in New Issue
Block a user