预设角色外貌,LLM 只需补充动作和互动标签
+快速测试
@@ -407,7 +441,9 @@ select.input { cursor: pointer; }API 配置
@@ -440,12 +476,15 @@ select.input { cursor: pointer; }绘图参数
-模型与生成参数设置
+模型、生成参数与标签设置
diff --git a/modules/fourth-wall/fourth-wall.js b/modules/fourth-wall/fourth-wall.js index 84eda6f..d0aad8b 100644 --- a/modules/fourth-wall/fourth-wall.js +++ b/modules/fourth-wall/fourth-wall.js @@ -506,13 +506,13 @@ async function handleGenerateImage(data) { return; } - const positive = [paramsPreset.positivePrefix, tags].filter(Boolean).join(', '); + const scene = [paramsPreset.positivePrefix, tags].filter(Boolean).join(', '); const base64 = await novelDraw.generateNovelImage({ - prompt: positive, + scene, + characterPrompts: [], negativePrompt: paramsPreset.negativePrefix || '', - params: paramsPreset.params || {}, - characters: [] + params: paramsPreset.params || {} }); await cacheImage(tags, base64); diff --git a/modules/novel-draw/TAG编写指南.md b/modules/novel-draw/TAG编写指南.md index 6eb4d01..ae8941e 100644 --- a/modules/novel-draw/TAG编写指南.md +++ b/modules/novel-draw/TAG编写指南.md @@ -300,13 +300,87 @@ masterpiece, best quality, very aesthetic, absurdres, ultra detailed --- -## 八、注意事项 +### 七、多角色互动前缀 + +多人场景里,动作有方向。谁主动、谁被动、还是互相的?用前缀区分: + +**三种前缀:** +- `source#` — 发起动作的人 +- `target#` — 承受动作的人 +- `mutual#` — 双方同时参与 + +**举例:** + +A 抱着 B: +``` +A: source#hug +B: target#hug +``` + +两人牵手(没有谁主动谁被动): +``` +A: mutual#holding hands +B: mutual#holding hands +``` + +A 盯着 B 看: +``` +A: source#staring +B: target#staring +``` + +**常见动作词:** + +| 类型 | 动作 | +|------|------| +| 肢体 | hug, carry, push, pull, hold | +| 亲密 | kiss, embrace, lap pillow, piggyback | +| 视线 | eye contact, staring, looking away | + +如果需要加权重,正常用 `::` 语法包裹整个标签即可。 + +--- + +### 八、负值权重 + +权重可以是负数。两个用途: + +**1. 移除特定元素** + +角色设定里自带眼镜,这张图不想要: +``` +-1::glasses:: +``` + +帽子挡脸了: +``` +-1::hat:: +``` + +**2. 反转概念** + +画面太平太素,想要反效果: +``` +-1::flat color:: // 平涂的反面 → 层次丰富 +-1::simple background:: // 简单背景的反面 → 场景丰富 +-1::monochrome:: // 单色的反面 → 色彩丰富 +``` + +**和 Undesired Content 的区别:** + +| 方式 | 特点 | 适合场景 | +|------|------|----------| +| 负值权重 | 精准、针对单个元素 | 移除某个具体东西、反转某个概念 | +| Undesired Content | 批量、一次列很多 | 整体风格控制、排除一类东西 | + +--- +## 九、注意事项 1. **具体优于抽象**:`blue pleated skirt` 优于 `skirt` 2. **避免冲突 Tag**:如同时使用 `short hair` 和 `long hair` 3. **肤色需明确指定**:不指定时默认倾向白皙 4. **服装描述要分层**:每个部件单独描述效果更好 -5. **NSFW 场景必须明确**:如果场景涉及性内容,必须使用精确的标签。包括: +5. **NSFW 场景必须明确**:如果场景涉及性内容,必须使用精确的标签,首先给上NSFW标签,然后: - 身体部位:`penis`, `anus`, `vagina`, `pubic_hair` 等 - 性行为方式:`oral`, `fellatio`, `gokkun`, `anal sex`, `vaginal sex` 等 - 体位:`missionary`, `doggystyle`, `mating_press`, `deepthroat` 等 diff --git a/modules/novel-draw/floating-panel.js b/modules/novel-draw/floating-panel.js index 2c89aad..f9c37dd 100644 --- a/modules/novel-draw/floating-panel.js +++ b/modules/novel-draw/floating-panel.js @@ -1,5 +1,4 @@ // floating-panel.js -// Novel Draw 悬浮面板 - 冷却倒计时优化版(修复版) import { openNovelDrawSettings, @@ -29,6 +28,16 @@ const FloatState = { ERROR: 'error', }; +// 尺寸预设 +const SIZE_OPTIONS = [ + { value: 'default', label: '跟随预设', width: null, height: null }, + { value: '832x1216', label: '832 × 1216 竖图', width: 832, height: 1216 }, + { value: '1216x832', label: '1216 × 832 横图', width: 1216, height: 832 }, + { value: '1024x1024', label: '1024 × 1024 方图', width: 1024, height: 1024 }, + { value: '768x1280', label: '768 x 1280 大竖', width: 768, height: 1280 }, + { value: '1280x768', label: '1280 x 768 大横', width: 1280, height: 768 }, +]; + // ═══════════════════════════════════════════════════════════════════════════ // 状态 // ═══════════════════════════════════════════════════════════════════════════ @@ -38,12 +47,9 @@ let dragState = null; let currentState = FloatState.IDLE; let currentResult = { success: 0, total: 0, error: null, startTime: 0 }; let autoResetTimer = null; - -// 冷却倒计时相关 -let cooldownTimer = null; +let cooldownRafId = null; let cooldownEndTime = 0; -// DOM 缓存 let $cache = {}; function cacheDOM() { @@ -57,213 +63,510 @@ function cacheDOM() { detailError: floatEl.querySelector('#nd-detail-error'), detailTime: floatEl.querySelector('#nd-detail-time'), presetSelect: floatEl.querySelector('#nd-preset-select'), - autoDot: floatEl.querySelector('#nd-menu-auto-dot'), + sizeSelect: floatEl.querySelector('#nd-size-select'), + autoToggle: floatEl.querySelector('#nd-auto-toggle'), }; } // ═══════════════════════════════════════════════════════════════════════════ -// 样式 +// 样式 - 精致简约 // ═══════════════════════════════════════════════════════════════════════════ const STYLES = ` +/* ═══════════════════════════════════════════════════════════════════════════ + 设计令牌 (Design Tokens) + ═══════════════════════════════════════════════════════════════════════════ */ :root { - --nd-w: 74px; --nd-h: 34px; - --nd-bg: rgba(28,28,32,0.96); - --nd-border: rgba(255,255,255,0.12); + /* 胶囊尺寸 */ + --nd-w: 74px; + --nd-h: 34px; + + /* 颜色系统 */ + --nd-bg-solid: rgba(24, 24, 28, 0.98); + --nd-bg-card: rgba(0, 0, 0, 0.35); + --nd-bg-hover: rgba(255, 255, 255, 0.06); + --nd-bg-active: rgba(255, 255, 255, 0.1); + + --nd-border-subtle: rgba(255, 255, 255, 0.08); + --nd-border-default: rgba(255, 255, 255, 0.12); + --nd-border-hover: rgba(255, 255, 255, 0.2); + + --nd-text-primary: rgba(255, 255, 255, 0.92); + --nd-text-secondary: rgba(255, 255, 255, 0.65); + --nd-text-muted: rgba(255, 255, 255, 0.5); + + /* 语义色 */ --nd-accent: #d4a574; --nd-success: #3ecf8e; --nd-warning: #f0b429; --nd-error: #f87171; - --nd-cooldown: #60a5fa; + --nd-info: #60a5fa; + + /* 阴影 */ + --nd-shadow-sm: 0 2px 8px rgba(0, 0, 0, 0.25); + --nd-shadow-md: 0 4px 16px rgba(0, 0, 0, 0.35); + --nd-shadow-lg: 0 12px 40px rgba(0, 0, 0, 0.5); + + /* 圆角 */ + --nd-radius-sm: 6px; + --nd-radius-md: 10px; + --nd-radius-lg: 14px; + --nd-radius-full: 9999px; + + /* 过渡 */ + --nd-transition-fast: 0.15s ease; + --nd-transition-normal: 0.25s ease; } -.nd-float { position: fixed; z-index: 10000; user-select: none; } + +/* ═══════════════════════════════════════════════════════════════════════════ + 悬浮容器 + ═══════════════════════════════════════════════════════════════════════════ */ +.nd-float { + position: fixed; + z-index: 10000; + user-select: none; + will-change: transform; + contain: layout style; +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 胶囊主体 + ═══════════════════════════════════════════════════════════════════════════ */ .nd-capsule { - width: var(--nd-w); height: var(--nd-h); - background: var(--nd-bg); - border: 1px solid var(--nd-border); + width: var(--nd-w); + height: var(--nd-h); + background: var(--nd-bg-solid); + border: 1px solid var(--nd-border-default); border-radius: 17px; - box-shadow: 0 4px 16px rgba(0,0,0,0.35); - position: relative; overflow: hidden; - transition: all 0.25s ease; - touch-action: none; cursor: grab; + box-shadow: var(--nd-shadow-md); + position: relative; + overflow: hidden; + transition: border-color var(--nd-transition-normal), + box-shadow var(--nd-transition-normal), + background var(--nd-transition-normal); + touch-action: none; + cursor: grab; } + .nd-capsule:active { cursor: grabbing; } + .nd-float:hover .nd-capsule { - border-color: rgba(255,255,255,0.25); - box-shadow: 0 6px 20px rgba(0,0,0,0.45); - transform: translateY(-1px); + border-color: var(--nd-border-hover); + box-shadow: 0 6px 24px rgba(0, 0, 0, 0.45); } /* 状态边框 */ -.nd-float.working .nd-capsule { border-color: rgba(240,180,41,0.5); } -.nd-float.cooldown .nd-capsule { border-color: rgba(96,165,250,0.6); background: rgba(96,165,250,0.08); } -.nd-float.success .nd-capsule { border-color: rgba(62,207,142,0.6); background: rgba(62,207,142,0.08); } -.nd-float.partial .nd-capsule { border-color: rgba(240,180,41,0.6); background: rgba(240,180,41,0.08); } -.nd-float.error .nd-capsule { border-color: rgba(248,113,113,0.6); background: rgba(248,113,113,0.08); } +.nd-float.working .nd-capsule { + border-color: rgba(240, 180, 41, 0.5); +} +.nd-float.cooldown .nd-capsule { + border-color: rgba(96, 165, 250, 0.6); + background: rgba(96, 165, 250, 0.06); +} +.nd-float.success .nd-capsule { + border-color: rgba(62, 207, 142, 0.6); + background: rgba(62, 207, 142, 0.06); +} +.nd-float.partial .nd-capsule { + border-color: rgba(240, 180, 41, 0.6); + background: rgba(240, 180, 41, 0.06); +} +.nd-float.error .nd-capsule { + border-color: rgba(248, 113, 113, 0.6); + background: rgba(248, 113, 113, 0.06); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 胶囊内层 + ═══════════════════════════════════════════════════════════════════════════ */ +.nd-inner { + display: grid; + width: 100%; + height: 100%; + grid-template-areas: "s"; + pointer-events: none; +} + +.nd-layer { + grid-area: s; + display: flex; + align-items: center; + width: 100%; + height: 100%; + transition: opacity 0.2s, transform 0.2s; + pointer-events: auto; +} -/* 层叠 */ -.nd-inner { display: grid; width: 100%; height: 100%; grid-template-areas: "s"; pointer-events: none; } -.nd-layer { grid-area: s; display: flex; align-items: center; width: 100%; height: 100%; transition: opacity 0.2s, transform 0.2s; pointer-events: auto; } .nd-layer-idle { opacity: 1; transform: translateY(0); } -.nd-float.working .nd-layer-idle, .nd-float.cooldown .nd-layer-idle, -.nd-float.success .nd-layer-idle, .nd-float.partial .nd-layer-idle, + +.nd-float.working .nd-layer-idle, +.nd-float.cooldown .nd-layer-idle, +.nd-float.success .nd-layer-idle, +.nd-float.partial .nd-layer-idle, .nd-float.error .nd-layer-idle { - opacity: 0; transform: translateY(-100%); pointer-events: none; + opacity: 0; + transform: translateY(-100%); + pointer-events: none; } -/* 按钮 */ +/* 绘制按钮 */ .nd-btn-draw { - flex: 1; height: 100%; border: none; background: transparent; - cursor: pointer; display: flex; align-items: center; justify-content: center; - position: relative; color: rgba(255,255,255,0.9); transition: background 0.15s; + flex: 1; + height: 100%; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + position: relative; + color: var(--nd-text-primary); + transition: background var(--nd-transition-fast); font-size: 16px; - font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif; } -.nd-btn-draw:hover { background: rgba(255,255,255,0.08); } -.nd-btn-draw:active { background: rgba(255,255,255,0.12); } +.nd-btn-draw:hover { background: var(--nd-bg-hover); } +.nd-btn-draw:active { background: var(--nd-bg-active); } +/* 自动模式指示点 */ .nd-auto-dot { - position: absolute; top: 7px; right: 6px; width: 6px; height: 6px; - background: var(--nd-success); border-radius: 50%; - box-shadow: 0 0 4px rgba(62,207,142,0.6); - opacity: 0; transform: scale(0); transition: all 0.2s; + position: absolute; + top: 7px; + right: 6px; + width: 6px; + height: 6px; + background: var(--nd-success); + border-radius: 50%; + box-shadow: 0 0 6px rgba(62, 207, 142, 0.6); + opacity: 0; + transform: scale(0); + transition: all 0.2s; +} +.nd-float.auto-on .nd-auto-dot { + opacity: 1; + transform: scale(1); } -.nd-float.auto-on .nd-auto-dot { opacity: 1; transform: scale(1); } -.nd-sep { width: 1px; height: 14px; background: rgba(255,255,255,0.1); } +/* 分隔线 */ +.nd-sep { + width: 1px; + height: 14px; + background: var(--nd-border-subtle); +} +/* 菜单按钮 */ .nd-btn-menu { - width: 28px; height: 100%; border: none; background: transparent; - cursor: pointer; display: flex; align-items: center; justify-content: center; - color: rgba(255,255,255,0.4); font-size: 8px; transition: all 0.15s; + width: 28px; + height: 100%; + border: none; + background: transparent; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + color: var(--nd-text-muted); + font-size: 8px; + transition: all var(--nd-transition-fast); +} +.nd-btn-menu:hover { + background: var(--nd-bg-hover); + color: var(--nd-text-secondary); } -.nd-btn-menu:hover { background: rgba(255,255,255,0.08); color: rgba(255,255,255,0.8); } .nd-arrow { transition: transform 0.2s; } .nd-float.expanded .nd-arrow { transform: rotate(180deg); } -/* 工作层 */ +/* ═══════════════════════════════════════════════════════════════════════════ + 工作状态层 + ═══════════════════════════════════════════════════════════════════════════ */ .nd-layer-active { - opacity: 0; transform: translateY(100%); - justify-content: center; gap: 6px; + opacity: 0; + transform: translateY(100%); + justify-content: center; + gap: 6px; font-size: 14px; - font-weight: 600; color: #fff; - cursor: pointer; pointer-events: none; - font-family: "Apple Color Emoji", "Segoe UI Emoji", "Noto Color Emoji", sans-serif; + font-weight: 600; + color: #fff; + cursor: pointer; + pointer-events: none; } -.nd-float.working .nd-layer-active, .nd-float.cooldown .nd-layer-active, -.nd-float.success .nd-layer-active, .nd-float.partial .nd-layer-active, + +.nd-float.working .nd-layer-active, +.nd-float.cooldown .nd-layer-active, +.nd-float.success .nd-layer-active, +.nd-float.partial .nd-layer-active, .nd-float.error .nd-layer-active { - opacity: 1; transform: translateY(0); pointer-events: auto; + opacity: 1; + transform: translateY(0); + pointer-events: auto; } -.nd-float.cooldown .nd-layer-active { color: var(--nd-cooldown); } + +.nd-float.cooldown .nd-layer-active { color: var(--nd-info); } .nd-float.success .nd-layer-active { color: var(--nd-success); } .nd-float.partial .nd-layer-active { color: var(--nd-warning); } .nd-float.error .nd-layer-active { color: var(--nd-error); } -/* 🔧 修复1:旋转动画 */ .nd-spin { display: inline-block; - animation: nd-spin 1.5s linear infinite; + animation: nd-spin 1.5s linear infinite; + will-change: transform; } @keyframes nd-spin { to { transform: rotate(360deg); } } -/* 倒计时数字 - 等宽显示 */ .nd-countdown { font-variant-numeric: tabular-nums; min-width: 36px; text-align: center; } -/* 详情气泡 */ +/* ═══════════════════════════════════════════════════════════════════════════ + 详情气泡 + ═══════════════════════════════════════════════════════════════════════════ */ .nd-detail { - position: absolute; bottom: calc(100% + 8px); left: 50%; + position: absolute; + bottom: calc(100% + 10px); + left: 50%; transform: translateX(-50%) translateY(4px); - background: rgba(20,20,24,0.98); - border: 1px solid rgba(255,255,255,0.12); - border-radius: 8px; padding: 10px 14px; - font-size: 11px; color: rgba(255,255,255,0.8); + background: var(--nd-bg-solid); + border: 1px solid var(--nd-border-default); + border-radius: var(--nd-radius-md); + padding: 12px 16px; + font-size: 12px; + color: var(--nd-text-secondary); white-space: nowrap; - box-shadow: 0 8px 24px rgba(0,0,0,0.5); - opacity: 0; visibility: hidden; - transition: all 0.15s ease; z-index: 10; + box-shadow: var(--nd-shadow-lg); + opacity: 0; + visibility: hidden; + transition: opacity var(--nd-transition-fast), transform var(--nd-transition-fast); + z-index: 10; } + .nd-detail::after { - content: ''; position: absolute; bottom: -5px; left: 50%; + content: ''; + position: absolute; + bottom: -6px; + left: 50%; transform: translateX(-50%); - border: 5px solid transparent; - border-top-color: rgba(20,20,24,0.98); + border: 6px solid transparent; + border-top-color: var(--nd-bg-solid); } + .nd-float.show-detail .nd-detail { - opacity: 1; visibility: visible; transform: translateX(-50%) translateY(0); + opacity: 1; + visibility: visible; + transform: translateX(-50%) translateY(0); +} + +.nd-detail-row { + display: flex; + align-items: center; + gap: 10px; + padding: 3px 0; } -.nd-detail-row { display: flex; align-items: center; gap: 8px; padding: 2px 0; } .nd-detail-row + .nd-detail-row { - margin-top: 4px; padding-top: 6px; - border-top: 1px solid rgba(255,255,255,0.08); + margin-top: 6px; + padding-top: 8px; + border-top: 1px solid var(--nd-border-subtle); } -.nd-detail-icon { opacity: 0.6; } -.nd-detail-label { color: rgba(255,255,255,0.5); } -.nd-detail-value { margin-left: auto; font-weight: 600; } + +.nd-detail-icon { opacity: 0.6; font-size: 13px; } +.nd-detail-label { color: var(--nd-text-muted); } +.nd-detail-value { margin-left: auto; font-weight: 600; color: var(--nd-text-primary); } .nd-detail-value.success { color: var(--nd-success); } .nd-detail-value.warning { color: var(--nd-warning); } .nd-detail-value.error { color: var(--nd-error); } -/* 菜单 */ +/* ═══════════════════════════════════════════════════════════════════════════ + 菜单面板 - 核心重构 + ═══════════════════════════════════════════════════════════════════════════ */ .nd-menu { - position: absolute; bottom: calc(100% + 8px); right: 0; - width: 180px; background: rgba(28,28,32,0.98); - border: 1px solid rgba(255,255,255,0.1); - border-radius: 10px; padding: 6px; - box-shadow: 0 8px 32px rgba(0,0,0,0.5); - opacity: 0; visibility: hidden; + position: absolute; + bottom: calc(100% + 10px); + right: 0; + width: 190px; + background: var(--nd-bg-solid); + border: 1px solid var(--nd-border-default); + border-radius: var(--nd-radius-lg); + padding: 10px; + box-shadow: var(--nd-shadow-lg); + opacity: 0; + visibility: hidden; transform: translateY(6px) scale(0.98); transform-origin: bottom right; - transition: all 0.15s cubic-bezier(0.34,1.56,0.64,1); + transition: opacity var(--nd-transition-fast), + transform 0.15s cubic-bezier(0.34, 1.56, 0.64, 1), + visibility var(--nd-transition-fast); z-index: 5; } + .nd-float.expanded .nd-menu { - opacity: 1; visibility: visible; transform: translateY(0) scale(1); -} -.nd-menu-header { - padding: 6px 10px 4px; - font-size: 10px; - color: rgba(255,255,255,0.35); -} -.nd-menu-item { - display: flex; align-items: center; gap: 10px; - padding: 8px 10px; border-radius: 6px; - cursor: pointer; color: rgba(255,255,255,0.75); - font-size: 12px; transition: background 0.1s; -} -.nd-menu-item:hover { background: rgba(255,255,255,0.08); } -.nd-menu-item.active { color: var(--accent); } -.nd-item-icon { width: 14px; text-align: center; font-size: 10px; opacity: 0.5; } -.nd-menu-item.active .nd-item-icon { opacity: 1; } -.nd-menu-divider { height: 1px; background: rgba(255,255,255,0.08); margin: 4px 0; } -.nd-menu-dot { - width: 6px; height: 6px; border-radius: 50%; - background: rgba(255,255,255,0.2); margin-left: auto; transition: all 0.2s; -} -.nd-menu-dot.active { - background: var(--nd-success); - box-shadow: 0 0 6px rgba(62,207,142,0.6); + opacity: 1; + visibility: visible; + transform: translateY(0) scale(1); } -/* 预设下拉框 */ -.nd-preset-row { padding: 4px 10px 8px; } -.nd-preset-select { - width: 100%; padding: 6px 8px; - background: rgba(0,0,0,0.3); - border: 1px solid rgba(255,255,255,0.15); - border-radius: 6px; - color: rgba(255,255,255,0.9); - font-size: 12px; cursor: pointer; outline: none; - transition: border-color 0.15s; +/* ═══════════════════════════════════════════════════════════════════════════ + 参数卡片 + ═══════════════════════════════════════════════════════════════════════════ */ +.nd-card { + background: var(--nd-bg-card); + border: 1px solid var(--nd-border-subtle); + border-radius: var(--nd-radius-md); + overflow: hidden; + box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.02); +} + +.nd-row { + display: flex; + align-items: center; + padding: 2px 0; +} + +/* 标签 - 提升可读性 */ +.nd-label { + width: 36px; + padding-left: 10px; + font-size: 10px; + font-weight: 500; + color: var(--nd-text-muted); + letter-spacing: 0.2px; + flex-shrink: 0; +} + +/* 选择框 - 统一风格 */ +.nd-select { + flex: 1; + min-width: 0; + border: none; + background: transparent; + color: var(--nd-text-primary); + font-size: 12px; + padding: 10px 8px; + outline: none; + cursor: pointer; + transition: color var(--nd-transition-fast); + text-align: center; + text-align-last: center; + margin: 0; + line-height: 1.2; +} + +.nd-select:hover { color: #fff; } +.nd-select:focus { color: #fff; } + +.nd-select option { + background: #1a1a1e; + color: #eee; + padding: 8px; + text-align: left; +} + +/* 尺寸选择框 - 等宽字体,白色文字 */ +.nd-select.size { + font-family: "SF Mono", "Menlo", "Consolas", "Liberation Mono", monospace; + font-size: 11px; + letter-spacing: -0.2px; +} + +/* 内部分隔线 */ +.nd-inner-sep { + height: 1px; + background: linear-gradient( + 90deg, + transparent 8px, + var(--nd-border-subtle) 8px, + var(--nd-border-subtle) calc(100% - 8px), + transparent calc(100% - 8px) + ); +} + +/* ═══════════════════════════════════════════════════════════════════════════ + 控制栏 + ═══════════════════════════════════════════════════════════════════════════ */ +.nd-controls { + display: flex; + align-items: center; + gap: 8px; + margin-top: 10px; +} + +/* 自动开关 */ +.nd-auto { + flex: 1; + display: flex; + align-items: center; + gap: 8px; + padding: 9px 12px; + background: rgba(255, 255, 255, 0.03); + border: 1px solid var(--nd-border-subtle); + border-radius: var(--nd-radius-sm); + cursor: pointer; + transition: all var(--nd-transition-fast); +} + +.nd-auto:hover { + background: var(--nd-bg-hover); + border-color: var(--nd-border-default); +} + +.nd-auto.on { + background: rgba(62, 207, 142, 0.08); + border-color: rgba(62, 207, 142, 0.3); +} + +.nd-dot { + width: 7px; + height: 7px; + border-radius: 50%; + background: rgba(255, 255, 255, 0.2); + transition: all 0.2s; + flex-shrink: 0; +} + +.nd-auto.on .nd-dot { + background: var(--nd-success); + box-shadow: 0 0 8px rgba(62, 207, 142, 0.5); +} + +.nd-auto-text { + font-size: 12px; + color: var(--nd-text-muted); + transition: color var(--nd-transition-fast); +} + +.nd-auto:hover .nd-auto-text { + color: var(--nd-text-secondary); +} + +.nd-auto.on .nd-auto-text { + color: rgba(62, 207, 142, 0.95); +} + +/* 设置按钮 */ +.nd-gear { + width: 36px; + height: 36px; + border: 1px solid var(--nd-border-subtle); + border-radius: var(--nd-radius-sm); + background: rgba(255, 255, 255, 0.03); + color: var(--nd-text-muted); + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + font-size: 14px; + transition: all var(--nd-transition-fast); + flex-shrink: 0; +} + +.nd-gear:hover { + background: var(--nd-bg-hover); + border-color: var(--nd-border-default); + color: var(--nd-text-secondary); +} + +.nd-gear:active { + background: var(--nd-bg-active); } -.nd-preset-select:hover { border-color: rgba(255,255,255,0.25); } -.nd-preset-select:focus { border-color: var(--nd-accent); } -.nd-preset-select option { background: #1a1a1e; color: #fff; } `; function injectStyles() { @@ -313,44 +616,40 @@ function applyPosition() { } // ═══════════════════════════════════════════════════════════════════════════ -// 冷却倒计时 +// 倒计时 // ═══════════════════════════════════════════════════════════════════════════ function clearCooldownTimer() { - if (cooldownTimer) { - clearInterval(cooldownTimer); - cooldownTimer = null; + if (cooldownRafId) { + cancelAnimationFrame(cooldownRafId); + cooldownRafId = null; } cooldownEndTime = 0; } function startCooldownTimer(duration) { clearCooldownTimer(); - cooldownEndTime = Date.now() + duration; - // 立即更新一次 - updateCooldownDisplay(); - - // 🔧 修复3:每50ms更新一次,更流畅,且始终更新显示 - cooldownTimer = setInterval(() => { + function tick() { + if (!cooldownEndTime) return; updateCooldownDisplay(); - - // 倒计时结束后清理定时器(但不切换状态,等 novel-draw.js 来切换) - if (cooldownEndTime - Date.now() <= -100) { + const remaining = cooldownEndTime - Date.now(); + if (remaining <= -100) { clearCooldownTimer(); + return; } - }, 50); + cooldownRafId = requestAnimationFrame(tick); + } + + cooldownRafId = requestAnimationFrame(tick); } function updateCooldownDisplay() { - const { statusIcon, statusText } = $cache; - if (!statusIcon || !statusText) return; - - // 🔧 修复2 & 3:显示小数点后一位,最小显示0.0 + const { statusText } = $cache; + if (!statusText) return; const remaining = Math.max(0, cooldownEndTime - Date.now()); const seconds = (remaining / 1000).toFixed(1); - statusText.textContent = `${seconds}s`; statusText.className = 'nd-countdown'; } @@ -359,7 +658,6 @@ function updateCooldownDisplay() { // 状态管理 // ═══════════════════════════════════════════════════════════════════════════ -// 🔧 修复1:spinning 设为 true const STATE_CONFIG = { [FloatState.IDLE]: { cls: '', icon: '', text: '', spinning: false }, [FloatState.LLM]: { cls: 'working', icon: '⏳', text: '分析', spinning: true }, @@ -375,18 +673,15 @@ function setState(state, data = {}) { currentState = state; - // 清理自动重置定时器 if (autoResetTimer) { clearTimeout(autoResetTimer); autoResetTimer = null; } - // 非冷却状态时清理冷却定时器 if (state !== FloatState.COOLDOWN) { clearCooldownTimer(); } - // 移除所有状态类 floatEl.classList.remove('working', 'cooldown', 'success', 'partial', 'error', 'show-detail'); const cfg = STATE_CONFIG[state]; @@ -395,7 +690,6 @@ function setState(state, data = {}) { const { statusIcon, statusText } = $cache; if (!statusIcon || !statusText) return; - // 🔧 修复1:根据 spinning 添加旋转类 statusIcon.textContent = cfg.icon; statusIcon.className = cfg.spinning ? 'nd-spin' : ''; statusText.className = ''; @@ -404,22 +698,17 @@ function setState(state, data = {}) { case FloatState.IDLE: currentResult = { success: 0, total: 0, error: null, startTime: 0 }; break; - case FloatState.LLM: currentResult.startTime = Date.now(); statusText.textContent = cfg.text; break; - case FloatState.GEN: statusText.textContent = `${data.current || 0}/${data.total || 0}`; currentResult.total = data.total || 0; break; - case FloatState.COOLDOWN: - // 启动冷却倒计时 startCooldownTimer(data.duration); break; - case FloatState.SUCCESS: case FloatState.PARTIAL: statusText.textContent = `${data.success}/${data.total}`; @@ -427,7 +716,6 @@ function setState(state, data = {}) { currentResult.total = data.total; autoResetTimer = setTimeout(() => setState(FloatState.IDLE), AUTO_RESET_DELAY); break; - case FloatState.ERROR: statusText.textContent = data.error?.label || '错误'; currentResult.error = data.error; @@ -531,10 +819,16 @@ function routeClick(target) { floatEl.classList.remove('show-detail'); if (!floatEl.classList.contains('expanded')) { refreshPresetSelect(); + refreshSizeSelect(); } floatEl.classList.toggle('expanded'); } else if (target.closest('#nd-layer-active')) { - if ([FloatState.SUCCESS, FloatState.PARTIAL, FloatState.ERROR].includes(currentState)) { + + if ([FloatState.LLM, FloatState.GEN, FloatState.COOLDOWN].includes(currentState)) { + + handleAbort(); + } else if ([FloatState.SUCCESS, FloatState.PARTIAL, FloatState.ERROR].includes(currentState)) { + updateDetailPopup(); floatEl.classList.toggle('show-detail'); } @@ -546,7 +840,7 @@ function routeClick(target) { // ═══════════════════════════════════════════════════════════════════════════ async function handleDrawClick() { - if (currentState !== FloatState.IDLE) return; + if (currentState !== FloatState.IDLE) return; // 非空闲状态不处理 const messageId = findLastAIMessageId(); if (messageId < 0) { @@ -559,65 +853,97 @@ async function handleDrawClick() { messageId, onStateChange: (state, data) => { switch (state) { - case 'llm': - setState(FloatState.LLM); - break; - case 'gen': - setState(FloatState.GEN, data); - break; - case 'progress': - setState(FloatState.GEN, data); // 用 GEN 状态显示进度 - break; - case 'cooldown': - setState(FloatState.COOLDOWN, data); - break; + case 'llm': setState(FloatState.LLM); break; + case 'gen': setState(FloatState.GEN, data); break; + case 'progress': setState(FloatState.GEN, data); break; + case 'cooldown': setState(FloatState.COOLDOWN, data); break; case 'success': - setState(data.success === data.total ? FloatState.SUCCESS : FloatState.PARTIAL, data); + // ▼ 修改:中止时也显示结果 + if (data.aborted && data.success === 0) { + setState(FloatState.IDLE); + } else if (data.aborted || data.success < data.total) { + setState(FloatState.PARTIAL, data); + } else { + setState(FloatState.SUCCESS, data); + } break; } } }); } catch (e) { console.error('[NovelDraw]', e); - setState(FloatState.ERROR, { error: classifyError(e) }); + // ▼ 修改:中止不显示错误 + if (e.message === '已取消') { + setState(FloatState.IDLE); + } else { + setState(FloatState.ERROR, { error: classifyError(e) }); + } + } +} + +async function handleAbort() { + try { + const { abortGeneration } = await import('./novel-draw.js'); + if (abortGeneration()) { + setState(FloatState.IDLE); + toastr?.info?.('已中止'); + } + } catch (e) { + console.error('[NovelDraw] 中止失败:', e); } } // ═══════════════════════════════════════════════════════════════════════════ -// 预设管理 +// 预设与尺寸管理 // ═══════════════════════════════════════════════════════════════════════════ function buildPresetOptions() { const settings = getSettings(); const presets = settings.paramsPresets || []; const currentId = settings.selectedParamsPresetId; - return presets.map(p => `` ).join(''); } +function buildSizeOptions() { + const settings = getSettings(); + const current = settings.overrideSize || 'default'; + return SIZE_OPTIONS.map(opt => + `` + ).join(''); +} + function refreshPresetSelect() { if (!$cache.presetSelect) return; $cache.presetSelect.innerHTML = buildPresetOptions(); } +function refreshSizeSelect() { + if (!$cache.sizeSelect) return; + $cache.sizeSelect.innerHTML = buildSizeOptions(); +} + function handlePresetChange(e) { const presetId = e.target.value; if (!presetId) return; - const settings = getSettings(); settings.selectedParamsPresetId = presetId; saveSettings(settings); } +function handleSizeChange(e) { + const value = e.target.value; + const settings = getSettings(); + settings.overrideSize = value; + saveSettings(settings); +} + export function updateAutoModeUI() { if (!floatEl) return; const isAuto = getSettings().mode === 'auto'; floatEl.classList.toggle('auto-on', isAuto); - - const menuDot = floatEl.querySelector('#nd-menu-auto-dot'); - menuDot?.classList.toggle('active', isAuto); + $cache.autoToggle?.classList.toggle('on', isAuto); } function handleAutoToggle() { @@ -644,6 +970,7 @@ export function createFloatingPanel() { floatEl.id = 'nd-floating-panel'; floatEl.innerHTML = ` +