diff --git a/web/src/modules/inferences/SiteProfilePage.tsx b/web/src/modules/inferences/SiteProfilePage.tsx index 7fc12ca..4a9f86e 100644 --- a/web/src/modules/inferences/SiteProfilePage.tsx +++ b/web/src/modules/inferences/SiteProfilePage.tsx @@ -227,6 +227,20 @@ function ProfileView({ p }: { p: SiteProfile }) { "sites", p.sites.map((s) => ({ label: s.name, value: mono(s.url) })), )} + {p.presets && Object.keys(p.presets).length > 0 && ( + +

{t("profile.hints.presets")}

+ {Object.entries(p.presets).map(([name, preset]) => ( +
+
+ {name} + {preset.description && {preset.description}} +
+ +
+ ))} +
+ )} {p.extra && Object.keys(p.extra).length > 0 && ( diff --git a/web/src/modules/inferences/components/DeployForm.tsx b/web/src/modules/inferences/components/DeployForm.tsx index 02c5e8b..037ed62 100644 --- a/web/src/modules/inferences/components/DeployForm.tsx +++ b/web/src/modules/inferences/components/DeployForm.tsx @@ -3,6 +3,7 @@ import { Badge, Button, Checkbox, + CodeBlock, DataSelect, DropdownMenu, DropdownMenuContent, @@ -23,12 +24,14 @@ import { cn, } from "@modelsphere/ui"; import { Plus, RefreshCw, X } from "lucide-react"; -import type { ClusterInfo } from "@swiss/lib/api"; +import type { ClusterInfo, Preset } from "@swiss/lib/api"; +import { editRule, listLabel, type AffinityRule } from "@swiss/lib/affinity"; +import { toYaml } from "@swiss/lib/yaml"; import { cacheFieldError, type CacheCard, type CacheDefaults } from "@swiss/lib/cache"; import { unlistedProducts, type GPUCapacity } from "@swiss/lib/gpu"; import { CHECK_MS, inputError, rememberable, type InputRow } from "@swiss/lib/inputs"; import { useDebounced } from "@swiss/lib/useDebounced"; -import { effective, imageChoices, type Form, type Toleration } from "@swiss/components/DeploySettings"; +import { effective, imageChoices, tolerationExtras, type Form, type Toleration } from "@swiss/components/DeploySettings"; import { MAX_VALUES, rejectKey } from "@swiss/lib/routeDirectives"; import { FALLBACK_ROUTE_OPTIONS, routeDirectives, runtimeValue, type RouteDirective, type RouteOptions } from "@/modules/inferences/routeOptions"; import { useT } from "@/modules/inferences/i18n"; @@ -67,9 +70,11 @@ export interface DeployFormProps { // Which typed answers to save to the site profile once the deploy applies; absent where it cannot be saved. remember?: Record; onRemember?: (key: string, on: boolean) => void; + // The site profile's presets, on a deploy. + presets?: Record; } -export function DeployForm({ form, onChange, upgrade, submitted, target, cluster, defaults, gpuOptions, gpuListed, gpuResources, capacity, expanded, onExpand, idPrefix, routeOptions, catalogRoute, helmValues, recovery, inputs, remember, onRemember }: DeployFormProps) { +export function DeployForm({ form, onChange, upgrade, submitted, target, cluster, defaults, gpuOptions, gpuListed, gpuResources, capacity, expanded, onExpand, idPrefix, routeOptions, catalogRoute, helmValues, recovery, inputs, remember, onRemember, presets }: DeployFormProps) { const t = useT(); const checked = useDebounced(form.inputs, CHECK_MS); const on = effective(form); @@ -145,7 +150,9 @@ export function DeployForm({ form, onChange, upgrade, submitted, target, cluster onChange({ gpuProducts: v })} options={gpuOptions.map((o) => ({ value: o, label: o, badge: unlistedProducts(gpuListed, [o]).length ? {f("gpuUnverified")} : {f("gpuVerified")} }))} placeholder={gpuOptions.length ? undefined : f("gpuProductsEmpty")} disabled={!gpuOptions.length} /> {capacity && } + {presets && Object.keys(presets).length > 0 && onChange({ preset })} />} onChange({ tolerations: v })} /> + {form.affinity.length > 0 && onChange({ affinity })} />}
onChange({ priorityClassName })} suggestion={cluster?.priorityClassName} /> @@ -396,13 +403,14 @@ function Tolerations({ rows, onChange }: { rows: Toleration[]; onChange: (v: Tol {rows.map((r, i) => (
- patch(i, { key: e.target.value })} placeholder={f("tolKey")} aria-label={f("tolKey")} /> + patch(i, { key: e.target.value })} placeholder={r.raw && !r.raw.key ? f("tolAny") : f("tolKey")} aria-label={f("tolKey")} /> patch(i, { operator: v as Toleration["operator"], value: v === "Exists" ? "" : r.value })} options={["Equal", "Exists"].map((o) => ({ value: o, label: o }))} /> patch(i, { value: e.target.value })} placeholder={f("tolValue")} aria-label={f("tolValue")} disabled={r.operator === "Exists"} /> patch(i, { effect: v })} options={EFFECTS.map((e) => ({ value: e, label: e || f("anyEffect") }))} /> + {tolerationExtras(r).length > 0 && {tolerationExtras(r).join(" · ")}}
))} +
+