arch(studio): 🏗️ Refactor studio entry files to reorganize root component and application initialization logic

Co-Authored-By: Lilith Autocommit <noreply@atlilith.com>
This commit is contained in:
Claude Code 2026-03-30 09:29:29 -07:00
parent ea972f1461
commit c4b3e36ed1
2 changed files with 49 additions and 9 deletions

View file

@ -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<string | undefined>(undefined);
const [scene, setScene] = useState<SceneState>(DEFAULT_SCENE);
const [advancedValues, setAdvancedValues] = useState(DEFAULT_ADVANCED);
const [images, setImages] = useState<GeneratedImage[]>([]);
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<typeof addImage>[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) ?? '—'}
</GenerateStatus>
)}
<LibraryLink to="/library">
Library {images.length > 0 ? `(${images.length})` : ''}
</LibraryLink>
</GenerateRow>
);

View file

@ -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(
<StrictMode>
<QueryClientProvider client={queryClient}>
<App />
<BrowserRouter>
<Routes>
<Route path="/" element={<App />} />
<Route path="/library" element={<Library />} />
</Routes>
</BrowserRouter>
</QueryClientProvider>
</StrictMode>,
);