Merge F9 (TranslatorPageClient 2-tab shell + integration tests + legacy *Mode removal) into refactor/pages-v3-19

This commit is contained in:
diegosouzapw
2026-05-28 02:43:06 -03:00
8 changed files with 813 additions and 2271 deletions

View File

@@ -1,171 +1,192 @@
"use client";
import { Suspense, useState } from "react";
import { useTranslations } from "next-intl";
import { useCallback, useState } from "react";
import { Badge, Card, SegmentedControl } from "@/shared/components";
import PlaygroundMode from "./components/PlaygroundMode";
import ChatTesterMode from "./components/ChatTesterMode";
import TestBenchMode from "./components/TestBenchMode";
import LiveMonitorMode from "./components/LiveMonitorMode";
import StreamTransformerMode from "./components/StreamTransformerMode";
import TranslatorConceptCard from "./components/TranslatorConceptCard";
import TranslateTab from "./components/TranslateTab";
import MonitorTab from "./components/MonitorTab";
import AdvancedSection from "./components/advanced/AdvancedSection";
import RawJsonPanel from "./components/advanced/RawJsonPanel";
import PipelineView from "./components/advanced/PipelineView";
import StreamTransformerAccordion from "./components/advanced/StreamTransformerAccordion";
import TestBenchAccordion from "./components/advanced/TestBenchAccordion";
import CompressionPreviewAccordion from "./components/advanced/CompressionPreviewAccordion";
import { useTranslateDeepLink } from "./hooks/useTranslateDeepLink";
import type { AdvancedSlug, TranslatorTab } from "./types";
export default function TranslatorPageClient() {
const t = useTranslations("translator");
const [showFeatures, setShowFeatures] = useState(false);
const translateOrFallback = useCallback(
(key: string, fallback: string) => {
try {
const translated = t(key);
return translated === key || translated === `translator.${key}` ? fallback : translated;
} catch {
return fallback;
}
},
[t]
return (
<Suspense fallback={<div className="p-8 text-text-muted">Loading</div>}>
<TranslatorPageClientInner />
</Suspense>
);
const [mode, setMode] = useState("playground");
const modes = [
{ value: "playground", label: translateOrFallback("playground", "Playground"), icon: "code" },
{
value: "chat-tester",
label: translateOrFallback("chatTester", "Chat Tester"),
icon: "chat",
},
{
value: "test-bench",
label: translateOrFallback("testBench", "Test Bench"),
icon: "science",
},
{
value: "stream-transformer",
label: translateOrFallback("streamTransformer", "Stream Transformer"),
icon: "swap_horiz",
},
{
value: "live-monitor",
label: translateOrFallback("liveMonitor", "Live Monitor"),
icon: "monitoring",
},
];
const modeDescriptions: Record<string, string> = {
playground: translateOrFallback(
"modeDescriptionPlayground",
"Inspect request translation step-by-step between API formats."
),
"chat-tester": translateOrFallback(
"modeDescriptionChatTester",
"Send a real prompt through the selected provider and inspect every translation stage."
),
"test-bench": translateOrFallback(
"modeDescriptionTestBench",
"Run compatibility scenarios across source formats and target providers."
),
"stream-transformer": translateOrFallback(
"modeDescriptionStreamTransformer",
"Transform Chat Completions SSE into Responses API SSE and inspect emitted events."
),
"live-monitor": translateOrFallback(
"modeDescriptionLiveMonitor",
"Watch translation events in real time as requests flow through OmniRoute."
),
}
function TranslatorPageClientInner() {
const t = useTranslations("translator");
const [sharedInputContent, setSharedInputContent] = useState("");
const { state, setTab, setAdvanced } = useTranslateDeepLink();
const makeOpenHandler = (slug: AdvancedSlug) => (open: boolean) => {
if (open) {
setAdvanced(slug);
} else if (state.advanced === slug) {
setAdvanced(null);
}
};
const advancedSlot = (
<AdvancedSection forceOpenSlug={state.advanced}>
<RawJsonPanel
slug="rawjson"
forceOpen={state.advanced === "rawjson"}
onOpenChange={makeOpenHandler("rawjson")}
/>
<PipelineView
slug="pipeline"
forceOpen={state.advanced === "pipeline"}
onOpenChange={makeOpenHandler("pipeline")}
steps={[]}
/>
<StreamTransformerAccordion
forceOpen={state.advanced === "streamtransform"}
onOpenChange={makeOpenHandler("streamtransform")}
/>
<TestBenchAccordion
slug="testbench"
forceOpen={state.advanced === "testbench"}
onOpenChange={makeOpenHandler("testbench")}
/>
<CompressionPreviewAccordion
forceOpen={state.advanced === "compression"}
onOpenChange={makeOpenHandler("compression")}
inputContent={sharedInputContent}
/>
</AdvancedSection>
);
const tabOptions = [
{ value: "translate", label: t("tabTranslate"), icon: "translate" },
{ value: "monitor", label: t("tabMonitor"), icon: "monitoring" },
];
return (
<div className="space-y-6 min-w-0">
<TranslatorConceptCard />
<AutoFeaturesCard />
<div className="flex justify-end min-w-0 overflow-x-auto">
<SegmentedControl
options={modes}
value={mode}
onChange={setMode}
options={tabOptions}
value={state.tab}
onChange={(v) => setTab(v as TranslatorTab)}
size="md"
aria-label={t("tabTranslateAriaLabel")}
className="min-w-max"
/>
</div>
<Card className="border-primary/10 bg-primary/5">
<button
onClick={() => setShowFeatures((prev) => !prev)}
className="flex w-full items-center justify-between p-4 text-left"
>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[20px] text-primary">
auto_fix_high
</span>
<h3 className="text-sm font-semibold text-text-main">{t("autoFeaturesTitle")}</h3>
<Badge variant="primary" size="sm">
{t("autoFeaturesCount")}
</Badge>
</div>
<span className="material-symbols-outlined text-[18px] text-text-muted">
{showFeatures ? "expand_less" : "expand_more"}
</span>
</button>
{state.tab === "translate" && (
<TranslateTab
forceOpenAdvancedSlug={state.advanced}
onAdvancedSlugChange={(slug) => setAdvanced(slug)}
/>
)}
{showFeatures && (
<div className="grid grid-cols-1 gap-3 px-4 pb-4 sm:grid-cols-2 lg:grid-cols-4">
<FeatureChip
icon="psychology"
title={t("featureReasoningCache")}
description={t("featureReasoningCacheDesc")}
color="purple"
/>
<FeatureChip
icon="schema"
title={t("featureSchemaCoercion")}
description={t("featureSchemaCoercionDesc")}
color="blue"
/>
<FeatureChip
icon="swap_vert"
title={t("featureRoleNormalization")}
description={t("featureRoleNormalizationDesc")}
color="amber"
/>
<FeatureChip
icon="fingerprint"
title={t("featureToolCallIds")}
description={t("featureToolCallIdsDesc")}
color="emerald"
/>
<FeatureChip
icon="add_circle"
title={t("featureMissingToolResponse")}
description={t("featureMissingToolResponseDesc")}
color="cyan"
/>
<FeatureChip
icon="tune"
title={t("featureThinkingBudget")}
description={t("featureThinkingBudgetDesc")}
color="orange"
/>
<FeatureChip
icon="alt_route"
title={t("featureDirectPaths")}
description={t("featureDirectPathsDesc")}
color="pink"
/>
<FeatureChip
icon="photo_size_select_large"
title={t("featureImageMapping")}
description={t("featureImageMappingDesc")}
color="indigo"
/>
</div>
)}
</Card>
{state.tab === "translate" && advancedSlot}
{/* Mode Content */}
{mode === "playground" && <PlaygroundMode />}
{mode === "chat-tester" && <ChatTesterMode />}
{mode === "test-bench" && <TestBenchMode />}
{mode === "stream-transformer" && <StreamTransformerMode />}
{mode === "live-monitor" && <LiveMonitorMode />}
{state.tab === "monitor" && (
<MonitorTab onGoToTranslate={() => setTab("translate")} />
)}
</div>
);
}
function AutoFeaturesCard() {
const t = useTranslations("translator");
const [showFeatures, setShowFeatures] = useState(false);
return (
<Card className="border-primary/10 bg-primary/5">
<button
onClick={() => setShowFeatures((prev) => !prev)}
className="flex w-full items-center justify-between p-4 text-left"
>
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[20px] text-primary">
auto_fix_high
</span>
<h3 className="text-sm font-semibold text-text-main">{t("autoFeaturesTitle")}</h3>
<Badge variant="primary" size="sm">
{t("autoFeaturesCount")}
</Badge>
</div>
<span className="material-symbols-outlined text-[18px] text-text-muted">
{showFeatures ? "expand_less" : "expand_more"}
</span>
</button>
{showFeatures && (
<div
className="grid grid-cols-1 gap-3 px-4 pb-4 sm:grid-cols-2 lg:grid-cols-4"
data-testid="auto-features-grid"
>
<FeatureChip
icon="psychology"
title={t("featureReasoningCache")}
description={t("featureReasoningCacheDesc")}
color="purple"
/>
<FeatureChip
icon="schema"
title={t("featureSchemaCoercion")}
description={t("featureSchemaCoercionDesc")}
color="blue"
/>
<FeatureChip
icon="swap_vert"
title={t("featureRoleNormalization")}
description={t("featureRoleNormalizationDesc")}
color="amber"
/>
<FeatureChip
icon="fingerprint"
title={t("featureToolCallIds")}
description={t("featureToolCallIdsDesc")}
color="emerald"
/>
<FeatureChip
icon="add_circle"
title={t("featureMissingToolResponse")}
description={t("featureMissingToolResponseDesc")}
color="cyan"
/>
<FeatureChip
icon="tune"
title={t("featureThinkingBudget")}
description={t("featureThinkingBudgetDesc")}
color="orange"
/>
<FeatureChip
icon="alt_route"
title={t("featureDirectPaths")}
description={t("featureDirectPathsDesc")}
color="pink"
/>
<FeatureChip
icon="photo_size_select_large"
title={t("featureImageMapping")}
description={t("featureImageMappingDesc")}
color="indigo"
/>
</div>
)}
</Card>
);
}
function FeatureChip({
icon,
title,
@@ -213,7 +234,7 @@ function FeatureChip({
}[color];
return (
<div className={`rounded-lg border p-3 ${colorMap.shell}`}>
<div className={`rounded-lg border p-3 ${colorMap.shell}`} data-testid="feature-chip">
<div className="mb-1 flex items-center gap-2">
<span className={`material-symbols-outlined text-[16px] ${colorMap.icon}`}>{icon}</span>
<p className="text-xs font-semibold text-text-main">{title}</p>

View File

@@ -1,543 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useRef } from "react";
import { Card, Button, Select, Badge } from "@/shared/components";
import { FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
import { useProviderOptions } from "../hooks/useProviderOptions";
import { useAvailableModels } from "../hooks/useAvailableModels";
import Editor from "@/shared/components/MonacoEditor";
/**
* Chat Tester Mode:
* - Left: Chat interface (send messages as a specific client format)
* - Right: {t("pipelineVisualization")} showing each translation step
*
* How it works:
* 1. You type a message and select a "Client Format" (how the request is structured)
* 2. The message is built into a request body matching the client format
* 3. OmniRoute detects the format, translates it through the pipeline, and sends to the provider
* 4. Each pipeline step is shown on the right: Client → Detect → OpenAI → Provider → Response
*/
export default function ChatTesterMode() {
const t = useTranslations("translator");
const { provider, setProvider, providerOptions } = useProviderOptions("openai");
const { model, setModel, availableModels, pickModelForFormat } = useAvailableModels();
const [clientFormat, setClientFormat] = useState("openai");
const [message, setMessage] = useState("");
const [sending, setSending] = useState(false);
const [chatHistory, setChatHistory] = useState([]);
const [pipeline, setPipeline] = useState(null);
const [expandedStep, setExpandedStep] = useState(null);
const messagesEndRef = useRef(null);
// Pick a smart default model when format changes or models finish loading
useEffect(() => {
const picked = pickModelForFormat(clientFormat);
if (picked) setModel(picked);
}, [clientFormat, pickModelForFormat, setModel]);
const scrollToBottom = () => {
messagesEndRef.current?.scrollIntoView({ behavior: "smooth" });
};
const handleSend = async () => {
if (!message.trim() || sending) return;
const userMessage = message.trim();
setMessage("");
setSending(true);
setChatHistory((prev) => [...prev, { role: "user", content: userMessage }]);
const steps = [];
try {
// Build the messages array
const allMessages = [
...chatHistory.map((m) => ({ role: m.role, content: m.content })),
{ role: "user", content: userMessage },
];
// Step 1: Build client request in the chosen format
let clientRequest;
if (clientFormat === "claude") {
clientRequest = {
model,
max_tokens: 1024,
messages: allMessages,
stream: true,
};
} else if (clientFormat === "gemini") {
clientRequest = {
model,
contents: allMessages.map((m) => ({
role: m.role === "assistant" ? "model" : "user",
parts: [{ text: m.content }],
})),
};
} else if (clientFormat === "antigravity") {
clientRequest = {
request: {
contents: allMessages.map((m) => ({
role: m.role === "assistant" ? "model" : "user",
parts: [{ text: m.content }],
})),
},
model,
userAgent: "antigravity",
};
} else if (clientFormat === "openai-responses") {
clientRequest = {
model,
input: allMessages.map((m) => ({
type: "message",
role: m.role,
content: [{ type: "input_text", text: m.content }],
})),
stream: true,
};
} else if (clientFormat === "cursor" || clientFormat === "kiro") {
clientRequest = {
model,
messages: allMessages,
stream: true,
};
} else {
clientRequest = {
model,
messages: allMessages,
stream: true,
};
}
steps.push({
id: 1,
name: t("clientRequest"),
description: t("clientRequestDescription"),
format: clientFormat,
content: JSON.stringify(clientRequest, null, 2),
status: "done",
});
// Step 2: Detect source format
const detectRes = await fetch("/api/translator/detect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: clientRequest }),
});
const detectData = await detectRes.json();
const detectedFormat = detectData.format || clientFormat;
steps.push({
id: 2,
name: t("formatDetected"),
description: t("formatDetectedDescription"),
format: detectedFormat,
content: JSON.stringify(
{ detectedFormat, clientFormat, match: detectedFormat === clientFormat },
null,
2
),
status: "done",
});
// Step 3: Translate to OpenAI intermediate
const toOpenaiRes = await fetch("/api/translator/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
step: "direct",
sourceFormat: detectedFormat,
targetFormat: "openai",
body: clientRequest,
}),
});
const toOpenaiData = await toOpenaiRes.json();
steps.push({
id: 3,
name: t("openaiIntermediate"),
description: t("openaiIntermediateDescription"),
format: "openai",
content: JSON.stringify(toOpenaiData.result || toOpenaiData, null, 2),
status: toOpenaiData.success ? "done" : "error",
});
// Step 4: Translate to provider target format
const providerTargetRes = await fetch("/api/translator/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
step: "direct",
sourceFormat: "openai",
provider,
body: toOpenaiData.result,
}),
});
const providerTargetData = await providerTargetRes.json();
const targetFmt = providerTargetData.targetFormat || "openai";
steps.push({
id: 4,
name: t("providerFormat"),
description: t("providerFormatDescription"),
format: targetFmt,
content: JSON.stringify(providerTargetData.result || providerTargetData, null, 2),
status: providerTargetData.success ? "done" : "error",
});
// Step 5: Send to provider
const sendRes = await fetch("/api/translator/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, body: providerTargetData.result || toOpenaiData.result }),
});
if (!sendRes.ok) {
const errData = await sendRes.json().catch(() => ({ error: t("requestFailed") }));
steps.push({
id: 5,
name: t("providerResponse"),
description: t("providerResponseRawDescription"),
format: targetFmt,
content: JSON.stringify(errData, null, 2),
status: "error",
});
setChatHistory((prev) => [
...prev,
{
role: "assistant",
content: t("errorMessage", { message: errData.error || t("requestFailed") }),
},
]);
} else {
// Read streaming response
const reader = sendRes.body.getReader();
const decoder = new TextDecoder();
let fullResponse = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
fullResponse += decoder.decode(value, { stream: true });
}
steps.push({
id: 5,
name: t("providerResponse"),
description: t("providerResponseSseDescription"),
format: targetFmt,
content:
fullResponse.slice(0, 5000) + (fullResponse.length > 5000 ? "\n... (truncated)" : ""),
status: "done",
});
// Extract assistant text from SSE
const assistantText = extractAssistantText(fullResponse);
setChatHistory((prev) => [
...prev,
{ role: "assistant", content: assistantText || t("noTextExtracted") },
]);
}
} catch (err) {
steps.push({
id: steps.length + 1,
name: t("error"),
description: t("unexpectedError"),
format: "error",
content: JSON.stringify({ error: err.message }, null, 2),
status: "error",
});
setChatHistory((prev) => [
...prev,
{ role: "assistant", content: t("errorMessage", { message: err.message }) },
]);
}
setPipeline(steps);
setExpandedStep(steps.length > 0 ? steps[steps.length - 1].id : null);
setSending(false);
setTimeout(scrollToBottom, 100);
};
return (
<div className="space-y-4 min-w-0">
{/* Info Banner */}
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
info
</span>
<div>
<p className="font-medium text-text-main mb-0.5">{t("pipelineDebugger")}</p>
<p>{t("chatTesterDescription")}</p>
<p>
<strong className="text-text-main">{t("chatTesterFlow")}</strong>.{" "}
{t("clickStepToInspect")}
</p>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-4 min-w-0">
{/* Left: Chat Interface */}
<div className="space-y-4">
{/* Controls */}
<Card>
<div className="p-4 flex flex-col gap-3">
<div className="flex flex-col sm:flex-row gap-3">
<div className="flex-1">
<label className="block text-xs font-medium text-text-muted mb-1 uppercase tracking-wider">
{t("clientFormat")}
</label>
<Select
value={clientFormat}
onChange={(e) => setClientFormat(e.target.value)}
options={FORMAT_OPTIONS}
/>
</div>
<div className="flex-1">
<label className="block text-xs font-medium text-text-muted mb-1 uppercase tracking-wider">
{t("provider")}
</label>
<Select
value={provider}
onChange={(e) => setProvider(e.target.value)}
options={providerOptions}
/>
</div>
</div>
<div>
<label className="block text-xs font-medium text-text-muted mb-1 uppercase tracking-wider">
{t("model")}
</label>
<div className="relative">
<input
type="text"
value={model}
onChange={(e) => setModel(e.target.value)}
list="model-suggestions"
placeholder={t("modelPlaceholder")}
className="w-full bg-bg-subtle border border-border rounded-lg px-3 py-2 text-sm text-text-main placeholder:text-text-muted focus:outline-none focus:border-primary transition-colors"
/>
<datalist id="model-suggestions">
{availableModels.map((m) => (
<option key={m} value={m} />
))}
</datalist>
</div>
</div>
</div>
</Card>
{/* Chat Messages */}
<Card className="min-h-[400px] flex flex-col">
<div className="p-4 flex-1 overflow-y-auto max-h-[500px] space-y-3">
{chatHistory.length === 0 && (
<div className="flex flex-col items-center justify-center h-full text-text-muted py-12">
<span className="material-symbols-outlined text-[48px] mb-3 opacity-30">
chat
</span>
<p className="text-sm font-medium mb-1">{t("sendMessageToSeePipeline")}</p>
<p className="text-xs text-center max-w-xs">
{t("chatMessageHintPrefix")} <strong>{FORMAT_META[clientFormat]?.label}</strong>{" "}
{t("chatMessageHintSuffix")}
</p>
</div>
)}
{chatHistory.map((msg, i) => (
<div
key={i}
className={`flex ${msg.role === "user" ? "justify-end" : "justify-start"}`}
>
<div
className={`max-w-[85%] rounded-lg px-3 py-2 text-sm ${
msg.role === "user"
? "bg-primary/10 text-text-main border border-primary/20"
: "bg-bg-subtle text-text-main border border-border"
}`}
>
<p className="text-[10px] font-semibold text-text-muted mb-1 uppercase">
{msg.role === "user"
? t("youWithFormat", { format: FORMAT_META[clientFormat]?.label })
: t("assistant")}
</p>
<p className="whitespace-pre-wrap break-words">{msg.content}</p>
</div>
</div>
))}
<div ref={messagesEndRef} />
</div>
{/* Input */}
<div className="p-3 border-t border-border">
<div className="flex gap-2">
<input
type="text"
value={message}
onChange={(e) => setMessage(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && !e.shiftKey && handleSend()}
placeholder={t("typeMessage")}
className="flex-1 bg-bg-subtle border border-border rounded-lg px-3 py-2 text-sm text-text-main placeholder:text-text-muted focus:outline-none focus:border-primary transition-colors"
disabled={sending}
/>
<Button
icon="send"
onClick={handleSend}
loading={sending}
disabled={!message.trim() || sending}
>
{t("send")}
</Button>
</div>
</div>
</Card>
</div>
{/* Right: Pipeline Visualization */}
<div className="space-y-4">
<Card>
<div className="p-4 space-y-1">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-primary">
account_tree
</span>
<h3 className="text-sm font-semibold text-text-main">{t("translationPipeline")}</h3>
</div>
<p className="text-xs text-text-muted">{t("clickStepToInspect")}</p>
</div>
</Card>
{!pipeline ? (
<Card>
<div className="p-8 flex flex-col items-center justify-center text-text-muted">
<span className="material-symbols-outlined text-[48px] mb-3 opacity-30">
account_tree
</span>
<p className="text-sm font-medium mb-1">{t("pipelineVisualization")}</p>
<p className="text-xs text-center max-w-xs">{t("pipelineVisualizationHint")}</p>
</div>
</Card>
) : (
<div className="space-y-2">
{pipeline.map((step, i) => {
const meta = FORMAT_META[step.format] || {
label: step.format,
color: "gray",
icon: "code",
};
const isExpanded = expandedStep === step.id;
return (
<div key={step.id}>
{/* Connector line */}
{i > 0 && (
<div className="flex justify-center py-1">
<div className="w-px h-4 bg-border" />
</div>
)}
<Card
className={
step.status === "error"
? "border-red-500/30"
: isExpanded
? "border-primary/30"
: ""
}
>
<button
onClick={() => setExpandedStep(isExpanded ? null : step.id)}
className="w-full p-3 flex items-center gap-3 text-left"
>
{/* Step number */}
<div
className={`flex items-center justify-center w-7 h-7 rounded-full text-xs font-bold ${
step.status === "error"
? "bg-red-500/10 text-red-500"
: step.status === "done"
? `bg-${meta.color}-500/10 text-${meta.color}-500`
: "bg-bg-subtle text-text-muted"
}`}
>
{step.status === "error" ? "!" : step.id}
</div>
{/* Step info */}
<div className="flex-1 min-w-0">
<p className="text-sm font-medium text-text-main">{step.name}</p>
{step.description && (
<p className="text-[10px] text-text-muted truncate">
{step.description}
</p>
)}
</div>
{/* Format badge */}
<Badge variant={step.status === "error" ? "error" : "default"} size="sm">
{meta.label}
</Badge>
{/* Expand icon */}
<span className="material-symbols-outlined text-[18px] text-text-muted">
{isExpanded ? "expand_less" : "expand_more"}
</span>
</button>
{/* Expanded content */}
{isExpanded && (
<div className="px-3 pb-3">
<div className="border border-border rounded-lg overflow-hidden">
<Editor
height="250px"
defaultLanguage="json"
value={step.content}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 11,
lineNumbers: "on",
scrollBeyondLastLine: false,
wordWrap: "on",
automaticLayout: true,
readOnly: true,
}}
/>
</div>
</div>
)}
</Card>
</div>
);
})}
</div>
)}
</div>
</div>
</div>
);
}
/** Extract assistant text from SSE stream */
function extractAssistantText(sseText) {
let text = "";
const lines = sseText.split("\n");
for (const line of lines) {
if (!line.startsWith("data: ")) continue;
const payload = line.slice(6).trim();
if (payload === "[DONE]") break;
try {
const parsed = JSON.parse(payload);
// OpenAI format
const delta = parsed.choices?.[0]?.delta;
if (delta?.content) text += delta.content;
// Claude format
if (parsed.type === "content_block_delta" && parsed.delta?.text) {
text += parsed.delta.text;
}
} catch {
/* not JSON, skip */
}
}
return text || sseText.slice(0, 500);
}

