310 lines
8.0 KiB
Vue
310 lines
8.0 KiB
Vue
<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>
|