| 177 | }; |
| 178 | |
| 179 | const handleSubmit = async (e: React.FormEvent) => { |
| 180 | e.preventDefault(); |
| 181 | setError(""); |
| 182 | |
| 183 | if (!name.trim()) { |
| 184 | setError("Please enter a skill name"); |
| 185 | return; |
| 186 | } |
| 187 | |
| 188 | if (!description.trim()) { |
| 189 | setError("Please enter a skill description (required by OpenCode)"); |
| 190 | return; |
| 191 | } |
| 192 | |
| 193 | const skillName = name.toLowerCase().replace(/[^a-z0-9-]/g, '-').replace(/-+/g, '-').replace(/^-|-$/g, ''); |
| 194 | |
| 195 | if (!/^[a-z0-9]+(-[a-z0-9]+)*$/.test(skillName)) { |
| 196 | setError("Name must be lowercase alphanumeric with single hyphens (e.g., my-skill)"); |
| 197 | return; |
| 198 | } |
| 199 | |
| 200 | if (skillName.length > 64) { |
| 201 | setError("Name must be 64 characters or less"); |
| 202 | return; |
| 203 | } |
| 204 | |
| 205 | if (description.length > 1024) { |
| 206 | setError("Description must be 1024 characters or less"); |
| 207 | return; |
| 208 | } |
| 209 | |
| 210 | try { |
| 211 | setLoading(true); |
| 212 | await saveSkill(skillName, description, content); |
| 213 | toast.success(`Created skill: ${skillName}`); |
| 214 | resetForm(); |
| 215 | setOpen(false); |
| 216 | onSuccess(); |
| 217 | } catch (err: any) { |
| 218 | const msg = err.response?.data?.error || err.message || "Failed to create skill"; |
| 219 | setError(msg); |
| 220 | } finally { |
| 221 | setLoading(false); |
| 222 | } |
| 223 | }; |
| 224 | |
| 225 | return ( |
| 226 | <Dialog open={open} onOpenChange={(o) => { setOpen(o); if (!o) resetForm(); }}> |