|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
const dynamicLoader = { |
|
|
apiBase: window.location.origin, |
|
|
registeredModels: [], |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async init() { |
|
|
console.log('🚀 Initializing Dynamic Model Loader...'); |
|
|
|
|
|
|
|
|
await this.refreshModelsList(); |
|
|
|
|
|
|
|
|
this.setupEventListeners(); |
|
|
|
|
|
console.log('✅ Dynamic Model Loader initialized'); |
|
|
}, |
|
|
|
|
|
setupEventListeners() { |
|
|
|
|
|
const manualForm = document.getElementById('manual-form'); |
|
|
if (manualForm) { |
|
|
manualForm.addEventListener('submit', async (e) => { |
|
|
e.preventDefault(); |
|
|
await this.submitManualConfig(); |
|
|
}); |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showPasteMode() { |
|
|
this.closeAllModes(); |
|
|
document.getElementById('paste-mode').style.display = 'block'; |
|
|
document.getElementById('paste-input').focus(); |
|
|
}, |
|
|
|
|
|
showManualMode() { |
|
|
this.closeAllModes(); |
|
|
document.getElementById('manual-mode').style.display = 'block'; |
|
|
document.getElementById('manual-model-id').focus(); |
|
|
}, |
|
|
|
|
|
showAutoMode() { |
|
|
this.closeAllModes(); |
|
|
document.getElementById('auto-mode').style.display = 'block'; |
|
|
document.getElementById('auto-url').focus(); |
|
|
}, |
|
|
|
|
|
closeAllModes() { |
|
|
document.getElementById('paste-mode').style.display = 'none'; |
|
|
document.getElementById('manual-mode').style.display = 'none'; |
|
|
document.getElementById('auto-mode').style.display = 'none'; |
|
|
}, |
|
|
|
|
|
closeTestPanel() { |
|
|
document.getElementById('test-panel').style.display = 'none'; |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async processPastedConfig() { |
|
|
const configText = document.getElementById('paste-input').value.trim(); |
|
|
const autoDetect = document.getElementById('auto-detect-paste').checked; |
|
|
|
|
|
if (!configText) { |
|
|
this.showError('Please paste a configuration'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.showInfo('Processing pasted configuration...'); |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${this.apiBase}/api/dynamic-models/paste-config`, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ |
|
|
config_text: configText, |
|
|
auto_detect: autoDetect |
|
|
}) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
this.showSuccess(`Model "${data.data.model_id}" registered successfully!`); |
|
|
await this.refreshModelsList(); |
|
|
this.closeAllModes(); |
|
|
document.getElementById('paste-input').value = ''; |
|
|
} else { |
|
|
this.showError(data.error || 'Failed to process configuration'); |
|
|
} |
|
|
} catch (error) { |
|
|
this.showError(`Error: ${error.message}`); |
|
|
console.error('Paste config error:', error); |
|
|
} |
|
|
}, |
|
|
|
|
|
async testPastedConfig() { |
|
|
const configText = document.getElementById('paste-input').value.trim(); |
|
|
|
|
|
if (!configText) { |
|
|
this.showError('Please paste a configuration'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.showInfo('Testing configuration...'); |
|
|
|
|
|
try { |
|
|
|
|
|
let parsedConfig; |
|
|
try { |
|
|
parsedConfig = JSON.parse(configText); |
|
|
} catch { |
|
|
this.showError('Invalid JSON. Please provide valid JSON configuration for testing.'); |
|
|
return; |
|
|
} |
|
|
|
|
|
const response = await fetch(`${this.apiBase}/api/dynamic-models/test-connection`, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(parsedConfig) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success && data.test_result.success) { |
|
|
this.showSuccess(`✅ Connection successful! (${Math.round(data.test_result.response_time_ms)}ms)`); |
|
|
} else { |
|
|
this.showError(`❌ Connection failed: ${data.test_result.error || 'Unknown error'}`); |
|
|
} |
|
|
} catch (error) { |
|
|
this.showError(`Test failed: ${error.message}`); |
|
|
console.error('Test error:', error); |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async submitManualConfig() { |
|
|
const config = { |
|
|
model_id: document.getElementById('manual-model-id').value.trim(), |
|
|
model_name: document.getElementById('manual-model-name').value.trim(), |
|
|
base_url: document.getElementById('manual-base-url').value.trim(), |
|
|
api_key: document.getElementById('manual-api-key').value.trim() || null, |
|
|
api_type: document.getElementById('manual-api-type').value === 'auto' |
|
|
? null |
|
|
: document.getElementById('manual-api-type').value, |
|
|
endpoints: document.getElementById('manual-endpoint').value.trim() || null |
|
|
}; |
|
|
|
|
|
const testFirst = document.getElementById('test-before-register').checked; |
|
|
|
|
|
if (testFirst) { |
|
|
this.showInfo('Testing connection first...'); |
|
|
|
|
|
try { |
|
|
const testResponse = await fetch(`${this.apiBase}/api/dynamic-models/test-connection`, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(config) |
|
|
}); |
|
|
|
|
|
const testData = await testResponse.json(); |
|
|
|
|
|
if (!testData.success || !testData.test_result.success) { |
|
|
const proceed = confirm( |
|
|
`Connection test failed: ${testData.test_result.error}\n\nDo you want to register anyway?` |
|
|
); |
|
|
if (!proceed) return; |
|
|
} |
|
|
} catch (error) { |
|
|
const proceed = confirm( |
|
|
`Test failed: ${error.message}\n\nDo you want to register anyway?` |
|
|
); |
|
|
if (!proceed) return; |
|
|
} |
|
|
} |
|
|
|
|
|
this.showInfo('Registering model...'); |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${this.apiBase}/api/dynamic-models/register`, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(config) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
this.showSuccess(`Model "${config.model_id}" registered successfully!`); |
|
|
await this.refreshModelsList(); |
|
|
this.closeAllModes(); |
|
|
document.getElementById('manual-form').reset(); |
|
|
} else { |
|
|
this.showError(data.message || 'Registration failed'); |
|
|
} |
|
|
} catch (error) { |
|
|
this.showError(`Error: ${error.message}`); |
|
|
console.error('Registration error:', error); |
|
|
} |
|
|
}, |
|
|
|
|
|
async testManualConfig() { |
|
|
const config = { |
|
|
model_id: document.getElementById('manual-model-id').value.trim(), |
|
|
model_name: document.getElementById('manual-model-name').value.trim(), |
|
|
base_url: document.getElementById('manual-base-url').value.trim(), |
|
|
api_key: document.getElementById('manual-api-key').value.trim() || null, |
|
|
api_type: document.getElementById('manual-api-type').value === 'auto' |
|
|
? null |
|
|
: document.getElementById('manual-api-type').value |
|
|
}; |
|
|
|
|
|
if (!config.base_url) { |
|
|
this.showError('Please enter a base URL'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.showInfo('Testing connection...'); |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${this.apiBase}/api/dynamic-models/test-connection`, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify(config) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success && data.test_result.success) { |
|
|
this.showSuccess( |
|
|
`✅ Connection successful!\n` + |
|
|
`API Type: ${data.test_result.api_type}\n` + |
|
|
`Response Time: ${Math.round(data.test_result.response_time_ms)}ms\n` + |
|
|
`Capabilities: ${data.test_result.detected_capabilities.join(', ')}` |
|
|
); |
|
|
} else { |
|
|
this.showError( |
|
|
`❌ Connection failed:\n${data.test_result.error || 'Unknown error'}` |
|
|
); |
|
|
} |
|
|
} catch (error) { |
|
|
this.showError(`Test failed: ${error.message}`); |
|
|
console.error('Test error:', error); |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async autoConfigureFromURL() { |
|
|
const url = document.getElementById('auto-url').value.trim(); |
|
|
|
|
|
if (!url) { |
|
|
this.showError('Please enter a URL'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.showInfo('Auto-configuring model...'); |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${this.apiBase}/api/dynamic-models/auto-configure`, { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ url }) |
|
|
}); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
this.showSuccess( |
|
|
`✅ Model auto-configured and registered!\n` + |
|
|
`Model ID: ${data.config.model_id}\n` + |
|
|
`API Type: ${data.config.api_type}\n` + |
|
|
`Endpoints discovered: ${Object.keys(data.config.endpoints?.endpoints || {}).length}` |
|
|
); |
|
|
await this.refreshModelsList(); |
|
|
this.closeAllModes(); |
|
|
document.getElementById('auto-url').value = ''; |
|
|
} else { |
|
|
this.showError(data.error || 'Auto-configuration failed'); |
|
|
} |
|
|
} catch (error) { |
|
|
this.showError(`Error: ${error.message}`); |
|
|
console.error('Auto-configure error:', error); |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async refreshModelsList() { |
|
|
const container = document.getElementById('models-list'); |
|
|
|
|
|
try { |
|
|
const response = await fetch(`${this.apiBase}/api/dynamic-models/models`); |
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
this.registeredModels = data.models; |
|
|
this.renderModelsList(data.models); |
|
|
} else { |
|
|
container.innerHTML = '<p class="error-text">Failed to load models</p>'; |
|
|
} |
|
|
} catch (error) { |
|
|
console.error('Failed to load models:', error); |
|
|
container.innerHTML = '<p class="error-text">Error loading models</p>'; |
|
|
} |
|
|
}, |
|
|
|
|
|
renderModelsList(models) { |
|
|
const container = document.getElementById('models-list'); |
|
|
|
|
|
if (models.length === 0) { |
|
|
container.innerHTML = ` |
|
|
<div class="empty-state"> |
|
|
<p>No models registered yet</p> |
|
|
<p class="text-secondary">Click one of the quick action buttons above to register your first model</p> |
|
|
</div> |
|
|
`; |
|
|
return; |
|
|
} |
|
|
|
|
|
container.innerHTML = models.map(model => ` |
|
|
<div class="model-card" data-model-id="${model.model_id}"> |
|
|
<div class="model-header"> |
|
|
<div class="model-info"> |
|
|
<h4>${this.escapeHtml(model.model_name)}</h4> |
|
|
<span class="model-type">${model.api_type || 'unknown'}</span> |
|
|
</div> |
|
|
<div class="model-actions"> |
|
|
<button |
|
|
class="btn-icon" |
|
|
title="Test model" |
|
|
onclick="dynamicLoader.openTestModel('${model.model_id}')" |
|
|
> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polygon points="5 3 19 12 5 21 5 3"></polygon></svg> |
|
|
</button> |
|
|
<button |
|
|
class="btn-icon" |
|
|
title="View details" |
|
|
onclick="dynamicLoader.viewModelDetails('${model.model_id}')" |
|
|
> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><circle cx="12" cy="12" r="10"></circle><line x1="12" y1="16" x2="12" y2="12"></line><line x1="12" y1="8" x2="12.01" y2="8"></line></svg> |
|
|
</button> |
|
|
<button |
|
|
class="btn-icon btn-danger" |
|
|
title="Delete model" |
|
|
onclick="dynamicLoader.deleteModel('${model.model_id}')" |
|
|
> |
|
|
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><polyline points="3 6 5 6 21 6"></polyline><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path></svg> |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
<div class="model-details"> |
|
|
<div><strong>ID:</strong> ${this.escapeHtml(model.model_id)}</div> |
|
|
<div><strong>URL:</strong> ${this.escapeHtml(model.base_url)}</div> |
|
|
${model.api_key ? '<div><strong>Auth:</strong> Yes (API key set)</div>' : ''} |
|
|
</div> |
|
|
<div class="model-meta"> |
|
|
<span>Created: ${new Date(model.created_at).toLocaleString()}</span> |
|
|
${model.last_used_at ? `<span>Last used: ${new Date(model.last_used_at).toLocaleString()}</span>` : ''} |
|
|
<span>Uses: ${model.use_count || 0}</span> |
|
|
</div> |
|
|
</div> |
|
|
`).join(''); |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
openTestModel(modelId) { |
|
|
|
|
|
const select = document.getElementById('test-model-select'); |
|
|
select.innerHTML = this.registeredModels.map(m => |
|
|
`<option value="${m.model_id}" ${m.model_id === modelId ? 'selected' : ''}>${m.model_name}</option>` |
|
|
).join(''); |
|
|
|
|
|
|
|
|
document.getElementById('test-panel').style.display = 'block'; |
|
|
document.getElementById('test-panel').scrollIntoView({ behavior: 'smooth' }); |
|
|
}, |
|
|
|
|
|
async executeTest() { |
|
|
const modelId = document.getElementById('test-model-select').value; |
|
|
const endpoint = document.getElementById('test-endpoint').value.trim(); |
|
|
const payloadText = document.getElementById('test-payload').value.trim(); |
|
|
|
|
|
if (!modelId) { |
|
|
this.showError('Please select a model'); |
|
|
return; |
|
|
} |
|
|
|
|
|
let payload; |
|
|
try { |
|
|
payload = JSON.parse(payloadText || '{}'); |
|
|
} catch { |
|
|
this.showError('Invalid JSON payload'); |
|
|
return; |
|
|
} |
|
|
|
|
|
this.showInfo('Testing model...'); |
|
|
|
|
|
const resultDiv = document.getElementById('test-result'); |
|
|
resultDiv.innerHTML = '<div class="spinner"></div><p>Running test...</p>'; |
|
|
|
|
|
try { |
|
|
const response = await fetch( |
|
|
`${this.apiBase}/api/dynamic-models/models/${modelId}/use`, |
|
|
{ |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ endpoint, payload }) |
|
|
} |
|
|
); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
this.showSuccess(`Test completed in ${Math.round(data.data.response_time_ms)}ms`); |
|
|
resultDiv.innerHTML = ` |
|
|
<div class="success-banner">✅ Test Successful</div> |
|
|
<div><strong>Response Time:</strong> ${Math.round(data.data.response_time_ms)}ms</div> |
|
|
<div><strong>Response Data:</strong></div> |
|
|
<pre>${JSON.stringify(data.data.data, null, 2)}</pre> |
|
|
`; |
|
|
} else { |
|
|
this.showError('Test failed'); |
|
|
resultDiv.innerHTML = ` |
|
|
<div class="error-banner">❌ Test Failed</div> |
|
|
<div><strong>Error:</strong> ${data.error}</div> |
|
|
`; |
|
|
} |
|
|
} catch (error) { |
|
|
this.showError(`Test error: ${error.message}`); |
|
|
resultDiv.innerHTML = ` |
|
|
<div class="error-banner">❌ Error</div> |
|
|
<div>${error.message}</div> |
|
|
`; |
|
|
} |
|
|
}, |
|
|
|
|
|
viewModelDetails(modelId) { |
|
|
const model = this.registeredModels.find(m => m.model_id === modelId); |
|
|
if (!model) return; |
|
|
|
|
|
alert(` |
|
|
Model Details: |
|
|
-------------- |
|
|
ID: ${model.model_id} |
|
|
Name: ${model.model_name} |
|
|
API Type: ${model.api_type} |
|
|
Base URL: ${model.base_url} |
|
|
Created: ${new Date(model.created_at).toLocaleString()} |
|
|
Use Count: ${model.use_count || 0} |
|
|
Auto-detected: ${model.auto_detected ? 'Yes' : 'No'} |
|
|
|
|
|
Config: |
|
|
${JSON.stringify(model.config, null, 2)} |
|
|
|
|
|
Endpoints: |
|
|
${JSON.stringify(model.endpoints, null, 2)} |
|
|
`.trim()); |
|
|
}, |
|
|
|
|
|
async deleteModel(modelId) { |
|
|
if (!confirm(`Are you sure you want to delete model "${modelId}"?`)) { |
|
|
return; |
|
|
} |
|
|
|
|
|
try { |
|
|
const response = await fetch( |
|
|
`${this.apiBase}/api/dynamic-models/models/${modelId}`, |
|
|
{ method: 'DELETE' } |
|
|
); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
if (data.success) { |
|
|
this.showSuccess(`Model "${modelId}" deleted`); |
|
|
await this.refreshModelsList(); |
|
|
} else { |
|
|
this.showError('Failed to delete model'); |
|
|
} |
|
|
} catch (error) { |
|
|
this.showError(`Error: ${error.message}`); |
|
|
} |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
showSuccess(message) { |
|
|
this.showMessage(message, 'success'); |
|
|
}, |
|
|
|
|
|
showError(message) { |
|
|
this.showMessage(message, 'error'); |
|
|
}, |
|
|
|
|
|
showInfo(message) { |
|
|
this.showMessage(message, 'info'); |
|
|
}, |
|
|
|
|
|
showMessage(message, type = 'info') { |
|
|
const container = document.getElementById('status-messages'); |
|
|
const messageDiv = document.createElement('div'); |
|
|
messageDiv.className = `status-message ${type}`; |
|
|
messageDiv.textContent = message; |
|
|
|
|
|
container.appendChild(messageDiv); |
|
|
|
|
|
setTimeout(() => { |
|
|
messageDiv.remove(); |
|
|
}, 5000); |
|
|
}, |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
escapeHtml(text) { |
|
|
const div = document.createElement('div'); |
|
|
div.textContent = text; |
|
|
return div.innerHTML; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
if (document.readyState === 'loading') { |
|
|
document.addEventListener('DOMContentLoaded', () => dynamicLoader.init()); |
|
|
} else { |
|
|
dynamicLoader.init(); |
|
|
} |
|
|
|
|
|
|
|
|
window.dynamicLoader = dynamicLoader; |
|
|
|
|
|
|