diff --git a/Assets/assistent.bundle.js b/Assets/assistent.bundle.js index 7af94e4..b4b9cfe 100644 --- a/Assets/assistent.bundle.js +++ b/Assets/assistent.bundle.js @@ -1,4 +1,59 @@ (() => { + // src/intent.js + function cyrTokenRe(alts) { + const boundary = "(^|[^0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451])"; + const end = "(?=$|[^0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451])"; + return new RegExp(`${boundary}(?:${alts})${end}`, "i"); + } + function userAsksNoGenerate(text) { + const t = String(text || "").trim(); + if (!t) { + return false; + } + if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) { + return true; + } + return cyrTokenRe( + "\u0437\u0430\u043F\u043E\u043C\u043D|\u0437\u0430\u043F\u043E\u043C\u043D\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u043C|\u0441\u043E\u0445\u0440\u0430\u043D\u0438|\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u043C|\u0448\u0430\u0431\u043B\u043E\u043D|\u0431\u0430\u0437\u043E\u0432(\u044B\u0439|\u043E\u0433\u043E|\u043E\u043C\u0443|\u044B\u043C|\u0430\u044F|\u0443\u044E|\u043E\u0435)?\\s+\u043F\u0440\u043E\u043C\u043F\u0442|\u043D\u0435\\s+\u0433\u0435\u043D\u0435\u0440\u0438\u0440[\u0430-\u044F\u0451]*|\u0431\u0435\u0437\\s+\u0433\u0435\u043D\u0435\u0440\u0430\u0446[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0442\u043E\u043B\u044C\u043A\u043E\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043F\u043E\u043A\u0430\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u0440\u0438\u0441\u0443\u0439|\u043D\u0435\\s+\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0442\u043E\u043B\u044C\u043A\u043E\\s+(\u043E\u0442\u0432\u0435\u0442\u044C|\u0441\u043A\u0430\u0436\u0438|\u043E\u0431\u044A\u044F\u0441\u043D\u0438)" + ).test(t); + } + function userAsksGenerate(text) { + const t = String(text || "").trim(); + if (!t || userAsksNoGenerate(t)) { + return false; + } + if (/\b(generat(e|ion)|draw|render|make\s+(an?\s+)?image|run\s+generate)\b/i.test(t)) { + return true; + } + return cyrTokenRe( + "\u0441\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043D\u0430\u0440\u0438\u0441\u0443\u0439\u0442\u0435|\u043D\u0430\u0440\u0438\u0441\u0443\u0435\u043C|\u0437\u0430\u043F\u0443\u0441\u0442\u0438\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0441\u0434\u0435\u043B\u0430(\u0439|\u0435\u043C|\u0439\u0442\u0435)\\s+(\u043A\u0430\u0434\u0440|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*)" + ).test(t); + } + function generateFlagOn(patch) { + if (!patch || typeof patch !== "object") { + return false; + } + const g = patch.generate; + if (g === true || g === 1) { + return true; + } + if (typeof g === "string" && /^(true|1|yes|on)$/i.test(g.trim())) { + return true; + } + return Array.isArray(patch.actions) && patch.actions.map(String).includes("generate"); + } + function resolveTurnIntent(patch, userText, opts = {}) { + const vetoed = !opts.machineTurn && userAsksNoGenerate(userText); + const modelAsked = generateFlagOn(patch); + const hasPrompt = !!(String(patch?.prompt || "").trim() || String(opts.sessionPrompt || "").trim()); + const userAsked = !opts.machineTurn && userAsksGenerate(userText) && (modelAsked || hasPrompt); + const generate = !vetoed && !opts.fromAutoCritique && (userAsked || !!opts.userWantsGenerate); + const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); + const look = !!(hasLook && !vetoed && !generate); + const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : typeof patch?.ask === "string" && patch.ask ? [patch.ask] : []; + return { generate, look, vetoed, ask, modelAsked }; + } + // src/api.js function createRequest() { return function request(name, body) { @@ -86,7 +141,7 @@ function has(obj, key) { return obj[key] !== void 0 && obj[key] !== null; } - function generateFlagOn(obj) { + function generateFlagOn2(obj) { if (!obj || typeof obj !== "object") { return false; } @@ -111,7 +166,7 @@ return patch; } const acts = Array.isArray(patch.actions) ? patch.actions.map(String) : []; - if (generateFlagOn(patch) || acts.includes("generate")) { + if (generateFlagOn2(patch) || acts.includes("generate")) { patch.generate = true; } if (typeof patch.ask === "string") { @@ -181,11 +236,18 @@ } return { prose: text, patch: null }; } + function visibleProse(text) { + const { prose, patch } = extractPatch(text); + if (patch) { + return prose || ""; + } + return String(text || ""); + } function isTerminalStreamPatch(obj) { if (!obj || typeof obj !== "object") { return false; } - if (generateFlagOn(obj)) { + if (generateFlagOn2(obj)) { return true; } if (Array.isArray(obj.ask) && obj.ask.length) { @@ -215,7 +277,8 @@ SA2.isTerminalStreamPatch = isTerminalStreamPatch; SA2.normalizePatch = normalizePatch; SA2.extractPatch = extractPatch; - SA2.generateFlagOn = generateFlagOn; + SA2.generateFlagOn = generateFlagOn2; + SA2.visibleProse = visibleProse; } // src/persist.js @@ -678,16 +741,23 @@ session_exact: extras.sessionExact || null }; } - function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) { + function resolveTurnIntent2(patch, userText, { + vetoFn, + askGenerateFn, + fromAutoCritique, + sessionPrompt, + userWantsGenerate + } = {}) { const delta = normalizeDelta(patch) || {}; const vetoed = typeof vetoFn === "function" ? !!vetoFn(userText) : false; const modelAsked = patchWantsGenerate(delta); - const userAsked = typeof askGenerateFn === "function" && !!askGenerateFn(userText) && !!(modelAsked || String(delta.prompt || "").trim()); - const generate = !vetoed && !fromAutoCritique && (modelAsked || userAsked); + const hasPrompt = !!(String(delta.prompt || "").trim() || String(sessionPrompt || "").trim()); + const userAsked = typeof askGenerateFn === "function" && !!askGenerateFn(userText) && (modelAsked || hasPrompt); + const generate = !vetoed && !fromAutoCritique && (userAsked || !!userWantsGenerate); const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null; const look = !!(hasLook && !generate && !vetoed); const ask = patchAskList(delta); - return { generate, look, vetoed, ask }; + return { generate, look, vetoed, ask, modelAsked }; } var EXACT_GENERATE_PARAM_KEYS = ["steps", "cfg", "sigma_shift"]; function resolveExactProfileDefaults({ exact, profiles, profileName } = {}) { @@ -749,7 +819,7 @@ toPersistParams, compactContext, fullSettingsDump, - resolveTurnIntent, + resolveTurnIntent: resolveTurnIntent2, resolveExactProfileDefaults, mergeExactParamsForGenerate, EXACT_GENERATE_PARAM_KEYS, @@ -1218,6 +1288,65 @@ }; } + // src/aspect.js + var SWARM_ASPECT_REF_512 = { + "1:1": [512, 512], + "4:3": [576, 448], + "3:2": [608, 416], + "8:5": [608, 384], + "16:9": [672, 384], + "21:9": [768, 320], + "2:3": [416, 608], + "5:8": [384, 608], + "9:16": [384, 672], + "9:21": [320, 768] + }; + var ASPECT_ALIASES = { + "2.35:1": "21:9" + }; + var KREA_EXTRA = { + "4:5": [928, 1152] + }; + function roundTo16(n) { + return Math.round(n / 16) * 16; + } + function swarmSizeFromRef(aspectKey, sideLen = 1024) { + const key = ASPECT_ALIASES[aspectKey] || aspectKey; + if (KREA_EXTRA[key]) { + return [...KREA_EXTRA[key]]; + } + const ref = SWARM_ASPECT_REF_512[key]; + if (!ref) { + return null; + } + const scale = sideLen / 512; + return [roundTo16(ref[0] * scale), roundTo16(ref[1] * scale)]; + } + function buildDefaultAspectTable(sideLen = 1024) { + const table = {}; + for (const key of Object.keys(SWARM_ASPECT_REF_512)) { + table[key] = swarmSizeFromRef(key, sideLen); + } + table["2.35:1"] = swarmSizeFromRef("21:9", sideLen); + for (const [key, size] of Object.entries(KREA_EXTRA)) { + table[key] = [...size]; + } + return table; + } + var DEFAULT_ASPECT_TABLE = buildDefaultAspectTable(1024); + function applyAspectTableFromObject(obj) { + if (!obj || typeof obj !== "object") { + return false; + } + const next = {}; + for (const [k, v] of Object.entries(obj)) { + if (Array.isArray(v) && v.length >= 2) { + next[k] = [Number(v[0]), Number(v[1])]; + } + } + return Object.keys(next).length ? next : false; + } + // src/app.js (function() { const LS_BASE = "swarm_assistent_base_url"; @@ -1250,16 +1379,7 @@ let COMPRESS_AT = 0.7; let CHARS_PER_TOKEN = 3.2; let COMPRESS_AUTO = true; - let ASPECT_TABLE = { - "1:1": [1024, 1024], - "4:3": [1184, 896], - "3:2": [1248, 832], - "16:9": [1376, 768], - "2.35:1": [1568, 672], - "4:5": [928, 1152], - "2:3": [832, 1248], - "9:16": [768, 1376] - }; + let ASPECT_TABLE = { ...DEFAULT_ASPECT_TABLE }; let PACK_ALIASES = { ordinary: "ordinary", combine: "ordinary", @@ -1337,6 +1457,8 @@ lastUserControlIntent: false, lastPatch: null, pendingSilentGen: false, + /** This user turn asked to draw — hops inherit it; model generate:true does not. */ + turnUserWantsGenerate: false, pendingPromptEnMerge: null, enabledSkills: [], kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } }, @@ -1847,6 +1969,7 @@ let text = String(raw || "").replace(/\r\n/g, "\n"); text = text.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, "\n"); text = text.replace(/(?:^|\n)\s*JSON\s*Patch\s*:?\s*(?=\n|$)/gi, "\n"); + text = text.replace(/```(?:json)?\s*[\s\S]*?```/gi, ""); text = text.replace(/\n{3,}/g, "\n\n").trim(); if (!text) { return ""; @@ -1927,10 +2050,39 @@ if (!el) { return; } - el.value = value; + const next = value == null ? "" : String(value); + if (el.value === next) { + return; + } + el.value = next; + if ((state._quietParamApply || 0) > 0) { + return; + } el.dispatchEvent(new Event("input", { bubbles: true })); el.dispatchEvent(new Event("change", { bubbles: true })); } + const QUICK_PARAM_KEYS = /* @__PURE__ */ new Set([ + "aspect", + "width", + "height", + "steps", + "cfg", + "sigma_shift", + "seed", + "vary", + "lock_seed", + "sampler", + "scheduler", + "batch", + "images" + ]); + function isParamOnlyQuickPatch(patch) { + if (!patch || typeof patch !== "object") { + return false; + } + const keys = Object.keys(patch).filter((k) => patch[k] != null && k !== "notes"); + return keys.length > 0 && keys.every((k) => QUICK_PARAM_KEYS.has(k)); + } function liveNegativePrompt() { return String(val("input_negativeprompt") || val("alt_negativeprompt_textbox") || "").trim(); } @@ -1976,7 +2128,7 @@ } return false; } - function cyrTokenRe(alts) { + function cyrTokenRe2(alts) { const boundary = "(^|[^0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451])"; const end = "(?=$|[^0-9A-Za-z_\u0410-\u042F\u0430-\u044F\u0401\u0451])"; return new RegExp(`${boundary}(?:${alts})${end}`, "i"); @@ -2004,10 +2156,10 @@ if (named) { return normalizeAspect(named[1]); } - if (cyrTokenRe("\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*").test(t) || /\b(portrait|vertical)\b/i.test(t)) { + if (cyrTokenRe2("\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*").test(t) || /\b(portrait|vertical)\b/i.test(t)) { return normalizeAspect("9:16") || normalizeAspect("2:3"); } - if (cyrTokenRe("\u0430\u043B\u044C\u0431\u043E\u043C|\u0433\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B[\u0430-\u044F\u0451]*").test(t) || /\b(landscape|horizontal|widescreen)\b/i.test(t)) { + if (cyrTokenRe2("\u0430\u043B\u044C\u0431\u043E\u043C|\u0433\u043E\u0440\u0438\u0437\u043E\u043D\u0442\u0430\u043B[\u0430-\u044F\u0451]*").test(t) || /\b(landscape|horizontal|widescreen)\b/i.test(t)) { return normalizeAspect("16:9"); } return null; @@ -2024,7 +2176,7 @@ if (/\b(horny|controls?|preference[_\s-]?bias)\b/i.test(t)) { return true; } - return cyrTokenRe("\u0445\u043E\u0440\u043D\u0438|\u043E\u0441\u0442\u044B\u043D\u044C|\u0441\u043B\u0430\u0439\u0434\u0435\u0440").test(t) || /\/\s*(остынь|ostyn|horny-game)/i.test(t) || /слайдер\s*вкус|вкус\s*(на|в)\s*\d|поставь\s*вкус|крутани\s*вкус/i.test(t); + return cyrTokenRe2("\u0445\u043E\u0440\u043D\u0438|\u043E\u0441\u0442\u044B\u043D\u044C|\u0441\u043B\u0430\u0439\u0434\u0435\u0440").test(t) || /\/\s*(остынь|ostyn|horny-game)/i.test(t) || /слайдер\s*вкус|вкус\s*(на|в)\s*\d|поставь\s*вкус|крутани\s*вкус/i.test(t); } function patchLooksLikeGeneration(patch) { if (!patch || typeof patch !== "object") { @@ -2072,7 +2224,7 @@ if (/\b(steps?|cfg|seed|sigma|aspect|resolution|batch|turbo|raw)\b/i.test(t)) { return true; } - return cyrTokenRe( + return cyrTokenRe2( "\u0440\u0430\u0437\u043C\u0435\u0440|\u0448\u0438\u0440\u0438\u043D[\u0430-\u044F\u0451]*|\u0432\u044B\u0441\u043E\u0442[\u0430-\u044F\u0451]*|\u0441\u043E\u043E\u0442\u043D\u043E\u0448\u0435\u043D[\u0430-\u044F\u0451]*|\u0442\u0443\u0440\u0431\u043E|\u043F\u043E\u0440\u0442\u0440\u0435\u0442|\u0432\u0435\u0440\u0442\u0438\u043A\u0430\u043B[\u0430-\u044F\u0451]*|\u0448\u0430\u0433|\u0448\u0430\u0433\u043E\u043C|\u0448\u0430\u0433\u0430\u043C\u0438|\u0448\u0430\u0433\u043E\u0432|\u0448\u0430\u0433\u0430" ).test(t); } @@ -2094,7 +2246,7 @@ if (/^(давай\s+дальше|продолжай|продолжим|go\s+on|continue|keep\s+going|next(\s+one)?|next\s+frame)([!.…\s]|$)/i.test(t)) { return true; } - return cyrTokenRe( + return cyrTokenRe2( "\u0434\u0430\u0432\u0430\u0439\\s+\u0434\u0430\u043B\u044C\u0448\u0435|\u0441\u043B\u0435\u0434\u0443\u044E\u0449(\u0438\u0439|\u0430\u044F|\u0435\u0435|\u0443\u044E)\\s+\u043A\u0430\u0434\u0440|\u0435\u0449\u0451\\s+\u043A\u0430\u0434\u0440|\u0435\u0449\u0435\\s+\u043A\u0430\u0434\u0440|\u043A\u0430\u0434\u0440\\s*\u2116?\\s*\\d+|\u0441\u0434\u0435\u043B\u0430\u0439\\s+\u0441\u043B\u0435\u0434\u0443\u044E\u0449" ).test(t); } @@ -2212,7 +2364,7 @@ ${patch.prompt}`; aspect: effective.aspect || base.aspect }; } - function userAsksNoGenerate(text) { + function userAsksNoGenerate2(text) { const t = String(text || "").trim(); if (!t) { return false; @@ -2220,7 +2372,7 @@ ${patch.prompt}`; if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) { return true; } - return cyrTokenRe( + return cyrTokenRe2( "\u0437\u0430\u043F\u043E\u043C\u043D|\u0437\u0430\u043F\u043E\u043C\u043D\u0438|\u0437\u0430\u043F\u043E\u043C\u043D\u0438\u043C|\u0441\u043E\u0445\u0440\u0430\u043D\u0438|\u0441\u043E\u0445\u0440\u0430\u043D\u0438\u043C|\u0448\u0430\u0431\u043B\u043E\u043D|\u0431\u0430\u0437\u043E\u0432(\u044B\u0439|\u043E\u0433\u043E|\u043E\u043C\u0443|\u044B\u043C|\u0430\u044F|\u0443\u044E|\u043E\u0435)?\\s+\u043F\u0440\u043E\u043C\u043F\u0442|\u043D\u0435\\s+\u0433\u0435\u043D\u0435\u0440\u0438\u0440[\u0430-\u044F\u0451]*|\u0431\u0435\u0437\\s+\u0433\u0435\u043D\u0435\u0440\u0430\u0446[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u043D\u0430\u0434\u043E\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0442\u043E\u043B\u044C\u043A\u043E\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043F\u043E\u043A\u0430\\s+\u0437\u0430\u043F\u043E\u043C\u043D[\u0430-\u044F\u0451]*|\u043D\u0435\\s+\u0440\u0438\u0441\u0443\u0439|\u043D\u0435\\s+\u0437\u0430\u043F\u0443\u0441\u043A\u0430\u0439\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*" ).test(t); } @@ -2232,50 +2384,53 @@ ${patch.prompt}`; if (/\b(look\s+at|critique|criticize|describe\s+(this|the|ref|image)|what\s+do\s+you\s+see)\b/i.test(t)) { return true; } - if (cyrTokenRe("\u043A\u0440\u0438\u0442\u0438\u043A[\u0430-\u044F\u0451]*|\u0447\u0442\u043E\\s+\u043D\u0435\\s+\u0442\u0430\u043A|\u0440\u0430\u0437\u0431\u0435\u0440\u0438").test(t)) { + if (cyrTokenRe2("\u043A\u0440\u0438\u0442\u0438\u043A[\u0430-\u044F\u0451]*|\u0447\u0442\u043E\\s+\u043D\u0435\\s+\u0442\u0430\u043A|\u0440\u0430\u0437\u0431\u0435\u0440\u0438").test(t)) { return true; } - if (cyrTokenRe("\u043E\u043F\u0438\u0448\u0438\\s+(\u044D\u0442\u043E|\u044D\u0442\u0443|\u0440\u0435\u0444|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u043A\u0430\u0434\u0440|\u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442|\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441)").test(t)) { + if (cyrTokenRe2("\u043E\u043F\u0438\u0448\u0438\\s+(\u044D\u0442\u043E|\u044D\u0442\u0443|\u0440\u0435\u0444|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u043A\u0430\u0434\u0440|\u0440\u0435\u0437\u0443\u043B\u044C\u0442\u0430\u0442|\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441)").test(t)) { return true; } return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t); } - function userAsksGenerate(text) { + function userAsksGenerate2(text) { if (window.SA && typeof SA.userAsksGenerate === "function") { return SA.userAsksGenerate(text); } const t = String(text || "").trim(); - if (!t || userAsksNoGenerate(t)) { + if (!t || userAsksNoGenerate2(t)) { return false; } if (/\b(generat(e|ion)|draw|render|make\s+(an?\s+)?image|run\s+generate)\b/i.test(t)) { return true; } - return cyrTokenRe( - "\u0441\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043D\u0430\u0440\u0438\u0441\u0443\u0439\u0442\u0435|\u0437\u0430\u043F\u0443\u0441\u0442\u0438\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0441\u0434\u0435\u043B\u0430\u0439\\s+(\u043A\u0430\u0434\u0440|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*)" + return cyrTokenRe2( + "\u0441\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u043D\u0430\u0440\u0438\u0441\u0443\u0439|\u043D\u0430\u0440\u0438\u0441\u0443\u0439\u0442\u0435|\u043D\u0430\u0440\u0438\u0441\u0443\u0435\u043C|\u0437\u0430\u043F\u0443\u0441\u0442\u0438\\s+\u0433\u0435\u043D\u0435\u0440[\u0430-\u044F\u0451]*|\u0441\u0434\u0435\u043B\u0430(\u0439|\u0435\u043C|\u0439\u0442\u0435)\\s+(\u043A\u0430\u0434\u0440|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*)" ).test(t); } function packWantsVision(pack) { const p = String(pack || ""); return p === "critique_image" || p === "describe_ref" || p === "compose_scene" || p === "inpaint_edit"; } - function resolveTurnIntent2(patch, userText, opts = {}) { + function resolveTurnIntent3(patch, userText, opts = {}) { const S = window.SA && window.SA.session; if (S && typeof S.resolveTurnIntent === "function") { return S.resolveTurnIntent(patch, userText, { - vetoFn: userAsksNoGenerate, - askGenerateFn: userAsksGenerate, - fromAutoCritique: !!opts.fromAutoCritique + vetoFn: userAsksNoGenerate2, + askGenerateFn: userAsksGenerate2, + fromAutoCritique: !!opts.fromAutoCritique, + sessionPrompt: opts.sessionPrompt || "", + userWantsGenerate: !!opts.userWantsGenerate }); } - const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText); + const vetoed = !isMachineTurn(opts) && userAsksNoGenerate2(userText); const modelAsked = !!(patch && (patch.generate === true || patch.generate === 1 || typeof patch.generate === "string" && /^(true|1|yes|on)$/i.test(patch.generate) || Array.isArray(patch.actions) && patch.actions.map(String).includes("generate"))); - const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText) && !!(modelAsked || String(patch?.prompt || "").trim()); - const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked); + const hasPrompt = !!(String(patch?.prompt || "").trim() || String(opts.sessionPrompt || "").trim()); + const userAsked = !isMachineTurn(opts) && userAsksGenerate2(userText) && (modelAsked || hasPrompt); + const generate = !vetoed && !opts.fromAutoCritique && (userAsked || !!opts.userWantsGenerate); const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); const look = !!(hasLook && !vetoed && !generate); const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : typeof patch?.ask === "string" && patch.ask ? [patch.ask] : []; - return { generate, look, vetoed, ask }; + return { generate, look, vetoed, ask, modelAsked }; } function stripGenerateAction(patch) { if (!patch || typeof patch !== "object") { @@ -2354,31 +2509,53 @@ ${patch.prompt}`; badge.title = `\u0420\u0435\u0436\u0438\u043C: ${pack}`; badge.classList.toggle("sa-mode-hot", pack === "critique_image" || pack === "inpaint_edit"); } - function syncLiveParamsBar() { - const el = $2("sa_live_params"); - if (!el) { - return; - } + function formatLiveParamsLine() { const w = parseInt(val("input_width") || "0", 10) || null; const h = parseInt(val("input_height") || "0", 10) || null; const aspect = guessAspectFromSize(w, h) || "\u2014"; const steps = val("input_steps") || "\u2014"; const cfg = val("input_cfgscale") || val("input_cfg") || "\u2014"; + const sigma = val("input_sigmashift") || ""; const seed = val("input_seed") || "\u2014"; const profile = detectKreaProfileName2(); - el.textContent = `${aspect} \xB7 ${w || "?"}\xD7${h || "?"} \xB7 steps ${steps} \xB7 cfg ${cfg} \xB7 ${profile} \xB7 seed ${seed}`; + const batch = val("input_images") || val("input_batchsize") || ""; + const sampler = val("input_sampler") || ""; + const scheduler = val("input_scheduler") || ""; + const parts = [ + aspect, + `${w || "?"}\xD7${h || "?"}`, + `steps ${steps}`, + `cfg ${cfg}` + ]; + if (sigma) { + parts.push(`\u03C3 ${sigma}`); + } + parts.push(profile, `seed ${seed}`); + if (batch && batch !== "1") { + parts.push(`\xD7${batch}`); + } + if (sampler) { + parts.push(sampler); + } + if (scheduler) { + parts.push(scheduler); + } + return parts.join(" \xB7 "); + } + function syncLiveParamsBar() { + const line = formatLiveParamsLine(); + const boardEl = $2("sa_live_params"); + const composerEl = $2("sa_composer_params"); + if (boardEl) { + boardEl.textContent = line; + } + if (composerEl) { + composerEl.textContent = line; + } } function applyAspectTableFrom(obj) { - if (!obj || typeof obj !== "object") { - return false; - } - const next = {}; - for (const [k, v] of Object.entries(obj)) { - if (Array.isArray(v) && v.length >= 2) { - next[k] = [Number(v[0]), Number(v[1])]; - } - } - if (!Object.keys(next).length) { + const next = applyAspectTableFromObject(obj); + if (!next) { return false; } ASPECT_TABLE = next; @@ -3436,7 +3613,7 @@ ${patch.prompt}`; actions.appendChild(toSession); const genBtn = document.createElement("button"); genBtn.type = "button"; - genBtn.className = "basic-button sa-btn-gen"; + genBtn.className = "basic-button sa-btn-gen sa-btn-gen-primary"; genBtn.textContent = "\u0421\u0433\u0435\u043D\u0435\u0440\u0438\u0440\u043E\u0432\u0430\u0442\u044C"; genBtn.addEventListener("click", async () => { if (isGenerateUnavailable()) { @@ -3449,6 +3626,9 @@ ${patch.prompt}`; state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch); } await pushSessionToSwarm(state.chatSession); + if (typeof appendSystemNote === "function") { + appendSystemNote("\u0417\u0430\u043F\u0443\u0441\u043A\u0430\u044E Generate"); + } await runGenerateFromPatch({ ...patch, actions: ["generate"] }, { force: true, fromSession: true }); }); actions.appendChild(genBtn); @@ -3582,6 +3762,22 @@ ${patch.prompt}`; }); return el; } + function genLightboxList() { + const fromResults = (state.genResults || []).filter((r) => r && r.src); + if (fromResults.length) { + return fromResults; + } + const slot = generateSlot(); + if (slot?.src) { + return [{ + id: slot.id || GEN_ID, + label: slot.label || "Generate", + src: slot.src, + patch: state.lastPatch || null + }]; + } + return []; + } function ensureGenLightbox() { let root = $2("sa_gen_lightbox"); if (root) { @@ -3646,7 +3842,7 @@ ${patch.prompt}`; return root; } function currentLightboxRow() { - const list = (state.genResults || []).filter((r) => r.src); + const list = genLightboxList(); if (!list.length || state.lightboxIndex < 0) { return null; } @@ -3654,7 +3850,7 @@ ${patch.prompt}`; } function syncGenLightbox() { const root = ensureGenLightbox(); - const list = (state.genResults || []).filter((r) => r.src); + const list = genLightboxList(); const row = list[state.lightboxIndex]; if (!row) { root.hidden = true; @@ -3676,7 +3872,7 @@ ${patch.prompt}`; } } function openGenLightbox(id) { - const list = (state.genResults || []).filter((r) => r.src); + const list = genLightboxList(); let idx = list.findIndex((r) => r.id === id); if (idx < 0) { idx = 0; @@ -3697,7 +3893,7 @@ ${patch.prompt}`; } } function stepGenLightbox(delta) { - const list = (state.genResults || []).filter((r) => r.src); + const list = genLightboxList(); if (list.length < 2) { return; } @@ -3817,10 +4013,22 @@ ${patch.prompt}`; busy.hidden = !(slot.type === "generate" && (state.generating || state.busyPhase === "generating")); busy.innerHTML = ''; el.appendChild(busy); - el.addEventListener("click", () => { + el.addEventListener("click", (e) => { + if (slot.type === "generate" && slot.src && e.target?.tagName === "IMG") { + openGenLightbox(slot.id || GEN_ID); + return; + } state.selectedSlotId = slot.id; renderBoard(); }); + if (slot.type === "generate") { + el.addEventListener("dblclick", (e) => { + if (slot.src && e.target?.tagName === "IMG") { + e.preventDefault(); + openGenLightbox(slot.id || GEN_ID); + } + }); + } el.addEventListener("dragover", (e) => { e.preventDefault(); e.stopPropagation(); @@ -5086,6 +5294,13 @@ ${patch.prompt}`; } return { prose: text || "", patch: null }; } + function visibleAssistantProse(text) { + if (window.SA && typeof SA.visibleProse === "function") { + return SA.visibleProse(text); + } + const { prose, patch } = extractPatch2(text); + return patch ? prose || "" : String(text || ""); + } function normalizeAspect(raw) { if (raw == null) { return null; @@ -5207,22 +5422,22 @@ ${patch.prompt}`; if (userTextMentionsParams(t) || parseAspectFromUserText(t)) { return cur === "critique_image" || cur === "describe_ref" ? "ordinary" : "fix_params"; } - if (cyrTokenRe("\u043F\u043E\u043F\u0440\u0430\u0432\u044C|\u0438\u0441\u043F\u0440\u0430\u0432\u044C|\u043F\u0435\u0440\u0435\u043F\u0438\u0448\u0438|\u0443\u043B\u0443\u0447\u0448\u0438").test(t) || /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) { + if (cyrTokenRe2("\u043F\u043E\u043F\u0440\u0430\u0432\u044C|\u0438\u0441\u043F\u0440\u0430\u0432\u044C|\u043F\u0435\u0440\u0435\u043F\u0438\u0448\u0438|\u0443\u043B\u0443\u0447\u0448\u0438").test(t) || /\b(fix\s+it|make\s+it\s+better|rewrite)\b/i.test(t)) { return "write_prompt"; } - if (cyrTokenRe("\u043E\u043F\u0438\u0448\u0438\\s+(\u0440\u0435\u0444|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*|\u044D\u0442\u043E\u0442|\u044D\u0442\u0443|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441)").test(t) || /\b(prompt\s+from\s+image|describe\s+(this|the|ref|image)|reverse\s*prompt)\b/i.test(t) || /опиши\s+(этот|эту|картинк|референс)/i.test(t)) { + if (cyrTokenRe2("\u043E\u043F\u0438\u0448\u0438\\s+(\u0440\u0435\u0444|\u0438\u0437\u043E\u0431\u0440\u0430\u0436[\u0430-\u044F\u0451]*|\u044D\u0442\u043E\u0442|\u044D\u0442\u0443|\u043A\u0430\u0440\u0442\u0438\u043D\u043A[\u0430-\u044F\u0451]*|\u0440\u0435\u0444\u0435\u0440\u0435\u043D\u0441)").test(t) || /\b(prompt\s+from\s+image|describe\s+(this|the|ref|image)|reverse\s*prompt)\b/i.test(t) || /опиши\s+(этот|эту|картинк|референс)/i.test(t)) { return "describe_ref"; } - if (/\b(critique|criticize)\b/i.test(t) || cyrTokenRe("\u043A\u0440\u0438\u0442\u0438\u043A[\u0430-\u044F\u0451]*|\u0447\u0442\u043E\\s+\u043D\u0435\\s+\u0442\u0430\u043A|\u0440\u0430\u0437\u0431\u0435\u0440\u0438").test(t) || /(?:^|[^а-яёa-z0-9_])(посмотри|смотри)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген)/i.test(t)) { + if (/\b(critique|criticize)\b/i.test(t) || cyrTokenRe2("\u043A\u0440\u0438\u0442\u0438\u043A[\u0430-\u044F\u0451]*|\u0447\u0442\u043E\\s+\u043D\u0435\\s+\u0442\u0430\u043A|\u0440\u0430\u0437\u0431\u0435\u0440\u0438").test(t) || /(?:^|[^а-яёa-z0-9_])(посмотри|смотри)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген)/i.test(t)) { return "critique_image"; } - if (/\b(inpaint|mask|img2img)\b/i.test(t) || cyrTokenRe("\u0437\u0430\u043C\u0430\u0436\u044C|\u0437\u0430\u043A\u0440\u0430\u0441\u044C|\u0440\u0443\u043A\u0438|\u043B\u0438\u0446\u043E|\u043C\u0430\u0441\u043A[\u0430-\u044F\u0451]*").test(t) || /init\s*image/i.test(t)) { + if (/\b(inpaint|mask|img2img)\b/i.test(t) || cyrTokenRe2("\u0437\u0430\u043C\u0430\u0436\u044C|\u0437\u0430\u043A\u0440\u0430\u0441\u044C|\u0440\u0443\u043A\u0438|\u043B\u0438\u0446\u043E|\u043C\u0430\u0441\u043A[\u0430-\u044F\u0451]*").test(t) || /init\s*image/i.test(t)) { return "inpaint_edit"; } if (cur === "critique_image") { return "ordinary"; } - if (/\b(moodboard|compose|scene)\b/i.test(t) || cyrTokenRe("\u0441\u0446\u0435\u043D[\u0430-\u044F\u0451]*|\u0430\u0442\u043C\u043E\u0441\u0444\u0435\u0440[\u0430-\u044F\u0451]*|\u043C\u0438\u0437\u0430\u043D\u0441\u0446\u0435\u043D[\u0430-\u044F\u0451]*").test(t)) { + if (/\b(moodboard|compose|scene)\b/i.test(t) || cyrTokenRe2("\u0441\u0446\u0435\u043D[\u0430-\u044F\u0451]*|\u0430\u0442\u043C\u043E\u0441\u0444\u0435\u0440[\u0430-\u044F\u0451]*|\u043C\u0438\u0437\u0430\u043D\u0441\u0446\u0435\u043D[\u0430-\u044F\u0451]*").test(t)) { return "compose_scene"; } return "write_prompt"; @@ -5700,7 +5915,7 @@ ${patch.prompt}`; } const persona = $2("sa_persona")?.value || localStorage.getItem(LS_PERSONA) || "neutral"; const pack = $2("sa_pack")?.value || defaultPackId(); - const prose = typeof extractPatch2 === "function" ? extractPatch2(text).prose || text : text; + const prose = visibleAssistantProse(text); if (state.streamEl) { finalizeStreamMessage(text, []); } else { @@ -6227,7 +6442,7 @@ ${patch.prompt}`; const { prose, patch: extracted } = role === "assistant" ? extractPatch2(text) : { prose: text, patch: null }; const finalPatch = patch || extracted; if (role === "assistant") { - setAssistantBody(div, prose || text || ""); + setAssistantBody(div, finalPatch ? prose || "" : prose || text || ""); } else { div.textContent = prose || text || ""; } @@ -6330,7 +6545,7 @@ ${patch.prompt}`; if (streamHasClosedPatchFence(state.streamText)) { state.streamText = trimToClosedPatchFence(state.streamText); state.streamFenceDone = true; - setAssistantBody(state.streamEl, state.streamText, { live: true }); + setAssistantBody(state.streamEl, visibleAssistantProse(state.streamText), { live: true }); scrollMessagesToBottom(); const fn = state.onClosedTerminalFence; state.onClosedTerminalFence = null; @@ -6364,7 +6579,7 @@ ${patch.prompt}`; el.classList.remove("sa-streaming", "sa-typing"); mountAssistantMeta(el, meta || void 0); const { prose, patch } = extractPatch2(fullReply); - setAssistantBody(el, prose || fullReply || ""); + setAssistantBody(el, patch ? prose || "" : prose || fullReply || ""); el.querySelectorAll(".sa-patch, .sa-civitai-list").forEach((n) => n.remove()); if (patch) { const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen; @@ -8437,8 +8652,32 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (effective && act && typeof act.noteModelCommands === "function") { act.noteModelCommands(effective); } + const sessionPrompt = String(state.chatSession?.gen?.prompt || "").trim(); + const userWantsGenerate = !!opts.userWantsGenerate || !!state.turnUserWantsGenerate; + let intent = resolveTurnIntent3(effective, opts.userText || "", { + ...opts, + sessionPrompt, + userWantsGenerate + }); + if (userWantsGenerate && !intent.vetoed && !fromAutoCritique) { + intent = { ...intent, generate: true }; + } + if (!effective && intent.generate && sessionPrompt) { + effective = { generate: true }; + } const promptChanged = !!(effective && String(effective.prompt || "").trim()); - if (effective && S) { + if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) { + if (typeof doInterruptNow === "function") doInterruptNow(); + } + if (effective) { + if (intent.generate) { + const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : []; + effective = { ...effective, actions: acts.includes("generate") ? acts : acts.concat("generate"), generate: true }; + } + if (!intent.look && typeof stripLookAt === "function") effective = stripLookAt(effective); + if (typeof rememberLastPatch === "function") rememberLastPatch(effective); + } + if (intent.generate && effective && S) { state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); activityDone("delta", { kind: "delta", @@ -8453,28 +8692,6 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; } } catch (e) { } - if (typeof rememberLastPatch === "function") rememberLastPatch(effective); - } - if (Array.isArray(effective?.actions) && effective.actions.map(String).includes("interrupt")) { - if (typeof doInterruptNow === "function") doInterruptNow(); - } - let intent = resolveTurnIntent2(effective, opts.userText || "", opts); - if (opts.userWantsGenerate && effective && !intent.vetoed && !fromAutoCritique) { - intent = { ...intent, generate: true }; - } - if (effective) { - if (intent.generate) { - const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : []; - effective = { ...effective, actions: acts.includes("generate") ? acts : acts.concat("generate"), generate: true }; - } else if (typeof stripGenerateAction === "function") { - effective = stripGenerateAction(effective); - if (effective && effective.generate) { - effective = { ...effective }; - delete effective.generate; - } - } - if (!intent.look && typeof stripLookAt === "function") effective = stripLookAt(effective); - if (typeof rememberLastPatch === "function") rememberLastPatch(effective); } if (intent.vetoed) { state.pendingSilentGen = false; @@ -8583,44 +8800,83 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (typeof maybeAutoVisionLook === "function") await maybeAutoVisionLook(srcOut); } } else if (effective) { - if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); - if (promptChanged || $2("sa_auto_apply")?.checked) { - await pushSessionToSwarm(state.chatSession); - if (typeof syncLiveParamsBar === "function") syncLiveParamsBar(); - if (promptChanged && typeof appendSystemNote === "function") { - appendSystemNote("\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D"); - } - if (promptChanged) { - setStatus("\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D"); - } - } if (!state.generating && typeof stopBusyUi === "function") { - stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : promptChanged ? "\u041F\u0440\u043E\u043C\u043F\u0442 \u043E\u0431\u043D\u043E\u0432\u043B\u0451\u043D" : ""); + stopBusyUi(intent.vetoed ? "\u0417\u0430\u043F\u043E\u043C\u043D\u0438\u043B \xB7 \u0431\u0435\u0437 Generate" : ""); } } state.pendingSilentGen = false; + reportDebugClientTurn({ + user: opts.userText || "", + reply: String(reply || "").slice(0, 8e3), + patch: effective, + intent, + generating: !!intent.generate, + swarm_prompt: val("alt_prompt_textbox") || val("input_prompt") || "", + persona: $2("sa_persona")?.value || "", + pack: $2("sa_pack")?.value || "" + }); } - async function applyQuickPatch(patch, note) { + function reportDebugClientTurn(payload) { + try { + const body = JSON.stringify({ + ts: Date.now(), + ...payload + }); + fetch("http://127.0.0.1:17821/assistent/client-event", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body, + mode: "cors" + }).catch(() => { + }); + } catch (e) { + } + } + async function applyQuickPatch(patch, note, opts = {}) { + const wantGenerate = opts.generate === true; let withActions = { ...patch }; - if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) { - withActions.actions = ["generate"]; + delete withActions.generate; + if (Array.isArray(withActions.actions)) { + withActions.actions = withActions.actions.filter((a) => String(a) !== "generate"); + if (!withActions.actions.length) { + delete withActions.actions; + } + } + if (wantGenerate) { + withActions.generate = true; + withActions.actions = [ + ...Array.isArray(withActions.actions) ? withActions.actions : [], + "generate" + ]; } const prevIntent = state.lastUserParamIntent; state.lastUserParamIntent = true; const S = window.SA && window.SA.session; - if (S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) { + if (wantGenerate && S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) { withActions = ensureExactParamsForGenerate(withActions); } if (S) { state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions); } - await pushSessionToSwarm(state.chatSession); + state._quietParamApply = (state._quietParamApply || 0) + 1; + try { + if (wantGenerate) { + await pushSessionToSwarm(state.chatSession); + } else if (isParamOnlyQuickPatch(withActions)) { + await applyPatch(withActions, "params"); + } else { + await pushSessionToSwarm(state.chatSession); + } + } finally { + state._quietParamApply = Math.max(0, (state._quietParamApply || 1) - 1); + } state.lastUserParamIntent = prevIntent; - setStatus(note || "Applied"); - if (patchHasGenTrigger(withActions)) { + setStatus(note || (wantGenerate ? "Applied" : "\u041F\u0430\u0440\u0430\u043C\u0435\u0442\u0440\u044B (\u0431\u0435\u0437 Generate)")); + if (wantGenerate) { await runGenerateFromPatch(withActions, { force: true, fromSession: true }); } syncChipHighlight(); + syncLiveParamsBar(); } function syncChipHighlight() { const bar = $2("sa_chips"); @@ -8881,7 +9137,7 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; setStatus(`Unknown aspect. Try: ${Object.keys(ASPECT_TABLE).join(", ")}`); return true; } - await applyQuickPatch({ aspect: key, actions: ["generate"] }, `Aspect ${key}`); + await applyQuickPatch({ aspect: key }, `Aspect ${key}`); return true; } if (cmd === "seed") { @@ -8889,12 +9145,12 @@ ${summary || "(\u043F\u0443\u0441\u0442\u043E)"}`; if (mode === "lock" || mode === "keep") { await applyQuickPatch({ lock_seed: true }, "Seed locked"); } else { - await applyQuickPatch({ seed: -1, vary: true, actions: ["generate"] }, "Seed random"); + await applyQuickPatch({ seed: -1, vary: true }, "Seed random"); } return true; } if (cmd === "vary") { - await applyQuickPatch({ vary: true, seed: -1, actions: ["generate"] }, "Vary (new seed)"); + await applyQuickPatch({ vary: true, seed: -1 }, "Vary (new seed)", { generate: true }); return true; } if (cmd === "inventory" || cmd === "inv") { @@ -9009,6 +9265,7 @@ ${HELP_TEXT}`); state.lastUserParamIntent = userTextMentionsParams(text); state.lastUserControlIntent = userTextMentionsControls(text); state.pendingSilentGen = false; + state.turnUserWantsGenerate = userAsksGenerate2(text); } if (!isMachineTurn(opts) && !opts.skipSlash) { if (rawInput.startsWith("/")) { @@ -9039,7 +9296,7 @@ ${HELP_TEXT}`); patch.loras = state.lastPatch.loras; } appendSystemNote(`\u0421\u0442\u0430\u0432\u043B\u044E ${aspect} \u0438 Generate (\u0442\u043E\u0442 \u0436\u0435 \u043F\u0440\u043E\u043C\u043F\u0442) \u2014 \u0431\u0435\u0437 \u043F\u043E\u0432\u0442\u043E\u0440\u043D\u043E\u0439 \u043A\u0440\u0438\u0442\u0438\u043A\u0438.`); - await applyQuickPatch(patch, `Aspect ${aspect}`); + await applyQuickPatch(patch, `Aspect ${aspect}`, { generate: true }); return; } } @@ -9228,7 +9485,7 @@ ${HELP_TEXT}`); state.lastContextChars = 0; } updateCtxChip(); - const prose = extractPatch2(reply).prose || reply; + const prose = visibleAssistantProse(reply); state.history.push({ role: "assistant", content: prose, persona, pack }); persistHistory(); setBusyPhase(state.pendingSilentGen ? "silent_gen" : "thinking"); @@ -9236,7 +9493,7 @@ ${HELP_TEXT}`); await handleReplySideEffects(reply, civitaiResults, { ...opts, userText: text, - userWantsGenerate: !!opts.userWantsGenerate || !isMachineTurn(opts) && state.pendingSilentGen, + userWantsGenerate: !!opts.userWantsGenerate || !!state.turnUserWantsGenerate || !isMachineTurn(opts) && state.pendingSilentGen, attachedSlotIds: visionSlots.map((s) => s.id) }); } finally { @@ -9425,8 +9682,14 @@ ${HELP_TEXT}`); closeGenLightbox(); return; } - if ((e.key === "Enter" || e.key === " ") && state.boardTab === "generate" && state.selectedGenResultId) { - const row = (state.genResults || []).find((r) => r.id === state.selectedGenResultId); + if ((e.key === "Enter" || e.key === " ") && state.boardTab === "generate") { + let row = (state.genResults || []).find((r) => r.id === state.selectedGenResultId); + if (!row?.src) { + const slot = generateSlot(); + if (slot?.src && (slot.id === state.selectedSlotId || !state.selectedGenResultId)) { + row = { id: slot.id || GEN_ID, src: slot.src }; + } + } if (row?.src) { e.preventDefault(); openGenLightbox(row.id); @@ -9582,6 +9845,7 @@ ${HELP_TEXT}`); window.__swarmAssistentWired = true; loadSettings(); setView(state.view || "chat"); + void window.SA?.training?.resumePolling?.(); updateGate(); ensureBoard(); setBoardTab(state.boardTab || "generate", { persist: false }); @@ -9901,19 +10165,27 @@ ${HELP_TEXT}`); const vary = btn.getAttribute("data-vary"); const profile = btn.getAttribute("data-krea-profile"); if (aspect) { - await applyQuickPatch({ aspect, actions: ["generate"] }, `Aspect ${aspect}`); + await applyQuickPatch({ aspect }, `Aspect ${aspect}`); } else if (seed === "lock") { await applyQuickPatch({ lock_seed: true }, "Seed locked"); } else if (seed === "random") { - await applyQuickPatch({ seed: -1, actions: ["generate"] }, "Seed random"); + await applyQuickPatch({ seed: -1 }, "Seed random"); } else if (vary) { - await applyQuickPatch({ vary: true, seed: -1, actions: ["generate"] }, "Vary"); + await applyQuickPatch({ vary: true, seed: -1 }, "Vary", { generate: true }); } else if (profile === "turbo") { const p = state.kreaProfiles?.turbo || mergedGenerationDefaults("turbo"); - await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ["generate"] }, "Turbo"); + await applyQuickPatch({ + steps: p.steps ?? 8, + cfg: p.cfg ?? 1, + sigma_shift: p.sigma_shift ?? 1.15 + }, "Turbo"); } else if (profile === "raw") { const p = state.kreaProfiles?.raw || mergedGenerationDefaults("raw"); - await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift, actions: ["generate"] }, "RAW"); + await applyQuickPatch({ + steps: p.steps ?? 28, + cfg: p.cfg ?? 4.5, + sigma_shift: p.sigma_shift + }, "RAW"); } renderLoraChips(); }); @@ -10139,6 +10411,7 @@ ${HELP_TEXT}`); if (state.ttab === "train") { syncModelfileModels(); syncQloraModels(); + void resumeTrainJobPolling(); } if (state.ttab === "models") refreshTrainModels(); } @@ -10330,17 +10603,47 @@ ${HELP_TEXT}`); async function importHf() { if (!state.hfSelected && !state.hfCheck?.id) { setTrainStatus("\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u043F\u0440\u043E\u0432\u0435\u0440\u044C \u043D\u0430\u0431\u043E\u0440"); + const hfSt2 = $("sa_hf_status"); + if (hfSt2) hfSt2.textContent = "\u0421\u043D\u0430\u0447\u0430\u043B\u0430 \u043D\u0430\u0436\u043C\u0438 \xAB\u041F\u0440\u043E\u0432\u0435\u0440\u0438\u0442\u044C\xBB"; return; } const id = state.hfSelected || state.hfCheck.id; const limit = Number($("sa_hf_import_limit")?.value) || 200; const mapping = buildHfMappingPayload(); + const btn = $("sa_btn_hf_import"); + const hfSt = $("sa_hf_status"); + const busy = "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u0443\u044E\u2026"; + setTrainStatus(busy); + if (hfSt) hfSt.textContent = busy; + if (btn) { + btn.disabled = true; + btn.dataset.label = btn.textContent; + btn.textContent = "\u2026"; + } try { const data = await SA2.request("AssistentImportHfDataset", { dataset: id, limit, mapping }); - setTrainStatus(`\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: ${data.imported}${data.runner_only ? " (runner-only)" : ""}`); + const n = Number(data?.imported) || 0; + let msg; + if (data?.runner_only) { + msg = `Runner-only: ${data.note || id} (\u0432 sqlite \u043D\u0435 \u0438\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043B\u0438)`; + } else if (n > 0) { + msg = `\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: ${n} \u2014 \u0447\u0435\u0440\u043D\u043E\u0432\u0438\u043A\u0438 \u0432 \u0441\u043F\u0438\u0441\u043A\u0435 \u043D\u0438\u0436\u0435`; + } else { + msg = "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u043D\u043E: 0 \u2014 \u043F\u0440\u043E\u0432\u0435\u0440\u044C HF token, \u043C\u0430\u043F\u043F\u0438\u043D\u0433 \u043A\u043E\u043B\u043E\u043D\u043E\u043A \u0438\u043B\u0438 \u043B\u0438\u043C\u0438\u0442 \u0441\u0442\u0440\u043E\u043A"; + } + setTrainStatus(msg); + if (hfSt) hfSt.textContent = msg; await refreshSamples(); + $("sa_train_samples")?.scrollIntoView({ behavior: "smooth", block: "nearest" }); } catch (e) { - setTrainStatus(String(e.message || e)); + const err = String(e.message || e); + setTrainStatus(err); + if (hfSt) hfSt.textContent = err; + } finally { + if (btn) { + btn.disabled = false; + btn.textContent = btn.dataset.label || "\u0418\u043C\u043F\u043E\u0440\u0442\u0438\u0440\u043E\u0432\u0430\u0442\u044C"; + } } } async function syncModelfileModels() { @@ -10370,7 +10673,9 @@ ${HELP_TEXT}`); } } async function createModelfile() { - setTrainStatus("\u0421\u043E\u0437\u0434\u0430\u044E \u043C\u043E\u0434\u0435\u043B\u044C\u2026"); + const btn = $("sa_btn_modelfile_create"); + btn?.setAttribute("disabled", "disabled"); + setTrainStatus("\u0421\u043E\u0437\u0434\u0430\u044E Modelfile \u0432 Ollama\u2026"); try { const data = await SA2.request("AssistentCreateOllamaModel", { base_url: $("sa_base_url")?.value, @@ -10386,11 +10691,15 @@ ${HELP_TEXT}`); SA2.app?.refreshModels?.(); } catch (e) { setTrainStatus(String(e.message || e)); + } finally { + btn?.removeAttribute("disabled"); } } function setTrainMode(mode) { $("sa_train_form_modelfile").hidden = mode !== "modelfile"; $("sa_train_form_qlora").hidden = mode !== "qlora"; + const radio = document.querySelector(`input[name="sa_train_mode"][value="${mode}"]`); + if (radio) radio.checked = true; } function setTrainingLock(on, text) { const root = $("swarm_assistent_root"); @@ -10409,15 +10718,20 @@ ${HELP_TEXT}`); const prog = data?.progress || (data?.job?.progress_json ? JSON.parse(data.job.progress_json) : null); const active = data?.training_active || data?.job?.status === "running"; const status = data?.job?.status || prog?.status; - setTrainingLock(active, prog?.status === "running" ? `\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \xB7 ${prog?.percent ?? 0}%` : "\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\u2026"); + const pct = prog?.percent; + const bannerText = active && pct != null ? `QLoRA \xB7 ${pct}%` : active ? "\u0418\u0434\u0451\u0442 QLoRA\u2026" : "\u0418\u0434\u0451\u0442 \u0442\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\u2026"; + setTrainingLock(active, bannerText); const logEl = $("sa_train_log"); const bar = $("sa_train_progress_fill"); const box = $("sa_train_progress"); if (prog) { if (box) box.hidden = false; - if (bar && prog.percent != null) bar.style.width = `${prog.percent}%`; + if (bar && pct != null) bar.style.width = `${pct}%`; if (logEl && prog.log) logEl.textContent = prog.log; } + if (active) { + setTrainStatus(pct != null ? `QLoRA \xB7 ${pct}% \u2014 \u043F\u043E\u043B\u043D\u044B\u0439 \u043B\u043E\u0433 \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0435 \xAB\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\xBB` : "QLoRA \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430 \u2014 \u043F\u043E\u043B\u043D\u044B\u0439 \u043B\u043E\u0433 \u043D\u0430 \u0432\u043A\u043B\u0430\u0434\u043A\u0435 \xAB\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430\xBB"); + } if (!active) { clearInterval(state.polling); state.polling = null; @@ -10444,6 +10758,19 @@ ${HELP_TEXT}`); } catch (e) { } } + async function resumeTrainJobPolling() { + try { + const data = await SA2.request("AssistentGetTrainJob", {}); + const active = data?.training_active || data?.job?.status === "running"; + await pollTrainJob(); + if (!active) return; + setTrainMode("qlora"); + $("sa_btn_qlora_cancel").hidden = false; + if (state.polling) clearInterval(state.polling); + state.polling = setInterval(pollTrainJob, 1500); + } catch (e) { + } + } async function startQlora() { setTrainStatus("\u0417\u0430\u043F\u0443\u0441\u043A\u2026"); try { @@ -10470,7 +10797,9 @@ ${HELP_TEXT}`); if (state.polling) clearInterval(state.polling); state.polling = setInterval(pollTrainJob, 1500); pollTrainJob(); - setTrainStatus("\u0422\u0440\u0435\u043D\u0438\u0440\u043E\u0432\u043A\u0430 \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430"); + setTrainMode("qlora"); + setTrainingTab("train"); + setTrainStatus("QLoRA \u0437\u0430\u043F\u0443\u0449\u0435\u043D\u0430 \u2014 \u043F\u0440\u043E\u0433\u0440\u0435\u0441\u0441 \u043D\u0438\u0436\u0435"); } catch (e) { setTrainStatus(String(e.message || e)); } @@ -10642,12 +10971,14 @@ ${HELP_TEXT}`); $("sa_btn_save_runner")?.addEventListener("click", saveRunner); loadRunner(); setTrainMode("modelfile"); + void resumeTrainJobPolling(); } SA2.training = { render() { wireTraining(); setTrainingTab(state.ttab); }, + resumePolling: resumeTrainJobPolling, async curateFromChat(messages, meta) { try { await SA2.request("AssistentUpsertTrainSample", { @@ -10671,6 +11002,9 @@ ${HELP_TEXT}`); // src/main.js window.SA = window.SA || {}; + window.SA.userAsksGenerate = userAsksGenerate; + window.SA.userAsksNoGenerate = userAsksNoGenerate; + window.SA.resolveTurnIntent = resolveTurnIntent; attachApi(window.SA); attachPatch(window.SA); attachPersist(window.SA); diff --git a/Assets/assistent.css b/Assets/assistent.css index 65a214e..11f3d2b 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -343,6 +343,10 @@ min-height: 7.5rem; } +.sa-slot.sa-has-image img { + cursor: zoom-in; +} + .sa-slot img { max-width: 100%; max-height: 100%; @@ -455,6 +459,16 @@ font-variant-numeric: tabular-nums; } +.sa-composer-params { + font-size: 0.72rem; + opacity: 0.82; + padding: 0.05rem 0 0.2rem; + letter-spacing: 0.01em; + line-height: 1.35; + font-variant-numeric: tabular-nums; + color: color-mix(in srgb, currentColor 88%, transparent); +} + .sa-more-wrap { position: relative; display: inline-flex; @@ -1712,6 +1726,12 @@ gap: 0.4rem; } +.sa-btn-gen-primary { + font-weight: 650; + border-color: color-mix(in srgb, #3fb950 70%, currentColor); + background: color-mix(in srgb, #3fb950 20%, transparent); +} + .sa-btn-gen:disabled { opacity: 0.65; pointer-events: none; @@ -2482,6 +2502,13 @@ flex-wrap: wrap; } +.sa-train-global-status { + display: block; + margin: 0.25rem 0.55rem 0.4rem; + min-height: 1.1em; + opacity: 0.9; +} + .sa-ttab { border: 1px solid color-mix(in srgb, currentColor 22%, transparent); background: transparent; @@ -2620,6 +2647,12 @@ min-height: 1.1rem; } +.sa-hf-status.sa-hf-busy { + opacity: 1; + color: var(--emphasis, #8ab4ff); + font-weight: 600; +} + .sa-hf-list { display: flex; flex-direction: column; diff --git a/AssistentHuggingFace.cs b/AssistentHuggingFace.cs index 51ac03b..ce98be9 100644 --- a/AssistentHuggingFace.cs +++ b/AssistentHuggingFace.cs @@ -506,7 +506,14 @@ public partial class SwarmAssistentExtension { return new JObject { ["success"] = true, ["imported"] = 0, ["runner_only"] = true, ["id"] = id, ["note"] = "Большой набор — используй HF id в QLoRA-раннере" }; } - return new JObject { ["success"] = true, ["imported"] = imported, ["id"] = id }; + return new JObject + { + ["success"] = true, + ["imported"] = imported, + ["id"] = id, + ["rows_fetched"] = rows.Count, + ["status"] = "draft", + }; } static JArray ConvertHfRowToMessages(JObject row, JObject schema, JObject mapping) diff --git a/AssistentTraining.cs b/AssistentTraining.cs index e8bb678..034c896 100644 --- a/AssistentTraining.cs +++ b/AssistentTraining.cs @@ -35,6 +35,7 @@ public partial class SwarmAssistentExtension ["success"] = true, ["samples"] = new JArray(list), ["approved"] = Memory.CountTrainSamples("approved"), + ["draft"] = Memory.CountTrainSamples("draft"), ["total"] = Memory.CountTrainSamples(null), }; } diff --git a/Config/_base/core/core.md b/Config/_base/core/core.md index d52e353..40f01c8 100644 --- a/Config/_base/core/core.md +++ b/Config/_base/core/core.md @@ -68,7 +68,7 @@ Several options (still one fence): ### Commands -- `"generate": true` — merge this delta into the chat session and run Generate (new/updated frame). **Omit** for chat, opinions, remember/save, look-only. Legacy `actions:["generate"]` is accepted as the same. +- `"generate": true` — you believe this turn is a draw request. The client **does not** start Swarm from this flag alone (small models often set it on chat questions). Set it **only** when the user asked to draw / generate / «нарисуй» / «сгенерируй». They can always click **Сгенерировать**. **Omit** the JSON for chat, opinions, Civitai/knowledge Q&A, remember/save, look-only. Legacy `actions:["generate"]` is the same advisory flag. - If the user says not to generate / only remember / only answer — **omit** `generate` and do not look. - `"look_at": ["generate"|"ref1"|…]` — vision hop (JPEG on follow-up). Use when you need pixels; do not pair with `generate` on the same normal write turn. - `"ask": ["settings"]` — request full settings dump (Exact + all fields). diff --git a/Config/_base/exact.json b/Config/_base/exact.json index cb9b3d1..0315ea5 100644 --- a/Config/_base/exact.json +++ b/Config/_base/exact.json @@ -21,13 +21,14 @@ }, "aspect_table": { "1:1": [1024, 1024], - "4:3": [1184, 896], - "3:2": [1248, 832], - "16:9": [1376, 768], - "2.35:1": [1568, 672], + "4:3": [1152, 896], + "3:2": [1216, 832], + "16:9": [1344, 768], + "21:9": [1536, 640], + "2.35:1": [1536, 640], "4:5": [928, 1152], - "2:3": [832, 1248], - "9:16": [768, 1376] + "2:3": [832, 1216], + "9:16": [768, 1344] }, "facts": { "architecture": "Krea 2 is a 12B DiT architecture. Not FLUX, not SDXL, not FLUX.1-Krea. Text encoder: Qwen3-VL 4B. VAE: Qwen Image VAE. Use only Krea2-trained LoRAs — never suggest FLUX/SDXL LoRAs.", diff --git a/Config/_base/memory-seed/aspect.json b/Config/_base/memory-seed/aspect.json index fb404a8..abf6d48 100644 --- a/Config/_base/memory-seed/aspect.json +++ b/Config/_base/memory-seed/aspect.json @@ -3,6 +3,6 @@ "kind": "aspect", "key": "table", "tags": ["aspect", "1k"], - "text": "Official Krea 1K aspect → width/height table lives in Exact memory aspect_table. Prefer patch field aspect over raw width/height. UI fills empties from Exact." + "text": "Official sizes: SwarmUI Side Length 1024 (ResolutionAspectReferences ×2, round 16). Krea API docs differ on 16:9 — follow Swarm dropdown. Prefer patch field aspect over raw width/height." } ] diff --git a/Config/_base/rules.json b/Config/_base/rules.json index c5ac22e..8ca3818 100644 --- a/Config/_base/rules.json +++ b/Config/_base/rules.json @@ -3,12 +3,14 @@ "Match the user's language (RU or EN) in chat — Generate prompt stays English", "Use only inventory / memory_hits / cards for LoRA names and triggers", "Emit valid JSON patches when changing generation state", - "Greet or dump your bio only on the first assistant turn; later turns answer directly" + "Greet or dump your bio only on the first assistant turn; later turns answer directly", + "JSON patch + generate:true only when the user asked to draw or change generation this turn; pure Q&A / Civitai / opinions = prose only, no JSON" ], "never": [ "Re-introduce yourself every turn or start with «О, привет» when history already has your replies", "Invent LoRA filenames or trigger words", "Lecture or moralize about NSFW", - "Dump all installed LoRAs — use memory_hits and enabled ones" + "Dump all installed LoRAs — use memory_hits and enabled ones", + "Emit generate:true or a prompt JSON on a knowledge/chat question" ] } diff --git a/Config/_base/ui.json b/Config/_base/ui.json index ad854fd..70d4155 100644 --- a/Config/_base/ui.json +++ b/Config/_base/ui.json @@ -1,12 +1,12 @@ { - "welcome_html": "
/help.generate./help.generate.Обученные и созданные модели (Ollama tags).
diff --git a/package.json b/package.json index eba8562..b0c12db 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "scripts": { "build": "node scripts/build.mjs", "watch": "node scripts/build.mjs --watch", - "test": "node --test test/intent.test.js test/patch.test.js test/context.test.js test/kreaProfile.test.js" + "test": "node --test test/intent.test.js test/patch.test.js test/context.test.js test/kreaProfile.test.js test/aspect.test.js" }, "devDependencies": { "esbuild": "^0.25.0" diff --git a/src/app.js b/src/app.js index 73a3813..0ec43ed 100644 --- a/src/app.js +++ b/src/app.js @@ -2,6 +2,8 @@ * Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API). * v0.8.0: Split assets — SA.request (assistent.api.js) and SA.*Patch (assistent.patch.js). */ +import { DEFAULT_ASPECT_TABLE, applyAspectTableFromObject as mergeAspectTable } from './aspect.js'; + (function () { const LS_BASE = 'swarm_assistent_base_url'; const LS_MODEL = 'swarm_assistent_model'; @@ -35,16 +37,7 @@ let CHARS_PER_TOKEN = 3.2; let COMPRESS_AUTO = true; - let ASPECT_TABLE = { - '1:1': [1024, 1024], - '4:3': [1184, 896], - '3:2': [1248, 832], - '16:9': [1376, 768], - '2.35:1': [1568, 672], - '4:5': [928, 1152], - '2:3': [832, 1248], - '9:16': [768, 1376], - }; + let ASPECT_TABLE = { ...DEFAULT_ASPECT_TABLE }; let PACK_ALIASES = { ordinary: 'ordinary', @@ -127,6 +120,8 @@ lastUserControlIntent: false, lastPatch: null, pendingSilentGen: false, + /** This user turn asked to draw — hops inherit it; model generate:true does not. */ + turnUserWantsGenerate: false, pendingPromptEnMerge: null, enabledSkills: [], kreaProfiles: { turbo: { steps: 8, cfg: 1, sigma_shift: 1.15 }, raw: { steps: 28, cfg: 4.5 } }, @@ -698,6 +693,7 @@ let text = String(raw || '').replace(/\r\n/g, '\n'); text = text.replace(/(?:^|\n)#{1,6}\s*JSON\s*Patch\s*(?=\n|$)/gi, '\n'); text = text.replace(/(?:^|\n)\s*JSON\s*Patch\s*:?\s*(?=\n|$)/gi, '\n'); + text = text.replace(/```(?:json)?\s*[\s\S]*?```/gi, ''); text = text.replace(/\n{3,}/g, '\n\n').trim(); if (!text) { return ''; @@ -782,11 +778,32 @@ if (!el) { return; } - el.value = value; + const next = value == null ? '' : String(value); + if (el.value === next) { + return; + } + el.value = next; + // SwarmUI may auto-Generate on input/change — suppress during param-only chip edits. + if ((state._quietParamApply || 0) > 0) { + return; + } el.dispatchEvent(new Event('input', { bubbles: true })); el.dispatchEvent(new Event('change', { bubbles: true })); } + const QUICK_PARAM_KEYS = new Set([ + 'aspect', 'width', 'height', 'steps', 'cfg', 'sigma_shift', 'seed', + 'vary', 'lock_seed', 'sampler', 'scheduler', 'batch', 'images', + ]); + + function isParamOnlyQuickPatch(patch) { + if (!patch || typeof patch !== 'object') { + return false; + } + const keys = Object.keys(patch).filter((k) => patch[k] != null && k !== 'notes'); + return keys.length > 0 && keys.every((k) => QUICK_PARAM_KEYS.has(k)); + } + function liveNegativePrompt() { return String(val('input_negativeprompt') || val('alt_negativeprompt_textbox') || '').trim(); } @@ -1162,8 +1179,9 @@ return true; } return cyrTokenRe( - 'сгенер[а-яё]*|нарисуй|нарисуйте|' - + 'запусти\\s+генер[а-яё]*|сделай\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)', + 'сгенер[а-яё]*|нарисуй|нарисуйте|нарисуем|' + + 'запусти\\s+генер[а-яё]*|' + + 'сдела(й|ем|йте)\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)', ).test(t); } @@ -1178,23 +1196,26 @@ vetoFn: userAsksNoGenerate, askGenerateFn: userAsksGenerate, fromAutoCritique: !!opts.fromAutoCritique, + sessionPrompt: opts.sessionPrompt || '', + userWantsGenerate: !!opts.userWantsGenerate, }); } - // generate:true / actions / user «сгенерируй» when a prompt already exists. + // User «нарисуй» / hops — never model generate:true alone. const vetoed = !isMachineTurn(opts) && userAsksNoGenerate(userText); const modelAsked = !!(patch && (patch.generate === true || patch.generate === 1 || (typeof patch.generate === 'string' && /^(true|1|yes|on)$/i.test(patch.generate)) || (Array.isArray(patch.actions) && patch.actions.map(String).includes('generate')))); + const hasPrompt = !!(String(patch?.prompt || '').trim() || String(opts.sessionPrompt || '').trim()); const userAsked = !isMachineTurn(opts) && userAsksGenerate(userText) - && !!(modelAsked || String(patch?.prompt || '').trim()); - const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked); + && (modelAsked || hasPrompt); + const generate = !vetoed && !opts.fromAutoCritique && (userAsked || !!opts.userWantsGenerate); const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); const look = !!(hasLook && !vetoed && !generate); const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []); - return { generate, look, vetoed, ask }; + return { generate, look, vetoed, ask, modelAsked }; } function stripGenerateAction(patch) { if (!patch || typeof patch !== 'object') { @@ -1281,32 +1302,55 @@ badge.classList.toggle('sa-mode-hot', pack === 'critique_image' || pack === 'inpaint_edit'); } - function syncLiveParamsBar() { - const el = $('sa_live_params'); - if (!el) { - return; - } + function formatLiveParamsLine() { const w = parseInt(val('input_width') || '0', 10) || null; const h = parseInt(val('input_height') || '0', 10) || null; const aspect = guessAspectFromSize(w, h) || '—'; const steps = val('input_steps') || '—'; const cfg = val('input_cfgscale') || val('input_cfg') || '—'; + const sigma = val('input_sigmashift') || ''; const seed = val('input_seed') || '—'; const profile = detectKreaProfileName(); - el.textContent = `${aspect} · ${w || '?'}×${h || '?'} · steps ${steps} · cfg ${cfg} · ${profile} · seed ${seed}`; + const batch = val('input_images') || val('input_batchsize') || ''; + const sampler = val('input_sampler') || ''; + const scheduler = val('input_scheduler') || ''; + const parts = [ + aspect, + `${w || '?'}×${h || '?'}`, + `steps ${steps}`, + `cfg ${cfg}`, + ]; + if (sigma) { + parts.push(`σ ${sigma}`); + } + parts.push(profile, `seed ${seed}`); + if (batch && batch !== '1') { + parts.push(`×${batch}`); + } + if (sampler) { + parts.push(sampler); + } + if (scheduler) { + parts.push(scheduler); + } + return parts.join(' · '); + } + + function syncLiveParamsBar() { + const line = formatLiveParamsLine(); + const boardEl = $('sa_live_params'); + const composerEl = $('sa_composer_params'); + if (boardEl) { + boardEl.textContent = line; + } + if (composerEl) { + composerEl.textContent = line; + } } function applyAspectTableFrom(obj) { - if (!obj || typeof obj !== 'object') { - return false; - } - const next = {}; - for (const [k, v] of Object.entries(obj)) { - if (Array.isArray(v) && v.length >= 2) { - next[k] = [Number(v[0]), Number(v[1])]; - } - } - if (!Object.keys(next).length) { + const next = mergeAspectTable(obj); + if (!next) { return false; } ASPECT_TABLE = next; @@ -2464,7 +2508,7 @@ actions.appendChild(toSession); const genBtn = document.createElement('button'); genBtn.type = 'button'; - genBtn.className = 'basic-button sa-btn-gen'; + genBtn.className = 'basic-button sa-btn-gen sa-btn-gen-primary'; genBtn.textContent = 'Сгенерировать'; genBtn.addEventListener('click', async () => { if (isGenerateUnavailable()) { @@ -2477,6 +2521,9 @@ state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), patch); } await pushSessionToSwarm(state.chatSession); + if (typeof appendSystemNote === 'function') { + appendSystemNote('Запускаю Generate'); + } await runGenerateFromPatch({ ...patch, actions: ['generate'] }, { force: true, fromSession: true }); }); actions.appendChild(genBtn); @@ -2617,6 +2664,24 @@ return el; } + /** Images available in the gen lightbox (variant grid or single live Generate slot). */ + function genLightboxList() { + const fromResults = (state.genResults || []).filter((r) => r && r.src); + if (fromResults.length) { + return fromResults; + } + const slot = generateSlot(); + if (slot?.src) { + return [{ + id: slot.id || GEN_ID, + label: slot.label || 'Generate', + src: slot.src, + patch: state.lastPatch || null, + }]; + } + return []; + } + function ensureGenLightbox() { let root = $('sa_gen_lightbox'); if (root) { @@ -2682,7 +2747,7 @@ } function currentLightboxRow() { - const list = (state.genResults || []).filter((r) => r.src); + const list = genLightboxList(); if (!list.length || state.lightboxIndex < 0) { return null; } @@ -2691,7 +2756,7 @@ function syncGenLightbox() { const root = ensureGenLightbox(); - const list = (state.genResults || []).filter((r) => r.src); + const list = genLightboxList(); const row = list[state.lightboxIndex]; if (!row) { root.hidden = true; @@ -2714,7 +2779,7 @@ } function openGenLightbox(id) { - const list = (state.genResults || []).filter((r) => r.src); + const list = genLightboxList(); let idx = list.findIndex((r) => r.id === id); if (idx < 0) { idx = 0; @@ -2737,7 +2802,7 @@ } function stepGenLightbox(delta) { - const list = (state.genResults || []).filter((r) => r.src); + const list = genLightboxList(); if (list.length < 2) { return; } @@ -2864,10 +2929,22 @@ busy.innerHTML = ''; el.appendChild(busy); - el.addEventListener('click', () => { + el.addEventListener('click', (e) => { + if (slot.type === 'generate' && slot.src && e.target?.tagName === 'IMG') { + openGenLightbox(slot.id || GEN_ID); + return; + } state.selectedSlotId = slot.id; renderBoard(); }); + if (slot.type === 'generate') { + el.addEventListener('dblclick', (e) => { + if (slot.src && e.target?.tagName === 'IMG') { + e.preventDefault(); + openGenLightbox(slot.id || GEN_ID); + } + }); + } el.addEventListener('dragover', (e) => { e.preventDefault(); e.stopPropagation(); @@ -4218,6 +4295,14 @@ return { prose: text || '', patch: null }; } + function visibleAssistantProse(text) { + if (window.SA && typeof SA.visibleProse === 'function') { + return SA.visibleProse(text); + } + const { prose, patch } = extractPatch(text); + return patch ? (prose || '') : String(text || ''); + } + function normalizeAspect(raw) { if (raw == null) { return null; @@ -4926,7 +5011,7 @@ } const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral'; const pack = $('sa_pack')?.value || defaultPackId(); - const prose = (typeof extractPatch === 'function' ? (extractPatch(text).prose || text) : text); + const prose = visibleAssistantProse(text); if (state.streamEl) { finalizeStreamMessage(text, []); } else { @@ -5500,7 +5585,7 @@ const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null }; const finalPatch = patch || extracted; if (role === 'assistant') { - setAssistantBody(div, prose || text || ''); + setAssistantBody(div, finalPatch ? (prose || '') : (prose || text || '')); } else { div.textContent = prose || text || ''; } @@ -5509,7 +5594,7 @@ const silent = !!(meta && meta.silentPatch); mountPatchBlock(div, finalPatch, { silent }); } -if (role === 'assistant' && !(meta && meta.historical)) { + if (role === 'assistant' && !(meta && meta.historical)) { mountCurateButtons(div, meta); } box.appendChild(div); @@ -5611,7 +5696,7 @@ if (role === 'assistant' && !(meta && meta.historical)) { if (streamHasClosedPatchFence(state.streamText)) { state.streamText = trimToClosedPatchFence(state.streamText); state.streamFenceDone = true; - setAssistantBody(state.streamEl, state.streamText, { live: true }); + setAssistantBody(state.streamEl, visibleAssistantProse(state.streamText), { live: true }); scrollMessagesToBottom(); const fn = state.onClosedTerminalFence; state.onClosedTerminalFence = null; @@ -5646,13 +5731,13 @@ if (role === 'assistant' && !(meta && meta.historical)) { el.classList.remove('sa-streaming', 'sa-typing'); mountAssistantMeta(el, meta || undefined); const { prose, patch } = extractPatch(fullReply); - setAssistantBody(el, prose || fullReply || ''); + setAssistantBody(el, patch ? (prose || '') : (prose || fullReply || '')); el.querySelectorAll('.sa-patch, .sa-civitai-list').forEach((n) => n.remove()); if (patch) { const silent = !!(meta && meta.silentPatch) || !!state.pendingSilentGen; mountPatchBlock(el, patch, { silent }); } -if (!(meta && meta.historical)) { + if (!(meta && meta.historical)) { mountCurateButtons(el, meta); } scrollMessagesToBottom(); @@ -7812,8 +7897,32 @@ if (!(meta && meta.historical)) { if (effective && act && typeof act.noteModelCommands === 'function') { act.noteModelCommands(effective); } + const sessionPrompt = String(state.chatSession?.gen?.prompt || '').trim(); + const userWantsGenerate = !!opts.userWantsGenerate || !!state.turnUserWantsGenerate; + let intent = resolveTurnIntent(effective, opts.userText || '', { + ...opts, + sessionPrompt, + userWantsGenerate, + }); + if (userWantsGenerate && !intent.vetoed && !fromAutoCritique) { + intent = { ...intent, generate: true }; + } + if (!effective && intent.generate && sessionPrompt) { + effective = { generate: true }; + } const promptChanged = !!(effective && String(effective.prompt || '').trim()); - if (effective && S) { + if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) { + if (typeof doInterruptNow === 'function') doInterruptNow(); + } + if (effective) { + if (intent.generate) { + const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : []; + effective = { ...effective, actions: acts.includes('generate') ? acts : acts.concat('generate'), generate: true }; + } + if (!intent.look && typeof stripLookAt === 'function') effective = stripLookAt(effective); + if (typeof rememberLastPatch === 'function') rememberLastPatch(effective); + } + if (intent.generate && effective && S) { state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); activityDone('delta', { kind: 'delta', @@ -7828,28 +7937,6 @@ if (!(meta && meta.historical)) { if (typeof persistChatsStore === 'function') persistChatsStore(); } } catch (e) { /* ignore */ } - if (typeof rememberLastPatch === 'function') rememberLastPatch(effective); - } - if (Array.isArray(effective?.actions) && effective.actions.map(String).includes('interrupt')) { - if (typeof doInterruptNow === 'function') doInterruptNow(); - } - let intent = resolveTurnIntent(effective, opts.userText || '', opts); - if (opts.userWantsGenerate && effective && !intent.vetoed && !fromAutoCritique) { - intent = { ...intent, generate: true }; - } - if (effective) { - if (intent.generate) { - const acts = Array.isArray(effective.actions) ? effective.actions.map(String) : []; - effective = { ...effective, actions: acts.includes('generate') ? acts : acts.concat('generate'), generate: true }; - } else if (typeof stripGenerateAction === 'function') { - effective = stripGenerateAction(effective); - if (effective && effective.generate) { - effective = { ...effective }; - delete effective.generate; - } - } - if (!intent.look && typeof stripLookAt === 'function') effective = stripLookAt(effective); - if (typeof rememberLastPatch === 'function') rememberLastPatch(effective); } if (intent.vetoed) { state.pendingSilentGen = false; @@ -7962,44 +8049,82 @@ if (!(meta && meta.historical)) { if (typeof maybeAutoVisionLook === 'function') await maybeAutoVisionLook(srcOut); } } else if (effective) { - if (S) state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), effective); - if (promptChanged || $('sa_auto_apply')?.checked) { - await pushSessionToSwarm(state.chatSession); - if (typeof syncLiveParamsBar === 'function') syncLiveParamsBar(); - if (promptChanged && typeof appendSystemNote === 'function') { - appendSystemNote('Промпт обновлён'); - } - if (promptChanged) { - setStatus('Промпт обновлён'); - } - } if (!state.generating && typeof stopBusyUi === 'function') { - stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : (promptChanged ? 'Промпт обновлён' : '')); + stopBusyUi(intent.vetoed ? 'Запомнил · без Generate' : ''); } } state.pendingSilentGen = false; + reportDebugClientTurn({ + user: opts.userText || '', + reply: String(reply || '').slice(0, 8000), + patch: effective, + intent, + generating: !!intent.generate, + swarm_prompt: val('alt_prompt_textbox') || val('input_prompt') || '', + persona: $('sa_persona')?.value || '', + pack: $('sa_pack')?.value || '', + }); } - async function applyQuickPatch(patch, note) { + + function reportDebugClientTurn(payload) { + try { + const body = JSON.stringify({ + ts: Date.now(), + ...payload, + }); + fetch('http://127.0.0.1:17821/assistent/client-event', { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body, + mode: 'cors', + }).catch(() => {}); + } catch (e) { /* sidecar optional */ } + } + async function applyQuickPatch(patch, note, opts = {}) { + const wantGenerate = opts.generate === true; let withActions = { ...patch }; - if (!Array.isArray(withActions.actions) && patchHasGenTrigger(withActions)) { - withActions.actions = ['generate']; + delete withActions.generate; + if (Array.isArray(withActions.actions)) { + withActions.actions = withActions.actions.filter((a) => String(a) !== 'generate'); + if (!withActions.actions.length) { + delete withActions.actions; + } + } + if (wantGenerate) { + withActions.generate = true; + withActions.actions = [ + ...(Array.isArray(withActions.actions) ? withActions.actions : []), + 'generate', + ]; } const prevIntent = state.lastUserParamIntent; state.lastUserParamIntent = true; const S = window.SA && window.SA.session; - if (S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) { + if (wantGenerate && S && S.patchWantsGenerate && S.patchWantsGenerate(withActions)) { withActions = ensureExactParamsForGenerate(withActions); } if (S) { state.chatSession = S.mergeDelta(state.chatSession || S.emptySession(), withActions); } - await pushSessionToSwarm(state.chatSession); + state._quietParamApply = (state._quietParamApply || 0) + 1; + try { + if (wantGenerate) { + await pushSessionToSwarm(state.chatSession); + } else if (isParamOnlyQuickPatch(withActions)) { + await applyPatch(withActions, 'params'); + } else { + await pushSessionToSwarm(state.chatSession); + } + } finally { + state._quietParamApply = Math.max(0, (state._quietParamApply || 1) - 1); + } state.lastUserParamIntent = prevIntent; - setStatus(note || 'Applied'); - if (patchHasGenTrigger(withActions)) { + setStatus(note || (wantGenerate ? 'Applied' : 'Параметры (без Generate)')); + if (wantGenerate) { await runGenerateFromPatch(withActions, { force: true, fromSession: true }); } syncChipHighlight(); + syncLiveParamsBar(); } function syncChipHighlight() { @@ -8275,7 +8400,7 @@ if (!(meta && meta.historical)) { setStatus(`Unknown aspect. Try: ${Object.keys(ASPECT_TABLE).join(', ')}`); return true; } - await applyQuickPatch({ aspect: key, actions: ['generate'] }, `Aspect ${key}`); + await applyQuickPatch({ aspect: key }, `Aspect ${key}`); return true; } if (cmd === 'seed') { @@ -8283,12 +8408,12 @@ if (!(meta && meta.historical)) { if (mode === 'lock' || mode === 'keep') { await applyQuickPatch({ lock_seed: true }, 'Seed locked'); } else { - await applyQuickPatch({ seed: -1, vary: true, actions: ['generate'] }, 'Seed random'); + await applyQuickPatch({ seed: -1, vary: true }, 'Seed random'); } return true; } if (cmd === 'vary') { - await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary (new seed)'); + await applyQuickPatch({ vary: true, seed: -1 }, 'Vary (new seed)', { generate: true }); return true; } if (cmd === 'inventory' || cmd === 'inv') { @@ -8411,6 +8536,7 @@ if (!(meta && meta.historical)) { state.lastUserParamIntent = userTextMentionsParams(text); state.lastUserControlIntent = userTextMentionsControls(text); state.pendingSilentGen = false; // 0.14 + state.turnUserWantsGenerate = userAsksGenerate(text); } if (!isMachineTurn(opts) && !opts.skipSlash) { @@ -8444,7 +8570,7 @@ if (!(meta && meta.historical)) { patch.loras = state.lastPatch.loras; } appendSystemNote(`Ставлю ${aspect} и Generate (тот же промпт) — без повторной критики.`); - await applyQuickPatch(patch, `Aspect ${aspect}`); + await applyQuickPatch(patch, `Aspect ${aspect}`, { generate: true }); return; } } @@ -8657,7 +8783,7 @@ if (!(meta && meta.historical)) { state.lastContextChars = 0; } updateCtxChip(); - const prose = extractPatch(reply).prose || reply; + const prose = visibleAssistantProse(reply); state.history.push({ role: 'assistant', content: prose, persona, pack }); persistHistory(); setBusyPhase(state.pendingSilentGen ? 'silent_gen' : 'thinking'); @@ -8666,6 +8792,7 @@ if (!(meta && meta.historical)) { ...opts, userText: text, userWantsGenerate: !!opts.userWantsGenerate + || !!state.turnUserWantsGenerate || (!isMachineTurn(opts) && state.pendingSilentGen), attachedSlotIds: visionSlots.map((s) => s.id), }); @@ -8866,8 +8993,14 @@ if (!(meta && meta.historical)) { closeGenLightbox(); return; } - if ((e.key === 'Enter' || e.key === ' ') && state.boardTab === 'generate' && state.selectedGenResultId) { - const row = (state.genResults || []).find((r) => r.id === state.selectedGenResultId); + if ((e.key === 'Enter' || e.key === ' ') && state.boardTab === 'generate') { + let row = (state.genResults || []).find((r) => r.id === state.selectedGenResultId); + if (!row?.src) { + const slot = generateSlot(); + if (slot?.src && (slot.id === state.selectedSlotId || !state.selectedGenResultId)) { + row = { id: slot.id || GEN_ID, src: slot.src }; + } + } if (row?.src) { e.preventDefault(); openGenLightbox(row.id); @@ -9031,6 +9164,7 @@ if (!(meta && meta.historical)) { window.__swarmAssistentWired = true; loadSettings(); setView(state.view || 'chat'); + void window.SA?.training?.resumePolling?.(); updateGate(); ensureBoard(); setBoardTab(state.boardTab || 'generate', { persist: false }); @@ -9357,19 +9491,27 @@ if (!(meta && meta.historical)) { const vary = btn.getAttribute('data-vary'); const profile = btn.getAttribute('data-krea-profile'); if (aspect) { - await applyQuickPatch({ aspect, actions: ['generate'] }, `Aspect ${aspect}`); + await applyQuickPatch({ aspect }, `Aspect ${aspect}`); } else if (seed === 'lock') { await applyQuickPatch({ lock_seed: true }, 'Seed locked'); } else if (seed === 'random') { - await applyQuickPatch({ seed: -1, actions: ['generate'] }, 'Seed random'); + await applyQuickPatch({ seed: -1 }, 'Seed random'); } else if (vary) { - await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary'); + await applyQuickPatch({ vary: true, seed: -1 }, 'Vary', { generate: true }); } else if (profile === 'turbo') { const p = state.kreaProfiles?.turbo || mergedGenerationDefaults('turbo'); - await applyQuickPatch({ steps: p.steps ?? 8, cfg: p.cfg ?? 1, sigma_shift: p.sigma_shift ?? 1.15, actions: ['generate'] }, 'Turbo'); + await applyQuickPatch({ + steps: p.steps ?? 8, + cfg: p.cfg ?? 1, + sigma_shift: p.sigma_shift ?? 1.15, + }, 'Turbo'); } else if (profile === 'raw') { const p = state.kreaProfiles?.raw || mergedGenerationDefaults('raw'); - await applyQuickPatch({ steps: p.steps ?? 28, cfg: p.cfg ?? 4.5, sigma_shift: p.sigma_shift, actions: ['generate'] }, 'RAW'); + await applyQuickPatch({ + steps: p.steps ?? 28, + cfg: p.cfg ?? 4.5, + sigma_shift: p.sigma_shift, + }, 'RAW'); } renderLoraChips(); }); diff --git a/src/aspect.js b/src/aspect.js new file mode 100644 index 0000000..853b63e --- /dev/null +++ b/src/aspect.js @@ -0,0 +1,108 @@ +/** + * Aspect → pixel sizes aligned with SwarmUI (Side Length 1024). + * Ref sheet @ 512px from SwarmUI T2IParamInput.ResolutionAspectReferences; + * width/height = round(ref * (sideLen / 512), 16). + */ + +export const SWARM_ASPECT_REF_512 = { + '1:1': [512, 512], + '4:3': [576, 448], + '3:2': [608, 416], + '8:5': [608, 384], + '16:9': [672, 384], + '21:9': [768, 320], + '2:3': [416, 608], + '5:8': [384, 608], + '9:16': [384, 672], + '9:21': [320, 768], +}; + +const ASPECT_ALIASES = { + '2.35:1': '21:9', +}; + +/** Krea 1K bucket; not in Swarm aspect dropdown (we keep 4:5 chip). */ +const KREA_EXTRA = { + '4:5': [928, 1152], +}; + +function roundTo16(n) { + return Math.round(n / 16) * 16; +} + +export function swarmSizeFromRef(aspectKey, sideLen = 1024) { + const key = ASPECT_ALIASES[aspectKey] || aspectKey; + if (KREA_EXTRA[key]) { + return [...KREA_EXTRA[key]]; + } + const ref = SWARM_ASPECT_REF_512[key]; + if (!ref) { + return null; + } + const scale = sideLen / 512; + return [roundTo16(ref[0] * scale), roundTo16(ref[1] * scale)]; +} + +export function buildDefaultAspectTable(sideLen = 1024) { + const table = {}; + for (const key of Object.keys(SWARM_ASPECT_REF_512)) { + table[key] = swarmSizeFromRef(key, sideLen); + } + table['2.35:1'] = swarmSizeFromRef('21:9', sideLen); + for (const [key, size] of Object.entries(KREA_EXTRA)) { + table[key] = [...size]; + } + return table; +} + +export const DEFAULT_ASPECT_TABLE = buildDefaultAspectTable(1024); + +export function normalizeAspectKey(raw, table = DEFAULT_ASPECT_TABLE) { + let s = String(raw ?? '').trim().toLowerCase().replace(/\s+/g, ''); + if (!s) { + return null; + } + if (s === 'square') { + s = '1:1'; + } else if (s === 'portrait' || s === 'vert') { + s = '2:3'; + } else if (s === 'landscape' || s === 'horiz') { + s = '16:9'; + } else if (s === 'cinematic' || s === 'ultrawide') { + s = '2.35:1'; + } + return table[s] ? s : null; +} + +export function guessAspectFromDimensions(w, h, table = DEFAULT_ASPECT_TABLE) { + const width = parseInt(w, 10); + const height = parseInt(h, 10); + if (!width || !height) { + return null; + } + let best = null; + let bestDist = Infinity; + for (const [key, [aw, ah]] of Object.entries(table)) { + const dist = Math.abs(width / height - aw / ah) + + Math.abs(width - aw) / 4000 + + Math.abs(height - ah) / 4000; + if (dist < bestDist) { + bestDist = dist; + best = key; + } + } + return bestDist < 0.12 ? best : null; +} + +export function applyAspectTableFromObject(obj) { + if (!obj || typeof obj !== 'object') { + return false; + } + const next = {}; + for (const [k, v] of Object.entries(obj)) { + if (Array.isArray(v) && v.length >= 2) { + next[k] = [Number(v[0]), Number(v[1])]; + } + } + return Object.keys(next).length ? next : false; +} diff --git a/src/intent.js b/src/intent.js index 84f4cb3..b491c2a 100644 --- a/src/intent.js +++ b/src/intent.js @@ -1,4 +1,4 @@ -/** Turn intent — veto + model generate; user «сгенерируй» also counts when a prompt/delta exists. */ +/** Turn intent — user-owned Generate; model generate:true is advisory only. */ export function cyrTokenRe(alts) { const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])'; @@ -32,8 +32,9 @@ export function userAsksGenerate(text) { return true; } return cyrTokenRe( - 'сгенер[а-яё]*|нарисуй|нарисуйте|' - + 'запусти\\s+генер[а-яё]*|сделай\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)', + 'сгенер[а-яё]*|нарисуй|нарисуйте|нарисуем|' + + 'запусти\\s+генер[а-яё]*|' + + 'сдела(й|ем|йте)\\s+(кадр|картинк[а-яё]*|изображ[а-яё]*)', ).test(t); } @@ -84,18 +85,22 @@ function generateFlagOn(patch) { return Array.isArray(patch.actions) && patch.actions.map(String).includes('generate'); } -/** Model generate flag, or user «сгенерируй» when a prompt/delta already exists. */ +/** + * Generate is user-owned. Model `generate:true` is advisory (Qwen dumps it on Q&A). + * User «нарисуй»/«сгенерируй» (or opts.userWantsGenerate from hops/buttons) starts Generate. + */ export function resolveTurnIntent(patch, userText, opts = {}) { const vetoed = !opts.machineTurn && userAsksNoGenerate(userText); const modelAsked = generateFlagOn(patch); + const hasPrompt = !!(String(patch?.prompt || '').trim() || String(opts.sessionPrompt || '').trim()); const userAsked = !opts.machineTurn && userAsksGenerate(userText) - && !!(modelAsked || String(patch?.prompt || '').trim()); - const generate = !vetoed && !opts.fromAutoCritique && (modelAsked || userAsked); + && (modelAsked || hasPrompt); + const generate = !vetoed && !opts.fromAutoCritique && (userAsked || !!opts.userWantsGenerate); const hasLook = !!patch && (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null); const look = !!(hasLook && !vetoed && !generate); const ask = Array.isArray(patch?.ask) ? patch.ask.map(String) : (typeof patch?.ask === 'string' && patch.ask ? [patch.ask] : []); - return { generate, look, vetoed, ask }; + return { generate, look, vetoed, ask, modelAsked }; } diff --git a/src/main.js b/src/main.js index 0e564ea..add36a6 100644 --- a/src/main.js +++ b/src/main.js @@ -1,3 +1,4 @@ +import { userAsksGenerate, userAsksNoGenerate, resolveTurnIntent } from './intent.js'; import { attachApi } from './api.js'; import { attachPatch, setPatchKeys } from './patch.js'; import { attachPersist } from './persist.js'; @@ -7,6 +8,9 @@ import { attachActivity } from './activity.js'; import { attachKreaProfile } from './kreaProfile.js'; window.SA = window.SA || {}; +window.SA.userAsksGenerate = userAsksGenerate; +window.SA.userAsksNoGenerate = userAsksNoGenerate; +window.SA.resolveTurnIntent = resolveTurnIntent; attachApi(window.SA); attachPatch(window.SA); attachPersist(window.SA); diff --git a/src/patch.js b/src/patch.js index ee7bf2f..d09e38c 100644 --- a/src/patch.js +++ b/src/patch.js @@ -131,6 +131,15 @@ export function extractPatch(text) { return { prose: text, patch: null }; } +/** Chat body text: never fall back to the raw fence when a patch was extracted. */ +export function visibleProse(text) { + const { prose, patch } = extractPatch(text); + if (patch) { + return prose || ''; + } + return String(text || ''); +} + export function isTerminalStreamPatch(obj) { if (!obj || typeof obj !== 'object') { return false; @@ -170,4 +179,5 @@ export function attachPatch(SA) { SA.normalizePatch = normalizePatch; SA.extractPatch = extractPatch; SA.generateFlagOn = generateFlagOn; + SA.visibleProse = visibleProse; } diff --git a/src/session.js b/src/session.js index 4354574..f550e88 100644 --- a/src/session.js +++ b/src/session.js @@ -345,18 +345,24 @@ export function fullSettingsDump(session, extras = {}) { }; } -/** Model generate/actions, or askGenerateFn when a prompt/delta exists. vetoFn / fromAutoCritique cancel. */ -export function resolveTurnIntent(patch, userText, { vetoFn, askGenerateFn, fromAutoCritique } = {}) { +/** + * Generate is user-owned. Model generate:true is advisory (Qwen dumps it on chat questions). + * askGenerateFn / userWantsGenerate start Generate; vetoFn / fromAutoCritique cancel. + */ +export function resolveTurnIntent(patch, userText, { + vetoFn, askGenerateFn, fromAutoCritique, sessionPrompt, userWantsGenerate, +} = {}) { const delta = normalizeDelta(patch) || {}; const vetoed = typeof vetoFn === 'function' ? !!vetoFn(userText) : false; const modelAsked = patchWantsGenerate(delta); + const hasPrompt = !!(String(delta.prompt || '').trim() || String(sessionPrompt || '').trim()); const userAsked = typeof askGenerateFn === 'function' && !!askGenerateFn(userText) - && !!(modelAsked || String(delta.prompt || '').trim()); - const generate = !vetoed && !fromAutoCritique && (modelAsked || userAsked); + && (modelAsked || hasPrompt); + const generate = !vetoed && !fromAutoCritique && (userAsked || !!userWantsGenerate); const hasLook = delta.look_at != null || delta.vision_from != null || delta.vision_slots != null; const look = !!(hasLook && !generate && !vetoed); const ask = patchAskList(delta); - return { generate, look, vetoed, ask }; + return { generate, look, vetoed, ask, modelAsked }; } /** Params the client fills from Exact on Generate when the LLM omits them (sparse contract). */ diff --git a/src/training.js b/src/training.js index cb39e0a..a968cb3 100644 --- a/src/training.js +++ b/src/training.js @@ -101,6 +101,7 @@ export function attachTraining(SA) { if (state.ttab === 'train') { syncModelfileModels(); syncQloraModels(); + void resumeTrainJobPolling(); } if (state.ttab === 'models') refreshTrainModels(); } @@ -112,7 +113,12 @@ export function attachTraining(SA) { const data = await SA.request('AssistentListTrainSamples', { status, persona, limit: 300 }); state.samples = data?.samples || []; const stats = $('sa_train_stats'); - if (stats) stats.textContent = `Одобрено: ${data?.approved ?? '—'} · всего: ${data?.total ?? '—'}`; + if (stats) { + const appr = data?.approved ?? '—'; + const draft = data?.draft ?? '—'; + const total = data?.total ?? '—'; + stats.textContent = `Одобрено: ${appr} · черновики: ${draft} · всего: ${total}`; + } const personaSel = $('sa_train_filter_persona'); if (personaSel && $('sa_persona')) { const cur = personaSel.value || 'all'; @@ -134,8 +140,14 @@ export function attachTraining(SA) { function renderSamples() { const root = $('sa_train_samples'); if (!root) return; + const filter = $('sa_train_filter_status')?.value || 'all'; if (!state.samples.length) { - root.innerHTML = '