mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-12 02:02:13 +03:00
feat(dashboard): relocate vision bridge card to link + media-providers shortcuts
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useEffect } from "react";
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Button } from "@/shared/components";
|
||||
import MediaProviderHeader from "../../components/MediaProviderHeader";
|
||||
@@ -39,7 +40,10 @@ interface MediaProviderPageClientProps {
|
||||
function renderPlayground(
|
||||
kind: MediaKind,
|
||||
providerId: string,
|
||||
imageToTextCopy: { title: string; description: React.ReactNode }
|
||||
bridgeCopy: {
|
||||
imageToText: { title: string; description: React.ReactNode; cta: string };
|
||||
sttCta: string;
|
||||
}
|
||||
) {
|
||||
switch (kind) {
|
||||
case "embedding":
|
||||
@@ -49,7 +53,17 @@ function renderPlayground(
|
||||
case "tts":
|
||||
return <TtsExampleCard providerId={providerId} />;
|
||||
case "stt":
|
||||
return <SttExampleCard providerId={providerId} />;
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
<SttExampleCard providerId={providerId} />
|
||||
<Link
|
||||
href="/dashboard/settings/modality-bridge?tab=audio"
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
{bridgeCopy.sttCta}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
case "webSearch":
|
||||
return <WebSearchExampleCard providerId={providerId} />;
|
||||
case "webFetch":
|
||||
@@ -66,9 +80,15 @@ function renderPlayground(
|
||||
<div className="flex flex-col gap-2 border border-dashed border-border rounded-xl p-6">
|
||||
<div className="flex items-center gap-2 text-text-muted">
|
||||
<span className="material-symbols-outlined text-[20px]">image_search</span>
|
||||
<h3 className="text-sm font-medium">{imageToTextCopy.title}</h3>
|
||||
<h3 className="text-sm font-medium">{bridgeCopy.imageToText.title}</h3>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">{imageToTextCopy.description}</p>
|
||||
<p className="text-xs text-text-muted">{bridgeCopy.imageToText.description}</p>
|
||||
<Link
|
||||
href="/dashboard/settings/modality-bridge?tab=vision"
|
||||
className="text-xs text-primary hover:underline"
|
||||
>
|
||||
{bridgeCopy.imageToText.cta}
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
@@ -197,10 +217,14 @@ export default function MediaProviderPageClient({
|
||||
|
||||
{/* Playground */}
|
||||
{renderPlayground(activeKind, providerId, {
|
||||
title: t("imageToText"),
|
||||
description: t.rich("imageToTextComingSoon", {
|
||||
code: (chunks) => <code className="font-mono bg-bg-subtle px-1 rounded">{chunks}</code>,
|
||||
}),
|
||||
imageToText: {
|
||||
title: t("imageToText"),
|
||||
description: t.rich("imageToTextComingSoon", {
|
||||
code: (chunks) => <code className="rounded bg-bg-subtle px-1 font-mono">{chunks}</code>,
|
||||
}),
|
||||
cta: t("imageToTextBridgeCta"),
|
||||
},
|
||||
sttCta: t("sttBridgeCta"),
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { useTranslations } from "next-intl";
|
||||
import ThinkingBudgetTab from "../components/ThinkingBudgetTab";
|
||||
import VisionBridgeSettingsTab from "../components/VisionBridgeSettingsTab";
|
||||
import ModalityBridgeMovedCard from "../components/ModalityBridgeMovedCard";
|
||||
import SystemPromptTab from "../components/SystemPromptTab";
|
||||
import ResponsesStatePolicyTab from "../components/ResponsesStatePolicyTab";
|
||||
import CodexFastTierTab from "../components/CodexFastTierTab";
|
||||
@@ -19,7 +19,7 @@ export default function SettingsAiPage() {
|
||||
<div className="space-y-6">
|
||||
<p className="text-sm text-text-muted">{t("aiSettingsIntro")}</p>
|
||||
<ThinkingBudgetTab />
|
||||
<VisionBridgeSettingsTab />
|
||||
<ModalityBridgeMovedCard />
|
||||
<SystemPromptTab />
|
||||
<ResponsesStatePolicyTab />
|
||||
<UsageTokenBufferTab />
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { useTranslations } from "next-intl";
|
||||
|
||||
// One-cycle migration notice. The legacy settings component remains in the
|
||||
// codebase for rollback, but Settings → AI no longer renders or writes it.
|
||||
export default function ModalityBridgeMovedCard() {
|
||||
const t = useTranslations("settings");
|
||||
|
||||
return (
|
||||
<section className="rounded-lg border border-border/70 bg-surface/40 p-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<span className="material-symbols-outlined text-[21px] text-fuchsia-500" aria-hidden="true">
|
||||
image_search
|
||||
</span>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h4 className="text-base font-semibold text-text-main">
|
||||
{t("modalityBridgeMovedTitle")}
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-text-muted">{t("modalityBridgeMovedBody")}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 border-t border-border pt-3 text-sm">
|
||||
<Link href="/dashboard/settings/modality-bridge" className="text-primary hover:underline">
|
||||
{t("modalityBridgeMovedCta")}
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
);
|
||||
}
|
||||
@@ -2295,7 +2295,7 @@
|
||||
"suggestedModels": "Suggested models from provider",
|
||||
"imageGeneration": "Image Generation",
|
||||
"imageToText": "Image to Text",
|
||||
"imageToTextComingSoon": "The inline Image-to-Text playground will be available when <code>/api/v1/images/understanding</code> is implemented.",
|
||||
"imageToTextComingSoon": "Modality Bridge can describe images for text-only models now. The inline playground will be available when <code>/api/v1/images/understanding</code> is implemented.",
|
||||
"imageToTextBridgeCta": "Configure the Image→Text bridge in Modality Bridge settings",
|
||||
"sttBridgeCta": "Configure the Speech→Text bridge in Modality Bridge settings",
|
||||
"disabled": "Disabled",
|
||||
|
||||
159
tests/unit/ui/modality-bridge-moved-card.test.tsx
Normal file
159
tests/unit/ui/modality-bridge-moved-card.test.tsx
Normal file
@@ -0,0 +1,159 @@
|
||||
// @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 MediaProviderPageClient from "@/app/(dashboard)/dashboard/media-providers/[kind]/[id]/MediaProviderPageClient";
|
||||
import SettingsAiPage from "@/app/(dashboard)/dashboard/settings/ai/page";
|
||||
import ModalityBridgeMovedCard from "@/app/(dashboard)/dashboard/settings/components/ModalityBridgeMovedCard";
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () =>
|
||||
Object.assign((key: string) => key, {
|
||||
rich: (key: string) => key,
|
||||
}),
|
||||
}));
|
||||
|
||||
vi.mock("@/shared/components", () => ({
|
||||
Button: ({ children }: { children: React.ReactNode }) => <button>{children}</button>,
|
||||
}));
|
||||
|
||||
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,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/settings/components/ResponsesStatePolicyTab", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/settings/components/CodexFastTierTab", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/settings/components/CodexAutoPingTab", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/settings/components/ClaudeFastModeTab", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/settings/components/MemorySkillsTab", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/settings/components/ModelsDevSyncTab", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/settings/components/UsageTokenBufferTab", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/settings/components/ModelCapabilityOverridesTab", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/MediaProviderHeader", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/MediaProviderKindNav", () => ({
|
||||
default: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/EmbeddingExampleCard", () => ({
|
||||
EmbeddingExampleCard: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/ImageExampleCard", () => ({
|
||||
ImageExampleCard: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/TtsExampleCard", () => ({
|
||||
TtsExampleCard: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/SttExampleCard", () => ({
|
||||
SttExampleCard: () => <div data-testid="stt-playground">stt</div>,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/WebSearchExampleCard", () => ({
|
||||
WebSearchExampleCard: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/WebFetchExampleCard", () => ({
|
||||
WebFetchExampleCard: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/VideoExampleCard", () => ({
|
||||
VideoExampleCard: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/MusicExampleCard", () => ({
|
||||
MusicExampleCard: () => null,
|
||||
}));
|
||||
vi.mock("@/app/(dashboard)/dashboard/media-providers/components/OcrExampleCard", () => ({
|
||||
OcrExampleCard: () => null,
|
||||
}));
|
||||
|
||||
const roots: Array<{ root: Root; el: HTMLDivElement }> = [];
|
||||
|
||||
async function render(node: React.ReactNode): Promise<HTMLDivElement> {
|
||||
const el = document.createElement("div");
|
||||
document.body.appendChild(el);
|
||||
const root = createRoot(el);
|
||||
await act(async () => {
|
||||
root.render(node);
|
||||
await new Promise((resolve) => setTimeout(resolve, 0));
|
||||
});
|
||||
roots.push({ root, el });
|
||||
return el;
|
||||
}
|
||||
|
||||
describe("Modality Bridge relocation and shortcuts", () => {
|
||||
beforeEach(() => {
|
||||
(globalThis as { IS_REACT_ACT_ENVIRONMENT?: boolean }).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
vi.stubGlobal(
|
||||
"fetch",
|
||||
vi.fn(async () => new Response(JSON.stringify({ connections: [] }), { status: 200 }))
|
||||
);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
for (const { root, el } of roots.splice(0)) {
|
||||
act(() => root.unmount());
|
||||
el.remove();
|
||||
}
|
||||
vi.unstubAllGlobals();
|
||||
});
|
||||
|
||||
it("renders the moved card as a link and removes the legacy settings card", async () => {
|
||||
const card = await render(<ModalityBridgeMovedCard />);
|
||||
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).toContain("modalityBridgeMovedTitle");
|
||||
});
|
||||
|
||||
it("links Image-to-Text providers to the Vision tab", async () => {
|
||||
const el = await render(
|
||||
<MediaProviderPageClient
|
||||
providerId="openai"
|
||||
providerName="OpenAI"
|
||||
kindLabel="Image to Text"
|
||||
activeKind="imageToText"
|
||||
/>
|
||||
);
|
||||
expect(
|
||||
el.querySelector('a[href="/dashboard/settings/modality-bridge?tab=vision"]')
|
||||
).toBeTruthy();
|
||||
expect(el.textContent).toContain("imageToTextBridgeCta");
|
||||
});
|
||||
|
||||
it("adds the Audio shortcut without removing the existing STT playground", async () => {
|
||||
const el = await render(
|
||||
<MediaProviderPageClient
|
||||
providerId="openrouter"
|
||||
providerName="OpenRouter"
|
||||
kindLabel="Speech to Text"
|
||||
activeKind="stt"
|
||||
/>
|
||||
);
|
||||
expect(el.querySelector('[data-testid="stt-playground"]')).toBeTruthy();
|
||||
expect(
|
||||
el.querySelector('a[href="/dashboard/settings/modality-bridge?tab=audio"]')
|
||||
).toBeTruthy();
|
||||
expect(el.textContent).toContain("sttBridgeCta");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user