import { createHigherOrderComponent } from "@wordpress/compose"; import { Fragment, useState } from "@wordpress/element"; import { Button } from "@wordpress/components"; import { useSelect, useDispatch } from "@wordpress/data"; import { suggestTerms, i18n, SUPPORTED_TAXONOMIES } from "./api"; // Gradient sparkle icon matching the "Analyze with BetterDocs AI" button. Each // instance takes a `uid` (the taxonomy slug) so the gradient ids stay unique // when multiple Suggest buttons render on the same screen. const SparkleIcon = ( { uid = "s", size = 16 } ) => ( ); /** * The "Suggest …" control rendered beneath a native taxonomy panel. Reads the * live editor content, asks the AI for term names, and lets the author add each * suggestion as a term — driving the canonical post attribute (key = taxonomy * slug, since doc_category / doc_tag have no rest_base) so the native * checkboxes/tokens update immediately. */ function SuggestPanel( { slug } ) { const [ loading, setLoading ] = useState( false ); const [ suggestions, setSuggestions ] = useState( [] ); const [ error, setError ] = useState( "" ); const [ notice, setNotice ] = useState( "" ); const { postId, content, title, selected } = useSelect( ( select ) => { const editor = select( "core/editor" ); return { postId: editor.getCurrentPostId(), content: editor.getEditedPostContent(), title: editor.getEditedPostAttribute( "title" ), selected: editor.getEditedPostAttribute( slug ) || [], }; }, [ slug ] ); const { editPost } = useDispatch( "core/editor" ); const { saveEntityRecord } = useDispatch( "core" ); const labelMap = { doc_category: i18n.suggestCategories, doc_tag: i18n.suggestTags, }; const label = labelMap[ slug ] || i18n.suggestTags; // An empty result is a valid "nothing matched" outcome, not a failure — so it // gets a calm notice rather than the red error styling. const emptyNotice = i18n.noSuggestions; const fetchSuggestions = async () => { setLoading( true ); setError( "" ); setNotice( "" ); try { const res = await suggestTerms( { taxonomy: slug, postId, content, title, maxSuggestions: 6, } ); const list = ( res && res.data && res.data.suggestions ) || []; setSuggestions( list ); if ( ! list.length ) { setNotice( emptyNotice ); } } catch ( e ) { setError( ( e && e.message ) || i18n.error ); } finally { setLoading( false ); } }; const addTerm = async ( suggestion, index ) => { setError( "" ); setNotice( "" ); try { let termId = suggestion.term_id; if ( ! termId ) { const created = await saveEntityRecord( "taxonomy", slug, { name: suggestion.name, } ); termId = created && created.id; } if ( ! termId ) { return; } const current = ( selected || [] ).map( Number ); if ( ! current.includes( Number( termId ) ) ) { editPost( { [ slug ]: [ ...current, Number( termId ) ] } ); } setSuggestions( ( prev ) => prev.filter( ( _, i ) => i !== index ) ); } catch ( e ) { const status = e && ( e.status || ( e.data && e.data.status ) ); const denied = status === 403 || ( e && e.code === "rest_cannot_create" ) || ( e && e.code === "rest_forbidden" ); setError( denied ? i18n.permissionError : ( ( e && e.message ) || i18n.error ) ); } }; return (
{ !! error && (

{ error }

) } { !! notice && ! error && (

{ notice }

) } { !! suggestions.length && (
{ suggestions.map( ( s, i ) => ( ) ) }
) }
); } /** * Wraps the native per-taxonomy panel (filter `editor.PostTaxonomyType`). Passes * non-doc taxonomies through untouched; appends the Suggest control only for * doc_category / doc_tag. */ export const withSuggestTerms = createHigherOrderComponent( ( OriginalComponent ) => { return ( props ) => { const { slug } = props; if ( ! SUPPORTED_TAXONOMIES.includes( slug ) ) { return ; } return ( ); }; }, "withBetterDocsSuggestTerms" );