Fixed SD Promt

This commit is contained in:
2026-06-02 15:03:39 +03:00
parent d4cd8f02f4
commit 03cbda5dce
46 changed files with 3285 additions and 429 deletions
+73 -26
View File
@@ -1,9 +1,9 @@
import { sessionId, currentPersona, dom } from './state.js';
import { parseImagePromptFromContent, copyToClipboard } from './utils.js';
import { parseImagePromptFromContent, copyToClipboard, splitSdPromptForCopy } from './utils.js';
export async function initChat(options = {}) {
if (!sessionId || !currentPersona) return;
const payload = { message: '', session_id: sessionId, persona_id: currentPersona };
if (!sessionId) return;
const payload = { message: '', session_id: sessionId };
if (options.first_mes_override?.trim()) payload.first_mes_override = options.first_mes_override.trim();
const res = await fetch('/chat/init', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) });
if (!res.ok) return;
@@ -16,19 +16,22 @@ export function updateEmptyState() {
dom.emptyState?.classList.toggle('hidden', !!hasMessages);
}
export function createImagePromptBlock(promptText) {
function createImagePromptBlockSingle(label, promptText) {
const block = document.createElement('div');
block.className = 'image-prompt-block';
const header = document.createElement('div');
header.className = 'image-prompt-header';
header.innerHTML = '<span>🎨 SD prompt</span>';
header.innerHTML = `<span>🎨 ${label}</span>`;
const copyBtn = document.createElement('button');
copyBtn.type = 'button';
copyBtn.className = 'copy-prompt-btn';
copyBtn.textContent = 'Копировать';
copyBtn.addEventListener('click', async () => {
const ok = await copyToClipboard(promptText);
copyBtn.addEventListener('click', async (e) => {
e.preventDefault();
e.stopPropagation();
const full = textEl.textContent?.trim() || promptText || '';
const ok = await copyToClipboard(splitSdPromptForCopy(full));
copyBtn.textContent = ok ? 'Скопировано' : 'Ошибка';
setTimeout(() => { copyBtn.textContent = 'Копировать'; }, 1500);
});
@@ -39,11 +42,10 @@ export function createImagePromptBlock(promptText) {
regenBtn.className = 'copy-prompt-btn';
regenBtn.textContent = '🖼 Перегенерировать';
regenBtn.addEventListener('click', async () => {
const wrapper = block.parentElement;
const wrapper = block.closest('.message');
regenBtn.disabled = true;
regenBtn.textContent = '⏳…';
wrapper?.querySelector('.chat-image')?.remove();
wrapper?.querySelector('.image-error')?.remove();
wrapper?.querySelectorAll('.chat-image-wrap, .chat-image, .image-error').forEach(el => el.remove());
showImageGenerating(wrapper);
try {
const res = await fetch('/images/generate', {
@@ -76,6 +78,26 @@ export function createImagePromptBlock(promptText) {
return block;
}
export function createImagePromptBlock(promptText, promptAlt = null) {
const wrap = document.createElement('div');
wrap.className = 'image-prompt-blocks';
wrap.appendChild(createImagePromptBlockSingle('SD prompt', promptText));
const alt = (promptAlt || '').trim();
const main = (promptText || '').trim();
if (alt && alt !== main) {
wrap.appendChild(createImagePromptBlockSingle('SD prompt (только теги)', promptAlt));
}
return wrap;
}
/** Replace or create tag + optional hybrid prompt blocks under a message. */
export function ensureImagePromptBlocks(wrapper, tagPrompt, altPrompt = null) {
if (!wrapper || !tagPrompt) return;
wrapper.querySelector('.image-prompt-blocks')?.remove();
wrapper.querySelectorAll('.image-prompt-block').forEach(el => el.remove());
wrapper.appendChild(createImagePromptBlock(tagPrompt, altPrompt || null));
}
const OUTCOME_CLASS = {
'critical failure': 'outcome-crit-fail',
'failure': 'outcome-fail',
@@ -113,7 +135,7 @@ function renderNarratorMessage(narrator) {
return el;
}
function renderChoices(wrapper, choices) {
export function renderChoices(wrapper, choices) {
if (!choices?.length) return;
const row = document.createElement('div');
row.className = 'choice-row';
@@ -169,12 +191,21 @@ export function updateAffinityDisplay(affinity) {
el.className = `affinity-display ${affinity > 5 ? 'affinity-high' : affinity < -3 ? 'affinity-low' : ''}`;
}
export function appendChatImage(wrapper, imagePath) {
export function appendChatImage(wrapper, imagePath, label = '') {
if (!imagePath) return;
const figure = document.createElement('figure');
figure.className = 'chat-image-wrap';
if (label) {
const cap = document.createElement('figcaption');
cap.className = 'chat-image-label';
cap.textContent = label;
figure.appendChild(cap);
}
const img = document.createElement('img');
img.className = 'chat-image';
img.src = imagePath;
wrapper.appendChild(img);
figure.appendChild(img);
wrapper.appendChild(figure);
}
export function showImageGenerating(wrapper) {
@@ -262,7 +293,7 @@ async function regenerateMessage(messageId, wrapper) {
const res = await fetch('/chat/regenerate', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: sessionId, persona_id: currentPersona, message_id: messageId }),
body: JSON.stringify({ session_id: sessionId, message_id: messageId }),
});
if (!res.ok) throw new Error('Ошибка: ' + res.status);
removeTyping();
@@ -303,6 +334,8 @@ export async function reloadChatFromServer(id) {
m.image_prompt,
m.image_path ? `/static/${m.image_path}` : null,
m.id,
m.image_prompt_alt,
m.image_path_alt ? `/static/${m.image_path_alt}` : null,
);
});
}
@@ -344,8 +377,12 @@ async function consumeStream(res) {
if (data.image_generating && bubble) {
bubble.classList.remove('typing-active');
const wrapper = bubble.parentElement;
if (data.image_prompt && !wrapper.querySelector('.image-prompt-block')) {
wrapper.appendChild(createImagePromptBlock(data.image_prompt));
if (data.image_prompt) {
ensureImagePromptBlocks(
wrapper,
data.image_prompt,
data.image_prompt_alt || null,
);
}
showImageGenerating(wrapper);
dom.messagesEl.scrollTop = dom.messagesEl.scrollHeight;
@@ -361,14 +398,15 @@ async function consumeStream(res) {
bubble.textContent = bubble.textContent.replace(IMAGE_PROMPT_RE, '').trim();
}
if (data.image_prompt && wrapper && !wrapper.querySelector('.image-prompt-block')) {
wrapper.appendChild(createImagePromptBlock(data.image_prompt));
if (data.image_prompt && wrapper) {
ensureImagePromptBlocks(
wrapper,
data.image_prompt,
data.image_prompt_alt || null,
);
}
if (data.image_path && wrapper) {
console.log('[image] appending', data.image_path, 'to', wrapper);
appendChatImage(wrapper, data.image_path);
} else {
console.log('[image] skip: image_path=', data.image_path, 'wrapper=', wrapper);
appendChatImage(wrapper, data.image_path, '');
}
if (data.image_error && wrapper) {
const err = document.createElement('div');
@@ -388,7 +426,15 @@ async function consumeStream(res) {
}
}
export function addMessage(role, content = '', imagePrompt = null, imagePath = null, messageId = null) {
export function addMessage(
role,
content = '',
imagePrompt = null,
imagePath = null,
messageId = null,
imagePromptAlt = null,
imagePathAlt = null,
) {
updateEmptyState();
const wrapper = document.createElement('div');
wrapper.className = `message ${role}`;
@@ -446,8 +492,9 @@ export function addMessage(role, content = '', imagePrompt = null, imagePath = n
wrapper.appendChild(translateBtn);
}
if (prompt) wrapper.appendChild(createImagePromptBlock(prompt));
if (imagePath) appendChatImage(wrapper, imagePath);
if (prompt) wrapper.appendChild(createImagePromptBlock(prompt, imagePromptAlt));
if (imagePath) appendChatImage(wrapper, imagePath, imagePathAlt ? 'Теги' : '');
if (imagePathAlt) appendChatImage(wrapper, imagePathAlt, 'Гибрид');
attachMessageActions(wrapper, messageId, role);
dom.messagesEl.appendChild(wrapper);
dom.messagesEl.scrollTop = dom.messagesEl.scrollHeight;
@@ -487,7 +534,7 @@ export async function sendMessage(text, isNarratorChoice = false) {
const res = await fetch('/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ message: text, session_id: sessionId, persona_id: currentPersona, is_narrator_choice: isNarratorChoice }),
body: JSON.stringify({ message: text, session_id: sessionId, is_narrator_choice: isNarratorChoice }),
});
if (!res.ok) throw new Error('Ошибка сервера: ' + res.status);
removeTyping();