"use client"; import { useState, useEffect, useCallback } from "react"; import { Card, Button, Badge, Input, ModelSelectModal } from "@/shared/components"; 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, 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 isWindows = typeof navigator !== "undefined" && navigator.userAgent?.includes("Windows"); 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 (isWindows || hasCachedPassword) { 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 && (
{/* 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={`flex-1 px-2 py-1.5 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 ${!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}`} /> ); }