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

557 lines
21 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;
category?: string;
description?: string;
triggers?: string;
}
const AGENTS = ["horus", "amun", "cleo"] as const;
const AGENT_LABELS: Record<string, string> = {
horus: "🦅 Horus (Local)",
amun: "🤖 Amun",
cleo: "👑 Cleo",
};
const CATEGORIES = [
{ id: "productivity", name: "⚡ Productivity", icon: "⚡" },
{ id: "automation", name: "🤖 Automation", icon: "🤖" },
{ id: "ai", name: "🧠 AI", icon: "🧠" },
{ id: "writing", name: "✍️ Writing", icon: "✍️" },
{ id: "research", name: "🔬 Research", icon: "🔬" },
{ id: "coding", name: "💻 Coding", icon: "💻" },
{ id: "business", name: "💼 Business", icon: "💼" },
];
const CATEGORY_QUERIES: Record<string, string> = {
productivity: "productivity focus work",
automation: "automation workflow",
ai: "artificial intelligence machine learning",
writing: "writing content copy",
research: "research data analysis",
coding: "code programming developer",
business: "business sales marketing",
};
export default function ClawHubMarketplace() {
const [skills, setSkills] = useState<Skill[]>([]);
const [categorySkills, setCategorySkills] = useState<Record<string, Skill[]>>({});
const [search, setSearch] = useState("");
const [loading, setLoading] = useState(false);
const [selectedSkill, setSelectedSkill] = useState<Skill | null>(null);
const [skillDetails, setSkillDetails] = useState<{description?: string; triggers?: string} | null>(null);
const [selectedAgents, setSelectedAgents] = useState<string[]>(["horus"]);
const [view, setView] = useState<"popular" | "categories" | "search">("popular");
const [categoryView, setCategoryView] = useState<string | null>(null);
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 [selectedCategory, setSelectedCategory] = useState<string | null>(null);
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 fetchAllCategories = useCallback(async () => {
setLoading(true);
const results: Record<string, Skill[]> = {};
for (const cat of CATEGORIES) {
try {
const res = await fetch(`/api/clawhub?action=search&q=${encodeURIComponent(CATEGORY_QUERIES[cat.id])}`);
const data = await res.json();
results[cat.id] = (data.skills || []).slice(0, 5); // Top 5 per category
} catch (e) {
results[cat.id] = [];
}
}
setCategorySkills(results);
setLoading(false);
}, []);
const refreshPopular = useCallback(async () => {
setLoading(true);
setView("popular");
// Get top skills from all categories combined
const allSkills: Skill[] = [];
const seen = new Set<string>();
for (const cat of CATEGORIES) {
try {
const res = await fetch(`/api/clawhub?action=search&q=${encodeURIComponent(CATEGORY_QUERIES[cat.id])}`);
const data = await res.json();
for (const skill of data.skills || []) {
if (!seen.has(skill.slug)) {
seen.add(skill.slug);
allSkills.push({ ...skill, category: cat.id });
}
}
} catch {}
}
// Sort by score and take top 30
allSkills.sort((a, b) => b.score - a.score);
setSkills(allSkills.slice(0, 30));
setLoading(false);
}, []);
useEffect(() => {
if (view === "popular") {
refreshPopular();
} else if (view === "categories") {
fetchAllCategories();
}
fetchInstalled();
}, [view, refreshPopular, fetchAllCategories, 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 handleAnalyze = async () => {
if (!selectedSkill) return;
setAction("analyze");
setOutput("Analyzing skill...\n");
try {
const res = await fetch(`/api/clawhub?action=analyze&slug=${encodeURIComponent(selectedSkill.slug)}`);
const data = await res.json();
setSkillDetails({
description: data.description,
triggers: data.triggers
});
let output = `📋 ${selectedSkill.name}\n\n`;
output += `Description: ${data.description || "N/A"}\n\n`;
output += `Triggers: ${data.triggers || "N/A"}\n\n`;
if (data.projectMatches && data.projectMatches.length > 0) {
output += `🎯 Project Matches:\n`;
data.projectMatches.forEach((m: string) => {
output += `${m}\n`;
});
output += `\n`;
}
output += (data.raw ? `📄 SKILL.md Preview:\n${data.raw.substring(0, 500)}...` : "");
setOutput(output);
} catch (e) {
setOutput("Error: " + String(e));
}
setAction(null);
};
const handleClone = async () => {
if (!selectedSkill) return;
setAction("clone");
setOutput("Creating custom version...\n");
try {
const res = await fetch("/api/clawhub", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ action: "clone", slug: selectedSkill.slug }),
});
const data = await res.json();
setOutput(data.output || data.error || "Clone complete!");
fetchInstalled();
} catch (e) {
setOutput("Error: " + String(e));
}
setAction(null);
};
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));
};
const getCategoryName = (catId: string) => {
return CATEGORIES.find((c) => c.id === catId)?.name || catId;
};
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>
{/* View Toggle */}
<div className="flex gap-2 mb-4">
<button
onClick={refreshPopular}
className={`px-4 py-2 rounded-lg font-medium transition ${
view === "popular" ? "bg-orange-600" : "bg-slate-700 hover:bg-slate-600"
}`}
>
🔥 Popular
</button>
<button
onClick={() => { setView("categories"); }}
className={`px-4 py-2 rounded-lg font-medium transition ${
view === "categories" ? "bg-orange-600" : "bg-slate-700 hover:bg-slate-600"
}`}
>
📂 Categories
</button>
</div>
{/* Search */}
<div className="flex gap-4 mb-6">
<input
type="text"
value={search}
onChange={(e) => setSearch(e.target.value)}
onKeyDown={(e) => { if (e.key === "Enter") { searchSkills(search); setView("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); setView("search"); }}
className="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded-lg font-medium transition"
>
Search
</button>
</div>
{/* Categories View */}
{view === "categories" && !categoryView && (
<div className="mb-6">
<div className="flex flex-wrap gap-3">
{CATEGORIES.map((cat) => (
<button
key={cat.id}
onClick={() => {
setCategoryView(cat.id);
setSkills(categorySkills[cat.id] || []);
}}
className="bg-slate-800 hover:bg-slate-700 border border-slate-700 hover:border-slate-600 rounded-xl px-6 py-4 flex flex-col items-center gap-2 transition min-w-[140px]"
>
<span className="text-2xl">{cat.icon}</span>
<span className="font-medium text-white">{cat.name}</span>
<span className="text-xs text-slate-400">{categorySkills[cat.id]?.length || 0} skills</span>
</button>
))}
</div>
</div>
)}
{view === "categories" && categoryView && (
<div className="mb-4">
<button
onClick={() => setCategoryView(null)}
className="text-slate-400 hover:text-white flex items-center gap-2 mb-4"
>
Back to Categories
</button>
<div className="flex items-center gap-3">
<span className="text-2xl">{CATEGORIES.find(c => c.id === categoryView)?.icon}</span>
<h2 className="text-xl font-bold">{CATEGORIES.find(c => c.id === categoryView)?.name}</h2>
</div>
</div>
)}
{/* Popular/Search/Category View */}
{(view === "popular" || view === "search" || (view === "categories" && categoryView)) && (
<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">
{view === "popular" ? "🔥 Popular Skills" : view === "categories" ? CATEGORIES.find(c => c.id === categoryView)?.name || "Category" : "Search Results"}
</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>
{skill.category && view === "popular" && (
<span className="text-xs bg-slate-600 px-2 py-0.5 rounded mt-1 inline-block">
{getCategoryName(skill.category)}
</span>
)}
</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">
</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={handleAnalyze}
disabled={action !== null}
className="bg-blue-600 hover:bg-blue-700 disabled:bg-blue-800 px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
>
{action === "analyze" ? "⏳ Analyzing..." : "🔍 Analyze Skill"}
</button>
<button
onClick={handleClone}
disabled={action !== null}
className="bg-purple-600 hover:bg-purple-700 disabled:bg-purple-800 px-4 py-2 rounded-lg font-medium transition flex items-center justify-center gap-2"
>
{action === "clone" ? "⏳ Creating..." : "🧬 Make My Version"}
</button>
<div className="border-t border-slate-600 pt-2 mt-2">
<button
onClick={handleInstall}
disabled={action !== null || selectedAgents.length === 0}
className="w-full 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="w-full 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 mt-2"
>
{action === "delete" ? "⏳ Removing..." : `🗑️ Remove from ${selectedAgents.length} agent(s)`}
</button>
</div>
</div>
{/* Output */}
{output && (
<div className="bg-slate-900 rounded-lg p-4 border border-slate-600">
<h3 className="text-sm font-semibold text-slate-300 mb-2">Output</h3>
<pre className="text-sm text-slate-200 whitespace-pre-wrap font-mono overflow-auto max-h-64 leading-relaxed">
{output}
</pre>
</div>
)}
</div>
)}
</div>
</div>
)}
</div>
</div>
);
}