View File

@@ -1,335 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useRef, useCallback } from "react";
import { Card, Badge } from "@/shared/components";
import { FORMAT_META } from "../exampleTemplates";
/**
* Live Monitor Mode:
* Shows recent translation activity from the proxy in real-time.
* Polls /api/translator/history for translation events.
*/
export default function LiveMonitorMode() {
const t = useTranslations("translator");
const tc = useTranslations("common");
const translateOrFallback = useCallback(
(key: string, fallback: string, values?: Record<string, unknown>) => {
try {
const translated = t(key, values);
return translated === key || translated === `translator.${key}` ? fallback : translated;
} catch {
return fallback;
}
},
[t]
);
const [events, setEvents] = useState([]);
const [loading, setLoading] = useState(true);
const [autoRefresh, setAutoRefresh] = useState(true);
const intervalRef = useRef(null);
const notAvailable = t("notAvailableSymbol");
const formatLatency = (value) => t("millisecondsShort", { value });
const fetchHistory = async () => {
try {
const res = await fetch("/api/translator/history?limit=50");
if (res.ok) {
const data = await res.json();
setEvents(data.events || []);
}
} catch {
// ignore
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchHistory();
if (autoRefresh) {
intervalRef.current = setInterval(fetchHistory, 3000);
}
return () => {
if (intervalRef.current) clearInterval(intervalRef.current);
};
}, [autoRefresh]);
// Stats
const successCount = events.filter((e) => e.status === "success").length;
const errorCount = events.filter((e) => e.status === "error").length;
const comboCount = events.filter((e) => e.isComboRouted).length;
const uniqueEndpoints = new Set(events.map((e) => e.routeEndpoint || e.endpoint).filter(Boolean))
.size;
const avgLatency =
events.length > 0
? Math.round(events.reduce((sum, e) => sum + (e.latency || 0), 0) / events.length)
: 0;
return (
<div className="space-y-5 min-w-0">
{/* Info Banner */}
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span
className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0"
aria-hidden="true"
>
info
</span>
<div>
<p className="font-medium text-text-main mb-0.5">{t("realtime")}</p>
<p>
{t("liveMonitorDescriptionPrefix")}{" "}
<strong className="text-text-main">{t("chatTester")}</strong>,{" "}
<strong className="text-text-main">{t("testBench")}</strong>
{t("liveMonitorDescriptionSuffix")}
</p>
</div>
</div>
{/* Stats Cards */}
<div className="grid grid-cols-2 md:grid-cols-3 xl:grid-cols-6 gap-3">
<StatCard
icon="translate"
label={t("totalTranslations")}
value={events.length}
color="blue"
/>
<StatCard icon="check_circle" label={t("successful")} value={successCount} color="green" />
<StatCard icon="error" label={t("errors")} value={errorCount} color="red" />
<StatCard
icon="speed"
label={t("avgLatency")}
value={formatLatency(avgLatency)}
color="purple"
/>
<StatCard
icon="hub"
label={translateOrFallback("comboRouted", "Combo-routed")}
value={comboCount}
color="amber"
/>
<StatCard
icon="lan"
label={translateOrFallback("uniqueEndpoints", "Unique endpoints")}
value={uniqueEndpoints}
color="cyan"
/>
</div>
<div className="flex items-center gap-2 rounded-lg border border-amber-500/10 bg-amber-500/5 px-3 py-2 text-xs text-amber-600 dark:text-amber-400">
<span className="material-symbols-outlined text-[14px]">memory</span>
<p>
{t("liveMonitorMemoryNote")}{" "}
<span className="text-text-muted">{t("liveMonitorMemoryCapNote")}</span>
</p>
</div>
{/* Controls */}
<Card>
<div className="p-3 flex items-center justify-between">
<div className="flex items-center gap-2">
<span
className={`material-symbols-outlined text-[18px] ${autoRefresh ? "text-green-500 animate-pulse" : "text-text-muted"}`}
aria-hidden="true"
>
{autoRefresh ? "radio_button_checked" : "radio_button_unchecked"}
</span>
<button
onClick={() => setAutoRefresh(!autoRefresh)}
className="text-sm text-text-main hover:text-primary transition-colors"
>
{autoRefresh ? t("liveAutoRefreshing") : t("paused")}
</button>
</div>
<button
onClick={fetchHistory}
className="flex items-center gap-1 text-xs text-text-muted hover:text-primary transition-colors"
>
<span className="material-symbols-outlined text-[16px]" aria-hidden="true">
refresh
</span>
{tc("refresh")}
</button>
</div>
</Card>
{/* Events Table */}
<Card>
<div className="p-4">
<h3 className="text-sm font-semibold text-text-main mb-3">{t("recentTranslations")}</h3>
{loading ? (
<div className="flex items-center justify-center py-12 text-text-muted">
<span className="material-symbols-outlined animate-spin mr-2" aria-hidden="true">
progress_activity
</span>
{tc("loading")}
</div>
) : events.length === 0 ? (
<div className="flex flex-col items-center justify-center py-12 text-text-muted">
<span
className="material-symbols-outlined text-[48px] mb-3 opacity-30"
aria-hidden="true"
>
monitoring
</span>
<p className="text-sm font-medium mb-1">{t("noTranslations")}</p>
<p className="text-xs text-center max-w-sm">{t("eventsAppearHint")}</p>
<div className="mt-3 rounded-lg border border-border/40 bg-bg-subtle/50 px-4 py-3 text-left">
<p className="text-[10px] font-semibold text-text-muted">
{t("eventSourcesLabel")}
</p>
<ul className="mt-1 space-y-1 text-[10px] text-text-muted">
<li>{t("eventSourceTranslatorPage")}</li>
<li>{t("eventSourceMainPipeline")}</li>
</ul>
</div>
<div className="flex flex-wrap gap-2 mt-3 text-xs">
<span className="px-2 py-1 rounded-md bg-bg-subtle border border-border">
{t("chatTesterTab")}
</span>
<span className="px-2 py-1 rounded-md bg-bg-subtle border border-border">
{t("testBenchTab")}
</span>
<span className="px-2 py-1 rounded-md bg-bg-subtle border border-border">
{t("externalApiCalls")}
</span>
<span className="px-2 py-1 rounded-md bg-bg-subtle border border-border">
{t("ideCliIntegrations")}
</span>
</div>
<p className="text-[10px] mt-3 text-text-muted/70">{t("inMemoryNote")}</p>
</div>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-text-muted border-b border-border">
<th className="pb-2 pr-4">{t("time")}</th>
<th className="pb-2 pr-4">{translateOrFallback("routeDetails", "Route")}</th>
<th className="pb-2 pr-4">{t("source")}</th>
<th className="pb-2 pr-4">{t("target")}</th>
<th className="pb-2 pr-4">{t("model")}</th>
<th className="pb-2 pr-4">{t("status")}</th>
<th className="pb-2 text-right">{t("latency")}</th>
</tr>
</thead>
<tbody>
{events.map((event, i) => {
const srcMeta = FORMAT_META[event.sourceFormat] || {
label: event.sourceFormat || "?",
color: "gray",
};
const tgtMeta = FORMAT_META[event.targetFormat] || {
label: event.targetFormat || "?",
color: "gray",
};
return (
<tr
key={event.id || i}
className="border-b border-border/50 hover:bg-bg-subtle/50 transition-colors"
>
<td className="py-2 pr-4 text-xs text-text-muted whitespace-nowrap">
{event.timestamp
? new Date(event.timestamp).toLocaleTimeString()
: notAvailable}
</td>
<td className="py-2 pr-4 min-w-[220px]">
<div className="flex flex-col gap-1">
<div className="flex flex-wrap items-center gap-1.5">
<Badge variant="default" size="sm">
{event.routeProvider || event.provider || notAvailable}
</Badge>
{event.routeCombo ? (
<Badge variant="primary" size="sm">
{translateOrFallback("comboBadge", "Combo")}: {event.routeCombo}
</Badge>
) : null}
</div>
<div className="flex flex-wrap gap-x-3 gap-y-1 text-[11px] text-text-muted">
<span>
{translateOrFallback("routeEndpointLabel", "Endpoint")}:{" "}
{event.routeEndpoint || event.endpoint || notAvailable}
</span>
{event.routeConnectionShortId ? (
<span>
{translateOrFallback("routeConnectionLabel", "Conn")}:{" "}
<span className="font-mono">{event.routeConnectionShortId}</span>
</span>
) : null}
</div>
</div>
</td>
<td className="py-2 pr-4">
<Badge variant="default" size="sm">
{srcMeta.label}
</Badge>
</td>
<td className="py-2 pr-4">
<Badge variant="primary" size="sm">
{tgtMeta.label}
</Badge>
</td>
<td className="py-2 pr-4 text-xs font-mono text-text-muted break-all">
{event.model || notAvailable}
</td>
<td className="py-2 pr-4">
{event.status === "success" ? (
<Badge variant="success" size="sm" dot>
{t("ok")}
</Badge>
) : (
<Badge variant="error" size="sm" dot>
{event.statusCode || t("errorShort")}
</Badge>
)}
</td>
<td className="py-2 text-right text-xs text-text-muted">
{event.latency ? formatLatency(event.latency) : notAvailable}
</td>
</tr>
);
})}
</tbody>
</table>
</div>
)}
</div>
</Card>
</div>
);
}
function StatCard({ icon, label, value, color }) {
const colorMap = {
blue: { shell: "bg-blue-500/10", icon: "text-blue-500" },
green: { shell: "bg-green-500/10", icon: "text-green-500" },
red: { shell: "bg-red-500/10", icon: "text-red-500" },
purple: { shell: "bg-purple-500/10", icon: "text-purple-500" },
amber: { shell: "bg-amber-500/10", icon: "text-amber-500" },
cyan: { shell: "bg-cyan-500/10", icon: "text-cyan-500" },
};
const resolved = colorMap[color as keyof typeof colorMap] || colorMap.blue;
return (
<Card>
<div className="p-4 flex items-center gap-3">
<div className={`flex items-center justify-center w-10 h-10 rounded-lg ${resolved.shell}`}>
<span
className={`material-symbols-outlined text-[22px] ${resolved.icon}`}
aria-hidden="true"
>
{icon}
</span>
</div>
<div>
<p className="text-lg font-bold text-text-main">{value}</p>
<p className="text-[10px] text-text-muted uppercase tracking-wider">{label}</p>
</div>
</div>
</Card>
);
}

