From b6a6586bf8fff9bf0820347e6be881d1b730e023 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 28 May 2026 02:27:28 -0300 Subject: [PATCH] feat(translator): rewrite TranslatorPageClient with 2-tab shell (F9) --- .../translator/TranslatorPageClient.tsx | 311 +++++----- .../translator/components/ChatTesterMode.tsx | 543 ---------------- .../translator/components/LiveMonitorMode.tsx | 335 ---------- .../translator/components/PlaygroundMode.tsx | 587 ------------------ .../components/StreamTransformerMode.tsx | 295 --------- .../translator/components/TestBenchMode.tsx | 366 ----------- .../translator-friendly-integration.test.tsx | 335 ++++++++++ .../translator-friendly-page-client.test.tsx | 312 ++++++++++ 8 files changed, 813 insertions(+), 2271 deletions(-) delete mode 100644 src/app/(dashboard)/dashboard/translator/components/ChatTesterMode.tsx delete mode 100644 src/app/(dashboard)/dashboard/translator/components/LiveMonitorMode.tsx delete mode 100644 src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx delete mode 100644 src/app/(dashboard)/dashboard/translator/components/StreamTransformerMode.tsx delete mode 100644 src/app/(dashboard)/dashboard/translator/components/TestBenchMode.tsx create mode 100644 tests/unit/translator-friendly-integration.test.tsx create mode 100644 tests/unit/translator-friendly-page-client.test.tsx diff --git a/src/app/(dashboard)/dashboard/translator/TranslatorPageClient.tsx b/src/app/(dashboard)/dashboard/translator/TranslatorPageClient.tsx index 213eb28a7f..2c07b9594f 100644 --- a/src/app/(dashboard)/dashboard/translator/TranslatorPageClient.tsx +++ b/src/app/(dashboard)/dashboard/translator/TranslatorPageClient.tsx @@ -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 ( + Loading…}> + + ); - 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 = { - 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 = ( + + + + + + + + ); + + const tabOptions = [ + { value: "translate", label: t("tabTranslate"), icon: "translate" }, + { value: "monitor", label: t("tabMonitor"), icon: "monitoring" }, + ]; + return (
+ + + +
setTab(v as TranslatorTab)} size="md" + aria-label={t("tabTranslateAriaLabel")} className="min-w-max" />
- - + {state.tab === "translate" && ( + setAdvanced(slug)} + /> + )} - {showFeatures && ( -
- - - - - - - - -
- )} -
+ {state.tab === "translate" && advancedSlot} - {/* Mode Content */} - {mode === "playground" && } - {mode === "chat-tester" && } - {mode === "test-bench" && } - {mode === "stream-transformer" && } - {mode === "live-monitor" && } + {state.tab === "monitor" && ( + setTab("translate")} /> + )}
); } +function AutoFeaturesCard() { + const t = useTranslations("translator"); + const [showFeatures, setShowFeatures] = useState(false); + + return ( + + + + {showFeatures && ( +
+ + + + + + + + +
+ )} +
+ ); +} + function FeatureChip({ icon, title, @@ -213,7 +234,7 @@ function FeatureChip({ }[color]; return ( -
+
{icon}

{title}

diff --git a/src/app/(dashboard)/dashboard/translator/components/ChatTesterMode.tsx b/src/app/(dashboard)/dashboard/translator/components/ChatTesterMode.tsx deleted file mode 100644 index 205783b0af..0000000000 --- a/src/app/(dashboard)/dashboard/translator/components/ChatTesterMode.tsx +++ /dev/null @@ -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 ( -
- {/* Info Banner */} -
- - info - -
-

{t("pipelineDebugger")}

-

{t("chatTesterDescription")}

-

- {t("chatTesterFlow")}.{" "} - {t("clickStepToInspect")} -

-
-
- -
- {/* Left: Chat Interface */} -
- {/* Controls */} - -
-
-
- - setProvider(e.target.value)} - options={providerOptions} - /> -
-
-
- -
- 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" - /> - - {availableModels.map((m) => ( - -
-
-
-
- - {/* Chat Messages */} - -
- {chatHistory.length === 0 && ( -
- - chat - -

{t("sendMessageToSeePipeline")}

-

- {t("chatMessageHintPrefix")} {FORMAT_META[clientFormat]?.label}{" "} - {t("chatMessageHintSuffix")} -

-
- )} - {chatHistory.map((msg, i) => ( -
-
-

- {msg.role === "user" - ? t("youWithFormat", { format: FORMAT_META[clientFormat]?.label }) - : t("assistant")} -

-

{msg.content}

-
-
- ))} -
-
- - {/* Input */} -
-
- 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} - /> - -
-
- -
- - {/* Right: Pipeline Visualization */} -
- -
-
- - account_tree - -

{t("translationPipeline")}

-
-

{t("clickStepToInspect")}

-
-
- - {!pipeline ? ( - -
- - account_tree - -

{t("pipelineVisualization")}

-

{t("pipelineVisualizationHint")}

-
-
- ) : ( -
- {pipeline.map((step, i) => { - const meta = FORMAT_META[step.format] || { - label: step.format, - color: "gray", - icon: "code", - }; - const isExpanded = expandedStep === step.id; - - return ( -
- {/* Connector line */} - {i > 0 && ( -
-
-
- )} - - - - - {/* Expanded content */} - {isExpanded && ( -
-
- -
-
- )} -
-
- ); - })} -
- )} -
-
-
- ); -} - -/** 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); -} diff --git a/src/app/(dashboard)/dashboard/translator/components/LiveMonitorMode.tsx b/src/app/(dashboard)/dashboard/translator/components/LiveMonitorMode.tsx deleted file mode 100644 index 4c5ca7ab69..0000000000 --- a/src/app/(dashboard)/dashboard/translator/components/LiveMonitorMode.tsx +++ /dev/null @@ -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) => { - 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 ( -
- {/* Info Banner */} -
- -
-

