indicators and plots
This commit is contained in:
@@ -13,6 +13,7 @@ import { wsManager } from './composables/useWebSocket'
|
||||
const isAuthenticated = ref(false)
|
||||
const needsConfirmation = ref(false)
|
||||
const authError = ref<string>()
|
||||
const isDragging = ref(false)
|
||||
let stateSyncCleanup: (() => void) | null = null
|
||||
|
||||
// Check if we need password confirmation on first load
|
||||
@@ -58,6 +59,21 @@ const handleAuthenticate = async (
|
||||
}
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Listen for splitter drag events
|
||||
document.addEventListener('mousedown', (e) => {
|
||||
// Check if the mousedown is on a splitter gutter
|
||||
const target = e.target as HTMLElement
|
||||
if (target.closest('.p-splitter-gutter')) {
|
||||
isDragging.value = true
|
||||
}
|
||||
})
|
||||
|
||||
document.addEventListener('mouseup', () => {
|
||||
isDragging.value = false
|
||||
})
|
||||
})
|
||||
|
||||
onBeforeUnmount(() => {
|
||||
if (stateSyncCleanup) {
|
||||
stateSyncCleanup()
|
||||
@@ -82,6 +98,8 @@ onBeforeUnmount(() => {
|
||||
<ChatPanel />
|
||||
</SplitterPanel>
|
||||
</Splitter>
|
||||
<!-- Transparent overlay to prevent iframe from capturing mouse events during drag -->
|
||||
<div v-if="isDragging" class="drag-overlay"></div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -90,19 +108,24 @@ onBeforeUnmount(() => {
|
||||
width: 100vw !important;
|
||||
height: 100vh !important;
|
||||
overflow: hidden;
|
||||
background: var(--p-surface-0);
|
||||
background: var(--p-surface-0) !important;
|
||||
}
|
||||
|
||||
.app-container.dark {
|
||||
background: var(--p-surface-0) !important;
|
||||
}
|
||||
|
||||
.main-splitter {
|
||||
height: 100vh !important;
|
||||
background: var(--p-surface-0) !important;
|
||||
}
|
||||
|
||||
.main-splitter :deep(.p-splitter-gutter) {
|
||||
background: var(--p-surface-100);
|
||||
background: var(--p-surface-0) !important;
|
||||
}
|
||||
|
||||
.main-splitter :deep(.p-splitter-gutter-handle) {
|
||||
background: var(--p-primary-color);
|
||||
background: var(--p-surface-400) !important;
|
||||
}
|
||||
|
||||
.chart-panel,
|
||||
@@ -119,4 +142,15 @@ onBeforeUnmount(() => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.drag-overlay {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
z-index: 9999;
|
||||
cursor: col-resize;
|
||||
background: transparent;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,3 +26,16 @@ html, body, #app {
|
||||
overflow: hidden;
|
||||
background-color: var(--p-surface-0) !important;
|
||||
}
|
||||
|
||||
.dark {
|
||||
background-color: var(--p-surface-0) !important;
|
||||
color: var(--p-surface-900) !important;
|
||||
}
|
||||
|
||||
/* Ensure dark background for main containers */
|
||||
.app-container,
|
||||
.main-splitter,
|
||||
.p-splitter,
|
||||
.p-splitter-panel {
|
||||
background-color: var(--p-surface-0) !important;
|
||||
}
|
||||
|
||||
@@ -191,9 +191,14 @@ onBeforeUnmount(() => {
|
||||
flex-direction: column;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
border-radius: 0 !important;
|
||||
background: var(--p-surface-0);
|
||||
}
|
||||
|
||||
.chart-card :deep(.p-card) {
|
||||
border-radius: 0 !important;
|
||||
}
|
||||
|
||||
.chart-card :deep(.p-card-body) {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { ref, onMounted, onUnmounted, computed } from 'vue'
|
||||
import { register } from 'vue-advanced-chat'
|
||||
import Badge from 'primevue/badge'
|
||||
import Button from 'primevue/button'
|
||||
import { wsManager } from '../composables/useWebSocket'
|
||||
import type { WebSocketMessage } from '../composables/useWebSocket'
|
||||
|
||||
@@ -17,7 +18,7 @@ const messages = ref<any[]>([])
|
||||
const messagesLoaded = ref(false)
|
||||
const isConnected = wsManager.isConnected
|
||||
|
||||
// Reactive rooms that update based on WebSocket connection
|
||||
// Reactive rooms that update based on WebSocket connection and agent processing state
|
||||
const rooms = computed(() => [{
|
||||
roomId: SESSION_ID,
|
||||
roomName: 'AI Agent',
|
||||
@@ -26,23 +27,29 @@ const rooms = computed(() => [{
|
||||
{ _id: CURRENT_USER_ID, username: 'You' },
|
||||
{ _id: AGENT_ID, username: 'AI Agent', status: { state: isConnected.value ? 'online' : 'offline' } }
|
||||
],
|
||||
unreadCount: 0
|
||||
unreadCount: 0,
|
||||
typingUsers: isAgentProcessing.value ? [AGENT_ID] : []
|
||||
}])
|
||||
|
||||
// Streaming state
|
||||
let currentStreamingMessageId: string | null = null
|
||||
let streamingBuffer = ''
|
||||
const isAgentProcessing = ref(false)
|
||||
|
||||
// Generate message ID
|
||||
const generateMessageId = () => `msg-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
|
||||
|
||||
// Handle WebSocket messages
|
||||
const handleMessage = (data: WebSocketMessage) => {
|
||||
console.log('[ChatPanel] Received message:', data)
|
||||
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
|
||||
isAgentProcessing.value = true
|
||||
currentStreamingMessageId = generateMessageId()
|
||||
streamingBuffer = data.content
|
||||
|
||||
@@ -54,7 +61,8 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
date: new Date().toLocaleDateString(),
|
||||
saved: false,
|
||||
distributed: false,
|
||||
seen: false
|
||||
seen: false,
|
||||
files: []
|
||||
}]
|
||||
} else {
|
||||
// Update existing streaming message
|
||||
@@ -62,10 +70,24 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
const msgIndex = messages.value.findIndex(m => m._id === currentStreamingMessageId)
|
||||
|
||||
if (msgIndex !== -1) {
|
||||
messages.value[msgIndex] = {
|
||||
const updatedMessage: any = {
|
||||
...messages.value[msgIndex],
|
||||
content: streamingBuffer
|
||||
}
|
||||
|
||||
// Add plot images if present in metadata
|
||||
if (data.metadata && data.metadata.plot_urls && Array.isArray(data.metadata.plot_urls)) {
|
||||
const plotFiles = data.metadata.plot_urls.map((url: string, idx: number) => ({
|
||||
name: `plot_${idx + 1}.png`,
|
||||
size: 0,
|
||||
type: 'png',
|
||||
url: `${BACKEND_URL}${url}`,
|
||||
preview: `${BACKEND_URL}${url}`
|
||||
}))
|
||||
updatedMessage.files = plotFiles
|
||||
}
|
||||
|
||||
messages.value[msgIndex] = updatedMessage
|
||||
messages.value = [...messages.value]
|
||||
}
|
||||
}
|
||||
@@ -74,21 +96,49 @@ const handleMessage = (data: WebSocketMessage) => {
|
||||
// Mark message as complete
|
||||
const msgIndex = messages.value.findIndex(m => m._id === currentStreamingMessageId)
|
||||
if (msgIndex !== -1) {
|
||||
messages.value[msgIndex] = {
|
||||
const finalMessage: any = {
|
||||
...messages.value[msgIndex],
|
||||
saved: true,
|
||||
distributed: true,
|
||||
seen: true
|
||||
}
|
||||
|
||||
// Ensure plot images are included in final message
|
||||
if (data.metadata && data.metadata.plot_urls && Array.isArray(data.metadata.plot_urls)) {
|
||||
const plotFiles = data.metadata.plot_urls.map((url: string, idx: number) => ({
|
||||
name: `plot_${idx + 1}.png`,
|
||||
size: 0,
|
||||
type: 'png',
|
||||
url: `${BACKEND_URL}${url}`,
|
||||
preview: `${BACKEND_URL}${url}`
|
||||
}))
|
||||
finalMessage.files = plotFiles
|
||||
}
|
||||
|
||||
messages.value[msgIndex] = finalMessage
|
||||
messages.value = [...messages.value]
|
||||
}
|
||||
|
||||
currentStreamingMessageId = null
|
||||
streamingBuffer = ''
|
||||
isAgentProcessing.value = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Stop agent processing
|
||||
const stopAgent = () => {
|
||||
// Send empty message to trigger interrupt without new agent round
|
||||
const wsMessage = {
|
||||
type: 'agent_user_message',
|
||||
session_id: SESSION_ID,
|
||||
content: '',
|
||||
attachments: []
|
||||
}
|
||||
wsManager.send(wsMessage)
|
||||
isAgentProcessing.value = false
|
||||
}
|
||||
|
||||
// Send message handler
|
||||
const sendMessage = async (event: any) => {
|
||||
// Extract data from CustomEvent.detail[0]
|
||||
@@ -191,39 +241,39 @@ const openFile = ({ file }: any) => {
|
||||
}
|
||||
|
||||
// Theme configuration for dark mode
|
||||
const chatTheme = 'light'
|
||||
const chatTheme = 'dark'
|
||||
|
||||
// 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)'
|
||||
color: '#cdd6e8',
|
||||
colorSpinner: '#00d4aa',
|
||||
borderStyle: '1px solid #263452'
|
||||
},
|
||||
container: {
|
||||
background: 'var(--p-surface-0)'
|
||||
background: '#0a0e1a'
|
||||
},
|
||||
header: {
|
||||
background: 'var(--p-surface-50)',
|
||||
colorRoomName: 'var(--p-surface-900)',
|
||||
colorRoomInfo: 'var(--p-surface-700)'
|
||||
background: '#0f1629',
|
||||
colorRoomName: '#cdd6e8',
|
||||
colorRoomInfo: '#8892a4'
|
||||
},
|
||||
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)'
|
||||
background: '#0f1629',
|
||||
borderStyleInput: '1px solid #263452',
|
||||
backgroundInput: '#161e35',
|
||||
colorInput: '#cdd6e8',
|
||||
colorPlaceholder: '#8892a4',
|
||||
colorIcons: '#8892a4'
|
||||
},
|
||||
content: {
|
||||
background: 'var(--p-surface-0)'
|
||||
background: '#0a0e1a'
|
||||
},
|
||||
message: {
|
||||
background: 'var(--p-surface-100)',
|
||||
backgroundMe: 'var(--p-primary-color)',
|
||||
color: 'var(--p-surface-900)',
|
||||
colorMe: 'var(--p-primary-contrast-color)'
|
||||
background: '#161e35',
|
||||
backgroundMe: '#00d4aa',
|
||||
color: '#cdd6e8',
|
||||
colorMe: '#0a0e1a'
|
||||
}
|
||||
}))
|
||||
|
||||
@@ -231,6 +281,14 @@ onMounted(() => {
|
||||
wsManager.addHandler(handleMessage)
|
||||
// Mark messages as loaded after initialization
|
||||
messagesLoaded.value = true
|
||||
|
||||
// Focus on the chat input when component mounts
|
||||
setTimeout(() => {
|
||||
const chatInput = document.querySelector('.vac-textarea') as HTMLTextAreaElement
|
||||
if (chatInput) {
|
||||
chatInput.focus()
|
||||
}
|
||||
}, 300)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -251,7 +309,7 @@ onUnmounted(() => {
|
||||
-->
|
||||
|
||||
<vue-advanced-chat
|
||||
height="calc(100vh - 60px)"
|
||||
height="100vh"
|
||||
:current-user-id="CURRENT_USER_ID"
|
||||
:rooms="JSON.stringify(rooms)"
|
||||
:messages="JSON.stringify(messages)"
|
||||
@@ -267,10 +325,22 @@ onUnmounted(() => {
|
||||
:show-emojis="true"
|
||||
:show-reaction-emojis="false"
|
||||
:accepted-files="'image/*,video/*,application/pdf'"
|
||||
:message-images="true"
|
||||
@send-message="sendMessage"
|
||||
@fetch-messages="fetchMessages"
|
||||
@open-file="openFile"
|
||||
/>
|
||||
|
||||
<!-- Stop button overlay -->
|
||||
<div v-if="isAgentProcessing" class="stop-button-container">
|
||||
<Button
|
||||
icon="pi pi-stop-circle"
|
||||
label="Stop"
|
||||
severity="danger"
|
||||
@click="stopAgent"
|
||||
class="stop-button"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -279,8 +349,9 @@ onUnmounted(() => {
|
||||
height: 100% !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
background: var(--p-surface-0);
|
||||
background: var(--p-surface-0) !important;
|
||||
overflow: hidden;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.chat-container :deep(.vac-container) {
|
||||
@@ -306,4 +377,25 @@ onUnmounted(() => {
|
||||
font-weight: 600;
|
||||
color: var(--p-surface-900);
|
||||
}
|
||||
|
||||
.stop-button-container {
|
||||
position: absolute;
|
||||
bottom: 80px;
|
||||
right: 20px;
|
||||
z-index: 1000;
|
||||
}
|
||||
|
||||
.stop-button {
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||||
animation: pulse 2s infinite;
|
||||
}
|
||||
|
||||
@keyframes pulse {
|
||||
0%, 100% {
|
||||
opacity: 1;
|
||||
}
|
||||
50% {
|
||||
opacity: 0.8;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed } from 'vue'
|
||||
import { ref, computed, onMounted } from 'vue'
|
||||
import Card from 'primevue/card'
|
||||
import InputText from 'primevue/inputtext'
|
||||
import Password from 'primevue/password'
|
||||
@@ -66,6 +66,14 @@ const togglePasswordChange = () => {
|
||||
newPassword.value = ''
|
||||
confirmNewPassword.value = ''
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
// Focus on the password input when component mounts
|
||||
const passwordInput = document.querySelector('#password input') as HTMLInputElement
|
||||
if (passwordInput) {
|
||||
passwordInput.focus()
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -81,13 +89,13 @@ const togglePasswordChange = () => {
|
||||
<template #content>
|
||||
<div class="login-content">
|
||||
<p v-if="needsConfirmation" class="welcome-message">
|
||||
This is your first time connecting. Please create a master password to secure your workspace.
|
||||
This is your first time connecting. Please create a password to secure your workspace.
|
||||
</p>
|
||||
<p v-else-if="isChangingPassword" class="welcome-message">
|
||||
Enter your current password and choose a new one.
|
||||
</p>
|
||||
<p v-else class="welcome-message">
|
||||
Enter your master password to connect.
|
||||
Enter your password to connect.
|
||||
</p>
|
||||
|
||||
<Message v-if="errorMessage" severity="error" :closable="false">
|
||||
|
||||
Reference in New Issue
Block a user