View File

@@ -1,587 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useCallback, useEffect, useMemo } from "react";
import { Card, Button, Select, Badge } from "@/shared/components";
import { getExampleTemplates, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
import Editor from "@/shared/components/MonacoEditor";
interface CompressionPreviewResult {
originalTokens: number;
compressedTokens: number;
tokensSaved: number;
savingsPct: number;
techniquesUsed: string[];
durationMs: number;
}
export default function PlaygroundMode() {
const t = useTranslations("translator");
const tc = useTranslations("common");
const [sourceFormat, setSourceFormat] = useState("claude");
const [targetFormat, setTargetFormat] = useState("openai");
const [inputContent, setInputContent] = useState("");
const [outputContent, setOutputContent] = useState("");
const [intermediateContent, setIntermediateContent] = useState("");
const [translationPath, setTranslationPath] = useState("");
const [detectedFormat, setDetectedFormat] = useState(null);
const [translating, setTranslating] = useState(false);
const [detecting, setDetecting] = useState(false);
const [activeTemplate, setActiveTemplate] = useState(null);
// Compression preview state
const [compressionMode, setCompressionMode] = useState<string>("standard");
const [compressionResult, setCompressionResult] = useState<CompressionPreviewResult | null>(null);
const [compressionLoading, setCompressionLoading] = useState(false);
const [compressionError, setCompressionError] = useState<string | null>(null);
const [showCompressionPanel, setShowCompressionPanel] = useState(false);
const templates = useMemo(() => getExampleTemplates(t), [t]);
// Auto-detect format when input changes
const detectFormatFromInput = useCallback(async (content) => {
if (!content || content.trim().length < 5) {
setDetectedFormat(null);
return;
}
try {
const parsed = JSON.parse(content);
setDetecting(true);
const res = await fetch("/api/translator/detect", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ body: parsed }),
});
const data = await res.json();
if (data.success) {
setDetectedFormat(data.format);
setSourceFormat(data.format);
}
} catch {
// Not valid JSON yet, ignore
} finally {
setDetecting(false);
}
}, []);
// Debounced auto-detect
useEffect(() => {
const timer = setTimeout(() => {
detectFormatFromInput(inputContent);
}, 600);
return () => clearTimeout(timer);
}, [inputContent, detectFormatFromInput]);
const handleTranslate = async () => {
if (!inputContent.trim()) return;
setTranslating(true);
setOutputContent("");
setIntermediateContent("");
setTranslationPath("");
try {
const parsed = JSON.parse(inputContent);
if (sourceFormat === targetFormat) {
setOutputContent(JSON.stringify(parsed, null, 2));
setTranslationPath("passthrough");
setTranslating(false);
return;
}
let intermediate = parsed;
let hasIntermediate = false;
if (sourceFormat !== "openai" && targetFormat !== "openai") {
const step1 = await fetch("/api/translator/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
step: "direct",
sourceFormat,
targetFormat: "openai",
body: parsed,
}),
});
const step1Data = await step1.json();
if (!step1Data.success) {
setOutputContent(JSON.stringify({ error: step1Data.error }, null, 2));
return;
}
intermediate = step1Data.result;
setIntermediateContent(JSON.stringify(intermediate, null, 2));
hasIntermediate = true;
}
const res = await fetch("/api/translator/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
step: "direct",
sourceFormat: hasIntermediate ? "openai" : sourceFormat,
targetFormat,
body: hasIntermediate ? intermediate : parsed,
}),
});
const data = await res.json();
if (data.success) {
setOutputContent(JSON.stringify(data.result, null, 2));
setTranslationPath(hasIntermediate ? "hub-and-spoke" : "direct");
} else {
setOutputContent(JSON.stringify({ error: data.error }, null, 2));
}
} catch (err) {
setOutputContent(
JSON.stringify({ error: err instanceof Error ? err.message : String(err) }, null, 2)
);
} finally {
setTranslating(false);
}
};
const loadTemplate = (template) => {
const formatData = template.formats[sourceFormat] || template.formats.openai;
setInputContent(JSON.stringify(formatData, null, 2));
setActiveTemplate(template.id);
setOutputContent("");
setIntermediateContent("");
setTranslationPath("");
};
const handleCopy = async (text) => {
try {
await navigator.clipboard.writeText(text);
} catch {
/* silent */
}
};
const handleSwapFormats = () => {
setSourceFormat(targetFormat);
setTargetFormat(sourceFormat);
setInputContent(outputContent);
setOutputContent("");
setIntermediateContent("");
setTranslationPath("");
setDetectedFormat(null);
};
const handleCompressionPreview = async () => {
if (!inputContent.trim()) return;
let messages;
try {
const parsed = JSON.parse(inputContent);
messages = parsed.messages ?? [{ role: "user", content: inputContent }];
} catch {
messages = [{ role: "user", content: inputContent }];
}
setCompressionLoading(true);
setCompressionError(null);
try {
const res = await fetch("/api/compression/preview", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ messages, mode: compressionMode }),
});
const data = await res.json();
if (!res.ok) throw new Error(data.error ?? "Preview failed");
setCompressionResult(data);
} catch (e: unknown) {
setCompressionError(e instanceof Error ? e.message : String(e));
} finally {
setCompressionLoading(false);
}
};
const srcMeta = FORMAT_META[sourceFormat] || FORMAT_META.openai;
const tgtMeta = FORMAT_META[targetFormat] || FORMAT_META.openai;
return (
<div className="space-y-5">
{/* Info Banner */}
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
info
</span>
<div>
<p className="font-medium text-text-main mb-0.5">{t("formatConverter")}</p>
<p>{t("formatConverterDescription")}</p>
</div>
</div>
{/* Format Controls Bar */}
<Card>
<div className="p-4 flex flex-col sm:flex-row items-center gap-4">
{/* Source Format */}
<div className="flex-1 w-full">
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
{t("source")}
</label>
<div className="flex items-center gap-2">
<span className={`material-symbols-outlined text-[20px] text-${srcMeta.color}-500`}>
{srcMeta.icon}
</span>
<Select
value={sourceFormat}
onChange={(e) => {
setSourceFormat(e.target.value);
setDetectedFormat(null);
}}
options={FORMAT_OPTIONS}
className="flex-1"
/>
{detectedFormat && (
<Badge variant="primary" size="sm" icon="auto_awesome">
{t("auto")}
</Badge>
)}
</div>
</div>
{/* Swap Button */}
<button
onClick={handleSwapFormats}
className="p-2 rounded-full hover:bg-primary/10 text-text-muted hover:text-primary transition-all mt-4 sm:mt-5"
title={t("swapFormats")}
>
<span className="material-symbols-outlined text-[24px]">swap_horiz</span>
</button>
{/* Target Format */}
<div className="flex-1 w-full">
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
{t("target")}
</label>
<div className="flex items-center gap-2">
<span className={`material-symbols-outlined text-[20px] text-${tgtMeta.color}-500`}>
{tgtMeta.icon}
</span>
<Select
value={targetFormat}
onChange={(e) => setTargetFormat(e.target.value)}
options={FORMAT_OPTIONS}
className="flex-1"
/>
</div>
</div>
{/* Translate Button */}
<div className="pt-0 sm:pt-5">
<Button
icon="arrow_forward"
onClick={handleTranslate}
loading={translating}
disabled={!inputContent.trim() || translating}
className="w-full sm:w-auto"
>
{t("translateAction")}
</Button>
</div>
</div>
</Card>
{translationPath && (
<div className="flex items-center gap-2 text-xs text-text-muted">
<span className="material-symbols-outlined text-[14px]">route</span>
{translationPath === "hub-and-spoke" ? (
<span>
{t("translationPathHubSpoke", {
source: FORMAT_META[sourceFormat]?.label || sourceFormat,
target: FORMAT_META[targetFormat]?.label || targetFormat,
})}
</span>
) : translationPath === "direct" ? (
<span>
{t("translationPathDirect", {
source: FORMAT_META[sourceFormat]?.label || sourceFormat,
target: FORMAT_META[targetFormat]?.label || targetFormat,
})}
</span>
) : (
<span>{t("translationPathPassthrough")}</span>
)}
</div>
)}
{/* Split Editor View */}
<div
className={`grid grid-cols-1 gap-4 ${
intermediateContent ? "xl:grid-cols-3" : "lg:grid-cols-2"
}`}
>
{/* Input Panel */}
<Card>
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-text-muted">input</span>
<h3 className="text-sm font-semibold text-text-main">{t("input")}</h3>
{detectedFormat && (
<Badge variant="info" size="sm" dot>
{FORMAT_META[detectedFormat]?.label || detectedFormat}
</Badge>
)}
{detecting && (
<span className="material-symbols-outlined text-[14px] text-text-muted animate-spin">
progress_activity
</span>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => handleCopy(inputContent)}
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
title={tc("copy")}
>
<span className="material-symbols-outlined text-[16px]">content_copy</span>
</button>
<button
onClick={() => {
setInputContent("");
setOutputContent("");
setDetectedFormat(null);
setActiveTemplate(null);
}}
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
title={t("clear")}
>
<span className="material-symbols-outlined text-[16px]">delete</span>
</button>
</div>
</div>
<div className="border border-border rounded-lg overflow-hidden">
<Editor
height="400px"
defaultLanguage="json"
value={inputContent}
onChange={(value) => setInputContent(value || "")}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 12,
lineNumbers: "on",
scrollBeyondLastLine: false,
wordWrap: "on",
automaticLayout: true,
formatOnPaste: true,
placeholder: t("inputPlaceholder"),
}}
/>
</div>
</div>
</Card>
{/* Intermediate Panel */}
{intermediateContent && (
<Card>
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-amber-500">hub</span>
<h3 className="text-sm font-semibold text-text-main">
{t("openaiIntermediatePanel")}
</h3>
<Badge variant="warning" size="sm">
Hub
</Badge>
</div>
<button
onClick={() => handleCopy(intermediateContent)}
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
title={tc("copy")}
>
<span className="material-symbols-outlined text-[16px]">content_copy</span>
</button>
</div>
<div className="border border-border rounded-lg overflow-hidden">
<Editor
height="400px"
defaultLanguage="json"
value={intermediateContent}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 12,
lineNumbers: "on",
scrollBeyondLastLine: false,
wordWrap: "on",
automaticLayout: true,
readOnly: true,
}}
/>
</div>
</div>
</Card>
)}
{/* Output Panel */}
<Card>
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-text-muted">
output
</span>
<h3 className="text-sm font-semibold text-text-main">{t("output")}</h3>
{outputContent && (
<Badge variant="success" size="sm" dot>
{FORMAT_META[targetFormat]?.label || targetFormat}
</Badge>
)}
</div>
<div className="flex items-center gap-1">
<button
onClick={() => handleCopy(outputContent)}
className="p-1.5 rounded hover:bg-black/5 dark:hover:bg-white/5 text-text-muted hover:text-text-main transition-colors"
title={tc("copy")}
>
<span className="material-symbols-outlined text-[16px]">content_copy</span>
</button>
</div>
</div>
<div className="border border-border rounded-lg overflow-hidden">
<Editor
height="400px"
defaultLanguage="json"
value={outputContent}
theme="vs-dark"
options={{
minimap: { enabled: false },
fontSize: 12,
lineNumbers: "on",
scrollBeyondLastLine: false,
wordWrap: "on",
automaticLayout: true,
readOnly: true,
}}
/>
</div>
</div>
</Card>
</div>
{/* {t("exampleTemplates")} */}
<Card>
<div className="p-4 space-y-3">
<div className="flex items-center gap-2">
<span className="material-symbols-outlined text-[18px] text-primary">
library_books
</span>
<h3 className="text-sm font-semibold text-text-main">{t("exampleTemplates")}</h3>
<span className="text-xs text-text-muted">{t("exampleTemplatesHint")}</span>
</div>
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-6 gap-2">
{templates.map((template) => (
<button
key={template.id}
onClick={() => loadTemplate(template)}
className={`
group flex flex-col items-center gap-1.5 p-3 rounded-lg border transition-all text-center
${
activeTemplate === template.id
? "border-primary bg-primary/5 text-primary"
: "border-border hover:border-primary/30 hover:bg-primary/5 text-text-muted hover:text-text-main"
}
`}
>
<span
className={`material-symbols-outlined text-[22px] ${activeTemplate === template.id ? "text-primary" : "text-text-muted group-hover:text-primary"} transition-colors`}
>
{template.icon}
</span>
<span className="text-xs font-medium leading-tight">{template.name}</span>
</button>
))}
</div>
{activeTemplate && (
<div className="flex items-center gap-2 text-xs text-text-muted">
<span className="material-symbols-outlined text-[14px]">info</span>
{t("templateLoadHint", {
format: FORMAT_META[sourceFormat]?.label || sourceFormat,
})}
</div>
)}
</div>
</Card>
{/* Compression Preview Panel */}
<Card>
<button
className="flex items-center gap-2 w-full text-left p-4 font-medium text-text"
onClick={() => setShowCompressionPanel((v) => !v)}
>
<span className="material-symbols-outlined text-primary text-[20px]">compress</span>
Compression Preview
<span className="material-symbols-outlined ml-auto text-text-muted text-[18px]">
{showCompressionPanel ? "expand_less" : "expand_more"}
</span>
</button>
{showCompressionPanel && (
<div className="p-4 space-y-4 border-t border-border">
<div className="flex items-center gap-3">
<Select
value={compressionMode}
onChange={(e) => setCompressionMode(e.target.value)}
options={[
{ value: "off", label: "Off" },
{ value: "lite", label: "Lite" },
{ value: "standard", label: "Standard" },
{ value: "aggressive", label: "Aggressive" },
{ value: "ultra", label: "Ultra" },
]}
className="text-sm"
/>
<Button
icon="play_arrow"
onClick={handleCompressionPreview}
loading={compressionLoading}
disabled={compressionLoading || !inputContent.trim()}
className="text-sm"
>
{compressionLoading ? "Previewing…" : "Preview Compression"}
</Button>
</div>
{compressionError && <div className="text-sm text-red-500">{compressionError}</div>}
{compressionResult && (
<div className="space-y-3">
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<div className="card p-3 text-center bg-black/5 dark:bg-white/5 rounded-lg border border-border">
<div className="text-xs text-text-muted">Original</div>
<div className="text-lg font-bold">{compressionResult.originalTokens}</div>
<div className="text-xs text-text-muted">tokens</div>
</div>
<div className="card p-3 text-center bg-black/5 dark:bg-white/5 rounded-lg border border-border">
<div className="text-xs text-text-muted">Compressed</div>
<div className="text-lg font-bold">{compressionResult.compressedTokens}</div>
<div className="text-xs text-text-muted">tokens</div>
</div>
<div className="card p-3 text-center bg-black/5 dark:bg-white/5 rounded-lg border border-border">
<div className="text-xs text-text-muted">Saved</div>
<div className="text-lg font-bold text-green-500">
{compressionResult.tokensSaved}
</div>
<div className="text-xs text-text-muted">{compressionResult.savingsPct}%</div>
</div>
<div className="card p-3 text-center bg-black/5 dark:bg-white/5 rounded-lg border border-border">
<div className="text-xs text-text-muted">Duration</div>
<div className="text-lg font-bold">{compressionResult.durationMs}</div>
<div className="text-xs text-text-muted">ms</div>
</div>
</div>
{compressionResult.techniquesUsed.length > 0 && (
<div className="text-xs text-text-muted">
<span className="font-semibold">{t("techniques")}</span>{" "}
{compressionResult.techniquesUsed.join(", ")}
</div>
)}
</div>
)}
</div>
)}
</Card>
</div>
);
}

