| <template> |
| <div class="modal-overlay" @click="$emit('close')"> |
| <div class="chat-modal" @click.stop> |
| |
| <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> |
| |
| |
| <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" |
| > |
| |
| <div class="message user-message"> |
| <div class="message-content"> |
| <p>{{ message.user_message }}</p> |
| </div> |
| <div class="message-time"> |
| {{ formatTime(message.timestamp) }} |
| </div> |
| </div> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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> |
| |
| |
| <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' |
| |
| |
| const props = defineProps({ |
| agent: { |
| type: Object, |
| required: true |
| } |
| }) |
| |
| |
| const emit = defineEmits(['close', 'send-message']) |
| |
| |
| const agentStore = useAgentStore() |
| |
| |
| const currentMessage = ref('') |
| const isLoading = ref(false) |
| const messagesContainer = ref(null) |
| const messageInput = ref(null) |
| |
| |
| const chatHistory = computed(() => { |
| return agentStore.getChatMessages(props.agent.id) |
| }) |
| |
| const canSend = computed(() => { |
| return currentMessage.value.trim() && !isLoading.value && props.agent.status === 'active' |
| }) |
| |
| |
| 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] || [])] |
| }) |
| |
| |
| 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) |
| |
| 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() |
| } |
| } |
| |
| |
| watch(chatHistory, () => { |
| scrollToBottom() |
| }, { deep: true }) |
| |
| |
| 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; |
| } |
| |
| |
| .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 { |
| @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 { |
| @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 { |
| @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; |
| } |
| |
| |
| .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-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; |
| } |
| |
| |
| .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; |
| } |
| |
| |
| @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; |
| } |
| } |
| |
| |
| .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> |