UNPKG

@vectorchat/mcp-server

Version:

VectorChat MCP Server - Encrypted AI-to-AI communication with hardware security (YubiKey/TPM). 45+ MCP tools for Windsurf, Claude, and AI assistants. Model-based identity with EMDM encryption. Dynamic AI playbook system, communication zones, message relay

707 lines (612 loc) 23.3 kB
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Model Selection - VectorChat Web</title> <style> * { margin: 0; padding: 0; box-sizing: border-box; } :root { --primary-gradient: linear-gradient(135deg, #667eea 0%, #764ba2 100%); --primary-color: #667eea; --secondary-color: #764ba2; --success-color: #4caf50; --warning-color: #ff9800; --error-color: #f44336; --surface-color: #ffffff; --background-color: #f5f5f5; --text-primary: #333333; --text-secondary: #666666; --border-color: #e0e0e0; --shadow: 0 4px 6px rgba(0, 0, 0, 0.1); } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; background: var(--background-color); color: var(--text-primary); line-height: 1.6; padding: 20px; } .container { max-width: 800px; margin: 0 auto; background: var(--surface-color); border-radius: 12px; box-shadow: var(--shadow); overflow: hidden; } .header { background: var(--primary-gradient); color: white; padding: 24px; text-align: center; } .header h1 { font-size: 1.8rem; font-weight: 600; margin-bottom: 8px; } .header p { opacity: 0.9; font-size: 1rem; } .content { padding: 24px; } .model-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); gap: 20px; margin-bottom: 32px; } .model-card { border: 2px solid var(--border-color); border-radius: 12px; padding: 20px; cursor: pointer; transition: all 0.3s ease; background: #fafafa; } .model-card:hover { border-color: var(--primary-color); transform: translateY(-2px); box-shadow: 0 8px 25px rgba(102, 126, 234, 0.15); } .model-card.selected { border-color: var(--primary-color); background: linear-gradient(135deg, rgba(102, 126, 234, 0.1) 0%, rgba(118, 75, 162, 0.1) 100%); } .model-header { display: flex; align-items: center; gap: 12px; margin-bottom: 12px; } .model-icon { width: 48px; height: 48px; border-radius: 50%; background: var(--primary-gradient); color: white; display: flex; align-items: center; justify-content: center; font-size: 1.2rem; font-weight: 600; } .model-title { font-size: 1.2rem; font-weight: 600; margin-bottom: 4px; } .model-subtitle { font-size: 0.9rem; color: var(--text-secondary); } .model-description { font-size: 0.9rem; color: var(--text-secondary); margin-bottom: 16px; line-height: 1.5; } .model-specs { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 16px; } .spec-item { display: flex; justify-content: space-between; font-size: 0.85rem; } .spec-label { color: var(--text-secondary); } .spec-value { font-weight: 500; } .model-features { margin-bottom: 16px; } .feature-tag { display: inline-block; background: rgba(102, 126, 234, 0.1); color: var(--primary-color); padding: 4px 8px; border-radius: 12px; font-size: 0.8rem; margin: 2px 4px 2px 0; } .model-actions { display: flex; gap: 8px; } .btn { padding: 8px 16px; border: none; border-radius: 6px; cursor: pointer; font-weight: 500; transition: all 0.3s ease; font-size: 0.9rem; } .btn-primary { background: var(--primary-gradient); color: white; } .btn-primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.3); } .btn-secondary { background: #f5f5f5; color: var(--text-primary); border: 1px solid var(--border-color); } .btn-secondary:hover { background: #e8e8e8; } .loading { text-align: center; padding: 40px; color: var(--text-secondary); } .loading-spinner { width: 32px; height: 32px; border: 3px solid var(--border-color); border-top: 3px solid var(--primary-color); border-radius: 50%; animation: spin 1s linear infinite; margin: 0 auto 16px; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .actions { padding: 24px; border-top: 1px solid var(--border-color); display: flex; gap: 12px; justify-content: space-between; } .back-btn { background: #f5f5f5; color: var(--text-primary); border: 1px solid var(--border-color); } .next-btn { background: var(--primary-gradient); color: white; } .next-btn:disabled { opacity: 0.5; cursor: not-allowed; transform: none !important; } .status-info { background: #e8f5e8; border: 1px solid #4caf50; border-radius: 8px; padding: 16px; margin-bottom: 20px; } .status-info h3 { color: #2e7d32; margin-bottom: 8px; font-size: 1rem; } .status-info p { color: #4caf50; font-size: 0.9rem; margin: 0; } @media (max-width: 768px) { .model-grid { grid-template-columns: 1fr; } .actions { flex-direction: column; } .model-actions { flex-direction: column; } } </style> </head> <body> <div class="container"> <div class="header"> <h1>🤖 Select AI Model</h1> <p>Choose the best model for your needs</p> </div> <div class="content"> <div class="status-info"> <h3>🔍 Scanning for available models...</h3> <p>Checking local storage, LM Studio, and Ollama for compatible models</p> </div> <div class="model-grid" id="modelGrid"> <!-- Models will be loaded here --> </div> <div class="loading" id="loadingIndicator"> <div class="loading-spinner"></div> <p>Detecting available models...</p> </div> </div> <div class="actions"> <button class="btn back-btn" onclick="goBack()">← Back</button> <div> <button class="btn btn-secondary" onclick="refreshModels()">🔄 Refresh</button> <button class="btn next-btn" onclick="selectModel()" id="selectButton" disabled> Select Model → </button> </div> </div> </div> <script> let availableModels = []; let selectedModel = null; // Model templates const modelTemplates = [ { id: 'qwen3-1.7b', name: 'Qwen 3 1.7B', icon: '🧠', description: 'Fast, efficient, and cryptographically verified. Perfect for secure AI conversations with EMDM encryption.', specs: { 'Parameters': '1.7B', 'Size': '1.6 GB', 'Context': '8K tokens', 'Type': 'GGUF' }, features: ['EMDM Compatible', 'GPU Accelerated', 'Secure', 'Fast'], recommended: true, downloadUrl: 'https://huggingface.co/Qwen/Qwen3-1.7B-Instruct-GGUF' }, { id: 'qwen2.5-7b', name: 'Qwen 2.5 7B', icon: '🚀', description: 'More capable model with better reasoning and creativity. Ideal for complex tasks and coding.', specs: { 'Parameters': '7.2B', 'Size': '4.2 GB', 'Context': '32K tokens', 'Type': 'GGUF' }, features: ['High Performance', 'Large Context', 'Creative', 'Coding'], recommended: false, downloadUrl: 'https://huggingface.co/Qwen/Qwen2.5-7B-Instruct-GGUF' }, { id: 'llama-7b', name: 'Llama 3.1 8B', icon: '🦙', description: 'Meta\'s open-source model with excellent instruction following and safety alignment.', specs: { 'Parameters': '8.0B', 'Size': '4.7 GB', 'Context': '128K tokens', 'Type': 'GGUF' }, features: ['Open Source', 'Safety Aligned', 'Large Context', 'Versatile'], recommended: false, downloadUrl: 'https://huggingface.co/meta-llama/Llama-3.1-8B-Instruct-GGUF' }, { id: 'mistral-7b', name: 'Mistral 7B', icon: '💨', description: 'High-quality model with strong performance and efficiency. Great for general use.', specs: { 'Parameters': '7.2B', 'Size': '4.1 GB', 'Context': '32K tokens', 'Type': 'GGUF' }, features: ['Efficient', 'High Quality', 'Fast', 'Balanced'], recommended: false, downloadUrl: 'https://huggingface.co/mistralai/Mistral-7B-Instruct-v0.1-GGUF' }, { id: 'custom', name: 'Custom Model', icon: '⚙️', description: 'Use your own local model file. Supports GGUF, HF, and other formats.', specs: { 'Parameters': 'Variable', 'Size': 'Variable', 'Context': 'Variable', 'Type': 'Custom' }, features: ['Local File', 'Any Format', 'Full Control', 'Private'], recommended: false, custom: true } ]; // Initialize document.addEventListener('DOMContentLoaded', function() { loadModels(); }); async function loadModels() { showLoading(); try { // First check for existing models const response = await fetch('http://localhost:3737/api/scan-models', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ paths: ['~/.vectorchat/models', '~/.lmstudio/models', '~/.ollama/models'] }) }); const data = await response.json(); availableModels = data.models || []; // Add template models that aren't already available modelTemplates.forEach(template => { if (!availableModels.some(m => m.name.includes(template.name))) { availableModels.push(template); } }); renderModels(); updateStatus(); } catch (error) { console.error('Error loading models:', error); // Fallback to template models only availableModels = modelTemplates; renderModels(); updateStatus(); } } function renderModels() { const grid = document.getElementById('modelGrid'); grid.innerHTML = ''; if (availableModels.length === 0) { grid.innerHTML = '<p style="text-align: center; color: var(--text-secondary); padding: 40px;">No models found. Please download a model or check your paths.</p>'; return; } availableModels.forEach(model => { const card = document.createElement('div'); card.className = `model-card ${selectedModel?.id === model.id ? 'selected' : ''}`; card.onclick = () => selectModelCard(model); const specsHtml = Object.entries(model.specs || {}) .map(([key, value]) => ` <div class="spec-item"> <span class="spec-label">${key}</span> <span class="spec-value">${value}</span> </div> `).join(''); const featuresHtml = (model.features || []) .map(feature => `<span class="feature-tag">${feature}</span>`) .join(''); const recommendedBadge = model.recommended ? '<div style="position: absolute; top: 12px; right: 12px; background: #4caf50; color: white; padding: 4px 8px; border-radius: 12px; font-size: 0.7rem; font-weight: 600;">RECOMMENDED</div>' : ''; card.innerHTML = ` ${recommendedBadge} <div class="model-header"> <div class="model-icon">${model.icon || '🤖'}</div> <div> <div class="model-title">${model.name}</div> <div class="model-subtitle">${model.type || 'AI Model'}</div> </div> </div> <div class="model-description">${model.description}</div> <div class="model-specs">${specsHtml}</div> <div class="model-features">${featuresHtml}</div> <div class="model-actions"> ${model.custom ? '<button class="btn btn-secondary" onclick="selectCustomModel()">Browse Files</button>' : '<button class="btn btn-primary" onclick="downloadModel(\'' + model.id + '\')">Download</button>' } <button class="btn btn-secondary" onclick="viewModelDetails(\'' + model.id + '\')">Details</button> </div> `; grid.appendChild(card); }); hideLoading(); } function selectModelCard(model) { // Remove previous selection document.querySelectorAll('.model-card').forEach(card => { card.classList.remove('selected'); }); // Select new model selectedModel = model; event.currentTarget.classList.add('selected'); // Update button document.getElementById('selectButton').disabled = false; console.log('Selected model:', model.name); } function selectModel() { if (!selectedModel) return; // Save selection localStorage.setItem('vectorchat-selected-model', JSON.stringify(selectedModel)); // Return to main app or continue setup if (window.opener) { window.opener.postMessage({ type: 'model_selected', model: selectedModel }, '*'); window.close(); } else { // Redirect back to main app window.location.href = 'web-app.html'; } } async function downloadModel(modelId) { const model = modelTemplates.find(m => m.id === modelId); if (!model) return; try { showNotification(`Starting download of ${model.name}...`, 'info'); const response = await fetch('http://localhost:3737/api/download-model', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ model: model.name.toLowerCase().replace(/\s+/g, ''), ipfsCid: 'QmXmPxxtLxtDgh4CYscgTDXyCbNfVWdzhG2UgS6fcX6mXS' }) }); if (response.ok) { showNotification(`${model.name} download started!`, 'success'); // Wait a moment then refresh setTimeout(() => { loadModels(); }, 2000); } else { throw new Error(`HTTP ${response.status}`); } } catch (error) { console.error('Download error:', error); showNotification(`Download failed: ${error.message}`, 'error'); } } function viewModelDetails(modelId) { const model = availableModels.find(m => m.id === modelId); if (!model) return; alert(`Model Details: ${model.name}\n\n${model.description}\n\nFeatures: ${model.features.join(', ')}\n\nDownload: ${model.downloadUrl}`); } function selectCustomModel() { const input = document.createElement('input'); input.type = 'file'; input.accept = '.gguf,.bin,.safetensors'; input.onchange = (event) => { const file = event.target.files[0]; if (file) { selectedModel = { id: 'custom', name: file.name, path: file.path || URL.createObjectURL(file), size: (file.size / 1024 / 1024 / 1024).toFixed(1) + ' GB', type: 'Custom', custom: true }; document.querySelectorAll('.model-card').forEach(card => { card.classList.remove('selected'); }); document.getElementById('selectButton').disabled = false; showNotification(`Selected custom model: ${file.name}`, 'success'); } }; input.click(); } function refreshModels() { loadModels(); } function goBack() { if (window.opener) { window.close(); } else { window.location.href = 'web-app.html'; } } function updateStatus() { const statusDiv = document.querySelector('.status-info'); const foundModels = availableModels.filter(m => !m.custom).length; const recommendedModel = availableModels.find(m => m.recommended); if (foundModels > 0) { statusDiv.innerHTML = ` <h3>✅ Models Detected</h3> <p>Found ${foundModels} models. ${recommendedModel ? 'We recommend Qwen 3 1.7B for the best experience.' : 'Select a model to continue.'}</p> `; } else { statusDiv.innerHTML = ` <h3>🔍 No Models Found</h3> <p>No local models detected. Select a model above to download or browse for a custom model.</p> `; } } function showLoading() { document.getElementById('loadingIndicator').style.display = 'block'; document.getElementById('modelGrid').style.display = 'none'; } function hideLoading() { document.getElementById('loadingIndicator').style.display = 'none'; document.getElementById('modelGrid').style.display = 'grid'; } function showNotification(message, type = 'info') { // Create notification element const notification = document.createElement('div'); notification.style.cssText = ` position: fixed; top: 20px; right: 20px; padding: 12px 20px; border-radius: 8px; color: white; font-weight: 500; z-index: 3000; opacity: 0; transform: translateX(100%); transition: all 0.3s ease; max-width: 400px; `; let backgroundColor; switch (type) { case 'error': backgroundColor = '#f44336'; break; case 'success': backgroundColor = '#4caf50'; break; case 'warning': backgroundColor = '#ff9800'; break; default: backgroundColor = '#667eea'; } notification.style.backgroundColor = backgroundColor; notification.textContent = message; document.body.appendChild(notification); // Animate in setTimeout(() => { notification.style.opacity = '1'; notification.style.transform = 'translateX(0)'; }, 100); // Animate out and remove setTimeout(() => { notification.style.opacity = '0'; notification.style.transform = 'translateX(100%)'; setTimeout(() => { document.body.removeChild(notification); }, 300); }, 3000); } // Handle messages from parent window window.addEventListener('message', function(event) { if (event.data.type === 'model_selected') { selectedModel = event.data.model; document.getElementById('selectButton').disabled = false; } }); </script> </body> </html>