Primer commit

This commit is contained in:
unknown
2026-07-14 14:50:48 -06:00
commit 2a1a3d40b7
51 changed files with 5905 additions and 0 deletions

View File

@@ -0,0 +1,422 @@
// Composable para gestionar los resultados de búsqueda y las sesiones de búsqueda.
import { computed, ref, watch } from 'vue'
import { useWsMonitor } from './useWsMonitor'
import { adaptSearchPayload } from '../services/resultAdapters'
const sessions = ref([])
const activeSessionId = ref('')
const pendingRequests = ref({})
const selectedCardIds = ref([])
let bridgeInitialized = false
let sequence = 0
const processedLogIds = new Set()
const uid = (prefix) => {
sequence += 1
return `${prefix}_${Date.now()}_${sequence}`
}
const hashString = (value) => {
const raw = String(value || '')
let hash = 5381
for (let i = 0; i < raw.length; i += 1) {
hash = ((hash << 5) + hash) + raw.charCodeAt(i)
hash &= 0xffffffff
}
return `h${Math.abs(hash)}`
}
const getActiveSession = () => sessions.value.find((item) => item.id === activeSessionId.value) || null
const activeSession = computed(() => getActiveSession())
const selectedSet = computed(() => new Set(selectedCardIds.value))
const findSessionById = (sessionId) => sessions.value.find((item) => item.id === sessionId) || null
const findCardById = (cardId) => {
for (const session of sessions.value) {
const card = session.cardsById[cardId]
if (card) {
return { session, card }
}
}
return { session: null, card: null }
}
const ensureSourceGroup = (session, sourceKey, sourceLabel) => {
if (session.groupMap[sourceKey]) {
return session.groupMap[sourceKey]
}
const group = {
id: uid('group'),
sourceKey,
sourceLabel,
status: 'ok',
error: '',
cards: [],
}
session.groupMap[sourceKey] = group
session.groups.push(group)
return group
}
const createSession = (rootRequestId, rootQuery, rootContext) => {
const session = {
id: uid('session'),
createdAt: new Date().toISOString(),
rootRequestId,
rootQuery,
rootContext,
groups: [],
groupMap: {},
cardsById: {},
}
sessions.value.unshift(session)
activeSessionId.value = session.id
return session
}
const pushChildRequestPlaceholder = (parentCardId, requestId, query, context) => {
if (!parentCardId) {
return
}
const { card } = findCardById(parentCardId)
if (!card) {
return
}
card.childRequests.unshift({
requestId,
query,
context,
status: 'loading',
error: '',
resultCardIds: [],
createdAt: new Date().toISOString(),
})
}
// Actualiza el estado de una solicitud de búsqueda secundaria asociada a un card padre, incluyendo el estado, los IDs de las cards resultantes y cualquier error.
const setChildRequestResult = (parentCardId, requestId, status, resultCardIds = [], error = '') => {
if (!parentCardId) {
return
}
const { card } = findCardById(parentCardId)
if (!card) {
return
}
const target = card.childRequests.find((item) => item.requestId === requestId)
if (!target) {
return
}
target.status = status
target.error = error
target.resultCardIds = resultCardIds
}
// Ejecuta una busqueda enviando la query y el context al backend a través del WebSocket
const dispatchSearch = ({ query, context, parentCardId = '', sessionId = '' }) => {
// Genera un ID único para la solicitud de búsqueda
const requestId = uid('req')
// Obtiene la función sendCommand del composable useWsMonitor para enviar comandos al backend
const { sendCommand } = useWsMonitor()
// Busca la sesión correspondiente al sessionId proporcionado, si no se encuentra, intenta buscarla a través del parentCardId. Si aún no se encuentra, crea una nueva sesión con la query y el context proporcionados.
let targetSession = sessionId ? findSessionById(sessionId) : null
if (!targetSession && parentCardId) {
const found = findCardById(parentCardId)
targetSession = found.session
}
// Si no se encuentra ninguna sesión existente, crea una nueva sesión con la query y el context proporcionados.
if (!targetSession) {
targetSession = createSession(requestId, query, context)
}
// Registra la solicitud de búsqueda pendiente en el objeto pendingRequests, asociando el requestId con la sesión, el parentCardId, la query y el context.
pendingRequests.value[requestId] = {
requestId,
sessionId: targetSession.id,
parentCardId,
query,
context,
status: 'pending',
error: '',
createdAt: new Date().toISOString(),
}
// Si se proporciona un parentCardId, agrega un marcador de posición para la solicitud de búsqueda secundaria en el card correspondiente.
pushChildRequestPlaceholder(parentCardId, requestId, query, context)
// Envía el comando 'busqueda' al backend a través del WebSocket, incluyendo la query y el context en el payload.
sendCommand('busqueda', {
busqueda: {
query,
context,
},
})
return { requestId, sessionId: targetSession.id }
}
const normalizeMessagePayload = (payload) => {
if (!payload) {
return null
}
if (typeof payload === 'string') {
try {
return JSON.parse(payload)
} catch {
return null
}
}
if (typeof payload === 'object') {
return payload
}
return null
}
const extractRequestId = (payload) => {
return (
payload?.requestId
|| payload?.context?.requestId
|| payload?.meta?.requestId
|| payload?.source?.requestId
|| ''
)
}
const pickLatestRequest = () => {
const entries = Object.values(pendingRequests.value)
.filter((item) => item.status !== 'error')
.sort((a, b) => new Date(b.createdAt).getTime() - new Date(a.createdAt).getTime())
return entries[0] || null
}
// Agrega nuevas cards a un grupo de resultados de búsqueda dentro de una sesión, asegurando que no se dupliquen y devolviendo los IDs de las cards agregadas.
const addCardsToGroup = ({ session, group, cards, parentCardId, requestId }) => {
const appendedIds = []
for (const card of cards) {
const stableSeed = `${group.sourceKey}:${card.stableKey}`
const stableId = `card_${hashString(stableSeed)}`
if (session.cardsById[stableId]) {
appendedIds.push(stableId)
continue
}
const normalized = {
id: stableId,
stableKey: card.stableKey,
sourceKey: group.sourceKey,
sourceLabel: group.sourceLabel,
title: card.title,
fields: Array.isArray(card.fields) ? card.fields : [],
raw: card.raw,
actions: Array.isArray(card.actions) ? card.actions : [],
parentCardId: parentCardId || '',
createdByRequestId: requestId,
childRequests: [],
}
session.cardsById[stableId] = normalized
group.cards.push(stableId)
appendedIds.push(stableId)
}
return appendedIds
}
const handleIncomingSearchPayload = (payload) => {
const requestId = extractRequestId(payload)
const pending = requestId ? pendingRequests.value[requestId] : pickLatestRequest()
if (!pending) {
return
}
const session = findSessionById(pending.sessionId)
if (!session) {
return
}
if (payload?.ok === false) {
const normalized = adaptSearchPayload(payload)
const group = ensureSourceGroup(session, normalized.sourceKey, normalized.sourceLabel)
group.status = 'error'
group.error = String(
payload?.error?.message
|| payload?.error?.detail
|| payload?.message
|| 'Error en respuesta'
)
// NOT_FOUND por fuente es esperado en búsquedas multi-base: no cancelar toda la solicitud.
const errorCode = String(payload?.error?.code || '').toUpperCase()
if (errorCode !== 'NOT_FOUND') {
pending.error = group.error
setChildRequestResult(pending.parentCardId, pending.requestId, 'error', [], pending.error)
}
return
}
const normalized = adaptSearchPayload(payload)
const group = ensureSourceGroup(session, normalized.sourceKey, normalized.sourceLabel)
const cardIds = addCardsToGroup({
session,
group,
cards: normalized.cards,
parentCardId: pending.parentCardId,
requestId: pending.requestId,
})
pending.status = 'done'
setChildRequestResult(pending.parentCardId, pending.requestId, 'done', cardIds)
}
// Inicializa el puente WebSocket para recibir los resultados de búsqueda en tiempo real
const initWsBridge = () => {
if (bridgeInitialized) {
return
}
bridgeInitialized = true
const { logs } = useWsMonitor()
// Observa los cambios en los logs del WebSocket para procesar los mensajes entrantes
watch(
logs,
(items) => {
for (const item of items) {
if (item.type !== 'incoming') {
continue
}
if (processedLogIds.has(item.id)) {
continue
}
processedLogIds.add(item.id)
const payload = normalizeMessagePayload(item.payload)
if (!payload) {
continue
}
const isSearchPayload = Object.prototype.hasOwnProperty.call(payload, 'ok')
&& Object.prototype.hasOwnProperty.call(payload, 'source')
&& Object.prototype.hasOwnProperty.call(payload, 'data')
if (isSearchPayload) {
handleIncomingSearchPayload(payload)
}
}
},
{ deep: true }
)
}
const toggleCardSelection = (cardId) => {
if (selectedSet.value.has(cardId)) {
selectedCardIds.value = selectedCardIds.value.filter((id) => id !== cardId)
} else {
selectedCardIds.value = [...selectedCardIds.value, cardId]
}
}
const isCardSelected = (cardId) => selectedSet.value.has(cardId)
const runCardAction = ({ sessionId, parentCardId, action }) => {
if (!action?.query || !action?.context) {
return
}
dispatchSearch({
query: action.query,
context: action.context,
parentCardId,
sessionId,
})
}
const selectedCards = computed(() => {
const session = activeSession.value
if (!session) {
return []
}
return selectedCardIds.value.map((id) => session.cardsById[id]).filter(Boolean)
})
const exportSelectedAsJson = () => {
const data = {
generatedAt: new Date().toISOString(),
sessionId: activeSessionId.value,
cards: selectedCards.value,
}
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
link.download = `reporte_resultados_${Date.now()}.json`
link.click()
URL.revokeObjectURL(url)
}
const escapeHtml = (value) => String(value ?? '')
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;')
const exportSelectedAsPdf = () => {
if (!selectedCards.value.length) {
return
}
const rows = selectedCards.value
.map((card, index) => {
const fields = card.fields
.map((field) => `<li><strong>${escapeHtml(field.label)}:</strong> ${escapeHtml(field.value)}</li>`)
.join('')
return `<section class="card"><h2>${index + 1}. ${escapeHtml(card.title)}</h2><p class="meta">Base: ${escapeHtml(card.sourceLabel || card.sourceKey)}</p><ul>${fields}</ul></section>`
})
.join('')
const html = `<!DOCTYPE html><html lang="es"><head><meta charset="UTF-8"/><title>Reporte</title><style>body{font-family:Segoe UI,Arial,sans-serif;margin:24px;color:#0f172a}.header{border-bottom:2px solid #1e293b;margin-bottom:16px;padding-bottom:10px}.card{border:1px solid #cbd5e1;padding:12px;margin-bottom:10px}.meta{color:#475569;font-size:12px;margin:0 0 8px}ul{margin:0;padding-left:16px}li{margin-bottom:4px}</style></head><body><header class="header"><h1>Reporte de resultados</h1><p>Generado: ${escapeHtml(new Date().toLocaleString())}</p><p>Total de cards: ${selectedCards.value.length}</p></header>${rows}</body></html>`
const win = window.open('', '_blank', 'noopener,noreferrer,width=1100,height=800')
if (!win) {
return
}
win.document.open()
win.document.write(html)
win.document.close()
win.focus()
win.print()
}
export const useSearchResultsStore = () => ({
sessions,
activeSession,
activeSessionId,
pendingRequests,
selectedCardIds,
selectedCards,
initWsBridge,
dispatchSearch,
toggleCardSelection,
isCardSelected,
runCardAction,
exportSelectedAsJson,
exportSelectedAsPdf,
})