Hwandji's picture
feat: initial HuggingFace Space deployment
4343907
raw
history blame
20.9 kB
<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>