{t("realtime")}

-

- {t("liveMonitorDescriptionPrefix")}{" "} - {t("chatTester")},{" "} - {t("testBench")} - {t("liveMonitorDescriptionSuffix")} -

-
-
- - {/* Stats Cards */} -
- - - - - - -
- -
- memory -

- {t("liveMonitorMemoryNote")}{" "} - {t("liveMonitorMemoryCapNote")} -

-
- - {/* Controls */} - -
-
- - -
- -
-
- - {/* Events Table */} - -
-

{t("recentTranslations")}

- - {loading ? ( -
- - {tc("loading")} -
- ) : events.length === 0 ? ( -
- -

{t("noTranslations")}

-

{t("eventsAppearHint")}

-
-

- {t("eventSourcesLabel")} -

-
    -
  • {t("eventSourceTranslatorPage")}
  • -
  • {t("eventSourceMainPipeline")}
  • -
-
-
- - {t("chatTesterTab")} - - - {t("testBenchTab")} - - - {t("externalApiCalls")} - - - {t("ideCliIntegrations")} - -
-

{t("inMemoryNote")}

-
- ) : ( -
- - - - - - - - - - - - - - {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 ( - - - - - - - - - - ); - })} - -
{t("time")}{translateOrFallback("routeDetails", "Route")}{t("source")}{t("target")}{t("model")}{t("status")}{t("latency")}
- {event.timestamp - ? new Date(event.timestamp).toLocaleTimeString() - : notAvailable} - -
-
- - {event.routeProvider || event.provider || notAvailable} - - {event.routeCombo ? ( - - {translateOrFallback("comboBadge", "Combo")}: {event.routeCombo} - - ) : null} -
-
- - {translateOrFallback("routeEndpointLabel", "Endpoint")}:{" "} - {event.routeEndpoint || event.endpoint || notAvailable} - - {event.routeConnectionShortId ? ( - - {translateOrFallback("routeConnectionLabel", "Conn")}:{" "} - {event.routeConnectionShortId} - - ) : null} -
-
-
- - {srcMeta.label} - - - - {tgtMeta.label} - - - {event.model || notAvailable} - - {event.status === "success" ? ( - - {t("ok")} - - ) : ( - - {event.statusCode || t("errorShort")} - - )} - - {event.latency ? formatLatency(event.latency) : notAvailable} -
-
- )} -
-
-
- ); -} - -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 ( - -
-
- -
-
-

{value}

-

{label}

-
-
-
- ); -} diff --git a/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx b/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx deleted file mode 100644 index 85c32414ac..0000000000 --- a/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx +++ /dev/null @@ -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("standard"); - const [compressionResult, setCompressionResult] = useState(null); - const [compressionLoading, setCompressionLoading] = useState(false); - const [compressionError, setCompressionError] = useState(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 ( -
- {/* Info Banner */} -
- - info - -
-

{t("formatConverter")}

-

{t("formatConverterDescription")}

-
-
- {/* Format Controls Bar */} - -
- {/* Source Format */} -
- -
- - {srcMeta.icon} - - setTargetFormat(e.target.value)} - options={FORMAT_OPTIONS} - className="flex-1" - /> -
-
- - {/* Translate Button */} -
- -
-
-
- - {translationPath && ( -
- route - {translationPath === "hub-and-spoke" ? ( - - {t("translationPathHubSpoke", { - source: FORMAT_META[sourceFormat]?.label || sourceFormat, - target: FORMAT_META[targetFormat]?.label || targetFormat, - })} - - ) : translationPath === "direct" ? ( - - {t("translationPathDirect", { - source: FORMAT_META[sourceFormat]?.label || sourceFormat, - target: FORMAT_META[targetFormat]?.label || targetFormat, - })} - - ) : ( - {t("translationPathPassthrough")} - )} -
- )} - - {/* Split Editor View */} -
- {/* Input Panel */} - -
-
-
- input -

{t("input")}

- {detectedFormat && ( - - {FORMAT_META[detectedFormat]?.label || detectedFormat} - - )} - {detecting && ( - - progress_activity - - )} -
-
- - -
-
-
- setInputContent(value || "")} - theme="vs-dark" - options={{ - minimap: { enabled: false }, - fontSize: 12, - lineNumbers: "on", - scrollBeyondLastLine: false, - wordWrap: "on", - automaticLayout: true, - formatOnPaste: true, - placeholder: t("inputPlaceholder"), - }} - /> -
-
-
- - {/* Intermediate Panel */} - {intermediateContent && ( - -
-
-
- hub -

- {t("openaiIntermediatePanel")} -

- - Hub - -
- -
-
- -
-
-
- )} - - {/* Output Panel */} - -
-
-
- - output - -

{t("output")}

- {outputContent && ( - - {FORMAT_META[targetFormat]?.label || targetFormat} - - )} -
-
- -
-
-
- -
-
-
-
- - {/* {t("exampleTemplates")} */} - -
-
- - library_books - -

{t("exampleTemplates")}

- {t("exampleTemplatesHint")} -
-
- {templates.map((template) => ( - - ))} -
- {activeTemplate && ( -
- info - {t("templateLoadHint", { - format: FORMAT_META[sourceFormat]?.label || sourceFormat, - })} -
- )} -
-
- - {/* Compression Preview Panel */} - - - - {showCompressionPanel && ( -
-
-