mirror of
https://github.com/stashapp/CommunityScripts.git
synced 2026-04-12 09:52:34 -05:00
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:
@@ -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
|
||||
|
||||
@@ -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 } },
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user