feat(dashboard): relocate vision bridge card to link + media-providers shortcuts

This commit is contained in:
diegosouzapw
2026-08-08 08:34:54 -03:00
parent 1b9cd59740
commit 904a54d602
5 changed files with 226 additions and 11 deletions

View File

@@ -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>
);

View File

@@ -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 />

View File

@@ -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>
);
}

View File

@@ -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",

View 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");
});
});