Add copy mode switching, refactor copy section UI, and harden XSS handling
This commit is contained in:
+198
-22
@@ -75,24 +75,31 @@
|
||||
}
|
||||
|
||||
.ml-loaded-filters {
|
||||
padding: var(--ml-spacing-12) var(--ml-spacing-20) var(--ml-spacing-6);
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.ml-loaded-filter-row {
|
||||
display: flex;
|
||||
gap: var(--ml-spacing-8);
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ml-btn-filter,
|
||||
#model-linker-modal .ml-btn-filter {
|
||||
.ml-copy-mode,
|
||||
.ml-btn-copy-compact,
|
||||
.ml-copy-main-btn,
|
||||
#model-linker-modal .ml-btn-filter,
|
||||
#model-linker-modal .ml-copy-mode,
|
||||
#model-linker-modal .ml-btn-copy-compact,
|
||||
#model-linker-modal .ml-copy-main-btn {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: var(--ml-spacing-5);
|
||||
min-height: var(--ml-missing-search-button-height, 34px);
|
||||
height: var(--ml-missing-search-button-height, 34px);
|
||||
min-height: 30px;
|
||||
height: 30px;
|
||||
padding: 0 12px;
|
||||
color: var(--ml-text);
|
||||
background: var(--ml-missing-control-bg, rgba(255,255,255,0.035));
|
||||
@@ -110,7 +117,13 @@
|
||||
}
|
||||
|
||||
.ml-btn-filter:hover:not(:disabled),
|
||||
#model-linker-modal .ml-btn-filter:hover:not(:disabled) {
|
||||
.ml-copy-mode:hover:not(:disabled),
|
||||
.ml-btn-copy-compact:hover:not(:disabled),
|
||||
.ml-copy-main-btn:hover:not(:disabled),
|
||||
#model-linker-modal .ml-btn-filter:hover:not(:disabled),
|
||||
#model-linker-modal .ml-copy-mode:hover:not(:disabled),
|
||||
#model-linker-modal .ml-btn-copy-compact:hover:not(:disabled),
|
||||
#model-linker-modal .ml-copy-main-btn:hover:not(:disabled) {
|
||||
color: var(--ml-text);
|
||||
background: var(--ml-missing-control-bg-hover, rgba(255,255,255,0.06));
|
||||
border-color: var(--ml-missing-control-border-hover, rgba(255,255,255,0.17));
|
||||
@@ -118,21 +131,31 @@
|
||||
}
|
||||
|
||||
.ml-btn-filter:focus-visible,
|
||||
#model-linker-modal .ml-btn-filter:focus-visible {
|
||||
.ml-copy-mode:focus-visible,
|
||||
.ml-btn-copy-compact:focus-visible,
|
||||
.ml-copy-main-btn:focus-visible,
|
||||
#model-linker-modal .ml-btn-filter:focus-visible,
|
||||
#model-linker-modal .ml-copy-mode:focus-visible,
|
||||
#model-linker-modal .ml-btn-copy-compact:focus-visible,
|
||||
#model-linker-modal .ml-copy-main-btn:focus-visible {
|
||||
outline: none;
|
||||
border-color: var(--ml-missing-control-focus-border, rgba(114,211,138,0.30));
|
||||
box-shadow: 0 0 0 2px var(--ml-missing-control-focus-ring, rgba(114,211,138,0.08));
|
||||
}
|
||||
|
||||
.ml-btn-filter.active,
|
||||
#model-linker-modal .ml-btn-filter.active {
|
||||
.ml-copy-mode.active,
|
||||
#model-linker-modal .ml-btn-filter.active,
|
||||
#model-linker-modal .ml-copy-mode.active {
|
||||
color: #dffbe5;
|
||||
background: rgba(114,211,138,0.13);
|
||||
border-color: rgba(114,211,138,0.32);
|
||||
}
|
||||
|
||||
.ml-btn-filter.active:hover:not(:disabled),
|
||||
#model-linker-modal .ml-btn-filter.active:hover:not(:disabled) {
|
||||
.ml-copy-mode.active:hover:not(:disabled),
|
||||
#model-linker-modal .ml-btn-filter.active:hover:not(:disabled),
|
||||
#model-linker-modal .ml-copy-mode.active:hover:not(:disabled) {
|
||||
background: rgba(114,211,138,0.17);
|
||||
border-color: rgba(114,211,138,0.42);
|
||||
}
|
||||
@@ -143,23 +166,74 @@
|
||||
|
||||
.ml-model-section {
|
||||
margin-bottom: var(--ml-spacing-8);
|
||||
padding: var(--ml-spacing-8);
|
||||
padding: var(--ml-spacing-10);
|
||||
background: var(--ml-card-bg-alt, #252525);
|
||||
border: 1px solid rgba(255, 255, 255, 0.055);
|
||||
border-radius: var(--ml-ui-radius-sm);
|
||||
}
|
||||
|
||||
.ml-model-section-header {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ml-spacing-10);
|
||||
margin-bottom: var(--ml-spacing-6);
|
||||
padding-bottom: var(--ml-spacing-5);
|
||||
border-bottom: 1px solid var(--ml-color-gray-900);
|
||||
}
|
||||
|
||||
.ml-model-section-heading {
|
||||
display: flex;
|
||||
align-items: baseline;
|
||||
gap: var(--ml-spacing-8);
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ml-model-section-title {
|
||||
color: var(--ml-text);
|
||||
font-size: var(--ml-font-lg);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ml-model-section-total {
|
||||
color: var(--ml-text-dim);
|
||||
font-size: var(--ml-font-xs);
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ml-model-section-counts {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ml-spacing-5);
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ml-model-count-pill {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 18px;
|
||||
padding: 0 var(--ml-spacing-7);
|
||||
border-radius: 999px;
|
||||
border: 1px solid rgba(255, 255, 255, 0.09);
|
||||
color: var(--ml-text-muted);
|
||||
background: rgba(255, 255, 255, 0.035);
|
||||
font-size: var(--ml-font-xs);
|
||||
font-weight: 700;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.ml-model-count-pill.is-active {
|
||||
color: #c9f6d0;
|
||||
border-color: rgba(114, 211, 138, 0.25);
|
||||
background: rgba(114, 211, 138, 0.09);
|
||||
}
|
||||
|
||||
.ml-model-count-pill.is-inactive {
|
||||
color: var(--ml-text-dim);
|
||||
}
|
||||
|
||||
.ml-model-group {
|
||||
margin-bottom: var(--ml-spacing-6);
|
||||
}
|
||||
@@ -168,14 +242,31 @@
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
gap: var(--ml-spacing-8);
|
||||
margin-bottom: var(--ml-spacing-5);
|
||||
}
|
||||
|
||||
.ml-model-group-label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: var(--ml-spacing-5);
|
||||
font-size: var(--ml-font-sm);
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.ml-model-group-dot {
|
||||
width: 6px;
|
||||
height: 6px;
|
||||
border-radius: 999px;
|
||||
background: currentColor;
|
||||
}
|
||||
|
||||
.ml-model-group-count {
|
||||
color: var(--ml-text-dim);
|
||||
font-size: var(--ml-font-xs);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.ml-model-group-label-active {
|
||||
color: #4caf50;
|
||||
}
|
||||
@@ -185,12 +276,16 @@
|
||||
}
|
||||
|
||||
.ml-btn-copy-compact {
|
||||
padding: var(--ml-spacing-3) var(--ml-spacing-8);
|
||||
font-size: var(--ml-font-xs);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ml-copy-btn-icon {
|
||||
width: 13px;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.ml-btn-copy-compact.is-muted {
|
||||
opacity: 0.6;
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.ml-model-chip-list {
|
||||
@@ -204,32 +299,85 @@
|
||||
}
|
||||
|
||||
.ml-copy-section {
|
||||
padding: var(--ml-spacing-16) var(--ml-spacing-20);
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
z-index: 2;
|
||||
padding: var(--ml-spacing-10) var(--ml-spacing-12) var(--ml-spacing-12);
|
||||
background: var(--ml-bg);
|
||||
border-top: 1px solid var(--ml-border);
|
||||
}
|
||||
|
||||
.ml-copy-label {
|
||||
margin: 0;
|
||||
font-size: var(--ml-font-md);
|
||||
font-weight: 700;
|
||||
color: var(--ml-text);
|
||||
}
|
||||
|
||||
.ml-copy-meta {
|
||||
margin-top: var(--ml-spacing-2);
|
||||
font-size: var(--ml-font-xs);
|
||||
color: var(--ml-text-muted);
|
||||
}
|
||||
|
||||
.ml-copy-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ml-spacing-10);
|
||||
margin-bottom: var(--ml-spacing-8);
|
||||
}
|
||||
|
||||
.ml-copy-row {
|
||||
.ml-copy-heading {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.ml-copy-actions {
|
||||
display: flex;
|
||||
gap: var(--ml-spacing-8);
|
||||
align-items: center;
|
||||
gap: var(--ml-spacing-6);
|
||||
flex-wrap: wrap;
|
||||
justify-content: flex-end;
|
||||
}
|
||||
|
||||
.ml-copy-mode {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ml-copy-main-btn {
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ml-copy-code {
|
||||
flex: 1;
|
||||
display: block;
|
||||
width: 100%;
|
||||
max-height: 92px;
|
||||
box-sizing: border-box;
|
||||
padding: var(--ml-spacing-8) var(--ml-spacing-12);
|
||||
background: var(--ml-card-bg);
|
||||
border-radius: var(--ml-radius-md);
|
||||
font-family: "SF Mono", "Consolas", monospace;
|
||||
font-size: var(--ml-font-sm);
|
||||
line-height: 1.45;
|
||||
color: #90caf9;
|
||||
overflow-x: auto;
|
||||
white-space: nowrap;
|
||||
overflow: auto;
|
||||
white-space: pre-wrap;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.ml-loaded-models-header,
|
||||
.ml-copy-toolbar,
|
||||
.ml-model-section-header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.ml-loaded-filter-row,
|
||||
.ml-copy-actions,
|
||||
.ml-model-section-counts {
|
||||
justify-content: flex-start;
|
||||
}
|
||||
}
|
||||
|
||||
.ml-error-text {
|
||||
@@ -2388,21 +2536,42 @@
|
||||
padding: 0;
|
||||
}
|
||||
.ml-loaded-models-header {
|
||||
padding: 16px 20px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: var(--ml-spacing-12);
|
||||
padding: 10px 12px;
|
||||
border-bottom: 1px solid var(--ml-border);
|
||||
}
|
||||
.ml-loaded-title-block {
|
||||
min-width: 0;
|
||||
}
|
||||
.ml-loaded-models-title {
|
||||
font-size: 15px;
|
||||
font-weight: 600;
|
||||
color: var(--ml-text);
|
||||
margin: 0 0 8px 0;
|
||||
margin: 0;
|
||||
}
|
||||
.ml-loaded-total {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-width: 22px;
|
||||
height: 20px;
|
||||
margin-left: var(--ml-spacing-6);
|
||||
padding: 0 var(--ml-spacing-7);
|
||||
border-radius: 999px;
|
||||
background: rgba(114, 211, 138, 0.12);
|
||||
color: #dffbe5;
|
||||
font-size: var(--ml-font-xs);
|
||||
}
|
||||
.ml-loaded-models-subtitle {
|
||||
margin: var(--ml-spacing-4) 0 0;
|
||||
font-size: 12px;
|
||||
color: var(--ml-text-muted);
|
||||
}
|
||||
.ml-models-list {
|
||||
padding: 4px 12px 8px;
|
||||
padding: 8px 12px 10px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 6px;
|
||||
@@ -2410,7 +2579,7 @@
|
||||
.ml-model-chip {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
padding: 6px 12px;
|
||||
padding: 5px 10px;
|
||||
background: #3a3a3a;
|
||||
border-radius: 6px;
|
||||
font-family: 'SF Mono', 'Consolas', 'Monaco', monospace;
|
||||
@@ -2435,6 +2604,13 @@
|
||||
margin-left: 8px;
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.ml-loaded-models-header {
|
||||
align-items: stretch;
|
||||
flex-direction: column;
|
||||
}
|
||||
}
|
||||
|
||||
.model-linker-selected {
|
||||
margin: 8px 0;
|
||||
padding: 10px 12px;
|
||||
|
||||
+96
-55
@@ -1581,6 +1581,10 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
escapeJsString(value) {
|
||||
return this.escapeHtml(JSON.stringify(String(value ?? '')));
|
||||
}
|
||||
|
||||
truncateText(value, maxLength = 160) {
|
||||
const text = String(value ?? '').trim();
|
||||
if (!text) return '';
|
||||
@@ -3936,14 +3940,26 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const inactiveString = buildCategoryStrings('inactive');
|
||||
const allString = buildCategoryStrings('all');
|
||||
|
||||
const self = this;
|
||||
const copyIcon = getSvgIcon('copy', 'currentColor', 'ml-copy-btn-icon');
|
||||
const stripModelExtension = (value) => String(value || 'Unknown').replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
|
||||
const modelToken = (model, category) => {
|
||||
const fullName = model.name || model.original_path?.split(/[\/\\]/).pop() || 'Unknown';
|
||||
const name = stripModelExtension(fullName);
|
||||
const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : '1.00';
|
||||
return `<${this.getCategoryTokenName(category)}:${name}:${strength}>`;
|
||||
};
|
||||
const copyButtonHtml = (text, extraClass = '') => `
|
||||
<button class="ml-btn-filter ml-btn-copy-compact ${extraClass}" onclick="window.MLCopy(${this.escapeJsString(text)}, this)">
|
||||
${copyIcon}<span>Copy</span>
|
||||
</button>`;
|
||||
const countText = (count, label) => `${count} ${label}${count === 1 ? '' : 's'}`;
|
||||
|
||||
let html = `
|
||||
<div class="ml-loaded-models-header">
|
||||
<h3 class="ml-loaded-models-title">${total} Model${total > 1 ? 's' : ''} in Workflow</h3>
|
||||
<p class="ml-loaded-models-subtitle">LoraManager / LoraLoaderV2 nodes distinguish active/inactive</p>
|
||||
</div>
|
||||
<div class="ml-loaded-filters">
|
||||
<div class="ml-loaded-title-block">
|
||||
<h3 class="ml-loaded-models-title">Loaded Models <span class="ml-loaded-total">${total}</span></h3>
|
||||
<p class="ml-loaded-models-subtitle">${countText(activeCount, 'active')} / ${countText(inactiveCount, 'inactive')}</p>
|
||||
</div>
|
||||
<div class="ml-loaded-filter-row">
|
||||
<button class="ml-btn-filter active" id="filter-all" onclick="window.MLFilterSwitch('all')">All (${activeCount + inactiveCount})</button>
|
||||
<button class="ml-btn-filter" id="filter-active" onclick="window.MLFilterSwitch('active')">Active (${activeCount})</button>
|
||||
@@ -3957,26 +3973,28 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const displayName = this.getCategoryDisplayName(category);
|
||||
const hasActive = modelsObj.active.length > 0;
|
||||
const hasInactive = modelsObj.inactive.length > 0;
|
||||
const sectionTotal = modelsObj.active.length + modelsObj.inactive.length;
|
||||
|
||||
html += `<div class="ml-model-section" data-ml-filter="all" data-ml-active="${hasActive}" data-ml-inactive="${hasInactive}">`;
|
||||
|
||||
// Add category header
|
||||
html += `<div class="ml-model-section-header">
|
||||
<span class="ml-model-section-title">${displayName.toUpperCase()}</span>
|
||||
<div class="ml-model-section-heading">
|
||||
<span class="ml-model-section-title">${this.escapeHtml(displayName.toUpperCase())}</span>
|
||||
<span class="ml-model-section-total">${countText(sectionTotal, 'model')}</span>
|
||||
</div>
|
||||
<div class="ml-model-section-counts">
|
||||
${hasActive ? `<span class="ml-model-count-pill is-active">${countText(modelsObj.active.length, 'active')}</span>` : ''}
|
||||
${hasInactive ? `<span class="ml-model-count-pill is-inactive">${countText(modelsObj.inactive.length, 'inactive')}</span>` : ''}
|
||||
</div>
|
||||
</div>`;
|
||||
|
||||
if (hasActive) {
|
||||
const activeStr = modelsObj.active.map(m => {
|
||||
const fullName = m.name || m.original_path?.split(/[\/\\]/).pop() || 'Unknown';
|
||||
let name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
|
||||
const strength = m.strength !== null && m.strength !== undefined ? m.strength.toFixed(2) : '1.00';
|
||||
return `<${this.getCategoryTokenName(category)}:${name}:${strength}>`;
|
||||
}).join(' ');
|
||||
const activeStr = modelsObj.active.map(m => modelToken(m, category)).join(' ');
|
||||
|
||||
html += `<div class="ml-model-group">
|
||||
html += `<div class="ml-model-group ml-model-group-active">
|
||||
<div class="ml-model-group-head">
|
||||
<span class="ml-model-group-label ml-model-group-label-active">● ACTIVE</span>
|
||||
<button class="ml-btn ml-btn-sm ml-btn-copy-compact" onclick="window.MLCopy('${activeStr.replace(/'/g, "\\'")}', this)">Copy</button>
|
||||
<span class="ml-model-group-label ml-model-group-label-active"><span class="ml-model-group-dot"></span>Active <span class="ml-model-group-count">${modelsObj.active.length}</span></span>
|
||||
${copyButtonHtml(activeStr)}
|
||||
</div>
|
||||
<div class="ml-model-chip-list">`;
|
||||
|
||||
@@ -3985,23 +4003,18 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
|
||||
const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null;
|
||||
const modelData = encodeURIComponent(JSON.stringify(model));
|
||||
html += `<span class="ml-model-chip" data-model="${modelData}" oncontextmenu="window.MLOpenContextMenu(event, this)">${name}${strength !== null ? `<span class="ml-model-chip-strength">${strength}</span>` : ''}</span>`;
|
||||
html += `<span class="ml-model-chip" data-model="${modelData}" oncontextmenu="window.MLOpenContextMenu(event, this)">${this.escapeHtml(name)}${strength !== null ? `<span class="ml-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
|
||||
if (hasInactive) {
|
||||
const inactiveStr = modelsObj.inactive.map(m => {
|
||||
const fullName = m.name || m.original_path?.split(/[\/\\]/).pop() || 'Unknown';
|
||||
let name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
|
||||
const strength = m.strength !== null && m.strength !== undefined ? m.strength.toFixed(2) : '1.00';
|
||||
return `<${this.getCategoryTokenName(category)}:${name}:${strength}>`;
|
||||
}).join(' ');
|
||||
const inactiveStr = modelsObj.inactive.map(m => modelToken(m, category)).join(' ');
|
||||
|
||||
html += `<div>
|
||||
html += `<div class="ml-model-group ml-model-group-inactive">
|
||||
<div class="ml-model-group-head">
|
||||
<span class="ml-model-group-label ml-model-group-label-inactive">○ INACTIVE</span>
|
||||
<button class="ml-btn ml-btn-sm ml-btn-copy-compact is-muted" onclick="window.MLCopy('${inactiveStr.replace(/'/g, "\\'")}', this)">Copy</button>
|
||||
<span class="ml-model-group-label ml-model-group-label-inactive"><span class="ml-model-group-dot"></span>Inactive <span class="ml-model-group-count">${modelsObj.inactive.length}</span></span>
|
||||
${copyButtonHtml(inactiveStr, 'is-muted')}
|
||||
</div>
|
||||
<div class="ml-model-chip-list ml-model-chip-list-inactive">`;
|
||||
|
||||
@@ -4010,7 +4023,7 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const name = fullName.replace(/\.(safetensors|ckpt|pt|pth|bin|pkl|sft|onnx|gguf)$/i, '');
|
||||
const strength = model.strength !== null && model.strength !== undefined ? model.strength.toFixed(2) : null;
|
||||
const modelData = encodeURIComponent(JSON.stringify(model));
|
||||
html += `<span class="ml-model-chip" data-model="${modelData}" oncontextmenu="window.MLOpenContextMenu(event, this)">${name}${strength !== null ? `<span class="ml-model-chip-strength">${strength}</span>` : ''}</span>`;
|
||||
html += `<span class="ml-model-chip" data-model="${modelData}" oncontextmenu="window.MLOpenContextMenu(event, this)">${this.escapeHtml(name)}${strength !== null ? `<span class="ml-model-chip-strength">${this.escapeHtml(strength)}</span>` : ''}</span>`;
|
||||
}
|
||||
html += `</div></div>`;
|
||||
}
|
||||
@@ -4021,12 +4034,20 @@ class LinkerManagerDialog extends ComfyDialog {
|
||||
const copySectionId = 'ml-copy-' + Date.now();
|
||||
html += `
|
||||
</div>
|
||||
<div id="${copySectionId}" class="ml-copy-section" data-ml-active="${this.escapeHtml(activeString)}" data-ml-inactive="${this.escapeHtml(inactiveString)}" data-ml-all="${this.escapeHtml(allString)}">
|
||||
<div class="ml-copy-label" id="${copySectionId}-label">Copy all:</div>
|
||||
<div class="ml-copy-row">
|
||||
<code class="ml-copy-code" id="${copySectionId}-code">${this.escapeHtml(allString)}</code>
|
||||
<button class="ml-btn ml-btn-secondary" onclick="window.MLCopyCode('${copySectionId}', this)">Copy</button>
|
||||
<div id="${copySectionId}" class="ml-copy-section" data-ml-active="${this.escapeHtml(activeString)}" data-ml-inactive="${this.escapeHtml(inactiveString)}" data-ml-all="${this.escapeHtml(allString)}" data-ml-active-count="${activeCount}" data-ml-inactive-count="${inactiveCount}" data-ml-all-count="${activeCount + inactiveCount}">
|
||||
<div class="ml-copy-toolbar">
|
||||
<div class="ml-copy-heading">
|
||||
<div class="ml-copy-label" id="${copySectionId}-label">Copy all</div>
|
||||
<div class="ml-copy-meta" id="${copySectionId}-meta">${countText(activeCount + inactiveCount, 'token')}</div>
|
||||
</div>
|
||||
<div class="ml-copy-actions">
|
||||
<button type="button" class="ml-btn-filter ml-copy-mode active" data-ml-copy-mode="all" onclick="window.MLSetCopyMode('${copySectionId}', 'all', this)">All</button>
|
||||
<button type="button" class="ml-btn-filter ml-copy-mode" data-ml-copy-mode="active" onclick="window.MLSetCopyMode('${copySectionId}', 'active', this)">Active</button>
|
||||
<button type="button" class="ml-btn-filter ml-copy-mode" data-ml-copy-mode="inactive" onclick="window.MLSetCopyMode('${copySectionId}', 'inactive', this)">Inactive</button>
|
||||
<button class="ml-btn-filter ml-copy-main-btn" onclick="window.MLCopyCode('${copySectionId}', this)">${copyIcon}<span>Copy</span></button>
|
||||
</div>
|
||||
</div>
|
||||
<code class="ml-copy-code" id="${copySectionId}-code">${this.escapeHtml(allString)}</code>
|
||||
</div>
|
||||
`;
|
||||
|
||||
@@ -8036,6 +8057,44 @@ window.MLToggleHidden = function(id, trigger, collapsedText, expandedText) {
|
||||
}
|
||||
};
|
||||
|
||||
window.MLSetCopyMode = function(sectionId, mode) {
|
||||
const section = document.getElementById(sectionId);
|
||||
if (!section) return;
|
||||
|
||||
const normalizedMode = ['all', 'active', 'inactive'].includes(mode) ? mode : 'all';
|
||||
const codeEl = section.querySelector('.ml-copy-code');
|
||||
const labelEl = section.querySelector('.ml-copy-label');
|
||||
const metaEl = section.querySelector('.ml-copy-meta');
|
||||
const dataKey = normalizedMode === 'all' ? 'mlAll' : normalizedMode === 'active' ? 'mlActive' : 'mlInactive';
|
||||
const countKey = normalizedMode === 'all' ? 'mlAllCount' : normalizedMode === 'active' ? 'mlActiveCount' : 'mlInactiveCount';
|
||||
const labelMap = {
|
||||
all: 'Copy all',
|
||||
active: 'Copy active',
|
||||
inactive: 'Copy inactive'
|
||||
};
|
||||
|
||||
if (codeEl) codeEl.textContent = section.dataset[dataKey] || '';
|
||||
if (labelEl) labelEl.textContent = labelMap[normalizedMode];
|
||||
if (metaEl) {
|
||||
const count = Number(section.dataset[countKey] || 0);
|
||||
metaEl.textContent = `${count} token${count === 1 ? '' : 's'}`;
|
||||
}
|
||||
|
||||
section.querySelectorAll('.ml-copy-mode').forEach(btn => {
|
||||
btn.classList.toggle('active', btn.dataset.mlCopyMode === normalizedMode);
|
||||
});
|
||||
};
|
||||
|
||||
window.MLSetCopyButtonFeedback = function(btn, text) {
|
||||
if (!btn) return;
|
||||
const originalHtml = btn.dataset.mlOriginalHtml || btn.innerHTML;
|
||||
btn.dataset.mlOriginalHtml = originalHtml;
|
||||
btn.textContent = text;
|
||||
setTimeout(() => {
|
||||
if (btn.isConnected) btn.innerHTML = originalHtml;
|
||||
}, 1500);
|
||||
};
|
||||
|
||||
window.MLFilterSwitch = function(filter) {
|
||||
const filterBtn = document.getElementById('filter-' + filter);
|
||||
if (!filterBtn) return;
|
||||
@@ -8049,10 +8108,8 @@ window.MLFilterSwitch = function(filter) {
|
||||
const hasActive = s.getAttribute('data-ml-active') === 'true';
|
||||
const hasInactive = s.getAttribute('data-ml-inactive') === 'true';
|
||||
|
||||
// Get child divs: category header, active section, inactive section
|
||||
const childDivs = Array.from(s.children).filter(c => c.tagName === 'DIV');
|
||||
const activeSection = childDivs[1];
|
||||
const inactiveSection = childDivs[2];
|
||||
const activeSection = s.querySelector('.ml-model-group-active');
|
||||
const inactiveSection = s.querySelector('.ml-model-group-inactive');
|
||||
|
||||
if (filter === 'all') {
|
||||
s.style.display = 'block';
|
||||
@@ -8071,27 +8128,13 @@ window.MLFilterSwitch = function(filter) {
|
||||
|
||||
const copySection = document.querySelector('[id^="ml-copy-"]');
|
||||
if (copySection) {
|
||||
const codeEl = copySection.querySelector('code');
|
||||
const labelEl = copySection.querySelector('div');
|
||||
|
||||
if (filter === 'all') {
|
||||
codeEl.textContent = copySection.dataset.mlAll;
|
||||
labelEl.textContent = 'Copy all:';
|
||||
} else if (filter === 'active') {
|
||||
codeEl.textContent = copySection.dataset.mlActive;
|
||||
labelEl.textContent = 'Copy active:';
|
||||
} else if (filter === 'inactive') {
|
||||
codeEl.textContent = copySection.dataset.mlInactive;
|
||||
labelEl.textContent = 'Copy inactive:';
|
||||
}
|
||||
window.MLSetCopyMode(copySection.id, filter);
|
||||
}
|
||||
};
|
||||
|
||||
window.MLCopy = function(text, btn) {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = '✓';
|
||||
setTimeout(() => btn.textContent = orig, 1500);
|
||||
window.MLSetCopyButtonFeedback(btn, 'Copied');
|
||||
});
|
||||
};
|
||||
|
||||
@@ -8099,9 +8142,7 @@ window.MLCopyCode = function(sectionId, btn) {
|
||||
const section = document.getElementById(sectionId);
|
||||
const codeEl = section.querySelector('code');
|
||||
navigator.clipboard.writeText(codeEl.textContent).then(() => {
|
||||
const orig = btn.textContent;
|
||||
btn.textContent = '✓ Copied!';
|
||||
setTimeout(() => btn.textContent = orig, 1500);
|
||||
window.MLSetCopyButtonFeedback(btn, 'Copied');
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@ const SVG_ICONS = {
|
||||
refreshCw: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 12a9 9 0 1 1-2.64-6.36"></path><path d="M21 3v6h-6"></path></svg>`,
|
||||
locate: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M2 12h3"></path><path d="M19 12h3"></path><path d="M12 2v3"></path><path d="M12 19v3"></path><circle cx="12" cy="12" r="7"></circle><circle cx="12" cy="12" r="2.5"></circle></svg>`,
|
||||
download: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2.1" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M12 3v12"></path><path d="m7 10 5 5 5-5"></path><path d="M5 21h14"></path></svg>`,
|
||||
copy: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><rect x="9" y="9" width="11" height="11" rx="2"></rect><path d="M5 15H4a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h9a2 2 0 0 1 2 2v1"></path></svg>`,
|
||||
externalLink: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8.5 12h10.5"></path><path d="m15.25 15.75 3.75-3.75-3.75-3.75"></path><path d="M19 7.75V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-.75"></path></svg>`,
|
||||
internalLink: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M8.5 12h10.5"></path><path d="M12.25 15.75 8.5 12l3.75-3.75"></path><path d="M19 7.75V7a2 2 0 0 0-2-2H7a2 2 0 0 0-2 2v10a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2v-.75"></path></svg>`,
|
||||
database: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><ellipse cx="12" cy="5" rx="8" ry="3"></ellipse><path d="M4 5v6c0 1.7 3.6 3 8 3s8-1.3 8-3V5"></path><path d="M4 11v6c0 1.7 3.6 3 8 3s8-1.3 8-3v-6"></path></svg>`,
|
||||
|
||||
Reference in New Issue
Block a user