9router / src /app /(dashboard) /dashboard /cli-tools /[toolId] /ToolDetailClient.js
2api
feat: configurable stream stall timeout + per-provider UI
88c4c60
Raw
History Blame Contribute Delete
7.84 kB
"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} />;
}
};
// 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>
);
}
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>
);
}