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:
Leonid Pershin
2026-08-21 20:47:10 +03:00
co-authored by Cursor
parent 3380206c6a
commit 57b2023a50
4 changed files with 441 additions and 40 deletions
+143
View File
@@ -160,6 +160,149 @@
.sa-chat-title { .sa-chat-title {
font-weight: 650; font-weight: 650;
letter-spacing: 0.03em; 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 { .sa-header-right {
+260 -29
View File
@@ -1,6 +1,6 @@
/** /**
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API). * 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 () { (function () {
const LS_BASE = 'swarm_assistent_base_url'; const LS_BASE = 'swarm_assistent_base_url';
@@ -25,12 +25,151 @@
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false }, inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
streamEl: null, streamEl: null,
critiqueHopUsed: false, critiqueHopUsed: false,
busyPhase: 'idle',
busyStarted: 0,
gotDelta: false,
busyTimer: null,
}; };
function $(id) { function $(id) {
return document.getElementById(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) { function setStatus(text) {
const el = $('sa_status'); const el = $('sa_status');
if (el) { if (el) {
@@ -773,7 +912,7 @@
const start = Date.now(); const start = Date.now();
const timer = setInterval(() => { const timer = setInterval(() => {
const src = findCurrentGenerateSrc(); const src = findCurrentGenerateSrc();
if (src && src !== prevSrc) { if (src && src !== prevSrc && !looksLikeModelPreview(src)) {
clearInterval(timer); clearInterval(timer);
resolve(src); resolve(src);
} else if (Date.now() - start > timeoutMs) { } else if (Date.now() - start > timeoutMs) {
@@ -790,6 +929,7 @@
} }
const prev = findCurrentGenerateSrc(); const prev = findCurrentGenerateSrc();
setStatus('Generating…'); setStatus('Generating…');
startBusyUi('generating');
state.generating = true; state.generating = true;
setInterruptVisible(true); setInterruptVisible(true);
const ok = triggerGenerate(); const ok = triggerGenerate();
@@ -802,11 +942,16 @@
const src = await waitForNewImage(prev); const src = await waitForNewImage(prev);
state.generating = false; state.generating = false;
setInterruptVisible(state.busy); setInterruptVisible(state.busy);
if (!state.busy) {
stopBusyUi(src ? 'Generate done' : 'Generate finished (no new image detected)');
}
if (src) { if (src) {
setImageFromSrc(src, { note: 'Result → vision' }); setImageFromSrc(src, { note: 'Result → vision' });
return src; return src;
} }
if (state.busy) {
setStatus('Generate finished (no new image detected)'); setStatus('Generate finished (no new image detected)');
}
return null; return null;
} }
@@ -832,6 +977,7 @@
if (!box) { if (!box) {
return null; return null;
} }
hideChatEmpty();
const div = document.createElement('div'); const div = document.createElement('div');
div.className = `sa-msg ${role}`; div.className = `sa-msg ${role}`;
const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null }; const { prose, patch: extracted } = role === 'assistant' ? extractPatch(text) : { prose: text, patch: null };
@@ -883,9 +1029,10 @@
if (!box) { if (!box) {
return null; return null;
} }
hideChatEmpty();
const div = document.createElement('div'); const div = document.createElement('div');
div.className = 'sa-msg assistant sa-streaming'; div.className = 'sa-msg assistant sa-streaming sa-typing';
div.textContent = ''; 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.appendChild(div);
box.scrollTop = box.scrollHeight; box.scrollTop = box.scrollHeight;
state.streamEl = div; state.streamEl = div;
@@ -897,6 +1044,14 @@
beginStreamMessage(); beginStreamMessage();
} }
if (state.streamEl) { 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; state.streamEl.textContent += delta;
const box = $('sa_messages'); const box = $('sa_messages');
if (box) { if (box) {
@@ -912,7 +1067,7 @@
appendMessage('assistant', fullReply, null, civitaiResults); appendMessage('assistant', fullReply, null, civitaiResults);
return; return;
} }
el.classList.remove('sa-streaming'); el.classList.remove('sa-streaming', 'sa-typing');
const { prose, patch } = extractPatch(fullReply); const { prose, patch } = extractPatch(fullReply);
el.textContent = prose || fullReply || ''; el.textContent = prose || fullReply || '';
if (patch) { 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 { try {
const cur = document.getElementById('current_image_img') const cur = document.getElementById('current_image_img')
|| document.querySelector('#current_image img') || document.querySelector('#current_image img')
|| document.querySelector('.current-image img') || document.querySelector('.current-image img')
|| document.querySelector('#current_image_batch img'); || document.querySelector('#current_image_batch img');
if (cur) { if (cur) {
return cur.dataset?.src || cur.src || null; src = cur.dataset?.src || cur.src || null;
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
if (!src) {
try { try {
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) { if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
return currentMetadataMap.image; src = currentMetadataMap.image;
} }
} catch (e) { /* ignore */ } } catch (e) { /* ignore */ }
}
if (!src) {
return null; return null;
} }
if (!allowPreview && looksLikeModelPreview(src)) {
return null;
}
return src;
}
function refreshImagePreview({ onlyIfEmpty = false } = {}) { function refreshImagePreview({ onlyIfEmpty = false } = {}) {
if (!wantsAutoVision()) {
return;
}
if (onlyIfEmpty && state.lastImageDataUrl) { if (onlyIfEmpty && state.lastImageDataUrl) {
return; return;
} }
const src = findCurrentGenerateSrc(); const src = findCurrentGenerateSrc();
if (src) { if (src) {
setImageFromSrc(src); setImageFromSrc(src);
} else if (!state.lastImageDataUrl) {
clearVisionImage({ silent: true });
} }
} }
@@ -1137,24 +1316,55 @@
return false; return false;
} }
async function imageToBase64ForOllama(src) { async function imageToBase64ForOllama(src, maxEdge = 1024) {
if (!src) { if (!src) {
return null; 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:')) { if (src.startsWith('data:')) {
const i = src.indexOf(','); return src;
return i >= 0 ? src.slice(i + 1) : null;
} }
try { try {
const resp = await fetch(src); const resp = await fetch(src);
const blob = await resp.blob(); const blob = await resp.blob();
return await new Promise((resolve, reject) => { return await new Promise((resolve, reject) => {
const reader = new FileReader(); const reader = new FileReader();
reader.onload = () => { reader.onload = () => resolve(String(reader.result || ''));
const data = String(reader.result || '');
const i = data.indexOf(',');
resolve(i >= 0 ? data.slice(i + 1) : null);
};
reader.onerror = reject; reader.onerror = reject;
reader.readAsDataURL(blob); reader.readAsDataURL(blob);
}); });
@@ -1355,12 +1565,13 @@
return; return;
} }
if (!state.lastImageDataUrl) { if (!state.lastImageDataUrl && wantsAutoVision()) {
refreshImagePreview(); 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; let images = null;
if (attach) { if (attach) {
startBusyUi('encoding');
setStatus('Encoding image…'); setStatus('Encoding image…');
const b64 = await imageToBase64ForOllama(state.lastImageDataUrl); const b64 = await imageToBase64ForOllama(state.lastImageDataUrl);
if (b64) { if (b64) {
@@ -1385,7 +1596,7 @@
state.busy = true; state.busy = true;
setInterruptVisible(true); setInterruptVisible(true);
setStatus('Thinking'); startBusyUi('thinking');
saveSettings(); saveSettings();
const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434'; const baseUrl = $('sa_base_url')?.value || 'http://127.0.0.1:11434';
@@ -1410,16 +1621,16 @@
state.busy = false; state.busy = false;
setInterruptVisible(state.generating); setInterruptVisible(state.generating);
state.history.push({ role: 'assistant', content: reply }); state.history.push({ role: 'assistant', content: reply });
setStatus('Done'); stopBusyUi('Done');
await handleReplySideEffects(reply, civitaiResults, opts); await handleReplySideEffects(reply, civitaiResults, opts);
}; };
const finishErr = (msg) => { const finishErr = (msg) => {
state.busy = false; state.busy = false;
setInterruptVisible(state.generating); setInterruptVisible(state.generating);
setStatus(msg); stopBusyUi(msg);
if (state.streamEl) { if (state.streamEl) {
state.streamEl.classList.remove('sa-streaming'); state.streamEl.classList.remove('sa-streaming', 'sa-typing');
state.streamEl.classList.add('error'); state.streamEl.classList.add('error');
state.streamEl.textContent = msg; state.streamEl.textContent = msg;
state.streamEl = null; state.streamEl = null;
@@ -1434,20 +1645,28 @@
'AssistentChatWS', 'AssistentChatWS',
payload, payload,
(data) => { (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) { if (data.error) {
finishErr(String(data.error)); finishErr(String(data.error));
return; return;
} }
if (data.clear_stream) { if (data.clear_stream) {
if (state.streamEl) { 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; return;
} }
if (data.delta) { if (data.delta) {
appendStreamDelta(data.delta); appendStreamDelta(data.delta);
setStatus('Thinking…');
return; return;
} }
if (data.done || data.reply != null) { if (data.done || data.reply != null) {
@@ -1673,7 +1892,9 @@
window.__swarmAssistentWired = true; window.__swarmAssistentWired = true;
loadSettings(); loadSettings();
updateGate(); updateGate();
if (wantsAutoVision()) {
refreshImagePreview(); refreshImagePreview();
}
refreshModels(); refreshModels();
refreshInventory(); refreshInventory();
wireDropZone(); wireDropZone();
@@ -1692,7 +1913,7 @@
}); });
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory()); $('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory());
$('sa_btn_use_current')?.addEventListener('click', () => { $('sa_btn_use_current')?.addEventListener('click', () => {
const src = findCurrentGenerateSrc(); const src = findCurrentGenerateSrc({ allowPreview: true });
if (src) { if (src) {
setImageFromSrc(src, { note: 'Using current Generate image' }); setImageFromSrc(src, { note: 'Using current Generate image' });
} else { } else {
@@ -1733,10 +1954,14 @@
$('sa_btn_send')?.addEventListener('click', () => sendChat()); $('sa_btn_send')?.addEventListener('click', () => sendChat());
$('sa_btn_interrupt')?.addEventListener('click', () => { $('sa_btn_interrupt')?.addEventListener('click', () => {
doInterruptNow(); doInterruptNow();
setStatus('Interrupted');
state.busy = false; state.busy = false;
state.generating = false; state.generating = false;
setInterruptVisible(false); setInterruptVisible(false);
if (state.streamEl?.classList.contains('sa-typing')) {
state.streamEl.remove();
state.streamEl = null;
}
stopBusyUi('Interrupted');
}); });
$('sa_btn_clear')?.addEventListener('click', () => { $('sa_btn_clear')?.addEventListener('click', () => {
state.history = []; state.history = [];
@@ -1744,7 +1969,13 @@
const box = $('sa_messages'); const box = $('sa_messages');
if (box) { if (box) {
box.innerHTML = ''; 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(''); setStatus('');
}); });
$('sa_input')?.addEventListener('keydown', (e) => { $('sa_input')?.addEventListener('keydown', (e) => {
+15 -1
View File
@@ -43,6 +43,8 @@ public class SwarmAssistentExtension : Extension
const int MaxCivitaiHops = 2; const int MaxCivitaiHops = 2;
const int MaxLorasInInventory = 120; const int MaxLorasInInventory = 120;
const int MaxWildcardsInInventory = 80; 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); static readonly Regex JsonFenceRe = new(@"```(?:json)?\s*([\s\S]*?)```", RegexOptions.IgnoreCase | RegexOptions.Compiled);
@@ -53,7 +55,7 @@ public class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid"; ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant via Ollama: chat, vision, img2img/inpaint, Generate loop, Civitai Confirm."; Description = "Collaborative Krea 2 assistant via Ollama: chat, vision, img2img/inpaint, Generate loop, Civitai Confirm.";
License = "MIT"; License = "MIT";
Version = "0.3.1"; Version = "0.3.4";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"]; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"];
} }
@@ -581,6 +583,10 @@ public class SwarmAssistentExtension : Extension
["model"] = modelName, ["model"] = modelName,
["stream"] = stream, ["stream"] = stream,
["messages"] = new JArray(ollamaMessages), ["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 StringContent content = new(payload.ToString(Newtonsoft.Json.Formatting.None), Encoding.UTF8, "application/json");
using HttpRequestMessage req = new(HttpMethod.Post, $"{root}/api/chat") { Content = content }; 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(); string packName = (pack ?? "write_prompt").Trim();
try 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) async Task OnDelta(string delta)
{ {
if (ws.State == WebSocketState.Open) if (ws.State == WebSocketState.Open)
+16 -3
View File
@@ -25,7 +25,10 @@
<div class="sa-splitter" id="sa_splitter" role="separator" aria-orientation="vertical" title="Drag to resize"></div> <div class="sa-splitter" id="sa_splitter" role="separator" aria-orientation="vertical" title="Drag to resize"></div>
<section class="sa-chat-pane"> <section class="sa-chat-pane">
<header class="sa-chat-header"> <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"> <div class="sa-header-right">
<select id="sa_pack" class="sa-select" title="Prompt pack"> <select id="sa_pack" class="sa-select" title="Prompt pack">
<option value="write_prompt">Write prompt</option> <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"><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> <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>
<div class="sa-messages" id="sa_messages"></div> <div class="sa-messages" id="sa_messages">
<div class="sa-composer"> <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> <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"> <div class="sa-composer-actions">
<button type="button" class="basic-button sa-primary" id="sa_btn_send">Send</button> <button type="button" class="basic-button sa-primary" id="sa_btn_send">Send</button>