Part 10: Vue.js Frontend Interface

10 June 2025 · netologist · 45 min, 9499 words ·

Why Vue.js for AI Assistant Interface?

Vue.js provides an excellent foundation for AI assistant interfaces:

Project Structure and Setup

First, let’s understand our frontend architecture:

frontend/
├── public/
│   ├── index.html
│   └── favicon.ico
├── src/
│   ├── components/
│   │   ├── Chat/
│   │   ├── Document/
│   │   ├── Voice/
│   │   ├── Knowledge/
│   │   └── System/
│   ├── services/
│   ├── stores/
│   ├── utils/
│   ├── App.vue
│   └── main.js
├── package.json
└── vite.config.js

Complete Vue.js Frontend Implementation

<!-- App.vue - Main Application Component -->
<template>
  <div id="app" class="min-h-screen bg-gray-50">
    <!-- Navigation Header -->
    <nav class="bg-blue-600 text-white shadow-lg">
      <div class="max-w-7xl mx-auto px-4">
        <div class="flex justify-between items-center h-16">
          <div class="flex items-center space-x-4">
            <div class="flex items-center">
              <svg class="w-8 h-8 mr-2" fill="currentColor" viewBox="0 0 20 20">
                <path d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"/>
              </svg>
              <h1 class="text-xl font-bold">AlexAI Assistant</h1>
            </div>
          </div>
          
          <div class="flex items-center space-x-4">
            <!-- Status Indicator -->
            <div class="flex items-center space-x-2">
              <div :class="[
                'w-3 h-3 rounded-full',
                systemStatus.status === 'running' ? 'bg-green-400' : 'bg-red-400'
              ]"></div>
              <span class="text-sm">{{ systemStatus.status }}</span>
            </div>
            
            <!-- Settings Button -->
            <button 
              @click="showSettings = !showSettings"
              class="p-2 rounded-lg hover:bg-blue-700 transition-colors"
            >
              <svg class="w-5 h-5" fill="currentColor" viewBox="0 0 20 20">
                <path fill-rule="evenodd" d="M11.49 3.17c-.38-1.56-2.6-1.56-2.98 0a1.532 1.532 0 01-2.286.948c-1.372-.836-2.942.734-2.106 2.106.54.886.061 2.042-.947 2.287-1.561.379-1.561 2.6 0 2.978a1.532 1.532 0 01.947 2.287c-.836 1.372.734 2.942 2.106 2.106a1.532 1.532 0 012.287.947c.379 1.561 2.6 1.561 2.978 0a1.533 1.533 0 012.287-.947c1.372.836 2.942-.734 2.106-2.106a1.533 1.533 0 01.947-2.287c1.561-.379 1.561-2.6 0-2.978a1.532 1.532 0 01-.947-2.287c.836-1.372-.734-2.942-2.106-2.106a1.532 1.532 0 01-2.287-.947zM10 13a3 3 0 100-6 3 3 0 000 6z" clip-rule="evenodd"/>
              </svg>
            </button>
          </div>
        </div>
      </div>
    </nav>

    <div class="flex h-[calc(100vh-4rem)]">
      <!-- Sidebar -->
      <aside class="w-64 bg-white shadow-lg border-r">
        <div class="p-4">
          <nav class="space-y-2">
            <button
              v-for="tab in tabs"
              :key="tab.id"
              @click="activeTab = tab.id"
              :class="[
                'w-full flex items-center px-4 py-3 text-left rounded-lg transition-colors',
                activeTab === tab.id 
                  ? 'bg-blue-100 text-blue-700 border-l-4 border-blue-700' 
                  : 'text-gray-600 hover:bg-gray-100'
              ]"
            >
              <component :is="tab.icon" class="w-5 h-5 mr-3" />
              {{ tab.name }}
            </button>
          </nav>
        </div>

        <!-- System Status Panel -->
        <div class="p-4 border-t">
          <h3 class="text-sm font-semibold text-gray-700 mb-2">System Status</h3>
          <div class="space-y-2 text-xs text-gray-600">
            <div class="flex justify-between">
              <span>Uptime:</span>
              <span>{{ formatUptime(systemStatus.uptime) }}</span>
            </div>
            <div class="flex justify-between">
              <span>Requests:</span>
              <span>{{ systemStatus.total_requests }}</span>
            </div>
            <div class="flex justify-between">
              <span>Sessions:</span>
              <span>{{ systemStatus.active_sessions }}</span>
            </div>
          </div>
        </div>
      </aside>

      <!-- Main Content -->
      <main class="flex-1 flex flex-col">
        <!-- Chat Interface -->
        <ChatInterface 
          v-if="activeTab === 'chat'" 
          :messages="chatMessages"
          :loading="chatLoading"
          @send-message="sendChatMessage"
          @clear-chat="clearChat"
        />

        <!-- Document Processing -->
        <DocumentProcessor 
          v-if="activeTab === 'documents'"
          @process-document="processDocument"
        />

        <!-- Voice Interaction -->
        <VoiceInterface 
          v-if="activeTab === 'voice'"
          :is-listening="isListening"
          :is-speaking="isSpeaking"
          @start-listening="startVoiceInteraction"
          @stop-listening="stopVoiceInteraction"
          @text-to-speech="textToSpeech"
        />

        <!-- Knowledge Base -->
        <KnowledgeBase 
          v-if="activeTab === 'knowledge'"
          :search-results="knowledgeResults"
          @search-knowledge="searchKnowledge"
          @add-knowledge="addKnowledge"
        />

        <!-- System Monitor -->
        <SystemMonitor 
          v-if="activeTab === 'system'"
          :status="systemStatus"
          :logs="systemLogs"
          @refresh-status="refreshSystemStatus"
        />
      </main>
    </div>

    <!-- Settings Modal -->
    <SettingsModal 
      v-if="showSettings"
      :settings="appSettings"
      @close="showSettings = false"
      @update-settings="updateSettings"
    />

    <!-- Loading Overlay -->
    <div 
      v-if="globalLoading"
      class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
    >
      <div class="bg-white rounded-lg p-6 flex items-center space-x-4">
        <div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
        <span class="text-gray-700">{{ loadingMessage }}</span>
      </div>
    </div>

    <!-- Toast Notifications -->
    <div class="fixed top-4 right-4 space-y-2 z-40">
      <div
        v-for="notification in notifications"
        :key="notification.id"
        :class="[
          'p-4 rounded-lg shadow-lg max-w-sm transition-all duration-300',
          notification.type === 'success' ? 'bg-green-500 text-white' :
          notification.type === 'error' ? 'bg-red-500 text-white' :
          notification.type === 'warning' ? 'bg-yellow-500 text-white' :
          'bg-blue-500 text-white'
        ]"
      >
        <div class="flex items-center justify-between">
          <span>{{ notification.message }}</span>
          <button @click="removeNotification(notification.id)" class="ml-2 text-white hover:text-gray-200">
            <svg class="w-4 h-4" fill="currentColor" viewBox="0 0 20 20">
              <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
            </svg>
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted, computed } from 'vue'
import { apiService } from './services/api'
import { useNotifications } from './composables/useNotifications'

// Import components
import ChatInterface from './components/Chat/ChatInterface.vue'
import DocumentProcessor from './components/Document/DocumentProcessor.vue'
import VoiceInterface from './components/Voice/VoiceInterface.vue'
import KnowledgeBase from './components/Knowledge/KnowledgeBase.vue'
import SystemMonitor from './components/System/SystemMonitor.vue'
import SettingsModal from './components/Settings/SettingsModal.vue'

// Icons
import ChatIcon from './components/Icons/ChatIcon.vue'
import DocumentIcon from './components/Icons/DocumentIcon.vue'
import VoiceIcon from './components/Icons/VoiceIcon.vue'
import KnowledgeIcon from './components/Icons/KnowledgeIcon.vue'
import SystemIcon from './components/Icons/SystemIcon.vue'

export default {
  name: 'App',
  components: {
    ChatInterface,
    DocumentProcessor,
    VoiceInterface,
    KnowledgeBase,
    SystemMonitor,
    SettingsModal
  },
  setup() {
    // Reactive state
    const activeTab = ref('chat')
    const showSettings = ref(false)
    const globalLoading = ref(false)
    const loadingMessage = ref('')

    // System status
    const systemStatus = ref({
      service: 'AlexAI',
      version: '1.0.0',
      status: 'connecting',
      uptime: 0,
      active_sessions: 0,
      total_requests: 0,
      memory_usage: {}
    })

    // Chat state
    const chatMessages = ref([])
    const chatLoading = ref(false)

    // Voice state
    const isListening = ref(false)
    const isSpeaking = ref(false)

    // Knowledge state
    const knowledgeResults = ref([])

    // System logs
    const systemLogs = ref([])

    // App settings
    const appSettings = ref({
      theme: 'light',
      voiceEnabled: true,
      autoSave: true,
      apiTimeout: 30000,
      sessionId: 'web_session_' + Date.now()
    })

    // Notifications composable
    const { notifications, addNotification, removeNotification } = useNotifications()

    // Navigation tabs
    const tabs = [
      { id: 'chat', name: 'Chat', icon: ChatIcon },
      { id: 'documents', name: 'Documents', icon: DocumentIcon },
      { id: 'voice', name: 'Voice', icon: VoiceIcon },
      { id: 'knowledge', name: 'Knowledge', icon: KnowledgeIcon },
      { id: 'system', name: 'System', icon: SystemIcon }
    ]

    // Methods
    const refreshSystemStatus = async () => {
      try {
        const status = await apiService.getSystemStatus()
        systemStatus.value = status
        
        if (status.status === 'running') {
          addNotification('Connected to AlexAI', 'success')
        }
      } catch (error) {
        console.error('Failed to get system status:', error)
        systemStatus.value.status = 'error'
        addNotification('Failed to connect to AlexAI', 'error')
      }
    }

    const sendChatMessage = async (message) => {
      if (!message.trim()) return

      // Add user message to chat
      chatMessages.value.push({
        id: Date.now(),
        type: 'user',
        content: message,
        timestamp: new Date()
      })

      chatLoading.value = true

      try {
        const response = await apiService.sendChatMessage({
          message: message,
          session_id: appSettings.value.sessionId,
          use_memory: true,
          use_rag: true,
          voice_response: false
        })

        // Add assistant response to chat
        chatMessages.value.push({
          id: Date.now() + 1,
          type: 'assistant',
          content: response.response,
          timestamp: new Date(),
          metadata: {
            response_time: response.response_time,
            used_memory: response.used_memory,
            used_rag: response.used_rag,
            confidence_score: response.confidence_score
          }
        })

        // Update system status
        systemStatus.value.total_requests++

      } catch (error) {
        console.error('Chat error:', error)
        chatMessages.value.push({
          id: Date.now() + 1,
          type: 'error',
          content: 'Sorry, I encountered an error processing your message.',
          timestamp: new Date()
        })
        addNotification('Chat error: ' + error.message, 'error')
      } finally {
        chatLoading.value = false
      }
    }

    const clearChat = () => {
      chatMessages.value = []
      addNotification('Chat cleared', 'info')
    }

    const processDocument = async (file, analysisType, question) => {
      globalLoading.value = true
      loadingMessage.value = 'Processing document...'

      try {
        // Create FormData for file upload
        const formData = new FormData()
        formData.append('file', file)
        formData.append('analysis_type', analysisType)
        if (question) {
          formData.append('question', question)
        }

        const response = await apiService.processDocument(formData)

        // Show results in notification or modal
        addNotification(`Document processed successfully. Confidence: ${response.confidence.toFixed(1)}%`, 'success')

        // Could also switch to results tab or show in modal
        return response

      } catch (error) {
        console.error('Document processing error:', error)
        addNotification('Document processing failed: ' + error.message, 'error')
      } finally {
        globalLoading.value = false
        loadingMessage.value = ''
      }
    }

    const startVoiceInteraction = async () => {
      isListening.value = true
      
      try {
        const response = await apiService.voiceInteraction({
          mode: 'conversation',
          timeout: 30,
          wake_word_detection: true
        })

        if (response.response_text) {
          // Add voice response to chat
          chatMessages.value.push({
            id: Date.now(),
            type: 'assistant',
            content: response.response_text,
            timestamp: new Date(),
            metadata: { source: 'voice' }
          })
        }

        addNotification('Voice interaction completed', 'success')

      } catch (error) {
        console.error('Voice interaction error:', error)
        addNotification('Voice interaction failed: ' + error.message, 'error')
      } finally {
        isListening.value = false
      }
    }

    const stopVoiceInteraction = () => {
      isListening.value = false
      addNotification('Voice interaction stopped', 'info')
    }

    const textToSpeech = async (text) => {
      if (!text.trim()) return

      isSpeaking.value = true

      try {
        await apiService.voiceInteraction({
          mode: 'speak',
          text: text
        })

        addNotification('Text spoken successfully', 'success')

      } catch (error) {
        console.error('Text-to-speech error:', error)
        addNotification('Text-to-speech failed: ' + error.message, 'error')
      } finally {
        isSpeaking.value = false
      }
    }

    const searchKnowledge = async (query, collection = 'documents') => {
      try {
        const response = await apiService.searchKnowledge({
          action: 'search',
          query: query,
          collection: collection
        })

        knowledgeResults.value = response.results || []
        addNotification(`Found ${knowledgeResults.value.length} results`, 'success')

      } catch (error) {
        console.error('Knowledge search error:', error)
        addNotification('Knowledge search failed: ' + error.message, 'error')
      }
    }

    const addKnowledge = async (content, metadata = {}) => {
      try {
        const response = await apiService.addKnowledge({
          action: 'add',
          query: content,
          metadata: metadata
        })

        addNotification(`Knowledge added with ID: ${response.document_id}`, 'success')

      } catch (error) {
        console.error('Add knowledge error:', error)
        addNotification('Failed to add knowledge: ' + error.message, 'error')
      }
    }

    const updateSettings = (newSettings) => {
      appSettings.value = { ...appSettings.value, ...newSettings }
      localStorage.setItem('alexai_settings', JSON.stringify(appSettings.value))
      addNotification('Settings updated', 'success')
    }

    const formatUptime = (seconds) => {
      if (seconds < 60) return `${seconds.toFixed(0)}s`
      if (seconds < 3600) return `${(seconds / 60).toFixed(0)}m`
      return `${(seconds / 3600).toFixed(1)}h`
    }

    // Initialize app
    onMounted(async () => {
      // Load settings from localStorage
      const savedSettings = localStorage.getItem('alexai_settings')
      if (savedSettings) {
        appSettings.value = { ...appSettings.value, ...JSON.parse(savedSettings) }
      }

      // Initial system status check
      await refreshSystemStatus()

      // Set up periodic status updates
      setInterval(refreshSystemStatus, 30000) // Every 30 seconds

      // Welcome message
      chatMessages.value.push({
        id: Date.now(),
        type: 'assistant',
        content: 'Hello! I\'m AlexAI, your personal assistant. How can I help you today?',
        timestamp: new Date()
      })
    })

    return {
      // State
      activeTab,
      showSettings,
      globalLoading,
      loadingMessage,
      systemStatus,
      chatMessages,
      chatLoading,
      isListening,
      isSpeaking,
      knowledgeResults,
      systemLogs,
      appSettings,
      notifications,
      tabs,

      // Methods
      refreshSystemStatus,
      sendChatMessage,
      clearChat,
      processDocument,
      startVoiceInteraction,
      stopVoiceInteraction,
      textToSpeech,
      searchKnowledge,
      addKnowledge,
      updateSettings,
      removeNotification,
      formatUptime
    }
  }
}
</script>

