Files
9router/src/app/(dashboard)/dashboard/cli-tools/CLIToolsPageClient.js
Zhen 0da61d8f7b Improve dashboard responsive layouts (#805)
* Improve dashboard responsive layouts

* Improve proxy pools mobile layout

* Improve CLI tool card input responsiveness

---------

Co-authored-by: Delynn Assistant <zhen@dkzhen.org>
2026-05-01 16:34:07 +07:00

214 lines
7.8 KiB
JavaScript

"use client";
import { useState, useEffect, useCallback } from "react";
import { Card, 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, MitmLinkCard } from "./components";
import { MITM_TOOLS } from "@/shared/constants/cliTools";
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
const STATUS_ENDPOINTS = {
claude: "/api/cli-tools/claude-settings",
codex: "/api/cli-tools/codex-settings",
opencode: "/api/cli-tools/opencode-settings",
droid: "/api/cli-tools/droid-settings",
openclaw: "/api/cli-tools/openclaw-settings",
hermes: "/api/cli-tools/hermes-settings",
};
export default function CLIToolsPageClient({ machineId }) {
const [connections, setConnections] = useState([]);
const [loading, setLoading] = useState(true);
const [expandedTool, setExpandedTool] = useState(null);
const [modelMappings, setModelMappings] = useState({});
const [cloudEnabled, setCloudEnabled] = useState(false);
const [tunnelEnabled, setTunnelEnabled] = useState(false);
const [tunnelPublicUrl, setTunnelPublicUrl] = useState("");
const [apiKeys, setApiKeys] = useState([]);
const [toolStatuses, setToolStatuses] = useState({});
useEffect(() => {
fetchConnections();
loadCloudSettings();
fetchApiKeys();
fetchAllStatuses();
}, []);
const fetchAllStatuses = async () => {
try {
const entries = await Promise.all(
Object.entries(STATUS_ENDPOINTS).map(async ([toolId, url]) => {
try {
const res = await fetch(url);
const data = await res.json();
return [toolId, data];
} catch {
return [toolId, null];
}
})
);
setToolStatuses(Object.fromEntries(entries));
} catch (error) {
console.log("Error fetching tool statuses:", error);
}
};
const loadCloudSettings = async () => {
try {
const [settingsRes, tunnelRes] = await Promise.all([
fetch("/api/settings"),
fetch("/api/tunnel/status"),
]);
if (settingsRes.ok) {
const data = await settingsRes.json();
setCloudEnabled(data.cloudEnabled || false);
}
if (tunnelRes.ok) {
const data = await tunnelRes.json();
setTunnelEnabled(data.enabled || false);
setTunnelPublicUrl(data.publicUrl || "");
}
} catch (error) {
console.log("Error loading settings:", error);
}
};
const fetchApiKeys = async () => {
try {
const res = await fetch("/api/keys");
if (res.ok) {
const data = await res.json();
setApiKeys(data.keys || []);
}
} catch (error) {
console.log("Error fetching API keys:", error);
}
};
const fetchConnections = async () => {
try {
const res = await fetch("/api/providers");
const data = await res.json();
if (res.ok) {
setConnections(data.connections || []);
}
} catch (error) {
console.log("Error fetching connections:", error);
} finally {
setLoading(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((toolId, modelAlias, targetModel) => {
setModelMappings(prev => {
if (prev[toolId]?.[modelAlias] === targetModel) return prev;
return { ...prev, [toolId]: { ...prev[toolId], [modelAlias]: targetModel } };
});
}, []);
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";
};
if (loading) {
return (
<div className="flex flex-col gap-4">
<CardSkeleton />
<CardSkeleton />
<CardSkeleton />
</div>
);
}
const availableModels = getAllAvailableModels();
const hasActiveProviders = availableModels.length > 0;
const renderToolCard = (toolId, tool) => {
const commonProps = {
tool,
isExpanded: expandedTool === toolId,
onToggle: () => setExpandedTool(expandedTool === toolId ? null : toolId),
baseUrl: getBaseUrl(),
apiKeys,
};
switch (toolId) {
case "claude":
return (
<ClaudeToolCard
key={toolId}
{...commonProps}
activeProviders={getActiveProviders()}
modelMappings={modelMappings[toolId] || {}}
onModelMappingChange={(alias, target) => handleModelMappingChange(toolId, alias, target)}
hasActiveProviders={hasActiveProviders}
cloudEnabled={cloudEnabled}
initialStatus={toolStatuses.claude}
/>
);
case "codex":
return <CodexToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.codex} />;
case "opencode":
return <OpenCodeToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.opencode} />;
case "droid":
return <DroidToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.droid} />;
case "openclaw":
return <OpenClawToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.openclaw} />;
case "hermes":
return <HermesToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} initialStatus={toolStatuses.hermes} />;
default:
return <DefaultToolCard key={toolId} toolId={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} tunnelEnabled={tunnelEnabled} />;
}
};
const regularTools = Object.entries(CLI_TOOLS);
const mitmTools = Object.entries(MITM_TOOLS);
return (
<div className="mx-auto flex w-full max-w-5xl flex-col gap-6 px-1 sm:px-0">
<div className="flex flex-col gap-1">
<h1 className="text-xl font-semibold text-text-main sm:text-2xl">CLI Tools</h1>
<p className="text-sm text-text-muted">Configure local coding tools to use your 9Router providers.</p>
</div>
<div className="grid gap-3 sm:gap-4">
{regularTools.map(([toolId, tool]) => renderToolCard(toolId, tool))}
</div>
<div className="grid gap-3 sm:gap-4">
<div className="flex items-center gap-2 px-1">
<span className="material-symbols-outlined text-[18px] text-primary">security</span>
<h2 className="text-sm font-semibold text-text-main">MITM Tools</h2>
</div>
{mitmTools.map(([toolId, tool]) => (
<MitmLinkCard key={toolId} tool={tool} />
))}
</div>
</div>
);
}