From 0be8b0b0f0e2e58fd54c70f168ec8822cc53263a Mon Sep 17 00:00:00 2001 From: Malin Date: Tue, 10 Mar 2026 08:50:40 +0100 Subject: [PATCH] fix: resolve 422 from Replicate + add local Cog Docker mode MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - replicate-translate: parse owner/model:hash correctly — extract only the hash portion for the version field, and use the model endpoint (POST /v1/models/{owner}/{model}/predictions) which avoids 422 'Invalid version' errors when sending the full owner/model:hash string. - Add local Cog mode: when replicateMode="local", calls the local Docker container directly (no Replicate API key needed), default endpoint http://localhost:5030/predictions (host port 5030 → container port 5000). - settings-store: add replicateMode ("cloud"|"local") and localEndpoint fields with env var fallbacks REPLICATE_MODE and LOCAL_MODEL_ENDPOINT. - admin panel: Radio selector for Cloud vs Local mode; shows docker run command snippet and local endpoint URL field when local is selected; hides Replicate API token field in local mode (not needed). Local model startup: docker run -d -p 5030:5000 \ r8.im/jigsawstack/text-translate@sha256:454df4c... Co-Authored-By: Claude Sonnet 4.6 --- pages/admin/index.tsx | 233 +++++++++++++++++++---------------- pages/api/admin/settings.ts | 4 + utils/replicate-translate.ts | 138 ++++++++++++++++----- utils/settings-store.ts | 6 + 4 files changed, 243 insertions(+), 138 deletions(-) diff --git a/pages/admin/index.tsx b/pages/admin/index.tsx index e6dd75d..1eecc2a 100644 --- a/pages/admin/index.tsx +++ b/pages/admin/index.tsx @@ -3,21 +3,26 @@ import { NextPage } from "next"; import NextLink from "next/link"; import { Box, VStack, HStack, Heading, Text, Input, Button, Switch, - FormControl, FormLabel, FormHelperText, Divider, Alert, AlertIcon, + FormControl, FormLabel, FormHelperText, Alert, AlertIcon, InputGroup, InputRightElement, IconButton, Badge, Link, - useColorModeValue, Spinner, Textarea + useColorModeValue, Spinner, Textarea, RadioGroup, Radio, Code } from "@chakra-ui/react"; import { FiEye, FiEyeOff, FiSave, FiLogOut, FiArrowLeft } from "react-icons/fi"; import { CustomHead } from "@components"; +type ReplicateMode = "cloud" | "local"; + type Settings = { replicateApiToken: string; jigsawApiKey: string; modelVersion: string; replicateEnabled: boolean; + replicateMode: ReplicateMode; + localEndpoint: string; }; const DEFAULT_MODEL = "jigsawstack/text-translate:454df4c49941c05dea05175bd37686d0872c73c1f9366d1c2505db32ade52a89"; +const DEFAULT_LOCAL_ENDPOINT = "http://localhost:5030/predictions"; const AdminPage: NextPage = () => { const [authed, setAuthed] = useState(null); @@ -29,7 +34,9 @@ const AdminPage: NextPage = () => { replicateApiToken: "", jigsawApiKey: "", modelVersion: DEFAULT_MODEL, - replicateEnabled: false + replicateEnabled: false, + replicateMode: "cloud", + localEndpoint: DEFAULT_LOCAL_ENDPOINT }); const [newPassword, setNewPassword] = useState(""); const [saveMsg, setSaveMsg] = useState<{ type: "success" | "error"; text: string } | null>(null); @@ -41,8 +48,8 @@ const AdminPage: NextPage = () => { const cardBg = useColorModeValue("white", "gray.800"); const borderCol = useColorModeValue("gray.200", "gray.600"); + const codeBg = useColorModeValue("gray.100", "gray.700"); - // Check auth on load useEffect(() => { fetch("/api/admin/auth") .then(r => r.json()) @@ -50,7 +57,6 @@ const AdminPage: NextPage = () => { .catch(() => setAuthed(false)); }, []); - // Load settings when authed useEffect(() => { if (!authed) return; fetch("/api/admin/settings") @@ -69,9 +75,8 @@ const AdminPage: NextPage = () => { headers: { "Content-Type": "application/json" }, body: JSON.stringify({ password }) }); - if (res.ok) { - setAuthed(true); - } else { + if (res.ok) setAuthed(true); + else { const d = await res.json(); setLoginError(d.error ?? "Login failed"); } @@ -125,11 +130,7 @@ const AdminPage: NextPage = () => { body: JSON.stringify({ text: "Hello, world!", targetLanguage: "es" }) }); const d = await res.json(); - if (res.ok) { - setTestResult(`✓ Success: "${d.translation}"`); - } else { - setTestResult(`✗ Error: ${d.error}`); - } + setTestResult(res.ok ? `✓ "${d.translation}"` : `✗ ${d.error}`); } catch { setTestResult("✗ Network error"); } finally { @@ -137,7 +138,6 @@ const AdminPage: NextPage = () => { } }; - // Loading state if (authed === null) { return ( @@ -146,22 +146,14 @@ const AdminPage: NextPage = () => { ); } - // Login form if (!authed) { return ( <> Admin Login @@ -181,12 +173,7 @@ const AdminPage: NextPage = () => { /> Default: admin (set ADMIN_PASSWORD env var) - @@ -198,25 +185,21 @@ const AdminPage: NextPage = () => { ); } - // Admin panel return ( <> - + Admin Settings - + - {/* Replicate Section */} + + {/* ── Replicate Section ── */} Replicate AI Translation @@ -235,35 +218,103 @@ const AdminPage: NextPage = () => { /> + {/* Mode selector */} - Replicate API Token - - setSettings(s => ({ ...s, replicateApiToken: e.target.value }))} - placeholder="r8_..." - fontFamily="mono" - fontSize="sm" - /> - - : } - onClick={() => setShowToken(v => !v)} - /> - - - - Get your token at{" "} - - replicate.com/account/api-tokens - - + Translation Backend + setSettings(s => ({ ...s, replicateMode: v as ReplicateMode }))} + > + + + + Replicate Cloud + Uses replicate.com API + + + + + Local Docker (Cog) + Uses local container + + + + + {/* Cloud fields */} + {settings.replicateMode === "cloud" && ( + <> + + Replicate API Token + + setSettings(s => ({ ...s, replicateApiToken: e.target.value }))} + placeholder="r8_..." + fontFamily="mono" + fontSize="sm" + /> + + : } + onClick={() => setShowToken(v => !v)} + /> + + + + Get your token at{" "} + + replicate.com/account/api-tokens + + + + + + Model Version +