feat(dashboard): ModalityBridgeVisionTab + stats row + test button

This commit is contained in:
diegosouzapw
2026-08-08 08:13:45 -03:00
parent 1e2ef990e6
commit b5c522604f
6 changed files with 719 additions and 1 deletions

View File

@@ -0,0 +1,80 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
type BridgeKind = "vision" | "audio";
interface BridgeStats {
bridged: number;
cacheHits: number;
failures: number;
lastUsedAt: string | null;
}
interface ModalityBridgeStatsRowProps {
kind: BridgeKind;
}
function parseStats(value: unknown): BridgeStats | null {
if (!value || typeof value !== "object") return null;
const record = value as Record<string, unknown>;
if (
typeof record.bridged !== "number" ||
typeof record.cacheHits !== "number" ||
typeof record.failures !== "number" ||
(record.lastUsedAt !== null && typeof record.lastUsedAt !== "string")
) {
return null;
}
return {
bridged: record.bridged,
cacheHits: record.cacheHits,
failures: record.failures,
lastUsedAt: record.lastUsedAt,
};
}
export default function ModalityBridgeStatsRow({ kind }: ModalityBridgeStatsRowProps) {
const t = useTranslations("settings");
const [stats, setStats] = useState<BridgeStats | null>(null);
useEffect(() => {
let cancelled = false;
fetch("/api/modality-bridge/stats")
.then((response) => (response.ok ? response.json() : Promise.reject(new Error("fetch"))))
.then((data: unknown) => {
if (cancelled || !data || typeof data !== "object") return;
setStats(parseStats((data as Record<string, unknown>)[kind]));
})
.catch(() => {
if (!cancelled) setStats(null);
});
return () => {
cancelled = true;
};
}, [kind]);
if (!stats) return null;
const lastUsed = stats.lastUsedAt
? new Date(stats.lastUsedAt).toLocaleString()
: t("modalityBridgeStatsNever");
return (
<div className="flex flex-wrap gap-x-3 gap-y-1 text-xs text-text-muted" aria-live="polite">
<span>
{stats.bridged} {t("modalityBridgeStatsBridged")}
</span>
<span>
{stats.cacheHits} {t("modalityBridgeStatsCacheHits")}
</span>
<span>
{stats.failures} {t("modalityBridgeStatsFailures")}
</span>
<span>
{t("modalityBridgeStatsLastUsed")}: {lastUsed}
</span>
</div>
);
}

View File

@@ -0,0 +1,110 @@
"use client";
import { useState } from "react";
import { useTranslations } from "next-intl";
const SAMPLE_INPUT = {
model: "modality-bridge/self-test",
messages: [
{
role: "user",
content: [
{ type: "text", text: "What is in this image?" },
{
type: "image_url",
image_url: {
url: "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg==",
},
},
],
},
],
};
const DISABLED_GUARDRAILS = ["pii-masker", "prompt-injection", "credential-masker"];
function asRecord(value: unknown): Record<string, unknown> | null {
return value && typeof value === "object" ? (value as Record<string, unknown>) : null;
}
function findVisionMeta(value: unknown): Record<string, unknown> | null {
const body = asRecord(value);
if (!Array.isArray(body?.results)) return null;
for (const entry of body.results) {
const result = asRecord(entry);
if (result?.guardrail === "vision-bridge") return asRecord(result.meta);
}
return null;
}
function readErrorMessage(value: unknown): string | null {
const body = asRecord(value);
const error = asRecord(body?.error);
return typeof error?.message === "string" ? error.message : null;
}
export default function ModalityBridgeTestButton() {
const t = useTranslations("settings");
const [running, setRunning] = useState(false);
const [result, setResult] = useState<string | null>(null);
const runTest = async () => {
setRunning(true);
setResult(null);
try {
const response = await fetch("/api/guardrails/test", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
input: SAMPLE_INPUT,
disabledGuardrails: DISABLED_GUARDRAILS,
}),
});
const body: unknown = await response.json().catch(() => null);
if (!response.ok) {
throw new Error(readErrorMessage(body) ?? `HTTP ${response.status}`);
}
const meta = findVisionMeta(body);
if (meta?.rerouted === true) {
setResult(
t("modalityBridgeTestReroute", {
model: String(meta.toModel ?? "unknown"),
})
);
} else if (typeof meta?.imagesProcessed === "number" && meta.imagesProcessed >= 1) {
setResult(
t("modalityBridgeTestOk", {
count: meta.imagesProcessed,
model: String(meta.visionModel ?? "unknown"),
})
);
} else {
setResult(t("modalityBridgeTestNoop"));
}
} catch (error) {
const message = error instanceof Error ? error.message : String(error);
setResult(t("modalityBridgeTestError", { message }));
} finally {
setRunning(false);
}
};
return (
<div className="flex flex-col items-start gap-2">
<button
type="button"
className="rounded-control border border-border px-3 py-2 text-sm font-medium hover:bg-surface-2 disabled:cursor-not-allowed disabled:opacity-50"
disabled={running}
onClick={() => void runTest()}
>
{t(running ? "modalityBridgeTestRunning" : "modalityBridgeTestButton")}
</button>
{result && (
<p className="text-xs text-text-muted" role="status">
{result}
</p>
)}
</div>
);
}