View File

@@ -1,295 +0,0 @@
"use client";
import { useMemo, useState, useCallback } from "react";
import { useTranslations } from "next-intl";
import { Button, Card } from "@/shared/components";
import { copyToClipboard } from "@/shared/utils/clipboard";
const TEXT_SAMPLE = `data: {"id":"chatcmpl_demo","object":"chat.completion.chunk","created":1745366400,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"role":"assistant","content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl_demo","object":"chat.completion.chunk","created":1745366400,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"content":" from OmniRoute"},"finish_reason":null}]}
data: {"id":"chatcmpl_demo","object":"chat.completion.chunk","created":1745366400,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":12,"completion_tokens":4,"total_tokens":16}}
data: [DONE]
`;
const TOOL_SAMPLE = `data: {"id":"chatcmpl_tool","object":"chat.completion.chunk","created":1745366400,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_123","type":"function","function":{"name":"lookup_weather","arguments":"{\\"city\\":\\"Tok"}}]},"finish_reason":null}]}
data: {"id":"chatcmpl_tool","object":"chat.completion.chunk","created":1745366400,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"yo\\"}"}}]},"finish_reason":null}]}
data: {"id":"chatcmpl_tool","object":"chat.completion.chunk","created":1745366400,"model":"gpt-4o-mini","choices":[{"index":0,"delta":{},"finish_reason":"tool_calls"}],"usage":{"prompt_tokens":23,"completion_tokens":9,"total_tokens":32}}
data: [DONE]
`;
function getFramePreview(data: unknown): string {
if (typeof data === "string") return data;
if (!data || typeof data !== "object") return "";
const record = data as Record<string, unknown>;
const delta = record.delta;
if (typeof delta === "string") return delta;
const item = record.item;
if (item && typeof item === "object") {
const itemRecord = item as Record<string, unknown>;
const type = itemRecord.type;
const text = itemRecord.text;
const name = itemRecord.name;
if (typeof text === "string" && text) return text;
if (typeof name === "string" && name) return `${type || "item"}: ${name}`;
if (typeof type === "string" && type) return type;
}
const text = record.text;
if (typeof text === "string" && text) return text;
return JSON.stringify(data).slice(0, 140);
}
function parseSseFrames(rawSse: string): Array<{ event: string; preview: string }> {
return rawSse
.split("\n\n")
.map((frame) => frame.trim())
.filter(Boolean)
.map((frame) => {
const eventLine = frame
.split("\n")
.find((line) => line.startsWith("event:"))
?.replace(/^event:\s*/, "")
.trim();
const dataLine = frame
.split("\n")
.find((line) => line.startsWith("data:"))
?.replace(/^data:\s*/, "");
if (dataLine === "[DONE]") {
return { event: "done", preview: "[DONE]" };
}
let parsedData: unknown = dataLine || "";
try {
parsedData = dataLine ? JSON.parse(dataLine) : "";
} catch {
parsedData = dataLine || "";
}
return {
event: eventLine || "message",
preview: getFramePreview(parsedData),
};
});
}
export default function StreamTransformerMode() {
const t = useTranslations("translator");
const translateOrFallback = useCallback(
(key: string, fallback: string, values?: Record<string, unknown>) => {
try {
const translated = t(key, values);
return translated === key || translated === `translator.${key}` ? fallback : translated;
} catch {
return fallback;
}
},
[t]
);
const [rawSse, setRawSse] = useState(TEXT_SAMPLE);
const [transformedSse, setTransformedSse] = useState("");
const [loading, setLoading] = useState(false);
const [copiedField, setCopiedField] = useState<string | null>(null);
const [error, setError] = useState<string | null>(null);
const transformedFrames = useMemo(() => parseSseFrames(transformedSse), [transformedSse]);
const eventCount = transformedFrames.length;
const uniqueEventCount = new Set(transformedFrames.map((frame) => frame.event)).size;
const handleCopy = async (value: string, field: string) => {
await copyToClipboard(value);
setCopiedField(field);
setTimeout(() => setCopiedField(null), 2000);
};
const runTransform = async () => {
setLoading(true);
setError(null);
try {
const res = await fetch("/api/translator/transform-stream", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ rawSse }),
});
const data = await res.json();
if (!res.ok || !data.success) {
throw new Error(data.error || translateOrFallback("requestFailed", "Request failed"));
}
setTransformedSse(data.transformed || "");
} catch (err) {
setError(err instanceof Error ? err.message : "Failed to transform stream");
} finally {
setLoading(false);
}
};
return (
<div className="space-y-5 min-w-0">
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
swap_horiz
</span>
<div>
<p className="font-medium text-text-main mb-0.5">
{translateOrFallback("streamTransformerTitle", "Responses Stream Transformer")}
</p>
<p>
{translateOrFallback(
"streamTransformerDescription",
"Paste a chat completions SSE stream, run it through OmniRoute's Responses transformer, and inspect the emitted response.* events before wiring a client."
)}
</p>
</div>
</div>
<Card>
<div className="p-4 flex flex-col gap-4">
<div className="flex flex-wrap gap-2">
<Button variant="outline" size="sm" onClick={() => setRawSse(TEXT_SAMPLE)}>
{translateOrFallback("loadTextSample", "Load text sample")}
</Button>
<Button variant="outline" size="sm" onClick={() => setRawSse(TOOL_SAMPLE)}>
{translateOrFallback("loadToolSample", "Load tool-call sample")}
</Button>
<Button size="sm" icon="play_arrow" onClick={runTransform} loading={loading}>
{translateOrFallback("transformToResponses", "Transform to Responses")}
</Button>
</div>
{error && (
<div className="rounded-lg border border-red-500/30 bg-red-500/10 px-3 py-2 text-sm text-red-600 dark:text-red-400">
{error}
</div>
)}
<div className="grid grid-cols-1 xl:grid-cols-2 gap-4">
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-text-main">
{translateOrFallback("rawChatSseInput", "Raw chat completions SSE")}
</h3>
<Button variant="ghost" size="sm" onClick={() => handleCopy(rawSse, "input")}>
<span className="material-symbols-outlined text-[14px]">
{copiedField === "input" ? "check" : "content_copy"}
</span>
</Button>
</div>
<textarea
value={rawSse}
onChange={(e) => setRawSse(e.target.value)}
className="min-h-[360px] w-full rounded-lg border border-border bg-bg-secondary px-3 py-3 text-xs font-mono text-text-main focus:outline-none focus:ring-1 focus:ring-primary/50"
spellCheck={false}
/>
</div>
<div className="space-y-2">
<div className="flex items-center justify-between gap-2">
<h3 className="text-sm font-semibold text-text-main">
{translateOrFallback("transformedResponsesSse", "Transformed Responses API SSE")}
</h3>
<Button
variant="ghost"
size="sm"
onClick={() => handleCopy(transformedSse, "output")}
disabled={!transformedSse}
>
<span className="material-symbols-outlined text-[14px]">
{copiedField === "output" ? "check" : "content_copy"}
</span>
</Button>
</div>
<pre className="min-h-[360px] overflow-auto rounded-lg border border-border bg-bg-secondary px-3 py-3 text-xs font-mono whitespace-pre-wrap break-all">
{transformedSse || translateOrFallback("noResultsYet", "No results yet")}
</pre>
</div>
</div>
</div>
</Card>
<div className="grid grid-cols-2 md:grid-cols-4 gap-3">
<MiniStat
label={translateOrFallback("transformedEvents", "Transformed events")}
value={eventCount}
/>
<MiniStat
label={translateOrFallback("uniqueEventTypes", "Unique event types")}
value={uniqueEventCount}
/>
<MiniStat
label={translateOrFallback("inputLines", "Input lines")}
value={rawSse.split("\n").length}
/>
<MiniStat
label={translateOrFallback("outputLines", "Output lines")}
value={transformedSse ? transformedSse.split("\n").length : 0}
/>
</div>
<Card>
<div className="p-4 space-y-3">
<h3 className="text-sm font-semibold text-text-main">
{translateOrFallback("transformedEventTimeline", "Transformed event timeline")}
</h3>
{transformedFrames.length === 0 ? (
<p className="text-sm text-text-muted">
{translateOrFallback(
"transformerTimelineHint",
"Run the transformer to inspect emitted response.output_* events in order."
)}
</p>
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="text-left text-xs text-text-muted border-b border-border">
<th className="pb-2 pr-4">#</th>
<th className="pb-2 pr-4">{translateOrFallback("eventType", "Event type")}</th>
<th className="pb-2">{translateOrFallback("eventPreview", "Preview")}</th>
</tr>
</thead>
<tbody>
{transformedFrames.map((frame, index) => (
<tr
key={`${frame.event}_${index}`}
className="border-b border-border/50 align-top"
>
<td className="py-2 pr-4 text-xs text-text-muted">{index + 1}</td>
<td className="py-2 pr-4 font-mono text-xs text-primary">{frame.event}</td>
<td className="py-2 text-xs text-text-muted break-all">{frame.preview}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</div>
</Card>
</div>
);
}
function MiniStat({ label, value }: { label: string; value: number }) {
return (
<Card>
<div className="p-4">
<p className="text-lg font-bold text-text-main">{value}</p>
<p className="text-[10px] uppercase tracking-wider text-text-muted">{label}</p>
</div>
</Card>
);
}

View File

@@ -1,366 +0,0 @@
"use client";
import { useTranslations } from "next-intl";
import { useState, useEffect, useMemo } from "react";
import { Card, Button, Select, Badge } from "@/shared/components";
import { getExampleTemplates, FORMAT_META, FORMAT_OPTIONS } from "../exampleTemplates";
import { useProviderOptions } from "../hooks/useProviderOptions";
import { useAvailableModels } from "../hooks/useAvailableModels";
/**
* Test Bench Mode:
* Run translation + send scenarios between providers to validate compatibility.
*
* How it works:
* Predefined scenarios (Simple Chat, Tool Calling, etc.) are loaded from example templates,
* translated from the source format to the target provider, and sent to the provider API.
* Results show pass/fail, latency, and chunk count, with a compatibility percentage.
*/
const SCENARIOS = [
{ id: "simple-chat", icon: "chat", templateId: "simple-chat" },
{ id: "tool-calling", icon: "build", templateId: "tool-calling" },
{ id: "multi-turn", icon: "forum", templateId: "multi-turn" },
{ id: "thinking", icon: "psychology", templateId: "thinking" },
{ id: "system-prompt", icon: "settings", templateId: "system-prompt" },
{ id: "streaming", icon: "stream", templateId: "streaming" },
{ id: "vision", icon: "image", templateId: "vision" },
{ id: "schema-coercion", icon: "schema", templateId: "schema-coercion" },
];
export default function TestBenchMode() {
const t = useTranslations("translator");
const translateOrFallback = (key: string, fallback: string) => {
try {
const translated = t(key);
return translated === key || translated === `translator.${key}` ? fallback : translated;
} catch {
return fallback;
}
};
const scenarioLabels: Record<string, string> = {
"simple-chat": t("scenarioSimpleChat"),
"tool-calling": t("scenarioToolCalling"),
"multi-turn": t("scenarioMultiTurn"),
thinking: t("scenarioThinking"),
"system-prompt": t("scenarioSystemPrompt"),
streaming: t("scenarioStreaming"),
vision: translateOrFallback("scenarioVision", "Vision"),
"schema-coercion": translateOrFallback("scenarioSchemaCoercion", "Schema Coercion"),
};
const templates = useMemo(() => getExampleTemplates(t), [t]);
const [sourceFormat, setSourceFormat] = useState("claude");
const { provider, setProvider, providerOptions } = useProviderOptions("openai");
const { model, setModel, availableModels, pickModelForFormat } = useAvailableModels();
const [results, setResults] = useState({});
const [runningAll, setRunningAll] = useState(false);
// Pick a smart default model when source format changes or models finish loading
useEffect(() => {
const picked = pickModelForFormat(sourceFormat);
if (picked) setModel(picked);
}, [sourceFormat, pickModelForFormat, setModel]);
const runScenario = async (scenario) => {
setResults((prev) => ({ ...prev, [scenario.id]: { status: "running" } }));
const start = Date.now();
try {
// Find template
const template = templates.find((item) => item.id === scenario.templateId);
const body = template?.formats[sourceFormat] || template?.formats.openai;
if (!body) {
setResults((prev) => ({
...prev,
[scenario.id]: { status: "error", error: t("noTemplateForFormat"), latency: 0 },
}));
return;
}
// Override model in template body with user-selected model
const bodyWithModel = { ...body, model };
// For Gemini format that uses 'contents' instead of 'messages'
if (body.contents) bodyWithModel.model = model;
// Step 1: Translate
const translateRes = await fetch("/api/translator/translate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ step: "direct", sourceFormat, provider, body: bodyWithModel }),
});
const translateData = await translateRes.json();
if (!translateData.success) {
setResults((prev) => ({
...prev,
[scenario.id]: {
status: "error",
error: t("translationFailed", { error: translateData.error }),
latency: Date.now() - start,
},
}));
return;
}
// Step 2: Send to provider
const sendRes = await fetch("/api/translator/send", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ provider, body: translateData.result }),
});
const latency = Date.now() - start;
if (!sendRes.ok) {
const errData = await sendRes.json().catch(() => ({}));
setResults((prev) => ({
...prev,
[scenario.id]: {
status: "error",
error: errData.error || t("errorMessage", { message: `HTTP ${sendRes.status}` }),
latency,
httpStatus: sendRes.status,
},
}));
return;
}
// Read response to consume stream
const reader = sendRes.body.getReader();
let chunks = 0;
while (true) {
const { done } = await reader.read();
if (done) break;
chunks++;
}
setResults((prev) => ({
...prev,
[scenario.id]: { status: "pass", latency: Date.now() - start, chunks },
}));
} catch (err) {
setResults((prev) => ({
...prev,
[scenario.id]: { status: "error", error: err.message, latency: Date.now() - start },
}));
}
};
const handleRunAll = async () => {
setRunningAll(true);
setResults({});
for (const scenario of SCENARIOS) {
await runScenario(scenario);
}
setRunningAll(false);
};
const passCount = Object.values(results).filter((r: any) => r.status === "pass").length;
const failCount = Object.values(results).filter((r: any) => r.status === "error").length;
const totalRun = passCount + failCount;
const compatibility = totalRun > 0 ? Math.round((passCount / totalRun) * 100) : 0;
const srcMeta = FORMAT_META[sourceFormat] || FORMAT_META.openai;
return (
<div className="space-y-5 min-w-0">
{/* Info Banner */}
<div className="flex items-start gap-3 px-4 py-3 rounded-lg bg-primary/5 border border-primary/10 text-sm text-text-muted">
<span className="material-symbols-outlined text-primary text-[20px] mt-0.5 shrink-0">
info
</span>
<div>
<p className="font-medium text-text-main mb-0.5">{t("compatibilityTester")}</p>
<p>{t("testBenchDescription")}</p>
</div>
</div>
{/* Controls */}
<Card>
<div className="p-4 flex flex-col gap-4">
<div className="flex flex-col sm:flex-row items-end gap-4 min-w-0">
<div className="flex-1 w-full">
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
{t("source")}
</label>
<Select
value={sourceFormat}
onChange={(e) => {
setSourceFormat(e.target.value);
setResults({});
}}
options={FORMAT_OPTIONS}
/>
</div>
<div className="flex items-center justify-center px-2">
<span className="material-symbols-outlined text-[22px] text-text-muted">
arrow_forward
</span>
</div>
<div className="flex-1 w-full">
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
{t("targetProvider")}
</label>
<Select
value={provider}
onChange={(e) => {
setProvider(e.target.value);
setResults({});
}}
options={providerOptions}
/>
</div>
<Button
icon="play_arrow"
onClick={handleRunAll}
loading={runningAll}
disabled={runningAll}
className="w-full sm:w-auto"
>
{t("runAllTests")}
</Button>
</div>
<div>
<label className="block text-xs font-medium text-text-muted mb-1.5 uppercase tracking-wider">
{t("model")}
</label>
<div className="relative">
<input
type="text"
value={model}
onChange={(e) => setModel(e.target.value)}
list="testbench-model-suggestions"
placeholder={t("modelPlaceholder")}
className="w-full bg-bg-subtle border border-border rounded-lg px-3 py-2 text-sm text-text-main placeholder:text-text-muted focus:outline-none focus:border-primary transition-colors"
/>
<datalist id="testbench-model-suggestions">
{availableModels.map((m) => (
<option key={m} value={m} />
))}
</datalist>
</div>
</div>
</div>
</Card>
{/* Results summary bar */}
{totalRun > 0 && (
<Card>
<div className="p-4">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-3">
<h3 className="text-sm font-semibold text-text-main">{t("compatibilityReport")}</h3>
<Badge
variant={
compatibility >= 80 ? "success" : compatibility >= 50 ? "warning" : "error"
}
size="lg"
>
{compatibility}%
</Badge>
</div>
<div className="flex items-center gap-3 text-xs text-text-muted">
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-green-500" /> {passCount} {t("passed")}
</span>
<span className="flex items-center gap-1">
<span className="size-2 rounded-full bg-red-500" /> {failCount} {t("failed")}
</span>
</div>
</div>
{/* Progress bar */}
<div className="w-full h-2 bg-bg-subtle rounded-full overflow-hidden">
<div
className="h-full bg-gradient-to-r from-green-500 to-emerald-400 rounded-full transition-all duration-500"
style={{ width: `${compatibility}%` }}
/>
</div>
</div>
</Card>
)}
{/* Scenario cards */}
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 gap-3">
{SCENARIOS.map((scenario) => {
const result = results[scenario.id];
const isRunning = result?.status === "running";
return (
<Card
key={scenario.id}
className={`transition-all ${result?.status === "pass" ? "border-green-500/30" : result?.status === "error" ? "border-red-500/30" : ""}`}
>
<div className="p-4 space-y-3">
<div className="flex items-center justify-between">
<div className="flex items-center gap-2">
<div
className={`flex items-center justify-center w-9 h-9 rounded-lg ${
result?.status === "pass"
? "bg-green-500/10 text-green-500"
: result?.status === "error"
? "bg-red-500/10 text-red-500"
: "bg-bg-subtle text-text-muted"
}`}
>
<span className="material-symbols-outlined text-[20px]">
{isRunning
? "progress_activity"
: result?.status === "pass"
? "check_circle"
: result?.status === "error"
? "error"
: scenario.icon}
</span>
</div>
<div>
<p className="text-sm font-medium text-text-main">
{scenarioLabels[scenario.id] || scenario.id}
</p>
<p className="text-[10px] text-text-muted uppercase">
{srcMeta.label} {" "}
{providerOptions.find((o) => o.value === provider)?.label || provider}
</p>
</div>
</div>
</div>
{/* Result details */}
{result && result.status !== "running" && (
<div
className={`rounded-lg p-2 text-xs ${result.status === "pass" ? "bg-green-500/5 text-green-600 dark:text-green-400" : "bg-red-500/5 text-red-600 dark:text-red-400"}`}
>
{result.status === "pass" ? (
<div className="flex items-center justify-between">
<span>{t("passedIconLabel")}</span>
<span className="text-text-muted">
{result.latency}ms {result.chunks} {t("chunks")}
</span>
</div>
) : (
<div>
<p> {result.error}</p>
<p className="text-text-muted mt-0.5">{result.latency}ms</p>
</div>
)}
</div>
)}
<Button
size="sm"
variant={result?.status === "pass" ? "ghost" : "outline"}
icon={isRunning ? "progress_activity" : "play_arrow"}
onClick={() => runScenario(scenario)}
disabled={isRunning || runningAll}
className="w-full"
>
{isRunning ? t("running") : result ? t("reRun") : t("runTest")}
</Button>
</div>
</Card>
);
})}
</div>
</div>
);
}

