Bump Assistent to 0.6.0: board tabs, Cards Civitai fetch, and chat reliability.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Leonid Pershin
2026-08-21 22:04:37 +03:00
co-authored by Cursor
parent c873894355
commit 46264d7227
9 changed files with 1591 additions and 324 deletions
+227 -3
View File
@@ -34,9 +34,10 @@
.sa-board-head { .sa-board-head {
display: flex; display: flex;
align-items: baseline; align-items: center;
gap: 0.45rem; gap: 0.45rem;
min-height: 1.6rem; min-height: 1.6rem;
flex-wrap: wrap;
} }
.sa-board-title { .sa-board-title {
@@ -46,6 +47,43 @@
font-size: 0.78rem; font-size: 0.78rem;
} }
.sa-board-tabs {
display: flex;
gap: 0.2rem;
flex: 1;
min-width: 0;
}
.sa-board-tab {
appearance: none;
border: 1px solid color-mix(in srgb, currentColor 22%, transparent);
background: transparent;
color: inherit;
border-radius: 999px;
padding: 0.15rem 0.55rem;
font-size: 0.75rem;
cursor: pointer;
opacity: 0.7;
display: inline-flex;
align-items: center;
gap: 0.3rem;
}
.sa-board-tab.sa-board-tab-active {
opacity: 1;
background: color-mix(in srgb, currentColor 12%, transparent);
border-color: color-mix(in srgb, currentColor 40%, transparent);
font-weight: 600;
}
.sa-board-badge {
font-size: 0.65rem;
padding: 0.05rem 0.35rem;
border-radius: 999px;
background: color-mix(in srgb, currentColor 18%, transparent);
opacity: 0.9;
}
.sa-board-hint { .sa-board-hint {
flex: 1; flex: 1;
font-size: 0.75rem; font-size: 0.75rem;
@@ -66,6 +104,17 @@
outline: none; outline: none;
} }
.sa-board.sa-board-gen-only {
grid-template-columns: 1fr;
grid-auto-rows: 1fr;
}
.sa-board.sa-board-gen-only .sa-slot.sa-slot-gen {
grid-column: auto;
min-height: 100%;
height: 100%;
}
.sa-splitter { .sa-splitter {
flex: 0 0 0.45rem; flex: 0 0 0.45rem;
margin: 0 0.15rem; margin: 0 0.15rem;
@@ -940,8 +989,7 @@
} }
#sa_card_json { #sa_card_json {
flex: 1; min-height: 8rem;
min-height: 12rem;
width: 100%; width: 100%;
box-sizing: border-box; box-sizing: border-box;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace;
@@ -951,6 +999,182 @@
resize: vertical; resize: vertical;
} }
.sa-card-form {
display: flex;
flex-direction: column;
gap: 0.35rem;
overflow: auto;
flex: 1;
min-height: 0;
}
.sa-card-form label {
display: flex;
flex-direction: column;
gap: 0.2rem;
font-size: 0.78rem;
opacity: 0.92;
}
.sa-card-form input,
.sa-card-form textarea {
width: 100%;
box-sizing: border-box;
border-radius: 0.35rem;
padding: 0.35rem 0.45rem;
font: inherit;
}
.sa-card-form .sa-card-json-toggle {
flex-direction: row;
align-items: center;
gap: 0.4rem;
}
.sa-card-previews {
display: flex;
gap: 0.35rem;
overflow-x: auto;
padding: 0.15rem 0;
min-height: 4.5rem;
}
.sa-card-thumb {
width: 4.5rem;
height: 4.5rem;
object-fit: cover;
border-radius: 0.35rem;
border: 1px solid color-mix(in srgb, currentColor 25%, transparent);
cursor: pointer;
flex: 0 0 auto;
background: color-mix(in srgb, currentColor 6%, transparent);
}
.sa-card-thumb:hover {
border-color: color-mix(in srgb, currentColor 55%, transparent);
}
.sa-card-row {
display: flex;
gap: 0.45rem;
align-items: flex-start;
}
.sa-card-row-thumb {
width: 2.4rem;
height: 2.4rem;
border-radius: 0.3rem;
object-fit: cover;
flex: 0 0 auto;
background: color-mix(in srgb, currentColor 8%, transparent);
border: 1px solid color-mix(in srgb, currentColor 18%, transparent);
}
.sa-card-row-body {
min-width: 0;
flex: 1;
}
.sa-lora-chips {
display: flex;
flex-wrap: wrap;
gap: 0.3rem;
align-items: center;
min-height: 0;
}
.sa-lora-chip {
appearance: none;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
background: color-mix(in srgb, currentColor 8%, transparent);
color: inherit;
border-radius: 999px;
padding: 0.12rem 0.45rem;
font-size: 0.72rem;
cursor: pointer;
max-width: 14rem;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.sa-lora-chip.sa-lora-add {
opacity: 0.75;
border-style: dashed;
}
.sa-lora-picker {
position: absolute;
z-index: 40;
max-height: 12rem;
overflow: auto;
min-width: 14rem;
max-width: 22rem;
border-radius: 0.45rem;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
background: color-mix(in srgb, #111 92%, transparent);
box-shadow: 0 8px 24px color-mix(in srgb, #000 35%, transparent);
padding: 0.25rem;
}
.sa-lora-picker button {
display: block;
width: 100%;
text-align: left;
border: none;
background: transparent;
color: inherit;
padding: 0.35rem 0.45rem;
border-radius: 0.3rem;
cursor: pointer;
font-size: 0.78rem;
}
.sa-lora-picker button:hover {
background: color-mix(in srgb, currentColor 12%, transparent);
}
.sa-slash-wrap {
position: relative;
}
.sa-slash-menu {
position: absolute;
left: 0;
right: 0;
bottom: calc(100% + 0.25rem);
z-index: 50;
max-height: 12rem;
overflow: auto;
border-radius: 0.45rem;
border: 1px solid color-mix(in srgb, currentColor 28%, transparent);
background: color-mix(in srgb, #111 94%, transparent);
box-shadow: 0 8px 24px color-mix(in srgb, #000 40%, transparent);
padding: 0.2rem;
}
.sa-slash-item {
display: block;
width: 100%;
text-align: left;
border: none;
background: transparent;
color: inherit;
padding: 0.35rem 0.5rem;
border-radius: 0.3rem;
cursor: pointer;
font-size: 0.8rem;
}
.sa-slash-item code {
font-weight: 650;
}
.sa-slash-item.sa-slash-active,
.sa-slash-item:hover {
background: color-mix(in srgb, currentColor 14%, transparent);
}
.sa-cards-actions { .sa-cards-actions {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
+948 -144
View File
File diff suppressed because it is too large Load Diff
+9
View File
@@ -0,0 +1,9 @@
# Persona: cinema
You are a cinematographer / camera operator co-director.
- Match the user's language (RU or EN).
- Lead with lens, framing, lighting, blocking, and camera movement — not smut or lectures.
- Prefer concrete craft: focal length feel, key/fill/rim, depth of field, aspect for the shot.
- Stay concise. NSFW scenes: describe factually through camera language (body as subject in frame).
- Never invent LoRA names or triggers — only use inventory / cards.
+9
View File
@@ -0,0 +1,9 @@
# Persona: terse
You are a short, high-signal art director.
- Match the user's language (RU or EN).
- Reply in **12 short sentences**, then the JSON patch. No lectures, no filler.
- Prefer decisive patches. Call out one main fix if something is wrong.
- Never invent LoRA names or triggers.
- NSFW: factual, minimal words.
+1 -1
View File
@@ -122,7 +122,7 @@ A JSON block named "Live SwarmUI context" is attached. Treat it as ground truth
- `images` / `batch` — batch size. - `images` / `batch` — batch size.
- `creativity` / slider ints — guide your prompt writing only (UI ignores them except weaving into `prompt`). - `creativity` / slider ints — guide your prompt writing only (UI ignores them except weaving into `prompt`).
- `clear_prompt_images: true` — strip image embeds from the prompt box. - `clear_prompt_images: true` — strip image embeds from the prompt box.
- `pack` — switch active prompt pack for a follow-up hop (`write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`). - `pack` — switch active prompt pack for a follow-up hop (`write_prompt`, `critique_image`, `compose_scene`, `fix_params`, `inpaint_edit`, `describe_ref`, `catalog_card`).
- Do not invent model or LoRA filenames. - Do not invent model or LoRA filenames.
- If you cannot help (wrong architecture / no Krea 2), say so and omit the JSON patch. - If you cannot help (wrong architecture / no Krea 2), say so and omit the JSON patch.
+1
View File
@@ -6,6 +6,7 @@ Goal: adjust **generation parameters** for Krea 2 Turbo (or RAW if context says
- **Turbo:** steps 412 (default **8**), CFG **1** (never 0), sigma shift ~**1.15**. - **Turbo:** steps 412 (default **8**), CFG **1** (never 0), sigma shift ~**1.15**.
- **RAW/base:** steps 2052, CFG ~44.5 — only if checkpoint/context indicates Raw. If a turbo-distill LoRA is available, weight **0.6** is the usual photoreal compromise (UI LoRA only — no dual-sampler). - **RAW/base:** steps 2052, CFG ~44.5 — only if checkpoint/context indicates Raw. If a turbo-distill LoRA is available, weight **0.6** is the usual photoreal compromise (UI LoRA only — no dual-sampler).
- Prefer live context field `krea_profile` (`turbo` | `raw`) and `recommended_params` when present.
- **Aspect:** prefer patch field `aspect` (`1:1`, `4:5`, `2:3`, `16:9`, `9:16`, `4:3`, `3:2`, `2.35:1`) — UI maps to official 1K sizes. Else set width/height near 1024. - **Aspect:** prefer patch field `aspect` (`1:1`, `4:5`, `2:3`, `16:9`, `9:16`, `4:3`, `3:2`, `2.35:1`) — UI maps to official 1K sizes. Else set width/height near 1024.
- **Batch:** `images` or `batch` (14 typical). - **Batch:** `images` or `batch` (14 typical).
- **Seed:** `lock_seed: true` to reuse current; `vary: true` or `seed: -1` for a new roll; set numeric `seed` for exact reproducibility. - **Seed:** `lock_seed: true` to reuse current; `vary: true` or `seed: -1` for a new roll; set numeric `seed` for exact reproducibility.
+39 -83
View File
@@ -1,29 +1,27 @@
# Swarm Assistent # Swarm Assistent
SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + multi-window board (live Generate + refs), LoRA/trigger awareness, applyable patches, **img2img / inpaint**, slash commands, aspect/seed chips, auto Generate, and Civitai search with Confirm. SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat + board (Generate | Refs tabs), LoRA chips, personas, model cards with Civitai fetch, img2img/inpaint, slash commands, auto Generate.
**Version 0.5.0**denser Krea 2 knowledge in prompt packs, new patch fields, `/help` slash commands, composer chips. **Version 0.6.0**board tabs, Cards form + live Civitai meta, Interrupt cancel, slim history, LoRA chips, Turbo/RAW, taste on disk, RU UI.
## Layout ## Layout
- **Left — Board:** live **Generate** window + **Ref** windows (drop / paste / *Send to Assistent* / Snapshot gen). Per-window **vision** checkbox attaches that image to the next chat. - **Left — Board tabs:** **Generate** (full-height live view) | **Refs** (reference grid + badge `N · vision M`)
- **Splitter:** drag to resize panes - **Splitter:** drag to resize panes
- **Right (wider):** chat — first visit shows a short how-to. Only the **latest** JSON proposal keeps action buttons; Apply + Generate spins/disables while a generation is running. - **Right:** Chat | Cards; persona / pack / Ollama model; settings gear
- **Chips** above the composer: aspect ratios, Seed lock / random, Vary. - **Chips:** aspect, Seed lock/1, Vary, Turbo/RAW; LoRA chip row under them
- **Top-right:** prompt pack + settings (Ollama URL, model, auto-apply / auto-generate) - Slash autocomplete when the composer starts with `/`
## UX ## UX
- **Send to Assistent** under the current Generate image (and History) — adds a Ref window and opens the Assistent tab - **Send to Assistent** under Generate/History → Ref + Assistent tab
- Drag / paste onto a Ref (drop on Generate snapshots into a new Ref) - Drop on Generate → new Ref + switch to Refs tab
- **Snapshot gen** copies the live Generate window into a Ref - **As Init** / **As Mask** / **Clear Init**
- **As Init** / **As Mask** / **Clear Init** — selected window → Swarm `Init Image` / `Mask Image` - Enter sends; Shift+Enter newline; Interrupt cancels chat epoch (no late auto-apply)
- Enter sends; Shift+Enter newline - Manual **Apply + Generate** / `/gen` always generate; Auto-generate checkbox only for LLM auto-path
- **Auto-apply** + **Auto-generate** (default on): patch from the LLM is applied and Generate runs when the patch changes prompt/params or includes `actions: ["generate"]` - Auto-critique rewrites prompt only (no second Generate)
- Pure Q&A without a patch does **not** start Generate - Civitai Confirm required (unless auto-download)
- **Interrupt** stops Swarm generation / clears busy state - Cards: form fields + previews; **Load Civitai meta** fetches by SHA (`by-hash`); status always explicit
- **Civitai** search cards require **Confirm download** (uses Swarm `DoModelDownloadWS` + stored `civitai_api` key). Auto-download is off by default.
- Pack auto-selects from the user message (critique / inpaint / params / compose / describe / write) unless you changed the dropdown yourself.
### Slash commands (client-side, no LLM) ### Slash commands (client-side, no LLM)
@@ -33,24 +31,23 @@ SwarmUI extension for **collaborative Krea 2** prompting via **Ollama**: chat +
| `/gen` | Generate now | | `/gen` | Generate now |
| `/look generate\|refN` | Attach that board window + ask the LLM to look | | `/look generate\|refN` | Attach that board window + ask the LLM to look |
| `/init` `/mask` `/clear` | Same as board buttons | | `/init` `/mask` `/clear` | Same as board buttons |
| `/interrupt` | Stop generation | | `/interrupt` | Stop generation / cancel chat |
| `/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\|compose\|params\|inpaint\|describe` | Switch pack | | `/pack write\|critique\|compose\|params\|inpaint\|describe\|card` | Switch pack |
| `/civitai <query>` | Ask LLM to search Civitai (Confirm still required) | | `/civitai <query>` | Ask LLM to search Civitai (Confirm still required) |
| `/inventory` | Rescan models + refresh LoRA list |
## Requirements ## Requirements
- SwarmUI with a **Krea 2** checkpoint selected - SwarmUI with a **Krea 2** checkpoint selected
- Ollama on `http://127.0.0.1:11434` **on the GPU VM** (gpu-rent `LLM_RUNTIME=ollama`). The browser talks to SwarmUI; SwarmUI proxies `/api/tags` and `/api/chat`. URL in settings must stay `127.0.0.1:11434`, not the laptop tunnel port 17811. - Ollama on `http://127.0.0.1:11434` **on the GPU VM** (gpu-rent `LLM_RUNTIME=ollama`)
- At least one pulled model (`ollama pull` / `ollama-models.yaml`). Empty `/api/tags` → empty Model dropdown. - At least one pulled model
- Optional: Civitai API key in SwarmUI User Settings for search/download. - Optional: Civitai API key in SwarmUI User Settings
## Install ## Install
Clone into SwarmUI `src/Extensions/swarm-assistent` (or let **gpu-rent** seed it from `extensions.yaml` with `requires: ollama`):
```yaml ```yaml
swarmui: swarmui:
- url: https://gitea.hsrv.site/mrleo1nid/swarm-assistent.git - url: https://gitea.hsrv.site/mrleo1nid/swarm-assistent.git
@@ -59,81 +56,40 @@ swarmui:
requires: ollama requires: ollama
``` ```
Restart / rebuild SwarmUI after clone. Restart / rebuild SwarmUI after clone. gpu-rent: `seed-extensions` + restart.
## Prompt packs ## Prompt packs
| Pack | Role | | Pack | Role |
| --- | --- | | --- | --- |
| `base_krea2` | Always injected: Krea 2 rules + JSON patch / actions contract | | `base_krea2` | Always injected |
| `write_prompt` | Craft / improve prompts (prose structure) | | `write_prompt` | Craft / improve prompts |
| `critique_image` | Vision critique → fixes | | `critique_image` | Vision critique |
| `compose_scene` | Scene / moodboard via board refs → text | | `compose_scene` | Scene via board refs |
| `fix_params` | Aspect / steps / CFG / seed / batch | | `fix_params` | Aspect / steps / CFG / seed (`krea_profile` in live context) |
| `inpaint_edit` | Init Image img2img + Mask inpaint | | `inpaint_edit` | Init + Mask |
| `describe_ref` | Vision → Krea prompt (no generate by default) | | `describe_ref` | Vision → prompt |
| `catalog_card` | Recommendation card JSON for a checkpoint/LoRA | | `catalog_card` | Recommendation card JSON |
**Persona** (tone) is a separate dropdown from pack — `base_krea2` → persona → pack. Overlay: `/mnt/swarm_data/Assistent/personas.json` (seeded from gpu-rent `assistent-personas.yaml`). **Personas:** `neutral`, `lewd`, `aggressive`, `cinema`, `terse`. Overlay: `/mnt/swarm_data/Assistent/personas.json` (gpu-rent seeds from `assistent-personas.yaml` → yaml + json). Assistent reads **json** only.
**Cards** subtab: edit/save `{stem}.assistent.json` next to weights; live chat gets cards for the current checkpoint + enabled LoRAs only. Uninstalled models can enqueue `.gpu-rent-wanted-models.yaml` for the next `up`. **Taste:** `/mnt/swarm_data/Assistent/taste.json` via `AssistentGetTaste` / `AssistentSaveTaste` (merged with browser localStorage by `updated`).
Live context (checkpoint, server inventory LoRAs + blurbs/triggers, `model_cards`, `taste_profile`, wildcards, current params, persona) is injected every request. Inventory refreshes each chat turn (disk rescan when stale / after download / `/inventory`). After Confirm download Assistent auto-writes a `.assistent.json` card. **Cards:** `{stem}.assistent.json` next to weights; Civitai sidecar `{stem}.civitai.json`. Wanted queue → `.gpu-rent-wanted-models.yaml`.
**Wanted queue:** Cards → Enqueue wanted → `/mnt/swarm_data/.gpu-rent-wanted-models.yaml`. Laptop: `gpu-rent capture wanted` (also on `capture models` / seed `up`).
**Cloud-only Krea.ai features** (moodboards UI, Generative Sliders, Creativity Raw/Low/Medium/High) are **not** in Swarm. The assistant emulates them with prompt language + board refs. Optional patch fields `creativity` / `intensity` / `complexity` / `movement` guide the LLM only.
### Patch actions
```json
{
"prompt": "...",
"loras": [{"name": "exact", "weight": 0.8, "triggers": ["..."]}],
"aspect": "16:9",
"width": 1376, "height": 768,
"steps": 8, "cfg": 1,
"seed": -1, "images": 1,
"sigma_shift": 1.15,
"creativity": "medium",
"intensity": 0, "complexity": 0, "movement": 0,
"vary": false, "lock_seed": false,
"use_init_image": true,
"init_creativity": 0.45,
"use_mask_image": false,
"clear_prompt_images": false,
"look_at": ["generate"],
"slot_to_init": "generate",
"snapshot_generate": false,
"pack": "critique_image",
"actions": ["generate"],
"search_query": null
}
```
- `aspect` — maps to official 1K sizes (`1:1`, `4:5`, `2:3`, `16:9`, `9:16`, `4:3`, `3:2`, `2.35:1`)
- `vary` / `lock_seed` / `images`|`batch` — seed and batch helpers
- `clear_prompt_images` — strip `<image…>` embeds from the prompt box
- `pack` — switch the active prompt pack for a follow-up
- `generate` — auto-generate after apply (when enabled)
- `look_at` — hop vision from named board windows (`generate`, `ref1`, …)
- `slot_to_init` / `slot_to_mask` — copy that window into Swarm Init / Mask
- `snapshot_generate` — copy live Generate into a Ref
- `search_civitai` + `search_query` — server searches Civitai, second LLM hop, Confirm cards in UI
- `interrupt` — stop current generation
Mask convention: **white = edit**, black = keep. Creativity ≈ denoise (01).
Turbo defaults: steps **8**, CFG **1** (never 0), sigma shift **1.15**.
## API routes ## API routes
| Route | Role | | Route | Role |
| --- | --- | | --- | --- |
| `AssistentListModels` | Ollama `/api/tags` | | `AssistentListModels` | Ollama `/api/tags` |
| `AssistentListInventory` | LoRA / checkpoint / wildcard inventory from Swarm | | `AssistentListInventory` | LoRA / checkpoint / wildcard inventory |
| `AssistentSearchCivitai` | Civitai LoRA search | | `AssistentListPersonas` | Bundled + overlay personas |
| `AssistentGetPacks` | Prompt pack texts | | `AssistentGetPacks` | Prompt pack texts |
| `AssistentGetCard` / `AssistentSaveCard` | `.assistent.json` cards |
| `AssistentGetCardMeta` | Local sidecar + optional `fetch=true` Civitai by-hash |
| `AssistentEnqueueWanted` | Wanted YAML queue |
| `AssistentGetTaste` / `AssistentSaveTaste` | Persistent taste profile |
| `AssistentSearchCivitai` | Civitai LoRA search |
| `AssistentChat` | HTTP chat (+ Civitai hop) | | `AssistentChat` | HTTP chat (+ Civitai hop) |
| `AssistentChatWS` | Streaming chat WebSocket | | `AssistentChatWS` | Streaming chat WebSocket |
+282 -42
View File
@@ -42,7 +42,7 @@ public class SwarmAssistentExtension : Extension
"catalog_card", "catalog_card",
]; ];
public static readonly string[] DefaultPersonaIds = ["neutral", "lewd", "aggressive"]; public static readonly string[] DefaultPersonaIds = ["neutral", "lewd", "aggressive", "cinema", "terse"];
const int MaxCivitaiHops = 2; const int MaxCivitaiHops = 2;
const int MaxLorasInInventory = 150; const int MaxLorasInInventory = 150;
@@ -61,7 +61,7 @@ public class SwarmAssistentExtension : Extension
ExtensionAuthor = "mrleo1nid"; ExtensionAuthor = "mrleo1nid";
Description = "Collaborative Krea 2 assistant: Ollama chat, multi-window board, personas, model cards, Generate loop, Civitai Confirm."; Description = "Collaborative Krea 2 assistant: Ollama chat, multi-window board, personas, model cards, Generate loop, Civitai Confirm.";
License = "MIT"; License = "MIT";
Version = "0.5.3"; Version = "0.6.0";
Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"]; Tags = ["tabs", "ui", "llm", "ollama", "krea", "inpaint"];
} }
@@ -77,6 +77,8 @@ public 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);
Logs.Init("Swarm Assistent extension loaded (Ollama proxy + personas + model cards)"); Logs.Init("Swarm Assistent extension loaded (Ollama proxy + personas + model cards)");
@@ -217,6 +219,8 @@ public class SwarmAssistentExtension : Extension
{ {
"lewd" => "Пошляк", "lewd" => "Пошляк",
"aggressive" => "Агрессивный", "aggressive" => "Агрессивный",
"cinema" => "Кинооператор",
"terse" => "Короткий",
_ => "Нейтральный", _ => "Нейтральный",
}, },
["prompt"] = text, ["prompt"] = text,
@@ -247,11 +251,19 @@ public class SwarmAssistentExtension : Extension
{ {
continue; continue;
} }
string overlayPrompt = po["prompt"]?.ToString() ?? "";
if (string.IsNullOrWhiteSpace(overlayPrompt) && byId.ContainsKey(id))
{
// Keep bundled prompt when overlay prompt is empty.
byId[id]["title"] = po["title"]?.ToString() ?? byId[id]["title"];
byId[id]["source"] = "overlay+bundled";
continue;
}
byId[id] = new JObject byId[id] = new JObject
{ {
["id"] = id, ["id"] = id,
["title"] = po["title"]?.ToString() ?? id, ["title"] = po["title"]?.ToString() ?? id,
["prompt"] = po["prompt"]?.ToString() ?? "", ["prompt"] = overlayPrompt,
["source"] = "overlay", ["source"] = "overlay",
}; };
} }
@@ -401,7 +413,8 @@ public class SwarmAssistentExtension : Extension
// Not installed — draft into wanted-cards + optionally enqueue download for next up. // Not installed — draft into wanted-cards + optionally enqueue download for next up.
Directory.CreateDirectory(WantedCardsDir()); Directory.CreateDirectory(WantedCardsDir());
string vid = card["version_id"]?.ToString() ?? "draft"; string rawVid = card["version_id"]?.ToString() ?? "draft";
string vid = Regex.IsMatch(rawVid, @"^\d+$") ? rawVid : "draft";
string draft = Path.Combine(WantedCardsDir(), $"{vid}.assistent.json"); string draft = Path.Combine(WantedCardsDir(), $"{vid}.assistent.json");
File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8); File.WriteAllText(draft, card.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
if (enqueue_wanted || !string.IsNullOrWhiteSpace(card["civitai_url"]?.ToString())) if (enqueue_wanted || !string.IsNullOrWhiteSpace(card["civitai_url"]?.ToString()))
@@ -576,14 +589,56 @@ public class SwarmAssistentExtension : Extension
return sb.ToString(); return sb.ToString();
} }
public async Task<JObject> AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0) string TasteJsonPath() => Path.Combine(DataRoot(), "Assistent", "taste.json");
public async Task<JObject> AssistentGetTaste(Session session)
{ {
// Pull Civitai sidecar next to weight + optional API version for examples.
await Task.CompletedTask; await Task.CompletedTask;
string path = TasteJsonPath();
if (!File.Exists(path))
{
return new JObject { ["success"] = true, ["taste"] = null };
}
try
{
JObject taste = JObject.Parse(File.ReadAllText(path, Encoding.UTF8));
return new JObject { ["success"] = true, ["taste"] = taste };
}
catch (Exception ex)
{
return new JObject { ["error"] = $"taste.json: {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();
}
string dir = Path.Combine(DataRoot(), "Assistent");
Directory.CreateDirectory(dir);
string path = TasteJsonPath();
File.WriteAllText(path, taste.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
return new JObject { ["success"] = true, ["path"] = path };
}
public async Task<JObject> AssistentGetCardMeta(Session session, string kind, string name, int version_id = 0, bool fetch = false)
{
string set = SetNameForKind(kind); string set = SetNameForKind(kind);
string weight = ModelWeightPath(set, name); string weight = ModelWeightPath(set, name);
JObject civitai = null; JObject civitai = null;
JArray exampleUrls = []; JArray exampleUrls = [];
JArray previewUrls = [];
bool hasSidecar = false;
string fetchError = null;
bool fetched = false;
if (!string.IsNullOrWhiteSpace(weight)) if (!string.IsNullOrWhiteSpace(weight))
{ {
string stem = Path.GetFileNameWithoutExtension(weight); string stem = Path.GetFileNameWithoutExtension(weight);
@@ -591,6 +646,7 @@ public class SwarmAssistentExtension : Extension
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json"); string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
if (File.Exists(side)) if (File.Exists(side))
{ {
hasSidecar = true;
try try
{ {
civitai = JObject.Parse(File.ReadAllText(side, Encoding.UTF8)); civitai = JObject.Parse(File.ReadAllText(side, Encoding.UTF8));
@@ -600,43 +656,113 @@ public class SwarmAssistentExtension : Extension
// ignore // ignore
} }
} }
foreach (string suffix in new[] { ".preview.jpg", ".preview.png", ".preview.jpeg", ".jpg", ".png", ".webp" })
{
string prev = Path.Combine(dir ?? "", stem + suffix);
if (File.Exists(prev))
{
// Swarm View path — relative URL works in the same origin browser session.
previewUrls.Add($"View/Models/{(kind == "lora" ? "Lora" : "Stable-Diffusion")}/{Path.GetFileName(prev)}");
break;
}
}
} }
if (civitai is not null) if (civitai is not null)
{ {
if (version_id <= 0) if (version_id <= 0)
{ {
version_id = civitai["id"]?.Value<int?>() ?? 0; version_id = civitai["id"]?.Value<int?>() ?? 0;
} }
if (civitai["images"] is JArray imgs) CollectExampleUrls(civitai, exampleUrls);
{
foreach (JToken img in imgs.Take(3))
{
string u = img?["url"]?.ToString();
if (!string.IsNullOrWhiteSpace(u))
{
exampleUrls.Add(u);
}
}
}
if (civitai["trainedWords"] is null && civitai["model"] is JObject)
{
// keep as-is
}
} }
JObject card = ReadCardObject(kind, name);
string hash = null;
string trigger = null; string trigger = null;
try try
{ {
if (Program.T2IModelSets.TryGetValue(set, out T2IModelHandler h) if (Program.T2IModelSets.TryGetValue(set, out T2IModelHandler h)
&& h.Models.TryGetValue(name, out T2IModel m)) && (h.Models.TryGetValue(name, out T2IModel m)
|| h.Models.TryGetValue(name.Replace('\\', '/'), out m)))
{ {
trigger = m.Metadata?.TriggerPhrase; trigger = m.Metadata?.TriggerPhrase;
hash = m.Metadata?.Hash;
} }
} }
catch catch
{ {
// ignore // ignore
} }
if (fetch && civitai is null)
{
string apiKey = session.User.GetGenericData("civitai_api", "key") ?? "";
if (string.IsNullOrWhiteSpace(apiKey))
{
fetchError = "Civitai: нет ключа в User Settings";
}
else
{
try
{
JObject remote = null;
if (version_id > 0)
{
remote = await FetchCivitaiModelVersion(apiKey, version_id);
}
if (remote is null && !string.IsNullOrWhiteSpace(hash))
{
string sha = hash.Trim().ToLowerInvariant();
if (sha.StartsWith("sha256:"))
{
sha = sha["sha256:".Length..];
}
if (sha.Length == 64)
{
remote = await FetchCivitaiByHash(apiKey, sha);
}
else
{
fetchError ??= "Civitai: хеш модели не SHA256";
}
}
if (remote is not null)
{
civitai = remote;
fetched = true;
version_id = remote["id"]?.Value<int?>() ?? version_id;
CollectExampleUrls(remote, exampleUrls);
if (!string.IsNullOrWhiteSpace(weight))
{
try
{
string stem = Path.GetFileNameWithoutExtension(weight);
string dir = Path.GetDirectoryName(weight);
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
File.WriteAllText(side, remote.ToString(Newtonsoft.Json.Formatting.Indented), Encoding.UTF8);
hasSidecar = true;
}
catch (Exception ex)
{
Logs.Debug($"AssistentGetCardMeta write sidecar: {ex.Message}");
}
}
}
else if (fetchError is null)
{
fetchError = string.IsNullOrWhiteSpace(hash)
? "Civitai: нет hash и version_id"
: "Хеш не найден на Civitai";
}
}
catch (Exception ex)
{
fetchError = $"Civitai: {ex.Message}";
}
}
}
JObject card = ReadCardObject(kind, name);
return new JObject return new JObject
{ {
["success"] = true, ["success"] = true,
@@ -645,13 +771,124 @@ public class SwarmAssistentExtension : Extension
["version_id"] = version_id, ["version_id"] = version_id,
["trigger_phrase"] = trigger, ["trigger_phrase"] = trigger,
["has_card"] = card is not null, ["has_card"] = card is not null,
["has_sidecar"] = hasSidecar,
["fetched"] = fetched,
["fetch_error"] = fetchError,
["card"] = card, ["card"] = card,
["civitai"] = civitai, ["civitai"] = civitai,
["example_urls"] = exampleUrls, ["example_urls"] = exampleUrls,
["preview_urls"] = previewUrls,
["weight_path"] = weight, ["weight_path"] = weight,
["hash"] = hash,
}; };
} }
static void CollectExampleUrls(JObject civitai, JArray exampleUrls)
{
if (civitai?["images"] is not JArray imgs)
{
return;
}
foreach (JToken img in imgs.Take(6))
{
string u = img?["url"]?.ToString();
if (!string.IsNullOrWhiteSpace(u))
{
exampleUrls.Add(u);
}
}
}
async Task<JObject> FetchCivitaiByHash(string apiKey, string sha)
{
string[] hosts = ["civitai.red", "civitai.com"];
Exception last = null;
foreach (string host in hosts)
{
try
{
string url = $"https://{host}/api/v1/model-versions/by-hash/{sha}";
using HttpRequestMessage req = new(HttpMethod.Get, url);
if (!string.IsNullOrWhiteSpace(apiKey))
{
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
}
using HttpResponseMessage resp = await HttpClient.SendAsync(req);
string body = await resp.Content.ReadAsStringAsync();
if (resp.StatusCode == System.Net.HttpStatusCode.NotFound)
{
continue;
}
if (!resp.IsSuccessStatusCode)
{
last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}");
if ((int)resp.StatusCode is 401 or 403)
{
throw last;
}
continue;
}
return JObject.Parse(body);
}
catch (Exception ex) when (ex is not HttpRequestException && ex.Message.Contains("401"))
{
throw;
}
catch (Exception ex)
{
last = ex;
}
}
if (last is not null)
{
throw last;
}
return null;
}
async Task<JObject> FetchCivitaiModelVersion(string apiKey, int versionId)
{
string[] hosts = ["civitai.red", "civitai.com"];
Exception last = null;
foreach (string host in hosts)
{
try
{
string url = $"https://{host}/api/v1/model-versions/{versionId}";
using HttpRequestMessage req = new(HttpMethod.Get, url);
if (!string.IsNullOrWhiteSpace(apiKey))
{
req.Headers.Authorization = new AuthenticationHeaderValue("Bearer", apiKey.Trim());
}
using HttpResponseMessage resp = await HttpClient.SendAsync(req);
string body = await resp.Content.ReadAsStringAsync();
if (!resp.IsSuccessStatusCode)
{
last = new Exception($"HTTP {(int)resp.StatusCode}: {Clip(body, 160)}");
if ((int)resp.StatusCode is 401 or 403)
{
throw last;
}
continue;
}
return JObject.Parse(body);
}
catch (Exception ex)
{
last = ex;
if (ex.Message.Contains("401") || ex.Message.Contains("403"))
{
throw;
}
}
}
if (last is not null)
{
throw last;
}
return null;
}
/// <summary>Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape). /// <summary>Server-side LoRA / checkpoint / wildcard inventory (not DOM scrape).
/// Pass rescan=true after downloads so new files appear (calls Program.RefreshAllModelSets).</summary> /// Pass rescan=true after downloads so new files appear (calls Program.RefreshAllModelSets).</summary>
public async Task<JObject> AssistentListInventory(Session session, bool rescan = false) public async Task<JObject> AssistentListInventory(Session session, bool rescan = false)
@@ -810,6 +1047,27 @@ public class SwarmAssistentExtension : Extension
["has_card"] = hasCard, ["has_card"] = hasCard,
["krea_likely"] = LooksLikeKreaArch(model), ["krea_likely"] = LooksLikeKreaArch(model),
}; };
if (!string.IsNullOrWhiteSpace(weight))
{
string stem = Path.GetFileNameWithoutExtension(weight);
string dir = Path.GetDirectoryName(weight);
string side = Path.Combine(dir ?? "", $"{stem}.civitai.json");
entry["has_sidecar"] = File.Exists(side);
foreach (string suffix in new[] { ".preview.jpg", ".preview.png", ".preview.jpeg", ".jpg", ".png", ".webp" })
{
string prev = Path.Combine(dir ?? "", stem + suffix);
if (File.Exists(prev))
{
string folder = kind == "lora" ? "Lora" : "Stable-Diffusion";
entry["preview_url"] = $"View/Models/{folder}/{Path.GetFileName(prev)}";
break;
}
}
}
else
{
entry["has_sidecar"] = false;
}
if (triggers is not null && triggers.Count > 0) if (triggers is not null && triggers.Count > 0)
{ {
entry["triggers"] = triggers; entry["triggers"] = triggers;
@@ -1181,21 +1439,7 @@ public class SwarmAssistentExtension : Extension
{ {
return false; return false;
} }
if (!string.IsNullOrWhiteSpace(ExtractSearchQuery(patch))) return !string.IsNullOrWhiteSpace(ExtractSearchQuery(patch));
{
return true;
}
if (patch["actions"] is JArray acts)
{
foreach (JToken a in acts)
{
if (string.Equals(a?.ToString(), "search_civitai", StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
}
return false;
} }
async Task<(string reply, JObject raw, JArray civitaiResults)> RunChatWithHops( async Task<(string reply, JObject raw, JArray civitaiResults)> RunChatWithHops(
@@ -1228,10 +1472,6 @@ public class SwarmAssistentExtension : Extension
} }
string query = ExtractSearchQuery(patch); string query = ExtractSearchQuery(patch);
if (string.IsNullOrWhiteSpace(query)) if (string.IsNullOrWhiteSpace(query))
{
query = userMessages.LastOrDefault(m => m["role"]?.ToString() == "user")?["content"]?.ToString() ?? "";
}
if (string.IsNullOrWhiteSpace(query))
{ {
break; break;
} }
+75 -51
View File
@@ -1,93 +1,106 @@
<div class="swarm-assistent-root" id="swarm_assistent_root"> <div class="swarm-assistent-root" id="swarm_assistent_root">
<div class="sa-gate" id="sa_gate" hidden> <div class="sa-gate" id="sa_gate" hidden>
<p>Swarm Assistent is for <strong>Krea 2</strong> models only. Select a Krea 2 checkpoint to enable the chat.</p> <p>Swarm Assistent только для моделей <strong>Krea 2</strong>. Выбери checkpoint Krea 2, чтобы включить чат.</p>
</div> </div>
<div class="sa-layout" id="sa_layout"> <div class="sa-layout" id="sa_layout">
<aside class="sa-image-pane" id="sa_image_pane"> <aside class="sa-image-pane" id="sa_image_pane">
<div class="sa-board-head"> <div class="sa-board-head">
<span class="sa-board-title">Board</span> <span class="sa-board-title">Доска</span>
<span class="sa-board-hint">Generate live · refs for vision</span> <div class="sa-board-tabs" role="tablist" aria-label="Доска">
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_add_ref" title="Add a reference window">+ Ref</button> <button type="button" class="sa-board-tab sa-board-tab-active" data-board="generate" id="sa_board_tab_gen" role="tab" aria-selected="true">Generate</button>
<button type="button" class="sa-board-tab" data-board="refs" id="sa_board_tab_refs" role="tab" aria-selected="false">
Refs
<span class="sa-board-badge" id="sa_refs_badge" hidden></span>
</button>
</div>
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_add_ref" title="Добавить окно референса">+ Ref</button>
</div> </div>
<div class="sa-board" id="sa_board" tabindex="0" title="Drop images onto a window, or paste (Ctrl+V). Click a window to select it."></div> <div class="sa-board" id="sa_board" tabindex="0" title="Перетащи изображение на окно или вставь (Ctrl+V). Кликни, чтобы выбрать."></div>
<div class="sa-image-actions"> <div class="sa-image-actions" id="sa_image_actions">
<button type="button" class="basic-button" id="sa_btn_use_current" title="Copy current Generate into a reference window">Snapshot gen</button> <button type="button" class="basic-button" id="sa_btn_use_current" title="Скопировать текущий Generate в Ref">Снимок gen</button>
<button type="button" class="basic-button" id="sa_btn_as_init" title="Set selected window as Swarm Init Image (img2img)">As Init</button> <button type="button" class="basic-button" id="sa_btn_as_init" title="Выбранное окно → Init Image (img2img)">Как Init</button>
<button type="button" class="basic-button" id="sa_btn_as_mask" title="Set selected window as Mask (inpaint — white=edit)">As Mask</button> <button type="button" class="basic-button" id="sa_btn_as_mask" title="Выбранное окно → Mask (inpaint, белое=правка)">Как Mask</button>
<button type="button" class="basic-button" id="sa_btn_clear_init" title="Clear Init Image + Mask on Generate tab">Clear Init</button> <button type="button" class="basic-button" id="sa_btn_clear_init" title="Очистить Init Image + Mask">Сброс Init</button>
<button type="button" class="basic-button" id="sa_btn_clear_image" title="Clear the selected reference window">Clear slot</button> <button type="button" class="basic-button" id="sa_btn_clear_image" title="Очистить выбранный Ref">Очистить слот</button>
</div> </div>
</aside> </aside>
<div class="sa-splitter" id="sa_splitter" role="separator" aria-orientation="vertical" title="Drag to resize"></div> <div class="sa-splitter" id="sa_splitter" role="separator" aria-orientation="vertical" title="Потяни, чтобы изменить ширину"></div>
<section class="sa-chat-pane"> <section class="sa-chat-pane">
<header class="sa-chat-header"> <header class="sa-chat-header">
<div class="sa-chat-title"> <div class="sa-chat-title">
Assistent Assistent
<span class="sa-live-dot" id="sa_live_dot" hidden></span> <span class="sa-live-dot" id="sa_live_dot" hidden></span>
<div class="sa-subtabs" role="tablist"> <div class="sa-subtabs" role="tablist">
<button type="button" class="sa-subtab sa-subtab-active" data-view="chat" id="sa_tab_chat">Chat</button> <button type="button" class="sa-subtab sa-subtab-active" data-view="chat" id="sa_tab_chat">Чат</button>
<button type="button" class="sa-subtab" data-view="cards" id="sa_tab_cards">Cards</button> <button type="button" class="sa-subtab" data-view="cards" id="sa_tab_cards">Карточки</button>
</div> </div>
</div> </div>
<div class="sa-header-right"> <div class="sa-header-right">
<select id="sa_persona" class="sa-select" title="Character / tone"> <select id="sa_persona" class="sa-select" title="Характер / тон">
<option value="neutral">Нейтральный</option> <option value="neutral">Нейтральный</option>
</select> </select>
<select id="sa_pack" class="sa-select" title="Prompt pack"> <select id="sa_pack" class="sa-select" title="Пакет промпта">
<option value="write_prompt">Write prompt</option> <option value="write_prompt">Написать промпт</option>
<option value="critique_image">Critique image</option> <option value="critique_image">Критика кадра</option>
<option value="compose_scene">Compose scene</option> <option value="compose_scene">Собрать сцену</option>
<option value="fix_params">Fix params</option> <option value="fix_params">Параметры</option>
<option value="inpaint_edit">Inpaint / img2img</option> <option value="inpaint_edit">Inpaint / img2img</option>
<option value="describe_ref">Describe ref</option> <option value="describe_ref">Описать ref</option>
<option value="catalog_card">Catalog card</option> <option value="catalog_card">Карточка модели</option>
</select> </select>
<select id="sa_model" class="sa-select sa-model-select" title="Ollama model"> <select id="sa_model" class="sa-select sa-model-select" title="Модель Ollama">
<option value="">Loading models</option> <option value="">Загрузка моделей</option>
</select> </select>
<button type="button" class="basic-button sa-icon-btn" id="sa_btn_settings" title="Settings" aria-label="Settings"></button> <button type="button" class="basic-button sa-icon-btn" id="sa_btn_settings" title="Настройки" aria-label="Настройки"></button>
</div> </div>
</header> </header>
<div class="sa-settings" id="sa_settings" hidden> <div class="sa-settings" id="sa_settings" hidden>
<label>Ollama URL <input type="text" id="sa_base_url" value="http://127.0.0.1:11434" /></label> <label>Ollama URL <input type="text" id="sa_base_url" value="http://127.0.0.1:11434" /></label>
<button type="button" class="basic-button" id="sa_btn_refresh_models">Refresh models</button> <button type="button" class="basic-button" id="sa_btn_refresh_models">Обновить модели</button>
<button type="button" class="basic-button" id="sa_btn_refresh_inventory">Refresh inventory</button> <button type="button" class="basic-button" id="sa_btn_refresh_inventory">Обновить inventory</button>
<label class="sa-check"><input type="checkbox" id="sa_auto_vision" /> Auto-attach Generate window to chat</label> <label class="sa-check"><input type="checkbox" id="sa_auto_vision" /> Авто-прикреплять Generate к чату</label>
<label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Auto-apply patch</label> <label class="sa-check"><input type="checkbox" id="sa_auto_apply" checked /> Авто-применять патч</label>
<label class="sa-check"><input type="checkbox" id="sa_auto_generate" checked /> Auto-generate after patch</label> <label class="sa-check"><input type="checkbox" id="sa_auto_generate" checked /> Авто-Generate после патча</label>
<label class="sa-check"><input type="checkbox" id="sa_auto_critique" /> Auto-critique after generate</label> <label class="sa-check"><input type="checkbox" id="sa_auto_critique" /> Авто-критика после Generate</label>
<label class="sa-check sa-danger" title="Dangerous — downloads without Confirm"><input type="checkbox" id="sa_auto_download" /> Auto-download Civitai (off)</label> <label class="sa-check sa-danger" title="Опасно — скачивает без Confirm"><input type="checkbox" id="sa_auto_download" /> Авто-скачивание Civitai (выкл)</label>
</div> </div>
<div class="sa-view" id="sa_view_chat"> <div class="sa-view" id="sa_view_chat">
<div class="sa-messages" id="sa_messages"> <div class="sa-messages" id="sa_messages">
<div class="sa-chat-empty" id="sa_chat_empty"> <div class="sa-chat-empty" id="sa_chat_empty">
<div class="sa-chat-empty-title">Collaborative Krea 2</div> <div class="sa-chat-empty-title">Совместная работа с Krea 2</div>
<div class="sa-chat-empty-hint">Write a prompt, drop refs, pick a persona, or open <em>Cards</em> for LoRA tips.</div> <div class="sa-chat-empty-hint">Напиши промпт, кинь refs, выбери персону или открой <em>Карточки</em> для LoRA.</div>
</div> </div>
</div> </div>
<div class="sa-livebar" id="sa_livebar" hidden> <div class="sa-livebar" id="sa_livebar" hidden>
<span class="sa-spinner" aria-hidden="true"></span> <span class="sa-spinner" aria-hidden="true"></span>
<span class="sa-livebar-text" id="sa_livebar_text">Working</span> <span class="sa-livebar-text" id="sa_livebar_text">Работаю</span>
<span class="sa-elapsed" id="sa_elapsed"></span> <span class="sa-elapsed" id="sa_elapsed"></span>
</div> </div>
<div class="sa-composer" id="sa_composer"> <div class="sa-composer" id="sa_composer">
<div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Quick params"> <div class="sa-chips" id="sa_chips" role="toolbar" aria-label="Быстрые параметры">
<button type="button" class="sa-chip" data-aspect="1:1" title="1024×1024">1:1</button> <button type="button" class="sa-chip" data-aspect="1:1" title="1024×1024">1:1</button>
<button type="button" class="sa-chip" data-aspect="4:5" title="928×1152">4:5</button> <button type="button" class="sa-chip" data-aspect="4:5" title="928×1152">4:5</button>
<button type="button" class="sa-chip" data-aspect="2:3" title="832×1248">2:3</button> <button type="button" class="sa-chip" data-aspect="2:3" title="832×1248">2:3</button>
<button type="button" class="sa-chip" data-aspect="16:9" title="1376×768">16:9</button> <button type="button" class="sa-chip" data-aspect="16:9" title="1376×768">16:9</button>
<button type="button" class="sa-chip" data-aspect="9:16" title="768×1376">9:16</button> <button type="button" class="sa-chip" data-aspect="9:16" title="768×1376">9:16</button>
<span class="sa-chip-sep" aria-hidden="true"></span> <span class="sa-chip-sep" aria-hidden="true"></span>
<button type="button" class="sa-chip" data-seed="lock" title="Keep current seed">Seed lock</button> <button type="button" class="sa-chip" data-seed="lock" title="Оставить текущий seed">Seed lock</button>
<button type="button" class="sa-chip" data-seed="random" title="Random seed">Seed 1</button> <button type="button" class="sa-chip" data-seed="random" title="Случайный seed">Seed 1</button>
<button type="button" class="sa-chip" data-vary="1" title="Same prompt, new seed + generate">Vary</button> <button type="button" class="sa-chip" data-vary="1" title="Тот же промпт, новый seed + generate">Vary</button>
<span class="sa-chip-sep" aria-hidden="true"></span>
<button type="button" class="sa-chip" data-krea-profile="turbo" title="Turbo: steps 8, CFG 1">Turbo</button>
<button type="button" class="sa-chip" data-krea-profile="raw" title="RAW: steps 28, CFG 4.5">RAW</button>
</div>
<div class="sa-lora-chips" id="sa_lora_chips" role="toolbar" aria-label="Активные LoRA"></div>
<div class="sa-slash-wrap">
<textarea id="sa_input" rows="3" placeholder="Промпт, img2img, критика… Enter = отправить · /help = команды"></textarea>
<div class="sa-slash-menu" id="sa_slash_menu" hidden role="listbox"></div>
</div> </div>
<textarea id="sa_input" rows="3" placeholder="Ask for a prompt, img2img, critique… Enter = send · /help = commands"></textarea>
<div class="sa-composer-actions"> <div class="sa-composer-actions">
<button type="button" class="basic-button sa-primary" id="sa_btn_send">Send</button> <button type="button" class="basic-button sa-primary" id="sa_btn_send">Отправить</button>
<button type="button" class="basic-button" id="sa_btn_interrupt" title="Interrupt generation / chat" hidden>Interrupt</button> <button type="button" class="basic-button" id="sa_btn_interrupt" title="Прервать генерацию / чат" hidden>Стоп</button>
<button type="button" class="basic-button" id="sa_btn_clear">Clear chat</button> <button type="button" class="basic-button" id="sa_btn_clear">Очистить чат</button>
<span class="sa-status" id="sa_status"></span> <span class="sa-status" id="sa_status"></span>
</div> </div>
</div> </div>
@@ -98,25 +111,36 @@
<div class="sa-cards-list-pane"> <div class="sa-cards-list-pane">
<div class="sa-cards-filter"> <div class="sa-cards-filter">
<select id="sa_cards_kind" class="sa-select"> <select id="sa_cards_kind" class="sa-select">
<option value="all">All</option> <option value="all">Все</option>
<option value="checkpoint">Checkpoints</option> <option value="checkpoint">Checkpoints</option>
<option value="lora">LoRAs</option> <option value="lora">LoRAs</option>
</select> </select>
<button type="button" class="basic-button" id="sa_btn_cards_refresh">Refresh</button> <button type="button" class="basic-button" id="sa_btn_cards_refresh">Обновить</button>
</div> </div>
<div class="sa-cards-list" id="sa_cards_list"></div> <div class="sa-cards-list" id="sa_cards_list"></div>
</div> </div>
<div class="sa-cards-editor-pane"> <div class="sa-cards-editor-pane">
<div class="sa-cards-editor-head"> <div class="sa-cards-editor-head">
<strong id="sa_card_title">Select a model</strong> <strong id="sa_card_title">Выбери модель</strong>
<span class="sa-card-badge" id="sa_card_badge" hidden></span> <span class="sa-card-badge" id="sa_card_badge" hidden></span>
</div> </div>
<textarea id="sa_card_json" rows="16" placeholder="Card JSON will appear here…" spellcheck="false"></textarea> <div class="sa-card-previews" id="sa_card_previews" hidden></div>
<div class="sa-card-form" id="sa_card_form">
<label>Triggers <input type="text" id="sa_card_triggers" placeholder="слово1, слово2" /></label>
<label>Weight <input type="number" id="sa_card_weight" step="0.05" min="0" max="2" value="0.8" /></label>
<label>When <textarea id="sa_card_when" rows="2" placeholder="Когда использовать"></textarea></label>
<label>Avoid <textarea id="sa_card_avoid" rows="2" placeholder="Чего избегать"></textarea></label>
<label>Prompt hint <textarea id="sa_card_hint" rows="2" placeholder="Подсказка для промпта"></textarea></label>
<label>Notes <textarea id="sa_card_notes" rows="2" placeholder="Заметки"></textarea></label>
<label>Civitai URL <input type="text" id="sa_card_url" placeholder="https://civitai…" /></label>
<label class="sa-card-json-toggle"><input type="checkbox" id="sa_card_show_json" /> Показать JSON</label>
<textarea id="sa_card_json" rows="8" hidden placeholder="Card JSON…" spellcheck="false"></textarea>
</div>
<div class="sa-cards-actions"> <div class="sa-cards-actions">
<button type="button" class="basic-button" id="sa_btn_card_meta">Load Civitai meta</button> <button type="button" class="basic-button" id="sa_btn_card_meta">Загрузить Civitai meta</button>
<button type="button" class="basic-button sa-primary" id="sa_btn_card_generate">Generate with Assistent</button> <button type="button" class="basic-button sa-primary" id="sa_btn_card_generate">Сгенерировать с Assistent</button>
<button type="button" class="basic-button" id="sa_btn_card_save">Save card</button> <button type="button" class="basic-button" id="sa_btn_card_save">Сохранить карточку</button>
<button type="button" class="basic-button" id="sa_btn_card_wanted" title="Add to next-up download list">Enqueue wanted</button> <button type="button" class="basic-button" id="sa_btn_card_wanted" title="В очередь на следующий up">В wanted</button>
<span class="sa-status" id="sa_card_status"></span> <span class="sa-status" id="sa_card_status"></span>
</div> </div>
</div> </div>