Spaces:
Sleeping
Sleeping
| <template> | |
| <div class="modal-overlay" @click.self="$emit('close')"> | |
| <div class="chat-modal-content"> | |
| <div class="chat-header"> | |
| <div class="agent-info"> | |
| <div class="agent-avatar" :style="{ backgroundColor: agent.color || '#6B7280' }"> | |
| {{ agent.name?.charAt(0) || 'A' }} | |
| </div> | |
| <div> | |
| <h2 class="agent-name">{{ agent.name }}</h2> | |
| <p class="agent-status" :class="statusClass"> | |
| {{ getStatusText() }} | |
| <span v-if="connectionError" class="error-indicator">⚠️</span> | |
| </p> | |
| </div> | |
| </div> | |
| <!-- 🚀 Provider Switcher --> | |
| <div class="provider-controls"> | |
| <div class="provider-switcher"> | |
| <button | |
| class="provider-btn" | |
| :class="{ active: currentProvider === 'colossus' }" | |
| @click="currentProvider = 'colossus'" | |
| title="Free but slower (15-60s)" | |
| > | |
| 🤖 colossus | |
| <span class="provider-label">Free</span> | |
| </button> | |
| <button | |
| class="provider-btn" | |
| :class="{ active: currentProvider === 'openrouter' }" | |
| @click="currentProvider = 'openrouter'" | |
| title="Fast but costs money (1-3s)" | |
| > | |
| ⚡ OpenRouter | |
| <span class="provider-label">Fast</span> | |
| </button> | |
| </div> | |
| </div> | |
| <button class="close-button" @click="$emit('close')"> | |
| ✕ | |
| </button> | |
| </div> | |
| <!-- 🚀 NEW: Loading Progress Bar --> | |
| <div v-if="sending && currentProvider === 'colossus'" class="loading-progress"> | |
| <div class="progress-bar"> | |
| <div class="progress-fill" :style="{ width: `${loadingProgress}%` }"></div> | |
| </div> | |
| <div class="progress-text"> | |
| {{ loadingMessage }} ({{ Math.ceil(loadingProgress/100 * 60) }}s / 60s) | |
| </div> | |
| <button v-if="loadingProgress > 50" @click="switchToOpenRouter" class="quick-switch-btn"> | |
| ⚡ Switch to OpenRouter (Fast) | |
| </button> | |
| </div> | |
| <div class="chat-body" ref="chatContainer"> | |
| <div v-if="messages.length === 0" class="empty-chat"> | |
| <div class="empty-icon">💬</div> | |
| <p>Start a conversation with {{ agent.name }}</p> | |
| <div class="provider-info"> | |
| <p><strong>Current Provider:</strong> | |
| <span :class="getProviderClass()"> | |
| {{ currentProvider === 'colossus' ? '🤖 colossus (Free, ~30-60s)' : '⚡ OpenRouter (Fast, ~2s)' }} | |
| </span> | |
| </p> | |
| <p v-if="currentProvider === 'colossus'" class="provider-warning"> | |
| ⏱️ <strong>Patience Required:</strong> colossus responses can take 30-60 seconds. | |
| Use ⚡ OpenRouter for faster responses. | |
| </p> | |
| </div> | |
| <p v-if="connectionError" class="connection-warning"> | |
| ⚠️ Connection issue detected. Messages may fail. | |
| </p> | |
| </div> | |
| <div v-for="(message, index) in messages" :key="index" class="message" :class="message.type"> | |
| <div class="message-content" :class="{ 'error-message': message.error, 'timeout-message': message.timeout }"> | |
| <div class="message-header" v-if="message.provider"> | |
| <span class="provider-badge" :class="`provider-${message.provider}`"> | |
| {{ message.provider === 'colossus' ? '🤖' : '⚡' }} {{ message.provider }} | |
| </span> | |
| <span v-if="message.performance" class="performance-info"> | |
| {{ message.performance.response_time }}s | |
| <span v-if="message.performance.cost_usd" class="cost-info"> | |
| ${{ message.performance.cost_usd.toFixed(4) }} | |
| </span> | |
| </span> | |
| </div> | |
| <div class="message-text">{{ message.text }}</div> | |
| <div class="message-time">{{ formatTime(message.timestamp) }}</div> | |
| <div v-if="message.error" class="error-details"> | |
| {{ message.errorDetails }} | |
| <button @click="retryMessage(message)" class="retry-button">🔄 Retry</button> | |
| <button v-if="message.provider === 'colossus'" @click="retryWithOpenRouter(message)" class="switch-retry-button"> | |
| ⚡ Try OpenRouter | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="chat-footer"> | |
| <div v-if="connectionError" class="connection-error"> | |
| <div class="error-message"> | |
| ⚠️ Connection Error: {{ connectionError }} | |
| <button @click="testConnection" class="test-connection-button">🔧 Test Connection</button> | |
| </div> | |
| </div> | |
| <form @submit.prevent="sendMessage"> | |
| <div class="message-input-container"> | |
| <input | |
| v-model="newMessage" | |
| type="text" | |
| class="message-input" | |
| :placeholder="`Send to ${currentProvider} (${currentProvider === 'colossus' ? 'Free, ~60s' : 'Fast, ~2s'})...`" | |
| :disabled="sending || agent.status !== 'active'" | |
| /> | |
| <button | |
| type="submit" | |
| class="send-button" | |
| :class="`provider-${currentProvider}`" | |
| :disabled="!newMessage.trim() || sending || agent.status !== 'active'" | |
| > | |
| {{ sending ? '⏳' : (currentProvider === 'colossus' ? '🤖' : '⚡') }} | |
| </button> | |
| </div> | |
| </form> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| <script> | |
| import { ref, reactive, nextTick, computed, onUnmounted } from 'vue' | |
| export default { | |
| name: 'ChatModal', | |
| props: { | |
| agent: { | |
| type: Object, | |
| required: true | |
| } | |
| }, | |
| emits: ['close'], | |
| setup(props) { | |
| const newMessage = ref('') | |
| const sending = ref(false) | |
| const chatContainer = ref(null) | |
| const connectionError = ref('') | |
| const currentProvider = ref('colossus') | |
| // 🚀 NEW: Loading progress tracking | |
| const loadingProgress = ref(0) | |
| const loadingMessage = ref('') | |
| let progressInterval = null | |
| let requestTimeout = null | |
| const messages = reactive([ | |
| { | |
| type: 'agent', | |
| text: `Hello! I'm ${props.agent.name}. How can I assist you today?`, | |
| timestamp: Date.now(), | |
| provider: 'system' | |
| } | |
| ]) | |
| const statusClass = computed(() => ({ | |
| 'text-green-600': props.agent.status === 'active' && !connectionError.value, | |
| 'text-red-600': connectionError.value, | |
| 'text-gray-600': props.agent.status !== 'active' | |
| })) | |
| const getProviderClass = () => ({ | |
| 'text-blue-600': currentProvider.value === 'colossus', | |
| 'text-green-600': currentProvider.value === 'openrouter' | |
| }) | |
| const getStatusText = () => { | |
| if (connectionError.value) return 'Connection Error' | |
| return props.agent.status === 'active' ? 'Online' : 'Offline' | |
| } | |
| // 🔧 ENHANCED: Response parsing function | |
| const parseResponse = (data) => { | |
| console.log('🔍 Parsing response:', data) | |
| // Priority order for response text extraction | |
| let responseText = null | |
| let performance = null | |
| if (data.error) { | |
| return { | |
| text: `❌ Error: ${data.error}`, | |
| error: true, | |
| errorDetails: data.error | |
| } | |
| } | |
| // Try different response formats | |
| if (data.response) { | |
| if (typeof data.response === 'string') { | |
| responseText = data.response | |
| } else if (typeof data.response === 'object' && data.response.content) { | |
| responseText = data.response.content | |
| } else if (typeof data.response === 'object') { | |
| responseText = JSON.stringify(data.response) | |
| } | |
| } else if (data.content) { | |
| responseText = data.content | |
| } else if (data.text) { | |
| responseText = data.text | |
| } else if (data.message) { | |
| responseText = data.message | |
| } | |
| // Extract performance data | |
| if (data.response_time || data.cost || data.model) { | |
| performance = { | |
| response_time: data.response_time || 0, | |
| cost_usd: data.cost || 0, | |
| model: data.model || 'unknown' | |
| } | |
| } else if (data.performance) { | |
| performance = data.performance | |
| } | |
| return { | |
| text: responseText || "⚠️ Received response but couldn't parse content", | |
| error: false, | |
| performance: performance | |
| } | |
| } | |
| // 🚀 NEW: Progress tracking functions | |
| const startProgressTracking = (provider) => { | |
| loadingProgress.value = 0 | |
| if (provider === 'colossus') { | |
| loadingMessage.value = 'Processing with colossus...' | |
| progressInterval = setInterval(() => { | |
| if (loadingProgress.value < 95) { | |
| loadingProgress.value += 1.67 // 60 seconds / 60 intervals | |
| } | |
| }, 1000) | |
| } else { | |
| loadingMessage.value = 'Processing with OpenRouter...' | |
| progressInterval = setInterval(() => { | |
| if (loadingProgress.value < 90) { | |
| loadingProgress.value += 15 // Faster for OpenRouter | |
| } | |
| }, 200) | |
| } | |
| } | |
| const stopProgressTracking = () => { | |
| if (progressInterval) { | |
| clearInterval(progressInterval) | |
| progressInterval = null | |
| } | |
| if (requestTimeout) { | |
| clearTimeout(requestTimeout) | |
| requestTimeout = null | |
| } | |
| loadingProgress.value = 0 | |
| } | |
| const switchToOpenRouter = () => { | |
| currentProvider.value = 'openrouter' | |
| // Cancel current request and resend | |
| if (sending.value && newMessage.value) { | |
| // We can't easily cancel the ongoing request, but we can switch provider | |
| stopProgressTracking() | |
| sending.value = false | |
| // Show switching message | |
| messages.push({ | |
| type: 'system', | |
| text: '⚡ Switching to OpenRouter for faster response...', | |
| timestamp: Date.now(), | |
| provider: 'system' | |
| }) | |
| } | |
| } | |
| const retryWithOpenRouter = (failedMessage) => { | |
| currentProvider.value = 'openrouter' | |
| if (failedMessage.originalText) { | |
| newMessage.value = failedMessage.originalText | |
| sendMessage() | |
| } | |
| } | |
| const testConnection = async () => { | |
| try { | |
| const response = await fetch(`http://localhost:8000/api/v1/health`) | |
| if (response.ok) { | |
| connectionError.value = '' | |
| } else { | |
| connectionError.value = `Server returned ${response.status}` | |
| } | |
| } catch (error) { | |
| connectionError.value = `Cannot reach server: ${error.message}` | |
| } | |
| } | |
| // 🚀 ENHANCED: Send message with improved response parsing | |
| const sendMessage = async () => { | |
| if (!newMessage.value.trim() || sending.value) return | |
| const userMessage = { | |
| type: 'user', | |
| text: newMessage.value, | |
| timestamp: Date.now(), | |
| provider: currentProvider.value, | |
| originalText: newMessage.value // Store for retry | |
| } | |
| messages.push(userMessage) | |
| const messageText = newMessage.value | |
| newMessage.value = '' | |
| sending.value = true | |
| startProgressTracking(currentProvider.value) | |
| await nextTick() | |
| scrollToBottom() | |
| try { | |
| console.log(`🚀 Sending to ${currentProvider.value}: ${messageText}`) | |
| // 🚀 ENHANCED: Custom timeout based on provider | |
| const timeoutDuration = currentProvider.value === 'colossus' ? 65000 : 15000 // 65s for colossus, 15s for OpenRouter | |
| const controller = new AbortController() | |
| requestTimeout = setTimeout(() => { | |
| controller.abort() | |
| }, timeoutDuration) | |
| const endpoint = currentProvider.value === 'openrouter' | |
| ? `/api/v1/agents/${props.agent.id}/chat/openrouter` | |
| : `/api/v1/agents/${props.agent.id}/chat` | |
| const response = await fetch(`http://localhost:8000${endpoint}`, { | |
| method: 'POST', | |
| headers: { | |
| 'Content-Type': 'application/json' | |
| }, | |
| body: JSON.stringify({ | |
| message: messageText, | |
| timestamp: new Date().toISOString() // 🔧 CRITICAL FIX: String timestamp instead of Date.now() | |
| }), | |
| signal: controller.signal | |
| }) | |
| clearTimeout(requestTimeout) | |
| console.log(`📡 API Response Status:`, response.status, response.statusText) | |
| if (response.ok) { | |
| const data = await response.json() | |
| console.log(`📦 API Response Data:`, data) | |
| // 🔧 ENHANCED: Use new parsing function | |
| const parsed = parseResponse(data) | |
| if (!parsed.error) { | |
| connectionError.value = '' | |
| } else { | |
| connectionError.value = parsed.errorDetails || 'Response parsing error' | |
| } | |
| const agentMessage = { | |
| type: 'agent', | |
| text: parsed.text, | |
| timestamp: Date.now(), | |
| error: parsed.error, | |
| errorDetails: parsed.errorDetails, | |
| provider: currentProvider.value, | |
| performance: parsed.performance, | |
| rawResponse: data, | |
| originalText: messageText // Store for retry | |
| } | |
| messages.push(agentMessage) | |
| } else { | |
| const errorText = await response.text() | |
| console.error('API HTTP Error:', response.status, errorText) | |
| connectionError.value = `HTTP ${response.status}: ${response.statusText}` | |
| messages.push({ | |
| type: 'agent', | |
| text: `❌ Server Error: ${response.status} ${response.statusText}`, | |
| timestamp: Date.now(), | |
| error: true, | |
| errorDetails: errorText, | |
| provider: currentProvider.value, | |
| originalText: messageText | |
| }) | |
| } | |
| } catch (error) { | |
| if (error.name === 'AbortError') { | |
| const timeoutMsg = currentProvider.value === 'colossus' | |
| ? '⏱️ colossus timeout after 65 seconds. The server might be overloaded. Try ⚡ OpenRouter for faster responses.' | |
| : '⏱️ OpenRouter timeout after 15 seconds. Network issue detected.' | |
| connectionError.value = `${currentProvider.value} timeout` | |
| messages.push({ | |
| type: 'agent', | |
| text: timeoutMsg, | |
| timestamp: Date.now(), | |
| error: true, | |
| timeout: true, | |
| errorDetails: 'Request timeout - server took too long to respond', | |
| provider: currentProvider.value, | |
| originalText: messageText | |
| }) | |
| } else { | |
| console.error('Chat network error:', error) | |
| connectionError.value = `Network error: ${error.message}` | |
| messages.push({ | |
| type: 'agent', | |
| text: `❌ Connection Error: ${error.message}`, | |
| timestamp: Date.now(), | |
| error: true, | |
| errorDetails: error.message, | |
| provider: currentProvider.value, | |
| originalText: messageText | |
| }) | |
| } | |
| } finally { | |
| stopProgressTracking() | |
| sending.value = false | |
| await nextTick() | |
| scrollToBottom() | |
| } | |
| } | |
| const retryMessage = async (failedMessage) => { | |
| if (!failedMessage.originalText) return | |
| newMessage.value = failedMessage.originalText | |
| await sendMessage() | |
| } | |
| const scrollToBottom = () => { | |
| if (chatContainer.value) { | |
| chatContainer.value.scrollTop = chatContainer.value.scrollHeight | |
| } | |
| } | |
| const formatTime = (timestamp) => { | |
| return new Date(timestamp).toLocaleTimeString('de-DE', { | |
| hour: '2-digit', | |
| minute: '2-digit' | |
| }) | |
| } | |
| // Cleanup on component unmount | |
| onUnmounted(() => { | |
| stopProgressTracking() | |
| }) | |
| return { | |
| newMessage, | |
| sending, | |
| messages, | |
| chatContainer, | |
| connectionError, | |
| currentProvider, | |
| loadingProgress, | |
| loadingMessage, | |
| statusClass, | |
| getProviderClass, | |
| getStatusText, | |
| testConnection, | |
| sendMessage, | |
| retryMessage, | |
| retryWithOpenRouter, | |
| switchToOpenRouter, | |
| formatTime | |
| } | |
| } | |
| } | |
| </script> | |
| <style scoped> | |
| .modal-overlay { | |
| @apply fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50; | |
| } | |
| .chat-modal-content { | |
| @apply bg-white rounded-lg shadow-xl w-full max-w-2xl mx-4 h-3/4 flex flex-col; | |
| } | |
| .chat-header { | |
| @apply flex items-center justify-between p-4 border-b border-gray-200; | |
| } | |
| .agent-info { | |
| @apply flex items-center space-x-3; | |
| } | |
| .agent-avatar { | |
| @apply w-10 h-10 rounded-full flex items-center justify-center text-white font-medium; | |
| } | |
| .agent-name { | |
| @apply font-semibold text-gray-900; | |
| } | |
| .agent-status { | |
| @apply text-sm font-medium; | |
| } | |
| .error-indicator { | |
| @apply text-orange-500; | |
| } | |
| /* Provider Controls */ | |
| .provider-controls { | |
| @apply flex items-center space-x-3; | |
| } | |
| .provider-switcher { | |
| @apply flex bg-gray-100 rounded-lg p-1; | |
| } | |
| .provider-btn { | |
| @apply px-3 py-2 rounded-md text-sm font-medium transition-colors duration-200; | |
| @apply text-gray-600 hover:text-gray-900 flex items-center space-x-1; | |
| } | |
| .provider-btn.active { | |
| @apply bg-white text-gray-900 shadow-sm; | |
| } | |
| .provider-label { | |
| @apply text-xs bg-gray-200 text-gray-600 px-1 rounded; | |
| } | |
| .provider-btn.active .provider-label { | |
| @apply bg-blue-100 text-blue-700; | |
| } | |
| .close-button { | |
| @apply text-gray-400 hover:text-gray-600 text-xl font-bold w-8 h-8 flex items-center justify-center; | |
| } | |
| /* 🚀 NEW: Loading Progress */ | |
| .loading-progress { | |
| @apply bg-blue-50 border-b border-blue-200 p-4; | |
| } | |
| .progress-bar { | |
| @apply w-full bg-blue-200 rounded-full h-3 mb-2; | |
| } | |
| .progress-fill { | |
| @apply bg-blue-600 h-full rounded-full transition-all duration-1000; | |
| } | |
| .progress-text { | |
| @apply text-sm text-blue-700 text-center mb-2; | |
| } | |
| .quick-switch-btn { | |
| @apply w-full px-3 py-2 bg-green-600 text-white rounded-md text-sm font-medium; | |
| @apply hover:bg-green-700 transition-colors duration-200; | |
| } | |
| .chat-body { | |
| @apply flex-1 overflow-y-auto p-4 space-y-4; | |
| } | |
| .empty-chat { | |
| @apply text-center py-12; | |
| } | |
| .empty-icon { | |
| @apply text-4xl mb-4; | |
| } | |
| .provider-info { | |
| @apply mt-4 p-3 bg-blue-50 rounded-lg; | |
| } | |
| .provider-warning { | |
| @apply text-sm text-orange-600 mt-2 font-medium; | |
| } | |
| .connection-warning { | |
| @apply text-sm text-orange-600 mt-2; | |
| } | |
| .message { | |
| @apply flex; | |
| } | |
| .message.user { | |
| @apply justify-end; | |
| } | |
| .message.agent { | |
| @apply justify-start; | |
| } | |
| .message-content { | |
| @apply max-w-xs lg:max-w-md px-4 py-2 rounded-lg; | |
| } | |
| .message.user .message-content { | |
| @apply bg-blue-600 text-white; | |
| } | |
| .message.agent .message-content { | |
| @apply bg-gray-200 text-gray-900; | |
| } | |
| .message-content.error-message { | |
| @apply bg-red-100 border border-red-300 text-red-800; | |
| } | |
| .message-content.timeout-message { | |
| @apply bg-orange-100 border border-orange-300 text-orange-800; | |
| } | |
| /* Provider-specific styling */ | |
| .message-header { | |
| @apply flex items-center justify-between text-xs mb-2; | |
| } | |
| .provider-badge { | |
| @apply px-2 py-1 rounded text-xs font-medium; | |
| } | |
| .provider-colossus { | |
| @apply bg-blue-100 text-blue-700; | |
| } | |
| .provider-openrouter { | |
| @apply bg-green-100 text-green-700; | |
| } | |
| .provider-comparison { | |
| @apply bg-purple-100 text-purple-700; | |
| } | |
| .provider-system { | |
| @apply bg-gray-100 text-gray-600; | |
| } | |
| .performance-info { | |
| @apply text-gray-500; | |
| } | |
| .cost-info { | |
| @apply text-green-600 font-medium; | |
| } | |
| .message-text { | |
| @apply text-sm; | |
| } | |
| .message-time { | |
| @apply text-xs opacity-70 mt-1; | |
| } | |
| .error-details { | |
| @apply text-xs mt-2 text-red-600 border-t border-red-200 pt-2; | |
| } | |
| .retry-button { | |
| @apply ml-2 text-xs bg-red-600 text-white px-2 py-1 rounded hover:bg-red-700; | |
| } | |
| .switch-retry-button { | |
| @apply ml-1 text-xs bg-green-600 text-white px-2 py-1 rounded hover:bg-green-700; | |
| } | |
| .chat-footer { | |
| @apply border-t border-gray-200 p-4; | |
| } | |
| .connection-error { | |
| @apply mb-3 p-3 bg-red-50 border border-red-200 rounded-md; | |
| } | |
| .error-message { | |
| @apply text-sm text-red-700 flex items-center justify-between; | |
| } | |
| .test-connection-button { | |
| @apply text-xs bg-red-600 text-white px-2 py-1 rounded hover:bg-red-700 ml-2; | |
| } | |
| .message-input-container { | |
| @apply flex space-x-2; | |
| } | |
| .message-input { | |
| @apply flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500 disabled:bg-gray-100; | |
| } | |
| .send-button { | |
| @apply px-4 py-2 text-white rounded-md hover:opacity-90 disabled:bg-gray-400 disabled:cursor-not-allowed; | |
| } | |
| .send-button.provider-colossus { | |
| @apply bg-blue-600; | |
| } | |
| .send-button.provider-openrouter { | |
| @apply bg-green-600; | |
| } | |
| </style> | |