import React from 'react'; import { Sparkles, Upload } from 'lucide-react'; import { Button, Field, Input, Modal, Textarea, toast } from '@/components/ds'; import { usePreferences, useUpdatePreferences } from '@/lib/krowHooks'; import { PAGE_KEYS, validateSkillSource } from '@/lib/skills/registry'; import { skillTemplate, upsertCustomSkill } from '@/lib/skills/customSkills'; /** * Add an Owliver skill. * * One dialog, opened from Settings and from Owliver's own header, writing to * one registry — a second registration path would be a second source of truth * by another name. * * What is stored is the Markdown itself, not a parsed record. A custom skill is * then exactly the artefact a file on disk is, read by the same parser, and can * be copied into `src/skills/` later without conversion. * * The fields above the editor are a starting point, not a second schema: typing * in them composes frontmatter in the existing format, which the author can * then edit directly. The Markdown is always what gets saved. */ export function AddSkillDialog({ open, onOpenChange }) { const preferences = usePreferences(); const updatePreferences = useUpdatePreferences(); const [meta, setMeta] = React.useState({ id: '', name: '', description: '', pages: '' }); const [source, setSource] = React.useState(''); const [touched, setTouched] = React.useState(false); const [error, setError] = React.useState(''); const fileRef = React.useRef(null); /* A fresh sheet each time it opens, so an abandoned draft is not waiting the next time someone adds a skill. */ React.useEffect(() => { if (!open) return; setMeta({ id: '', name: '', description: '', pages: '' }); setSource(''); setTouched(false); setError(''); }, [open]); /* The fields drive the Markdown until the author edits the Markdown, after which the Markdown wins — it is the thing being saved. */ const syncFromMeta = (next) => { setMeta(next); if (touched) return; setSource(skillTemplate({ id: next.id, name: next.name, description: next.description, pages: next.pages.split(',').map((p) => p.trim()).filter(Boolean), })); }; const readFile = (event) => { const file = event.target.files?.[0]; if (!file) return; const reader = new FileReader(); reader.onload = () => { setSource(String(reader.result)); setTouched(true); setError(''); }; reader.readAsText(file); event.target.value = ''; }; const save = () => { const problem = validateSkillSource(source); if (problem) { setError(problem); return; } const { skill, next } = upsertCustomSkill(preferences.customSkills || [], source); updatePreferences.mutate({ customSkills: next }); toast.success(`${skill.name} added`); onOpenChange(false); }; return ( } >
syncFromMeta({ ...meta, name: e.target.value })} /> syncFromMeta({ ...meta, id: e.target.value })} />
syncFromMeta({ ...meta, description: e.target.value })} /> syncFromMeta({ ...meta, pages: e.target.value })} />