diff --git a/Assets/assistent.css b/Assets/assistent.css index 1e93c87..df63fcd 100644 --- a/Assets/assistent.css +++ b/Assets/assistent.css @@ -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; } } diff --git a/Assets/assistent.js b/Assets/assistent.js index 8dd5754..5f1272b 100644 --- a/Assets/assistent.js +++ b/Assets/assistent.js @@ -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; } - refreshImagePreview(); + 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') { diff --git a/README.md b/README.md index 19411f6..409b966 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/SwarmAssistentExtension.cs b/SwarmAssistentExtension.cs index 7d55436..28584be 100644 --- a/SwarmAssistentExtension.cs +++ b/SwarmAssistentExtension.cs @@ -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"]; } diff --git a/Tabs/Text2Image/Assistent.html b/Tabs/Text2Image/Assistent.html index ec2be17..b934645 100644 --- a/Tabs/Text2Image/Assistent.html +++ b/Tabs/Text2Image/Assistent.html @@ -3,16 +3,23 @@

Swarm Assistent is for Krea 2 models only. Select a Krea 2 checkpoint to enable the chat.

-