Part 10: Vue.js Frontend Interface
Why Vue.js for AI Assistant Interface?
Vue.js provides an excellent foundation for AI assistant interfaces:
- Reactive data: Real-time updates for chat conversations
- Component-based: Modular UI components for different features
- Easy integration: Simple API communication with our FastAPI backend
- Progressive enhancement: Can be added incrementally to existing apps
- Developer experience: Excellent tooling and debugging support
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:
- Modern UI/UX: Clean, responsive interface with Tailwind CSS
- Real-time Chat: Interactive chat with message history and metadata
- Document Processing: Drag-and-drop file upload with OCR analysis
- Voice Interface: Voice interaction with visual feedback
- Knowledge Management: Search and manage personal knowledge base
- System Monitoring: Real-time status and performance metrics
- Type Safety: Proper data validation and error handling
- Production Ready: Docker, nginx, testing, and optimization
Key Features
- Component Architecture: Modular, reusable Vue 3 components
- Reactive State: Composition API for better state management
- API Integration: Seamless communication with FastAPI backend
- Error Handling: Comprehensive error boundaries and user feedback
- Performance: Debouncing, throttling, and virtual scrolling
- Accessibility: ARIA labels and keyboard navigation
- PWA Ready: Service worker and offline capabilities
- Testing: Unit tests and integration testing setup
Deployment Options
- Development:
npm run dev
for hot-reload development - Production: Docker container with nginx reverse proxy
- Static Hosting: Build and deploy to CDN or static hosting
- 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>