2026-02-08 16:28:13 +07:00
"use client" ;
import { useState , useEffect } from "react" ;
import { Card , Button , Badge , Modal , Input , ModelSelectModal } from "@/shared/components" ;
import Image from "next/image" ;
export default function AntigravityToolCard ({
tool ,
isExpanded ,
onToggle ,
baseUrl ,
apiKeys ,
activeProviders ,
hasActiveProviders ,
cloudEnabled ,
2026-02-25 16:32:05 +07:00
initialStatus ,
2026-02-08 16:28:13 +07:00
}) {
2026-02-25 16:32:05 +07:00
const [ status , setStatus ] = useState ( initialStatus || null );
2026-02-08 16:28:13 +07:00
const [ loading , setLoading ] = useState ( false );
2026-03-03 15:52:20 +07:00
const [ startingStep , setStartingStep ] = useState ( null ); // "cert" | "server" | "dns" | null
2026-02-08 16:28:13 +07:00
const [ showPasswordModal , setShowPasswordModal ] = useState ( false );
const [ sudoPassword , setSudoPassword ] = useState ( "" );
const [ selectedApiKey , setSelectedApiKey ] = useState ( "" );
const [ message , setMessage ] = useState ( null );
const [ modelMappings , setModelMappings ] = useState ({});
const [ modalOpen , setModalOpen ] = useState ( false );
const [ currentEditingAlias , setCurrentEditingAlias ] = useState ( null );
2026-03-05 13:17:22 +05:30
const [ modelAliases , setModelAliases ] = useState ({});
2026-02-08 16:28:13 +07:00
useEffect (() => {
if ( apiKeys ? . length > 0 && ! selectedApiKey ) {
setSelectedApiKey ( apiKeys [ 0 ]. key );
}
}, [ apiKeys , selectedApiKey ]);
2026-02-25 16:32:05 +07:00
useEffect (() => {
if ( initialStatus ) setStatus ( initialStatus );
}, [ initialStatus ]);
2026-02-08 16:28:13 +07:00
useEffect (() => {
if ( isExpanded && ! status ) {
fetchStatus ();
loadSavedMappings ();
2026-03-05 13:17:22 +05:30
fetchModelAliases ();
}
if ( isExpanded ) {
loadSavedMappings ();
fetchModelAliases ();
2026-02-08 16:28:13 +07:00
}
2026-02-25 16:32:05 +07:00
}, [ isExpanded ]);
2026-02-08 16:28:13 +07:00
const loadSavedMappings = async () => {
try {
const res = await fetch ( "/api/cli-tools/antigravity-mitm/alias?tool=antigravity" );
if ( res . ok ) {
const data = await res . json ();
const aliases = data . aliases || {};
2026-03-05 13:17:22 +05:30
2026-02-08 16:28:13 +07:00
if ( Object . keys ( aliases ). length > 0 ) {
setModelMappings ( aliases );
}
}
} catch ( error ) {
console . log ( "Error loading saved mappings:" , error );
}
};
2026-03-05 13:17:22 +05:30
const fetchModelAliases = async () => {
try {
const res = await fetch ( "/api/models/alias" );
const data = await res . json ();
if ( res . ok ) setModelAliases ( data . aliases || {});
} catch ( error ) {
console . log ( "Error fetching model aliases:" , error );
}
};
2026-02-08 16:28:13 +07:00
const fetchStatus = async () => {
try {
const res = await fetch ( "/api/cli-tools/antigravity-mitm" );
if ( res . ok ) {
const data = await res . json ();
setStatus ( data );
}
} catch ( error ) {
console . log ( "Error fetching status:" , error );
setStatus ({ running : false });
}
};
// Windows uses UAC dialog, no sudo needed
const isWindows = typeof navigator !== "undefined" && navigator . userAgent ? . includes ( "Windows" );
const handleStart = () => {
if ( isWindows || status ? . hasCachedPassword ) {
doStart ( "" );
} else {
setShowPasswordModal ( true );
setMessage ( null );
}
};
const handleStop = () => {
if ( isWindows || status ? . hasCachedPassword ) {
doStop ( "" );
} else {
setShowPasswordModal ( true );
setMessage ( null );
}
};
const doStart = async ( password ) => {
setLoading ( true );
setMessage ( null );
2026-03-03 15:52:20 +07:00
// Show steps progressing in order
setStartingStep ( "cert" );
2026-02-08 16:28:13 +07:00
try {
2026-03-05 13:17:22 +05:30
const keyToUse = selectedApiKey ? . trim ()
2026-02-08 16:28:13 +07:00
|| ( apiKeys ? . length > 0 ? apiKeys [ 0 ]. key : null )
|| ( ! cloudEnabled ? "sk_9router" : null );
const res = await fetch ( "/api/cli-tools/antigravity-mitm" , {
method : "POST" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ apiKey : keyToUse , sudoPassword : password }),
});
const data = await res . json ();
if ( res . ok ) {
2026-03-03 15:52:20 +07:00
setStartingStep ( null );
2026-02-08 16:28:13 +07:00
setMessage ({ type : "success" , text : "MITM started" });
setShowPasswordModal ( false );
setSudoPassword ( "" );
fetchStatus ();
} else {
2026-03-03 15:52:20 +07:00
setStartingStep ( null );
2026-02-08 16:28:13 +07:00
setMessage ({ type : "error" , text : data . error || "Failed to start" });
}
} catch ( error ) {
2026-03-03 15:52:20 +07:00
setStartingStep ( null );
2026-02-08 16:28:13 +07:00
setMessage ({ type : "error" , text : error . message });
} finally {
setLoading ( false );
}
};
const doStop = async ( password ) => {
setLoading ( true );
setMessage ( null );
try {
const res = await fetch ( "/api/cli-tools/antigravity-mitm" , {
method : "DELETE" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ sudoPassword : password }),
});
const data = await res . json ();
if ( res . ok ) {
setMessage ({ type : "success" , text : "MITM stopped" });
setShowPasswordModal ( false );
setSudoPassword ( "" );
fetchStatus ();
} else {
setMessage ({ type : "error" , text : data . error || "Failed to stop" });
}
} catch ( error ) {
setMessage ({ type : "error" , text : error . message });
} finally {
setLoading ( false );
}
};
const handleConfirmPassword = () => {
if ( ! sudoPassword . trim ()) {
setMessage ({ type : "error" , text : "Sudo password is required" });
return ;
}
if ( status ? . running ) {
doStop ( sudoPassword );
} else {
doStart ( sudoPassword );
}
};
const openModelSelector = ( alias ) => {
setCurrentEditingAlias ( alias );
setModalOpen ( true );
};
const handleModelSelect = ( model ) => {
if ( currentEditingAlias ) {
setModelMappings ( prev => ({
... prev ,
[ currentEditingAlias ] : model . value ,
}));
}
};
const handleModelMappingChange = ( alias , value ) => {
setModelMappings ( prev => ({
... prev ,
[ alias ] : value ,
}));
};
const handleSaveMappings = async () => {
setLoading ( true );
setMessage ( null );
try {
const res = await fetch ( "/api/cli-tools/antigravity-mitm/alias" , {
method : "PUT" ,
headers : { "Content-Type" : "application/json" },
body : JSON . stringify ({ tool : "antigravity" , mappings : modelMappings }),
});
if ( ! res . ok ) {
const data = await res . json ();
throw new Error ( data . error || "Failed to save mappings" );
}
setMessage ({ type : "success" , text : "Mappings saved!" });
} catch ( error ) {
setMessage ({ type : "error" , text : error . message });
} finally {
setLoading ( false );
}
};
const isRunning = status ? . running ;
return (
2026-03-03 10:10:03 +07:00
< Card padding = "xs" className = "overflow-hidden" >
2026-05-01 16:34:07 +07:00
< div className = "flex items-start justify-between gap-3 hover:cursor-pointer sm:items-center" onClick = { onToggle } >
< div className = "flex min-w-0 items-center gap-3" >
2026-02-08 16:28:13 +07:00
< div className = "size-8 flex items-center justify-center shrink-0" >
2026-03-05 13:17:22 +05:30
< Image
src = "/providers/antigravity.png"
alt = { tool . name }
width = { 32 }
height = { 32 }
className = "size-8 object-contain rounded-lg"
sizes = "32px"
onError = {( e ) => { e . target . style . display = "none" ; }}
2026-02-08 16:28:13 +07:00
/>
< /div>
< div className = "min-w-0" >
2026-05-01 16:34:07 +07:00
< div className = "flex min-w-0 flex-wrap items-center gap-2" >
2026-02-08 16:28:13 +07:00
< h3 className = "font-medium text-sm" > { tool . name } < /h3>
{ isRunning ? (
< Badge variant = "success" size = "sm" > Active < /Badge>
) : (
< Badge variant = "default" size = "sm" > Inactive < /Badge>
)}
< /div>
< p className = "text-xs text-text-muted truncate" > { tool . description } < /p>
< /div>
< /div>
< span className = { `material-symbols-outlined text-text-muted text-[20px] transition-transform ${ isExpanded ? "rotate-180" : "" } ` } > expand_more < /span>
< /div>
{ isExpanded && (
< div className = "mt-4 pt-4 border-t border-border flex flex-col gap-4" >
2026-03-03 15:52:20 +07:00
{ /* Status indicators — ordered: Cert → Server → DNS */ }
< div className = "flex items-center gap-1" >
2026-02-28 10:04:57 +07:00
{[
2026-03-05 13:17:22 +05:30
{ key : "cert" , label : "Cert" , ok : status ? . certExists },
2026-03-03 15:52:20 +07:00
{ key : "server" , label : "Server" , ok : status ? . running },
2026-03-05 13:17:22 +05:30
{ key : "dns" , label : "DNS" , ok : status ? . dnsConfigured },
2026-03-03 15:52:20 +07:00
]. map (({ key , label , ok }, i ) => {
const isLoading = startingStep === key ;
return (
< div key = { key } className = "flex items-center" >
< div className = "flex items-center gap-1 px-2 py-1 rounded-md" >
{ isLoading ? (
< span className = "material-symbols-outlined text-[14px] text-primary animate-spin" > progress_activity < /span>
) : (
< span className = { `material-symbols-outlined text-[14px] ${ ok ? "text-green-500" : "text-text-muted" } ` } >
{ ok ? "check_circle" : "radio_button_unchecked" }
< /span>
)}
< span className = { `text-xs font-medium ${ isLoading ? "text-primary" : ok ? "text-green-500" : "text-text-muted" } ` } >
{ label }
< /span>
< /div>
{ i < 2 && < span className = "material-symbols-outlined text-[12px] text-text-muted" > arrow_forward < /span>}
< /div>
);
})}
2026-02-28 10:04:57 +07:00
< /div>
{ /* Start/Stop Button */ }
2026-05-03 14:55:43 +07:00
< div className = "grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2" >
2026-02-08 16:28:13 +07:00
{ isRunning ? (
2026-03-05 13:17:22 +05:30
< button
onClick = { handleStop }
2026-02-08 16:28:13 +07:00
disabled = { loading }
className = "px-4 py-2 rounded-lg bg-red-500/10 border border-red-500/30 text-red-500 font-medium text-sm flex items-center gap-2 hover:bg-red-500/20 transition-colors disabled:opacity-50"
>
< span className = "material-symbols-outlined text-[18px]" > stop_circle < /span>
Stop MITM
< /button>
) : (
2026-03-05 13:17:22 +05:30
< button
onClick = { handleStart }
2026-02-08 16:28:13 +07:00
disabled = { loading || ! hasActiveProviders }
className = "px-4 py-2 rounded-lg bg-primary/10 border border-primary/30 text-primary font-medium text-sm flex items-center gap-2 hover:bg-primary/20 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
>
< span className = "material-symbols-outlined text-[18px]" > play_circle < /span>
Start MITM
< /button>
)}
< /div>
{ message ? . type === "error" && (
< div className = "flex items-center gap-2 px-2 py-1.5 rounded text-xs bg-red-500/10 text-red-600" >
< span className = "material-symbols-outlined text-[14px]" > error < /span>
< span > { message . text } < /span>
< /div>
)}
{ /* When running: API Key + Model Mappings */ }
{ isRunning && (
<>
2026-05-03 14:55:43 +07:00
< div className = "grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2" >
2026-05-01 16:34:07 +07:00
< span className = "text-xs font-semibold text-text-main sm:text-right sm:text-sm" > API Key < /span>
< span className = "material-symbols-outlined hidden text-text-muted text-[14px] sm:inline" > arrow_forward < /span>
2026-02-08 16:28:13 +07:00
{ apiKeys . length > 0 ? (
2026-03-05 13:17:22 +05:30
< select
value = { selectedApiKey }
onChange = {( e ) => setSelectedApiKey ( e . target . value )}
2026-05-03 14:55:43 +07:00
className = "w-full min-w-0 px-2 py-2 bg-surface rounded text-xs border border-border focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
2026-02-08 16:28:13 +07:00
>
{ apiKeys . map (( key ) => < option key = { key . id } value = { key . key } > { key . key } < /option>)}
< /select>
) : (
2026-05-01 16:34:07 +07:00
< span className = "min-w-0 rounded bg-surface/40 px-2 py-2 text-xs text-text-muted sm:py-1.5" >
2026-02-08 16:28:13 +07:00
{ cloudEnabled ? "No API keys - Create one in Keys page" : "sk_9router (default)" }
< /span>
)}
< /div>
{ tool . defaultModels . map (( model ) => (
2026-05-03 14:55:43 +07:00
< div key = { model . alias } className = "grid grid-cols-1 gap-1.5 sm:grid-cols-[8rem_auto_1fr_auto] sm:items-center sm:gap-2" >
2026-05-01 16:34:07 +07:00
< span className = "text-xs font-semibold text-text-main sm:text-right sm:text-sm" > { model . name } < /span>
< span className = "material-symbols-outlined hidden text-text-muted text-[14px] sm:inline" > arrow_forward < /span>
2026-03-05 13:17:22 +05:30
< input
type = "text"
value = { modelMappings [ model . alias ] || "" }
onChange = {( e ) => handleModelMappingChange ( model . alias , e . target . value )}
placeholder = "provider/model-id"
2026-05-03 14:55:43 +07:00
className = "w-full min-w-0 px-2 py-2 bg-surface rounded border border-border text-xs focus:outline-none focus:ring-1 focus:ring-primary/50 sm:py-1.5"
2026-02-08 16:28:13 +07:00
/>
2026-03-05 13:17:22 +05:30
< button
onClick = {() => openModelSelector ( model . alias )}
disabled = { ! hasActiveProviders }
2026-05-03 14:55:43 +07:00
className = { `w-full sm:w-auto rounded border px-2 py-2 text-xs transition-colors sm:py-1.5 whitespace-nowrap sm:shrink-0 ${ hasActiveProviders ? "bg-surface border-border text-text-main hover:border-primary cursor-pointer" : "opacity-50 cursor-not-allowed border-border" } ` }
2026-02-08 16:28:13 +07:00
>
Select
< /button>
{ modelMappings [ model . alias ] && (
2026-03-05 13:17:22 +05:30
< button
onClick = {() => handleModelMappingChange ( model . alias , "" )}
className = "p-1 text-text-muted hover:text-red-500 rounded transition-colors"
2026-02-08 16:28:13 +07:00
title = "Clear"
>
< span className = "material-symbols-outlined text-[14px]" > close < /span>
< /button>
)}
< /div>
))}
2026-05-01 16:34:07 +07:00
< div className = "grid grid-cols-1 gap-2 sm:flex sm:items-center" >
2026-03-05 13:17:22 +05:30
< Button
variant = "primary"
size = "sm"
2026-02-08 16:28:13 +07:00
onClick = { handleSaveMappings }
disabled = { loading || Object . keys ( modelMappings ). length === 0 }
>
< span className = "material-symbols-outlined text-[14px] mr-1" > save < /span>
Save Mappings
< /Button>
< /div>
< />
)}
2026-02-28 16:33:18 +07:00
{ /* Windows admin warning */ }
{ ! isRunning && isWindows && (
< div className = "flex items-center gap-2 px-2 py-1.5 rounded text-xs bg-yellow-500/10 text-yellow-600 border border-yellow-500/20" >
< span className = "material-symbols-outlined text-[14px]" > warning < /span>
< span > Windows : Run terminal ( 9 Router ) as Administrator to enable MITM < /span>
< /div>
)}
2026-02-08 16:28:13 +07:00
{ /* When stopped: how it works */ }
{ ! isRunning && (
< div className = "flex flex-col gap-1.5 px-1" >
< p className = "text-xs text-text-muted" >
< span className = "font-medium text-text-main" > How it works :< /span> Intercepts Antigravity traffic via DNS redirect, letting you reroute models through 9Router.
< /p>
< div className = "flex flex-col gap-0.5 text-[11px] text-text-muted" >
< span > 1. Generates SSL cert & adds to system keychain < /span>
< span > 2. Redirects < code className = "text-[10px] bg-surface px-1 rounded" > daily - cloudcode - pa . googleapis . com < /code> → localhost</span>
< span > 3. Maps Antigravity models to any provider via 9 Router < /span>
< /div>
< /div>
)}
< /div>
)}
{ /* Password Modal */ }
< Modal
isOpen = { showPasswordModal }
onClose = {() => {
setShowPasswordModal ( false );
setSudoPassword ( "" );
setMessage ( null );
}}
title = "Sudo Password Required"
size = "sm"
>
< div className = "flex flex-col gap-4" >
< div className = "flex items-start gap-3 p-3 bg-yellow-500/10 border border-yellow-500/30 rounded-lg" >
< span className = "material-symbols-outlined text-yellow-500 text-[20px]" > warning < /span>
< p className = "text-xs text-text-muted" > Required for SSL certificate and DNS configuration < /p>
< /div>
< Input
type = "password"
placeholder = "Enter sudo password"
value = { sudoPassword }
onChange = {( e ) => setSudoPassword ( e . target . value )}
onKeyDown = {( e ) => {
if ( e . key === "Enter" && ! loading ) handleConfirmPassword ();
}}
/>
{ message && (
< div className = { `flex items-center gap-2 px-2 py-1.5 rounded text-xs ${ message . type === "success" ? "bg-green-500/10 text-green-600" : "bg-red-500/10 text-red-600" } ` } >
< span className = "material-symbols-outlined text-[14px]" > { message . type === "success" ? "check_circle" : "error" } < /span>
< span > { message . text } < /span>
< /div>
)}
< div className = "flex items-center justify-end gap-2" >
2026-03-05 13:17:22 +05:30
< Button
variant = "ghost"
size = "sm"
2026-02-08 16:28:13 +07:00
onClick = {() => { setShowPasswordModal ( false ); setSudoPassword ( "" ); setMessage ( null ); }}
disabled = { loading }
>
Cancel
< /Button>
2026-03-05 13:17:22 +05:30
< Button
variant = "primary"
size = "sm"
2026-02-08 16:28:13 +07:00
onClick = { handleConfirmPassword }
loading = { loading }
>
Confirm
< /Button>
< /div>
< /div>
< /Modal>
{ /* Model Select Modal */ }
2026-03-05 13:17:22 +05:30
< ModelSelectModal
isOpen = { modalOpen }
onClose = {() => setModalOpen ( false )}
onSelect = { handleModelSelect }
selectedModel = { currentEditingAlias ? modelMappings [ currentEditingAlias ] : null }
activeProviders = { activeProviders }
modelAliases = { modelAliases }
title = { `Select model for ${ currentEditingAlias } ` }
2026-02-08 16:28:13 +07:00
/>
< /Card>
);
}