sandbox connected and streaming
This commit is contained in:
@@ -5,9 +5,12 @@ import Badge from 'primevue/badge'
|
||||
import Button from 'primevue/button'
|
||||
import { wsManager } from '../composables/useWebSocket'
|
||||
import type { WebSocketMessage } from '../composables/useWebSocket'
|
||||
import { useChannelStore } from '../stores/channel'
|
||||
|
||||
register()
|
||||
|
||||
const channelStore = useChannelStore()
|
||||
|
||||
const SESSION_ID = 'default'
|
||||
const CURRENT_USER_ID = 'user-123'
|
||||
const AGENT_ID = 'agent'
|
||||
@@ -33,46 +36,110 @@ const rooms = computed(() => [{
|
||||
|
||||
// Streaming state
|
||||
let currentStreamingMessageId: string | null = null
|
||||
let lastSentMessageId: string | null = null
|
||||
let streamingBuffer = ''
|
||||
const isAgentProcessing = ref(false)
|
||||
const toolCallStatus = ref<string | null>(null)
|
||||
|
||||
// Generate message ID
|
||||
const generateMessageId = () => `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// Storage for images received during streaming
|
||||
const streamingImages = ref<any[]>([])
|
||||
|
||||
// Handle WebSocket messages
|
||||
const handleMessage = (data: WebSocketMessage) => {
|
||||
console.log('[ChatPanel] Received message:', data)
|
||||
if (data.type === 'agent_chunk') {
|
||||
|
||||
if (data.type === 'agent_tool_call') {
|
||||
toolCallStatus.value = data.label ?? data.toolName ?? null
|
||||
return
|
||||
}
|
||||
|
||||
if (data.type === 'image') {
|
||||
// Handle image message - attach to current streaming message or create standalone
|
||||
console.log('[ChatPanel] Processing image message')
|
||||
const imageFile = {
|
||||
name: `chart_${Date.now()}.png`,
|
||||
size: 0,
|
||||
type: 'png',
|
||||
url: `data:${data.mimeType};base64,${data.data}`,
|
||||
preview: `data:${data.mimeType};base64,${data.data}`
|
||||
}
|
||||
|
||||
if (currentStreamingMessageId) {
|
||||
// Attach to current streaming message
|
||||
streamingImages.value.push(imageFile)
|
||||
const msgIndex = messages.value.findIndex(m => m._id === currentStreamingMessageId)
|
||||
if (msgIndex !== -1) {
|
||||
messages.value[msgIndex] = {
|
||||
...messages.value[msgIndex],
|
||||
files: [...streamingImages.value]
|
||||
}
|
||||
messages.value = [...messages.value]
|
||||
}
|
||||
} else {
|
||||
// No active streaming message - create a standalone image message
|
||||
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
||||
messages.value = [...messages.value, {
|
||||
_id: generateMessageId(),
|
||||
content: data.caption || '',
|
||||
senderId: AGENT_ID,
|
||||
timestamp: timestamp,
|
||||
date: new Date().toLocaleDateString(),
|
||||
saved: true,
|
||||
distributed: true,
|
||||
seen: true,
|
||||
files: [imageFile]
|
||||
}]
|
||||
}
|
||||
} else if (data.type === 'agent_chunk') {
|
||||
console.log('[ChatPanel] Processing agent_chunk, content:', data.content, 'done:', data.done)
|
||||
const timestamp = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
||||
|
||||
if (!currentStreamingMessageId) {
|
||||
console.log('[ChatPanel] Starting new streaming message')
|
||||
// Start new streaming message
|
||||
// Set up streaming state and mark user message as seen
|
||||
isAgentProcessing.value = true
|
||||
currentStreamingMessageId = generateMessageId()
|
||||
streamingBuffer = data.content
|
||||
streamingImages.value = []
|
||||
toolCallStatus.value = null
|
||||
|
||||
messages.value = [...messages.value, {
|
||||
_id: currentStreamingMessageId,
|
||||
content: streamingBuffer,
|
||||
senderId: AGENT_ID,
|
||||
timestamp: timestamp,
|
||||
date: new Date().toLocaleDateString(),
|
||||
saved: false,
|
||||
distributed: false,
|
||||
seen: false,
|
||||
files: []
|
||||
}]
|
||||
// Mark the last sent user message as seen (double-checkmark)
|
||||
if (lastSentMessageId) {
|
||||
const idx = messages.value.findIndex(m => m._id === lastSentMessageId)
|
||||
if (idx !== -1) {
|
||||
messages.value[idx] = { ...messages.value[idx], seen: true }
|
||||
messages.value = [...messages.value]
|
||||
}
|
||||
lastSentMessageId = null
|
||||
}
|
||||
|
||||
// Only add the agent bubble once there is actual content to show
|
||||
if (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,
|
||||
files: []
|
||||
}]
|
||||
}
|
||||
} else {
|
||||
// Update existing streaming message
|
||||
// Update (or lazily create) the streaming message
|
||||
streamingBuffer += data.content
|
||||
const msgIndex = messages.value.findIndex(m => m._id === currentStreamingMessageId)
|
||||
|
||||
if (msgIndex !== -1) {
|
||||
const updatedMessage: any = {
|
||||
...messages.value[msgIndex],
|
||||
content: streamingBuffer
|
||||
content: streamingBuffer,
|
||||
files: [...streamingImages.value] // Include accumulated images
|
||||
}
|
||||
|
||||
// Add plot images if present in metadata
|
||||
@@ -84,11 +151,25 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
url: `${BACKEND_URL}${url}`,
|
||||
preview: `${BACKEND_URL}${url}`
|
||||
}))
|
||||
updatedMessage.files = plotFiles
|
||||
updatedMessage.files = [...updatedMessage.files, ...plotFiles]
|
||||
}
|
||||
|
||||
messages.value[msgIndex] = updatedMessage
|
||||
messages.value = [...messages.value]
|
||||
} else if (streamingBuffer) {
|
||||
// First chunk with content after an empty ack — create the bubble now
|
||||
const timestamp2 = new Date().toTimeString().split(' ')[0].slice(0, 5)
|
||||
messages.value = [...messages.value, {
|
||||
_id: currentStreamingMessageId!,
|
||||
content: streamingBuffer,
|
||||
senderId: AGENT_ID,
|
||||
timestamp: timestamp2,
|
||||
date: new Date().toLocaleDateString(),
|
||||
saved: false,
|
||||
distributed: false,
|
||||
seen: false,
|
||||
files: [...streamingImages.value]
|
||||
}]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +181,8 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
...messages.value[msgIndex],
|
||||
saved: true,
|
||||
distributed: true,
|
||||
seen: true
|
||||
seen: true,
|
||||
files: [...streamingImages.value] // Include all accumulated images
|
||||
}
|
||||
|
||||
// Ensure plot images are included in final message
|
||||
@@ -112,7 +194,7 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
url: `${BACKEND_URL}${url}`,
|
||||
preview: `${BACKEND_URL}${url}`
|
||||
}))
|
||||
finalMessage.files = plotFiles
|
||||
finalMessage.files = [...finalMessage.files, ...plotFiles]
|
||||
}
|
||||
|
||||
messages.value[msgIndex] = finalMessage
|
||||
@@ -121,7 +203,9 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
|
||||
currentStreamingMessageId = null
|
||||
streamingBuffer = ''
|
||||
streamingImages.value = []
|
||||
isAgentProcessing.value = false
|
||||
toolCallStatus.value = null
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -137,6 +221,8 @@ const stopAgent = () => {
|
||||
}
|
||||
wsManager.send(wsMessage)
|
||||
isAgentProcessing.value = false
|
||||
toolCallStatus.value = null
|
||||
lastSentMessageId = null
|
||||
}
|
||||
|
||||
// Send message handler
|
||||
@@ -216,14 +302,18 @@ const sendMessage = async (event: any) => {
|
||||
}
|
||||
wsManager.send(wsMessage)
|
||||
|
||||
// Mark as distributed
|
||||
// Track this message so the agent_chunk handler can mark it seen
|
||||
lastSentMessageId = messageId
|
||||
// Show typing indicator immediately (before first chunk arrives)
|
||||
isAgentProcessing.value = true
|
||||
|
||||
// Mark as distributed (single checkmark) after confirming WS send
|
||||
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]
|
||||
}
|
||||
@@ -243,44 +333,42 @@ const openFile = ({ file }: any) => {
|
||||
// Theme configuration for dark mode
|
||||
const chatTheme = 'dark'
|
||||
|
||||
// Styles to match PrimeVue theme
|
||||
// Styles to match TradingView dark theme
|
||||
const chatStyles = computed(() => JSON.stringify({
|
||||
general: {
|
||||
color: '#cdd6e8',
|
||||
colorSpinner: '#00d4aa',
|
||||
borderStyle: '1px solid #263452'
|
||||
color: '#d1d4dc',
|
||||
colorSpinner: '#2962ff',
|
||||
borderStyle: '1px solid #2a2e39'
|
||||
},
|
||||
container: {
|
||||
background: '#0a0e1a'
|
||||
background: '#131722'
|
||||
},
|
||||
header: {
|
||||
background: '#0f1629',
|
||||
colorRoomName: '#cdd6e8',
|
||||
colorRoomInfo: '#8892a4'
|
||||
background: '#1e222d',
|
||||
colorRoomName: '#d1d4dc',
|
||||
colorRoomInfo: '#787b86'
|
||||
},
|
||||
footer: {
|
||||
background: '#0f1629',
|
||||
borderStyleInput: '1px solid #263452',
|
||||
backgroundInput: '#161e35',
|
||||
colorInput: '#cdd6e8',
|
||||
colorPlaceholder: '#8892a4',
|
||||
colorIcons: '#8892a4'
|
||||
background: '#1e222d',
|
||||
borderStyleInput: '1px solid #2a2e39',
|
||||
backgroundInput: '#1e222d',
|
||||
colorInput: '#d1d4dc',
|
||||
colorPlaceholder: '#787b86',
|
||||
colorIcons: '#787b86'
|
||||
},
|
||||
content: {
|
||||
background: '#0a0e1a'
|
||||
background: '#131722'
|
||||
},
|
||||
message: {
|
||||
background: '#161e35',
|
||||
backgroundMe: '#1e2d4f',
|
||||
color: '#cdd6e8',
|
||||
colorMe: '#cdd6e8'
|
||||
background: '#1e222d',
|
||||
backgroundMe: '#2962ff',
|
||||
color: '#d1d4dc',
|
||||
colorMe: '#ffffff'
|
||||
}
|
||||
}))
|
||||
|
||||
onMounted(() => {
|
||||
wsManager.addHandler(handleMessage)
|
||||
// Mark messages as loaded after initialization
|
||||
messagesLoaded.value = true
|
||||
|
||||
// Focus on the chat input when component mounts
|
||||
setTimeout(() => {
|
||||
@@ -308,7 +396,14 @@ onUnmounted(() => {
|
||||
</div>
|
||||
-->
|
||||
|
||||
<!-- Workspace loading overlay -->
|
||||
<div v-if="!channelStore.isReady" class="workspace-loading">
|
||||
<i class="pi pi-spin pi-spinner workspace-loading-spinner" />
|
||||
<span class="workspace-loading-message">{{ channelStore.statusMessage || 'Connecting...' }}</span>
|
||||
</div>
|
||||
|
||||
<vue-advanced-chat
|
||||
v-else
|
||||
height="100vh"
|
||||
:current-user-id="CURRENT_USER_ID"
|
||||
:rooms="JSON.stringify(rooms)"
|
||||
@@ -324,6 +419,7 @@ onUnmounted(() => {
|
||||
:show-files="false"
|
||||
:show-emojis="true"
|
||||
:show-reaction-emojis="false"
|
||||
:message-actions="JSON.stringify([])"
|
||||
:accepted-files="'image/*,video/*,application/pdf'"
|
||||
:message-images="true"
|
||||
@send-message="sendMessage"
|
||||
@@ -333,6 +429,7 @@ onUnmounted(() => {
|
||||
|
||||
<!-- Stop button overlay -->
|
||||
<div v-if="isAgentProcessing" class="stop-button-container">
|
||||
<div v-if="toolCallStatus" class="tool-call-status">{{ toolCallStatus }}</div>
|
||||
<Button
|
||||
icon="pi pi-stop-circle"
|
||||
label="Stop"
|
||||
@@ -354,6 +451,26 @@ onUnmounted(() => {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.workspace-loading {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 1rem;
|
||||
background: #131722;
|
||||
color: #787b86;
|
||||
}
|
||||
|
||||
.workspace-loading-spinner {
|
||||
font-size: 2rem;
|
||||
color: #787b86;
|
||||
}
|
||||
|
||||
.workspace-loading-message {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.chat-container :deep(.vac-container) {
|
||||
height: 100% !important;
|
||||
}
|
||||
@@ -362,6 +479,12 @@ onUnmounted(() => {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
/* Override the default 50% desktop width on the inner message box */
|
||||
.chat-container :deep(.vac-message-wrapper .vac-message-box) {
|
||||
flex: 0 0 80% !important;
|
||||
max-width: 80% !important;
|
||||
}
|
||||
|
||||
.chat-header-custom {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
@@ -385,6 +508,17 @@ onUnmounted(() => {
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.tool-call-status {
|
||||
background: rgba(30, 34, 45, 0.92);
|
||||
color: #787b86;
|
||||
font-size: 0.75rem;
|
||||
padding: 4px 10px;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 6px;
|
||||
text-align: center;
|
||||
border: 1px solid #2a2e39;
|
||||
}
|
||||
|
||||
.stop-button {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
animation: pulse 2s infinite;
|
||||
|
||||
Reference in New Issue
Block a user