@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
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 ;
}
.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>