Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
422 changes: 422 additions & 0 deletions plugins/AIOverhaul/AIButton.js

Large diffs are not rendered by default.

110 changes: 110 additions & 0 deletions plugins/AIOverhaul/AIButtonIntegration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
(function(){
// =============================================================================
// Unified Integration for AI Button + Task Dashboard
// - Injects MinimalAIButton into MainNavBar.UtilityItems
// - Registers /plugins/ai-tasks route mounting TaskDashboard
// - Adds SettingsToolsSection entry linking to the dashboard
// - Adds simple "AI" nav utility link (in case button not visible)
// - All logging gated by window.AIDebug
// =============================================================================
(function () {
var _a, _b, _c;
const g = window;
const PluginApi = g.PluginApi;
if (!PluginApi) {
console.warn('[AIIntegration] PluginApi not ready');
return;
}
const React = PluginApi.React;
const debug = !!g.AIDebug;
const dlog = (...a) => { if (debug)
console.log('[AIIntegration]', ...a); };
// Helper to safely get components
const Button = ((_b = (_a = PluginApi.libraries) === null || _a === void 0 ? void 0 : _a.Bootstrap) === null || _b === void 0 ? void 0 : _b.Button) || ((p) => React.createElement('button', p, p.children));
const { Link, NavLink } = ((_c = PluginApi.libraries) === null || _c === void 0 ? void 0 : _c.ReactRouterDOM) || {};
function getMinimalButton() { return g.MinimalAIButton || g.AIButton; }
function getTaskDashboard() { return g.TaskDashboard || g.AITaskDashboard; }
function getPluginSettings() { return g.AIPluginSettings; }
// Main nav utility items: inject AI button + nav link
try {
PluginApi.patch.before('MainNavBar.UtilityItems', function (props) {
const MinimalAIButton = getMinimalButton();
const children = [props.children];
if (MinimalAIButton) {
children.push(React.createElement('div', { key: 'ai-btn-wrap', style: { marginRight: 8, display: 'flex', alignItems: 'center' } }, React.createElement(MinimalAIButton)));
}
return [{ children }];
});
dlog('Patched MainNavBar.UtilityItems');
}
catch (e) {
if (debug)
console.warn('[AIIntegration] main nav patch failed', e);
}
// Register dashboard route
try {
PluginApi.register.route('/plugins/ai-tasks', () => {
const Dash = getTaskDashboard();
return Dash ? React.createElement(Dash, {}) : React.createElement('div', { style: { padding: 16 } }, 'Loading AI Tasks...');
});
dlog('Registered /plugins/ai-tasks route');
}
catch (e) {
if (debug)
console.warn('[AIIntegration] route register failed', e);
}
// Register settings route (event-driven, no polling)
try {
const SettingsWrapper = () => {
const [Comp, setComp] = React.useState(() => getPluginSettings());
React.useEffect(() => {
if (Comp)
return; // already there
const handler = () => {
const found = getPluginSettings();
if (found) {
if (debug)
console.debug('[AIIntegration] AIPluginSettingsReady event captured');
setComp(() => found);
}
};
window.addEventListener('AIPluginSettingsReady', handler);
// one immediate async attempt (in case script loaded right after)
setTimeout(handler, 0);
return () => window.removeEventListener('AIPluginSettingsReady', handler);
}, [Comp]);
const C = Comp;
return C ? React.createElement(C, {}) : React.createElement('div', { style: { padding: 16 } }, 'Loading AI Overhaul Settings...');
};
PluginApi.register.route('/plugins/ai-settings', () => React.createElement(SettingsWrapper));
dlog('Registered /plugins/ai-settings route (event)');
}
catch (e) {
if (debug)
console.warn('[AIIntegration] settings route register failed', e);
}
// Settings tools entry
try {
PluginApi.patch.before('SettingsToolsSection', function (props) {
var _a;
const Setting = (_a = PluginApi.components) === null || _a === void 0 ? void 0 : _a.Setting;
if (!Setting)
return props;
return [{ children: (React.createElement(React.Fragment, null,
props.children,
React.createElement(Setting, { heading: Link ? React.createElement(Link, { to: "/plugins/ai-tasks" },
React.createElement(Button, null, "AI Tasks")) : React.createElement(Button, { onClick: () => (location.href = '/plugins/ai-tasks') }, 'AI Tasks') }),
React.createElement(Setting, { heading: Link ? React.createElement(Link, { to: "/plugins/ai-settings" },
React.createElement(Button, null, "AI Overhaul Settings")) : React.createElement(Button, { onClick: () => (location.href = '/plugins/ai-settings') }, 'AI Overhaul Settings') }))) }];
});
dlog('Patched SettingsToolsSection');
}
catch (e) {
if (debug)
console.warn('[AIIntegration] settings tools patch failed', e);
}
if (debug)
console.log('[AIIntegration] Unified integration loaded');
})();
})();

