2025-12-19 02:19:10 +08:00
<!DOCTYPE html>
< html lang = "zh-CN" >
< head >
< meta charset = "UTF-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" >
2025-12-28 00:49:25 +08:00
< meta name = "apple-mobile-web-app-capable" content = "yes" >
< meta name = "mobile-web-app-capable" content = "yes" >
< title > Novel Draw< / title >
< link rel = "stylesheet" href = "https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" >
2025-12-19 02:19:10 +08:00
< style >
* { margin: 0; padding: 0; box-sizing: border-box; }
:root {
2025-12-28 00:49:25 +08:00
--bg-primary: #0d1117;
--bg-secondary: #161b22;
--bg-tertiary: #21262d;
--bg-input: rgba(0, 0, 0, 0.25);
--text-primary: #e6edf3;
--text-secondary: #8b949e;
--text-muted: #484f58;
--border: rgba(255, 255, 255, 0.1);
--border-focus: rgba(212, 165, 116, 0.5);
2025-12-19 02:19:10 +08:00
--accent: #d4a574;
2025-12-28 00:49:25 +08:00
--accent-soft: rgba(212, 165, 116, 0.15);
--success: #3fb950;
--danger: #f85149;
--warning: #d29922;
}
html, body { height: auto; overflow-y: auto; -webkit-overflow-scrolling: touch; }
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
2025-12-19 02:19:10 +08:00
background: var(--bg-primary);
color: var(--text-primary);
2025-12-28 00:49:25 +08:00
font-size: 14px;
2025-12-19 02:19:10 +08:00
line-height: 1.5;
2025-12-28 00:49:25 +08:00
min-height: 100vh;
}
.app-container { display: flex; flex-direction: column; min-height: 100vh; }
.app-header {
display: flex; align-items: center; gap: 12px;
padding: 12px 20px; background: var(--bg-secondary);
border-bottom: 1px solid var(--border); flex-wrap: wrap;
}
.app-body { display: flex; flex: 1; min-height: 0; }
.app-sidebar {
width: 200px; min-width: 200px; background: var(--bg-secondary);
border-right: 1px solid var(--border); padding: 16px 8px;
display: flex; flex-direction: column; gap: 4px;
}
.app-main { flex: 1; padding: 24px; overflow-y: auto; }
.header-logo { display: flex; align-items: center; gap: 8px; font-size: 16px; font-weight: 600; white-space: nowrap; }
.header-logo i { color: var(--accent); }
.header-badge {
display: flex; align-items: center; gap: 6px; padding: 4px 10px;
background: var(--bg-input); border: 1px solid var(--border);
border-radius: 12px; font-size: 11px; color: var(--text-muted);
}
.header-badge.on { color: var(--success); border-color: rgba(63, 185, 80, 0.3); }
.header-badge i { font-size: 6px; }
.header-spacer { flex: 1; min-width: 10px; }
.header-mode {
display: flex; background: var(--bg-input);
border: 1px solid var(--border); border-radius: 16px; padding: 2px;
}
.header-mode button {
padding: 6px 14px; border: none; border-radius: 14px;
background: transparent; color: var(--text-secondary);
font-size: 12px; cursor: pointer; transition: all 0.15s; white-space: nowrap;
}
.header-mode button.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
.header-close {
width: 36px; height: 36px; min-width: 36px;
border: 1px solid var(--border); border-radius: 8px;
background: transparent; color: var(--text-secondary);
cursor: pointer; display: flex; align-items: center; justify-content: center; font-size: 16px;
}
.header-close:hover { background: rgba(255,255,255,0.08); color: var(--text-primary); }
.nav-item {
display: flex; align-items: center; gap: 10px; padding: 10px 14px;
border-radius: 8px; color: var(--text-secondary); cursor: pointer;
transition: all 0.15s; font-size: 13px;
}
.nav-item:hover { background: rgba(255,255,255,0.04); color: var(--text-primary); }
.nav-item.active { background: var(--accent-soft); color: var(--accent); font-weight: 500; }
.nav-item i { width: 18px; text-align: center; }
.nav-divider { height: 1px; background: var(--border); margin: 8px 0; }
.view { display: none; max-width: 800px; margin: 0 auto; }
.view.active { display: block; animation: viewIn 0.2s ease; }
.view.wide { max-width: 1200px; }
@keyframes viewIn { from { opacity: 0; transform: translateY(8px); } to { opacity: 1; } }
.view-header { margin-bottom: 20px; }
.view-title { font-size: 20px; font-weight: 600; margin-bottom: 4px; }
.view-desc { font-size: 13px; color: var(--text-secondary); }
.card {
background: var(--bg-secondary); border: 1px solid var(--border);
border-radius: 12px; padding: 20px; margin-bottom: 16px;
}
.card-title { font-size: 13px; font-weight: 600; margin-bottom: 16px; color: var(--accent); text-transform: uppercase; letter-spacing: 0.05em; }
.form-group { margin-bottom: 16px; }
.form-group:last-child { margin-bottom: 0; }
.form-label { display: block; font-size: 12px; color: var(--text-secondary); margin-bottom: 6px; font-weight: 500; }
.form-hint { font-size: 11px; color: var(--text-muted); margin-top: 4px; }
.form-row { display: grid; grid-template-columns: repeat(auto-fit, minmax(140px, 1fr)); gap: 12px; }
.input {
width: 100%; padding: 10px 12px; background: var(--bg-input);
border: 1px solid var(--border); border-radius: 8px;
color: var(--text-primary); font-size: 13px; transition: border-color 0.15s;
}
.input:focus { outline: none; border-color: var(--border-focus); }
.input::placeholder { color: var(--text-muted); }
textarea.input { min-height: 80px; resize: vertical; font-family: inherit; }
select.input { cursor: pointer; }
.input-row { display: flex; gap: 8px; }
.input-row .input { flex: 1; min-width: 0; }
.btn {
display: inline-flex; align-items: center; justify-content: center; gap: 6px;
padding: 10px 16px; min-height: 40px; border: 1px solid var(--border);
border-radius: 8px; background: var(--bg-tertiary); color: var(--text-primary);
font-size: 13px; cursor: pointer; transition: all 0.15s; white-space: nowrap;
}
.btn:hover { background: rgba(255,255,255,0.08); }
.btn:active { transform: scale(0.98); }
.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn-primary { background: var(--accent); border-color: var(--accent); color: #000; font-weight: 500; }
.btn-primary:hover { background: #e5b685; }
.btn-danger { color: var(--danger); border-color: rgba(248, 81, 73, 0.3); }
.btn-danger:hover { background: rgba(248, 81, 73, 0.1); }
.btn-icon { width: 40px; padding: 0; }
.btn-group { display: flex; gap: 8px; flex-wrap: wrap; }
.btn-sm { padding: 6px 10px; min-height: 32px; font-size: 12px; }
.btn.saving { pointer-events: none; opacity: 0.7; }
.btn.save-success { background: var(--success) !important; border-color: var(--success) !important; color: #fff !important; pointer-events: none; }
.btn.save-success i { animation: checkBounce 0.4s cubic-bezier(0.175, 0.885, 0.32, 1.275); }
@keyframes checkBounce { 0% { transform: scale(0) rotate(-45deg); } 50% { transform: scale(1.3) rotate(0deg); } 100% { transform: scale(1) rotate(0deg); } }
.preset-bar {
display: flex; align-items: center; gap: 8px; padding: 12px 16px;
background: var(--bg-tertiary); border: 1px solid var(--border);
border-radius: 10px; margin-bottom: 16px; flex-wrap: wrap;
}
.preset-bar select { flex: 1; min-width: 120px; max-width: 200px; }
.char-grid { display: flex; flex-direction: column; gap: 12px; }
.char-card {
background: var(--bg-tertiary); border: 1px solid var(--border);
border-radius: 10px; padding: 16px; display: flex; gap: 12px; transition: border-color 0.2s;
}
.char-card:hover { border-color: var(--accent); }
.char-card.editing { border-color: var(--accent); background: var(--accent-soft); }
.char-avatar {
width: 44px; height: 44px; border-radius: 50%; background: var(--accent-soft);
display: flex; align-items: center; justify-content: center; font-size: 20px; flex-shrink: 0;
}
.char-info { flex: 1; min-width: 0; }
.char-name { font-weight: 600; font-size: 14px; margin-bottom: 2px; }
.char-aliases { font-size: 11px; color: var(--text-muted); margin-bottom: 4px; }
.char-tags { font-size: 12px; color: var(--text-secondary); line-height: 1.5; word-break: break-word; }
.char-actions { display: flex; flex-direction: column; gap: 6px; flex-shrink: 0; }
.char-actions .btn { padding: 6px 10px; min-height: 32px; font-size: 12px; }
.char-empty { text-align: center; padding: 40px 20px; color: var(--text-muted); }
.char-empty i { font-size: 32px; margin-bottom: 12px; opacity: 0.5; display: block; }
.char-edit-form { margin-top: 12px; padding-top: 12px; border-top: 1px solid var(--border); }
.char-edit-form .form-group { margin-bottom: 10px; }
.char-edit-form .form-label { font-size: 11px; margin-bottom: 4px; }
.char-edit-form .input { padding: 8px 10px; font-size: 12px; }
.char-edit-form textarea.input { min-height: 60px; }
.char-pos-row { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; }
.preview-box {
margin-top: 16px; background: var(--bg-input); border: 1px solid var(--border);
border-radius: 10px; padding: 16px; text-align: center; display: none;
}
.preview-box.visible { display: block; }
.preview-box img { max-width: 100%; max-height: 400px; border-radius: 6px; }
.status-text { font-size: 12px; color: var(--text-secondary); margin-top: 8px; min-height: 18px; }
.status-text.success { color: var(--success); }
.status-text.error { color: var(--danger); }
.status-text.loading { color: var(--warning); }
.tip-box {
display: flex; gap: 10px; padding: 12px 14px; background: var(--accent-soft);
border: 1px solid rgba(212, 165, 116, 0.2); border-radius: 8px;
font-size: 12px; color: var(--text-secondary); line-height: 1.6;
}
.tip-box i { color: var(--accent); flex-shrink: 0; margin-top: 2px; }
.stat-grid { display: grid; grid-template-columns: repeat(2, 1fr); gap: 16px; margin-bottom: 20px; }
.stat-item { text-align: center; }
.stat-value { font-size: 28px; font-weight: 700; color: var(--accent); }
.stat-label { font-size: 12px; color: var(--text-secondary); margin-top: 4px; }
/* ═══════════════════════════════════════════════════════════════════════════
画廊样式
═══════════════════════════════════════════════════════════════════════════ */
.gallery-char-section { margin-bottom: 16px; }
.gallery-char-header {
display: flex; align-items: center; gap: 12px; padding: 14px 18px;
background: var(--bg-tertiary); border: 1px solid var(--border);
border-radius: 10px; cursor: pointer; transition: all 0.15s;
}
.gallery-char-section:not(.collapsed) .gallery-char-header {
border-radius: 10px 10px 0 0;
}
.gallery-char-header:hover { background: rgba(255,255,255,0.05); }
.gallery-char-avatar {
width: 36px; height: 36px; border-radius: 50%; background: var(--accent-soft);
display: flex; align-items: center; justify-content: center; font-size: 18px;
}
.gallery-char-name { font-weight: 600; flex: 1; }
.gallery-char-stats { font-size: 12px; color: var(--text-secondary); }
.gallery-char-toggle { color: var(--text-muted); transition: transform 0.2s; }
.gallery-char-section.collapsed .gallery-char-toggle { transform: rotate(-90deg); }
.gallery-char-section.collapsed .gallery-slots { display: none; }
.gallery-slots {
display: grid; grid-template-columns: repeat(auto-fill, minmax(160px, 1fr)); gap: 12px;
padding: 16px; background: var(--bg-secondary);
border: 1px solid var(--border); border-top: none; border-radius: 0 0 10px 10px;
}
.gallery-slot {
background: var(--bg-tertiary); border: 1px solid var(--border);
border-radius: 10px; overflow: hidden; cursor: pointer; transition: all 0.2s; position: relative;
}
.gallery-slot:hover { border-color: var(--accent); transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0,0,0,0.3); }
.gallery-slot-img { width: 100%; aspect-ratio: 4/3; object-fit: cover; display: block; background: var(--bg-input); }
.gallery-slot-placeholder {
width: 100%; aspect-ratio: 4/3; display: flex; align-items: center; justify-content: center;
background: var(--bg-input); color: var(--text-muted); font-size: 24px;
}
.gallery-slot-overlay {
position: absolute; top: 0; left: 0; right: 0; bottom: 0;
background: linear-gradient(to bottom, transparent 50%, rgba(0,0,0,0.8));
opacity: 0; transition: opacity 0.2s; display: flex;
flex-direction: column; justify-content: flex-end; padding: 10px;
}
.gallery-slot:hover .gallery-slot-overlay { opacity: 1; }
.gallery-slot-actions { display: flex; gap: 6px; justify-content: center; }
.gallery-slot-actions .btn { padding: 6px 10px; min-height: 28px; font-size: 11px; background: rgba(0,0,0,0.6); backdrop-filter: blur(4px); }
.gallery-slot-info {
padding: 10px 12px; font-size: 11px; color: var(--text-secondary);
display: flex; justify-content: space-between; align-items: center; border-top: 1px solid var(--border);
}
.gallery-slot-badge {
display: inline-flex; align-items: center; gap: 4px;
padding: 2px 8px; border-radius: 10px; font-size: 10px; font-weight: 600;
}
.gallery-slot-badge.count { background: var(--accent-soft); color: var(--accent); }
.gallery-slot-badge.saved { background: rgba(63, 185, 80, 0.15); color: var(--success); }
.gallery-empty {
text-align: center; padding: 60px 20px; color: var(--text-muted);
}
.gallery-empty i { font-size: 48px; margin-bottom: 16px; opacity: 0.4; display: block; }
.gallery-empty p { font-size: 13px; margin-top: 8px; }
.gallery-loading {
grid-column: 1 / -1; text-align: center; padding: 40px 20px;
color: var(--text-muted); font-size: 13px;
}
.gallery-loading i { margin-right: 8px; }
.gallery-empty-hint {
grid-column: 1 / -1; text-align: center; padding: 30px 20px;
color: var(--text-muted); font-size: 13px;
}
/* 画廊弹窗 */
.gallery-modal {
position: fixed; top: 0; left: 0; right: 0; bottom: 0;
background: rgba(0,0,0,0.9); z-index: 1000;
display: none; flex-direction: column; align-items: center; justify-content: center; padding: 20px;
}
.gallery-modal.visible { display: flex; }
.gallery-modal-close {
position: absolute; top: 16px; right: 16px; width: 44px; height: 44px;
border: none; background: rgba(255,255,255,0.1); border-radius: 50%;
color: #fff; font-size: 20px; cursor: pointer; z-index: 10;
}
.gallery-modal-close:hover { background: rgba(255,255,255,0.2); }
.gallery-modal-content { max-width: 90vw; max-height: 80vh; display: flex; flex-direction: column; align-items: center; gap: 16px; }
.gallery-modal-img { max-width: 100%; max-height: 60vh; border-radius: 12px; box-shadow: 0 8px 32px rgba(0,0,0,0.5); }
.gallery-modal-thumbs {
display: flex; gap: 8px; padding: 12px; background: rgba(0,0,0,0.5);
border-radius: 12px; max-width: 100%; overflow-x: auto;
}
.gallery-modal-thumb {
width: 60px; height: 60px; border-radius: 8px; object-fit: cover;
cursor: pointer; border: 2px solid transparent; opacity: 0.6;
transition: all 0.15s; flex-shrink: 0;
}
.gallery-modal-thumb:hover { opacity: 0.9; }
.gallery-modal-thumb.active { border-color: var(--accent); opacity: 1; }
.gallery-modal-thumb.saved { border-color: var(--success); }
.gallery-modal-actions { display: flex; gap: 12px; }
.gallery-modal-info { font-size: 12px; color: rgba(255,255,255,0.6); text-align: center; }
/* 移动端底部导航 */
.mobile-nav {
display: none; position: fixed; bottom: 0; left: 0; right: 0;
height: 60px; background: var(--bg-secondary); border-top: 1px solid var(--border); z-index: 100;
}
.mobile-nav-inner { display: flex; height: 100%; overflow-x: auto; -webkit-overflow-scrolling: touch; }
.mobile-nav-inner::-webkit-scrollbar { display: none; }
.mobile-nav-item {
flex: 1; min-width: 55px; display: flex; flex-direction: column;
align-items: center; justify-content: center; gap: 4px;
color: var(--text-muted); font-size: 10px; cursor: pointer; padding: 8px 4px;
}
.mobile-nav-item i { font-size: 18px; }
.mobile-nav-item.active { color: var(--accent); }
@media (max-width: 768px) {
.app-sidebar { display: none; }
.mobile-nav { display: block; }
.app-body { padding-bottom: 60px; }
.app-main { padding: 16px; padding-bottom: 76px; }
.app-header { padding: 10px 12px; gap: 8px; }
.header-logo span { display: none; }
.header-badge { padding: 4px 8px; font-size: 10px; }
.header-badge span { display: none; }
.header-mode button { padding: 5px 10px; font-size: 11px; }
.header-close { width: 32px; height: 32px; min-width: 32px; font-size: 14px; }
.view-title { font-size: 18px; }
.card { padding: 16px; }
.form-row { grid-template-columns: 1fr; }
.preset-bar { padding: 10px 12px; }
.preset-bar select { max-width: none; }
.stat-value { font-size: 24px; }
.gallery-slots { grid-template-columns: repeat(auto-fill, minmax(120px, 1fr)); gap: 10px; padding: 12px; }
}
@media (max-width: 400px) {
.app-header { padding: 8px 10px; }
.header-mode button { padding: 4px 8px; font-size: 10px; }
.mobile-nav-item { min-width: 48px; font-size: 9px; }
.mobile-nav-item i { font-size: 16px; }
}
@media (hover: none) and (pointer: coarse) {
.btn { min-height: 44px; }
.input { min-height: 44px; padding: 12px; }
.nav-item { min-height: 44px; }
.header-close { width: 44px; height: 44px; min-width: 44px; }
.gallery-slot-overlay { opacity: 1; background: linear-gradient(to bottom, transparent 60%, rgba(0,0,0,0.7)); }
}
::-webkit-scrollbar { width: 6px; height: 6px; }
::-webkit-scrollbar-track { background: transparent; }
::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.1); border-radius: 3px; }
::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.2); }
.hidden { display: none !important; }
.header-credit {
font-size: 11px;
2025-12-19 02:19:10 +08:00
color: var(--text-muted);
2025-12-28 00:49:25 +08:00
opacity: 0.5;
margin-left: 6px;
letter-spacing: 0.02em;
font-style: italic;
transition: opacity 0.2s;
2025-12-19 02:19:10 +08:00
white-space: nowrap;
}
2025-12-28 00:49:25 +08:00
.header-credit:hover {
opacity: 0.9;
2025-12-19 02:19:10 +08:00
}
2025-12-28 00:49:25 +08:00
.credit-author {
font-style: normal;
2025-12-19 02:19:10 +08:00
color: var(--text-secondary);
}
2025-12-28 00:49:25 +08:00
@media (max-width: 768px) {
.header-credit { display: none; }
2025-12-19 02:19:10 +08:00
}
< / style >
< / head >
< body >
2025-12-28 00:49:25 +08:00
< div class = "app-container" >
< header class = "app-header" >
< div class = "header-logo" > < i class = "fa-solid fa-palette" > < / i > < span > Novel Draw< / span > < / div >
< div id = "nd_badge" class = "header-badge" > < i class = "fa-solid fa-circle" > < / i > < span > 未启用< / span > < / div >
< span class = "header-credit" > · < span class = "credit-author" > 𝓡 𝓸 𝓻 𝓸 𝓵 𝓵 𝓵 𝓷 < / span > < / span >
< div class = "header-spacer" > < / div >
< div class = "header-mode" >
< button data-mode = "manual" class = "active" > 手动< / button >
< button data-mode = "auto" > 自动< / button >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< button id = "nd_close" class = "header-close" > ✕< / button >
2025-12-19 02:19:10 +08:00
< / header >
2025-12-28 00:49:25 +08:00
< div class = "app-body" >
< nav class = "app-sidebar" >
< div class = "nav-item active" data-view = "test" > < i class = "fa-solid fa-flask" > < / i > 快速测试< / div >
< div class = "nav-item" data-view = "api" > < i class = "fa-solid fa-key" > < / i > API 配置< / div >
< div class = "nav-divider" > < / div >
< div class = "nav-item" data-view = "params" > < i class = "fa-solid fa-sliders" > < / i > 绘图参数< / div >
< div class = "nav-item" data-view = "prompts" > < i class = "fa-solid fa-tags" > < / i > 固定标签< / div >
< div class = "nav-item" data-view = "llm" > < i class = "fa-solid fa-robot" > < / i > LLM 配置< / div >
< div class = "nav-divider" > < / div >
< div class = "nav-item" data-view = "gallery" > < i class = "fa-solid fa-images" > < / i > 图片管理< / div >
< / nav >
< main class = "app-main" >
<!-- 快速测试 -->
< div id = "view-test" class = "view active" >
< div class = "view-header" >
< h2 class = "view-title" > 快速测试< / h2 >
< p class = "view-desc" > 验证 API 连接和生成效果< / p >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div class = "card" >
< div class = "form-group" >
< label class = "form-label" > 测试 TAG< / label >
< div class = "input-row" >
< input id = "nd_test_tags" type = "text" class = "input" value = "1girl, smile, upper body, simple background" >
< button id = "nd_test_single" class = "btn btn-primary" > < i class = "fa-solid fa-play" > < / i > 生成< / button >
< / div >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div id = "nd_preview" class = "preview-box" > < img id = "nd_preview_img" alt = "" > < / div >
< div id = "nd_status" class = "status-text" > < / div >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div class = "tip-box" >
< i class = "fa-solid fa-lightbulb" > < / i >
< div > 聊天界面点击悬浮球 🎨 即可为最后一条AI消息生成配图。开启自动模式后, AI回复时会自动配图。< / div >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
<!-- API 配置 -->
< div id = "view-api" class = "view" >
< div class = "view-header" >
< h2 class = "view-title" > API 配置< / h2 >
< p class = "view-desc" > NovelAI 服务连接设置< / p >
< / div >
< div class = "card" >
< div class = "form-group" >
< label class = "form-label" > NovelAI API Key< / label >
< div class = "input-row" >
< input id = "nd_api_key" type = "password" class = "input" placeholder = "pst-xxxxxxxx..." >
< button id = "nd_toggle_key" class = "btn btn-icon" > < i class = "fa-solid fa-eye" > < / i > < / button >
< / div >
< p class = "form-hint" > 仅存储在本地浏览器< / p >
< / div >
< div class = "form-row" >
< div class = "form-group" >
< label class = "form-label" > 超时时间 (秒)< / label >
< input id = "nd_timeout" type = "number" class = "input" value = "60" >
< / div >
< div class = "form-group" >
< label class = "form-label" > 请求间隔 (毫秒)< / label >
< input id = "nd_delay" type = "text" class = "input" value = "15000-30000" >
< / div >
< / div >
< div class = "btn-group" style = "margin-top: 16px;" >
< button id = "nd_save_api" class = "btn btn-primary" > < i class = "fa-solid fa-floppy-disk" > < / i > 保存< / button >
< button id = "nd_test_api" class = "btn" > < i class = "fa-solid fa-plug" > < / i > 测试连接< / button >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div id = "nd_api_status" class = "status-text" > < / div >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< / div >
2025-12-19 02:19:10 +08:00
2025-12-28 00:49:25 +08:00
<!-- 绘图参数 -->
< div id = "view-params" class = "view" >
< div class = "view-header" >
< h2 class = "view-title" > 绘图参数< / h2 >
< p class = "view-desc" > 模型与生成参数设置< / p >
< / div >
< div class = "preset-bar" >
< select id = "nd_params_preset" class = "input" > < / select >
< div class = "btn-group" >
< button id = "nd_params_add" class = "btn btn-icon" title = "新建" > < i class = "fa-solid fa-plus" > < / i > < / button >
< button id = "nd_params_rename" class = "btn btn-icon" title = "重命名" > < i class = "fa-solid fa-pen" > < / i > < / button >
< button id = "nd_params_save" class = "btn btn-primary" title = "保存" > < i class = "fa-solid fa-floppy-disk" > < / i > < / button >
< button id = "nd_params_del" class = "btn btn-danger btn-icon" title = "删除" > < i class = "fa-solid fa-trash" > < / i > < / button >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
< div class = "card" >
< div class = "card-title" > 模型与采样< / div >
< div class = "form-row" >
< div class = "form-group" >
< label class = "form-label" > 模型< / label >
< select id = "nd_model_sel" class = "input" >
< option value = "nai-diffusion-4-5-full" > NAI V4.5 Full< / option >
< option value = "nai-diffusion-4-5-curated" > NAI V4.5 Curated< / option >
< option value = "nai-diffusion-4-full" > NAI V4 Full< / option >
< option value = "nai-diffusion-3" > NAI V3< / option >
< option value = "nai-diffusion-furry-3" > Furry V3< / option >
< option value = "custom" > 自定义...< / option >
< / select >
< input id = "nd_model" type = "text" class = "input hidden" placeholder = "模型ID" style = "margin-top:6px;" >
< / div >
< div class = "form-group" >
< label class = "form-label" > 采样器< / label >
< select id = "nd_sampler_sel" class = "input" >
< option value = "k_euler_ancestral" > Euler Ancestral< / option >
< option value = "k_euler" > Euler< / option >
< option value = "k_dpmpp_2m" > DPM++ 2M< / option >
< option value = "k_dpmpp_sde" > DPM++ SDE< / option >
< option value = "ddim" > DDIM< / option >
< option value = "custom" > 自定义...< / option >
< / select >
< input id = "nd_sampler" type = "text" class = "input hidden" style = "margin-top:6px;" >
< / div >
< div class = "form-group" >
< label class = "form-label" > 调度器< / label >
< select id = "nd_scheduler_sel" class = "input" >
< option value = "native" > Native< / option >
< option value = "karras" > Karras< / option >
< option value = "exponential" > Exponential< / option >
< option value = "custom" > 自定义...< / option >
< / select >
< input id = "nd_scheduler" type = "text" class = "input hidden" style = "margin-top:6px;" >
< / div >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
< div class = "card" >
< div class = "card-title" > 尺寸与参数< / div >
< div class = "form-row" >
< div class = "form-group" >
< label class = "form-label" > 尺寸预设< / label >
< select id = "nd_size_preset" class = "input" >
< option value = "832x1216" > 832× 1216 竖图< / option >
< option value = "1216x832" > 1216× 832 横图< / option >
< option value = "1024x1024" > 1024× 1024 方图< / option >
< option value = "768x1280" > 768× 1280 竖图< / option >
< option value = "1280x768" > 1280× 768 横图< / option >
< option value = "custom" > 自定义...< / option >
< / select >
< div id = "nd_custom_size" class = "input-row hidden" style = "margin-top:6px;" >
< input id = "nd_width" type = "number" class = "input" placeholder = "宽度" >
< input id = "nd_height" type = "number" class = "input" placeholder = "高度" >
< / div >
< / div >
< div class = "form-group" >
< label class = "form-label" > Steps / CFG / Seed< / label >
< div class = "input-row" >
< input id = "nd_steps" type = "number" class = "input" placeholder = "23" title = "Steps" >
< input id = "nd_scale" type = "number" class = "input" placeholder = "5" title = "CFG" >
< input id = "nd_seed" type = "number" class = "input" placeholder = "-1" title = "Seed" >
< / div >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
< / div >
< div class = "card" >
< div class = "card-title" > 增强选项< / div >
< div class = "form-row" >
< div class = "form-group" >
< label class = "form-label" > 质量增强< / label >
< select id = "nd_quality_toggle" class = "input" >
< option value = "true" > 开启< / option >
< option value = "false" > 关闭< / option >
< / select >
< / div >
< div class = "form-group" >
< label class = "form-label" > 负面预设< / label >
< select id = "nd_uc_preset" class = "input" >
< option value = "0" > Heavy< / option >
< option value = "1" > Light< / option >
< option value = "2" > Human Focus< / option >
< option value = "3" > None< / option >
< / select >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
< div class = "form-row" >
< div class = "form-group" >
< label class = "form-label" > 自动 SMEA< / label >
< select id = "nd_auto_smea" class = "input" >
< option value = "false" > 关闭< / option >
< option value = "true" > 开启< / option >
< / select >
< / div >
< div class = "form-group" >
< label class = "form-label" > CFG 重缩放< / label >
< input id = "nd_cfg_rescale" type = "number" class = "input" value = "0" min = "0" max = "1" step = "0.05" >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
< div class = "form-group" style = "margin-top:12px;" >
< label style = "display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;" >
< input type = "checkbox" id = "nd_variety_boost" > 多样性增强 (V4.5)
< / label >
< / div >
< div id = "nd_v3_opts" class = "form-group hidden" style = "margin-top:12px;" >
< label style = "display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;" > < input type = "checkbox" id = "nd_sm" > SMEA< / label >
< label style = "display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;margin-top:8px;" > < input type = "checkbox" id = "nd_sm_dyn" > SMEA 动态< / label >
< label style = "display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;margin-top:8px;" > < input type = "checkbox" id = "nd_decrisper" > Decrisper< / label >
< / div >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
<!-- 固定标签 -->
< div id = "view-prompts" class = "view" >
< div class = "view-header" >
< h2 class = "view-title" > 固定标签< / h2 >
< p class = "view-desc" > 全局标签和参数预设绑定,角色标签在所有预设间共享< / p >
< / div >
< div class = "card" >
< div class = "card-title" > 🌐 全局标签< / div >
< div class = "form-group" >
< label class = "form-label" > 正向固定< / label >
< textarea id = "nd_positive" class = "input" rows = "3" placeholder = "masterpiece, best quality..." > < / textarea >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div class = "form-group" >
< label class = "form-label" > 负向固定< / label >
< textarea id = "nd_negative" class = "input" rows = "3" placeholder = "lowres, bad anatomy..." > < / textarea >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div class = "btn-group" style = "margin-top:12px;justify-content:flex-end;" >
< button id = "nd_prompts_save" class = "btn btn-primary" > < i class = "fa-solid fa-floppy-disk" > < / i > 保存< / button >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
< div class = "card" >
< div class = "card-title" > 👥 角色标签< / div >
< div class = "btn-group" style = "margin-bottom:16px;" >
< button id = "nd_char_add" class = "btn btn-primary" > < i class = "fa-solid fa-plus" > < / i > 添加角色< / button >
< button id = "nd_char_export" class = "btn" > < i class = "fa-solid fa-download" > < / i > 导出< / button >
< label class = "btn" style = "cursor:pointer;" > < i class = "fa-solid fa-upload" > < / i > 导入< input type = "file" id = "nd_char_import" accept = ".json" style = "display:none;" > < / label >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div id = "nd_char_list" class = "char-grid" > < / div >
< div id = "nd_char_empty" class = "char-empty" >
< i class = "fa-solid fa-user-plus" > < / i >
< div > 暂无角色配置< / div >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
< / div >
2025-12-28 00:49:25 +08:00
<!-- LLM 配置 -->
< div id = "view-llm" class = "view" >
< div class = "view-header" >
< h2 class = "view-title" > LLM 配置< / h2 >
< p class = "view-desc" > 场景分析所用的大语言模型设置< / p >
< / div >
< div class = "preset-bar" >
< select id = "nd_llm_preset" class = "input" > < / select >
< div class = "btn-group" >
< button id = "nd_llm_add" class = "btn btn-icon" title = "新建" > < i class = "fa-solid fa-plus" > < / i > < / button >
< button id = "nd_llm_rename" class = "btn btn-icon" title = "重命名" > < i class = "fa-solid fa-pen" > < / i > < / button >
< button id = "nd_llm_save" class = "btn btn-primary" title = "保存" > < i class = "fa-solid fa-floppy-disk" > < / i > < / button >
< button id = "nd_llm_reset" class = "btn btn-icon" title = "恢复默认" > < i class = "fa-solid fa-rotate-left" > < / i > < / button >
< button id = "nd_llm_del" class = "btn btn-danger btn-icon" title = "删除" > < i class = "fa-solid fa-trash" > < / i > < / button >
< / div >
< / div >
< div class = "card" >
< div class = "card-title" > 渠道配置< / div >
< div class = "form-group" >
< label class = "form-label" > LLM 渠道< / label >
< select id = "nd_llm_provider" class = "input" >
< option value = "st" > 酒馆主 API (推荐)< / option >
< option value = "openai" > OpenAI 兼容< / option >
< option value = "google" > Google Gemini< / option >
< option value = "claude" > Claude< / option >
< option value = "deepseek" > DeepSeek< / option >
< option value = "cohere" > Cohere< / option >
< option value = "custom" > 自定义< / option >
< / select >
< / div >
< div id = "nd_llm_url_row" class = "form-group hidden" >
< label class = "form-label" > API URL< / label >
< input id = "nd_llm_url" type = "text" class = "input" placeholder = "https://api.openai.com/v1" >
< / div >
< div id = "nd_llm_key_row" class = "form-group hidden" >
< label class = "form-label" > API Key< / label >
< input id = "nd_llm_key" type = "password" class = "input" placeholder = "sk-xxx..." >
< / div >
< div id = "nd_llm_model_manual_row" class = "form-group hidden" >
< label class = "form-label" > 模型< / label >
< input id = "nd_llm_model_manual" type = "text" class = "input" placeholder = "gemini-1.5-pro" >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div id = "nd_llm_model_select_row" class = "form-group hidden" >
< label class = "form-label" > 模型< / label >
< select id = "nd_llm_model_select" class = "input" > < option value = "" > 请先拉取模型列表< / option > < / select >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div id = "nd_llm_connect_row" class = "btn-group hidden" style = "margin-top:12px;" >
< button id = "nd_llm_fetch" class = "btn" > < i class = "fa-solid fa-plug" > < / i > 连接 / 拉取模型列表< / button >
< / div >
< div class = "form-group" style = "margin-top:16px;" >
< label style = "display:flex;align-items:center;gap:8px;font-size:13px;cursor:pointer;" >
< input type = "checkbox" id = "nd_use_stream" checked > 启用流式生成
< / label >
< / div >
< div id = "nd_llm_status" class = "status-text" > < / div >
< / div >
< div class = "card" >
< div class = "card-title" > LLM 提示词< / div >
< div class = "form-group" > < label class = "form-label" > USER< / label > < textarea id = "nd_llm_system" class = "input" rows = "5" > < / textarea > < / div >
< div class = "form-group" > < label class = "form-label" > AI< / label > < input id = "nd_llm_ack" type = "text" class = "input" > < / div >
< div class = "form-group" > < label class = "form-label" > USER< / label > < textarea id = "nd_llm_user" class = "input" rows = "5" > < / textarea > < p class = "form-hint" > 可用变量: {{lastMessage}} {{characterInfo}}< / p > < / div >
< div class = "form-group" > < label class = "form-label" > AI< / label > < input id = "nd_llm_prefix" type = "text" class = "input" > < / div >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
<!-- 图片管理(画廊 + 缓存) -->
< div id = "view-gallery" class = "view wide" >
< div class = "view-header" >
< h2 class = "view-title" > 图片管理< / h2 >
< p class = "view-desc" > 查看、管理和清理生成的配图< / p >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div class = "card" style = "margin-bottom:20px;" >
< div style = "display:flex;align-items:center;justify-content:space-between;flex-wrap:wrap;gap:16px;" >
< div style = "display:flex;gap:32px;" >
< div style = "text-align:center;" >
< div style = "font-size:28px;font-weight:700;color:var(--accent);" id = "nd_cache_count" > 0< / div >
< div style = "font-size:11px;color:var(--text-secondary);margin-top:2px;" > 图片数量< / div >
< / div >
< div style = "text-align:center;" >
< div style = "font-size:28px;font-weight:700;" id = "nd_cache_size" > 0 MB< / div >
< div style = "font-size:11px;color:var(--text-secondary);margin-top:2px;" > 占用空间< / div >
< / div >
< / div >
< div style = "display:flex;gap:8px;align-items:center;flex-wrap:wrap;" >
< div style = "display:flex;align-items:center;gap:6px;background:var(--bg-input);padding:6px 10px;border-radius:8px;border:1px solid var(--border);" >
< span style = "font-size:12px;color:var(--text-secondary);" > 自动清理< / span >
< input id = "nd_cache_days" type = "number" class = "input" style = "width:50px;padding:4px 6px;min-height:28px;text-align:center;" value = "3" min = "1" max = "30" >
< span style = "font-size:12px;color:var(--text-secondary);" > 天< / span >
< button id = "nd_save_cache_days" class = "btn btn-sm btn-icon" style = "min-height:28px;width:28px;padding:0;" > < i class = "fa-solid fa-check" > < / i > < / button >
< / div >
< button id = "nd_clear_expired" class = "btn btn-sm" > < i class = "fa-solid fa-broom" > < / i > 清理过期< / button >
< button id = "nd_clear_all" class = "btn btn-sm btn-danger" > < i class = "fa-solid fa-trash" > < / i > 清空全部< / button >
< button id = "nd_refresh_stats" class = "btn btn-sm btn-icon" > < i class = "fa-solid fa-arrows-rotate" > < / i > < / button >
< / div >
< / div >
2025-12-19 02:19:10 +08:00
< / div >
2025-12-28 00:49:25 +08:00
< div id = "nd_gallery_container" > < / div >
< div id = "nd_gallery_empty" class = "gallery-empty" style = "display:none;" >
< i class = "fa-solid fa-images" > < / i >
< div > 暂无图片< / div >
< p > 生成的配图会显示在这里,点击角色展开查看< / p >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
2025-12-28 00:49:25 +08:00
< / main >
< / div >
2025-12-19 02:19:10 +08:00
2025-12-28 00:49:25 +08:00
< nav class = "mobile-nav" >
< div class = "mobile-nav-inner" >
< div class = "mobile-nav-item active" data-view = "test" > < i class = "fa-solid fa-flask" > < / i > < span > 测试< / span > < / div >
< div class = "mobile-nav-item" data-view = "api" > < i class = "fa-solid fa-key" > < / i > < span > API< / span > < / div >
< div class = "mobile-nav-item" data-view = "params" > < i class = "fa-solid fa-sliders" > < / i > < span > 参数< / span > < / div >
< div class = "mobile-nav-item" data-view = "prompts" > < i class = "fa-solid fa-tags" > < / i > < span > 标签< / span > < / div >
< div class = "mobile-nav-item" data-view = "llm" > < i class = "fa-solid fa-robot" > < / i > < span > LLM< / span > < / div >
< div class = "mobile-nav-item" data-view = "gallery" > < i class = "fa-solid fa-images" > < / i > < span > 图片< / span > < / div >
< / div >
< / nav >
<!-- 画廊弹窗 -->
< div id = "nd_gallery_modal" class = "gallery-modal" >
< button class = "gallery-modal-close" id = "nd_modal_close" > ✕< / button >
< div class = "gallery-modal-content" >
< img class = "gallery-modal-img" id = "nd_modal_img" src = "" alt = "" >
< div class = "gallery-modal-thumbs" id = "nd_modal_thumbs" > < / div >
< div class = "gallery-modal-actions" >
< button class = "btn btn-primary" id = "nd_modal_use" > < i class = "fa-solid fa-check" > < / i > 使用此图< / button >
< button class = "btn" id = "nd_modal_save" > < i class = "fa-solid fa-floppy-disk" > < / i > 保存到服务器< / button >
< button class = "btn btn-danger" id = "nd_modal_delete" > < i class = "fa-solid fa-trash" > < / i > 删除< / button >
< / div >
< div class = "gallery-modal-info" id = "nd_modal_info" > < / div >
< / div >
2025-12-19 02:19:10 +08:00
< / div >
< / div >
< script >
2025-12-28 00:49:25 +08:00
const DEFAULTS = {
timeout: 60000, requestDelay: { min: 15000, max: 30000 }, cacheDays: 3,
params: { model: 'nai-diffusion-4-5-full', sampler: 'k_euler_ancestral', scheduler: 'karras', steps: 28, scale: 6, width: 1216, height: 832, seed: -1, qualityToggle: true, autoSmea: false, ucPreset: 0, cfg_rescale: 0, variety_boost: false, sm: false, sm_dyn: false, decrisper: false }
};
const providerDefaults = {
st: { url: '', needKey: false, canFetch: false, needManualModel: false },
openai: { url: 'https://api.openai.com', needKey: true, canFetch: true, needManualModel: false },
google: { url: 'https://generativelanguage.googleapis.com', needKey: true, canFetch: false, needManualModel: true },
claude: { url: 'https://api.anthropic.com', needKey: true, canFetch: false, needManualModel: true },
deepseek: { url: 'https://api.deepseek.com', needKey: true, canFetch: true, needManualModel: false },
cohere: { url: 'https://api.cohere.ai', needKey: true, canFetch: false, needManualModel: true },
custom: { url: '', needKey: true, canFetch: true, needManualModel: false }
};
const $ = id => document.getElementById(id);
const $$ = sel => document.querySelectorAll(sel);
2025-12-19 02:19:10 +08:00
let state = {
2025-12-28 00:49:25 +08:00
enabled: false, mode: 'manual', apiKey: '', timeout: DEFAULTS.timeout,
requestDelay: { ...DEFAULTS.requestDelay }, cacheDays: DEFAULTS.cacheDays,
cacheStats: { count: 0, sizeMB: '0' },
selectedParamsPresetId: null, selectedLlmPresetId: null,
paramsPresets: [], llmPresets: [],
llmApi: { provider: 'st', url: '', key: '', model: '', modelCache: [] },
useStream: true, characterTags: []
2025-12-19 02:19:10 +08:00
};
2025-12-28 00:49:25 +08:00
let gallerySummary = {}; // ★ 只存摘要
let loadedCharPreviews = {}; // ★ 懒加载的完整数据
let editingCharId = null;
let activeSaveBtn = null;
let modalData = { slotId: null, images: [], currentIndex: 0, charName: null };
function postToParent(payload) { window.parent.postMessage({ source: 'NovelDraw-Frame', ...payload }, '*'); }
function switchView(viewId) {
$$('.view').forEach(v => v.classList.remove('active'));
$$('.nav-item, .mobile-nav-item').forEach(n => n.classList.remove('active'));
$(`view-${viewId}`)?.classList.add('active');
$$(`[data-view="${viewId}"]`).forEach(n => n.classList.add('active'));
if (viewId === 'gallery') postToParent({ type: 'REFRESH_CACHE_STATS' });
}
function updateModeButtons(mode) { $$('.header-mode button').forEach(btn => btn.classList.toggle('active', btn.dataset.mode === mode)); }
function updateBadge(enabled) { const b = $('nd_badge'); b.className = 'header-badge' + (enabled ? ' on' : ''); b.querySelector('span').textContent = enabled ? '已启用' : '未启用'; }
function updateStatus(el, st, text) { if (!el) return; el.textContent = text || ''; el.className = 'status-text' + (st ? ' ' + st : ''); }
function escapeHtml(str) { return String(str || '').replace(/& /g, '& ').replace(/< /g, '< ').replace(/>/g, '> ').replace(/"/g, '" '); }
function getCharEmoji(name) { const e = ['👤','👩','👨','🧑','👧','👦','👸','🤴','🧙','🧝','🧛','🦸']; return e[String(name||'').split('').reduce((a,c)=>a+c.charCodeAt(0),0) % e.length]; }
function setSavingState(btn) { if (!btn) return; activeSaveBtn = btn; const i = btn.querySelector('i'); if (i) { btn._origIcon = i.className; i.className = 'fa-solid fa-spinner fa-spin'; } btn.classList.add('saving'); }
function handleSaveResult(success) { if (!activeSaveBtn) return; const btn = activeSaveBtn; activeSaveBtn = null; btn.classList.remove('saving'); const i = btn.querySelector('i'); if (success & & i) { i.className = 'fa-solid fa-check'; btn.classList.add('save-success'); setTimeout(() => { btn.classList.remove('save-success'); i.className = btn._origIcon || 'fa-solid fa-floppy-disk'; }, 1500); } else if (i) { i.className = btn._origIcon || 'fa-solid fa-floppy-disk'; } }
// ═══════════════════════════════════════════════════════════════════════════
// 画廊(懒加载版)
// ═══════════════════════════════════════════════════════════════════════════
function renderGalleryView() {
const container = $('nd_gallery_container');
const empty = $('nd_gallery_empty');
const chars = Object.keys(gallerySummary);
if (!chars.length) {
container.innerHTML = '';
container.style.display = 'none';
empty.style.display = 'block';
return;
}
container.style.display = 'block';
empty.style.display = 'none';
// 按最新时间排序
chars.sort((a, b) => (gallerySummary[b].latestTimestamp || 0) - (gallerySummary[a].latestTimestamp || 0));
container.innerHTML = chars.map(charName => {
const charData = gallerySummary[charName];
const slotCount = Object.keys(charData.slots || {}).length;
const sizeMB = ((charData.totalSize || 0) / 1024 / 1024).toFixed(2);
// ★ 默认折叠
return `
< div class = "gallery-char-section collapsed" data-char = "${escapeHtml(charName)}" >
< div class = "gallery-char-header" >
< div class = "gallery-char-avatar" > ${getCharEmoji(charName)}< / div >
< span class = "gallery-char-name" > ${escapeHtml(charName)}< / span >
< span class = "gallery-char-stats" > ${charData.count || 0} 张 · ${slotCount} 组 · ${sizeMB} MB< / span >
< i class = "fa-solid fa-chevron-down gallery-char-toggle" > < / i >
< / div >
< div class = "gallery-slots" data-char = "${escapeHtml(charName)}" >
< div class = "gallery-loading" > < i class = "fa-solid fa-spinner fa-spin" > < / i > 点击展开加载...< / div >
< / div >
< / div > `;
}).join('');
// 绑定展开事件
container.querySelectorAll('.gallery-char-header').forEach(header => {
header.addEventListener('click', () => {
const section = header.closest('.gallery-char-section');
const charName = section.dataset.char;
const wasCollapsed = section.classList.contains('collapsed');
section.classList.toggle('collapsed');
// ★ 展开时懒加载
if (wasCollapsed & & !loadedCharPreviews[charName]) {
const slotsDiv = section.querySelector('.gallery-slots');
slotsDiv.innerHTML = '< div class = "gallery-loading" > < i class = "fa-solid fa-spinner fa-spin" > < / i > 加载中...< / div > ';
postToParent({ type: 'LOAD_CHARACTER_PREVIEWS', charName });
}
});
});
}
2025-12-19 02:19:10 +08:00
2025-12-28 00:49:25 +08:00
// ★ 渲染某个角色的图片
function renderCharacterSlots(charName, slots) {
const slotsContainer = document.querySelector(`.gallery-slots[data-char="${CSS.escape(charName)}"]`);
if (!slotsContainer) return;
const slotEntries = Object.entries(slots || {});
if (!slotEntries.length) {
slotsContainer.innerHTML = '< div class = "gallery-empty-hint" > 暂无图片< / div > ';
return;
}
// 按最新时间排序
slotEntries.sort((a, b) => (b[1][0]?.timestamp || 0) - (a[1][0]?.timestamp || 0));
slotsContainer.innerHTML = slotEntries.map(([slotId, images]) => {
const latest = images[0];
const hasSaved = images.some(img => img.savedUrl);
const imgSrc = latest.savedUrl || `data:image/png;base64,${latest.base64}`;
return `
< div class = "gallery-slot" data-slot = "${slotId}" data-char = "${escapeHtml(charName)}" >
< img class = "gallery-slot-img" src = "${imgSrc}" loading = "lazy" decoding = "async" >
< div class = "gallery-slot-overlay" >
< div class = "gallery-slot-actions" >
< button class = "btn btn-sm" data-action = "view" data-slot = "${slotId}" data-char = "${escapeHtml(charName)}" >
< i class = "fa-solid fa-expand" > < / i > 查看
< / button >
< / div >
< / div >
< div class = "gallery-slot-info" >
< span > ${new Date(latest.timestamp).toLocaleDateString()}< / span >
< div style = "display:flex;gap:4px;" >
${images.length > 1 ? `< span class = "gallery-slot-badge count" > ${images.length} 版< / span > ` : ''}
${hasSaved ? '< span class = "gallery-slot-badge saved" > ✓< / span > ' : ''}
< / div >
< / div >
< / div > `;
}).join('');
// 绑定事件
slotsContainer.querySelectorAll('[data-action="view"]').forEach(btn => {
btn.addEventListener('click', e => { e.stopPropagation(); openGalleryModalFromCache(btn.dataset.char, btn.dataset.slot); });
});
slotsContainer.querySelectorAll('.gallery-slot').forEach(slot => {
slot.addEventListener('click', e => { if (!e.target.closest('[data-action]')) openGalleryModalFromCache(slot.dataset.char, slot.dataset.slot); });
});
2025-12-19 02:19:10 +08:00
}
2025-12-28 00:49:25 +08:00
function openGalleryModalFromCache(charName, slotId) {
const slots = loadedCharPreviews[charName];
if (!slots || !slots[slotId]) { console.warn('[Gallery] 未找到数据:', charName, slotId); return; }
modalData = { slotId, images: slots[slotId], currentIndex: 0, charName };
renderModalContent();
$('nd_gallery_modal').classList.add('visible');
}
function closeGalleryModal() { $('nd_gallery_modal').classList.remove('visible'); modalData = { slotId: null, images: [], currentIndex: 0, charName: null }; }
function renderModalContent() {
const { images, currentIndex } = modalData;
if (!images.length) return;
const current = images[currentIndex];
const imgSrc = current.savedUrl || `data:image/png;base64,${current.base64}`;
$('nd_modal_img').src = imgSrc;
$('nd_modal_thumbs').innerHTML = images.map((img, i) => {
const src = img.savedUrl || `data:image/png;base64,${img.base64}`;
const cls = ['gallery-modal-thumb'];
if (i === currentIndex) cls.push('active');
if (img.savedUrl) cls.push('saved');
return `< img class = "${cls.join(' ')}" src = "${src}" data-index = "${i}" > `;
}).join('');
$('nd_modal_thumbs').querySelectorAll('img').forEach(t => t.addEventListener('click', () => { modalData.currentIndex = parseInt(t.dataset.index); renderModalContent(); }));
const saveBtn = $('nd_modal_save');
if (current.savedUrl) { saveBtn.innerHTML = '< i class = "fa-solid fa-check" > < / i > 已保存'; saveBtn.disabled = true; }
else { saveBtn.innerHTML = '< i class = "fa-solid fa-floppy-disk" > < / i > 保存到服务器'; saveBtn.disabled = false; }
$('nd_modal_info').textContent = `${currentIndex + 1} / ${images.length} · ${new Date(current.timestamp).toLocaleString()}`;
}
// ═══════════════════════════════════════════════════════════════════════════
// 角色列表
// ═══════════════════════════════════════════════════════════════════════════
function renderCharList() {
const list = $('nd_char_list'); const empty = $('nd_char_empty'); const chars = state.characterTags || [];
if (!chars.length) { list.innerHTML = ''; list.style.display = 'none'; empty.style.display = 'block'; return; }
list.style.display = 'flex'; empty.style.display = 'none';
list.innerHTML = chars.map(c => {
const isEditing = editingCharId === c.id;
const aliasText = (c.aliases || []).length > 0 ? `别名: ${c.aliases.join(', ')}` : '';
if (isEditing) return `< div class = "char-card editing" data-id = "${c.id}" > < div class = "char-avatar" > ${getCharEmoji(c.name)}< / div > < div class = "char-info" > < div class = "char-edit-form" > < div class = "form-group" > < label class = "form-label" > 角色名称< / label > < input type = "text" class = "input char-edit-name" value = "${escapeHtml(c.name)}" placeholder = "角色名称" > < / div > < div class = "form-group" > < label class = "form-label" > 别名(逗号分隔)< / label > < input type = "text" class = "input char-edit-aliases" value = "${escapeHtml((c.aliases || []).join(', '))}" placeholder = "小名, 昵称, ..." > < / div > < div class = "form-group" > < label class = "form-label" > 正向标签< / label > < textarea class = "input char-edit-tags" placeholder = "1girl, black hair, blue eyes" > ${escapeHtml(c.tags)}< / textarea > < / div > < div class = "form-group" > < label class = "form-label" > 负向标签(可选)< / label > < textarea class = "input char-edit-neg" placeholder = "避免的特征..." > ${escapeHtml(c.negativeTags || '')}< / textarea > < / div > < div class = "char-pos-row" > < div class = "form-group" > < label class = "form-label" > 位置 X (0-1)< / label > < input type = "number" class = "input char-edit-posx" value = "${c.posX ?? 0.5}" min = "0" max = "1" step = "0.1" > < / div > < div class = "form-group" > < label class = "form-label" > 位置 Y (0-1)< / label > < input type = "number" class = "input char-edit-posy" value = "${c.posY ?? 0.5}" min = "0" max = "1" step = "0.1" > < / div > < / div > < div class = "btn-group" style = "margin-top:12px;" > < button class = "btn btn-primary" data-action = "save" data-id = "${c.id}" > < i class = "fa-solid fa-check" > < / i > 保存< / button > < button class = "btn" data-action = "cancel" data-id = "${c.id}" > 取消< / button > < / div > < / div > < / div > < / div > `;
return `< div class = "char-card" data-id = "${c.id}" > < div class = "char-avatar" > ${getCharEmoji(c.name)}< / div > < div class = "char-info" > < div class = "char-name" > ${escapeHtml(c.name)}< / div > < div class = "char-aliases" > ${escapeHtml(aliasText)}< / div > < div class = "char-tags" > ✅ ${escapeHtml(c.tags || '未设置标签')}< / div > ${c.negativeTags ? `< div class = "char-tags" style = "color:var(--text-muted);font-size:11px;" > ❌ ${escapeHtml(c.negativeTags.slice(0, 50))}...< / div > ` : ''}< / div > < div class = "char-actions" > < button class = "btn" data-action = "edit" data-id = "${c.id}" > < i class = "fa-solid fa-pen" > < / i > < / button > < button class = "btn btn-danger" data-action = "delete" data-id = "${c.id}" > < i class = "fa-solid fa-trash" > < / i > < / button > < / div > < / div > `;
}).join('');
}
function handleCharAction(action, id, card) {
if (action === 'edit') { editingCharId = id; renderCharList(); }
else if (action === 'cancel') { editingCharId = null; renderCharList(); }
else if (action === 'save') {
const char = state.characterTags.find(c => c.id === id); if (!char) return;
const name = card.querySelector('.char-edit-name')?.value?.trim(); if (!name) { alert('请输入角色名称'); return; }
char.name = name; char.aliases = (card.querySelector('.char-edit-aliases')?.value || '').split(/[,, ]/).map(s => s.trim()).filter(Boolean);
char.tags = card.querySelector('.char-edit-tags')?.value?.trim() || ''; char.negativeTags = card.querySelector('.char-edit-neg')?.value?.trim() || '';
char.posX = Math.max(0, Math.min(1, parseFloat(card.querySelector('.char-edit-posx')?.value) || 0.5));
char.posY = Math.max(0, Math.min(1, parseFloat(card.querySelector('.char-edit-posy')?.value) || 0.5));
editingCharId = null; postToParent({ type: 'SAVE_CHARACTER_TAGS', characterTags: state.characterTags }); renderCharList();
} else if (action === 'delete') { if (confirm('确定删除此角色?')) { state.characterTags = state.characterTags.filter(c => c.id !== id); postToParent({ type: 'SAVE_CHARACTER_TAGS', characterTags: state.characterTags }); renderCharList(); } }
}
function updateLlmProviderUI() {
const provider = $('nd_llm_provider').value; const pv = providerDefaults[provider] || providerDefaults.custom; const isSt = provider === 'st';
const hasCache = (state.llmApi?.modelCache?.length || 0) > 0;
$('nd_llm_url_row').classList.toggle('hidden', isSt); $('nd_llm_key_row').classList.toggle('hidden', isSt);
$('nd_llm_model_manual_row').classList.toggle('hidden', isSt || !pv.needManualModel);
$('nd_llm_model_select_row').classList.toggle('hidden', isSt || pv.needManualModel || !hasCache);
$('nd_llm_connect_row').classList.toggle('hidden', isSt || !pv.canFetch);
}
function applyStateToUI() {
updateBadge(state.enabled); updateModeButtons(state.mode);
$('nd_api_key').value = state.apiKey || '';
$('nd_timeout').value = Math.round((state.timeout > 0 ? state.timeout : DEFAULTS.timeout) / 1000);
const dMin = state.requestDelay?.min > 0 ? state.requestDelay.min : DEFAULTS.requestDelay.min;
const dMax = state.requestDelay?.max > 0 ? state.requestDelay.max : DEFAULTS.requestDelay.max;
$('nd_delay').value = `${dMin}-${dMax}`;
$('nd_cache_days').value = state.cacheDays >= 1 ? state.cacheDays : DEFAULTS.cacheDays;
$('nd_cache_count').textContent = state.cacheStats?.count || 0;
$('nd_cache_size').textContent = (state.cacheStats?.sizeMB || 0) + ' MB';
const pSel = $('nd_params_preset'); pSel.innerHTML = state.paramsPresets.map(p => `< option value = "${p.id}" > ${escapeHtml(p.name || p.id)}< / option > `).join(''); pSel.value = state.selectedParamsPresetId || '';
const lSel = $('nd_llm_preset'); lSel.innerHTML = state.llmPresets.map(p => `< option value = "${p.id}" > ${escapeHtml(p.name || p.id)}< / option > `).join(''); lSel.value = state.selectedLlmPresetId || '';
applyParamsPreset(); applyLlmPreset(); applyLlmApi(); renderCharList(); renderGalleryView();
}
function applyParamsPreset() {
const p = state.paramsPresets.find(x => x.id === state.selectedParamsPresetId) || state.paramsPresets[0]; if (!p) return;
$('nd_positive').value = p.positivePrefix || ''; $('nd_negative').value = p.negativePrefix || '';
const pm = p.params || {}; const dp = DEFAULTS.params;
setSelectOrCustom('model', pm.model || dp.model); setSelectOrCustom('sampler', pm.sampler || dp.sampler); setSelectOrCustom('scheduler', pm.scheduler || dp.scheduler);
$('nd_steps').value = pm.steps ?? dp.steps; $('nd_scale').value = pm.scale ?? dp.scale; $('nd_seed').value = pm.seed ?? dp.seed;
$('nd_width').value = pm.width ?? dp.width; $('nd_height').value = pm.height ?? dp.height; updateSizePreset();
$('nd_quality_toggle').value = (pm.qualityToggle ?? dp.qualityToggle) ? 'true' : 'false';
$('nd_auto_smea').value = (pm.autoSmea ?? dp.autoSmea) ? 'true' : 'false';
$('nd_uc_preset').value = pm.ucPreset ?? dp.ucPreset; $('nd_cfg_rescale').value = pm.cfg_rescale ?? dp.cfg_rescale;
$('nd_variety_boost').checked = pm.variety_boost ?? dp.variety_boost; $('nd_sm').checked = pm.sm ?? dp.sm;
$('nd_sm_dyn').checked = pm.sm_dyn ?? dp.sm_dyn; $('nd_decrisper').checked = pm.decrisper ?? dp.decrisper;
updateModelOptions();
}
function applyLlmPreset() { const p = state.llmPresets.find(x => x.id === state.selectedLlmPresetId) || state.llmPresets[0]; if (!p) return; $('nd_llm_system').value = p.systemPrompt || ''; $('nd_llm_ack').value = p.assistantAck || ''; $('nd_llm_user').value = p.userTemplate || ''; $('nd_llm_prefix').value = p.assistantPrefix || ''; }
function applyLlmApi() {
const api = state.llmApi || {}; const provider = api.provider || 'st'; const pv = providerDefaults[provider] || providerDefaults.custom;
$('nd_llm_provider').value = provider; $('nd_llm_url').value = api.url || pv.url || ''; $('nd_llm_key').value = api.key || ''; $('nd_use_stream').checked = state.useStream !== false;
if (pv.needManualModel) $('nd_llm_model_manual').value = api.model || '';
const mc = api.modelCache || []; const sel = $('nd_llm_model_select');
sel.innerHTML = mc.length > 0 ? mc.map(m => `< option value = "${escapeHtml(m)}" $ { m = == api . model ? ' selected ' : ' ' } > ${escapeHtml(m)}< / option > `).join('') : '< option value = "" > 请先拉取模型列表< / option > ';
updateLlmProviderUI();
}
function setSelectOrCustom(kind, value) { const sel = $(`nd_${kind}_sel`); const input = $(`nd_${kind}`); const has = [...sel.options].some(o => o.value === value); sel.value = has ? value : 'custom'; input.classList.toggle('hidden', sel.value !== 'custom'); input.value = value || ''; }
function updateSizePreset() { const w = $('nd_width').value; const h = $('nd_height').value; const val = `${w}x${h}`; const sel = $('nd_size_preset'); const has = [...sel.options].some(o => o.value === val); sel.value = has ? val : 'custom'; $('nd_custom_size').classList.toggle('hidden', sel.value !== 'custom'); }
function updateModelOptions() { const model = $('nd_model_sel').value === 'custom' ? $('nd_model').value : $('nd_model_sel').value; const isV3 = model.includes('nai-diffusion-3') || model.includes('furry-3'); $('nd_v3_opts').classList.toggle('hidden', !isV3); }
function collectParamsPreset() { const p = state.paramsPresets.find(x => x.id === state.selectedParamsPresetId); if (!p) return; p.positivePrefix = $('nd_positive').value; p.negativePrefix = $('nd_negative').value; p.params = p.params || {}; p.params.model = $('nd_model_sel').value === 'custom' ? $('nd_model').value : $('nd_model_sel').value; p.params.sampler = $('nd_sampler_sel').value === 'custom' ? $('nd_sampler').value : $('nd_sampler_sel').value; p.params.scheduler = $('nd_scheduler_sel').value === 'custom' ? $('nd_scheduler').value : $('nd_scheduler_sel').value; p.params.steps = Number($('nd_steps').value) || 23; p.params.scale = Number($('nd_scale').value) || 5; p.params.seed = Number($('nd_seed').value); p.params.width = Number($('nd_width').value) || 832; p.params.height = Number($('nd_height').value) || 1216; p.params.qualityToggle = $('nd_quality_toggle').value === 'true'; p.params.autoSmea = $('nd_auto_smea').value === 'true'; p.params.ucPreset = Number($('nd_uc_preset').value) || 0; p.params.cfg_rescale = Number($('nd_cfg_rescale').value) || 0; p.params.variety_boost = $('nd_variety_boost').checked; p.params.sm = $('nd_sm').checked; p.params.sm_dyn = $('nd_sm_dyn').checked; p.params.decrisper = $('nd_decrisper').checked; }
function collectLlmPreset() { const p = state.llmPresets.find(x => x.id === state.selectedLlmPresetId); if (!p) return; p.systemPrompt = $('nd_llm_system').value; p.assistantAck = $('nd_llm_ack').value; p.userTemplate = $('nd_llm_user').value; p.assistantPrefix = $('nd_llm_prefix').value; }
function getCurrentLlmModel() { const provider = $('nd_llm_provider').value; const pv = providerDefaults[provider] || providerDefaults.custom; if (pv.needManualModel) return $('nd_llm_model_manual').value.trim(); else if (pv.canFetch) return $('nd_llm_model_select').value || ''; return ''; }
function parseDelay(str) { const parts = String(str).split('-').map(s => parseInt(s.trim())); if (parts.length === 2 & & !isNaN(parts[0]) & & !isNaN(parts[1])) return { min: Math.min(parts[0], parts[1]), max: Math.max(parts[0], parts[1]) }; const single = parseInt(str); if (!isNaN(single) & & single > 0) return { min: single, max: single }; return { ...DEFAULTS.requestDelay }; }
// ═══════════════════════════════════════════════════════════════════════════
// 消息处理
// ═══════════════════════════════════════════════════════════════════════════
2025-12-19 02:19:10 +08:00
window.addEventListener('message', event => {
const data = event.data;
if (!data || data.source !== 'LittleWhiteBox-NovelDraw') return;
2025-12-28 00:49:25 +08:00
2025-12-19 02:19:10 +08:00
switch (data.type) {
case 'INIT_DATA':
2025-12-28 00:49:25 +08:00
state = { ...state, ...data.settings, cacheStats: data.cacheStats || state.cacheStats };
gallerySummary = data.gallerySummary || {};
loadedCharPreviews = {}; // ★ 重置已加载的数据
2025-12-19 02:19:10 +08:00
applyStateToUI();
break;
2025-12-28 00:49:25 +08:00
case 'CHARACTER_PREVIEWS_LOADED':
// ★ 懒加载数据到达
loadedCharPreviews[data.charName] = data.slots;
renderCharacterSlots(data.charName, data.slots);
break;
case 'TEST_RESULT':
2025-12-19 02:19:10 +08:00
$('nd_preview_img').src = data.url;
$('nd_preview').classList.add('visible');
break;
2025-12-28 00:49:25 +08:00
2025-12-19 02:19:10 +08:00
case 'STATUS':
2025-12-28 00:49:25 +08:00
updateStatus($('nd_status'), data.state, data.text);
updateStatus($('nd_api_status'), data.state, data.text);
updateStatus($('nd_llm_status'), data.state, data.text);
const fetchBtn = $('nd_llm_fetch');
if (fetchBtn?.disabled) { fetchBtn.disabled = false; fetchBtn.innerHTML = '< i class = "fa-solid fa-plug" > < / i > 连接 / 拉取模型列表'; }
if (data.state === 'success' || data.state === 'error') handleSaveResult(data.state === 'success');
break;
case 'GALLERY_IMAGE_DELETED':
if (modalData.charName & & loadedCharPreviews[modalData.charName]) {
const slots = loadedCharPreviews[modalData.charName];
for (const slotId in slots) { slots[slotId] = slots[slotId].filter(img => img.imgId !== data.imgId); if (slots[slotId].length === 0) delete slots[slotId]; }
if (Object.keys(slots).length === 0) { delete loadedCharPreviews[modalData.charName]; delete gallerySummary[modalData.charName]; renderGalleryView(); }
else renderCharacterSlots(modalData.charName, slots);
}
if (modalData.slotId) {
modalData.images = modalData.images.filter(img => img.imgId !== data.imgId);
if (modalData.images.length === 0) closeGalleryModal();
else { if (modalData.currentIndex >= modalData.images.length) modalData.currentIndex = modalData.images.length - 1; renderModalContent(); }
}
postToParent({ type: 'REFRESH_CACHE_STATS' });
break;
case 'GALLERY_IMAGE_SAVED':
if (modalData.charName & & loadedCharPreviews[modalData.charName]) {
for (const slotId in loadedCharPreviews[modalData.charName]) {
const img = loadedCharPreviews[modalData.charName][slotId].find(i => i.imgId === data.imgId);
if (img) { img.savedUrl = data.savedUrl; renderCharacterSlots(modalData.charName, loadedCharPreviews[modalData.charName]); break; }
}
}
if (modalData.slotId) { const img = modalData.images.find(i => i.imgId === data.imgId); if (img) { img.savedUrl = data.savedUrl; renderModalContent(); } }
2025-12-19 02:19:10 +08:00
break;
}
});
2025-12-28 00:49:25 +08:00
// ═══════════════════════════════════════════════════════════════════════════
// 初始化
// ═══════════════════════════════════════════════════════════════════════════
2025-12-19 02:19:10 +08:00
document.addEventListener('DOMContentLoaded', () => {
2025-12-28 00:49:25 +08:00
$$('.nav-item, .mobile-nav-item').forEach(item => item.addEventListener('click', () => switchView(item.dataset.view)));
$$('.header-mode button').forEach(btn => btn.addEventListener('click', () => { state.mode = btn.dataset.mode; updateModeButtons(state.mode); postToParent({ type: 'SAVE_MODE', mode: state.mode }); }));
$('nd_close').addEventListener('click', () => postToParent({ type: 'CLOSE' }));
$('nd_toggle_key').addEventListener('click', () => { const i = $('nd_api_key'); const ic = $('nd_toggle_key').querySelector('i'); if (i.type === 'password') { i.type = 'text'; ic.className = 'fa-solid fa-eye-slash'; } else { i.type = 'password'; ic.className = 'fa-solid fa-eye'; } });
$('nd_save_api').addEventListener('click', () => { setSavingState($('nd_save_api')); postToParent({ type: 'SAVE_API_KEY', apiKey: $('nd_api_key').value.trim() }); postToParent({ type: 'SAVE_TIMEOUT', timeout: Number($('nd_timeout').value) * 1000 || 180000, requestDelay: parseDelay($('nd_delay').value) }); });
$('nd_test_api').addEventListener('click', () => postToParent({ type: 'TEST_API', apiKey: $('nd_api_key').value.trim() }));
$('nd_test_single').addEventListener('click', () => postToParent({ type: 'TEST_SINGLE', tags: $('nd_test_tags').value }));
['model', 'sampler', 'scheduler'].forEach(k => $(`nd_${k}_sel`).addEventListener('change', function() { $(`nd_${k}`).classList.toggle('hidden', this.value !== 'custom'); if (k === 'model') updateModelOptions(); }));
$('nd_size_preset').addEventListener('change', function() { if (this.value === 'custom') $('nd_custom_size').classList.remove('hidden'); else { const [w, h] = this.value.split('x'); $('nd_width').value = w; $('nd_height').value = h; $('nd_custom_size').classList.add('hidden'); } });
$('nd_params_preset').addEventListener('change', () => { state.selectedParamsPresetId = $('nd_params_preset').value; applyParamsPreset(); });
$('nd_params_save').addEventListener('click', () => { setSavingState($('nd_params_save')); collectParamsPreset(); postToParent({ type: 'SAVE_PARAMS_PRESET', selectedParamsPresetId: state.selectedParamsPresetId, paramsPresets: state.paramsPresets }); });
$('nd_params_add').addEventListener('click', () => postToParent({ type: 'ADD_PARAMS_PRESET' }));
$('nd_params_del').addEventListener('click', () => { if (confirm('确定删除当前参数预设?')) postToParent({ type: 'DEL_PARAMS_PRESET' }); });
$('nd_params_rename').addEventListener('click', () => { const p = state.paramsPresets.find(x => x.id === state.selectedParamsPresetId); if (!p) return; const name = prompt('输入新名称:', p.name || ''); if (name & & name.trim()) { p.name = name.trim(); postToParent({ type: 'SAVE_PARAMS_PRESET', selectedParamsPresetId: state.selectedParamsPresetId, paramsPresets: state.paramsPresets }); } });
$('nd_prompts_save').addEventListener('click', () => { setSavingState($('nd_prompts_save')); collectParamsPreset(); postToParent({ type: 'SAVE_PARAMS_PRESET', selectedParamsPresetId: state.selectedParamsPresetId, paramsPresets: state.paramsPresets }); });
$('nd_llm_provider').addEventListener('change', function() { const pv = providerDefaults[this.value] || providerDefaults.custom; if (pv.url) $('nd_llm_url').value = pv.url; if (!pv.canFetch) { state.llmApi = state.llmApi || {}; state.llmApi.modelCache = []; } updateLlmProviderUI(); });
$('nd_llm_fetch').addEventListener('click', () => { const btn = $('nd_llm_fetch'); btn.disabled = true; btn.innerHTML = '< i class = "fa-solid fa-spinner fa-spin" > < / i > 连接中...'; postToParent({ type: 'FETCH_LLM_MODELS', llmApi: { provider: $('nd_llm_provider').value, url: $('nd_llm_url').value.trim(), key: $('nd_llm_key').value.trim() } }); });
$('nd_llm_preset').addEventListener('change', () => { state.selectedLlmPresetId = $('nd_llm_preset').value; applyLlmPreset(); });
$('nd_llm_save').addEventListener('click', () => { setSavingState($('nd_llm_save')); collectLlmPreset(); postToParent({ type: 'SAVE_LLM_PRESET', selectedLlmPresetId: state.selectedLlmPresetId, llmPresets: state.llmPresets, llmApi: { provider: $('nd_llm_provider').value, url: $('nd_llm_url').value.trim(), key: $('nd_llm_key').value.trim(), model: getCurrentLlmModel(), modelCache: state.llmApi?.modelCache || [] }, useStream: $('nd_use_stream').checked }); });
$('nd_llm_add').addEventListener('click', () => postToParent({ type: 'ADD_LLM_PRESET' }));
$('nd_llm_del').addEventListener('click', () => { if (confirm('确定删除当前 LLM 预设?')) postToParent({ type: 'DEL_LLM_PRESET' }); });
$('nd_llm_rename').addEventListener('click', () => { const p = state.llmPresets.find(x => x.id === state.selectedLlmPresetId); if (!p) return; const name = prompt('输入新名称:', p.name || ''); if (name & & name.trim()) { p.name = name.trim(); postToParent({ type: 'SAVE_LLM_PRESET', selectedLlmPresetId: state.selectedLlmPresetId, llmPresets: state.llmPresets }); } });
$('nd_llm_reset').addEventListener('click', () => { if (confirm('确定将当前 LLM 预设恢复为插件内置默认值?')) postToParent({ type: 'RESET_CURRENT_LLM_PRESET' }); });
$('nd_char_add').addEventListener('click', () => { const nc = { id: `char-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`, name: '', aliases: [], tags: '', negativeTags: '', posX: 0.5, posY: 0.5 }; state.characterTags.push(nc); editingCharId = nc.id; renderCharList(); setTimeout(() => document.querySelector('.char-edit-name')?.focus(), 50); });
$('nd_char_list').addEventListener('click', e => { const btn = e.target.closest('[data-action]'); if (!btn) return; const card = btn.closest('.char-card'); const id = btn.dataset.id || card?.dataset.id; if (id) handleCharAction(btn.dataset.action, id, card); });
$('nd_char_export').addEventListener('click', () => { if (!state.characterTags?.length) { alert('没有可导出的角色'); return; } const d = { type: 'novel-draw-characters', version: 1, characters: state.characterTags }; const blob = new Blob([JSON.stringify(d, null, 2)], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = 'character-tags.json'; document.body.appendChild(a); a.click(); document.body.removeChild(a); URL.revokeObjectURL(url); });
$('nd_char_import').addEventListener('change', async e => { const file = e.target.files[0]; if (!file) return; try { const text = await file.text(); const d = JSON.parse(text); if (d.type !== 'novel-draw-characters' || !Array.isArray(d.characters)) throw new Error('无效文件'); for (const char of d.characters) { if (!char.name) continue; const ex = state.characterTags.find(c => c.name === char.name); if (ex) Object.assign(ex, char, { id: ex.id }); else state.characterTags.push({ ...char, id: `char-${Date.now()}-${Math.random().toString(36).slice(2, 6)}` }); } postToParent({ type: 'SAVE_CHARACTER_TAGS', characterTags: state.characterTags }); renderCharList(); alert(`导入 ${d.characters.length} 个角色`); } catch (err) { alert('导入失败: ' + err.message); } e.target.value = ''; });
$('nd_save_cache_days').addEventListener('click', () => { setSavingState($('nd_save_cache_days')); postToParent({ type: 'SAVE_CACHE_DAYS', cacheDays: Number($('nd_cache_days').value) || 3 }); });
$('nd_clear_expired').addEventListener('click', () => postToParent({ type: 'CLEAR_EXPIRED_CACHE' }));
$('nd_clear_all').addEventListener('click', () => { if (confirm('确定清空全部图片记录?已保存到服务器的文件不会被删除。')) postToParent({ type: 'CLEAR_ALL_CACHE' }); });
$('nd_refresh_stats').addEventListener('click', () => postToParent({ type: 'REFRESH_CACHE_STATS' }));
// 画廊弹窗
$('nd_modal_close').addEventListener('click', closeGalleryModal);
$('nd_gallery_modal').addEventListener('click', e => { if (e.target.id === 'nd_gallery_modal') closeGalleryModal(); });
$('nd_modal_use').addEventListener('click', () => { if (!modalData.slotId || !modalData.images.length) return; const c = modalData.images[modalData.currentIndex]; postToParent({ type: 'USE_GALLERY_IMAGE', slotId: modalData.slotId, imgId: c.imgId }); closeGalleryModal(); });
$('nd_modal_save').addEventListener('click', () => { if (!modalData.images.length) return; const c = modalData.images[modalData.currentIndex]; if (c.savedUrl) return; postToParent({ type: 'SAVE_GALLERY_IMAGE', imgId: c.imgId }); });
$('nd_modal_delete').addEventListener('click', () => { if (!modalData.images.length) return; const c = modalData.images[modalData.currentIndex]; const msg = c.savedUrl ? '确定删除这条记录吗?服务器上的图片文件不会被删除。' : '确定删除这张图片吗?'; if (confirm(msg)) postToParent({ type: 'DELETE_GALLERY_IMAGE', imgId: c.imgId }); });
2025-12-19 02:19:10 +08:00
postToParent({ type: 'FRAME_READY' });
});
< / script >
< / body >
< / html >