Combo group on the pointer: Esc cancels, prompt nodes always, common negative
- While a combo group follows the pointer, Esc removes it again; the activity log holds back until it is put down, so a cancelled group leaves no entry. - The positive and negative prompt nodes are always laid out (positive empty when the combo has no prompt); the negative takes Prompt Studio's common negative card (starter cards moved to prompt_composition.js as PROMPT_PRESETS). - The UNet "connect clip yourself" message shows once the group is down; it used to pop up while the group followed the pointer, and closing it put the group down. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
7f96afe459
commit
01dcdf37a9
+2
-2
@@ -390,7 +390,7 @@ covered by the `styles.css` manifest.
|
||||
unfolded companion models card with unconstrained multi-column tile flow eliminating nested gallery scrollbars,
|
||||
prompt composer with dynamic field-sizing and compact inline find & replace toolbar, and unified dark slim scrollbar ergonomics with complete bilingual dictionary coverage in `locales.js`), and
|
||||
`notebook_canvas.js` puts a combo on the canvas as a new wired group of nodes (following the pointer, or at a dropped
|
||||
card's position); it never changes nodes already there. Combos are a rail page. `ui_recipe_detail.js`
|
||||
card's position; Esc takes a following group off again); it never changes nodes already there. Combos are a rail page. `ui_recipe_detail.js`
|
||||
coordinates the Workflow Recipe detail session and model composition. `ui_recipe_overview.js`
|
||||
owns the Overview prompt showcase (with `entry.text` fallback, guarded non-shrinking primary action CTA, and floating Popover More dropdown menu), and `ui_recipe_parameters.js`
|
||||
owns the responsive Parameter Presets workspace (featuring default-expanded raw node parameter inspection,
|
||||
@@ -436,7 +436,7 @@ covered by the `styles.css` manifest.
|
||||
corridor, pin, Copy / Add / Rename / Delete); `prompt_card_drag.js` lets a card or a box be
|
||||
dragged out of the drawer onto a canvas prompt box or empty canvas (through `material_drag.js`
|
||||
and `prompt_drop.js`), passing through inside the drawer so the boxes still take it.
|
||||
`prompt_composition.js` composes a saved plan's text and names and sorts prompts.
|
||||
`prompt_composition.js` holds the starter cards, composes a saved plan's text and names and sorts prompts.
|
||||
- `ui_node_prompts.js` is the current-node panel's prompt boxes: role and text, with
|
||||
Edit in Prompt Studio. The studio uses `ui_lifecycle.js` for global listeners, request
|
||||
cancellation and resize cleanup. Translation requests go through `translation_service.js`.
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@
|
||||
### Fixes
|
||||
- **Prompt Notes are now Combos, a page of their own (提示词笔记改名“搭配”,有了自己的页面)**: a note was always a main model, a few LoRAs and a prompt kept together, so it is called a combo and sits on the rail as **Combos**, with a card on Home. The page shows every combo as a card with its model's cover, its LoRAs and the start of its prompt, a search box and **New combo**; a card opens the combo to edit, and **‹ All combos** goes back. The old notes column with its quick-jump list is gone. Your notes are the same files and show up as combos; prompts saved from notes before still show in the Material Library and can be saved as a new combo.
|
||||
- **Whole workflows live on the Workflows page (完整工作流都在“工作流”页)**: a result kept whole (now with the gallery's ☆, or from the image workbench) is a Workflow Recipe with that picture as its cover (it was a material), so versions, parameter sets and Model Check work on it. The Material Library keeps the pieces (nodes, parameters, prompts) and lost its Workflow category. Whole workflows kept as materials before show on the Workflows page as "N more in the library — Move them here"; moving saves each as a recipe and leaves the material file where it is, just no longer listed. Recipes that are not whole workflows are called Modules everywhere (the filter said "Sub-graph").
|
||||
- **Put a combo on the canvas (搭配一键放到画布)**: **Put on canvas** on a combo's card or in its editor (formerly Send to canvas, under More) lays the combo out as a group of nodes wired together: its main model loader, its LoRAs and a positive and a negative prompt node; the group follows the pointer until you click. A combo card can also be dragged onto the canvas, and the group is built where it is dropped. Nothing already on the canvas is changed (to change an existing node's values, use Current node). A new group from a UNet combo connects its prompt nodes to the canvas's CLIP loader when there is exactly one, and says so when not.
|
||||
- **Put a combo on the canvas (搭配一键放到画布)**: **Put on canvas** on a combo's card or in its editor (formerly Send to canvas, under More) lays the combo out as a group of nodes wired together: its main model loader, its LoRAs and a positive and a negative prompt node; the group follows the pointer until you click, and Esc takes it off again (nothing is logged or left behind). The positive node holds the combo's prompt (left empty when it has none, rather than leaving the prompt nodes out) and the negative node gets Prompt Studio's common negative card. A combo card can also be dragged onto the canvas, and the group is built where it is dropped. Nothing already on the canvas is changed (to change an existing node's values, use Current node). A new group from a UNet combo connects its prompt nodes to the canvas's CLIP loader when there is exactly one, and says so once the group is down (the message no longer pops up while the group still follows the pointer, where closing it put the group down).
|
||||
- **Recipe detail, plainer (配方详情更清爽)**: the models a recipe uses are one list: preview, name, the node, whether this computer has it under the saved name, size, your note and its download page. The recipe's own "match local model / apply match" buttons, the identity badges, the advanced blocks with paths and hashes, the origin editor and the recipe fingerprint are gone: when the workflow opens, Model Check finds renamed or moved models and puts them in, and saving the recipe again keeps that (the old version is kept). The status says "Every model is on this computer" or "N models missing" with what happens next. The save dialog and the empty page speak plainly (no more "unclosed node chain" or "self-healing"), and the parameters page lost its save-to-library button on every node (pick nodes with "select some parameters" instead).
|
||||
- **Shared workflows say where their models come from (分享的工作流带上模型来源)**: saving a workflow or an image now also stores each model's download link (the one you set in the model editor, else its Civitai page) unless the workflow already keeps one, and drops links of models it no longer uses, so after Model Check swaps in your own model, what you export names your file, fingerprint and link while the workflow you opened stays as it was. GGUF, .pth and .sft models get fingerprints too. The share-code export says how many models carry a fingerprint, warns when some have none, and can leave the links out.
|
||||
- **Model Check finds renamed models by fingerprint again (按指纹找回改名的模型)**: when a workflow's model was renamed or moved and no scan had hashed the file yet, Model Check failed to hash the same-size files itself (an error in the background) and called the model missing. It now hashes them and finds it.
|
||||
|
||||
@@ -76,7 +76,8 @@ The main surfaces are:
|
||||
owns header/zoom/filmstrip interaction, and `ui_image_inspector.js` owns metadata tabs.
|
||||
- Workspace: Combos (搭配, formerly Prompt Notes) and Workflow Recipes. The combo list, workspace/persistence, editing, and canvas use are separated
|
||||
across `ui_combos.js`, `ui_notebooks.js`, `ui_notebook_editor.js`, and
|
||||
`notebook_canvas.js`; Put on canvas lays a combo out as a new wired group of nodes and
|
||||
`notebook_canvas.js`; Put on canvas lays a combo out as a new wired group of nodes (Esc while it
|
||||
follows the pointer removes it; the activity log holds back while `owner.placingCombo` is set) and
|
||||
never changes nodes already on the canvas (values of an existing node are changed in
|
||||
Current node).
|
||||
- Prompts: the rail entry opens Prompt Studio (below); there is no Material Library page.
|
||||
@@ -291,7 +292,7 @@ drawer the drag passes through to the boxes.
|
||||
| `prompt_tags.js` | Pure tag split/join, weights, insert/remove/replace/move keeping separators |
|
||||
| `prompt_gloss.js` | Meaning languages, session cache and batched lookup of tags' meanings |
|
||||
| `prompt_material_source.js` | Saved prompts as cards (paginated list, text read once) and saving a card |
|
||||
| `prompt_composition.js` | Composing a saved plan's text, prompt titles and categories; no DOM |
|
||||
| `prompt_composition.js` | Starter cards (`PROMPT_PRESETS`, also the combo group's negative), composing a saved plan's text, prompt titles and categories; no DOM |
|
||||
| `ui_lifecycle.js` | View-scoped listeners, AbortSignal, cleanup callbacks and resizing |
|
||||
|
||||
Every close route (Close, Esc outside a text field, replacement) disposes the same view
|
||||
|
||||
@@ -35,7 +35,8 @@ export function watchCanvasChanges(owner) {
|
||||
let baseline = null;
|
||||
|
||||
const flush = () => {
|
||||
if (!baseline) return;
|
||||
// A combo group following the pointer is logged once it is put down; Esc removes it.
|
||||
if (!baseline || owner.placingCombo) return;
|
||||
const { graph, workflow, nodes } = baseline;
|
||||
baseline = null;
|
||||
const now = currentGraph();
|
||||
|
||||
@@ -490,7 +490,7 @@ export const i18n = {
|
||||
comboNew: "新建搭配",
|
||||
comboNewName: "新搭配叫什么?",
|
||||
comboUse: "放到画布",
|
||||
comboUseHint: "把主模型、LoRA 和正负提示词连好线放到画布上,跟着鼠标走,点一下放下",
|
||||
comboUseHint: "把主模型、LoRA 和正负提示词连好线放到画布上,跟着鼠标走,点一下放下,Esc 取消",
|
||||
comboNoModel: "没选模型",
|
||||
comboLoraCount: "{count} 个 LoRA",
|
||||
comboNoPrompt: "(还没写提示词)",
|
||||
@@ -2414,7 +2414,7 @@ export const i18n = {
|
||||
comboNew: "New combo",
|
||||
comboNewName: "Name the new combo",
|
||||
comboUse: "Put on canvas",
|
||||
comboUseHint: "Lays the main model, LoRAs and prompts out wired together; the group follows the pointer until you click",
|
||||
comboUseHint: "Lays the main model, LoRAs and prompts out wired together; the group follows the pointer until you click, Esc cancels",
|
||||
comboNoModel: "No model",
|
||||
comboLoraCount: "{count} LoRAs",
|
||||
comboNoPrompt: "(no prompt yet)",
|
||||
|
||||
+131
-132
@@ -1,18 +1,23 @@
|
||||
/**
|
||||
* Puts the active combo (搭配) on the canvas as a new group of nodes: its main model loader,
|
||||
* its LoRA chain and a positive and a negative prompt node, wired together. From a button the
|
||||
* group follows the pointer until a click; a card dropped on the canvas builds it where it
|
||||
* was dropped. Nothing already on the canvas changes, except that a UNet combo's prompt nodes
|
||||
* take the canvas's CLIP loader when there is exactly one.
|
||||
* its LoRA chain and a positive and a negative prompt node (the positive holds the combo's
|
||||
* prompt, the negative the common negative starter card), wired together. From a button the
|
||||
* group follows the pointer until a click, and Esc takes it off again; a card dropped on the
|
||||
* canvas builds it where it was dropped. Nothing already on the canvas changes, except that a
|
||||
* UNet combo's prompt nodes take the canvas's CLIP loader when there is exactly one.
|
||||
*/
|
||||
|
||||
import { app } from '../../../scripts/app.js';
|
||||
import { translate } from './locales.js';
|
||||
import { recordCanvasStep } from './canvas_history.js';
|
||||
import { PROMPT_PRESETS } from './prompt_composition.js';
|
||||
import { anomalousAlert } from './ui_dialog.js';
|
||||
|
||||
const t = (key, params) => translate(key, params);
|
||||
|
||||
const NEGATIVE_PROMPT = PROMPT_PRESETS.find(card => card.id === 'preset_negative').content;
|
||||
const STEP_X = 350;
|
||||
|
||||
/** Base-model value the backend uses for models whose metadata names no base model. */
|
||||
export const UNLABELED_BASE_MODEL = '__unlabeled__';
|
||||
|
||||
@@ -29,134 +34,128 @@ function canvasClipSource(exclude) {
|
||||
return sources.length === 1 ? sources[0] : null;
|
||||
}
|
||||
|
||||
const modelPath = (model) => {
|
||||
const sub = String(model.subfolder || '').replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
return sub ? `${sub}/${model.filename}` : model.filename;
|
||||
};
|
||||
|
||||
/** The pointer's position in canvas coordinates. */
|
||||
function canvasPoint(event) {
|
||||
const canvas = app.canvas;
|
||||
if (canvas.convertEventToCanvasOffset) return canvas.convertEventToCanvasOffset(event);
|
||||
const rect = canvas.canvas.getBoundingClientRect();
|
||||
return [
|
||||
(event.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale,
|
||||
(event.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* The group follows the pointer until a click puts it down (then `placed()`); Esc removes it
|
||||
* again. While it follows, `owner.placingCombo` holds back the activity log, which is told once
|
||||
* the group is down (after Esc there is nothing to log).
|
||||
*/
|
||||
function followPointer(owner, groupNodes, placed) {
|
||||
const half = (groupNodes[0].node.size?.[0] || 200) / 2;
|
||||
const move = (event) => {
|
||||
if (!app.canvas) return;
|
||||
const [x, y] = canvasPoint(event);
|
||||
groupNodes.forEach(item => { item.node.pos = [x - half + item.relX, y - 20 + item.relY]; });
|
||||
app.canvas.setDirty(true, true);
|
||||
};
|
||||
let armTimer = 0;
|
||||
const stop = (event) => {
|
||||
owner.placingCombo = false;
|
||||
clearTimeout(armTimer);
|
||||
window.removeEventListener('mousemove', move, true);
|
||||
window.removeEventListener('keydown', cancel, true);
|
||||
['pointerdown', 'mousedown', 'click'].forEach(type => window.removeEventListener(type, drop, true));
|
||||
event.preventDefault();
|
||||
event.stopPropagation();
|
||||
};
|
||||
const drop = (event) => {
|
||||
stop(event);
|
||||
placed();
|
||||
owner.flushCanvasActivity?.();
|
||||
};
|
||||
const cancel = (event) => {
|
||||
if (event.key !== 'Escape') return;
|
||||
stop(event);
|
||||
groupNodes.forEach(item => app.graph.remove(item.node));
|
||||
app.graph.setDirtyCanvas?.(true, true);
|
||||
};
|
||||
|
||||
owner.placingCombo = true;
|
||||
window.addEventListener('mousemove', move, true);
|
||||
window.addEventListener('keydown', cancel, true);
|
||||
// Later than the click that pressed the button, so that click does not put the group down.
|
||||
armTimer = setTimeout(() => {
|
||||
['pointerdown', 'mousedown', 'click'].forEach(type => window.addEventListener(type, drop, true));
|
||||
}, 100);
|
||||
}
|
||||
|
||||
/** Builds the combo as a new group of nodes: at `position` (canvas coordinates), or following the pointer. */
|
||||
export function sendNotebookToCanvas(position = null) {
|
||||
if (!this.currentNotebook) return;
|
||||
const data = this.currentNotebook.data || {};
|
||||
if (!data.mainModel) {
|
||||
void anomalousAlert(t('notebookSelectMain'));
|
||||
return;
|
||||
}
|
||||
|
||||
const groupNodes = [];
|
||||
const isUnet = isUnetModel(data.mainModel);
|
||||
|
||||
const ckptNode = LiteGraph.createNode(isUnet ? "UNETLoader" : "CheckpointLoaderSimple");
|
||||
app.graph.add(ckptNode);
|
||||
groupNodes.push({ node: ckptNode, relX: 0, relY: 0 });
|
||||
|
||||
const sub = data.mainModel.subfolder.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const relPath = sub ? `${sub}/${data.mainModel.filename}` : data.mainModel.filename;
|
||||
this.setWidgetValuePath(ckptNode, relPath);
|
||||
|
||||
let lastNode = ckptNode;
|
||||
let lastModelSlot = 0;
|
||||
let lastClipSlot = isUnet ? null : 1;
|
||||
|
||||
let relX = 350;
|
||||
let relY = 0;
|
||||
|
||||
data.loras.forEach((lora, idx) => {
|
||||
const loraNode = LiteGraph.createNode(isUnet ? "LoraLoaderModelOnly" : "LoraLoader");
|
||||
app.graph.add(loraNode);
|
||||
groupNodes.push({ node: loraNode, relX: relX, relY: relY });
|
||||
|
||||
const lsub = lora.subfolder.replace(/^\/+/, '').replace(/\/+$/, '');
|
||||
const lrelPath = lsub ? `${lsub}/${lora.filename}` : lora.filename;
|
||||
this.setWidgetValuePath(loraNode, lrelPath);
|
||||
|
||||
lastNode.connect(lastModelSlot, loraNode, 0);
|
||||
if (lastClipSlot !== null) lastNode.connect(lastClipSlot, loraNode, 1);
|
||||
|
||||
lastNode = loraNode;
|
||||
lastModelSlot = 0;
|
||||
if (!isUnet) lastClipSlot = 1;
|
||||
relX += 350;
|
||||
});
|
||||
|
||||
if (data.promptEn) {
|
||||
const posNode = LiteGraph.createNode("CLIPTextEncode");
|
||||
posNode.title = "CLIP Text Encode (Positive)";
|
||||
app.graph.add(posNode);
|
||||
groupNodes.push({ node: posNode, relX: relX, relY: 0 });
|
||||
|
||||
if (posNode.widgets && posNode.widgets.length > 0) {
|
||||
const tw = posNode.widgets.find(w => w.name === 'text' || w.type === 'customtext');
|
||||
if (tw) tw.value = data.promptEn;
|
||||
}
|
||||
const clipSource = lastClipSlot === null ? canvasClipSource(groupNodes.map(item => item.node)) : null;
|
||||
if (lastClipSlot !== null) {
|
||||
lastNode.connect(lastClipSlot, posNode, 0);
|
||||
} else if (clipSource) {
|
||||
clipSource.connect(clipSource.outputs.findIndex(output => output.type === 'CLIP'), posNode, 0);
|
||||
} else {
|
||||
anomalousAlert(t('noteNeedsClip'));
|
||||
}
|
||||
|
||||
const negNode = LiteGraph.createNode("CLIPTextEncode");
|
||||
negNode.title = "CLIP Text Encode (Negative)";
|
||||
app.graph.add(negNode);
|
||||
groupNodes.push({ node: negNode, relX: relX, relY: 250 });
|
||||
|
||||
if (negNode.widgets && negNode.widgets.length > 0) {
|
||||
const tw = negNode.widgets.find(w => w.name === 'text' || w.type === 'customtext');
|
||||
if (tw) tw.value = "text, watermark, ugly, bad anatomy";
|
||||
}
|
||||
if (lastClipSlot !== null) {
|
||||
lastNode.connect(lastClipSlot, negNode, 0);
|
||||
} else if (clipSource) {
|
||||
clipSource.connect(clipSource.outputs.findIndex(output => output.type === 'CLIP'), negNode, 0);
|
||||
}
|
||||
}
|
||||
|
||||
if (position) {
|
||||
groupNodes.forEach(item => { item.node.pos = [position[0] + item.relX, position[1] + item.relY]; });
|
||||
app.graph.setDirtyCanvas?.(true, true);
|
||||
}
|
||||
recordCanvasStep(app);
|
||||
this.nbPanel.style.display = 'none';
|
||||
this.close();
|
||||
if (position) return;
|
||||
|
||||
// Magnetic Sticking Logic
|
||||
let isSticking = true;
|
||||
const stickHandler = (e) => {
|
||||
if (!isSticking || !app.canvas) return;
|
||||
const canvas = app.canvas;
|
||||
|
||||
let canvasX, canvasY;
|
||||
if (canvas.convertEventToCanvasOffset) {
|
||||
const pos = canvas.convertEventToCanvasOffset(e);
|
||||
canvasX = pos[0];
|
||||
canvasY = pos[1];
|
||||
} else {
|
||||
const rect = canvas.canvas.getBoundingClientRect();
|
||||
canvasX = (e.clientX - rect.left - canvas.ds.offset[0]) / canvas.ds.scale;
|
||||
canvasY = (e.clientY - rect.top - canvas.ds.offset[1]) / canvas.ds.scale;
|
||||
}
|
||||
|
||||
groupNodes.forEach(item => {
|
||||
const w = (item.node.size && item.node.size[0]) ? item.node.size[0] : 200;
|
||||
item.node.pos = [canvasX - (w / 2) + item.relX, canvasY - 20 + item.relY];
|
||||
});
|
||||
canvas.setDirty(true, true);
|
||||
};
|
||||
|
||||
const dropHandler = (e) => {
|
||||
if (!isSticking) return;
|
||||
isSticking = false;
|
||||
window.removeEventListener('mousemove', stickHandler, true);
|
||||
window.removeEventListener('pointerdown', dropHandler, true);
|
||||
window.removeEventListener('mousedown', dropHandler, true);
|
||||
window.removeEventListener('click', dropHandler, true);
|
||||
e.preventDefault();
|
||||
e.stopPropagation();
|
||||
};
|
||||
|
||||
window.addEventListener('mousemove', stickHandler, true);
|
||||
setTimeout(() => {
|
||||
window.addEventListener('pointerdown', dropHandler, true);
|
||||
window.addEventListener('mousedown', dropHandler, true);
|
||||
window.addEventListener('click', dropHandler, true);
|
||||
}, 100);
|
||||
if (!this.currentNotebook) return;
|
||||
const data = this.currentNotebook.data || {};
|
||||
if (!data.mainModel) {
|
||||
void anomalousAlert(t('notebookSelectMain'));
|
||||
return;
|
||||
}
|
||||
|
||||
const groupNodes = [];
|
||||
const add = (type, relX, relY) => {
|
||||
const node = LiteGraph.createNode(type);
|
||||
app.graph.add(node);
|
||||
groupNodes.push({ node, relX, relY });
|
||||
return node;
|
||||
};
|
||||
const isUnet = isUnetModel(data.mainModel);
|
||||
|
||||
const loader = add(isUnet ? 'UNETLoader' : 'CheckpointLoaderSimple', 0, 0);
|
||||
this.setWidgetValuePath(loader, modelPath(data.mainModel));
|
||||
let last = loader;
|
||||
const clipSlot = isUnet ? null : 1; // a checkpoint loader and a LoraLoader both give CLIP second
|
||||
let relX = STEP_X;
|
||||
(data.loras || []).forEach(lora => {
|
||||
const node = add(isUnet ? 'LoraLoaderModelOnly' : 'LoraLoader', relX, 0);
|
||||
this.setWidgetValuePath(node, modelPath(lora));
|
||||
last.connect(0, node, 0);
|
||||
if (clipSlot !== null) last.connect(clipSlot, node, 1);
|
||||
last = node;
|
||||
relX += STEP_X;
|
||||
});
|
||||
|
||||
const promptNode = (title, text, relY) => {
|
||||
const node = add('CLIPTextEncode', relX, relY);
|
||||
node.title = title;
|
||||
const box = node.widgets?.find(w => w.name === 'text' || w.type === 'customtext');
|
||||
if (box) box.value = text;
|
||||
return node;
|
||||
};
|
||||
const prompts = [
|
||||
promptNode('CLIP Text Encode (Positive)', String(data.promptEn || ''), 0),
|
||||
promptNode('CLIP Text Encode (Negative)', NEGATIVE_PROMPT, 250),
|
||||
];
|
||||
let clip = clipSlot === null ? null : { node: last, slot: clipSlot };
|
||||
if (!clip) {
|
||||
const source = canvasClipSource(groupNodes.map(item => item.node));
|
||||
if (source) clip = { node: source, slot: source.outputs.findIndex(output => output.type === 'CLIP') };
|
||||
}
|
||||
if (clip) prompts.forEach(node => clip.node.connect(clip.slot, node, 0));
|
||||
|
||||
const placed = () => {
|
||||
recordCanvasStep(app);
|
||||
if (!clip) void anomalousAlert(t('noteNeedsClip'));
|
||||
};
|
||||
this.nbPanel.style.display = 'none';
|
||||
if (!position) {
|
||||
followPointer(this, groupNodes, placed); // first, so closing does not log the group yet
|
||||
this.close();
|
||||
return;
|
||||
}
|
||||
this.close();
|
||||
groupNodes.forEach(item => { item.node.pos = [position[0] + item.relX, position[1] + item.relY]; });
|
||||
app.graph.setDirtyCanvas?.(true, true);
|
||||
placed();
|
||||
}
|
||||
|
||||
@@ -1,3 +1,10 @@
|
||||
/** Prompt Studio's starter cards; a combo put on the canvas takes the common negative one. */
|
||||
export const PROMPT_PRESETS = [
|
||||
{ id: 'preset_quality', titleKey: 'promptPresetQuality', role: 'positive', content: 'masterpiece, best quality, highly detailed' },
|
||||
{ id: 'preset_negative', titleKey: 'promptPresetNegative', role: 'negative', content: 'worst quality, low quality, lowres, blurry, jpeg artifacts, watermark, text' },
|
||||
{ id: 'preset_anatomy', titleKey: 'promptPresetAnatomy', role: 'negative', content: 'bad anatomy, bad hands, extra fingers, missing fingers, deformed' },
|
||||
];
|
||||
|
||||
export function composePromptPlan(plan) {
|
||||
if (!plan) return { positive: '', negative: '' };
|
||||
const parts = Array.isArray(plan.parts) ? plan.parts : [];
|
||||
|
||||
@@ -8,18 +8,13 @@
|
||||
|
||||
import { translate as t } from './locales.js';
|
||||
import { bindPromptCardDrag } from './prompt_card_drag.js';
|
||||
import { promptTitle } from './prompt_composition.js';
|
||||
import { PROMPT_PRESETS, promptTitle } from './prompt_composition.js';
|
||||
import { loadPromptSourceCards, savePromptCard } from './prompt_material_source.js';
|
||||
import { anomalousAlert, anomalousConfirm, anomalousPrompt } from './ui_dialog.js';
|
||||
import { jsonResponse } from './ui_dom.js';
|
||||
import { createCardPopover } from './ui_prompt_card_popover.js';
|
||||
import { showWorkbenchToast } from './ui_prompt_toast.js';
|
||||
|
||||
const PRESET_CARDS = [
|
||||
{ id: 'preset_quality', titleKey: 'promptPresetQuality', role: 'positive', content: 'masterpiece, best quality, highly detailed' },
|
||||
{ id: 'preset_negative', titleKey: 'promptPresetNegative', role: 'negative', content: 'worst quality, low quality, lowres, blurry, jpeg artifacts, watermark, text' },
|
||||
{ id: 'preset_anatomy', titleKey: 'promptPresetAnatomy', role: 'negative', content: 'bad anatomy, bad hands, extra fingers, missing fingers, deformed' },
|
||||
];
|
||||
const SYNC_MS = 30000;
|
||||
|
||||
function el(tag, className, content) {
|
||||
@@ -39,7 +34,7 @@ function button(className, label, title, onClick) {
|
||||
|
||||
/** Fills `parent`; `onPick(card)` takes a clicked card. Returns { sync() }. */
|
||||
export function createPromptSourceDeck(parent, drawer, scope, onPick) {
|
||||
const presets = PRESET_CARDS.map(card => ({ ...card, title: t(card.titleKey) }));
|
||||
const presets = PROMPT_PRESETS.map(card => ({ ...card, title: t(card.titleKey) }));
|
||||
let saved = [];
|
||||
let keyword = '';
|
||||
const popover = createCardPopover({
|
||||
|
||||
Reference in New Issue
Block a user