indicators and plots

This commit is contained in:
2026-03-02 18:34:38 -04:00
parent 3b29096dab
commit 3ffce97b3e
43 changed files with 6690 additions and 878 deletions

View File

@@ -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>

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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>

View File

@@ -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">