From 904a54d60226dbf1f95f565bc423a08f9f2d20ac Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 8 Aug 2026 08:34:54 -0300 Subject: [PATCH] feat(dashboard): relocate vision bridge card to link + media-providers shortcuts --- .../[kind]/[id]/MediaProviderPageClient.tsx | 40 ++++- .../dashboard/settings/ai/page.tsx | 4 +- .../components/ModalityBridgeMovedCard.tsx | 32 ++++ src/i18n/messages/en.json | 2 +- .../ui/modality-bridge-moved-card.test.tsx | 159 ++++++++++++++++++ 5 files changed, 226 insertions(+), 11 deletions(-) create mode 100644 src/app/(dashboard)/dashboard/settings/components/ModalityBridgeMovedCard.tsx create mode 100644 tests/unit/ui/modality-bridge-moved-card.test.tsx diff --git a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/MediaProviderPageClient.tsx b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/MediaProviderPageClient.tsx index f2430c68d2..3bfd7c850b 100644 --- a/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/MediaProviderPageClient.tsx +++ b/src/app/(dashboard)/dashboard/media-providers/[kind]/[id]/MediaProviderPageClient.tsx @@ -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 ; case "stt": - return ; + return ( +
+ + + {bridgeCopy.sttCta} + +
+ ); case "webSearch": return ; case "webFetch": @@ -66,9 +80,15 @@ function renderPlayground(
image_search -

{imageToTextCopy.title}

+

{bridgeCopy.imageToText.title}

-

{imageToTextCopy.description}

+

{bridgeCopy.imageToText.description}

+ + {bridgeCopy.imageToText.cta} +
); default: @@ -197,10 +217,14 @@ export default function MediaProviderPageClient({ {/* Playground */} {renderPlayground(activeKind, providerId, { - title: t("imageToText"), - description: t.rich("imageToTextComingSoon", { - code: (chunks) => {chunks}, - }), + imageToText: { + title: t("imageToText"), + description: t.rich("imageToTextComingSoon", { + code: (chunks) => {chunks}, + }), + cta: t("imageToTextBridgeCta"), + }, + sttCta: t("sttBridgeCta"), })} ); diff --git a/src/app/(dashboard)/dashboard/settings/ai/page.tsx b/src/app/(dashboard)/dashboard/settings/ai/page.tsx index a640fd356e..754942c534 100644 --- a/src/app/(dashboard)/dashboard/settings/ai/page.tsx +++ b/src/app/(dashboard)/dashboard/settings/ai/page.tsx @@ -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() {

{t("aiSettingsIntro")}

- + diff --git a/src/app/(dashboard)/dashboard/settings/components/ModalityBridgeMovedCard.tsx b/src/app/(dashboard)/dashboard/settings/components/ModalityBridgeMovedCard.tsx new file mode 100644 index 0000000000..fcdde6f1f2 --- /dev/null +++ b/src/app/(dashboard)/dashboard/settings/components/ModalityBridgeMovedCard.tsx @@ -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 ( +
+
+ +
+

+ {t("modalityBridgeMovedTitle")} +

+

{t("modalityBridgeMovedBody")}

+
+
+ +
+ + {t("modalityBridgeMovedCta")} + +
+
+ ); +} diff --git a/src/i18n/messages/en.json b/src/i18n/messages/en.json index c01891be5f..f51be80b07 100644 --- a/src/i18n/messages/en.json +++ b/src/i18n/messages/en.json @@ -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 /api/v1/images/understanding is implemented.", + "imageToTextComingSoon": "Modality Bridge can describe images for text-only models now. The inline playground will be available when /api/v1/images/understanding is implemented.", "imageToTextBridgeCta": "Configure the Image→Text bridge in Modality Bridge settings", "sttBridgeCta": "Configure the Speech→Text bridge in Modality Bridge settings", "disabled": "Disabled", diff --git a/tests/unit/ui/modality-bridge-moved-card.test.tsx b/tests/unit/ui/modality-bridge-moved-card.test.tsx new file mode 100644 index 0000000000..8eb1b83335 --- /dev/null +++ b/tests/unit/ui/modality-bridge-moved-card.test.tsx @@ -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 }) => , +})); + +vi.mock("@/app/(dashboard)/dashboard/settings/components/ThinkingBudgetTab", () => ({ + default: () => null, +})); +vi.mock("@/app/(dashboard)/dashboard/settings/components/VisionBridgeSettingsTab", () => ({ + default: () =>
legacy
, +})); +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: () =>
stt
, +})); +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 { + 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(); + expect(card.querySelector('a[href="/dashboard/settings/modality-bridge"]')).toBeTruthy(); + + const page = await render(); + 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( + + ); + 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( + + ); + 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"); + }); +});