Merge branch 'feat/modality-bridge-page' into feat/audio-bridge

This commit is contained in:
diegosouzapw
2026-08-08 14:53:11 -03:00
3 changed files with 5 additions and 187 deletions

View File

@@ -1,180 +0,0 @@
"use client";
import { useEffect, useState } from "react";
import { useTranslations } from "next-intl";
import { Card, Toggle } from "@/shared/components";
import { VISION_BRIDGE_DEFAULTS } from "@/shared/constants/visionBridgeDefaults";
type SettingsState = {
visionBridgeEnabled: boolean;
visionBridgeModel: string;
visionBridgePrompt: string;
visionBridgeTimeout: number;
visionBridgeMaxImages: number;
};
export default function VisionBridgeSettingsTab() {
const t = useTranslations("settings");
const [settings, setSettings] = useState<SettingsState>({
visionBridgeEnabled: VISION_BRIDGE_DEFAULTS.enabled,
visionBridgeModel: VISION_BRIDGE_DEFAULTS.model,
visionBridgePrompt: VISION_BRIDGE_DEFAULTS.prompt,
visionBridgeTimeout: VISION_BRIDGE_DEFAULTS.timeoutMs,
visionBridgeMaxImages: VISION_BRIDGE_DEFAULTS.maxImagesPerRequest,
});
const [loading, setLoading] = useState(true);
useEffect(() => {
fetch("/api/settings")
.then((res) => (res.ok ? res.json() : null))
.then((data) => {
if (!data) return;
setSettings({
visionBridgeEnabled: data.visionBridgeEnabled ?? VISION_BRIDGE_DEFAULTS.enabled,
visionBridgeModel: data.visionBridgeModel ?? VISION_BRIDGE_DEFAULTS.model,
visionBridgePrompt: data.visionBridgePrompt ?? VISION_BRIDGE_DEFAULTS.prompt,
visionBridgeTimeout: data.visionBridgeTimeout ?? VISION_BRIDGE_DEFAULTS.timeoutMs,
visionBridgeMaxImages:
data.visionBridgeMaxImages ?? VISION_BRIDGE_DEFAULTS.maxImagesPerRequest,
});
})
.finally(() => setLoading(false));
}, []);
const updateSetting = async (patch: Partial<SettingsState>) => {
try {
const res = await fetch("/api/settings", {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(patch),
});
if (res.ok) {
setSettings((prev) => ({ ...prev, ...patch }));
}
} catch (error) {
console.error("Failed to update Vision Bridge settings:", error);
}
};
return (
<Card>
<div className="flex items-center gap-3 mb-4">
<div className="p-2 rounded-lg bg-fuchsia-500/10 text-fuchsia-500">
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
image_search
</span>
</div>
<div>
<h3 className="text-lg font-semibold">{t("visionBridge")}</h3>
<p className="text-sm text-text-muted">{t("visionBridgeDesc")}</p>
</div>
</div>
<div className="flex flex-col gap-4">
<div className="flex items-center justify-between gap-4">
<div>
<p className="font-medium">{t("visionBridgeEnabledLabel")}</p>
<p className="text-sm text-text-muted">{t("visionBridgeEnabledDesc")}</p>
</div>
<Toggle
checked={settings.visionBridgeEnabled}
onChange={(checked) => updateSetting({ visionBridgeEnabled: checked })}
disabled={loading}
/>
</div>
<div className="pt-4 border-t border-border space-y-4">
<div>
<label className="block text-sm font-medium mb-1">{t("visionBridgeModel")}</label>
<input
type="text"
value={settings.visionBridgeModel}
onChange={(e) =>
setSettings((prev) => ({ ...prev, visionBridgeModel: e.target.value }))
}
onBlur={() => updateSetting({ visionBridgeModel: settings.visionBridgeModel.trim() })}
className="w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm"
placeholder={t("visionBridgeModelPlaceholder")}
/>
<p className="text-xs text-text-muted mt-1">{t("visionBridgeModelHint")}</p>
</div>
<div>
<label className="block text-sm font-medium mb-1">{t("visionBridgePrompt")}</label>
<textarea
value={settings.visionBridgePrompt}
onChange={(e) =>
setSettings((prev) => ({ ...prev, visionBridgePrompt: e.target.value }))
}
onBlur={() =>
updateSetting({ visionBridgePrompt: settings.visionBridgePrompt.trim() })
}
className="min-h-[100px] w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm"
placeholder={t("visionBridgePromptPlaceholder")}
/>
<p className="text-xs text-text-muted mt-1">{t("visionBridgePromptHint")}</p>
</div>
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
<div>
<label className="block text-sm font-medium mb-1">{t("visionBridgeTimeoutMs")}</label>
<input
type="number"
min={1000}
max={300000}
value={settings.visionBridgeTimeout}
onChange={(e) =>
setSettings((prev) => ({
...prev,
visionBridgeTimeout: Number.parseInt(e.target.value, 10) || 0,
}))
}
onBlur={() =>
updateSetting({
visionBridgeTimeout: Math.min(
300000,
Math.max(
1000,
settings.visionBridgeTimeout || VISION_BRIDGE_DEFAULTS.timeoutMs
)
),
})
}
className="w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm"
/>
</div>
<div>
<label className="block text-sm font-medium mb-1">
{t("visionBridgeMaxImagesPerRequest")}
</label>
<input
type="number"
min={1}
max={20}
value={settings.visionBridgeMaxImages}
onChange={(e) =>
setSettings((prev) => ({
...prev,
visionBridgeMaxImages: Number.parseInt(e.target.value, 10) || 0,
}))
}
onBlur={() =>
updateSetting({
visionBridgeMaxImages: Math.min(
20,
Math.max(
1,
settings.visionBridgeMaxImages || VISION_BRIDGE_DEFAULTS.maxImagesPerRequest
)
),
})
}
className="w-full rounded-lg border border-border bg-surface px-3 py-2 text-sm"
/>
</div>
</div>
</div>
</div>
</Card>
);
}

