diff --git a/src/components/StudioApp.tsx b/src/components/StudioApp.tsx index 57cbb4a..2c73103 100644 --- a/src/components/StudioApp.tsx +++ b/src/components/StudioApp.tsx @@ -31,11 +31,19 @@ const TASKS = [ { id: 'contour', name: 'Kontur', hint: 'Weißen Stickerrand anlegen (nur mit Freistellen).' }, ]; +type Mode = 'each' | 'compose' | 'generate'; +const MODES: { id: Mode; name: string; hint: string }[] = [ + { id: 'each', name: 'Bearbeiten', hint: 'Screenshots bereinigen, freistellen, aufs Format bringen.' }, + { id: 'compose', name: 'Kombinieren', hint: 'Mehrere Bilder + Beschreibung zu einem neuen Bild.' }, + { id: 'generate', name: 'Neu erzeugen', hint: 'Ein komplett neues Bild allein aus Text.' }, +]; + const px = (cm: number, dpi = 300) => Math.round((cm / 2.54) * dpi); interface Pic { id: string; src: string; name: string; source_path?: string; error?: string; uploading?: boolean } export default function StudioApp({ recipes }: { recipes: any[] }) { + const [mode, setMode] = useState('each'); const [pics, setPics] = useState([]); const [tasks, setTasks] = useState(['clean', 'format']); const [format, setFormat] = useState('30x40'); @@ -43,6 +51,7 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { const [crop, setCrop] = useState<'crop' | 'extend'>('crop'); const [contourMm, setContourMm] = useState(3); const [custom, setCustom] = useState(''); + const [desc, setDesc] = useState(''); const [delivery, setDelivery] = useState<'library' | 'picdrop' | 'both'>('library'); const [models, setModels] = useState([]); const [modelKey, setModelKey] = useState(''); @@ -55,6 +64,9 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { const hasCutout = tasks.includes('cutout'); const hasFormat = tasks.includes('format'); const theframeConflict = hasCutout && (format === 'theframe' || format === 'hochformat'); + // Format ist in compose/generate immer verfügbar (ohne „Format"-Aufgabe). + const showFormat = mode === 'each' ? hasFormat : true; + const wantsFormat = mode === 'each' ? hasFormat : format !== 'keep'; const zielPx: [number, number] | null = fmt?.screen ? (fmt.screen as [number, number]) @@ -87,12 +99,13 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { useEffect(() => { const onPaste = (e: ClipboardEvent) => { + if (mode === 'generate') return; const imgs = Array.from(e.clipboardData?.items || []).filter((i) => i.type.startsWith('image/')); if (imgs.length) upload(imgs.map((i) => i.getAsFile()!).filter(Boolean)); }; window.addEventListener('paste', onPaste); return () => window.removeEventListener('paste', onPaste); - }, [upload]); + }, [upload, mode]); useEffect(() => { fetch('/api/models').then((r) => r.json()).then((j) => { @@ -105,8 +118,7 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { function toggleTask(id: string) { setTasks((t) => { let next = t.includes(id) ? t.filter((x) => x !== id) : [...t, id]; - if (id === 'cutout' && !next.includes('cutout')) next = next.filter((x) => x !== 'contour'); // Kontur braucht Freistellen - if (id === 'contour' && next.includes('contour') && !next.includes('cutout')) next = next; // wird per disabled verhindert + if (id === 'cutout' && !next.includes('cutout')) next = next.filter((x) => x !== 'contour'); return next; }); } @@ -124,24 +136,34 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { } const ready = pics.filter((p) => p.source_path && !p.error); - const canRun = ready.length > 0 && !busy && tasks.length > 0 && !theframeConflict && - !(tasks.includes('contour') && !hasCutout); + const canRun = !busy && ( + mode === 'each' ? (ready.length > 0 && tasks.length > 0 && !theframeConflict && !(tasks.includes('contour') && !hasCutout)) + : mode === 'compose' ? (ready.length >= 2 && desc.trim().length > 0) + : desc.trim().length > 0 + ); async function run() { if (!canRun) return; setBusy(true); + const order = ['clean', 'cutout', 'format', 'contour', 'deliver']; + const tasksOut = mode === 'each' + ? [...tasks].sort((a, b) => order.indexOf(a) - order.indexOf(b)) + : (wantsFormat ? ['format'] : []); const recipe = { - tasks: [...tasks].sort((a, b) => ['clean', 'cutout', 'format', 'contour', 'deliver'].indexOf(a) - ['clean', 'cutout', 'format', 'contour', 'deliver'].indexOf(b)), - output_format: hasFormat ? format : 'keep', + tasks: tasksOut, + output_format: wantsFormat ? format : 'keep', orientation: portrait ? 'portrait' : 'landscape', crop_mode: crop, dpi: 300, contour_mm: tasks.includes('contour') ? contourMm : null, - custom_instruction: custom || null, delivery, model_key: modelKey || null, + custom_instruction: mode === 'each' ? (custom || null) : null, + delivery, model_key: modelKey || null, }; + const body: any = { recipe, mode, delivery }; + if (mode !== 'each') body.prompt_text = desc.trim(); + body.sources = mode === 'generate' ? [] : ready.map((p) => ({ source_path: p.source_path, filename: p.name })); try { const res = await fetch('/api/jobs', { - method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ recipe, sources: ready.map((p) => ({ source_path: p.source_path, filename: p.name })) }), + method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(body), }); const j = await res.json(); if (j.jobId) location.href = `/warteschlange?job=${j.jobId}`; @@ -149,136 +171,175 @@ export default function StudioApp({ recipes }: { recipes: any[] }) { } catch { notify('Netzwerkfehler.'); setBusy(false); } } + const uploadHint = mode === 'compose' + ? 'Mindestens 2 Bilder — z. B. das Motiv und ein Foto von Frieda.' + : 'Mehrere gleichzeitig, bis zu 100. PNG, JPG, WEBP, HEIC.'; + return (
-
-
- Vorlagen{pics.length ? ` · ${pics.length}` : ''} - {pics.length > 0 && } -
-
- {pics.length === 0 ? ( -
fileRef.current?.click()} - onDragOver={(e) => { e.preventDefault(); setDragging(true); }} - onDragLeave={() => setDragging(false)} - onDrop={(e) => { e.preventDefault(); setDragging(false); upload(e.dataTransfer.files); }}> - -

Bilder hierher ziehen,
mit ⌘/Strg + V einfügen
oder zum Auswählen tippen.

-

Mehrere gleichzeitig, bis zu 100. PNG, JPG, WEBP, HEIC.

-
- ) : ( -
- {pics.map((b) => ( -
- {b.src && } - {b.uploading && } - {b.error && !} - -
- ))} - -
- )} - e.target.files && upload(e.target.files)} /> -
-
- -
-
- {recipes?.length > 0 && ( -
- - -
- )} - -
- -
- {TASKS.map((m) => { - const on = tasks.includes(m.id); - const disabled = m.id === 'contour' && !hasCutout; - return ( - - ); - })} -
-
- - {hasFormat && ( -
- - - {theframeConflict &&
Freistellen + The Frame ergibt keinen Sinn — bitte eins wählen.
} - {fmt?.cm && !fmt.screen && ( -
- - -
- )} - {zielPx &&
Ergibt exakt {zielPx[0]} × {zielPx[1]} Pixel bei 300 dpi — druckfertig.
} - {zielPx && ( -
- - -
- )} -
- )} - - {tasks.includes('contour') && ( -
- - setContourMm(Number(e.target.value))} /> -
- )} - - {models.length > 0 && ( -
- -
- {models.map((m) => ( - - ))} -
-
{models.find((m) => m.model_id === modelKey)?.description || ''}
-
- )} - -
- -