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>
249 lines
7.2 KiB
TypeScript
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;
|