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:
parent
ea972f1461
commit
c4b3e36ed1
2 changed files with 49 additions and 9 deletions
|
|
@ -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>
|
||||
);
|
||||
|
||||
|
|
|
|||
|
|
@ -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>,
|
||||
);
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue