Add files via upload
This commit is contained in:
@@ -858,7 +858,7 @@ function renderContent(text) {
|
|||||||
if (!text) return '';
|
if (!text) return '';
|
||||||
let html = String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
let html = String(text).replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>');
|
||||||
|
|
||||||
html = html.replace(/\[(?:image|图片)\s*:\s*([^\]]+)\]/gi, (_, inner) => {
|
html = html.replace(/\[(?:img|图片)\s*:\s*([^\]]+)\]/gi, (_, inner) => {
|
||||||
const tags = parseImageToken(inner);
|
const tags = parseImageToken(inner);
|
||||||
if (!tags) return _;
|
if (!tags) return _;
|
||||||
return `<div class="fw-img-slot" data-raw="${encodeURIComponent(inner)}"></div>`;
|
return `<div class="fw-img-slot" data-raw="${encodeURIComponent(inner)}"></div>`;
|
||||||
@@ -900,6 +900,7 @@ function renderContent(text) {
|
|||||||
return html;
|
return html;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
function renderMessages() {
|
function renderMessages() {
|
||||||
const container = document.getElementById('messages');
|
const container = document.getElementById('messages');
|
||||||
const { history, isStreaming, editingIndex } = state;
|
const { history, isStreaming, editingIndex } = state;
|
||||||
|
|||||||
@@ -272,9 +272,9 @@ export async function handleGenerate(data, postToFrame) {
|
|||||||
|
|
||||||
export const IMG_GUIDELINE = `## 模拟图片
|
export const IMG_GUIDELINE = `## 模拟图片
|
||||||
如果需要发图、照片给对方时,可以在聊天文本中穿插以下格式行,进行图片模拟:
|
如果需要发图、照片给对方时,可以在聊天文本中穿插以下格式行,进行图片模拟:
|
||||||
[image: Subject, Appearance, Background, Atmosphere, Extra descriptors]
|
[img: Subject, Appearance, Background, Atmosphere, Extra descriptors]
|
||||||
- tag必须为英文,用逗号分隔,使用Danbooru风格的tag,5-15个tag
|
- tag必须为英文,用逗号分隔,使用Danbooru风格的tag,5-15个tag
|
||||||
- 第一个tag须固定为人物数量标签,如: 1girl, 1boy, 2girls, solo, etc.
|
- 第一个tag须固定为人物数量标签,如: 1girl, 1boy, 2girls, solo, etc.
|
||||||
- 可以多张照片: 每行一张 [image: ...]
|
- 可以多张照片: 每行一张 [img: ...]
|
||||||
- 当需要发送的内容尺度较大时加上nsfw相关tag
|
- 当需要发送的内容尺度较大时加上nsfw相关tag
|
||||||
- image部分也需要在<msg>内`;
|
- image部分也需要在<msg>内`;
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ const CSS_INJECTED_KEY = 'xb-me-css-injected';
|
|||||||
|
|
||||||
let currentAudio = null;
|
let currentAudio = null;
|
||||||
let imageObserver = null;
|
let imageObserver = null;
|
||||||
|
let domObserver = null; // ▼ 新增
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
// 初始化与清理
|
// 初始化与清理
|
||||||
@@ -39,6 +40,7 @@ export async function initMessageEnhancer() {
|
|||||||
injectStyles();
|
injectStyles();
|
||||||
await loadVoices();
|
await loadVoices();
|
||||||
initImageObserver();
|
initImageObserver();
|
||||||
|
initDomObserver(); // ▼ 新增
|
||||||
|
|
||||||
events.on(event_types.CHAT_CHANGED, () => {
|
events.on(event_types.CHAT_CHANGED, () => {
|
||||||
clearQueue();
|
clearQueue();
|
||||||
@@ -65,12 +67,99 @@ export function cleanupMessageEnhancer() {
|
|||||||
imageObserver = null;
|
imageObserver = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ▼ 新增
|
||||||
|
if (domObserver) {
|
||||||
|
domObserver.disconnect();
|
||||||
|
domObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
if (currentAudio) {
|
if (currentAudio) {
|
||||||
currentAudio.pause();
|
currentAudio.pause();
|
||||||
currentAudio = null;
|
currentAudio = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
// DOM 变化观察器(新增)
|
||||||
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
|
function initDomObserver() {
|
||||||
|
if (domObserver) return;
|
||||||
|
|
||||||
|
const chatContainer = document.getElementById('chat');
|
||||||
|
if (!chatContainer) {
|
||||||
|
// 如果 chat 容器还没加载,延迟重试
|
||||||
|
setTimeout(initDomObserver, 500);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 用于防抖处理
|
||||||
|
let pendingTexts = new Set();
|
||||||
|
let debounceTimer = null;
|
||||||
|
|
||||||
|
domObserver = new MutationObserver((mutations) => {
|
||||||
|
const settings = extension_settings[EXT_ID];
|
||||||
|
if (!settings?.fourthWall?.enabled) return;
|
||||||
|
|
||||||
|
for (const mutation of mutations) {
|
||||||
|
let mesText = null;
|
||||||
|
|
||||||
|
if (mutation.type === 'childList') {
|
||||||
|
for (const node of mutation.addedNodes) {
|
||||||
|
if (node.nodeType !== Node.ELEMENT_NODE) continue;
|
||||||
|
|
||||||
|
if (node.classList?.contains('mes_text')) {
|
||||||
|
mesText = node;
|
||||||
|
} else if (node.classList?.contains('mes')) {
|
||||||
|
mesText = node.querySelector('.mes_text');
|
||||||
|
} else {
|
||||||
|
mesText = node.querySelector?.('.mes_text');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mesText && hasUnrenderedPlaceholders(mesText)) {
|
||||||
|
pendingTexts.add(mesText);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mutation.target?.classList?.contains('mes_text')) {
|
||||||
|
if (hasUnrenderedPlaceholders(mutation.target)) {
|
||||||
|
pendingTexts.add(mutation.target);
|
||||||
|
}
|
||||||
|
} else if (mutation.target?.closest?.('.mes_text')) {
|
||||||
|
const target = mutation.target.closest('.mes_text');
|
||||||
|
if (hasUnrenderedPlaceholders(target)) {
|
||||||
|
pendingTexts.add(target);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pendingTexts.size > 0 && !debounceTimer) {
|
||||||
|
debounceTimer = setTimeout(() => {
|
||||||
|
pendingTexts.forEach(mesText => {
|
||||||
|
if (document.contains(mesText)) {
|
||||||
|
enhanceMessageContent(mesText);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
pendingTexts.clear();
|
||||||
|
debounceTimer = null;
|
||||||
|
}, 50);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
domObserver.observe(chatContainer, {
|
||||||
|
childList: true,
|
||||||
|
subtree: true,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasUnrenderedPlaceholders(mesText) {
|
||||||
|
if (!mesText) return false;
|
||||||
|
const html = mesText.innerHTML;
|
||||||
|
return /\[(?:img|图片)\s*:\s*[^\]]+\]/i.test(html) ||
|
||||||
|
/\[(?:voice|语音)\s*:[^\]]+\]/i.test(html);
|
||||||
|
}
|
||||||
|
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
// 事件处理
|
// 事件处理
|
||||||
// ════════════════════════════════════════════════════════════════════════════
|
// ════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -271,7 +360,7 @@ function enhanceMessageContent(container) {
|
|||||||
let enhanced = html;
|
let enhanced = html;
|
||||||
let hasChanges = false;
|
let hasChanges = false;
|
||||||
|
|
||||||
enhanced = enhanced.replace(/\[(?:image|图片)\s*:\s*([^\]]+)\]/gi, (match, inner) => {
|
enhanced = enhanced.replace(/\[(?:img|图片)\s*:\s*([^\]]+)\]/gi, (match, inner) => {
|
||||||
const tags = parseImageToken(inner);
|
const tags = parseImageToken(inner);
|
||||||
if (!tags) return match;
|
if (!tags) return match;
|
||||||
hasChanges = true;
|
hasChanges = true;
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1545,7 +1545,74 @@ async function removePlaceholder(container) {
|
|||||||
container.remove();
|
container.remove();
|
||||||
showToast('占位符已移除');
|
showToast('占位符已移除');
|
||||||
}
|
}
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
// 图片懒加载
|
||||||
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
|
let slotObserver = null;
|
||||||
|
function initSlotObserver() {
|
||||||
|
if (slotObserver) return;
|
||||||
|
|
||||||
|
slotObserver = new IntersectionObserver((entries) => {
|
||||||
|
entries.forEach(entry => {
|
||||||
|
if (!entry.isIntersecting) return;
|
||||||
|
const slot = entry.target;
|
||||||
|
if (slot.dataset.loaded === '1' || slot.dataset.loading === '1') return;
|
||||||
|
slot.dataset.loading = '1';
|
||||||
|
loadSlotImage(slot);
|
||||||
|
});
|
||||||
|
}, { rootMargin: '200px 0px', threshold: 0.01 });
|
||||||
|
}
|
||||||
|
async function loadSlotImage(slot) {
|
||||||
|
const slotId = slot.dataset.slotId;
|
||||||
|
const messageId = parseInt(slot.dataset.mesid);
|
||||||
|
|
||||||
|
try {
|
||||||
|
const displayData = await getDisplayPreviewForSlot(slotId);
|
||||||
|
|
||||||
|
if (displayData.isFailed) {
|
||||||
|
slot.outerHTML = buildFailedPlaceholderHtml({
|
||||||
|
slotId, messageId,
|
||||||
|
tags: displayData.failedInfo?.tags || '',
|
||||||
|
positive: displayData.failedInfo?.positive || '',
|
||||||
|
errorType: displayData.failedInfo?.errorType || ErrorType.CACHE_LOST.label,
|
||||||
|
errorMessage: displayData.failedInfo?.errorMessage || ErrorType.CACHE_LOST.desc
|
||||||
|
});
|
||||||
|
} else if (displayData.hasData && displayData.preview) {
|
||||||
|
const url = displayData.preview.savedUrl || `data:image/png;base64,${displayData.preview.base64}`;
|
||||||
|
slot.outerHTML = buildImageHtml({
|
||||||
|
slotId,
|
||||||
|
imgId: displayData.preview.imgId,
|
||||||
|
url,
|
||||||
|
tags: displayData.preview.tags,
|
||||||
|
positive: displayData.preview.positive,
|
||||||
|
messageId,
|
||||||
|
state: displayData.preview.savedUrl ? ImageState.SAVED : ImageState.PREVIEW,
|
||||||
|
historyCount: displayData.historyCount,
|
||||||
|
currentIndex: 0
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
slot.outerHTML = buildFailedPlaceholderHtml({
|
||||||
|
slotId, messageId, tags: '', positive: '',
|
||||||
|
errorType: ErrorType.CACHE_LOST.label,
|
||||||
|
errorMessage: ErrorType.CACHE_LOST.desc
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
slot.dataset.loading = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
function buildLoadingPlaceholderHtml(slotId, messageId) {
|
||||||
|
return `<div class="xb-nd-img xb-nd-loading-slot" data-slot-id="${slotId}" data-mesid="${messageId}" style="margin:0.8em 0;text-align:center;padding:20px;background:rgba(0,0,0,0.03);border:1px dashed rgba(255,255,255,0.1);border-radius:14px;">
|
||||||
|
<div style="color:rgba(255,255,255,0.4);font-size:12px;">📷 滚动加载</div>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
function hydrateSlots(container) {
|
||||||
|
initSlotObserver();
|
||||||
|
container.querySelectorAll('.xb-nd-loading-slot:not([data-observed])').forEach(slot => {
|
||||||
|
slot.dataset.observed = '1';
|
||||||
|
slotObserver.observe(slot);
|
||||||
|
});
|
||||||
|
}
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 预览渲染
|
// 预览渲染
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -1560,59 +1627,25 @@ async function renderPreviewsForMessage(messageId) {
|
|||||||
|
|
||||||
const $mesText = $(`#chat .mes[mesid="${messageId}"] .mes_text`);
|
const $mesText = $(`#chat .mes[mesid="${messageId}"] .mes_text`);
|
||||||
if (!$mesText.length) return;
|
if (!$mesText.length) return;
|
||||||
|
|
||||||
let html = $mesText.html();
|
let html = $mesText.html();
|
||||||
let replaced = false;
|
let replaced = false;
|
||||||
|
|
||||||
for (const slotId of slotIds) {
|
for (const slotId of slotIds) {
|
||||||
if (html.includes(`data-slot-id="${slotId}"`)) continue;
|
if (html.includes(`data-slot-id="${slotId}"`)) continue;
|
||||||
|
|
||||||
const displayData = await getDisplayPreviewForSlot(slotId);
|
|
||||||
const placeholder = createPlaceholder(slotId);
|
const placeholder = createPlaceholder(slotId);
|
||||||
const escapedPlaceholder = placeholder.replace(/[[\]]/g, '\\$&');
|
const escapedPlaceholder = placeholder.replace(/[[\]]/g, '\\$&');
|
||||||
if (!new RegExp(escapedPlaceholder).test(html)) continue;
|
if (!new RegExp(escapedPlaceholder).test(html)) continue;
|
||||||
|
|
||||||
let imgHtml;
|
const loadingHtml = buildLoadingPlaceholderHtml(slotId, messageId);
|
||||||
if (displayData.isFailed) {
|
html = html.replace(new RegExp(escapedPlaceholder, 'g'), loadingHtml);
|
||||||
imgHtml = buildFailedPlaceholderHtml({
|
|
||||||
slotId,
|
|
||||||
messageId,
|
|
||||||
tags: displayData.failedInfo?.tags || '',
|
|
||||||
positive: displayData.failedInfo?.positive || '',
|
|
||||||
errorType: displayData.failedInfo?.errorType || ErrorType.CACHE_LOST.label,
|
|
||||||
errorMessage: displayData.failedInfo?.errorMessage || ErrorType.CACHE_LOST.desc
|
|
||||||
});
|
|
||||||
} else if (displayData.hasData && displayData.preview) {
|
|
||||||
const url = displayData.preview.savedUrl || `data:image/png;base64,${displayData.preview.base64}`;
|
|
||||||
const allPreviews = await getPreviewsBySlot(slotId);
|
|
||||||
const successPreviews = allPreviews.filter(p => p.status !== 'failed' && p.base64);
|
|
||||||
const currentIndex = successPreviews.findIndex(p => p.imgId === displayData.preview.imgId);
|
|
||||||
imgHtml = buildImageHtml({
|
|
||||||
slotId,
|
|
||||||
imgId: displayData.preview.imgId,
|
|
||||||
url,
|
|
||||||
tags: displayData.preview.tags,
|
|
||||||
positive: displayData.preview.positive,
|
|
||||||
messageId,
|
|
||||||
state: displayData.preview.savedUrl ? ImageState.SAVED : ImageState.PREVIEW,
|
|
||||||
historyCount: displayData.historyCount,
|
|
||||||
currentIndex: currentIndex >= 0 ? currentIndex : 0
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
imgHtml = buildFailedPlaceholderHtml({
|
|
||||||
slotId,
|
|
||||||
messageId,
|
|
||||||
tags: '',
|
|
||||||
positive: '',
|
|
||||||
errorType: ErrorType.CACHE_LOST.label,
|
|
||||||
errorMessage: ErrorType.CACHE_LOST.desc
|
|
||||||
});
|
|
||||||
}
|
|
||||||
html = html.replace(new RegExp(escapedPlaceholder, 'g'), imgHtml);
|
|
||||||
replaced = true;
|
replaced = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (replaced && !isMessageBeingEdited(messageId)) {
|
if (replaced && !isMessageBeingEdited(messageId)) {
|
||||||
$mesText.html(html);
|
$mesText.html(html);
|
||||||
|
hydrateSlots($mesText[0]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1620,7 +1653,9 @@ async function renderAllPreviews() {
|
|||||||
const ctx = getContext();
|
const ctx = getContext();
|
||||||
const chat = ctx.chat || [];
|
const chat = ctx.chat || [];
|
||||||
for (let i = 0; i < chat.length; i++) {
|
for (let i = 0; i < chat.length; i++) {
|
||||||
if (extractSlotIds(chat[i]?.mes).size > 0) await renderPreviewsForMessage(i);
|
if (extractSlotIds(chat[i]?.mes).size > 0) {
|
||||||
|
await renderPreviewsForMessage(i);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1630,7 +1665,7 @@ async function handleMessageRendered(data) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleChatChanged() {
|
async function handleChatChanged() {
|
||||||
await new Promise(r => setTimeout(r, 200));
|
await new Promise(r => setTimeout(r, 50));
|
||||||
await renderAllPreviews();
|
await renderAllPreviews();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1642,6 +1677,20 @@ async function handleMessageModified(data) {
|
|||||||
await renderPreviewsForMessage(messageId);
|
await renderPreviewsForMessage(messageId);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleVisibilityChange() {
|
||||||
|
if (document.visibilityState === 'visible' && moduleInitialized) {
|
||||||
|
document.querySelectorAll('.xb-nd-loading-slot[data-observed="1"]').forEach(slot => {
|
||||||
|
if (slot.dataset.loaded !== '1' && slot.dataset.loading !== '1') {
|
||||||
|
const rect = slot.getBoundingClientRect();
|
||||||
|
if (rect.bottom >= 0 && rect.top <= window.innerHeight + 200) {
|
||||||
|
slot.dataset.loading = '1';
|
||||||
|
loadSlotImage(slot);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
// 多图生成
|
// 多图生成
|
||||||
// ═══════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════
|
||||||
@@ -2375,6 +2424,8 @@ export async function initNovelDraw() {
|
|||||||
events.on(event_types.MESSAGE_SWIPED, handleMessageModified);
|
events.on(event_types.MESSAGE_SWIPED, handleMessageModified);
|
||||||
events.on(event_types.GENERATION_ENDED, async () => { try { await autoGenerateForLastAI(); } catch (e) { console.error('[NovelDraw]', e); } });
|
events.on(event_types.GENERATION_ENDED, async () => { try { await autoGenerateForLastAI(); } catch (e) { console.error('[NovelDraw]', e); } });
|
||||||
|
|
||||||
|
document.addEventListener('visibilitychange', handleVisibilityChange);
|
||||||
|
|
||||||
window.xiaobaixNovelDraw = {
|
window.xiaobaixNovelDraw = {
|
||||||
getSettings,
|
getSettings,
|
||||||
saveSettings,
|
saveSettings,
|
||||||
@@ -2416,7 +2467,14 @@ export async function cleanupNovelDraw() {
|
|||||||
destroyGalleryCache();
|
destroyGalleryCache();
|
||||||
overlayCreated = false;
|
overlayCreated = false;
|
||||||
frameReady = false;
|
frameReady = false;
|
||||||
|
|
||||||
|
if (slotObserver) {
|
||||||
|
slotObserver.disconnect();
|
||||||
|
slotObserver = null;
|
||||||
|
}
|
||||||
|
|
||||||
window.removeEventListener('message', handleFrameMessage);
|
window.removeEventListener('message', handleFrameMessage);
|
||||||
|
document.removeEventListener('visibilitychange', handleVisibilityChange);
|
||||||
document.getElementById('xiaobaix-novel-draw-overlay')?.remove();
|
document.getElementById('xiaobaix-novel-draw-overlay')?.remove();
|
||||||
|
|
||||||
const { destroyFloatingPanel } = await import('./floating-panel.js');
|
const { destroyFloatingPanel } = await import('./floating-panel.js');
|
||||||
|
|||||||
Reference in New Issue
Block a user