187 lines
5.8 KiB
JavaScript
187 lines
5.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 { PROVIDER_MODELS, getModelsByProviderId, PROVIDER_ID_TO_ALIAS } from "@/shared/constants/models";
|
|
import { ClaudeToolCard, CodexToolCard, DroidToolCard, OpenClawToolCard, DefaultToolCard } from "./components";
|
|
|
|
const CLOUD_URL = process.env.NEXT_PUBLIC_CLOUD_URL;
|
|
|
|
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 [apiKeys, setApiKeys] = useState([]);
|
|
|
|
useEffect(() => {
|
|
fetchConnections();
|
|
loadCloudSettings();
|
|
fetchApiKeys();
|
|
}, []);
|
|
|
|
const loadCloudSettings = async () => {
|
|
try {
|
|
const res = await fetch("/api/settings");
|
|
if (res.ok) {
|
|
const data = await res.json();
|
|
setCloudEnabled(data.cloudEnabled || false);
|
|
}
|
|
} catch (error) {
|
|
console.log("Error loading cloud 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 = () => {
|
|
return 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: alias,
|
|
connectionName: conn.name,
|
|
modelId: m.id,
|
|
});
|
|
}
|
|
});
|
|
});
|
|
|
|
return models;
|
|
};
|
|
|
|
const handleModelMappingChange = useCallback((toolId, modelAlias, targetModel) => {
|
|
setModelMappings(prev => {
|
|
// Prevent unnecessary updates if value hasn't changed
|
|
if (prev[toolId]?.[modelAlias] === targetModel) {
|
|
return prev;
|
|
}
|
|
return {
|
|
...prev,
|
|
[toolId]: {
|
|
...prev[toolId],
|
|
[modelAlias]: targetModel,
|
|
},
|
|
};
|
|
});
|
|
}, []);
|
|
|
|
const getBaseUrl = () => {
|
|
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-6">
|
|
<div className="flex flex-col gap-4">
|
|
<CardSkeleton />
|
|
<CardSkeleton />
|
|
<CardSkeleton />
|
|
</div>
|
|
</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}
|
|
/>
|
|
);
|
|
case "codex":
|
|
return <CodexToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />;
|
|
case "droid":
|
|
return <DroidToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />;
|
|
case "openclaw":
|
|
return <OpenClawToolCard key={toolId} {...commonProps} activeProviders={getActiveProviders()} hasActiveProviders={hasActiveProviders} cloudEnabled={cloudEnabled} />;
|
|
default:
|
|
return <DefaultToolCard key={toolId} toolId={toolId} {...commonProps} activeProviders={getActiveProviders()} cloudEnabled={cloudEnabled} />;
|
|
}
|
|
};
|
|
|
|
return (
|
|
<div className="flex flex-col gap-6">
|
|
{!hasActiveProviders && (
|
|
<Card className="border-yellow-500/50 bg-yellow-500/5">
|
|
<div className="flex items-center gap-3">
|
|
<span className="material-symbols-outlined text-yellow-500">warning</span>
|
|
<div>
|
|
<p className="font-medium text-yellow-600 dark:text-yellow-400">No active providers</p>
|
|
<p className="text-sm text-text-muted">Please add and connect providers first to configure CLI tools.</p>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
)}
|
|
|
|
<div className="flex flex-col gap-4">
|
|
{Object.entries(CLI_TOOLS).map(([toolId, tool]) => renderToolCard(toolId, tool))}
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|