- StudioGenerate 拆为 generate/ 下的 Composer、WorkflowPicker、SlotTray、ParamChip、 TakeFeed/TakeCard、Inspector、ParamsPanel、ContextBar,逻辑收进 schema/takes/context - 输入槽只按工作流 JSON 实际接线渲染;常用参数芯片 + 高级参数折叠,显示原值与已改标记 - 工作台素材/镜头卡新增「在生成台调参」,带项目上下文进入;结果不回填(后端无目标) - 共享样式(a-chip/a-seg/a-slot/a-tile 等)进 app.css;ShellPop 支持向上弹出 - /image、/video 仍是同一生成台,按用途预选默认工作流与筛选 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
199 lines
4.8 KiB
Svelte
199 lines
4.8 KiB
Svelte
<script lang="ts">
|
|
import ComfyDynamicForm from '$lib/components/ComfyDynamicForm.svelte';
|
|
import Icon from '$lib/components/ui/Icon.svelte';
|
|
import type { ComfyDynamicInput, Workflow } from '$lib/comfy/types';
|
|
import { t } from '$lib/i18n.svelte';
|
|
import type { DeskSchema, FormValue, FormValues } from './schema';
|
|
|
|
/**
|
|
* 参数 (spec-v2 §3.3 检视区 · 参数): every non-media input the workflow
|
|
* declares, each with its 原值 and a gold edge once changed. Common ones
|
|
* first; the rest fold under 高级参数 — untouched, they keep the
|
|
* workflow's own values.
|
|
*/
|
|
interface Props {
|
|
workflow: Workflow | null;
|
|
ds: DeskSchema;
|
|
values: FormValues;
|
|
errors: Record<string, string>;
|
|
showErrors: boolean;
|
|
changedCount: number;
|
|
onSet: (nodeId: string, value: FormValue) => void;
|
|
onRestoreAll: () => void;
|
|
}
|
|
|
|
let { workflow, ds, values, errors, showErrors, changedCount, onSet, onRestoreAll }: Props = $props();
|
|
|
|
let advancedOpen = $state(false);
|
|
const brokenReasons = $derived(
|
|
(workflow?.warnings ?? []).filter((w) => w.level === 'error').map((w) => w.message),
|
|
);
|
|
const advancedErr = $derived(showErrors && ds.advanced.some((i) => !!errors[i.nodeId]?.trim()));
|
|
|
|
$effect(() => {
|
|
if (advancedErr) advancedOpen = true;
|
|
});
|
|
|
|
function patch(inputs: ComfyDynamicInput[], next: Record<string, string | number>) {
|
|
for (const inp of inputs) {
|
|
const v = next[inp.nodeId];
|
|
if (v !== undefined && v !== values[inp.nodeId]) onSet(inp.nodeId, v);
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<div class="params">
|
|
{#if !workflow}
|
|
<p class="note">{t('desk.pickWorkflow')}</p>
|
|
{:else}
|
|
{#if brokenReasons.length}
|
|
<div class="note err" role="alert">
|
|
<strong>{t('studio.wfBrokenTitle')}</strong>
|
|
<ul>
|
|
{#each brokenReasons as r (r)}<li>{r}</li>{/each}
|
|
</ul>
|
|
<span>{t('studio.wfBrokenHint')}</span>
|
|
</div>
|
|
{/if}
|
|
{#if !(workflow.input_schema ?? []).length}
|
|
<p class="note">{t('studio.wfNoParamsNote')}</p>
|
|
{:else if (workflow.input_schema ?? []).some((i) => i.auto)}
|
|
<p class="note">{t('studio.wfAutoParamsNote', { n: workflow.input_schema.length })}</p>
|
|
{/if}
|
|
|
|
{#if ds.common.length}
|
|
<div class="section-title">{t('desk.paramsCommon')}</div>
|
|
<ComfyDynamicForm
|
|
inputs={ds.common}
|
|
values={values as Record<string, string | number>}
|
|
quiet
|
|
showOrig
|
|
{errors}
|
|
{showErrors}
|
|
onChange={(v) => patch(ds.common, v)}
|
|
/>
|
|
{/if}
|
|
|
|
{#if ds.advanced.length}
|
|
<div class="fold">
|
|
<button
|
|
type="button"
|
|
class="fold-head"
|
|
class:err={advancedErr}
|
|
aria-expanded={advancedOpen}
|
|
aria-controls="desk-advanced"
|
|
onclick={() => (advancedOpen = !advancedOpen)}
|
|
>
|
|
<span class="chev" aria-hidden="true"><Icon name="play" size={10} /></span>
|
|
{t('desk.advanced', { n: ds.advanced.length })}
|
|
</button>
|
|
<div id="desk-advanced" class="collapse" class:open={advancedOpen} inert={!advancedOpen}>
|
|
<div class="collapse-in">
|
|
<ComfyDynamicForm
|
|
inputs={ds.advanced}
|
|
values={values as Record<string, string | number>}
|
|
quiet
|
|
showOrig
|
|
{errors}
|
|
{showErrors}
|
|
onChange={(v) => patch(ds.advanced, v)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{/if}
|
|
|
|
{#if changedCount > 0}
|
|
<div class="restore">
|
|
<button type="button" class="a-btn ghost sm" onclick={onRestoreAll}>
|
|
<Icon name="retry" size={13} />
|
|
{t('desk.restoreAll')}
|
|
</button>
|
|
</div>
|
|
{/if}
|
|
{/if}
|
|
</div>
|
|
|
|
<style>
|
|
.params {
|
|
display: grid;
|
|
gap: var(--sp-4);
|
|
}
|
|
.note {
|
|
margin: 0;
|
|
padding: var(--sp-3) var(--sp-4);
|
|
border-radius: var(--r-md);
|
|
background: var(--bg-3);
|
|
color: var(--text-3);
|
|
font-size: var(--fs-aux);
|
|
}
|
|
.note.err {
|
|
background: var(--err-soft);
|
|
color: var(--err);
|
|
}
|
|
.note ul {
|
|
margin: var(--sp-2) 0;
|
|
padding-left: 18px;
|
|
}
|
|
.section-title {
|
|
font-size: var(--fs-aux);
|
|
color: var(--text-3);
|
|
}
|
|
.fold {
|
|
border-top: 1px solid var(--line);
|
|
}
|
|
.fold-head {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: var(--sp-2);
|
|
width: 100%;
|
|
padding: var(--sp-4) 0;
|
|
border: 0;
|
|
background: none;
|
|
color: var(--text-2);
|
|
font: inherit;
|
|
font-size: var(--fs-aux);
|
|
text-align: left;
|
|
cursor: pointer;
|
|
transition: color var(--d-state) ease;
|
|
}
|
|
.fold-head.err {
|
|
color: var(--err);
|
|
}
|
|
.chev {
|
|
display: inline-grid;
|
|
transition: transform var(--d-pop) var(--ease-out);
|
|
}
|
|
.fold-head[aria-expanded='true'] .chev {
|
|
transform: rotate(90deg);
|
|
}
|
|
@media (hover: hover) and (pointer: fine) {
|
|
.fold-head:hover {
|
|
color: var(--text);
|
|
}
|
|
}
|
|
.collapse {
|
|
display: grid;
|
|
grid-template-rows: 0fr;
|
|
opacity: 0;
|
|
transition:
|
|
grid-template-rows var(--d-enter) var(--ease-out),
|
|
opacity var(--d-state) ease;
|
|
}
|
|
.collapse.open {
|
|
grid-template-rows: 1fr;
|
|
opacity: 1;
|
|
}
|
|
.collapse-in {
|
|
min-height: 0;
|
|
overflow: hidden;
|
|
}
|
|
.collapse.open .collapse-in {
|
|
padding-bottom: var(--sp-2);
|
|
}
|
|
.restore {
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
</style>
|