From c4b3e36ed17fe73ad13d1077747b5fe09bf1b96d Mon Sep 17 00:00:00 2001 From: Claude Code Date: Mon, 30 Mar 2026 09:29:29 -0700 Subject: [PATCH] =?UTF-8?q?arch(studio):=20=F0=9F=8F=97=EF=B8=8F=20Refacto?= =?UTF-8?q?r=20studio=20entry=20files=20to=20reorganize=20root=20component?= =?UTF-8?q?=20and=20application=20initialization=20logic?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Lilith Autocommit --- studio/src/App.tsx | 49 +++++++++++++++++++++++++++++++++++++-------- studio/src/main.tsx | 9 ++++++++- 2 files changed, 49 insertions(+), 9 deletions(-) diff --git a/studio/src/App.tsx b/studio/src/App.tsx index cc4efafc..30208566 100644 --- a/studio/src/App.tsx +++ b/studio/src/App.tsx @@ -1,4 +1,5 @@ -import { ReactElement, useState } from 'react'; +import { ReactElement, useEffect, useState } from 'react'; +import { Link } from 'react-router-dom'; import styled from 'styled-components'; import { AdvancedPanel } from './components/AdvancedPanel'; import { IdentityPanel } from './components/IdentityPanel'; @@ -6,15 +7,23 @@ import { ResultsGallery } from './components/ResultsGallery'; import { SceneBuilder } from './components/SceneBuilder'; import { StudioLayout } from './components/StudioLayout'; import { useGenerate } from './hooks/useGenerate'; +import { useIdentities } from './hooks/useIdentities'; +import { useImageLibrary } from './hooks/useImageLibrary'; import { theme } from './theme'; -import type { GeneratedImage, LayoutId, MaturityRating, ModelId, PersonAppearance, SceneState, StudioRequest } from './types'; +import type { LayoutId, MaturityRating, ModelId, PersonAppearance, SceneState, StudioRequest } from './types'; // ─── Prompt builder ─────────────────────────────────────────────────────────── -function buildPrompt(scene: SceneState): string { +function buildPrompt(scene: SceneState, hasIdentity: boolean): string { const parts: string[] = []; - if (scene.promptCore.trim()) parts.push(scene.promptCore.trim()); + if (scene.promptCore.trim()) { + parts.push(scene.promptCore.trim()); + } else if (hasIdentity) { + // Without a text subject, InstantID ControlNet produces garbage. + // "1woman" anchors the diffusion model to generate a person. + parts.push('1woman'); + } if (scene.outfitDescription.trim()) parts.push(scene.outfitDescription.trim()); if (scene.selectedPose) { @@ -136,6 +145,19 @@ const GenerateStatus = styled.div` white-space: nowrap; `; +const LibraryLink = styled(Link)` + font-size: ${theme.font.size.sm}; + color: ${theme.colors.textMuted}; + text-decoration: none; + white-space: nowrap; + transition: ${theme.transition}; + padding: ${theme.spacing.sm} ${theme.spacing.md}; + border: 1px solid ${theme.colors.border}; + border-radius: ${theme.radius.md}; + + &:hover { color: ${theme.colors.text}; border-color: ${theme.colors.borderHover}; } +`; + // ─── Sidebar ────────────────────────────────────────────────────────────────── const SidebarStack = styled.div` @@ -155,16 +177,24 @@ export function App(): ReactElement { const [selectedIdentityId, setSelectedIdentityId] = useState(undefined); const [scene, setScene] = useState(DEFAULT_SCENE); const [advancedValues, setAdvancedValues] = useState(DEFAULT_ADVANCED); - const [images, setImages] = useState([]); + const { images, add: addImage } = useImageLibrary(); + const { data: identities } = useIdentities(); - function handleImageReady(img: GeneratedImage): void { - setImages((prev) => [...prev, img]); + // Auto-select quinn (or first identity) on initial load if nothing is selected + useEffect(() => { + if (selectedIdentityId !== undefined || !identities || identities.length === 0) return; + const quinn = identities.find((id) => id.id === 'quinn') ?? identities[0]; + setSelectedIdentityId(quinn.id); + }, [identities, selectedIdentityId]); + + function handleImageReady(img: Parameters[0]): void { + void addImage(img); } const { isPending: isGenerating, attempt, totalAttempts, lastScore, exhausted, isError, error, generate } = useGenerate(handleImageReady); function buildRequest(): StudioRequest { - const prompt = buildPrompt(scene); + const prompt = buildPrompt(scene, !!selectedIdentityId); // Only pose types with preset skeletons can drive ControlNet const PRESET_POSE_TYPES = new Set(['standing', 'sitting', 'walking', 'running']); @@ -238,6 +268,9 @@ export function App(): ReactElement { All {totalAttempts} attempts used · score {lastScore?.toFixed(2) ?? '—'} )} + + Library {images.length > 0 ? `(${images.length})` : ''} + ); diff --git a/studio/src/main.tsx b/studio/src/main.tsx index 1de42dc9..7b59874c 100644 --- a/studio/src/main.tsx +++ b/studio/src/main.tsx @@ -1,7 +1,9 @@ import { QueryClient, QueryClientProvider } from '@tanstack/react-query'; import { StrictMode } from 'react'; import { createRoot } from 'react-dom/client'; +import { BrowserRouter, Route, Routes } from 'react-router-dom'; import { App } from './App'; +import { Library } from './pages/Library'; const queryClient = new QueryClient({ defaultOptions: { @@ -18,7 +20,12 @@ if (!rootEl) throw new Error('Root element not found'); createRoot(rootEl).render( - + + + } /> + } /> + + , );