Hwandji's picture
feat: initial HuggingFace Space deployment
4343907
raw
history blame
11.2 kB
<template>
<div class="modal-overlay" @click="$emit('close')">
<div class="chat-modal" @click.stop>
<!-- Modal Header -->
<div class="chat-header">
<div class="agent-info">
<div
class="agent-avatar"
:class="`bg-agent-${agent.id.replace('_alesi', '')}`"
>
{{ getAgentInitials(agent.name) }}
</div>
<div>
<h3 class="saap-heading-3">{{ agent.name }}</h3>
<p class="saap-body-small text-saap-gray-600">
{{ agent.type }} Agent • {{ agent.status }}
</p>
</div>
</div>
<button
@click="$emit('close')"
class="close-button"
title="Close Chat"
>
<XMarkIcon class="w-5 h-5" />
</button>
</div>
<!-- Chat Messages -->
<div class="chat-messages" ref="messagesContainer">
<div v-if="chatHistory.length === 0" class="empty-chat">
<ChatBubbleLeftEllipsisIcon class="w-12 h-12 text-saap-gray-400 mx-auto mb-3" />
<p class="saap-body text-saap-gray-600">
Start a conversation with {{ agent.name }}
</p>
</div>
<div
v-for="message in chatHistory"
:key="message.id"
class="message-group"
>
<!-- User Message -->
<div class="message user-message">
<div class="message-content">
<p>{{ message.user_message }}</p>
</div>
<div class="message-time">
{{ formatTime(message.timestamp) }}
</div>
</div>
<!-- Agent Response -->
<div class="message agent-message">
<div
class="agent-message-avatar"
:class="`bg-agent-${agent.id.replace('_alesi', '')}`"
>
{{ getAgentInitials(agent.name) }}
</div>
<div class="message-content">
<div class="agent-name">{{ agent.name }}</div>
<p>{{ message.agent_response }}</p>
</div>
<div class="message-time">
{{ formatTime(message.timestamp) }}
</div>
</div>
</div>
<!-- Loading indicator -->
<div v-if="isLoading" class="message agent-message loading">
<div
class="agent-message-avatar"
:class="`bg-agent-${agent.id.replace('_alesi', '')}`"
>
{{ getAgentInitials(agent.name) }}
</div>
<div class="message-content">
<div class="agent-name">{{ agent.name }}</div>
<div class="typing-indicator">
<span></span>
<span></span>
<span></span>
</div>
</div>
</div>
</div>
<!-- Chat Input -->
<div class="chat-input-section">
<form @submit.prevent="sendMessage" class="chat-form">
<div class="input-container">
<textarea
v-model="currentMessage"
@keydown.enter.exact.prevent="sendMessage"
@keydown.enter.shift.exact="addNewLine"
placeholder="Type your message... (Enter to send, Shift+Enter for new line)"
class="message-input"
rows="1"
ref="messageInput"
:disabled="isLoading"
></textarea>
<button
type="submit"
:disabled="!canSend"
class="send-button"
title="Send Message"
>
<PaperAirplaneIcon class="w-5 h-5" />
</button>
</div>
<!-- Quick Actions -->
<div class="quick-actions">
<button
v-for="quickAction in quickActions"
:key="quickAction.text"
@click="setQuickMessage(quickAction.text)"
class="quick-action"
type="button"
>
{{ quickAction.label }}
</button>
</div>
</form>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch, nextTick, onMounted } from 'vue'
import { useAgentStore } from '@/stores/agents'
import {
XMarkIcon,
PaperAirplaneIcon,
ChatBubbleLeftEllipsisIcon
} from '@heroicons/vue/24/outline'
// Props
const props = defineProps({
agent: {
type: Object,
required: true
}
})
// Emits
const emit = defineEmits(['close', 'send-message'])
// Store
const agentStore = useAgentStore()
// Reactive state
const currentMessage = ref('')
const isLoading = ref(false)
const messagesContainer = ref(null)
const messageInput = ref(null)
// Computed
const chatHistory = computed(() => {
return agentStore.getChatMessages(props.agent.id)
})
const canSend = computed(() => {
return currentMessage.value.trim() && !isLoading.value && props.agent.status === 'active'
})
// Quick actions based on agent type
const quickActions = computed(() => {
const baseActions = [
{ label: 'Status', text: 'What is your current status?' },
{ label: 'Help', text: 'What can you help me with?' }
]
const agentSpecificActions = {
jane_alesi: [
{ label: 'Coordinate', text: 'Help me coordinate a multi-agent project' },
{ label: 'Strategy', text: 'What is your recommended strategy for this task?' }
],
john_alesi: [
{ label: 'Code Review', text: 'Can you review my code?' },
{ label: 'Architecture', text: 'Help me design the system architecture' }
],
lara_alesi: [
{ label: 'Medical Analysis', text: 'Provide medical analysis for this case' },
{ label: 'Compliance', text: 'Check medical compliance requirements' }
]
}
return [...baseActions, ...(agentSpecificActions[props.agent.id] || [])]
})
// Methods
const getAgentInitials = (name) => {
return name.split(' ').map(word => word[0]).join('').toUpperCase().slice(0, 2)
}
const formatTime = (timestamp) => {
return new Date(timestamp).toLocaleTimeString('de-DE', {
hour: '2-digit',
minute: '2-digit'
})
}
const sendMessage = async () => {
const message = currentMessage.value.trim()
if (!canSend.value) return
isLoading.value = true
const originalMessage = currentMessage.value
currentMessage.value = ''
try {
await agentStore.chatWithAgent(props.agent.id, message)
await scrollToBottom()
} catch (error) {
console.error('Failed to send message:', error)
// Restore message on error
currentMessage.value = originalMessage
} finally {
isLoading.value = false
focusInput()
}
}
const addNewLine = () => {
currentMessage.value += '\n'
}
const setQuickMessage = (message) => {
currentMessage.value = message
focusInput()
}
const scrollToBottom = async () => {
await nextTick()
if (messagesContainer.value) {
messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
}
}
const focusInput = async () => {
await nextTick()
if (messageInput.value) {
messageInput.value.focus()
}
}
// Watchers
watch(chatHistory, () => {
scrollToBottom()
}, { deep: true })
// Lifecycle
onMounted(() => {
focusInput()
scrollToBottom()
})
</script>
<style scoped>
.modal-overlay {
@apply fixed inset-0 bg-black bg-opacity-50 z-50;
@apply flex items-center justify-center p-4;
}
.chat-modal {
@apply bg-white rounded-xl shadow-2xl;
@apply w-full max-w-2xl h-[600px];
@apply flex flex-col;
}
/* Header */
.chat-header {
@apply flex items-center justify-between;
@apply p-6 border-b border-saap-gray-200;
}
.agent-info {
@apply flex items-center space-x-3;
}
.agent-avatar {
@apply w-10 h-10 rounded-full;
@apply flex items-center justify-center;
@apply text-white font-semibold text-sm;
}
.close-button {
@apply p-2 hover:bg-saap-gray-100 rounded-lg;
@apply transition-colors duration-200;
@apply text-saap-gray-500 hover:text-saap-gray-700;
}
/* Chat Messages */
.chat-messages {
@apply flex-1 overflow-y-auto p-6 space-y-4;
@apply bg-saap-gray-50;
}
.empty-chat {
@apply text-center py-12;
}
.message-group {
@apply space-y-3;
}
.message {
@apply flex space-x-3;
}
/* User Message */
.user-message {
@apply justify-end;
}
.user-message .message-content {
@apply bg-saap-primary-600 text-white;
@apply px-4 py-2 rounded-2xl rounded-tr-md;
@apply max-w-xs sm:max-w-md;
}
.user-message .message-time {
@apply text-xs text-saap-gray-500 mt-1 text-right;
}
/* Agent Message */
.agent-message {
@apply justify-start;
}
.agent-message-avatar {
@apply w-8 h-8 rounded-full flex-shrink-0;
@apply flex items-center justify-center;
@apply text-white font-semibold text-xs;
}
.agent-message .message-content {
@apply bg-white border border-saap-gray-200;
@apply px-4 py-2 rounded-2xl rounded-tl-md;
@apply max-w-xs sm:max-w-md;
}
.agent-name {
@apply text-xs font-medium text-saap-gray-600 mb-1;
}
.agent-message .message-time {
@apply text-xs text-saap-gray-500 mt-1;
}
/* Loading Message */
.message.loading .message-content {
@apply bg-white border border-saap-gray-200;
}
.typing-indicator {
@apply flex space-x-1 py-2;
}
.typing-indicator span {
@apply w-2 h-2 bg-saap-gray-400 rounded-full;
@apply animate-pulse;
}
.typing-indicator span:nth-child(1) {
animation-delay: 0s;
}
.typing-indicator span:nth-child(2) {
animation-delay: 0.2s;
}
.typing-indicator span:nth-child(3) {
animation-delay: 0.4s;
}
/* Chat Input */
.chat-input-section {
@apply p-6 border-t border-saap-gray-200 bg-white;
}
.chat-form {
@apply space-y-3;
}
.input-container {
@apply flex items-end space-x-3;
}
.message-input {
@apply flex-1 resize-none;
@apply border border-saap-gray-300 rounded-lg px-4 py-3;
@apply focus:outline-none focus:ring-2 focus:ring-saap-primary-500 focus:border-transparent;
@apply disabled:bg-saap-gray-100 disabled:cursor-not-allowed;
min-height: 44px;
max-height: 120px;
}
.send-button {
@apply bg-saap-primary-600 hover:bg-saap-primary-700;
@apply text-white p-3 rounded-lg;
@apply disabled:bg-saap-gray-400 disabled:cursor-not-allowed;
@apply transition-colors duration-200;
@apply flex-shrink-0;
}
.quick-actions {
@apply flex flex-wrap gap-2;
}
.quick-action {
@apply px-3 py-1 text-xs;
@apply bg-saap-gray-100 hover:bg-saap-gray-200;
@apply text-saap-gray-700 rounded-full;
@apply transition-colors duration-200;
}
/* Agent-specific colors */
.bg-agent-jane {
@apply bg-saap-jane;
}
.bg-agent-john {
@apply bg-saap-john;
}
.bg-agent-lara {
@apply bg-saap-lara;
}
.bg-agent-justus {
@apply bg-saap-justus;
}
/* Responsive */
@media (max-width: 640px) {
.chat-modal {
@apply h-[90vh] m-2;
}
.user-message .message-content,
.agent-message .message-content {
@apply max-w-xs;
}
.quick-actions {
@apply hidden;
}
}
/* Scrollbar */
.chat-messages::-webkit-scrollbar {
width: 6px;
}
.chat-messages::-webkit-scrollbar-track {
@apply bg-saap-gray-200;
}
.chat-messages::-webkit-scrollbar-thumb {
@apply bg-saap-gray-400 rounded-full;
}
.chat-messages::-webkit-scrollbar-thumb:hover {
@apply bg-saap-gray-500;
}
</style>