Skip to content

Commit 89483e8

Browse files
committed
feat: Hook up to pagetitle, longtitle, description and introtext
1 parent 23cc847 commit 89483e8

File tree

5 files changed

+315
-78
lines changed

5 files changed

+315
-78
lines changed

_build/gpm.yaml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,40 @@ plugins:
88
file: modai.php
99
events:
1010
- OnManagerPageBeforeRender
11+
1112
systemSettings:
1213
- key: chatgpt.key
1314
type: text-password
1415
area: chatgpt
16+
17+
- key: global.model
18+
area: global
19+
value: gpt-4o
20+
21+
- key: global.temperature
22+
area: global
23+
value: 0.7
24+
25+
- key: global.max_tokens
26+
area: global
27+
value: 2048
28+
29+
- key: prompt.base
30+
area: prompt
31+
32+
- key: prompt.pagetitle
33+
area: prompt
34+
type: 'textarea'
35+
value: "- You are a SEO expert\n- Your task is to generate Page Title from the content provided by the user\n- Title can't exceed 191 characters\n- Don't use any HTML or markdown tags"
36+
- key: prompt.longtitle
37+
area: prompt
38+
type: 'textarea'
39+
value: "- You are a SEO expert\n- Your task is to generate SEO Title from the content provided by the user\n- Title can't exceed 70 characters\n- Don't use any HTML or markdown tags"
40+
- key: prompt.introtext
41+
area: prompt
42+
type: 'textarea'
43+
value: "- You are an assistant that summarizes content.\n- Your task is to generate concise summary from the content user provides.\n- Don't use any HTML or markdown tags"
44+
- key: prompt.description
45+
area: prompt
46+
type: 'textarea'
47+
value: "- You are a SEO expert\n- Your task is to generate SEO description from the content provided by the user\n- Description can't exceed 155 characters\n- Don't use any HTML or markdown tags"
Lines changed: 159 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,39 +1,98 @@
11
Ext.onReady(function() {
22

3-
Ext.defer(function() {
3+
const cache = {
4+
_cache: {},
5+
_setFieldValue (key, value) {
6+
const cachedItem = this._cache[key];
47

5-
const labelEl = Ext.select('label[for="modx-resource-introtext"]').first();
6-
if (!labelEl) {
7-
return;
8-
}
8+
cachedItem.els.forEach(({wrapper, field}) => {
9+
const prevValue = field.value;
10+
field.setValue(value);
11+
field.fireEvent('change', field, value, prevValue);
12+
13+
wrapper.historyNav.info.update(cachedItem.visible + 1, cachedItem.values.length);
14+
15+
if (cachedItem.visible <= 0) {
16+
wrapper.historyNav.prevButton.disable();
17+
} else {
18+
wrapper.historyNav.prevButton.enable();
19+
}
20+
21+
if (cachedItem.visible === cachedItem.values.length - 1) {
22+
wrapper.historyNav.nextButton.disable();
23+
} else {
24+
wrapper.historyNav.nextButton.enable();
25+
}
26+
});
27+
},
28+
init (key, field, wrapper) {
29+
if (!this._cache[key]) {
30+
this._cache[key] = {
31+
els: [{ field, wrapper }],
32+
visible: -1,
33+
values: []
34+
};
35+
}
36+
37+
this._cache[key].els.push({ field, wrapper });
38+
},
39+
store (key, value) {
40+
const cachedItem = this._cache[key];
41+
42+
cachedItem.visible = cachedItem.values.push(value) - 1;
43+
44+
if (cachedItem.values.length > 1) {
45+
cachedItem.els.forEach(({wrapper}) => {
46+
wrapper.historyNav.show();
47+
});
48+
}
49+
50+
this._setFieldValue(key, cachedItem.values[cachedItem.visible]);
51+
},
52+
next (key) {
53+
const cachedItem = this._cache[key];
954

10-
const introText = Ext.getCmp('modx-resource-introtext');
11-
if (!introText) return;
55+
if (cachedItem.visible === cachedItem.values.length - 1) {
56+
return;
57+
}
1258

59+
this._setFieldValue(key, cachedItem.values[++cachedItem.visible]);
60+
},
61+
prev (key) {
62+
const cachedItem = this._cache[key];
63+
64+
if (cachedItem.visible <= 0) {
65+
return;
66+
}
67+
68+
this._setFieldValue(key, cachedItem.values[--cachedItem.visible]);
69+
}
70+
};
71+
72+
const createGenerateButton = (field, fieldName) => {
1373
const wandEl = document.createElement('span');
1474
wandEl.style.cursor = 'pointer';
1575
wandEl.style.marginLeft = '5px';
1676
wandEl.style.verticalAlign = 'middle';
1777
wandEl.style.fontSize = '24px';
1878
wandEl.innerText = '🪄'
1979

20-
introText.label.appendChild(wandEl);
2180

2281
wandEl.addEventListener('click', () => {
23-
Ext.Msg.wait('Generating summary...', 'Please wait');
82+
Ext.Msg.wait('Generating ...', 'Please wait');
2483

2584
MODx.Ajax.request({
2685
url: MODx.config.connector_url,
2786
params: {
28-
action: 'modAI\\Processors\\Prompt\\Summarize',
29-
id: MODx.request.id
87+
action: 'modAI\\Processors\\Prompt\\Generate',
88+
id: MODx.request.id,
89+
field: fieldName
3090
},
3191
listeners: {
3292
success: {
3393
fn: (r) => {
34-
introText.setValue(r.object.summary);
94+
cache.store(fieldName, r.object.content);
3595
Ext.Msg.hide();
36-
3796
}
3897
},
3998
failure: {
@@ -45,5 +104,92 @@ Ext.onReady(function() {
45104
}
46105
});
47106
});
107+
108+
return wandEl;
109+
}
110+
111+
const createHistoryNav = (field, fieldName) => {
112+
const prevButton = document.createElement('button');
113+
prevButton.disable = () => {
114+
prevButton.disabled = true;
115+
}
116+
prevButton.enable = () => {
117+
prevButton.disabled = false;
118+
}
119+
prevButton.innerHTML = '<<';
120+
prevButton.addEventListener('click', () => {
121+
cache.prev(fieldName);
122+
});
123+
124+
const nextButton = document.createElement('button');
125+
nextButton.disable = () => {
126+
nextButton.disabled = true;
127+
}
128+
nextButton.enable = () => {
129+
nextButton.disabled = false;
130+
}
131+
nextButton.innerHTML = '>>';
132+
nextButton.addEventListener('click', () => {
133+
cache.next(fieldName);
134+
});
135+
136+
const info = document.createElement('span');
137+
info.update = (showing, total) => {
138+
info.innerText = `${showing}/${total}`;
139+
}
140+
info.innerText = '';
141+
142+
const wrapper = document.createElement('span');
143+
wrapper.show = () => {
144+
wrapper.style.display = 'initial';
145+
}
146+
147+
wrapper.hide = () => {
148+
wrapper.style.display = 'none';
149+
}
150+
151+
wrapper.prevButton = prevButton;
152+
wrapper.nextButton = nextButton;
153+
wrapper.info = info;
154+
155+
wrapper.appendChild(prevButton);
156+
wrapper.appendChild(nextButton);
157+
wrapper.appendChild(info);
158+
159+
wrapper.hide();
160+
prevButton.disable();
161+
nextButton.disable();
162+
163+
return wrapper;
164+
}
165+
166+
const attach = (cmp, fieldName) => {
167+
const field = Ext.getCmp(cmp);
168+
if (!field) return;
169+
170+
const wrapper = document.createElement('span');
171+
const historyNav = createHistoryNav(field, fieldName);
172+
173+
wrapper.historyNav = historyNav;
174+
175+
wrapper.appendChild(createGenerateButton(field, fieldName));
176+
wrapper.appendChild(historyNav);
177+
178+
cache.init(fieldName, field, wrapper);
179+
180+
field.label.appendChild(wrapper);
181+
}
182+
183+
Ext.defer(function() {
184+
attach('modx-resource-pagetitle', 'pagetitle');
185+
186+
attach('modx-resource-introtext', 'introtext');
187+
188+
attach('modx-resource-longtitle', 'longtitle');
189+
attach('seosuite-longtitle', 'longtitle');
190+
191+
attach('modx-resource-description', 'description');
192+
attach('seosuite-description', 'description');
193+
48194
}, 500);
49195
});
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
<?php
2+
namespace modAI\Processors\Prompt;
3+
4+
use modAI\RequiredSettingException;
5+
use modAI\Services\ChatGPT;
6+
use modAI\Settings;
7+
use MODX\Revolution\Processors\Processor;
8+
9+
class Generate extends Processor
10+
{
11+
private static array $validFields = ['pagetitle', 'longtitle', 'introtext', 'description'];
12+
13+
public function process()
14+
{
15+
$fields = array_flip(self::$validFields);
16+
$field = $this->getProperty('field');
17+
18+
if (!isset($fields[$field])) {
19+
return $this->failure('Unsupported field.');
20+
}
21+
22+
$id = $this->getProperty('id');
23+
if (empty($id)) {
24+
return $this->failure('No resource specified.');
25+
}
26+
27+
$resource = $this->modx->getObject('modResource', $id);
28+
if (!$resource) {
29+
return $this->failure('Resource not found.');
30+
}
31+
32+
$content = $resource->getContent();
33+
if (empty($content)) {
34+
return $this->failure('There\'s no content');
35+
}
36+
37+
$chatGPT = new ChatGPT($this->modx);
38+
39+
40+
$messages = [];
41+
42+
try {
43+
$model = Settings::getPromptSetting($this->modx, $field, 'model');
44+
$temperature = (float)Settings::getPromptSetting($this->modx, $field, 'temperature', $model);
45+
$maxTokens = (int)Settings::getPromptSetting($this->modx, $field, 'max_tokens', $model);
46+
} catch (RequiredSettingException $e) {
47+
return $this->failure($e->getMessage());
48+
}
49+
50+
$base = Settings::getPrompt($this->modx, 'base');
51+
if (!empty($base)) {
52+
$messages[] = [
53+
'role' => 'system',
54+
'content' => $base,
55+
];
56+
}
57+
58+
$fieldPrompt = Settings::getPrompt($this->modx, $field);
59+
if (!empty($fieldPrompt)) {
60+
$messages[] = [
61+
'role' => 'system',
62+
'content' => $fieldPrompt,
63+
];
64+
}
65+
66+
$messages[] = [
67+
'role' => 'user',
68+
'content' => $content
69+
];
70+
71+
$data = [
72+
'model' => $model,
73+
'messages' => $messages,
74+
'max_tokens' => $maxTokens,
75+
'temperature' => $temperature
76+
];
77+
78+
try {
79+
$result = $chatGPT->getCompletions($data);
80+
81+
if (!isset($result['choices'][0]['message']['content'])) {
82+
return $this->failure('Error from ChatGPT API: ' . print_r($result, true));
83+
}
84+
85+
$response = trim($result['choices'][0]['message']['content']);
86+
87+
return $this->success('', ['content' => $response]);
88+
} catch (\Exception $e) {
89+
return $this->failure($e->getMessage());
90+
}
91+
}
92+
93+
}

0 commit comments

Comments
 (0)