Skip to content

Commit 9ab77a0

Browse files
committed
feat: init modAI in the media browser
Resolves #61
1 parent 8c38fb2 commit 9ab77a0

File tree

17 files changed

+289
-124
lines changed

17 files changed

+289
-124
lines changed

_build/gpm.yaml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,15 @@ systemSettings:
3131
area: cache
3232
value: 0
3333

34+
- key: init.global_chat
35+
area: init
36+
type: combo-boolean
37+
value: 1
38+
- key: init.media_browser
39+
area: init
40+
type: combo-boolean
41+
value: 1
42+
3443
- key: api.execute_on_server
3544
area: api
3645
type: combo-boolean

_build/js/src/@types/global.d.ts

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,28 @@
1+
// eslint-disable @typescript-eslint/no-explicit-any
2+
13
declare const modAI: {
24
apiURL: string;
35
resourceFields?: string[];
46
tvs?: string[];
57
};
68

7-
declare const MODx: {
8-
config: Record<string, string>;
9-
request: Record<string, string>;
9+
declare namespace MODx {
10+
const config: Record<string, string>;
11+
const request: Record<string, string>;
12+
13+
namespace tree {
14+
class Directory {
15+
initComponent(): void;
16+
}
17+
}
1018
};
1119

1220
declare namespace Ext {
1321
export function onReady(fn: () => void): void;
1422
export function defer(fn: () => void, timeout: number): void;
1523
export function get(id: string): Ext.Element;
1624
export function getCmp(id?: string): Ext.form.Field;
25+
export function override(target: any, override: any);
1726

1827
class Msg {
1928
static alert(title: string, description: string): void;
@@ -40,7 +49,6 @@ declare namespace Ext {
4049

4150
label: HTMLElement;
4251

43-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
4452
[key: string]: any;
4553
}
4654
}

_build/js/src/executor/types.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ export type DownloadImageParams = {
144144
namespace?: string;
145145
resource?: string | number;
146146
mediaSource?: string | number;
147+
path?: string;
147148
};
148149

149150
export type ChunkStream<D = unknown> = (data: D) => void;

_build/js/src/index.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,10 @@
11
import { chatHistory } from './chatHistory';
22
import { executor } from './executor';
3-
import { initGlobalButton } from './globalButton';
43
import { globalState } from './globalState';
54
import { history } from './history';
65
import { lng } from './lng';
6+
import { mgr } from './mgr';
77
import { checkPermissions } from './permissions';
8-
import { initOnResource } from './resource';
98
import { ui } from './ui';
109

1110
import type { Permissions } from './permissions';
@@ -46,8 +45,7 @@ export const init = (config: Config) => {
4645
executor,
4746
ui,
4847
lng,
49-
initOnResource,
48+
mgr,
5049
checkPermissions,
51-
initGlobalButton,
5250
};
5351
};

_build/js/src/globalButton.ts renamed to _build/js/src/mgr/globalButton.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,12 @@
1-
import { lng } from './lng';
2-
import { ui } from './ui';
3-
import { button } from './ui/dom/button';
4-
import { icon } from './ui/dom/icon';
5-
import { createModAIShadow } from './ui/dom/modAIShadow';
6-
import { bot } from './ui/icons';
7-
import { createElement } from './ui/utils';
8-
9-
import type { LocalChatConfig } from './ui/localChat/types';
1+
import { lng } from '../lng';
2+
import { ui } from '../ui';
3+
import { button } from '../ui/dom/button';
4+
import { icon } from '../ui/dom/icon';
5+
import { createModAIShadow } from '../ui/dom/modAIShadow';
6+
import { bot } from '../ui/icons';
7+
import { createElement } from '../ui/utils';
8+
9+
import type { LocalChatConfig } from '../ui/localChat/types';
1010

1111
export const initGlobalButton = () => {
1212
const config: LocalChatConfig = {

_build/js/src/mgr/index.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { initGlobalButton } from './globalButton';
2+
import { initOnMediaBrowser } from './mediaBrowser';
3+
import { initOnResource } from './resource';
4+
5+
export const mgr = {
6+
initOnResource,
7+
initGlobalButton,
8+
initOnMediaBrowser,
9+
};

_build/js/src/mgr/mediaBrowser.ts

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { ui } from '../ui';
2+
3+
export const initOnMediaBrowser = () => {
4+
Ext.override(MODx.tree.Directory, {
5+
_modAIOriginals: {
6+
initComponent: MODx.tree.Directory.prototype.initComponent,
7+
},
8+
initComponent: function () {
9+
this.on('afterrender', () => {
10+
const tbar = this.tbar.dom.querySelector('.x-toolbar-left-row');
11+
if (!tbar) {
12+
return;
13+
}
14+
15+
const wrapper = document.createElement('td');
16+
wrapper.classList.add('x-toolbar-cell');
17+
18+
const { shadow } = ui.generateButton.rawButton(
19+
() => {
20+
const node = this.cm && this.cm.activeNode ? this.cm.activeNode : false;
21+
const path = node && node.attributes.type == 'dir' ? node.attributes.pathRelative : '/';
22+
const filePath = (path.endsWith('/') ? path : path + '/') + '{hash}.png';
23+
24+
ui.localChat.createModal({
25+
key: `media_browser/${this.config.id}`,
26+
type: 'image',
27+
image: {
28+
mediaSource: this.getSource(),
29+
path: filePath,
30+
},
31+
imageActions: {
32+
insert: (_, modal) => {
33+
this.fireEvent('afterUpload');
34+
modal.api.closeModal();
35+
},
36+
},
37+
});
38+
},
39+
{
40+
iconSize: 16,
41+
},
42+
);
43+
44+
wrapper.appendChild(shadow);
45+
tbar.appendChild(wrapper);
46+
});
47+
48+
this._modAIOriginals.initComponent.call(this);
49+
},
50+
});
51+
};

_build/js/src/resource.ts renamed to _build/js/src/mgr/resource.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ui } from './ui';
1+
import { ui } from '../ui';
22

33
const attachImagePlus = (imgPlusPanel: Element, fieldName: string) => {
44
const imagePlus = Ext.getCmp(imgPlusPanel.firstElementChild?.id);

_build/js/src/ui/generateButton/index.ts

Lines changed: 89 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -37,13 +37,16 @@ type DataContext = {
3737
}[];
3838
};
3939

40-
const createWandEl = <R extends HTMLElement>(onClick: () => void | Promise<void>) => {
40+
const createWandEl = <R extends HTMLElement>(
41+
onClick: () => void | Promise<void>,
42+
config?: { iconSize?: number },
43+
) => {
4144
const { shadow, shadowRoot } = createModAIShadow<R>(true);
4245

4346
const generate = createElement(
4447
'div',
4548
'modai--root generate',
46-
button(icon(14, sparkle), onClick, 'btn', {
49+
button(icon(config?.iconSize || 14, sparkle), onClick, 'btn', {
4750
type: 'button',
4851
title: lng('modai.ui.generate_using_ai'),
4952
}),
@@ -112,16 +115,20 @@ const createHistoryNav = (cache: ReturnType<typeof history.init<DataContext>>) =
112115

113116
type Target = {
114117
targetEl: HTMLElement;
118+
iconSize?: number;
115119
};
116120

117121
const createLocalChat = (config: LocalChatConfig & Target) => {
118122
if (!ui.localChat.verifyPermissions(config)) {
119123
return;
120124
}
121125

122-
const { shadow } = createWandEl(() => {
123-
ui.localChat.createModal(config);
124-
});
126+
const { shadow } = createWandEl(
127+
() => {
128+
ui.localChat.createModal(config);
129+
},
130+
{ iconSize: config.iconSize },
131+
);
125132

126133
config.targetEl.appendChild(shadow);
127134

@@ -137,6 +144,7 @@ type ForcedTextConfig = {
137144
Target;
138145
const createForcedTextPrompt = ({
139146
targetEl,
147+
iconSize,
140148
input,
141149
onChange,
142150
initialValue,
@@ -147,34 +155,37 @@ const createForcedTextPrompt = ({
147155
return;
148156
}
149157

150-
const { shadow, generate } = createWandEl<HistoryElement>(async () => {
151-
const done = createLoadingOverlay(input);
152-
153-
try {
154-
const result = await executor.prompt.text(
155-
{
156-
field,
157-
...rest,
158-
},
159-
(data) => {
160-
cache.insert(data.content, true);
161-
},
162-
);
163-
cache.insert(result.content);
164-
done();
165-
} catch (err) {
166-
done();
167-
confirmDialog({
168-
title: 'Failed',
169-
content: lng('modai.error.failed_try_again', {
170-
msg: err instanceof Error ? err.message : '',
171-
}),
172-
confirmText: 'Close',
173-
showCancel: false,
174-
onConfirm: () => {},
175-
});
176-
}
177-
});
158+
const { shadow, generate } = createWandEl<HistoryElement>(
159+
async () => {
160+
const done = createLoadingOverlay(input);
161+
162+
try {
163+
const result = await executor.prompt.text(
164+
{
165+
field,
166+
...rest,
167+
},
168+
(data) => {
169+
cache.insert(data.content, true);
170+
},
171+
);
172+
cache.insert(result.content);
173+
done();
174+
} catch (err) {
175+
done();
176+
confirmDialog({
177+
title: 'Failed',
178+
content: lng('modai.error.failed_try_again', {
179+
msg: err instanceof Error ? err.message : '',
180+
}),
181+
confirmText: 'Close',
182+
showCancel: false,
183+
onConfirm: () => {},
184+
});
185+
}
186+
},
187+
{ iconSize: iconSize },
188+
);
178189

179190
const cache = history.init<DataContext>(
180191
field,
@@ -245,54 +256,58 @@ const createVisionPrompt = (config: VisionConfig & Target) => {
245256
return;
246257
}
247258

248-
const { shadow } = createWandEl(async () => {
249-
const canvas = document.createElement('canvas');
250-
const ctx = canvas.getContext('2d');
251-
if (!ctx) return;
252-
253-
canvas.width = config.image.width;
254-
canvas.height = config.image.height;
255-
256-
ctx.drawImage(config.image, 0, 0);
257-
258-
const base64Data = canvas.toDataURL('image/png');
259-
260-
const done = createLoadingOverlay(config.input);
261-
262-
try {
263-
const result = await executor.prompt.vision(
264-
{
265-
image: base64Data,
266-
field: config.field,
267-
namespace: config.namespace,
268-
},
269-
(data) => {
270-
config.onUpdate(data);
271-
},
272-
);
273-
274-
config.onUpdate(result);
275-
done();
276-
} catch (err) {
277-
done();
278-
confirmDialog({
279-
title: lng('modai.error.failed'),
280-
content: lng('modai.error.failed_try_again', {
281-
msg: err instanceof Error ? err.message : '',
282-
}),
283-
confirmText: lng('modai.ui.close'),
284-
showCancel: false,
285-
onConfirm: () => {},
286-
});
287-
}
288-
});
259+
const { shadow } = createWandEl(
260+
async () => {
261+
const canvas = document.createElement('canvas');
262+
const ctx = canvas.getContext('2d');
263+
if (!ctx) return;
264+
265+
canvas.width = config.image.width;
266+
canvas.height = config.image.height;
267+
268+
ctx.drawImage(config.image, 0, 0);
269+
270+
const base64Data = canvas.toDataURL('image/png');
271+
272+
const done = createLoadingOverlay(config.input);
273+
274+
try {
275+
const result = await executor.prompt.vision(
276+
{
277+
image: base64Data,
278+
field: config.field,
279+
namespace: config.namespace,
280+
},
281+
(data) => {
282+
config.onUpdate(data);
283+
},
284+
);
285+
286+
config.onUpdate(result);
287+
done();
288+
} catch (err) {
289+
done();
290+
confirmDialog({
291+
title: lng('modai.error.failed'),
292+
content: lng('modai.error.failed_try_again', {
293+
msg: err instanceof Error ? err.message : '',
294+
}),
295+
confirmText: lng('modai.ui.close'),
296+
showCancel: false,
297+
onConfirm: () => {},
298+
});
299+
}
300+
},
301+
{ iconSize: config.iconSize },
302+
);
289303

290304
config.targetEl.appendChild(shadow);
291305

292306
return shadow;
293307
};
294308

295309
export const createGenerateButton = {
310+
rawButton: createWandEl,
296311
localChat: createLocalChat,
297312
forcedText: createForcedTextPrompt,
298313
vision: createVisionPrompt,

0 commit comments

Comments
 (0)