Files
chrmc40 de699b78b8 Z-Image: regional 3D-poser attention couple + pickable upscale/inpaint engines + true-depth fix
- PromptChain_ZImageRegionalCouple: $name{} per-character regional conditioning on
  Z-Image's joint attention (DiT analog of AttentionCouple); base+turbo 3D-regional templates.
- Z-Image as a pickable UPSCALE engine + 'use current model' INPAINT (source path traces
  CLIPLoader/lumina2); family-aware so base=dpmpp_sde/cfg4, turbo=res_multistep/cfg1.3/0.7.
- PoseStudio true-depth fix: range from posed bones (was bind-box -> figures under-exposed).
- Base model config + templates aligned to dpmpp_sde/cfg4; user-saved templates keep their
  settings (lockSettings) without freezing model-swap.
- Edit-modal region-upscale: first-apply blank-layer fix (mask snapshot vs deselect race).
2026-06-23 23:24:55 -07:00

485 lines
16 KiB
Svelte
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
// TemplatesTab — template list with drag-reorder, category grouping,
// save-as-new form, edit/hide/restore/delete actions.
import "./model-panel-shared.css";
let {
modelInfo = {},
savedConfig = null,
onApplyTemplate,
captureWorkflowGraph,
} = $props();
const arch = savedConfig?.architecture || modelInfo.architecture || "";
const family = savedConfig?.family || "";
const modelName = savedConfig?.model_name || modelInfo.filename || "";
const version = savedConfig?.version || "";
let templates = $state([]);
let categoryOrder = $state([]);
let loading = $state(true);
let editMode = $state(false);
let showHidden = $state(false);
// Save-new form state
let newName = $state("");
let newScope = $state("architecture");
let newGroup = $state("");
let existingCategories = $state([]);
function loadTemplates() {
loading = true;
const params = new URLSearchParams();
if (arch) params.set("arch", arch);
if (family) params.set("family", family);
if (modelName) params.set("name", modelName);
if (modelInfo.hash) params.set("hash", modelInfo.hash);
if (showHidden) params.set("include_hidden", "1");
fetch(`/promptchain/templates/list?${params}`)
.then(r => r.json())
.then(data => {
templates = data.templates || [];
categoryOrder = data.category_order || [];
loading = false;
})
.catch(() => { templates = []; loading = false; });
// Load all categories for the save form
fetch(`/promptchain/templates/list?${new URLSearchParams(arch ? { arch } : {})}`)
.then(r => r.json())
.then(({ templates: all }) => {
const cats = new Set();
for (const t of all) if (t.category) cats.add(t.category);
existingCategories = [...cats].sort();
})
.catch(() => {});
}
loadTemplates();
// Grouped templates
let grouped = $derived.by(() => {
const groups = {};
for (const tpl of templates) {
const cat = tpl.category || "";
if (!groups[cat]) groups[cat] = [];
groups[cat].push(tpl);
}
const known = categoryOrder.filter(c => groups[c]);
const unknown = Object.keys(groups).filter(c => c && !categoryOrder.includes(c)).sort();
return { uncategorized: groups[""] || [], ordered: [...known, ...unknown], groups };
});
let scopeOptions = $derived.by(() => {
const opts = [{ value: "architecture", label: `Architecture (${arch || "any"})` }];
if (family) opts.push({ value: "family", label: `Family (${family})` });
if (modelName) opts.push({ value: "model", label: `Model (${modelName})` });
opts.push({ value: "version", label: version ? `Version (${version})` : "This version" });
return opts;
});
function scopeLabel(tpl) {
return tpl.scope?.type === "version" ? (version || "this version")
: tpl.scope?.type === "model" ? (tpl.scope.model_name || "model")
: tpl.scope?.type === "family" ? (tpl.scope.family || "family")
: (tpl.scope?.architecture || "arch");
}
function sourceLabel(tpl) {
return tpl._hidden ? "hidden"
: tpl._source === "overlay" ? "modified"
: tpl._source === "user" ? "custom" : "system";
}
function handleApply(tpl) {
if (!confirm("Replace current downstream nodes with this template?")) return;
onApplyTemplate(tpl);
}
function handleRestore(tpl) {
fetch(`/promptchain/templates/${tpl.id}/reset`, { method: "POST" })
.then(() => loadTemplates());
}
function handleReset(tpl) {
if (!confirm(`Reset "${tpl.name}" to system defaults?`)) return;
fetch(`/promptchain/templates/${tpl.id}/reset`, { method: "POST" })
.then(() => loadTemplates());
}
function handleDelete(tpl) {
if (tpl._source === "user") {
if (!confirm(`Delete "${tpl.name}"?`)) return;
}
fetch(`/promptchain/templates/${tpl.id}`, { method: "DELETE" })
.then(() => loadTemplates());
}
function handleDissolveGroup(category, items) {
// allSettled: one failed PATCH shouldn't abort the rest. Log any
// rejections so a silent half-done state is still visible.
Promise.allSettled(items.map(t =>
fetch(`/promptchain/templates/${t.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ category: "" }),
}).then(r => { if (!r.ok) throw new Error(`HTTP ${r.status}`); })
)).then(results => {
const failed = results.filter(r => r.status === "rejected");
if (failed.length) console.error("[PromptChain] dissolve-group partial failure:", failed);
loadTemplates();
});
}
function handleResetAll() {
if (!confirm("Reset all templates to system defaults? This removes all custom templates, hidden states, and ordering.")) return;
fetch("/promptchain/templates/reset-all", { method: "POST" })
.then(() => loadTemplates());
}
async function handleSaveNew() {
if (!newName.trim()) return;
let tplCategory = newGroup;
if (tplCategory === "__new__") {
tplCategory = prompt("New group name:");
if (!tplCategory?.trim()) return;
tplCategory = tplCategory.trim();
}
const graphData = captureWorkflowGraph();
if (!graphData) {
alert("No downstream nodes to capture");
return;
}
const scope = { type: newScope, architecture: arch };
if (newScope === "family") scope.family = family;
if (newScope === "model") scope.model_name = modelName;
if (newScope === "version") scope.model_hash = modelInfo.hash;
const template = {
name: newName.trim(),
scope,
created_at: Math.floor(Date.now() / 1000),
// A user-saved template's widget values are deliberate, so its settings win
// over the model config's generic presets (applyTemplate honors lockSettings).
// This is NOT lockModels — model-swap-in-place still applies to user templates.
lockSettings: true,
...(tplCategory ? { category: tplCategory } : {}),
...graphData,
};
await fetch("/promptchain/templates", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(template),
});
newName = "";
loadTemplates();
}
</script>
<div class="pcr-model-panel-body">
{#if loading}
<div class="pcr-model-panel-empty">Loading...</div>
{:else if !templates.length && !editMode}
<div class="pcr-model-panel-empty">No templates saved yet</div>
{:else}
<div class="pcr-tpl-list">
{#each grouped.uncategorized as tpl (tpl.id)}
<div class="pcr-tpl-item" class:pcr-tpl-hidden={tpl._hidden} data-template-id={tpl.id}>
{#if editMode}
<span class="pcr-tpl-drag-handle">≡</span>
{/if}
<div class="pcr-tpl-item-info">
<span class="pcr-tpl-item-name">{tpl.name}</span>
<span class="pcr-tpl-item-meta">
{scopeLabel(tpl)} · {tpl.nodes?.length || 0} nodes
<span class="pcr-tpl-source-badge"> · {sourceLabel(tpl)}</span>
</span>
</div>
<div class="pcr-tpl-item-actions">
{#if tpl._hidden}
<button class="pcr-tpl-apply-btn" onclick={() => handleRestore(tpl)}>Restore</button>
{:else if editMode}
{#if tpl._source === "overlay"}
<button class="pcr-tpl-reset-btn" onclick={() => handleReset(tpl)}>Reset</button>
{/if}
{#if tpl._source === "user"}
<button class="pcr-tpl-delete-btn" onclick={() => handleDelete(tpl)}>×</button>
{:else}
<button class="pcr-tpl-hide-btn" onclick={() => handleDelete(tpl)}>×</button>
{/if}
{:else}
<button class="pcr-tpl-apply-btn" onclick={() => handleApply(tpl)}>Apply</button>
{/if}
</div>
</div>
{/each}
{#each grouped.ordered as category}
{@const items = grouped.groups[category] || []}
{#if items.length}
<div class="pcr-tpl-group-header" data-category={category}>
{#if editMode}
<span class="pcr-tpl-group-grip">⠿</span>
{/if}
<span class="pcr-tpl-group-toggle">▾</span>
<span>{category}</span>
{#if editMode}
<span class="pcr-tpl-group-dissolve"
onclick={(e) => { e.stopPropagation(); handleDissolveGroup(category, items); }}>×</span>
{/if}
</div>
<div class="pcr-tpl-group-body" data-category={category}>
{#each items as tpl (tpl.id)}
<div class="pcr-tpl-item" class:pcr-tpl-hidden={tpl._hidden} data-template-id={tpl.id}>
{#if editMode}
<span class="pcr-tpl-drag-handle">≡</span>
{/if}
<div class="pcr-tpl-item-info">
<span class="pcr-tpl-item-name">{tpl.name}</span>
<span class="pcr-tpl-item-meta">
{scopeLabel(tpl)} · {tpl.nodes?.length || 0} nodes
<span class="pcr-tpl-source-badge"> · {sourceLabel(tpl)}</span>
</span>
</div>
<div class="pcr-tpl-item-actions">
{#if tpl._hidden}
<button class="pcr-tpl-apply-btn" onclick={() => handleRestore(tpl)}>Restore</button>
{:else if editMode}
{#if tpl._source === "overlay"}
<button class="pcr-tpl-reset-btn" onclick={() => handleReset(tpl)}>Reset</button>
{/if}
{#if tpl._source === "user"}
<button class="pcr-tpl-delete-btn" onclick={() => handleDelete(tpl)}>×</button>
{:else}
<button class="pcr-tpl-hide-btn" onclick={() => handleDelete(tpl)}>×</button>
{/if}
{:else}
<button class="pcr-tpl-apply-btn" onclick={() => handleApply(tpl)}>Apply</button>
{/if}
</div>
</div>
{/each}
</div>
{/if}
{/each}
</div>
{/if}
{#if editMode}
<div class="pcr-tpl-save-section">
<div class="pcr-tpl-save-row">
<input type="text" class="pcr-tpl-save-name" placeholder="Template name..."
bind:value={newName} />
<select class="pcr-tpl-save-scope" bind:value={newGroup}>
<option value="">No Group</option>
{#each existingCategories as cat}
<option value={cat}>{cat}</option>
{/each}
<option value="__new__">+ New Group...</option>
</select>
<select class="pcr-tpl-save-scope" bind:value={newScope}>
{#each scopeOptions as opt}
<option value={opt.value}>{opt.label}</option>
{/each}
</select>
</div>
<button class="pcr-model-panel-save" onclick={handleSaveNew}>Save Current</button>
</div>
{/if}
<div style="border-top:1px solid #333;margin-top:8px;padding-top:8px;display:flex;gap:8px;">
<button class="pcr-model-panel-apply"
onclick={(e) => { e.stopPropagation(); showHidden = !showHidden; loadTemplates(); }}>
{showHidden ? "Hide Hidden" : "Show Hidden"}
</button>
{#if editMode}
<button class="pcr-model-panel-apply" onclick={handleResetAll}>Reset All</button>
{/if}
<button class="pcr-model-panel-apply"
onclick={(e) => { e.stopPropagation(); editMode = !editMode; if (!editMode) showHidden = false; loadTemplates(); }}>
{editMode ? "Done" : "Edit"}
</button>
</div>
</div>
<style>
/* template list */
.pcr-tpl-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.pcr-tpl-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 8px;
border-radius: 4px;
cursor: default;
}
.pcr-tpl-item:hover { background: rgba(255, 255, 255, 0.05); }
.pcr-tpl-item-info {
display: flex;
flex-direction: column;
min-width: 0;
flex: 1;
}
.pcr-tpl-item-name {
font-size: 12px;
color: #ddd;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.pcr-tpl-item-meta {
font-size: 10px;
color: #777;
}
.pcr-tpl-item-actions {
display: flex;
gap: 4px;
flex-shrink: 0;
margin-left: 8px;
}
.pcr-tpl-apply-btn {
font-size: 11px;
padding: 2px 8px;
background: #1a6b9c;
color: #ddd;
border: none;
border-radius: 3px;
cursor: pointer;
}
.pcr-tpl-apply-btn:hover { background: #1177bb; }
.pcr-tpl-delete-btn {
font-size: 14px;
line-height: 1;
padding: 1px 5px;
background: transparent;
color: #666;
border: none;
border-radius: 3px;
cursor: pointer;
}
.pcr-tpl-delete-btn:hover { color: #e44; background: rgba(255, 50, 50, 0.1); }
/* save section */
.pcr-tpl-save-section {
border-top: 1px solid #333;
padding-top: 8px;
margin-top: 4px;
display: flex;
flex-direction: column;
gap: 6px;
}
.pcr-tpl-save-row {
display: flex;
gap: 6px;
min-width: 0;
}
.pcr-tpl-save-name {
flex: 1;
min-width: 0;
font-size: 12px;
padding: 4px 8px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid #444;
border-radius: 4px;
color: #ddd;
outline: none;
}
.pcr-tpl-save-name:focus { border-color: #4fc3f7; }
.pcr-tpl-save-name::placeholder { color: #666; }
.pcr-tpl-save-scope {
font-size: 11px;
padding: 3px 6px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid #444;
border-radius: 4px;
color: #aaa;
cursor: pointer;
appearance: none;
-webkit-appearance: none;
min-width: 0;
}
.pcr-tpl-save-scope:focus { border-color: #4fc3f7; outline: none; }
/* drag reorder */
.pcr-tpl-drag-handle { color: #555; cursor: grab; font-size: 14px; padding: 0 4px; }
.pcr-tpl-item.dragging { opacity: 0.4; }
.pcr-tpl-item.drag-over-top { border-top: 2px solid #4fc3f7; }
.pcr-tpl-item.drag-over-bottom { border-bottom: 2px solid #4fc3f7; }
/* category groups */
.pcr-tpl-group-header {
display: flex;
align-items: center;
gap: 6px;
padding: 6px 8px 2px;
font-size: 10px;
font-weight: 600;
color: #888;
text-transform: uppercase;
letter-spacing: 0.5px;
cursor: pointer;
user-select: none;
}
.pcr-tpl-group-header:hover { color: #aaa; }
.pcr-tpl-group-toggle { font-size: 9px; transition: transform 0.15s; }
.pcr-tpl-group-toggle.collapsed { transform: rotate(-90deg); }
.pcr-tpl-group-body { display: flex; flex-direction: column; gap: 2px; }
.pcr-tpl-group-body.collapsed { display: none; }
.pcr-tpl-group-dissolve {
margin-left: auto;
font-size: 12px;
color: #555;
cursor: pointer;
padding: 0 4px;
}
.pcr-tpl-group-dissolve:hover { color: #e44; }
.pcr-tpl-group-grip {
font-size: 10px;
cursor: grab;
color: #555;
padding: 0 2px;
}
.pcr-tpl-group-grip:hover { color: #aaa; }
.pcr-tpl-group-header.dragging { opacity: 0.4; }
/* source + reset */
.pcr-tpl-source-badge {
font-size: 9px;
color: #555;
font-style: italic;
}
.pcr-tpl-reset-btn {
font-size: 11px;
padding: 2px 8px;
background: #6b5a1a;
color: #ddd;
border: none;
border-radius: 3px;
cursor: pointer;
}
.pcr-tpl-reset-btn:hover { background: #8a7422; }
.pcr-tpl-hide-btn {
font-size: 14px;
line-height: 1;
padding: 1px 5px;
background: transparent;
color: #666;
border: none;
border-radius: 3px;
cursor: pointer;
}
.pcr-tpl-hide-btn:hover { color: #e44; background: rgba(255, 50, 50, 0.1); }
.pcr-tpl-hidden { opacity: 0.4; }
.pcr-tpl-hidden .pcr-tpl-item-name { text-decoration: line-through; }
</style>