diff --git a/studio/frontend/src/features/images/images-page.tsx b/studio/frontend/src/features/images/images-page.tsx index e263a372d4..27fecd1f69 100644 --- a/studio/frontend/src/features/images/images-page.tsx +++ b/studio/frontend/src/features/images/images-page.tsx @@ -11,6 +11,7 @@ import { Delete02Icon, Download01Icon, Edit03Icon, + Image03Icon, ImageAdd02Icon, ImageUpload01Icon, InformationCircleIcon, @@ -22,6 +23,9 @@ import { ZoomInAreaIcon, } from "@hugeicons/core-free-icons"; import { HugeiconsIcon } from "@hugeicons/react"; +// The raw Radix trigger: the app's TooltipTrigger adds a click-to-toggle handler that +// has no business inside a menu row. +import { Tooltip as TooltipPrimitive } from "radix-ui"; import { TestTubeOutlineIcon } from "@/lib/hugeicons-derived"; import { Button } from "@/components/ui/button"; @@ -48,6 +52,7 @@ import { Slider } from "@/components/ui/slider"; import { Spinner } from "@/components/ui/spinner"; import { Switch } from "@/components/ui/switch"; import { Textarea } from "@/components/ui/textarea"; +import { Tooltip, TooltipContent } from "@/components/ui/tooltip"; import { InfoHint } from "@/components/ui/info-hint"; import { ModelSelector } from "@/features/model-picker/components/model-selector"; import { IMAGE_GEN_TASKS } from "@/features/model-picker/components/model-selector/pickers"; @@ -120,42 +125,42 @@ const WORKFLOW_TABS: Array<{ label: "Transform", icon: MagicWand01Icon, requires: "img2img", - hint: "Redraw an uploaded image guided by your prompt (img2img)", + hint: "Redraw an image from your prompt", }, { id: "inpaint", label: "Inpaint", icon: PaintBrush02Icon, requires: "inpaint", - hint: "Paint over a region to regenerate just that area, keeping the rest", + hint: "Regenerate a painted region", }, { id: "extend", label: "Extend", icon: ArrowExpand01Icon, requires: "outpaint", - hint: "Outpaint: grow the canvas and fill the new edges from your prompt", + hint: "Grow the canvas and fill the edges", }, { id: "upscale", label: "Upscale", icon: ZoomInAreaIcon, requires: "upscale", - hint: "Hires fix: enlarge an uploaded image and re-detail it at higher resolution", + hint: "Enlarge and re-detail an image", }, { id: "reference", label: "Reference", icon: ImageUpload01Icon, requires: "reference", - hint: "Generate a new image guided by a reference image + your prompt (FLUX.2)", + hint: "Generate guided by a reference image", }, { id: "edit", label: "Edit", icon: Edit03Icon, requires: "edit", - hint: "Instruction editing: change an image with a prompt (Qwen-Image-Edit)", + hint: "Change an image with an instruction", }, ]; @@ -2312,20 +2317,16 @@ export function ImagesPage({ active = true }: { active?: boolean }) { onDeploy={handleDeployAdapter} /> ) : ( - /* ── Controls rail + preview canvas. Padding mirrors the other tabs - (Export, Data Recipes): px-5 / sm:px-9, with a roomy bottom. pt-3 keeps the - cards off the model selector row. ── */ -
- {/* Controls and preview share one card, split by a divider, so the page - reads as a single surface instead of two floating boxes. */} -
-
-
- {/* Workflow picker. Seven workflows don't fit a segmented strip in a 340px - rail, so it's a dropdown: the trigger carries the current workflow and - its hint, and each row explains itself. A row is disabled until the - loaded model supports it (status.workflows), with the reason in place of - the hint. New workflows slot in without shrinking anything. */} + /* ── Controls rail + preview canvas. No card: both sit on the page background + like the Hub, divided by a rule, so nothing is spent on box chrome. Padding + mirrors the other tabs (Export, Data Recipes): px-5 / sm:px-9. ── */ +
+
+ {/* pl-0.5 keeps focus rings off the scroll container's edge. */} +
+ {/* Workflow picker. Seven workflows don't fit a segmented strip in this rail, + so it's a dropdown; a row stays disabled until the loaded model supports + it (status.workflows). New workflows slot in without shrinking anything. */}
@@ -2347,40 +2348,45 @@ export function ImagesPage({ active = true }: { active?: boolean }) { /> - {/* DropdownMenuContent already tracks the trigger width, so the rows get - the full rail width for their hints. */} {WORKFLOW_TABS.map((t) => { const enabled = workflowEnabled(t); return ( - setWorkflow(t.id)} - // The selected row's description shows under the trigger, and a - // disabled row explains itself on hover, so rows stay one line. - title={ - enabled + // Rows stay one line; what the workflow does (or why it is + // unavailable) arrives as a tooltip after a short hover. The + // trigger wraps the row rather than being the row: a disabled item + // has pointer-events: none, so it would never hover. + + +
+ setWorkflow(t.id)} + className="gap-2" + > + + + {t.label} + + + +
+
+ + {enabled ? t.hint - : `Needs a loaded model that supports ${t.label.toLowerCase()}` - } - className="gap-2" - > - - - {t.label} - - -
+ : `Needs a loaded model that supports ${t.label.toLowerCase()}`} + + ); })}
@@ -2881,8 +2887,8 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
-
-
+
+
{selected && selectedSrc ? ( <> ) : busy === "generating" ? null : (
- + {/* Same icon as the Images nav item. */} +

{status?.loaded ? "Enter a prompt and hit Generate." @@ -2980,7 +2987,7 @@ export function ImagesPage({ active = true }: { active?: boolean }) { {(images.length > 0 || busy === "generating") && (

{ // Near the right edge: pull the next older page (infinite scroll). const el = e.currentTarget; @@ -3028,21 +3035,20 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
)}
-
{/* Right-docked Advanced panel (mirrors Chat's settings panel): closed by default, opened by the single fixed top-bar toggle above (which never moves between states, like Chat's run-settings toggle), so the optimisation controls are discoverable without being docked open or buried at the bottom of the left rail. */} {advancedOpen && ( -
-
+
+
Advanced
-
+

Load-time tuning. Changes apply on the next load; Reapply reloads the current model.

diff --git a/studio/frontend/src/features/images/train/dataset-labeling-grid.tsx b/studio/frontend/src/features/images/train/dataset-labeling-grid.tsx index 49c2178754..07ca862c96 100644 --- a/studio/frontend/src/features/images/train/dataset-labeling-grid.tsx +++ b/studio/frontend/src/features/images/train/dataset-labeling-grid.tsx @@ -259,7 +259,7 @@ export function DatasetLabelingGrid({
)}
-
+
{pageRecords.map((r) => ( {names === null ? (
diff --git a/studio/frontend/src/features/images/train/diffusion-train-panel.tsx b/studio/frontend/src/features/images/train/diffusion-train-panel.tsx index 0942a694bf..0e2d46f466 100644 --- a/studio/frontend/src/features/images/train/diffusion-train-panel.tsx +++ b/studio/frontend/src/features/images/train/diffusion-train-panel.tsx @@ -53,7 +53,11 @@ import { import { DatasetLabelingGrid, LabelingGridToggle } from "./dataset-labeling-grid"; import { DatasetShowcase } from "./dataset-showcase"; import { DiffusionCharts } from "./diffusion-charts"; -import { ExampleDatasetCards, runExampleImport } from "./example-dataset-cards"; +import { + ExampleDatasetCards, + runExampleImport, + shortExampleLabel, +} from "./example-dataset-cards"; // The families the Train tab can train, in the popularity order the user asked for. This is // the fallback used when the backend's /info does not yet report families (older backend); @@ -1020,10 +1024,12 @@ export function DiffusionTrainPanel({ ); return ( -
- {/* Left: configure */} -
-
+
+ {/* Left: configure. No card here or on the right: both panes sit on the page + background like the Hub, divided by a rule. */} +
+ {/* pl-0.5 keeps focus rings off the scroll container's edge. */} +

Train a LoRA

@@ -1098,18 +1104,19 @@ export function DiffusionTrainPanel({ + {/* Rows stay short: name plus image count. Caption counts show under the + picker, and an example's license shows on its card below. */} {(info?.datasets ?? []).map((d) => ( - {d.name} ({d.image_count} image{d.image_count === 1 ? "" : "s"} - {d.caption_count > 0 ? `, ${d.caption_count} captions` : ""}) + {d.name} - {d.image_count} image{d.image_count === 1 ? "" : "s"} ))} {pendingExamples.length > 0 && ( - Examples (one-click import) + Examples {pendingExamples.map((ex) => ( - {ex.label} ({ex.image_cap} images, {ex.license}) + {shortExampleLabel(ex.label)} - {ex.image_cap} images ))} @@ -1124,7 +1131,7 @@ export function DiffusionTrainPanel({ )} {dataset === UPLOAD_DATASET ? ( -

+
fileInputRef.current?.click()} disabled={uploading} > Choose images - - {pickedFileCount > 0 - ? `${pickedFileCount} file${pickedFileCount === 1 ? "" : "s"} selected` - : "No files selected"} - - + {/* Nothing to upload until files are picked, so the count and Upload + only appear then. */} + {pickedFileCount > 0 && ( + <> + + {pickedFileCount} file{pickedFileCount === 1 ? "" : "s"} selected + + + + )}
-

+

10-50 images are plenty. Captions are optional: without them, the trigger prompt below describes every image.

@@ -1271,12 +1282,12 @@ export function DiffusionTrainPanel({ front) plus the previous-runs history; during/after a run the live view takes over (progress with Stop, then the saved-adapter card ABOVE the charts). Selecting a previous run re-plots its persisted logs read-only. */} - {/* p-1.5 so the cards' shadow (drawn outside the box) isn't clipped by this - scroll container. */} -
+ {/* Sections here carry no card of their own: spacing and a rule separate them. + p-1.5 keeps the chart cards' outer ring from being clipped. */} +
{viewRun && !hasRun ? ( <> -
+
Previous run: {viewRun.adapter || viewRun.job_id.slice(0, 8)} @@ -1339,7 +1350,7 @@ export function DiffusionTrainPanel({ ) : !hasRun ? ( <> -
+
@@ -1357,7 +1368,7 @@ export function DiffusionTrainPanel({
{prevRuns.length > 0 && ( -
+
Previous runs
{prevRuns.map((r) => ( @@ -1397,7 +1408,7 @@ export function DiffusionTrainPanel({ ) : ( <> -
+
{status?.status === "completed" ? "Training complete \u{1F389}" : status?.status} @@ -1473,7 +1484,7 @@ export function DiffusionTrainPanel({
{(completed || stoppedWithAdapter) && ( -
+
{completed ? "Adapter ready" : "Partial adapter saved"} diff --git a/studio/frontend/src/features/images/train/example-dataset-cards.tsx b/studio/frontend/src/features/images/train/example-dataset-cards.tsx index 52fb966217..1129908d43 100644 --- a/studio/frontend/src/features/images/train/example-dataset-cards.tsx +++ b/studio/frontend/src/features/images/train/example-dataset-cards.tsx @@ -50,6 +50,12 @@ async function fetchPreviews(repo: string): Promise { return p; } +// "Dog (DreamBooth subject)" -> "Dog". The parenthetical is context, not a name, so +// one-line picker rows and card titles drop it. +export function shortExampleLabel(label: string): string { + return label.replace(/\s*\(.*$/, ""); +} + function ExamplePreviews({ repo }: { repo: string }) { const [urls, setUrls] = useState(null); useEffect(() => { @@ -105,7 +111,12 @@ export function ExampleDatasetCards({
- {ex.label} + + {shortExampleLabel(ex.label)} +