Primer commit
This commit is contained in:
422
src/composables/useSearchResultsStore.js
Normal file
422
src/composables/useSearchResultsStore.js
Normal 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''')
|
||||
|
||||
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,
|
||||
})
|
||||
Reference in New Issue
Block a user