diff --git a/app/components/entrelineas/EntrelineaDetail.vue b/app/components/entrelineas/EntrelineaDetail.vue index e817449..bcc1a31 100644 --- a/app/components/entrelineas/EntrelineaDetail.vue +++ b/app/components/entrelineas/EntrelineaDetail.vue @@ -47,8 +47,38 @@ const title = computed(() => origin.value || (props.document?.id as string) || 'Entrelínea' ) +function formatEntrelineaText(html: string): string { + if (!html) return '' + + const sPrefix = "font-family:'Times New Roman',serif;font-style:italic;color:#c00000" + const sBracket = "font-family:'Times New Roman',serif;font-style:italic;color:#c00000" + const sInner = "font-family:'Times New Roman',serif;font-style:italic;color:#c00000;text-decoration:underline" + + const wrapLine = (content: string, isFirst: boolean): string => + (isFirst ? `[WSS] ` : '') + + `«` + + `${content}` + + `»` + + if (/]/i.test(html)) { + const lines = html + .split(/]*)>/i) + .map(part => part.replace(/<\/p>/gi, '').trim()) + .filter(Boolean) + if (lines.length) return lines.map((l, i) => wrapLine(l, i === 0)).join('
') + return html + } + + if (/
/i).map(p => p.trim()).filter(Boolean) + if (parts.length) return parts.map((l, i) => wrapLine(l, i === 0)).join('
') + } + + return wrapLine(html.trim(), true) +} + const bodyHtml = computed(() => - props.highlightedText || props.document?.text || '' + formatEntrelineaText(props.highlightedText || props.document?.text || '') ) /* -------------------------------------------------------------------------- */ diff --git a/app/pages/conferencias-typensense.vue b/app/pages/conferencias-typensense.vue index 79d1642..64effea 100644 --- a/app/pages/conferencias-typensense.vue +++ b/app/pages/conferencias-typensense.vue @@ -79,9 +79,20 @@ export interface TypesenseParagraphHit { text_match?: number } +interface TypesenseGroupedHit { + group_key: string[] + hits: TypesenseParagraphHit[] +} + interface TypesenseSearchResponse { found: number - hits?: TypesenseParagraphHit[] + grouped_hits?: TypesenseGroupedHit[] +} + +interface SearchGroup { + docId: string + firstHit: TypesenseParagraphHit + allHits: TypesenseParagraphHit[] } interface BrowseItem { @@ -97,31 +108,18 @@ interface DisplayGroup { // ---- State ---------------------------------------------------------------- -// Modo búsqueda (con query): hits de párrafos agrupados por documento -const hits = ref([]) +// Modo búsqueda (con query): grupos devueltos por Typesense +const groupedHits = ref([]) const total = ref(0) const currentPage = ref(1) const hasMore = computed(() => - settings.paginationType === 'infinite_scroll' ? hits.value.length < total.value : false + settings.paginationType === 'infinite_scroll' ? groupedHits.value.length < total.value : false ) // Progressive display (sólo en scroll infinito) const visibleGroupCount = ref(10) -const groupedHits = computed(() => { - const map = new Map() - for (const hit of hits.value) { - const id = hit.document.document_id - if (!map.has(id)) map.set(id, []) - map.get(id)!.push(hit) - } - return [...map.entries()].map(([docId, docHits]) => ({ - docId, - firstHit: docHits[0]! - })) -}) - const visibleGroups = computed(() => settings.paginationType === 'infinite_scroll' ? groupedHits.value.slice(0, visibleGroupCount.value) @@ -243,9 +241,12 @@ async function runSearch(q: string, page = 1, append = false) { page: typePage, highlightFullFields: QUERY_BY, highlightFields: QUERY_BY, + highlightFullFields: QUERY_BY, + highlightFields: QUERY_BY, highlightStartTag: '', highlightEndTag: '', - snippetThreshold: 100 + highlightAffixNumTokens: 30, + groupBy: 'document_id' }] } }) @@ -253,16 +254,21 @@ async function runSearch(q: string, page = 1, append = false) { if (seq !== searchSeq) return const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse - const newHits = res?.hits ?? [] + const rawGroups = res?.grouped_hits ?? [] + const newGroups: SearchGroup[] = rawGroups.map(g => ({ + docId: g.group_key[0]!, + firstHit: g.hits[0]!, + allHits: g.hits + })) if (!append) docCache.value = {} - const newDocIds = [...new Set(newHits.map(h => h.document.document_id).filter(Boolean))] + const newDocIds = newGroups.map(g => g.docId).filter(Boolean) await fetchDocumentMeta(newDocIds) if (seq !== searchSeq) return - hits.value = append ? hits.value.concat(newHits) : newHits - total.value = res?.found ?? hits.value.length + groupedHits.value = append ? groupedHits.value.concat(newGroups) : newGroups + total.value = res?.found ?? groupedHits.value.length currentPage.value = typePage if (!append) activePage.value = page } catch (err: unknown) { @@ -270,7 +276,7 @@ async function runSearch(q: string, page = 1, append = false) { console.error('Typesense error', err) errorMsg.value = (err as Error)?.message || 'Error al buscar.' if (!append) { - hits.value = [] + groupedHits.value = [] total.value = 0 } } finally { @@ -360,7 +366,7 @@ function goToPage(p: number) { browseItems.value = [] runBrowse(p, false) } else { - hits.value = [] + groupedHits.value = [] runSearch(query.value, p, false) } } @@ -399,7 +405,7 @@ onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) }) watch(debouncedQuery, (q) => { activePage.value = 1 if (!q.trim()) { - hits.value = [] + groupedHits.value = [] total.value = 0 currentPage.value = 1 visibleGroupCount.value = 10 @@ -411,7 +417,7 @@ watch(debouncedQuery, (q) => { browseItems.value = [] browseTotal.value = 0 browsePage.value = 1 - hits.value = [] + groupedHits.value = [] total.value = 0 currentPage.value = 1 visibleGroupCount.value = 10 @@ -498,9 +504,8 @@ async function fetchDocumentParagraphs(docId: string) { async function selectGroup(group: DisplayGroup) { selectedDocId.value = group.docId selectedHit.value = group.firstHit - selectedMatchingHits.value = group.firstHit - ? hits.value.filter(h => h.document.document_id === group.docId) - : [] + const searchGroup = groupedHits.value.find(g => g.docId === group.docId) + selectedMatchingHits.value = searchGroup?.allHits ?? [] fetchFullDocument(group.docId) fetchDocumentParagraphs(group.docId) } diff --git a/app/pages/estudios-typensense.vue b/app/pages/estudios-typensense.vue index d809c88..4968a06 100644 --- a/app/pages/estudios-typensense.vue +++ b/app/pages/estudios-typensense.vue @@ -79,9 +79,20 @@ export interface TypesenseParagraphHit { text_match?: number } +interface TypesenseGroupedHit { + groupKey: string[] + hits: TypesenseParagraphHit[] +} + interface TypesenseSearchResponse { found: number - hits?: TypesenseParagraphHit[] + groupedHits?: TypesenseGroupedHit[] +} + +interface SearchGroup { + docId: string + firstHit: TypesenseParagraphHit + allHits: TypesenseParagraphHit[] } interface BrowseItem { @@ -97,31 +108,18 @@ interface DisplayGroup { // ---- State ---------------------------------------------------------------- -// Modo búsqueda (con query): hits de párrafos agrupados por documento -const hits = ref([]) +// Modo búsqueda (con query): grupos devueltos por Typesense +const groupedHits = ref([]) const total = ref(0) const currentPage = ref(1) const hasMore = computed(() => - settings.paginationType === 'infinite_scroll' ? hits.value.length < total.value : false + settings.paginationType === 'infinite_scroll' ? groupedHits.value.length < total.value : false ) // Progressive display (sólo en scroll infinito) const visibleGroupCount = ref(10) -const groupedHits = computed(() => { - const map = new Map() - for (const hit of hits.value) { - const id = hit.document.document_id - if (!map.has(id)) map.set(id, []) - map.get(id)!.push(hit) - } - return [...map.entries()].map(([docId, docHits]) => ({ - docId, - firstHit: docHits[0]! - })) -}) - const visibleGroups = computed(() => settings.paginationType === 'infinite_scroll' ? groupedHits.value.slice(0, visibleGroupCount.value) @@ -245,24 +243,32 @@ async function runSearch(q: string, page = 1, append = false) { highlightFields: QUERY_BY, highlightStartTag: '', highlightEndTag: '', - highlightAffixNumTokens: 30 + highlightAffixNumTokens: 30, + groupBy: 'document_id' }] } }) - + console.log('Search response', multi) if (seq !== searchSeq) return const res = (multi?.results?.[0] ?? {}) as TypesenseSearchResponse - const newHits = res?.hits ?? [] + const rawGroups = res?.groupedHits ?? [] + + console.log('Raw groups', rawGroups) + const newGroups: SearchGroup[] = rawGroups.map(g => ({ + docId: g.groupKey[0]!, + firstHit: g.hits[0]!, + allHits: g.hits + })) if (!append) docCache.value = {} - const newDocIds = [...new Set(newHits.map(h => h.document.document_id).filter(Boolean))] + const newDocIds = newGroups.map(g => g.docId).filter(Boolean) await fetchDocumentMeta(newDocIds) if (seq !== searchSeq) return - hits.value = append ? hits.value.concat(newHits) : newHits - total.value = res?.found ?? hits.value.length + groupedHits.value = append ? groupedHits.value.concat(newGroups) : newGroups + total.value = res?.found ?? groupedHits.value.length currentPage.value = typePage if (!append) activePage.value = page } catch (err: unknown) { @@ -270,7 +276,7 @@ async function runSearch(q: string, page = 1, append = false) { console.error('Typesense error', err) errorMsg.value = (err as Error)?.message || 'Error al buscar.' if (!append) { - hits.value = [] + groupedHits.value = [] total.value = 0 } } finally { @@ -320,7 +326,6 @@ async function runBrowse(page = 1, append = false) { }) if (seq !== searchSeq) return - const result = (multi?.results?.[0] as { found?: number, hits?: Array<{ document: DocMeta }> } | undefined) const newItems = (result?.hits ?? []).map(h => ({ docId: h.document.id!, @@ -360,7 +365,7 @@ function goToPage(p: number) { browseItems.value = [] runBrowse(p, false) } else { - hits.value = [] + groupedHits.value = [] runSearch(query.value, p, false) } } @@ -399,7 +404,7 @@ onBeforeUnmount(() => { if (timeoutId) clearTimeout(timeoutId) }) watch(debouncedQuery, (q) => { activePage.value = 1 if (!q.trim()) { - hits.value = [] + groupedHits.value = [] total.value = 0 currentPage.value = 1 visibleGroupCount.value = 10 @@ -411,7 +416,7 @@ watch(debouncedQuery, (q) => { browseItems.value = [] browseTotal.value = 0 browsePage.value = 1 - hits.value = [] + groupedHits.value = [] total.value = 0 currentPage.value = 1 visibleGroupCount.value = 10 @@ -498,9 +503,8 @@ async function fetchDocumentParagraphs(docId: string) { async function selectGroup(group: DisplayGroup) { selectedDocId.value = group.docId selectedHit.value = group.firstHit - selectedMatchingHits.value = group.firstHit - ? hits.value.filter(h => h.document.document_id === group.docId) - : [] + const searchGroup = groupedHits.value.find(g => g.docId === group.docId) + selectedMatchingHits.value = searchGroup?.allHits ?? [] fetchFullDocument(group.docId) fetchDocumentParagraphs(group.docId) }