View File

@@ -0,0 +1,335 @@
// @vitest-environment jsdom
/**
* F9 Integration tests — TranslatorPageClient wired to child components via
* mocked hooks. Tests verify deep-link prop propagation and conditional rendering.
*/
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ── i18n stub ─────────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// ── next/navigation — factory so each test can override mockGet ───────────────
const mockReplace = vi.fn();
let mockGetImpl: (key: string) => string | null = (key) => {
if (key === "tab") return "translate";
if (key === "mode") return "send";
if (key === "advanced") return null;
return null;
};
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace: mockReplace }),
useSearchParams: () => ({
get: (key: string) => mockGetImpl(key),
toString: () => {
const tab = mockGetImpl("tab") ?? "translate";
const mode = mockGetImpl("mode") ?? "send";
const adv = mockGetImpl("advanced");
return adv ? `tab=${tab}&mode=${mode}&advanced=${adv}` : `tab=${tab}&mode=${mode}`;
},
}),
}));
// ── Shared component stubs ────────────────────────────────────────────────────
vi.mock("@/shared/components", () => ({
Card: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="card" className={className}>
{children}
</div>
),
Badge: ({ children }: { children: React.ReactNode }) => (
<span data-testid="badge">{children}</span>
),
SegmentedControl: ({
options,
value,
onChange,
"aria-label": ariaLabel,
}: {
options: Array<{ value: string; label: string }>;
value: string;
onChange: (v: string) => void;
"aria-label"?: string;
}) => (
<div role="tablist" aria-label={ariaLabel} data-testid="segmented-control" data-value={value}>
{options.map((opt) => (
<button
key={opt.value}
role="tab"
data-testid={`tab-${opt.value}`}
onClick={() => onChange(opt.value)}
>
{opt.label}
</button>
))}
</div>
),
Button: ({ children }: { children: React.ReactNode }) => (
<button data-testid="button">{children}</button>
),
Select: ({ children }: { children: React.ReactNode }) => (
<div data-testid="select">{children}</div>
),
Collapsible: ({ children }: { children: React.ReactNode }) => (
<div data-testid="collapsible">{children}</div>
),
}));
// ── Sub-component stubs that capture received props ───────────────────────────
const capturedTranslateTabProps: Array<{
forceOpenAdvancedSlug?: string | null;
}> = [];
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard",
() => ({
default: () => <div data-testid="translator-concept-card" />,
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/TranslateTab",
() => ({
default: ({
forceOpenAdvancedSlug,
}: {
forceOpenAdvancedSlug?: string | null;
onAdvancedSlugChange?: (slug: string | null) => void;
}) => {
capturedTranslateTabProps.push({ forceOpenAdvancedSlug });
return (
<div data-testid="translate-tab" data-force-open={forceOpenAdvancedSlug ?? "none"} />
);
},
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab",
() => ({
default: ({ onGoToTranslate }: { onGoToTranslate?: () => void }) => (
<div data-testid="monitor-tab" data-has-callback={String(!!onGoToTranslate)} />
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/AdvancedSection",
() => ({
default: ({
children,
forceOpenSlug,
}: {
children?: React.ReactNode;
forceOpenSlug?: string | null;
}) => (
<div
data-testid="advanced-section"
data-force-open-slug={forceOpenSlug ?? "none"}
>
{children}
</div>
),
}),
);
const capturedRawJsonProps: Array<{ forceOpen?: boolean }> = [];
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/RawJsonPanel",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => {
capturedRawJsonProps.push({ forceOpen });
return (
<div data-testid="raw-json-panel" data-force-open={String(forceOpen ?? false)} />
);
},
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/PipelineView",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean; steps?: unknown[] }) => (
<div data-testid="pipeline-view" data-force-open={String(forceOpen ?? false)} />
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/StreamTransformerAccordion",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => (
<div
data-testid="stream-transformer-accordion"
data-force-open={String(forceOpen ?? false)}
/>
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => (
<div data-testid="test-bench-accordion" data-force-open={String(forceOpen ?? false)} />
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => (
<div
data-testid="compression-preview-accordion"
data-force-open={String(forceOpen ?? false)}
/>
),
}),
);
// ── DOM lifecycle helpers ─────────────────────────────────────────────────────
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
async function mount(component: React.ReactElement, container: HTMLElement) {
const root = createRoot(container);
await act(async () => {
root.render(component);
});
return root;
}
// ── Import AFTER mocks ────────────────────────────────────────────────────────
import TranslatorPageClient from "@/app/(dashboard)/dashboard/translator/TranslatorPageClient";
describe("TranslatorPageClient — integration", () => {
beforeEach(() => {
vi.clearAllMocks();
capturedTranslateTabProps.length = 0;
capturedRawJsonProps.length = 0;
// Reset to default translate tab
mockGetImpl = (key: string) => {
if (key === "tab") return "translate";
if (key === "mode") return "send";
if (key === "advanced") return null;
return null;
};
});
afterEach(() => {
cleanupCallbacks.forEach((fn) => fn());
cleanupCallbacks.length = 0;
});
it("smoke render — full component tree mounts without errors", async () => {
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
expect(container.querySelector('[data-testid="translator-concept-card"]')).toBeTruthy();
expect(container.querySelector('[data-testid="segmented-control"]')).toBeTruthy();
expect(container.querySelector('[data-testid="translate-tab"]')).toBeTruthy();
expect(container.querySelector('[data-testid="advanced-section"]')).toBeTruthy();
});
it("when ?advanced=rawjson, RawJsonPanel receives forceOpen=true", async () => {
mockGetImpl = (key: string) => {
if (key === "tab") return "translate";
if (key === "mode") return "send";
if (key === "advanced") return "rawjson";
return null;
};
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
const rawJsonPanel = container.querySelector('[data-testid="raw-json-panel"]');
expect(rawJsonPanel).toBeTruthy();
expect(rawJsonPanel?.getAttribute("data-force-open")).toBe("true");
// AdvancedSection also receives the correct slug
const advSection = container.querySelector('[data-testid="advanced-section"]');
expect(advSection?.getAttribute("data-force-open-slug")).toBe("rawjson");
});
it("when ?tab=monitor, MonitorTab renders and TranslateTab does NOT render", async () => {
mockGetImpl = (key: string) => {
if (key === "tab") return "monitor";
if (key === "mode") return "send";
if (key === "advanced") return null;
return null;
};
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
expect(container.querySelector('[data-testid="monitor-tab"]')).toBeTruthy();
expect(container.querySelector('[data-testid="translate-tab"]')).toBeNull();
// AdvancedSection should also not render in monitor tab
expect(container.querySelector('[data-testid="advanced-section"]')).toBeNull();
});
it("AdvancedSection receives all 5 accordion slots in the DOM", async () => {
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
const advSection = container.querySelector('[data-testid="advanced-section"]');
expect(advSection).toBeTruthy();
const accordions = [
"raw-json-panel",
"pipeline-view",
"stream-transformer-accordion",
"test-bench-accordion",
"compression-preview-accordion",
];
for (const testId of accordions) {
expect(advSection?.querySelector(`[data-testid="${testId}"]`)).toBeTruthy();
}
});
it("when ?advanced=pipeline, only PipelineView forceOpen=true (others false)", async () => {
mockGetImpl = (key: string) => {
if (key === "tab") return "translate";
if (key === "mode") return "send";
if (key === "advanced") return "pipeline";
return null;
};
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
const pipeline = container.querySelector('[data-testid="pipeline-view"]');
expect(pipeline?.getAttribute("data-force-open")).toBe("true");
const rawJson = container.querySelector('[data-testid="raw-json-panel"]');
expect(rawJson?.getAttribute("data-force-open")).toBe("false");
});
it("MonitorTab receives onGoToTranslate callback", async () => {
mockGetImpl = (key: string) => {
if (key === "tab") return "monitor";
if (key === "mode") return "send";
if (key === "advanced") return null;
return null;
};
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
const monitorTab = container.querySelector('[data-testid="monitor-tab"]');
expect(monitorTab?.getAttribute("data-has-callback")).toBe("true");
});
});

View File

@@ -0,0 +1,312 @@
// @vitest-environment jsdom
import React from "react";
import { act } from "react";
import { createRoot } from "react-dom/client";
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
// ── i18n stub ─────────────────────────────────────────────────────────────────
vi.mock("next-intl", () => ({
useTranslations: () => (key: string) => key,
}));
// ── next/navigation stub ──────────────────────────────────────────────────────
const mockReplace = vi.fn();
const mockGet = vi.fn((key: string) => {
if (key === "tab") return "translate";
if (key === "mode") return "send";
if (key === "advanced") return null;
return null;
});
vi.mock("next/navigation", () => ({
useRouter: () => ({ replace: mockReplace }),
useSearchParams: () => ({
get: mockGet,
toString: () => "tab=translate&mode=send",
}),
}));
// ── Shared component stubs ────────────────────────────────────────────────────
vi.mock("@/shared/components", () => ({
Card: ({ children, className }: { children: React.ReactNode; className?: string }) => (
<div data-testid="card" className={className}>
{children}
</div>
),
Badge: ({
children,
variant,
size,
}: {
children: React.ReactNode;
variant?: string;
size?: string;
}) => (
<span data-testid="badge" data-variant={variant} data-size={size}>
{children}
</span>
),
SegmentedControl: ({
options,
value,
onChange,
size,
"aria-label": ariaLabel,
className,
}: {
options: Array<{ value: string; label: string; icon?: string }>;
value: string;
onChange: (v: string) => void;
size?: string;
"aria-label"?: string;
className?: string;
}) => (
<div
role="tablist"
aria-label={ariaLabel}
data-testid="segmented-control"
data-value={value}
className={className}
>
{options.map((opt) => (
<button
key={opt.value}
role="tab"
aria-selected={value === opt.value}
data-testid={`tab-${opt.value}`}
onClick={() => onChange(opt.value)}
>
{opt.label}
</button>
))}
</div>
),
Button: ({ children, onClick }: { children: React.ReactNode; onClick?: () => void }) => (
<button data-testid="button" onClick={onClick}>
{children}
</button>
),
Select: ({ children }: { children: React.ReactNode }) => (
<div data-testid="select">{children}</div>
),
Collapsible: ({ children }: { children: React.ReactNode }) => (
<div data-testid="collapsible">{children}</div>
),
}));
// ── Sub-component stubs ───────────────────────────────────────────────────────
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/TranslatorConceptCard",
() => ({
default: () => <div data-testid="translator-concept-card" />,
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/TranslateTab",
() => ({
default: ({
forceOpenAdvancedSlug,
onAdvancedSlugChange,
}: {
forceOpenAdvancedSlug?: string | null;
onAdvancedSlugChange?: (slug: string | null) => void;
}) => (
<div
data-testid="translate-tab"
data-force-open={forceOpenAdvancedSlug ?? "none"}
onClick={() => onAdvancedSlugChange?.("rawjson")}
/>
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/MonitorTab",
() => ({
default: ({ onGoToTranslate }: { onGoToTranslate?: () => void }) => (
<div data-testid="monitor-tab" onClick={onGoToTranslate} />
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/AdvancedSection",
() => ({
default: ({
children,
forceOpenSlug,
}: {
children?: React.ReactNode;
forceOpenSlug?: string | null;
}) => (
<div data-testid="advanced-section" data-force-open-slug={forceOpenSlug ?? "none"}>
{children}
</div>
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/RawJsonPanel",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => (
<div data-testid="raw-json-panel" data-force-open={String(forceOpen ?? false)} />
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/PipelineView",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => (
<div data-testid="pipeline-view" data-force-open={String(forceOpen ?? false)} />
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/StreamTransformerAccordion",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => (
<div data-testid="stream-transformer-accordion" data-force-open={String(forceOpen ?? false)} />
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/TestBenchAccordion",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => (
<div data-testid="test-bench-accordion" data-force-open={String(forceOpen ?? false)} />
),
}),
);
vi.mock(
"@/app/(dashboard)/dashboard/translator/components/advanced/CompressionPreviewAccordion",
() => ({
default: ({ forceOpen }: { forceOpen?: boolean }) => (
<div
data-testid="compression-preview-accordion"
data-force-open={String(forceOpen ?? false)}
/>
),
}),
);
// ── DOM lifecycle helpers ─────────────────────────────────────────────────────
const cleanupCallbacks: Array<() => void> = [];
function makeContainer(): HTMLElement {
const container = document.createElement("div");
document.body.appendChild(container);
cleanupCallbacks.push(() => container.remove());
return container;
}
async function mount(component: React.ReactElement, container: HTMLElement) {
const root = createRoot(container);
await act(async () => {
root.render(component);
});
return root;
}
// ── Import component AFTER mocks ──────────────────────────────────────────────
import TranslatorPageClient from "@/app/(dashboard)/dashboard/translator/TranslatorPageClient";
describe("TranslatorPageClient", () => {
beforeEach(() => {
vi.clearAllMocks();
// Reset mockGet to default (translate tab, no advanced)
mockGet.mockImplementation((key: string) => {
if (key === "tab") return "translate";
if (key === "mode") return "send";
if (key === "advanced") return null;
return null;
});
});
afterEach(() => {
cleanupCallbacks.forEach((fn) => fn());
cleanupCallbacks.length = 0;
});
it("renders smoke — default tab=translate shows TranslateTab and AdvancedSection", async () => {
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
expect(container.querySelector('[data-testid="translator-concept-card"]')).toBeTruthy();
expect(container.querySelector('[data-testid="translate-tab"]')).toBeTruthy();
expect(container.querySelector('[data-testid="advanced-section"]')).toBeTruthy();
expect(container.querySelector('[data-testid="monitor-tab"]')).toBeNull();
});
it("SegmentedControl has role=tablist and aria-label", async () => {
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
const ctrl = container.querySelector('[role="tablist"]');
expect(ctrl).toBeTruthy();
expect(ctrl?.getAttribute("aria-label")).toBeTruthy();
});
it("ConceptCard and AutoFeaturesCard (Card) render", async () => {
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
expect(container.querySelector('[data-testid="translator-concept-card"]')).toBeTruthy();
// AutoFeaturesCard renders as a Card with a button
const cards = container.querySelectorAll('[data-testid="card"]');
expect(cards.length).toBeGreaterThan(0);
});
it("clicking Monitor tab calls router.replace with tab=monitor", async () => {
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
const monitorTabBtn = container.querySelector('[data-testid="tab-monitor"]');
expect(monitorTabBtn).toBeTruthy();
await act(async () => {
(monitorTabBtn as HTMLElement).click();
});
expect(mockReplace).toHaveBeenCalledWith(
expect.stringContaining("tab=monitor"),
expect.any(Object),
);
});
it("8 FeatureChips render inside AutoFeaturesCard when expanded", async () => {
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
// Find the toggle button inside the AutoFeaturesCard (Card)
const toggleBtn = Array.from(container.querySelectorAll("button")).find(
(btn) => btn.querySelector(".material-symbols-outlined")?.textContent === "auto_fix_high",
);
expect(toggleBtn).toBeTruthy();
await act(async () => {
toggleBtn!.click();
});
const chips = container.querySelectorAll('[data-testid="feature-chip"]');
expect(chips.length).toBe(8);
});
it("AdvancedSection receives 5 accordion children", async () => {
const container = makeContainer();
await mount(<TranslatorPageClient />, container);
const advSection = container.querySelector('[data-testid="advanced-section"]');
expect(advSection).toBeTruthy();
expect(advSection?.querySelector('[data-testid="raw-json-panel"]')).toBeTruthy();
expect(advSection?.querySelector('[data-testid="pipeline-view"]')).toBeTruthy();
expect(advSection?.querySelector('[data-testid="stream-transformer-accordion"]')).toBeTruthy();
expect(advSection?.querySelector('[data-testid="test-bench-accordion"]')).toBeTruthy();
expect(advSection?.querySelector('[data-testid="compression-preview-accordion"]')).toBeTruthy();
});
});