mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 18:22:48 +03:00
Merge branch 'feat/modality-bridge-page' into feat/audio-bridge
This commit is contained in:
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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");
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user