Files
chrmc40 8e3dceae99 ComfyUI-PromptChain
A prompt-authoring and image-iteration suite for ComfyUI.
2026-06-17 19:35:25 -07:00

238 lines
8.5 KiB
Svelte

<script>
// InfoTab — model metadata form (author, description, trigger words, etc.)
import "./model-panel-shared.css";
import ClickToEditField from "./ClickToEditField.svelte";
let {
modelInfo = {},
savedConfig = null,
architectures = [],
families = {},
onConfigUpdate,
} = $props();
let config = $state({ ...(savedConfig || {}) });
let saveStatus = $state("");
let saveTimer = null;
$effect(() => () => { clearTimeout(saveTimer); });
// Classification fields
let currentArch = $state(config.architecture || modelInfo.architecture || "");
let currentFamily = $state(config.family || "");
let currentModelName = $state(config.model_name || modelInfo.filename || "");
let currentVersion = $state(config.version || "");
// Info fields
let author = $state(config.author || "");
let description = $state(config.description || "");
let license = $state(config.license || "");
let tags = $state((config.tags || []).join(", "));
let url = $state(config.url || "");
let downloadUrl = $state(config.download_url || "");
let civitaiId = $state(config.civitai_model_id || "");
let trigger = $state(config.trigger || "");
let negative = $state(config.negative || "");
let qualityPosition = $state(config.quality_position || "");
let releaseDate = $state(config.release_date || "");
let notes = $state(config.notes || "");
function familiesForArch(arch) { return families[arch] || []; }
function saveClassification() {
const mName = currentModelName.trim() || modelInfo.filename;
const ver = currentVersion.trim();
const composed = ver ? `${mName} - ${ver}` : mName;
config = {
...config,
architecture: currentArch,
family: currentFamily || undefined,
model_name: mName,
version: ver || undefined,
display_name: composed,
};
if (!config.nodes) config.nodes = {};
doSave(config, true);
}
async function doSave(cfg, isClassification = false) {
saveStatus = "Saving...";
try {
await fetch(`/promptchain/models/settings/${modelInfo.hash}`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(cfg),
});
saveStatus = isClassification ? "" : "Saved";
onConfigUpdate?.(cfg);
if (isClassification) {
const composed = cfg.display_name || cfg.model_name || modelInfo.filename;
window.dispatchEvent(new CustomEvent("pcr-model-renamed", { detail: { hash: modelInfo.hash, name: composed } }));
}
if (!isClassification) { saveTimer = setTimeout(() => { saveStatus = ""; }, 1000); }
} catch {
saveStatus = "Error";
}
}
async function handleSaveInfo() {
const cfg = { ...config };
cfg.author = author.trim() || undefined;
cfg.description = description.trim() || undefined;
cfg.license = license.trim() || undefined;
const tagsVal = tags.trim();
cfg.tags = tagsVal ? tagsVal.split(",").map(t => t.trim()).filter(Boolean) : undefined;
cfg.url = url.trim() || undefined;
cfg.download_url = downloadUrl.trim() || undefined;
const cid = civitaiId.toString().trim();
cfg.civitai_model_id = cid ? parseInt(cid, 10) || undefined : undefined;
cfg.trigger = trigger.trim() || undefined;
cfg.negative = negative.trim() || undefined;
cfg.quality_position = qualityPosition.trim() || undefined;
cfg.release_date = releaseDate.trim() || undefined;
cfg.notes = notes.trim() || undefined;
if (!cfg.nodes) cfg.nodes = {};
config = cfg;
doSave(cfg);
}
let archOptions = $derived.by(() => {
const opts = [...architectures];
if (currentArch && !architectures.some(a => a.id === currentArch)) {
opts.push({ id: currentArch, label: currentArch });
}
return opts;
});
</script>
<div class="pcr-model-panel-body">
<div class="pcr-info-form">
<!-- Classification -->
<div class="pcr-info-classification">
<div class="pcr-model-panel-field-row">
<ClickToEditField label="Architecture" value={currentArch} type="select"
options={archOptions}
onChange={(val) => { currentArch = val; currentFamily = ""; saveClassification(); }} />
<ClickToEditField label="Family" value={currentFamily} type="select"
options={familiesForArch(currentArch)}
onChange={(val) => { currentFamily = val; saveClassification(); }} />
</div>
<div class="pcr-model-panel-field-row">
<ClickToEditField label="Model" value={currentModelName} type="text"
onChange={(val) => { currentModelName = val; saveClassification(); }} />
<ClickToEditField label="Version" value={currentVersion} type="text"
onChange={(val) => { currentVersion = val; saveClassification(); }} />
</div>
</div>
<!-- Info fields -->
<div class="pcr-info-field">
<div class="pcr-info-field-label">Author</div>
<input type="text" bind:value={author} />
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Description</div>
<textarea bind:value={description}></textarea>
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">License</div>
<input type="text" bind:value={license} />
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Tags</div>
<input type="text" bind:value={tags} />
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Model URL</div>
<input type="text" bind:value={url} />
{#if url}
<a href={url} target="_blank" rel="noopener"
style="font-size:11px;color:#4fc3f7;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;margin-top:2px">
{url}
</a>
{/if}
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Download URL</div>
<input type="text" bind:value={downloadUrl} />
{#if downloadUrl}
<a href={downloadUrl} target="_blank" rel="noopener"
style="font-size:11px;color:#4fc3f7;text-decoration:none;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;display:block;margin-top:2px">
{downloadUrl}
</a>
{/if}
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">CivitAI Model ID</div>
<input type="text" bind:value={civitaiId} />
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Trigger Words</div>
<textarea bind:value={trigger}></textarea>
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Default Negative</div>
<textarea bind:value={negative}></textarea>
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Quality Tag Position</div>
<input type="text" bind:value={qualityPosition} />
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Release Date</div>
<input type="text" bind:value={releaseDate} />
</div>
<div class="pcr-info-field">
<div class="pcr-info-field-label">Notes</div>
<textarea bind:value={notes}></textarea>
</div>
<!-- Hash (read-only) -->
<div class="pcr-info-field">
<div class="pcr-info-field-label">Fingerprint</div>
<div class="pcr-info-field-readonly">{modelInfo.hash}</div>
</div>
<button class="pcr-model-panel-save" onclick={handleSaveInfo}
disabled={saveStatus === "Saving..."}>
{saveStatus || "Save Info"}
</button>
</div>
</div>
<style>
/* classification section */
.pcr-info-classification { padding-bottom: 8px; margin-bottom: 8px; border-bottom: 1px solid #333; }
.pcr-model-panel-field-row { display: flex; gap: 16px; }
/* info tab form */
.pcr-info-form { display: flex; flex-direction: column; gap: 10px; }
.pcr-info-field { display: flex; flex-direction: column; gap: 3px; }
.pcr-info-field-label { font-size: 11px; color: #666; text-transform: uppercase; letter-spacing: 0.5px; }
.pcr-info-field input,
.pcr-info-field textarea {
font-size: 12px;
padding: 5px 8px;
background: rgba(255, 255, 255, 0.06);
border: 1px solid #444;
border-radius: 4px;
color: #ddd;
outline: none;
}
.pcr-info-field input:focus,
.pcr-info-field textarea:focus { border-color: #4fc3f7; }
.pcr-info-field textarea { resize: vertical; min-height: 60px; font-family: inherit; }
.pcr-info-field-readonly {
font-family: monospace;
font-size: 11px;
color: #555;
user-select: all;
padding: 5px 8px;
background: rgba(0, 0, 0, 0.2);
border-radius: 4px;
}
</style>