This commit is contained in:
2026-03-02 01:36:14 -04:00
parent d907c5765e
commit 311df9aeda
21 changed files with 2772 additions and 33 deletions

View File

@@ -0,0 +1,309 @@
<script setup lang="ts">
import { ref, onMounted, onUnmounted, computed } from 'vue'
import { register } from 'vue-advanced-chat'
import Badge from 'primevue/badge'
import { wsManager } from '../composables/useWebSocket'
import type { WebSocketMessage } from '../composables/useWebSocket'
register()
const SESSION_ID = 'default'
const CURRENT_USER_ID = 'user-123'
const AGENT_ID = 'agent'
const BACKEND_URL = 'http://localhost:8080'
// Chat state
const messages = ref<any[]>([])
const messagesLoaded = ref(false)
const isConnected = wsManager.isConnected
// Reactive rooms that update based on WebSocket connection
const rooms = computed(() => [{
roomId: SESSION_ID,
roomName: 'AI Agent',
avatar: null,
users: [
{ _id: CURRENT_USER_ID, username: 'You' },
{ _id: AGENT_ID, username: 'AI Agent', status: { state: isConnected.value ? 'online' : 'offline' } }
],
unreadCount: 0
}])
// Streaming state
let currentStreamingMessageId: string | null = null
let streamingBuffer = ''
// Generate message ID
const generateMessageId = () => `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
// Handle WebSocket messages
const handleMessage = (data: WebSocketMessage) => {
if (data.type === 'agent_chunk') {
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
if (!currentStreamingMessageId) {
// Start new streaming message
currentStreamingMessageId = generateMessageId()
streamingBuffer = data.content
messages.value = [...messages.value, {
_id: currentStreamingMessageId,
content: streamingBuffer,
senderId: AGENT_ID,
timestamp: timestamp,
date: new Date().toLocaleDateString(),
saved: false,
distributed: false,
seen: false
}]
} else {
// Update existing streaming message
streamingBuffer += data.content
const msgIndex = messages.value.findIndex(m => m._id === currentStreamingMessageId)
if (msgIndex !== -1) {
messages.value[msgIndex] = {
...messages.value[msgIndex],
content: streamingBuffer
}
messages.value = [...messages.value]
}
}
if (data.done) {
// Mark message as complete
const msgIndex = messages.value.findIndex(m => m._id === currentStreamingMessageId)
if (msgIndex !== -1) {
messages.value[msgIndex] = {
...messages.value[msgIndex],
saved: true,
distributed: true,
seen: true
}
messages.value = [...messages.value]
}
currentStreamingMessageId = null
streamingBuffer = ''
}
}
}
// Send message handler
const sendMessage = async (event: any) => {
// Extract data from CustomEvent.detail[0]
const data = event.detail?.[0] || event
const content = data.content || ''
const files = data.files
const roomId = data.roomId
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
const messageId = generateMessageId()
// Process files
let attachments: string[] = []
let messageFiles: any[] = []
if (files && files.length > 0) {
// Upload files to backend
for (const file of files) {
try {
const formData = new FormData()
// Convert blob URL to actual file if needed
const response = await fetch(file.url)
const blob = await response.blob()
const actualFile = new File([blob], file.name, { type: file.type })
formData.append('file', actualFile)
const uploadResponse = await fetch(`${BACKEND_URL}/api/upload`, {
method: 'POST',
body: formData
})
if (!uploadResponse.ok) {
throw new Error('Upload failed')
}
const uploadData = await uploadResponse.json()
const fileUrl = `${BACKEND_URL}${uploadData.url}`
attachments.push(fileUrl)
messageFiles.push({
name: file.name,
size: uploadData.size,
type: file.extension || file.type,
url: fileUrl,
preview: file.url // Keep the blob URL for preview
})
} catch (error) {
console.error('File upload error:', error)
}
}
}
// Add user message to UI
const userMessage = {
_id: messageId,
content: content,
senderId: CURRENT_USER_ID,
timestamp: timestamp,
date: new Date().toLocaleDateString(),
saved: true,
distributed: false,
seen: false,
files: messageFiles
}
messages.value = [...messages.value, userMessage]
// Send to backend via WebSocket
const wsMessage = {
type: 'agent_user_message',
session_id: roomId || SESSION_ID,
content: content,
attachments: attachments
}
wsManager.send(wsMessage)
// Mark as distributed
setTimeout(() => {
const msgIndex = messages.value.findIndex(m => m._id === messageId)
if (msgIndex !== -1) {
messages.value[msgIndex] = {
...messages.value[msgIndex],
distributed: true,
seen: true
}
messages.value = [...messages.value]
}
}, 100)
}
// Fetch messages handler (for pagination, not needed for simple chat)
const fetchMessages = ({ room, options }: any) => {
messagesLoaded.value = true
}
// Open file handler
const openFile = ({ file }: any) => {
window.open(file.url, '_blank')
}
// Theme configuration for dark mode
const chatTheme = 'light'
// Styles to match PrimeVue theme
const chatStyles = computed(() => JSON.stringify({
general: {
color: 'var(--p-surface-900)',
colorSpinner: 'var(--p-primary-color)',
borderStyle: '1px solid var(--p-surface-200)'
},
container: {
background: 'var(--p-surface-0)'
},
header: {
background: 'var(--p-surface-50)',
colorRoomName: 'var(--p-surface-900)',
colorRoomInfo: 'var(--p-surface-700)'
},
footer: {
background: 'var(--p-surface-50)',
borderStyleInput: '1px solid var(--p-surface-300)',
backgroundInput: 'var(--p-surface-200)',
colorInput: 'var(--p-surface-900)',
colorPlaceholder: 'var(--p-surface-400)',
colorIcons: 'var(--p-surface-400)'
},
content: {
background: 'var(--p-surface-0)'
},
message: {
background: 'var(--p-surface-100)',
backgroundMe: 'var(--p-primary-color)',
color: 'var(--p-surface-900)',
colorMe: 'var(--p-primary-contrast-color)'
}
}))
onMounted(() => {
wsManager.addHandler(handleMessage)
// Mark messages as loaded after initialization
messagesLoaded.value = true
})
onUnmounted(() => {
wsManager.removeHandler(handleMessage)
})
</script>
<template>
<div class="chat-container">
<!--
<div class="chat-header-custom">
<span class="chat-title">AI Agent Chat</span>
<Badge
:value="isConnected ? 'Connected' : 'Disconnected'"
:severity="isConnected ? 'success' : 'danger'"
/>
</div>
-->
<vue-advanced-chat
height="calc(100vh - 60px)"
:current-user-id="CURRENT_USER_ID"
:rooms="JSON.stringify(rooms)"
:messages="JSON.stringify(messages)"
:messages-loaded="messagesLoaded"
:room-id="SESSION_ID"
:theme="chatTheme"
:styles="chatStyles"
:single-room="true"
:rooms-list-opened="false"
:show-add-room="false"
:show-audio="false"
:show-files="true"
:show-emojis="true"
:show-reaction-emojis="false"
:accepted-files="'image/*,video/*,application/pdf'"
@send-message="sendMessage"
@fetch-messages="fetchMessages"
@open-file="openFile"
/>
</div>
</template>
<style scoped>
.chat-container {
height: 100% !important;
display: flex;
flex-direction: column;
background: var(--p-surface-0);
overflow: hidden;
}
.chat-container :deep(.vac-container) {
height: 100% !important;
}
.chat-container :deep(.vac-text-message) {
white-space: pre-wrap;
}
.chat-header-custom {
display: flex;
justify-content: space-between;
align-items: center;
padding: 1rem 1.5rem;
background: var(--p-surface-50);
border-bottom: 1px solid var(--p-surface-200);
flex-shrink: 0;
}
.chat-title {
font-size: 1.25rem;
font-weight: 600;
color: var(--p-surface-900);
}
</style>