<style>
/* Global styles */
#app {
  font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

/* Custom scrollbar */
::-webkit-scrollbar {
  width: 6px;
}

::-webkit-scrollbar-track {
  background: #f1f1f1;
}

::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 3px;
}

::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}

/* Animation classes */
.fade-enter-active, .fade-leave-active {
  transition: opacity 0.3s ease;
}

.fade-enter-from, .fade-leave-to {
  opacity: 0;
}

.slide-up-enter-active {
  transition: all 0.3s ease-out;
}

.slide-up-leave-active {
  transition: all 0.2s ease-in;
}

.slide-up-enter-from {
  transform: translateY(20px);
  opacity: 0;
}

.slide-up-leave-to {
  transform: translateY(-20px);
  opacity: 0;
}
</style>

API Service Layer

// services/api.js
class ApiService {
  constructor() {
    this.baseURL = process.env.VUE_APP_API_URL || 'http://localhost:8000'
    this.timeout = 30000
  }

  async request(endpoint, options = {}) {
    const url = `${this.baseURL}${endpoint}`
    const config = {
      timeout: this.timeout,
      headers: {
        'Content-Type': 'application/json',
        ...options.headers
      },
      ...options
    }

    try {
      const response = await fetch(url, config)
      
      if (!response.ok) {
        throw new Error(`HTTP ${response.status}: ${response.statusText}`)
      }

      const contentType = response.headers.get('content-type')
      if (contentType && contentType.includes('application/json')) {
        return await response.json()
      }
      
      return await response.text()
    } catch (error) {
      console.error(`API request failed: ${endpoint}`, error)
      throw error
    }
  }

  // System endpoints
  async getSystemStatus() {
    return this.request('/status')
  }

  // Chat endpoints
  async sendChatMessage(data) {
    return this.request('/chat', {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }

  // Document processing endpoints
  async processDocument(formData) {
    return this.request('/document', {
      method: 'POST',
      headers: {}, // Remove Content-Type to let browser set multipart boundary
      body: formData
    })
  }

  // Voice interaction endpoints
  async voiceInteraction(data) {
    return this.request('/voice', {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }

  // Knowledge base endpoints
  async searchKnowledge(data) {
    return this.request('/knowledge', {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }

  async addKnowledge(data) {
    return this.request('/knowledge', {
      method: 'POST',
      body: JSON.stringify(data)
    })
  }
}

export const apiService = new ApiService()

Key Components

Chat Interface Component

<!-- components/Chat/ChatInterface.vue -->
<template>
  <div class="flex flex-col h-full">
    <!-- Chat Header -->
    <div class="bg-white border-b px-6 py-4 flex justify-between items-center">
      <h2 class="text-lg font-semibold text-gray-800">Chat with AlexAI</h2>
      <div class="flex space-x-2">
        <button
          @click="toggleVoiceResponse"
          :class="[
            'px-3 py-1 rounded text-sm transition-colors',
            voiceEnabled ? 'bg-green-100 text-green-700' : 'bg-gray-100 text-gray-700'
          ]"
        >
          🔊 Voice {{ voiceEnabled ? 'On' : 'Off' }}
        </button>
        <button
          @click="$emit('clear-chat')"
          class="px-3 py-1 bg-red-100 text-red-700 rounded text-sm hover:bg-red-200 transition-colors"
        >
          Clear Chat
        </button>
      </div>
    </div>

    <!-- Messages Container -->
    <div class="flex-1 overflow-y-auto p-6 space-y-4" ref="messagesContainer">
      <div
        v-for="message in messages"
        :key="message.id"
        :class="[
          'flex',
          message.type === 'user' ? 'justify-end' : 'justify-start'
        ]"
      >
        <div
          :class="[
            'max-w-xs lg:max-w-md px-4 py-2 rounded-lg',
            message.type === 'user' 
              ? 'bg-blue-600 text-white' 
              : message.type === 'error'
              ? 'bg-red-100 text-red-800 border border-red-200'
              : 'bg-gray-100 text-gray-800'
          ]"
        >
          <div class="text-sm">{{ message.content }}</div>
          
          <!-- Message metadata -->
          <div 
            v-if="message.metadata"
            class="text-xs mt-2 opacity-75"
          >
            <div v-if="message.metadata.response_time">
              Response: {{ message.metadata.response_time.toFixed(2) }}s
            </div>
            <div v-if="message.metadata.confidence_score">
              Confidence: {{ (message.metadata.confidence_score * 100).toFixed(1) }}%
            </div>
            <div class="flex space-x-2 mt-1">
              <span v-if="message.metadata.used_memory" class="bg-blue-500 px-1 rounded">Memory</span>
              <span v-if="message.metadata.used_rag" class="bg-green-500 px-1 rounded">RAG</span>
              <span v-if="message.metadata.source" class="bg-purple-500 px-1 rounded">{{ message.metadata.source }}</span>
            </div>
          </div>
          
          <div class="text-xs mt-1 opacity-75">
            {{ formatTime(message.timestamp) }}
          </div>
        </div>
      </div>

      <!-- Loading indicator -->
      <div v-if="loading" class="flex justify-start">
        <div class="bg-gray-100 text-gray-800 px-4 py-2 rounded-lg flex items-center space-x-2">
          <div class="animate-spin w-4 h-4 border-2 border-gray-400 border-t-transparent rounded-full"></div>
          <span>AlexAI is thinking...</span>
        </div>
      </div>
    </div>

    <!-- Message Input -->
    <div class="bg-white border-t p-4">
      <div class="flex space-x-2">
        <input
          v-model="currentMessage"
          @keyup.enter="sendMessage"
          @keydown.ctrl.enter="sendMessage"
          type="text"
          placeholder="Type your message... (Enter to send, Ctrl+Enter for new line)"
          class="flex-1 border border-gray-300 rounded-lg px-4 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          :disabled="loading"
        />
        <button
          @click="sendMessage"
          :disabled="loading || !currentMessage.trim()"
          class="px-6 py-2 bg-blue-600 text-white rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
        >
          Send
        </button>
      </div>
      
      <!-- Quick actions -->
      <div class="flex space-x-2 mt-2">
        <button
          v-for="quick in quickActions"
          :key="quick.text"
          @click="sendQuickMessage(quick.text)"
          class="px-3 py-1 bg-gray-100 text-gray-700 rounded text-sm hover:bg-gray-200 transition-colors"
        >
          {{ quick.label }}
        </button>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, nextTick, watch } from 'vue'

export default {
  name: 'ChatInterface',
  props: {
    messages: {
      type: Array,
      default: () => []
    },
    loading: {
      type: Boolean,
      default: false
    }
  },
  emits: ['send-message', 'clear-chat'],
  setup(props, { emit }) {
    const currentMessage = ref('')
    const voiceEnabled = ref(false)
    const messagesContainer = ref(null)

    const quickActions = [
      { label: 'Help', text: 'What can you help me with?' },
      { label: 'Status', text: 'What is your current status?' },
      { label: 'Capabilities', text: 'What are your capabilities?' },
      { label: 'Privacy', text: 'How do you protect my privacy?' }
    ]

    const sendMessage = () => {
      if (currentMessage.value.trim() && !props.loading) {
        emit('send-message', currentMessage.value.trim())
        currentMessage.value = ''
      }
    }

    const sendQuickMessage = (message) => {
      if (!props.loading) {
        emit('send-message', message)
      }
    }

    const toggleVoiceResponse = () => {
      voiceEnabled.value = !voiceEnabled.value
    }

    const formatTime = (date) => {
      return new Intl.DateTimeFormat('en-US', {
        hour: '2-digit',
        minute: '2-digit',
        second: '2-digit'
      }).format(date)
    }

    const scrollToBottom = () => {
      nextTick(() => {
        if (messagesContainer.value) {
          messagesContainer.value.scrollTop = messagesContainer.value.scrollHeight
        }
      })
    }

    // Auto-scroll when new messages arrive
    watch(() => props.messages.length, scrollToBottom)

    return {
      currentMessage,
      voiceEnabled,
      messagesContainer,
      quickActions,
      sendMessage,
      sendQuickMessage,
      toggleVoiceResponse,
      formatTime
    }
  }
}
</script>

Document Processor Component

<!-- components/Document/DocumentProcessor.vue -->
<template>
  <div class="flex flex-col h-full">
    <!-- Header -->
    <div class="bg-white border-b px-6 py-4">
      <h2 class="text-lg font-semibold text-gray-800">Document Processing</h2>
      <p class="text-sm text-gray-600 mt-1">Upload images and documents for OCR and AI analysis</p>
    </div>

    <div class="flex-1 p-6 overflow-y-auto">
      <!-- Upload Area -->
      <div
        @drop="handleDrop"
        @dragover.prevent
        @dragenter.prevent
        :class="[
          'border-2 border-dashed rounded-lg p-8 text-center transition-colors',
          isDragging ? 'border-blue-500 bg-blue-50' : 'border-gray-300'
        ]"
      >
        <svg class="mx-auto h-12 w-12 text-gray-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
          <path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
        </svg>
        <p class="mt-2 text-sm text-gray-600">
          <span class="font-medium">Drop files here</span> or 
          <label class="text-blue-600 hover:text-blue-500 cursor-pointer">
            <span>browse</span>
            <input
              ref="fileInput"
              type="file"
              class="sr-only"
              accept="image/*,.pdf"
              multiple
              @change="handleFileSelect"
            />
          </label>
        </p>
        <p class="text-xs text-gray-500 mt-1">Supports: JPG, PNG, PDF (up to 10MB)</p>
      </div>

