Files
maskanx_cm_frontend/src/pages/Agent/Skills/index.tsx
T
AFFAANhandClaude Opus 5 b42b7406dc Initial commit: MaskanX frontend
Independent React, TypeScript and Vite web application for MaskanX.

Includes the chat console, agent and persona configuration, MCP client
management, model and provider settings, cron jobs, sessions, and
diagnostics.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-01 10:28:39 +05:30

249 lines
7.2 KiB
TypeScript

import { useState } from "react";
import { Button, Form, Modal } from "@agentscope-ai/design";
import { DownloadOutlined, PlusOutlined } from "@ant-design/icons";
import type { SkillSpec } from "../../../api/types";
import { SkillCard, SkillDrawer } from "./components";
import { useSkills } from "./useSkills";
import { useTranslation } from "react-i18next";
import styles from "./index.module.less";
function SkillsPage() {
const { t } = useTranslation();
const {
skills,
loading,
importing,
createSkill,
importFromHub,
toggleEnabled,
deleteSkill,
} = useSkills();
const [drawerOpen, setDrawerOpen] = useState(false);
const [importModalOpen, setImportModalOpen] = useState(false);
const [importUrl, setImportUrl] = useState("");
const [importUrlError, setImportUrlError] = useState("");
const [editingSkill, setEditingSkill] = useState<SkillSpec | null>(null);
const [hoverKey, setHoverKey] = useState<string | null>(null);
const [form] = Form.useForm<SkillSpec>();
const supportedSkillUrlPrefixes = [
"https://skills.sh/",
"https://clawhub.ai/",
"https://skillsmp.com/",
"https://github.com/",
];
const isSupportedSkillUrl = (url: string) => {
return supportedSkillUrlPrefixes.some((prefix) => url.startsWith(prefix));
};
const handleCreate = () => {
setEditingSkill(null);
form.resetFields();
form.setFieldsValue({
enabled: false,
});
setDrawerOpen(true);
};
const closeImportModal = () => {
if (importing) {
return;
}
setImportModalOpen(false);
setImportUrl("");
setImportUrlError("");
};
const handleImportFromHub = () => {
setImportModalOpen(true);
};
const handleImportUrlChange = (value: string) => {
setImportUrl(value);
const trimmed = value.trim();
if (trimmed && !isSupportedSkillUrl(trimmed)) {
setImportUrlError(t("skills.invalidSkillUrlSource"));
return;
}
setImportUrlError("");
};
const handleConfirmImport = async () => {
if (importing) return;
const trimmed = importUrl.trim();
if (!trimmed) return;
if (!isSupportedSkillUrl(trimmed)) {
setImportUrlError(t("skills.invalidSkillUrlSource"));
return;
}
const success = await importFromHub(trimmed);
if (success) {
closeImportModal();
}
};
const handleEdit = (skill: SkillSpec) => {
setEditingSkill(skill);
form.setFieldsValue(skill);
setDrawerOpen(true);
};
const handleToggleEnabled = async (skill: SkillSpec, e: React.MouseEvent) => {
e.stopPropagation();
await toggleEnabled(skill);
};
const handleDelete = async (skill: SkillSpec, e?: React.MouseEvent) => {
e?.stopPropagation();
await deleteSkill(skill);
};
const handleDrawerClose = () => {
setDrawerOpen(false);
setEditingSkill(null);
};
const handleSubmit = async (values: { name: string; content: string }) => {
try {
const success = await createSkill(values.name, values.content);
if (success) {
setDrawerOpen(false);
}
} catch (error) {
console.error("Submit failed", error);
}
};
return (
<div className={styles.skillsPage}>
<div className={styles.header}>
<div className={styles.headerInfo}>
<h1 className={styles.title}>{t("skills.title")}</h1>
<p className={styles.description}>{t("skills.description")}</p>
</div>
<div className={styles.headerActions}>
<Button
className={styles.headerActionButton}
type="primary"
onClick={handleImportFromHub}
icon={<DownloadOutlined />}
>
{t("skills.importSkills")}
</Button>
<Button
className={styles.headerActionButton}
type="primary"
onClick={handleCreate}
icon={<PlusOutlined />}
>
{t("skills.createSkill")}
</Button>
</div>
</div>
<Modal
title={t("skills.importSkills")}
open={importModalOpen}
onCancel={closeImportModal}
maskClosable={!importing}
closable={!importing}
keyboard={!importing}
footer={
<div style={{ textAlign: "right" }}>
<Button
onClick={closeImportModal}
style={{ marginRight: 8 }}
disabled={importing}
>
{t("common.cancel")}
</Button>
<Button
type="primary"
onClick={handleConfirmImport}
loading={importing}
disabled={importing || !importUrl.trim() || !!importUrlError}
>
{t("skills.importSkills")}
</Button>
</div>
}
width={760}
>
<div className={styles.importHintBlock}>
<p className={styles.importHintTitle}>
{t("skills.supportedSkillUrlSources")}
</p>
<ul className={styles.importHintList}>
<li>https://skills.sh/</li>
<li>https://clawhub.ai/</li>
<li>https://skillsmp.com/</li>
<li>https://github.com/</li>
</ul>
<p className={styles.importHintTitle}>{t("skills.urlExamples")}</p>
<ul className={styles.importHintList}>
<li>https://skills.sh/vercel-labs/skills/find-skills</li>
<li>
https://github.com/anthropics/skills/tree/main/skills/skill-creator
</li>
</ul>
</div>
<input
className={styles.importUrlInput}
value={importUrl}
onChange={(e) => handleImportUrlChange(e.target.value)}
placeholder={t("skills.enterSkillUrl")}
disabled={importing}
/>
{importUrlError ? (
<div className={styles.importUrlError}>{importUrlError}</div>
) : null}
{importing ? (
<div className={styles.importLoadingText}>{t("common.loading")}</div>
) : null}
</Modal>
{loading ? (
<div className={styles.loading}>
<span className={styles.loadingText}>{t("common.loading")}</span>
</div>
) : (
<div className={styles.skillsGrid}>
{skills
.slice()
.sort((a, b) => {
if (a.enabled && !b.enabled) return -1;
if (!a.enabled && b.enabled) return 1;
return a.name.localeCompare(b.name);
})
.map((skill) => (
<SkillCard
key={`${skill.source}:${skill.name}`}
skill={skill}
isHover={hoverKey === `${skill.source}:${skill.name}`}
onClick={() => handleEdit(skill)}
onMouseEnter={() =>
setHoverKey(`${skill.source}:${skill.name}`)
}
onMouseLeave={() => setHoverKey(null)}
onToggleEnabled={(e) => handleToggleEnabled(skill, e)}
onDelete={(e) => handleDelete(skill, e)}
/>
))}
</div>
)}
<SkillDrawer
open={drawerOpen}
editingSkill={editingSkill}
form={form}
onClose={handleDrawerClose}
onSubmit={handleSubmit}
/>
</div>
);
}
export default SkillsPage;