Ship Assistent 0.11.9: esbuild bundle and patch-keys manifest.
Replace split Assets JS with a built bundle and aligned C#/config so SwarmUI loads one script and patch apply stays consistent; drop legacy terse persona and memory seed. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -1,5 +1,6 @@
|
|||||||
bin/
|
bin/
|
||||||
obj/
|
obj/
|
||||||
|
node_modules/
|
||||||
.vs/
|
.vs/
|
||||||
*.user
|
*.user
|
||||||
*.suo
|
*.suo
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
/**
|
|
||||||
* Swarm Assistent — promise wrapper around SwarmUI's genericRequest.
|
|
||||||
* Loaded before assistent.js.
|
|
||||||
*/
|
|
||||||
window.SA = window.SA || {};
|
|
||||||
|
|
||||||
SA.request = function (name, body) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (typeof genericRequest !== 'function') {
|
|
||||||
reject(new Error('genericRequest unavailable'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
genericRequest(
|
|
||||||
name,
|
|
||||||
body || {},
|
|
||||||
(data) => {
|
|
||||||
if (data && data.error) {
|
|
||||||
reject(new Error(String(data.error)));
|
|
||||||
} else {
|
|
||||||
resolve(data);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
0,
|
|
||||||
(err) => reject(err instanceof Error ? err : new Error(String(err || 'request failed'))),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -95,6 +95,10 @@
|
|||||||
outline: none;
|
outline: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-board:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.sa-board.sa-board-gen-only {
|
.sa-board.sa-board-gen-only {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
grid-auto-rows: 1fr;
|
grid-auto-rows: 1fr;
|
||||||
@@ -138,6 +142,10 @@
|
|||||||
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
transition: border-color 0.15s ease, box-shadow 0.15s ease;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sa-slot:focus-visible {
|
||||||
|
box-shadow: 0 0 0 2px color-mix(in srgb, currentColor 55%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
.sa-slot.sa-has-image {
|
.sa-slot.sa-has-image {
|
||||||
border-style: solid;
|
border-style: solid;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,145 +0,0 @@
|
|||||||
/**
|
|
||||||
* Swarm Assistent — patch detection / extraction / alias normalization.
|
|
||||||
* Loaded before assistent.js; mirrors AssistentPatch.cs on the server side.
|
|
||||||
*/
|
|
||||||
window.SA = window.SA || {};
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
const PATCH_KEYS = [
|
|
||||||
'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler',
|
|
||||||
'actions', 'search_query', 'civitai_query',
|
|
||||||
'use_init_image', 'clear_init_image', 'init_creativity', 'denoise',
|
|
||||||
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
|
||||||
'look_at', 'vision_from', 'vision_slots', 'slot_to_init', 'slot_to_mask',
|
|
||||||
'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed',
|
|
||||||
'creativity', 'intensity', 'complexity', 'movement',
|
|
||||||
'clear_prompt_images', 'slot_to_prompt_image', 'pack',
|
|
||||||
'memories', 'memory_query', 'memory_kind', 'tag_query', 'user_prefs',
|
|
||||||
'controls', 'persona_clone', 'persona_shelves', 'persona', 'notes',
|
|
||||||
'variants',
|
|
||||||
];
|
|
||||||
|
|
||||||
const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
|
||||||
|
|
||||||
function has(obj, key) {
|
|
||||||
return obj[key] !== undefined && obj[key] !== null;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Model-card JSON (catalog_card) — must not be treated as a Generate patch. */
|
|
||||||
function isCardObject(obj) {
|
|
||||||
if (!obj || typeof obj !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
|
|
||||||
const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions
|
|
||||||
|| obj.width || obj.height || obj.steps || obj.cfg || obj.aspect || obj.seed != null
|
|
||||||
|| obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
|
|
||||||
if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** True when the object looks like a generation patch rather than a catalog card / arbitrary JSON. */
|
|
||||||
function isPatchObject(obj) {
|
|
||||||
if (!obj || typeof obj !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isCardObject(obj)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return PATCH_KEYS.some((k) => has(obj, k));
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Maps alias fields onto canonical names, keeping the aliases in place. */
|
|
||||||
function normalizePatch(patch) {
|
|
||||||
if (!patch || typeof patch !== 'object') {
|
|
||||||
return patch;
|
|
||||||
}
|
|
||||||
if (!has(patch, 'search_query') && has(patch, 'civitai_query')) {
|
|
||||||
patch.search_query = patch.civitai_query;
|
|
||||||
}
|
|
||||||
if (!has(patch, 'init_creativity') && has(patch, 'denoise')) {
|
|
||||||
patch.init_creativity = patch.denoise;
|
|
||||||
}
|
|
||||||
if (!has(patch, 'look_at')) {
|
|
||||||
if (has(patch, 'vision_from')) {
|
|
||||||
patch.look_at = patch.vision_from;
|
|
||||||
} else if (has(patch, 'vision_slots')) {
|
|
||||||
patch.look_at = patch.vision_slots;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return patch;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Splits a reply into prose and the last fenced patch object found in it. */
|
|
||||||
function extractPatch(text) {
|
|
||||||
if (!text) {
|
|
||||||
return { prose: text || '', patch: null };
|
|
||||||
}
|
|
||||||
const re = new RegExp(FENCE_RE.source, 'gi');
|
|
||||||
let match;
|
|
||||||
let lastPatch = null;
|
|
||||||
let prose = text;
|
|
||||||
while ((match = re.exec(text)) !== null) {
|
|
||||||
try {
|
|
||||||
const obj = JSON.parse(match[1].trim());
|
|
||||||
if (isPatchObject(obj)) {
|
|
||||||
lastPatch = normalizePatch(obj);
|
|
||||||
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
|
|
||||||
}
|
|
||||||
} catch (e) { /* not json */ }
|
|
||||||
}
|
|
||||||
return { prose, patch: lastPatch };
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Closed fence worth freezing the stream / stopping Ollama early.
|
|
||||||
* Weak fences (pack / creativity / empty) must NOT stop — model often continues with the real patch.
|
|
||||||
*/
|
|
||||||
function isTerminalStreamPatch(obj) {
|
|
||||||
if (!obj || typeof obj !== 'object') {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (isCardObject(obj)) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (Array.isArray(obj.variants) && obj.variants.length) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj.search_query != null || obj.civitai_query != null
|
|
||||||
|| obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
|
|
||||||
const hopOrGen = [
|
|
||||||
'skill_load', 'persona_read', 'memory_get', 'memory_search', 'lookup_tags',
|
|
||||||
'list_inventory', 'search_civitai', 'interrupt', 'generate',
|
|
||||||
'memory_upsert', 'user_pref_upsert',
|
|
||||||
];
|
|
||||||
if (acts.some((a) => hopOrGen.includes(a))) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (String(obj.prompt || '').trim().length >= 48) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
if (obj.loras != null || obj.aspect != null || obj.steps != null
|
|
||||||
|| obj.width != null || obj.height != null || obj.cfg != null
|
|
||||||
|| obj.seed != null || obj.controls != null
|
|
||||||
|| obj.memories != null || obj.user_prefs != null) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
SA.PATCH_KEYS = PATCH_KEYS;
|
|
||||||
SA.isCardObject = isCardObject;
|
|
||||||
SA.isPatchObject = isPatchObject;
|
|
||||||
SA.isTerminalStreamPatch = isTerminalStreamPatch;
|
|
||||||
SA.normalizePatch = normalizePatch;
|
|
||||||
SA.extractPatch = extractPatch;
|
|
||||||
})();
|
|
||||||
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
/**
|
|
||||||
* Swarm Assistent — sqlite persistence for chats + UI state (Assistent/memory/assistent.sqlite).
|
|
||||||
* Loaded after assistent.api.js and before assistent.js.
|
|
||||||
*/
|
|
||||||
window.SA = window.SA || {};
|
|
||||||
|
|
||||||
(function () {
|
|
||||||
const LS_CHATS = 'swarm_assistent_chats_v1';
|
|
||||||
const LS_MIGRATED = 'swarm_assistent_chats_on_disk_v1';
|
|
||||||
const SAVE_DEBOUNCE_MS = 700;
|
|
||||||
|
|
||||||
const timers = { chats: new Map(), ui: null };
|
|
||||||
|
|
||||||
function request(name, body) {
|
|
||||||
if (typeof SA.request === 'function') {
|
|
||||||
return SA.request(name, body);
|
|
||||||
}
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
if (typeof genericRequest !== 'function') {
|
|
||||||
reject(new Error('genericRequest unavailable'));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
genericRequest(
|
|
||||||
name,
|
|
||||||
body || {},
|
|
||||||
(data) => (data && data.error ? reject(new Error(String(data.error))) : resolve(data)),
|
|
||||||
0,
|
|
||||||
(err) => reject(err instanceof Error ? err : new Error(String(err || 'request failed'))),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeChat(raw) {
|
|
||||||
if (!raw || !raw.id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
id: String(raw.id),
|
|
||||||
title: String(raw.title || 'Новый чат'),
|
|
||||||
createdAt: Number(raw.createdAt) || Date.now(),
|
|
||||||
updatedAt: Number(raw.updatedAt) || Number(raw.createdAt) || Date.now(),
|
|
||||||
messages: Array.isArray(raw.messages) ? raw.messages : [],
|
|
||||||
messages_count: Number(raw.messages_count) || (Array.isArray(raw.messages) ? raw.messages.length : 0),
|
|
||||||
params: raw.params && typeof raw.params === 'object' ? raw.params : null,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
function readLocalChats() {
|
|
||||||
try {
|
|
||||||
const parsed = JSON.parse(localStorage.getItem(LS_CHATS) || 'null');
|
|
||||||
if (Array.isArray(parsed?.chats)) {
|
|
||||||
return parsed.chats.map(normalizeChat).filter(Boolean);
|
|
||||||
}
|
|
||||||
} catch (e) { /* ignore */ }
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/** One-shot lift of the browser-only history onto the data volume. */
|
|
||||||
async function migrateLocalChatsToDisk() {
|
|
||||||
if (localStorage.getItem(LS_MIGRATED) === '1') {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const local = readLocalChats().filter((c) => (c.messages || []).length > 0);
|
|
||||||
localStorage.setItem(LS_MIGRATED, '1');
|
|
||||||
if (!local.length) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
for (const chat of local) {
|
|
||||||
try {
|
|
||||||
await saveChat(chat, { immediate: true });
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Assistent: chat migration failed', chat.id, e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return local;
|
|
||||||
}
|
|
||||||
|
|
||||||
/** Sqlite chats, newest first. Falls back to a localStorage migration when the store is empty. */
|
|
||||||
async function loadChats() {
|
|
||||||
let chats = [];
|
|
||||||
try {
|
|
||||||
const data = await request('AssistentListChats', { with_messages: true });
|
|
||||||
chats = (data?.chats || []).map(normalizeChat).filter(Boolean);
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('Assistent: disk chats unavailable', e);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
if (!chats.length) {
|
|
||||||
const migrated = await migrateLocalChatsToDisk();
|
|
||||||
if (migrated.length) {
|
|
||||||
chats = migrated;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
localStorage.setItem(LS_MIGRATED, '1');
|
|
||||||
}
|
|
||||||
return chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getChat(id) {
|
|
||||||
if (!id) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const data = await request('AssistentGetChat', { id });
|
|
||||||
return normalizeChat(data?.chat);
|
|
||||||
}
|
|
||||||
|
|
||||||
async function searchChats(q) {
|
|
||||||
const query = String(q || '').trim();
|
|
||||||
if (query.length < 2) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const data = await request('AssistentListChats', { q: query, with_messages: false, limit: 40 });
|
|
||||||
return (data?.chats || []).map(normalizeChat).filter(Boolean);
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveChat(chat, { immediate = false } = {}) {
|
|
||||||
const clean = normalizeChat(chat);
|
|
||||||
if (!clean) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
const send = () => {
|
|
||||||
timers.chats.delete(clean.id);
|
|
||||||
return request('AssistentSaveChat', {
|
|
||||||
id: clean.id,
|
|
||||||
title: clean.title,
|
|
||||||
messages: clean.messages,
|
|
||||||
params: clean.params,
|
|
||||||
createdAt: clean.createdAt,
|
|
||||||
updatedAt: clean.updatedAt,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
if (immediate) {
|
|
||||||
const pending = timers.chats.get(clean.id);
|
|
||||||
if (pending) {
|
|
||||||
clearTimeout(pending);
|
|
||||||
}
|
|
||||||
return send();
|
|
||||||
}
|
|
||||||
const pending = timers.chats.get(clean.id);
|
|
||||||
if (pending) {
|
|
||||||
clearTimeout(pending);
|
|
||||||
}
|
|
||||||
timers.chats.set(clean.id, setTimeout(() => {
|
|
||||||
send().catch((e) => console.warn('Assistent: chat save failed', clean.id, e));
|
|
||||||
}, SAVE_DEBOUNCE_MS));
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
function deleteChat(id) {
|
|
||||||
if (!id) {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
const pending = timers.chats.get(id);
|
|
||||||
if (pending) {
|
|
||||||
clearTimeout(pending);
|
|
||||||
timers.chats.delete(id);
|
|
||||||
}
|
|
||||||
return request('AssistentDeleteChat', { id });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function loadUiState() {
|
|
||||||
try {
|
|
||||||
const data = await request('AssistentGetUiState', {});
|
|
||||||
const ui = data?.ui_state;
|
|
||||||
return ui && typeof ui === 'object' ? ui : null;
|
|
||||||
} catch (e) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function saveUiState(uiState, { immediate = false } = {}) {
|
|
||||||
if (!uiState || typeof uiState !== 'object') {
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
const send = () => {
|
|
||||||
timers.ui = null;
|
|
||||||
return request('AssistentSaveUiState', { ui_state: uiState });
|
|
||||||
};
|
|
||||||
if (timers.ui) {
|
|
||||||
clearTimeout(timers.ui);
|
|
||||||
timers.ui = null;
|
|
||||||
}
|
|
||||||
if (immediate) {
|
|
||||||
return send();
|
|
||||||
}
|
|
||||||
timers.ui = setTimeout(() => {
|
|
||||||
send().catch((e) => console.warn('Assistent: ui-state save failed', e));
|
|
||||||
}, SAVE_DEBOUNCE_MS);
|
|
||||||
return Promise.resolve(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
SA.persist = {
|
|
||||||
LS_CHATS,
|
|
||||||
loadChats,
|
|
||||||
getChat,
|
|
||||||
searchChats,
|
|
||||||
saveChat,
|
|
||||||
deleteChat,
|
|
||||||
loadUiState,
|
|
||||||
saveUiState,
|
|
||||||
};
|
|
||||||
})();
|
|
||||||
@@ -228,12 +228,24 @@ public partial class SwarmAssistentExtension
|
|||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
string tool = NextToolHop(patch);
|
string follow = null;
|
||||||
|
JArray civitaiHop = null;
|
||||||
|
HashSet<string> hopSkip = new(StringComparer.OrdinalIgnoreCase);
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
string tool = NextToolHop(patch, hopSkip);
|
||||||
if (string.IsNullOrWhiteSpace(tool))
|
if (string.IsNullOrWhiteSpace(tool))
|
||||||
|
{
|
||||||
|
follow = null;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
(follow, civitaiHop) = await RunToolHop(session, root, embed, pid, chain, patch, tool, hopDone);
|
||||||
|
if (follow is not null)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
(string follow, JArray civitaiHop) = await RunToolHop(session, root, embed, pid, chain, patch, tool, hopDone);
|
hopSkip.Add(tool);
|
||||||
|
}
|
||||||
if (follow is null)
|
if (follow is null)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
|
|||||||
+34
-86
@@ -29,6 +29,10 @@ public sealed class AssistentConfig
|
|||||||
public static string SafeId(string id)
|
public static string SafeId(string id)
|
||||||
{
|
{
|
||||||
string s = (id ?? "").Replace('\\', '/').AfterLast('/').Replace("..", "").Trim();
|
string s = (id ?? "").Replace('\\', '/').AfterLast('/').Replace("..", "").Trim();
|
||||||
|
if (string.Equals(s, "terse", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
s = "aggressive";
|
||||||
|
}
|
||||||
if (string.IsNullOrWhiteSpace(s) || !Regex.IsMatch(s, @"^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$"))
|
if (string.IsNullOrWhiteSpace(s) || !Regex.IsMatch(s, @"^[A-Za-z0-9][A-Za-z0-9_\-]{0,63}$"))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
@@ -122,21 +126,29 @@ public sealed class AssistentConfig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public JArray TryReadJsonArray(string path)
|
public string[] LoadPatchKeys()
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(path) || !File.Exists(path))
|
string path = ResolveUnder(_bundledRoot, "_base/patch-keys.json");
|
||||||
|
JObject doc = TryReadJson(path);
|
||||||
|
if (doc?["keys"] is JArray arr && arr.Count > 0)
|
||||||
{
|
{
|
||||||
return null;
|
return arr.Select(t => t?.ToString()).Where(s => !string.IsNullOrWhiteSpace(s)).ToArray();
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return JArray.Parse(File.ReadAllText(path, Encoding.UTF8));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logs.Debug($"AssistentConfig json-array {path}: {ex.Message}");
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
// Fallback if bundled json missing
|
||||||
|
return
|
||||||
|
[
|
||||||
|
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler",
|
||||||
|
"actions", "search_query", "civitai_query",
|
||||||
|
"use_init_image", "clear_init_image", "init_creativity", "denoise",
|
||||||
|
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
|
||||||
|
"look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask",
|
||||||
|
"snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed",
|
||||||
|
"creativity", "intensity", "complexity", "movement",
|
||||||
|
"clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
|
||||||
|
"memory_query", "memory_kind", "tag_query", "user_prefs",
|
||||||
|
"inventory_query", "skills", "persona_shelves", "persona_clone", "persona", "controls",
|
||||||
|
"variants",
|
||||||
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
public string TryReadText(string path)
|
public string TryReadText(string path)
|
||||||
@@ -272,33 +284,6 @@ public sealed class AssistentConfig
|
|||||||
byId[id] = (cur.title, cur.accent, PersonaSource(id));
|
byId[id] = (cur.title, cur.accent, PersonaSource(id));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Legacy personas.json titles
|
|
||||||
string overlayJson = Path.Combine(_overlayRoot, "personas.json");
|
|
||||||
JObject legacy = TryReadJson(overlayJson);
|
|
||||||
if (legacy?["personas"] is JArray arr)
|
|
||||||
{
|
|
||||||
foreach (JToken t in arr)
|
|
||||||
{
|
|
||||||
if (t is not JObject po)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
string id = SafeId(po["id"]?.ToString());
|
|
||||||
if (id is null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (byId.TryGetValue(id, out var cur))
|
|
||||||
{
|
|
||||||
byId[id] = (po["title"]?.ToString() ?? cur.title, cur.accent, cur.source);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
byId[id] = (po["title"]?.ToString() ?? id, "#8b949e", "legacy");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return byId.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)
|
return byId.OrderBy(kv => kv.Key, StringComparer.OrdinalIgnoreCase)
|
||||||
.Select(kv => (kv.Key, kv.Value.title, kv.Value.accent, kv.Value.source))
|
.Select(kv => (kv.Key, kv.Value.title, kv.Value.accent, kv.Value.source))
|
||||||
.ToList();
|
.ToList();
|
||||||
@@ -308,13 +293,6 @@ public sealed class AssistentConfig
|
|||||||
{
|
{
|
||||||
JObject assistant = MergeJsonLayers("assistant.json", LayerRoots("neutral"));
|
JObject assistant = MergeJsonLayers("assistant.json", LayerRoots("neutral"));
|
||||||
string def = SafeId(assistant["default_persona"]?.ToString()) ?? "neutral";
|
string def = SafeId(assistant["default_persona"]?.ToString()) ?? "neutral";
|
||||||
string overlayJson = Path.Combine(_overlayRoot, "personas.json");
|
|
||||||
JObject legacy = TryReadJson(overlayJson);
|
|
||||||
string fromLegacy = SafeId(legacy?["default"]?.ToString());
|
|
||||||
if (fromLegacy is not null)
|
|
||||||
{
|
|
||||||
def = fromLegacy;
|
|
||||||
}
|
|
||||||
var catalog = ListPersonaCatalog();
|
var catalog = ListPersonaCatalog();
|
||||||
if (catalog.All(p => !string.Equals(p.id, def, StringComparison.OrdinalIgnoreCase)) && catalog.Count > 0)
|
if (catalog.All(p => !string.Equals(p.id, def, StringComparison.OrdinalIgnoreCase)) && catalog.Count > 0)
|
||||||
{
|
{
|
||||||
@@ -409,6 +387,8 @@ public sealed class AssistentConfig
|
|||||||
|
|
||||||
/// <summary>Clamp and merge control values into overlay exact.json (controls key only).</summary>
|
/// <summary>Clamp and merge control values into overlay exact.json (controls key only).</summary>
|
||||||
public JObject SaveControlValues(string personaId, JObject values)
|
public JObject SaveControlValues(string personaId, JObject values)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
{
|
{
|
||||||
string id = SafeId(personaId) ?? "neutral";
|
string id = SafeId(personaId) ?? "neutral";
|
||||||
JObject schema = LoadControlsSchema(id);
|
JObject schema = LoadControlsSchema(id);
|
||||||
@@ -422,6 +402,7 @@ public sealed class AssistentConfig
|
|||||||
File.WriteAllText(path, existing.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
File.WriteAllText(path, existing.ToString(Newtonsoft.Json.Formatting.Indented) + "\n", Encoding.UTF8);
|
||||||
return LoadControlValues(id);
|
return LoadControlValues(id);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static JObject ClampControls(JObject schema, JObject values)
|
public static JObject ClampControls(JObject schema, JObject values)
|
||||||
{
|
{
|
||||||
@@ -727,11 +708,6 @@ public sealed class AssistentConfig
|
|||||||
{
|
{
|
||||||
string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id");
|
string id = SafeId(personaId) ?? throw new ArgumentException("invalid persona id");
|
||||||
if (IsBundledPersona(id) && !allowBundledShadow && !IsOverlayPersona(id))
|
if (IsBundledPersona(id) && !allowBundledShadow && !IsOverlayPersona(id))
|
||||||
{
|
|
||||||
// v1: do not shadow-write bundled; require clone to a new overlay id.
|
|
||||||
throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id");
|
|
||||||
}
|
|
||||||
if (IsBundledPersona(id) && !IsOverlayPersona(id))
|
|
||||||
{
|
{
|
||||||
throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id");
|
throw new InvalidOperationException($"cannot write bundled persona '{id}' — clone to a new overlay id");
|
||||||
}
|
}
|
||||||
@@ -1099,39 +1075,6 @@ public sealed class AssistentConfig
|
|||||||
}
|
}
|
||||||
string extra = MergeTextLayers("extra.md", roots);
|
string extra = MergeTextLayers("extra.md", roots);
|
||||||
|
|
||||||
// Legacy personas.json: only when no overlay persona folder exists for this id.
|
|
||||||
string overlayPersonaDir = Path.Combine(_overlayRoot, "personas", id);
|
|
||||||
bool hasOverlayFolder = Directory.Exists(overlayPersonaDir)
|
|
||||||
&& (File.Exists(Path.Combine(overlayPersonaDir, "persona.json"))
|
|
||||||
|| File.Exists(Path.Combine(overlayPersonaDir, "extra.md")));
|
|
||||||
if (!hasOverlayFolder)
|
|
||||||
{
|
|
||||||
string overlayJson = Path.Combine(_overlayRoot, "personas.json");
|
|
||||||
JObject legacy = TryReadJson(overlayJson);
|
|
||||||
if (legacy?["personas"] is JArray arr)
|
|
||||||
{
|
|
||||||
foreach (JToken t in arr)
|
|
||||||
{
|
|
||||||
if (t is JObject po && string.Equals(SafeId(po["id"]?.ToString()), id, StringComparison.OrdinalIgnoreCase))
|
|
||||||
{
|
|
||||||
JObject persona = shelves["persona"] as JObject ?? new JObject();
|
|
||||||
string title = po["title"]?.ToString();
|
|
||||||
if (!string.IsNullOrWhiteSpace(title))
|
|
||||||
{
|
|
||||||
persona["title"] = title;
|
|
||||||
}
|
|
||||||
shelves["persona"] = persona;
|
|
||||||
string prompt = po["prompt"]?.ToString();
|
|
||||||
if (!string.IsNullOrWhiteSpace(prompt))
|
|
||||||
{
|
|
||||||
extra = string.IsNullOrWhiteSpace(extra) ? prompt : extra + "\n\n" + prompt;
|
|
||||||
}
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shelves["extra"] = extra ?? "";
|
shelves["extra"] = extra ?? "";
|
||||||
return shelves;
|
return shelves;
|
||||||
}
|
}
|
||||||
@@ -1427,10 +1370,14 @@ public sealed class AssistentConfig
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void SaveSettings(JObject settings)
|
public void SaveSettings(JObject settings)
|
||||||
|
{
|
||||||
|
lock (_lock)
|
||||||
{
|
{
|
||||||
Directory.CreateDirectory(_overlayRoot);
|
Directory.CreateDirectory(_overlayRoot);
|
||||||
string path = Path.Combine(_overlayRoot, "settings.json");
|
string path = Path.Combine(_overlayRoot, "settings.json");
|
||||||
File.WriteAllText(path, (settings ?? new JObject()).ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
JObject merged = DeepMerge(LoadSettings(), settings ?? new JObject());
|
||||||
|
File.WriteAllText(path, merged.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public JObject LoadOllamaRoles()
|
public JObject LoadOllamaRoles()
|
||||||
@@ -1471,7 +1418,7 @@ public sealed class AssistentConfig
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (clientSkills is not null && clientSkills.Count > 0)
|
if (clientSkills is not null)
|
||||||
{
|
{
|
||||||
enabled.Clear();
|
enabled.Clear();
|
||||||
foreach (JToken t in clientSkills)
|
foreach (JToken t in clientSkills)
|
||||||
@@ -1534,6 +1481,7 @@ public sealed class AssistentConfig
|
|||||||
["identity"] = identity,
|
["identity"] = identity,
|
||||||
["identity_summary"] = RenderIdentityBlock(id, includeAllShelves: true),
|
["identity_summary"] = RenderIdentityBlock(id, includeAllShelves: true),
|
||||||
["enabled_skills"] = new JArray(ResolveEnabledSkills(id, null)),
|
["enabled_skills"] = new JArray(ResolveEnabledSkills(id, null)),
|
||||||
|
["patch_keys"] = new JArray(LoadPatchKeys()),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -9,13 +9,12 @@ using SwarmUI.Utils;
|
|||||||
|
|
||||||
namespace Mrleo1nid.SwarmAssistent;
|
namespace Mrleo1nid.SwarmAssistent;
|
||||||
|
|
||||||
/// <summary>Runtime store in the same sqlite file: chats, ui-state, taste.
|
/// <summary>Runtime store in the same sqlite file: chats, ui-state.
|
||||||
/// Config overlays, sidecar cards, and ollama-roles stay on disk.</summary>
|
/// Config overlays, sidecar cards, and ollama-roles stay on disk.</summary>
|
||||||
public sealed partial class AssistentMemory
|
public sealed partial class AssistentMemory
|
||||||
{
|
{
|
||||||
public const int MaxChatsStored = 200;
|
public const int MaxChatsStored = 200;
|
||||||
public const string KvUiState = "ui_state";
|
public const string KvUiState = "ui_state";
|
||||||
public const string KvTaste = "taste";
|
|
||||||
|
|
||||||
void EnsureStoreSchema()
|
void EnsureStoreSchema()
|
||||||
{
|
{
|
||||||
@@ -39,7 +38,6 @@ public sealed partial class AssistentMemory
|
|||||||
CREATE INDEX IF NOT EXISTS idx_chats_updated ON chats(updated_at DESC);
|
CREATE INDEX IF NOT EXISTS idx_chats_updated ON chats(updated_at DESC);
|
||||||
""");
|
""");
|
||||||
EnsureChatsFts();
|
EnsureChatsFts();
|
||||||
MigrateJsonStoreOnce();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void EnsureChatsFts()
|
void EnsureChatsFts()
|
||||||
@@ -96,102 +94,6 @@ public sealed partial class AssistentMemory
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void MigrateJsonStoreOnce()
|
|
||||||
{
|
|
||||||
if (GetMeta("json_store_migrated") == "1")
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
string root = Path.Combine(_dataRoot, "Assistent");
|
|
||||||
int chats = 0;
|
|
||||||
string chatsDir = Path.Combine(root, "chats");
|
|
||||||
if (Directory.Exists(chatsDir))
|
|
||||||
{
|
|
||||||
foreach (string file in Directory.EnumerateFiles(chatsDir, "*.json"))
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
JObject chat = JObject.Parse(File.ReadAllText(file, Encoding.UTF8));
|
|
||||||
string id = (chat["id"]?.ToString() ?? Path.GetFileNameWithoutExtension(file) ?? "").Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(id) || GetChatUnlocked(id) is not null)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
UpsertChatUnlocked(chat, id);
|
|
||||||
chats++;
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logs.Debug($"AssistentMemory migrate chat {file}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ImportKvFile(Path.Combine(root, "ui-state.json"), KvUiState);
|
|
||||||
ImportKvFile(Path.Combine(root, "taste.json"), KvTaste);
|
|
||||||
SetMeta("json_store_migrated", "1");
|
|
||||||
TryArchiveMigratedJson(root, chatsDir);
|
|
||||||
if (chats > 0)
|
|
||||||
{
|
|
||||||
Logs.Debug($"AssistentMemory: migrated {chats} chats from JSON into sqlite");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void ImportKvFile(string path, string key)
|
|
||||||
{
|
|
||||||
if (!File.Exists(path) || !string.IsNullOrEmpty(GetKvUnlocked(key)))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
SetKvUnlocked(key, File.ReadAllText(path, Encoding.UTF8));
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logs.Debug($"AssistentMemory migrate {key}: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void TryArchiveMigratedJson(string root, string chatsDir)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
string dest = Path.Combine(root, "_migrated_json");
|
|
||||||
Directory.CreateDirectory(dest);
|
|
||||||
MoveIfExists(Path.Combine(root, "ui-state.json"), Path.Combine(dest, "ui-state.json"));
|
|
||||||
MoveIfExists(Path.Combine(root, "taste.json"), Path.Combine(dest, "taste.json"));
|
|
||||||
if (!Directory.Exists(chatsDir))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
string chatsDest = Path.Combine(dest, "chats");
|
|
||||||
Directory.CreateDirectory(chatsDest);
|
|
||||||
foreach (string file in Directory.EnumerateFiles(chatsDir, "*.json"))
|
|
||||||
{
|
|
||||||
MoveIfExists(file, Path.Combine(chatsDest, Path.GetFileName(file)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logs.Debug($"AssistentMemory archive json: {ex.Message}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static void MoveIfExists(string src, string dest)
|
|
||||||
{
|
|
||||||
if (!File.Exists(src))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (File.Exists(dest))
|
|
||||||
{
|
|
||||||
File.Delete(src);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(dest)!);
|
|
||||||
File.Move(src, dest);
|
|
||||||
}
|
|
||||||
|
|
||||||
public string GetKv(string key)
|
public string GetKv(string key)
|
||||||
{
|
{
|
||||||
lock (_lock)
|
lock (_lock)
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ namespace Mrleo1nid.SwarmAssistent;
|
|||||||
/// <summary>Facts about the human at the desk — global + per-persona, separate from craft RAG.</summary>
|
/// <summary>Facts about the human at the desk — global + per-persona, separate from craft RAG.</summary>
|
||||||
public sealed partial class AssistentMemory
|
public sealed partial class AssistentMemory
|
||||||
{
|
{
|
||||||
const string MetaTasteMigrated = "user_prefs_taste_migrated";
|
|
||||||
|
|
||||||
void EnsureUserPrefsSchema()
|
void EnsureUserPrefsSchema()
|
||||||
{
|
{
|
||||||
Exec(
|
Exec(
|
||||||
@@ -29,53 +27,6 @@ public sealed partial class AssistentMemory
|
|||||||
);
|
);
|
||||||
CREATE INDEX IF NOT EXISTS idx_user_prefs_scope ON user_prefs(scope, persona_id);
|
CREATE INDEX IF NOT EXISTS idx_user_prefs_scope ON user_prefs(scope, persona_id);
|
||||||
""");
|
""");
|
||||||
MigrateTasteToUserPrefsOnce();
|
|
||||||
}
|
|
||||||
|
|
||||||
void MigrateTasteToUserPrefsOnce()
|
|
||||||
{
|
|
||||||
if (GetMeta(MetaTasteMigrated) == "1")
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
JObject taste = GetKvObject(KvTaste);
|
|
||||||
if (taste is not null && taste.Count > 0)
|
|
||||||
{
|
|
||||||
long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
|
||||||
void AddList(string prefix, JToken arr)
|
|
||||||
{
|
|
||||||
if (arr is not JArray a)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
int i = 0;
|
|
||||||
foreach (JToken t in a)
|
|
||||||
{
|
|
||||||
string text = (t?.ToString() ?? "").Trim();
|
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
UpsertUserPrefUnlocked($"{prefix}_{i++}", text, "global", "", "migrated_taste", pinned: false, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
AddList("like", taste["likes"]);
|
|
||||||
AddList("avoid", taste["avoid"]);
|
|
||||||
AddList("style", taste["styles"]);
|
|
||||||
string notes = (taste["notes"]?.ToString() ?? "").Trim();
|
|
||||||
if (!string.IsNullOrWhiteSpace(notes))
|
|
||||||
{
|
|
||||||
UpsertUserPrefUnlocked("notes", notes, "global", "", "migrated_taste", pinned: false, now);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
Logs.Debug($"AssistentMemory taste→user_prefs: {ex.Message}");
|
|
||||||
}
|
|
||||||
SetMeta(MetaTasteMigrated, "1");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static string NormalizePrefScope(string scope)
|
static string NormalizePrefScope(string scope)
|
||||||
|
|||||||
+19
-1
@@ -747,7 +747,25 @@ public sealed partial class AssistentMemory : IDisposable
|
|||||||
EnsureOpen();
|
EnsureOpen();
|
||||||
Dictionary<long, int> ftsRanks = FtsRowRanks(ftsMatch, Math.Max(40, options.TopK * 4));
|
Dictionary<long, int> ftsRanks = FtsRowRanks(ftsMatch, Math.Max(40, options.TopK * 4));
|
||||||
using SqliteCommand cmd = _conn.CreateCommand();
|
using SqliteCommand cmd = _conn.CreateCommand();
|
||||||
cmd.CommandText = "SELECT id, kind, key, text, source, embedding, persona FROM memories";
|
List<string> personaKeys = rank.Keys.ToList();
|
||||||
|
StringBuilder sql = new("SELECT id, kind, key, text, source, embedding, persona FROM memories WHERE persona IN (");
|
||||||
|
for (int i = 0; i < personaKeys.Count; i++)
|
||||||
|
{
|
||||||
|
if (i > 0)
|
||||||
|
{
|
||||||
|
sql.Append(',');
|
||||||
|
}
|
||||||
|
string pName = "$p" + i;
|
||||||
|
sql.Append(pName);
|
||||||
|
cmd.Parameters.AddWithValue(pName, personaKeys[i]);
|
||||||
|
}
|
||||||
|
sql.Append(')');
|
||||||
|
if (kindFilter is not null)
|
||||||
|
{
|
||||||
|
sql.Append(" AND kind = $kind");
|
||||||
|
cmd.Parameters.AddWithValue("$kind", kindFilter);
|
||||||
|
}
|
||||||
|
cmd.CommandText = sql.ToString();
|
||||||
using SqliteDataReader reader = cmd.ExecuteReader();
|
using SqliteDataReader reader = cmd.ExecuteReader();
|
||||||
while (reader.Read())
|
while (reader.Read())
|
||||||
{
|
{
|
||||||
|
|||||||
+26
-27
@@ -1,4 +1,5 @@
|
|||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
|
|
||||||
@@ -9,20 +10,9 @@ public partial class SwarmAssistentExtension
|
|||||||
{
|
{
|
||||||
static readonly Regex JsonFenceRe = new(@"```(?:json)?\s*([\s\S]*?)```", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
static readonly Regex JsonFenceRe = new(@"```(?:json)?\s*([\s\S]*?)```", RegexOptions.IgnoreCase | RegexOptions.Compiled);
|
||||||
|
|
||||||
static readonly string[] PatchKeys =
|
static string[] PatchKeys => _patchKeys ??= Config?.LoadPatchKeys() ?? [];
|
||||||
[
|
|
||||||
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler",
|
static string[] _patchKeys;
|
||||||
"actions", "search_query", "civitai_query",
|
|
||||||
"use_init_image", "clear_init_image", "init_creativity", "denoise",
|
|
||||||
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
|
|
||||||
"look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask",
|
|
||||||
"snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed",
|
|
||||||
"creativity", "intensity", "complexity", "movement",
|
|
||||||
"clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
|
|
||||||
"memory_query", "memory_kind", "tag_query", "user_prefs",
|
|
||||||
"inventory_query", "skills", "persona_shelves", "controls",
|
|
||||||
"variants",
|
|
||||||
];
|
|
||||||
|
|
||||||
static bool HasValue(JObject obj, string key)
|
static bool HasValue(JObject obj, string key)
|
||||||
{
|
{
|
||||||
@@ -84,6 +74,8 @@ public partial class SwarmAssistentExtension
|
|||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
JObject lastAny = null;
|
||||||
|
JObject lastTerminal = null;
|
||||||
foreach (Match match in JsonFenceRe.Matches(reply))
|
foreach (Match match in JsonFenceRe.Matches(reply))
|
||||||
{
|
{
|
||||||
string raw = match.Groups[1].Value.Trim();
|
string raw = match.Groups[1].Value.Trim();
|
||||||
@@ -96,7 +88,12 @@ public partial class SwarmAssistentExtension
|
|||||||
}
|
}
|
||||||
if (Array.Exists(PatchKeys, k => obj[k] is not null))
|
if (Array.Exists(PatchKeys, k => obj[k] is not null))
|
||||||
{
|
{
|
||||||
return NormalizePatch(obj);
|
JObject normalized = NormalizePatch(obj);
|
||||||
|
lastAny = normalized;
|
||||||
|
if (FenceIsTerminalPatch(obj))
|
||||||
|
{
|
||||||
|
lastTerminal = normalized;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -104,7 +101,7 @@ public partial class SwarmAssistentExtension
|
|||||||
// not json
|
// not json
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return null;
|
return lastTerminal ?? lastAny;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
@@ -264,43 +261,45 @@ public partial class SwarmAssistentExtension
|
|||||||
return ActionsContain(patch, "lookup_tags") ? ExtractSearchQuery(patch) : null;
|
return ActionsContain(patch, "lookup_tags") ? ExtractSearchQuery(patch) : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static string NextToolHop(JObject patch)
|
static string NextToolHop(JObject patch, HashSet<string> skip = null)
|
||||||
{
|
{
|
||||||
if (patch is null)
|
if (patch is null)
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
if (ActionsContain(patch, "memory_get"))
|
bool Skip(string tool) => skip is not null && skip.Contains(tool);
|
||||||
|
if (ActionsContain(patch, "memory_get") && !Skip("memory_get"))
|
||||||
{
|
{
|
||||||
return "memory_get";
|
return "memory_get";
|
||||||
}
|
}
|
||||||
if (ActionsContain(patch, "memory_search") || !string.IsNullOrWhiteSpace(patch["memory_query"]?.ToString()))
|
if ((ActionsContain(patch, "memory_search") || !string.IsNullOrWhiteSpace(patch["memory_query"]?.ToString()))
|
||||||
|
&& !Skip("memory_search"))
|
||||||
{
|
{
|
||||||
return "memory_search";
|
return "memory_search";
|
||||||
}
|
}
|
||||||
if (ActionsContain(patch, "lookup_tags") || !string.IsNullOrWhiteSpace(patch["tag_query"]?.ToString()))
|
if ((ActionsContain(patch, "lookup_tags") || !string.IsNullOrWhiteSpace(patch["tag_query"]?.ToString()))
|
||||||
|
&& !Skip("lookup_tags"))
|
||||||
{
|
{
|
||||||
return "lookup_tags";
|
return "lookup_tags";
|
||||||
}
|
}
|
||||||
if (ActionsContain(patch, "list_inventory") || !string.IsNullOrWhiteSpace(patch["inventory_query"]?.ToString()))
|
if ((ActionsContain(patch, "list_inventory") || !string.IsNullOrWhiteSpace(patch["inventory_query"]?.ToString()))
|
||||||
|
&& !Skip("list_inventory"))
|
||||||
{
|
{
|
||||||
return "list_inventory";
|
return "list_inventory";
|
||||||
}
|
}
|
||||||
if (ActionsContain(patch, "skill_load"))
|
if (ActionsContain(patch, "skill_load") && !Skip("skill_load"))
|
||||||
{
|
{
|
||||||
return "skill_load";
|
return "skill_load";
|
||||||
}
|
}
|
||||||
if (ActionsContain(patch, "persona_read"))
|
if (ActionsContain(patch, "persona_read") && !Skip("persona_read"))
|
||||||
{
|
{
|
||||||
return "persona_read";
|
return "persona_read";
|
||||||
}
|
}
|
||||||
if (ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch)))
|
if ((ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch)))
|
||||||
|
&& !Skip("civitai"))
|
||||||
{
|
{
|
||||||
return "civitai";
|
return "civitai";
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
static bool WantsCivitaiSearch(JObject patch)
|
|
||||||
=> ActionsContain(patch, "search_civitai") || !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch));
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -21,7 +21,7 @@ public partial class SwarmAssistentExtension
|
|||||||
static readonly string[] UiStateKeys =
|
static readonly string[] UiStateKeys =
|
||||||
[
|
[
|
||||||
"pack", "persona", "auto_vision", "auto_apply", "auto_generate", "auto_critique",
|
"pack", "persona", "auto_vision", "auto_apply", "auto_generate", "auto_critique",
|
||||||
"auto_download", "pane_width", "embed_model", "base_url", "model", "view", "board_tab",
|
"auto_download", "pane_width", "embed_model", "base_url", "model", "view", "board_tab", "park_llm",
|
||||||
];
|
];
|
||||||
|
|
||||||
static string SafeChatId(string id)
|
static string SafeChatId(string id)
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ namespace Mrleo1nid.SwarmAssistent;
|
|||||||
/// <summary>Queue of models the assistant wants downloaded (merged into gpu-rent models.yaml on next up/capture).</summary>
|
/// <summary>Queue of models the assistant wants downloaded (merged into gpu-rent models.yaml on next up/capture).</summary>
|
||||||
public partial class SwarmAssistentExtension
|
public partial class SwarmAssistentExtension
|
||||||
{
|
{
|
||||||
|
static readonly object WantedFileLock = new();
|
||||||
|
|
||||||
string WantedModelsPath() => Path.Combine(DataRoot(), ".gpu-rent-wanted-models.yaml");
|
string WantedModelsPath() => Path.Combine(DataRoot(), ".gpu-rent-wanted-models.yaml");
|
||||||
|
|
||||||
string WantedCardsDir() => Path.Combine(DataRoot(), ".gpu-rent-wanted-cards");
|
string WantedCardsDir() => Path.Combine(DataRoot(), ".gpu-rent-wanted-cards");
|
||||||
@@ -45,6 +47,8 @@ public partial class SwarmAssistentExtension
|
|||||||
|
|
||||||
string path = WantedModelsPath();
|
string path = WantedModelsPath();
|
||||||
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? DataRoot());
|
Directory.CreateDirectory(Path.GetDirectoryName(path) ?? DataRoot());
|
||||||
|
lock (WantedFileLock)
|
||||||
|
{
|
||||||
Dictionary<string, List<WantedEntry>> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : "");
|
Dictionary<string, List<WantedEntry>> sections = LoadWantedYaml(File.Exists(path) ? File.ReadAllText(path, Encoding.UTF8) : "");
|
||||||
|
|
||||||
if (version_id > 0)
|
if (version_id > 0)
|
||||||
@@ -75,6 +79,7 @@ public partial class SwarmAssistentExtension
|
|||||||
}
|
}
|
||||||
bucket.Add(new WantedEntry { Url = url, Title = title, VersionId = version_id });
|
bucket.Add(new WantedEntry { Url = url, Title = title, VersionId = version_id });
|
||||||
File.WriteAllText(path, WriteWantedYaml(sections), Encoding.UTF8);
|
File.WriteAllText(path, WriteWantedYaml(sections), Encoding.UTF8);
|
||||||
|
}
|
||||||
|
|
||||||
if (card is not null && version_id > 0)
|
if (card is not null && version_id > 0)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json
|
|||||||
- Rich entries (blurbs/triggers) are selected + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them.
|
- Rich entries (blurbs/triggers) are selected + top krea-likely. Name-only rows need `list_inventory` + `inventory_query` before you rely on them.
|
||||||
- `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text.
|
- `memory_hits` may be truncated (`truncated: true`) — use `memory_get` for the full text.
|
||||||
- `has_vision_image` true means a real board frame exists. `images_in_request` true means JPEG bytes are in **this** request. **Do not** `look_at` just because a frame exists. Emit `look_at` only when you cannot continue without pixels (user asked to look/critique/describe/compare, or a defect you cannot infer from the prompt). Never invent what the image looks like. A new Generate / «ещё» / prompt edit does **not** need vision.
|
- `has_vision_image` true means a real board frame exists. `images_in_request` true means JPEG bytes are in **this** request. **Do not** `look_at` just because a frame exists. Emit `look_at` only when you cannot continue without pixels (user asked to look/critique/describe/compare, or a defect you cannot infer from the prompt). Never invent what the image looks like. A new Generate / «ещё» / prompt edit does **not** need vision.
|
||||||
- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `form_params`.
|
- Prefer `krea_likely` / Krea architecture; ignore FLUX/SDXL. Respect current params unless asked or pack is `fix_params`.
|
||||||
- Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields are documented in the active pack.
|
- Init/inpaint flags and `image_slots` are in the JSON. Extra pack fields are documented in the active pack.
|
||||||
|
|
||||||
## Memory (short)
|
## Memory (short)
|
||||||
@@ -40,7 +40,7 @@ Never write a “JSON Patch” section in prose without an actual fenced ```json
|
|||||||
## Output contract (mandatory)
|
## Output contract (mandatory)
|
||||||
|
|
||||||
1. Short helpful reply in the user's language (RU or EN).
|
1. Short helpful reply in the user's language (RU or EN).
|
||||||
2. One fenced JSON patch with **only fields you want to change**:
|
2. **Frame turns only:** one fenced JSON patch with **only fields you want to change**. Chat / Q&A / opinion / remember: **prose only — omit the JSON patch.**
|
||||||
|
|
||||||
```json
|
```json
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
[]
|
|
||||||
@@ -26,6 +26,6 @@ Otherwise **stay in ordinary** and just do the work.
|
|||||||
|
|
||||||
## Deliverable
|
## Deliverable
|
||||||
|
|
||||||
Same as write_prompt: short reply + fenced JSON **only if** this turn is a frame; then `actions: ["generate"]`. Chat/Q&A/opinion: no generate (prose is enough).
|
Short reply + fenced JSON **only on frame turns** (`actions: ["generate"]` when they want a new/updated image). Chat/Q&A/opinion/remember: prose only — no fence.
|
||||||
«давай дальше» / next frame = new English `prompt` + `negative` (echo live if unchanged) + `actions:["generate"]` in the **same** turn — never leave an empty `### JSON Patch` header. Chat may be RU; **Generate `prompt` is always EN** (skill `prompting`).
|
«давай дальше» / next frame = new English `prompt` + `negative` (echo live if unchanged) + `actions:["generate"]` in the **same** turn — never leave an empty `### JSON Patch` header. Chat may be RU; **Generate `prompt` is always EN** (skill `prompting`).
|
||||||
Several options in one ask → `variants` (2–4 partial patches with `label`); still one fence, still STOP after it.
|
Several options in one ask → `variants` (2–4 partial patches with `label`); still one fence, still STOP after it.
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
{
|
{
|
||||||
"id": "write_prompt",
|
"id": "write_prompt",
|
||||||
"title": "Написать промпт",
|
"title": "Написать промпт",
|
||||||
"order": 10,
|
"order": 2,
|
||||||
"aliases": ["write"],
|
"aliases": ["write"],
|
||||||
"prompt_file": "write_prompt.md"
|
"prompt_file": "ordinary.md",
|
||||||
|
"enabled": true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +1,3 @@
|
|||||||
# Mode: write_prompt
|
# Mode: write_prompt (alias)
|
||||||
|
|
||||||
Goal: craft or improve a **Krea 2** prompt that will generate well on Turbo (local Swarm). Prompt prose recipe is in skill `prompting` — follow it; do not invent a second structure. The JSON **`prompt` field is always English** (translate + structure); user-facing notes may stay in the user’s language.
|
Alias of **ordinary** — same deliverable. Use pack id `write` / `write_prompt` when the user wants prompt-focused wording; behavior and JSON contract are identical to ordinary.
|
||||||
|
|
||||||
## Deliverable
|
|
||||||
|
|
||||||
- Brief note of what you changed.
|
|
||||||
- JSON patch with at least `prompt` and `negative` (create / supplement / echo live), and `loras` when relevant.
|
|
||||||
- `actions: ["generate"]` only when they want a new/updated image (scene, edit, «ещё») — they do **not** have to type «генерируй». Skip generate for chat / Q&A / remember / look-only. Do **not** `look_at` unless they asked to see/critique the last frame.
|
|
||||||
- Prefer Exact Turbo defaults / `recommended_params`. Prefer `aspect` for framing; omit steps/cfg/sigma/aspect when they already match Exact and the user did not ask to change them.
|
|
||||||
- Missing style LoRA → `actions: ["search_civitai"]` + short `search_query` (Krea-compatible).
|
|
||||||
- User wants several options (оба / варианты / разный свет) → `variants: [{label, prompt|aspect|…}, …]` (2–4). Base keys inherit; each item overrides only its diffs. Still one fence.
|
|
||||||
|
|
||||||
### Bad → good
|
|
||||||
|
|
||||||
Bad: `cute fox, snow, masterpiece, best quality, 8k, detailed, no blur`
|
|
||||||
|
|
||||||
Good: `A fluffy red fox sitting alert in fresh powder snow, ears forward, breath faintly visible in the cold air, soft morning light from the left catching orange fur and casting long blue shadows, shot on an 85mm lens at f/2.8 with creamy bokeh, calm winter atmosphere, sharp eyes and whiskers.`
|
|
||||||
|
|||||||
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"keys": [
|
||||||
|
"prompt", "negative", "loras", "width", "height", "steps", "cfg", "seed", "sigma_shift", "sampler", "scheduler",
|
||||||
|
"actions", "search_query", "civitai_query",
|
||||||
|
"use_init_image", "clear_init_image", "init_creativity", "denoise",
|
||||||
|
"use_mask_image", "clear_mask_image", "mask_blur", "mask_grow",
|
||||||
|
"look_at", "vision_from", "vision_slots", "slot_to_init", "slot_to_mask",
|
||||||
|
"snapshot_generate", "select_slot", "aspect", "images", "batch", "vary", "lock_seed",
|
||||||
|
"creativity", "intensity", "complexity", "movement",
|
||||||
|
"clear_prompt_images", "slot_to_prompt_image", "pack", "memories", "memory",
|
||||||
|
"memory_query", "memory_kind", "tag_query", "user_prefs",
|
||||||
|
"inventory_query", "skills", "persona_shelves", "persona_clone", "persona", "controls",
|
||||||
|
"variants"
|
||||||
|
]
|
||||||
|
}
|
||||||
@@ -26,3 +26,9 @@ The **chat model** prepares the Generate-box text for **Krea 2** (Qwen3-VL). Do
|
|||||||
|
|
||||||
User-facing prose: short, in the user’s language.
|
User-facing prose: short, in the user’s language.
|
||||||
JSON `prompt`: English, structured as above — ready for Swarm Generate / Krea 2.
|
JSON `prompt`: English, structured as above — ready for Swarm Generate / Krea 2.
|
||||||
|
|
||||||
|
### Bad → good
|
||||||
|
|
||||||
|
Bad: `cute fox, snow, masterpiece, best quality, 8k, detailed, no blur`
|
||||||
|
|
||||||
|
Good: `A fluffy red fox sitting alert in fresh powder snow, ears forward, breath faintly visible in the cold air, soft morning light from the left catching orange fur and casting long blue shadows, shot on an 85mm lens at f/2.8 with creamy bokeh, calm winter atmosphere, sharp eyes and whiskers.`
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ Reserved (not identity dump): `assistant.json`, `ui.json`, `skills.json`, packs/
|
|||||||
|
|
||||||
## Overlay vs bundled
|
## Overlay vs bundled
|
||||||
|
|
||||||
- **Bundled** ships with the extension (`neutral`, `lewd`, `leonid`, …).
|
- **Bundled** ships with the extension (`neutral`, `lewd`, `aggressive`, `cinema`, `leonid`, …). Persona `terse` was removed; saved ids map to `aggressive`.
|
||||||
- **Overlay** on the data volume = this install. Clones from chat go here only.
|
- **Overlay** on the data volume = this install. Clones from chat go here only.
|
||||||
- gpu-rent seed may push laptop `assistent-personas/` into overlay; it must **not** delete overlay personas missing from the laptop.
|
- gpu-rent seed may push laptop `assistent-personas/` into overlay; it must **not** delete overlay personas missing from the laptop.
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
{
|
{
|
||||||
"always": [
|
"always": [
|
||||||
"Short sentences",
|
"Short sentences",
|
||||||
"Say what is wrong and what to change",
|
"Say what is wrong and what to change"
|
||||||
"Prefer actions generate when a re-roll is obviously needed"
|
|
||||||
],
|
],
|
||||||
"never": [
|
"never": [
|
||||||
"Invent LoRA names — aggression is tone, not hallucination",
|
"Invent LoRA names — aggression is tone, not hallucination",
|
||||||
"Soft padding"
|
"Soft padding",
|
||||||
|
"Force generate on Q&A or remember-only turns — core contract decides frame vs chat"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"notes": ["lectures", "filler", "long explanations"]
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
{
|
|
||||||
"notes": ["decisive patches", "one main fix"]
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
{
|
|
||||||
"title": "Короткий",
|
|
||||||
"tagline": "High-signal short replies",
|
|
||||||
"accent": "#56b6c2"
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
{
|
|
||||||
"always": [
|
|
||||||
"Reply in 1–2 short sentences, then the JSON patch",
|
|
||||||
"NSFW: factual, minimal words"
|
|
||||||
],
|
|
||||||
"never": [
|
|
||||||
"Lectures or filler",
|
|
||||||
"Invent LoRA names or triggers"
|
|
||||||
]
|
|
||||||
}
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
{
|
|
||||||
"verbosity": "minimal",
|
|
||||||
"tone": ["short", "high-signal"],
|
|
||||||
"humor": "none",
|
|
||||||
"nsfw": "factual",
|
|
||||||
"address": "peer",
|
|
||||||
"language": "match_user"
|
|
||||||
}
|
|
||||||
@@ -2,6 +2,12 @@
|
|||||||
|
|
||||||
SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **About the user** prefs + craft vector memory, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate.
|
SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, **persona presets** (`Config/personas/`), **About the user** prefs + craft vector memory, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate.
|
||||||
|
|
||||||
|
**Turn model:** one user message is one *turn*. A turn may fan out into nested LLM *hops* — Krea prompt prep, empty-patch retry, vision, auto-critique. Hops share one `HOP_BUDGET`, never re-read the user's text (their prompt is client-authored), and pass the busy gate that blocks new user sends. What a reply does to generation state is decided once, in `resolveTurnIntent`: the model's `actions:["generate"]` / `look_at` win, RU intent heuristics only back it up when the model forgets, and an explicit «запомни, не генерируй» vetoes both.
|
||||||
|
|
||||||
|
**Version 0.11.9** — Distilled client: esbuild bundle (`Assets/assistent.bundle.js`), unified patch keys (`Config/_base/patch-keys.json`), taste stack removed (UserPrefs only), chat storage merge + all-chats disk save, `write_prompt` → alias of `ordinary`. Builds on prior 0.11.9 turn-intent work.
|
||||||
|
|
||||||
|
**Version 0.11.9** — One turn, one decision. Nested hops (Krea prep, empty-patch retry, vision, critique) share a `turnHops` budget and pass the busy gate — Krea prep and the empty-patch retry were silently no-ops since 0.10.22/0.11.2. Generate / `look_at` are decided in a single `resolveTurnIntent`; `ensureGenerateAction`, `shouldHonorLookAt` and the `wantsGen`/`willGen`/`suppressGen` tangle are gone. Builds on 0.11.8.
|
||||||
|
|
||||||
**Version 0.11.8** — `session_exact` remembers applied params that differ from Exact (not only when the user typed the knob). `/debug ask` uses a hidden Q&A pack: 5–10 line explain, no JSON/generate, dump stays a system note. Builds on 0.11.7.
|
**Version 0.11.8** — `session_exact` remembers applied params that differ from Exact (not only when the user typed the knob). `/debug ask` uses a hidden Q&A pack: 5–10 line explain, no JSON/generate, dump stays a system note. Builds on 0.11.7.
|
||||||
|
|
||||||
**Version 0.11.7** — Generate only for a real frame request: chat/opinions no longer auto-run Swarm. Context still counts («нарисуй», «ещё одну», «другая поза»), not only «генерируй». Builds on 0.11.6.
|
**Version 0.11.7** — Generate only for a real frame request: chat/opinions no longer auto-run Swarm. Context still counts («нарисуй», «ещё одну», «другая поза»), not only «генерируй». Builds on 0.11.6.
|
||||||
@@ -50,10 +56,22 @@ Assistent/
|
|||||||
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
|
_base/ personas/<id>/ # overlay presets — same names as Config/, sparse
|
||||||
settings.json # embed_model, base_url, per-persona skills
|
settings.json # embed_model, base_url, per-persona skills
|
||||||
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
|
ollama-roles.json # chat vs memory model tags (gpu-rent writes this)
|
||||||
memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state + taste (legacy)
|
memory/assistent.sqlite # craft RAG + user_prefs + tags FTS + chats + ui_state
|
||||||
_migrated_json/ # one-shot archive of old chats/*.json, ui-state.json, taste.json
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Client build
|
||||||
|
|
||||||
|
Sources live in `src/` (ES modules). The VM ships the committed bundle only (no Node required at runtime):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
npm install
|
||||||
|
npm run build # → Assets/assistent.bundle.js
|
||||||
|
npm run watch # rebuild on save
|
||||||
|
npm test # intent.js + patch.js
|
||||||
|
```
|
||||||
|
|
||||||
|
SwarmUI loads a single script: `Assets/assistent.bundle.js`.
|
||||||
|
|
||||||
Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`.
|
Copy `personas/leonid/` → new id, edit only differing JSON. See `Config/personas/README.md`.
|
||||||
|
|
||||||
**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). Leonid: **Вкус** + **Хорни**; `/остынь`, `/horny-game`.
|
**Controls:** optional `controls.json` schema + `exact.controls` values. UI auto-draws every slider (`order`, `display: percent`). LLM may patch `"controls": {…}`. Values persist in overlay Exact (DeepMerge partial saves). Leonid: **Вкус** + **Хорни**; `/остынь`, `/horny-game`.
|
||||||
@@ -76,7 +94,6 @@ Separate sqlite table `user_prefs` (not craft RAG):
|
|||||||
- **Persona** — only the current agent
|
- **Persona** — only the current agent
|
||||||
- Injected as `## About the user`; strength via `user_prefs_weight` / `user_prefs_max` in `assistant.json` (⚙ → О пользователе)
|
- Injected as `## About the user`; strength via `user_prefs_weight` / `user_prefs_max` in `assistant.json` (⚙ → О пользователе)
|
||||||
- Agent write: `actions: ["user_pref_upsert"]` + `user_prefs: [{key,text,scope}]`
|
- Agent write: `actions: ["user_pref_upsert"]` + `user_prefs: [{key,text,scope}]`
|
||||||
- Legacy `kv.taste` migrates once into global prefs
|
|
||||||
|
|
||||||
## Craft vector memory
|
## Craft vector memory
|
||||||
|
|
||||||
@@ -92,9 +109,8 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
|
|||||||
## Chats and runtime KV
|
## Chats and runtime KV
|
||||||
|
|
||||||
- Every chat (messages + Generate params snapshot) is a row in `assistent.sqlite`, newest **200** kept. History search uses FTS over title + body.
|
- Every chat (messages + Generate params snapshot) is a row in `assistent.sqlite`, newest **200** kept. History search uses FTS over title + body.
|
||||||
- First launch after 0.8.3 copies `chats/*.json`, `ui-state.json`, and `taste.json` into sqlite, then archives them under `_migrated_json/`.
|
- All chats with messages are debounced to disk (not only the active one). Load merges disk + localStorage by `updatedAt`.
|
||||||
- localStorage stays as a fast cache; on first run with an empty store the old `swarm_assistent_chats_v1` browser history is migrated up once.
|
- UI state whitelists `park_llm` among other keys in sqlite `kv.ui_state`.
|
||||||
- UI state seeds a **fresh** browser only — anything already in localStorage wins, and `auto_download` is never restored as on
|
|
||||||
- `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights.
|
- `settings.json` and persona overlays stay files (layered merge + git). `.assistent.json` cards stay next to weights.
|
||||||
## VRAM handover
|
## VRAM handover
|
||||||
|
|
||||||
@@ -115,6 +131,8 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
|
|||||||
| Command | Effect |
|
| Command | Effect |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `/help` | List commands |
|
| `/help` | List commands |
|
||||||
|
| `/new` | New chat (current one is saved) |
|
||||||
|
| `/history` | Open saved chats |
|
||||||
| `/debug` | Short UI/Exact dump (no LLM) |
|
| `/debug` | Short UI/Exact dump (no LLM) |
|
||||||
| `/debug ask` / `/why` | Dump + short model explanation |
|
| `/debug ask` / `/why` | Dump + short model explanation |
|
||||||
| `/gen` | Generate now |
|
| `/gen` | Generate now |
|
||||||
@@ -124,7 +142,8 @@ Two layers in `memory/assistent.sqlite` (`persona` column; empty = shared):
|
|||||||
| `/aspect 16:9` | Set size from the official 1K table |
|
| `/aspect 16:9` | Set size from the official 1K table |
|
||||||
| `/seed lock\|random` | Lock or randomize seed |
|
| `/seed lock\|random` | Lock or randomize seed |
|
||||||
| `/vary` | New seed, same prompt (+ generate if auto) |
|
| `/vary` | New seed, same prompt (+ generate if auto) |
|
||||||
| `/pack write\|critique\|…` | Switch pack |
|
| `/pack write\|ordinary\|critique\|compose\|params\|inpaint\|describe\|card\|persona` | Switch pack |
|
||||||
|
| `/persona new\|clone\|save` | Overlay persona authoring |
|
||||||
| `/civitai <query>` | Ask LLM to search Civitai |
|
| `/civitai <query>` | Ask LLM to search Civitai |
|
||||||
| `/inventory` | Rescan models + refresh LoRA list |
|
| `/inventory` | Rescan models + refresh LoRA list |
|
||||||
|
|
||||||
@@ -149,18 +168,21 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
|
|||||||
|
|
||||||
## Packs & skills
|
## Packs & skills
|
||||||
|
|
||||||
**Packs** (one active): `ordinary` (default комбайн), `write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`.
|
**Packs** (one active): `ordinary` (default комбайн; covers write/critique/params flows), `write_prompt` (alias → same as ordinary), `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`, `author_persona`.
|
||||||
|
|
||||||
|
Patch fence keys: single source `Config/_base/patch-keys.json` → C# + client via `AssistentGetConfig.patch_keys`.
|
||||||
|
|
||||||
**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs.
|
**Skills** (checkboxes): `prompting`, `creativity_sliders`, `memory` — procedures; encyclopedia numbers live in Exact, soft notes in memory-seed / RAG, human taste in UserPrefs.
|
||||||
|
|
||||||
**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `terse`, `leonid` under `Config/personas/`. Overlay clones via `/persona new` or ⚙ → Личности.
|
**Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `leonid` under `Config/personas/`. Saved `terse` falls back to `aggressive`. Overlay clones via `/persona new` or ⚙ → Личности.
|
||||||
|
|
||||||
## API routes
|
## API routes
|
||||||
|
|
||||||
| Route | Role |
|
| Route | Role |
|
||||||
| --- | --- |
|
| --- | --- |
|
||||||
| `AssistentListModels` | Ollama tags → `models` (chat) + `memory_models` |
|
| `AssistentListModels` | Ollama tags → `models` (chat) + `memory_models` |
|
||||||
| `AssistentGetConfig` | Merged preset for persona (ui, packs, skills, identity, controls) |
|
| `AssistentGetConfig` | Merged preset for persona (ui, packs, skills, identity, controls, **patch_keys**) |
|
||||||
|
| `AssistentSaveSettings` | Overlay settings DeepMerge (skills, embed_model) |
|
||||||
| `AssistentSaveControls` | Persist Exact `controls` values for a persona (overlay) |
|
| `AssistentSaveControls` | Persist Exact `controls` values for a persona (overlay) |
|
||||||
| `AssistentGetPersonaShelves` | Merged identity shelves + controls |
|
| `AssistentGetPersonaShelves` | Merged identity shelves + controls |
|
||||||
| `AssistentClonePersona` | Snapshot clone → overlay id |
|
| `AssistentClonePersona` | Snapshot clone → overlay id |
|
||||||
@@ -168,14 +190,11 @@ Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
|
|||||||
| `AssistentDeletePersona` | UI-only delete of overlay persona |
|
| `AssistentDeletePersona` | UI-only delete of overlay persona |
|
||||||
| `AssistentExportPersona` / `AssistentImportPersona` | Shareable `.assistent-persona.json` pack |
|
| `AssistentExportPersona` / `AssistentImportPersona` | Shareable `.assistent-persona.json` pack |
|
||||||
| `AssistentSaveKnobs` | Overlay `_base/assistant.json` + Exact turbo/raw profiles |
|
| `AssistentSaveKnobs` | Overlay `_base/assistant.json` + Exact turbo/raw profiles |
|
||||||
| `AssistentGetSettings` / `AssistentSaveSettings` | Overlay settings (skills, embed_model) |
|
|
||||||
| `AssistentListInventory` | LoRA / checkpoint / wildcard inventory |
|
| `AssistentListInventory` | LoRA / checkpoint / wildcard inventory |
|
||||||
| `AssistentListPersonas` | Persona catalog |
|
| `AssistentListPersonas` | Persona catalog |
|
||||||
| `AssistentGetPacks` | Prompt pack texts |
|
|
||||||
| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) |
|
| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards (+ memory ingest) |
|
||||||
| `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash |
|
| `AssistentGetCardMeta` | Local sidecar + optional Civitai by-hash |
|
||||||
| `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) |
|
| `AssistentEnqueueWanted` / `AssistentListWanted` | Wanted YAML queue (write / read + count) |
|
||||||
| `AssistentGetTaste` / `AssistentSaveTaste` | sqlite `kv.taste` (legacy; prefer UserPrefs) |
|
|
||||||
| `AssistentListUserPrefs` / `AssistentUpsertUserPref` / `AssistentForgetUserPref` / `AssistentClearUserPrefs` | About the user |
|
| `AssistentListUserPrefs` / `AssistentUpsertUserPref` / `AssistentForgetUserPref` / `AssistentClearUserPrefs` | About the user |
|
||||||
| `AssistentSearchCivitai` | Civitai LoRA search |
|
| `AssistentSearchCivitai` | Civitai LoRA search |
|
||||||
| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) |
|
| `AssistentChat` / `AssistentChatWS` | Chat (+ user prefs + hybrid craft memory + hops) |
|
||||||
|
|||||||
@@ -28,15 +28,12 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
|
|
||||||
public override void OnPreInit()
|
public override void OnPreInit()
|
||||||
{
|
{
|
||||||
ScriptFiles.Add("Assets/assistent.api.js");
|
ScriptFiles.Add("Assets/assistent.bundle.js");
|
||||||
ScriptFiles.Add("Assets/assistent.patch.js");
|
|
||||||
ScriptFiles.Add("Assets/assistent.persist.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, persona presets, vector memory, model cards, Generate loop.";
|
Description = "Collaborative Krea 2 assistant: Ollama chat, persona presets, vector memory, model cards, Generate loop.";
|
||||||
License = "MIT";
|
License = "MIT";
|
||||||
Version = "0.11.8";
|
Version = "0.11.9";
|
||||||
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint", "memory"];
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,10 +43,8 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
Config = new AssistentConfig(FilePath, DataRoot());
|
Config = new AssistentConfig(FilePath, DataRoot());
|
||||||
Memory = new AssistentMemory(DataRoot(), HttpClient, Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString() ?? "nomic-embed-text");
|
Memory = new AssistentMemory(DataRoot(), HttpClient, Config.LoadAssistant(Config.DefaultPersonaId())["embed_model"]?.ToString() ?? "nomic-embed-text");
|
||||||
API.RegisterAPICall(AssistentListModels, false, PermUse);
|
API.RegisterAPICall(AssistentListModels, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentGetPacks, false, PermUse);
|
|
||||||
API.RegisterAPICall(AssistentListPersonas, false, PermUse);
|
API.RegisterAPICall(AssistentListPersonas, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentGetConfig, false, PermUse);
|
API.RegisterAPICall(AssistentGetConfig, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentGetSettings, false, PermUse);
|
|
||||||
API.RegisterAPICall(AssistentSaveSettings, true, PermUse);
|
API.RegisterAPICall(AssistentSaveSettings, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentListInventory, false, PermUse);
|
API.RegisterAPICall(AssistentListInventory, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentGetCard, false, PermUse);
|
API.RegisterAPICall(AssistentGetCard, false, PermUse);
|
||||||
@@ -57,8 +52,6 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse);
|
API.RegisterAPICall(AssistentEnqueueWanted, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentGetCardMeta, false, PermUse);
|
API.RegisterAPICall(AssistentGetCardMeta, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentSearchCivitai, false, PermUse);
|
API.RegisterAPICall(AssistentSearchCivitai, false, PermUse);
|
||||||
API.RegisterAPICall(AssistentGetTaste, false, PermUse);
|
|
||||||
API.RegisterAPICall(AssistentSaveTaste, true, PermUse);
|
|
||||||
API.RegisterAPICall(AssistentChat, true, PermUse);
|
API.RegisterAPICall(AssistentChat, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentChatWS, true, PermUse);
|
API.RegisterAPICall(AssistentChatWS, true, PermUse);
|
||||||
API.RegisterAPICall(AssistentListChats, false, PermUse);
|
API.RegisterAPICall(AssistentListChats, false, PermUse);
|
||||||
@@ -153,29 +146,6 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
return Environment.CurrentDirectory;
|
return Environment.CurrentDirectory;
|
||||||
}
|
}
|
||||||
|
|
||||||
public string ReadPackFile(string name)
|
|
||||||
{
|
|
||||||
return Config?.LoadPackPrompt(Config.DefaultPersonaId(), name);
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<JObject> AssistentGetPacks(Session session, string persona = null)
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
string pid = AssistentConfig.SafeId(persona) ?? Config.DefaultPersonaId();
|
|
||||||
JObject packs = new();
|
|
||||||
JArray order = [];
|
|
||||||
foreach (var p in Config.ListPacks(pid))
|
|
||||||
{
|
|
||||||
string text = Config.LoadPackPrompt(pid, p.id);
|
|
||||||
if (text is not null)
|
|
||||||
{
|
|
||||||
packs[p.id] = text;
|
|
||||||
}
|
|
||||||
order.Add(p.id);
|
|
||||||
}
|
|
||||||
return new JObject { ["success"] = true, ["packs"] = packs, ["order"] = order, ["persona"] = pid };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<JObject> AssistentGetConfig(Session session, string persona = null)
|
public async Task<JObject> AssistentGetConfig(Session session, string persona = null)
|
||||||
{
|
{
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
@@ -183,12 +153,6 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
return Config.BuildMergedConfigPayload(pid);
|
return Config.BuildMergedConfigPayload(pid);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<JObject> AssistentGetSettings(Session session)
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
return new JObject { ["success"] = true, ["settings"] = Config.LoadSettings() };
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<JObject> AssistentSaveSettings(Session session, JObject settings)
|
public async Task<JObject> AssistentSaveSettings(Session session, JObject settings)
|
||||||
{
|
{
|
||||||
await Task.CompletedTask;
|
await Task.CompletedTask;
|
||||||
@@ -236,39 +200,4 @@ public partial class SwarmAssistentExtension : Extension
|
|||||||
["personas"] = list,
|
["personas"] = list,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
public async Task<JObject> AssistentGetTaste(Session session)
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
return new JObject { ["success"] = true, ["taste"] = Memory.GetKvObject(AssistentMemory.KvTaste) };
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new JObject { ["error"] = $"taste: {ex.Message}" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async Task<JObject> AssistentSaveTaste(Session session, JObject taste)
|
|
||||||
{
|
|
||||||
await Task.CompletedTask;
|
|
||||||
if (taste is null)
|
|
||||||
{
|
|
||||||
return new JObject { ["error"] = "taste required" };
|
|
||||||
}
|
|
||||||
if (taste["updated"] == null)
|
|
||||||
{
|
|
||||||
taste["updated"] = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
|
||||||
}
|
|
||||||
try
|
|
||||||
{
|
|
||||||
Memory.SetKvObject(AssistentMemory.KvTaste, taste);
|
|
||||||
return new JObject { ["success"] = true, ["path"] = "Assistent/memory/assistent.sqlite" };
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return new JObject { ["error"] = $"taste save: {ex.Message}" };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -43,10 +43,10 @@
|
|||||||
<button type="button" class="basic-button sa-sessions-toggle" id="sa_btn_chats" title="История чатов" aria-expanded="false">История</button>
|
<button type="button" class="basic-button sa-sessions-toggle" id="sa_btn_chats" title="История чатов" aria-expanded="false">История</button>
|
||||||
<span class="sa-session-label" id="sa_session_label" title="Текущий чат — клик открывает Историю" role="button" tabindex="0">Новый чат</span>
|
<span class="sa-session-label" id="sa_session_label" title="Текущий чат — клик открывает Историю" role="button" tabindex="0">Новый чат</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-subtabs" role="tablist">
|
<div class="sa-subtabs" role="tablist" aria-label="Разделы Assistent">
|
||||||
<button type="button" class="sa-subtab sa-subtab-active" data-view="chat" id="sa_tab_chat">Чат</button>
|
<button type="button" class="sa-subtab sa-subtab-active" data-view="chat" id="sa_tab_chat" role="tab" aria-selected="true">Чат</button>
|
||||||
<button type="button" class="sa-subtab" data-view="cards" id="sa_tab_cards">Карточки</button>
|
<button type="button" class="sa-subtab" data-view="cards" id="sa_tab_cards" role="tab" aria-selected="false">Карточки</button>
|
||||||
<button type="button" class="sa-subtab" data-view="settings" id="sa_tab_settings">Настройки</button>
|
<button type="button" class="sa-subtab" data-view="settings" id="sa_tab_settings" role="tab" aria-selected="false">Настройки</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-chats-panel" id="sa_chats_panel" hidden>
|
<div class="sa-chats-panel" id="sa_chats_panel" hidden>
|
||||||
@@ -57,18 +57,18 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="sa-header-right">
|
<div class="sa-header-right">
|
||||||
<div class="sa-persona-wrap">
|
<div class="sa-persona-wrap">
|
||||||
<select id="sa_persona" class="sa-select" title="Характер / тон">
|
<select id="sa_persona" class="sa-select" title="Характер / тон" aria-label="Характер">
|
||||||
<option value="neutral">Нейтральный</option>
|
<option value="neutral">Нейтральный</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="basic-button sa-icon-btn" id="sa_persona_delete" title="Удалить overlay-личность" hidden aria-label="Удалить личность">✕</button>
|
<button type="button" class="basic-button sa-icon-btn" id="sa_persona_delete" title="Удалить overlay-личность" hidden aria-label="Удалить личность">✕</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-persona-controls" id="sa_persona_controls" hidden></div>
|
<div class="sa-persona-controls" id="sa_persona_controls" hidden></div>
|
||||||
<select id="sa_pack" class="sa-select" title="Пакет промпта">
|
<select id="sa_pack" class="sa-select" title="Пакет промпта" aria-label="Пакет промпта">
|
||||||
<option value="ordinary">Обычный</option>
|
<option value="ordinary">Обычный</option>
|
||||||
<option value="write_prompt">Написать промпт</option>
|
<option value="write_prompt">Написать промпт</option>
|
||||||
</select>
|
</select>
|
||||||
<span class="sa-mode-badge" id="sa_mode_badge" title="Активный pack">обычный</span>
|
<span class="sa-mode-badge" id="sa_mode_badge" title="Активный pack">обычный</span>
|
||||||
<select id="sa_model" class="sa-select sa-model-select" title="Модель Ollama (чат)">
|
<select id="sa_model" class="sa-select sa-model-select" title="Модель Ollama (чат)" aria-label="Модель Ollama">
|
||||||
<option value="">Загрузка моделей…</option>
|
<option value="">Загрузка моделей…</option>
|
||||||
</select>
|
</select>
|
||||||
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_settings" title="Настройки" aria-label="Настройки">⚙</button>
|
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_settings" title="Настройки" aria-label="Настройки">⚙</button>
|
||||||
@@ -90,7 +90,7 @@
|
|||||||
<div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры"></div>
|
<div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры"></div>
|
||||||
<div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div>
|
<div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div>
|
||||||
<div class="sa-slash-wrap">
|
<div class="sa-slash-wrap">
|
||||||
<textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить · /help = команды"></textarea>
|
<textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить · /help = команды" aria-label="Сообщение Assistent"></textarea>
|
||||||
<div class="sa-slash-menu" id="sa_slash_menu" hidden role="listbox"></div>
|
<div class="sa-slash-menu" id="sa_slash_menu" hidden role="listbox"></div>
|
||||||
</div>
|
</div>
|
||||||
<div class="sa-composer-actions">
|
<div class="sa-composer-actions">
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
<button type="button" class="sa-more-item" id="sa_btn_clear_patches" role="menuitem">Убрать только патчи</button>
|
<button type="button" class="sa-more-item" id="sa_btn_clear_patches" role="menuitem">Убрать только патчи</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<span class="sa-status" id="sa_status"></span>
|
<span class="sa-status" id="sa_status" role="status" aria-live="polite"></span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Generated
+497
@@ -0,0 +1,497 @@
|
|||||||
|
{
|
||||||
|
"name": "swarm-assistent",
|
||||||
|
"lockfileVersion": 3,
|
||||||
|
"requires": true,
|
||||||
|
"packages": {
|
||||||
|
"": {
|
||||||
|
"name": "swarm-assistent",
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/aix-ppc64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"aix"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/android-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"android"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/darwin-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"darwin"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/freebsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"freebsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==",
|
||||||
|
"cpu": [
|
||||||
|
"arm"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ia32": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-loong64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==",
|
||||||
|
"cpu": [
|
||||||
|
"loong64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-mips64el": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==",
|
||||||
|
"cpu": [
|
||||||
|
"mips64el"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-ppc64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==",
|
||||||
|
"cpu": [
|
||||||
|
"ppc64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-riscv64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==",
|
||||||
|
"cpu": [
|
||||||
|
"riscv64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-s390x": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==",
|
||||||
|
"cpu": [
|
||||||
|
"s390x"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/linux-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"linux"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/netbsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"netbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openbsd-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openbsd"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/openharmony-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"openharmony"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/sunos-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"sunos"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-arm64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==",
|
||||||
|
"cpu": [
|
||||||
|
"arm64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-ia32": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==",
|
||||||
|
"cpu": [
|
||||||
|
"ia32"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@esbuild/win32-x64": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==",
|
||||||
|
"cpu": [
|
||||||
|
"x64"
|
||||||
|
],
|
||||||
|
"dev": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"optional": true,
|
||||||
|
"os": [
|
||||||
|
"win32"
|
||||||
|
],
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/esbuild": {
|
||||||
|
"version": "0.25.12",
|
||||||
|
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz",
|
||||||
|
"integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==",
|
||||||
|
"dev": true,
|
||||||
|
"hasInstallScript": true,
|
||||||
|
"license": "MIT",
|
||||||
|
"bin": {
|
||||||
|
"esbuild": "bin/esbuild"
|
||||||
|
},
|
||||||
|
"engines": {
|
||||||
|
"node": ">=18"
|
||||||
|
},
|
||||||
|
"optionalDependencies": {
|
||||||
|
"@esbuild/aix-ppc64": "0.25.12",
|
||||||
|
"@esbuild/android-arm": "0.25.12",
|
||||||
|
"@esbuild/android-arm64": "0.25.12",
|
||||||
|
"@esbuild/android-x64": "0.25.12",
|
||||||
|
"@esbuild/darwin-arm64": "0.25.12",
|
||||||
|
"@esbuild/darwin-x64": "0.25.12",
|
||||||
|
"@esbuild/freebsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/freebsd-x64": "0.25.12",
|
||||||
|
"@esbuild/linux-arm": "0.25.12",
|
||||||
|
"@esbuild/linux-arm64": "0.25.12",
|
||||||
|
"@esbuild/linux-ia32": "0.25.12",
|
||||||
|
"@esbuild/linux-loong64": "0.25.12",
|
||||||
|
"@esbuild/linux-mips64el": "0.25.12",
|
||||||
|
"@esbuild/linux-ppc64": "0.25.12",
|
||||||
|
"@esbuild/linux-riscv64": "0.25.12",
|
||||||
|
"@esbuild/linux-s390x": "0.25.12",
|
||||||
|
"@esbuild/linux-x64": "0.25.12",
|
||||||
|
"@esbuild/netbsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/netbsd-x64": "0.25.12",
|
||||||
|
"@esbuild/openbsd-arm64": "0.25.12",
|
||||||
|
"@esbuild/openbsd-x64": "0.25.12",
|
||||||
|
"@esbuild/openharmony-arm64": "0.25.12",
|
||||||
|
"@esbuild/sunos-x64": "0.25.12",
|
||||||
|
"@esbuild/win32-arm64": "0.25.12",
|
||||||
|
"@esbuild/win32-ia32": "0.25.12",
|
||||||
|
"@esbuild/win32-x64": "0.25.12"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
{
|
||||||
|
"name": "swarm-assistent",
|
||||||
|
"private": true,
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"build": "node scripts/build.mjs",
|
||||||
|
"watch": "node scripts/build.mjs --watch",
|
||||||
|
"test": "node --test test/intent.test.js test/patch.test.js"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"esbuild": "^0.25.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
import * as esbuild from 'esbuild';
|
||||||
|
import { existsSync } from 'node:fs';
|
||||||
|
|
||||||
|
const watch = process.argv.includes('--watch');
|
||||||
|
const ctxOpts = {
|
||||||
|
entryPoints: ['src/main.js'],
|
||||||
|
outfile: 'Assets/assistent.bundle.js',
|
||||||
|
bundle: true,
|
||||||
|
format: 'iife',
|
||||||
|
target: ['es2020'],
|
||||||
|
sourcemap: false,
|
||||||
|
logLevel: 'info',
|
||||||
|
};
|
||||||
|
|
||||||
|
if (watch) {
|
||||||
|
const ctx = await esbuild.context(ctxOpts);
|
||||||
|
await ctx.watch();
|
||||||
|
console.log('watching src/ → Assets/assistent.bundle.js');
|
||||||
|
} else {
|
||||||
|
await esbuild.build(ctxOpts);
|
||||||
|
if (!existsSync('Assets/assistent.bundle.js')) {
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
console.log('built Assets/assistent.bundle.js');
|
||||||
|
}
|
||||||
+28
@@ -0,0 +1,28 @@
|
|||||||
|
/** Promise wrapper around SwarmUI genericRequest. */
|
||||||
|
export function createRequest() {
|
||||||
|
return function request(name, body) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (typeof genericRequest !== 'function') {
|
||||||
|
reject(new Error('genericRequest unavailable'));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
genericRequest(
|
||||||
|
name,
|
||||||
|
body || {},
|
||||||
|
(data) => {
|
||||||
|
if (data && data.error) {
|
||||||
|
reject(new Error(String(data.error)));
|
||||||
|
} else {
|
||||||
|
resolve(data);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
0,
|
||||||
|
(err) => reject(err instanceof Error ? err : new Error(String(err || 'request failed'))),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachApi(SA) {
|
||||||
|
SA.request = createRequest();
|
||||||
|
}
|
||||||
+233
-451
File diff suppressed because it is too large
Load Diff
+191
@@ -0,0 +1,191 @@
|
|||||||
|
/** Turn intent heuristics — pure functions testable with node --test. */
|
||||||
|
|
||||||
|
export function cyrTokenRe(alts) {
|
||||||
|
const boundary = '(^|[^0-9A-Za-z_А-Яа-яЁё])';
|
||||||
|
const end = '(?=$|[^0-9A-Za-z_А-Яа-яЁё])';
|
||||||
|
return new RegExp(`${boundary}(?:${alts})${end}`, 'i');
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userAsksGenerate(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^(gen|generate|go|рисуй|нарисуй)([!.…\s]|$)/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/^(ещё|еще)(\s+раз)?([!.…\s]|$)/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const letter = '[0-9A-Za-z_А-Яа-яЁё]';
|
||||||
|
const stem = `${letter}*`;
|
||||||
|
return cyrTokenRe(
|
||||||
|
'сгенерируй|сгенерировать|генерируй|generate|нарисуй|перегенерируй|перерисуй|'
|
||||||
|
+ `сделай\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
|
||||||
|
+ `хочу\\s+(картинк${stem}|изображен${stem}|фото${stem})|`
|
||||||
|
+ 'run\\s+generat|/gen',
|
||||||
|
).test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userAsksContinue(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^(давай\s+дальше|продолжай|продолжим|go\s+on|continue|keep\s+going|next(\s+one)?|next\s+frame)([!.…\s]|$)/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return cyrTokenRe(
|
||||||
|
'давай\\s+дальше|следующ(ий|ая|ее|ую)\\s+кадр|ещё\\s+кадр|еще\\s+кадр|'
|
||||||
|
+ 'кадр\\s*№?\\s*\\d+|сделай\\s+следующ',
|
||||||
|
).test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isSameButAspectRequest(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return cyrTokenRe(
|
||||||
|
'тот\\s+же\\s+(кадр|сцена|промпт|prompt)|так\\s+же\\s+но\\s+(друг|иной)\\s+(формат|размер|aspect|соотношен)|'
|
||||||
|
+ 'same\\s+but\\s+(wider|taller|16:9|4:3|portrait|landscape)',
|
||||||
|
).test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userAsksNoGenerate(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t || userAsksGenerate(t)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/\b(remember|save\s+(this\s+)?(as\s+)?(the\s+)?(base\s+)?(prompt|template)|don'?t\s+generat|do\s+not\s+generat|no\s+generat|without\s+generat)\b/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return cyrTokenRe(
|
||||||
|
'запомн|запомни|запомним|сохрани|сохраним|шаблон|'
|
||||||
|
+ 'базов(ый|ого|ому|ым|ая|ую|ое)?\\s+промпт|'
|
||||||
|
+ 'не\\s+генерир|без\\s+генерац|не\\s+надо\\s+генер|только\\s+запомн|пока\\s+запомн|'
|
||||||
|
+ 'не\\s+рисуй|не\\s+запускай\\s+генер',
|
||||||
|
).test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userCommandsGenerate(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t || userAsksNoGenerate(t)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return userAsksGenerate(t) || userAsksContinue(t) || isSameButAspectRequest(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userAsksLook(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/\b(look\s+at|critique|criticize|describe\s+(this|the|ref|image)|what\s+do\s+you\s+see)\b/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cyrTokenRe('критик[а-яё]*|что\\s+не\\s+так|разбери').test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cyrTokenRe('опиши\\s+(это|эту|реф|изображ[а-яё]*|картинк[а-яё]*|кадр|результат|референс)').test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return /(?:^|[^а-яёa-z0-9_])(посмотри|смотри|глянь)\s+(на\s+)?(результат|картинк[а-яё]*|изображен[а-яё]*|кадр|ген|реф)/i.test(t);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userIsChatNotFrame(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (/^(ок|окей|ok|okay|ладно|хорошо|понял|ясно|спасибо|thanks)([!.…\s]*)$/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (cyrTokenRe(
|
||||||
|
'что\\s+такое|как\\s+работает|зачем\\s+|какие\\s+(лор|модел|чекпоинт)|'
|
||||||
|
+ 'список\\s+лор|где\\s+настрой|что\\s+значит|'
|
||||||
|
+ 'нравит|спасибо|благодар|почему\\s+так|что\\s+ты\\s+(сделал|изменил)|'
|
||||||
|
+ 'только\\s+(ответь|скажи|объясни)|без\\s+(кадр|генерац)|не\\s+надо\\s+кадр',
|
||||||
|
).test(t) && !userAsksGenerate(t) && !userAsksContinue(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function userImpliesGenerate(text) {
|
||||||
|
const t = String(text || '').trim();
|
||||||
|
if (!t || userAsksNoGenerate(t) || userIsChatNotFrame(t)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (userCommandsGenerate(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (t.length < 8) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const wantsLook = userAsksLook(t);
|
||||||
|
const wantsRedraw = cyrTokenRe('поправь|исправь|перегенерир|перерисуй|улучши|переделай').test(t)
|
||||||
|
|| /\b(fix|redo|redraw|improve)\b/i.test(t);
|
||||||
|
if (wantsLook && !wantsRedraw) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (cyrTokenRe(
|
||||||
|
'нарису|сгенер|перерису|'
|
||||||
|
+ 'сделай\\s+(картинк|изображен|фото|кадр)|'
|
||||||
|
+ 'хочу\\s+(картинк|изображен|фото|увидеть|видеть)|'
|
||||||
|
+ 'покажи\\s+как\\s+(она|он|это)|'
|
||||||
|
+ 'сделай\\s+(её|ее|его|мне)\\s|'
|
||||||
|
+ 'пусть\\s+будет|'
|
||||||
|
+ 'другой\\s+(ракурс|свет|наряд|поза)|'
|
||||||
|
+ 'поменяй\\s+(позу|свет|одежд|фон)|добавь\\s+(свет|детал)|'
|
||||||
|
+ 'ещё\\s+одн|еще\\s+одн',
|
||||||
|
).test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (/\b(draw|paint|render|make her|make him|another one|new frame)\b/i.test(t)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const isQuestion = /[??]\s*$/.test(t);
|
||||||
|
if (isQuestion) {
|
||||||
|
return cyrTokenRe('нарису|сгенер|можешь\\s+(сделать|нарисовать)|можно\\s+(картинк|сгенер)').test(t);
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function packBlocksAutoGenerate(pack) {
|
||||||
|
const p = String(pack || '');
|
||||||
|
return p === 'describe_ref' || p === 'catalog_card' || p === 'author_persona' || p === 'debug_explain';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function packWantsVision(pack) {
|
||||||
|
const p = String(pack || '');
|
||||||
|
return p === 'critique_image' || p === 'describe_ref' || p === 'compose_scene' || p === 'inpaint_edit';
|
||||||
|
}
|
||||||
|
|
||||||
|
export function resolveTurnIntent(patch, userText, opts = {}, packId = '') {
|
||||||
|
const machine = !!opts.machineTurn;
|
||||||
|
const vetoed = !machine && userAsksNoGenerate(userText);
|
||||||
|
const commanded = !!opts.userWantsGenerate || (!machine && userCommandsGenerate(userText));
|
||||||
|
const implied = !machine && userImpliesGenerate(userText);
|
||||||
|
const modelAsked = Array.isArray(patch?.actions) && patch.actions.map(String).includes('generate');
|
||||||
|
|
||||||
|
let generate;
|
||||||
|
if (vetoed || opts.fromAutoCritique) {
|
||||||
|
generate = false;
|
||||||
|
} else if (commanded) {
|
||||||
|
generate = true;
|
||||||
|
} else if (packBlocksAutoGenerate(packId)) {
|
||||||
|
generate = false;
|
||||||
|
} else {
|
||||||
|
generate = modelAsked || implied;
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasLook = !!patch
|
||||||
|
&& (patch.look_at != null || patch.vision_from != null || patch.vision_slots != null);
|
||||||
|
const honorLook = opts.fromAutoCritique || opts.fromVisionHop
|
||||||
|
|| (!machine && userAsksLook(userText))
|
||||||
|
|| packWantsVision(packId);
|
||||||
|
const look = !!(hasLook && !vetoed && !generate && honorLook);
|
||||||
|
|
||||||
|
return { generate, look, vetoed };
|
||||||
|
}
|
||||||
+19
@@ -0,0 +1,19 @@
|
|||||||
|
import { attachApi } from './api.js';
|
||||||
|
import { attachPatch, setPatchKeys } from './patch.js';
|
||||||
|
import { attachPersist } from './persist.js';
|
||||||
|
|
||||||
|
window.SA = window.SA || {};
|
||||||
|
attachApi(window.SA);
|
||||||
|
attachPatch(window.SA);
|
||||||
|
attachPersist(window.SA);
|
||||||
|
|
||||||
|
/** Called from app after AssistentGetConfig — single source: Config/_base/patch-keys.json */
|
||||||
|
window.SA.applyConfigPatchKeys = function (config) {
|
||||||
|
const keys = config?.patch_keys;
|
||||||
|
if (Array.isArray(keys) && keys.length) {
|
||||||
|
setPatchKeys(keys);
|
||||||
|
window.SA.PATCH_KEYS = keys;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
import './app.js';
|
||||||
+145
@@ -0,0 +1,145 @@
|
|||||||
|
/** Patch detection / extraction — mirrors AssistentPatch.cs. */
|
||||||
|
|
||||||
|
const DEFAULT_PATCH_KEYS = [
|
||||||
|
'prompt', 'negative', 'loras', 'width', 'height', 'steps', 'cfg', 'seed', 'sigma_shift', 'sampler', 'scheduler',
|
||||||
|
'actions', 'search_query', 'civitai_query',
|
||||||
|
'use_init_image', 'clear_init_image', 'init_creativity', 'denoise',
|
||||||
|
'use_mask_image', 'clear_mask_image', 'mask_blur', 'mask_grow',
|
||||||
|
'look_at', 'vision_from', 'vision_slots', 'slot_to_init', 'slot_to_mask',
|
||||||
|
'snapshot_generate', 'select_slot', 'aspect', 'images', 'batch', 'vary', 'lock_seed',
|
||||||
|
'creativity', 'intensity', 'complexity', 'movement',
|
||||||
|
'clear_prompt_images', 'slot_to_prompt_image', 'pack', 'memories', 'memory',
|
||||||
|
'memory_query', 'memory_kind', 'tag_query', 'user_prefs',
|
||||||
|
'inventory_query', 'skills', 'persona_shelves', 'persona_clone', 'persona', 'controls',
|
||||||
|
'variants',
|
||||||
|
];
|
||||||
|
|
||||||
|
let PATCH_KEYS = DEFAULT_PATCH_KEYS.slice();
|
||||||
|
|
||||||
|
export function setPatchKeys(keys) {
|
||||||
|
if (Array.isArray(keys) && keys.length) {
|
||||||
|
PATCH_KEYS = keys.map(String);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getPatchKeys() {
|
||||||
|
return PATCH_KEYS.slice();
|
||||||
|
}
|
||||||
|
|
||||||
|
const FENCE_RE = /```(?:json)?\s*([\s\S]*?)```/gi;
|
||||||
|
|
||||||
|
function has(obj, key) {
|
||||||
|
return obj[key] !== undefined && obj[key] !== null;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isCardObject(obj) {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
const cardish = !!(obj.kind || obj.triggers || obj.when || obj.prompt_hint);
|
||||||
|
const genish = !!(obj.prompt != null || obj.negative != null || obj.loras || obj.actions
|
||||||
|
|| obj.width || obj.height || obj.steps != null || obj.cfg != null || obj.aspect || obj.seed != null
|
||||||
|
|| obj.search_query || obj.civitai_query || obj.look_at || obj.controls);
|
||||||
|
if (cardish && !genish && (obj.name || obj.triggers || obj.when)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return !!(obj.kind && obj.name && (obj.triggers || obj.when || obj.prompt_hint || obj.notes != null));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isPatchObject(obj) {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isCardObject(obj)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return PATCH_KEYS.some((k) => has(obj, k));
|
||||||
|
}
|
||||||
|
|
||||||
|
export function normalizePatch(patch) {
|
||||||
|
if (!patch || typeof patch !== 'object') {
|
||||||
|
return patch;
|
||||||
|
}
|
||||||
|
if (!has(patch, 'search_query') && has(patch, 'civitai_query')) {
|
||||||
|
patch.search_query = patch.civitai_query;
|
||||||
|
}
|
||||||
|
if (!has(patch, 'init_creativity') && has(patch, 'denoise')) {
|
||||||
|
patch.init_creativity = patch.denoise;
|
||||||
|
}
|
||||||
|
if (!has(patch, 'look_at')) {
|
||||||
|
if (has(patch, 'vision_from')) {
|
||||||
|
patch.look_at = patch.vision_from;
|
||||||
|
} else if (has(patch, 'vision_slots')) {
|
||||||
|
patch.look_at = patch.vision_slots;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return patch;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function extractPatch(text) {
|
||||||
|
if (!text) {
|
||||||
|
return { prose: text || '', patch: null };
|
||||||
|
}
|
||||||
|
const re = new RegExp(FENCE_RE.source, 'gi');
|
||||||
|
let match;
|
||||||
|
let lastPatch = null;
|
||||||
|
let prose = text;
|
||||||
|
while ((match = re.exec(text)) !== null) {
|
||||||
|
try {
|
||||||
|
const obj = JSON.parse(match[1].trim());
|
||||||
|
if (isPatchObject(obj)) {
|
||||||
|
lastPatch = normalizePatch(obj);
|
||||||
|
prose = (text.slice(0, match.index) + text.slice(match.index + match[0].length)).trim();
|
||||||
|
}
|
||||||
|
} catch (e) { /* not json */ }
|
||||||
|
}
|
||||||
|
return { prose, patch: lastPatch };
|
||||||
|
}
|
||||||
|
|
||||||
|
export function isTerminalStreamPatch(obj) {
|
||||||
|
if (!obj || typeof obj !== 'object') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (isCardObject(obj)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (Array.isArray(obj.variants) && obj.variants.length) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj.look_at != null || obj.vision_from != null || obj.vision_slots != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj.search_query != null || obj.civitai_query != null
|
||||||
|
|| obj.memory_query != null || obj.tag_query != null || obj.inventory_query != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
const acts = Array.isArray(obj.actions) ? obj.actions.map(String) : [];
|
||||||
|
const hopOrGen = [
|
||||||
|
'skill_load', 'persona_read', 'memory_get', 'memory_search', 'lookup_tags',
|
||||||
|
'list_inventory', 'search_civitai', 'interrupt', 'generate',
|
||||||
|
'memory_upsert', 'user_pref_upsert',
|
||||||
|
];
|
||||||
|
if (acts.some((a) => hopOrGen.includes(a))) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (String(obj.prompt || '').trim().length >= 48) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (obj.loras != null || obj.aspect != null || obj.steps != null
|
||||||
|
|| obj.width != null || obj.height != null || obj.cfg != null
|
||||||
|
|| obj.seed != null || obj.controls != null
|
||||||
|
|| obj.memories != null || obj.user_prefs != null) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachPatch(SA) {
|
||||||
|
SA.PATCH_KEYS = PATCH_KEYS;
|
||||||
|
SA.setPatchKeys = setPatchKeys;
|
||||||
|
SA.isCardObject = isCardObject;
|
||||||
|
SA.isPatchObject = isPatchObject;
|
||||||
|
SA.isTerminalStreamPatch = isTerminalStreamPatch;
|
||||||
|
SA.normalizePatch = normalizePatch;
|
||||||
|
SA.extractPatch = extractPatch;
|
||||||
|
}
|
||||||
+139
@@ -0,0 +1,139 @@
|
|||||||
|
/** Sqlite persistence for chats + UI state. */
|
||||||
|
|
||||||
|
const LS_CHATS = 'swarm_assistent_chats_v1';
|
||||||
|
const SAVE_DEBOUNCE_MS = 700;
|
||||||
|
|
||||||
|
const timers = { chats: new Map(), ui: null };
|
||||||
|
|
||||||
|
function normalizeChat(raw) {
|
||||||
|
if (!raw || !raw.id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
id: String(raw.id),
|
||||||
|
title: String(raw.title || 'Новый чат'),
|
||||||
|
createdAt: Number(raw.createdAt) || Date.now(),
|
||||||
|
updatedAt: Number(raw.updatedAt) || Date.now(),
|
||||||
|
messages: Array.isArray(raw.messages) ? raw.messages : [],
|
||||||
|
messages_count: Number(raw.messages_count) || (Array.isArray(raw.messages) ? raw.messages.length : 0),
|
||||||
|
params: raw.params && typeof raw.params === 'object' ? raw.params : null,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function attachPersist(SA, request = SA.request) {
|
||||||
|
async function loadChats() {
|
||||||
|
let chats = [];
|
||||||
|
try {
|
||||||
|
const data = await request('AssistentListChats', { with_messages: true });
|
||||||
|
chats = (data?.chats || []).map(normalizeChat).filter(Boolean);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Assistent: disk chats unavailable', e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
return chats.sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getChat(id) {
|
||||||
|
if (!id) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const data = await request('AssistentGetChat', { id });
|
||||||
|
return normalizeChat(data?.chat);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function searchChats(q) {
|
||||||
|
const query = String(q || '').trim();
|
||||||
|
if (query.length < 2) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const data = await request('AssistentListChats', { q: query, with_messages: false, limit: 40 });
|
||||||
|
return (data?.chats || []).map(normalizeChat).filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveChat(chat, { immediate = false } = {}) {
|
||||||
|
const clean = normalizeChat(chat);
|
||||||
|
if (!clean) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
const send = () => {
|
||||||
|
timers.chats.delete(clean.id);
|
||||||
|
return request('AssistentSaveChat', {
|
||||||
|
id: clean.id,
|
||||||
|
title: clean.title,
|
||||||
|
messages: clean.messages,
|
||||||
|
params: clean.params,
|
||||||
|
createdAt: clean.createdAt,
|
||||||
|
updatedAt: clean.updatedAt,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
if (immediate) {
|
||||||
|
const pending = timers.chats.get(clean.id);
|
||||||
|
if (pending) {
|
||||||
|
clearTimeout(pending);
|
||||||
|
}
|
||||||
|
return send();
|
||||||
|
}
|
||||||
|
const pending = timers.chats.get(clean.id);
|
||||||
|
if (pending) {
|
||||||
|
clearTimeout(pending);
|
||||||
|
}
|
||||||
|
timers.chats.set(clean.id, setTimeout(() => {
|
||||||
|
send().catch((e) => console.warn('Assistent: chat save failed', clean.id, e));
|
||||||
|
}, SAVE_DEBOUNCE_MS));
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
function deleteChat(id) {
|
||||||
|
if (!id) {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
const pending = timers.chats.get(id);
|
||||||
|
if (pending) {
|
||||||
|
clearTimeout(pending);
|
||||||
|
timers.chats.delete(id);
|
||||||
|
}
|
||||||
|
return request('AssistentDeleteChat', { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadUiState() {
|
||||||
|
try {
|
||||||
|
const data = await request('AssistentGetUiState', {});
|
||||||
|
const ui = data?.ui_state;
|
||||||
|
return ui && typeof ui === 'object' ? ui : null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function saveUiState(uiState, { immediate = false } = {}) {
|
||||||
|
if (!uiState || typeof uiState !== 'object') {
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
const send = () => {
|
||||||
|
timers.ui = null;
|
||||||
|
return request('AssistentSaveUiState', { ui_state: uiState });
|
||||||
|
};
|
||||||
|
if (timers.ui) {
|
||||||
|
clearTimeout(timers.ui);
|
||||||
|
timers.ui = null;
|
||||||
|
}
|
||||||
|
if (immediate) {
|
||||||
|
return send();
|
||||||
|
}
|
||||||
|
timers.ui = setTimeout(() => {
|
||||||
|
send().catch((e) => console.warn('Assistent: ui-state save failed', e));
|
||||||
|
}, SAVE_DEBOUNCE_MS);
|
||||||
|
return Promise.resolve(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
SA.persist = {
|
||||||
|
LS_CHATS,
|
||||||
|
loadChats,
|
||||||
|
getChat,
|
||||||
|
searchChats,
|
||||||
|
saveChat,
|
||||||
|
deleteChat,
|
||||||
|
loadUiState,
|
||||||
|
saveUiState,
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
userAsksLook,
|
||||||
|
userCommandsGenerate,
|
||||||
|
userAsksNoGenerate,
|
||||||
|
resolveTurnIntent,
|
||||||
|
packWantsVision,
|
||||||
|
} from '../src/intent.js';
|
||||||
|
|
||||||
|
describe('intent.js', () => {
|
||||||
|
it('userAsksLook detects critique RU', () => {
|
||||||
|
assert.equal(userAsksLook('посмотри на результат'), true);
|
||||||
|
assert.equal(userAsksLook('нарисуй лису'), false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('userCommandsGenerate respects veto', () => {
|
||||||
|
assert.equal(userCommandsGenerate('сгенерируй кадр'), true);
|
||||||
|
assert.equal(userCommandsGenerate('только запомни промпт'), false);
|
||||||
|
assert.equal(userAsksNoGenerate('только запомни промпт'), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('resolveTurnIntent honors look without generate', () => {
|
||||||
|
const patch = { look_at: ['generate'] };
|
||||||
|
const intent = resolveTurnIntent(patch, 'что не так с кадром?', {}, 'ordinary');
|
||||||
|
assert.equal(intent.generate, false);
|
||||||
|
assert.equal(intent.look, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('packWantsVision for critique pack', () => {
|
||||||
|
assert.equal(packWantsVision('critique_image'), true);
|
||||||
|
assert.equal(packWantsVision('ordinary'), false);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -0,0 +1,37 @@
|
|||||||
|
import { describe, it } from 'node:test';
|
||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import {
|
||||||
|
extractPatch,
|
||||||
|
isPatchObject,
|
||||||
|
isCardObject,
|
||||||
|
normalizePatch,
|
||||||
|
setPatchKeys,
|
||||||
|
} from '../src/patch.js';
|
||||||
|
|
||||||
|
describe('patch.js', () => {
|
||||||
|
it('extractPatch finds generation patch in fence', () => {
|
||||||
|
const text = 'Here you go\n```json\n{"prompt":"A red fox in snow","actions":["generate"]}\n```';
|
||||||
|
const { prose, patch } = extractPatch(text);
|
||||||
|
assert.ok(patch);
|
||||||
|
assert.equal(patch.prompt, 'A red fox in snow');
|
||||||
|
assert.ok(!prose.includes('```'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isCardObject vs isPatchObject', () => {
|
||||||
|
const card = { kind: 'lora', name: 'Foo', triggers: ['bar'] };
|
||||||
|
const gen = { prompt: 'test', actions: ['generate'] };
|
||||||
|
assert.equal(isCardObject(card), true);
|
||||||
|
assert.equal(isPatchObject(card), false);
|
||||||
|
assert.equal(isPatchObject(gen), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('normalizePatch aliases civitai_query', () => {
|
||||||
|
const p = normalizePatch({ civitai_query: 'anime style' });
|
||||||
|
assert.equal(p.search_query, 'anime style');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('scheduler-only patch is detected with full key list', () => {
|
||||||
|
setPatchKeys(['prompt', 'scheduler']);
|
||||||
|
assert.equal(isPatchObject({ scheduler: 'euler' }), true);
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user