1628 lines
47 KiB
JavaScript
1628 lines
47 KiB
JavaScript
/**
|
|
* LoRA Notes Drawer (✍︎)
|
|
* Floating sleek side panel & modal for per-LoRA notes, prompt tips, compressed embedded images,
|
|
* Civitai gallery auto-fetch with full generation parameters, rich metadata lightbox viewer,
|
|
* dynamic node accent colors, and one-click JSON compression.
|
|
*/
|
|
|
|
const STYLES = `
|
|
.trix-notes-backdrop {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.65);
|
|
backdrop-filter: blur(4px);
|
|
z-index: 10005;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
opacity: 0;
|
|
transition: opacity 0.22s ease-out;
|
|
--trix-acc: #387aff;
|
|
}
|
|
.trix-notes-backdrop.is-open {
|
|
opacity: 1;
|
|
}
|
|
.trix-notes-drawer {
|
|
width: 540px;
|
|
max-width: 95vw;
|
|
height: 100vh;
|
|
background: #141418;
|
|
border-left: 1px solid #282832;
|
|
box-shadow: -10px 0 35px rgba(0,0,0,0.6);
|
|
display: flex;
|
|
flex-direction: column;
|
|
color: #e2e2e9;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
transform: translateX(100%);
|
|
transition: transform 0.25s cubic-bezier(0.16, 1, 0.3, 1), width 0.2s ease;
|
|
box-sizing: border-box;
|
|
--trix-acc: #387aff;
|
|
}
|
|
.trix-notes-backdrop.is-open .trix-notes-drawer {
|
|
transform: translateX(0);
|
|
}
|
|
.trix-notes-drawer.is-maximized {
|
|
width: 880px;
|
|
}
|
|
|
|
/* Header */
|
|
.trix-notes-header {
|
|
padding: 14px 18px;
|
|
background: #1a1a20;
|
|
border-bottom: 1px solid #282832;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 12px;
|
|
flex-shrink: 0;
|
|
}
|
|
.trix-notes-title-wrap {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 10px;
|
|
min-width: 0;
|
|
flex: 1;
|
|
}
|
|
.trix-notes-icon {
|
|
font-size: 18px;
|
|
color: var(--trix-acc, #387aff);
|
|
display: inline-block;
|
|
}
|
|
.trix-notes-title {
|
|
font-size: 13.5px;
|
|
font-weight: 600;
|
|
color: #f0f0f5;
|
|
white-space: nowrap;
|
|
overflow: hidden;
|
|
text-overflow: ellipsis;
|
|
}
|
|
.trix-notes-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 8px;
|
|
flex-shrink: 0;
|
|
}
|
|
.trix-notes-btn {
|
|
background: #22222a;
|
|
border: 1px solid #32323e;
|
|
color: #ccc;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: all 0.15s ease;
|
|
user-select: none;
|
|
}
|
|
.trix-notes-btn:hover {
|
|
background: #2c2c38;
|
|
color: #fff;
|
|
border-color: #444455;
|
|
}
|
|
.trix-notes-btn:disabled {
|
|
opacity: 0.55;
|
|
cursor: not-allowed;
|
|
}
|
|
.trix-notes-btn-primary {
|
|
background: var(--trix-acc, #2563eb);
|
|
border: 1px solid var(--trix-acc, #3b82f6);
|
|
color: #fff;
|
|
font-weight: 600;
|
|
}
|
|
.trix-notes-btn.trix-notes-btn-primary:hover {
|
|
background: var(--trix-acc, #2563eb);
|
|
border-color: rgba(255, 255, 255, 0.4);
|
|
color: #fff;
|
|
filter: brightness(1.18);
|
|
box-shadow: 0 0 14px color-mix(in srgb, var(--trix-acc, #2563eb) 65%, transparent);
|
|
}
|
|
.trix-notes-btn-civitai, .trix-notes-btn-add {
|
|
background: #1f2937;
|
|
border: 1px solid #374151;
|
|
color: #e5e7eb;
|
|
font-weight: 500;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
transition: all 0.15s ease;
|
|
user-select: none;
|
|
}
|
|
.trix-notes-btn-civitai:hover, .trix-notes-btn-add:hover {
|
|
background: #374151;
|
|
border-color: #4b5563;
|
|
color: #fff;
|
|
}
|
|
.trix-notes-btn-icon {
|
|
padding: 6px 9px;
|
|
font-size: 14px;
|
|
}
|
|
|
|
/* Body */
|
|
.trix-notes-body {
|
|
flex: 1;
|
|
overflow-y: auto;
|
|
padding: 16px 18px;
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 16px;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.trix-notes-section-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
margin-bottom: 8px;
|
|
}
|
|
|
|
.trix-notes-section-title {
|
|
font-size: 11px;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.8px;
|
|
color: #888899;
|
|
font-weight: 600;
|
|
}
|
|
|
|
.trix-notes-gallery-actions {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
|
|
/* Gallery Container & Drop Zone */
|
|
.trix-notes-gallery-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 8px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
}
|
|
|
|
.trix-notes-drop-zone {
|
|
position: relative;
|
|
width: 100%;
|
|
border: 1.5px dashed transparent;
|
|
border-radius: 8px;
|
|
transition: border-color 0.18s, background-color 0.18s;
|
|
box-sizing: border-box;
|
|
}
|
|
.trix-notes-drop-zone.is-dragover {
|
|
border-color: var(--trix-acc, #387aff);
|
|
background: rgba(56, 122, 255, 0.05);
|
|
}
|
|
|
|
.trix-notes-images-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
justify-content: center;
|
|
align-items: center;
|
|
gap: 10px;
|
|
width: 100%;
|
|
box-sizing: border-box;
|
|
padding: 6px 0;
|
|
min-height: 80px;
|
|
--trix-img-size: 140px;
|
|
}
|
|
|
|
.trix-notes-empty-gallery {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
justify-content: center;
|
|
gap: 6px;
|
|
padding: 24px 12px;
|
|
color: #64748b;
|
|
font-size: 11.5px;
|
|
text-align: center;
|
|
user-select: none;
|
|
width: 100%;
|
|
}
|
|
.trix-notes-empty-gallery .empty-icon {
|
|
font-size: 26px;
|
|
opacity: 0.7;
|
|
}
|
|
|
|
.trix-notes-img-card {
|
|
position: relative;
|
|
width: var(--trix-img-size, 140px);
|
|
height: var(--trix-img-size, 140px);
|
|
aspect-ratio: 1;
|
|
background: #18181e;
|
|
border: 1px solid #2a2a34;
|
|
border-radius: 8px;
|
|
overflow: hidden;
|
|
cursor: pointer;
|
|
user-select: none;
|
|
flex: 0 0 auto;
|
|
box-sizing: border-box;
|
|
transition: transform 0.15s, border-color 0.15s, opacity 0.15s, box-shadow 0.15s;
|
|
}
|
|
.trix-notes-img-card:hover {
|
|
transform: translateY(-2px);
|
|
border-color: var(--trix-acc, #387aff);
|
|
box-shadow: 0 4px 14px rgba(0, 0, 0, 0.45);
|
|
}
|
|
.trix-notes-img-card.dragging {
|
|
opacity: 0.35;
|
|
cursor: grabbing;
|
|
border-style: dashed;
|
|
}
|
|
.trix-notes-img-card.drag-over {
|
|
border-color: var(--trix-acc, #387aff);
|
|
box-shadow: 0 0 0 2px var(--trix-acc, #387aff);
|
|
transform: scale(1.04);
|
|
}
|
|
.trix-notes-img-card img {
|
|
width: 100%;
|
|
height: 100%;
|
|
object-fit: cover;
|
|
display: block;
|
|
pointer-events: none;
|
|
}
|
|
.trix-notes-img-del {
|
|
position: absolute;
|
|
top: 5px;
|
|
right: 5px;
|
|
background: rgba(0, 0, 0, 0.75);
|
|
color: #ff5555;
|
|
border: none;
|
|
border-radius: 4px;
|
|
width: 22px;
|
|
height: 22px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
opacity: 0;
|
|
font-size: 11px;
|
|
transition: opacity 0.15s, background 0.15s;
|
|
z-index: 2;
|
|
}
|
|
.trix-notes-img-card:hover .trix-notes-img-del {
|
|
opacity: 1;
|
|
}
|
|
.trix-notes-img-del:hover {
|
|
background: #dc2626;
|
|
color: #fff;
|
|
}
|
|
.trix-notes-img-badge {
|
|
position: absolute;
|
|
bottom: 5px;
|
|
left: 5px;
|
|
background: rgba(0, 0, 0, 0.7);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
border-radius: 3px;
|
|
padding: 1px 4px;
|
|
color: #93c5fd;
|
|
font-size: 8px;
|
|
font-weight: 700;
|
|
letter-spacing: 0.3px;
|
|
text-transform: uppercase;
|
|
pointer-events: none;
|
|
backdrop-filter: blur(4px);
|
|
}
|
|
|
|
/* Gallery Footer (Size Slider & Count) */
|
|
.trix-notes-gallery-footer {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
padding: 2px 2px 6px 2px;
|
|
font-size: 10.5px;
|
|
color: #727584;
|
|
}
|
|
.trix-notes-size-ctrl {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.trix-notes-size-slider {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 80px;
|
|
height: 4px;
|
|
border-radius: 2px;
|
|
background: #2d2e3b;
|
|
outline: none;
|
|
cursor: pointer;
|
|
}
|
|
.trix-notes-size-slider::-webkit-slider-thumb {
|
|
-webkit-appearance: none;
|
|
appearance: none;
|
|
width: 12px;
|
|
height: 12px;
|
|
border-radius: 50%;
|
|
background: var(--trix-acc, #387aff);
|
|
cursor: pointer;
|
|
transition: transform 0.1s;
|
|
}
|
|
.trix-notes-size-slider::-webkit-slider-thumb:hover {
|
|
transform: scale(1.25);
|
|
}
|
|
|
|
/* Text Section */
|
|
.trix-notes-text-wrap {
|
|
display: flex;
|
|
flex-direction: column;
|
|
flex: 1 1 auto;
|
|
min-height: 200px;
|
|
}
|
|
.trix-notes-textarea {
|
|
width: 100%;
|
|
height: 280px;
|
|
min-height: 180px;
|
|
background: #101014;
|
|
border: 1px solid #282832;
|
|
border-radius: 8px;
|
|
padding: 12px 14px;
|
|
color: #f0f0f5;
|
|
font-family: inherit;
|
|
font-size: 13px;
|
|
line-height: 1.5;
|
|
resize: vertical;
|
|
box-sizing: border-box;
|
|
outline: none;
|
|
flex: 1 1 auto;
|
|
transition: border-color 0.2s;
|
|
}
|
|
.trix-notes-textarea:focus {
|
|
border-color: var(--trix-acc, #387aff);
|
|
}
|
|
|
|
/* Footer / Status */
|
|
.trix-notes-footer {
|
|
padding: 12px 18px;
|
|
background: #18181e;
|
|
border-top: 1px solid #282832;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
font-size: 11px;
|
|
color: #777;
|
|
flex-shrink: 0;
|
|
}
|
|
.trix-notes-status {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
}
|
|
.trix-notes-status-dot {
|
|
width: 6px;
|
|
height: 6px;
|
|
border-radius: 50%;
|
|
background: #10b981;
|
|
}
|
|
|
|
/* Rich Lightbox Viewer (Screenshot 3 style) */
|
|
.trix-notes-lightbox {
|
|
position: fixed;
|
|
inset: 0;
|
|
background: rgba(0, 0, 0, 0.92);
|
|
backdrop-filter: blur(10px);
|
|
z-index: 10010;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
user-select: none;
|
|
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
|
--trix-acc: #387aff;
|
|
}
|
|
.trix-lb-img-wrap {
|
|
position: relative;
|
|
max-width: 90vw;
|
|
max-height: 92vh;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
.trix-lb-img {
|
|
max-width: 88vw;
|
|
max-height: 88vh;
|
|
object-fit: contain;
|
|
border-radius: 6px;
|
|
box-shadow: 0 25px 70px rgba(0,0,0,0.85);
|
|
pointer-events: auto;
|
|
}
|
|
.trix-lb-topbar {
|
|
position: absolute;
|
|
top: 18px;
|
|
right: 24px;
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 14px;
|
|
z-index: 20;
|
|
}
|
|
.trix-lb-counter {
|
|
font-size: 13px;
|
|
font-weight: 600;
|
|
color: #e2e8f0;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
padding: 4px 10px;
|
|
border-radius: 6px;
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
backdrop-filter: blur(6px);
|
|
}
|
|
.trix-lb-close-btn {
|
|
background: rgba(0, 0, 0, 0.6);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
color: #fff;
|
|
width: 32px;
|
|
height: 32px;
|
|
border-radius: 6px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
font-size: 15px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
backdrop-filter: blur(6px);
|
|
}
|
|
.trix-lb-close-btn:hover {
|
|
background: #dc2626;
|
|
border-color: #ef4444;
|
|
}
|
|
.trix-lb-nav-btn {
|
|
position: absolute;
|
|
top: 50%;
|
|
transform: translateY(-50%);
|
|
width: 44px;
|
|
height: 44px;
|
|
border-radius: 8px;
|
|
background: rgba(0, 0, 0, 0.6);
|
|
border: 1px solid rgba(255, 255, 255, 0.15);
|
|
color: #fff;
|
|
font-size: 18px;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
z-index: 15;
|
|
backdrop-filter: blur(6px);
|
|
}
|
|
.trix-lb-nav-btn:hover {
|
|
background: var(--trix-acc, #387aff);
|
|
border-color: var(--trix-acc, #387aff);
|
|
transform: translateY(-50%) scale(1.08);
|
|
}
|
|
.trix-lb-nav-btn.left {
|
|
left: 20px;
|
|
}
|
|
.trix-lb-nav-btn.right {
|
|
right: 20px;
|
|
}
|
|
|
|
/* Floating Metadata Card (Bottom-Left overlay, Screenshot 3) */
|
|
.trix-lb-meta-card {
|
|
position: absolute;
|
|
bottom: 24px;
|
|
left: 24px;
|
|
max-width: 520px;
|
|
width: calc(100% - 48px);
|
|
max-height: 45vh;
|
|
background: rgba(18, 18, 24, 0.88);
|
|
backdrop-filter: blur(14px);
|
|
border: 1px solid rgba(255, 255, 255, 0.12);
|
|
border-radius: 10px;
|
|
padding: 14px 16px;
|
|
box-shadow: 0 16px 40px rgba(0, 0, 0, 0.7);
|
|
display: flex;
|
|
flex-direction: column;
|
|
gap: 10px;
|
|
z-index: 20;
|
|
box-sizing: border-box;
|
|
overflow-y: auto;
|
|
pointer-events: auto;
|
|
}
|
|
.trix-lb-model-badge {
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 6px;
|
|
background: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 5px;
|
|
padding: 3px 8px;
|
|
font-size: 10.5px;
|
|
color: #cbd5e1;
|
|
font-family: monospace;
|
|
align-self: flex-start;
|
|
}
|
|
.trix-lb-model-badge b {
|
|
color: #94a3b8;
|
|
font-weight: 600;
|
|
}
|
|
.trix-lb-meta-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
font-size: 11px;
|
|
font-weight: 700;
|
|
color: #f1f5f9;
|
|
text-transform: uppercase;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
.trix-lb-copy-icon-btn {
|
|
background: #272730;
|
|
border: 1px solid #3a3a46;
|
|
border-radius: 4px;
|
|
color: #94a3b8;
|
|
padding: 3px 8px;
|
|
font-size: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
display: inline-flex;
|
|
align-items: center;
|
|
gap: 4px;
|
|
}
|
|
.trix-lb-copy-icon-btn:hover {
|
|
background: var(--trix-acc, #387aff);
|
|
color: #fff;
|
|
border-color: var(--trix-acc, #387aff);
|
|
}
|
|
.trix-lb-prompt-box {
|
|
background: #111116;
|
|
border: 1px solid #282834;
|
|
border-radius: 6px;
|
|
padding: 9px 11px;
|
|
font-size: 11.5px;
|
|
line-height: 1.45;
|
|
color: #e2e8f0;
|
|
max-height: 120px;
|
|
overflow-y: auto;
|
|
white-space: pre-wrap;
|
|
word-break: break-word;
|
|
user-select: text;
|
|
}
|
|
.trix-lb-prompt-box.neg {
|
|
color: #fca5a5;
|
|
border-color: #451a1a;
|
|
background: #1a0f0f;
|
|
}
|
|
.trix-lb-params-row {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 6px;
|
|
align-items: center;
|
|
}
|
|
.trix-lb-param-pill {
|
|
background: #1e293b;
|
|
border: 1px solid #334155;
|
|
border-radius: 4px;
|
|
padding: 2px 7px;
|
|
font-size: 9.5px;
|
|
color: #94a3b8;
|
|
font-family: monospace;
|
|
letter-spacing: 0.3px;
|
|
text-transform: uppercase;
|
|
}
|
|
.trix-lb-param-pill b {
|
|
color: #38bdf8;
|
|
font-weight: 700;
|
|
}
|
|
.trix-lb-actions-row {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
gap: 8px;
|
|
padding-top: 4px;
|
|
border-top: 1px solid rgba(255, 255, 255, 0.08);
|
|
}
|
|
.trix-lb-del-btn {
|
|
background: transparent;
|
|
border: 1px solid #7f1d1d;
|
|
color: #f87171;
|
|
border-radius: 4px;
|
|
padding: 4px 8px;
|
|
font-size: 10px;
|
|
cursor: pointer;
|
|
transition: all 0.15s;
|
|
}
|
|
.trix-lb-del-btn:hover {
|
|
background: #dc2626;
|
|
color: #fff;
|
|
border-color: #ef4444;
|
|
}
|
|
`;
|
|
|
|
function injectStyles() {
|
|
if (document.getElementById("trix-lora-notes-css")) return;
|
|
const style = document.createElement("style");
|
|
style.id = "trix-lora-notes-css";
|
|
style.textContent = STYLES;
|
|
document.head.appendChild(style);
|
|
}
|
|
|
|
// Client-side image compressor: converts File / Blob / dataURL to WebP Base64 string
|
|
async function compressImageSource(src, maxWidth = 1280, quality = 0.82) {
|
|
return new Promise((resolve, reject) => {
|
|
const img = new Image();
|
|
img.crossOrigin = "anonymous";
|
|
img.onload = () => {
|
|
let w = img.naturalWidth;
|
|
let h = img.naturalHeight;
|
|
if (w > maxWidth || h > maxWidth) {
|
|
if (w > h) {
|
|
h = Math.round((h * maxWidth) / w);
|
|
w = maxWidth;
|
|
} else {
|
|
w = Math.round((w * maxWidth) / h);
|
|
h = maxWidth;
|
|
}
|
|
}
|
|
const canvas = document.createElement("canvas");
|
|
canvas.width = Math.max(1, w);
|
|
canvas.height = Math.max(1, h);
|
|
const ctx = canvas.getContext("2d");
|
|
ctx.imageSmoothingEnabled = true;
|
|
ctx.imageSmoothingQuality = "high";
|
|
ctx.drawImage(img, 0, 0, w, h);
|
|
|
|
try {
|
|
const webp = canvas.toDataURL("image/webp", quality);
|
|
resolve(webp);
|
|
} catch {
|
|
const jpeg = canvas.toDataURL("image/jpeg", quality);
|
|
resolve(jpeg);
|
|
}
|
|
};
|
|
img.onerror = reject;
|
|
|
|
if (typeof src === "string") {
|
|
img.src = src;
|
|
} else if (src instanceof Blob || src instanceof File) {
|
|
const reader = new FileReader();
|
|
reader.onload = (e) => { img.src = e.target.result; };
|
|
reader.onerror = reject;
|
|
reader.readAsDataURL(src);
|
|
} else {
|
|
reject(new Error("Unsupported image source"));
|
|
}
|
|
});
|
|
}
|
|
|
|
export class LoraNotesDrawer {
|
|
static instance = null;
|
|
|
|
static getInstance() {
|
|
if (!LoraNotesDrawer.instance) {
|
|
LoraNotesDrawer.instance = new LoraNotesDrawer();
|
|
}
|
|
return LoraNotesDrawer.instance;
|
|
}
|
|
|
|
static open(loraName, node) {
|
|
if (!loraName || loraName === "None") {
|
|
alert("Please select a LoRA first before opening notes.");
|
|
return;
|
|
}
|
|
const drawer = LoraNotesDrawer.getInstance();
|
|
drawer.show(loraName, node);
|
|
}
|
|
|
|
static async renameNote(oldLora, newLora) {
|
|
if (!oldLora || !newLora || oldLora === newLora) return;
|
|
const drawer = LoraNotesDrawer.getInstance();
|
|
|
|
const hadNote = Boolean(drawer.notesMap?.[oldLora] || drawer.notesCache?.has(oldLora));
|
|
if (hadNote) {
|
|
if (drawer.notesMap) {
|
|
drawer.notesMap[newLora] = drawer.notesMap[oldLora];
|
|
delete drawer.notesMap[oldLora];
|
|
}
|
|
if (drawer.notesCache?.has(oldLora)) {
|
|
const cached = drawer.notesCache.get(oldLora);
|
|
if (cached) {
|
|
cached.lora = newLora;
|
|
drawer.notesCache.set(newLora, cached);
|
|
}
|
|
drawer.notesCache.delete(oldLora);
|
|
}
|
|
}
|
|
|
|
try {
|
|
await fetch("/super_lora/notes/rename", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify({ oldLora, newLora })
|
|
});
|
|
} catch (_) {}
|
|
}
|
|
|
|
constructor() {
|
|
injectStyles();
|
|
this.currentLora = null;
|
|
this.currentNode = null;
|
|
this.notesText = "";
|
|
this.images = []; // Array of { url, civitaiUrl, meta: { prompt, negativePrompt, model, ... } } or string
|
|
this.imageSize = 140;
|
|
this.textareaHeight = 280;
|
|
this.isMaximized = false;
|
|
this.autoSaveTimer = null;
|
|
this.notesCache = new Map();
|
|
this.notesMap = {};
|
|
this.lightboxCurrentIndex = 0;
|
|
this.activeLightboxEl = null;
|
|
this._buildDOM();
|
|
this._bindEvents();
|
|
this._loadNotesMap();
|
|
}
|
|
|
|
_buildDOM() {
|
|
this.backdrop = document.createElement("div");
|
|
this.backdrop.className = "trix-notes-backdrop";
|
|
|
|
this.drawer = document.createElement("div");
|
|
this.drawer.className = "trix-notes-drawer";
|
|
|
|
// Header
|
|
this.header = document.createElement("div");
|
|
this.header.className = "trix-notes-header";
|
|
this.header.innerHTML = `
|
|
<div class="trix-notes-title-wrap">
|
|
<span class="trix-notes-icon">✍︎</span>
|
|
<span class="trix-notes-title" id="trix-notes-lora-title">LoRA Notes</span>
|
|
</div>
|
|
<div class="trix-notes-actions">
|
|
<button class="trix-notes-btn" id="trix-notes-copy-btn" title="Copy text to clipboard">⎘ Copy</button>
|
|
<button class="trix-notes-btn trix-notes-btn-icon" id="trix-notes-max-btn" title="Toggle maximize">⛶</button>
|
|
<button class="trix-notes-btn trix-notes-btn-icon" id="trix-notes-close-btn" title="Close (Esc)">✕</button>
|
|
</div>
|
|
`;
|
|
|
|
// Body — 1. Visual Reference Gallery with Top Actions & Bottom Size Slider, 2. Notes Textarea
|
|
this.body = document.createElement("div");
|
|
this.body.className = "trix-notes-body";
|
|
this.body.innerHTML = `
|
|
<!-- TOP: Visual Reference Gallery -->
|
|
<div class="trix-notes-gallery-wrap">
|
|
<div class="trix-notes-section-header">
|
|
<span class="trix-notes-section-title">Visual Reference Gallery</span>
|
|
<div class="trix-notes-gallery-actions">
|
|
<button class="trix-notes-btn trix-notes-btn-civitai" id="trix-notes-civitai-btn" title="Fetch previews & metadata from Civitai">
|
|
<span>ᨒ</span> Civitai Previews
|
|
</button>
|
|
<button class="trix-notes-btn trix-notes-btn-add" id="trix-notes-add-img-btn" title="Add image (Drop or Click)">
|
|
<span>+</span> Add Image
|
|
</button>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="trix-notes-drop-zone" id="trix-notes-drop-zone">
|
|
<div class="trix-notes-images-grid" id="trix-notes-gallery"></div>
|
|
</div>
|
|
|
|
<div class="trix-notes-gallery-footer">
|
|
<div class="trix-notes-size-ctrl">
|
|
<span>Size:</span>
|
|
<input type="range" class="trix-notes-size-slider" id="trix-notes-size-slider" min="80" max="260" step="5" value="140" title="Adjust thumbnail size" />
|
|
<span id="trix-notes-size-val">140px</span>
|
|
</div>
|
|
<span class="trix-notes-img-count" id="trix-notes-img-count">0 images</span>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- BOTTOM: Notes & Prompt Recommendations -->
|
|
<div class="trix-notes-text-wrap">
|
|
<div class="trix-notes-section-header">
|
|
<span class="trix-notes-section-title">Notes & Prompt Recommendations</span>
|
|
<span id="trix-notes-word-count" style="font-size:10px;color:#666;">0 words</span>
|
|
</div>
|
|
<textarea class="trix-notes-textarea" id="trix-notes-input" placeholder="Type prompt tips, trigger words, recommended CFG, weight settings, or notes for this LoRA..."></textarea>
|
|
</div>
|
|
|
|
<input type="file" id="trix-notes-file-input" accept="image/*" multiple style="display:none;" />
|
|
`;
|
|
|
|
// Footer
|
|
this.footer = document.createElement("div");
|
|
this.footer.className = "trix-notes-footer";
|
|
this.footer.innerHTML = `
|
|
<div class="trix-notes-status">
|
|
<span class="trix-notes-status-dot"></span>
|
|
<span id="trix-notes-status-text">Ready</span>
|
|
</div>
|
|
<div style="display:flex;align-items:center;gap:8px;">
|
|
<button class="trix-notes-btn" id="trix-notes-compress-btn" title="Compress all loaded images to 512px to reduce JSON size">🗜 Compress JSON</button>
|
|
<button class="trix-notes-btn trix-notes-btn-primary" id="trix-notes-save-btn">Save Notes</button>
|
|
</div>
|
|
`;
|
|
|
|
this.drawer.appendChild(this.header);
|
|
this.drawer.appendChild(this.body);
|
|
this.drawer.appendChild(this.footer);
|
|
this.backdrop.appendChild(this.drawer);
|
|
}
|
|
|
|
_bindEvents() {
|
|
// Backdrop click outside
|
|
this.backdrop.addEventListener("click", (e) => {
|
|
if (e.target === this.backdrop) this.hide();
|
|
});
|
|
|
|
// Close button
|
|
this.drawer.querySelector("#trix-notes-close-btn").addEventListener("click", () => this.hide());
|
|
|
|
// Maximize button
|
|
this.drawer.querySelector("#trix-notes-max-btn").addEventListener("click", () => {
|
|
this.isMaximized = !this.isMaximized;
|
|
this.drawer.classList.toggle("is-maximized", this.isMaximized);
|
|
});
|
|
|
|
// Copy button
|
|
this.drawer.querySelector("#trix-notes-copy-btn").addEventListener("click", () => {
|
|
const text = this.textarea.value;
|
|
if (!text) return;
|
|
navigator.clipboard.writeText(text).then(() => {
|
|
this._setStatus("Copied to clipboard!");
|
|
setTimeout(() => this._setStatus("Saved ✓"), 1500);
|
|
});
|
|
});
|
|
|
|
// Civitai Previews Fetch Button
|
|
this.drawer.querySelector("#trix-notes-civitai-btn").addEventListener("click", () => {
|
|
this._fetchCivitaiGallery();
|
|
});
|
|
|
|
// Add Image Button
|
|
const fileInput = this.drawer.querySelector("#trix-notes-file-input");
|
|
this.drawer.querySelector("#trix-notes-add-img-btn").addEventListener("click", () => {
|
|
fileInput.click();
|
|
});
|
|
|
|
fileInput.addEventListener("change", async (e) => {
|
|
if (e.target.files) {
|
|
await this._handleFiles(e.target.files);
|
|
fileInput.value = "";
|
|
}
|
|
});
|
|
|
|
// Drop Zone Events for Drag & Drop image files
|
|
const dropZone = this.drawer.querySelector("#trix-notes-drop-zone");
|
|
dropZone.addEventListener("dragover", (e) => {
|
|
e.preventDefault();
|
|
if (e.dataTransfer?.types?.includes("Files")) {
|
|
dropZone.classList.add("is-dragover");
|
|
}
|
|
});
|
|
dropZone.addEventListener("dragleave", (e) => {
|
|
if (!dropZone.contains(e.relatedTarget)) {
|
|
dropZone.classList.remove("is-dragover");
|
|
}
|
|
});
|
|
dropZone.addEventListener("drop", async (e) => {
|
|
e.preventDefault();
|
|
dropZone.classList.remove("is-dragover");
|
|
if (e.dataTransfer?.files?.length) {
|
|
await this._handleFiles(e.dataTransfer.files);
|
|
}
|
|
});
|
|
|
|
// Textarea input & resize tracking
|
|
this.textarea = this.drawer.querySelector("#trix-notes-input");
|
|
this.textarea.addEventListener("input", () => {
|
|
this.notesText = this.textarea.value;
|
|
this._updateWordCount();
|
|
this._scheduleAutoSave();
|
|
});
|
|
this.textarea.addEventListener("keydown", (e) => {
|
|
if ((e.ctrlKey || e.metaKey) && (e.key === "Enter" || e.keyCode === 13)) {
|
|
this.save(true);
|
|
try {
|
|
window.app?.queuePrompt?.(0);
|
|
} catch (_) {}
|
|
}
|
|
});
|
|
|
|
// Track textarea user resize
|
|
const ro = new ResizeObserver((entries) => {
|
|
for (const entry of entries) {
|
|
if (entry.contentRect.height > 50) {
|
|
const h = Math.round(entry.contentRect.height);
|
|
if (Math.abs(h - this.textareaHeight) > 10) {
|
|
this.textareaHeight = h;
|
|
this._scheduleAutoSave();
|
|
}
|
|
}
|
|
}
|
|
});
|
|
ro.observe(this.textarea);
|
|
|
|
// Size Slider
|
|
const sizeSlider = this.drawer.querySelector("#trix-notes-size-slider");
|
|
const sizeVal = this.drawer.querySelector("#trix-notes-size-val");
|
|
sizeSlider.addEventListener("input", (e) => {
|
|
const sz = parseInt(e.target.value, 10) || 140;
|
|
this.imageSize = sz;
|
|
sizeVal.textContent = `${sz}px`;
|
|
const gallery = this.drawer.querySelector("#trix-notes-gallery");
|
|
if (gallery) {
|
|
gallery.style.setProperty("--trix-img-size", `${sz}px`);
|
|
}
|
|
this._scheduleAutoSave();
|
|
});
|
|
|
|
// Save button
|
|
this.drawer.querySelector("#trix-notes-save-btn").addEventListener("click", () => {
|
|
this.save();
|
|
});
|
|
|
|
// Compress JSON button
|
|
this.drawer.querySelector("#trix-notes-compress-btn").addEventListener("click", () => {
|
|
this._compressAllImagesTo512();
|
|
});
|
|
|
|
// Paste handler for Ctrl+V anywhere while drawer is open
|
|
window.addEventListener("paste", async (e) => {
|
|
if (!this.backdrop.classList.contains("is-open")) return;
|
|
const items = e.clipboardData?.items;
|
|
if (!items) return;
|
|
for (const item of items) {
|
|
if (item.type.startsWith("image/")) {
|
|
const file = item.getAsFile();
|
|
if (file) {
|
|
await this._handleFiles([file]);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
// ESC key listener
|
|
window.addEventListener("keydown", (e) => {
|
|
if (e.key === "Escape" && this.backdrop.classList.contains("is-open")) {
|
|
if (this.activeLightboxEl) {
|
|
this.activeLightboxEl.remove();
|
|
this.activeLightboxEl = null;
|
|
} else {
|
|
this.hide();
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
async _loadNotesMap() {
|
|
try {
|
|
const resp = await fetch("/super_lora/notes");
|
|
if (resp.ok) {
|
|
const json = await resp.json();
|
|
this.notesMap = json.notes_map || {};
|
|
window.dispatchEvent(new CustomEvent("trix_lora_notes_map_loaded", { detail: this.notesMap }));
|
|
}
|
|
} catch (_) {}
|
|
}
|
|
|
|
hasNotes(loraName) {
|
|
if (!loraName) return false;
|
|
return Boolean(this.notesMap && this.notesMap[loraName]);
|
|
}
|
|
|
|
async _handleFiles(files) {
|
|
for (const file of files) {
|
|
if (!file.type.startsWith("image/")) continue;
|
|
this._setStatus("Compressing image...");
|
|
try {
|
|
const compressedBase64 = await compressImageSource(file, 1280, 0.82);
|
|
this.images.push({
|
|
url: compressedBase64,
|
|
meta: {
|
|
prompt: "",
|
|
negativePrompt: "",
|
|
model: "",
|
|
size: "",
|
|
sampler: "",
|
|
steps: "",
|
|
cfgScale: ""
|
|
}
|
|
});
|
|
this._renderGallery();
|
|
this._scheduleAutoSave();
|
|
this._setStatus("Image added");
|
|
} catch (err) {
|
|
console.error("Image compression error:", err);
|
|
}
|
|
}
|
|
}
|
|
|
|
_updateWordCount() {
|
|
const text = this.textarea.value.trim();
|
|
const count = text ? text.split(/\s+/).length : 0;
|
|
const el = this.drawer.querySelector("#trix-notes-word-count");
|
|
if (el) el.textContent = `${count} words (${text.length} chars)`;
|
|
}
|
|
|
|
_setStatus(text) {
|
|
const el = this.drawer.querySelector("#trix-notes-status-text");
|
|
if (el) el.textContent = text;
|
|
}
|
|
|
|
_scheduleAutoSave() {
|
|
if (this.autoSaveTimer) clearTimeout(this.autoSaveTimer);
|
|
this._setStatus("Unsaved changes...");
|
|
this.autoSaveTimer = setTimeout(() => {
|
|
this.save(true);
|
|
}, 1200);
|
|
}
|
|
|
|
/**
|
|
* Fetch previews, trigger words, description, and metadata from CivitAI
|
|
*/
|
|
async _fetchCivitaiGallery() {
|
|
if (!this.currentLora) return;
|
|
const civitaiBtn = this.drawer.querySelector("#trix-notes-civitai-btn");
|
|
const origText = civitaiBtn ? civitaiBtn.innerHTML : "";
|
|
if (civitaiBtn) {
|
|
civitaiBtn.disabled = true;
|
|
civitaiBtn.innerHTML = `<span>⌛︎</span> Fetching...`;
|
|
}
|
|
this._setStatus("Fetching previews from Civitai...");
|
|
|
|
try {
|
|
const resp = await fetch(`/super_lora/civitai_gallery?lora=${encodeURIComponent(this.currentLora)}`);
|
|
if (!resp.ok) {
|
|
throw new Error(`HTTP ${resp.status}`);
|
|
}
|
|
const data = await resp.json();
|
|
if (!data.success || !data.images || data.images.length === 0) {
|
|
this._setStatus("No Civitai previews found");
|
|
alert(`Civitai search for '${this.currentLora}' found no preview images.`);
|
|
return;
|
|
}
|
|
|
|
this._setStatus(`Downloading ${data.images.length} previews...`);
|
|
let addedCount = 0;
|
|
|
|
for (const item of data.images) {
|
|
if (!item.url) continue;
|
|
try {
|
|
// Download and compress image through proxy to bypass CORS
|
|
const proxyUrl = `/super_lora/proxy_image?url=${encodeURIComponent(item.url)}`;
|
|
const imgResp = await fetch(proxyUrl);
|
|
if (imgResp.ok) {
|
|
const blob = await imgResp.blob();
|
|
const base64 = await compressImageSource(blob, 1280, 0.82);
|
|
this.images.push({
|
|
url: base64,
|
|
civitaiUrl: item.url,
|
|
meta: item.meta || {}
|
|
});
|
|
addedCount++;
|
|
}
|
|
} catch (e) {
|
|
console.warn("[LoRA Notes] failed to download civitai preview image:", e);
|
|
}
|
|
}
|
|
|
|
// Auto-fill trigger words and prompt notes if textarea is currently empty
|
|
if (!this.notesText.trim()) {
|
|
const parts = [];
|
|
if (data.trainedWords && data.trainedWords.length > 0) {
|
|
parts.push(`Trigger Words: ${data.trainedWords.join(", ")}`);
|
|
}
|
|
if (data.modelName) {
|
|
parts.push(`Model: ${data.modelName}`);
|
|
}
|
|
if (data.description) {
|
|
const cleanDesc = data.description.replace(/<[^>]*>?/gm, "").trim();
|
|
if (cleanDesc) {
|
|
parts.push(`\nDescription:\n${cleanDesc.slice(0, 600)}`);
|
|
}
|
|
}
|
|
if (parts.length > 0) {
|
|
this.notesText = parts.join("\n");
|
|
this.textarea.value = this.notesText;
|
|
this._updateWordCount();
|
|
}
|
|
}
|
|
|
|
this._renderGallery();
|
|
this._scheduleAutoSave();
|
|
this._setStatus(`Loaded ${addedCount} previews from Civitai ✓`);
|
|
} catch (err) {
|
|
console.error("[LoRA Notes] Civitai fetch error:", err);
|
|
this._setStatus("Civitai fetch failed");
|
|
alert(`Failed to fetch Civitai previews: ${err.message}`);
|
|
} finally {
|
|
if (civitaiBtn) {
|
|
civitaiBtn.disabled = false;
|
|
civitaiBtn.innerHTML = origText || `<span>ᨒ</span> Civitai Previews`;
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* One-click Compress all gallery images down to max 512px to make JSON file much smaller
|
|
*/
|
|
async _compressAllImagesTo512() {
|
|
if (!this.images || this.images.length === 0) {
|
|
this._setStatus("No images to compress");
|
|
return;
|
|
}
|
|
const compressBtn = this.drawer.querySelector("#trix-notes-compress-btn");
|
|
const origBtnHtml = compressBtn ? compressBtn.innerHTML : "";
|
|
if (compressBtn) {
|
|
compressBtn.disabled = true;
|
|
compressBtn.innerHTML = `<span>⌛︎</span> Compressing...`;
|
|
}
|
|
this._setStatus("Compressing images to 512px...");
|
|
|
|
let count = 0;
|
|
for (let i = 0; i < this.images.length; i++) {
|
|
const item = this.images[i];
|
|
const norm = this._normalizeImageItem(item);
|
|
if (!norm.url) continue;
|
|
|
|
try {
|
|
let compressed512 = null;
|
|
if (norm.url.startsWith("data:")) {
|
|
compressed512 = await compressImageSource(norm.url, 512, 0.78);
|
|
} else {
|
|
const blob = await fetch(norm.url).then(r => r.blob()).catch(async () => {
|
|
if (norm.civitaiUrl) {
|
|
const pUrl = `/super_lora/proxy_image?url=${encodeURIComponent(norm.civitaiUrl)}`;
|
|
return await fetch(pUrl).then(r => r.blob());
|
|
}
|
|
return null;
|
|
});
|
|
if (blob) {
|
|
compressed512 = await compressImageSource(blob, 512, 0.78);
|
|
}
|
|
}
|
|
|
|
if (compressed512) {
|
|
if (typeof item === "string") {
|
|
this.images[i] = compressed512;
|
|
} else {
|
|
this.images[i] = {
|
|
...item,
|
|
url: compressed512
|
|
};
|
|
}
|
|
count++;
|
|
}
|
|
} catch (err) {
|
|
console.warn("[LoRA Notes] error compressing image to 512px:", err);
|
|
}
|
|
}
|
|
|
|
this._renderGallery();
|
|
await this.save(false);
|
|
this._setStatus(`Compressed ${count} images to 512px ✓`);
|
|
|
|
if (compressBtn) {
|
|
compressBtn.disabled = false;
|
|
compressBtn.innerHTML = origBtnHtml || `🗜 Compress JSON`;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to normalize an image item into { url, meta }
|
|
*/
|
|
_normalizeImageItem(item) {
|
|
if (!item) return { url: "", meta: {} };
|
|
if (typeof item === "string") {
|
|
return { url: item, meta: {} };
|
|
}
|
|
return {
|
|
url: item.url || item.civitaiUrl || "",
|
|
civitaiUrl: item.civitaiUrl,
|
|
meta: item.meta || {}
|
|
};
|
|
}
|
|
|
|
/**
|
|
* Renders the Centered Gallery with Drag & Drop Reordering
|
|
*/
|
|
_renderGallery() {
|
|
const gallery = this.drawer.querySelector("#trix-notes-gallery");
|
|
const countEl = this.drawer.querySelector("#trix-notes-img-count");
|
|
gallery.innerHTML = "";
|
|
gallery.style.setProperty("--trix-img-size", `${this.imageSize || 140}px`);
|
|
|
|
if (countEl) {
|
|
countEl.textContent = `${this.images.length} ${this.images.length === 1 ? 'image' : 'images'}`;
|
|
}
|
|
|
|
if (this.images.length === 0) {
|
|
const empty = document.createElement("div");
|
|
empty.className = "trix-notes-empty-gallery";
|
|
empty.innerHTML = `
|
|
<span class="empty-icon">🖼</span>
|
|
<span>No reference images yet</span>
|
|
<span style="color:#475569;font-size:10.5px;">Click "Civitai Previews" or drop images here</span>
|
|
`;
|
|
gallery.appendChild(empty);
|
|
return;
|
|
}
|
|
|
|
let draggedIndex = null;
|
|
|
|
this.images.forEach((item, index) => {
|
|
const norm = this._normalizeImageItem(item);
|
|
const card = document.createElement("div");
|
|
card.className = "trix-notes-img-card";
|
|
card.draggable = true;
|
|
card.dataset.index = index;
|
|
|
|
const img = document.createElement("img");
|
|
img.src = norm.url;
|
|
img.alt = `LoRA Reference ${index + 1}`;
|
|
|
|
const delBtn = document.createElement("button");
|
|
delBtn.className = "trix-notes-img-del";
|
|
delBtn.innerHTML = "✖︎";
|
|
delBtn.title = "Delete image";
|
|
delBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.images.splice(index, 1);
|
|
this._renderGallery();
|
|
this._scheduleAutoSave();
|
|
};
|
|
|
|
if (norm.meta?.prompt || norm.meta?.model) {
|
|
const badge = document.createElement("div");
|
|
badge.className = "trix-notes-img-badge";
|
|
badge.textContent = norm.meta.model ? "prompt+" : "prompt";
|
|
card.appendChild(badge);
|
|
}
|
|
|
|
card.onclick = () => this._openLightbox(index);
|
|
|
|
// Drag & Drop Reordering Events
|
|
card.addEventListener("dragstart", (e) => {
|
|
draggedIndex = index;
|
|
card.classList.add("dragging");
|
|
e.dataTransfer.effectAllowed = "move";
|
|
e.dataTransfer.setData("text/plain", String(index));
|
|
});
|
|
|
|
card.addEventListener("dragend", () => {
|
|
card.classList.remove("dragging");
|
|
gallery.querySelectorAll(".trix-notes-img-card").forEach((c) => c.classList.remove("drag-over"));
|
|
draggedIndex = null;
|
|
});
|
|
|
|
card.addEventListener("dragover", (e) => {
|
|
e.preventDefault();
|
|
if (draggedIndex !== null && draggedIndex !== index) {
|
|
card.classList.add("drag-over");
|
|
}
|
|
});
|
|
|
|
card.addEventListener("dragleave", () => {
|
|
card.classList.remove("drag-over");
|
|
});
|
|
|
|
card.addEventListener("drop", (e) => {
|
|
e.preventDefault();
|
|
card.classList.remove("drag-over");
|
|
if (draggedIndex !== null && draggedIndex !== index) {
|
|
const moved = this.images.splice(draggedIndex, 1)[0];
|
|
this.images.splice(index, 0, moved);
|
|
this._renderGallery();
|
|
this._scheduleAutoSave();
|
|
}
|
|
});
|
|
|
|
card.appendChild(img);
|
|
card.appendChild(delBtn);
|
|
gallery.appendChild(card);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Rich Lightbox Modal with Prompt & Generation Parameters Overlay (Screenshot 3 style)
|
|
*/
|
|
_openLightbox(initialIndex = 0) {
|
|
if (this.images.length === 0) return;
|
|
this.lightboxCurrentIndex = Math.max(0, Math.min(initialIndex, this.images.length - 1));
|
|
|
|
if (this.activeLightboxEl) {
|
|
this.activeLightboxEl.remove();
|
|
this.activeLightboxEl = null;
|
|
}
|
|
|
|
const lightbox = document.createElement("div");
|
|
lightbox.className = "trix-notes-lightbox";
|
|
|
|
// Apply node accent color to lightbox
|
|
const accent = this.currentNode?.properties?.highlightColor || this.currentNode?.color || "#387aff";
|
|
lightbox.style.setProperty("--trix-acc", accent);
|
|
this.activeLightboxEl = lightbox;
|
|
|
|
const renderCurrent = () => {
|
|
const idx = this.lightboxCurrentIndex;
|
|
const total = this.images.length;
|
|
const item = this.images[idx];
|
|
const norm = this._normalizeImageItem(item);
|
|
const meta = norm.meta || {};
|
|
|
|
lightbox.innerHTML = `
|
|
<!-- Top Bar -->
|
|
<div class="trix-lb-topbar">
|
|
<div class="trix-lb-counter">${idx + 1} / ${total}</div>
|
|
<button class="trix-lb-close-btn" id="trix-lb-close" title="Close (Esc)">✕</button>
|
|
</div>
|
|
|
|
<!-- Prev / Next Navigation -->
|
|
${total > 1 ? `<button class="trix-lb-nav-btn left" id="trix-lb-prev" title="Previous (Left Arrow)">◀</button>` : ""}
|
|
${total > 1 ? `<button class="trix-lb-nav-btn right" id="trix-lb-next" title="Next (Right Arrow)">▶</button>` : ""}
|
|
|
|
<!-- Center Big Image -->
|
|
<div class="trix-lb-img-wrap">
|
|
<img class="trix-lb-img" src="${norm.url}" alt="LoRA Preview ${idx + 1}" />
|
|
</div>
|
|
|
|
<!-- Floating Metadata Card (Bottom-Left) -->
|
|
<div class="trix-lb-meta-card" id="trix-lb-meta-card">
|
|
${meta.model ? `<div class="trix-lb-model-badge"><b>model</b> ${meta.model}</div>` : ""}
|
|
|
|
<div class="trix-lb-meta-header">
|
|
<span>Positive Prompt</span>
|
|
${meta.prompt ? `<button class="trix-lb-copy-icon-btn" id="trix-lb-copy-prompt" title="Copy Positive Prompt">⎘ Copy</button>` : ""}
|
|
</div>
|
|
|
|
<div class="trix-lb-prompt-box" id="trix-lb-prompt-text" contenteditable="true" spellcheck="false">${meta.prompt || "(No prompt recorded for this image. Click to edit/type...)"}</div>
|
|
|
|
${meta.negativePrompt ? `
|
|
<div class="trix-lb-meta-header" style="font-size:10px;color:#fca5a5;">Negative Prompt</div>
|
|
<div class="trix-lb-prompt-box neg" id="trix-lb-neg-prompt" contenteditable="true" spellcheck="false">${meta.negativePrompt}</div>
|
|
` : ""}
|
|
|
|
<!-- Other Generation Parameters Pills -->
|
|
<div class="trix-lb-params-row">
|
|
${meta.cfgScale ? `<span class="trix-lb-param-pill"><b>CFG:</b> ${meta.cfgScale}</span>` : ""}
|
|
${meta.steps ? `<span class="trix-lb-param-pill"><b>STEPS:</b> ${meta.steps}</span>` : ""}
|
|
${meta.sampler ? `<span class="trix-lb-param-pill"><b>SAMPLER:</b> ${meta.sampler}</span>` : ""}
|
|
${meta.size ? `<span class="trix-lb-param-pill"><b>SIZE:</b> ${meta.size}</span>` : ""}
|
|
${meta.seed ? `<span class="trix-lb-param-pill"><b>SEED:</b> ${meta.seed}</span>` : ""}
|
|
</div>
|
|
|
|
<!-- Bottom Actions -->
|
|
<div class="trix-lb-actions-row">
|
|
<div style="display:flex;gap:6px;">
|
|
<button class="trix-lb-copy-icon-btn" id="trix-lb-insert-notes" title="Append prompt into notes textarea">⬇ Add to Notes</button>
|
|
${norm.civitaiUrl ? `<a href="${norm.civitaiUrl}" target="_blank" class="trix-lb-copy-icon-btn" style="text-decoration:none;color:#93c5fd;">🔗 Civitai</a>` : ""}
|
|
</div>
|
|
<button class="trix-lb-del-btn" id="trix-lb-delete-img" title="Delete this image">🗑 Delete</button>
|
|
</div>
|
|
</div>
|
|
`;
|
|
|
|
// Event Listeners for Lightbox Controls
|
|
lightbox.querySelector("#trix-lb-close").onclick = (e) => {
|
|
e.stopPropagation();
|
|
lightbox.remove();
|
|
this.activeLightboxEl = null;
|
|
};
|
|
|
|
const prevBtn = lightbox.querySelector("#trix-lb-prev");
|
|
if (prevBtn) {
|
|
prevBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.lightboxCurrentIndex = (this.lightboxCurrentIndex - 1 + total) % total;
|
|
renderCurrent();
|
|
};
|
|
}
|
|
|
|
const nextBtn = lightbox.querySelector("#trix-lb-next");
|
|
if (nextBtn) {
|
|
nextBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.lightboxCurrentIndex = (this.lightboxCurrentIndex + 1) % total;
|
|
renderCurrent();
|
|
};
|
|
}
|
|
|
|
const copyPromptBtn = lightbox.querySelector("#trix-lb-copy-prompt");
|
|
if (copyPromptBtn && meta.prompt) {
|
|
copyPromptBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
navigator.clipboard.writeText(meta.prompt).then(() => {
|
|
copyPromptBtn.textContent = "✓ Copied";
|
|
setTimeout(() => { copyPromptBtn.textContent = "⎘ Copy"; }, 1500);
|
|
});
|
|
};
|
|
}
|
|
|
|
const insertNotesBtn = lightbox.querySelector("#trix-lb-insert-notes");
|
|
if (insertNotesBtn) {
|
|
insertNotesBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
const promptText = lightbox.querySelector("#trix-lb-prompt-text")?.innerText || meta.prompt || "";
|
|
if (promptText && promptText !== "(No prompt recorded for this image. Click to edit/type...)") {
|
|
this.textarea.value = (this.textarea.value ? this.textarea.value + "\n\n" : "") + promptText;
|
|
this.notesText = this.textarea.value;
|
|
this._updateWordCount();
|
|
this._scheduleAutoSave();
|
|
insertNotesBtn.textContent = "✓ Added!";
|
|
setTimeout(() => { insertNotesBtn.textContent = "⬇ Add to Notes"; }, 1500);
|
|
}
|
|
};
|
|
}
|
|
|
|
// Live editable prompt saving
|
|
const promptBox = lightbox.querySelector("#trix-lb-prompt-text");
|
|
if (promptBox) {
|
|
promptBox.onblur = () => {
|
|
const val = promptBox.innerText.trim();
|
|
if (val && val !== "(No prompt recorded for this image. Click to edit/type...)") {
|
|
if (typeof this.images[idx] === "string") {
|
|
this.images[idx] = { url: this.images[idx], meta: { prompt: val } };
|
|
} else {
|
|
this.images[idx].meta = this.images[idx].meta || {};
|
|
this.images[idx].meta.prompt = val;
|
|
}
|
|
this._scheduleAutoSave();
|
|
}
|
|
};
|
|
}
|
|
|
|
const delBtn = lightbox.querySelector("#trix-lb-delete-img");
|
|
if (delBtn) {
|
|
delBtn.onclick = (e) => {
|
|
e.stopPropagation();
|
|
this.images.splice(idx, 1);
|
|
this._renderGallery();
|
|
this._scheduleAutoSave();
|
|
if (this.images.length === 0) {
|
|
lightbox.remove();
|
|
this.activeLightboxEl = null;
|
|
} else {
|
|
this.lightboxCurrentIndex = Math.min(idx, this.images.length - 1);
|
|
renderCurrent();
|
|
}
|
|
};
|
|
}
|
|
|
|
// Prevent backdrop click when clicking inside metadata card or image
|
|
const metaCard = lightbox.querySelector("#trix-lb-meta-card");
|
|
if (metaCard) metaCard.onclick = (e) => e.stopPropagation();
|
|
const imgEl = lightbox.querySelector(".trix-lb-img");
|
|
if (imgEl) imgEl.onclick = (e) => e.stopPropagation();
|
|
};
|
|
|
|
lightbox.onclick = (e) => {
|
|
if (e.target === lightbox || e.target.classList.contains("trix-lb-img-wrap")) {
|
|
lightbox.remove();
|
|
this.activeLightboxEl = null;
|
|
}
|
|
};
|
|
|
|
// Keyboard arrow navigation
|
|
const keyHandler = (e) => {
|
|
if (!this.activeLightboxEl) {
|
|
window.removeEventListener("keydown", keyHandler);
|
|
return;
|
|
}
|
|
if (e.key === "ArrowLeft") {
|
|
this.lightboxCurrentIndex = (this.lightboxCurrentIndex - 1 + this.images.length) % this.images.length;
|
|
renderCurrent();
|
|
} else if (e.key === "ArrowRight") {
|
|
this.lightboxCurrentIndex = (this.lightboxCurrentIndex + 1) % this.images.length;
|
|
renderCurrent();
|
|
}
|
|
};
|
|
window.addEventListener("keydown", keyHandler);
|
|
|
|
renderCurrent();
|
|
document.body.appendChild(lightbox);
|
|
}
|
|
|
|
async show(loraName, node) {
|
|
this.currentLora = loraName;
|
|
this.currentNode = node;
|
|
|
|
// Apply node accent color dynamically
|
|
const accent = this.currentNode?.properties?.highlightColor || this.currentNode?.color || "#387aff";
|
|
this.drawer.style.setProperty("--trix-acc", accent);
|
|
this.backdrop.style.setProperty("--trix-acc", accent);
|
|
|
|
this.drawer.querySelector("#trix-notes-lora-title").textContent = loraName;
|
|
this.textarea.value = "";
|
|
this.images = [];
|
|
this.imageSize = 140;
|
|
this.textareaHeight = 280;
|
|
|
|
const sizeSlider = this.drawer.querySelector("#trix-notes-size-slider");
|
|
const sizeVal = this.drawer.querySelector("#trix-notes-size-val");
|
|
if (sizeSlider) sizeSlider.value = 140;
|
|
if (sizeVal) sizeVal.textContent = "140px";
|
|
|
|
this._renderGallery();
|
|
this._updateWordCount();
|
|
this._setStatus("Loading notes...");
|
|
|
|
if (!document.body.contains(this.backdrop)) {
|
|
document.body.appendChild(this.backdrop);
|
|
}
|
|
|
|
// Trigger open animation
|
|
requestAnimationFrame(() => {
|
|
this.backdrop.classList.add("is-open");
|
|
});
|
|
|
|
// Load from backend
|
|
try {
|
|
const resp = await fetch(`/super_lora/notes?lora=${encodeURIComponent(loraName)}`);
|
|
if (resp.ok) {
|
|
const data = await resp.json();
|
|
this.notesText = data.notes || "";
|
|
this.images = Array.isArray(data.images) ? data.images : [];
|
|
this.imageSize = Number(data.imageSize) || 140;
|
|
this.textareaHeight = Number(data.textareaHeight) || 280;
|
|
|
|
this.textarea.value = this.notesText;
|
|
this.textarea.style.height = `${this.textareaHeight}px`;
|
|
|
|
if (sizeSlider) sizeSlider.value = this.imageSize;
|
|
if (sizeVal) sizeVal.textContent = `${this.imageSize}px`;
|
|
|
|
this._updateWordCount();
|
|
this._renderGallery();
|
|
this._setStatus(data.updatedAt ? "Saved ✓" : "New Note");
|
|
} else {
|
|
this._setStatus("Ready");
|
|
}
|
|
} catch (err) {
|
|
console.warn("[LoRA Notes] load error:", err);
|
|
this._setStatus("Offline (Local)");
|
|
}
|
|
}
|
|
|
|
hide() {
|
|
this.backdrop.classList.remove("is-open");
|
|
if (this.activeLightboxEl) {
|
|
this.activeLightboxEl.remove();
|
|
this.activeLightboxEl = null;
|
|
}
|
|
setTimeout(() => {
|
|
if (this.backdrop.parentNode) {
|
|
this.backdrop.parentNode.removeChild(this.backdrop);
|
|
}
|
|
}, 250);
|
|
}
|
|
|
|
async save(isAuto = false) {
|
|
if (!this.currentLora) return;
|
|
this._setStatus("Saving...");
|
|
try {
|
|
const payload = {
|
|
lora: this.currentLora,
|
|
notes: this.notesText,
|
|
images: this.images,
|
|
imageSize: this.imageSize || 140,
|
|
textareaHeight: this.textareaHeight || 280
|
|
};
|
|
const resp = await fetch("/super_lora/notes", {
|
|
method: "POST",
|
|
headers: { "Content-Type": "application/json" },
|
|
body: JSON.stringify(payload)
|
|
});
|
|
if (resp.ok) {
|
|
this._setStatus("Saved ✓");
|
|
this.notesCache.set(this.currentLora, payload);
|
|
if (this.notesMap) {
|
|
this.notesMap[this.currentLora] = Boolean(this.notesText || this.images.length > 0);
|
|
}
|
|
window.dispatchEvent(new CustomEvent("trix_lora_notes_updated", { detail: { lora: this.currentLora, hasNotes: Boolean(this.notesText || this.images.length > 0) } }));
|
|
if (this.currentNode?.setDirtyCanvas) {
|
|
this.currentNode.setDirtyCanvas(true, false);
|
|
}
|
|
} else {
|
|
const errText = await resp.text().catch(() => "");
|
|
console.error(`[LoRA Notes] save failed HTTP ${resp.status}:`, errText);
|
|
this._setStatus(`Error ${resp.status}`);
|
|
}
|
|
} catch (err) {
|
|
console.error("[LoRA Notes] save exception:", err);
|
|
this._setStatus("Save failed");
|
|
}
|
|
}
|
|
}
|
|
|