2026-05-14 10:46:11 +07:00
|
|
|
"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} />;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-29 15:41:50 +07:00
|
|
|
// Guard removed/unknown tools (e.g. disabled Cowork) to avoid crash on direct URL.
|
|
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-14 10:46:11 +07:00
|
|
|
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>
|
|
|
|
|
);
|
|
|
|
|
}
|