feat(studio): Add workspace management feature with new types for workspace, project, and session and update root App component for navigation integration

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-29 23:16:20 -07:00
parent f8b7c0cdd8
commit bd40aed30f
2 changed files with 25 additions and 17 deletions

View file

@ -97,7 +97,7 @@ const DEFAULT_ADVANCED: AdvancedValues = {
ip_adapter_scale: 0.6,
num_candidates: 1,
enable_anatomy_fix: false,
negative_prompt: undefined,
negative_prompt: 'worst quality, low quality, blurry, deformed, bad anatomy, extra limbs, fused legs, bad hands, extra fingers, missing fingers, watermark, text, ugly, disfigured',
seed: undefined,
enable_background_removal: false,
};
@ -161,17 +161,21 @@ export function App(): ReactElement {
setImages((prev) => [...prev, img]);
}
const generateMutation = useGenerate(handleImageReady);
const { isPending: isGenerating, attempt, totalAttempts, lastScore, exhausted, isError, error, generate } = useGenerate(handleImageReady);
function buildRequest(): StudioRequest {
const prompt = buildPrompt(scene);
const hasPoseOrOutfit = !!scene.selectedPose || !!scene.outfitDescription.trim();
// Only pose types with preset skeletons can drive ControlNet
const PRESET_POSE_TYPES = new Set(['standing', 'sitting', 'walking', 'running']);
const rawPoseType = scene.selectedPose?.poseType;
const poseType: PersonAppearance['pose_type'] =
rawPoseType === 'lying' || rawPoseType === 'kneeling' || rawPoseType === 'leaning'
? 'custom'
: (rawPoseType as PersonAppearance['pose_type']);
rawPoseType && PRESET_POSE_TYPES.has(rawPoseType)
? (rawPoseType as PersonAppearance['pose_type'])
: undefined;
// Send person_appearance only when there's actual ControlNet conditioning to apply
const hasPoseOrOutfit = !!poseType || !!scene.outfitDescription.trim();
return {
...advancedValues,
@ -189,15 +193,13 @@ export function App(): ReactElement {
}
function handleGenerate(): void {
generateMutation.mutate(buildRequest());
generate(buildRequest());
}
function handleAdvancedChange(patch: Partial<typeof advancedValues>): void {
setAdvancedValues((prev) => ({ ...prev, ...patch }));
}
const isGenerating = generateMutation.isPending;
const sidebar = (
<SidebarStack>
<IdentityPanel
@ -213,6 +215,10 @@ export function App(): ReactElement {
</SidebarStack>
);
const attemptLabel = attempt !== null
? `Attempt ${attempt}/${totalAttempts}${lastScore !== null ? ` · score ${lastScore.toFixed(2)}` : '…'}`
: 'Starting…';
const generateBar = (
<GenerateRow>
<GenerateBtn
@ -220,15 +226,17 @@ export function App(): ReactElement {
disabled={isGenerating}
onClick={handleGenerate}
>
{isGenerating ? 'Generating…' : 'Generate'}
{isGenerating ? attemptLabel : 'Generate'}
</GenerateBtn>
{generateMutation.isError && (
{isError && error && (
<GenerateStatus style={{ color: theme.colors.error }}>
{generateMutation.error.message}
{error.message}
</GenerateStatus>
)}
{isGenerating && (
<GenerateStatus>Running pipeline</GenerateStatus>
{!isGenerating && exhausted && (
<GenerateStatus style={{ color: theme.colors.textMuted }}>
All {totalAttempts} attempts used · score {lastScore?.toFixed(2) ?? '—'}
</GenerateStatus>
)}
</GenerateRow>
);
@ -236,7 +244,7 @@ export function App(): ReactElement {
return (
<StudioLayout
sidebar={sidebar}
main={<SceneBuilder scene={scene} onChange={setScene} />}
main={<SceneBuilder scene={scene} maturityRating={advancedValues.maturity_rating} onChange={setScene} />}
generateBar={generateBar}
gallery={<ResultsGallery images={images} />}
imageCount={images.length}

View file

@ -3,10 +3,10 @@ import type { PoseCategory, PoseDefinition } from '@lilith/imajin-config';
// ─── Identity ────────────────────────────────────────────────────────────────
export interface Identity {
id: string;
name: string;
photo_count: number;
image_count: number;
created_at: string;
embedding_quality?: number;
}
export interface CreateIdentityPayload {