Ship Assistent 0.15.3: user-owned Generate, SwarmUI aspects, HF import UX.
Chips and quick patches apply params without auto-Generate; aspect sizes match Swarm Side Length 1024. HF import shows drafts after switching filter from approved-only; training status and job polling improved. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
+241
-99
@@ -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 = '<span class="sa-spinner" aria-hidden="true"></span>';
|
||||
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();
|
||||
});
|
||||
|
||||
+108
@@ -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;
|
||||
}
|
||||
+12
-7
@@ -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 };
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
+11
-5
@@ -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). */
|
||||
|
||||
+98
-8
@@ -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 = '<div class="sa-mem-empty">Нет примеров. Отметь ответы в чате или импортируй датасет.</div>';
|
||||
const hint = filter === 'approved'
|
||||
? 'Под фильтром «Одобренные» пусто. HF-импорт создаёт <strong>черновики</strong> — переключи на «Черновики» или «Все статусы».'
|
||||
: filter === 'draft'
|
||||
? 'Нет черновиков. Импортируй HF или отметь примеры в чате.'
|
||||
: 'Нет примеров. Отметь ответы в чате или импортируй датасет (HF / файл).';
|
||||
root.innerHTML = `<div class="sa-mem-empty">${hint}</div>`;
|
||||
return;
|
||||
}
|
||||
root.innerHTML = '';
|
||||
@@ -302,19 +314,63 @@ export function attachTraining(SA) {
|
||||
}
|
||||
|
||||
async function importHf() {
|
||||
const link = ($('sa_hf_link')?.value || '').trim();
|
||||
if (!state.hfCheck?.id && link) {
|
||||
await checkHfLink();
|
||||
}
|
||||
if (!state.hfSelected && !state.hfCheck?.id) {
|
||||
setTrainStatus('Сначала проверь набор');
|
||||
const hfSt = $('sa_hf_status');
|
||||
if (hfSt) hfSt.textContent = 'Вставь ссылку и нажми «Проверить»';
|
||||
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 = limit > 400
|
||||
? `Импортирую до ${limit} строк… (1–2 мин)`
|
||||
: `Импортирую до ${limit}…`;
|
||||
setTrainStatus(busy);
|
||||
if (hfSt) {
|
||||
hfSt.textContent = busy;
|
||||
hfSt.classList.add('sa-hf-busy');
|
||||
}
|
||||
if (btn) {
|
||||
btn.disabled = true;
|
||||
btn.dataset.label = btn.textContent;
|
||||
btn.textContent = '…';
|
||||
}
|
||||
try {
|
||||
const data = await SA.request('AssistentImportHfDataset', { dataset: id, limit, mapping });
|
||||
setTrainStatus(`Импортировано: ${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} (в sqlite не импортировали)`;
|
||||
} else if (n > 0) {
|
||||
const filt = $('sa_train_filter_status');
|
||||
if (filt && filt.value === 'approved') {
|
||||
filt.value = 'draft';
|
||||
}
|
||||
msg = `Импортировано: ${n} черновик(ов) — список ниже (фильтр → черновики)`;
|
||||
} else {
|
||||
msg = 'Импортировано: 0 — HF token в User Settings, маппинг или gated-набор';
|
||||
}
|
||||
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 (hfSt) hfSt.classList.remove('sa-hf-busy');
|
||||
if (btn) {
|
||||
btn.disabled = false;
|
||||
btn.textContent = btn.dataset.label || 'Импортировать';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -345,7 +401,9 @@ export function attachTraining(SA) {
|
||||
}
|
||||
|
||||
async function createModelfile() {
|
||||
setTrainStatus('Создаю модель…');
|
||||
const btn = $('sa_btn_modelfile_create');
|
||||
btn?.setAttribute('disabled', 'disabled');
|
||||
setTrainStatus('Создаю Modelfile в Ollama…');
|
||||
try {
|
||||
const data = await SA.request('AssistentCreateOllamaModel', {
|
||||
base_url: $('sa_base_url')?.value,
|
||||
@@ -361,12 +419,16 @@ export function attachTraining(SA) {
|
||||
SA.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) {
|
||||
@@ -387,15 +449,26 @@ export function attachTraining(SA) {
|
||||
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' ? `Тренировка · ${prog?.percent ?? 0}%` : 'Идёт тренировка…');
|
||||
const pct = prog?.percent;
|
||||
const bannerText = active && pct != null
|
||||
? `QLoRA · ${pct}%`
|
||||
: active
|
||||
? 'Идёт QLoRA…'
|
||||
: 'Идёт тренировка…';
|
||||
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 · ${pct}% — полный лог на вкладке «Тренировка»`
|
||||
: 'QLoRA запущена — полный лог на вкладке «Тренировка»');
|
||||
}
|
||||
if (!active) {
|
||||
clearInterval(state.polling);
|
||||
state.polling = null;
|
||||
@@ -422,6 +495,19 @@ export function attachTraining(SA) {
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
|
||||
async function resumeTrainJobPolling() {
|
||||
try {
|
||||
const data = await SA.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) { /* ignore */ }
|
||||
}
|
||||
|
||||
async function startQlora() {
|
||||
setTrainStatus('Запуск…');
|
||||
try {
|
||||
@@ -448,7 +534,9 @@ export function attachTraining(SA) {
|
||||
if (state.polling) clearInterval(state.polling);
|
||||
state.polling = setInterval(pollTrainJob, 1500);
|
||||
pollTrainJob();
|
||||
setTrainStatus('Тренировка запущена');
|
||||
setTrainMode('qlora');
|
||||
setTrainingTab('train');
|
||||
setTrainStatus('QLoRA запущена — прогресс ниже');
|
||||
} catch (e) {
|
||||
setTrainStatus(String(e.message || e));
|
||||
}
|
||||
@@ -614,6 +702,7 @@ export function attachTraining(SA) {
|
||||
$('sa_btn_save_runner')?.addEventListener('click', saveRunner);
|
||||
loadRunner();
|
||||
setTrainMode('modelfile');
|
||||
void resumeTrainJobPolling();
|
||||
}
|
||||
|
||||
SA.training = {
|
||||
@@ -621,6 +710,7 @@ export function attachTraining(SA) {
|
||||
wireTraining();
|
||||
setTrainingTab(state.ttab);
|
||||
},
|
||||
resumePolling: resumeTrainJobPolling,
|
||||
async curateFromChat(messages, meta) {
|
||||
try {
|
||||
await SA.request('AssistentUpsertTrainSample', {
|
||||
|
||||
Reference in New Issue
Block a user