qwen_agent/public/model-manager.html
2026-01-16 23:05:30 +08:00

1071 lines
36 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>模型管理</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@300;400;500;600;700&family=Poppins:wght@400;500;600;700&display=swap" rel="stylesheet">
<!-- Lucide Icons -->
<script src="https://unpkg.com/lucide@latest/dist/umd/lucide.js"></script>
<style>
:root {
--primary: #2563EB;
--primary-hover: #1D4ED8;
--secondary: #3B82F6;
--background: #F8FAFC;
--surface: #FFFFFF;
--text: #1E293B;
--text-muted: #64748B;
--border: #E2E8F0;
--success: #10B981;
--warning: #F59E0B;
--error: #EF4444;
--glass-bg: rgba(255, 255, 255, 0.8);
--glass-blur: 12px;
}
.dark {
--primary: #3B82F6;
--primary-hover: #60A5FA;
--secondary: #60A5FA;
--background: #0F172A;
--surface: #1E293B;
--text: #F1F5F9;
--text-muted: #94A3B8;
--border: #334155;
--glass-bg: rgba(30, 41, 59, 0.8);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Open Sans', sans-serif;
background: var(--background);
color: var(--text);
min-height: 100vh;
transition: background-color 0.3s ease, color 0.3s ease;
}
/* ===== Header ===== */
.header {
height: 64px;
background: var(--glass-bg);
backdrop-filter: blur(var(--glass-blur));
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 24px;
position: sticky;
top: 0;
z-index: 10;
}
.header-left {
display: flex;
align-items: center;
gap: 12px;
}
.back-btn {
width: 38px;
height: 38px;
border-radius: 10px;
border: none;
background: transparent;
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
}
.back-btn:hover {
background: var(--background);
}
.header-logo {
width: 36px;
height: 36px;
border-radius: 10px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
}
.header-title {
font-family: 'Poppins', sans-serif;
font-size: 18px;
font-weight: 600;
}
.header-right {
display: flex;
align-items: center;
gap: 12px;
}
.header-btn {
width: 38px;
height: 38px;
border-radius: 10px;
border: none;
background: transparent;
color: var(--text);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.15s ease;
}
.header-btn:hover {
background: var(--background);
}
.header-btn.primary {
background: var(--primary);
color: white;
padding: 0 16px;
width: auto;
gap: 8px;
}
.header-btn.primary:hover {
background: var(--primary-hover);
}
/* ===== Main Content ===== */
.main-content {
max-width: 900px;
margin: 0 auto;
padding: 32px 24px;
}
.page-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 24px;
}
.page-title {
font-family: 'Poppins', sans-serif;
font-size: 24px;
font-weight: 600;
}
.page-subtitle {
font-size: 14px;
color: var(--text-muted);
margin-top: 4px;
}
/* ===== Model List ===== */
.model-list {
display: flex;
flex-direction: column;
gap: 16px;
}
.model-card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: 16px;
padding: 20px;
transition: all 0.2s ease;
}
.model-card:hover {
border-color: var(--primary);
box-shadow: 0 4px 20px rgba(37, 99, 235, 0.1);
}
.model-card-header {
display: flex;
align-items: flex-start;
gap: 14px;
margin-bottom: 16px;
}
.model-card-icon {
width: 48px;
height: 48px;
border-radius: 12px;
background: linear-gradient(135deg, var(--primary), var(--secondary));
display: flex;
align-items: center;
justify-content: center;
color: white;
flex-shrink: 0;
}
.model-card-info {
flex: 1;
min-width: 0;
}
.model-card-name {
font-family: 'Poppins', sans-serif;
font-size: 16px;
font-weight: 600;
margin-bottom: 6px;
}
.model-card-provider {
font-size: 12px;
color: var(--text-muted);
background: var(--background);
padding: 4px 8px;
border-radius: 6px;
display: inline-block;
}
.model-card-details {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 16px;
padding: 14px;
background: var(--background);
border-radius: 10px;
}
.model-detail-item {
font-size: 13px;
}
.model-detail-label {
color: var(--text-muted);
margin-bottom: 4px;
}
.model-detail-value {
font-family: 'Monaco', 'Consolas', monospace;
word-break: break-all;
}
.model-card-actions {
display: flex;
gap: 10px;
padding-top: 14px;
border-top: 1px solid var(--border);
}
.model-card-action {
flex: 1;
padding: 10px 14px;
border-radius: 10px;
border: 1px solid var(--border);
background: transparent;
color: var(--text);
font-size: 13px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
transition: all 0.15s ease;
}
.model-card-action:hover {
background: var(--background);
border-color: var(--primary);
}
.model-card-action.set-default {
background: rgba(37, 99, 235, 0.1);
border-color: var(--primary);
color: var(--primary);
}
.model-card-action.delete:hover {
background: rgba(239, 68, 68, 0.1);
border-color: var(--error);
color: var(--error);
}
.model-card-action.default {
background: var(--success);
border-color: var(--success);
color: white;
}
/* ===== Empty State ===== */
.empty-state {
text-align: center;
padding: 80px 20px;
}
.empty-state-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
border-radius: 20px;
background: var(--background);
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
}
.empty-state-title {
font-family: 'Poppins', sans-serif;
font-size: 20px;
font-weight: 600;
margin-bottom: 8px;
}
.empty-state-subtitle {
font-size: 14px;
color: var(--text-muted);
margin-bottom: 24px;
}
/* ===== Modal ===== */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: none;
align-items: center;
justify-content: center;
z-index: 100;
}
.modal-overlay.active {
display: flex;
}
.modal {
background: var(--surface);
border-radius: 16px;
width: 90%;
max-width: 540px;
max-height: 85vh;
overflow: hidden;
display: flex;
flex-direction: column;
box-shadow: 0 20px 40px rgba(0, 0, 0, 0.2);
}
.modal-header {
padding: 20px 24px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
justify-content: space-between;
}
.modal-title {
font-family: 'Poppins', sans-serif;
font-size: 18px;
font-weight: 600;
}
.modal-close {
width: 32px;
height: 32px;
border-radius: 8px;
border: none;
background: transparent;
color: var(--text-muted);
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
.modal-close:hover {
background: var(--background);
color: var(--text);
}
.modal-body {
padding: 24px;
overflow-y: auto;
}
.form-group {
margin-bottom: 18px;
}
.form-group:last-child {
margin-bottom: 0;
}
.form-label {
display: block;
font-size: 13px;
font-weight: 500;
color: var(--text-muted);
margin-bottom: 8px;
}
.form-input,
.form-select {
width: 100%;
padding: 12px 14px;
border: 1px solid var(--border);
border-radius: 10px;
font-size: 14px;
background: var(--background);
color: var(--text);
outline: none;
transition: border-color 0.15s ease;
}
.form-input:focus,
.form-select:focus {
border-color: var(--primary);
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.1);
}
.form-input::placeholder {
color: var(--text-muted);
}
.form-hint {
font-size: 12px;
color: var(--text-muted);
margin-top: 6px;
}
.modal-footer {
padding: 16px 24px;
border-top: 1px solid var(--border);
display: flex;
justify-content: flex-end;
gap: 10px;
}
.btn {
padding: 10px 20px;
border-radius: 8px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
transition: all 0.15s ease;
}
.btn-secondary {
background: var(--background);
border: 1px solid var(--border);
color: var(--text);
}
.btn-secondary:hover {
border-color: var(--primary);
}
.btn-primary {
background: var(--primary);
border: none;
color: white;
}
.btn-primary:hover {
background: var(--primary-hover);
}
/* ===== Delete Confirmation Modal ===== */
.delete-confirm-content {
text-align: center;
padding: 20px 0;
}
.delete-confirm-icon {
width: 56px;
height: 56px;
margin: 0 auto 16px;
border-radius: 50%;
background: rgba(239, 68, 68, 0.1);
display: flex;
align-items: center;
justify-content: center;
color: var(--error);
}
.delete-confirm-title {
font-family: 'Poppins', sans-serif;
font-size: 18px;
font-weight: 600;
margin-bottom: 8px;
}
.delete-confirm-text {
font-size: 14px;
color: var(--text-muted);
}
/* ===== Checkbox ===== */
.checkbox-wrapper {
display: flex;
align-items: center;
gap: 8px;
padding: 12px;
background: var(--background);
border-radius: 10px;
border: 1px solid var(--border);
cursor: pointer;
transition: all 0.15s ease;
}
.checkbox-wrapper:hover {
border-color: var(--primary);
}
.checkbox-wrapper input[type="checkbox"] {
width: 18px;
height: 18px;
cursor: pointer;
}
.checkbox-wrapper label {
flex: 1;
cursor: pointer;
font-size: 14px;
}
/* ===== Scrollbar ===== */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: var(--border);
border-radius: 3px;
}
::-webkit-scrollbar-thumb:hover {
background: var(--text-muted);
}
/* ===== Responsive ===== */
@media (max-width: 768px) {
.header {
padding: 0 16px;
}
.main-content {
padding: 20px 16px;
}
.model-card-details {
grid-template-columns: 1fr;
}
.model-card-actions {
flex-wrap: wrap;
}
.model-card-action {
flex: 1 1 calc(50% - 5px);
min-width: 120px;
}
.page-header {
flex-direction: column;
align-items: flex-start;
gap: 16px;
}
.header-btn.primary span {
display: none;
}
}
</style>
</head>
<body>
<!-- Header -->
<header class="header">
<div class="header-left">
<button class="back-btn" id="back-btn" title="返回">
<i data-lucide="arrow-left" style="width: 20px; height: 20px;"></i>
</button>
<div class="header-logo">
<i data-lucide="cpu" style="width: 20px; height: 20px;"></i>
</div>
<div>
<div class="header-title">模型管理</div>
</div>
</div>
<div class="header-right">
<button class="header-btn" id="theme-toggle" title="切换主题">
<i data-lucide="moon" style="width: 18px; height: 18px;"></i>
</button>
<button class="header-btn primary" id="add-model-btn">
<i data-lucide="plus" style="width: 16px; height: 16px;"></i>
<span>添加模型</span>
</button>
</div>
</header>
<!-- Main Content -->
<main class="main-content">
<div class="page-header">
<div>
<h1 class="page-title">我的模型</h1>
<p class="page-subtitle">管理您的大语言模型配置</p>
</div>
</div>
<!-- Model List -->
<div class="model-list" id="model-list">
<!-- 动态生成 -->
</div>
</main>
<!-- Add/Edit Model Modal -->
<div class="modal-overlay" id="model-modal">
<div class="modal">
<div class="modal-header">
<h3 class="modal-title" id="model-modal-title">添加模型</h3>
<button class="modal-close" id="model-modal-close">
<i data-lucide="x" style="width: 18px; height: 18px;"></i>
</button>
</div>
<div class="modal-body">
<div class="form-group">
<label class="form-label" for="model-name">模型名称 *</label>
<input type="text" id="model-name" class="form-input" placeholder="例如GPT-4、Claude 3.5">
</div>
<div class="form-group">
<label class="form-label" for="model-provider">服务提供商</label>
<select id="model-provider" class="form-select">
<option value="OpenAI">OpenAI</option>
<option value="Anthropic">Anthropic</option>
<option value="通义千问">通义千问</option>
<option value="DeepSeek">DeepSeek</option>
<option value="OpenRouter">OpenRouter</option>
<option value="其他">其他</option>
</select>
</div>
<div class="form-group">
<label class="form-label" for="model-model">模型标识符 *</label>
<input type="text" id="model-model" class="form-input" placeholder="例如gpt-4、claude-3-5-sonnet-20241022">
<p class="form-hint">用于 API 调用的模型名称</p>
</div>
<div class="form-group">
<label class="form-label" for="model-server">模型服务器地址</label>
<input type="text" id="model-server" class="form-input" placeholder="例如https://api.openai.com/v1">
<p class="form-hint">API 服务器的基础 URL</p>
</div>
<div class="form-group">
<label class="form-label" for="model-api-key">API Key</label>
<input type="password" id="model-api-key" class="form-input" placeholder="输入您的 API Key">
<p class="form-hint">您的 API 访问密钥</p>
</div>
<div class="form-group">
<label class="checkbox-wrapper">
<input type="checkbox" id="model-is-default">
<label for="model-is-default">设为默认模型</label>
</label>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="model-modal-cancel">取消</button>
<button class="btn btn-primary" id="model-modal-save">保存</button>
</div>
</div>
</div>
<!-- Delete Confirmation Modal -->
<div class="modal-overlay" id="delete-modal">
<div class="modal" style="max-width: 400px;">
<div class="modal-body">
<div class="delete-confirm-content">
<div class="delete-confirm-icon">
<i data-lucide="alert-triangle" style="width: 28px; height: 28px;"></i>
</div>
<h3 class="delete-confirm-title">确认删除</h3>
<p class="delete-confirm-text">确定要删除这个模型配置吗?此操作无法撤销。</p>
</div>
</div>
<div class="modal-footer">
<button class="btn btn-secondary" id="delete-modal-cancel">取消</button>
<button class="btn btn-primary" id="delete-modal-confirm" style="background: var(--error);">删除</button>
</div>
</div>
</div>
<script>
// Initialize Lucide icons
lucide.createIcons();
class ModelManager {
constructor() {
this.elements = {
themeToggle: document.getElementById('theme-toggle'),
backBtn: document.getElementById('back-btn'),
addModelBtn: document.getElementById('add-model-btn'),
modelList: document.getElementById('model-list'),
// Model Modal
modelModal: document.getElementById('model-modal'),
modelModalTitle: document.getElementById('model-modal-title'),
modelModalClose: document.getElementById('model-modal-close'),
modelModalCancel: document.getElementById('model-modal-cancel'),
modelModalSave: document.getElementById('model-modal-save'),
modelName: document.getElementById('model-name'),
modelProvider: document.getElementById('model-provider'),
modelModel: document.getElementById('model-model'),
modelServer: document.getElementById('model-server'),
modelApiKey: document.getElementById('model-api-key'),
modelIsDefault: document.getElementById('model-is-default'),
// Delete Modal
deleteModal: document.getElementById('delete-modal'),
deleteModalCancel: document.getElementById('delete-modal-cancel'),
deleteModalConfirm: document.getElementById('delete-modal-confirm')
};
this.models = [];
this.editingModelId = null;
this.deletingModelId = null;
this.initializeEventListeners();
this.loadTheme();
this.loadModels();
}
initializeEventListeners() {
// Theme toggle
this.elements.themeToggle.addEventListener('click', () => this.toggleTheme());
// Back button
this.elements.backBtn.addEventListener('click', () => {
window.history.back();
});
// Add model
this.elements.addModelBtn.addEventListener('click', () => this.openModelModal());
// Model modal
this.elements.modelModalClose.addEventListener('click', () => this.closeModelModal());
this.elements.modelModalCancel.addEventListener('click', () => this.closeModelModal());
this.elements.modelModalSave.addEventListener('click', () => this.saveModel());
this.elements.modelModal.addEventListener('click', (e) => {
if (e.target === this.elements.modelModal) this.closeModelModal();
});
// Delete modal
this.elements.deleteModalCancel.addEventListener('click', () => this.closeDeleteModal());
this.elements.deleteModalConfirm.addEventListener('click', () => this.confirmDelete());
}
loadTheme() {
const theme = localStorage.getItem('theme') || 'light';
if (theme === 'dark') {
document.documentElement.classList.add('dark');
}
this.updateThemeIcon(theme === 'dark');
}
toggleTheme() {
document.documentElement.classList.toggle('dark');
const isDark = document.documentElement.classList.contains('dark');
localStorage.setItem('theme', isDark ? 'dark' : 'light');
this.updateThemeIcon(isDark);
}
updateThemeIcon(isDark) {
const icon = this.elements.themeToggle.querySelector('i');
if (!icon) return;
icon.setAttribute('data-lucide', isDark ? 'sun' : 'moon');
lucide.createIcons();
}
loadModels() {
const stored = localStorage.getItem('model-list');
if (stored) {
try {
this.models = JSON.parse(stored);
} catch (e) {
this.models = [];
}
}
this.renderModels();
}
saveModels() {
localStorage.setItem('model-list', JSON.stringify(this.models));
}
renderModels() {
const list = this.elements.modelList;
if (this.models.length === 0) {
list.innerHTML = `
<div class="empty-state">
<div class="empty-state-icon">
<i data-lucide="cpu" style="width: 40px; height: 40px;"></i>
</div>
<h2 class="empty-state-title">暂无模型配置</h2>
<p class="empty-state-subtitle">点击上方按钮添加您的第一个模型</p>
</div>
`;
lucide.createIcons();
return;
}
list.innerHTML = '';
this.models.forEach(model => {
const card = this.createModelCard(model);
list.appendChild(card);
});
lucide.createIcons();
}
createModelCard(model) {
const card = document.createElement('div');
card.className = 'model-card';
const isDefault = model.isDefault;
// Mask API key for display
const maskApiKey = (key) => {
if (!key) return '未设置';
if (key.length <= 8) return '*'.repeat(key.length);
return key.substring(0, 4) + '*'.repeat(key.length - 8) + key.substring(key.length - 4);
};
card.innerHTML = `
<div class="model-card-header">
<div class="model-card-icon">
<i data-lucide="cpu" style="width: 24px; height: 24px;"></i>
</div>
<div class="model-card-info">
<div class="model-card-name">${this.escapeHtml(model.name)} ${isDefault ? '<span style="color: var(--success); font-size: 12px;">(默认)</span>' : ''}</div>
<span class="model-card-provider">${this.escapeHtml(model.provider || '自定义')}</span>
</div>
</div>
<div class="model-card-details">
<div class="model-detail-item">
<div class="model-detail-label">模型标识</div>
<div class="model-detail-value">${this.escapeHtml(model.model)}</div>
</div>
<div class="model-detail-item">
<div class="model-detail-label">服务器</div>
<div class="model-detail-value">${this.escapeHtml(model.server || '默认')}</div>
</div>
<div class="model-detail-item">
<div class="model-detail-label">API Key</div>
<div class="model-detail-value">${maskApiKey(model.apiKey)}</div>
</div>
</div>
<div class="model-card-actions">
${!isDefault ? `
<button class="model-card-action set-default" data-action="set-default" data-model-id="${model.id}">
<i data-lucide="star" style="width: 14px; height: 14px;"></i>
设为默认
</button>
` : `
<button class="model-card-action default" disabled>
<i data-lucide="check" style="width: 14px; height: 14px;"></i>
默认模型
</button>
`}
<button class="model-card-action" data-action="edit" data-model-id="${model.id}">
<i data-lucide="pencil" style="width: 14px; height: 14px;"></i>
编辑
</button>
<button class="model-card-action delete" data-action="delete" data-model-id="${model.id}">
<i data-lucide="trash-2" style="width: 14px; height: 14px;"></i>
删除
</button>
</div>
`;
// Event listeners
card.addEventListener('click', (e) => {
const action = e.target.closest('[data-action]')?.dataset.action;
const modelId = e.target.closest('[data-model-id]')?.dataset.modelId;
if (action === 'set-default' && modelId) {
e.stopPropagation();
this.setDefaultModel(modelId);
} else if (action === 'edit' && modelId) {
e.stopPropagation();
this.openEditModal(modelId);
} else if (action === 'delete' && modelId) {
e.stopPropagation();
this.openDeleteModal(modelId);
}
});
return card;
}
setDefaultModel(modelId) {
this.models.forEach(m => m.isDefault = (m.id === modelId));
this.saveModels();
this.renderModels();
}
openModelModal() {
this.editingModelId = null;
this.elements.modelModalTitle.textContent = '添加模型';
this.elements.modelName.value = '';
this.elements.modelProvider.value = 'OpenAI';
this.elements.modelModel.value = '';
this.elements.modelServer.value = '';
this.elements.modelApiKey.value = '';
this.elements.modelIsDefault.checked = this.models.length === 0;
this.elements.modelModal.classList.add('active');
this.elements.modelName.focus();
}
openEditModal(modelId) {
const model = this.models.find(m => m.id === modelId);
if (!model) return;
this.editingModelId = modelId;
this.elements.modelModalTitle.textContent = '编辑模型';
this.elements.modelName.value = model.name;
this.elements.modelProvider.value = model.provider || 'OpenAI';
this.elements.modelModel.value = model.model;
this.elements.modelServer.value = model.server || '';
this.elements.modelApiKey.value = model.apiKey || '';
this.elements.modelIsDefault.checked = model.isDefault || false;
this.elements.modelModal.classList.add('active');
this.elements.modelName.focus();
}
closeModelModal() {
this.elements.modelModal.classList.remove('active');
this.editingModelId = null;
}
saveModel() {
const name = this.elements.modelName.value.trim();
const model = this.elements.modelModel.value.trim();
const provider = this.elements.modelProvider.value;
const server = this.elements.modelServer.value.trim();
const apiKey = this.elements.modelApiKey.value.trim();
const isDefault = this.elements.modelIsDefault.checked;
if (!name) {
alert('请输入模型名称');
return;
}
if (!model) {
alert('请输入模型标识符');
return;
}
const now = Date.now();
if (this.editingModelId) {
// 编辑现有模型
const modelConfig = this.models.find(m => m.id === this.editingModelId);
if (modelConfig) {
modelConfig.name = name;
modelConfig.provider = provider;
modelConfig.model = model;
modelConfig.server = server;
modelConfig.apiKey = apiKey;
modelConfig.isDefault = isDefault;
modelConfig.updatedAt = now;
}
// 如果设为默认,取消其他模型的默认状态
if (isDefault) {
this.models.forEach(m => {
if (m.id !== this.editingModelId) m.isDefault = false;
});
}
} else {
// 添加新模型
// 如果设为默认,取消其他模型的默认状态
if (isDefault) {
this.models.forEach(m => m.isDefault = false);
}
const newModel = {
id: this.generateUUID(),
name: name,
provider: provider,
model: model,
server: server,
apiKey: apiKey,
isDefault: isDefault || this.models.length === 0,
createdAt: now,
updatedAt: now
};
this.models.unshift(newModel);
}
this.saveModels();
this.renderModels();
this.closeModelModal();
}
openDeleteModal(modelId) {
this.deletingModelId = modelId;
this.elements.deleteModal.classList.add('active');
}
closeDeleteModal() {
this.elements.deleteModal.classList.remove('active');
this.deletingModelId = null;
}
confirmDelete() {
if (!this.deletingModelId) return;
const deletingModel = this.models.find(m => m.id === this.deletingModelId);
this.models = this.models.filter(m => m.id !== this.deletingModelId);
// 如果删除的是默认模型,设置第一个为默认
if (deletingModel?.isDefault && this.models.length > 0) {
this.models[0].isDefault = true;
}
this.saveModels();
this.renderModels();
this.closeDeleteModal();
}
generateUUID() {
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
const r = Math.random() * 16 | 0;
const v = c === 'x' ? r : (r & 0x3 | 0x8);
return v.toString(16);
});
}
escapeHtml(text) {
const div = document.createElement('div');
div.textContent = text;
return div.innerHTML;
}
}
// Initialize app
document.addEventListener('DOMContentLoaded', () => {
new ModelManager();
});
</script>
</body>
</html>