11import { useForm } from 'react-hook-form' ;
22import { useNavigate } from 'react-router-dom' ;
3- import {
4- SKILL_NAME_PATTERN ,
5- SKILL_NAME_MAX_LENGTH ,
6- SKILL_DESCRIPTION_MAX_LENGTH ,
7- } from 'librechat-data-provider' ;
83import {
94 Input ,
105 Label ,
@@ -14,6 +9,12 @@ import {
149 TextareaAutosize ,
1510 useToastContext ,
1611} from '@librechat/client' ;
12+ import {
13+ SKILL_NAME_PATTERN ,
14+ SKILL_NAME_MAX_LENGTH ,
15+ SKILL_BODY_MAX_LENGTH ,
16+ SKILL_DESCRIPTION_MAX_LENGTH ,
17+ } from 'librechat-data-provider' ;
1718import type { TSkill } from 'librechat-data-provider' ;
1819import type { FormEvent } from 'react' ;
1920import { useCreateSkillMutation } from '~/data-provider' ;
@@ -40,6 +41,11 @@ interface FormValues {
4041 body : string ;
4142}
4243
44+ interface SkillValidationIssue {
45+ field : string ;
46+ code : string ;
47+ }
48+
4349/**
4450 * Minimal create-skill dialog matching Claude.ai's "Write skill instructions"
4551 * modal: name, description, instructions. No category, no invocation mode.
@@ -78,9 +84,48 @@ export default function CreateSkillDialog({
7884 navigate ( `/skills/${ skill . _id } ` ) ;
7985 } ,
8086 onError : ( error : unknown ) => {
81- const message =
82- ( error as { response ?: { data ?: { message ?: string } } } ) ?. response ?. data ?. message ??
83- localize ( 'com_ui_skill_create_error' ) ;
87+ const response = (
88+ error as {
89+ response ?: {
90+ status ?: number ;
91+ data ?: { error ?: string ; message ?: string ; issues ?: SkillValidationIssue [ ] } ;
92+ } ;
93+ }
94+ ) ?. response ;
95+ const getIssueMessage = ( { field, code } : SkillValidationIssue ) => {
96+ if ( field === 'name' && code === 'REQUIRED' ) {
97+ return localize ( 'com_ui_skill_name_required' ) ;
98+ }
99+ if ( field === 'name' && code === 'TOO_LONG' ) {
100+ return localize ( 'com_ui_skill_name_too_long' , { 0 : SKILL_NAME_MAX_LENGTH } ) ;
101+ }
102+ if ( field === 'name' && code === 'INVALID_FORMAT' ) {
103+ return localize ( 'com_ui_skill_name_invalid' ) ;
104+ }
105+ if ( field === 'name' && ( code === 'RESERVED_PREFIX' || code === 'RESERVED_WORD' ) ) {
106+ return localize ( 'com_ui_skill_name_reserved' ) ;
107+ }
108+ if ( field === 'description' && code === 'REQUIRED' ) {
109+ return localize ( 'com_ui_skill_description_required' ) ;
110+ }
111+ if ( field === 'description' && code === 'TOO_LONG' ) {
112+ return localize ( 'com_ui_skill_description_too_long' , {
113+ 0 : SKILL_DESCRIPTION_MAX_LENGTH ,
114+ } ) ;
115+ }
116+ if ( field === 'body' && code === 'TOO_LONG' ) {
117+ return localize ( 'com_ui_skill_instructions_too_long' , { 0 : SKILL_BODY_MAX_LENGTH } ) ;
118+ }
119+ return localize ( 'com_ui_skill_validation_error' ) ;
120+ } ;
121+ const data = response ?. data ;
122+ let message = data ?. message || localize ( 'com_ui_skill_create_error' ) ;
123+ if ( response ?. status === 409 ) {
124+ message = localize ( 'com_ui_skill_name_exists' ) ;
125+ }
126+ if ( data ?. issues ?. length ) {
127+ message = data . issues . map ( getIssueMessage ) . join ( '; ' ) ;
128+ }
84129 showToast ( { status : 'error' , message } ) ;
85130 } ,
86131 } ) ;
@@ -166,6 +211,8 @@ export default function CreateSkillDialog({
166211 maxRows = { 4 }
167212 placeholder = { localize ( 'com_ui_skill_description_placeholder' ) }
168213 aria-label = { localize ( 'com_ui_description' ) }
214+ aria-invalid = { errors . description ? 'true' : 'false' }
215+ aria-describedby = { errors . description ? 'create-skill-description-error' : undefined }
169216 className = "w-full resize-none rounded-xl border border-border-medium bg-transparent px-3 py-2 text-sm text-text-primary placeholder:text-text-secondary focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring-primary"
170217 { ...register ( 'description' , {
171218 required : localize ( 'com_ui_skill_description_required' ) ,
@@ -177,6 +224,15 @@ export default function CreateSkillDialog({
177224 } ,
178225 } ) }
179226 />
227+ { errors . description && (
228+ < p
229+ id = "create-skill-description-error"
230+ className = "mt-1 text-sm text-text-destructive"
231+ role = "alert"
232+ >
233+ { errors . description . message }
234+ </ p >
235+ ) }
180236 </ div >
181237
182238 { /* Instructions (body) */ }
@@ -204,9 +260,10 @@ export default function CreateSkillDialog({
204260 type = "submit"
205261 variant = "submit"
206262 disabled = { submitDisabled }
263+ aria-busy = { createSkill . isLoading }
207264 className = { cn ( submitDisabled && 'opacity-50' ) }
208265 >
209- { localize ( 'com_ui_create' ) }
266+ { localize ( createSkill . isLoading ? 'com_ui_creating' : 'com_ui_create' ) }
210267 </ Button >
211268 </ div >
212269 </ form >
0 commit comments