Show loading progress and send num_ctx 16384 so vision chat fits.
Livebar and waiting_ollama phase while the model loads into GPU; bump context past Ollama's 4096 default that truncated Assistent packs. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -160,6 +160,149 @@
|
||||
.sa-chat-title {
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.03em;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.45rem;
|
||||
}
|
||||
|
||||
.sa-live-dot {
|
||||
width: 0.55rem;
|
||||
height: 0.55rem;
|
||||
border-radius: 50%;
|
||||
background: #6ee7a8;
|
||||
box-shadow: 0 0 0 0 color-mix(in srgb, #6ee7a8 70%, transparent);
|
||||
animation: sa-pulse 1.4s ease-out infinite;
|
||||
}
|
||||
|
||||
.sa-chat-empty {
|
||||
margin: auto;
|
||||
padding: 1.5rem 1.1rem;
|
||||
text-align: center;
|
||||
opacity: 0.72;
|
||||
max-width: 22rem;
|
||||
}
|
||||
|
||||
.sa-chat-empty-title {
|
||||
font-weight: 650;
|
||||
letter-spacing: 0.03em;
|
||||
margin-bottom: 0.4rem;
|
||||
}
|
||||
|
||||
.sa-chat-empty-hint {
|
||||
font-size: 0.9rem;
|
||||
line-height: 1.45;
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.sa-livebar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
padding: 0.45rem 0.85rem;
|
||||
border-top: 1px solid color-mix(in srgb, currentColor 16%, transparent);
|
||||
background: color-mix(in srgb, currentColor 6%, transparent);
|
||||
font-size: 0.88rem;
|
||||
min-height: 2.1rem;
|
||||
}
|
||||
|
||||
.sa-livebar[hidden] {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.sa-livebar-text {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
opacity: 0.92;
|
||||
}
|
||||
|
||||
.sa-elapsed {
|
||||
font-variant-numeric: tabular-nums;
|
||||
opacity: 0.65;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
|
||||
.sa-spinner {
|
||||
width: 0.85rem;
|
||||
height: 0.85rem;
|
||||
border-radius: 50%;
|
||||
border: 2px solid color-mix(in srgb, currentColor 22%, transparent);
|
||||
border-top-color: currentColor;
|
||||
animation: sa-spin 0.7s linear infinite;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.sa-msg {
|
||||
animation: sa-msg-in 0.28s ease;
|
||||
}
|
||||
|
||||
.sa-msg.sa-typing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 0.55rem;
|
||||
font-style: normal;
|
||||
opacity: 0.85;
|
||||
min-height: 2.4rem;
|
||||
}
|
||||
|
||||
.sa-typing-label {
|
||||
opacity: 0.75;
|
||||
font-size: 0.9rem;
|
||||
}
|
||||
|
||||
.sa-dots {
|
||||
display: inline-flex;
|
||||
gap: 0.22rem;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.sa-dots i {
|
||||
width: 0.42rem;
|
||||
height: 0.42rem;
|
||||
border-radius: 50%;
|
||||
background: currentColor;
|
||||
opacity: 0.35;
|
||||
animation: sa-dot 1.1s ease-in-out infinite;
|
||||
}
|
||||
|
||||
.sa-dots i:nth-child(2) { animation-delay: 0.15s; }
|
||||
.sa-dots i:nth-child(3) { animation-delay: 0.3s; }
|
||||
|
||||
.sa-is-busy .sa-primary {
|
||||
opacity: 0.55;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.sa-input-busy {
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.sa-composer-busy {
|
||||
box-shadow: inset 0 1px 0 color-mix(in srgb, currentColor 12%, transparent);
|
||||
}
|
||||
|
||||
.sa-status.sa-status-busy {
|
||||
opacity: 0.9;
|
||||
font-weight: 560;
|
||||
}
|
||||
|
||||
@keyframes sa-spin {
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
@keyframes sa-pulse {
|
||||
0% { box-shadow: 0 0 0 0 color-mix(in srgb, #6ee7a8 55%, transparent); }
|
||||
70% { box-shadow: 0 0 0 0.45rem transparent; }
|
||||
100% { box-shadow: 0 0 0 0 transparent; }
|
||||
}
|
||||
|
||||
@keyframes sa-dot {
|
||||
0%, 80%, 100% { opacity: 0.25; transform: translateY(0); }
|
||||
40% { opacity: 1; transform: translateY(-0.18rem); }
|
||||
}
|
||||
|
||||
@keyframes sa-msg-in {
|
||||
from { opacity: 0; transform: translateY(0.35rem); }
|
||||
to { opacity: 1; transform: none; }
|
||||
}
|
||||
|
||||
.sa-header-right {
|
||||
|
||||
+267
-36
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
|
||||
* v0.3: inventory, streaming, auto-apply/generate, Civitai Confirm.
|
||||
* v0.3.4: do not auto-fill Vision from the checkpoint preview.
|
||||
*/
|
||||
(function () {
|
||||
const LS_BASE = 'swarm_assistent_base_url';
|
||||
@@ -25,12 +25,151 @@
|
||||
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
|
||||
streamEl: null,
|
||||
critiqueHopUsed: false,
|
||||
busyPhase: 'idle',
|
||||
busyStarted: 0,
|
||||
gotDelta: false,
|
||||
busyTimer: null,
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function modelShort(name) {
|
||||
const s = String(name || '');
|
||||
const slash = s.lastIndexOf('/');
|
||||
return (slash >= 0 ? s.slice(slash + 1) : s) || 'model';
|
||||
}
|
||||
|
||||
function fmtElapsed(ms) {
|
||||
const s = Math.max(0, Math.floor(ms / 1000));
|
||||
if (s < 60) {
|
||||
return `${s}s`;
|
||||
}
|
||||
return `${Math.floor(s / 60)}m ${String(s % 60).padStart(2, '0')}s`;
|
||||
}
|
||||
|
||||
function hideChatEmpty() {
|
||||
const empty = $('sa_chat_empty');
|
||||
if (empty) {
|
||||
empty.hidden = true;
|
||||
}
|
||||
}
|
||||
|
||||
function showChatEmptyIfIdle() {
|
||||
const box = $('sa_messages');
|
||||
const empty = $('sa_chat_empty');
|
||||
if (!box || !empty) {
|
||||
return;
|
||||
}
|
||||
const hasMsg = [...box.children].some((el) => el.id !== 'sa_chat_empty');
|
||||
empty.hidden = hasMsg;
|
||||
}
|
||||
|
||||
function setBusyPhase(phase) {
|
||||
state.busyPhase = phase || 'thinking';
|
||||
tickBusyUi();
|
||||
}
|
||||
|
||||
function tickBusyUi() {
|
||||
if (state.busyPhase === 'idle') {
|
||||
return;
|
||||
}
|
||||
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
||||
if (!state.gotDelta && (state.busyPhase === 'thinking' || state.busyPhase === 'waiting') && elapsed > 1600) {
|
||||
state.busyPhase = 'loading';
|
||||
}
|
||||
const model = modelShort($('sa_model')?.value);
|
||||
const labels = {
|
||||
encoding: 'Encoding image…',
|
||||
waiting: 'Waiting for Ollama…',
|
||||
loading: `Loading ${model} into GPU… first load can take a minute`,
|
||||
thinking: 'Thinking…',
|
||||
streaming: 'Writing…',
|
||||
generating: 'Generating image…',
|
||||
applying: 'Applying patch…',
|
||||
refining: 'Civitai search done — refining…',
|
||||
};
|
||||
const text = labels[state.busyPhase] || 'Working…';
|
||||
const barText = $('sa_livebar_text');
|
||||
if (barText) {
|
||||
barText.textContent = text;
|
||||
}
|
||||
const elapsedEl = $('sa_elapsed');
|
||||
if (elapsedEl) {
|
||||
elapsedEl.textContent = fmtElapsed(elapsed);
|
||||
}
|
||||
const status = $('sa_status');
|
||||
if (status) {
|
||||
status.textContent = text;
|
||||
status.classList.add('sa-status-busy');
|
||||
}
|
||||
}
|
||||
|
||||
function startBusyUi(phase) {
|
||||
state.busyStarted = Date.now();
|
||||
state.gotDelta = false;
|
||||
state.busyPhase = phase || 'thinking';
|
||||
$('swarm_assistent_root')?.classList.add('sa-is-busy');
|
||||
$('sa_composer')?.classList.add('sa-composer-busy');
|
||||
const send = $('sa_btn_send');
|
||||
if (send) {
|
||||
send.disabled = true;
|
||||
}
|
||||
const input = $('sa_input');
|
||||
if (input) {
|
||||
input.classList.add('sa-input-busy');
|
||||
}
|
||||
const bar = $('sa_livebar');
|
||||
if (bar) {
|
||||
bar.hidden = false;
|
||||
}
|
||||
const dot = $('sa_live_dot');
|
||||
if (dot) {
|
||||
dot.hidden = false;
|
||||
}
|
||||
tickBusyUi();
|
||||
if (state.busyTimer) {
|
||||
clearInterval(state.busyTimer);
|
||||
}
|
||||
state.busyTimer = setInterval(tickBusyUi, 400);
|
||||
}
|
||||
|
||||
function stopBusyUi(finalStatus) {
|
||||
if (state.busyTimer) {
|
||||
clearInterval(state.busyTimer);
|
||||
state.busyTimer = null;
|
||||
}
|
||||
const elapsed = Date.now() - (state.busyStarted || Date.now());
|
||||
state.busyPhase = 'idle';
|
||||
$('swarm_assistent_root')?.classList.remove('sa-is-busy');
|
||||
$('sa_composer')?.classList.remove('sa-composer-busy');
|
||||
const send = $('sa_btn_send');
|
||||
if (send) {
|
||||
send.disabled = false;
|
||||
}
|
||||
const input = $('sa_input');
|
||||
if (input) {
|
||||
input.classList.remove('sa-input-busy');
|
||||
}
|
||||
const bar = $('sa_livebar');
|
||||
if (bar) {
|
||||
bar.hidden = true;
|
||||
}
|
||||
const dot = $('sa_live_dot');
|
||||
if (dot) {
|
||||
dot.hidden = true;
|
||||
}
|
||||
const status = $('sa_status');
|
||||
if (status) {
|
||||
status.classList.remove('sa-status-busy');
|
||||
}
|
||||
if (finalStatus != null) {
|
||||
const suffix = elapsed >= 1000 ? ` · ${fmtElapsed(elapsed)}` : '';
|
||||
setStatus(finalStatus + suffix);
|
||||
}
|
||||
}
|
||||
|
||||
function setStatus(text) {
|
||||
const el = $('sa_status');
|
||||
if (el) {
|
||||
@@ -773,7 +912,7 @@
|
||||
const start = Date.now();
|
||||
const timer = setInterval(() => {
|
||||
const src = findCurrentGenerateSrc();
|
||||
if (src && src !== prevSrc) {
|
||||
if (src && src !== prevSrc && !looksLikeModelPreview(src)) {
|
||||
clearInterval(timer);
|
||||
resolve(src);
|
||||
} else if (Date.now() - start > timeoutMs) {
|
||||
@@ -790,6 +929,7 @@
|
||||
}
|
||||
const prev = findCurrentGenerateSrc();
|
||||
setStatus('Generating…');
|
||||
startBusyUi('generating');
|
||||
state.generating = true;
|
||||
setInterruptVisible(true);
|
||||
const ok = triggerGenerate();
|
||||
@@ -802,11 +942,16 @@
|
||||
const src = await waitForNewImage(prev);
|
||||
state.generating = false;
|
||||
setInterruptVisible(state.busy);
|
||||
if (!state.busy) {
|
||||
stopBusyUi(src ? 'Generate done' : 'Generate finished (no new image detected)');
|
||||
}
|
||||
if (src) {
|
||||
setImageFromSrc(src, { note: 'Result → vision' });
|
||||
return src;
|
||||
}
|
||||
setStatus('Generate finished (no new image detected)');
|
||||
if (state.busy) {
|
||||
setStatus('Generate finished (no new image detected)');
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
@@ -832,6 +977,7 @@
|
||||
if (!box) {
|
||||
return null;
|
||||
}
|
||||
hideChatEmpty();
|
||||
const div = document.createElement('div');
|
||||
div.className = `sa-msg ${role}`;
|
||||
const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null };
|
||||
@@ -883,9 +1029,10 @@
|
||||
if (!box) {
|
||||
return null;
|
||||
}
|
||||
hideChatEmpty();
|
||||
const div = document.createElement('div');
|
||||
div.className = 'sa-msg assistant sa-streaming';
|
||||
div.textContent = '';
|
||||
div.className = 'sa-msg assistant sa-streaming sa-typing';
|
||||
div.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">Waiting for the model…</span>';
|
||||
box.appendChild(div);
|
||||
box.scrollTop = box.scrollHeight;
|
||||
state.streamEl = div;
|
||||
@@ -897,6 +1044,14 @@
|
||||
beginStreamMessage();
|
||||
}
|
||||
if (state.streamEl) {
|
||||
if (state.streamEl.classList.contains('sa-typing')) {
|
||||
state.streamEl.classList.remove('sa-typing');
|
||||
state.streamEl.textContent = '';
|
||||
}
|
||||
state.gotDelta = true;
|
||||
if (state.busyPhase !== 'refining') {
|
||||
setBusyPhase('streaming');
|
||||
}
|
||||
state.streamEl.textContent += delta;
|
||||
const box = $('sa_messages');
|
||||
if (box) {
|
||||
@@ -912,7 +1067,7 @@
|
||||
appendMessage('assistant', fullReply, null, civitaiResults);
|
||||
return;
|
||||
}
|
||||
el.classList.remove('sa-streaming');
|
||||
el.classList.remove('sa-streaming', 'sa-typing');
|
||||
const { prose, patch } = extractPatch(fullReply);
|
||||
el.textContent = prose || fullReply || '';
|
||||
if (patch) {
|
||||
@@ -1063,33 +1218,57 @@
|
||||
}
|
||||
}
|
||||
|
||||
function findCurrentGenerateSrc() {
|
||||
function wantsAutoVision() {
|
||||
return !!$('sa_auto_vision')?.checked;
|
||||
}
|
||||
|
||||
function looksLikeModelPreview(src) {
|
||||
const s = String(src || '').toLowerCase();
|
||||
if (!s) {
|
||||
return false;
|
||||
}
|
||||
return s.includes('.preview.')
|
||||
|| s.includes('placeholder')
|
||||
|| /\/models\//i.test(s);
|
||||
}
|
||||
|
||||
function findCurrentGenerateSrc({ allowPreview = false } = {}) {
|
||||
let src = null;
|
||||
try {
|
||||
const cur = document.getElementById('current_image_img')
|
||||
|| document.querySelector('#current_image img')
|
||||
|| document.querySelector('.current-image img')
|
||||
|| document.querySelector('#current_image_batch img');
|
||||
if (cur) {
|
||||
return cur.dataset?.src || cur.src || null;
|
||||
src = cur.dataset?.src || cur.src || null;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
try {
|
||||
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
|
||||
return currentMetadataMap.image;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
return null;
|
||||
if (!src) {
|
||||
try {
|
||||
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
|
||||
src = currentMetadataMap.image;
|
||||
}
|
||||
} catch (e) { /* ignore */ }
|
||||
}
|
||||
if (!src) {
|
||||
return null;
|
||||
}
|
||||
if (!allowPreview && looksLikeModelPreview(src)) {
|
||||
return null;
|
||||
}
|
||||
return src;
|
||||
}
|
||||
|
||||
function refreshImagePreview({ onlyIfEmpty = false } = {}) {
|
||||
if (!wantsAutoVision()) {
|
||||
return;
|
||||
}
|
||||
if (onlyIfEmpty && state.lastImageDataUrl) {
|
||||
return;
|
||||
}
|
||||
const src = findCurrentGenerateSrc();
|
||||
if (src) {
|
||||
setImageFromSrc(src);
|
||||
} else if (!state.lastImageDataUrl) {
|
||||
clearVisionImage({ silent: true });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1137,24 +1316,55 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
async function imageToBase64ForOllama(src) {
|
||||
async function imageToBase64ForOllama(src, maxEdge = 1024) {
|
||||
if (!src) {
|
||||
return null;
|
||||
}
|
||||
const dataUrl = await srcToDataUrl(src);
|
||||
if (!dataUrl) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const img = await new Promise((resolve, reject) => {
|
||||
const el = new Image();
|
||||
el.onload = () => resolve(el);
|
||||
el.onerror = reject;
|
||||
el.src = dataUrl;
|
||||
});
|
||||
const w = img.naturalWidth || img.width || 0;
|
||||
const h = img.naturalHeight || img.height || 0;
|
||||
const edge = Math.max(w, h);
|
||||
const canvas = document.createElement('canvas');
|
||||
if (!edge || edge <= maxEdge) {
|
||||
canvas.width = Math.max(w, 1);
|
||||
canvas.height = Math.max(h, 1);
|
||||
canvas.getContext('2d').drawImage(img, 0, 0);
|
||||
} else {
|
||||
const scale = maxEdge / edge;
|
||||
canvas.width = Math.max(1, Math.round(w * scale));
|
||||
canvas.height = Math.max(1, Math.round(h * scale));
|
||||
canvas.getContext('2d').drawImage(img, 0, 0, canvas.width, canvas.height);
|
||||
}
|
||||
const jpeg = canvas.toDataURL('image/jpeg', 0.85);
|
||||
const i = jpeg.indexOf(',');
|
||||
return i >= 0 ? jpeg.slice(i + 1) : null;
|
||||
} catch (e) {
|
||||
console.warn('Assistent: vision resize failed', e);
|
||||
const i = dataUrl.indexOf(',');
|
||||
return i >= 0 ? dataUrl.slice(i + 1) : null;
|
||||
}
|
||||
}
|
||||
|
||||
async function srcToDataUrl(src) {
|
||||
if (src.startsWith('data:')) {
|
||||
const i = src.indexOf(',');
|
||||
return i >= 0 ? src.slice(i + 1) : null;
|
||||
return src;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch(src);
|
||||
const blob = await resp.blob();
|
||||
return await new Promise((resolve, reject) => {
|
||||
const reader = new FileReader();
|
||||
reader.onload = () => {
|
||||
const data = String(reader.result || '');
|
||||
const i = data.indexOf(',');
|
||||
resolve(i >= 0 ? data.slice(i + 1) : null);
|
||||
};
|
||||
reader.onload = () => resolve(String(reader.result || ''));
|
||||
reader.onerror = reject;
|
||||
reader.readAsDataURL(blob);
|
||||
});
|
||||
@@ -1355,12 +1565,13 @@
|
||||
return;
|
||||
}
|
||||
|
||||
if (!state.lastImageDataUrl) {
|
||||
if (!state.lastImageDataUrl && wantsAutoVision()) {
|
||||
refreshImagePreview();
|
||||
}
|
||||
const attach = ($('sa_attach_vision')?.checked || $('sa_auto_vision')?.checked) && state.lastImageDataUrl;
|
||||
const attach = ($('sa_attach_vision')?.checked || wantsAutoVision()) && state.lastImageDataUrl;
|
||||
let images = null;
|
||||
if (attach) {
|
||||
startBusyUi('encoding');
|
||||
setStatus('Encoding image…');
|
||||
const b64 = await imageToBase64ForOllama(state.lastImageDataUrl);
|
||||
if (b64) {
|
||||
@@ -1385,7 +1596,7 @@
|
||||
|
||||
state.busy = true;
|
||||
setInterruptVisible(true);
|
||||
setStatus('Thinking…');
|
||||
startBusyUi('thinking');
|
||||
saveSettings();
|
||||
|
||||
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
|
||||
@@ -1410,16 +1621,16 @@
|
||||
state.busy = false;
|
||||
setInterruptVisible(state.generating);
|
||||
state.history.push({ role: 'assistant', content: reply });
|
||||
setStatus('Done');
|
||||
stopBusyUi('Done');
|
||||
await handleReplySideEffects(reply, civitaiResults, opts);
|
||||
};
|
||||
|
||||
const finishErr = (msg) => {
|
||||
state.busy = false;
|
||||
setInterruptVisible(state.generating);
|
||||
setStatus(msg);
|
||||
stopBusyUi(msg);
|
||||
if (state.streamEl) {
|
||||
state.streamEl.classList.remove('sa-streaming');
|
||||
state.streamEl.classList.remove('sa-streaming', 'sa-typing');
|
||||
state.streamEl.classList.add('error');
|
||||
state.streamEl.textContent = msg;
|
||||
state.streamEl = null;
|
||||
@@ -1434,20 +1645,28 @@
|
||||
'AssistentChatWS',
|
||||
payload,
|
||||
(data) => {
|
||||
if (data.phase === 'waiting_ollama') {
|
||||
setBusyPhase('loading');
|
||||
const label = state.streamEl?.querySelector('.sa-typing-label');
|
||||
if (label) {
|
||||
label.textContent = `Loading ${modelShort(model)} into GPU…`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (data.error) {
|
||||
finishErr(String(data.error));
|
||||
return;
|
||||
}
|
||||
if (data.clear_stream) {
|
||||
if (state.streamEl) {
|
||||
state.streamEl.textContent = '';
|
||||
state.streamEl.classList.add('sa-typing');
|
||||
state.streamEl.innerHTML = '<span class="sa-dots" aria-hidden="true"><i></i><i></i><i></i></span><span class="sa-typing-label">Refining…</span>';
|
||||
}
|
||||
setStatus(data.notice || 'Refining…');
|
||||
setBusyPhase('refining');
|
||||
return;
|
||||
}
|
||||
if (data.delta) {
|
||||
appendStreamDelta(data.delta);
|
||||
setStatus('Thinking…');
|
||||
return;
|
||||
}
|
||||
if (data.done || data.reply != null) {
|
||||
@@ -1673,7 +1892,9 @@
|
||||
window.__swarmAssistentWired = true;
|
||||
loadSettings();
|
||||
updateGate();
|
||||
refreshImagePreview();
|
||||
if (wantsAutoVision()) {
|
||||
refreshImagePreview();
|
||||
}
|
||||
refreshModels();
|
||||
refreshInventory();
|
||||
wireDropZone();
|
||||
@@ -1692,7 +1913,7 @@
|
||||
});
|
||||
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory());
|
||||
$('sa_btn_use_current')?.addEventListener('click', () => {
|
||||
const src = findCurrentGenerateSrc();
|
||||
const src = findCurrentGenerateSrc({ allowPreview: true });
|
||||
if (src) {
|
||||
setImageFromSrc(src, { note: 'Using current Generate image' });
|
||||
} else {
|
||||
@@ -1733,10 +1954,14 @@
|
||||
$('sa_btn_send')?.addEventListener('click', () => sendChat());
|
||||
$('sa_btn_interrupt')?.addEventListener('click', () => {
|
||||
doInterruptNow();
|
||||
setStatus('Interrupted');
|
||||
state.busy = false;
|
||||
state.generating = false;
|
||||
setInterruptVisible(false);
|
||||
if (state.streamEl?.classList.contains('sa-typing')) {
|
||||
state.streamEl.remove();
|
||||
state.streamEl = null;
|
||||
}
|
||||
stopBusyUi('Interrupted');
|
||||
});
|
||||
$('sa_btn_clear')?.addEventListener('click', () => {
|
||||
state.history = [];
|
||||
@@ -1744,7 +1969,13 @@
|
||||
const box = $('sa_messages');
|
||||
if (box) {
|
||||
box.innerHTML = '';
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'sa-chat-empty';
|
||||
empty.id = 'sa_chat_empty';
|
||||
empty.innerHTML = '<div class="sa-chat-empty-title">Collaborative Krea 2</div><div class="sa-chat-empty-hint">Write a prompt, drop a reference on the left, or send the current Generate image.</div>';
|
||||
box.appendChild(empty);
|
||||
}
|
||||
stopBusyUi('');
|
||||
setStatus('');
|
||||
});
|
||||
$('sa_input')?.addEventListener('keydown', (e) => {
|
||||
|
||||
@@ -43,6 +43,8 @@ public class SwarmAssistentExtension : Extension
|
||||
const int MaxCivitaiHops = 2;
|
||||
const int MaxLorasInInventory = 120;
|
||||
const int MaxWildcardsInInventory = 80;
|
||||
/// <summary>Ollama default num_ctx is 4096; Assistent system+inventory+vision exceeds that.</summary>
|
||||
const int DefaultNumCtx = 16384;
|
||||
|
||||
static readonly Regex JsonFenceRe = new(@"```(?:json)?\s*([\s\S]*?)```", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||
|
||||
@@ -53,7 +55,7 @@ public class SwarmAssistentExtension : Extension
|
||||
ExtensionAuthor = "mrleo1nid";
|
||||
Description = "Collaborative Krea 2 assistant via Ollama: chat, vision, img2img/inpaint, Generate loop, Civitai Confirm.";
|
||||
License = "MIT";
|
||||
Version = "0.3.1";
|
||||
Version = "0.3.4";
|
||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"];
|
||||
}
|
||||
|
||||
@@ -581,6 +583,10 @@ public class SwarmAssistentExtension : Extension
|
||||
["model"] = modelName,
|
||||
["stream"] = stream,
|
||||
["messages"] = new JArray(ollamaMessages),
|
||||
["options"] = new JObject
|
||||
{
|
||||
["num_ctx"] = DefaultNumCtx,
|
||||
},
|
||||
};
|
||||
using StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
|
||||
using HttpRequestMessage req = new(HttpMethod.Post, $"{root}/api/chat") { Content = content };
|
||||
@@ -718,6 +724,14 @@ public class SwarmAssistentExtension : Extension
|
||||
string packName = (pack ?? "write_prompt").Trim();
|
||||
try
|
||||
{
|
||||
if (ws.State == WebSocketState.Open)
|
||||
{
|
||||
await ws.SendJson(new JObject
|
||||
{
|
||||
["phase"] = "waiting_ollama",
|
||||
["notice"] = "Loading model into GPU…",
|
||||
}, API.WebsocketTimeout);
|
||||
}
|
||||
async Task OnDelta(string delta)
|
||||
{
|
||||
if (ws.State == WebSocketState.Open)
|
||||
|
||||
@@ -25,7 +25,10 @@
|
||||
<div class="sa-splitter" id="sa_splitter" role="separator" aria-orientation="vertical" title="Drag to resize"></div>
|
||||
<section class="sa-chat-pane">
|
||||
<header class="sa-chat-header">
|
||||
<div class="sa-chat-title">Assistent</div>
|
||||
<div class="sa-chat-title">
|
||||
Assistent
|
||||
<span class="sa-live-dot" id="sa_live_dot" hidden></span>
|
||||
</div>
|
||||
<div class="sa-header-right">
|
||||
<select id="sa_pack" class="sa-select" title="Prompt pack">
|
||||
<option value="write_prompt">Write prompt</option>
|
||||
@@ -50,8 +53,18 @@
|
||||
<label class="sa-check"><input type="checkbox" id="sa_auto_critique" /> Auto-critique after generate</label>
|
||||
<label class="sa-check sa-danger" title="Dangerous — downloads without Confirm"><input type="checkbox" id="sa_auto_download" /> Auto-download Civitai (off)</label>
|
||||
</div>
|
||||
<div class="sa-messages" id="sa_messages"></div>
|
||||
<div class="sa-composer">
|
||||
<div class="sa-messages" id="sa_messages">
|
||||
<div class="sa-chat-empty" id="sa_chat_empty">
|
||||
<div class="sa-chat-empty-title">Collaborative Krea 2</div>
|
||||
<div class="sa-chat-empty-hint">Write a prompt, drop a reference on the left, or send the current Generate image.</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="sa-livebar" id="sa_livebar" hidden>
|
||||
<span class="sa-spinner" aria-hidden="true"></span>
|
||||
<span class="sa-livebar-text" id="sa_livebar_text">Working…</span>
|
||||
<span class="sa-elapsed" id="sa_elapsed"></span>
|
||||
</div>
|
||||
<div class="sa-composer" id="sa_composer">
|
||||
<textarea id="sa_input" rows="3" placeholder="Ask for a prompt, img2img, inpaint, critique… (Enter = send, Shift+Enter = newline)"></textarea>
|
||||
<div class="sa-composer-actions">
|
||||
<button type="button" class="basic-button sa-primary" id="sa_btn_send">Send</button>
|
||||
|
||||
Reference in New Issue
Block a user