View File

@@ -0,0 +1,303 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Card, ModelSelectField, Toggle } from "@/shared/components";
import {
MODALITY_BRIDGE_DEFAULTS,
resolveVisionBridgeRuntimeSettings,
type VisionBridgeMode,
} from "@/shared/constants/modalityBridgeDefaults";
import { VISION_BRIDGE_DEFAULTS } from "@/shared/constants/visionBridgeDefaults";
import ModalityBridgeStatsRow from "./ModalityBridgeStatsRow";
import ModalityBridgeTestButton from "./ModalityBridgeTestButton";
interface VisionState {
modalityBridgeVisionEnabled: boolean;
modalityBridgeVisionMode: VisionBridgeMode;
modalityBridgeVisionModel: string;
modalityBridgeVisionTaskAware: boolean;
modalityBridgeVisionPrompt: string;
modalityBridgeVisionTimeout: number;
modalityBridgeVisionMaxImages: number;
modalityBridgeCacheEnabled: boolean;
modalityBridgeCacheTtlMinutes: number;
modalityBridgeCacheMaxEntries: number;
}
function fromApi(data: Record<string, unknown>): VisionState {
const runtime = resolveVisionBridgeRuntimeSettings(data);
return {
modalityBridgeVisionEnabled: runtime.enabled,
modalityBridgeVisionMode: runtime.mode,
modalityBridgeVisionModel: runtime.model,
modalityBridgeVisionTaskAware: runtime.taskAware,
modalityBridgeVisionPrompt: runtime.prompt,
modalityBridgeVisionTimeout: runtime.timeoutMs,
modalityBridgeVisionMaxImages: runtime.maxImages,
modalityBridgeCacheEnabled: runtime.cacheEnabled,
modalityBridgeCacheTtlMinutes: runtime.cacheTtlMinutes,
modalityBridgeCacheMaxEntries: runtime.cacheMaxEntries,
};
}
function clampNumber(raw: string, min: number, max: number, fallback: number): number {
const parsed = Number.parseInt(raw, 10);
return Math.min(max, Math.max(min, Number.isFinite(parsed) ? parsed : fallback));
}
export default function ModalityBridgeVisionTab() {
const t = useTranslations("settings");
const [settings, setSettings] = useState<VisionState | null>(null);
useEffect(() => {
let cancelled = false;
fetch("/api/settings")
.then((response) => (response.ok ? response.json() : null))
.then((data: unknown) => {
if (cancelled) return;
setSettings(fromApi(asSettingsRecord(data)));
})
.catch(() => {
if (!cancelled) setSettings(fromApi({}));
});
return () => {
cancelled = true;
};
}, []);
const update = async (patch: Partial<VisionState>) => {
try {
const response = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
if (response.ok) {
setSettings((previous) => (previous ? { ...previous, ...patch } : previous));
}
} catch (error) {
console.error("Failed to update Modality Bridge settings:", error);
}
};
if (!settings) return null;
const setLocal = (patch: Partial<VisionState>) => {
setSettings((previous) => (previous ? { ...previous, ...patch } : previous));
};
const commitNumber = (
key:
| "modalityBridgeVisionTimeout"
| "modalityBridgeVisionMaxImages"
| "modalityBridgeCacheTtlMinutes"
| "modalityBridgeCacheMaxEntries",
raw: string,
min: number,
max: number,
fallback: number
) => {
const value = clampNumber(raw, min, max, fallback);
setLocal({ [key]: value });
void update({ [key]: value });
};
return (
<Card
title={t("modalityBridgeVisionTitle")}
subtitle={t("modalityBridgeVisionDesc")}
icon="image_search"
>
<div className="space-y-4">
<Toggle
checked={settings.modalityBridgeVisionEnabled}
onChange={(checked) => void update({ modalityBridgeVisionEnabled: checked })}
label={t("visionBridgeEnabledLabel")}
description={t("visionBridgeEnabledDesc")}
/>
<div>
<label className="text-sm font-medium" htmlFor="modality-bridge-mode">
{t("modalityBridgeMode")}
</label>
<select
id="modality-bridge-mode"
data-testid="modality-bridge-mode"
className="mt-1 w-full rounded-control border border-border bg-surface px-3 py-2 text-sm"
value={settings.modalityBridgeVisionMode}
onChange={(event) =>
void update({ modalityBridgeVisionMode: event.target.value as VisionBridgeMode })
}
>
<option value="auto">{t("modalityBridgeModeAuto")}</option>
<option value="describe">{t("modalityBridgeModeDescribe")}</option>
<option value="reroute">{t("modalityBridgeModeReroute")}</option>
</select>
<p className="mt-1 text-xs text-text-muted">
{settings.modalityBridgeVisionMode === "auto" && t("modalityBridgeModeAutoHint")}
{settings.modalityBridgeVisionMode === "describe" &&
t("modalityBridgeModeDescribeHint")}
{settings.modalityBridgeVisionMode === "reroute" && t("modalityBridgeModeRerouteHint")}
</p>
</div>
<ModelSelectField
label={t("modalityBridgeVisionModel")}
value={settings.modalityBridgeVisionModel}
placeholder={t("modalityBridgeVisionModelAuto")}
allowEmpty
onChange={(value) => void update({ modalityBridgeVisionModel: value })}
className="text-sm"
/>
<Toggle
checked={settings.modalityBridgeVisionTaskAware}
onChange={(checked) => void update({ modalityBridgeVisionTaskAware: checked })}
label={t("modalityBridgeTaskAware")}
description={t("modalityBridgeTaskAwareDesc")}
/>
<div>
<label className="block text-sm font-medium" htmlFor="modality-bridge-prompt">
{t("modalityBridgePrompt")}
</label>
<textarea
id="modality-bridge-prompt"
className="mt-1 min-h-[100px] w-full rounded-control border border-border bg-surface px-3 py-2 text-sm"
value={settings.modalityBridgeVisionPrompt}
onChange={(event) =>
setLocal({ modalityBridgeVisionPrompt: event.currentTarget.value })
}
onBlur={(event) => {
const value = event.currentTarget.value.trim();
setLocal({ modalityBridgeVisionPrompt: value });
void update({ modalityBridgeVisionPrompt: value });
}}
/>
</div>
<details className="rounded-control border border-border p-3">
<summary className="cursor-pointer text-sm font-medium">
{t("modalityBridgeAdvanced")}
</summary>
<div className="mt-4 grid grid-cols-1 gap-4 md:grid-cols-2">
<NumberField
testId="modality-bridge-timeout"
label={t("modalityBridgeTimeoutMs")}
min={1000}
max={300000}
value={settings.modalityBridgeVisionTimeout}
onChange={(value) => setLocal({ modalityBridgeVisionTimeout: value })}
onBlur={(raw) =>
commitNumber(
"modalityBridgeVisionTimeout",
raw,
1000,
300000,
VISION_BRIDGE_DEFAULTS.timeoutMs
)
}
/>
<NumberField
testId="modality-bridge-max-images"
label={t("modalityBridgeMaxImages")}
min={1}
max={20}
value={settings.modalityBridgeVisionMaxImages}
onChange={(value) => setLocal({ modalityBridgeVisionMaxImages: value })}
onBlur={(raw) =>
commitNumber(
"modalityBridgeVisionMaxImages",
raw,
1,
20,
VISION_BRIDGE_DEFAULTS.maxImagesPerRequest
)
}
/>
<div className="md:col-span-2">
<Toggle
checked={settings.modalityBridgeCacheEnabled}
onChange={(checked) => void update({ modalityBridgeCacheEnabled: checked })}
label={t("modalityBridgeCacheEnabled")}
description={t("modalityBridgeCacheEnabledDesc")}
/>
</div>
<NumberField
testId="modality-bridge-cache-ttl"
label={t("modalityBridgeCacheTtlMinutes")}
min={1}
max={1440}
value={settings.modalityBridgeCacheTtlMinutes}
onChange={(value) => setLocal({ modalityBridgeCacheTtlMinutes: value })}
onBlur={(raw) =>
commitNumber(
"modalityBridgeCacheTtlMinutes",
raw,
1,
1440,
MODALITY_BRIDGE_DEFAULTS.cacheTtlMinutes
)
}
/>
<NumberField
testId="modality-bridge-cache-max-entries"
label={t("modalityBridgeCacheMaxEntries")}
min={10}
max={5000}
value={settings.modalityBridgeCacheMaxEntries}
onChange={(value) => setLocal({ modalityBridgeCacheMaxEntries: value })}
onBlur={(raw) =>
commitNumber(
"modalityBridgeCacheMaxEntries",
raw,
10,
5000,
MODALITY_BRIDGE_DEFAULTS.cacheMaxEntries
)
}
/>
</div>
</details>
<ModalityBridgeStatsRow kind="vision" />
<ModalityBridgeTestButton />
</div>
</Card>
);
}
function asSettingsRecord(value: unknown): Record<string, unknown> {
return value && typeof value === "object" ? (value as Record<string, unknown>) : {};
}
interface NumberFieldProps {
testId: string;
label: string;
min: number;
max: number;
value: number;
onChange: (value: number) => void;
onBlur: (raw: string) => void;
}
function NumberField({ testId, label, min, max, value, onChange, onBlur }: NumberFieldProps) {
return (
<label className="block text-sm font-medium">
{label}
<input
type="number"
data-testid={testId}
min={min}
max={max}
value={value}
onChange={(event) => onChange(Number.parseInt(event.currentTarget.value, 10) || 0)}
onBlur={(event) => onBlur(event.currentTarget.value)}
className="mt-1 w-full rounded-control border border-border bg-surface px-3 py-2 text-sm"
/>
</label>
);
}

