makerstash/client/app.js

922 lines
30 KiB
JavaScript

// API Base URL
const API_BASE = '/api';
// State
let currentUser = null;
let authToken = null;
let allCollections = [];
let allTags = [];
// Initialize
document.addEventListener('DOMContentLoaded', () => {
checkAuth();
// loadAllModels, loadCollections, and loadTags will be called after auth check succeeds
});
// Auth Functions
function checkAuth() {
authToken = localStorage.getItem('authToken');
if (authToken) {
fetch(`${API_BASE}/auth/me`, {
headers: { 'Authorization': `Bearer ${authToken}` }
})
.then(res => res.json())
.then(data => {
if (data.user) {
currentUser = data.user;
// Apply user's theme preference
if (currentUser.theme && typeof setTheme === 'function') {
setTheme(currentUser.theme);
}
updateUIForAuth();
} else {
logout();
}
})
.catch(() => logout());
}
}
function updateUIForAuth() {
document.getElementById('loginBtn').style.display = 'none';
document.getElementById('registerBtn').style.display = 'none';
document.getElementById('userMenu').style.display = 'flex';
document.getElementById('username').textContent = currentUser.username;
document.getElementById('uploadBtn').style.display = 'block';
document.getElementById('createCollectionBtn').style.display = 'block';
// Load user's data after authentication
loadAllModels();
loadCollections();
loadTags();
}
async function handleLogin(event) {
event.preventDefault();
const username = document.getElementById('loginUsername').value;
const password = document.getElementById('loginPassword').value;
try {
const response = await fetch(`${API_BASE}/auth/login`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, password })
});
const data = await response.json();
if (response.ok) {
authToken = data.token;
currentUser = data.user;
localStorage.setItem('authToken', authToken);
closeModal('loginModal');
updateUIForAuth();
} else {
showNotification(data.error || 'Login failed', 'error');
}
} catch (error) {
showNotification('Login failed: ' + error.message, 'error');
}
}
async function handleRegister(event) {
event.preventDefault();
const username = document.getElementById('registerUsername').value;
const email = document.getElementById('registerEmail').value;
const password = document.getElementById('registerPassword').value;
try {
const response = await fetch(`${API_BASE}/auth/register`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username, email, password })
});
const data = await response.json();
if (response.ok) {
authToken = data.token;
currentUser = data.user;
localStorage.setItem('authToken', authToken);
closeModal('registerModal');
updateUIForAuth();
showNotification('Registration successful!', 'success');
} else {
showNotification(data.error || 'Registration failed', 'error');
}
} catch (error) {
showNotification('Registration failed: ' + error.message, 'error');
}
}
function logout() {
authToken = null;
currentUser = null;
localStorage.removeItem('authToken');
document.getElementById('loginBtn').style.display = 'block';
document.getElementById('registerBtn').style.display = 'block';
document.getElementById('userMenu').style.display = 'none';
document.getElementById('uploadBtn').style.display = 'none';
document.getElementById('createCollectionBtn').style.display = 'none';
// Clear models display
const grid = document.getElementById('modelsGrid');
grid.innerHTML = '<div class="loading-spinner"><p>Please log in to view models</p></div>';
// Clear sidebar sections
document.getElementById('printQueueSection').style.display = 'none';
document.getElementById('exportSection').style.display = 'none';
}
// Models Functions
async function loadAllModels(search = '', tag = '', collection = '') {
const grid = document.getElementById('modelsGrid');
grid.innerHTML = '<div class="loading-spinner"><i class="fas fa-spinner fa-spin"></i><p>Loading models...</p></div>';
try {
let url = `${API_BASE}/models?`;
if (search) url += `search=${encodeURIComponent(search)}&`;
if (tag) url += `tag=${encodeURIComponent(tag)}&`;
if (collection) url += `collection=${encodeURIComponent(collection)}&`;
const response = await fetch(url);
const data = await response.json();
if (data.models && data.models.length > 0) {
displayModels(data.models);
} else {
grid.innerHTML = '<div class="loading-spinner"><p>No models found</p></div>';
}
} catch (error) {
grid.innerHTML = '<div class="loading-spinner"><p>Error loading models</p></div>';
console.error('Error loading models:', error);
}
}
function displayModels(models) {
const grid = document.getElementById('modelsGrid');
grid.innerHTML = '';
models.forEach(model => {
const card = document.createElement('div');
card.className = 'model-card';
card.onclick = () => showModelDetails(model.id);
const fileIcon = getFileIcon(model.file_type);
const fileSize = formatFileSize(model.file_size);
const createdDate = new Date(model.created_at).toLocaleDateString();
// Check if thumbnail exists
const previewContent = model.preview_image
? `<img src="/${model.preview_image}" alt="${escapeHtml(model.name)}" />`
: `<i class="fas ${fileIcon}"></i>`;
// Check if 3D viewer is available
const has3DViewer = ['.stl', '.obj', '.3mf'].includes(model.file_type);
card.innerHTML = `
<div class="model-preview ${model.preview_image ? 'has-thumbnail' : ''}">
${previewContent}
${has3DViewer ? `
<button class="btn-3d-preview" onclick="event.stopPropagation(); open3DViewer(${model.id})" title="View in 3D">
<i class="fas fa-cube"></i>
</button>
` : ''}
</div>
<div class="model-info">
<h3>${escapeHtml(model.name)}</h3>
<p>${escapeHtml(model.description || 'No description')}</p>
<div class="model-meta">
<span><i class="fas fa-file"></i> ${fileSize}</span>
<span><i class="fas fa-calendar"></i> ${createdDate}</span>
</div>
${model.tags && model.tags.length > 0 ? `
<div class="model-tags">
${model.tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('')}
</div>
` : ''}
</div>
`;
grid.appendChild(card);
});
}
async function showModelDetails(modelId) {
try {
const response = await fetch(`${API_BASE}/models/${modelId}`);
const model = await response.json();
const detailsHtml = `
<div class="model-details-header">
<div>
<h2>${escapeHtml(model.name)}</h2>
${model.creator ? `<p>By ${escapeHtml(model.creator)}</p>` : ''}
</div>
<div class="model-details-actions">
${['.stl', '.obj', '.3mf'].includes(model.file_type) ? `
<button class="btn btn-primary" onclick="closeModal('modelModal'); open3DViewer(${model.id})">
<i class="fas fa-cube"></i> View in 3D
</button>
` : ''}
<a href="/api/models/${model.id}/download" class="btn btn-primary" download>
<i class="fas fa-download"></i> ${model.files && model.files.length > 0 ? 'Download All (ZIP)' : 'Download'}
</a>
${currentUser && currentUser.id === model.user_id ? `
<button class="btn btn-primary" onclick="showEditModal(${model.id})">
<i class="fas fa-edit"></i> Edit
</button>
<button class="btn btn-danger" onclick="deleteModel(${model.id})">
<i class="fas fa-trash"></i> Delete
</button>
` : ''}
</div>
</div>
<div class="model-details-content">
<div>
<div class="details-section">
<h3>Description</h3>
<p>${escapeHtml(model.description || 'No description available')}</p>
</div>
${model.notes ? `
<div class="details-section">
<h3>Notes</h3>
<p>${escapeHtml(model.notes)}</p>
</div>
` : ''}
<div class="details-section">
<h3>Tags</h3>
<div class="tags-container">
${model.tags && model.tags.length > 0
? model.tags.map(tag => `<span class="tag">${escapeHtml(tag)}</span>`).join('')
: '<span class="loading">No tags</span>'}
</div>
</div>
</div>
<div>
<div class="details-section">
<h3>File Information</h3>
<p><span class="details-label">Primary File:</span> ${escapeHtml(model.file_name)}</p>
<p><span class="details-label">File Type:</span> ${escapeHtml(model.file_type)}</p>
<p><span class="details-label">File Size:</span> ${formatFileSize(model.file_size)}</p>
<p><span class="details-label">Uploaded:</span> ${new Date(model.created_at).toLocaleString()}</p>
${model.files && model.files.length > 0 ? `
<div style="margin-top: 1rem;">
<p><strong>Additional Files (${model.files.length}):</strong></p>
<ul style="margin-left: 1.5rem; margin-top: 0.5rem;">
${model.files.map(file => `
<li style="margin-bottom: 0.5rem;">
${escapeHtml(file.file_name)}
<span style="color: var(--text-secondary);">(${formatFileSize(file.file_size)})</span>
</li>
`).join('')}
</ul>
</div>
` : ''}
</div>
${model.collection_name ? `
<div class="details-section">
<h3>Collection</h3>
<p>${escapeHtml(model.collection_name)}</p>
</div>
` : ''}
${model.source_url ? `
<div class="details-section">
<h3>Source</h3>
<p><a href="${escapeHtml(model.source_url)}" target="_blank">${escapeHtml(model.source_url)}</a></p>
</div>
` : ''}
<div class="details-section">
<h3>Support Status</h3>
<p>${model.is_supported ? 'Includes supports' : 'No supports'}</p>
</div>
</div>
</div>
`;
document.getElementById('modelDetails').innerHTML = detailsHtml;
showModal('modelModal');
} catch (error) {
showNotification('Error loading model details', 'error');
console.error('Error:', error);
}
}
async function handleUpload(event) {
event.preventDefault();
if (!authToken) {
showNotification('Please login to upload models', 'error');
return;
}
const formData = new FormData();
// Handle multiple files
const fileInput = document.getElementById('uploadFile');
const files = fileInput.files;
if (files.length === 0) {
showNotification('Please select at least one file', 'error');
return;
}
// Append all files
for (let i = 0; i < files.length; i++) {
formData.append('files', files[i]);
}
formData.append('name', document.getElementById('uploadName').value);
formData.append('description', document.getElementById('uploadDescription').value);
formData.append('creator', document.getElementById('uploadCreator').value);
formData.append('source_url', document.getElementById('uploadSourceUrl').value);
formData.append('collection_id', document.getElementById('uploadCollection').value);
formData.append('is_supported', document.getElementById('uploadSupported').checked);
formData.append('notes', document.getElementById('uploadNotes').value);
const tagsInput = document.getElementById('uploadTags').value;
if (tagsInput) {
const tags = tagsInput.split(',').map(t => t.trim()).filter(t => t);
formData.append('tags', JSON.stringify(tags));
}
try {
const response = await fetch(`${API_BASE}/models`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${authToken}` },
body: formData
});
const data = await response.json();
if (response.ok) {
closeModal('uploadModal');
const message = data.fileCount > 1
? `Model uploaded with ${data.fileCount} files!`
: 'Model uploaded successfully!';
showNotification(message, 'success');
loadAllModels();
event.target.reset();
} else {
showNotification(data.error || 'Upload failed', 'error');
}
} catch (error) {
showNotification('Upload failed: ' + error.message, 'error');
}
}
async function deleteModel(modelId) {
if (!confirm('Are you sure you want to delete this model?')) {
return;
}
try {
const response = await fetch(`${API_BASE}/models/${modelId}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${authToken}` }
});
if (response.ok) {
closeModal('modelModal');
showNotification('Model deleted successfully', 'success');
loadAllModels();
} else {
const data = await response.json();
showNotification(data.error || 'Delete failed', 'error');
}
} catch (error) {
showNotification('Delete failed: ' + error.message, 'error');
}
}
// Collections Functions
async function loadCollections() {
try {
const headers = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/collections`, { headers });
const data = await response.json();
allCollections = data.collections || [];
const collectionsList = document.getElementById('collectionsList');
if (data.collections && data.collections.length > 0) {
collectionsList.innerHTML = data.collections.map(collection => `
<li>
<a href="#" onclick="loadAllModels('', '', ${collection.id}); return false;">
<i class="fas fa-folder"></i> ${escapeHtml(collection.name)} (${collection.model_count})
</a>
</li>
`).join('');
} else {
collectionsList.innerHTML = '<li class="loading">No collections</li>';
}
// Update upload modal collection select
const uploadCollectionSelect = document.getElementById('uploadCollection');
uploadCollectionSelect.innerHTML = '<option value="">None</option>' +
(data.collections ? data.collections.map(c => `<option value="${c.id}">${escapeHtml(c.name)}</option>`).join('') : '');
} catch (error) {
console.error('Error loading collections:', error);
const collectionsList = document.getElementById('collectionsList');
if (collectionsList) {
collectionsList.innerHTML = '<li class="loading">Error loading collections</li>';
}
}
}
async function handleCreateCollection(event) {
event.preventDefault();
if (!authToken) {
showNotification('Please login to create collections', 'error');
return;
}
const name = document.getElementById('collectionName').value;
const description = document.getElementById('collectionDescription').value;
try {
const response = await fetch(`${API_BASE}/collections`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify({ name, description })
});
const data = await response.json();
if (response.ok) {
closeModal('createCollectionModal');
showNotification('Collection created successfully!', 'success');
loadCollections();
event.target.reset();
} else {
showNotification(data.error || 'Failed to create collection', 'error');
}
} catch (error) {
showNotification('Failed to create collection: ' + error.message, 'error');
}
}
function showCollections() {
loadCollections();
showNotification('Showing all collections', 'success');
}
// Tags Functions
async function loadTags() {
try {
const headers = {};
if (authToken) {
headers['Authorization'] = `Bearer ${authToken}`;
}
const response = await fetch(`${API_BASE}/tags`, { headers });
const data = await response.json();
allTags = data.tags || [];
const tagsList = document.getElementById('tagsList');
if (data.tags && data.tags.length > 0) {
const topTags = data.tags.sort((a, b) => b.model_count - a.model_count).slice(0, 10);
tagsList.innerHTML = topTags.map(tag => `
<span class="tag" style="background-color: ${tag.color}" onclick="loadAllModels('', '${escapeHtml(tag.name)}', '')">
${escapeHtml(tag.name)} (${tag.model_count})
</span>
`).join('');
} else {
tagsList.innerHTML = '<span class="loading">No tags</span>';
}
} catch (error) {
console.error('Error loading tags:', error);
const tagsList = document.getElementById('tagsList');
if (tagsList) {
tagsList.innerHTML = '<span class="loading">Error loading tags</span>';
}
}
}
function showTags() {
loadTags();
showNotification('Showing all tags', 'success');
}
// Search Function
let searchTimeout;
function handleSearch() {
clearTimeout(searchTimeout);
searchTimeout = setTimeout(() => {
const searchTerm = document.getElementById('searchInput').value;
loadAllModels(searchTerm);
}, 300);
}
// Modal Functions
function showModal(modalId) {
document.getElementById(modalId).style.display = 'block';
}
function closeModal(modalId) {
document.getElementById(modalId).style.display = 'none';
}
function showLoginModal() {
showModal('loginModal');
}
function showRegisterModal() {
showModal('registerModal');
}
function showUploadModal() {
if (!authToken) {
showNotification('Please login to upload models', 'error');
showLoginModal();
return;
}
showModal('uploadModal');
}
function showCreateCollectionModal() {
if (!authToken) {
showNotification('Please login to create collections', 'error');
showLoginModal();
return;
}
showModal('createCollectionModal');
}
function showSettingsModal() {
if (!authToken) {
showNotification('Please login to access settings', 'error');
showLoginModal();
return;
}
// Populate current email
document.getElementById('currentEmailDisplay').value = currentUser.email;
// Reset forms
document.getElementById('emailTab').querySelector('form').reset();
document.getElementById('passwordTab').querySelector('form').reset();
// Load connected accounts
loadConnectedAccounts();
// Show email tab by default
switchSettingsTab('email');
showModal('settingsModal');
}
function switchSettingsTab(tab) {
// Hide all tabs
document.querySelectorAll('.tab-content').forEach(el => el.classList.remove('active'));
document.querySelectorAll('.tab-button').forEach(el => el.classList.remove('active'));
// Show selected tab
document.getElementById(tab + 'Tab').classList.add('active');
event.target.classList.add('active');
}
function updateEmail(event) {
event.preventDefault();
const newEmail = document.getElementById('newEmail').value;
const password = document.getElementById('emailConfirmPassword').value;
fetch(`${API_BASE}/auth/me/email`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ newEmail, password })
})
.then(res => res.json())
.then(data => {
if (data.error) {
showNotification(data.error, 'error');
} else {
showNotification('Email updated successfully', 'success');
currentUser.email = newEmail;
document.getElementById('currentEmailDisplay').value = newEmail;
event.target.reset();
}
})
.catch(err => showNotification('Error updating email', 'error'));
}
function updatePassword(event) {
event.preventDefault();
const currentPassword = document.getElementById('currentPassword').value;
const newPassword = document.getElementById('newPassword').value;
const confirmNewPassword = document.getElementById('confirmNewPassword').value;
if (newPassword !== confirmNewPassword) {
showNotification('Passwords do not match', 'error');
return;
}
fetch(`${API_BASE}/auth/me/password`, {
method: 'PUT',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ currentPassword, newPassword })
})
.then(res => res.json())
.then(data => {
if (data.error) {
showNotification(data.error, 'error');
} else {
showNotification('Password updated successfully', 'success');
event.target.reset();
}
})
.catch(err => showNotification('Error updating password', 'error'));
}
// Connected Accounts Management
let availableSites = [];
async function loadConnectedAccounts() {
try {
const response = await fetch(`${API_BASE}/credentials/my-accounts`, {
headers: { 'Authorization': `Bearer ${authToken}` }
});
const data = await response.json();
availableSites = data.sites || [];
// Populate site selector
const siteSelect = document.getElementById('accountSiteSelect');
siteSelect.innerHTML = '<option value="">Select a website...</option>' +
availableSites.map(site => `<option value="${site.id}">${site.name}</option>`).join('');
// Display connected accounts
const accountsList = document.getElementById('connectedAccountsList');
if (data.accounts && data.accounts.length > 0) {
accountsList.innerHTML = data.accounts.map(account => {
const statusColor = account.is_connected ? '#00ae42' : '#dc3545';
const statusText = account.is_connected ? '✓ Connected' : '✗ Disconnected';
return `
<div style="border: 1px solid var(--border-color); padding: 1rem; border-radius: 4px; display: flex; justify-content: space-between; align-items: center;">
<div>
<h4 style="margin: 0 0 0.5rem 0;">${account.site_name}</h4>
<p style="margin: 0; font-size: 0.9rem; color: var(--text-secondary);">Username: ${account.username}</p>
<p style="margin: 0.25rem 0 0 0; font-size: 0.85rem; color: ${statusColor};">
${statusText}
</p>
</div>
<button class="btn btn-sm btn-danger" onclick="removeConnectedAccount('${account.site_name}')">Remove</button>
</div>
`;
}).join('');
} else {
accountsList.innerHTML = '<p style="color: var(--text-secondary); text-align: center;">No connected accounts yet</p>';
}
} catch (error) {
console.error('Error loading connected accounts:', error);
showNotification('Failed to load connected accounts', 'error');
}
}
function updateAccountForm() {
const siteSelect = document.getElementById('accountSiteSelect').value;
const formFields = document.getElementById('accountFormFields');
if (siteSelect) {
formFields.style.display = 'block';
// Clear fields
document.getElementById('accountUsername').value = '';
document.getElementById('accountPassword').value = '';
document.getElementById('accountApiKey').value = '';
document.getElementById('accountAccessToken').value = '';
} else {
formFields.style.display = 'none';
}
}
async function addConnectedAccount() {
const siteName = document.getElementById('accountSiteSelect').value;
const username = document.getElementById('accountUsername').value;
const password = document.getElementById('accountPassword').value;
const apiKey = document.getElementById('accountApiKey').value;
const accessToken = document.getElementById('accountAccessToken').value;
if (!siteName || (!username && !apiKey && !accessToken)) {
showNotification('Please provide at least a username or API key', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/credentials/connect`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${authToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ siteName, username, password, apiKey, accessToken })
});
const data = await response.json();
if (response.ok) {
showNotification(data.message, 'success');
await loadConnectedAccounts();
} else {
showNotification(data.error || 'Failed to add account', 'error');
}
} catch (error) {
showNotification('Error adding account: ' + error.message, 'error');
}
}
async function removeConnectedAccount(siteName) {
if (!confirm(`Remove ${siteName} account?`)) return;
try {
const response = await fetch(`${API_BASE}/credentials/disconnect/${siteName}`, {
method: 'DELETE',
headers: { 'Authorization': `Bearer ${authToken}` }
});
const data = await response.json();
if (response.ok) {
showNotification(data.message, 'success');
await loadConnectedAccounts();
} else {
showNotification(data.error || 'Failed to remove account', 'error');
}
} catch (error) {
showNotification('Error removing account: ' + error.message, 'error');
}
}
async function testAccountConnection() {
const siteName = document.getElementById('accountSiteSelect').value;
if (!siteName) {
showNotification('Please select a website', 'error');
return;
}
try {
const response = await fetch(`${API_BASE}/credentials/test-connection/${siteName}`, {
method: 'POST',
headers: { 'Authorization': `Bearer ${authToken}` }
});
const data = await response.json();
if (response.ok) {
showNotification(data.message, 'success');
} else {
showNotification(data.error || 'Connection test failed', 'error');
}
} catch (error) {
showNotification('Error testing connection: ' + error.message, 'error');
}
}
// Close modals when clicking outside
window.onclick = function(event) {
if (event.target.classList.contains('modal')) {
event.target.style.display = 'none';
}
}
// Utility Functions
function escapeHtml(text) {
const map = {
'&': '&amp;',
'<': '&lt;',
'>': '&gt;',
'"': '&quot;',
"'": '&#039;'
};
return text ? String(text).replace(/[&<>"']/g, m => map[m]) : '';
}
function formatFileSize(bytes) {
if (!bytes) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return Math.round(bytes / Math.pow(k, i) * 100) / 100 + ' ' + sizes[i];
}
function getFileIcon(fileType) {
const iconMap = {
'.stl': 'fa-cube',
'.obj': 'fa-cubes',
'.3mf': 'fa-box',
'.gcode': 'fa-code',
'.zip': 'fa-file-archive'
};
return iconMap[fileType] || 'fa-file';
}
function showNotification(message, type) {
// Simple console notification for now
console.log(`${type.toUpperCase()}: ${message}`);
// You can implement a toast notification system here
alert(message);
}
// Edit Model Functions
async function showEditModal(modelId) {
try {
// Fetch model details
const response = await fetch(`${API_BASE}/models/${modelId}`);
const model = await response.json();
// Populate form fields
document.getElementById('editModelId').value = model.id;
document.getElementById('editModelName').value = model.name || '';
document.getElementById('editModelDescription').value = model.description || '';
document.getElementById('editModelCreator').value = model.creator || '';
document.getElementById('editModelSourceUrl').value = model.source_url || '';
document.getElementById('editModelNotes').value = model.notes || '';
document.getElementById('editModelSupported').checked = model.is_supported === 1;
// Populate tags
if (model.tags && model.tags.length > 0) {
document.getElementById('editModelTags').value = model.tags.join(', ');
} else {
document.getElementById('editModelTags').value = '';
}
// Populate collection dropdown
const collectionSelect = document.getElementById('editModelCollection');
collectionSelect.innerHTML = '<option value="">None</option>' +
allCollections.map(c =>
`<option value="${c.id}" ${c.id === model.collection_id ? 'selected' : ''}>${escapeHtml(c.name)}</option>`
).join('');
// Close model details modal and show edit modal
closeModal('modelModal');
showModal('editModelModal');
} catch (error) {
console.error('Error loading model for edit:', error);
showNotification('Failed to load model details', 'error');
}
}
async function handleEditModel(event) {
event.preventDefault();
if (!authToken) {
showNotification('Please login to edit models', 'error');
return;
}
const modelId = document.getElementById('editModelId').value;
const tagsInput = document.getElementById('editModelTags').value;
const tags = tagsInput ? tagsInput.split(',').map(t => t.trim()).filter(t => t) : [];
const updateData = {
name: document.getElementById('editModelName').value,
description: document.getElementById('editModelDescription').value,
creator: document.getElementById('editModelCreator').value,
source_url: document.getElementById('editModelSourceUrl').value,
collection_id: document.getElementById('editModelCollection').value || null,
is_supported: document.getElementById('editModelSupported').checked,
notes: document.getElementById('editModelNotes').value,
tags: tags
};
try {
const response = await fetch(`${API_BASE}/models/${modelId}`, {
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${authToken}`
},
body: JSON.stringify(updateData)
});
const data = await response.json();
if (response.ok) {
closeModal('editModelModal');
showNotification('Model updated successfully!', 'success');
loadAllModels(); // Refresh the models list
} else {
showNotification(data.error || 'Failed to update model', 'error');
}
} catch (error) {
showNotification('Failed to update model: ' + error.message, 'error');
}
}