Skip to content

Commit 74b0986

Browse files
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>
1 parent 2112d4e commit 74b0986

File tree

3 files changed

+224
-16
lines changed

3 files changed

+224
-16
lines changed

plugins/AIOverhaul/AIOverhaul.yml

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
name: AIOverhaul
22
description: AI Overhaul for Stash with a full plugin engine included to install and manage asynchronous stash plugins for AI or other purposes.
3-
version: 0.9.2
3+
version: 0.9.3
44
url: https://discourse.stashapp.cc/t/aioverhaul/4847
55
ui:
66
javascript:
@@ -30,6 +30,13 @@ ui:
3030
- ws://127.0.0.1:4153
3131
- https://127.0.0.1:4153
3232
# Add additional urls here for the stash-ai-server if your browser is not on the same host
33+
script-src:
34+
- 'self'
35+
- http://localhost:4153
36+
- https://localhost:4153
37+
- 'unsafe-inline'
38+
- 'unsafe-eval'
39+
# Allow plugin JavaScript files to be loaded from the backend server
3340
interface: raw
3441
exec:
3542
- python

plugins/AIOverhaul/PluginSettings.js

Lines changed: 207 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -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 } },

plugins/AIOverhaul/SimilarScenes.js

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -619,11 +619,13 @@
619619
}, [onSceneClicked]);
620620
// Render scene in queue list format (matching the Queue tab exactly)
621621
const renderQueueScene = useCallback((scene, index) => {
622-
var _a, _b, _c;
623-
const title = scene.title || `Scene ${scene.id}`;
624-
const studio = ((_a = scene.studio) === null || _a === void 0 ? void 0 : _a.name) || '';
625-
const performers = ((_b = scene.performers) === null || _b === void 0 ? void 0 : _b.map(p => p.name).join(', ')) || '';
626-
const screenshot = (_c = scene.paths) === null || _c === void 0 ? void 0 : _c.screenshot;
622+
var _a, _b, _c, _d, _e;
623+
const filepath = ((_b = (_a = scene.files) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.path) || '';
624+
const filename = filepath ? filepath.replace(/\\/g, '/').split('/').pop() || '' : '';
625+
const title = scene.title || filename || `Scene ${scene.id}`;
626+
const studio = ((_c = scene.studio) === null || _c === void 0 ? void 0 : _c.name) || '';
627+
const performers = ((_d = scene.performers) === null || _d === void 0 ? void 0 : _d.map(p => p.name).join(', ')) || '';
628+
const screenshot = (_e = scene.paths) === null || _e === void 0 ? void 0 : _e.screenshot;
627629
const date = scene.date || scene.created_at || '';
628630
return React.createElement('li', {
629631
key: scene.id,
@@ -647,10 +649,11 @@
647649
className: 'queue-scene-details'
648650
}, [
649651
React.createElement('span', { key: 'title', className: 'queue-scene-title' }, title),
652+
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,
650653
React.createElement('span', { key: 'studio', className: 'queue-scene-studio' }, studio),
651654
React.createElement('span', { key: 'performers', className: 'queue-scene-performers' }, performers),
652655
React.createElement('span', { key: 'date', className: 'queue-scene-date' }, date)
653-
])
656+
].filter(Boolean))
654657
])));
655658
}, [handleSceneClick]);
656659
// Render recommender selector when recommenders are available

0 commit comments

Comments
 (0)