Download web/src/components/SkillEditorDialog.tsx from SaylorTwift/hermes-agent: direct link, hf CLI and curl.
- Browser
- Download file 7.09 kB
-
https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/web/src/components/SkillEditorDialog.tsx
- Command line
-
hf download hf://SaylorTwift/hermes-agent/web/src/components/SkillEditorDialog.tsx
-
curl -L -o SkillEditorDialog.tsx https://huggingface.co/SaylorTwift/hermes-agent/resolve/main/web/src/components/SkillEditorDialog.tsx
7.09 kB
| import { useEffect, useState } from "react"; | |
| import { api } from "@/lib/api"; | |
| import { Button } from "@nous-research/ui/ui/components/button"; | |
| import { Input } from "@nous-research/ui/ui/components/input"; | |
| import { Label } from "@nous-research/ui/ui/components/label"; | |
| import { Spinner } from "@nous-research/ui/ui/components/spinner"; | |
| import { | |
| Dialog, | |
| DialogContent, | |
| DialogDescription, | |
| DialogHeader, | |
| DialogTitle, | |
| } from "@nous-research/ui/ui/components/dialog"; | |
| /* ------------------------------------------------------------------ */ | |
| /* SkillEditorDialog — create or edit a SKILL.md from the dashboard */ | |
| /* */ | |
| /* Headless/VPS users have no editor besides this: the only other way */ | |
| /* to author a custom skill is SSH + a terminal editor. Create mode */ | |
| /* posts a brand-new skill (name + optional category + SKILL.md); */ | |
| /* edit mode loads the existing SKILL.md raw text and rewrites it. */ | |
| /* Validation (frontmatter, name, size) happens server-side via the */ | |
| /* same path the agent's skill_manage tool uses, so errors come back */ | |
| /* as actionable messages rendered inline. */ | |
| /* ------------------------------------------------------------------ */ | |
| const CREATE_TEMPLATE = `--- | |
| name: my-skill | |
| description: One-line description of when to use this skill. | |
| --- | |
| # My Skill | |
| Numbered steps, exact commands, and pitfalls go here. | |
| `; | |
| export interface SkillEditorDialogProps { | |
| open: boolean; | |
| /** Skill name to edit, or null for create mode. */ | |
| editName: string | null; | |
| /** Profile to scope reads/writes to ("" = the dashboard's own profile). */ | |
| profile?: string; | |
| onClose: () => void; | |
| /** Called after a successful save so the page can refresh its list. */ | |
| onSaved: (name: string) => void; | |
| } | |
| export function SkillEditorDialog({ | |
| open, | |
| editName, | |
| profile, | |
| onClose, | |
| onSaved, | |
| }: SkillEditorDialogProps) { | |
| // The body is remounted via `key` every time the dialog opens or the | |
| // target skill changes, so all form state initializes through useState | |
| // initializers — no reset-on-open effect (react-hooks/set-state-in-effect). | |
| return ( | |
| <Dialog open={open} onOpenChange={(o) => !o && onClose()}> | |
| <DialogContent className="max-w-3xl"> | |
| {open && ( | |
| <EditorBody | |
| key={editName ?? "__create__"} | |
| editName={editName} | |
| profile={profile} | |
| onClose={onClose} | |
| onSaved={onSaved} | |
| /> | |
| )} | |
| </DialogContent> | |
| </Dialog> | |
| ); | |
| } | |
| function EditorBody({ | |
| editName, | |
| profile, | |
| onClose, | |
| onSaved, | |
| }: Omit<SkillEditorDialogProps, "open">) { | |
| const isEdit = editName !== null; | |
| const [name, setName] = useState(""); | |
| const [category, setCategory] = useState(""); | |
| const [content, setContent] = useState(isEdit ? "" : CREATE_TEMPLATE); | |
| const [loading, setLoading] = useState(isEdit); | |
| const [saving, setSaving] = useState(false); | |
| const [error, setError] = useState<string | null>(null); | |
| useEffect(() => { | |
| if (!editName) return; | |
| let cancelled = false; | |
| api | |
| .getSkillContent(editName, profile || undefined) | |
| .then((res) => !cancelled && setContent(res.content)) | |
| .catch((e) => !cancelled && setError(String(e))) | |
| .finally(() => !cancelled && setLoading(false)); | |
| return () => { | |
| cancelled = true; | |
| }; | |
| }, [editName, profile]); | |
| const handleSave = async () => { | |
| setError(null); | |
| if (!isEdit && !name.trim()) { | |
| setError("Skill name is required."); | |
| return; | |
| } | |
| if (!content.trim()) { | |
| setError("SKILL.md content is required."); | |
| return; | |
| } | |
| setSaving(true); | |
| try { | |
| if (isEdit) { | |
| await api.updateSkillContent(editName, content, profile || undefined); | |
| onSaved(editName); | |
| } else { | |
| const trimmed = name.trim(); | |
| await api.createSkill( | |
| { | |
| name: trimmed, | |
| content, | |
| category: category.trim() || undefined, | |
| }, | |
| profile || undefined, | |
| ); | |
| onSaved(trimmed); | |
| } | |
| onClose(); | |
| } catch (e) { | |
| setError(String(e)); | |
| } finally { | |
| setSaving(false); | |
| } | |
| }; | |
| return ( | |
| <> | |
| <DialogHeader> | |
| <DialogTitle> | |
| {isEdit ? `Edit skill: ${editName}` : "New skill"} | |
| </DialogTitle> | |
| <DialogDescription> | |
| {isEdit | |
| ? "Rewrite this skill's SKILL.md. Frontmatter (name, description) is validated on save." | |
| : "Author a custom skill — YAML frontmatter plus markdown instructions. It becomes available to the agent and attachable to cron jobs."} | |
| </DialogDescription> | |
| </DialogHeader> | |
| <div className="grid gap-3"> | |
| {!isEdit && ( | |
| <div className="grid grid-cols-1 sm:grid-cols-2 gap-3"> | |
| <div className="grid gap-1.5"> | |
| <Label htmlFor="skill-editor-name">Name</Label> | |
| <Input | |
| id="skill-editor-name" | |
| autoFocus | |
| placeholder="my-skill" | |
| value={name} | |
| onChange={(e) => setName(e.target.value)} | |
| /> | |
| </div> | |
| <div className="grid gap-1.5"> | |
| <Label htmlFor="skill-editor-category">Category (optional)</Label> | |
| <Input | |
| id="skill-editor-category" | |
| placeholder="devops" | |
| value={category} | |
| onChange={(e) => setCategory(e.target.value)} | |
| /> | |
| </div> | |
| </div> | |
| )} | |
| <div className="grid gap-1.5"> | |
| <Label htmlFor="skill-editor-content">SKILL.md</Label> | |
| {loading ? ( | |
| <div className="flex items-center justify-center py-16"> | |
| <Spinner className="text-xl text-primary" /> | |
| </div> | |
| ) : ( | |
| <textarea | |
| id="skill-editor-content" | |
| spellCheck={false} | |
| className="min-h-[320px] max-h-[55vh] w-full resize-y border border-border bg-background/40 px-3 py-2 font-mono text-xs leading-relaxed shadow-sm placeholder:text-muted-foreground focus-visible:outline-none focus-visible:ring-1 focus-visible:ring-foreground/30 focus-visible:border-foreground/25" | |
| value={content} | |
| onChange={(e) => setContent(e.target.value)} | |
| /> | |
| )} | |
| </div> | |
| {error && ( | |
| <p className="whitespace-pre-wrap text-xs text-destructive"> | |
| {error} | |
| </p> | |
| )} | |
| <div className="flex items-center justify-end gap-2"> | |
| <Button ghost size="sm" onClick={onClose} disabled={saving}> | |
| Cancel | |
| </Button> | |
| <Button | |
| size="sm" | |
| className="uppercase" | |
| onClick={handleSave} | |
| disabled={saving || loading} | |
| prefix={saving ? <Spinner /> : undefined} | |
| > | |
| {saving ? "Saving…" : isEdit ? "Save changes" : "Create skill"} | |
| </Button> | |
| </div> | |
| </div> | |
| </> | |
| ); | |
| } | |