      <!-- Analysis Options -->
      <div class="mt-6 bg-white rounded-lg border p-4">
        <h3 class="text-sm font-medium text-gray-800 mb-3">Analysis Options</h3>
        
        <div class="space-y-3">
          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">Analysis Type</label>
            <select 
              v-model="analysisType" 
              class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            >
              <option value="extract">Extract Text Only</option>
              <option value="analyze">Full Analysis</option>
              <option value="qa">Question & Answer</option>
            </select>
          </div>

          <div>
            <label class="block text-sm font-medium text-gray-700 mb-1">Enhancement Type</label>
            <select 
              v-model="enhancementType" 
              class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            >
              <option value="default">Default</option>
              <option value="scan">Scanned Document</option>
              <option value="photo">Photo of Document</option>
              <option value="screenshot">Screenshot</option>
            </select>
          </div>

          <div v-if="analysisType === 'qa'">
            <label class="block text-sm font-medium text-gray-700 mb-1">Question</label>
            <input
              v-model="question"
              type="text"
              placeholder="What would you like to know about this document?"
              class="w-full border border-gray-300 rounded-md px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent"
            />
          </div>
        </div>
      </div>

      <!-- File Queue -->
      <div v-if="fileQueue.length > 0" class="mt-6">
        <h3 class="text-sm font-medium text-gray-800 mb-3">Files to Process</h3>
        <div class="space-y-2">
          <div
            v-for="(file, index) in fileQueue"
            :key="index"
            class="flex items-center justify-between bg-gray-50 rounded-lg p-3"
          >
            <div class="flex items-center space-x-3">
              <div class="flex-shrink-0">
                <svg class="h-8 w-8 text-gray-400" fill="currentColor" viewBox="0 0 20 20">
                  <path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z" clip-rule="evenodd"/>
                </svg>
              </div>
              <div>
                <p class="text-sm font-medium text-gray-900">{{ file.name }}</p>
                <p class="text-xs text-gray-500">{{ formatFileSize(file.size) }}</p>
              </div>
            </div>
            
            <div class="flex items-center space-x-2">
              <span
                v-if="file.status === 'processing'"
                class="text-xs text-blue-600 bg-blue-100 px-2 py-1 rounded"
              >
                Processing...
              </span>
              <span
                v-else-if="file.status === 'completed'"
                class="text-xs text-green-600 bg-green-100 px-2 py-1 rounded"
              >
                Completed
              </span>
              <span
                v-else-if="file.status === 'error'"
                class="text-xs text-red-600 bg-red-100 px-2 py-1 rounded"
              >
                Error
              </span>
              
              <button
                @click="removeFile(index)"
                class="text-gray-400 hover:text-red-500 transition-colors"
              >
                <svg class="h-4 w-4" fill="currentColor" viewBox="0 0 20 20">
                  <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
                </svg>
              </button>
            </div>
          </div>
        </div>

        <!-- Process Button -->
        <div class="mt-4">
          <button
            @click="processFiles"
            :disabled="processing || fileQueue.every(f => f.status === 'completed')"
            class="w-full bg-blue-600 text-white py-2 px-4 rounded-lg hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
          >
            {{ processing ? 'Processing...' : 'Process Documents' }}
          </button>
        </div>
      </div>

      <!-- Results -->
      <div v-if="results.length > 0" class="mt-6">
        <h3 class="text-sm font-medium text-gray-800 mb-3">Processing Results</h3>
        <div class="space-y-4">
          <div
            v-for="(result, index) in results"
            :key="index"
            class="bg-white border rounded-lg p-4"
          >
            <div class="flex justify-between items-start mb-3">
              <h4 class="font-medium text-gray-900">{{ result.filename }}</h4>
              <div class="flex space-x-2 text-xs">
                <span class="bg-blue-100 text-blue-800 px-2 py-1 rounded">
                  {{ result.confidence.toFixed(1) }}% confidence
                </span>
                <span class="bg-gray-100 text-gray-800 px-2 py-1 rounded">
                  {{ result.word_count }} words
                </span>
              </div>
            </div>

            <!-- Extracted Text -->
            <div v-if="result.text" class="mb-3">
              <h5 class="text-sm font-medium text-gray-700 mb-1">Extracted Text:</h5>
              <div class="bg-gray-50 rounded p-3 text-sm max-h-32 overflow-y-auto">
                {{ result.text }}
              </div>
            </div>

            <!-- Analysis -->
            <div v-if="result.analysis" class="mb-3">
              <h5 class="text-sm font-medium text-gray-700 mb-1">Analysis:</h5>
              <div class="bg-blue-50 rounded p-3 text-sm max-h-32 overflow-y-auto">
                {{ result.analysis }}
              </div>
            </div>

            <!-- Actions -->
            <div class="flex space-x-2">
              <button
                @click="copyToClipboard(result.text || result.analysis)"
                class="text-xs bg-gray-100 text-gray-700 px-3 py-1 rounded hover:bg-gray-200 transition-colors"
              >
                Copy Text
              </button>
              <button
                @click="addToKnowledge(result)"
                class="text-xs bg-green-100 text-green-700 px-3 py-1 rounded hover:bg-green-200 transition-colors"
              >
                Add to Knowledge Base
              </button>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref } from 'vue'

export default {
  name: 'DocumentProcessor',
  emits: ['process-document'],
  setup(props, { emit }) {
    const fileInput = ref(null)
    const isDragging = ref(false)
    const fileQueue = ref([])
    const processing = ref(false)
    const results = ref([])
    
    // Options
    const analysisType = ref('analyze')
    const enhancementType = ref('default')
    const question = ref('')

    const handleDrop = (e) => {
      e.preventDefault()
      isDragging.value = false
      
      const files = Array.from(e.dataTransfer.files)
      addFiles(files)
    }

    const handleFileSelect = (e) => {
      const files = Array.from(e.target.files)
      addFiles(files)
      e.target.value = '' // Reset input
    }

    const addFiles = (files) => {
      const validFiles = files.filter(file => {
        const isValidType = file.type.startsWith('image/') || file.type === 'application/pdf'
        const isValidSize = file.size <= 10 * 1024 * 1024 // 10MB
        return isValidType && isValidSize
      })

      validFiles.forEach(file => {
        fileQueue.value.push({
          file,
          name: file.name,
          size: file.size,
          status: 'pending'
        })
      })
    }

    const removeFile = (index) => {
      fileQueue.value.splice(index, 1)
    }

    const processFiles = async () => {
      if (processing.value) return

      processing.value = true
      
      try {
        for (let i = 0; i < fileQueue.value.length; i++) {
          const fileItem = fileQueue.value[i]
          
          if (fileItem.status === 'completed') continue
          
          fileItem.status = 'processing'
          
          try {
            const result = await emit('process-document', 
              fileItem.file, 
              analysisType.value, 
              question.value
            )
            
            results.value.push({
              filename: fileItem.name,
              ...result
            })
            
            fileItem.status = 'completed'
            
          } catch (error) {
            console.error('Processing failed:', error)
            fileItem.status = 'error'
          }
        }
      } finally {
        processing.value = false
      }
    }

    const formatFileSize = (bytes) => {
      if (bytes === 0) return '0 Bytes'
      const k = 1024
      const sizes = ['Bytes', 'KB', 'MB', 'GB']
      const i = Math.floor(Math.log(bytes) / Math.log(k))
      return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]
    }

    const copyToClipboard = async (text) => {
      try {
        await navigator.clipboard.writeText(text)
        // Could add notification here
      } catch (error) {
        console.error('Failed to copy:', error)
      }
    }

    const addToKnowledge = (result) => {
      // Could emit event to parent to add to knowledge base
      emit('add-to-knowledge', {
        content: result.text || result.analysis,
        metadata: {
          source: 'document_processing',
          filename: result.filename,
          confidence: result.confidence
        }
      })
    }

    return {
      fileInput,
      isDragging,
      fileQueue,
      processing,
      results,
      analysisType,
      enhancementType,
      question,
      handleDrop,
      handleFileSelect,
      removeFile,
      processFiles,
      formatFileSize,
      copyToClipboard,
      addToKnowledge
    }
  }
}
</script>

Voice Interface Component

