Files
atelier/web/Generate/Generate.tsx
T
pastilhas 4533ef03ec fix(web): make every panel scroll
Each panel's root was a plain div with padding, so anything past the panel's
height was simply not reachable — the gallery showed its first two rows of
sixty images, and the Providers panel cut off mid-card. The shell hands a
panel a fixed-height box and does not scroll it for you; plugins/music has
had `h-full overflow-y-auto` on every panel root since it was written.

Collections gets it in both places, the list and the detail view, which are
two separate returns in one file and would otherwise disagree.
2026-08-20 21:15:39 +01:00

147 lines
4.8 KiB
TypeScript

import type { ReactNode } from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Textarea } from '@/components/ui/textarea';
import { fileUrl } from '../lib/api';
import { BACKGROUNDS, QUALITIES, SIZES, useGenerateManager } from './useGenerateManager';
export const Generate = () => {
const manager = useGenerateManager();
if (manager.providers.length > 0 && manager.usableProviders.length === 0) {
return (
<Card>
<CardContent className="pt-6 text-sm text-muted-foreground">
No provider is ready. Add an API key in the Providers panel first.
</CardContent>
</Card>
);
}
return (
<div className="h-full space-y-6 overflow-y-auto p-4">
<div className="space-y-2">
<Label htmlFor="atelier-prompt">Prompt</Label>
<Textarea
id="atelier-prompt"
rows={4}
placeholder="a voxel duck in a beret, painting at an easel"
value={manager.prompt}
onChange={(ev) => manager.setPrompt(ev.target.value)}
/>
</div>
<div className="grid gap-4 sm:grid-cols-2">
<Field label="Model" id="atelier-model">
<Select value={manager.model} onValueChange={manager.setModel}>
<SelectTrigger id="atelier-model" className="w-full">
<SelectValue placeholder="Choose" />
</SelectTrigger>
<SelectContent>
{manager.models.map((model) => (
<SelectItem key={model} value={model}>
{model}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
<Field label="Count" id="atelier-n">
<Input
id="atelier-n"
type="number"
min={1}
max={10}
value={manager.n}
onChange={(ev) => manager.setN(Math.min(10, Math.max(1, Number(ev.target.value) || 1)))}
/>
</Field>
<Choice label="Size" id="atelier-size" value={manager.size} onChange={manager.setSize} options={SIZES} />
<Choice
label="Quality"
id="atelier-quality"
value={manager.quality}
onChange={manager.setQuality}
options={QUALITIES}
/>
<Choice
label="Background"
id="atelier-background"
value={manager.background}
onChange={manager.setBackground}
options={BACKGROUNDS}
/>
</div>
<div className="flex items-center gap-4">
<Button disabled={!manager.ready} onClick={manager.generate}>
{manager.running ? 'Generating…' : 'Generate'}
</Button>
{manager.job?.atelier && manager.running && (
<span className="text-sm text-muted-foreground">{manager.job.atelier.status}…</span>
)}
</div>
{manager.error && <p className="text-sm text-destructive">{manager.error}</p>}
{manager.job?.data && manager.job.data.length > 0 && (
<div className="grid gap-4 sm:grid-cols-2">
{manager.job.data.map((image) => (
<figure key={image.url} className="space-y-2">
<img src={fileUrl(image.url) ?? undefined} alt={manager.prompt} className="w-full rounded-lg border border-border" />
<figcaption className="truncate text-xs text-muted-foreground">{image.revised_prompt ?? ''}</figcaption>
</figure>
))}
</div>
)}
{manager.job?.atelier?.images.some((image) => image.error) && (
<ul className="space-y-1 text-sm text-destructive">
{manager.job.atelier.images
.filter((image) => image.error)
.map((image) => (
<li key={image.id}>{image.error}</li>
))}
</ul>
)}
</div>
);
};
const Field = ({ label, id, children }: { label: string; id: string; children: ReactNode }) => (
<div className="space-y-2">
<Label htmlFor={id}>{label}</Label>
{children}
</div>
);
type ChoiceProps = {
label: string;
id: string;
value: string;
onChange: (value: string) => void;
options: readonly string[];
};
const Choice = ({ label, id, value, onChange, options }: ChoiceProps) => (
<Field label={label} id={id}>
<Select value={value} onValueChange={onChange}>
<SelectTrigger id={id} className="w-full">
<SelectValue />
</SelectTrigger>
<SelectContent>
{options.map((option) => (
<SelectItem key={option} value={option}>
{option}
</SelectItem>
))}
</SelectContent>
</Select>
</Field>
);