159 lines
5.5 KiB
JavaScript
159 lines
5.5 KiB
JavaScript
export function parseImagePromptFromContent(content) {
|
|
if (!content || !content.includes('[IMAGE_PROMPT:')) return { text: content, prompt: null };
|
|
const match = content.match(/\[IMAGE_PROMPT:(.*?)\]/s);
|
|
const prompt = match ? match[1].trim() : null;
|
|
const text = content.replace(/\[IMAGE_PROMPT:.*?\]/gs, '').trim();
|
|
return { text, prompt };
|
|
}
|
|
|
|
export function splitSdPromptForCopy(fullPrompt) {
|
|
if (!fullPrompt) return '';
|
|
const marker = '\n\nNegative prompt:';
|
|
const i = fullPrompt.indexOf(marker);
|
|
return (i >= 0 ? fullPrompt.slice(0, i) : fullPrompt).trim();
|
|
}
|
|
|
|
export async function copyToClipboard(text) {
|
|
if (!text) return false;
|
|
try {
|
|
await navigator.clipboard.writeText(text);
|
|
return true;
|
|
} catch {
|
|
try {
|
|
const ta = document.createElement('textarea');
|
|
ta.value = text;
|
|
ta.setAttribute('readonly', '');
|
|
ta.style.cssText = 'position:fixed;left:-9999px;top:0';
|
|
document.body.appendChild(ta);
|
|
ta.select();
|
|
const ok = document.execCommand('copy');
|
|
document.body.removeChild(ta);
|
|
return ok;
|
|
} catch {
|
|
return false;
|
|
}
|
|
}
|
|
}
|
|
|
|
export const GENRE_LABELS = {
|
|
adventure: 'Приключение',
|
|
horror: 'Хоррор',
|
|
romance: 'Романтика',
|
|
slice_of_life: 'Повседневность',
|
|
fantasy: 'Фэнтези',
|
|
sci_fi: 'Sci-Fi',
|
|
};
|
|
|
|
export function initWizard(modalEl, { totalSteps, onStepChange, validateStep }) {
|
|
let step = 1;
|
|
const pages = modalEl.querySelectorAll('.wizard-page');
|
|
const dots = modalEl.querySelectorAll('.wizard-step-dot');
|
|
const prevBtn = modalEl.querySelector('[id$="Prev"]');
|
|
const nextBtn = modalEl.querySelector('[id$="Next"]');
|
|
const saveBtn = modalEl.querySelector(
|
|
'[id$="Save"], [id$="Confirm"], [id$="Create"], [id$="Import"]',
|
|
);
|
|
|
|
function render() {
|
|
pages.forEach(p => p.classList.toggle('active', Number(p.dataset.step) === step));
|
|
dots.forEach(d => {
|
|
const n = Number(d.dataset.step);
|
|
d.classList.toggle('active', n === step);
|
|
d.classList.toggle('done', n < step);
|
|
});
|
|
prevBtn?.classList.toggle('hidden', step <= 1);
|
|
nextBtn?.classList.toggle('hidden', step >= totalSteps);
|
|
saveBtn?.classList.toggle('hidden', step < totalSteps);
|
|
onStepChange?.(step);
|
|
}
|
|
|
|
async function goTo(next) {
|
|
if (next > step && validateStep) {
|
|
const ok = await Promise.resolve(validateStep(step));
|
|
if (!ok) return;
|
|
}
|
|
step = Math.max(1, Math.min(totalSteps, next));
|
|
render();
|
|
}
|
|
|
|
prevBtn?.addEventListener('click', () => { goTo(step - 1); });
|
|
nextBtn?.addEventListener('click', () => { goTo(step + 1); });
|
|
|
|
render();
|
|
|
|
return {
|
|
reset() { step = 1; render(); },
|
|
getStep: () => step,
|
|
goTo,
|
|
render,
|
|
};
|
|
}
|
|
|
|
export function bindGenreGrid(gridEl, selectedSet, onChange) {
|
|
gridEl.addEventListener('click', (e) => {
|
|
const btn = e.target.closest('.genre-btn');
|
|
if (!btn) return;
|
|
const genre = btn.dataset.genre;
|
|
if (selectedSet.has(genre)) {
|
|
selectedSet.delete(genre);
|
|
btn.classList.remove('selected');
|
|
} else {
|
|
selectedSet.add(genre);
|
|
btn.classList.add('selected');
|
|
}
|
|
onChange?.();
|
|
});
|
|
}
|
|
|
|
export function resetGenreGrid(gridEl, selectedSet) {
|
|
selectedSet.clear();
|
|
gridEl.querySelectorAll('.genre-btn').forEach(b => b.classList.remove('selected'));
|
|
}
|
|
|
|
export function getRpgSettingsFromDom(prefix = '') {
|
|
const id = (name) => document.getElementById(prefix + name);
|
|
return {
|
|
dice: id('settingDice')?.checked ?? true,
|
|
narrator: id('settingNarrator')?.checked ?? true,
|
|
quests: id('settingQuests')?.checked ?? true,
|
|
affinity: id('settingAffinity')?.checked ?? true,
|
|
choices: id('settingChoices')?.checked ?? true,
|
|
};
|
|
}
|
|
|
|
export function fillGreetingSelect(selectEl, firstMes, alternates = []) {
|
|
if (!selectEl) return;
|
|
selectEl.innerHTML = '';
|
|
const main = document.createElement('option');
|
|
main.value = '0';
|
|
const mainPreview = (firstMes || '').replace(/\s+/g, ' ').trim();
|
|
main.textContent = mainPreview
|
|
? `Основное: ${mainPreview.slice(0, 50)}${mainPreview.length > 50 ? '…' : ''}`
|
|
: 'Основное (first_mes)';
|
|
selectEl.appendChild(main);
|
|
alternates.forEach((text, i) => {
|
|
const opt = document.createElement('option');
|
|
opt.value = String(i + 1);
|
|
const preview = String(text).replace(/\s+/g, ' ').trim();
|
|
opt.textContent = `Альт. ${i + 1}: ${preview.slice(0, 50)}${preview.length > 50 ? '…' : ''}`;
|
|
selectEl.appendChild(opt);
|
|
});
|
|
}
|
|
|
|
export function getSelectedGreeting(selectEl, firstMes, alternates = []) {
|
|
const v = selectEl?.value ?? '0';
|
|
if (v === '0') return firstMes || '';
|
|
const idx = parseInt(v, 10) - 1;
|
|
return alternates[idx] ?? firstMes ?? '';
|
|
}
|
|
|
|
export function formatSessionDate(iso) {
|
|
if (!iso) return '';
|
|
const d = new Date(iso.includes('T') ? iso : iso.replace(' ', 'T') + 'Z');
|
|
if (Number.isNaN(d.getTime())) return '';
|
|
const now = new Date();
|
|
const sameDay = d.toDateString() === now.toDateString();
|
|
if (sameDay) return d.toLocaleTimeString('ru-RU', { hour: '2-digit', minute: '2-digit' });
|
|
return d.toLocaleDateString('ru-RU', { day: 'numeric', month: 'short' });
|
|
}
|