View File

@@ -19,11 +19,12 @@ interface ModalityBridgeStatsRowProps {
function parseStats(value: unknown): BridgeStats | null {
if (!value || typeof value !== "object") return null;
const record = value as Record<string, unknown>;
const lastUsedAt = record.lastUsedAt;
if (lastUsedAt !== null && typeof lastUsedAt !== "string") return null;
if (
typeof record.bridged !== "number" ||
typeof record.cacheHits !== "number" ||
typeof record.failures !== "number" ||
(record.lastUsedAt !== null && typeof record.lastUsedAt !== "string")
typeof record.failures !== "number"
) {
return null;
}
@@ -31,7 +32,7 @@ function parseStats(value: unknown): BridgeStats | null {
bridged: record.bridged,
cacheHits: record.cacheHits,
failures: record.failures,
lastUsedAt: record.lastUsedAt,
lastUsedAt: typeof lastUsedAt === "string" ? lastUsedAt : null,
};
}

View File

@@ -21,9 +21,6 @@ vi.mock("@/shared/components", () => ({
vi.mock("@/app/(dashboard)/dashboard/settings/components/ThinkingBudgetTab", () => ({
default: () => null,
}));
vi.mock("@/app/(dashboard)/dashboard/settings/components/VisionBridgeSettingsTab", () => ({
default: () => <div data-testid="legacy-vision-card">legacy</div>,
}));
vi.mock("@/app/(dashboard)/dashboard/settings/components/SystemPromptTab", () => ({
default: () => null,
}));
@@ -122,7 +119,7 @@ describe("Modality Bridge relocation and shortcuts", () => {
expect(card.querySelector('a[href="/dashboard/settings/modality-bridge"]')).toBeTruthy();
const page = await render(<SettingsAiPage />);
expect(page.querySelector('[data-testid="legacy-vision-card"]')).toBeNull();
expect(page.textContent).not.toContain("visionBridgeEnabledLabel");
expect(page.textContent).toContain("modalityBridgeMovedTitle");
});