| "use client"; |
|
|
| import { useState, useEffect, useCallback } from "react"; |
| import Link from "next/link"; |
| import { CardSkeleton } from "@/shared/components"; |
| import { CLI_TOOLS } from "@/shared/constants/cliTools"; |
| import { getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models"; |
| import { |
| ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, |
| HermesToolCard, DefaultToolCard, OpenCodeToolCard, CoworkToolCard, |
| CopilotToolCard, ClineToolCard, KiloToolCard, DeepSeekTuiToolCard, |
| JcodeToolCard, |
| } from "../components"; |
|
|
| const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL; |
|
|
| export default function ToolDetailClient({ toolId, machineId }) { |
| const tool = CLI_TOOLS[toolId]; |
| const [connections, setConnections] = useState([]); |
| const [loading, setLoading] = useState(true); |
| const [modelMappings, setModelMappings] = useState({}); |
| const [cloudEnabled, setCloudEnabled] = useState(false); |
| const [tunnelEnabled, setTunnelEnabled] = useState(false); |
| const [tunnelPublicUrl, setTunnelPublicUrl] = useState(""); |
| const [tailscaleEnabled, setTailscaleEnabled] = useState(false); |
| const [tailscaleUrl, setTailscaleUrl] = useState(""); |
| const [apiKeys, setApiKeys] = useState([]); |
|
|
| useEffect(() => { |
| let mounted = true; |
| (async () => { |
| try { |
| const [provRes, settingsRes, tunnelRes, keysRes] = await Promise.all([ |
| fetch("/api/providers"), |
| fetch("/api/settings"), |
| fetch("/api/tunnel/status"), |
| fetch("/api/keys"), |
| ]); |
| if (!mounted) return; |
| if (provRes.ok) { |
| const data = await provRes.json(); |
| setConnections(data.connections || []); |
| } |
| if (settingsRes.ok) { |
| const data = await settingsRes.json(); |
| setCloudEnabled(data.cloudEnabled || false); |
| } |
| if (tunnelRes.ok) { |
| const data = await tunnelRes.json(); |
| setTunnelEnabled(!!(data.tunnel?.enabled || data.tunnel?.settingsEnabled)); |
| setTunnelPublicUrl(data.tunnel?.publicUrl || ""); |
| setTailscaleEnabled(!!(data.tailscale?.enabled || data.tailscale?.settingsEnabled)); |
| setTailscaleUrl(data.tailscale?.tunnelUrl || ""); |
| } |
| if (keysRes.ok) { |
| const data = await keysRes.json(); |
| setApiKeys(data.keys || []); |
| } |
| } catch (error) { |
| console.log("Error loading tool data:", error); |
| } finally { |
| if (mounted) setLoading(false); |
| } |
| })(); |
| return () => { mounted = false; }; |
| }, []); |
|
|
| const getActiveProviders = () => connections.filter(c => c.isActive !== false); |
|
|
| const getAllAvailableModels = () => { |
| const activeProviders = getActiveProviders(); |
| const models = []; |
| const seenModels = new Set(); |
| activeProviders.forEach(conn => { |
| const alias = PROVIDER_ID_TO_ALIAS[conn.provider] || conn.provider; |
| const providerModels = getModelsByProviderId(conn.provider); |
| providerModels.forEach(m => { |
| const modelValue = `${alias}/${m.id}`; |
| if (!seenModels.has(modelValue)) { |
| seenModels.add(modelValue); |
| models.push({ value: modelValue, label: `${alias}/${m.id}`, provider: conn.provider, alias, connectionName: conn.name, modelId: m.id }); |
| } |
| }); |
| }); |
| return models; |
| }; |
|
|
| const handleModelMappingChange = useCallback((tId, alias, target) => { |
| setModelMappings(prev => { |
| if (prev[tId]?.[alias] === target) return prev; |
| return { ...prev, [tId]: { ...prev[tId], [alias]: target } }; |
| }); |
| }, []); |
|
|
| const getBaseUrl = () => { |
| if (tunnelEnabled && tunnelPublicUrl) return tunnelPublicUrl; |
| if (cloudEnabled && CLOUD_URL) return CLOUD_URL; |
| if (typeof window !== "undefined") return window.location.origin; |
| return "http://localhost:20128"; |
| }; |
|
|
| const renderToolCard = () => { |
| const availableModels = getAllAvailableModels(); |
| const hasActiveProviders = availableModels.length > 0; |
| const commonProps = { |
| tool, |
| isExpanded: true, |
| onToggle: () => {}, |
| baseUrl: getBaseUrl(), |
| apiKeys, |
| tunnelEnabled, |
| tunnelPublicUrl, |
| tailscaleEnabled, |
| tailscaleUrl, |
| }; |
|
|
| switch (toolId) { |
| case "claude": |
| return <ClaudeToolCard {...commonProps} activeProviders={getActiveProviders()} modelMappings={modelMappings[toolId] || {}} onModelMappingChange={(a, t) => handleModelMappingChange(toolId, a, t)} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />; |
| case "codex": |
| return <CodexToolCard {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />; |
| case "opencode": |
| return <OpenCodeToolCard {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />; |
| case "cowork": |
| return <CoworkToolCard {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} cloudUrl={CLOUD_URL} tunnelEnabled={tunnelEnabled} tunnelPublicUrl={tunnelPublicUrl} tailscaleEnabled={tailscaleEnabled} tailscaleUrl={tailscaleUrl} />; |
| case "droid": |
| return <DroidToolCard {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />; |
| case "openclaw": |
| return <OpenClawToolCard {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />; |
| case "hermes": |
| return <HermesToolCard {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />; |
| case "copilot": |
| return <CopilotToolCard {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />; |
| case "cline": |
| return <ClineToolCard {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />; |
| case "kilo": |
| return <KiloToolCard {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />; |
| case "deepseek-tui": |
| return <DeepSeekTuiToolCard {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />; |
| case "jcode": |
| return <JcodeToolCard {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />; |
| default: |
| return <DefaultToolCard toolId={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} tunnelEnabled={tunnelEnabled} />; |
| } |
| }; |
|
|
| |
| if (!tool) { |
| return ( |
| <div className="mx-auto flex w-full max-w-5xl flex-col gap-4 px-1 sm:px-0"> |
| <Link href="/dashboard/cli-tools" className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary w-fit"> |
| <span className="material-symbols-outlined text-[18px]">arrow_back</span> |
| Back to CLI Tools |
| </Link> |
| <p className="text-sm text-text-muted">Tool not found or disabled.</p> |
| </div> |
| ); |
| } |
|
|
| return ( |
| <div className="mx-auto flex w-full max-w-5xl flex-col gap-4 px-1 sm:px-0"> |
| <Link href="/dashboard/cli-tools" className="inline-flex items-center gap-1 text-sm text-text-muted hover:text-primary w-fit"> |
| <span className="material-symbols-outlined text-[18px]">arrow_back</span> |
| Back to CLI Tools |
| </Link> |
| <div className="flex flex-col gap-1"> |
| <h1 className="text-xl font-semibold text-text-main sm:text-2xl">{tool.name}</h1> |
| <p className="text-sm text-text-muted">{tool.description}</p> |
| </div> |
| {loading ? <CardSkeleton /> : renderToolCard()} |
| </div> |
| ); |
| } |
|
|