<!-- components/Voice/VoiceInterface.vue -->
<template>
  <div class="flex flex-col h-full">
    <!-- Header -->
    <div class="bg-white border-b px-6 py-4">
      <h2 class="text-lg font-semibold text-gray-800">Voice Interaction</h2>
      <p class="text-sm text-gray-600 mt-1">Talk to AlexAI using voice commands</p>
    </div>

    <div class="flex-1 p-6">
      <!-- Voice Status -->
      <div class="text-center mb-8">
        <div 
          :class="[
            'w-32 h-32 rounded-full mx-auto mb-4 flex items-center justify-center transition-all duration-300',
            isListening ? 'bg-red-100 border-4 border-red-300 animate-pulse' :
            isSpeaking ? 'bg-blue-100 border-4 border-blue-300 animate-pulse' :
            'bg-gray-100 border-4 border-gray-300'
          ]"
        >
          <svg 
            :class="[
              'w-16 h-16 transition-colors',
              isListening ? 'text-red-600' :
              isSpeaking ? 'text-blue-600' :
              'text-gray-600'
            ]" 
            fill="currentColor" 
            viewBox="0 0 20 20"
          >
            <path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd"/>
          </svg>
        </div>

        <p class="text-lg font-medium text-gray-900 mb-2">
          {{ statusText }}
        </p>
        <p class="text-sm text-gray-600">
          {{ statusDescription }}
        </p>
      </div>

      <!-- Voice Controls -->
      <div class="max-w-md mx-auto space-y-4">
        <!-- Quick Voice Actions -->
        <div class="grid grid-cols-2 gap-4">
          <button
            @click="startListening"
            :disabled="isListening || isSpeaking"
            class="flex flex-col items-center p-4 border-2 border-dashed border-gray-300 rounded-lg hover:border-blue-500 hover:bg-blue-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
          >
            <svg class="w-8 h-8 text-gray-600 mb-2" fill="currentColor" viewBox="0 0 20 20">
              <path fill-rule="evenodd" d="M7 4a3 3 0 016 0v4a3 3 0 11-6 0V4zm4 10.93A7.001 7.001 0 0017 8a1 1 0 10-2 0A5 5 0 015 8a1 1 0 00-2 0 7.001 7.001 0 006 6.93V17H6a1 1 0 100 2h8a1 1 0 100-2h-3v-2.07z" clip-rule="evenodd"/>
            </svg>
            <span class="text-sm font-medium">Start Conversation</span>
          </button>

          <button
            @click="showTextToSpeech = true"
            :disabled="isListening || isSpeaking"
            class="flex flex-col items-center p-4 border-2 border-dashed border-gray-300 rounded-lg hover:border-green-500 hover:bg-green-50 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
          >
            <svg class="w-8 h-8 text-gray-600 mb-2" fill="currentColor" viewBox="0 0 20 20">
              <path fill-rule="evenodd" d="M9.383 3.076A1 1 0 0110 4v12a1 1 0 01-1.617.768L4.383 13H2a1 1 0 01-1-1V8a1 1 0 011-1h2.383l4-3.232zm7.617 2.924a1 1 0 01.293.707v6.586a1 1 0 01-1.707.707L14 12.414V7.586l1.586-1.586a1 1 0 011.414 0z" clip-rule="evenodd"/>
            </svg>
            <span class="text-sm font-medium">Text to Speech</span>
          </button>
        </div>

        <!-- Stop Button (when active) -->
        <button
          v-if="isListening"
          @click="stopListening"
          class="w-full bg-red-600 text-white py-3 px-6 rounded-lg hover:bg-red-700 transition-colors flex items-center justify-center"
        >
          <svg class="w-5 h-5 mr-2" fill="currentColor" viewBox="0 0 20 20">
            <path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8 7a1 1 0 00-1 1v4a1 1 0 001 1h4a1 1 0 001-1V8a1 1 0 00-1-1H8z" clip-rule="evenodd"/>
          </svg>
          Stop Listening
        </button>

        <!-- Settings -->
        <div class="bg-gray-50 rounded-lg p-4">
          <h3 class="text-sm font-medium text-gray-800 mb-3">Voice Settings</h3>
          
          <div class="space-y-3">
            <div class="flex items-center justify-between">
              <label class="text-sm text-gray-700">Wake Word Detection</label>
              <input
                v-model="wakeWordEnabled"
                type="checkbox"
                class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
              />
            </div>
            
            <div class="flex items-center justify-between">
              <label class="text-sm text-gray-700">Auto Response</label>
              <input
                v-model="autoResponse"
                type="checkbox"
                class="rounded border-gray-300 text-blue-600 focus:ring-blue-500"
              />
            </div>

            <div>
              <label class="block text-sm text-gray-700 mb-1">Timeout (seconds)</label>
              <input
                v-model.number="timeout"
                type="range"
                min="10"
                max="60"
                class="w-full"
              />
              <div class="text-xs text-gray-500 text-center">{{ timeout }}s</div>
            </div>
          </div>
        </div>

        <!-- Voice History -->
        <div v-if="voiceHistory.length > 0" class="bg-white border rounded-lg p-4">
          <h3 class="text-sm font-medium text-gray-800 mb-3">Recent Voice Interactions</h3>
          <div class="space-y-2 max-h-32 overflow-y-auto">
            <div
              v-for="(interaction, index) in voiceHistory.slice(-5)"
              :key="index"
              class="text-xs p-2 bg-gray-50 rounded"
            >
              <div class="font-medium text-gray-900">{{ interaction.timestamp }}</div>
              <div class="text-gray-600">{{ interaction.text }}</div>
            </div>
          </div>
        </div>
      </div>

      <!-- Text to Speech Modal -->
      <div 
        v-if="showTextToSpeech"
        class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50"
        @click="showTextToSpeech = false"
      >
        <div 
          class="bg-white rounded-lg p-6 max-w-md w-full mx-4"
          @click.stop
        >
          <h3 class="text-lg font-medium text-gray-900 mb-4">Text to Speech</h3>
          
          <textarea
            v-model="textToSpeak"
            placeholder="Enter text to speak..."
            class="w-full border border-gray-300 rounded-md px-3 py-2 h-24 resize-none focus:ring-2 focus:ring-blue-500 focus:border-transparent"
          ></textarea>

          <div class="flex justify-end space-x-3 mt-4">
            <button
              @click="showTextToSpeech = false"
              class="px-4 py-2 text-gray-700 border border-gray-300 rounded-md hover:bg-gray-50 transition-colors"
            >
              Cancel
            </button>
            <button
              @click="speakText"
              :disabled="!textToSpeak.trim() || isSpeaking"
              class="px-4 py-2 bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50 disabled:cursor-not-allowed transition-colors"
            >
              {{ isSpeaking ? 'Speaking...' : 'Speak' }}
            </button>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, computed } from 'vue'

export default {
  name: 'VoiceInterface',
  props: {
    isListening: {
      type: Boolean,
      default: false
    },
    isSpeaking: {
      type: Boolean,
      default: false
    }
  },
  emits: ['start-listening', 'stop-listening', 'text-to-speech'],
  setup(props, { emit }) {
    const showTextToSpeech = ref(false)
    const textToSpeak = ref('')
    const wakeWordEnabled = ref(true)
    const autoResponse = ref(true)
    const timeout = ref(30)
    const voiceHistory = ref([])

    const statusText = computed(() => {
      if (props.isListening) return 'Listening...'
      if (props.isSpeaking) return 'Speaking...'
      return 'Ready'
    })

    const statusDescription = computed(() => {
      if (props.isListening) return 'Say something to AlexAI'
      if (props.isSpeaking) return 'AlexAI is responding'
      return 'Click to start voice interaction'
    })

    const startListening = () => {
      emit('start-listening')
      addToHistory('Voice interaction started')
    }

    const stopListening = () => {
      emit('stop-listening')
      addToHistory('Voice interaction stopped')
    }

    const speakText = () => {
      if (textToSpeak.value.trim()) {
        emit('text-to-speech', textToSpeak.value.trim())
        addToHistory(`TTS: ${textToSpeak.value.substring(0, 50)}...`)
        textToSpeak.value = ''
        showTextToSpeech.value = false
      }
    }

    const addToHistory = (text) => {
      voiceHistory.value.push({
        timestamp: new Date().toLocaleTimeString(),
        text: text
      })
      
      // Keep only last 10 entries
      if (voiceHistory.value.length > 10) {
        voiceHistory.value = voiceHistory.value.slice(-10)
      }
    }

    return {
      showTextToSpeech,
      textToSpeak,
      wakeWordEnabled,
      autoResponse,
      timeout,
      voiceHistory,
      statusText,
      statusDescription,
      startListening,
      stopListening,
      speakText
    }
  }
}
</script>

Notifications Composable

// composables/useNotifications.js
import { ref } from 'vue'

export function useNotifications() {
  const notifications = ref([])
  let notificationId = 0

  const addNotification = (message, type = 'info', duration = 5000) => {
    const id = ++notificationId
    
    notifications.value.push({
      id,
      message,
      type,
      timestamp: new Date()
    })

    // Auto remove after duration
    if (duration > 0) {
      setTimeout(() => {
        removeNotification(id)
      }, duration)
    }

    return id
  }

  const removeNotification = (id) => {
    const index = notifications.value.findIndex(n => n.id === id)
    if (index > -1) {
      notifications.value.splice(index, 1)
    }
  }

  const clearNotifications = () => {
    notifications.value = []
  }

  return {
    notifications,
    addNotification,
    removeNotification,
    clearNotifications
  }
}

Project Setup Files

// package.json
{
  "name": "alexai-frontend",
  "version": "1.0.0",
  "scripts": {
    "dev": "vite",
    "build": "vite build",
    "preview": "vite preview",
    "lint": "eslint src --ext .vue,.js,.ts",
    "format": "prettier --write src/"
  },
  "dependencies": {
    "vue": "^3.4.0",
    "@vue/composition-api": "^1.7.2",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0"
  },
  "devDependencies": {
    "@vitejs/plugin-vue": "^4.5.0",
    "vite": "^5.0.0",
    "tailwindcss": "^3.3.0",
    "autoprefixer": "^10.4.0",
    "postcss": "^8.4.0",
    "eslint": "^8.0.0",
    "prettier": "^3.0.0"
  }
}
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import path from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': path.resolve(__dirname, 'src')
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8000',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  build: {
    outDir: 'dist',
    sourcemap: true
  }
})
// tailwind.config.js
module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {
      fontFamily: {
        sans: ['Inter', 'system-ui', 'sans-serif'],
      },
      colors: {
        primary: {
          50: '#eff6ff',
          500: '#3b82f6',
          600: '#2563eb',
          700: '#1d4ed8',
        }
      }
    },
  },
  plugins: [],
}## Part 7: MCP (Model Context Protocol) Integration

### Why MCP?

The Model Context Protocol standardizes how AI models interact with external tools and data sources:
- **Standardized interface**: Consistent way to connect tools across different AI systems
- **Security**: Controlled access to external resources
- **Extensibility**: Easy addition of new capabilities
- **Interoperability**: Tools work across different AI platforms

### MCP Server Implementation

