All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m2s
293 lines
9.5 KiB
Vue
293 lines
9.5 KiB
Vue
<template>
|
|
<div class="space-y-6">
|
|
<!-- Header -->
|
|
<div class="flex items-center justify-between">
|
|
<div>
|
|
<h1 class="text-2xl font-bold text-[var(--wa-text)]">Mensajes</h1>
|
|
<p class="text-[var(--wa-text-muted)]">Vista de conversaciones de todas las instancias</p>
|
|
</div>
|
|
<div class="flex items-center gap-2">
|
|
<!-- SSE Status -->
|
|
<span
|
|
class="flex items-center gap-1 text-xs px-2 py-1 rounded"
|
|
:class="isConnected ? 'bg-green-900/50 text-green-400' : 'bg-red-900/50 text-red-400'"
|
|
>
|
|
<span class="w-2 h-2 rounded-full" :class="isConnected ? 'bg-green-400' : 'bg-red-400'" />
|
|
{{ isConnected ? 'Realtime' : 'Offline' }}
|
|
</span>
|
|
<!-- Debug toggle -->
|
|
<button
|
|
@click="showDebugPanel = !showDebugPanel"
|
|
class="text-xs px-2 py-1 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
|
>
|
|
<UIcon name="i-lucide-bug" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Debug Panel -->
|
|
<div
|
|
v-if="showDebugPanel"
|
|
class="p-4 rounded bg-gray-900 border border-gray-700 text-xs font-mono"
|
|
>
|
|
<div class="flex items-center justify-between mb-2">
|
|
<span class="text-gray-400">Last SSE Event:</span>
|
|
<span class="text-gray-500">{{ lastEvent?.timestamp?.toLocaleTimeString() || 'N/A' }}</span>
|
|
</div>
|
|
<pre v-if="lastEvent" class="text-green-400 whitespace-pre-wrap overflow-x-auto max-h-40">{{ JSON.stringify(lastEvent, null, 2) }}</pre>
|
|
<p v-else class="text-gray-500">No events received yet</p>
|
|
</div>
|
|
|
|
<!-- Instance Selector -->
|
|
<div class="flex items-center gap-4">
|
|
<USelectMenu
|
|
v-model="selectedInstance"
|
|
:items="instanceOptions"
|
|
placeholder="Seleccionar instancia"
|
|
class="w-64"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Chat Interface -->
|
|
<div class="grid grid-cols-12 gap-4 h-[calc(100vh-300px)]">
|
|
<!-- Chat List -->
|
|
<div class="col-span-4 instance-card overflow-hidden flex flex-col">
|
|
<div class="p-4 border-b border-[var(--wa-border)]">
|
|
<div class="flex items-center gap-2">
|
|
<UInput
|
|
v-model="searchQuery"
|
|
placeholder="Buscar conversacion..."
|
|
icon="i-lucide-search"
|
|
class="flex-1"
|
|
/>
|
|
<button
|
|
@click="showChatsDebug = !showChatsDebug"
|
|
class="text-xs px-2 py-2 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
|
title="Debug chats array"
|
|
>
|
|
<UIcon name="i-lucide-bug" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<!-- Chats Debug Panel -->
|
|
<div
|
|
v-if="showChatsDebug"
|
|
class="mt-2 p-2 rounded bg-gray-900 border border-gray-700 text-xs font-mono max-h-60 overflow-auto"
|
|
>
|
|
<div class="text-gray-400 mb-1">Chats ({{ chats.length }}):</div>
|
|
<pre class="text-green-400 whitespace-pre-wrap">{{ JSON.stringify(chats, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto">
|
|
<div v-if="chats.length === 0" class="p-8 text-center">
|
|
<UIcon name="i-lucide-message-square" class="w-12 h-12 text-[var(--wa-text-muted)] mx-auto mb-3" />
|
|
<p class="text-[var(--wa-text-muted)]">No hay conversaciones</p>
|
|
</div>
|
|
|
|
<div v-else>
|
|
<MessagesChatItem
|
|
v-for="chat in filteredChats"
|
|
:key="chat.id"
|
|
:chat="chat"
|
|
:active="selectedChat?.id === chat.id"
|
|
@click="selectedChat = chat"
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Message View -->
|
|
<div class="col-span-8 instance-card overflow-hidden flex flex-col">
|
|
<div v-if="!selectedChat" class="flex-1 flex items-center justify-center">
|
|
<div class="text-center">
|
|
<UIcon name="i-lucide-message-circle" class="w-16 h-16 text-[var(--wa-text-muted)] mx-auto mb-4" />
|
|
<p class="text-[var(--wa-text-muted)]">Selecciona una conversacion</p>
|
|
</div>
|
|
</div>
|
|
|
|
<template v-else>
|
|
<!-- Chat Header -->
|
|
<div class="p-4 border-b border-[var(--wa-border)]">
|
|
<div class="flex items-center gap-3">
|
|
<UAvatar :alt="selectedChat.name" size="md" />
|
|
<div class="flex-1">
|
|
<p class="font-medium text-[var(--wa-text)]">{{ selectedChat.name }}</p>
|
|
<p class="text-sm text-[var(--wa-text-muted)]">{{ selectedChat.jid }}</p>
|
|
</div>
|
|
<button
|
|
@click="showSelectedChatDebug = !showSelectedChatDebug"
|
|
class="text-xs px-2 py-2 rounded bg-gray-700 hover:bg-gray-600 text-gray-300"
|
|
title="Debug selected chat"
|
|
>
|
|
<UIcon name="i-lucide-bug" class="w-4 h-4" />
|
|
</button>
|
|
</div>
|
|
<!-- Selected Chat Debug Panel -->
|
|
<div
|
|
v-if="showSelectedChatDebug"
|
|
class="mt-2 p-2 rounded bg-gray-900 border border-gray-700 text-xs font-mono max-h-40 overflow-auto"
|
|
>
|
|
<div class="text-gray-400 mb-1">Selected Chat:</div>
|
|
<pre class="text-green-400 whitespace-pre-wrap">{{ JSON.stringify(selectedChat, null, 2) }}</pre>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Messages -->
|
|
<div class="flex-1 overflow-y-auto p-4 space-y-2">
|
|
<MessagesMessageBubble
|
|
v-for="message in messages"
|
|
:key="message.id"
|
|
:message="message"
|
|
/>
|
|
</div>
|
|
|
|
<!-- Input -->
|
|
<div class="p-4 border-t border-[var(--wa-border)]">
|
|
<MessagesMessageInput @send="handleSendMessage" />
|
|
</div>
|
|
</template>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
definePageMeta({
|
|
layout: 'dashboard',
|
|
title: 'Mensajes',
|
|
icon: 'i-lucide-message-square'
|
|
})
|
|
|
|
const { instances, fetchInstances } = useInstances()
|
|
const { isConnected, lastEvent, on } = useRealtime()
|
|
|
|
const selectedInstance = ref<{ label: string; value: string } | null>(null)
|
|
const searchQuery = ref('')
|
|
const selectedChat = ref<any>(null)
|
|
const chats = ref<any[]>([])
|
|
const messages = ref<any[]>([])
|
|
const loadingChats = ref(false)
|
|
const loadingMessages = ref(false)
|
|
const showDebugPanel = ref(false)
|
|
const showChatsDebug = ref(false)
|
|
const showSelectedChatDebug = ref(false)
|
|
|
|
// Instance options for selector
|
|
const instanceOptions = computed(() =>
|
|
instances.value
|
|
.filter(i => i.status === 'connected')
|
|
.map(i => ({ label: i.name, value: i.id }))
|
|
)
|
|
|
|
// Auto-select first connected instance
|
|
watch(instanceOptions, (opts) => {
|
|
if (opts.length > 0 && !selectedInstance.value) {
|
|
selectedInstance.value = opts[0]
|
|
}
|
|
}, { immediate: true })
|
|
|
|
// Load chats when instance changes
|
|
watch(selectedInstance, async (instance) => {
|
|
if (!instance?.value) {
|
|
chats.value = []
|
|
return
|
|
}
|
|
|
|
loadingChats.value = true
|
|
try {
|
|
chats.value = await $fetch(`/api/messages/${instance.value}/chats`)
|
|
} catch (e) {
|
|
console.error('Error loading chats:', e)
|
|
chats.value = []
|
|
} finally {
|
|
loadingChats.value = false
|
|
}
|
|
})
|
|
|
|
// Load messages when chat changes
|
|
watch(selectedChat, async (chat) => {
|
|
if (!chat || !selectedInstance.value?.value) {
|
|
messages.value = []
|
|
return
|
|
}
|
|
|
|
loadingMessages.value = true
|
|
try {
|
|
messages.value = await $fetch(`/api/messages/${selectedInstance.value.value}/${chat.id}`)
|
|
} catch (e) {
|
|
console.error('Error loading messages:', e)
|
|
messages.value = []
|
|
} finally {
|
|
loadingMessages.value = false
|
|
}
|
|
})
|
|
|
|
const filteredChats = computed(() => {
|
|
if (!searchQuery.value) return chats.value
|
|
return chats.value.filter(chat =>
|
|
chat.name?.toLowerCase().includes(searchQuery.value.toLowerCase())
|
|
)
|
|
})
|
|
|
|
const handleSendMessage = async (content: string) => {
|
|
if (!selectedInstance.value?.value || !selectedChat.value) return
|
|
|
|
try {
|
|
await $fetch(`/api/messages/${selectedInstance.value.value}/${selectedChat.value.id}/send`, {
|
|
method: 'POST',
|
|
body: { content }
|
|
})
|
|
|
|
// Reload messages
|
|
messages.value = await $fetch(`/api/messages/${selectedInstance.value.value}/${selectedChat.value.id}`)
|
|
} catch (e) {
|
|
console.error('Error sending message:', e)
|
|
}
|
|
}
|
|
|
|
// Reload chats for current instance
|
|
const reloadChats = async () => {
|
|
if (!selectedInstance.value?.value) return
|
|
try {
|
|
chats.value = await $fetch(`/api/messages/${selectedInstance.value.value}/chats`)
|
|
} catch (e) {
|
|
console.error('Error reloading chats:', e)
|
|
}
|
|
}
|
|
|
|
// Reload messages for current chat
|
|
const reloadMessages = async () => {
|
|
if (!selectedInstance.value?.value || !selectedChat.value) return
|
|
try {
|
|
messages.value = await $fetch(`/api/messages/${selectedInstance.value.value}/${selectedChat.value.id}`)
|
|
} catch (e) {
|
|
console.error('Error reloading messages:', e)
|
|
}
|
|
}
|
|
|
|
// Load instances on mount
|
|
onMounted(() => {
|
|
fetchInstances()
|
|
|
|
// Listen for real-time message events
|
|
on('message.received', (data) => {
|
|
console.log('[Messages] New message received:', data)
|
|
|
|
// If it's for the current instance, reload chats
|
|
if (data.instanceId === selectedInstance.value?.value) {
|
|
reloadChats()
|
|
|
|
// If it's for the current chat, reload messages
|
|
if (selectedChat.value?.jid === data.message?.key?.remoteJid) {
|
|
reloadMessages()
|
|
}
|
|
}
|
|
})
|
|
|
|
on('message.sent', (data) => {
|
|
console.log('[Messages] Message sent:', data)
|
|
// Already handled by the send function
|
|
})
|
|
})
|
|
</script>
|