View File

@@ -19,6 +19,8 @@ export interface ModelSelectFieldProps {
ariaLabel?: string;
/** Render a plain text fallback (custom option / off-catalog) — default true. */
allowCustom?: boolean;
/** Let operators select the empty-value placeholder (for Auto/default semantics). */
allowEmpty?: boolean;
className?: string;
}
@@ -43,6 +45,7 @@ export default function ModelSelectField({
placeholder,
ariaLabel,
allowCustom = true,
allowEmpty = false,
className,
}: ModelSelectFieldProps) {
const [state, setState] = useState<FetchState>({ status: "loading", options: [] });
@@ -95,6 +98,7 @@ export default function ModelSelectField({
onChange={(e) => onChange(e.target.value)}
options={options}
placeholder={state.status === "loading" ? "Loading models…" : placeholder || "Select a model"}
placeholderDisabled={!allowEmpty}
disabled={disabled || state.status === "loading"}
aria-label={ariaLabel}
className={className}

View File

@@ -16,6 +16,8 @@ interface SelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>
error?: React.ReactNode;
hint?: React.ReactNode;
selectClassName?: string;
/** Keep the placeholder selectable after a real value is chosen. */
placeholderDisabled?: boolean;
}
export default function Select({
@@ -30,6 +32,7 @@ export default function Select({
required = false,
className,
selectClassName,
placeholderDisabled = true,
id: externalId,
children,
...props
@@ -75,7 +78,7 @@ export default function Select({
{...props}
>
{!children && (placeholder ?? t("selectOption")) && (
<option value="" disabled className="bg-surface text-text-muted">
<option value="" disabled={placeholderDisabled} className="bg-surface text-text-muted">
{placeholder ?? t("selectOption")}
</option>
)}

View File

@@ -0,0 +1,218 @@
// @vitest-environment jsdom
import React, { act } from "react";
import { createRoot, type Root } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
import ModalityBridgeVisionTab from "@/app/(dashboard)/dashboard/settings/components/modalityBridge/ModalityBridgeVisionTab";
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
type MountedRoot = { root: Root; el: HTMLDivElement };
const roots: MountedRoot[] = [];
async function waitFor(predicate: () => boolean, label: string): Promise<void> {
const startedAt = Date.now();
while (!predicate()) {
if (Date.now() - startedAt > 2000) {
throw new Error(`Timed out waiting for: ${label}`);
}
await act(async () => {
await new Promise((resolve) => setTimeout(resolve, 10));
});
}
}
describe("ModalityBridgeVisionTab", () => {
let fetchMock: ReturnType<typeof vi.fn>;
beforeEach(() => {
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
fetchMock = vi.fn(async (input: RequestInfo | URL, init?: RequestInit) => {
const url = String(input);
if (url.includes("/api/models")) {
return new Response(
JSON.stringify({ models: [{ provider: "openai", model: "gpt-4o-mini" }] }),
{ status: 200 }
);
}
if (url.includes("/api/modality-bridge/stats")) {
return new Response(
JSON.stringify({
vision: { bridged: 3, cacheHits: 1, failures: 0, lastUsedAt: null },
audio: { bridged: 0, cacheHits: 0, failures: 0, lastUsedAt: null },
}),
{ status: 200 }
);
}
if (url.includes("/api/guardrails/test")) {
return new Response(
JSON.stringify({
blocked: false,
results: [
{
guardrail: "vision-bridge",
meta: { imagesProcessed: 1, visionModel: "openai/gpt-4o-mini" },
},
],
payload: {},
}),
{ status: 200 }
);
}
if (url.includes("/api/settings")) {
if (init?.method === "PATCH") return new Response("{}", { status: 200 });
return new Response(
JSON.stringify({ visionBridgeModel: "legacy/model", visionBridgeEnabled: true }),
{ status: 200 }
);
}
return new Response("{}", { status: 200 });
});
vi.stubGlobal("fetch", fetchMock);
});
afterEach(() => {
for (const { root, el } of roots.splice(0)) {
act(() => root.unmount());
el.remove();
}
vi.unstubAllGlobals();
});
async function render(): Promise<HTMLDivElement> {
const el = document.createElement("div");
document.body.appendChild(el);
const root = createRoot(el);
await act(async () => {
root.render(<ModalityBridgeVisionTab />);
});
roots.push({ root, el });
await waitFor(
() => el.querySelector('[data-testid="modality-bridge-mode"]') !== null,
"vision settings to load"
);
return el;
}
it("PATCHes only the new modalityBridge* key when toggling", async () => {
const el = await render();
const toggle = el.querySelector('[role="switch"]') as HTMLButtonElement | null;
expect(toggle).toBeTruthy();
await act(async () => {
toggle?.click();
});
await waitFor(
() =>
fetchMock.mock.calls.some(
([url, init]) =>
String(url).includes("/api/settings") &&
(init as RequestInit | undefined)?.method === "PATCH"
),
"settings PATCH"
);
const patch = fetchMock.mock.calls.find(
([url, init]) =>
String(url).includes("/api/settings") &&
(init as RequestInit | undefined)?.method === "PATCH"
);
const body = JSON.parse(String((patch?.[1] as RequestInit | undefined)?.body)) as Record<
string,
unknown
>;
expect(body).toHaveProperty("modalityBridgeVisionEnabled", false);
expect(body).not.toHaveProperty("visionBridgeEnabled");
});
it("reads the legacy model as fallback when the new key is absent", async () => {
const el = await render();
expect(el.innerHTML).toContain("legacy/model");
const modelLabel = Array.from(el.querySelectorAll("label")).find((label) =>
label.textContent?.includes("modalityBridgeVisionModel")
);
const modelSelect = modelLabel?.parentElement?.querySelector("select") ?? null;
const autoOption = modelSelect?.querySelector('option[value=""]') as HTMLOptionElement | null;
expect(autoOption).toBeTruthy();
expect(autoOption?.disabled).toBe(false);
});
it("renders the mode selector, live stats, and all three mode options", async () => {
const el = await render();
const select = el.querySelector(
'select[data-testid="modality-bridge-mode"]'
) as HTMLSelectElement | null;
expect(select).toBeTruthy();
expect(Array.from(select?.options ?? []).map((option) => option.value)).toEqual([
"auto",
"describe",
"reroute",
]);
await waitFor(() => el.textContent?.includes("3 modalityBridgeStatsBridged") ?? false, "stats");
expect(el.textContent).toContain("1 modalityBridgeStatsCacheHits");
});
it("clamps advanced numeric settings to the schema bounds before PATCHing", async () => {
const el = await render();
const timeout = el.querySelector(
'[data-testid="modality-bridge-timeout"]'
) as HTMLInputElement | null;
expect(timeout).toBeTruthy();
act(() => {
const setter = Object.getOwnPropertyDescriptor(
window.HTMLInputElement.prototype,
"value"
)?.set;
setter?.call(timeout, "500");
timeout?.dispatchEvent(new Event("input", { bubbles: true }));
});
await act(async () => {
timeout?.dispatchEvent(new FocusEvent("focusout", { bubbles: true }));
await new Promise((resolve) => setTimeout(resolve, 0));
});
await waitFor(
() =>
fetchMock.mock.calls.some(([, init]) => {
if ((init as RequestInit | undefined)?.method !== "PATCH") return false;
const body = JSON.parse(String((init as RequestInit).body)) as Record<string, unknown>;
return body.modalityBridgeVisionTimeout === 1000;
}),
"clamped timeout PATCH"
);
});
it("runs the guardrail self-test with an image and renders the bridge result", async () => {
const el = await render();
const button = Array.from(el.querySelectorAll("button")).find((candidate) =>
candidate.textContent?.includes("modalityBridgeTestButton")
);
expect(button).toBeTruthy();
await act(async () => {
button?.click();
});
await waitFor(
() => el.textContent?.includes("modalityBridgeTestOk") ?? false,
"bridge self-test result"
);
const call = fetchMock.mock.calls.find(([url]) => String(url).includes("/api/guardrails/test"));
expect(call).toBeTruthy();
const body = JSON.parse(String((call?.[1] as RequestInit | undefined)?.body)) as {
input?: { messages?: unknown[] };
disabledGuardrails?: string[];
};
expect(body.input?.messages).toHaveLength(1);
expect(body.disabledGuardrails).toEqual([
"pii-masker",
"prompt-injection",
"credential-masker",
]);
});
});