42 changes: 42 additions & 0 deletions plugins/AIOverhaul/AIOverhaul.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
name: AIOverhaul
description: AI Overhaul for Stash
version: 0.5.0
ui:
javascript:
- BackendBase.js
- BackendHealth.js
- PageContext.js
- RecommendationUtils.js
- AIButton.js
- TaskDashboard.js
- PluginSettings.js # ensure settings component registers before integration
- RecommendedScenes.js
- SimilarScenes.js
- SimilarTabIntegration.js
- InteractionTracker.js
- AIButtonIntegration.js # integration last after components
css:
- css/AIOverhaul.css
- css/recommendedscenes.css
- css/SimilarScenes.css
csp:
connect-src:
- http://localhost:4153
- ws://localhost:4153
- https://localhost:4153
interface: raw
exec:
- python
- "{pluginDir}/plugin_setup.py"
tasks:
- name: Setup AI Overhaul Plugin settings
description: Use to set automatically set AI Overhaul Plugin settings
defaultArgs:
mode: plugin_setup
settings:
backend_base_url:
displayName: Backend Base URL Override
type: STRING
capture_events:
displayName: Capture Interaction Events
type: BOOLEAN
139 changes: 139 additions & 0 deletions plugins/AIOverhaul/BackendBase.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,139 @@
(function(){
// Shared helper to determine the backend base URL used by the frontend.
// Exposes a default export and also attaches to window.AIDefaultBackendBase for
// non-module consumers in the minimal build.
defaultBackendBase;
const PLUGIN_NAME = 'AIOverhaul';
// Local default to keep the UI functional before plugin config loads.
const DEFAULT_BACKEND_BASE = 'http://localhost:4153';
const CONFIG_QUERY = `query AIOverhaulPluginConfig($ids: [ID!]) {
configuration {
plugins(include: $ids)
}
}`;
let configLoaded = false;
let configLoading = false;
function getOrigin() {
try {
if (typeof location !== 'undefined' && location.origin) {
return location.origin.replace(/\/$/, '');
}
}
catch { }
return '';
}
function normalizeBase(raw) {
if (typeof raw !== 'string')
return null;
const trimmed = raw.trim();
if (!trimmed)
return '';
const cleaned = trimmed.replace(/\/$/, '');
const origin = getOrigin();
if (origin && cleaned === origin) {
return '';
}
return cleaned;
}
function interpretBool(raw) {
if (typeof raw === 'boolean')
return raw;
if (typeof raw === 'number')
return raw !== 0;
if (typeof raw === 'string') {
const lowered = raw.trim().toLowerCase();
if (!lowered)
return false;
if (['1', 'true', 'yes', 'on'].includes(lowered))
return true;
if (['0', 'false', 'no', 'off'].includes(lowered))
return false;
}
return null;
}
function applyPluginConfig(base, captureEvents) {
if (base !== undefined) {
const normalized = normalizeBase(base);
if (normalized !== null) {
const value = normalized || '';
try {
window.AI_BACKEND_URL = value;
window.dispatchEvent(new CustomEvent('AIBackendBaseUpdated', { detail: value }));
}
catch { }
}
}
if (captureEvents !== undefined && captureEvents !== null) {
const normalized = !!captureEvents;
try {
window.__AI_INTERACTIONS_ENABLED__ = normalized;
}
catch { }
try {
const tracker = window.stashAIInteractionTracker;
if (tracker) {
if (typeof tracker.setEnabled === 'function')
tracker.setEnabled(normalized);
else if (typeof tracker.configure === 'function')
tracker.configure({ enabled: normalized });
}
}
catch { }
}
}
async function loadPluginConfig() {
var _a, _b, _c, _d, _e, _f;
if (configLoaded || configLoading)
return;
configLoading = true;
try {
const resp = await fetch('/graphql', {
method: 'POST',
headers: { 'content-type': 'application/json' },
credentials: 'same-origin',
body: JSON.stringify({ query: CONFIG_QUERY, variables: { ids: [PLUGIN_NAME] } }),
});
if (!resp.ok)
return;
const payload = await resp.json().catch(() => null);
const plugins = (_b = (_a = payload === null || payload === void 0 ? void 0 : payload.data) === null || _a === void 0 ? void 0 : _a.configuration) === null || _b === void 0 ? void 0 : _b.plugins;
if (plugins && typeof plugins === 'object') {
const entry = plugins[PLUGIN_NAME];
if (entry && typeof entry === 'object') {
const backendBase = (_d = (_c = entry.backend_base_url) !== null && _c !== void 0 ? _c : entry.backendBaseUrl) !== null && _d !== void 0 ? _d : entry.backendBaseURL;
const captureEvents = (_f = (_e = entry.capture_events) !== null && _e !== void 0 ? _e : entry.captureEvents) !== null && _f !== void 0 ? _f : entry.captureEventsEnabled;
applyPluginConfig(backendBase, interpretBool(captureEvents));
}
}
}
catch { }
finally {
configLoaded = true;
configLoading = false;
}
}
function defaultBackendBase() {
try {
if (!configLoaded)
loadPluginConfig();
}
catch { }
if (typeof window.AI_BACKEND_URL === 'string') {
const explicit = normalizeBase(window.AI_BACKEND_URL);
if (explicit !== null && explicit !== undefined) {
return explicit;
}
return '';
}
return DEFAULT_BACKEND_BASE;
}
// Also attach as a global so files that are executed before this module can still
// use the shared function when available.
try {
window.AIDefaultBackendBase = defaultBackendBase;
defaultBackendBase.loadPluginConfig = loadPluginConfig;
defaultBackendBase.applyPluginConfig = applyPluginConfig;
}
catch { }
})();

Loading