feat: add commands for clearing candidates and reusing candidate settings, update UI controls
This commit is contained in:
@@ -5,6 +5,8 @@ import {
|
|||||||
generationAddCandidateCommand,
|
generationAddCandidateCommand,
|
||||||
generationApplyCandidateAsLayerCommand,
|
generationApplyCandidateAsLayerCommand,
|
||||||
generationRemoveCandidateCommand,
|
generationRemoveCandidateCommand,
|
||||||
|
generationClearCandidatesCommand,
|
||||||
|
generationReuseCandidateSettingsCommand,
|
||||||
generationReplaceCandidatePixelsCommand,
|
generationReplaceCandidatePixelsCommand,
|
||||||
generationSetCompareModeCommand,
|
generationSetCompareModeCommand,
|
||||||
generationFailJobCommand,
|
generationFailJobCommand,
|
||||||
@@ -38,6 +40,38 @@ describe("generation commands", () => {
|
|||||||
expect(next.editor.generation.compareMode).toBe("split");
|
expect(next.editor.generation.compareMode).toBe("split");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("clears the candidate session and resets comparison", () => {
|
||||||
|
const withCandidate = generationAddCandidateCommand.execute(
|
||||||
|
{ state: createInitialAppState("Test") },
|
||||||
|
{ candidate: generationCandidate("candidate-1") },
|
||||||
|
);
|
||||||
|
const comparing = generationSetCompareModeCommand.execute({ state: withCandidate }, { mode: "split" });
|
||||||
|
|
||||||
|
const next = generationClearCandidatesCommand.execute({ state: comparing }, undefined);
|
||||||
|
|
||||||
|
expect(next.editor.generation.candidates).toEqual([]);
|
||||||
|
expect(next.editor.generation.selectedCandidateId).toBeUndefined();
|
||||||
|
expect(next.editor.generation.compareMode).toBe("result");
|
||||||
|
});
|
||||||
|
|
||||||
|
test("reuses a candidate's full settings with its resolved seed", () => {
|
||||||
|
const candidate = generationCandidate("candidate-1");
|
||||||
|
candidate.settings = {
|
||||||
|
...candidate.settings,
|
||||||
|
prompt: "Stored prompt",
|
||||||
|
model: "stored-model.safetensors",
|
||||||
|
seed: -1,
|
||||||
|
inpaint: { ...candidate.settings.inpaint, maskBlur: 7 },
|
||||||
|
};
|
||||||
|
candidate.seed = 987654;
|
||||||
|
const state = generationAddCandidateCommand.execute({ state: createInitialAppState("Test") }, { candidate });
|
||||||
|
|
||||||
|
const next = generationReuseCandidateSettingsCommand.execute({ state }, { candidateId: candidate.id });
|
||||||
|
|
||||||
|
expect(next.editor.tools.generate).toEqual({ ...candidate.settings, seed: 987654 });
|
||||||
|
expect(next.editor.generation).toBe(state.editor.generation);
|
||||||
|
});
|
||||||
|
|
||||||
test("applies candidates as top-level layers", () => {
|
test("applies candidates as top-level layers", () => {
|
||||||
const withRemainingCandidate = generationAddCandidateCommand.execute(
|
const withRemainingCandidate = generationAddCandidateCommand.execute(
|
||||||
{ state: documentWithSourceLayer() },
|
{ state: documentWithSourceLayer() },
|
||||||
|
|||||||
@@ -24,6 +24,10 @@ export type GenerationRemoveCandidatePayload = {
|
|||||||
candidateId: GenerationCandidateId;
|
candidateId: GenerationCandidateId;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export type GenerationReuseCandidateSettingsPayload = {
|
||||||
|
candidateId: GenerationCandidateId;
|
||||||
|
};
|
||||||
|
|
||||||
export type GenerationApplyCandidateAsLayerPayload = {
|
export type GenerationApplyCandidateAsLayerPayload = {
|
||||||
candidateId: GenerationCandidateId;
|
candidateId: GenerationCandidateId;
|
||||||
assetId: AssetId;
|
assetId: AssetId;
|
||||||
@@ -140,6 +144,30 @@ export const generationClearCandidatesCommand: Command = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const generationReuseCandidateSettingsCommand: Command<GenerationReuseCandidateSettingsPayload> = {
|
||||||
|
id: commandIds.generationReuseCandidateSettings,
|
||||||
|
name: "Reuse generation candidate settings",
|
||||||
|
execute({ state }, payload) {
|
||||||
|
const candidate = state.editor.generation.candidates.find((item) => item.id === payload.candidateId);
|
||||||
|
if (!candidate) return state;
|
||||||
|
return {
|
||||||
|
...state,
|
||||||
|
editor: {
|
||||||
|
...state.editor,
|
||||||
|
tools: {
|
||||||
|
...state.editor.tools,
|
||||||
|
generate: {
|
||||||
|
...candidate.settings,
|
||||||
|
seed: candidate.seed,
|
||||||
|
outpaint: { ...candidate.settings.outpaint },
|
||||||
|
inpaint: { ...candidate.settings.inpaint },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
export const generationApplyCandidateAsLayerCommand: Command<GenerationApplyCandidateAsLayerPayload> = {
|
export const generationApplyCandidateAsLayerCommand: Command<GenerationApplyCandidateAsLayerPayload> = {
|
||||||
id: commandIds.generationApplyCandidateAsLayer,
|
id: commandIds.generationApplyCandidateAsLayer,
|
||||||
name: "Apply generation candidate as layer",
|
name: "Apply generation candidate as layer",
|
||||||
@@ -284,6 +312,7 @@ export const generationCommands = [
|
|||||||
generationSetCompareModeCommand,
|
generationSetCompareModeCommand,
|
||||||
generationRemoveCandidateCommand,
|
generationRemoveCandidateCommand,
|
||||||
generationClearCandidatesCommand,
|
generationClearCandidatesCommand,
|
||||||
|
generationReuseCandidateSettingsCommand,
|
||||||
generationApplyCandidateAsLayerCommand,
|
generationApplyCandidateAsLayerCommand,
|
||||||
generationReplaceCandidatePixelsCommand,
|
generationReplaceCandidatePixelsCommand,
|
||||||
generationStartJobCommand,
|
generationStartJobCommand,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export const commandIds = {
|
|||||||
generationSetCompareMode: "generation.setCompareMode",
|
generationSetCompareMode: "generation.setCompareMode",
|
||||||
generationRemoveCandidate: "generation.removeCandidate",
|
generationRemoveCandidate: "generation.removeCandidate",
|
||||||
generationClearCandidates: "generation.clearCandidates",
|
generationClearCandidates: "generation.clearCandidates",
|
||||||
|
generationReuseCandidateSettings: "generation.reuseCandidateSettings",
|
||||||
generationApplyCandidateAsLayer: "generation.applyCandidateAsLayer",
|
generationApplyCandidateAsLayer: "generation.applyCandidateAsLayer",
|
||||||
generationReplaceCandidatePixels: "generation.replaceCandidatePixels",
|
generationReplaceCandidatePixels: "generation.replaceCandidatePixels",
|
||||||
generationStartJob: "generation.startJob",
|
generationStartJob: "generation.startJob",
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import type {
|
|||||||
GenerationAddCandidatePayload,
|
GenerationAddCandidatePayload,
|
||||||
GenerationApplyCandidateAsLayerPayload,
|
GenerationApplyCandidateAsLayerPayload,
|
||||||
GenerationRemoveCandidatePayload,
|
GenerationRemoveCandidatePayload,
|
||||||
|
GenerationReuseCandidateSettingsPayload,
|
||||||
GenerationReplaceCandidatePixelsPayload,
|
GenerationReplaceCandidatePixelsPayload,
|
||||||
GenerationSelectCandidatePayload,
|
GenerationSelectCandidatePayload,
|
||||||
GenerationSetCompareModePayload,
|
GenerationSetCompareModePayload,
|
||||||
@@ -99,6 +100,7 @@ export type CommandPayloads = {
|
|||||||
[commandIds.generationSetCompareMode]: GenerationSetCompareModePayload;
|
[commandIds.generationSetCompareMode]: GenerationSetCompareModePayload;
|
||||||
[commandIds.generationRemoveCandidate]: GenerationRemoveCandidatePayload;
|
[commandIds.generationRemoveCandidate]: GenerationRemoveCandidatePayload;
|
||||||
[commandIds.generationClearCandidates]: void;
|
[commandIds.generationClearCandidates]: void;
|
||||||
|
[commandIds.generationReuseCandidateSettings]: GenerationReuseCandidateSettingsPayload;
|
||||||
[commandIds.generationApplyCandidateAsLayer]: GenerationApplyCandidateAsLayerPayload;
|
[commandIds.generationApplyCandidateAsLayer]: GenerationApplyCandidateAsLayerPayload;
|
||||||
[commandIds.generationReplaceCandidatePixels]: GenerationReplaceCandidatePixelsPayload;
|
[commandIds.generationReplaceCandidatePixels]: GenerationReplaceCandidatePixelsPayload;
|
||||||
[commandIds.generationStartJob]: GenerationStartJobPayload;
|
[commandIds.generationStartJob]: GenerationStartJobPayload;
|
||||||
|
|||||||
@@ -53,7 +53,6 @@ export function GenerateActionControls({ settings, generation, dispatch, workflo
|
|||||||
}
|
}
|
||||||
|
|
||||||
function CandidatePicker({ generation, dispatch }: { generation: GenerationState; dispatch: AppStore["dispatch"] }) {
|
function CandidatePicker({ generation, dispatch }: { generation: GenerationState; dispatch: AppStore["dispatch"] }) {
|
||||||
if (generation.candidates.length < 2) return null;
|
|
||||||
return (
|
return (
|
||||||
<div className="subtle-scrollbar flex max-w-[min(28rem,calc(100vw-2rem))] items-center gap-1 overflow-x-auto rounded-full bg-white/[0.04] px-1.5 py-1 ring-1 ring-white/[0.05]" role="group" aria-label="Generation candidates">
|
<div className="subtle-scrollbar flex max-w-[min(28rem,calc(100vw-2rem))] items-center gap-1 overflow-x-auto rounded-full bg-white/[0.04] px-1.5 py-1 ring-1 ring-white/[0.05]" role="group" aria-label="Generation candidates">
|
||||||
{generation.candidates.map((candidate) => {
|
{generation.candidates.map((candidate) => {
|
||||||
@@ -70,6 +69,15 @@ function CandidatePicker({ generation, dispatch }: { generation: GenerationState
|
|||||||
</button>
|
</button>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
disabled={generation.candidates.length === 0}
|
||||||
|
className="h-9 shrink-0 rounded-full px-3 text-xs font-semibold text-white/55 transition hover:bg-white/10 hover:text-white disabled:pointer-events-none disabled:opacity-35"
|
||||||
|
title="Dismiss every candidate and reset comparison"
|
||||||
|
onClick={() => dispatch(commandIds.generationClearCandidates, undefined)}
|
||||||
|
>
|
||||||
|
Clear all
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -97,7 +105,9 @@ function CandidateControls({
|
|||||||
return (
|
return (
|
||||||
<div className="flex flex-wrap items-center justify-center gap-1 rounded-full bg-white/[0.04] px-2 py-1 ring-1 ring-white/[0.05]">
|
<div className="flex flex-wrap items-center justify-center gap-1 rounded-full bg-white/[0.04] px-2 py-1 ring-1 ring-white/[0.05]">
|
||||||
<CandidatePreview candidate={candidate} />
|
<CandidatePreview candidate={candidate} />
|
||||||
<span className="px-2 text-xs font-medium text-white/55">Seed {candidate.seed}</span>
|
<span className="px-2 text-xs font-medium text-white/55" title={`${candidate.settings.model} · ${candidate.mode}`}>
|
||||||
|
Seed {candidate.seed}
|
||||||
|
</span>
|
||||||
<CandidateCompareControls compareMode={compareMode} disabled={disabled} dispatch={dispatch} />
|
<CandidateCompareControls compareMode={compareMode} disabled={disabled} dispatch={dispatch} />
|
||||||
<CandidateButton disabled={disabled} label="Regenerate" title="Regenerate same mask and crop" onClick={() => rerun("Regenerate", candidate.settings)} />
|
<CandidateButton disabled={disabled} label="Regenerate" title="Regenerate same mask and crop" onClick={() => rerun("Regenerate", candidate.settings)} />
|
||||||
<CandidateButton
|
<CandidateButton
|
||||||
@@ -108,6 +118,12 @@ function CandidateControls({
|
|||||||
/>
|
/>
|
||||||
<CandidateButton disabled={disabled} label="Reuse seed" title="Regenerate with the same seed" onClick={() => rerun("Reuse seed", { ...candidate.settings, seed: candidate.seed })} />
|
<CandidateButton disabled={disabled} label="Reuse seed" title="Regenerate with the same seed" onClick={() => rerun("Reuse seed", { ...candidate.settings, seed: candidate.seed })} />
|
||||||
<CandidateButton disabled={disabled} label="New seed" title="Regenerate with a new seed" onClick={() => rerun("New seed", { ...candidate.settings, seed: -1 })} />
|
<CandidateButton disabled={disabled} label="New seed" title="Regenerate with a new seed" onClick={() => rerun("New seed", { ...candidate.settings, seed: -1 })} />
|
||||||
|
<CandidateButton
|
||||||
|
disabled={disabled}
|
||||||
|
label="Use settings"
|
||||||
|
title="Restore this candidate's prompt, model, generation settings, and resolved seed"
|
||||||
|
onClick={() => dispatch(commandIds.generationReuseCandidateSettings, { candidateId: candidate.id })}
|
||||||
|
/>
|
||||||
<CandidateButton disabled={disabled} label="Add as layer" title="Add candidate to the document as a layer" onClick={() => workflow.applyCandidateAsLayer(candidate.id)} />
|
<CandidateButton disabled={disabled} label="Add as layer" title="Add candidate to the document as a layer" onClick={() => workflow.applyCandidateAsLayer(candidate.id)} />
|
||||||
<CandidateButton
|
<CandidateButton
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
@@ -136,7 +152,7 @@ function CandidateControls({
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<CandidateButton disabled={disabled} label="Dismiss" title="Remove this candidate" onClick={() => dispatch(commandIds.generationRemoveCandidate, { candidateId: candidate.id })} />
|
<CandidateButton disabled={disabled} label="Dismiss" title="Remove this candidate" onClick={() => dispatch(commandIds.generationRemoveCandidate, { candidateId: candidate.id })} />
|
||||||
{settings.seed !== candidate.seed ? null : <span className="sr-only">Current settings reuse this seed</span>}
|
{settings.seed !== candidate.seed ? null : <span className="sr-only">Current settings use this candidate's resolved seed</span>}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user