hermes-agent/web/src/components/AutoField.tsx
Austin Pickett 487c398dcf refactor(web): dashboard typography & contrast pass
Removes the global `uppercase` + `font-mondwest` from the App.tsx root
that forced every page to opt-out, replaces stacked-alpha text colors
with semantic tokens for WCAG-AA contrast across all 7 themes, and
applies the new `text-display` utility from @nous-research/ui@0.16.0
on intentional brand chrome (page titles, sidebar headings, segmented
filters) only. Bumps every sub-12px arbitrary text size to text-xs.

Also widens the dashboard plugin routes (/api/dashboard/agent-plugins/
{name:path}/...) so category-namespaced plugins like observability/
langfuse and image_gen/openai can be enable/disabled from the dashboard
— previously the FE encodeURIComponent-ed the slash and the backend
{name} route rejected it. _validate_plugin_name still blocks .. and
backslash, and strips leading/trailing slash.

Touches sessions/env/keys page chrome and adds two new i18n keys
(`overview`, `showMore`/`showLess`) across all 18 locales.

Squashes 19 commits from PR #28832.

Co-authored-by: Hermes <noreply@nousresearch.com>
2026-05-22 19:50:32 -07:00

206 lines
6.2 KiB
TypeScript

import { Select, SelectOption } from "@nous-research/ui/ui/components/select";
import { Switch } from "@nous-research/ui/ui/components/switch";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
function FieldHint({ schema, schemaKey }: { schema: Record<string, unknown>; schemaKey: string }) {
const keyPath = schemaKey.includes(".") ? schemaKey : "";
const description = schema.description ? String(schema.description) : "";
if (!keyPath && !description) return null;
return (
<div className="flex flex-col gap-0.5">
{keyPath && <span className="text-xs font-mono text-text-tertiary">{keyPath}</span>}
{description && <span className="text-xs text-text-secondary">{description}</span>}
</div>
);
}
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === "object" && value !== null && !Array.isArray(value);
}
function formatScalar(value: unknown): string {
if (value === undefined || value === null) return "";
if (typeof value === "string") return value;
if (typeof value === "number" || typeof value === "boolean") return String(value);
return JSON.stringify(value);
}
function NestedValueEditor({
fieldKey,
value,
onChange,
}: {
fieldKey: string;
value: unknown;
onChange: (v: unknown) => void;
}) {
if (isRecord(value)) {
return (
<div className="grid gap-2 border border-border p-2">
{Object.entries(value).map(([subKey, subVal]) => (
<div key={subKey} className="grid gap-1">
<Label className="text-xs text-muted-foreground">{subKey}</Label>
<NestedValueEditor
fieldKey={`${fieldKey}.${subKey}`}
value={subVal}
onChange={(next) => onChange({ ...value, [subKey]: next })}
/>
</div>
))}
</div>
);
}
if (Array.isArray(value)) {
return (
<div className="grid gap-2">
{value.map((item, index) => (
<div key={`${fieldKey}.${index}`} className="grid gap-1">
<Label className="text-xs text-muted-foreground">Item {index + 1}</Label>
<NestedValueEditor
fieldKey={`${fieldKey}.${index}`}
value={item}
onChange={(next) =>
onChange(value.map((existing, i) => (i === index ? next : existing)))
}
/>
</div>
))}
</div>
);
}
return (
<Input
value={formatScalar(value)}
onChange={(e) => onChange(e.target.value)}
className="text-xs"
/>
);
}
export function AutoField({
schemaKey,
schema,
value,
onChange,
}: AutoFieldProps) {
const rawLabel = schemaKey.split(".").pop() ?? schemaKey;
const label = rawLabel.replace(/_/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
if (isRecord(value) || (Array.isArray(value) && value.some((item) => isRecord(item)))) {
return (
<div className="grid gap-3 border border-border p-3">
<Label className="text-xs font-medium">{label}</Label>
<FieldHint schema={schema} schemaKey={schemaKey} />
<NestedValueEditor fieldKey={schemaKey} value={value} onChange={onChange} />
</div>
);
}
if (schema.type === "boolean") {
return (
<div className="flex items-center justify-between gap-4">
<div className="flex flex-col gap-0.5">
<Label className="text-sm">{label}</Label>
<FieldHint schema={schema} schemaKey={schemaKey} />
</div>
<Switch checked={!!value} onCheckedChange={onChange} />
</div>
);
}
if (schema.type === "select") {
const options = (schema.options as string[]) ?? [];
return (
<div className="grid gap-1.5">
<Label className="text-sm">{label}</Label>
<FieldHint schema={schema} schemaKey={schemaKey} />
<Select value={String(value ?? "")} onValueChange={(v) => onChange(v)}>
{options.map((opt) => (
<SelectOption key={opt} value={opt}>
{opt || "(none)"}
</SelectOption>
))}
</Select>
</div>
);
}
if (schema.type === "number") {
return (
<div className="grid gap-1.5">
<Label className="text-sm">{label}</Label>
<FieldHint schema={schema} schemaKey={schemaKey} />
<Input
type="number"
value={value === undefined || value === null ? "" : String(value)}
onChange={(e) => {
const raw = e.target.value;
if (raw === "") {
onChange(0);
return;
}
const n = Number(raw);
if (!Number.isNaN(n)) {
onChange(n);
}
}}
/>
</div>
);
}
if (schema.type === "text") {
return (
<div className="grid gap-1.5">
<Label className="text-sm">{label}</Label>
<FieldHint schema={schema} schemaKey={schemaKey} />
<textarea
className="flex min-h-[80px] w-full border border-input bg-transparent px-3 py-2 text-sm shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-ring"
value={String(value ?? "")}
onChange={(e) => onChange(e.target.value)}
/>
</div>
);
}
if (schema.type === "list") {
return (
<div className="grid gap-1.5">
<Label className="text-sm">{label}</Label>
<FieldHint schema={schema} schemaKey={schemaKey} />
<Input
value={Array.isArray(value) ? value.join(", ") : String(value ?? "")}
onChange={(e) =>
onChange(
e.target.value
.split(",")
.map((s) => s.trim())
.filter(Boolean),
)
}
placeholder="comma-separated values"
/>
</div>
);
}
return (
<div className="grid gap-1.5">
<Label className="text-sm">{label}</Label>
<FieldHint schema={schema} schemaKey={schemaKey} />
<Input value={String(value ?? "")} onChange={(e) => onChange(e.target.value)} />
</div>
);
}
interface AutoFieldProps {
schemaKey: string;
schema: Record<string, unknown>;
value: unknown;
onChange: (v: unknown) => void;
}