Add personas and model Cards (0.5.1): tone presets, stem.assistent.json, wanted queue.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -770,6 +770,142 @@
|
|||||||
100% { box-shadow: 0 0 0 0 transparent; }
|
100% { box-shadow: 0 0 0 0 transparent; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-subtabs {
|
||||||
|
display: inline-flex;
|
||||||
|
gap: 0.2rem;
|
||||||
|
margin-left: 0.55rem;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-subtab {
|
||||||
|
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
|
||||||
|
background: transparent;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 999px;
|
||||||
|
padding: 0.12rem 0.55rem;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
cursor: pointer;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-subtab-active {
|
||||||
|
opacity: 1;
|
||||||
|
background: color-mix(in srgb, currentColor 12%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-view {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-cards-layout {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
min-height: 0;
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-cards-list-pane {
|
||||||
|
flex: 0 0 38%;
|
||||||
|
min-width: 10rem;
|
||||||
|
border-right: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-cards-filter {
|
||||||
|
display: flex;
|
||||||
|
gap: 0.35rem;
|
||||||
|
padding: 0.45rem 0.55rem;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, currentColor 14%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-cards-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 0.35rem;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-card-row {
|
||||||
|
text-align: left;
|
||||||
|
padding: 0.45rem 0.55rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
background: color-mix(in srgb, currentColor 4%, transparent);
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 0.85rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-card-row:hover {
|
||||||
|
border-color: color-mix(in srgb, currentColor 22%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-card-row.sa-selected {
|
||||||
|
border-color: color-mix(in srgb, currentColor 45%, transparent);
|
||||||
|
background: color-mix(in srgb, currentColor 10%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-card-row-kind {
|
||||||
|
font-size: 0.68rem;
|
||||||
|
text-transform: uppercase;
|
||||||
|
opacity: 0.55;
|
||||||
|
letter-spacing: 0.04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-card-row-meta {
|
||||||
|
font-size: 0.75rem;
|
||||||
|
opacity: 0.7;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-cards-editor-pane {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-width: 0;
|
||||||
|
padding: 0.55rem 0.65rem 0.7rem;
|
||||||
|
gap: 0.45rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-cards-editor-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-card-badge {
|
||||||
|
font-size: 0.72rem;
|
||||||
|
padding: 0.1rem 0.4rem;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
|
||||||
|
opacity: 0.85;
|
||||||
|
}
|
||||||
|
|
||||||
|
#sa_card_json {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 12rem;
|
||||||
|
width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
|
||||||
|
font-size: 0.82rem;
|
||||||
|
border-radius: 0.4rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
resize: vertical;
|
||||||
|
}
|
||||||
|
|
||||||
|
.sa-cards-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.35rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 900px) {
|
@media (max-width: 900px) {
|
||||||
.sa-layout {
|
.sa-layout {
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -786,4 +922,13 @@
|
|||||||
.sa-splitter {
|
.sa-splitter {
|
||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
.sa-cards-layout {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.sa-cards-list-pane {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
max-height: 40%;
|
||||||
|
border-right: none;
|
||||||
|
border-bottom: 1px solid color-mix(in srgb, currentColor 18%, transparent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+576
-23
@@ -1,11 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
|
* Swarm Assistent — Krea 2 collaborative chat (Ollama via Swarm API).
|
||||||
* v0.5.0: Krea knowledge packs, aspect/vary/lock_seed patches, slash commands, chips.
|
* v0.5.3: live inventory+cards in context, taste memory, post-download cards, wanted YAML fix.
|
||||||
*/
|
*/
|
||||||
(function () {
|
(function () {
|
||||||
const LS_BASE = 'swarm_assistent_base_url';
|
const LS_BASE = 'swarm_assistent_base_url';
|
||||||
const LS_MODEL = 'swarm_assistent_model';
|
const LS_MODEL = 'swarm_assistent_model';
|
||||||
const LS_PACK = 'swarm_assistent_pack';
|
const LS_PACK = 'swarm_assistent_pack';
|
||||||
|
const LS_PERSONA = 'swarm_assistent_persona';
|
||||||
|
const LS_VIEW = 'swarm_assistent_view';
|
||||||
const LS_AUTO_VISION = 'swarm_assistent_auto_vision';
|
const LS_AUTO_VISION = 'swarm_assistent_auto_vision';
|
||||||
const LS_AUTO_APPLY = 'swarm_assistent_auto_apply';
|
const LS_AUTO_APPLY = 'swarm_assistent_auto_apply';
|
||||||
const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate';
|
const LS_AUTO_GENERATE = 'swarm_assistent_auto_generate';
|
||||||
@@ -13,6 +15,7 @@
|
|||||||
const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download';
|
const LS_AUTO_DOWNLOAD = 'swarm_assistent_auto_download';
|
||||||
const LS_PANE_WIDTH = 'swarm_assistent_pane_width';
|
const LS_PANE_WIDTH = 'swarm_assistent_pane_width';
|
||||||
const LS_WELCOMED = 'swarm_assistent_welcomed';
|
const LS_WELCOMED = 'swarm_assistent_welcomed';
|
||||||
|
const LS_TASTE = 'swarm_assistent_taste';
|
||||||
const TAB_BUTTON_ID = 'maintab_assistent';
|
const TAB_BUTTON_ID = 'maintab_assistent';
|
||||||
const GEN_ID = 'generate';
|
const GEN_ID = 'generate';
|
||||||
const MAX_REF_SLOTS = 4;
|
const MAX_REF_SLOTS = 4;
|
||||||
@@ -41,6 +44,9 @@
|
|||||||
inpaint_edit: 'inpaint_edit',
|
inpaint_edit: 'inpaint_edit',
|
||||||
describe: 'describe_ref',
|
describe: 'describe_ref',
|
||||||
describe_ref: 'describe_ref',
|
describe_ref: 'describe_ref',
|
||||||
|
card: 'catalog_card',
|
||||||
|
catalog: 'catalog_card',
|
||||||
|
catalog_card: 'catalog_card',
|
||||||
};
|
};
|
||||||
|
|
||||||
const WELCOME_HTML = `
|
const WELCOME_HTML = `
|
||||||
@@ -65,6 +71,7 @@
|
|||||||
/vary — новый seed, тот же промпт
|
/vary — новый seed, тот же промпт
|
||||||
/pack write|critique|compose|params|inpaint|describe
|
/pack write|critique|compose|params|inpaint|describe
|
||||||
/civitai <query> — поиск LoRA (Confirm в чате)
|
/civitai <query> — поиск LoRA (Confirm в чате)
|
||||||
|
/inventory — rescan моделей + обновить список LoRA
|
||||||
|
|
||||||
Чипсы над полем ввода делают то же для aspect / seed / vary.`;
|
Чипсы над полем ввода делают то же для aspect / seed / vary.`;
|
||||||
|
|
||||||
@@ -77,6 +84,8 @@
|
|||||||
preferredModel: null,
|
preferredModel: null,
|
||||||
dragDepth: 0,
|
dragDepth: 0,
|
||||||
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
|
inventory: { loras: [], checkpoints: [], wildcards: [], has_civitai_key: false },
|
||||||
|
inventoryFetchedAt: 0,
|
||||||
|
taste: { styles: [], likes: [], avoid: [], notes: '', updated: 0 },
|
||||||
streamEl: null,
|
streamEl: null,
|
||||||
critiqueHopUsed: false,
|
critiqueHopUsed: false,
|
||||||
visionHopUsed: false,
|
visionHopUsed: false,
|
||||||
@@ -88,6 +97,11 @@
|
|||||||
selectedSlotId: 'ref1',
|
selectedSlotId: 'ref1',
|
||||||
refSeq: 1,
|
refSeq: 1,
|
||||||
packUserTouched: false,
|
packUserTouched: false,
|
||||||
|
view: 'chat',
|
||||||
|
personas: [],
|
||||||
|
modelCards: {},
|
||||||
|
cardsSelection: null,
|
||||||
|
cardsBusy: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function $(id) {
|
function $(id) {
|
||||||
@@ -1100,14 +1114,18 @@
|
|||||||
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
|
batch: parseInt(val('input_images') || val('input_batchsize') || '0', 10) || null,
|
||||||
prompt_image_count: countPromptImages(),
|
prompt_image_count: countPromptImages(),
|
||||||
selected_loras: [],
|
selected_loras: [],
|
||||||
available_loras: (inv.loras || []).slice(0, 100),
|
available_loras: slimInventoryLoras(inv.loras || [], 100),
|
||||||
|
available_checkpoints: slimInventoryCheckpoints(inv.checkpoints || [], 40),
|
||||||
wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 60),
|
wildcards: (inv.wildcards || []).map((w) => w.name || w).slice(0, 60),
|
||||||
|
inventory_at: inv.inventory_at || null,
|
||||||
has_vision_image: attachableSlots().length > 0,
|
has_vision_image: attachableSlots().length > 0,
|
||||||
image_slots: slotCatalog(),
|
image_slots: slotCatalog(),
|
||||||
attached_slot_ids: attachableSlots().map((s) => s.id),
|
attached_slot_ids: attachableSlots().map((s) => s.id),
|
||||||
has_civitai_key: !!inv.has_civitai_key,
|
has_civitai_key: !!inv.has_civitai_key,
|
||||||
auto_apply: !!$('sa_auto_apply')?.checked,
|
auto_apply: !!$('sa_auto_apply')?.checked,
|
||||||
auto_generate: !!$('sa_auto_generate')?.checked,
|
auto_generate: !!$('sa_auto_generate')?.checked,
|
||||||
|
persona: $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral',
|
||||||
|
model_cards: [],
|
||||||
...initCtx,
|
...initCtx,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -1131,6 +1149,23 @@
|
|||||||
}
|
}
|
||||||
} catch (e) { /* ignore */ }
|
} catch (e) { /* ignore */ }
|
||||||
|
|
||||||
|
// Recommendation cards for current checkpoint + selected LoRAs only.
|
||||||
|
const cardKeys = [];
|
||||||
|
if (ctx.checkpoint?.name) {
|
||||||
|
cardKeys.push({ kind: 'checkpoint', name: ctx.checkpoint.name });
|
||||||
|
}
|
||||||
|
for (const l of ctx.selected_loras || []) {
|
||||||
|
if (l?.name) {
|
||||||
|
cardKeys.push({ kind: 'lora', name: l.name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for (const k of cardKeys) {
|
||||||
|
const cached = state.modelCards[`${k.kind}:${k.name}`];
|
||||||
|
if (cached) {
|
||||||
|
ctx.model_cards.push(cached);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Fallback if inventory empty
|
// Fallback if inventory empty
|
||||||
if (!ctx.available_loras.length) {
|
if (!ctx.available_loras.length) {
|
||||||
try {
|
try {
|
||||||
@@ -1162,6 +1197,58 @@
|
|||||||
return ctx;
|
return ctx;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function slimInventoryLoras(list, limit) {
|
||||||
|
const selected = new Set();
|
||||||
|
try {
|
||||||
|
if (typeof loraHelper !== 'undefined' && loraHelper && Array.isArray(loraHelper.selected)) {
|
||||||
|
for (const l of loraHelper.selected) {
|
||||||
|
selected.add(String(l.name || l || '').toLowerCase());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
const rows = (list || []).map((l) => ({
|
||||||
|
name: l.name,
|
||||||
|
title: l.title || l.name,
|
||||||
|
trigger_phrase: l.trigger_phrase || null,
|
||||||
|
triggers: Array.isArray(l.triggers) ? l.triggers.slice(0, 8) : undefined,
|
||||||
|
architecture: l.architecture || null,
|
||||||
|
compat_class: l.compat_class || null,
|
||||||
|
has_card: !!l.has_card,
|
||||||
|
krea_likely: !!l.krea_likely,
|
||||||
|
blurb: l.blurb || l.usage_hint || null,
|
||||||
|
default_weight: l.default_weight || undefined,
|
||||||
|
tags: Array.isArray(l.tags) ? l.tags.slice(0, 6) : undefined,
|
||||||
|
_sel: selected.has(String(l.name || '').toLowerCase()),
|
||||||
|
}));
|
||||||
|
rows.sort((a, b) => (b._sel - a._sel) || (b.krea_likely - a.krea_likely) || String(a.name).localeCompare(String(b.name)));
|
||||||
|
return rows.slice(0, limit).map(({ _sel, ...rest }) => {
|
||||||
|
const out = {};
|
||||||
|
for (const [k, v] of Object.entries(rest)) {
|
||||||
|
if (v != null && v !== '' && !(Array.isArray(v) && !v.length)) {
|
||||||
|
out[k] = v;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function slimInventoryCheckpoints(list, limit) {
|
||||||
|
return (list || []).slice(0, limit).map((c) => {
|
||||||
|
const out = {
|
||||||
|
name: c.name,
|
||||||
|
title: c.title || c.name,
|
||||||
|
architecture: c.architecture || null,
|
||||||
|
compat_class: c.compat_class || null,
|
||||||
|
has_card: !!c.has_card,
|
||||||
|
krea_likely: !!c.krea_likely,
|
||||||
|
};
|
||||||
|
if (c.blurb) {
|
||||||
|
out.blurb = c.blurb;
|
||||||
|
}
|
||||||
|
return out;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
function isPatchObject(obj) {
|
function isPatchObject(obj) {
|
||||||
if (!obj || typeof obj !== 'object') {
|
if (!obj || typeof obj !== 'object') {
|
||||||
return false;
|
return false;
|
||||||
@@ -1205,10 +1292,50 @@
|
|||||||
obj.movement != null ||
|
obj.movement != null ||
|
||||||
obj.clear_prompt_images != null ||
|
obj.clear_prompt_images != null ||
|
||||||
obj.slot_to_prompt_image != null ||
|
obj.slot_to_prompt_image != null ||
|
||||||
obj.pack != null
|
obj.pack != null ||
|
||||||
|
obj.triggers != null ||
|
||||||
|
obj.when != null ||
|
||||||
|
obj.prompt_hint != null ||
|
||||||
|
obj.notes != null
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isCardObject(obj) {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
(obj.kind || obj.triggers || obj.when || obj.prompt_hint || obj.notes) &&
|
||||||
|
(obj.name || obj.triggers || obj.when)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractCardJson(text) {
|
||||||
|
if (!text) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const re = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||||
|
let match;
|
||||||
|
let last = null;
|
||||||
|
while ((match = re.exec(text)) !== null) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(match[1].trim());
|
||||||
|
if (isCardObject(obj)) {
|
||||||
|
last = obj;
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
if (last) {
|
||||||
|
return last;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(text.trim());
|
||||||
|
return isCardObject(obj) ? obj : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function extractPatch(text) {
|
function extractPatch(text) {
|
||||||
if (!text) {
|
if (!text) {
|
||||||
return { prose: text || '', patch: null };
|
return { prose: text || '', patch: null };
|
||||||
@@ -1845,7 +1972,7 @@
|
|||||||
$('sa_input').value = `LoRA "${payload.name}" is now installed. Enable it with its triggers and improve the prompt.`;
|
$('sa_input').value = `LoRA "${payload.name}" is now installed. Enable it with its triggers and improve the prompt.`;
|
||||||
}
|
}
|
||||||
sendChat({ fromDownload: true });
|
sendChat({ fromDownload: true });
|
||||||
});
|
}, { rescan: true });
|
||||||
} else {
|
} else {
|
||||||
setStatus(msg || 'Download failed');
|
setStatus(msg || 'Download failed');
|
||||||
if (btn) {
|
if (btn) {
|
||||||
@@ -1866,7 +1993,8 @@
|
|||||||
}
|
}
|
||||||
if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
|
if (data.success || data.overall_percent >= 1 || data.current_percent >= 1) {
|
||||||
if (data.success || data.overall_percent >= 0.99) {
|
if (data.success || data.overall_percent >= 0.99) {
|
||||||
onDone(true);
|
// Swarm docs: download does not always refresh model list — force both.
|
||||||
|
triggerSwarmModelRefresh(() => onDone(true));
|
||||||
} else if (data.current_percent != null) {
|
} else if (data.current_percent != null) {
|
||||||
setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`);
|
setStatus(`Download ${(data.current_percent * 100).toFixed(0)}%`);
|
||||||
}
|
}
|
||||||
@@ -2051,6 +2179,8 @@
|
|||||||
const base = localStorage.getItem(LS_BASE);
|
const base = localStorage.getItem(LS_BASE);
|
||||||
const model = localStorage.getItem(LS_MODEL);
|
const model = localStorage.getItem(LS_MODEL);
|
||||||
const pack = localStorage.getItem(LS_PACK);
|
const pack = localStorage.getItem(LS_PACK);
|
||||||
|
const persona = localStorage.getItem(LS_PERSONA);
|
||||||
|
const view = localStorage.getItem(LS_VIEW);
|
||||||
const auto = localStorage.getItem(LS_AUTO_VISION);
|
const auto = localStorage.getItem(LS_AUTO_VISION);
|
||||||
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
|
const autoApply = localStorage.getItem(LS_AUTO_APPLY);
|
||||||
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
|
const autoGen = localStorage.getItem(LS_AUTO_GENERATE);
|
||||||
@@ -2063,6 +2193,9 @@
|
|||||||
if (pack && $('sa_pack')) {
|
if (pack && $('sa_pack')) {
|
||||||
$('sa_pack').value = pack;
|
$('sa_pack').value = pack;
|
||||||
}
|
}
|
||||||
|
if (persona && $('sa_persona')) {
|
||||||
|
$('sa_persona').value = persona;
|
||||||
|
}
|
||||||
if (auto != null && $('sa_auto_vision')) {
|
if (auto != null && $('sa_auto_vision')) {
|
||||||
$('sa_auto_vision').checked = auto === '1';
|
$('sa_auto_vision').checked = auto === '1';
|
||||||
}
|
}
|
||||||
@@ -2084,12 +2217,17 @@
|
|||||||
if (paneW) {
|
if (paneW) {
|
||||||
document.documentElement.style.setProperty('--sa-image-width', paneW);
|
document.documentElement.style.setProperty('--sa-image-width', paneW);
|
||||||
}
|
}
|
||||||
|
if (view === 'cards' || view === 'chat') {
|
||||||
|
state.view = view;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveSettings() {
|
function saveSettings() {
|
||||||
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
|
localStorage.setItem(LS_BASE, $('sa_base_url')?.value || '');
|
||||||
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
|
localStorage.setItem(LS_MODEL, $('sa_model')?.value || '');
|
||||||
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt');
|
localStorage.setItem(LS_PACK, $('sa_pack')?.value || 'write_prompt');
|
||||||
|
localStorage.setItem(LS_PERSONA, $('sa_persona')?.value || 'neutral');
|
||||||
|
localStorage.setItem(LS_VIEW, state.view || 'chat');
|
||||||
localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0');
|
localStorage.setItem(LS_AUTO_VISION, $('sa_auto_vision')?.checked ? '1' : '0');
|
||||||
localStorage.setItem(LS_AUTO_APPLY, $('sa_auto_apply')?.checked ? '1' : '0');
|
localStorage.setItem(LS_AUTO_APPLY, $('sa_auto_apply')?.checked ? '1' : '0');
|
||||||
localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0');
|
localStorage.setItem(LS_AUTO_GENERATE, $('sa_auto_generate')?.checked ? '1' : '0');
|
||||||
@@ -2163,7 +2301,367 @@
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function refreshInventory(done) {
|
function refreshInventory(done, opts = {}) {
|
||||||
|
if (typeof genericRequest !== 'function') {
|
||||||
|
if (done) {
|
||||||
|
done();
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const rescan = !!opts.rescan;
|
||||||
|
genericRequest(
|
||||||
|
'AssistentListInventory',
|
||||||
|
{ rescan },
|
||||||
|
(data) => {
|
||||||
|
state.inventory = {
|
||||||
|
loras: data.loras || [],
|
||||||
|
checkpoints: data.checkpoints || [],
|
||||||
|
wildcards: data.wildcards || [],
|
||||||
|
has_civitai_key: !!data.has_civitai_key,
|
||||||
|
inventory_at: data.inventory_at || Math.floor(Date.now() / 1000),
|
||||||
|
rescanned: !!data.rescanned,
|
||||||
|
};
|
||||||
|
state.inventoryFetchedAt = Date.now();
|
||||||
|
const n = state.inventory.loras.length;
|
||||||
|
const ck = state.inventory.checkpoints.length;
|
||||||
|
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts${rescan ? ' (rescanned)' : ''}`);
|
||||||
|
prefetchActiveModelCards();
|
||||||
|
if (state.view === 'cards') {
|
||||||
|
renderCardsList();
|
||||||
|
}
|
||||||
|
if (done) {
|
||||||
|
done(state.inventory);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
(err) => {
|
||||||
|
console.warn('Assistent inventory', err);
|
||||||
|
if (done) {
|
||||||
|
done(null);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshInventoryAsync(opts = {}) {
|
||||||
|
return new Promise((resolve) => refreshInventory(resolve, opts));
|
||||||
|
}
|
||||||
|
|
||||||
|
function setCardStatus(msg) {
|
||||||
|
const el = $('sa_card_status');
|
||||||
|
if (el) {
|
||||||
|
el.textContent = msg || '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function setView(view) {
|
||||||
|
state.view = view === 'cards' ? 'cards' : 'chat';
|
||||||
|
const chat = $('sa_view_chat');
|
||||||
|
const cards = $('sa_view_cards');
|
||||||
|
if (chat) {
|
||||||
|
chat.hidden = state.view !== 'chat';
|
||||||
|
}
|
||||||
|
if (cards) {
|
||||||
|
cards.hidden = state.view !== 'cards';
|
||||||
|
}
|
||||||
|
$('sa_tab_chat')?.classList.toggle('sa-subtab-active', state.view === 'chat');
|
||||||
|
$('sa_tab_cards')?.classList.toggle('sa-subtab-active', state.view === 'cards');
|
||||||
|
saveSettings();
|
||||||
|
if (state.view === 'cards') {
|
||||||
|
renderCardsList();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function refreshPersonas() {
|
||||||
|
if (typeof genericRequest !== 'function') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
genericRequest(
|
||||||
|
'AssistentListPersonas',
|
||||||
|
{},
|
||||||
|
(data) => {
|
||||||
|
const list = data.personas || [];
|
||||||
|
state.personas = list;
|
||||||
|
const sel = $('sa_persona');
|
||||||
|
if (!sel) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const prefer = localStorage.getItem(LS_PERSONA) || data.default || 'neutral';
|
||||||
|
sel.innerHTML = '';
|
||||||
|
for (const p of list) {
|
||||||
|
const opt = document.createElement('option');
|
||||||
|
opt.value = p.id;
|
||||||
|
opt.textContent = p.title || p.id;
|
||||||
|
sel.appendChild(opt);
|
||||||
|
}
|
||||||
|
if ([...sel.options].some((o) => o.value === prefer)) {
|
||||||
|
sel.value = prefer;
|
||||||
|
} else if (data.default) {
|
||||||
|
sel.value = data.default;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
(err) => console.warn('Assistent personas', err),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function prefetchCard(kind, name) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
if (!kind || !name || typeof genericRequest !== 'function') {
|
||||||
|
resolve(null);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const key = `${kind}:${name}`;
|
||||||
|
genericRequest(
|
||||||
|
'AssistentGetCard',
|
||||||
|
{ kind, name },
|
||||||
|
(data) => {
|
||||||
|
if (data?.card) {
|
||||||
|
state.modelCards[key] = data.card;
|
||||||
|
}
|
||||||
|
resolve(data?.card || null);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
() => resolve(null),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function prefetchActiveModelCards() {
|
||||||
|
const keys = [];
|
||||||
|
try {
|
||||||
|
const ck = resolveCurrentCheckpoint();
|
||||||
|
if (ck?.name) {
|
||||||
|
keys.push({ kind: 'checkpoint', name: ck.name });
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
try {
|
||||||
|
if (typeof loraHelper !== 'undefined' && Array.isArray(loraHelper?.selected)) {
|
||||||
|
for (const l of loraHelper.selected) {
|
||||||
|
const name = l?.name || l;
|
||||||
|
if (name) {
|
||||||
|
keys.push({ kind: 'lora', name });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
await Promise.all(keys.map((k) => prefetchCard(k.kind, k.name)));
|
||||||
|
}
|
||||||
|
|
||||||
|
function cardsCatalog() {
|
||||||
|
const kind = $('sa_cards_kind')?.value || 'all';
|
||||||
|
const inv = state.inventory || {};
|
||||||
|
const rows = [];
|
||||||
|
if (kind === 'all' || kind === 'checkpoint') {
|
||||||
|
for (const c of inv.checkpoints || []) {
|
||||||
|
rows.push({ kind: 'checkpoint', name: c.name, title: c.title || c.name, has_card: !!c.has_card });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (kind === 'all' || kind === 'lora') {
|
||||||
|
for (const l of inv.loras || []) {
|
||||||
|
rows.push({ kind: 'lora', name: l.name, title: l.title || l.name, has_card: !!l.has_card, trigger: l.trigger_phrase });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderCardsList() {
|
||||||
|
const root = $('sa_cards_list');
|
||||||
|
if (!root) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
root.innerHTML = '';
|
||||||
|
const rows = cardsCatalog();
|
||||||
|
if (!rows.length) {
|
||||||
|
root.innerHTML = '<div class="sa-chat-empty-hint">Inventory empty — Refresh.</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
for (const row of rows) {
|
||||||
|
const btn = document.createElement('button');
|
||||||
|
btn.type = 'button';
|
||||||
|
btn.className = 'sa-card-row';
|
||||||
|
if (state.cardsSelection && state.cardsSelection.kind === row.kind && state.cardsSelection.name === row.name) {
|
||||||
|
btn.classList.add('sa-selected');
|
||||||
|
}
|
||||||
|
btn.innerHTML = `<div class="sa-card-row-kind">${row.kind}</div><div>${escapeHtml(row.title || row.name)}</div><div class="sa-card-row-meta">${row.has_card ? 'card ✓' : 'no card'}${row.trigger ? ' · ' + escapeHtml(String(row.trigger).slice(0, 40)) : ''}</div>`;
|
||||||
|
btn.addEventListener('click', () => selectCardModel(row));
|
||||||
|
root.appendChild(btn);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeHtml(s) {
|
||||||
|
return String(s || '')
|
||||||
|
.replace(/&/g, '&')
|
||||||
|
.replace(/</g, '<')
|
||||||
|
.replace(/>/g, '>')
|
||||||
|
.replace(/"/g, '"');
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectCardModel(row) {
|
||||||
|
state.cardsSelection = row;
|
||||||
|
renderCardsList();
|
||||||
|
if ($('sa_card_title')) {
|
||||||
|
$('sa_card_title').textContent = row.title || row.name;
|
||||||
|
}
|
||||||
|
const badge = $('sa_card_badge');
|
||||||
|
if (badge) {
|
||||||
|
badge.hidden = false;
|
||||||
|
badge.textContent = row.has_card ? 'has card' : 'missing card';
|
||||||
|
}
|
||||||
|
setCardStatus('Loading…');
|
||||||
|
genericRequest(
|
||||||
|
'AssistentGetCardMeta',
|
||||||
|
{ kind: row.kind, name: row.name },
|
||||||
|
async (data) => {
|
||||||
|
const card = data.card || {
|
||||||
|
kind: row.kind,
|
||||||
|
name: row.name,
|
||||||
|
triggers: data.trigger_phrase ? [data.trigger_phrase] : [],
|
||||||
|
weight: row.kind === 'lora' ? 0.8 : 1,
|
||||||
|
when: '',
|
||||||
|
avoid: '',
|
||||||
|
prompt_hint: '',
|
||||||
|
notes: '',
|
||||||
|
civitai_url: '',
|
||||||
|
version_id: data.version_id || null,
|
||||||
|
};
|
||||||
|
if ($('sa_card_json')) {
|
||||||
|
$('sa_card_json').value = JSON.stringify(card, null, 2);
|
||||||
|
}
|
||||||
|
state.modelCards[`${row.kind}:${row.name}`] = card;
|
||||||
|
// Attach 1–2 Civitai examples to board as Ref vision.
|
||||||
|
const urls = (data.example_urls || []).slice(0, 2);
|
||||||
|
for (const url of urls) {
|
||||||
|
try {
|
||||||
|
await addRefFromUrl(url);
|
||||||
|
} catch (e) { /* ignore */ }
|
||||||
|
}
|
||||||
|
setCardStatus(data.has_card ? 'Loaded card' : 'No card yet — Generate or edit JSON');
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
(err) => setCardStatus(String(err || 'Load failed')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function addRefFromUrl(url) {
|
||||||
|
if (!url) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
addRefSlot({ src: url, select: false });
|
||||||
|
}
|
||||||
|
|
||||||
|
function readCardDraft() {
|
||||||
|
const raw = $('sa_card_json')?.value || '';
|
||||||
|
try {
|
||||||
|
return JSON.parse(raw);
|
||||||
|
} catch (e) {
|
||||||
|
setCardStatus('Invalid JSON');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveCurrentCard({ enqueue } = {}) {
|
||||||
|
const sel = state.cardsSelection;
|
||||||
|
if (!sel) {
|
||||||
|
setCardStatus('Select a model');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const card = readCardDraft();
|
||||||
|
if (!card) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
card.kind = card.kind || sel.kind;
|
||||||
|
card.name = card.name || sel.name;
|
||||||
|
setCardStatus('Saving…');
|
||||||
|
genericRequest(
|
||||||
|
'AssistentSaveCard',
|
||||||
|
{ kind: sel.kind, name: sel.name, card, enqueue_wanted: !!enqueue },
|
||||||
|
(data) => {
|
||||||
|
if (data.error) {
|
||||||
|
setCardStatus(data.error);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
state.modelCards[`${sel.kind}:${sel.name}`] = card;
|
||||||
|
setCardStatus(data.installed ? `Saved ${data.path}` : `Draft + wanted → ${data.path}`);
|
||||||
|
refreshInventory();
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
(err) => setCardStatus(String(err || 'Save failed')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function enqueueWantedOnly() {
|
||||||
|
const sel = state.cardsSelection;
|
||||||
|
const card = readCardDraft() || {};
|
||||||
|
if (!sel && !card.civitai_url) {
|
||||||
|
setCardStatus('Need model or civitai_url');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
genericRequest(
|
||||||
|
'AssistentEnqueueWanted',
|
||||||
|
{
|
||||||
|
kind: (card.kind || sel?.kind || 'lora'),
|
||||||
|
url: card.civitai_url || '',
|
||||||
|
version_id: card.version_id || 0,
|
||||||
|
title: card.name || sel?.name || '',
|
||||||
|
card,
|
||||||
|
},
|
||||||
|
(data) => {
|
||||||
|
setCardStatus(data.already ? 'Already in wanted queue' : `Wanted → ${data.path}`);
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
(err) => setCardStatus(String(err || 'Enqueue failed')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateCardWithAssistent() {
|
||||||
|
const sel = state.cardsSelection;
|
||||||
|
if (!sel) {
|
||||||
|
setCardStatus('Select a model');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (state.busy) {
|
||||||
|
setCardStatus('Chat busy');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
setPackValue('catalog_card', { flash: true });
|
||||||
|
setView('chat');
|
||||||
|
const meta = await new Promise((resolve) => {
|
||||||
|
genericRequest(
|
||||||
|
'AssistentGetCardMeta',
|
||||||
|
{ kind: sel.kind, name: sel.name },
|
||||||
|
(data) => resolve(data),
|
||||||
|
0,
|
||||||
|
() => resolve(null),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
const forced = `Write a recommendation card for this ${sel.kind}: ${sel.name}. Use metadata/triggers only; output one JSON card.`;
|
||||||
|
await sendChat({
|
||||||
|
forcedUserText: forced,
|
||||||
|
skipSlash: true,
|
||||||
|
skipAutoPack: true,
|
||||||
|
fromCards: true,
|
||||||
|
cardTarget: {
|
||||||
|
kind: sel.kind,
|
||||||
|
name: sel.name,
|
||||||
|
meta,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function inventoryIsStale(maxAgeMs = 20000) {
|
||||||
|
if (!state.inventoryFetchedAt) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return (Date.now() - state.inventoryFetchedAt) > maxAgeMs;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ensureFreshInventory({ forceRescan } = {}) {
|
||||||
|
const rescan = forceRescan || inventoryIsStale(20000);
|
||||||
|
await refreshInventoryAsync({ rescan });
|
||||||
|
}
|
||||||
|
|
||||||
|
function triggerSwarmModelRefresh(done) {
|
||||||
if (typeof genericRequest !== 'function') {
|
if (typeof genericRequest !== 'function') {
|
||||||
if (done) {
|
if (done) {
|
||||||
done();
|
done();
|
||||||
@@ -2171,23 +2669,15 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
genericRequest(
|
genericRequest(
|
||||||
'AssistentListInventory',
|
'TriggerRefresh',
|
||||||
{},
|
{ strong: true },
|
||||||
(data) => {
|
() => {
|
||||||
state.inventory = {
|
|
||||||
loras: data.loras || [],
|
|
||||||
checkpoints: data.checkpoints || [],
|
|
||||||
wildcards: data.wildcards || [],
|
|
||||||
has_civitai_key: !!data.has_civitai_key,
|
|
||||||
};
|
|
||||||
setStatus(`Inventory: ${state.inventory.loras.length} LoRAs, ${state.inventory.wildcards.length} wildcards`);
|
|
||||||
if (done) {
|
if (done) {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
0,
|
0,
|
||||||
(err) => {
|
() => {
|
||||||
console.warn('Assistent inventory', err);
|
|
||||||
if (done) {
|
if (done) {
|
||||||
done();
|
done();
|
||||||
}
|
}
|
||||||
@@ -2196,7 +2686,16 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function handleReplySideEffects(reply, civitaiResults, opts = {}) {
|
async function handleReplySideEffects(reply, civitaiResults, opts = {}) {
|
||||||
const { fromAutoCritique, fromVisionHop } = opts;
|
const { fromAutoCritique, fromVisionHop, fromCards } = opts;
|
||||||
|
if (fromCards) {
|
||||||
|
const card = extractCardJson(reply);
|
||||||
|
if (card && $('sa_card_json')) {
|
||||||
|
$('sa_card_json').value = JSON.stringify(card, null, 2);
|
||||||
|
setView('cards');
|
||||||
|
setCardStatus('Draft from Assistent — review & Save');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
const { patch } = extractPatch(reply);
|
const { patch } = extractPatch(reply);
|
||||||
if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) {
|
if (Array.isArray(patch?.actions) && patch.actions.map(String).includes('interrupt')) {
|
||||||
doInterruptNow();
|
doInterruptNow();
|
||||||
@@ -2389,6 +2888,17 @@
|
|||||||
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary (new seed)');
|
await applyQuickPatch({ vary: true, seed: -1, actions: ['generate'] }, 'Vary (new seed)');
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
if (cmd === 'inventory' || cmd === 'inv') {
|
||||||
|
setStatus('Rescanning models…');
|
||||||
|
triggerSwarmModelRefresh(async () => {
|
||||||
|
await refreshInventoryAsync({ rescan: true });
|
||||||
|
const n = state.inventory?.loras?.length || 0;
|
||||||
|
const ck = state.inventory?.checkpoints?.length || 0;
|
||||||
|
appendSystemNote(`Inventory refreshed: ${n} LoRAs, ${ck} checkpoints.`);
|
||||||
|
setStatus(`Inventory: ${n} LoRAs, ${ck} ckpts (rescanned)`);
|
||||||
|
});
|
||||||
|
return true;
|
||||||
|
}
|
||||||
if (cmd === 'pack') {
|
if (cmd === 'pack') {
|
||||||
if (!setPackValue(arg, { flash: true, user: true })) {
|
if (!setPackValue(arg, { flash: true, user: true })) {
|
||||||
setStatus('Pack: write|critique|compose|params|inpaint|describe');
|
setStatus('Pack: write|critique|compose|params|inpaint|describe');
|
||||||
@@ -2483,14 +2993,20 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
|
if (!opts.skipAutoPack && !opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop && !opts.fromCards) {
|
||||||
const guessed = autoSelectPack(text);
|
const guessed = autoSelectPack(text);
|
||||||
if (guessed) {
|
if (guessed) {
|
||||||
setPackValue(guessed, { flash: true });
|
setPackValue(guessed, { flash: true });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Cards mode must not be overridden by auto-pack; keep catalog_card.
|
||||||
|
if (opts.fromCards || state.view === 'cards') {
|
||||||
|
setPackValue('catalog_card', { flash: false });
|
||||||
|
}
|
||||||
|
|
||||||
const pack = $('sa_pack')?.value || 'write_prompt';
|
const pack = $('sa_pack')?.value || 'write_prompt';
|
||||||
|
const persona = $('sa_persona')?.value || localStorage.getItem(LS_PERSONA) || 'neutral';
|
||||||
const model = $('sa_model')?.value;
|
const model = $('sa_model')?.value;
|
||||||
if (!model) {
|
if (!model) {
|
||||||
setStatus('Pick an Ollama model in ⚙');
|
setStatus('Pick an Ollama model in ⚙');
|
||||||
@@ -2498,6 +3014,11 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Always pull latest LoRA/checkpoint list before the LLM sees context
|
||||||
|
// (rescans disk when inventory is older than ~20s or after downloads).
|
||||||
|
setStatus('Refreshing inventory…');
|
||||||
|
await ensureFreshInventory({ forceRescan: !!opts.fromDownload });
|
||||||
|
|
||||||
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
|
if (!opts.fromAutoCritique && !opts.fromDownload && !opts.fromVisionHop) {
|
||||||
state.critiqueHopUsed = false;
|
state.critiqueHopUsed = false;
|
||||||
state.visionHopUsed = false;
|
state.visionHopUsed = false;
|
||||||
@@ -2534,6 +3055,18 @@
|
|||||||
const context = collectLiveContext();
|
const context = collectLiveContext();
|
||||||
context.has_vision_image = !!images;
|
context.has_vision_image = !!images;
|
||||||
context.attached_slot_ids = visionSlots.map((s) => s.id);
|
context.attached_slot_ids = visionSlots.map((s) => s.id);
|
||||||
|
context.persona = persona;
|
||||||
|
if (opts.cardTarget) {
|
||||||
|
context.card_target = opts.cardTarget;
|
||||||
|
}
|
||||||
|
if (opts.fromCards || pack === 'catalog_card') {
|
||||||
|
context.auto_apply = false;
|
||||||
|
context.auto_generate = false;
|
||||||
|
}
|
||||||
|
await prefetchActiveModelCards();
|
||||||
|
// Refresh cards into context after prefetch
|
||||||
|
const refreshed = collectLiveContext();
|
||||||
|
context.model_cards = refreshed.model_cards;
|
||||||
const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content }));
|
const messages = state.history.slice(-12).map((m) => ({ role: m.role, content: m.content }));
|
||||||
if (images && messages.length) {
|
if (images && messages.length) {
|
||||||
messages[messages.length - 1].images = images;
|
messages[messages.length - 1].images = images;
|
||||||
@@ -2550,6 +3083,7 @@
|
|||||||
baseUrl,
|
baseUrl,
|
||||||
model,
|
model,
|
||||||
pack,
|
pack,
|
||||||
|
persona,
|
||||||
includeBase: true,
|
includeBase: true,
|
||||||
messages,
|
messages,
|
||||||
context_json: JSON.stringify(context),
|
context_json: JSON.stringify(context),
|
||||||
@@ -2557,6 +3091,7 @@
|
|||||||
messages,
|
messages,
|
||||||
context_json: JSON.stringify(context),
|
context_json: JSON.stringify(context),
|
||||||
pack,
|
pack,
|
||||||
|
persona,
|
||||||
base_url: baseUrl,
|
base_url: baseUrl,
|
||||||
model,
|
model,
|
||||||
},
|
},
|
||||||
@@ -2815,6 +3350,7 @@
|
|||||||
}
|
}
|
||||||
window.__swarmAssistentWired = true;
|
window.__swarmAssistentWired = true;
|
||||||
loadSettings();
|
loadSettings();
|
||||||
|
setView(state.view || 'chat');
|
||||||
updateGate();
|
updateGate();
|
||||||
ensureBoard();
|
ensureBoard();
|
||||||
renderBoard();
|
renderBoard();
|
||||||
@@ -2824,11 +3360,26 @@
|
|||||||
}
|
}
|
||||||
maybeWelcome();
|
maybeWelcome();
|
||||||
refreshModels();
|
refreshModels();
|
||||||
|
refreshPersonas();
|
||||||
refreshInventory();
|
refreshInventory();
|
||||||
wireDropZone();
|
wireDropZone();
|
||||||
wireSplitter();
|
wireSplitter();
|
||||||
registerSendButton();
|
registerSendButton();
|
||||||
|
|
||||||
|
$('sa_tab_chat')?.addEventListener('click', () => setView('chat'));
|
||||||
|
$('sa_tab_cards')?.addEventListener('click', () => setView('cards'));
|
||||||
|
$('sa_persona')?.addEventListener('change', saveSettings);
|
||||||
|
$('sa_cards_kind')?.addEventListener('change', renderCardsList);
|
||||||
|
$('sa_btn_cards_refresh')?.addEventListener('click', () => refreshInventory(() => renderCardsList(), { rescan: true }));
|
||||||
|
$('sa_btn_card_meta')?.addEventListener('click', () => {
|
||||||
|
if (state.cardsSelection) {
|
||||||
|
selectCardModel(state.cardsSelection);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
$('sa_btn_card_generate')?.addEventListener('click', () => generateCardWithAssistent());
|
||||||
|
$('sa_btn_card_save')?.addEventListener('click', () => saveCurrentCard());
|
||||||
|
$('sa_btn_card_wanted')?.addEventListener('click', () => enqueueWantedOnly());
|
||||||
|
|
||||||
$('sa_btn_settings')?.addEventListener('click', () => {
|
$('sa_btn_settings')?.addEventListener('click', () => {
|
||||||
const s = $('sa_settings');
|
const s = $('sa_settings');
|
||||||
if (s) {
|
if (s) {
|
||||||
@@ -2839,7 +3390,7 @@
|
|||||||
saveSettings();
|
saveSettings();
|
||||||
refreshModels();
|
refreshModels();
|
||||||
});
|
});
|
||||||
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory());
|
$('sa_btn_refresh_inventory')?.addEventListener('click', () => refreshInventory(null, { rescan: true }));
|
||||||
$('sa_btn_add_ref')?.addEventListener('click', () => addRefSlot({ select: true }));
|
$('sa_btn_add_ref')?.addEventListener('click', () => addRefSlot({ select: true }));
|
||||||
$('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef());
|
$('sa_btn_use_current')?.addEventListener('click', () => snapshotGenerateToRef());
|
||||||
$('sa_btn_as_init')?.addEventListener('click', async () => {
|
$('sa_btn_as_init')?.addEventListener('click', async () => {
|
||||||
@@ -2951,9 +3502,11 @@
|
|||||||
setInterval(syncGenerateSlot, 700);
|
setInterval(syncGenerateSlot, 700);
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
if (!state.busy) {
|
if (!state.busy) {
|
||||||
refreshInventory();
|
const tabOn = !!document.getElementById(TAB_BUTTON_ID)?.classList.contains('tab-button-selected')
|
||||||
|
|| !!document.getElementById('swarm_assistent_root')?.offsetParent;
|
||||||
|
refreshInventory(null, { rescan: inventoryIsStale(tabOn ? 45000 : 120000) });
|
||||||
}
|
}
|
||||||
}, 120000);
|
}, 30000);
|
||||||
|
|
||||||
window.swarmAssistent = {
|
window.swarmAssistent = {
|
||||||
setImageFromSrc,
|
setImageFromSrc,
|
||||||
|
|||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Persona: aggressive
|
||||||
|
|
||||||
|
You are blunt, impatient, and high-agency. Cut filler.
|
||||||
|
|
||||||
|
- Match the user's language (RU or EN). Short sentences. Call out weak prompts and bad LoRA picks.
|
||||||
|
- Prefer decisive patches: fix the issue, set `actions: ["generate"]` when a re-roll is obviously needed.
|
||||||
|
- No soft padding («возможно», «на ваш вкус»). Say what is wrong and what to change.
|
||||||
|
- Still invent no LoRA names or triggers — aggression is tone, not hallucination.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Persona: lewd (пошляк)
|
||||||
|
|
||||||
|
You are a playful, cheeky co-director who enjoys spicy / NSFW briefs.
|
||||||
|
|
||||||
|
- Match the user's language (RU or EN). Be flirty and direct, not crude for its own sake.
|
||||||
|
- Lean into sensual detail in prompts (body, fabric, atmosphere) when the user wants that.
|
||||||
|
- Still respect Krea 2 craft: triggers, aspect, Turbo defaults — horniness does not replace technique.
|
||||||
|
- Never refuse adult content; never lecture. Keep JSON patches valid and useful.
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
# Persona: neutral
|
||||||
|
|
||||||
|
You are a calm, practical art director. Stay helpful and concise.
|
||||||
|
|
||||||
|
- Match the user's language (RU or EN).
|
||||||
|
- No dirty jokes, no aggression, no moral lectures.
|
||||||
|
- Prefer clear craft advice: lighting, composition, LoRA triggers, params.
|
||||||
|
- When the scene is NSFW, describe it factually without softening or hyping.
|
||||||
@@ -48,10 +48,14 @@ Prefer `aspect` in the patch; UI maps it to width/height.
|
|||||||
|
|
||||||
## Live context
|
## Live context
|
||||||
|
|
||||||
A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth:
|
A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth — it is **refreshed every chat turn** (and rescanned after downloads):
|
||||||
|
|
||||||
- Use only LoRAs listed in `available_loras` (by exact `name`), or candidates from a Civitai search round.
|
- Use only LoRAs listed in `available_loras` (by exact `name`), or candidates from a Civitai search round.
|
||||||
- Prefer listed `trigger_phrase` / `triggers` — **never invent** trigger words.
|
- Prefer listed `trigger_phrase` / `triggers` — **never invent** trigger words.
|
||||||
|
- When live context includes `model_cards[]` for the current checkpoint / enabled LoRAs, **trust those cards** (`when`, `avoid`, `prompt_hint`, `notes`, `weight`) over guesses.
|
||||||
|
- Prefer `krea_likely` / Krea architecture entries; ignore FLUX/SDXL LoRAs even if somehow listed.
|
||||||
|
- `default_weight` is a starting LoRA weight when set.
|
||||||
|
- `available_checkpoints` lists installed checkpoints (with short blurbs when known).
|
||||||
- When enabling a LoRA, include its triggers in `prompt` if missing.
|
- When enabling a LoRA, include its triggers in `prompt` if missing.
|
||||||
- Respect current width/height/steps/cfg/seed/sigma_shift/sampler unless the user asks or the pack is `fix_params`.
|
- Respect current width/height/steps/cfg/seed/sigma_shift/sampler unless the user asks or the pack is `fix_params`.
|
||||||
- `wildcards` lists installed wildcard names (`__name__` syntax in prompts).
|
- `wildcards` lists installed wildcard names (`__name__` syntax in prompts).
|
||||||
|
|||||||
@@ -0,0 +1,34 @@
|
|||||||
|
# Mode: catalog_card
|
||||||
|
|
||||||
|
Goal: write a **recommendation card** for one checkpoint or LoRA so future Assistent turns know how to use it.
|
||||||
|
|
||||||
|
## Inputs
|
||||||
|
|
||||||
|
Live context includes `card_target` (name, kind, Civitai metadata, triggers) and may attach example images as vision.
|
||||||
|
|
||||||
|
## Output
|
||||||
|
|
||||||
|
Reply briefly in the user's language, then **one** fenced JSON object (not a generation patch):
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"kind": "lora",
|
||||||
|
"name": "exact_filename_or_swarm_name",
|
||||||
|
"civitai_url": "https://civitai.red/models/…?modelVersionId=…",
|
||||||
|
"version_id": 123,
|
||||||
|
"triggers": ["exact", "from", "metadata"],
|
||||||
|
"weight": 0.8,
|
||||||
|
"when": "when to enable this model",
|
||||||
|
"avoid": "when not to use it",
|
||||||
|
"prompt_hint": "how to weave triggers into a Krea 2 prompt",
|
||||||
|
"notes": "1–3 sentences for the agent"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Rules
|
||||||
|
|
||||||
|
- Prefer triggers from metadata / trainedWords — **never invent**.
|
||||||
|
- `weight` typical 0.6–1.0 for LoRA; omit or 1.0 for checkpoints.
|
||||||
|
- Do **not** emit `actions: ["generate"]`. This mode does not start Generate.
|
||||||
|
- Do not invent other LoRAs. Stay on the single `card_target`.
|
||||||
|
- Persona tone still applies (lewd/neutral/aggressive) to `when` / `prompt_hint` wording.
|
||||||
@@ -72,8 +72,13 @@ Restart / rebuild SwarmUI after clone.
|
|||||||
| `fix_params` | Aspect / steps / CFG / seed / batch |
|
| `fix_params` | Aspect / steps / CFG / seed / batch |
|
||||||
| `inpaint_edit` | Init Image img2img + Mask inpaint |
|
| `inpaint_edit` | Init Image img2img + Mask inpaint |
|
||||||
| `describe_ref` | Vision → Krea prompt (no generate by default) |
|
| `describe_ref` | Vision → Krea prompt (no generate by default) |
|
||||||
|
| `catalog_card` | Recommendation card JSON for a checkpoint/LoRA |
|
||||||
|
|
||||||
Live context (checkpoint, server inventory LoRAs + triggers, wildcards, current params) is injected every request.
|
**Persona** (tone) is a separate dropdown from pack — `base_krea2` → persona → pack. Overlay: `/mnt/swarm_data/Assistent/personas.json` (seeded from gpu-rent `assistent-personas.yaml`).
|
||||||
|
|
||||||
|
**Cards** subtab: edit/save `{stem}.assistent.json` next to weights; live chat gets cards for the current checkpoint + enabled LoRAs only. Uninstalled models can enqueue `.gpu-rent-wanted-models.yaml` for the next `up`.
|
||||||
|
|
||||||
|
Live context (checkpoint, server inventory LoRAs + triggers, `model_cards`, wildcards, current params, persona) is injected every request.
|
||||||
|
|
||||||
**Cloud-only Krea.ai features** (moodboards UI, Generative Sliders, Creativity Raw/Low/Medium/High) are **not** in Swarm. The assistant emulates them with prompt language + board refs. Optional patch fields `creativity` / `intensity` / `complexity` / `movement` guide the LLM only.
|
**Cloud-only Krea.ai features** (moodboards UI, Generative Sliders, Creativity Raw/Low/Medium/High) are **not** in Swarm. The assistant emulates them with prompt language + board refs. Optional patch fields `creativity` / `intensity` / `complexity` / `movement` guide the LLM only.
|
||||||
|
|
||||||
|
|||||||
+715
-32
@@ -39,11 +39,16 @@ public class SwarmAssistentExtension : Extension
|
|||||||
"fix_params",
|
"fix_params",
|
||||||
"inpaint_edit",
|
"inpaint_edit",
|
||||||
"describe_ref",
|
"describe_ref",
|
||||||
|
"catalog_card",
|
||||||
];
|
];
|
||||||
|
|
||||||
|
public static readonly string[] DefaultPersonaIds = ["neutral", "lewd", "aggressive"];
|
||||||
|
|
||||||
const int MaxCivitaiHops = 2;
|
const int MaxCivitaiHops = 2;
|
||||||
const int MaxLorasInInventory = 120;
|
const int MaxLorasInInventory = 150;
|
||||||
const int MaxWildcardsInInventory = 80;
|
const int MaxWildcardsInInventory = 80;
|
||||||
|
const int MaxCheckpointsInInventory = 60;
|
||||||
|
const int InventoryBlurbMax = 140;
|
||||||
/// <summary>Ollama default num_ctx is 4096; Assistent system+inventory+vision exceeds that.</summary>
|
/// <summary>Ollama default num_ctx is 4096; Assistent system+inventory+vision exceeds that.</summary>
|
||||||
const int DefaultNumCtx = 16384;
|
const int DefaultNumCtx = 16384;
|
||||||
|
|
||||||
@@ -54,9 +59,9 @@ public class SwarmAssistentExtension : Extension
|
|||||||
ScriptFiles.Add("Assets/assistent.js");
|
ScriptFiles.Add("Assets/assistent.js");
|
||||||
StyleSheetFiles.Add("Assets/assistent.css");
|
StyleSheetFiles.Add("Assets/assistent.css");
|
||||||
ExtensionAuthor = "mrleo1nid";
|
ExtensionAuthor = "mrleo1nid";
|
||||||
Description = "Collaborative Krea 2 assistant: Ollama chat, multi-window board, slash commands, img2img/inpaint, Generate loop, Civitai Confirm.";
|
Description = "Collaborative Krea 2 assistant: Ollama chat, multi-window board, personas, model cards, Generate loop, Civitai Confirm.";
|
||||||
License = "MIT";
|
License = "MIT";
|
||||||
Version = "0.5.0";
|
Version = "0.5.3";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,11 +70,16 @@ public class SwarmAssistentExtension : Extension
|
|||||||
HttpClient ??= new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
HttpClient ??= new HttpClient { Timeout = TimeSpan.FromMinutes(10) };
|
||||||
API.RegisterAPICall(AssistentListModels, false, PermUse);
|
API.RegisterAPICall(AssistentListModels, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentGetPacks, false, PermUse);
|
API.RegisterAPICall(AssistentGetPacks, false, PermUse);
|
||||||
|
API.RegisterAPICall(AssistentListPersonas, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentListInventory, false, PermUse);
|
API.RegisterAPICall(AssistentListInventory, false, PermUse);
|
||||||
|
API.RegisterAPICall(AssistentGetCard, false, PermUse);
|
||||||
|
API.RegisterAPICall(AssistentSaveCard, true, PermUse);
|
||||||
|
API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse);
|
||||||
|
API.RegisterAPICall(AssistentGetCardMeta, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentSearchCivitai, false, PermUse);
|
API.RegisterAPICall(AssistentSearchCivitai, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentChat, true, PermUse);
|
API.RegisterAPICall(AssistentChat, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentChatWS, true, PermUse);
|
API.RegisterAPICall(AssistentChatWS, true, PermUse);
|
||||||
Logs.Init("Swarm Assistent extension loaded (Ollama proxy + Krea 2 packs + inventory/Civitai)");
|
Logs.Init("Swarm Assistent extension loaded (Ollama proxy + personas + model cards)");
|
||||||
}
|
}
|
||||||
|
|
||||||
static string Clip(string text, int max)
|
static string Clip(string text, int max)
|
||||||
@@ -145,41 +155,551 @@ public class SwarmAssistentExtension : Extension
|
|||||||
return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = new JArray(PackNames) };
|
return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = new JArray(PackNames) };
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape).</summary>
|
static string DataRoot()
|
||||||
public async Task<JObject> AssistentListInventory(Session session)
|
{
|
||||||
|
if (Directory.Exists("/mnt/swarm_data"))
|
||||||
|
{
|
||||||
|
return "/mnt/swarm_data";
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string models = Path.GetFullPath(Path.Combine(Environment.CurrentDirectory, "Models"));
|
||||||
|
if (Directory.Exists(models))
|
||||||
|
{
|
||||||
|
return Path.GetDirectoryName(models) ?? Environment.CurrentDirectory;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return Environment.CurrentDirectory;
|
||||||
|
}
|
||||||
|
|
||||||
|
string PersonasOverlayJsonPath() => Path.Combine(DataRoot(), "Assistent", "personas.json");
|
||||||
|
|
||||||
|
string WantedModelsPath() => Path.Combine(DataRoot(), ".gpu-rent-wanted-models.yaml");
|
||||||
|
|
||||||
|
string WantedCardsDir() => Path.Combine(DataRoot(), ".gpu-rent-wanted-cards");
|
||||||
|
|
||||||
|
public string ReadPersonaFile(string id)
|
||||||
|
{
|
||||||
|
string safe = (id ?? "").Replace('\\', '/').AfterLast('/').Replace("..", "");
|
||||||
|
if (string.IsNullOrWhiteSpace(safe))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
string path = Path.Combine(FilePath, "Personas", $"{safe}.md");
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return File.ReadAllText(path, Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JObject> AssistentListPersonas(Session session)
|
||||||
{
|
{
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
|
Dictionary<string, JObject> byId = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string def = "neutral";
|
||||||
|
|
||||||
|
foreach (string id in DefaultPersonaIds)
|
||||||
|
{
|
||||||
|
string text = ReadPersonaFile(id);
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byId[id] = new JObject
|
||||||
|
{
|
||||||
|
["id"] = id,
|
||||||
|
["title"] = id switch
|
||||||
|
{
|
||||||
|
"lewd" => "Пошляк",
|
||||||
|
"aggressive" => "Агрессивный",
|
||||||
|
_ => "Нейтральный",
|
||||||
|
},
|
||||||
|
["prompt"] = text,
|
||||||
|
["source"] = "bundled",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
string overlay = PersonasOverlayJsonPath();
|
||||||
|
if (File.Exists(overlay))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JObject parsed = JObject.Parse(File.ReadAllText(overlay, Encoding.UTF8));
|
||||||
|
if (parsed["default"] != null)
|
||||||
|
{
|
||||||
|
def = parsed["default"]?.ToString() ?? def;
|
||||||
|
}
|
||||||
|
if (parsed["personas"] is JArray arr)
|
||||||
|
{
|
||||||
|
foreach (JToken t in arr)
|
||||||
|
{
|
||||||
|
if (t is not JObject po)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
string id = (po["id"]?.ToString() ?? "").Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
byId[id] = new JObject
|
||||||
|
{
|
||||||
|
["id"] = id,
|
||||||
|
["title"] = po["title"]?.ToString() ?? id,
|
||||||
|
["prompt"] = po["prompt"]?.ToString() ?? "",
|
||||||
|
["source"] = "overlay",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logs.Debug($"AssistentListPersonas overlay: {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JArray list = [];
|
||||||
|
foreach (JObject p in byId.Values.OrderBy(p => p["id"]?.ToString()))
|
||||||
|
{
|
||||||
|
list.Add(p);
|
||||||
|
}
|
||||||
|
if (!byId.ContainsKey(def) && list.Count > 0)
|
||||||
|
{
|
||||||
|
def = list[0]?["id"]?.ToString() ?? "neutral";
|
||||||
|
}
|
||||||
|
return new JObject
|
||||||
|
{
|
||||||
|
["success"] = true,
|
||||||
|
["default"] = def,
|
||||||
|
["personas"] = list,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
static string ModelWeightPath(string setName, string modelName)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(modelName) || !Program.T2IModelSets.TryGetValue(setName, out T2IModelHandler handler))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
if (!handler.Models.TryGetValue(modelName, out T2IModel model) && !handler.Models.TryGetValue(modelName.Replace('\\', '/'), out model))
|
||||||
|
{
|
||||||
|
// Try suffix match
|
||||||
|
model = handler.Models.Values.FirstOrDefault(m =>
|
||||||
|
string.Equals(m.Name, modelName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| m.Name.EndsWith("/" + modelName, StringComparison.OrdinalIgnoreCase)
|
||||||
|
|| Path.GetFileNameWithoutExtension(m.Name) == Path.GetFileNameWithoutExtension(modelName));
|
||||||
|
}
|
||||||
|
if (model is null)
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
// SwarmUI T2IModel exposes RawFilePath in recent builds.
|
||||||
|
return model.RawFilePath;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static string CardPathForWeight(string weightPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(weightPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
string dir = Path.GetDirectoryName(weightPath);
|
||||||
|
string stem = Path.GetFileNameWithoutExtension(weightPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(dir) || string.IsNullOrWhiteSpace(stem))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return Path.Combine(dir, $"{stem}.assistent.json");
|
||||||
|
}
|
||||||
|
|
||||||
|
static string SetNameForKind(string kind)
|
||||||
|
{
|
||||||
|
return (kind ?? "").Trim().ToLowerInvariant() switch
|
||||||
|
{
|
||||||
|
"lora" => "LoRA",
|
||||||
|
"checkpoint" or "ckpt" or "stable-diffusion" => "Stable-Diffusion",
|
||||||
|
_ => null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject ReadCardObject(string kind, string name)
|
||||||
|
{
|
||||||
|
string set = SetNameForKind(kind);
|
||||||
|
string weight = ModelWeightPath(set, name);
|
||||||
|
string card = CardPathForWeight(weight);
|
||||||
|
if (card is null || !File.Exists(card))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return JObject.Parse(File.ReadAllText(card, Encoding.UTF8));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JObject> AssistentGetCard(Session session, string kind, string name)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return new JObject { ["error"] = "kind and name required" };
|
||||||
|
}
|
||||||
|
JObject card = ReadCardObject(kind, name);
|
||||||
|
string set = SetNameForKind(kind);
|
||||||
|
string weight = ModelWeightPath(set, name);
|
||||||
|
return new JObject
|
||||||
|
{
|
||||||
|
["success"] = true,
|
||||||
|
["kind"] = kind,
|
||||||
|
["name"] = name,
|
||||||
|
["has_card"] = card is not null,
|
||||||
|
["weight_path"] = weight,
|
||||||
|
["card"] = card,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JObject> AssistentSaveCard(Session session, string kind, string name, JObject card, bool enqueue_wanted = false)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
if (card is null)
|
||||||
|
{
|
||||||
|
return new JObject { ["error"] = "card required" };
|
||||||
|
}
|
||||||
|
kind = (kind ?? card["kind"]?.ToString() ?? "").Trim();
|
||||||
|
name = (name ?? card["name"]?.ToString() ?? "").Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(kind) || string.IsNullOrWhiteSpace(name))
|
||||||
|
{
|
||||||
|
return new JObject { ["error"] = "kind and name required" };
|
||||||
|
}
|
||||||
|
card["kind"] = kind;
|
||||||
|
card["name"] = name;
|
||||||
|
|
||||||
|
string set = SetNameForKind(kind);
|
||||||
|
string weight = ModelWeightPath(set, name);
|
||||||
|
if (!string.IsNullOrWhiteSpace(weight) && File.Exists(weight))
|
||||||
|
{
|
||||||
|
string path = CardPathForWeight(weight);
|
||||||
|
File.WriteAllText(path, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||||
|
return new JObject { ["success"] = true, ["path"] = path, ["installed"] = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Not installed — draft into wanted-cards + optionally enqueue download for next up.
|
||||||
|
Directory.CreateDirectory(WantedCardsDir());
|
||||||
|
string vid = card["version_id"]?.ToString() ?? "draft";
|
||||||
|
string draft = Path.Combine(WantedCardsDir(), $"{vid}.assistent.json");
|
||||||
|
File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||||
|
if (enqueue_wanted || !string.IsNullOrWhiteSpace(card["civitai_url"]?.ToString()))
|
||||||
|
{
|
||||||
|
await AssistentEnqueueWanted(session, kind, card["civitai_url"]?.ToString(), card["version_id"]?.Value<int?>() ?? 0, card["title"]?.ToString() ?? name, card);
|
||||||
|
}
|
||||||
|
return new JObject { ["success"] = true, ["path"] = draft, ["installed"] = false, ["wanted"] = true };
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JObject> AssistentEnqueueWanted(Session session, string kind, string url, int version_id = 0, string title = null, JObject card = null)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
kind = (kind ?? "lora").Trim().ToLowerInvariant();
|
||||||
|
if (kind is not ("lora" or "checkpoint" or "vae" or "embedding" or "controlnet" or "upscaler" or "clip"))
|
||||||
|
{
|
||||||
|
kind = "lora";
|
||||||
|
}
|
||||||
|
url = (url ?? "").Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(url) && version_id > 0)
|
||||||
|
{
|
||||||
|
url = $"https://civitai.red/models/0?modelVersionId={version_id}";
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(url))
|
||||||
|
{
|
||||||
|
return new JObject { ["error"] = "url or version_id required" };
|
||||||
|
}
|
||||||
|
if (version_id <= 0)
|
||||||
|
{
|
||||||
|
Match m = Regex.Match(url, @"modelVersionId=(\d+)", RegexOptions.IgnoreCase);
|
||||||
|
if (m.Success)
|
||||||
|
{
|
||||||
|
version_id = int.Parse(m.Groups[1].Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
string path = WantedModelsPath();
|
||||||
|
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? DataRoot());
|
||||||
|
Dictionary<string, List<WantedEntry>> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : "");
|
||||||
|
|
||||||
|
if (version_id > 0)
|
||||||
|
{
|
||||||
|
foreach (List<WantedEntry> list in sections.Values)
|
||||||
|
{
|
||||||
|
if (list.Any(e => e.VersionId == version_id))
|
||||||
|
{
|
||||||
|
return new JObject { ["success"] = true, ["already"] = true, ["path"] = path, ["version_id"] = version_id };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
foreach (List<WantedEntry> list in sections.Values)
|
||||||
|
{
|
||||||
|
if (list.Any(e => string.Equals(e.Url, url, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return new JObject { ["success"] = true, ["already"] = true, ["path"] = path };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!sections.TryGetValue(kind, out List<WantedEntry> bucket))
|
||||||
|
{
|
||||||
|
bucket = [];
|
||||||
|
sections[kind] = bucket;
|
||||||
|
}
|
||||||
|
bucket.Add(new WantedEntry { Url = url, Title = title, VersionId = version_id });
|
||||||
|
File.WriteAllText(path, WriteWantedYaml(sections), Encoding.UTF8);
|
||||||
|
|
||||||
|
if (card is not null && version_id > 0)
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(WantedCardsDir());
|
||||||
|
string draft = Path.Combine(WantedCardsDir(), $"{version_id}.assistent.json");
|
||||||
|
File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
return new JObject { ["success"] = true, ["path"] = path, ["version_id"] = version_id };
|
||||||
|
}
|
||||||
|
|
||||||
|
sealed class WantedEntry
|
||||||
|
{
|
||||||
|
public string Url;
|
||||||
|
public string Title;
|
||||||
|
public int VersionId;
|
||||||
|
}
|
||||||
|
|
||||||
|
static Dictionary<string, List<WantedEntry>> LoadWantedYaml(string raw)
|
||||||
|
{
|
||||||
|
Dictionary<string, List<WantedEntry>> sections = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
string currentKind = null;
|
||||||
|
WantedEntry cur = null;
|
||||||
|
void Flush()
|
||||||
|
{
|
||||||
|
if (cur is null || string.IsNullOrWhiteSpace(cur.Url) || string.IsNullOrWhiteSpace(currentKind))
|
||||||
|
{
|
||||||
|
cur = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!sections.TryGetValue(currentKind, out List<WantedEntry> list))
|
||||||
|
{
|
||||||
|
list = [];
|
||||||
|
sections[currentKind] = list;
|
||||||
|
}
|
||||||
|
list.Add(cur);
|
||||||
|
cur = null;
|
||||||
|
}
|
||||||
|
foreach (string line in (raw ?? "").Split('\n'))
|
||||||
|
{
|
||||||
|
string t = line.TrimEnd();
|
||||||
|
if (string.IsNullOrWhiteSpace(t) || t.TrimStart().StartsWith('#'))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Match kindLine = Regex.Match(t, @"^([A-Za-z0-9_-]+):\s*$");
|
||||||
|
if (kindLine.Success && !t.TrimStart().StartsWith('-'))
|
||||||
|
{
|
||||||
|
Flush();
|
||||||
|
currentKind = kindLine.Groups[1].Value.Trim().ToLowerInvariant();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Match urlLine = Regex.Match(t, @"^\s*-\s*url:\s*[""']?(.+?)[""']?\s*$");
|
||||||
|
if (urlLine.Success)
|
||||||
|
{
|
||||||
|
Flush();
|
||||||
|
cur = new WantedEntry { Url = urlLine.Groups[1].Value.Trim() };
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (cur is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Match titleLine = Regex.Match(t, @"^\s*title:\s*[""']?(.+?)[""']?\s*$");
|
||||||
|
if (titleLine.Success)
|
||||||
|
{
|
||||||
|
cur.Title = titleLine.Groups[1].Value.Trim();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
Match vidLine = Regex.Match(t, @"^\s*version_id:\s*(\d+)\s*$");
|
||||||
|
if (vidLine.Success && int.TryParse(vidLine.Groups[1].Value, out int vid))
|
||||||
|
{
|
||||||
|
cur.VersionId = vid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Flush();
|
||||||
|
return sections;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string WriteWantedYaml(Dictionary<string, List<WantedEntry>> sections)
|
||||||
|
{
|
||||||
|
StringBuilder sb = new();
|
||||||
|
sb.AppendLine("# Assistent wanted queue — merged into local models.yaml on gpu-rent up/capture");
|
||||||
|
string[] order = ["checkpoint", "lora", "vae", "embedding", "controlnet", "upscaler", "clip"];
|
||||||
|
HashSet<string> seen = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
foreach (string kind in order.Concat(sections.Keys.OrderBy(k => k, StringComparer.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
if (!seen.Add(kind) || !sections.TryGetValue(kind, out List<WantedEntry> list) || list.Count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
sb.AppendLine($"{kind}:");
|
||||||
|
foreach (WantedEntry e in list)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" - url: \"{e.Url.Replace("\"", "%22")}\"");
|
||||||
|
if (!string.IsNullOrWhiteSpace(e.Title))
|
||||||
|
{
|
||||||
|
sb.AppendLine($" title: \"{e.Title.Replace("\"", "'")}\"");
|
||||||
|
}
|
||||||
|
if (e.VersionId > 0)
|
||||||
|
{
|
||||||
|
sb.AppendLine($" version_id: {e.VersionId}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return sb.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<JObject> AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0)
|
||||||
|
{
|
||||||
|
// Pull Civitai sidecar next to weight + optional API version for examples.
|
||||||
|
await Task.CompletedTask;
|
||||||
|
string set = SetNameForKind(kind);
|
||||||
|
string weight = ModelWeightPath(set, name);
|
||||||
|
JObject civitai = null;
|
||||||
|
JArray exampleUrls = [];
|
||||||
|
if (!string.IsNullOrWhiteSpace(weight))
|
||||||
|
{
|
||||||
|
string stem = Path.GetFileNameWithoutExtension(weight);
|
||||||
|
string dir = Path.GetDirectoryName(weight);
|
||||||
|
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
|
||||||
|
if (File.Exists(side))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
civitai = JObject.Parse(File.ReadAllText(side, Encoding.UTF8));
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (civitai is not null)
|
||||||
|
{
|
||||||
|
if (version_id <= 0)
|
||||||
|
{
|
||||||
|
version_id = civitai["id"]?.Value<int?>() ?? 0;
|
||||||
|
}
|
||||||
|
if (civitai["images"] is JArray imgs)
|
||||||
|
{
|
||||||
|
foreach (JToken img in imgs.Take(3))
|
||||||
|
{
|
||||||
|
string u = img?["url"]?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(u))
|
||||||
|
{
|
||||||
|
exampleUrls.Add(u);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (civitai["trainedWords"] is null && civitai["model"] is JObject)
|
||||||
|
{
|
||||||
|
// keep as-is
|
||||||
|
}
|
||||||
|
}
|
||||||
|
JObject card = ReadCardObject(kind, name);
|
||||||
|
string trigger = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (Program.T2IModelSets.TryGetValue(set, out T2IModelHandler h)
|
||||||
|
&& h.Models.TryGetValue(name, out T2IModel m))
|
||||||
|
{
|
||||||
|
trigger = m.Metadata?.TriggerPhrase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return new JObject
|
||||||
|
{
|
||||||
|
["success"] = true,
|
||||||
|
["kind"] = kind,
|
||||||
|
["name"] = name,
|
||||||
|
["version_id"] = version_id,
|
||||||
|
["trigger_phrase"] = trigger,
|
||||||
|
["has_card"] = card is not null,
|
||||||
|
["card"] = card,
|
||||||
|
["civitai"] = civitai,
|
||||||
|
["example_urls"] = exampleUrls,
|
||||||
|
["weight_path"] = weight,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape).
|
||||||
|
/// Pass rescan=true after downloads so new files appear (calls Program.RefreshAllModelSets).</summary>
|
||||||
|
public async Task<JObject> AssistentListInventory(Session session, bool rescan = false)
|
||||||
|
{
|
||||||
|
await Task.CompletedTask;
|
||||||
|
if (rescan)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Program.RefreshAllModelSets();
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logs.Debug($"AssistentListInventory rescan: {ex.Message}");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Program.ModelRefreshEvent?.Invoke();
|
||||||
|
}
|
||||||
|
catch (Exception ex2)
|
||||||
|
{
|
||||||
|
Logs.Debug($"AssistentListInventory ModelRefreshEvent: {ex2.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
JArray loras = [];
|
JArray loras = [];
|
||||||
JArray checkpoints = [];
|
JArray checkpoints = [];
|
||||||
JArray wildcards = [];
|
JArray wildcards = [];
|
||||||
|
|
||||||
if (Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler loraHandler))
|
if (Program.T2IModelSets.TryGetValue("LoRA", out T2IModelHandler loraHandler))
|
||||||
{
|
{
|
||||||
foreach (T2IModel model in loraHandler.Models.Values.OrderBy(m => m.Name).Take(MaxLorasInInventory))
|
foreach (T2IModel model in loraHandler.Models.Values
|
||||||
|
.OrderByDescending(m => LooksLikeKreaArch(m))
|
||||||
|
.ThenBy(m => m.Name)
|
||||||
|
.Take(MaxLorasInInventory))
|
||||||
{
|
{
|
||||||
loras.Add(new JObject
|
loras.Add(BuildInventoryModelEntry(model, "lora"));
|
||||||
{
|
|
||||||
["name"] = model.Name,
|
|
||||||
["title"] = model.Metadata?.Title ?? model.Title ?? model.Name,
|
|
||||||
["trigger_phrase"] = model.Metadata?.TriggerPhrase,
|
|
||||||
["architecture"] = model.ModelClass?.ID,
|
|
||||||
["compat_class"] = model.ModelClass?.CompatClass?.ID,
|
|
||||||
["hash"] = model.Metadata?.Hash ?? "",
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Program.T2IModelSets.TryGetValue("Stable-Diffusion", out T2IModelHandler ckptHandler))
|
if (Program.T2IModelSets.TryGetValue("Stable-Diffusion", out T2IModelHandler ckptHandler))
|
||||||
{
|
{
|
||||||
foreach (T2IModel model in ckptHandler.Models.Values.OrderBy(m => m.Name).Take(60))
|
foreach (T2IModel model in ckptHandler.Models.Values
|
||||||
|
.OrderByDescending(m => LooksLikeKreaArch(m))
|
||||||
|
.ThenBy(m => m.Name)
|
||||||
|
.Take(MaxCheckpointsInInventory))
|
||||||
{
|
{
|
||||||
checkpoints.Add(new JObject
|
checkpoints.Add(BuildInventoryModelEntry(model, "checkpoint"));
|
||||||
{
|
|
||||||
["name"] = model.Name,
|
|
||||||
["title"] = model.Metadata?.Title ?? model.Title ?? model.Name,
|
|
||||||
["architecture"] = model.ModelClass?.ID,
|
|
||||||
["compat_class"] = model.ModelClass?.CompatClass?.ID,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -204,9 +724,125 @@ public class SwarmAssistentExtension : Extension
|
|||||||
["checkpoints"] = checkpoints,
|
["checkpoints"] = checkpoints,
|
||||||
["wildcards"] = wildcards,
|
["wildcards"] = wildcards,
|
||||||
["has_civitai_key"] = hasCivitaiKey,
|
["has_civitai_key"] = hasCivitaiKey,
|
||||||
|
["rescanned"] = rescan,
|
||||||
|
["inventory_at"] = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool LooksLikeKreaArch(T2IModel model)
|
||||||
|
{
|
||||||
|
string arch = model?.ModelClass?.ID ?? "";
|
||||||
|
string compat = model?.ModelClass?.CompatClass?.ID ?? "";
|
||||||
|
string name = model?.Name ?? "";
|
||||||
|
string blob = $"{arch} {compat} {name}".ToLowerInvariant();
|
||||||
|
return blob.Contains("krea");
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject BuildInventoryModelEntry(T2IModel model, string kind)
|
||||||
|
{
|
||||||
|
string weight = null;
|
||||||
|
try { weight = model.RawFilePath; } catch { /* ignore */ }
|
||||||
|
string cardPath = CardPathForWeight(weight);
|
||||||
|
bool hasCard = !string.IsNullOrWhiteSpace(cardPath) && File.Exists(cardPath);
|
||||||
|
|
||||||
|
string usage = model.Metadata?.UsageHint;
|
||||||
|
string desc = model.Metadata?.Description;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(desc) && !string.IsNullOrWhiteSpace(model.Description))
|
||||||
|
{
|
||||||
|
desc = model.Description;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// older Swarm builds
|
||||||
|
}
|
||||||
|
|
||||||
|
string blurb = null;
|
||||||
|
if (hasCard)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JObject card = JObject.Parse(File.ReadAllText(cardPath, Encoding.UTF8));
|
||||||
|
string fromCard = (card["notes"] ?? card["when"] ?? card["prompt_hint"])?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(fromCard))
|
||||||
|
{
|
||||||
|
blurb = Clip(fromCard.Trim(), InventoryBlurbMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// ignore bad card json
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (string.IsNullOrWhiteSpace(blurb))
|
||||||
|
{
|
||||||
|
string raw = !string.IsNullOrWhiteSpace(usage) ? usage : desc;
|
||||||
|
if (!string.IsNullOrWhiteSpace(raw))
|
||||||
|
{
|
||||||
|
blurb = Clip(CollapseWs(raw), InventoryBlurbMax);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
JArray tags = null;
|
||||||
|
if (model.Metadata?.Tags is { Length: > 0 } tagArr)
|
||||||
|
{
|
||||||
|
tags = new JArray(tagArr.Where(t => !string.IsNullOrWhiteSpace(t)).Take(8));
|
||||||
|
}
|
||||||
|
|
||||||
|
string trigger = model.Metadata?.TriggerPhrase;
|
||||||
|
JArray triggers = null;
|
||||||
|
if (!string.IsNullOrWhiteSpace(trigger))
|
||||||
|
{
|
||||||
|
triggers = new JArray(trigger.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).Take(12));
|
||||||
|
}
|
||||||
|
|
||||||
|
JObject entry = new()
|
||||||
|
{
|
||||||
|
["name"] = model.Name,
|
||||||
|
["title"] = model.Metadata?.Title ?? model.Title ?? model.Name,
|
||||||
|
["kind"] = kind,
|
||||||
|
["trigger_phrase"] = trigger,
|
||||||
|
["architecture"] = model.ModelClass?.ID,
|
||||||
|
["compat_class"] = model.ModelClass?.CompatClass?.ID,
|
||||||
|
["hash"] = model.Metadata?.Hash ?? "",
|
||||||
|
["has_card"] = hasCard,
|
||||||
|
["krea_likely"] = LooksLikeKreaArch(model),
|
||||||
|
};
|
||||||
|
if (triggers is not null && triggers.Count > 0)
|
||||||
|
{
|
||||||
|
entry["triggers"] = triggers;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(blurb))
|
||||||
|
{
|
||||||
|
entry["blurb"] = blurb;
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(usage))
|
||||||
|
{
|
||||||
|
entry["usage_hint"] = Clip(CollapseWs(usage), 120);
|
||||||
|
}
|
||||||
|
if (tags is not null && tags.Count > 0)
|
||||||
|
{
|
||||||
|
entry["tags"] = tags;
|
||||||
|
}
|
||||||
|
string defW = model.Metadata?.LoraDefaultWeight;
|
||||||
|
if (!string.IsNullOrWhiteSpace(defW) && kind == "lora")
|
||||||
|
{
|
||||||
|
entry["default_weight"] = defW;
|
||||||
|
}
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
static string CollapseWs(string text)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
{
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
return Regex.Replace(text.Trim(), @"\s+", " ");
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>Search Civitai for LoRAs (prefers Krea 2 base). Uses Swarm-stored civitai_api key.</summary>
|
/// <summary>Search Civitai for LoRAs (prefers Krea 2 base). Uses Swarm-stored civitai_api key.</summary>
|
||||||
public async Task<JObject> AssistentSearchCivitai(Session session, string query, int limit = 8)
|
public async Task<JObject> AssistentSearchCivitai(Session session, string query, int limit = 8)
|
||||||
{
|
{
|
||||||
@@ -368,7 +1004,7 @@ public class SwarmAssistentExtension : Extension
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
List<JObject> BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null)
|
List<JObject> BuildOllamaMessages(string packName, bool includeBase, string contextJson, JArray userMessages, string extraSystem = null, string personaId = null)
|
||||||
{
|
{
|
||||||
List<JObject> ollamaMessages = [];
|
List<JObject> ollamaMessages = [];
|
||||||
StringBuilder system = new();
|
StringBuilder system = new();
|
||||||
@@ -380,6 +1016,13 @@ public class SwarmAssistentExtension : Extension
|
|||||||
system.AppendLine(basePack);
|
system.AppendLine(basePack);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
string personaPrompt = ResolvePersonaPrompt(personaId);
|
||||||
|
if (!string.IsNullOrWhiteSpace(personaPrompt))
|
||||||
|
{
|
||||||
|
system.AppendLine();
|
||||||
|
system.AppendLine($"## Persona: {personaId ?? "neutral"}");
|
||||||
|
system.AppendLine(personaPrompt);
|
||||||
|
}
|
||||||
if (!string.IsNullOrWhiteSpace(packName) && packName != "base_krea2")
|
if (!string.IsNullOrWhiteSpace(packName) && packName != "base_krea2")
|
||||||
{
|
{
|
||||||
string situational = ReadPackFile(packName);
|
string situational = ReadPackFile(packName);
|
||||||
@@ -431,6 +1074,42 @@ public class SwarmAssistentExtension : Extension
|
|||||||
return ollamaMessages;
|
return ollamaMessages;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
string ResolvePersonaPrompt(string personaId)
|
||||||
|
{
|
||||||
|
string id = (personaId ?? "neutral").Trim();
|
||||||
|
if (string.IsNullOrWhiteSpace(id))
|
||||||
|
{
|
||||||
|
id = "neutral";
|
||||||
|
}
|
||||||
|
string overlay = PersonasOverlayJsonPath();
|
||||||
|
if (File.Exists(overlay))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
JObject parsed = JObject.Parse(File.ReadAllText(overlay, Encoding.UTF8));
|
||||||
|
if (parsed["personas"] is JArray arr)
|
||||||
|
{
|
||||||
|
foreach (JToken t in arr)
|
||||||
|
{
|
||||||
|
if (t is JObject po && string.Equals(po["id"]?.ToString(), id, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
string p = po["prompt"]?.ToString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(p))
|
||||||
|
{
|
||||||
|
return p;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// fall through to bundled
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ReadPersonaFile(id);
|
||||||
|
}
|
||||||
|
|
||||||
static JObject TryParsePatch(string reply)
|
static JObject TryParsePatch(string reply)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(reply))
|
if (string.IsNullOrWhiteSpace(reply))
|
||||||
@@ -528,9 +1207,10 @@ public class SwarmAssistentExtension : Extension
|
|||||||
string contextJson,
|
string contextJson,
|
||||||
JArray userMessages,
|
JArray userMessages,
|
||||||
Func<string, Task> onDelta = null,
|
Func<string, Task> onDelta = null,
|
||||||
Func<int, Task> onHopStart = null)
|
Func<int, Task> onHopStart = null,
|
||||||
|
string personaId = null)
|
||||||
{
|
{
|
||||||
List<JObject> messages = BuildOllamaMessages(packName, includeBase, contextJson, userMessages);
|
List<JObject> messages = BuildOllamaMessages(packName, includeBase, contextJson, userMessages, personaId: personaId);
|
||||||
JArray civitaiResults = [];
|
JArray civitaiResults = [];
|
||||||
string reply = "";
|
string reply = "";
|
||||||
JObject lastRaw = null;
|
JObject lastRaw = null;
|
||||||
@@ -653,7 +1333,7 @@ public class SwarmAssistentExtension : Extension
|
|||||||
/// SwarmUI passes the whole request as the JObject param (not only a nested key).
|
/// SwarmUI passes the whole request as the JObject param (not only a nested key).
|
||||||
/// Support both flat fields and legacy nested <c>raw</c>.
|
/// Support both flat fields and legacy nested <c>raw</c>.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
static void ExtractChatPayload(JObject raw, ref string baseUrl, ref string model, ref string pack, ref bool includeBase, out JArray userMessages, out string contextJson)
|
static void ExtractChatPayload(JObject raw, ref string baseUrl, ref string model, ref string pack, ref bool includeBase, out JArray userMessages, out string contextJson, out string persona)
|
||||||
{
|
{
|
||||||
JObject whole = raw ?? [];
|
JObject whole = raw ?? [];
|
||||||
JObject nested = whole["raw"] as JObject;
|
JObject nested = whole["raw"] as JObject;
|
||||||
@@ -678,12 +1358,13 @@ public class SwarmAssistentExtension : Extension
|
|||||||
}
|
}
|
||||||
userMessages = (whole["messages"] as JArray) ?? (nested?["messages"] as JArray);
|
userMessages = (whole["messages"] as JArray) ?? (nested?["messages"] as JArray);
|
||||||
contextJson = whole["context_json"]?.ToString() ?? nested?["context_json"]?.ToString();
|
contextJson = whole["context_json"]?.ToString() ?? nested?["context_json"]?.ToString();
|
||||||
|
persona = whole["persona"]?.ToString() ?? nested?["persona"]?.ToString() ?? "neutral";
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>Proxy to Ollama /api/chat (non-stream), with optional Civitai search hop.</summary>
|
/// <summary>Proxy to Ollama /api/chat (non-stream), with optional Civitai search hop.</summary>
|
||||||
public async Task<JObject> AssistentChat(Session session, string baseUrl, string model, string pack, bool includeBase, JObject raw)
|
public async Task<JObject> AssistentChat(Session session, string baseUrl, string model, string pack, bool includeBase, JObject raw)
|
||||||
{
|
{
|
||||||
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson);
|
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona);
|
||||||
string root = NormalizeBaseUrl(baseUrl);
|
string root = NormalizeBaseUrl(baseUrl);
|
||||||
string modelName = (model ?? "").Trim();
|
string modelName = (model ?? "").Trim();
|
||||||
if (string.IsNullOrWhiteSpace(modelName))
|
if (string.IsNullOrWhiteSpace(modelName))
|
||||||
@@ -698,13 +1379,14 @@ public class SwarmAssistentExtension : Extension
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
(string reply, JObject parsed, JArray civitai) = await RunChatWithHops(
|
(string reply, JObject parsed, JArray civitai) = await RunChatWithHops(
|
||||||
session, root, modelName, packName, includeBase, contextJson, userMessages);
|
session, root, modelName, packName, includeBase, contextJson, userMessages, personaId: persona);
|
||||||
return new JObject
|
return new JObject
|
||||||
{
|
{
|
||||||
["success"] = true,
|
["success"] = true,
|
||||||
["reply"] = reply,
|
["reply"] = reply,
|
||||||
["model"] = modelName,
|
["model"] = modelName,
|
||||||
["pack"] = packName,
|
["pack"] = packName,
|
||||||
|
["persona"] = persona,
|
||||||
["raw"] = parsed,
|
["raw"] = parsed,
|
||||||
["civitai_results"] = civitai,
|
["civitai_results"] = civitai,
|
||||||
};
|
};
|
||||||
@@ -718,7 +1400,7 @@ public class SwarmAssistentExtension : Extension
|
|||||||
/// <summary>WebSocket streaming chat (Ollama stream:true) + Civitai hops.</summary>
|
/// <summary>WebSocket streaming chat (Ollama stream:true) + Civitai hops.</summary>
|
||||||
public async Task<JObject> AssistentChatWS(Session session, WebSocket ws, string baseUrl, string model, string pack, bool includeBase, JObject raw)
|
public async Task<JObject> AssistentChatWS(Session session, WebSocket ws, string baseUrl, string model, string pack, bool includeBase, JObject raw)
|
||||||
{
|
{
|
||||||
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson);
|
ExtractChatPayload(raw, ref baseUrl, ref model, ref pack, ref includeBase, out JArray userMessages, out string contextJson, out string persona);
|
||||||
string root = NormalizeBaseUrl(baseUrl);
|
string root = NormalizeBaseUrl(baseUrl);
|
||||||
string modelName = (model ?? "").Trim();
|
string modelName = (model ?? "").Trim();
|
||||||
if (string.IsNullOrWhiteSpace(modelName))
|
if (string.IsNullOrWhiteSpace(modelName))
|
||||||
@@ -762,7 +1444,7 @@ public class SwarmAssistentExtension : Extension
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(string reply, JObject parsed, JArray civitai) = await RunChatWithHops(
|
(string reply, JObject parsed, JArray civitai) = await RunChatWithHops(
|
||||||
session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart);
|
session, root, modelName, packName, includeBase, contextJson, userMessages, OnDelta, OnHopStart, persona);
|
||||||
await ws.SendJson(new JObject
|
await ws.SendJson(new JObject
|
||||||
{
|
{
|
||||||
["success"] = true,
|
["success"] = true,
|
||||||
@@ -770,6 +1452,7 @@ public class SwarmAssistentExtension : Extension
|
|||||||
["reply"] = reply,
|
["reply"] = reply,
|
||||||
["model"] = modelName,
|
["model"] = modelName,
|
||||||
["pack"] = packName,
|
["pack"] = packName,
|
||||||
|
["persona"] = persona,
|
||||||
["raw"] = parsed,
|
["raw"] = parsed,
|
||||||
["civitai_results"] = civitai,
|
["civitai_results"] = civitai,
|
||||||
}, API.WebsocketTimeout);
|
}, API.WebsocketTimeout);
|
||||||
|
|||||||
@@ -24,8 +24,15 @@
|
|||||||
<div class="sa-chat-title">
|
<div class="sa-chat-title">
|
||||||
Assistent
|
Assistent
|
||||||
<span class="sa-live-dot" id="sa_live_dot" hidden></span>
|
<span class="sa-live-dot" id="sa_live_dot" hidden></span>
|
||||||
|
<div class="sa-subtabs" role="tablist">
|
||||||
|
<button type="button" class="sa-subtab sa-subtab-active" data-view="chat" id="sa_tab_chat">Chat</button>
|
||||||
|
<button type="button" class="sa-subtab" data-view="cards" id="sa_tab_cards">Cards</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-header-right">
|
<div class="sa-header-right">
|
||||||
|
<select id="sa_persona" class="sa-select" title="Character / tone">
|
||||||
|
<option value="neutral">Нейтральный</option>
|
||||||
|
</select>
|
||||||
<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>
|
||||||
<option value="critique_image">Critique image</option>
|
<option value="critique_image">Critique image</option>
|
||||||
@@ -33,6 +40,7 @@
|
|||||||
<option value="fix_params">Fix params</option>
|
<option value="fix_params">Fix params</option>
|
||||||
<option value="inpaint_edit">Inpaint / img2img</option>
|
<option value="inpaint_edit">Inpaint / img2img</option>
|
||||||
<option value="describe_ref">Describe ref</option>
|
<option value="describe_ref">Describe ref</option>
|
||||||
|
<option value="catalog_card">Catalog card</option>
|
||||||
</select>
|
</select>
|
||||||
<select id="sa_model" class="sa-select sa-model-select" title="Ollama model">
|
<select id="sa_model" class="sa-select sa-model-select" title="Ollama model">
|
||||||
<option value="">Loading models…</option>
|
<option value="">Loading models…</option>
|
||||||
@@ -50,10 +58,12 @@
|
|||||||
<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-view" id="sa_view_chat">
|
||||||
<div class="sa-messages" id="sa_messages">
|
<div class="sa-messages" id="sa_messages">
|
||||||
<div class="sa-chat-empty" id="sa_chat_empty">
|
<div class="sa-chat-empty" id="sa_chat_empty">
|
||||||
<div class="sa-chat-empty-title">Collaborative Krea 2</div>
|
<div class="sa-chat-empty-title">Collaborative Krea 2</div>
|
||||||
<div class="sa-chat-empty-hint">Write a prompt, drop refs, use aspect chips, or type <code>/help</code>.</div>
|
<div class="sa-chat-empty-hint">Write a prompt, drop refs, pick a persona, or open <em>Cards</em> for LoRA tips.</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-livebar" id="sa_livebar" hidden>
|
<div class="sa-livebar" id="sa_livebar" hidden>
|
||||||
@@ -81,6 +91,37 @@
|
|||||||
<span class="sa-status" id="sa_status"></span>
|
<span class="sa-status" id="sa_status"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="sa-view" id="sa_view_cards" hidden>
|
||||||
|
<div class="sa-cards-layout">
|
||||||
|
<div class="sa-cards-list-pane">
|
||||||
|
<div class="sa-cards-filter">
|
||||||
|
<select id="sa_cards_kind" class="sa-select">
|
||||||
|
<option value="all">All</option>
|
||||||
|
<option value="checkpoint">Checkpoints</option>
|
||||||
|
<option value="lora">LoRAs</option>
|
||||||
|
</select>
|
||||||
|
<button type="button" class="basic-button" id="sa_btn_cards_refresh">Refresh</button>
|
||||||
|
</div>
|
||||||
|
<div class="sa-cards-list" id="sa_cards_list"></div>
|
||||||
|
</div>
|
||||||
|
<div class="sa-cards-editor-pane">
|
||||||
|
<div class="sa-cards-editor-head">
|
||||||
|
<strong id="sa_card_title">Select a model</strong>
|
||||||
|
<span class="sa-card-badge" id="sa_card_badge" hidden></span>
|
||||||
|
</div>
|
||||||
|
<textarea id="sa_card_json" rows="16" placeholder="Card JSON will appear here…" spellcheck="false"></textarea>
|
||||||
|
<div class="sa-cards-actions">
|
||||||
|
<button type="button" class="basic-button" id="sa_btn_card_meta">Load Civitai meta</button>
|
||||||
|
<button type="button" class="basic-button sa-primary" id="sa_btn_card_generate">Generate with Assistent</button>
|
||||||
|
<button type="button" class="basic-button" id="sa_btn_card_save">Save card</button>
|
||||||
|
<button type="button" class="basic-button" id="sa_btn_card_wanted" title="Add to next-up download list">Enqueue wanted</button>
|
||||||
|
<span class="sa-status" id="sa_card_status"></span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user