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 {
|
.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 {
|
||||||
|
|||||||
+267
-36
@@ -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;
|
||||||
}
|
}
|
||||||
setStatus('Generate finished (no new image detected)');
|
if (state.busy) {
|
||||||
|
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 */ }
|
||||||
try {
|
if (!src) {
|
||||||
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
|
try {
|
||||||
return currentMetadataMap.image;
|
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
|
||||||
}
|
src = currentMetadataMap.image;
|
||||||
} catch (e) { /* ignore */ }
|
}
|
||||||
return null;
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (!src) {
|
||||||
|
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();
|
||||||
refreshImagePreview();
|
if (wantsAutoVision()) {
|
||||||
|
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) => {
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
Reference in New Issue
Block a user