```python
# mcp/mcp_server.py
from fastapi import FastAPI, HTTPException, Depends
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
from pydantic import BaseModel, Field
from typing import List, Dict, Any, Optional, Union
import asyncio
import logging
from datetime import datetime
import json

# MCP Protocol Models
class MCPResource(BaseModel):
    uri: str
    name: str
    description: Optional[str] = None
    mime_type: Optional[str] = None

class MCPTool(BaseModel):
    name: str
    description: str
    input_schema: Dict[str, Any]

class MCPPrompt(BaseModel):
    name: str
    description: str
    arguments: Optional[List[Dict[str, Any]]] = None

class MCPRequest(BaseModel):
    method: str
    params: Optional[Dict[str, Any]] = None

class MCPResponse(BaseModel):
    result: Optional[Any] = None
    error: Optional[Dict[str, Any]] = None

class MCPServer:
    def __init__(self):
        self.app = FastAPI(title="AlexAI MCP Server", version="1.0.0")
        self.security = HTTPBearer()
        
        # Register available tools and resources
        self.tools = {}
        self.resources = {}
        self.prompts = {}
        
        # Setup routes
        self._setup_routes()
        
        # Initialize integrations
        self._setup_integrations()
    
    def _setup_integrations(self):
        """Initialize all system integrations"""
        from models.ollama_manager import OllamaManager
        from rag.vector_store import VectorStore, RAGGenerator
        from memory.memory_manager import MemoryManager
        from voice.speech_processor import VoiceInterface
        from vision.ocr_processor import VisionLanguageProcessor
        
        self.ollama_manager = OllamaManager()
        self.vector_store = VectorStore()
        self.rag_generator = RAGGenerator()
        self.memory_manager = MemoryManager()
        self.voice_interface = VoiceInterface()
        self.vision_processor = VisionLanguageProcessor(self.ollama_manager)
        
        # Register tools
        self._register_tools()
    
    def _register_tools(self):
        """Register all available tools"""
        
        # Chat tool
        self.tools['chat'] = MCPTool(
            name="chat",
            description="Have a conversation with the AI assistant",
            input_schema={
                "type": "object",
                "properties": {
                    "message": {"type": "string", "description": "User message"},
                    "session_id": {"type": "string", "description": "Session identifier"},
                    "use_memory": {"type": "boolean", "default": True},
                    "use_rag": {"type": "boolean", "default": True}
                },
                "required": ["message"]
            }
        )
        
        # Document processing tool
        self.tools['process_document'] = MCPTool(
            name="process_document",
            description="Process and analyze documents using OCR and LLM",
            input_schema={
                "type": "object",
                "properties": {
                    "file_path": {"type": "string", "description": "Path to document image"},
                    "analysis_type": {"type": "string", "enum": ["extract", "analyze", "qa"], "default": "analyze"},
                    "question": {"type": "string", "description": "Question for QA mode"}
                },
                "required": ["file_path"]
            }
        )
        
        # Voice interaction tool
        self.tools['voice_chat'] = MCPTool(
            name="voice_chat",
            description="Voice-based conversation with the assistant",
            input_schema={
                "type": "object",
                "properties": {
                    "mode": {"type": "string", "enum": ["listen", "speak", "conversation"], "default": "conversation"},
                    "text": {"type": "string", "description": "Text to speak (for speak mode)"},
                    "timeout": {"type": "number", "default": 30}
                }
            }
        )
        
        # Memory management tool
        self.tools['manage_memory'] = MCPTool(
            name="manage_memory",
            description="Manage conversation memory and user preferences",
            input_schema={
                "type": "object",
                "properties": {
                    "action": {"type": "string", "enum": ["get_context", "get_preferences", "learn_preference"]},
                    "session_id": {"type": "string"},
                    "category": {"type": "string"},
                    "key": {"type": "string"},
                    "value": {"type": "string"},
                    "confidence": {"type": "number", "default": 0.5}
                },
                "required": ["action"]
            }
        )
        
        # Knowledge base tool
        self.tools['knowledge_base'] = MCPTool(
            name="knowledge_base",
            description="Search and manage knowledge base",
            input_schema={
                "type": "object",
                "properties": {
                    "action": {"type": "string", "enum": ["search", "add", "update", "delete"]},
                    "query": {"type": "string", "description": "Search query or content"},
                    "collection": {"type": "string", "default": "documents"},
                    "metadata": {"type": "object", "description": "Document metadata"}
                },
                "required": ["action"]
            }
        )
    
    def _setup_routes(self):
        """Setup FastAPI routes for MCP protocol"""
        
        @self.app.get("/")
        async def root():
            return {"service": "AlexAI MCP Server", "version": "1.0.0", "status": "running"}
        
        @self.app.post("/mcp/initialize")
        async def initialize(credentials: HTTPAuthorizationCredentials = Depends(self.security)):
            """Initialize MCP session"""
            return MCPResponse(result={
                "protocol_version": "1.0",
                "server_info": {
                    "name": "AlexAI",
                    "version": "1.0.0"
                },
                "capabilities": {
                    "tools": True,
                    "resources": True,
                    "prompts": True
                }
            })
        
        @self.app.get("/mcp/tools")
        async def list_tools():
            """List available tools"""
            return MCPResponse(result={"tools": list(self.tools.values())})
        
        @self.app.post("/mcp/tools/call")
        async def call_tool(request: MCPRequest):
            """Call a specific tool"""
            tool_name = request.params.get("name")
            arguments = request.params.get("arguments", {})
            
            if tool_name not in self.tools:
                return MCPResponse(error={"code": -32601, "message": f"Tool {tool_name} not found"})
            
            try:
                result = await self._execute_tool(tool_name, arguments)
                return MCPResponse(result=result)
            except Exception as e:
                logging.error(f"Tool execution failed: {e}")
                return MCPResponse(error={"code": -32603, "message": str(e)})
        
        @self.app.get("/mcp/resources")
        async def list_resources():
            """List available resources"""
            return MCPResponse(result={"resources": list(self.resources.values())})
        
        @self.app.post("/mcp/resources/read")
        async def read_resource(request: MCPRequest):
            """Read a specific resource"""
            uri = request.params.get("uri")
            
            try:
                content = await self._read_resource(uri)
                return MCPResponse(result={"contents": [{"uri": uri, "text": content}]})
            except Exception as e:
                return MCPResponse(error={"code": -32603, "message": str(e)})
    
    async def _execute_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Any:
        """Execute a tool with given arguments"""
        
        if tool_name == "chat":
            return await self._handle_chat(arguments)
        elif tool_name == "process_document":
            return await self._handle_document_processing(arguments)
        elif tool_name == "voice_chat":
            return await self._handle_voice_chat(arguments)
        elif tool_name == "manage_memory":
            return await self._handle_memory_management(arguments)
        elif tool_name == "knowledge_base":
            return await self._handle_knowledge_base(arguments)
        else:
            raise ValueError(f"Unknown tool: {tool_name}")
    
    async def _handle_chat(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Handle chat tool execution"""
        message = args["message"]
        session_id = args.get("session_id", "default")
        use_memory = args.get("use_memory", True)
        use_rag = args.get("use_rag", True)
        
        # Get conversation context if memory is enabled
        context = ""
        if use_memory:
            conversations = await self.memory_manager.get_conversation_context(
                session_id=session_id,
                query=message
            )
            if conversations:
                context_parts = []
                for conv in conversations[-3:]:  # Last 3 relevant conversations
                    context_parts.append(f"User: {conv['user_message']}")
                    context_parts.append(f"Assistant: {conv['assistant_response']}")
                context = "\n".join(context_parts)
        
        # Generate response
        if use_rag:
            # Use RAG for enhanced responses
            response = await self.rag_generator.generate_with_context(
                query=message,
                system_prompt=f"Previous conversation context:\n{context}" if context else None
            )
        else:
            # Direct LLM response
            prompt = f"{context}\n\nUser: {message}" if context else message
            response = await self.ollama_manager.generate_response(
                prompt=prompt,
                system_prompt="You are AlexAI, a helpful personal assistant."
            )
        
        # Store conversation in memory
        if use_memory:
            await self.memory_manager.store_conversation(
                session_id=session_id,
                user_message=message,
                assistant_response=response
            )
        
        return {
            "response": response,
            "session_id": session_id,
            "used_memory": use_memory,
            "used_rag": use_rag
        }
    
    async def _handle_document_processing(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Handle document processing tool"""
        file_path = args["file_path"]
        analysis_type = args.get("analysis_type", "analyze")
        question = args.get("question")
        
        if analysis_type == "extract":
            # Simple OCR extraction
            result = self.vision_processor.ocr.extract_text(file_path)
            return {
                "text": result["text"],
                "confidence": result.get("confidence", 0),
                "word_count": result.get("word_count", 0)
            }
        
        elif analysis_type == "analyze":
            # Full document analysis
            analysis = await self.vision_processor.analyze_document(file_path)
            return analysis
        
        elif analysis_type == "qa":
            if not question:
                raise ValueError("Question required for QA mode")
            
            answer = await self.vision_processor.answer_document_questions(file_path, question)
            return {
                "question": question,
                "answer": answer
            }
        
        else:
            raise ValueError(f"Unknown analysis type: {analysis_type}")
    
    async def _handle_voice_chat(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Handle voice chat tool"""
        mode = args.get("mode", "conversation")
        text = args.get("text")
        timeout = args.get("timeout", 30)
        
        if mode == "speak":
            if not text:
                raise ValueError("Text required for speak mode")
            
            await self.voice_interface.tts.speak_async(text)
            return {"status": "spoken", "text": text}
        
        elif mode == "listen":
            # Listen for wake word
            detected = await self.voice_interface.listen_for_wake_word(timeout)
            return {"wake_word_detected": detected}
        
        elif mode == "conversation":
            # Full voice conversation
            async def chat_callback(user_text: str) -> str:
                # Use chat tool to generate response
                chat_result = await self._handle_chat({
                    "message": user_text,
                    "session_id": "voice_session",
                    "use_memory": True,
                    "use_rag": True
                })
                return chat_result["response"]
            
            response = await self.voice_interface.voice_conversation(chat_callback)
            return {"response": response}
        
        else:
            raise ValueError(f"Unknown voice mode: {mode}")
    
    async def _handle_memory_management(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Handle memory management tool"""
        action = args["action"]
        
        if action == "get_context":
            session_id = args.get("session_id", "default")
            context = await self.memory_manager.get_conversation_context(session_id)
            return {"context": context}
        
        elif action == "get_preferences":
            category = args.get("category")
            preferences = self.memory_manager.get_user_preferences(category)
            return {"preferences": preferences}
        
        elif action == "learn_preference":
            category = args["category"]
            key = args["key"]
            value = args["value"]
            confidence = args.get("confidence", 0.5)
            
            await self.memory_manager.learn_preference(category, key, value, confidence)
            return {"status": "preference_learned"}
        
        else:
            raise ValueError(f"Unknown memory action: {action}")
    
    async def _handle_knowledge_base(self, args: Dict[str, Any]) -> Dict[str, Any]:
        """Handle knowledge base tool"""
        action = args["action"]
        
        if action == "search":
            query = args["query"]
            collection = args.get("collection", "documents")
            
            results = await self.vector_store.similarity_search(
                query=query,
                collection_name=collection
            )
            return {"results": results}
        
        elif action == "add":
            content = args["query"]  # Using query field for content
            collection = args.get("collection", "documents")
            metadata = args.get("metadata", {})
            
            doc_id = await self.vector_store.add_document(
                content=content,
                metadata=metadata,
                collection_name=collection
            )
            return {"document_id": doc_id}
        
        else:
            raise ValueError(f"Unknown knowledge base action: {action}")
    
    async def _read_resource(self, uri: str) -> str:
        """Read content from a resource URI"""
        # Implement resource reading logic based on URI scheme
        if uri.startswith("file://"):
            file_path = uri[7:]  # Remove file:// prefix
            with open(file_path, 'r', encoding='utf-8') as f:
                return f.read()
        elif uri.startswith("memory://"):
            # Read from memory system
            # Implementation depends on specific memory resource format
            return "Memory resource content"
        else:
            raise ValueError(f"Unsupported resource URI: {uri}")

# MCP Client for connecting to other services
class MCPClient:
    def __init__(self, server_url: str, auth_token: Optional[str] = None):
        self.server_url = server_url
        self.auth_token = auth_token
        self.session = None
    
    async def initialize(self) -> Dict[str, Any]:
        """Initialize connection with MCP server"""
        import aiohttp
        
        headers = {}
        if self.auth_token:
            headers["Authorization"] = f"Bearer {self.auth_token}"
        
        async with aiohttp.ClientSession() as session:
            async with session.post(
                f"{self.server_url}/mcp/initialize",
                headers=headers
            ) as response:
                result = await response.json()
                return result
    
    async def call_tool(self, tool_name: str, arguments: Dict[str, Any]) -> Dict[str, Any]:
        """Call a tool on the MCP server"""
        import aiohttp
        
        request_data = {
            "method": "tools/call",
            "params": {
                "name": tool_name,
                "arguments": arguments
            }
        }
        
        headers = {}
        if self.auth_token:
            headers["Authorization"] = f"Bearer {self.auth_token}"
        
        async with aiohttp.ClientSession() as session:
            async with session.post(
                f"{self.server_url}/mcp/tools/call",
                json=request_data,
                headers=headers
            ) as response:
                result = await response.json()
                return result
    
    async def list_tools(self) -> List[Dict[str, Any]]:
        """List available tools"""
        import aiohttp
        
        async with aiohttp.ClientSession() as session:
            async with session.get(f"{self.server_url}/mcp/tools") as response:
                result = await response.json()
                return result.get("result", {}).get("tools", [])

# Usage example
async def demo_mcp_server():
    """Demonstrate MCP server functionality"""
    
    # Start MCP server
    server = MCPServer()
    
    # Test chat tool
    chat_result = await server._execute_tool("chat", {
        "message": "Hello, how are you?",
        "session_id": "demo_session"
    })
    print("Chat result:", chat_result)
    
    # Test knowledge base
    kb_result = await server._execute_tool("knowledge_base", {
        "action": "add",
        "query": "AlexAI is a privacy-focused personal assistant that runs locally.",
        "metadata": {"category": "system_info"}
    })
    print("Knowledge base result:", kb_result)
    
    # Search knowledge base
    search_result = await server._execute_tool("knowledge_base", {
        "action": "search",
        "query": "privacy-focused assistant"
    })
    print("Search result:", search_result)

if __name__ == "__main__":
    import uvicorn
    
    # Create and run MCP server
    server = MCPServer()
    
    # Run with uvicorn
    uvicorn.run(
        server.app,
        host="0.0.0.0",
        port=8000,
        log_level="info"
    )

Deployment and Build Configuration

<!-- public/index.html -->
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta name="description" content="AlexAI - Privacy-focused AI Personal Assistant" />
    <title>AlexAI Assistant</title>
    
    <!-- PWA manifest -->
    <link rel="manifest" href="/manifest.json" />
    <meta name="theme-color" content="#2563eb" />
    
    <!-- Apple touch icon -->
    <link rel="apple-touch-icon" href="/apple-touch-icon.png" />
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
// public/manifest.json
{
  "name": "AlexAI Assistant",
  "short_name": "AlexAI",
  "description": "Privacy-focused AI Personal Assistant",
  "icons": [
    {
      "src": "/icon-192.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    {
      "src": "/icon-512.png",
      "sizes": "512x512",
      "type": "image/png"
    }
  ],
  "start_url": "/",
  "display": "standalone",
  "background_color": "#ffffff",
  "theme_color": "#2563eb",
  "categories": ["productivity", "utilities"]
}

Production Build Script

#!/bin/bash
# build.sh - Production build script

echo "🚀 Building AlexAI Frontend..."

# Install dependencies
echo "📦 Installing dependencies..."
npm ci

# Run linting
echo "🔍 Running linter..."
npm run lint

# Build for production
echo "🏗️ Building for production..."
npm run build

# Copy to backend static directory
echo "📁 Copying to backend..."
if [ -d "../backend/static" ]; then
    rm -rf ../backend/static/*
    cp -r dist/* ../backend/static/
    echo "✅ Static files copied to backend"
else
    echo "⚠️ Backend static directory not found"
fi

echo "✨ Build complete!"

Docker Configuration for Frontend

# Dockerfile.frontend
FROM node:18-alpine as build

WORKDIR /app

# Copy package files
COPY package*.json ./

# Install dependencies
RUN npm ci --only=production

# Copy source code
COPY . .

# Build application
RUN npm run build

# Production stage
FROM nginx:alpine

# Copy built files
COPY --from=build /app/dist /usr/share/nginx/html

# Copy nginx configuration
COPY nginx.conf /etc/nginx/nginx.conf

# Expose port
EXPOSE 80

# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
    CMD curl -f http://localhost/ || exit 1

CMD ["nginx", "-g", "daemon off;"]
# nginx.conf
events {
    worker_connections 1024;
}

http {
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;

    # Gzip compression
    gzip on;
    gzip_vary on;
    gzip_min_length 1024;
    gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;

    server {
        listen 80;
        server_name localhost;
        root /usr/share/nginx/html;
        index index.html;

        # Security headers
        add_header X-Frame-Options "SAMEORIGIN" always;
        add_header X-Content-Type-Options "nosniff" always;
        add_header X-XSS-Protection "1; mode=block" always;

        # Cache static assets
        location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
            expires 1y;
            add_header Cache-Control "public, immutable";
        }

        # API proxy
        location /api/ {
            proxy_pass http://backend:8000/;
            proxy_set_header Host $host;
            proxy_set_header X-Real-IP $remote_addr;
            proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
            proxy_set_header X-Forwarded-Proto $scheme;
        }

        # SPA fallback
        location / {
            try_files $uri $uri/ /index.html;
        }
    }
}

Complete Docker Compose with Frontend

# docker-compose.full.yml
version: '3.8'

services:
  # Backend API
  alexai-backend:
    build:
      context: ./backend
      dockerfile: Dockerfile
    container_name: alexai-backend
    ports:
      - "8000:8000"
    volumes:
      - ./data:/app/data
      - ./documents:/app/documents
    environment:
      - OLLAMA_BASE_URL=http://ollama:11434
      - MQTT_BROKER=mqtt
      - CHROMA_PERSIST_DIR=/app/data/chroma_db
      - MEMORY_DB_URL=sqlite:///./data/memory.db
    depends_on:
      - ollama
      - mqtt
      - chroma
    restart: unless-stopped
    networks:
      - alexai-network

  # Frontend
  alexai-frontend:
    build:
      context: ./frontend
      dockerfile: Dockerfile.frontend
    container_name: alexai-frontend
    ports:
      - "80:80"
      - "443:443"
    volumes:
      - ./ssl:/etc/nginx/ssl:ro
    depends_on:
      - alexai-backend
    restart: unless-stopped
    networks:
      - alexai-network

  # Ollama for local LLMs
  ollama:
    image: ollama/ollama:latest
    container_name: alexai-ollama
    ports:
      - "11434:11434"
    volumes:
      - ollama_data:/root/.ollama
    environment:
      - OLLAMA_KEEP_ALIVE=24h
    restart: unless-stopped
    networks:
      - alexai-network

  # MQTT broker for A2A communication
  mqtt:
    image: eclipse-mosquitto:2
    container_name: alexai-mqtt
    ports:
      - "1883:1883"
      - "9001:9001"
    volumes:
      - ./mosquitto.conf:/mosquitto/config/mosquitto.conf
      - mqtt_data:/mosquitto/data
      - mqtt_logs:/mosquitto/log
    restart: unless-stopped
    networks:
      - alexai-network

  # ChromaDB for vector storage
  chroma:
    image: chromadb/chroma:latest
    container_name: alexai-chroma
    ports:
      - "8001:8000"
    volumes:
      - chroma_data:/chroma/chroma
    environment:
      - CHROMA_SERVER_AUTH_CREDENTIALS_PROVIDER=chromadb.auth.token.TokenAuthCredentialsProvider
      - CHROMA_SERVER_AUTH_CREDENTIALS=alexai-token
    restart: unless-stopped
    networks:
      - alexai-network

volumes:
  ollama_data:
  chroma_data:
  mqtt_data:
  mqtt_logs:

networks:
  alexai-network:
    driver: bridge

Environment Configuration

# .env.production
VUE_APP_API_URL=http://localhost:8000
VUE_APP_WS_URL=ws://localhost:8000/ws
VUE_APP_VERSION=1.0.0
VUE_APP_SENTRY_DSN=
VUE_APP_ANALYTICS_ID=

# Feature flags
VUE_APP_ENABLE_VOICE=true
VUE_APP_ENABLE_DOCUMENT_PROCESSING=true
VUE_APP_ENABLE_A2A=true
VUE_APP_ENABLE_DEBUG=false

# Performance
VUE_APP_MAX_FILE_SIZE=10485760
VUE_APP_REQUEST_TIMEOUT=30000
VUE_APP_RETRY_ATTEMPTS=3

Testing Configuration

// tests/unit/ChatInterface.spec.js
import { mount } from '@vue/test-utils'
import ChatInterface from '@/components/Chat/ChatInterface.vue'

describe('ChatInterface', () => {
  it('renders properly', () => {
    const wrapper = mount(ChatInterface, {
      props: {
        messages: [],
        loading: false
      }
    })
    
    expect(wrapper.find('h2').text()).toBe('Chat with AlexAI')
  })

  it('emits send-message when form is submitted', async () => {
    const wrapper = mount(ChatInterface, {
      props: {
        messages: [],
        loading: false
      }
    })

    const input = wrapper.find('input[type="text"]')
    const button = wrapper.find('button')

    await input.setValue('Hello AlexAI')
    await button.trigger('click')

    expect(wrapper.emitted('send-message')).toBeTruthy()
    expect(wrapper.emitted('send-message')[0]).toEqual(['Hello AlexAI'])
  })

  it('disables input when loading', () => {
    const wrapper = mount(ChatInterface, {
      props: {
        messages: [],
        loading: true
      }
    })

    const input = wrapper.find('input[type="text"]')
    const button = wrapper.find('button')

    expect(input.attributes('disabled')).toBeDefined()
    expect(button.attributes('disabled')).toBeDefined()
  })
})

Performance Optimizations

// src/utils/performance.js
export class PerformanceOptimizer {
  constructor() {
    this.debounceTimers = new Map()
    this.throttleTimers = new Map()
  }

  // Debounce function calls
  debounce(key, func, delay = 300) {
    if (this.debounceTimers.has(key)) {
      clearTimeout(this.debounceTimers.get(key))
    }

    const timer = setTimeout(() => {
      func()
      this.debounceTimers.delete(key)
    }, delay)

    this.debounceTimers.set(key, timer)
  }

  // Throttle function calls
  throttle(key, func, delay = 100) {
    if (this.throttleTimers.has(key)) {
      return
    }

    func()
    
    const timer = setTimeout(() => {
      this.throttleTimers.delete(key)
    }, delay)

    this.throttleTimers.set(key, timer)
  }

  // Lazy load images
  lazyLoadImages() {
    const images = document.querySelectorAll('img[data-src]')
    
    const imageObserver = new IntersectionObserver((entries, observer) => {
      entries.forEach(entry => {
        if (entry.isIntersecting) {
          const img = entry.target
          img.src = img.dataset.src
          img.removeAttribute('data-src')
          observer.unobserve(img)
        }
      })
    })

    images.forEach(img => imageObserver.observe(img))
  }

  // Virtual scrolling for large lists
  virtualScroll(container, items, itemHeight, renderItem) {
    const containerHeight = container.clientHeight
    const scrollTop = container.scrollTop
    const startIndex = Math.floor(scrollTop / itemHeight)
    const endIndex = Math.min(
      startIndex + Math.ceil(containerHeight / itemHeight) + 1,
      items.length
    )

    // Clear container
    container.innerHTML = ''

    // Create spacer for items above viewport
    if (startIndex > 0) {
      const topSpacer = document.createElement('div')
      topSpacer.style.height = `${startIndex * itemHeight}px`
      container.appendChild(topSpacer)
    }

    // Render visible items
    for (let i = startIndex; i < endIndex; i++) {
      const element = renderItem(items[i], i)
      container.appendChild(element)
    }

    // Create spacer for items below viewport
    const remainingItems = items.length - endIndex
    if (remainingItems > 0) {
      const bottomSpacer = document.createElement('div')
      bottomSpacer.style.height = `${remainingItems * itemHeight}px`
      container.appendChild(bottomSpacer)
    }
  }
}

export const performanceOptimizer = new PerformanceOptimizer()

Summary: Complete Vue.js Frontend

What We’ve Built

This comprehensive Vue.js frontend provides:

  1. Modern UI/UX: Clean, responsive interface with Tailwind CSS
  2. Real-time Chat: Interactive chat with message history and metadata
  3. Document Processing: Drag-and-drop file upload with OCR analysis
  4. Voice Interface: Voice interaction with visual feedback
  5. Knowledge Management: Search and manage personal knowledge base
  6. System Monitoring: Real-time status and performance metrics
  7. Type Safety: Proper data validation and error handling
  8. Production Ready: Docker, nginx, testing, and optimization

Key Features

Deployment Options

  1. Development: npm run dev for hot-reload development
  2. Production: Docker container with nginx reverse proxy
  3. Static Hosting: Build and deploy to CDN or static hosting
  4. Embedded: Serve from FastAPI backend as static files

This frontend completes our AlexAI ecosystem, providing users with an intuitive, powerful interface to interact with all the AI capabilities we’ve built throughout this series. The modular architecture makes it easy to extend with new features while maintaining performance and user experience.```

