Images: center the mode switch, flip the arrow with the orientation, app tooltips everywhere
The Create/Train switch is centered on the page instead of trailing the model selector, with wider buttons. The flip control's arrows now rotate with the orientation and its label says which way the flip goes. Every native title tooltip on the page is now the app's tooltip, so they all get the rounded surface instead of the OS box.
This commit is contained in:
parent
f5e05b4816
commit
7e5f4d87de
4 changed files with 188 additions and 118 deletions
|
|
@ -52,7 +52,11 @@ 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 {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} 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";
|
||||
|
|
@ -508,14 +512,18 @@ function ResolvedBadge({
|
|||
}) {
|
||||
const resolved = status?.resolved?.[controlKey];
|
||||
if (!resolved || resolved.source !== "auto") return null;
|
||||
return (
|
||||
<span
|
||||
title={resolved.reason || undefined}
|
||||
className="shrink-0 rounded-sm bg-muted px-1 py-px text-[9px] font-medium uppercase tracking-wider text-muted-foreground"
|
||||
>
|
||||
const badge = (
|
||||
<span className="shrink-0 rounded-sm bg-muted px-1 py-px text-[9px] font-medium uppercase tracking-wider text-muted-foreground">
|
||||
Auto: {formatResolvedValue(controlKey, resolved.value)}
|
||||
</span>
|
||||
);
|
||||
if (!resolved.reason) return badge;
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>{badge}</TooltipTrigger>
|
||||
<TooltipContent>{resolved.reason}</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
||||
// A compact labeled Select row for the Advanced Options panel.
|
||||
|
|
@ -596,20 +604,24 @@ function ImageDropzone({
|
|||
return (
|
||||
<div className="relative overflow-hidden rounded-[10px] border border-border">
|
||||
<img src={value} alt="Source" className="max-h-44 w-full object-contain bg-muted/30" />
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
aria-label="Remove source image"
|
||||
title="Remove"
|
||||
className="absolute right-1.5 top-1.5 size-7"
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
}}
|
||||
>
|
||||
<HugeiconsIcon icon={Delete02Icon} className="size-3.5" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
aria-label="Remove source image"
|
||||
className="absolute right-1.5 top-1.5 size-7"
|
||||
onClick={() => {
|
||||
onChange(null);
|
||||
if (inputRef.current) inputRef.current.value = "";
|
||||
}}
|
||||
>
|
||||
<HugeiconsIcon icon={Delete02Icon} className="size-3.5" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Remove</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
@ -2214,16 +2226,20 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
|
|||
wires lastLoad for it), so it keeps Reapply even before any user-initiated
|
||||
load; GGUF/single_file residents have no reload target and hide the button. */}
|
||||
{status?.loaded && (canReapply || status?.model_kind === "pipeline") && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={busy !== null}
|
||||
onClick={handleReapply}
|
||||
title="Reload the current model with these advanced options"
|
||||
>
|
||||
<HugeiconsIcon icon={ArrowReloadHorizontalIcon} className="mr-2 size-3.5" />
|
||||
Reapply to loaded model
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
disabled={busy !== null}
|
||||
onClick={handleReapply}
|
||||
>
|
||||
<HugeiconsIcon icon={ArrowReloadHorizontalIcon} className="mr-2 size-3.5" />
|
||||
Reapply to loaded model
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Reload the current model with these advanced options</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
|
@ -2233,7 +2249,7 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
|
|||
{/* ── Top: the model selector, kept at the chat tab's exact position so the
|
||||
shared element matches. The load progress shows in a chat-style toast,
|
||||
not here. ── */}
|
||||
<div className="flex h-[48px] shrink-0 items-start justify-between pl-2 pr-2 pt-[11px]">
|
||||
<div className="relative flex h-[48px] shrink-0 items-start justify-between pl-2 pr-2 pt-[11px]">
|
||||
<div className="flex items-center gap-2">
|
||||
<ModelSelector
|
||||
models={MODELS}
|
||||
|
|
@ -2248,17 +2264,18 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
|
|||
open={active && selectorOpen}
|
||||
onOpenChange={(o) => setSelectorOpen(active && o)}
|
||||
/>
|
||||
{/* Create | Train page-mode switch, next to the model selector (which stays
|
||||
leftmost: its position is shared with Chat's). PillTabs is the app's
|
||||
segmented control, so this matches the picker and Hub toggles. Icons match
|
||||
the sidebar's New chat / Train rows. Height pinned to the row's 34px. */}
|
||||
</div>
|
||||
{/* Create | Train page-mode switch, centered on the page rather than tied to the
|
||||
selector's width (the selector stays leftmost: its position is shared with
|
||||
Chat's). PillTabs is the app's segmented control, so this matches the picker
|
||||
and Hub toggles. Icons match the sidebar's New chat / Train rows. */}
|
||||
<div className="pointer-events-none absolute inset-x-0 top-[11px] flex justify-center">
|
||||
<PillTabs
|
||||
ariaLabel="Page mode"
|
||||
value={pageMode}
|
||||
onValueChange={(v) => setPageMode(v as "create" | "train")}
|
||||
fit={true}
|
||||
// ml-5 sets it off from the selector rather than sitting against it.
|
||||
className="ml-5 h-[34px] [&>button]:h-[34px] [&>button]:px-4"
|
||||
className="pointer-events-auto h-[34px] [&>button]:h-[34px] [&>button]:px-7"
|
||||
tabs={[
|
||||
{
|
||||
value: "create",
|
||||
|
|
@ -2285,21 +2302,25 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
|
|||
toggle, same icon in both states so it never moves). Highlighted when open.
|
||||
Only meaningful in Create mode (load-time tuning), so hidden while training. */}
|
||||
{pageMode === "create" && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAdvancedOpen((o) => !o)}
|
||||
aria-label={advancedOpen ? "Hide advanced options" : "Show advanced options"}
|
||||
aria-pressed={advancedOpen}
|
||||
title="Advanced options"
|
||||
className={cn(
|
||||
"flex h-[34px] w-[34px] items-center justify-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
advancedOpen
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon icon={LayoutAlignRightIcon} className="size-4" />
|
||||
</button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => setAdvancedOpen((o) => !o)}
|
||||
aria-label={advancedOpen ? "Hide advanced options" : "Show advanced options"}
|
||||
aria-pressed={advancedOpen}
|
||||
className={cn(
|
||||
"flex h-[34px] w-[34px] items-center justify-center rounded-full transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
||||
advancedOpen
|
||||
? "bg-muted text-foreground"
|
||||
: "text-muted-foreground hover:bg-muted hover:text-foreground",
|
||||
)}
|
||||
>
|
||||
<HugeiconsIcon icon={LayoutAlignRightIcon} className="size-4" />
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Advanced options</TooltipContent>
|
||||
</Tooltip>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
|
@ -2821,16 +2842,30 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
|
|||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
aria-label="Flip width and height"
|
||||
title="Flip orientation"
|
||||
onClick={flipDimensions}
|
||||
>
|
||||
<HugeiconsIcon icon={ArrowLeftRightIcon} className="size-4" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="secondary"
|
||||
size="icon"
|
||||
aria-label="Flip width and height"
|
||||
onClick={flipDimensions}
|
||||
>
|
||||
{/* The arrows turn with the orientation, so the button shows
|
||||
which way the flip goes. */}
|
||||
<HugeiconsIcon
|
||||
icon={ArrowLeftRightIcon}
|
||||
className={cn(
|
||||
"size-4 transition-transform duration-200",
|
||||
portrait && "rotate-90",
|
||||
)}
|
||||
/>
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>
|
||||
{portrait ? "Switch to landscape" : "Switch to portrait"}
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</Field>
|
||||
<SliderField label="Width" value={width} min={MIN_DIM} max={MAX_DIM} step={16} onChange={changeWidth} />
|
||||
|
|
@ -2929,16 +2964,20 @@ export function ImagesPage({ active = true }: { active?: boolean }) {
|
|||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="Delete image"
|
||||
title="Delete"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={() => void handleDelete(selected.id)}
|
||||
>
|
||||
<HugeiconsIcon icon={Delete02Icon} className="size-4" />
|
||||
</Button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="Delete image"
|
||||
className="text-muted-foreground hover:text-destructive"
|
||||
onClick={() => void handleDelete(selected.id)}
|
||||
>
|
||||
<HugeiconsIcon icon={Delete02Icon} className="size-4" />
|
||||
</Button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Delete</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</>
|
||||
) : selected ? (
|
||||
|
|
|
|||
|
|
@ -9,6 +9,11 @@ import { HugeiconsIcon } from "@hugeicons/react";
|
|||
import { Button } from "@/components/ui/button";
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import { Textarea } from "@/components/ui/textarea";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
|
|
@ -114,15 +119,19 @@ function LabelTile({
|
|||
<Spinner className="size-4 text-muted-foreground" />
|
||||
</div>
|
||||
)}
|
||||
<button
|
||||
type="button"
|
||||
onClick={remove}
|
||||
disabled={deleting}
|
||||
title="Remove this image from the dataset"
|
||||
className="absolute right-1 top-1 rounded-md bg-background/80 px-1.5 py-0.5 text-[11px] text-muted-foreground opacity-0 transition-opacity hover:bg-background hover:text-destructive group-hover:opacity-100 focus:opacity-100"
|
||||
>
|
||||
{deleting ? "..." : "Remove"}
|
||||
</button>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={remove}
|
||||
disabled={deleting}
|
||||
className="absolute right-1 top-1 rounded-md bg-background/80 px-1.5 py-0.5 text-[11px] text-muted-foreground opacity-0 transition-opacity hover:bg-background hover:text-destructive group-hover:opacity-100 focus:opacity-100"
|
||||
>
|
||||
{deleting ? "..." : "Remove"}
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Remove this image from the dataset</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<Textarea
|
||||
value={caption}
|
||||
|
|
@ -135,9 +144,12 @@ function LabelTile({
|
|||
aria-label={`Caption for ${record.filename}`}
|
||||
/>
|
||||
<div className="flex items-center justify-between text-[10px] text-muted-foreground">
|
||||
<span className="truncate" title={record.filename}>
|
||||
{record.filename}
|
||||
</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<span className="truncate">{record.filename}</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{record.filename}</TooltipContent>
|
||||
</Tooltip>
|
||||
{saving ? (
|
||||
<span>Saving...</span>
|
||||
) : savedTick ? (
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Spinner } from "@/components/ui/spinner";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
|
||||
import {
|
||||
diffusionDatasetImageUrl,
|
||||
|
|
@ -99,29 +104,33 @@ export function DatasetShowcase({
|
|||
const remaining = imageCount - (names?.length ?? 0);
|
||||
|
||||
return (
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBrowse}
|
||||
title="Browse and caption these images"
|
||||
className="hover-scrollbar flex w-full items-center gap-1.5 overflow-x-auto rounded-lg border border-border bg-muted/20 p-1.5 text-left transition-colors hover:border-foreground/20"
|
||||
>
|
||||
{names === null ? (
|
||||
<div className="flex h-14 items-center gap-2 px-2 text-[11px] text-muted-foreground">
|
||||
<Spinner className="size-3.5" /> Loading preview...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{names.map((n) => (
|
||||
<ShowcaseTile key={n} dataset={dataset} filename={n} />
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<div className="flex size-14 shrink-0 flex-col items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||
<span className="text-sm font-medium">+{remaining}</span>
|
||||
<span className="text-[9px]">more</span>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<button
|
||||
type="button"
|
||||
onClick={onBrowse}
|
||||
className="hover-scrollbar flex w-full items-center gap-1.5 overflow-x-auto rounded-lg border border-border bg-muted/20 p-1.5 text-left transition-colors hover:border-foreground/20"
|
||||
>
|
||||
{names === null ? (
|
||||
<div className="flex h-14 items-center gap-2 px-2 text-[11px] text-muted-foreground">
|
||||
<Spinner className="size-3.5" /> Loading preview...
|
||||
</div>
|
||||
) : (
|
||||
<>
|
||||
{names.map((n) => (
|
||||
<ShowcaseTile key={n} dataset={dataset} filename={n} />
|
||||
))}
|
||||
{remaining > 0 && (
|
||||
<div className="flex size-14 shrink-0 flex-col items-center justify-center rounded-md bg-muted text-muted-foreground">
|
||||
<span className="text-sm font-medium">+{remaining}</span>
|
||||
<span className="text-[9px]">more</span>
|
||||
</div>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</button>
|
||||
</button>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>Browse and caption these images</TooltipContent>
|
||||
</Tooltip>
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,6 +4,11 @@
|
|||
import { useEffect, useState } from "react";
|
||||
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Tooltip,
|
||||
TooltipContent,
|
||||
TooltipTrigger,
|
||||
} from "@/components/ui/tooltip";
|
||||
import { toast } from "@/lib/toast";
|
||||
|
||||
import {
|
||||
|
|
@ -111,18 +116,23 @@ export function ExampleDatasetCards({
|
|||
<div className="flex min-w-0 items-center gap-3">
|
||||
<div className="flex min-w-0 flex-1 flex-col gap-1">
|
||||
<div className="flex min-w-0 items-center gap-1.5">
|
||||
<span
|
||||
className="min-w-0 flex-1 truncate text-xs font-medium"
|
||||
title={ex.label}
|
||||
>
|
||||
{shortExampleLabel(ex.label)}
|
||||
</span>
|
||||
<span
|
||||
className="max-w-[110px] shrink truncate rounded-full bg-secondary px-2 py-0.5 text-[10px] font-normal text-secondary-foreground"
|
||||
title={ex.license}
|
||||
>
|
||||
{ex.license}
|
||||
</span>
|
||||
{/* Both are truncated, so the full text lives in a tooltip. */}
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<span className="min-w-0 flex-1 truncate text-xs font-medium">
|
||||
{shortExampleLabel(ex.label)}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{ex.label}</TooltipContent>
|
||||
</Tooltip>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild={true}>
|
||||
<span className="max-w-[110px] shrink truncate rounded-full bg-secondary px-2 py-0.5 text-[10px] font-normal text-secondary-foreground">
|
||||
{ex.license}
|
||||
</span>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent>{ex.license}</TooltipContent>
|
||||
</Tooltip>
|
||||
</div>
|
||||
<p className="line-clamp-2 text-[11px] leading-snug text-muted-foreground">
|
||||
{ex.description}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue