Skip to content

Commit 54ff5ca

Browse files
Add AI Overhaul plugin (#645)
Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
1 parent c3de80e commit 54ff5ca

19 files changed

+11252
-0
lines changed

plugins/AIOverhaul/AIButton.js

Lines changed: 898 additions & 0 deletions
Large diffs are not rendered by default.
Lines changed: 110 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,110 @@
1+
(function(){
2+
// =============================================================================
3+
// Unified Integration for AI Button + Task Dashboard
4+
// - Injects MinimalAIButton into MainNavBar.UtilityItems
5+
// - Registers /plugins/ai-tasks route mounting TaskDashboard
6+
// - Adds SettingsToolsSection entry linking to the dashboard
7+
// - Adds simple "AI" nav utility link (in case button not visible)
8+
// - All logging gated by window.AIDebug
9+
// =============================================================================
10+
(function () {
11+
var _a, _b, _c;
12+
const g = window;
13+
const PluginApi = g.PluginApi;
14+
if (!PluginApi) {
15+
console.warn('[AIIntegration] PluginApi not ready');
16+
return;
17+
}
18+
const React = PluginApi.React;
19+
const debug = !!g.AIDebug;
20+
const dlog = (...a) => { if (debug)
21+
console.log('[AIIntegration]', ...a); };
22+
// Helper to safely get components
23+
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));
24+
const { Link, NavLink } = ((_c = PluginApi.libraries) === null || _c === void 0 ? void 0 : _c.ReactRouterDOM) || {};
25+
function getMinimalButton() { return g.MinimalAIButton || g.AIButton; }
26+
function getTaskDashboard() { return g.TaskDashboard || g.AITaskDashboard; }
27+
function getPluginSettings() { return g.AIPluginSettings; }
28+
// Main nav utility items: inject AI button + nav link
29+
try {
30+
PluginApi.patch.before('MainNavBar.UtilityItems', function (props) {
31+
const MinimalAIButton = getMinimalButton();
32+
const children = [props.children];
33+
if (MinimalAIButton) {
34+
children.push(React.createElement('div', { key: 'ai-btn-wrap', style: { marginRight: 8, display: 'flex', alignItems: 'center' } }, React.createElement(MinimalAIButton)));
35+
}
36+
return [{ children }];
37+
});
38+
dlog('Patched MainNavBar.UtilityItems');
39+
}
40+
catch (e) {
41+
if (debug)
42+
console.warn('[AIIntegration] main nav patch failed', e);
43+
}
44+
// Register dashboard route
45+
try {
46+
PluginApi.register.route('/plugins/ai-tasks', () => {
47+
const Dash = getTaskDashboard();
48+
return Dash ? React.createElement(Dash, {}) : React.createElement('div', { style: { padding: 16 } }, 'Loading AI Tasks...');
49+
});
50+
dlog('Registered /plugins/ai-tasks route');
51+
}
52+
catch (e) {
53+
if (debug)
54+
console.warn('[AIIntegration] route register failed', e);
55+
}
56+
// Register settings route (event-driven, no polling)
57+
try {
58+
const SettingsWrapper = () => {
59+
const [Comp, setComp] = React.useState(() => getPluginSettings());
60+
React.useEffect(() => {
61+
if (Comp)
62+
return; // already there
63+
const handler = () => {
64+
const found = getPluginSettings();
65+
if (found) {
66+
if (debug)
67+
console.debug('[AIIntegration] AIPluginSettingsReady event captured');
68+
setComp(() => found);
69+
}
70+
};
71+
window.addEventListener('AIPluginSettingsReady', handler);
72+
// one immediate async attempt (in case script loaded right after)
73+
setTimeout(handler, 0);
74+
return () => window.removeEventListener('AIPluginSettingsReady', handler);
75+
}, [Comp]);
76+
const C = Comp;
77+
return C ? React.createElement(C, {}) : React.createElement('div', { style: { padding: 16 } }, 'Loading AI Overhaul Settings...');
78+
};
79+
PluginApi.register.route('/plugins/ai-settings', () => React.createElement(SettingsWrapper));
80+
dlog('Registered /plugins/ai-settings route (event)');
81+
}
82+
catch (e) {
83+
if (debug)
84+
console.warn('[AIIntegration] settings route register failed', e);
85+
}
86+
// Settings tools entry
87+
try {
88+
PluginApi.patch.before('SettingsToolsSection', function (props) {
89+
var _a;
90+
const Setting = (_a = PluginApi.components) === null || _a === void 0 ? void 0 : _a.Setting;
91+
if (!Setting)
92+
return props;
93+
return [{ children: (React.createElement(React.Fragment, null,
94+
props.children,
95+
React.createElement(Setting, { heading: Link ? React.createElement(Link, { to: "/plugins/ai-tasks" },
96+
React.createElement(Button, null, "AI Tasks")) : React.createElement(Button, { onClick: () => (location.href = '/plugins/ai-tasks') }, 'AI Tasks') }),
97+
React.createElement(Setting, { heading: Link ? React.createElement(Link, { to: "/plugins/ai-settings" },
98+
React.createElement(Button, null, "AI Overhaul Settings")) : React.createElement(Button, { onClick: () => (location.href = '/plugins/ai-settings') }, 'AI Overhaul Settings') }))) }];
99+
});
100+
dlog('Patched SettingsToolsSection');
101+
}
102+
catch (e) {
103+
if (debug)
104+
console.warn('[AIIntegration] settings tools patch failed', e);
105+
}
106+
if (debug)
107+
console.log('[AIIntegration] Unified integration loaded');
108+
})();
109+
})();
110+

plugins/AIOverhaul/AIOverhaul.yml

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
name: AIOverhaul
2+
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.0
4+
ui:
5+
javascript:
6+
- VersionInfo.js
7+
- BackendBase.js
8+
- BackendHealth.js
9+
- PageContext.js
10+
- RecommendationUtils.js
11+
- AIButton.js
12+
- TaskDashboard.js
13+
- PluginSettings.js # ensure settings component registers before integration
14+
- RecommendedScenes.js
15+
- SimilarScenes.js
16+
- SimilarTabIntegration.js
17+
- InteractionTracker.js
18+
- AIButtonIntegration.js # integration last after components
19+
css:
20+
- css/AIOverhaul.css
21+
- css/recommendedscenes.css
22+
- css/SimilarScenes.css
23+
csp:
24+
connect-src:
25+
- http://localhost:4153
26+
- ws://localhost:4153
27+
- https://localhost:4153
28+
# Add additional urls here for the stash-ai-server if your browser is not on the same host
29+
interface: raw
30+
exec:
31+
- python
32+
- "{pluginDir}/plugin_setup.py"
33+
tasks:
34+
- name: Setup AI Overhaul Plugin settings
35+
description: Use to set automatically set AI Overhaul Plugin settings
36+
defaultArgs:
37+
mode: plugin_setup
38+
settings:
39+
backend_base_url:
40+
displayName: Backend Base URL Override
41+
type: STRING
42+
capture_events:
43+
displayName: Capture Interaction Events
44+
type: BOOLEAN

plugins/AIOverhaul/BackendBase.js

Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
(function(){
2+
// Shared helper to determine the backend base URL used by the frontend.
3+
// Exposes a default export and also attaches to window.AIDefaultBackendBase for
4+
// non-module consumers in the minimal build.
5+
getSharedApiKey;
6+
defaultBackendBase;
7+
const PLUGIN_NAME = 'AIOverhaul';
8+
// Local default to keep the UI functional before plugin config loads.
9+
const DEFAULT_BACKEND_BASE = 'http://localhost:4153';
10+
const CONFIG_QUERY = `query AIOverhaulPluginConfig($ids: [ID!]) {
11+
configuration {
12+
plugins(include: $ids)
13+
}
14+
}`;
15+
const SHARED_KEY_EVENT = 'AISharedApiKeyUpdated';
16+
const SHARED_KEY_HEADER = 'x-ai-api-key';
17+
const SHARED_KEY_QUERY = 'api_key';
18+
const SHARED_KEY_STORAGE = 'ai_shared_api_key';
19+
let configLoaded = false;
20+
let configLoading = false;
21+
let sharedApiKeyValue = '';
22+
function getOrigin() {
23+
try {
24+
if (typeof location !== 'undefined' && location.origin) {
25+
return location.origin.replace(/\/$/, '');
26+
}
27+
}
28+
catch { }
29+
return '';
30+
}
31+
function normalizeBase(raw) {
32+
if (typeof raw !== 'string')
33+
return null;
34+
const trimmed = raw.trim();
35+
if (!trimmed)
36+
return '';
37+
const cleaned = trimmed.replace(/\/$/, '');
38+
const origin = getOrigin();
39+
if (origin && cleaned === origin) {
40+
return '';
41+
}
42+
return cleaned;
43+
}
44+
function interpretBool(raw) {
45+
if (typeof raw === 'boolean')
46+
return raw;
47+
if (typeof raw === 'number')
48+
return raw !== 0;
49+
if (typeof raw === 'string') {
50+
const lowered = raw.trim().toLowerCase();
51+
if (!lowered)
52+
return false;
53+
if (['1', 'true', 'yes', 'on'].includes(lowered))
54+
return true;
55+
if (['0', 'false', 'no', 'off'].includes(lowered))
56+
return false;
57+
}
58+
return null;
59+
}
60+
function normalizeSharedKey(raw) {
61+
if (typeof raw !== 'string')
62+
return '';
63+
return raw.trim();
64+
}
65+
function setSharedApiKey(raw) {
66+
const normalized = normalizeSharedKey(raw);
67+
if (normalized === sharedApiKeyValue)
68+
return;
69+
sharedApiKeyValue = normalized;
70+
try {
71+
if (normalized) {
72+
try {
73+
sessionStorage.setItem(SHARED_KEY_STORAGE, normalized);
74+
}
75+
catch { }
76+
}
77+
else {
78+
try {
79+
sessionStorage.removeItem(SHARED_KEY_STORAGE);
80+
}
81+
catch { }
82+
}
83+
window.AI_SHARED_API_KEY = normalized;
84+
window.dispatchEvent(new CustomEvent(SHARED_KEY_EVENT, { detail: normalized }));
85+
}
86+
catch { }
87+
}
88+
function getSharedApiKey() {
89+
if (sharedApiKeyValue)
90+
return sharedApiKeyValue;
91+
try {
92+
const stored = sessionStorage.getItem(SHARED_KEY_STORAGE);
93+
if (typeof stored === 'string' && stored.trim()) {
94+
sharedApiKeyValue = stored.trim();
95+
return sharedApiKeyValue;
96+
}
97+
}
98+
catch { }
99+
try {
100+
const globalValue = window.AI_SHARED_API_KEY;
101+
if (typeof globalValue === 'string') {
102+
sharedApiKeyValue = globalValue.trim();
103+
return sharedApiKeyValue;
104+
}
105+
}
106+
catch { }
107+
return '';
108+
}
109+
function withSharedKeyHeaders(init) {
110+
const key = getSharedApiKey();
111+
if (!key)
112+
return init ? init : {};
113+
const next = { ...(init || {}) };
114+
const headers = new Headers((init === null || init === void 0 ? void 0 : init.headers) || {});
115+
headers.set(SHARED_KEY_HEADER, key);
116+
next.headers = headers;
117+
return next;
118+
}
119+
function appendSharedApiKeyQuery(url) {
120+
const key = getSharedApiKey();
121+
if (!key)
122+
return url;
123+
try {
124+
const base = getOrigin() || undefined;
125+
const resolved = new URL(url, url.startsWith('http://') || url.startsWith('https://') || url.startsWith('ws://') || url.startsWith('wss://') ? undefined : base);
126+
resolved.searchParams.set(SHARED_KEY_QUERY, key);
127+
return resolved.toString();
128+
}
129+
catch {
130+
const sep = url.includes('?') ? '&' : '?';
131+
return `${url}${sep}${SHARED_KEY_QUERY}=${encodeURIComponent(key)}`;
132+
}
133+
}
134+
function applyPluginConfig(base, captureEvents, sharedKey) {
135+
if (base !== undefined) {
136+
const normalized = normalizeBase(base);
137+
if (normalized !== null) {
138+
const value = normalized || '';
139+
try {
140+
window.AI_BACKEND_URL = value;
141+
window.dispatchEvent(new CustomEvent('AIBackendBaseUpdated', { detail: value }));
142+
}
143+
catch { }
144+
}
145+
}
146+
if (captureEvents !== undefined && captureEvents !== null) {
147+
const normalized = !!captureEvents;
148+
try {
149+
window.__AI_INTERACTIONS_ENABLED__ = normalized;
150+
}
151+
catch { }
152+
try {
153+
const tracker = window.stashAIInteractionTracker;
154+
if (tracker) {
155+
if (typeof tracker.setEnabled === 'function')
156+
tracker.setEnabled(normalized);
157+
else if (typeof tracker.configure === 'function')
158+
tracker.configure({ enabled: normalized });
159+
}
160+
}
161+
catch { }
162+
}
163+
if (sharedKey !== undefined) {
164+
setSharedApiKey(sharedKey);
165+
}
166+
}
167+
async function loadPluginConfig() {
168+
var _a, _b, _c, _d, _e, _f, _g, _h;
169+
if (configLoaded || configLoading)
170+
return;
171+
configLoading = true;
172+
try {
173+
const resp = await fetch('/graphql', {
174+
method: 'POST',
175+
headers: { 'content-type': 'application/json' },
176+
credentials: 'same-origin',
177+
body: JSON.stringify({ query: CONFIG_QUERY, variables: { ids: [PLUGIN_NAME] } }),
178+
});
179+
if (!resp.ok)
180+
return;
181+
const payload = await resp.json().catch(() => null);
182+
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;
183+
if (plugins && typeof plugins === 'object') {
184+
const entry = plugins[PLUGIN_NAME];
185+
if (entry && typeof entry === 'object') {
186+
const backendBase = (_d = (_c = entry.backend_base_url) !== null && _c !== void 0 ? _c : entry.backendBaseUrl) !== null && _d !== void 0 ? _d : entry.backendBaseURL;
187+
const captureEvents = (_f = (_e = entry.capture_events) !== null && _e !== void 0 ? _e : entry.captureEvents) !== null && _f !== void 0 ? _f : entry.captureEventsEnabled;
188+
const sharedKey = (_h = (_g = entry.shared_api_key) !== null && _g !== void 0 ? _g : entry.sharedApiKey) !== null && _h !== void 0 ? _h : entry.sharedKey;
189+
applyPluginConfig(backendBase, interpretBool(captureEvents), typeof sharedKey === 'string' ? sharedKey : undefined);
190+
}
191+
}
192+
}
193+
catch { }
194+
finally {
195+
configLoaded = true;
196+
configLoading = false;
197+
}
198+
}
199+
function defaultBackendBase() {
200+
try {
201+
if (!configLoaded)
202+
loadPluginConfig();
203+
}
204+
catch { }
205+
if (typeof window.AI_BACKEND_URL === 'string') {
206+
const explicit = normalizeBase(window.AI_BACKEND_URL);
207+
if (explicit !== null && explicit !== undefined) {
208+
return explicit;
209+
}
210+
return '';
211+
}
212+
return DEFAULT_BACKEND_BASE;
213+
}
214+
// Also attach as a global so files that are executed before this module can still
215+
// use the shared function when available.
216+
try {
217+
window.AIDefaultBackendBase = defaultBackendBase;
218+
defaultBackendBase.loadPluginConfig = loadPluginConfig;
219+
defaultBackendBase.applyPluginConfig = applyPluginConfig;
220+
window.AISharedApiKeyHelper = {
221+
get: getSharedApiKey,
222+
withHeaders: withSharedKeyHeaders,
223+
appendQuery: appendSharedApiKeyQuery,
224+
};
225+
}
226+
catch { }
227+
})();
228+

0 commit comments

Comments
 (0)