| <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> |
| |
| |
| <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> |
| |
| |
| <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') |
| |
| |
| 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' |
| } |
| |
| |
| const parseResponse = (data) => { |
| console.log('🔍 Parsing response:', data) |
| |
| |
| let responseText = null |
| let performance = null |
| |
| if (data.error) { |
| return { |
| text: `❌ Error: ${data.error}`, |
| error: true, |
| errorDetails: data.error |
| } |
| } |
| |
| |
| 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 |
| } |
| |
| |
| 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 |
| } |
| } |
| |
| |
| const startProgressTracking = (provider) => { |
| loadingProgress.value = 0 |
| |
| if (provider === 'colossus') { |
| loadingMessage.value = 'Processing with colossus...' |
| progressInterval = setInterval(() => { |
| if (loadingProgress.value < 95) { |
| loadingProgress.value += 1.67 |
| } |
| }, 1000) |
| } else { |
| loadingMessage.value = 'Processing with OpenRouter...' |
| progressInterval = setInterval(() => { |
| if (loadingProgress.value < 90) { |
| loadingProgress.value += 15 |
| } |
| }, 200) |
| } |
| } |
| |
| const stopProgressTracking = () => { |
| if (progressInterval) { |
| clearInterval(progressInterval) |
| progressInterval = null |
| } |
| if (requestTimeout) { |
| clearTimeout(requestTimeout) |
| requestTimeout = null |
| } |
| loadingProgress.value = 0 |
| } |
| |
| const switchToOpenRouter = () => { |
| currentProvider.value = 'openrouter' |
| |
| if (sending.value && newMessage.value) { |
| |
| stopProgressTracking() |
| sending.value = false |
| |
| |
| 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}` |
| } |
| } |
| |
| |
| 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 |
| } |
| |
| 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}`) |
| |
| |
| const timeoutDuration = currentProvider.value === 'colossus' ? 65000 : 15000 |
| 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() |
| }), |
| 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) |
| |
| |
| 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 |
| } |
| |
| 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' |
| }) |
| } |
| |
| |
| 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 { |
| @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; |
| } |
| |
| |
| .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; |
| } |
| |
| |
| .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> |
| |