// src/main.js
import { createApp } from 'vue'
import App from './App.vue'
import './style.css'

const app = createApp(App)

// Global error handler
app.config.errorHandler = (err, vm, info) => {
  console.error('Vue error:', err, info)
}

app.mount('#app')
/* src/style.css */
@import 'tailwindcss/base';
@import 'tailwindcss/components';
@import 'tailwindcss/utilities';

@import url('https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap');

/* Custom components */
@layer components {
  .btn-primary {
    @apply bg-blue-600 text-white px-4 py-2 rounded-lg hover:bg-blue-700 transition-colors;
  }
  
  .btn-secondary {
    @apply bg-gray-200 text-gray-800 px-4 py-2 rounded-lg hover:bg-gray-300 transition-colors;
  }
  
  .input-field {
    @apply border border-gray-300 rounded-lg px-3 py-2 focus:ring-2 focus:ring-blue-500 focus:border-transparent;
  }
  
  .card {
    @apply bg-white rounded-lg border shadow-sm p-4;
  }
}

/* Custom animations */
@keyframes float {
  0%, 100% { transform: translateY(0px); }
  50% { transform: translateY(-10px); }
}

.float-animation {
  animation: float 3s ease-in-out infinite;
}

/* Loading states */
.loading-dots::after {
  content: '';
  animation: dots 2s infinite;
}

@keyframes dots {
  0%, 20% { content: ''; }
  40% { content: '.'; }
  60% { content: '..'; }
  80%, 100% { content: '...'; }
}

/* Scrollbar styling */
.custom-scrollbar::-webkit-scrollbar {
  width: 6px;
}

.custom-scrollbar::-webkit-scrollbar-track {
  background: #f1f1f1;
  border-radius: 3px;
}

.custom-scrollbar::-webkit-scrollbar-thumb {
  background: #c1c1c1;
  border-radius: 3px;
}

.custom-scrollbar::-webkit-scrollbar-thumb:hover {
  background: #a8a8a8;
}

Additional Components

Knowledge Base Component

