@@ -1621,10 +1621,142 @@ const PluginSettings = () => {
16211621 React . createElement ( "button" , { style : smallBtn , onClick : handleConfigure } , openConfig === p . name ? 'Close' : 'Configure' ) ) ) ) ;
16221622 } ) ) ) ) ;
16231623 }
1624+ // Component to handle dynamic loading of custom field renderer scripts
1625+ function CustomFieldLoader ( { fieldType, pluginName, field, backendBase, savePluginSetting, loadPluginSettings, setError, renderDefaultInput } ) {
1626+ var _a ;
1627+ const React = ( ( _a = window . PluginApi ) === null || _a === void 0 ? void 0 : _a . React ) || window . React ;
1628+ const [ renderer , setRenderer ] = React . useState ( null ) ;
1629+ const [ loading , setLoading ] = React . useState ( true ) ;
1630+ const [ failed , setFailed ] = React . useState ( false ) ;
1631+ React . useEffect ( ( ) => {
1632+ const pluginSpecificName = `${ pluginName } _${ fieldType } _Renderer` ;
1633+ const genericName = `${ fieldType } _Renderer` ;
1634+ const legacyName = fieldType === 'tag_list_editor' ? 'SkierAITaggingTagListEditor' : null ;
1635+ // Check if renderer is already available
1636+ const checkRenderer = ( ) => {
1637+ const found = window [ pluginSpecificName ] ||
1638+ window [ genericName ] ||
1639+ ( legacyName ? window [ legacyName ] : null ) ;
1640+ if ( found && typeof found === 'function' ) {
1641+ setRenderer ( ( ) => found ) ;
1642+ setLoading ( false ) ;
1643+ return true ;
1644+ }
1645+ return false ;
1646+ } ;
1647+ if ( checkRenderer ( ) )
1648+ return ;
1649+ // Try to load the script from the backend server
1650+ // Normalize backendBase to ensure it doesn't end with a slash
1651+ const normalizedBackendBase = backendBase . replace ( / \/ + $ / , '' ) ;
1652+ const possiblePaths = [
1653+ `${ normalizedBackendBase } /plugins/${ pluginName } /${ fieldType } .js` ,
1654+ `${ normalizedBackendBase } /dist/plugins/${ pluginName } /${ fieldType } .js` ,
1655+ ] ;
1656+ // Also try camelCase version
1657+ const typeParts = fieldType . split ( '_' ) ;
1658+ if ( typeParts . length > 1 ) {
1659+ const camelCase = typeParts [ 0 ] + typeParts . slice ( 1 ) . map ( p => p . charAt ( 0 ) . toUpperCase ( ) + p . slice ( 1 ) ) . join ( '' ) ;
1660+ possiblePaths . push ( `${ normalizedBackendBase } /plugins/${ pluginName } /${ camelCase } .js` ) ;
1661+ possiblePaths . push ( `${ normalizedBackendBase } /dist/plugins/${ pluginName } /${ camelCase } .js` ) ;
1662+ }
1663+ let attemptIndex = 0 ;
1664+ const tryLoad = ( ) => {
1665+ if ( attemptIndex >= possiblePaths . length ) {
1666+ setLoading ( false ) ;
1667+ setFailed ( true ) ;
1668+ if ( window . AIDebug ) {
1669+ console . warn ( '[PluginSettings.CustomFieldLoader] Failed to load renderer for' , fieldType , 'tried:' , possiblePaths ) ;
1670+ }
1671+ return ;
1672+ }
1673+ const path = possiblePaths [ attemptIndex ] ;
1674+ // Use fetch + eval instead of script tag to work around CSP script-src-elem restrictions
1675+ // This uses script-src (which has unsafe-eval) instead of script-src-elem
1676+ fetch ( path )
1677+ . then ( response => {
1678+ if ( ! response . ok ) {
1679+ throw new Error ( `HTTP ${ response . status } ` ) ;
1680+ }
1681+ return response . text ( ) ;
1682+ } )
1683+ . then ( scriptText => {
1684+ console . log ( '[PluginSettings.CustomFieldLoader] Fetched script:' , path ) ;
1685+ try {
1686+ // Eval the script - this uses script-src (with unsafe-eval) instead of script-src-elem
1687+ // Create a new function context to avoid polluting global scope
1688+ const scriptFunction = new Function ( scriptText ) ;
1689+ scriptFunction ( ) ;
1690+ // Wait a bit for the script to register, then check again
1691+ setTimeout ( ( ) => {
1692+ if ( checkRenderer ( ) ) {
1693+ return ;
1694+ }
1695+ // Script loaded but renderer not found, try next path
1696+ attemptIndex ++ ;
1697+ tryLoad ( ) ;
1698+ } , 200 ) ;
1699+ }
1700+ catch ( evalError ) {
1701+ console . error ( '[PluginSettings.CustomFieldLoader] Error evaluating script:' , path , evalError ) ;
1702+ attemptIndex ++ ;
1703+ tryLoad ( ) ;
1704+ }
1705+ } )
1706+ . catch ( error => {
1707+ console . warn ( '[PluginSettings.CustomFieldLoader] Failed to fetch script:' , path , error ) ;
1708+ attemptIndex ++ ;
1709+ tryLoad ( ) ;
1710+ } ) ;
1711+ } ;
1712+ tryLoad ( ) ;
1713+ // Also poll for renderer in case it loads asynchronously (max 10 seconds)
1714+ let pollCount = 0 ;
1715+ const pollInterval = setInterval ( ( ) => {
1716+ pollCount ++ ;
1717+ if ( checkRenderer ( ) || pollCount > 20 ) {
1718+ clearInterval ( pollInterval ) ;
1719+ if ( pollCount > 20 && ! renderer ) {
1720+ setLoading ( false ) ;
1721+ setFailed ( true ) ;
1722+ }
1723+ }
1724+ } , 500 ) ;
1725+ return ( ) => clearInterval ( pollInterval ) ;
1726+ } , [ fieldType , pluginName ] ) ;
1727+ if ( renderer ) {
1728+ return React . createElement ( renderer , {
1729+ field : field ,
1730+ pluginName : pluginName ,
1731+ backendBase : backendBase ,
1732+ savePluginSetting : savePluginSetting ,
1733+ loadPluginSettings : loadPluginSettings ,
1734+ setError : setError
1735+ } ) ;
1736+ }
1737+ if ( loading ) {
1738+ return React . createElement ( 'div' , { style : { padding : 8 , fontSize : 11 , color : '#888' , fontStyle : 'italic' } } , `Loading ${ fieldType } editor...` ) ;
1739+ }
1740+ // Failed to load - use default input if provided, otherwise show error message
1741+ if ( failed && renderDefaultInput ) {
1742+ return renderDefaultInput ( ) ;
1743+ }
1744+ if ( failed ) {
1745+ return React . createElement ( 'div' , { style : { padding : 8 , fontSize : 11 , color : '#f85149' } } , `Failed to load ${ fieldType } editor. Using default input.` ) ;
1746+ }
1747+ return null ;
1748+ }
16241749 function FieldRenderer ( { f, pluginName } ) {
16251750 const t = f . type || 'string' ;
16261751 const label = f . label || f . key ;
16271752 const savedValue = f . value === undefined ? f . default : f . value ;
1753+ // Define styles and computed values early so they're available to callbacks
1754+ const changed = savedValue !== undefined && savedValue !== null && f . default !== undefined && savedValue !== f . default ;
1755+ const inputStyle = { padding : 6 , background : '#111' , color : '#eee' , border : '1px solid #333' , minWidth : 120 } ;
1756+ const wrap = { position : 'relative' , padding : '4px 4px 6px' , border : '1px solid #2a2a2a' , borderRadius : 4 , background : '#101010' } ;
1757+ const resetStyle = { position : 'absolute' , top : 2 , right : 4 , fontSize : 9 , padding : '1px 4px' , cursor : 'pointer' } ;
1758+ const labelTitle = f && f . description ? String ( f . description ) : undefined ;
1759+ const labelEl = React . createElement ( 'span' , { title : labelTitle } , React . createElement ( React . Fragment , null , label , changed ? React . createElement ( 'span' , { style : { color : '#ffa657' , fontSize : 10 } } , ' •' ) : null ) ) ;
16281760 if ( t === 'path_map' ) {
16291761 const containerStyle = {
16301762 position : 'relative' ,
@@ -1643,15 +1775,81 @@ const PluginSettings = () => {
16431775 changedMap && React . createElement ( "span" , { style : { color : '#ffa657' , fontSize : 10 } } , "\u2022" ) ) ,
16441776 React . createElement ( PathMapEditor , { value : savedValue , defaultValue : f . default , onChange : async ( next ) => { await savePluginSetting ( pluginName , f . key , next ) ; } , onReset : async ( ) => { await savePluginSetting ( pluginName , f . key , null ) ; } , variant : "plugin" } ) ) ) ;
16451777 }
1646- const changed = savedValue !== undefined && savedValue !== null && f . default !== undefined && savedValue !== f . default ;
1647- const inputStyle = { padding : 6 , background : '#111' , color : '#eee' , border : '1px solid #333' , minWidth : 120 } ;
1648- const wrap = { position : 'relative' , padding : '4px 4px 6px' , border : '1px solid #2a2a2a' , borderRadius : 4 , background : '#101010' } ;
1649- const resetStyle = { position : 'absolute' , top : 2 , right : 4 , fontSize : 9 , padding : '1px 4px' , cursor : 'pointer' } ;
1650- const labelTitle = f && f . description ? String ( f . description ) : undefined ;
1651- const labelEl = React . createElement ( "span" , { title : labelTitle } ,
1652- label ,
1653- " " ,
1654- changed && React . createElement ( "span" , { style : { color : '#ffa657' , fontSize : 10 } } , "\u2022" ) ) ;
1778+ // Check for custom field renderers registered by plugins
1779+ // Supports both plugin-specific (pluginName_type_Renderer) and generic (type_Renderer) naming
1780+ if ( t && typeof t === 'string' && t !== 'string' && t !== 'boolean' && t !== 'number' && t !== 'select' && t !== 'path_map' ) {
1781+ const pluginSpecificName = `${ pluginName } _${ t } _Renderer` ;
1782+ const genericName = `${ t } _Renderer` ;
1783+ const customRenderer = window [ pluginSpecificName ] || window [ genericName ] ;
1784+ const renderer = customRenderer ;
1785+ // Debug logging
1786+ if ( window . AIDebug ) {
1787+ console . log ( '[PluginSettings.FieldRenderer] Custom field type detected:' , {
1788+ type : t ,
1789+ pluginName : pluginName ,
1790+ pluginSpecificName : pluginSpecificName ,
1791+ genericName : genericName ,
1792+ hasPluginSpecific : ! ! window [ pluginSpecificName ] ,
1793+ hasGeneric : ! ! window [ genericName ] ,
1794+ renderer : renderer ? typeof renderer : 'null'
1795+ } ) ;
1796+ }
1797+ if ( renderer && typeof renderer === 'function' ) {
1798+ if ( window . AIDebug ) {
1799+ console . log ( '[PluginSettings.FieldRenderer] Using custom renderer for' , t ) ;
1800+ }
1801+ return React . createElement ( renderer , {
1802+ field : f ,
1803+ pluginName : pluginName ,
1804+ backendBase : backendBase ,
1805+ savePluginSetting : savePluginSetting ,
1806+ loadPluginSettings : loadPluginSettings ,
1807+ setError : setError
1808+ } ) ;
1809+ }
1810+ else {
1811+ // Renderer not found - use CustomFieldLoader to dynamically load it
1812+ // CustomFieldLoader will handle fallback to default input if renderer not found
1813+ return React . createElement ( CustomFieldLoader , {
1814+ fieldType : t ,
1815+ pluginName : pluginName ,
1816+ field : f ,
1817+ backendBase : backendBase ,
1818+ savePluginSetting : savePluginSetting ,
1819+ loadPluginSettings : loadPluginSettings ,
1820+ setError : setError ,
1821+ // Pass the default input rendering logic as fallback
1822+ renderDefaultInput : ( ) => {
1823+ // This will be called if renderer not found - render default text input
1824+ const display = savedValue === undefined || savedValue === null ? '' : String ( savedValue ) ;
1825+ const inputKey = `${ pluginName } :${ f . key } :${ display } ` ;
1826+ const handleBlur = async ( event ) => {
1827+ var _a ;
1828+ const next = ( _a = event . target . value ) !== null && _a !== void 0 ? _a : '' ;
1829+ if ( next === display )
1830+ return ;
1831+ await savePluginSetting ( pluginName , f . key , next ) ;
1832+ } ;
1833+ const handleKeyDown = ( event ) => {
1834+ if ( event . key === 'Enter' ) {
1835+ event . preventDefault ( ) ;
1836+ event . target . blur ( ) ;
1837+ }
1838+ } ;
1839+ const handleReset = async ( ) => {
1840+ await savePluginSetting ( pluginName , f . key , null ) ;
1841+ } ;
1842+ return React . createElement ( 'div' , { style : wrap } , React . createElement ( 'label' , { style : { fontSize : 12 } } , React . createElement ( React . Fragment , null , labelEl , React . createElement ( 'br' ) , React . createElement ( 'input' , {
1843+ key : inputKey ,
1844+ style : inputStyle ,
1845+ defaultValue : display ,
1846+ onBlur : handleBlur ,
1847+ onKeyDown : handleKeyDown
1848+ } ) ) ) , changed ? React . createElement ( 'button' , { style : resetStyle , onClick : handleReset } , 'Reset' ) : null ) ;
1849+ }
1850+ } ) ;
1851+ }
1852+ }
16551853 if ( t === 'boolean' ) {
16561854 return ( React . createElement ( "div" , { style : wrap } ,
16571855 React . createElement ( "label" , { style : { fontSize : 12 , display : 'flex' , alignItems : 'center' , gap : 8 } } ,
0 commit comments