1- import React , { useMemo } from 'react'
1+ import React , { useMemo , useState } from 'react'
22import PropTypes from 'prop-types'
33import Select from '../../Select'
44import { searchSkills } from '../../../services/skills'
55import cn from 'classnames'
66import styles from './styles.module.scss'
77import { AUTOCOMPLETE_DEBOUNCE_TIME_MS , SKILLS_OPTIONAL_BILLING_ACCOUNT_IDS } from '../../../config/constants'
88import _ from 'lodash'
9+ import { extractSkillsFromText } from '../../../services/workflowAI'
10+ import { toastSuccess , toastFailure } from '../../../util/toaster'
11+ import { OutlineButton } from '../../Buttons'
12+ import Loader from '../../Loader'
913
1014const fetchSkills = _ . debounce ( ( inputValue , callback ) => {
1115 searchSkills ( inputValue ) . then (
@@ -22,6 +26,8 @@ const fetchSkills = _.debounce((inputValue, callback) => {
2226} , AUTOCOMPLETE_DEBOUNCE_TIME_MS )
2327
2428const SkillsField = ( { readOnly, challenge, onUpdateSkills, embedded } ) => {
29+ const [ isLoadingAI , setIsLoadingAI ] = useState ( false )
30+
2531 const selectedSkills = useMemo ( ( ) => ( challenge . skills || [ ] ) . map ( skill => ( {
2632 label : skill . name ,
2733 value : skill . id
@@ -31,30 +37,86 @@ const SkillsField = ({ readOnly, challenge, onUpdateSkills, embedded }) => {
3137 const normalizedBillingAccountId = _ . isNil ( billingAccountId ) ? null : String ( billingAccountId )
3238 const skillsRequired = normalizedBillingAccountId ? ! SKILLS_OPTIONAL_BILLING_ACCOUNT_IDS . includes ( normalizedBillingAccountId ) : true
3339 const showRequiredError = ! readOnly && skillsRequired && challenge . submitTriggered && ( ! selectedSkills || ! selectedSkills . length )
40+
41+ // Check if description exists to show AI button
42+ const hasDescription = challenge . description && challenge . description . trim ( ) . length > 0
43+
44+ const handleAISuggest = async ( ) => {
45+ if ( ! hasDescription || isLoadingAI ) {
46+ return
47+ }
48+
49+ setIsLoadingAI ( true )
50+ try {
51+ const result = await extractSkillsFromText ( challenge . description )
52+ const matches = result . matches || [ ]
53+
54+ if ( matches . length === 0 ) {
55+ toastFailure ( 'No Skills Found' , 'No matching standardized skills found based on the description.' )
56+ } else {
57+ // Merge with existing skills, avoiding duplicates
58+ const existingSkillIds = new Set ( ( challenge . skills || [ ] ) . map ( s => s . id ) )
59+ const newSkills = matches . filter ( skill => ! existingSkillIds . has ( skill . id ) )
60+
61+ if ( newSkills . length === 0 ) {
62+ toastSuccess ( 'Skills Already Added' , 'All suggested skills are already in your selection.' )
63+ } else {
64+ const updatedSkills = [ ...( challenge . skills || [ ] ) , ...newSkills ]
65+ onUpdateSkills ( updatedSkills )
66+ toastSuccess ( 'Skills Added' , `${ newSkills . length } skill(s) were added from AI suggestions.` )
67+ }
68+ }
69+ } catch ( error ) {
70+ console . error ( 'AI skill extraction error:' , error )
71+ toastFailure ( 'Error' , 'Failed to extract skills. Please try again or add skills manually.' )
72+ } finally {
73+ setIsLoadingAI ( false )
74+ }
75+ }
3476
3577 if ( embedded ) {
3678 return (
3779 < div className = { styles . embeddedWrapper } >
3880 < input type = 'hidden' />
39- { readOnly ? (
40- < div className = { styles . embeddedReadOnly } > { existingSkills || '-' } </ div >
41- ) : (
42- < Select
43- id = 'skill-select'
44- isMulti
45- simpleValue
46- isAsync
47- value = { selectedSkills }
48- onChange = { ( values ) => {
49- onUpdateSkills ( ( values || [ ] ) . map ( value => ( {
50- name : value . label ,
51- id : value . value
52- } ) ) )
53- } }
54- cacheOptions
55- loadOptions = { fetchSkills }
56- />
57- ) }
81+ < div className = { styles . embeddedContent } >
82+ { isLoadingAI && (
83+ < div className = { styles . loadingOverlay } >
84+ < Loader />
85+ < span className = { styles . loadingText } > Generating skill suggestions...</ span >
86+ </ div >
87+ ) }
88+ { readOnly ? (
89+ < div className = { styles . embeddedReadOnly } > { existingSkills || '-' } </ div >
90+ ) : (
91+ < >
92+ < Select
93+ id = 'skill-select'
94+ isMulti
95+ simpleValue
96+ isAsync
97+ value = { selectedSkills }
98+ onChange = { ( values ) => {
99+ onUpdateSkills ( ( values || [ ] ) . map ( value => ( {
100+ name : value . label ,
101+ id : value . value
102+ } ) ) )
103+ } }
104+ cacheOptions
105+ loadOptions = { fetchSkills }
106+ isDisabled = { isLoadingAI }
107+ />
108+ { hasDescription && (
109+ < OutlineButton
110+ type = 'info'
111+ text = 'AI Suggest'
112+ onClick = { handleAISuggest }
113+ disabled = { isLoadingAI }
114+ className = { styles . aiSuggestButton }
115+ />
116+ ) }
117+ </ >
118+ ) }
119+ </ div >
58120 { showRequiredError && (
59121 < div className = { styles . embeddedError } > Select at least one skill</ div >
60122 ) }
@@ -70,25 +132,45 @@ const SkillsField = ({ readOnly, challenge, onUpdateSkills, embedded }) => {
70132 </ div >
71133 < div className = { cn ( styles . field , styles . col2 ) } >
72134 < input type = 'hidden' />
73- { readOnly ? (
74- < span > { existingSkills } </ span >
75- ) : (
76- < Select
77- id = 'skill-select'
78- isMulti
79- simpleValue
80- isAsync
81- value = { selectedSkills }
82- onChange = { ( values ) => {
83- onUpdateSkills ( ( values || [ ] ) . map ( value => ( {
84- name : value . label ,
85- id : value . value
86- } ) ) )
87- } }
88- cacheOptions
89- loadOptions = { fetchSkills }
90- />
91- ) }
135+ < div className = { styles . skillsFieldWrapper } >
136+ { isLoadingAI && (
137+ < div className = { styles . loadingOverlay } >
138+ < Loader />
139+ < span className = { styles . loadingText } > Generating skill suggestions...</ span >
140+ </ div >
141+ ) }
142+ { readOnly ? (
143+ < span > { existingSkills } </ span >
144+ ) : (
145+ < >
146+ < Select
147+ id = 'skill-select'
148+ isMulti
149+ simpleValue
150+ isAsync
151+ value = { selectedSkills }
152+ onChange = { ( values ) => {
153+ onUpdateSkills ( ( values || [ ] ) . map ( value => ( {
154+ name : value . label ,
155+ id : value . value
156+ } ) ) )
157+ } }
158+ cacheOptions
159+ loadOptions = { fetchSkills }
160+ isDisabled = { isLoadingAI }
161+ />
162+ { hasDescription && (
163+ < OutlineButton
164+ type = 'info'
165+ text = 'AI Suggest'
166+ onClick = { handleAISuggest }
167+ disabled = { isLoadingAI }
168+ className = { styles . aiSuggestButton }
169+ />
170+ ) }
171+ </ >
172+ ) }
173+ </ div >
92174 </ div >
93175 </ div >
94176
0 commit comments