"use client"; import { useState, useEffect, useRef } from "react"; import { Card, Button, ModelSelectModal, ManualConfigModal } from "@/shared/components"; import Image from "next/image"; import BaseUrlSelect from "./BaseUrlSelect"; import ApiKeySelect from "./ApiKeySelect"; import { matchKnownEndpoint } from "./cliEndpointMatch"; const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; export default function DroidToolCard({ tool, isExpanded, onToggle, baseUrl, hasActiveProviders, apiKeys, activeProviders, cloudEnabled, initialStatus, tunnelEnabled, tunnelPublicUrl, tailscaleEnabled, tailscaleUrl, }) { const [droidStatus, setDroidStatus] = useState(initialStatus || null); const [checkingDroid, setCheckingDroid] = useState(false); const [applying, setApplying] = useState(false); const [restoring, setRestoring] = useState(false); const [message, setMessage] = useState(null); const [selectedApiKey, setSelectedApiKey] = useState(""); const [modelList, setModelList] = useState([]); const [modelInput, setModelInput] = useState(""); const [modalOpen, setModalOpen] = useState(false); const [modelAliases, setModelAliases] = useState({}); const [showManualConfigModal, setShowManualConfigModal] = useState(false); const [showInstallGuide, setShowInstallGuide] = useState(false); const [customBaseUrl, setCustomBaseUrl] = useState(""); const hasInitializedModel = useRef(false); const getConfigStatus = () => { if (!droidStatus?.installed) return null; // Check for any 9Router model entry (support multi-model: custom:9Router-0, custom:9Router-1, ...) const currentConfig = droidStatus.settings?.customModels?.find(m => m.id?.startsWith("custom:9Router")); if (!currentConfig) return "not_configured"; return matchKnownEndpoint(currentConfig.baseUrl, { tunnelPublicUrl, tailscaleUrl, cloudUrl: cloudEnabled ? CLOUD_URL : null }) ? "configured" : "other"; }; const configStatus = getConfigStatus(); useEffect(() => { if (apiKeys?.length > 0 && !selectedApiKey) { setSelectedApiKey(apiKeys[0].key); } }, [apiKeys, selectedApiKey]); useEffect(() => { if (initialStatus) setDroidStatus(initialStatus); }, [initialStatus]); useEffect(() => { if (isExpanded && !droidStatus) { checkDroidStatus(); fetchModelAliases(); } if (isExpanded) fetchModelAliases(); }, [isExpanded]); 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); } }; // Pre-fill model list from existing config (supports multi-model) useEffect(() => { if (droidStatus?.installed && !hasInitializedModel.current) { hasInitializedModel.current = true; const existingModels = (droidStatus.settings?.customModels || []) .filter(m => m.id?.startsWith("custom:9Router")) .sort((a, b) => (a.index || 0) - (b.index || 0)) .map(m => m.model); if (existingModels.length > 0) { setModelList(existingModels); } else { // Legacy: single model stored as custom:9Router-0 const legacy = droidStatus.settings?.customModels?.find(m => m.id === "custom:9Router-0"); if (legacy?.model) { setModelList([legacy.model]); } } } }, [droidStatus]); const checkDroidStatus = async () => { setCheckingDroid(true); try { const res = await fetch("/api/cli-tools/droid-settings"); const data = await res.json(); setDroidStatus(data); } catch (error) { setDroidStatus({ installed: false, error: error.message }); } finally { setCheckingDroid(false); } }; const getEffectiveBaseUrl = () => { const url = customBaseUrl || baseUrl; return url.endsWith("/v1") ? url : `${url}/v1`; }; const getDisplayUrl = () => { const url = customBaseUrl || baseUrl; return url.endsWith("/v1") ? url : `${url}/v1`; }; const addModel = () => { const val = modelInput.trim(); if (!val || modelList.includes(val)) return; setModelList((prev) => [...prev, val]); setModelInput(""); }; const removeModel = (id) => setModelList((prev) => prev.filter((m) => m !== id)); const handleModelSelect = (model) => { if (!model.value || modelList.includes(model.value)) return; setModelList((prev) => [...prev, model.value]); setModalOpen(false); }; const handleApplySettings = async () => { setApplying(true); setMessage(null); try { const keyToUse = selectedApiKey?.trim() || (apiKeys?.length > 0 ? apiKeys[0].key : null) || (!cloudEnabled ? "sk_9router" : null); const res = await fetch("/api/cli-tools/droid-settings", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, models: modelList, activeModel: modelList[0] || "", }), }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: "Settings applied successfully!" }); checkDroidStatus(); } else { setMessage({ type: "error", text: data.error || "Failed to apply settings" }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setApplying(false); } }; const handleResetSettings = async () => { setRestoring(true); setMessage(null); try { const res = await fetch("/api/cli-tools/droid-settings", { method: "DELETE" }); const data = await res.json(); if (res.ok) { setMessage({ type: "success", text: "Settings reset successfully!" }); setModelList([]); checkDroidStatus(); } else { setMessage({ type: "error", text: data.error || "Failed to reset settings" }); } } catch (error) { setMessage({ type: "error", text: error.message }); } finally { setRestoring(false); } }; const getManualConfigs = () => { const keyToUse = (selectedApiKey && selectedApiKey.trim()) ? selectedApiKey : (!cloudEnabled ? "sk_9router" : ""); const settingsContent = { customModels: modelList.map((m, i) => ({ model: m, id: `custom:9Router-${i}`, index: i, baseUrl: getEffectiveBaseUrl(), apiKey: keyToUse, displayName: m, maxOutputTokens: 131072, noImageSupport: false, provider: "openai", })), }; const platform = typeof navigator !== "undefined" && navigator.platform; const isWindows = platform?.toLowerCase().includes("win"); const settingsPath = isWindows ? "%USERPROFILE%\\.factory\\settings.json" : "~/.factory/settings.json"; return [ { filename: settingsPath, content: JSON.stringify(settingsContent, null, 2), }, ]; }; return (
{tool.name} { e.target.style.display = "none"; }} />

{tool.name}

{configStatus === "configured" && Connected} {configStatus === "not_configured" && Not configured} {configStatus === "other" && Other}

{tool.description}

expand_more
{isExpanded && (
{checkingDroid && (
progress_activity Checking Factory Droid CLI...
)} {!checkingDroid && droidStatus && !droidStatus.installed && (
warning

Factory Droid CLI not detected locally

Manual configuration is still available if 9router is deployed on a remote server.

{showInstallGuide && (

Installation Guide

macOS / Linux / Windows:

curl -fsSL https://app.factory.ai/cli | sh

After installation, run droid to verify.

)}
)} {!checkingDroid && droidStatus?.installed && ( <>
{/* Endpoint (selector) */}
Select Endpoint arrow_forward
{/* Current configured */} {droidStatus?.settings?.customModels?.find(m => m.id?.startsWith("custom:9Router"))?.baseUrl && (
Current arrow_forward {droidStatus.settings.customModels.find(m => m.id?.startsWith("custom:9Router")).baseUrl}
)} {/* API Key */}
API Key arrow_forward
{/* Models */}
Models {modelList.length > 0 && ({modelList.length})} arrow_forward
{/* Model list */} {modelList.length > 0 && (
{modelList.map((id) => (
{id}
))}
)} {/* Model input row */}
setModelInput(e.target.value)} onKeyDown={(e) => { if (e.key === "Enter") { e.preventDefault(); addModel(); } }} placeholder="provider/model-id" 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" />
{message && (
{message.type === "success" ? "check_circle" : "error"} {message.text}
)}
)}
)} setModalOpen(false)} onSelect={handleModelSelect} selectedModel={null} activeProviders={activeProviders} modelAliases={modelAliases} title="Select Model for Factory Droid" /> setShowManualConfigModal(false)} title="Factory Droid - Manual Configuration" configs={getManualConfigs()} />
); }