Update AI Overhaul plugin for 0.9.3 (#685)

* Update AI Overhaul plugin for 0.9.3

* comment fix

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
This commit is contained in:
skier233
2026-03-20 12:59:27 -04:00
committed by GitHub
parent 2112d4e685
commit 74b0986b19
3 changed files with 224 additions and 16 deletions

View File

@@ -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,13 @@ 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
- 'unsafe-inline'
- 'unsafe-eval'
# Allow plugin JavaScript files to be loaded from the backend server
interface: raw
exec:
- python

View File

@@ -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 } },

View File

@@ -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