<!-- components/Knowledge/KnowledgeBase.vue -->
<template>
  <div class="flex flex-col h-full">
    <!-- Header -->
    <div class="bg-white border-b px-6 py-4">
      <h2 class="text-lg font-semibold text-gray-800">Knowledge Base</h2>
      <p class="text-sm text-gray-600 mt-1">Search and manage your personal knowledge</p>
    </div>

    <div class="flex-1 p-6 overflow-y-auto">
      <!-- Search Section -->
      <div class="mb-6">
        <div class="flex space-x-2">
          <input
            v-model="searchQuery"
            @keyup.enter="performSearch"
            type="text"
            placeholder="Search knowledge base..."
            class="flex-1 input-field"
          />
          <select v-model="selectedCollection" class="input-field w-40">
            <option value="documents">Documents</option>
            <option value="conversations">Conversations</option>
            <option value="web_searches">Web Searches</option>
            <option value="personal_notes">Personal Notes</option>
          </select>
          <button
            @click="performSearch"
            :disabled="!searchQuery.trim() || searching"
            class="btn-primary"
          >
            {{ searching ? 'Searching...' : 'Search' }}
          </button>
        </div>
      </div>

      <!-- Add Knowledge Section -->
      <div class="mb-6 card">
        <h3 class="text-sm font-medium text-gray-800 mb-3">Add Knowledge</h3>
        
        <div class="space-y-3">
          <textarea
            v-model="newKnowledgeContent"
            placeholder="Enter content to add to knowledge base..."
            class="w-full input-field h-24 resize-none"
          ></textarea>
          
          <div class="grid grid-cols-2 gap-3">
            <input
              v-model="newKnowledgeCategory"
              type="text"
              placeholder="Category (optional)"
              class="input-field"
            />
            <select v-model="newKnowledgeCollection" class="input-field">
              <option value="documents">Documents</option>
              <option value="personal_notes">Personal Notes</option>
            </select>
          </div>
          
          <button
            @click="addNewKnowledge"
            :disabled="!newKnowledgeContent.trim() || adding"
            class="btn-primary w-full"
          >
            {{ adding ? 'Adding...' : 'Add to Knowledge Base' }}
          </button>
        </div>
      </div>

      <!-- Search Results -->
      <div v-if="searchResults.length > 0">
        <h3 class="text-sm font-medium text-gray-800 mb-3">
          Search Results ({{ searchResults.length }} found)
        </h3>
        
        <div class="space-y-3">
          <div
            v-for="(result, index) in searchResults"
            :key="index"
            class="card hover:shadow-md transition-shadow"
          >
            <div class="flex justify-between items-start mb-2">
              <div class="flex-1">
                <div class="flex items-center space-x-2 mb-1">
                  <span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
                    {{ result.collection }}
                  </span>
                  <span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
                    {{ (result.similarity_score * 100).toFixed(1) }}% match
                  </span>
                  <span v-if="result.metadata?.category" class="text-xs bg-gray-100 text-gray-800 px-2 py-1 rounded">
                    {{ result.metadata.category }}
                  </span>
                </div>
                
                <div class="text-sm text-gray-900 mb-2">
                  {{ truncateText(result.content, 200) }}
                </div>
                
                <div v-if="result.metadata" class="text-xs text-gray-500">
                  <span v-if="result.metadata.timestamp">
                    Added: {{ formatDate(result.metadata.timestamp) }}
                  </span>
                  <span v-if="result.metadata.content_length" class="ml-3">
                    {{ result.metadata.content_length }} characters
                  </span>
                </div>
              </div>
              
              <div class="flex space-x-1 ml-4">
                <button
                  @click="copyToClipboard(result.content)"
                  class="text-xs bg-gray-100 text-gray-700 px-2 py-1 rounded hover:bg-gray-200 transition-colors"
                  title="Copy content"
                >
                  📋
                </button>
                <button
                  @click="expandResult(result)"
                  class="text-xs bg-blue-100 text-blue-700 px-2 py-1 rounded hover:bg-blue-200 transition-colors"
                  title="View full content"
                >
                  👁️
                </button>
              </div>
            </div>
          </div>
        </div>
      </div>

      <!-- No Results -->
      <div v-else-if="hasSearched && !searching" class="text-center py-8">
        <svg class="mx-auto h-12 w-12 text-gray-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
          <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9.172 16.172a4 4 0 015.656 0M9 12h6m-6-4h6m2 5.291A7.962 7.962 0 0112 15c-2.34 0-4.29-1.175-5.5-2.967L3.5 9.5m17 4.5L17.5 9.5C16.29 10.825 14.34 12 12 12c-2.34 0-4.29-1.175-5.5-2.967"/>
        </svg>
        <p class="mt-2 text-sm text-gray-500">No results found for "{{ lastSearchQuery }}"</p>
        <p class="text-xs text-gray-400 mt-1">Try different keywords or check spelling</p>
      </div>

      <!-- Knowledge Stats -->
      <div class="mt-8 grid grid-cols-2 gap-4">
        <div class="card text-center">
          <div class="text-2xl font-bold text-blue-600">{{ knowledgeStats.total_documents }}</div>
          <div class="text-sm text-gray-600">Total Documents</div>
        </div>
        <div class="card text-center">
          <div class="text-2xl font-bold text-green-600">{{ knowledgeStats.total_searches }}</div>
          <div class="text-sm text-gray-600">Searches Performed</div>
        </div>
      </div>
    </div>

    <!-- Expanded Content Modal -->
    <div
      v-if="expandedResult"
      class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
      @click="expandedResult = null"
    >
      <div
        class="bg-white rounded-lg max-w-4xl w-full max-h-[80vh] overflow-hidden"
        @click.stop
      >
        <div class="bg-gray-50 px-6 py-4 border-b">
          <div class="flex justify-between items-center">
            <h3 class="text-lg font-medium text-gray-900">Knowledge Content</h3>
            <button
              @click="expandedResult = null"
              class="text-gray-400 hover:text-gray-600 transition-colors"
            >
              <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
                <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
              </svg>
            </button>
          </div>
        </div>
        
        <div class="p-6 overflow-y-auto max-h-[60vh]">
          <div class="mb-4">
            <div class="flex space-x-2 mb-2">
              <span class="text-xs bg-blue-100 text-blue-800 px-2 py-1 rounded">
                {{ expandedResult.collection }}
              </span>
              <span class="text-xs bg-green-100 text-green-800 px-2 py-1 rounded">
                {{ (expandedResult.similarity_score * 100).toFixed(1) }}% match
              </span>
            </div>
          </div>
          
          <div class="prose max-w-none">
            <pre class="whitespace-pre-wrap text-sm text-gray-900 bg-gray-50 p-4 rounded">{{ expandedResult.content }}</pre>
          </div>
        </div>
        
        <div class="bg-gray-50 px-6 py-4 border-t flex justify-end space-x-3">
          <button
            @click="copyToClipboard(expandedResult.content)"
            class="btn-secondary"
          >
            Copy Content
          </button>
          <button
            @click="expandedResult = null"
            class="btn-primary"
          >
            Close
          </button>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
  name: 'KnowledgeBase',
  props: {
    searchResults: {
      type: Array,
      default: () => []
    }
  },
  emits: ['search-knowledge', 'add-knowledge'],
  setup(props, { emit }) {
    const searchQuery = ref('')
    const selectedCollection = ref('documents')
    const searching = ref(false)
    const hasSearched = ref(false)
    const lastSearchQuery = ref('')
    
    const newKnowledgeContent = ref('')
    const newKnowledgeCategory = ref('')
    const newKnowledgeCollection = ref('documents')
    const adding = ref(false)
    
    const expandedResult = ref(null)
    
    const knowledgeStats = ref({
      total_documents: 0,
      total_searches: 0
    })

    const performSearch = async () => {
      if (!searchQuery.value.trim() || searching.value) return

      searching.value = true
      hasSearched.value = true
      lastSearchQuery.value = searchQuery.value

      try {
        await emit('search-knowledge', searchQuery.value, selectedCollection.value)
        knowledgeStats.value.total_searches++
      } finally {
        searching.value = false
      }
    }

    const addNewKnowledge = async () => {
      if (!newKnowledgeContent.value.trim() || adding.value) return

      adding.value = true

      try {
        const metadata = {}
        if (newKnowledgeCategory.value.trim()) {
          metadata.category = newKnowledgeCategory.value.trim()
        }

        await emit('add-knowledge', newKnowledgeContent.value.trim(), metadata)
        
        // Clear form
        newKnowledgeContent.value = ''
        newKnowledgeCategory.value = ''
        
        knowledgeStats.value.total_documents++
      } finally {
        adding.value = false
      }
    }

    const expandResult = (result) => {
      expandedResult.value = result
    }

    const truncateText = (text, maxLength) => {
      if (text.length <= maxLength) return text
      return text.substring(0, maxLength) + '...'
    }

    const formatDate = (dateString) => {
      return new Date(dateString).toLocaleDateString()
    }

    const copyToClipboard = async (text) => {
      try {
        await navigator.clipboard.writeText(text)
        // Could add notification here
      } catch (error) {
        console.error('Failed to copy:', error)
      }
    }

    onMounted(() => {
      // Load initial stats
      knowledgeStats.value = {
        total_documents: 25, // Could be loaded from API
        total_searches: 127
      }
    })

    return {
      searchQuery,
      selectedCollection,
      searching,
      hasSearched,
      lastSearchQuery,
      newKnowledgeContent,
      newKnowledgeCategory,
      newKnowledgeCollection,
      adding,
      expandedResult,
      knowledgeStats,
      performSearch,
      addNewKnowledge,
      expandResult,
      truncateText,
      formatDate,
      copyToClipboard
    }
  }
}
</script>

System Monitor Component

