Files
sitemente/app/mission-control/clawhub/page.tsx
T

365 lines
13 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useState, useEffect, useCallback } from "react";
import BackToMC from "@/components/mission-control/BackToMC";
interface Skill {
slug: string;
name: string;
score: number;
description?: string;
}
const AGENTS = ["horus", "amun", "cleo"] as const;
const AGENT_LABELS: Record<string, string> = {
horus: "🦅 Horus (Local)",
amun: "🤖 Amun",
cleo: "👑 Cleo",
};
export default function ClawHubMarketplace() {
const [skills, setSkills] = useState<Skill[]>([]);
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
const [selectedAgents, setSelectedAgents] = useState<string[]>(["horus"]);
const [action, setAction] = useState<"install" | "delete" | "sync" | "push" | null>(null);
const [output, setOutput] = useState("");
const [installedSkills, setInstalledSkills] = useState<Record<string, string[]>>({});
const [syncing, setSyncing] = useState(false);
const fetchInstalled = useCallback(async () => {
try {
const res = await fetch("/api/clawhub?action=list");
const data = await res.json();
setInstalledSkills(data.installed || {});
} catch (e) {
console.error(e);
}
}, []);
const searchSkills = useCallback(async (query: string) => {
setLoading(true);
try {
const res = await fetch(`/api/clawhub?action=search&q=${encodeURIComponent(query)}`);
const data = await res.json();
setSkills(data.skills || []);
} catch (e) {
console.error(e);
}
setLoading(false);
}, []);
const refreshPopular = useCallback(async () => {
setLoading(true);
try {
const res = await fetch("/api/clawhub?action=popular");
const data = await res.json();
setSkills(data.skills || []);
} catch (e) {
console.error(e);
}
setLoading(false);
}, []);
useEffect(() => {
refreshPopular();
fetchInstalled();
}, [refreshPopular, fetchInstalled]);
const toggleAgent = (agent: string) => {
setSelectedAgents((prev) =>
prev.includes(agent) ? prev.filter((a) => a !== agent) : [...prev, agent]
);
};
const selectAll = () => setSelectedAgents([...AGENTS]);
const handleInstall = async () => {
if (!selectedSkill || selectedAgents.length === 0) return;
setAction("install");
setOutput("Installing...\n");
try {
const res = await fetch("/api/clawhub", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "install",
slug: selectedSkill.slug,
agents: selectedAgents,
}),
});
const data = await res.json();
setOutput(data.output || data.error || "Done");
fetchInstalled();
} catch (e) {
setOutput("Error: " + String(e));
}
setAction(null);
};
const handleDelete = async () => {
if (!selectedSkill || selectedAgents.length === 0) return;
setAction("delete");
setOutput("Removing...\n");
try {
const res = await fetch("/api/clawhub", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action: "delete",
slug: selectedSkill.slug,
agents: selectedAgents,
}),
});
const data = await res.json();
setOutput(data.output || data.error || "Done");
fetchInstalled();
} catch (e) {
setOutput("Error: " + String(e));
}
setAction(null);
};
const handleSync = async () => {
setSyncing(true);
setAction("sync");
setOutput("Syncing with GitHub...\n");
try {
const res = await fetch("/api/clawhub", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "sync" }),
});
const data = await res.json();
setOutput(data.output || data.error || "Sync complete");
fetchInstalled();
} catch (e) {
setOutput("Error: " + String(e));
}
setAction(null);
setSyncing(false);
};
const handlePush = async () => {
setAction("push");
setOutput("Pushing to GitHub...\n");
try {
const res = await fetch("/api/clawhub", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "push" }),
});
const data = await res.json();
setOutput(data.output || data.error || "Push complete");
} catch (e) {
setOutput("Error: " + String(e));
}
setAction(null);
};
const isInstalled = (slug: string) => {
return AGENTS.some((agent) => installedSkills[agent]?.includes(slug));
};
return (
<div className="min-h-screen bg-slate-950 text-white">
<BackToMC />
<div className="p-6">
<div className="mb-6">
<h1 className="text-3xl font-bold text-white mb-2">🛒 ClawHub Marketplace</h1>
<p className="text-slate-400">Search, install, and manage skills across all agents</p>
</div>
{/* Agent Status Bar */}
<div className="bg-slate-800 rounded-xl p-4 mb-6 border border-slate-700">
<div className="flex items-center justify-between mb-3">
<h2 className="font-semibold text-lg">Agent Skills Status</h2>
<div className="flex gap-2">
<button
onClick={handleSync}
disabled={syncing}
className="text-sm bg-blue-700 hover:bg-blue-600 disabled:bg-blue-800 px-3 py-1 rounded transition flex items-center gap-1"
>
{syncing ? "⏳" : "🔄"} Sync
</button>
<button
onClick={handlePush}
className="text-sm bg-green-700 hover:bg-green-600 px-3 py-1 rounded transition"
>
Push
</button>
<button
onClick={fetchInstalled}
className="text-sm bg-slate-700 hover:bg-slate-600 px-3 py-1 rounded transition"
>
Refresh
</button>
</div>
</div>
<div className="grid grid-cols-3 gap-4">
{AGENTS.map((agent) => (
<div key={agent} className="bg-slate-700 rounded-lg p-3">
<div className="font-medium mb-2">{AGENT_LABELS[agent]}</div>
<div className="text-xs text-slate-400">
{installedSkills[agent]?.length || 0} skills installed
</div>
<div className="text-xs text-slate-500 mt-1">
{installedSkills[agent]?.slice(0, 3).join(", ") || "None"}
{installedSkills[agent]?.length > 3 && "..."}
</div>
</div>
))}
</div>
</div>
{/* Search */}
<div className="flex gap-4 mb-6">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => e.key === "Enter" && searchSkills(search)}
placeholder="Search skills..."
className="flex-1 bg-slate-800 border border-slate-700 rounded-lg px-4 py-2 text-white placeholder-slate-400 focus:outline-none focus:border-blue-500"
/>
<button
onClick={() => searchSkills(search)}
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-lg font-medium transition"
>
Search
</button>
<button
onClick={refreshPopular}
className="bg-slate-700 hover:bg-slate-600 px-6 py-2 rounded-lg font-medium transition"
>
🔥 Popular
</button>
</div>
<div className="grid grid-cols-1 lg:grid-cols-2 gap-6">
{/* Skills List */}
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
<div className="p-4 border-b border-slate-700 bg-slate-800">
<h2 className="font-semibold text-lg">Available Skills</h2>
<p className="text-sm text-slate-400">{skills.length} skills found</p>
</div>
<div className="max-h-[500px] overflow-y-auto">
{loading ? (
<div className="p-8 text-center text-slate-400">Loading...</div>
) : skills.length === 0 ? (
<div className="p-8 text-center text-slate-400">No skills found</div>
) : (
skills.map((skill) => {
const installed = isInstalled(skill.slug);
return (
<button
key={skill.slug}
onClick={() => setSelectedSkill(skill)}
className={`w-full text-left p-4 border-b border-slate-700 hover:bg-slate-700 transition ${
selectedSkill?.slug === skill.slug ? "bg-slate-700" : ""
} ${installed ? "border-l-4 border-l-green-500" : ""}`}
>
<div className="flex items-start justify-between">
<div>
<div className="font-medium text-white">{skill.name}</div>
<div className="text-sm text-slate-400 mt-1">@{skill.slug}</div>
</div>
<div className="flex flex-col items-end gap-1">
<span className="text-xs bg-slate-600 px-2 py-0.5 rounded">
{skill.score?.toFixed(3)}
</span>
{installed && (
<span className="text-xs bg-green-600 px-2 py-0.5 rounded">
Installed
</span>
)}
</div>
</div>
</button>
);
})
)}
</div>
</div>
{/* Detail Panel */}
<div className="bg-slate-800 rounded-xl border border-slate-700 overflow-hidden">
<div className="p-4 border-b border-slate-700 bg-slate-800">
<h2 className="font-semibold text-lg">
{selectedSkill ? selectedSkill.name : "Select a Skill"}
</h2>
{selectedSkill && (
<p className="text-sm text-slate-400">@{selectedSkill.slug}</p>
)}
</div>
{!selectedSkill ? (
<div className="p-8 text-center text-slate-400">
Click a skill to see details
</div>
) : (
<div className="p-4">
{/* Agent Selector */}
<div className="mb-4">
<h3 className="text-sm font-semibold text-slate-300 mb-2">Install On:</h3>
<div className="flex flex-wrap gap-2 mb-2">
{AGENTS.map((agent) => (
<button
key={agent}
onClick={() => toggleAgent(agent)}
className={`px-3 py-1.5 rounded-lg text-sm font-medium transition ${
selectedAgents.includes(agent)
? "bg-blue-600 text-white"
: "bg-slate-700 text-slate-300 hover:bg-slate-600"
}`}
>
{AGENT_LABELS[agent]}
</button>
))}
</div>
<button
onClick={selectAll}
className="text-xs text-slate-400 hover:text-white transition"
>
Select All
</button>
</div>
{/* Action Buttons */}
<div className="flex flex-col gap-2 mb-4">
<button
onClick={handleInstall}
disabled={action !== null || selectedAgents.length === 0}
className="bg-green-600 hover:bg-green-700 disabled:bg-green-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
>
{action === "install" ? "⏳ Installing..." : `✅ Install on ${selectedAgents.length} agent(s)`}
</button>
<button
onClick={handleDelete}
disabled={action !== null || selectedAgents.length === 0 || !isInstalled(selectedSkill.slug)}
className="bg-red-600 hover:bg-red-700 disabled:bg-red-800 disabled:cursor-not-allowed px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
>
{action === "delete" ? "⏳ Removing..." : `🗑️ Remove from ${selectedAgents.length} agent(s)`}
</button>
</div>
{/* Output */}
{output && (
<div className="bg-slate-900 rounded-lg p-4 border border-slate-600">
<h3 className="text-xs font-semibold text-slate-300 mb-2">Output</h3>
<pre className="text-xs text-slate-300 whitespace-pre-wrap font-mono overflow-auto max-h-48">
{output}
</pre>
</div>
)}
</div>
)}
</div>
</div>
</div>
</div>
);
}