@@ -8,7 +8,7 @@ import { FormTokenField } from '@wordpress/components';
88import { useSelect } from '@wordpress/data' ;
99import { store as coreStore } from '@wordpress/core-data' ;
1010import { getEntitiesInfo , useTaxonomies } from './utils' ;
11- import { useState , Fragment } from '@wordpress/element' ;
11+ import { useState , Fragment , useRef , useEffect , useCallback } from '@wordpress/element' ;
1212import { __ , sprintf } from '@wordpress/i18n' ;
1313
1414const termsPerPage = - 1 ;
@@ -47,6 +47,10 @@ const TaxonomyControls = ( { onChange, query } ) => {
4747 const taxTermsAI = query . taxTermsAI || [ ] ;
4848 const [ newTermsInfo , setNewTermsInfo ] = useState ( { } ) ;
4949
50+ // State for editing AI tokens inline
51+ const [ editingToken , setEditingToken ] = useState ( null ) ;
52+ const tokenFieldRefs = useRef ( { } ) ;
53+
5054 const appendAIPrefix = ( terms , slug ) => {
5155 if (
5256 undefined !== terms &&
@@ -99,6 +103,168 @@ const TaxonomyControls = ( { onChange, query } ) => {
99103 taxonomiesInfo = newTermsInfo ;
100104 }
101105
106+ /**
107+ * Handle clicking on an [AI] token to edit it inline.
108+ * This allows users to tweak AI-suggested terms before saving.
109+ *
110+ * @param {string } taxonomySlug The taxonomy slug.
111+ * @param {number } termId The term ID being edited.
112+ * @param {string } termName The term name (with [AI] prefix).
113+ */
114+ const handleAITokenClick = useCallback (
115+ ( taxonomySlug , termId , termName ) => {
116+ // Extract the term name without the [AI] prefix
117+ const cleanName = termName . replace ( / ^ \[ A I \] \s * / , '' ) ;
118+
119+ // Store the editing state
120+ setEditingToken ( {
121+ taxonomySlug,
122+ termId,
123+ originalName : termName ,
124+ cleanName,
125+ } ) ;
126+
127+ // Get current terms and remove the clicked AI term
128+ const currentTerms = query . taxQuery [ taxonomySlug ] || [ ] ;
129+ const termValues = Object . values ( currentTerms ) ;
130+ const newTermValues = termValues . filter ( ( id ) => id !== termId ) ;
131+
132+ // Update the taxQuery to remove the AI term
133+ const newTaxQuery = {
134+ ...query . taxQuery ,
135+ [ taxonomySlug ] : newTermValues . reduce (
136+ ( acc , id , idx ) => ( { ...acc , [ idx ] : id } ) ,
137+ { }
138+ ) ,
139+ } ;
140+
141+ onChange ( { taxQuery : newTaxQuery } ) ;
142+
143+ // Focus the input and pre-fill with the clean term name
144+ setTimeout ( ( ) => {
145+ const fieldRef = tokenFieldRefs . current [ taxonomySlug ] ;
146+ if ( fieldRef ) {
147+ const input = fieldRef . querySelector (
148+ '.components-form-token-field__input'
149+ ) ;
150+ if ( input ) {
151+ input . value = cleanName ;
152+ input . focus ( ) ;
153+ // Trigger input event to update React state
154+ const event = new Event ( 'input' , { bubbles : true } ) ;
155+ input . dispatchEvent ( event ) ;
156+ }
157+ }
158+ } , 50 ) ;
159+ } ,
160+ [ query . taxQuery , onChange ]
161+ ) ;
162+
163+ /**
164+ * Mark [AI] tokens as editable (for styling) after render.
165+ */
166+ useEffect ( ( ) => {
167+ const markEditableTokens = ( ) => {
168+ Object . keys ( tokenFieldRefs . current ) . forEach ( ( taxonomySlug ) => {
169+ const fieldRef = tokenFieldRefs . current [ taxonomySlug ] ;
170+ if ( ! fieldRef ) {
171+ return ;
172+ }
173+
174+ // Find all token buttons that contain [AI] text
175+ const tokens = fieldRef . querySelectorAll (
176+ '.components-form-token-field__token'
177+ ) ;
178+
179+ tokens . forEach ( ( token ) => {
180+ const tokenText =
181+ token . querySelector (
182+ '.components-form-token-field__token-text'
183+ ) ?. textContent || '' ;
184+
185+ if ( tokenText . includes ( '[AI]' ) ) {
186+ // Add clickable class for styling
187+ token . classList . add ( 'classifai-ai-token--editable' ) ;
188+
189+ // Add tooltip to token text
190+ const tokenTextEl = token . querySelector (
191+ '.components-form-token-field__token-text'
192+ ) ;
193+ if ( tokenTextEl ) {
194+ tokenTextEl . title = __ (
195+ 'Click to edit this term' ,
196+ 'classifai'
197+ ) ;
198+ }
199+ }
200+ } ) ;
201+ } ) ;
202+ } ;
203+
204+ // Small delay to ensure DOM is updated
205+ const timeoutId = setTimeout ( markEditableTokens , 100 ) ;
206+ return ( ) => clearTimeout ( timeoutId ) ;
207+ } , [ taxonomiesInfo , query . taxQuery ] ) ;
208+
209+ /**
210+ * Handle click events on the wrapper using event delegation.
211+ * This avoids stale closure issues with direct event listeners.
212+ *
213+ * @param {string } taxonomySlug The taxonomy slug for this field.
214+ * @return {Function } Click handler function.
215+ */
216+ const handleWrapperClick = useCallback (
217+ ( taxonomySlug ) => ( e ) => {
218+ // Check if the click was on a token text element
219+ const tokenTextEl = e . target . closest (
220+ '.components-form-token-field__token-text'
221+ ) ;
222+ if ( ! tokenTextEl ) {
223+ return ;
224+ }
225+
226+ // Get the token element and check if it's an AI token
227+ const tokenEl = tokenTextEl . closest (
228+ '.components-form-token-field__token'
229+ ) ;
230+ if (
231+ ! tokenEl ||
232+ ! tokenEl . classList . contains ( 'classifai-ai-token--editable' )
233+ ) {
234+ return ;
235+ }
236+
237+ // Get the token text
238+ const tokenText = tokenTextEl . textContent || '' ;
239+ if ( ! tokenText . includes ( '[AI]' ) ) {
240+ return ;
241+ }
242+
243+ e . preventDefault ( ) ;
244+ e . stopPropagation ( ) ;
245+
246+ // Find the term ID from the token
247+ const taxonomyInfo = taxonomiesInfo ?. find (
248+ ( { slug } ) => slug === taxonomySlug
249+ ) ;
250+ if ( taxonomyInfo ) {
251+ // Find the term by matching the name
252+ const termEntry = Object . entries (
253+ taxonomyInfo . terms . mapById
254+ ) . find ( ( [ , term ] ) => term . name === tokenText ) ;
255+
256+ if ( termEntry ) {
257+ handleAITokenClick (
258+ taxonomySlug ,
259+ parseInt ( termEntry [ 0 ] , 10 ) ,
260+ tokenText
261+ ) ;
262+ }
263+ }
264+ } ,
265+ [ taxonomiesInfo , handleAITokenClick ]
266+ ) ;
267+
102268 const onTermsChange = ( taxonomySlug ) => async ( newTermValues ) => {
103269 const taxonomyInfo = taxonomiesInfo . find (
104270 ( { slug } ) => slug === taxonomySlug
@@ -272,13 +438,22 @@ const TaxonomyControls = ( { onChange, query } ) => {
272438
273439 return (
274440 < Fragment key = { slug } >
275- < FormTokenField
276- key = { slug }
277- label = { name }
278- value = { getExistingTaxQueryValue ( slug ) }
279- suggestions = { terms . names }
280- onChange = { onTermsChange ( slug ) }
281- />
441+ { /* eslint-disable-next-line jsx-a11y/click-events-have-key-events, jsx-a11y/no-static-element-interactions */ }
442+ < div
443+ ref = { ( el ) => {
444+ tokenFieldRefs . current [ slug ] = el ;
445+ } }
446+ className = "classifai-taxonomy-field-wrapper"
447+ onClick = { handleWrapperClick ( slug ) }
448+ >
449+ < FormTokenField
450+ key = { slug }
451+ label = { name }
452+ value = { getExistingTaxQueryValue ( slug ) }
453+ suggestions = { terms . names }
454+ onChange = { onTermsChange ( slug ) }
455+ />
456+ </ div >
282457 { ! hasAI && (
283458 < >
284459 < p
0 commit comments