diff --git a/src/app/(dashboard)/dashboard/omni-skills/OmniSkillsPageClient.tsx b/src/app/(dashboard)/dashboard/omni-skills/OmniSkillsPageClient.tsx new file mode 100644 index 0000000000..64cefa9e3d --- /dev/null +++ b/src/app/(dashboard)/dashboard/omni-skills/OmniSkillsPageClient.tsx @@ -0,0 +1,384 @@ +"use client"; + +import { useState, useEffect, useRef } from "react"; +import { useTranslations } from "next-intl"; +import { Card } from "@/shared/components"; +import { SkillsConceptCard } from "@/shared/components"; +import type { SkillsProvider } from "@/lib/skills/providerSettings"; +import { OmniSkillsList } from "./components/OmniSkillsList"; +import { OmniExecutionsTab } from "./components/OmniExecutionsTab"; +import { OmniSandboxTab } from "./components/OmniSandboxTab"; +import { OmniMarketplaceTab } from "./components/OmniMarketplaceTab"; +import type { OmniSkill } from "./components/OmniSkillCard"; + +interface Execution { + id: string; + skillId: string; + skillName: string; + status: string; + duration: number; + createdAt: string; +} + +export function OmniSkillsPageClient(): JSX.Element { + const [skills, setSkills] = useState([]); + const [executions, setExecutions] = useState([]); + const [loading, setLoading] = useState(true); + const [skillsPage, setSkillsPage] = useState(1); + const [skillsTotal, setSkillsTotal] = useState(0); + const [skillsTotalPages, setSkillsTotalPages] = useState(1); + const [popularDefaults, setPopularDefaults] = useState([]); + const [searchTerm, setSearchTerm] = useState(""); + const [modeFilter, setModeFilter] = useState<"all" | "on" | "off" | "auto">("all"); + const [selectedSkillId, setSelectedSkillId] = useState(null); + + const [execPage, setExecPage] = useState(1); + const [execTotal, setExecTotal] = useState(0); + const [execTotalPages, setExecTotalPages] = useState(1); + + const [activeTab, setActiveTab] = useState<"skills" | "executions" | "sandbox" | "marketplace">( + "skills" + ); + const [showInstallModal, setShowInstallModal] = useState(false); + const [installJson, setInstallJson] = useState(""); + const [installStatus, setInstallStatus] = useState<{ + type: "success" | "error"; + message: string; + } | null>(null); + const [installing, setInstalling] = useState(false); + const fileInputRef = useRef(null); + const [skillsProvider, setSkillsProvider] = useState("skillsmp"); + + const t = useTranslations("skills"); + // NOTE: commonT is intentionally unused here but retained for potential future use + useTranslations("common"); + + const fetchSkills = async (page: number) => { + const params = new URLSearchParams({ page: String(page), limit: "20" }); + if (searchTerm.trim()) params.set("q", searchTerm.trim()); + if (modeFilter !== "all") params.set("mode", modeFilter); + + const res = await fetch(`/api/skills?${params.toString()}`).then((r) => r.json()); + setSkills((res as { data?: OmniSkill[] }).data || []); + setSkillsTotal((res as { total?: number }).total || 0); + setSkillsTotalPages((res as { totalPages?: number }).totalPages || 1); + setPopularDefaults( + Array.isArray((res as { popularDefaults?: string[] }).popularDefaults) + ? (res as { popularDefaults: string[] }).popularDefaults + : [] + ); + }; + + const fetchExecutions = async (page: number) => { + const res = await fetch(`/api/skills/executions?page=${page}&limit=20`).then((r) => r.json()); + setExecutions((res as { data?: Execution[] }).data || []); + setExecTotal((res as { total?: number }).total || 0); + setExecTotalPages((res as { totalPages?: number }).totalPages || 1); + }; + + useEffect(() => { + Promise.all([ + fetch("/api/skills?page=1&limit=20").then((r) => r.json()), + fetch("/api/skills/executions?page=1&limit=20").then((r) => r.json()), + fetch("/api/settings").then((r) => (r.ok ? r.json() : null)), + ]) + .then(([skillsData, executionsData, settingsData]) => { + setSkills((skillsData as { data?: OmniSkill[] }).data || []); + setSkillsTotal((skillsData as { total?: number }).total || 0); + setSkillsTotalPages((skillsData as { totalPages?: number }).totalPages || 1); + setPopularDefaults( + Array.isArray((skillsData as { popularDefaults?: string[] }).popularDefaults) + ? (skillsData as { popularDefaults: string[] }).popularDefaults + : [] + ); + + setExecutions((executionsData as { data?: Execution[] }).data || []); + setExecTotal((executionsData as { total?: number }).total || 0); + setExecTotalPages((executionsData as { totalPages?: number }).totalPages || 1); + + const sd = settingsData as { skillsProvider?: SkillsProvider } | null; + if (sd?.skillsProvider === "skillsmp" || sd?.skillsProvider === "skillssh") { + setSkillsProvider(sd.skillsProvider); + } + + setLoading(false); + }) + .catch(() => setLoading(false)); + }, []); + + const refreshSkills = async () => { + setSkillsPage(1); + await fetchSkills(1); + }; + + const setSkillMode = async (skillId: string, mode: "on" | "off" | "auto") => { + await fetch(`/api/skills/${skillId}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ mode }), + }); + setSkills(skills.map((s) => (s.id === skillId ? { ...s, mode, enabled: mode !== "off" } : s))); + }; + + const deleteSkill = async (skillId: string) => { + const res = await fetch(`/api/skills/${skillId}`, { method: "DELETE" }); + if (res.ok) { + setSkills(skills.filter((s) => s.id !== skillId)); + if (selectedSkillId === skillId) setSelectedSkillId(null); + } + }; + + const handleInstall = async () => { + setInstalling(true); + setInstallStatus(null); + try { + const manifest = JSON.parse(installJson) as Record; + const res = await fetch("/api/skills/install", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(manifest), + }); + const data = await res.json(); + if (res.ok && (data as { success?: boolean }).success) { + setInstallStatus({ + type: "success", + message: t("installSuccess", { id: (data as { id?: string }).id || "" }), + }); + setInstallJson(""); + await refreshSkills(); + } else { + setInstallStatus({ + type: "error", + message: + (data as { error?: string; message?: string }).error || + (data as { error?: string; message?: string }).message || + t("installError"), + }); + } + } catch (err) { + setInstallStatus({ + type: "error", + message: err instanceof Error ? err.message : t("invalidJson"), + }); + } finally { + setInstalling(false); + } + }; + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + const reader = new FileReader(); + reader.onload = (ev) => { + setInstallJson((ev.target?.result as string) || ""); + }; + reader.readAsText(file); + }; + + if (loading) { + return ( +
+
{t("loading")}...
+
+ ); + } + + const enabledCount = skills.filter((s) => s.enabled).length; + const execSuccessCount = executions.filter((e) => e.status === "success").length; + const successRate = + executions.length > 0 ? Math.round((execSuccessCount / executions.length) * 100) : 0; + + const tabs: { id: "skills" | "executions" | "sandbox" | "marketplace"; labelKey: string }[] = [ + { id: "skills", labelKey: "skillsTab" }, + { id: "executions", labelKey: "executionsTab" }, + { id: "sandbox", labelKey: "sandboxTab" }, + { id: "marketplace", labelKey: "marketplaceTab" }, + ]; + + return ( +
+ {/* Concept card */} + + + {/* Stats Cards */} +
+ +

{t("totalSkills")}

+

{skillsTotal}

+
+ +

{t("enabledSkills")}

+

{enabledCount}

+
+ +

+ {t("totalExecutions")} +

+

{execTotal}

+
+ +

{t("successRate")}

+

{successRate}%

+
+
+ +
+ +
+ + {/* Tab bar */} +
+ {tabs.map(({ id, labelKey }) => ( + + ))} +
+ + {/* Tab content */} + {activeTab === "skills" && ( + { + setSkillsPage(1); + void fetchSkills(1); + }} + onPagePrev={() => { + const p = Math.max(1, skillsPage - 1); + setSkillsPage(p); + void fetchSkills(p); + }} + onPageNext={() => { + const p = Math.min(skillsTotalPages, skillsPage + 1); + setSkillsPage(p); + void fetchSkills(p); + }} + onSelectSkill={setSelectedSkillId} + onSetMode={setSkillMode} + onUninstall={deleteSkill} + /> + )} + + {activeTab === "executions" && ( + { + const p = Math.max(1, execPage - 1); + setExecPage(p); + void fetchExecutions(p); + }} + onPageNext={() => { + const p = Math.min(execTotalPages, execPage + 1); + setExecPage(p); + void fetchExecutions(p); + }} + /> + )} + + {activeTab === "sandbox" && } + + {activeTab === "marketplace" && ( + + )} + + {/* Install modal */} + {showInstallModal && ( +
+
+
+

{t("installSkillModalTitle")}

+ +
+

{t("installSkillModalDesc")}

+