<!-- components/System/SystemMonitor.vue -->
<template>
  <div class="flex flex-col h-full">
    <!-- Header -->
    <div class="bg-white border-b px-6 py-4">
      <div class="flex justify-between items-center">
        <div>
          <h2 class="text-lg font-semibold text-gray-800">System Monitor</h2>
          <p class="text-sm text-gray-600 mt-1">AlexAI system status and performance</p>
        </div>
        <button
          @click="$emit('refresh-status')"
          :class="[
            'px-4 py-2 rounded-lg transition-colors flex items-center space-x-2',
            refreshing ? 'bg-gray-100 text-gray-500' : 'bg-blue-100 text-blue-700 hover:bg-blue-200'
          ]"
          :disabled="refreshing"
        >
          <svg 
            :class="['w-4 h-4', refreshing ? 'animate-spin' : '']" 
            fill="currentColor" 
            viewBox="0 0 20 20"
          >
            <path fill-rule="evenodd" d="M4 2a1 1 0 011 1v2.101a7.002 7.002 0 0111.601 2.566 1 1 0 11-1.885.666A5.002 5.002 0 005.999 7H9a1 1 0 010 2H4a1 1 0 01-1-1V3a1 1 0 011-1zm.008 9.057a1 1 0 011.276.61A5.002 5.002 0 0014.001 13H11a1 1 0 110-2h5a1 1 0 011 1v5a1 1 0 11-2 0v-2.101a7.002 7.002 0 01-11.601-2.566 1 1 0 01.61-1.276z" clip-rule="evenodd"/>
          </svg>
          <span>{{ refreshing ? 'Refreshing...' : 'Refresh' }}</span>
        </button>
      </div>
    </div>

    <div class="flex-1 p-6 overflow-y-auto">
      <!-- Status Overview -->
      <div class="grid grid-cols-1 md:grid-cols-3 gap-6 mb-6">
        <!-- Service Status -->
        <div class="card">
          <div class="flex items-center justify-between mb-2">
            <h3 class="text-sm font-medium text-gray-700">Service Status</h3>
            <div :class="[
              'w-3 h-3 rounded-full',
              status.status === 'running' ? 'bg-green-500' :
              status.status === 'error' ? 'bg-red-500' :
              'bg-yellow-500'
            ]"></div>
          </div>
          <div class="text-2xl font-bold text-gray-900 mb-1">
            {{ status.status?.toUpperCase() }}
          </div>
          <div class="text-sm text-gray-600">
            {{ status.service }} v{{ status.version }}
          </div>
        </div>

        <!-- Uptime -->
        <div class="card">
          <h3 class="text-sm font-medium text-gray-700 mb-2">Uptime</h3>
          <div class="text-2xl font-bold text-blue-600 mb-1">
            {{ formatUptime(status.uptime) }}
          </div>
          <div class="text-sm text-gray-600">
            Since {{ startTime }}
          </div>
        </div>

        <!-- Active Sessions -->
        <div class="card">
          <h3 class="text-sm font-medium text-gray-700 mb-2">Activity</h3>
          <div class="space-y-2">
            <div class="flex justify-between">
              <span class="text-sm text-gray-600">Sessions:</span>
              <span class="text-sm font-medium">{{ status.active_sessions }}</span>
            </div>
            <div class="flex justify-between">
              <span class="text-sm text-gray-600">Requests:</span>
              <span class="text-sm font-medium">{{ status.total_requests }}</span>
            </div>
          </div>
        </div>
      </div>

      <!-- Performance Metrics -->
      <div class="grid grid-cols-1 lg:grid-cols-2 gap-6 mb-6">
        <!-- Memory Usage -->
        <div class="card">
          <h3 class="text-sm font-medium text-gray-700 mb-4">Memory Usage</h3>
          <div class="space-y-3">
            <div v-for="(value, key) in status.memory_usage" :key="key">
              <div class="flex justify-between text-sm mb-1">
                <span class="text-gray-600">{{ formatMemoryKey(key) }}:</span>
                <span class="font-medium">{{ formatMemoryValue(value) }}</span>
              </div>
              <div class="w-full bg-gray-200 rounded-full h-2">
                <div 
                  class="bg-blue-500 h-2 rounded-full transition-all duration-300"
                  :style="{ width: getMemoryPercentage(value) + '%' }"
                ></div>
              </div>
            </div>
          </div>
        </div>

        <!-- System Health -->
        <div class="card">
          <h3 class="text-sm font-medium text-gray-700 mb-4">System Health</h3>
          <div class="space-y-3">
            <div class="flex items-center justify-between">
              <span class="text-sm text-gray-600">Ollama Connection</span>
              <span :class="[
                'text-xs px-2 py-1 rounded',
                ollamaStatus === 'connected' ? 'bg-green-100 text-green-800' :
                'bg-red-100 text-red-800'
              ]">
                {{ ollamaStatus }}
              </span>
            </div>
            
            <div class="flex items-center justify-between">
              <span class="text-sm text-gray-600">Vector Database</span>
              <span :class="[
                'text-xs px-2 py-1 rounded',
                vectorDbStatus === 'connected' ? 'bg-green-100 text-green-800' :
                'bg-red-100 text-red-800'
              ]">
                {{ vectorDbStatus }}
              </span>
            </div>
            
            <div class="flex items-center justify-between">
              <span class="text-sm text-gray-600">Memory System</span>
              <span :class="[
                'text-xs px-2 py-1 rounded',
                memoryStatus === 'connected' ? 'bg-green-100 text-green-800' :
                'bg-red-100 text-red-800'
              ]">
                {{ memoryStatus }}
              </span>
            </div>

            <div class="flex items-center justify-between">
              <span class="text-sm text-gray-600">Voice Interface</span>
              <span :class="[
                'text-xs px-2 py-1 rounded',
                voiceStatus === 'available' ? 'bg-green-100 text-green-800' :
                'bg-yellow-100 text-yellow-800'
              ]">
                {{ voiceStatus }}
              </span>
            </div>
          </div>
        </div>
      </div>

      <!-- Recent Activity -->
      <div class="card">
        <div class="flex justify-between items-center mb-4">
          <h3 class="text-sm font-medium text-gray-700">Recent Activity</h3>
          <select v-model="activityFilter" class="text-xs border border-gray-300 rounded px-2 py-1">
            <option value="all">All</option>
            <option value="chat">Chat</option>
            <option value="document">Document</option>
            <option value="voice">Voice</option>
            <option value="system">System</option>
          </select>
        </div>
        
        <div class="space-y-2 max-h-64 overflow-y-auto custom-scrollbar">
          <div
            v-for="(log, index) in filteredLogs"
            :key="index"
            class="flex items-center space-x-3 text-sm p-2 rounded hover:bg-gray-50"
          >
            <div :class="[
              'w-2 h-2 rounded-full flex-shrink-0',
              log.level === 'error' ? 'bg-red-500' :
              log.level === 'warning' ? 'bg-yellow-500' :
              log.level === 'info' ? 'bg-blue-500' :
              'bg-gray-500'
            ]"></div>
            
            <div class="flex-1 min-w-0">
              <div class="text-gray-900 truncate">{{ log.message }}</div>
              <div class="text-xs text-gray-500">
                {{ formatLogTime(log.timestamp) }}  {{ log.component }}
              </div>
            </div>
            
            <div v-if="log.metadata" class="text-xs text-gray-400">
              {{ log.metadata.duration || log.metadata.status }}
            </div>
          </div>
        </div>
        
        <div v-if="filteredLogs.length === 0" class="text-center py-4 text-gray-500 text-sm">
          No activity logs available
        </div>
      </div>

      <!-- Quick Actions -->
      <div class="mt-6 grid grid-cols-2 md:grid-cols-4 gap-3">
        <button
          @click="performHealthCheck"
          :disabled="performingHealthCheck"
          class="btn-secondary text-sm"
        >
          {{ performingHealthCheck ? 'Checking...' : 'Health Check' }}
        </button>
        
        <button
          @click="clearLogs"
          class="btn-secondary text-sm"
        >
          Clear Logs
        </button>
        
        <button
          @click="exportLogs"
          class="btn-secondary text-sm"
        >
          Export Logs
        </button>
        
        <button
          @click="showSystemInfo = true"
          class="btn-secondary text-sm"
        >
          System Info
        </button>
      </div>
    </div>

    <!-- System Info Modal -->
    <div
      v-if="showSystemInfo"
      class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50 p-4"
      @click="showSystemInfo = false"
    >
      <div
        class="bg-white rounded-lg max-w-2xl w-full max-h-[80vh] overflow-hidden"
        @click.stop
      >
        <div class="bg-gray-50 px-6 py-4 border-b">
          <div class="flex justify-between items-center">
            <h3 class="text-lg font-medium text-gray-900">System Information</h3>
            <button
              @click="showSystemInfo = false"
              class="text-gray-400 hover:text-gray-600"
            >
              <svg class="w-6 h-6" fill="currentColor" viewBox="0 0 20 20">
                <path fill-rule="evenodd" d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z" clip-rule="evenodd"/>
              </svg>
            </button>
          </div>
        </div>
        
        <div class="p-6 overflow-y-auto max-h-[60vh]">
          <div class="space-y-4">
            <div>
              <h4 class="font-medium text-gray-900 mb-2">Environment</h4>
              <div class="bg-gray-50 rounded p-3 text-sm font-mono">
                <div>Browser: {{ systemInfo.browser }}</div>
                <div>Platform: {{ systemInfo.platform }}</div>
                <div>User Agent: {{ systemInfo.userAgent }}</div>
              </div>
            </div>
            
            <div>
              <h4 class="font-medium text-gray-900 mb-2">API Configuration</h4>
              <div class="bg-gray-50 rounded p-3 text-sm">
                <div>Base URL: {{ apiBaseUrl }}</div>
                <div>Timeout: {{ apiTimeout }}ms</div>
                <div>Version: {{ status.version }}</div>
              </div>
            </div>
            
            <div>
              <h4 class="font-medium text-gray-900 mb-2">Features</h4>
              <div class="grid grid-cols-2 gap-2 text-sm">
                <div class="flex items-center space-x-2">
                  <div class="w-2 h-2 bg-green-500 rounded-full"></div>
                  <span>Chat Interface</span>
                </div>
                <div class="flex items-center space-x-2">
                  <div class="w-2 h-2 bg-green-500 rounded-full"></div>
                  <span>Document Processing</span>
                </div>
                <div class="flex items-center space-x-2">
                  <div :class="[
                    'w-2 h-2 rounded-full',
                    voiceStatus === 'available' ? 'bg-green-500' : 'bg-yellow-500'
                  ]"></div>
                  <span>Voice Interface</span>
                </div>
                <div class="flex items-center space-x-2">
                  <div class="w-2 h-2 bg-green-500 rounded-full"></div>
                  <span>Knowledge Base</span>
                </div>
              </div>
            </div>
          </div>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import { ref, computed, onMounted } from 'vue'

export default {
  name: 'SystemMonitor',
  props: {
    status: {
      type: Object,
      required: true
    },
    logs: {
      type: Array,
      default: () => []
    }
  },
  emits: ['refresh-status'],
  setup(props, { emit }) {
    const refreshing = ref(false)
    const activityFilter = ref('all')
    const performingHealthCheck = ref(false)
    const showSystemInfo = ref(false)
    
    // System status states
    const ollamaStatus = ref('connected')
    const vectorDbStatus = ref('connected')
    const memoryStatus = ref('connected')
    const voiceStatus = ref('available')
    
    // System info
    const systemInfo = ref({
      browser: navigator.userAgent.split(' ').slice(-1)[0],
      platform: navigator.platform,
      userAgent: navigator.userAgent
    })
    
    const apiBaseUrl = ref('http://localhost:8000')
    const apiTimeout = ref(30000)

    const startTime = computed(() => {
      const now = new Date()
      const uptime = props.status.uptime || 0
      const startDate = new Date(now.getTime() - (uptime * 1000))
      return startDate.toLocaleString()
    })

    const filteredLogs = computed(() => {
      if (activityFilter.value === 'all') {
        return props.logs.slice(-20) // Show last 20 logs
      }
      return props.logs
        .filter(log => log.component === activityFilter.value)
        .slice(-20)
    })

    const formatUptime = (seconds) => {
      if (!seconds) return '0s'
      
      const days = Math.floor(seconds / 86400)
      const hours = Math.floor((seconds % 86400) / 3600)
      const minutes = Math.floor((seconds % 3600) / 60)
      const secs = Math.floor(seconds % 60)
      
      if (days > 0) return `${days}d ${hours}h`
      if (hours > 0) return `${hours}h ${minutes}m`
      if (minutes > 0) return `${minutes}m ${secs}s`
      return `${secs}s`
    }

    const formatMemoryKey = (key) => {
      return key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())
    }

    const formatMemoryValue = (value) => {
      if (typeof value === 'number') {
        if (value > 1024 * 1024) {
          return `${(value / (1024 * 1024)).toFixed(1)} MB`
        }
        if (value > 1024) {
          return `${(value / 1024).toFixed(1)} KB`
        }
        return `${value} B`
      }
      return String(value)
    }

    const getMemoryPercentage = (value) => {
      // Simple percentage calculation for demo
      if (typeof value === 'number') {
        return Math.min((value / (100 * 1024 * 1024)) * 100, 100)
      }
      return 0
    }

    const formatLogTime = (timestamp) => {
      return new Date(timestamp).toLocaleTimeString()
    }

    const performHealthCheck = async () => {
      performingHealthCheck.value = true
      
      try {
        // Simulate health check
        await new Promise(resolve => setTimeout(resolve, 2000))
        
        // Update component statuses
        ollamaStatus.value = 'connected'
        vectorDbStatus.value = 'connected'
        memoryStatus.value = 'connected'
        voiceStatus.value = navigator.mediaDevices ? 'available' : 'unavailable'
        
        // Emit refresh to get latest status
        emit('refresh-status')
        
      } catch (error) {
        console.error('Health check failed:', error)
      } finally {
        performingHealthCheck.value = false
      }
    }

    const clearLogs = () => {
      // This would typically emit an event to clear logs
      console.log('Clearing logs...')
    }

    const exportLogs = () => {
      const logsText = props.logs
        .map(log => `${log.timestamp} [${log.level.toUpperCase()}] ${log.component}: ${log.message}`)
        .join('\n')
      
      const blob = new Blob([logsText], { type: 'text/plain' })
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = `alexai-logs-${new Date().toISOString().split('T')[0]}.txt`
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
      URL.revokeObjectURL(url)
    }

    onMounted(() => {
      // Check voice capabilities
      if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
        voiceStatus.value = 'unavailable'
      }
    })

    return {
      refreshing,
      activityFilter,
      performingHealthCheck,
      showSystemInfo,
      ollamaStatus,
      vectorDbStatus,
      memoryStatus,
      voiceStatus,
      systemInfo,
      apiBaseUrl,
      apiTimeout,
      startTime,
      filteredLogs,
      formatUptime,
      formatMemoryKey,
      formatMemoryValue,
      getMemoryPercentage,
      formatLogTime,
      performHealthCheck,
      clearLogs,
      exportLogs
    }
  }
}
</script>