"use client"; import { useState, useEffect, useCallback } from "react"; import { Card, Button, Badge, Input, ModelSelectModal } from "@/shared/components"; import { TOOL_HOSTS } from "@/shared/constants/mitmToolHosts"; import Image from "next/image"; /** * Per-tool MITM card — shows DNS status + model mappings. * - Auto-saves model mapping on blur or modal select * - Skips sudo modal if password is already cached * - Model mappings can only be edited when DNS is active */ export default function MitmToolCard({ tool, isExpanded, onToggle, serverRunning, dnsActive, hasCachedPassword, needsSudoPassword, isWin, apiKeys, activeProviders, hasActiveProviders, modelAliases = {}, cloudEnabled, onDnsChange, }) { const [loading, setLoading] = useState(false); const [warning, setWarning] = useState(null); const [showPasswordModal, setShowPasswordModal] = useState(false); const [sudoPassword, setSudoPassword] = useState(""); const [pendingDnsAction, setPendingDnsAction] = useState(null); const [modalError, setModalError] = useState(null); const [modelMappings, setModelMappings] = useState({}); const [modalOpen, setModalOpen] = useState(false); const [currentEditingAlias, setCurrentEditingAlias] = useState(null); const mitmHosts = TOOL_HOSTS[tool.id] ?? []; const canRunWithoutPassword = isWin || hasCachedPassword || needsSudoPassword === false; useEffect(() => { if (isExpanded) loadSavedMappings(); }, [isExpanded]); const loadSavedMappings = async () => { try { const res = await fetch(`/api/cli-tools/antigravity-mitm/alias?tool=${tool.id}`); if (res.ok) { const data = await res.json(); if (Object.keys(data.aliases || {}).length > 0) setModelMappings(data.aliases); } } catch { /* ignore */ } }; const saveMappings = useCallback(async (mappings) => { try { await fetch("/api/cli-tools/antigravity-mitm/alias", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tool: tool.id, mappings }), }); } catch { /* ignore */ } }, [tool.id]); const handleMappingBlur = (alias, value) => { saveMappings({ ...modelMappings, [alias]: value }); }; const handleModelMappingChange = (alias, value) => { setModelMappings(prev => ({ ...prev, [alias]: value })); }; const openModelSelector = (alias) => { setCurrentEditingAlias(alias); setModalOpen(true); }; const handleModelSelect = (model) => { if (!currentEditingAlias || model.isPlaceholder) return; const updated = { ...modelMappings, [currentEditingAlias]: model.value }; setModelMappings(updated); saveMappings(updated); }; const handleDnsToggle = () => { if (!serverRunning) return; const action = dnsActive ? "disable" : "enable"; if (canRunWithoutPassword) { doDnsAction(action, ""); } else { setPendingDnsAction(action); setShowPasswordModal(true); setModalError(null); } }; const doDnsAction = async (action, password) => { setLoading(true); setWarning(null); try { const res = await fetch("/api/cli-tools/antigravity-mitm", { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ tool: tool.id, action, sudoPassword: password }), }); const data = await res.json(); if (!res.ok) throw new Error(data.error || "Failed to toggle DNS"); if (action === "enable") { setWarning(`Restart ${tool.name} to apply changes`); } setShowPasswordModal(false); setSudoPassword(""); onDnsChange?.(data); } catch { /* ignore */ } finally { setLoading(false); setPendingDnsAction(null); } }; const handleConfirmPassword = () => { if (!sudoPassword.trim()) { setModalError("Sudo password is required"); return; } doDnsAction(pendingDnsAction, sudoPassword); }; return ( <>
{tool.name} { e.target.style.display = "none"; }} />

{tool.name}

{!serverRunning ? ( Server off ) : dnsActive ? ( Active ) : ( DNS off )}

Intercept {tool.name} requests via MITM proxy

expand_more
{isExpanded && (
{/* Hosts */} {mitmHosts.length > 0 && (

Edit hosts file manually to add the following entries:

    {mitmHosts.map((h) => (
  • 127.0.0.1 {h}
  • ))}
)} {/* Info */}

Toggle DNS to redirect {tool.name} traffic through 9Router via MITM.

{!dnsActive && (

⚠️ Enable DNS to edit model mappings

)}
{/* Model Mappings */} {tool.defaultModels?.length > 0 && (
{tool.defaultModels.map((model) => (
{model.name} arrow_forward
handleModelMappingChange(model.alias, e.target.value)} onBlur={(e) => handleMappingBlur(model.alias, e.target.value)} placeholder="provider/model-id" disabled={!dnsActive} className={`w-full min-w-0 pl-2 pr-7 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5 ${!dnsActive ? "opacity-50 cursor-not-allowed" : ""}`} /> {modelMappings[model.alias] && ( )}
))}
)} {tool.defaultModels?.length === 0 && (

Model mappings will be available soon.

)} {/* Start / Stop DNS button */}
{dnsActive ? ( ) : ( )} {/* Warning below button */} {warning && (
warning {warning}
)}
)}
{/* Password Modal */} {showPasswordModal && (

Sudo Password Required

warning

Required to modify /etc/hosts and flush DNS cache

setSudoPassword(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter" && !loading) handleConfirmPassword(); }} /> {modalError && (
error {modalError}
)}
)} {/* Model Select Modal */} setModalOpen(false)} onSelect={handleModelSelect} selectedModel={currentEditingAlias ? modelMappings[currentEditingAlias] : null} activeProviders={activeProviders} modelAliases={modelAliases} title={`Select model for ${currentEditingAlias}`} /> ); }