From 4ea4c780df0edf3e7de3a8abfa6f6b57da195af6 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Thu, 19 Mar 2026 21:54:45 +0000 Subject: [PATCH 1/2] Update AI Overhaul plugin for 0.9.3 --- plugins/AIOverhaul/AIOverhaul.yml | 11 +- plugins/AIOverhaul/PluginSettings.js | 216 +++++++++++++++++++++++++-- plugins/AIOverhaul/SimilarScenes.js | 15 +- 3 files changed, 226 insertions(+), 16 deletions(-) diff --git a/plugins/AIOverhaul/AIOverhaul.yml b/plugins/AIOverhaul/AIOverhaul.yml index 4821917b..65ee26ec 100644 --- a/plugins/AIOverhaul/AIOverhaul.yml +++ b/plugins/AIOverhaul/AIOverhaul.yml @@ -1,6 +1,6 @@ name: AIOverhaul description: AI Overhaul for Stash with a full plugin engine included to install and manage asynchronous stash plugins for AI or other purposes. -version: 0.9.2 +version: 0.9.3 url: https://discourse.stashapp.cc/t/aioverhaul/4847 ui: javascript: @@ -30,6 +30,15 @@ ui: - ws://127.0.0.1:4153 - https://127.0.0.1:4153 # Add additional urls here for the stash-ai-server if your browser is not on the same host + script-src: + - 'self' + - http://localhost:4153 + - https://localhost:4153 + - http://www.gstatic.com + - https://www.gstatic.com + - 'unsafe-inline' + - 'unsafe-eval' + # Allow plugin JavaScript files to be loaded from the backend server interface: raw exec: - python diff --git a/plugins/AIOverhaul/PluginSettings.js b/plugins/AIOverhaul/PluginSettings.js index 5e29e2b2..bc05e292 100644 --- a/plugins/AIOverhaul/PluginSettings.js +++ b/plugins/AIOverhaul/PluginSettings.js @@ -1621,10 +1621,142 @@ const PluginSettings = () => { React.createElement("button", { style: smallBtn, onClick: handleConfigure }, openConfig === p.name ? 'Close' : 'Configure')))); })))); } + // Component to handle dynamic loading of custom field renderer scripts + function CustomFieldLoader({ fieldType, pluginName, field, backendBase, savePluginSetting, loadPluginSettings, setError, renderDefaultInput }) { + var _a; + const React = ((_a = window.PluginApi) === null || _a === void 0 ? void 0 : _a.React) || window.React; + const [renderer, setRenderer] = React.useState(null); + const [loading, setLoading] = React.useState(true); + const [failed, setFailed] = React.useState(false); + React.useEffect(() => { + const pluginSpecificName = `${pluginName}_${fieldType}_Renderer`; + const genericName = `${fieldType}_Renderer`; + const legacyName = fieldType === 'tag_list_editor' ? 'SkierAITaggingTagListEditor' : null; + // Check if renderer is already available + const checkRenderer = () => { + const found = window[pluginSpecificName] || + window[genericName] || + (legacyName ? window[legacyName] : null); + if (found && typeof found === 'function') { + setRenderer(() => found); + setLoading(false); + return true; + } + return false; + }; + if (checkRenderer()) + return; + // Try to load the script from the backend server + // Normalize backendBase to ensure it doesn't end with a slash + const normalizedBackendBase = backendBase.replace(/\/+$/, ''); + const possiblePaths = [ + `${normalizedBackendBase}/plugins/${pluginName}/${fieldType}.js`, + `${normalizedBackendBase}/dist/plugins/${pluginName}/${fieldType}.js`, + ]; + // Also try camelCase version + const typeParts = fieldType.split('_'); + if (typeParts.length > 1) { + const camelCase = typeParts[0] + typeParts.slice(1).map(p => p.charAt(0).toUpperCase() + p.slice(1)).join(''); + possiblePaths.push(`${normalizedBackendBase}/plugins/${pluginName}/${camelCase}.js`); + possiblePaths.push(`${normalizedBackendBase}/dist/plugins/${pluginName}/${camelCase}.js`); + } + let attemptIndex = 0; + const tryLoad = () => { + if (attemptIndex >= possiblePaths.length) { + setLoading(false); + setFailed(true); + if (window.AIDebug) { + console.warn('[PluginSettings.CustomFieldLoader] Failed to load renderer for', fieldType, 'tried:', possiblePaths); + } + return; + } + const path = possiblePaths[attemptIndex]; + // Use fetch + eval instead of script tag to work around CSP script-src-elem restrictions + // This uses script-src (which has unsafe-eval) instead of script-src-elem + fetch(path) + .then(response => { + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + return response.text(); + }) + .then(scriptText => { + console.log('[PluginSettings.CustomFieldLoader] Fetched script:', path); + try { + // Eval the script - this uses script-src (with unsafe-eval) instead of script-src-elem + // Create a new function context to avoid polluting global scope + const scriptFunction = new Function(scriptText); + scriptFunction(); + // Wait a bit for the script to register, then check again + setTimeout(() => { + if (checkRenderer()) { + return; + } + // Script loaded but renderer not found, try next path + attemptIndex++; + tryLoad(); + }, 200); + } + catch (evalError) { + console.error('[PluginSettings.CustomFieldLoader] Error evaluating script:', path, evalError); + attemptIndex++; + tryLoad(); + } + }) + .catch(error => { + console.warn('[PluginSettings.CustomFieldLoader] Failed to fetch script:', path, error); + attemptIndex++; + tryLoad(); + }); + }; + tryLoad(); + // Also poll for renderer in case it loads asynchronously (max 10 seconds) + let pollCount = 0; + const pollInterval = setInterval(() => { + pollCount++; + if (checkRenderer() || pollCount > 20) { + clearInterval(pollInterval); + if (pollCount > 20 && !renderer) { + setLoading(false); + setFailed(true); + } + } + }, 500); + return () => clearInterval(pollInterval); + }, [fieldType, pluginName]); + if (renderer) { + return React.createElement(renderer, { + field: field, + pluginName: pluginName, + backendBase: backendBase, + savePluginSetting: savePluginSetting, + loadPluginSettings: loadPluginSettings, + setError: setError + }); + } + if (loading) { + return React.createElement('div', { style: { padding: 8, fontSize: 11, color: '#888', fontStyle: 'italic' } }, `Loading ${fieldType} editor...`); + } + // Failed to load - use default input if provided, otherwise show error message + if (failed && renderDefaultInput) { + return renderDefaultInput(); + } + if (failed) { + return React.createElement('div', { style: { padding: 8, fontSize: 11, color: '#f85149' } }, `Failed to load ${fieldType} editor. Using default input.`); + } + return null; + } function FieldRenderer({ f, pluginName }) { const t = f.type || 'string'; const label = f.label || f.key; const savedValue = f.value === undefined ? f.default : f.value; + // Define styles and computed values early so they're available to callbacks + const changed = savedValue !== undefined && savedValue !== null && f.default !== undefined && savedValue !== f.default; + const inputStyle = { padding: 6, background: '#111', color: '#eee', border: '1px solid #333', minWidth: 120 }; + const wrap = { position: 'relative', padding: '4px 4px 6px', border: '1px solid #2a2a2a', borderRadius: 4, background: '#101010' }; + const resetStyle = { position: 'absolute', top: 2, right: 4, fontSize: 9, padding: '1px 4px', cursor: 'pointer' }; + const labelTitle = f && f.description ? String(f.description) : undefined; + const labelEl = React.createElement('span', { title: labelTitle }, React.createElement(React.Fragment, null, label, changed ? React.createElement('span', { style: { color: '#ffa657', fontSize: 10 } }, ' •') : null)); if (t === 'path_map') { const containerStyle = { position: 'relative', @@ -1643,15 +1775,81 @@ const PluginSettings = () => { changedMap && React.createElement("span", { style: { color: '#ffa657', fontSize: 10 } }, "\u2022")), 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" }))); } - const changed = savedValue !== undefined && savedValue !== null && f.default !== undefined && savedValue !== f.default; - const inputStyle = { padding: 6, background: '#111', color: '#eee', border: '1px solid #333', minWidth: 120 }; - const wrap = { position: 'relative', padding: '4px 4px 6px', border: '1px solid #2a2a2a', borderRadius: 4, background: '#101010' }; - const resetStyle = { position: 'absolute', top: 2, right: 4, fontSize: 9, padding: '1px 4px', cursor: 'pointer' }; - const labelTitle = f && f.description ? String(f.description) : undefined; - const labelEl = React.createElement("span", { title: labelTitle }, - label, - " ", - changed && React.createElement("span", { style: { color: '#ffa657', fontSize: 10 } }, "\u2022")); + // Check for custom field renderers registered by plugins + // Supports both plugin-specific (pluginName_type_Renderer) and generic (type_Renderer) naming + if (t && typeof t === 'string' && t !== 'string' && t !== 'boolean' && t !== 'number' && t !== 'select' && t !== 'path_map') { + const pluginSpecificName = `${pluginName}_${t}_Renderer`; + const genericName = `${t}_Renderer`; + const customRenderer = window[pluginSpecificName] || window[genericName]; + const renderer = customRenderer; + // Debug logging + if (window.AIDebug) { + console.log('[PluginSettings.FieldRenderer] Custom field type detected:', { + type: t, + pluginName: pluginName, + pluginSpecificName: pluginSpecificName, + genericName: genericName, + hasPluginSpecific: !!window[pluginSpecificName], + hasGeneric: !!window[genericName], + renderer: renderer ? typeof renderer : 'null' + }); + } + if (renderer && typeof renderer === 'function') { + if (window.AIDebug) { + console.log('[PluginSettings.FieldRenderer] Using custom renderer for', t); + } + return React.createElement(renderer, { + field: f, + pluginName: pluginName, + backendBase: backendBase, + savePluginSetting: savePluginSetting, + loadPluginSettings: loadPluginSettings, + setError: setError + }); + } + else { + // Renderer not found - use CustomFieldLoader to dynamically load it + // CustomFieldLoader will handle fallback to default input if renderer not found + return React.createElement(CustomFieldLoader, { + fieldType: t, + pluginName: pluginName, + field: f, + backendBase: backendBase, + savePluginSetting: savePluginSetting, + loadPluginSettings: loadPluginSettings, + setError: setError, + // Pass the default input rendering logic as fallback + renderDefaultInput: () => { + // This will be called if renderer not found - render default text input + const display = savedValue === undefined || savedValue === null ? '' : String(savedValue); + const inputKey = `${pluginName}:${f.key}:${display}`; + const handleBlur = async (event) => { + var _a; + const next = (_a = event.target.value) !== null && _a !== void 0 ? _a : ''; + if (next === display) + return; + await savePluginSetting(pluginName, f.key, next); + }; + const handleKeyDown = (event) => { + if (event.key === 'Enter') { + event.preventDefault(); + event.target.blur(); + } + }; + const handleReset = async () => { + await savePluginSetting(pluginName, f.key, null); + }; + return React.createElement('div', { style: wrap }, React.createElement('label', { style: { fontSize: 12 } }, React.createElement(React.Fragment, null, labelEl, React.createElement('br'), React.createElement('input', { + key: inputKey, + style: inputStyle, + defaultValue: display, + onBlur: handleBlur, + onKeyDown: handleKeyDown + }))), changed ? React.createElement('button', { style: resetStyle, onClick: handleReset }, 'Reset') : null); + } + }); + } + } if (t === 'boolean') { return (React.createElement("div", { style: wrap }, React.createElement("label", { style: { fontSize: 12, display: 'flex', alignItems: 'center', gap: 8 } }, diff --git a/plugins/AIOverhaul/SimilarScenes.js b/plugins/AIOverhaul/SimilarScenes.js index d688dde2..a3a3c42f 100644 --- a/plugins/AIOverhaul/SimilarScenes.js +++ b/plugins/AIOverhaul/SimilarScenes.js @@ -619,11 +619,13 @@ }, [onSceneClicked]); // Render scene in queue list format (matching the Queue tab exactly) const renderQueueScene = useCallback((scene, index) => { - var _a, _b, _c; - const title = scene.title || `Scene ${scene.id}`; - const studio = ((_a = scene.studio) === null || _a === void 0 ? void 0 : _a.name) || ''; - const performers = ((_b = scene.performers) === null || _b === void 0 ? void 0 : _b.map(p => p.name).join(', ')) || ''; - const screenshot = (_c = scene.paths) === null || _c === void 0 ? void 0 : _c.screenshot; + var _a, _b, _c, _d, _e; + const filepath = ((_b = (_a = scene.files) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.path) || ''; + const filename = filepath ? filepath.replace(/\\/g, '/').split('/').pop() || '' : ''; + const title = scene.title || filename || `Scene ${scene.id}`; + const studio = ((_c = scene.studio) === null || _c === void 0 ? void 0 : _c.name) || ''; + const performers = ((_d = scene.performers) === null || _d === void 0 ? void 0 : _d.map(p => p.name).join(', ')) || ''; + const screenshot = (_e = scene.paths) === null || _e === void 0 ? void 0 : _e.screenshot; const date = scene.date || scene.created_at || ''; return React.createElement('li', { key: scene.id, @@ -647,10 +649,11 @@ className: 'queue-scene-details' }, [ React.createElement('span', { key: 'title', className: 'queue-scene-title' }, title), + filepath ? React.createElement('span', { key: 'filepath', className: 'queue-scene-filepath', title: filepath, style: { fontSize: '0.75em', color: '#888', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap', maxWidth: '300px', display: 'block' } }, filepath) : null, React.createElement('span', { key: 'studio', className: 'queue-scene-studio' }, studio), React.createElement('span', { key: 'performers', className: 'queue-scene-performers' }, performers), React.createElement('span', { key: 'date', className: 'queue-scene-date' }, date) - ]) + ].filter(Boolean)) ]))); }, [handleSceneClick]); // Render recommender selector when recommenders are available From 6da7ee5b9787297afeabaf618681ca783e47360e Mon Sep 17 00:00:00 2001 From: skier233 Date: Fri, 20 Mar 2026 12:25:49 -0400 Subject: [PATCH 2/2] comment fix --- plugins/AIOverhaul/AIOverhaul.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/plugins/AIOverhaul/AIOverhaul.yml b/plugins/AIOverhaul/AIOverhaul.yml index 65ee26ec..afac97cd 100644 --- a/plugins/AIOverhaul/AIOverhaul.yml +++ b/plugins/AIOverhaul/AIOverhaul.yml @@ -34,8 +34,6 @@ ui: - 'self' - http://localhost:4153 - https://localhost:4153 - - http://www.gstatic.com - - https://www.gstatic.com - 'unsafe-inline' - 'unsafe-eval' # Allow plugin JavaScript files to be loaded from the backend server