Improve Assistent UX: send from Generate, drag-drop, resize.

Add Send to Assistent media button, paste/drop vision image, pane splitter, and polish layout for Krea collaborative chat.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 07:45:37 +03:00
co-authored by Cursor
parent 5ce3fbf27d
commit 80119c78be
5 changed files with 564 additions and 94 deletions
+172 -40
View File
@@ -3,71 +3,148 @@
flex-direction: column;
height: calc(100vh - 8rem);
min-height: 28rem;
padding: 0.5rem;
padding: 0.5rem 0.65rem 0.65rem;
box-sizing: border-box;
}
.sa-gate {
padding: 1.25rem;
opacity: 0.9;
padding: 1.25rem 1.5rem;
margin-bottom: 0.5rem;
border-radius: 0.5rem;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: color-mix(in srgb, currentColor 6%, transparent);
opacity: 0.95;
}
.sa-layout {
display: flex;
flex: 1;
gap: 0.75rem;
gap: 0;
min-height: 0;
}
.sa-image-pane {
flex: 0 0 32%;
max-width: 22rem;
flex: 0 0 var(--sa-image-width, 34%);
max-width: 40%;
min-width: 11rem;
display: flex;
flex-direction: column;
gap: 0.5rem;
min-width: 12rem;
}
.sa-splitter {
flex: 0 0 0.45rem;
margin: 0 0.15rem;
cursor: col-resize;
border-radius: 999px;
background: color-mix(in srgb, currentColor 12%, transparent);
transition: background 0.15s ease;
align-self: stretch;
}
.sa-splitter:hover,
.sa-splitter.sa-dragging {
background: color-mix(in srgb, currentColor 35%, transparent);
}
.sa-image-frame {
position: relative;
flex: 1;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
border-radius: 0.35rem;
border: 1px dashed color-mix(in srgb, currentColor 28%, transparent);
border-radius: 0.55rem;
overflow: hidden;
background: color-mix(in srgb, currentColor 6%, transparent);
background:
radial-gradient(ellipse at 30% 20%, color-mix(in srgb, currentColor 8%, transparent), transparent 55%),
color-mix(in srgb, currentColor 4%, transparent);
min-height: 14rem;
outline: none;
transition: border-color 0.15s ease, box-shadow 0.15s ease;
}
.sa-image-frame:focus-visible {
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 35%, transparent);
}
.sa-image-frame.sa-has-image {
border-style: solid;
}
.sa-image-frame.sa-dragover {
border-color: color-mix(in srgb, currentColor 70%, transparent);
box-shadow: inset 0 0 0 2px color-mix(in srgb, currentColor 25%, transparent);
}
.sa-image-frame img {
max-width: 100%;
max-height: 100%;
object-fit: contain;
display: block;
}
.sa-image-empty {
padding: 1rem;
padding: 1.25rem 1rem;
text-align: center;
opacity: 0.65;
font-size: 0.95rem;
opacity: 0.7;
}
.sa-empty-title {
font-weight: 600;
letter-spacing: 0.02em;
margin-bottom: 0.35rem;
}
.sa-empty-hint {
font-size: 0.88rem;
line-height: 1.4;
opacity: 0.9;
}
.sa-drop-overlay {
position: absolute;
inset: 0;
display: flex;
align-items: center;
justify-content: center;
background: color-mix(in srgb, currentColor 18%, transparent);
font-weight: 600;
letter-spacing: 0.04em;
pointer-events: none;
}
.sa-vision-chip {
position: absolute;
top: 0.45rem;
left: 0.45rem;
padding: 0.15rem 0.45rem;
border-radius: 999px;
font-size: 0.72rem;
font-weight: 600;
letter-spacing: 0.04em;
text-transform: uppercase;
background: color-mix(in srgb, currentColor 16%, transparent);
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
}
.sa-image-actions {
display: flex;
flex-wrap: wrap;
gap: 0.5rem;
gap: 0.45rem;
align-items: center;
}
.sa-chat-pane {
flex: 1 1 68%;
flex: 1 1 auto;
display: flex;
flex-direction: column;
min-width: 0;
min-height: 0;
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
border-radius: 0.35rem;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
border-radius: 0.55rem;
background: color-mix(in srgb, currentColor 3%, transparent);
overflow: hidden;
}
.sa-chat-header {
@@ -75,13 +152,14 @@
align-items: center;
justify-content: space-between;
gap: 0.5rem;
padding: 0.45rem 0.65rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
padding: 0.5rem 0.75rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
background: color-mix(in srgb, currentColor 5%, transparent);
}
.sa-chat-title {
font-weight: 600;
letter-spacing: 0.02em;
font-weight: 650;
letter-spacing: 0.03em;
}
.sa-header-right {
@@ -99,8 +177,8 @@
.sa-settings {
display: grid;
gap: 0.45rem;
padding: 0.65rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 20%, transparent);
padding: 0.7rem 0.75rem;
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
background: color-mix(in srgb, currentColor 5%, transparent);
}
@@ -117,72 +195,107 @@
box-sizing: border-box;
}
.sa-header-right .sa-select {
width: auto;
min-width: 9rem;
max-width: 12rem;
}
.sa-check {
flex-direction: row !important;
align-items: center;
gap: 0.4rem !important;
font-size: 0.9rem;
}
.sa-messages {
flex: 1;
overflow: auto;
padding: 0.75rem;
padding: 0.85rem 0.9rem;
display: flex;
flex-direction: column;
gap: 0.65rem;
gap: 0.7rem;
scroll-behavior: smooth;
}
.sa-msg {
max-width: 95%;
padding: 0.55rem 0.7rem;
border-radius: 0.35rem;
max-width: 92%;
padding: 0.6rem 0.8rem;
border-radius: 0.75rem;
white-space: pre-wrap;
word-break: break-word;
line-height: 1.35;
line-height: 1.4;
box-shadow: 0 1px 0 color-mix(in srgb, currentColor 8%, transparent);
}
.sa-msg.user {
align-self: flex-end;
background: color-mix(in srgb, currentColor 12%, transparent);
border-bottom-right-radius: 0.25rem;
background: color-mix(in srgb, currentColor 14%, transparent);
}
.sa-msg.assistant {
align-self: flex-start;
border-bottom-left-radius: 0.25rem;
background: color-mix(in srgb, currentColor 7%, transparent);
}
.sa-msg.error {
align-self: stretch;
border: 1px solid color-mix(in srgb, #c44 50%, transparent);
border: 1px solid color-mix(in srgb, #c44 55%, transparent);
background: color-mix(in srgb, #c44 12%, transparent);
}
.sa-msg.sa-system {
align-self: center;
max-width: 100%;
font-size: 0.85rem;
opacity: 0.75;
background: transparent;
box-shadow: none;
text-align: center;
}
.sa-patch {
margin-top: 0.5rem;
padding-top: 0.45rem;
margin-top: 0.55rem;
padding-top: 0.5rem;
border-top: 1px dashed color-mix(in srgb, currentColor 25%, transparent);
font-size: 0.9rem;
font-size: 0.88rem;
}
.sa-patch pre {
margin: 0;
max-height: 10rem;
overflow: auto;
padding: 0.45rem 0.55rem;
border-radius: 0.35rem;
background: color-mix(in srgb, currentColor 6%, transparent);
}
.sa-patch-actions {
display: flex;
flex-wrap: wrap;
gap: 0.35rem;
margin-top: 0.4rem;
margin-top: 0.45rem;
}
.sa-composer {
border-top: 1px solid color-mix(in srgb, currentColor 20%, transparent);
padding: 0.55rem;
border-top: 1px solid color-mix(in srgb, currentColor 18%, transparent);
padding: 0.6rem 0.7rem 0.7rem;
display: flex;
flex-direction: column;
gap: 0.4rem;
gap: 0.45rem;
background: color-mix(in srgb, currentColor 4%, transparent);
}
.sa-composer textarea {
width: 100%;
resize: vertical;
box-sizing: border-box;
min-height: 4rem;
min-height: 4.25rem;
border-radius: 0.45rem;
padding: 0.55rem 0.65rem;
line-height: 1.35;
}
.sa-composer-actions {
@@ -192,9 +305,14 @@
align-items: center;
}
.sa-primary {
font-weight: 600;
}
.sa-status {
font-size: 0.85rem;
opacity: 0.75;
margin-left: 0.15rem;
}
.sa-disabled {
@@ -202,6 +320,16 @@
pointer-events: none;
}
.sa-flash {
animation: sa-flash 0.85s ease;
}
@keyframes sa-flash {
0% { box-shadow: 0 0 0 0 color-mix(in srgb, currentColor 45%, transparent); }
40% { box-shadow: 0 0 0 6px color-mix(in srgb, currentColor 20%, transparent); }
100% { box-shadow: 0 0 0 0 transparent; }
}
@media (max-width: 900px) {
.sa-layout {
flex-direction: column;
@@ -209,6 +337,10 @@
.sa-image-pane {
flex: 0 0 auto;
max-width: none;
max-height: 40%;
max-height: 38%;
width: 100% !important;
}
.sa-splitter {
display: none;
}
}
+364 -42
View File
@@ -6,12 +6,16 @@
const LS_MODEL = 'swarm_assistent_model';
const LS_PACK = 'swarm_assistent_pack';
const LS_AUTO_VISION = 'swarm_assistent_auto_vision';
const LS_PANE_WIDTH = 'swarm_assistent_pane_width';
const TAB_BUTTON_ID = 'maintab_assistent';
const state = {
history: [],
packsLoaded: false,
busy: false,
lastImageDataUrl: null,
preferredModel: null,
dragDepth: 0,
};
function $(id) {
@@ -66,6 +70,93 @@
el.dispatchEvent(new Event('change', { bubbles: true }));
}
function openAssistentTab() {
const tab = document.getElementById(TAB_BUTTON_ID);
if (tab) {
tab.click();
return true;
}
// Fallback: try hash / pane
const pane = document.getElementById('assistent');
if (pane && typeof bootstrap !== 'undefined' && bootstrap.Tab) {
try {
bootstrap.Tab.getOrCreateInstance(tab || pane).show();
} catch (e) { /* ignore */ }
}
return !!tab;
}
function flashImagePane() {
const frame = $('sa_image_frame');
if (!frame) {
return;
}
frame.classList.remove('sa-flash');
// reflow
void frame.offsetWidth;
frame.classList.add('sa-flash');
}
function setImageFromSrc(src, { switchTab = false, note = null } = {}) {
if (!src) {
return false;
}
const cleaned = String(src).trim().split(/\s+/)[0];
if (!cleaned || cleaned.startsWith('#')) {
return false;
}
state.lastImageDataUrl = cleaned;
const img = $('sa_image_preview');
const empty = $('sa_image_empty');
const chip = $('sa_vision_chip');
const frame = $('sa_image_frame');
if (img) {
img.src = cleaned;
img.hidden = false;
}
if (empty) {
empty.hidden = true;
}
if (chip) {
chip.hidden = false;
}
if (frame) {
frame.classList.add('sa-has-image');
}
if (switchTab) {
openAssistentTab();
}
flashImagePane();
if (note) {
setStatus(note);
}
return true;
}
function clearVisionImage({ silent = false } = {}) {
state.lastImageDataUrl = null;
const img = $('sa_image_preview');
const empty = $('sa_image_empty');
const chip = $('sa_vision_chip');
const frame = $('sa_image_frame');
if (img) {
img.removeAttribute('src');
img.hidden = true;
}
if (empty) {
empty.hidden = false;
}
if (chip) {
chip.hidden = true;
}
if (frame) {
frame.classList.remove('sa-has-image');
}
if (!silent) {
setStatus('Vision image cleared');
}
}
function collectLiveContext() {
const ctx = {
architecture_ok: isKreaSelected(),
@@ -110,17 +201,14 @@
if (!m) {
continue;
}
const arch = `${m.architecture || ''} ${m.class || ''} ${m.name || ''} ${m.title || ''}`;
const isLora = /lora/i.test(m.category || m.type || '') || (m.name && String(m.name).toLowerCase().includes('lora'));
const folder = `${m.folder || m.path || ''}`;
const isLora = /lora/i.test(m.category || m.type || '') || (m.name && String(m.name).toLowerCase().includes('lora'));
const inLoraFolder = /lora/i.test(folder);
if (!(isLora || inLoraFolder)) {
// Still include if metadata says lora
if (!/lora/i.test(JSON.stringify(m).slice(0, 200))) {
continue;
}
}
// Prefer Krea-tagged or unknown; skip obvious FLUX/SDXL-only names when tagged
ctx.available_loras.push({
name: m.name || m.title,
title: m.title || m.name,
@@ -128,13 +216,11 @@
architecture: m.architecture || null,
});
}
// Cap list size for context window
if (ctx.available_loras.length > 80) {
ctx.available_loras = ctx.available_loras.slice(0, 80);
}
} catch (e) { /* ignore */ }
// Fallback: parse multi-select input_loras options as available names
try {
const sel = document.getElementById('input_loras');
if (sel && sel.options && ctx.available_loras.length === 0) {
@@ -188,7 +274,6 @@
if (patch.negative != null) {
setVal('input_negativeprompt', patch.negative);
}
// Ensure triggers present
if (Array.isArray(patch.loras)) {
for (const l of patch.loras) {
const triggers = l.triggers || (l.trigger_phrase ? [l.trigger_phrase] : []);
@@ -290,47 +375,85 @@
box.scrollTop = box.scrollHeight;
}
function refreshImagePreview() {
let src = null;
function findCurrentGenerateSrc() {
try {
const cur = document.getElementById('current_image_img') || document.querySelector('#current_image img') || document.querySelector('.current-image img');
if (cur && cur.src) {
src = cur.src;
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;
}
} catch (e) { /* ignore */ }
try {
if (!src && typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
src = currentMetadataMap.image;
if (typeof currentMetadataMap !== 'undefined' && currentMetadataMap && currentMetadataMap.image) {
return currentMetadataMap.image;
}
} catch (e) { /* ignore */ }
return null;
}
const img = $('sa_image_preview');
const empty = $('sa_image_empty');
function refreshImagePreview({ onlyIfEmpty = false } = {}) {
if (onlyIfEmpty && state.lastImageDataUrl) {
return;
}
const src = findCurrentGenerateSrc();
if (src) {
state.lastImageDataUrl = src;
if (img) {
img.src = src;
img.hidden = false;
}
if (empty) {
empty.hidden = true;
}
} else {
state.lastImageDataUrl = null;
if (img) {
img.hidden = true;
}
if (empty) {
empty.hidden = false;
setImageFromSrc(src);
} else if (!state.lastImageDataUrl) {
clearVisionImage({ silent: true });
}
}
function fileToDataUrl(file) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onload = () => resolve(String(reader.result || ''));
reader.onerror = reject;
reader.readAsDataURL(file);
});
}
async function acceptImageFile(file) {
if (!file || !String(file.type || '').startsWith('image/')) {
setStatus('Not an image file');
return false;
}
const dataUrl = await fileToDataUrl(file);
return setImageFromSrc(dataUrl, { note: `Loaded ${file.name || 'image'}` });
}
async function handleDropDataTransfer(dt) {
if (!dt) {
return false;
}
if (dt.files && dt.files.length) {
for (const file of dt.files) {
if (String(file.type || '').startsWith('image/')) {
return acceptImageFile(file);
}
}
}
const uri = (dt.getData('text/uri-list') || dt.getData('text/plain') || '').trim();
if (uri) {
const first = uri.split('\n').map((l) => l.trim()).find((l) => l && !l.startsWith('#'));
if (first) {
return setImageFromSrc(first, { note: 'Image from drag' });
}
}
// HTML img drag
const html = dt.getData('text/html') || '';
const m = html.match(/src=["']([^"']+)["']/i);
if (m && m[1]) {
return setImageFromSrc(m[1], { note: 'Image from drag' });
}
return false;
}
async function imageToBase64ForOllama(src) {
if (!src) {
return null;
}
// Already data URL
if (src.startsWith('data:')) {
const i = src.indexOf(',');
return i >= 0 ? src.slice(i + 1) : null;
@@ -359,6 +482,7 @@
const model = localStorage.getItem(LS_MODEL);
const pack = localStorage.getItem(LS_PACK);
const auto = localStorage.getItem(LS_AUTO_VISION);
const paneW = localStorage.getItem(LS_PANE_WIDTH);
if (base && $('sa_base_url')) {
$('sa_base_url').value = base;
}
@@ -371,6 +495,9 @@
if (model) {
state.preferredModel = model;
}
if (paneW) {
document.documentElement.style.setProperty('--sa-image-width', paneW);
}
}
function saveSettings() {
@@ -433,7 +560,9 @@
return;
}
if (!state.lastImageDataUrl) {
refreshImagePreview();
}
const attach = ($('sa_attach_vision')?.checked || $('sa_auto_vision')?.checked) && state.lastImageDataUrl;
let images = null;
if (attach) {
@@ -444,19 +573,13 @@
}
}
const userMsg = { role: 'user', content: text };
if (images) {
userMsg.images = images;
}
state.history.push({ role: 'user', content: text });
appendMessage('user', text);
$('sa_input').value = '';
const context = collectLiveContext();
context.has_vision_image = !!images;
// Strip huge fields from history replay — only send recent turns without images in history payload
const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content }));
// Last user message may include images
if (images && messages.length) {
messages[messages.length - 1].images = images;
}
@@ -493,6 +616,186 @@
});
}
function wireDropZone() {
const frame = $('sa_image_frame');
const overlay = $('sa_drop_overlay');
if (!frame) {
return;
}
const setDrag = (on) => {
frame.classList.toggle('sa-dragover', on);
if (overlay) {
overlay.hidden = !on;
}
};
['dragenter', 'dragover'].forEach((ev) => {
frame.addEventListener(ev, (e) => {
e.preventDefault();
e.stopPropagation();
state.dragDepth += ev === 'dragenter' ? 1 : 0;
setDrag(true);
if (e.dataTransfer) {
e.dataTransfer.dropEffect = 'copy';
}
});
});
frame.addEventListener('dragleave', (e) => {
e.preventDefault();
state.dragDepth = Math.max(0, state.dragDepth - 1);
if (state.dragDepth === 0) {
setDrag(false);
}
});
frame.addEventListener('drop', async (e) => {
e.preventDefault();
e.stopPropagation();
state.dragDepth = 0;
setDrag(false);
await handleDropDataTransfer(e.dataTransfer);
});
// Also accept drops on the whole layout (easier target)
const layout = $('sa_layout');
layout?.addEventListener('dragover', (e) => {
if (e.dataTransfer?.types?.includes('Files') || e.dataTransfer?.types?.includes('text/uri-list')) {
e.preventDefault();
}
});
layout?.addEventListener('drop', async (e) => {
if (!e.dataTransfer) {
return;
}
e.preventDefault();
await handleDropDataTransfer(e.dataTransfer);
});
frame.addEventListener('paste', async (e) => {
const items = e.clipboardData?.items;
if (!items) {
return;
}
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (file) {
await acceptImageFile(file);
}
return;
}
}
});
// Global paste when Assistent tab is visible
document.addEventListener('paste', async (e) => {
const pane = document.getElementById('assistent');
if (!pane || !pane.classList.contains('active')) {
return;
}
if (e.target && (e.target.tagName === 'TEXTAREA' || e.target.tagName === 'INPUT')) {
// Still allow image paste over text fields when clipboard has image
const items = e.clipboardData?.items;
let hasImage = false;
if (items) {
for (const item of items) {
if (item.type.startsWith('image/')) {
hasImage = true;
break;
}
}
}
if (!hasImage) {
return;
}
}
const items = e.clipboardData?.items;
if (!items) {
return;
}
for (const item of items) {
if (item.type.startsWith('image/')) {
e.preventDefault();
const file = item.getAsFile();
if (file) {
await acceptImageFile(file);
}
return;
}
}
});
}
function wireSplitter() {
const splitter = $('sa_splitter');
const layout = $('sa_layout');
const pane = $('sa_image_pane');
if (!splitter || !layout || !pane) {
return;
}
let dragging = false;
splitter.addEventListener('mousedown', (e) => {
e.preventDefault();
dragging = true;
splitter.classList.add('sa-dragging');
document.body.style.cursor = 'col-resize';
document.body.style.userSelect = 'none';
});
window.addEventListener('mousemove', (e) => {
if (!dragging) {
return;
}
const rect = layout.getBoundingClientRect();
const x = e.clientX - rect.left;
const pct = Math.min(45, Math.max(18, (x / rect.width) * 100));
const value = `${pct}%`;
document.documentElement.style.setProperty('--sa-image-width', value);
localStorage.setItem(LS_PANE_WIDTH, value);
});
window.addEventListener('mouseup', () => {
if (!dragging) {
return;
}
dragging = false;
splitter.classList.remove('sa-dragging');
document.body.style.cursor = '';
document.body.style.userSelect = '';
});
}
function registerSendButton() {
if (typeof registerMediaButton !== 'function') {
// SwarmUI core not ready yet — retry briefly
setTimeout(registerSendButton, 500);
return;
}
// Avoid duplicate registrations on hot-reload
if (window.__swarmAssistentMediaRegistered) {
return;
}
window.__swarmAssistentMediaRegistered = true;
registerMediaButton(
'Send to Assistent',
(src) => {
setImageFromSrc(src, {
switchTab: true,
note: 'Image sent to Assistent',
});
// Prefer critique pack when coming from an image
const pack = $('sa_pack');
if (pack && pack.value === 'write_prompt') {
pack.value = 'critique_image';
saveSettings();
}
},
'Open Assistent with this image (vision / critique / prompt help)',
['image'],
true,
true,
);
}
function wire() {
if (!$('swarm_assistent_root')) {
return;
@@ -501,6 +804,9 @@
updateGate();
refreshImagePreview();
refreshModels();
wireDropZone();
wireSplitter();
registerSendButton();
$('sa_btn_settings')?.addEventListener('click', () => {
const s = $('sa_settings');
@@ -512,7 +818,15 @@
saveSettings();
refreshModels();
});
$('sa_btn_refresh_image')?.addEventListener('click', refreshImagePreview);
$('sa_btn_use_current')?.addEventListener('click', () => {
const src = findCurrentGenerateSrc();
if (src) {
setImageFromSrc(src, { note: 'Using current Generate image' });
} else {
setStatus('No current Generate image');
}
});
$('sa_btn_clear_image')?.addEventListener('click', clearVisionImage);
$('sa_btn_send')?.addEventListener('click', () => sendChat());
$('sa_btn_clear')?.addEventListener('click', () => {
state.history = [];
@@ -523,7 +837,7 @@
setStatus('');
});
$('sa_input')?.addEventListener('keydown', (e) => {
if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) {
if (e.key === 'Enter' && !e.shiftKey && !e.altKey) {
e.preventDefault();
sendChat();
}
@@ -533,9 +847,17 @@
$('sa_pack')?.addEventListener('change', saveSettings);
$('sa_auto_vision')?.addEventListener('change', saveSettings);
// Re-check Krea gate when user may swap models
setInterval(updateGate, 2000);
setInterval(refreshImagePreview, 4000);
// Soft sync from Generate only when we have no pinned vision image
setInterval(() => refreshImagePreview({ onlyIfEmpty: true }), 5000);
// Expose for console / other extensions
window.swarmAssistent = {
setImageFromSrc,
clearVisionImage,
openAssistentTab,
sendToAssistent: (src) => setImageFromSrc(src, { switchTab: true, note: 'Image sent to Assistent' }),
};
}
if (document.readyState === 'loading') {
+11 -2
View File
@@ -4,9 +4,18 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
## Layout
- **Left:** current / selected image preview (attach for vision)
- **Left:** vision reference (drop / paste / *Send to Assistent* from Generate)
- **Splitter:** drag to resize panes
- **Right (wider):** chat
- **Top-right:** settings (Ollama URL, model, auto-attach)
- **Top-right:** prompt pack + settings (Ollama URL, model, auto-attach)
## UX
- **Send to Assistent** under the current Generate image (and History) — loads vision + opens the Assistent tab
- Drag images from Generate/History or the OS onto the left pane
- Paste (`Ctrl+V`) while the Assistent tab is open
- **Use current** / **Clear** for the vision reference
- Enter sends; Shift+Enter newline
## Requirements
+1 -1
View File
@@ -42,7 +42,7 @@ public class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant via Ollama: chat, vision, prompts, LoRA triggers, size patches.";
License = "MIT";
Version = "0.1.0";
Version = "0.2.0";
Tags = ["tabs", "ui", "llm", "ollama", "krea"];
}
+15 -8
View File
@@ -3,16 +3,23 @@
<p>Swarm Assistent is for <strong>Krea 2</strong> models only. Select a Krea 2 checkpoint to enable the chat.</p>
</div>
<div class="sa-layout" id="sa_layout">
<aside class="sa-image-pane">
<div class="sa-image-frame" id="sa_image_frame">
<img id="sa_image_preview" alt="" hidden />
<div class="sa-image-empty" id="sa_image_empty">Generate or select an image</div>
<aside class="sa-image-pane" id="sa_image_pane">
<div class="sa-image-frame" id="sa_image_frame" tabindex="0" title="Drop an image here, or paste (Ctrl+V). Drag from Generate / History.">
<img id="sa_image_preview" alt="Vision reference" hidden />
<div class="sa-image-empty" id="sa_image_empty">
<div class="sa-empty-title">Vision reference</div>
<div class="sa-empty-hint">Drop · paste · <em>Send to Assistent</em> from Generate</div>
</div>
<div class="sa-drop-overlay" id="sa_drop_overlay" hidden>Drop image</div>
<span class="sa-vision-chip" id="sa_vision_chip" hidden>vision</span>
</div>
<div class="sa-image-actions">
<button type="button" class="basic-button" id="sa_btn_refresh_image">Refresh preview</button>
<label class="sa-check"><input type="checkbox" id="sa_attach_vision" checked /> Attach to next message</label>
<button type="button" class="basic-button" id="sa_btn_use_current" title="Grab the current Generate preview">Use current</button>
<button type="button" class="basic-button" id="sa_btn_clear_image" title="Clear vision image">Clear</button>
<label class="sa-check"><input type="checkbox" id="sa_attach_vision" checked /> Attach next</label>
</div>
</aside>
<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>
@@ -36,9 +43,9 @@
</div>
<div class="sa-messages" id="sa_messages"></div>
<div class="sa-composer">
<textarea id="sa_input" rows="3" placeholder="Ask for a prompt, critique the image, change aspect…"></textarea>
<textarea id="sa_input" rows="3" placeholder="Ask for a prompt, critique the image, change aspect… (Enter = send, Shift+Enter = newline)"></textarea>
<div class="sa-composer-actions">
<button type="button" class="basic-button" id="sa_btn_send">Send</button>
<button type="button" class="basic-button sa-primary" id="sa_btn_send">Send</button>
<button type="button" class="basic-button" id="sa_btn_clear">Clear chat</button>
<span class="sa-status" id="sa_status"></span>
</div>