formstrap 0.4.9 → 0.4.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,1077 +1,1053 @@
1
- /*jshint esversion: 6 */
1
+ /* global Redactor */
2
+ /* eslint-disable no-useless-escape */
3
+ /* jshint esversion: 6 */
2
4
  Redactor.add('plugin', 'ai', {
3
- translations: {
4
- en: {
5
- 'ai': {
6
- "placeholder-image": "Describe the image you want to generate.",
7
- "placeholder-text": "Tell me what you want to write.",
8
- "send": "Send",
9
- "stop": "Stop",
10
- "discard": "Discard",
11
- "insert": "Insert",
12
- "prompt": "Prompt",
13
- "image-style": "Image style",
14
- "change-tone": "Change tone"
15
- }
16
- }
17
- },
18
- dropdowns: {
19
- items: {
20
- improve: { title: 'Improve it', command: 'ai.set', params: { prompt: 'Improve it' } },
21
- simplify: { title: 'Simplify it', command: 'ai.set', params: { prompt: 'Simplify it' } },
22
- fix: { title: 'Fix any mistakes', command: 'ai.set', params: { prompt: 'Fix any mistakes' } },
23
- shorten: { title: 'Make it shorter', command: 'ai.set', params: { prompt: 'Make it shorter' } },
24
- detailed: { title: 'Make it more detailed', command: 'ai.set', params: { prompt: 'Make it more detailed' } },
25
- complete: { title: 'Complete sentence', command: 'ai.set', params: { prompt: 'Complete sentence' } },
26
- tone: { title: 'Change tone', command: 'ai.popupTone' },
27
- translate: { title: 'Translate', command: 'ai.popupTranslate' }
28
- }
29
- },
30
- defaults: {
31
- tone: [
32
- 'Academic',
33
- 'Assertive',
34
- 'Casual',
35
- 'Confident',
36
- 'Constructive',
37
- 'Empathetic',
38
- 'Exciting',
39
- 'Fluent',
40
- 'Formal',
41
- 'Friendly',
42
- 'Inspirational',
43
- 'Professional'
44
- ],
45
- style: [
46
- '3d model',
47
- 'Digital art',
48
- 'Isometric',
49
- 'Line art',
50
- 'Photorealistic',
51
- 'Pixel art'
52
- ],
53
- translate: [
54
- 'Arabic',
55
- 'Chinese',
56
- 'English',
57
- 'French',
58
- 'German',
59
- 'Greek',
60
- 'Italian',
61
- 'Japanese',
62
- 'Korean',
63
- 'Portuguese',
64
- 'Russian',
65
- 'Spanish',
66
- 'Swedish',
67
- 'Ukrainian'
68
- ],
69
- size: {
70
- '1792x1024': 'Landscape',
71
- '1024x1792': 'Portrait',
72
- '1024x1024': 'Square'
73
- },
74
- text: {
75
- stream: true
76
- },
77
- image: {
78
- save: false
79
- },
80
- makeit: 'Make it',
81
- translateto: 'Translate to',
82
- spinner: '<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_nOfF{animation:spinner_qtyZ 2s cubic-bezier(0.36,.6,.31,1) infinite}.spinner_fVhf{animation-delay:-.5s}.spinner_piVe{animation-delay:-1s}.spinner_MSNs{animation-delay:-1.5s}@keyframes spinner_qtyZ{0%{r:0}25%{r:3px;cx:4px}50%{r:3px;cx:12px}75%{r:3px;cx:20px}100%{r:0;cx:20px}}</style><circle class="spinner_nOfF" cx="4" cy="12" r="3"/><circle class="spinner_nOfF spinner_fVhf" cx="4" cy="12" r="3"/><circle class="spinner_nOfF spinner_piVe" cx="4" cy="12" r="3"/><circle class="spinner_nOfF spinner_MSNs" cx="4" cy="12" r="3"/></svg>'
83
- },
84
- observe(obj, name, toolbar) {
85
- if (name === 'ai-tools' && !this.opts.is('ai.text.url')) {
86
- return;
87
- }
88
- else if (name === 'ai-image' && !this.opts.is('ai.image.url')) {
89
- return;
90
- }
91
-
92
- return obj;
93
- },
94
- popup(e, button) {
95
- let uiState = this.app.ui.getState();
96
- if (uiState.type !== 'addbar') {
97
- this.app.dropdown.create('ai-tools', { items: this.opts.get('ai.items') || this.dropdowns.items });
98
- this.app.dropdown.open(e, button);
99
- }
100
- else {
101
- this._buildPrompt();
102
- }
103
- },
104
- promptImage(e, button) {
105
- this._buildPrompt({ image: true });
5
+ translations: {
6
+ en: {
7
+ ai: {
8
+ 'placeholder-image': 'Describe the image you want to generate.',
9
+ 'placeholder-text': 'Tell me what you want to write.',
10
+ send: 'Send',
11
+ stop: 'Stop',
12
+ discard: 'Discard',
13
+ insert: 'Insert',
14
+ prompt: 'Prompt',
15
+ 'image-style': 'Image style',
16
+ 'change-tone': 'Change tone'
17
+ }
18
+ }
19
+ },
20
+ dropdowns: {
21
+ items: {
22
+ improve: { title: 'Improve it', command: 'ai.set', params: { prompt: 'Improve it' } },
23
+ simplify: { title: 'Simplify it', command: 'ai.set', params: { prompt: 'Simplify it' } },
24
+ fix: { title: 'Fix any mistakes', command: 'ai.set', params: { prompt: 'Fix any mistakes' } },
25
+ shorten: { title: 'Make it shorter', command: 'ai.set', params: { prompt: 'Make it shorter' } },
26
+ detailed: { title: 'Make it more detailed', command: 'ai.set', params: { prompt: 'Make it more detailed' } },
27
+ complete: { title: 'Complete sentence', command: 'ai.set', params: { prompt: 'Complete sentence' } },
28
+ tone: { title: 'Change tone', command: 'ai.popupTone' },
29
+ translate: { title: 'Translate', command: 'ai.popupTranslate' }
30
+ }
31
+ },
32
+ defaults: {
33
+ tone: [
34
+ 'Academic',
35
+ 'Assertive',
36
+ 'Casual',
37
+ 'Confident',
38
+ 'Constructive',
39
+ 'Empathetic',
40
+ 'Exciting',
41
+ 'Fluent',
42
+ 'Formal',
43
+ 'Friendly',
44
+ 'Inspirational',
45
+ 'Professional'
46
+ ],
47
+ style: [
48
+ '3d model',
49
+ 'Digital art',
50
+ 'Isometric',
51
+ 'Line art',
52
+ 'Photorealistic',
53
+ 'Pixel art'
54
+ ],
55
+ translate: [
56
+ 'Arabic',
57
+ 'Chinese',
58
+ 'English',
59
+ 'French',
60
+ 'German',
61
+ 'Greek',
62
+ 'Italian',
63
+ 'Japanese',
64
+ 'Korean',
65
+ 'Portuguese',
66
+ 'Russian',
67
+ 'Spanish',
68
+ 'Swedish',
69
+ 'Ukrainian'
70
+ ],
71
+ size: {
72
+ '1792x1024': 'Landscape',
73
+ '1024x1792': 'Portrait',
74
+ '1024x1024': 'Square'
106
75
  },
107
- popupTone(e, button) {
108
- let buttons = {};
109
- let items = this.opts.get('ai.tone') || this.defaults.tone;
110
- const makeit = this.opts.get('ai.makeit') || this.defaults.makeit;
111
-
112
- for (let i = 0; i < items.length; i++) {
113
- buttons[i] = { title: items[i], command: 'ai.set', params: { prompt: makeit + ' ' + items[i] } };
114
- }
115
-
116
- this.app.dropdown.create('ai-tone', { items: buttons });
117
- this.app.dropdown.open(e, button);
76
+ text: {
77
+ stream: true
118
78
  },
119
- popupTranslate(e, button) {
120
- let buttons = {};
121
- let items = this.opts.get('ai.translate') || this.defaults.translate;
122
- const translateto = this.opts.get('ai.translateto') || this.defaults.translateto;
123
-
124
- for (let i = 0; i < items.length; i++) {
125
- buttons[i] = { title: items[i], command: 'ai.set', params: { prompt: translateto + ' ' + items[i] } };
126
- }
127
-
128
- this.app.dropdown.create('ai-translate', { items: buttons });
129
- this.app.dropdown.open(e, button);
130
- },
131
- set(params, button) {
132
- this.promptButton = button;
133
- let text = this._getText();
134
- let html = this._getHtml();
135
-
136
- // spinner
137
- if (text !== '' && params.empty !== true) {
138
- this.promptButton.setIcon(this.defaults.spinner);
139
- }
140
-
141
- // empty
142
- if (params.empty) {
143
- text = '';
144
- html = '';
145
- this.promptButton.setIcon(this.defaults.spinner);
146
- }
147
-
148
- // broadcast
149
- let message = params.prompt;
150
- let event = this.app.broadcast('ai.create', { prompt: message });
151
- message = event.get('prompt');
152
- this.modifiedValue = message;
153
-
154
- this._setPrompt(text, html, message, params.empty);
79
+ image: {
80
+ save: false
155
81
  },
156
- sendPrompt(e) {
157
- e.preventDefault();
158
- e.stopPropagation();
159
-
160
- let apimodel = this.opts.get('ai.' + this.promptType + '.model');
161
- let message = this._getMessage();
162
-
163
- // broadcast
164
- let event = this.app.broadcast('ai.create', { prompt: message });
165
- message = event.get('prompt');
166
- this.modifiedValue = message;
167
-
168
- if (message === '') return;
169
- let tone = this._getTone(message);
170
-
171
- if (this.promptType === 'text') {
172
- this.conversation.push({ "role": "user", "content": message });
173
- if (tone) {
174
- this.conversation.push({ "role": "user", "content": tone });
175
- }
176
- }
177
-
178
- let request = {
179
- model: apimodel,
180
- stream: this.opts.get('ai.text.stream'),
181
- messages: this.conversation
182
- };
183
-
184
- let size = "1024x1024";
185
- if (this.promptType === 'image') {
186
- size = this.$size.val();
187
- }
188
-
189
- request = (this.promptType === 'image') ? { model: apimodel, n: 1, size: size, prompt: message } : request;
82
+ makeit: 'Make it',
83
+ translateto: 'Translate to',
84
+ spinner: '<svg width="24" height="24" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg"><style>.spinner_nOfF{animation:spinner_qtyZ 2s cubic-bezier(0.36,.6,.31,1) infinite}.spinner_fVhf{animation-delay:-.5s}.spinner_piVe{animation-delay:-1s}.spinner_MSNs{animation-delay:-1.5s}@keyframes spinner_qtyZ{0%{r:0}25%{r:3px;cx:4px}50%{r:3px;cx:12px}75%{r:3px;cx:20px}100%{r:0;cx:20px}}</style><circle class="spinner_nOfF" cx="4" cy="12" r="3"/><circle class="spinner_nOfF spinner_fVhf" cx="4" cy="12" r="3"/><circle class="spinner_nOfF spinner_piVe" cx="4" cy="12" r="3"/><circle class="spinner_nOfF spinner_MSNs" cx="4" cy="12" r="3"/></svg>'
85
+ },
86
+ observe (obj, name, toolbar) {
87
+ if (name === 'ai-tools' && !this.opts.is('ai.text.url')) {
88
+ return
89
+ } else if (name === 'ai-image' && !this.opts.is('ai.image.url')) {
90
+ return
91
+ }
190
92
 
191
- // loading
192
- this.$progress.html(this.defaults.spinner);
93
+ return obj
94
+ },
95
+ popup (e, button) {
96
+ const uiState = this.app.ui.getState()
97
+ if (uiState.type !== 'addbar') {
98
+ this.app.dropdown.create('ai-tools', { items: this.opts.get('ai.items') || this.dropdowns.items })
99
+ this.app.dropdown.open(e, button)
100
+ } else {
101
+ this._buildPrompt()
102
+ }
103
+ },
104
+ promptImage (e, button) {
105
+ this._buildPrompt({ image: true })
106
+ },
107
+ popupTone (e, button) {
108
+ const buttons = {}
109
+ const items = this.opts.get('ai.tone') || this.defaults.tone
110
+ const makeit = this.opts.get('ai.makeit') || this.defaults.makeit
111
+
112
+ for (let i = 0; i < items.length; i++) {
113
+ buttons[i] = { title: items[i], command: 'ai.set', params: { prompt: makeit + ' ' + items[i] } }
114
+ }
193
115
 
194
- // send
195
- if (this.promptType !== 'image' && this.opts.is('ai.text.stream')) {
196
- this._sendStream(this.$preview, message, true);
197
- }
198
- else {
199
- this._sendPrompt(request, '_complete');
200
- }
201
- },
202
- insertPrompt(e) {
203
- e.preventDefault();
204
- e.stopPropagation();
116
+ this.app.dropdown.create('ai-tone', { items: buttons })
117
+ this.app.dropdown.open(e, button)
118
+ },
119
+ popupTranslate (e, button) {
120
+ const buttons = {}
121
+ const items = this.opts.get('ai.translate') || this.defaults.translate
122
+ const translateto = this.opts.get('ai.translateto') || this.defaults.translateto
205
123
 
206
- let insertion = this.app.create('insertion');
207
- let html = this.$preview.html();
124
+ for (let i = 0; i < items.length; i++) {
125
+ buttons[i] = { title: items[i], command: 'ai.set', params: { prompt: translateto + ' ' + items[i] } }
126
+ }
208
127
 
209
- if (this.promptType === 'image') {
210
- const tag = this.opts.get('image.tag');
211
- html = `<${tag}>${html}</${tag}>`;
212
- }
128
+ this.app.dropdown.create('ai-translate', { items: buttons })
129
+ this.app.dropdown.open(e, button)
130
+ },
131
+ set (params, button) {
132
+ this.promptButton = button
133
+ let text = this._getText()
134
+ let html = this._getHtml()
135
+
136
+ // spinner
137
+ if (text !== '' && params.empty !== true) {
138
+ this.promptButton.setIcon(this.defaults.spinner)
139
+ }
213
140
 
214
- // broadcast
215
- let event = this.app.broadcast('ai.before.insert', { html: html });
216
- html = event.get('html');
141
+ // empty
142
+ if (params.empty) {
143
+ text = ''
144
+ html = ''
145
+ this.promptButton.setIcon(this.defaults.spinner)
146
+ }
217
147
 
218
- let $target = this.savedInstance ? this.savedInstance.getBlock() : this.$prompt;
219
- let position = this.savedInstance ? 'after' : 'before';
220
- let remove = this.savedInstance ? false : true;
148
+ // broadcast
149
+ let message = params.prompt
150
+ const event = this.app.broadcast('ai.create', { prompt: message })
151
+ message = event.get('prompt')
152
+ this.modifiedValue = message
153
+
154
+ this._setPrompt(text, html, message, params.empty)
155
+ },
156
+ sendPrompt (e) {
157
+ e.preventDefault()
158
+ e.stopPropagation()
159
+
160
+ const apimodel = this.opts.get('ai.' + this.promptType + '.model')
161
+ let message = this._getMessage()
162
+
163
+ // broadcast
164
+ const event = this.app.broadcast('ai.create', { prompt: message })
165
+ message = event.get('prompt')
166
+ this.modifiedValue = message
167
+
168
+ if (message === '') return
169
+ const tone = this._getTone(message)
170
+
171
+ if (this.promptType === 'text') {
172
+ this.conversation.push({ role: 'user', content: message })
173
+ if (tone) {
174
+ this.conversation.push({ role: 'user', content: tone })
175
+ }
176
+ }
221
177
 
222
- setTimeout(function() {
223
- this.app.block.setTool(false);
224
- let inserted = insertion.insert({ html: html, target: $target, position: position, remove: remove });
225
- this.$prompt.remove();
226
- this.conversation = [];
178
+ let request = {
179
+ model: apimodel,
180
+ stream: this.opts.get('ai.text.stream'),
181
+ messages: this.conversation
182
+ }
227
183
 
228
- // broadcast
229
- this.app.broadcast('ai.insert', { nodes: inserted });
230
- }.bind(this), 3);
184
+ let size = '1024x1024'
185
+ if (this.promptType === 'image') {
186
+ size = this.$size.val()
187
+ }
231
188
 
232
- },
233
- stopPrompt(e, reply) {
234
- if (e) {
235
- e.preventDefault();
236
- e.stopPropagation();
237
-
238
- reply = this.$preview.text();
239
- if (this.isEvent) {
240
- this.isEvent.close();
241
- }
242
- }
189
+ request = (this.promptType === 'image') ? { model: apimodel, n: 1, size, prompt: message } : request
243
190
 
244
- this.$preview.css({ 'white-space': '' });
245
- this.$preview.html(this._parseReply(reply));
246
- this.$insert.show();
247
- this.$stop.hide();
248
- this.$generate.show();
249
-
250
- // broadcast
251
- let eventName = (e) ? 'ai.stop' : 'ai.complete';
252
- let prompt = this.$previewLabel.text();
253
- let result = (e) ? { prompt: prompt } : { prompt: prompt, response: this._parseReply(reply) };
254
- this.app.broadcast(eventName, result);
255
- },
256
- closePrompt(e) {
257
- e.preventDefault();
258
- e.stopPropagation();
191
+ // loading
192
+ this.$progress.html(this.defaults.spinner)
259
193
 
260
- this.app.block.setTool(false);
261
- this.$prompt.remove();
262
- this.conversation = [];
194
+ // send
195
+ if (this.promptType !== 'image' && this.opts.is('ai.text.stream')) {
196
+ this._sendStream(this.$preview, message, true)
197
+ } else {
198
+ this._sendPrompt(request, '_complete')
199
+ }
200
+ },
201
+ insertPrompt (e) {
202
+ e.preventDefault()
203
+ e.stopPropagation()
263
204
 
264
- if (this.isEvent) {
265
- this.isEvent.close();
266
- }
205
+ const insertion = this.app.create('insertion')
206
+ let html = this.$preview.html()
267
207
 
268
- // broadcast
269
- this.app.broadcast('ai.discard');
270
- },
208
+ if (this.promptType === 'image') {
209
+ const tag = this.opts.get('image.tag')
210
+ html = `<${tag}>${html}</${tag}>`
211
+ }
271
212
 
272
- // =private
213
+ // broadcast
214
+ const event = this.app.broadcast('ai.before.insert', { html })
215
+ html = event.get('html')
216
+
217
+ const $target = this.savedInstance ? this.savedInstance.getBlock() : this.$prompt
218
+ const position = this.savedInstance ? 'after' : 'before'
219
+ const remove = !this.savedInstance
220
+
221
+ setTimeout(function () {
222
+ this.app.block.setTool(false)
223
+ const inserted = insertion.insert({ html, target: $target, position, remove })
224
+ this.$prompt.remove()
225
+ this.conversation = []
226
+
227
+ // broadcast
228
+ this.app.broadcast('ai.insert', { nodes: inserted })
229
+ }.bind(this), 3)
230
+ },
231
+ stopPrompt (e, reply) {
232
+ if (e) {
233
+ e.preventDefault()
234
+ e.stopPropagation()
235
+
236
+ reply = this.$preview.text()
237
+ if (this.isEvent) {
238
+ this.isEvent.close()
239
+ }
240
+ }
273
241
 
274
- // get
275
- _getTone(message) {
276
- let tone = this.$select.val();
277
- if (tone === '0' || tone === '1') {
278
- return false;
279
- }
242
+ this.$preview.css({ 'white-space': '' })
243
+ this.$preview.html(this._parseReply(reply))
244
+ this.$insert.show()
245
+ this.$stop.hide()
246
+ this.$generate.show()
247
+
248
+ // broadcast
249
+ const eventName = (e) ? 'ai.stop' : 'ai.complete'
250
+ const prompt = this.$previewLabel.text()
251
+ const result = (e) ? { prompt } : { prompt, response: this._parseReply(reply) }
252
+ this.app.broadcast(eventName, result)
253
+ },
254
+ closePrompt (e) {
255
+ e.preventDefault()
256
+ e.stopPropagation()
257
+
258
+ this.app.block.setTool(false)
259
+ this.$prompt.remove()
260
+ this.conversation = []
261
+
262
+ if (this.isEvent) {
263
+ this.isEvent.close()
264
+ }
280
265
 
281
- return `${this.opts.get('ai.makeit') || this.defaults.makeit} ${tone}`;
282
- },
283
- _getHtml() {
284
- let html = '';
285
- let instances = this.app.blocks.get({ selected: true, instances: true });
286
- let instance = this.app.block.get();
266
+ // broadcast
267
+ this.app.broadcast('ai.discard')
268
+ },
287
269
 
288
- if (instances.length === 0 && instance) {
289
- instances = [instance];
290
- }
270
+ // =private
291
271
 
292
- for (let i = 0; i < instances.length; i++) {
293
- html = html + instances[i].getOuterHtml();
294
- }
272
+ // get
273
+ _getTone (message) {
274
+ const tone = this.$select.val()
275
+ if (tone === '0' || tone === '1') {
276
+ return false
277
+ }
295
278
 
296
- return html;
297
- },
298
- _getText() {
299
- let text = '';
300
- let instances = this.app.blocks.get({ selected: true, instances: true });
301
- let instance = this.app.block.get();
279
+ return `${this.opts.get('ai.makeit') || this.defaults.makeit} ${tone}`
280
+ },
281
+ _getHtml () {
282
+ let html = ''
283
+ let instances = this.app.blocks.get({ selected: true, instances: true })
284
+ const instance = this.app.block.get()
302
285
 
303
- if (instances.length === 0 && instance) {
304
- instances = [instance];
305
- }
286
+ if (instances.length === 0 && instance) {
287
+ instances = [instance]
288
+ }
306
289
 
307
- // normalize
308
- for (let i = 0; i < instances.length; i++) {
309
- if (instances[i].isEditable()) {
310
- let type = instances[i].getType(),
311
- prefix = (type === 'listitem') ? '- ' : '';
290
+ for (let i = 0; i < instances.length; i++) {
291
+ html = html + instances[i].getOuterHtml()
292
+ }
312
293
 
313
- text = text + prefix + instances[i].getPlainText() + '\n';
314
- }
315
- }
294
+ return html
295
+ },
296
+ _getText () {
297
+ let text = ''
298
+ let instances = this.app.blocks.get({ selected: true, instances: true })
299
+ const instance = this.app.block.get()
316
300
 
317
- text = text.trim();
318
- text = text.replace(/\n$/, '');
301
+ if (instances.length === 0 && instance) {
302
+ instances = [instance]
303
+ }
319
304
 
320
- return text;
321
- },
322
- _getMessage() {
323
- return this.$textarea.val().trim();
324
- },
325
- _getNode() {
326
- let node = this.app.block.create();
327
- let $node = node.getBlock();
305
+ // normalize
306
+ for (let i = 0; i < instances.length; i++) {
307
+ if (instances[i].isEditable()) {
308
+ const type = instances[i].getType()
309
+ const prefix = (type === 'listitem') ? '- ' : ''
328
310
 
329
- $node = this._buildNode($node, { traverse: true });
311
+ text = text + prefix + instances[i].getPlainText() + '\n'
312
+ }
313
+ }
330
314
 
331
- return $node;
332
- },
333
- _getInsertedNode() {
334
- let $node = this.dom('<div class="rx-inserted-node" style="white-space: pre-wrap;">');
315
+ text = text.trim()
316
+ text = text.replace(/\n$/, '')
335
317
 
336
- $node.html(this.opts.get('ai.spinner'));
337
- $node = this._buildNode($node, { traverse: false });
318
+ return text
319
+ },
320
+ _getMessage () {
321
+ return this.$textarea.val().trim()
322
+ },
323
+ _getNode () {
324
+ const node = this.app.block.create()
325
+ let $node = node.getBlock()
338
326
 
339
- return $node;
340
- },
327
+ $node = this._buildNode($node, { traverse: true })
341
328
 
342
- // set
343
- _setPrompt(text, html, prompt, empty) {
344
- if (text === '' && empty !== true) return;
329
+ return $node
330
+ },
331
+ _getInsertedNode () {
332
+ let $node = this.dom('<div class="rx-inserted-node" style="white-space: pre-wrap;">')
345
333
 
346
- this.promptType = 'text';
347
- this.promptText = text;
348
- this.promptHtml = html;
334
+ $node.html(this.opts.get('ai.spinner'))
335
+ $node = this._buildNode($node, { traverse: false })
349
336
 
350
- let messages = [{ "role": "user", "content": text }, { "role": "user", "content": prompt }];
351
- let request = {
352
- model: this.opts.get('ai.' + this.promptType + '.model'),
353
- messages: messages
354
- };
337
+ return $node
338
+ },
355
339
 
356
- // send
357
- if (this.opts.is('ai.text.stream')) {
358
- let $node = this._getInsertedNode();
359
- $node.html(this.defaults.spinner);
340
+ // set
341
+ _setPrompt (text, html, prompt, empty) {
342
+ if (text === '' && empty !== true) return
360
343
 
361
- this.app.dropdown.close();
362
- this.app.context.close();
344
+ this.promptType = 'text'
345
+ this.promptText = text
346
+ this.promptHtml = html
363
347
 
364
- this._sendStream($node, messages);
365
- }
366
- else {
367
- this._sendPrompt(request, '_insert');
368
- }
369
- },
348
+ const messages = [{ role: 'user', content: text }, { role: 'user', content: prompt }]
349
+ const request = {
350
+ model: this.opts.get('ai.' + this.promptType + '.model'),
351
+ messages
352
+ }
370
353
 
371
354
  // send
372
- _sendPrompt(request, complete) {
373
- let data = {
374
- url: this.opts.get('ai.' + this.promptType + '.endpoint'),
375
- data: JSON.stringify(request)
376
- };
377
-
378
- const utils = this.app.create('utils');
379
- data = utils.extendData(data, this.opts.get('ai.' + this.promptType + '.data'));
380
-
381
- this.ajax.request('post', {
382
- url: this.opts.get('ai.' + this.promptType + '.url'),
383
- data: data,
384
- before: function(xhr) {
385
- let event = this.app.broadcast('ai.before.send', { xhr: xhr, data: data });
386
- if (event.isStopped()) {
387
- return false;
388
- }
389
- }.bind(this),
390
- success: this[complete].bind(this),
391
- error: this._error.bind(this)
392
- });
393
- },
394
- _sendStream($node, message, preview) {
395
- const apimodel = this.opts.get('ai.' + this.promptType + '.model');
396
- const apiurl = this.opts.get('ai.' + this.promptType + '.endpoint');
397
- const serverurl = this.opts.get('ai.' + this.promptType + '.url');
398
-
399
- let request = {
400
- model: apimodel,
401
- stream: this.opts.get('ai.text.stream'),
402
- messages: (preview) ? this.conversation : message
403
- };
404
-
405
- let data = {
406
- url: apiurl,
407
- data: JSON.stringify(request)
408
- };
409
-
410
- const utils = this.app.create('utils');
411
- data = utils.extendData(data, this.opts.get('ai.' + this.promptType + '.data'));
355
+ if (this.opts.is('ai.text.stream')) {
356
+ const $node = this._getInsertedNode()
357
+ $node.html(this.defaults.spinner)
412
358
 
413
- let responseContent = '';
414
- let source = this._createSource(serverurl, data);
415
- this.isEvent = source;
416
- this.currentIndex = 0;
359
+ this.app.dropdown.close()
360
+ this.app.context.close()
417
361
 
418
- // win target
419
- let $target = this.app.scroll.getTarget();
420
-
421
- // node class
422
- $node.removeClass('rx-inserted-node-started');
423
-
424
- // on message
425
- source.addEventListener('message', function (event) {
426
- // hide ui
427
- this.app.dropdown.close();
428
- this.app.context.close();
362
+ this._sendStream($node, messages)
363
+ } else {
364
+ this._sendPrompt(request, '_insert')
365
+ }
366
+ },
429
367
 
430
- let message = event.data,
431
- start = message.indexOf(': ', 'data') + 2,
432
- data = message.slice(start, message.length);
368
+ // send
369
+ _sendPrompt (request, complete) {
370
+ let data = {
371
+ url: this.opts.get('ai.' + this.promptType + '.endpoint'),
372
+ data: JSON.stringify(request)
373
+ }
433
374
 
375
+ const utils = this.app.create('utils')
376
+ data = utils.extendData(data, this.opts.get('ai.' + this.promptType + '.data'))
434
377
 
435
- if (data === '[DONE]') {
436
- this._sendStreamDone(source, $node, responseContent, preview);
437
- }
438
- else {
439
- data = JSON.parse(data);
440
- if (data.notification) {
441
- this._sendStreamDone(source, $node, data.notification, preview);
442
- return;
443
- }
444
-
445
- const choices = data.choices;
446
- if (choices && choices.length > 0) {
447
- let content = choices[0].delta.content;
448
-
449
- if (content) {
450
- if (!$node.hasClass('rx-inserted-node-started')) {
451
- $node.html('');
452
- this._sendStreamPreviewSet(preview);
453
- }
454
-
455
- responseContent += content;
456
- this._typeCharacter($node, responseContent);
457
-
458
- // Auto-scroll to the bottom as new content is added
459
- if (this._isElementBottomBeyond($node.get())) {
460
- $node.get().scrollIntoView(false);
461
- $target.scrollTop($target.scrollTop() + 20);
462
-
463
- }
464
- $node.addClass('rx-inserted-node-started');
465
- }
466
- }
467
- }
468
- }.bind(this));
469
-
470
- // on error
471
- source.addEventListener('error', function(error) {
472
- this._error(error);
473
- source.close();
474
- this.isEvent = false;
475
- }.bind(this));
476
- },
477
- _sendStreamDone(source, $node, responseContent, preview) {
478
- if (!preview) {
479
- this._insertAfterNode($node, responseContent);
378
+ this.ajax.request('post', {
379
+ url: this.opts.get('ai.' + this.promptType + '.url'),
380
+ data,
381
+ before: function (xhr) {
382
+ const event = this.app.broadcast('ai.before.send', { xhr, data })
383
+ if (event.isStopped()) {
384
+ return false
480
385
  }
481
- else {
482
- let checkInterval = setInterval(function() {
483
- if (this.currentIndex === responseContent.length) {
484
- clearInterval(checkInterval);
485
- this.stopPrompt(false, responseContent);
486
- this.conversation.push({ "role": "assistant", "content": responseContent });
487
- }
488
-
489
- }.bind(this), 100);
490
- }
491
-
492
- source.close();
493
- this.isEvent = false;
494
- },
495
- _sendStreamPreviewSet(preview) {
496
- if (!preview) return;
497
-
498
- let value = this.modifiedValue || this.$textarea.val();
499
- this.$progress.html('');
500
- this.$previewLabel.html(this._sanitize(value));
501
- this.$textarea.val('');
502
- this.$preview.html('');
503
- this.$preview.css({ 'white-space': 'pre-wrap' });
504
- this.$generate.hide();
505
- this.$stop.show();
506
- },
507
-
508
- // build
509
- _buildNode($node, traverse) {
510
- let $last;
511
-
512
- this.instance = false;
513
- if (this.app.blocks.is()) {
514
- if (!this.app.editor.isSelectAll()) {
515
- $last = this.app.blocks.get({ first: true, selected: true });
516
- $last = $last.closest('[data-rx-first-level]');
517
- $last.before($node);
518
- }
386
+ }.bind(this),
387
+ success: this[complete].bind(this),
388
+ error: this._error.bind(this)
389
+ })
390
+ },
391
+ _sendStream ($node, message, preview) {
392
+ const apimodel = this.opts.get('ai.' + this.promptType + '.model')
393
+ const apiurl = this.opts.get('ai.' + this.promptType + '.endpoint')
394
+ const serverurl = this.opts.get('ai.' + this.promptType + '.url')
395
+
396
+ const request = {
397
+ model: apimodel,
398
+ stream: this.opts.get('ai.text.stream'),
399
+ messages: (preview) ? this.conversation : message
400
+ }
519
401
 
520
- let isAll = this.app.editor.isSelectAll();
521
- $last = this.app.blocks.removeAll();
522
- if (isAll) {
523
- $node = $last;
524
- }
525
- }
526
- else {
527
- this.instance = this.app.block.get();
528
- if (!this.instance) {
529
- this.instance = this.app.block.create();
530
- let $first = this.app.blocks.get({ first: true });
531
- $first.before(this.instance.getBlock());
532
- }
402
+ let data = {
403
+ url: apiurl,
404
+ data: JSON.stringify(request)
405
+ }
533
406
 
534
- if (this.instance.isType('listitem')) {
535
- this.instance.getBlock().html('').append($node);
536
- }
537
- else if (this.instance.isType('todoitem')) {
538
- this.instance.getContentItem().html('').append($node);
539
- }
540
- else {
541
- this.instance.getBlock().before($node);
542
- this.instance.remove(traverse);
543
- }
407
+ const utils = this.app.create('utils')
408
+ data = utils.extendData(data, this.opts.get('ai.' + this.promptType + '.data'))
409
+
410
+ let responseContent = ''
411
+ const source = this._createSource(serverurl, data)
412
+ this.isEvent = source
413
+ this.currentIndex = 0
414
+
415
+ // win target
416
+ const $target = this.app.scroll.getTarget()
417
+
418
+ // node class
419
+ $node.removeClass('rx-inserted-node-started')
420
+
421
+ // on message
422
+ source.addEventListener('message', function (event) {
423
+ // hide ui
424
+ this.app.dropdown.close()
425
+ this.app.context.close()
426
+
427
+ const message = event.data
428
+ const start = message.indexOf(': ', 'data') + 2
429
+ let data = message.slice(start, message.length)
430
+
431
+ if (data === '[DONE]') {
432
+ this._sendStreamDone(source, $node, responseContent, preview)
433
+ } else {
434
+ data = JSON.parse(data)
435
+ if (data.notification) {
436
+ this._sendStreamDone(source, $node, data.notification, preview)
437
+ return
544
438
  }
545
439
 
546
- return $node;
547
- },
548
- _buildPrompt(params) {
549
- this.savedInstance = false;
550
- this.conversation = [];
551
- this.$prompt = this._createPrompt(params);
552
- this.$textarea.on('input focus', function() {
553
- this.app.block.setTool('ai');
554
- }.bind(this));
555
-
556
- let instance = this.app.block.get();
557
- let isMultiple = this.app.blocks.is();
558
-
559
- // hide ui
560
- this.app.dropdown.close();
561
- this.app.context.close();
562
-
563
- if (instance || isMultiple) {
564
- if (isMultiple) {
565
- instance = this.app.blocks.get({ last: true, selected: true, instances: true });
566
- }
440
+ const choices = data.choices
441
+ if (choices && choices.length > 0) {
442
+ const content = choices[0].delta.content
567
443
 
568
- // find parent
569
- const types = ['layout', 'table', 'quote', 'list', 'todo', 'image', 'embed']
570
- let $parent = instance.getBlock().closest('[data-rx-type=' + types.join('],[data-rx-type=') + ']');
571
- let $column = instance.getBlock().closest('[data-rx-type=column]');
572
- if ($parent.length !== 0) {
573
- if ($column.length !== 0) {
574
- this.savedInstance = instance;
575
- }
576
- instance = $parent.dataget('instance');
444
+ if (content) {
445
+ if (!$node.hasClass('rx-inserted-node-started')) {
446
+ $node.html('')
447
+ this._sendStreamPreviewSet(preview)
577
448
  }
578
449
 
579
- // insert ai form
580
- this._insertPrompt(this.$prompt, instance);
450
+ responseContent += content
451
+ this._typeCharacter($node, responseContent)
581
452
 
582
- if (isMultiple) {
583
- this.app.blocks.unset();
453
+ // Auto-scroll to the bottom as new content is added
454
+ if (this._isElementBottomBeyond($node.get())) {
455
+ $node.get().scrollIntoView(false)
456
+ $target.scrollTop($target.scrollTop() + 20)
584
457
  }
458
+ $node.addClass('rx-inserted-node-started')
459
+ }
585
460
  }
586
- else {
587
- this._insertPrompt(this.$prompt);
588
- }
589
-
590
- // adjust height
591
- this.app.editor.adjustHeight();
592
- },
593
-
594
- // error
595
- _error(error, response) {
596
- let $node = this.app.editor.getEditor().find('.rx-inserted-node');
597
- if ($node.length !== 0) {
598
- // hide ui
599
- this.app.dropdown.close();
600
- this.app.context.close();
601
-
602
- let insertion = this.app.create('insertion');
603
- insertion.insert({ target: $node, remove: true, caret: 'end', html: this.promptHtml });
604
- }
605
-
606
- // hide loading
607
- if (this.$progress) {
608
- this.$progress.fadeOut(500, function() {
609
- this.$progress.html('').removeAttr('style');
610
- }.bind(this));
611
-
461
+ }
462
+ }.bind(this))
463
+
464
+ // on error
465
+ source.addEventListener('error', function (error) {
466
+ this._error(error)
467
+ source.close()
468
+ this.isEvent = false
469
+ }.bind(this))
470
+ },
471
+ _sendStreamDone (source, $node, responseContent, preview) {
472
+ if (!preview) {
473
+ this._insertAfterNode($node, responseContent)
474
+ } else {
475
+ const checkInterval = setInterval(function () {
476
+ if (this.currentIndex === responseContent.length) {
477
+ clearInterval(checkInterval)
478
+ this.stopPrompt(false, responseContent)
479
+ this.conversation.push({ role: 'assistant', content: responseContent })
612
480
  }
481
+ }.bind(this), 100)
482
+ }
613
483
 
614
- // broadcast
615
- this.app.broadcast('ai.error', error ? error : response);
616
- },
617
-
618
- // complete
619
- _insert(response) {
620
- this.promptButton.setIcon('');
621
-
622
- // error
623
- if (response.error) return this._error(response.error.message, response);
624
- if (!response.choices) return this._error(response);
625
-
626
- let reply = response.choices[0].message.content;
627
- let html = this._parseReply(reply);
628
- let insertion = this.app.create('insertion');
629
-
630
- // broadcast
631
- let event = this.app.broadcast('ai.before.insert', { html: html });
632
- html = event.get('html');
633
-
634
- // hide ui
635
- this.app.dropdown.close();
636
- this.app.context.close();
637
-
638
- let inserted;
639
- let instanceType = (this.instance && this.instance.isType(['listitem', 'todoitem']));
640
- if (instanceType) {
641
- this.instance.setContent(reply);
642
- inserted = this.instance.getBlock();
643
- }
644
- else {
645
- let $node = this._getNode();
646
- this.app.block.set($node);
647
- inserted = insertion.insert({ html: html, caret: 'end' });
648
-
649
- }
484
+ source.close()
485
+ this.isEvent = false
486
+ },
487
+ _sendStreamPreviewSet (preview) {
488
+ if (!preview) return
489
+
490
+ const value = this.modifiedValue || this.$textarea.val()
491
+ this.$progress.html('')
492
+ this.$previewLabel.html(this._sanitize(value))
493
+ this.$textarea.val('')
494
+ this.$preview.html('')
495
+ this.$preview.css({ 'white-space': 'pre-wrap' })
496
+ this.$generate.hide()
497
+ this.$stop.show()
498
+ },
499
+
500
+ // build
501
+ _buildNode ($node, traverse) {
502
+ let $last
503
+
504
+ this.instance = false
505
+ if (this.app.blocks.is()) {
506
+ if (!this.app.editor.isSelectAll()) {
507
+ $last = this.app.blocks.get({ first: true, selected: true })
508
+ $last = $last.closest('[data-rx-first-level]')
509
+ $last.before($node)
510
+ }
511
+
512
+ const isAll = this.app.editor.isSelectAll()
513
+ $last = this.app.blocks.removeAll()
514
+ if (isAll) {
515
+ $node = $last
516
+ }
517
+ } else {
518
+ this.instance = this.app.block.get()
519
+ if (!this.instance) {
520
+ this.instance = this.app.block.create()
521
+ const $first = this.app.blocks.get({ first: true })
522
+ $first.before(this.instance.getBlock())
523
+ }
524
+
525
+ if (this.instance.isType('listitem')) {
526
+ this.instance.getBlock().html('').append($node)
527
+ } else if (this.instance.isType('todoitem')) {
528
+ this.instance.getContentItem().html('').append($node)
529
+ } else {
530
+ this.instance.getBlock().before($node)
531
+ this.instance.remove(traverse)
532
+ }
533
+ }
650
534
 
651
- this.app.broadcast('ai.insert', { nodes: inserted });
652
- },
653
- _complete(response) {
654
- let reply;
655
- let html;
656
- let imageUrl;
657
- let value = this.modifiedValue || this.$textarea.val();
658
- let result;
659
-
660
- this.$progress.html('');
661
- this.$previewLabel.html('');
662
-
663
- // error
664
- if (response.error) return this._error(response.error.message, response);
665
- if (response.notification) {
666
- this.$preview.html(response.notification + '<br>');
667
- return;
535
+ return $node
536
+ },
537
+ _buildPrompt (params) {
538
+ this.savedInstance = false
539
+ this.conversation = []
540
+ this.$prompt = this._createPrompt(params)
541
+ this.$textarea.on('input focus', function () {
542
+ this.app.block.setTool('ai')
543
+ }.bind(this))
544
+
545
+ let instance = this.app.block.get()
546
+ const isMultiple = this.app.blocks.is()
547
+
548
+ // hide ui
549
+ this.app.dropdown.close()
550
+ this.app.context.close()
551
+
552
+ if (instance || isMultiple) {
553
+ if (isMultiple) {
554
+ instance = this.app.blocks.get({ last: true, selected: true, instances: true })
555
+ }
556
+
557
+ // find parent
558
+ const types = ['layout', 'table', 'quote', 'list', 'todo', 'image', 'embed']
559
+ const $parent = instance.getBlock().closest('[data-rx-type=' + types.join('],[data-rx-type=') + ']')
560
+ const $column = instance.getBlock().closest('[data-rx-type=column]')
561
+ if ($parent.length !== 0) {
562
+ if ($column.length !== 0) {
563
+ this.savedInstance = instance
668
564
  }
565
+ instance = $parent.dataget('instance')
566
+ }
669
567
 
670
- // current prompt
671
- this.$previewLabel.html(this._sanitize(value));
672
- this.$textarea.val('');
673
- this.$textarea.focus();
568
+ // insert ai form
569
+ this._insertPrompt(this.$prompt, instance)
674
570
 
675
- // complete
676
- if (this.promptType === 'text') {
677
- if (!response.choices) return this._error(response);
571
+ if (isMultiple) {
572
+ this.app.blocks.unset()
573
+ }
574
+ } else {
575
+ this._insertPrompt(this.$prompt)
576
+ }
678
577
 
679
- reply = response.choices[0].message.content;
680
- this.conversation.push({ role: "assistant", content: reply });
681
- html = this._parseReply(reply);
578
+ // adjust height
579
+ this.app.editor.adjustHeight()
580
+ },
682
581
 
683
- result = html;
684
- this.$preview.html(html);
685
- this.$insert.show();
582
+ // error
583
+ _error (error, response) {
584
+ const $node = this.app.editor.getEditor().find('.rx-inserted-node')
585
+ if ($node.length !== 0) {
586
+ // hide ui
587
+ this.app.dropdown.close()
588
+ this.app.context.close()
686
589
 
687
- // broadcast
688
- let prompt = this.$previewLabel.text();
689
- this.app.broadcast('ai.complete', { prompt: prompt, response: result });
590
+ const insertion = this.app.create('insertion')
591
+ insertion.insert({ target: $node, remove: true, caret: 'end', html: this.promptHtml })
592
+ }
690
593
 
691
- // adjust height
692
- this.app.editor.adjustHeight();
693
- }
694
- else if (this.promptType === 'image') {
695
- if (!response.data) return this._error(response);
696
-
697
- imageUrl = response.data[0].url;
698
- //html = response.data[0].revised_prompt;
699
-
700
- const saveUrl = this.opts.get('ai.image.save');
701
- if (saveUrl) {
702
- const utils = this.app.create('utils');
703
- let data = {
704
- url: imageUrl
705
- };
706
- data = utils.extendData(data, this.opts.get('ai.' + this.promptType + '.data'));
707
-
708
- // save request
709
- this.ajax.request('post', {
710
- url: saveUrl,
711
- data: data,
712
- before: function(xhr) {
713
- let event = this.app.broadcast('ai.before.save', { xhr: xhr, data: data });
714
- if (event.isStopped()) {
715
- return false;
716
- }
717
- }.bind(this),
718
- success: function(response) {
719
- this.app.broadcast('ai.save', response);
720
- this._completeImage(response.filename);
721
- }.bind(this),
722
- error: this._error.bind(this)
723
- });
724
- }
725
- else {
726
- this._completeImage(imageUrl);
727
- }
594
+ // hide loading
595
+ if (this.$progress) {
596
+ this.$progress.fadeOut(500, function () {
597
+ this.$progress.html('').removeAttr('style')
598
+ }.bind(this))
599
+ }
728
600
 
729
- }
730
- },
731
- _completeImage(imageUrl) {
732
- let $image = this.dom('<img>').attr('src', imageUrl);
733
- let result = $image.get().outerHTML;
734
- this.$preview.html($image);
735
- this.$insert.show();
736
-
737
- // broadcast
738
- let prompt = this.$previewLabel.text();
739
- this.app.broadcast('ai.complete', { prompt: prompt, response: result });
740
-
741
- // adjust height
742
- this.app.editor.adjustHeight();
743
- },
601
+ // broadcast
602
+ this.app.broadcast('ai.error', error || response)
603
+ },
744
604
 
745
- // parse
746
- _parseReply(reply) {
747
- let utils = this.app.create('utils');
748
- let cleaner = this.app.create('cleaner');
749
-
750
- let text = utils.parseMarkdown(reply);
751
- text = cleaner.store(text, 'lists');
752
- text = cleaner.store(text, 'headings');
753
- text = cleaner.store(text, 'images');
754
- text = cleaner.store(text, 'links');
755
-
756
- text = this._parseMarkdown(text);
757
- text = cleaner.restore(text, 'lists');
758
- text = cleaner.restore(text, 'headings');
759
- text = cleaner.restore(text, 'images');
760
- text = cleaner.restore(text, 'links');
761
-
762
- // clean up
763
- text = text.replace(/<p><(ul|ol)>/g, '<$1>');
764
- text = text.replace(/<\/(ul|ol)><\/p>/g, '</$1>');
765
- text = text.replace(/<p><\/p>/g, '');
766
-
767
- return text;
768
- },
605
+ // complete
606
+ _insert (response) {
607
+ this.promptButton.setIcon('')
769
608
 
770
- // create
771
- _createPrompt(params) {
772
- params = Redactor.extend(true, {}, { image: false }, params);
609
+ // error
610
+ if (response.error) return this._error(response.error.message, response)
611
+ if (!response.choices) return this._error(response)
612
+
613
+ const reply = response.choices[0].message.content
614
+ let html = this._parseReply(reply)
615
+ const insertion = this.app.create('insertion')
616
+
617
+ // broadcast
618
+ const event = this.app.broadcast('ai.before.insert', { html })
619
+ html = event.get('html')
620
+
621
+ // hide ui
622
+ this.app.dropdown.close()
623
+ this.app.context.close()
624
+
625
+ let inserted
626
+ const instanceType = (this.instance && this.instance.isType(['listitem', 'todoitem']))
627
+ if (instanceType) {
628
+ this.instance.setContent(reply)
629
+ inserted = this.instance.getBlock()
630
+ } else {
631
+ const $node = this._getNode()
632
+ this.app.block.set($node)
633
+ inserted = insertion.insert({ html, caret: 'end' })
634
+ }
773
635
 
774
- // type
775
- this.promptType = (params.image) ? 'image' : 'text';
636
+ this.app.broadcast('ai.insert', { nodes: inserted })
637
+ },
638
+ _complete (response) {
639
+ let reply
640
+ let html
641
+ let imageUrl
642
+ const value = this.modifiedValue || this.$textarea.val()
643
+ let result
776
644
 
777
- let placeholder = this.lang.get('ai.placeholder-' + this.promptType);
778
- let $editor = this.app.editor.getEditor();
779
- $editor.find('.rx-ai-main').remove();
645
+ this.$progress.html('')
646
+ this.$previewLabel.html('')
780
647
 
781
- let $main = this.dom('<div class="rx-in-tool rx-ai-main">').attr({ 'contenteditable': false });
782
- let $body = this.dom('<div class="rx-ai-body">');
783
- let $footer = this.dom('<div class="rx-ai-footer">');
784
- let $buttons = this.dom('<div class="rx-ai-buttons">');
648
+ // error
649
+ if (response.error) return this._error(response.error.message, response)
650
+ if (response.notification) {
651
+ this.$preview.html(response.notification + '<br>')
652
+ return
653
+ }
785
654
 
786
- this.$progress = this.dom('<div class="rx-ai-progress">');
787
- this.$previewLabel = this.dom('<div class="rx-ai-preview-label">');
788
- this.$preview = this.dom('<div class="rx-ai-preview">');
789
- this.$prompt = this.dom('<div class="rx-ai-prompt">');
790
- this.$label = this.dom('<label class="rx-ai-label">').html(this.lang.get('ai.prompt'));
791
- this.$textarea = this.dom('<textarea class="rx-ai-textarea rx-form-textarea">').attr({ 'placeholder': placeholder });
792
- this.$select = this.dom('<select class="rx-ai-select rx-form-select">');
793
- this.$size = this.dom('<select class="rx-ai-size rx-form-select">');
655
+ // current prompt
656
+ this.$previewLabel.html(this._sanitize(value))
657
+ this.$textarea.val('')
658
+ this.$textarea.focus()
794
659
 
795
- // footer
796
- this._createPromptFooter($footer, $buttons);
660
+ // complete
661
+ if (this.promptType === 'text') {
662
+ if (!response.choices) return this._error(response)
797
663
 
798
- this.$prompt.append(this.$label);
799
- this.$prompt.append(this.$textarea);
664
+ reply = response.choices[0].message.content
665
+ this.conversation.push({ role: 'assistant', content: reply })
666
+ html = this._parseReply(reply)
800
667
 
801
- $body.append(this.$progress);
802
- $body.append(this.$previewLabel);
803
- $body.append(this.$preview);
804
- $body.append(this.$prompt);
668
+ result = html
669
+ this.$preview.html(html)
670
+ this.$insert.show()
805
671
 
806
- $main.append($body);
807
- $main.append($footer);
672
+ // broadcast
673
+ const prompt = this.$previewLabel.text()
674
+ this.app.broadcast('ai.complete', { prompt, response: result })
808
675
 
809
- return $main;
810
- },
811
- _createPromptFooter($footer, $buttons) {
676
+ // adjust height
677
+ this.app.editor.adjustHeight()
678
+ } else if (this.promptType === 'image') {
679
+ if (!response.data) return this._error(response)
812
680
 
813
- this._createTone(this.$select);
814
- this._createSize(this.$size);
815
- this._createPromptButtons($buttons);
681
+ imageUrl = response.data[0].url
682
+ // html = response.data[0].revised_prompt;
816
683
 
817
- $footer.append(this.$select);
818
- if (this.promptType === 'image') {
819
- $footer.append(this.$size);
820
- }
821
- $footer.append($buttons);
822
- },
823
- _createPromptButton(label) {
824
- return this.dom('<button class="rx-ai-button rx-form-button">').html(label);
825
- },
826
- _createSize($size) {
827
- let items = this.opts.get('ai.size');
828
- for (let [key, name] of Object.entries(items)) {
829
- let $option = this.dom('<option>').val(key).html(name);
830
- $size.append($option);
831
- }
832
- },
833
- _createTone($select) {
834
- let items = (this.promptType === 'image') ? this.opts.get('ai.style') || this.defaults.style : this.opts.get('ai.tone') || this.defaults.tone;
835
- let name = (this.promptType === 'image') ? this.lang.get('ai.image-style') : this.lang.get('ai.change-tone');
836
- let $option = this.dom('<option>').val(0).html(name);
837
- $select.append($option);
838
-
839
- $option = this.dom('<option>').val(1).html('---');
840
- $select.append($option);
841
-
842
- for (let i = 0; i < items.length; i++) {
843
- name = this.lang.parse(items[i]);
844
- $option = this.dom('<option>').val(name).html(name);
845
- $select.append($option);
684
+ const saveUrl = this.opts.get('ai.image.save')
685
+ if (saveUrl) {
686
+ const utils = this.app.create('utils')
687
+ let data = {
688
+ url: imageUrl
846
689
  }
847
- },
848
- _createPromptButtons($buttons) {
849
- this.$generate = this._createPromptButton(this.lang.get('ai.send')).addClass('rx-form-button-primary').on('click.rx-ai', this.sendPrompt.bind(this));
850
- this.$stop = this._createPromptButton(this.lang.get('ai.stop')).addClass('rx-form-button-primary').on('click.rx-ai', this.stopPrompt.bind(this)).hide();
851
- this.$discard = this._createPromptButton(this.lang.get('ai.discard')).addClass('rx-form-button-danger').on('click.rx-ai', this.closePrompt.bind(this));
852
- this.$insert = this._createPromptButton(this.lang.get('ai.insert')).on('click.rx-ai', this.insertPrompt.bind(this)).hide();
853
-
690
+ data = utils.extendData(data, this.opts.get('ai.' + this.promptType + '.data'))
854
691
 
855
- $buttons.append(this.$discard);
856
- $buttons.append(this.$insert);
857
- $buttons.append(this.$stop);
858
- $buttons.append(this.$generate);
859
- },
860
- _createSource(url, data) {
861
- const eventTarget = new EventTarget();
862
-
863
- let ajax = this.ajax.post({
864
- url: url,
865
- data: data,
866
- before: function(xhr) {
867
- let event = this.app.broadcast('ai.before.send', { xhr: xhr, data: data });
868
- if (event.isStopped()) {
869
- return false;
870
- }
871
- }.bind(this)
872
- });
873
- let xhr = ajax.xhr;
874
- let that = this;
875
-
876
- var ongoing = false, start = 0;
877
- xhr.onprogress = function() {
878
- if (!ongoing) {
879
- ongoing = true;
880
- eventTarget.dispatchEvent(new Event('open', {
881
- status: xhr.status,
882
- headers: xhr.getAllResponseHeaders(),
883
- url: xhr.responseUrl,
884
- }));
885
- }
886
-
887
- let i, chunk;
888
-
889
- // error
890
- if (that._isJsonString(xhr.responseText)) {
891
- let response = JSON.parse(xhr.responseText);
892
- if (response.error) {
893
- that._error(response.error.message, response);
894
- eventTarget.close();
895
- return;
896
- }
897
- }
898
-
899
- // chunk
900
- while ((i = xhr.responseText.indexOf('\n\n', start)) >= 0) {
901
- chunk = xhr.responseText.slice(start, i);
902
-
903
- start = i + 2;
904
- if (chunk.length) {
905
- eventTarget.dispatchEvent(new MessageEvent('message', {data: chunk}));
906
- }
692
+ // save request
693
+ this.ajax.request('post', {
694
+ url: saveUrl,
695
+ data,
696
+ before: function (xhr) {
697
+ const event = this.app.broadcast('ai.before.save', { xhr, data })
698
+ if (event.isStopped()) {
699
+ return false
907
700
  }
908
- };
909
-
910
- // close func
911
- eventTarget.close = function() {
912
- xhr.abort();
913
- };
914
-
915
- return eventTarget;
916
- },
917
-
918
- // is
919
- _isElementBottomBeyond(element) {
920
- let $target = this.app.scroll.getTarget(),
921
- rect = element.getBoundingClientRect(),
922
- elementBottom = rect.top + rect.height;
923
-
924
- return elementBottom > $target.get().innerHeight;
925
- },
926
- _isJsonString(str) {
927
- try {
928
- JSON.parse(str);
929
- } catch (e) {
930
- return false;
931
- }
932
- return true;
933
- },
934
-
935
- // insert
936
- _insertAfterNode($tmp, content) {
937
- let inserted;
938
- let instanceType = (this.instance && this.instance.isType(['listitem', 'todoitem']));
939
- // broadcast
940
- let event = this.app.broadcast('ai.before.insert', { html: content });
941
- content = event.get('html');
942
-
943
- if (instanceType) {
944
- this.instance.setContent(content);
945
- inserted = this.instance.getBlock();
701
+ }.bind(this),
702
+ success: function (response) {
703
+ this.app.broadcast('ai.save', response)
704
+ this._completeImage(response.filename)
705
+ }.bind(this),
706
+ error: this._error.bind(this)
707
+ })
708
+ } else {
709
+ this._completeImage(imageUrl)
710
+ }
711
+ }
712
+ },
713
+ _completeImage (imageUrl) {
714
+ const $image = this.dom('<img>').attr('src', imageUrl)
715
+ const result = $image.get().outerHTML
716
+ this.$preview.html($image)
717
+ this.$insert.show()
718
+
719
+ // broadcast
720
+ const prompt = this.$previewLabel.text()
721
+ this.app.broadcast('ai.complete', { prompt, response: result })
722
+
723
+ // adjust height
724
+ this.app.editor.adjustHeight()
725
+ },
726
+
727
+ // parse
728
+ _parseReply (reply) {
729
+ const utils = this.app.create('utils')
730
+ const cleaner = this.app.create('cleaner')
731
+
732
+ let text = utils.parseMarkdown(reply)
733
+ text = cleaner.store(text, 'lists')
734
+ text = cleaner.store(text, 'headings')
735
+ text = cleaner.store(text, 'images')
736
+ text = cleaner.store(text, 'links')
737
+
738
+ text = this._parseMarkdown(text)
739
+ text = cleaner.restore(text, 'lists')
740
+ text = cleaner.restore(text, 'headings')
741
+ text = cleaner.restore(text, 'images')
742
+ text = cleaner.restore(text, 'links')
743
+
744
+ // clean up
745
+ text = text.replace(/<p><(ul|ol)>/g, '<$1>')
746
+ text = text.replace(/<\/(ul|ol)><\/p>/g, '</$1>')
747
+ text = text.replace(/<p><\/p>/g, '')
748
+
749
+ return text
750
+ },
751
+
752
+ // create
753
+ _createPrompt (params) {
754
+ params = Redactor.extend(true, {}, { image: false }, params)
755
+
756
+ // type
757
+ this.promptType = (params.image) ? 'image' : 'text'
758
+
759
+ const placeholder = this.lang.get('ai.placeholder-' + this.promptType)
760
+ const $editor = this.app.editor.getEditor()
761
+ $editor.find('.rx-ai-main').remove()
762
+
763
+ const $main = this.dom('<div class="rx-in-tool rx-ai-main">').attr({ contenteditable: false })
764
+ const $body = this.dom('<div class="rx-ai-body">')
765
+ const $footer = this.dom('<div class="rx-ai-footer">')
766
+ const $buttons = this.dom('<div class="rx-ai-buttons">')
767
+
768
+ this.$progress = this.dom('<div class="rx-ai-progress">')
769
+ this.$previewLabel = this.dom('<div class="rx-ai-preview-label">')
770
+ this.$preview = this.dom('<div class="rx-ai-preview">')
771
+ this.$prompt = this.dom('<div class="rx-ai-prompt">')
772
+ this.$label = this.dom('<label class="rx-ai-label">').html(this.lang.get('ai.prompt'))
773
+ this.$textarea = this.dom('<textarea class="rx-ai-textarea rx-form-textarea">').attr({ placeholder })
774
+ this.$select = this.dom('<select class="rx-ai-select rx-form-select">')
775
+ this.$size = this.dom('<select class="rx-ai-size rx-form-select">')
776
+
777
+ // footer
778
+ this._createPromptFooter($footer, $buttons)
779
+
780
+ this.$prompt.append(this.$label)
781
+ this.$prompt.append(this.$textarea)
782
+
783
+ $body.append(this.$progress)
784
+ $body.append(this.$previewLabel)
785
+ $body.append(this.$preview)
786
+ $body.append(this.$prompt)
787
+
788
+ $main.append($body)
789
+ $main.append($footer)
790
+
791
+ return $main
792
+ },
793
+ _createPromptFooter ($footer, $buttons) {
794
+ this._createTone(this.$select)
795
+ this._createSize(this.$size)
796
+ this._createPromptButtons($buttons)
797
+
798
+ $footer.append(this.$select)
799
+ if (this.promptType === 'image') {
800
+ $footer.append(this.$size)
801
+ }
802
+ $footer.append($buttons)
803
+ },
804
+ _createPromptButton (label) {
805
+ return this.dom('<button class="rx-ai-button rx-form-button">').html(label)
806
+ },
807
+ _createSize ($size) {
808
+ const items = this.opts.get('ai.size')
809
+ for (const [key, name] of Object.entries(items)) {
810
+ const $option = this.dom('<option>').val(key).html(name)
811
+ $size.append($option)
812
+ }
813
+ },
814
+ _createTone ($select) {
815
+ const items = (this.promptType === 'image') ? this.opts.get('ai.style') || this.defaults.style : this.opts.get('ai.tone') || this.defaults.tone
816
+ let name = (this.promptType === 'image') ? this.lang.get('ai.image-style') : this.lang.get('ai.change-tone')
817
+ let $option = this.dom('<option>').val(0).html(name)
818
+ $select.append($option)
819
+
820
+ $option = this.dom('<option>').val(1).html('---')
821
+ $select.append($option)
822
+
823
+ for (let i = 0; i < items.length; i++) {
824
+ name = this.lang.parse(items[i])
825
+ $option = this.dom('<option>').val(name).html(name)
826
+ $select.append($option)
827
+ }
828
+ },
829
+ _createPromptButtons ($buttons) {
830
+ this.$generate = this._createPromptButton(this.lang.get('ai.send')).addClass('rx-form-button-primary').on('click.rx-ai', this.sendPrompt.bind(this))
831
+ this.$stop = this._createPromptButton(this.lang.get('ai.stop')).addClass('rx-form-button-primary').on('click.rx-ai', this.stopPrompt.bind(this)).hide()
832
+ this.$discard = this._createPromptButton(this.lang.get('ai.discard')).addClass('rx-form-button-danger').on('click.rx-ai', this.closePrompt.bind(this))
833
+ this.$insert = this._createPromptButton(this.lang.get('ai.insert')).on('click.rx-ai', this.insertPrompt.bind(this)).hide()
834
+
835
+ $buttons.append(this.$discard)
836
+ $buttons.append(this.$insert)
837
+ $buttons.append(this.$stop)
838
+ $buttons.append(this.$generate)
839
+ },
840
+ _createSource (url, data) {
841
+ const eventTarget = new EventTarget()
842
+
843
+ const ajax = this.ajax.post({
844
+ url,
845
+ data,
846
+ before: function (xhr) {
847
+ const event = this.app.broadcast('ai.before.send', { xhr, data })
848
+ if (event.isStopped()) {
849
+ return false
946
850
  }
947
- else {
948
- let insertion = this.app.create('insertion');
949
- let node = this.app.block.create();
950
- let $node = node.getBlock();
951
-
952
- // parse
953
- content = this._parseReply(content);
954
-
955
- $tmp.after($node);
956
- $tmp.remove();
957
- this.app.block.set($node);
958
- inserted = insertion.insert({ html: content, caret: 'end' });
851
+ }.bind(this)
852
+ })
853
+ const xhr = ajax.xhr
854
+ const that = this
855
+
856
+ let ongoing = false; let start = 0
857
+ xhr.onprogress = function () {
858
+ if (!ongoing) {
859
+ ongoing = true
860
+ eventTarget.dispatchEvent(new Event('open', {
861
+ status: xhr.status,
862
+ headers: xhr.getAllResponseHeaders(),
863
+ url: xhr.responseUrl
864
+ }))
865
+ }
866
+
867
+ let i, chunk
868
+
869
+ // error
870
+ if (that._isJsonString(xhr.responseText)) {
871
+ const response = JSON.parse(xhr.responseText)
872
+ if (response.error) {
873
+ that._error(response.error.message, response)
874
+ eventTarget.close()
875
+ return
959
876
  }
877
+ }
960
878
 
961
- // broadcast
962
- this.app.broadcast('ai.insert', { nodes: inserted });
963
- },
964
- _insertPrompt($prompt, current, params) {
965
- let elm = this.app.create('element');
966
- let position = 'after';
967
-
968
- // position
969
- if (!current) {
970
- if (this.opts.get('addPosition') === 'top') {
971
- current = this.app.blocks.get({ first: true, instances: true });
972
- position = 'before';
973
- }
974
- else {
975
- current = this.app.blocks.get({ last: true, instances: true });
976
- position = 'after';
977
- }
978
- }
879
+ // chunk
880
+ while ((i = xhr.responseText.indexOf('\n\n', start)) >= 0) {
881
+ chunk = xhr.responseText.slice(start, i)
979
882
 
980
- // insert
981
- let $current = current.getBlock();
982
- $current[position]($prompt);
883
+ start = i + 2
884
+ if (chunk.length) {
885
+ eventTarget.dispatchEvent(new MessageEvent('message', { data: chunk }))
886
+ }
887
+ }
888
+ }
983
889
 
984
- // scroll
985
- elm.scrollTo($prompt);
890
+ // close func
891
+ eventTarget.close = function () {
892
+ xhr.abort()
893
+ }
986
894
 
987
- // build
988
- this.app.observer.observeUnset();
989
- this.$textarea.focus();
990
- this.$textarea.on('input.rx-ai-autoresize keyup.rx-ai-autoreize', this._resize.bind(this));
991
- this.$textarea.on('keydown.rx-ai-event', this._promptKeydown.bind(this));
992
- },
895
+ return eventTarget
896
+ },
897
+
898
+ // is
899
+ _isElementBottomBeyond (element) {
900
+ const $target = this.app.scroll.getTarget()
901
+ const rect = element.getBoundingClientRect()
902
+ const elementBottom = rect.top + rect.height
903
+
904
+ return elementBottom > $target.get().innerHeight
905
+ },
906
+ _isJsonString (str) {
907
+ try {
908
+ JSON.parse(str)
909
+ } catch (e) {
910
+ return false
911
+ }
912
+ return true
913
+ },
914
+
915
+ // insert
916
+ _insertAfterNode ($tmp, content) {
917
+ let inserted
918
+ const instanceType = (this.instance && this.instance.isType(['listitem', 'todoitem']))
919
+ // broadcast
920
+ const event = this.app.broadcast('ai.before.insert', { html: content })
921
+ content = event.get('html')
922
+
923
+ if (instanceType) {
924
+ this.instance.setContent(content)
925
+ inserted = this.instance.getBlock()
926
+ } else {
927
+ const insertion = this.app.create('insertion')
928
+ const node = this.app.block.create()
929
+ const $node = node.getBlock()
930
+
931
+ // parse
932
+ content = this._parseReply(content)
933
+
934
+ $tmp.after($node)
935
+ $tmp.remove()
936
+ this.app.block.set($node)
937
+ inserted = insertion.insert({ html: content, caret: 'end' })
938
+ }
993
939
 
994
- // other
995
- _promptKeydown(e) {
996
- if (e.key === 'Enter' && !e.shiftKey) {
997
- e.preventDefault();
998
- this.sendPrompt(e);
999
- }
1000
- },
1001
- _typeCharacter($node, responseContent) {
1002
- if (this.currentIndex < responseContent.length) {
1003
- $node.get().textContent += responseContent.charAt(this.currentIndex);
1004
- this.currentIndex++;
1005
- // adjust height
1006
- this.app.editor.adjustHeight();
1007
- setTimeout(function() {
1008
- this._typeCharacter($node, responseContent);
1009
- }.bind(this), 100);
1010
- }
1011
- },
1012
- _resize() {
1013
- this.$textarea.css('height', 'auto');
1014
- this.$textarea.css('height', this.$textarea.get().scrollHeight + 'px');
940
+ // broadcast
941
+ this.app.broadcast('ai.insert', { nodes: inserted })
942
+ },
943
+ _insertPrompt ($prompt, current, params) {
944
+ const elm = this.app.create('element')
945
+ let position = 'after'
946
+
947
+ // position
948
+ if (!current) {
949
+ if (this.opts.get('addPosition') === 'top') {
950
+ current = this.app.blocks.get({ first: true, instances: true })
951
+ position = 'before'
952
+ } else {
953
+ current = this.app.blocks.get({ last: true, instances: true })
954
+ position = 'after'
955
+ }
956
+ }
1015
957
 
1016
- // adjust height
1017
- this.app.editor.adjustHeight();
1018
- },
1019
- _sanitize(str) {
1020
- return str.replace(/[&<>"']/g, char => {
1021
- switch (char) {
1022
- case '&': return '&amp;';
1023
- case '<': return '&lt;';
1024
- case '>': return '&gt;';
1025
- case '"': return '&quot;';
1026
- case "'": return '&#39;';
1027
- }
1028
- });
1029
- },
1030
- _parseMarkdown(markdown) {
1031
- let inCodeBlock = false;
1032
- let result = '';
1033
- const lines = markdown.split('\n');
1034
- const tags = this.opts.get('replaceTags');
1035
-
1036
- // code parsing
1037
- for (const line of lines) {
1038
- if (line.startsWith('```')) {
1039
- inCodeBlock = !inCodeBlock;
1040
- result += inCodeBlock ? this._replaceCodeLine(line) : '</code></pre>';
1041
- }
1042
- else if (line.startsWith('####_')) {
1043
- result += line;
1044
- }
1045
- else {
1046
- result += inCodeBlock ? this._escapeHtml(line) + '\n' : `<p>${this._escapeHtml(line)}</p>`;
1047
- }
1048
- }
958
+ // insert
959
+ const $current = current.getBlock()
960
+ $current[position]($prompt)
1049
961
 
1050
- // bold/italic parsing
1051
- let bTag = (tags.b) ? tags.b : 'b';
1052
- let iTag = (tags.i) ? tags.i : 'i';
962
+ // scroll
963
+ elm.scrollTo($prompt)
1053
964
 
1054
- result = result.replace(/\*\*\_(.*?)\_\*\*/g, '<' + bTag + '><' + iTag + '>$1</' + iTag + '></' + bTag + '>');
1055
- result = result.replace(/\*\*(.*?)\*\*/g, '<' + bTag + '>$1</' + bTag + '>');
1056
- result = result.replace(/\*(.*?)\*/g, '<' + iTag + '>$1</' + iTag + '>');
965
+ // build
966
+ this.app.observer.observeUnset()
967
+ this.$textarea.focus()
968
+ this.$textarea.on('input.rx-ai-autoresize keyup.rx-ai-autoreize', this._resize.bind(this))
969
+ this.$textarea.on('keydown.rx-ai-event', this._promptKeydown.bind(this))
970
+ },
971
+
972
+ // other
973
+ _promptKeydown (e) {
974
+ if (e.key === 'Enter' && !e.shiftKey) {
975
+ e.preventDefault()
976
+ this.sendPrompt(e)
977
+ }
978
+ },
979
+ _typeCharacter ($node, responseContent) {
980
+ if (this.currentIndex < responseContent.length) {
981
+ $node.get().textContent += responseContent.charAt(this.currentIndex)
982
+ this.currentIndex++
983
+ // adjust height
984
+ this.app.editor.adjustHeight()
985
+ setTimeout(function () {
986
+ this._typeCharacter($node, responseContent)
987
+ }.bind(this), 100)
988
+ }
989
+ },
990
+ _resize () {
991
+ this.$textarea.css('height', 'auto')
992
+ this.$textarea.css('height', this.$textarea.get().scrollHeight + 'px')
993
+
994
+ // adjust height
995
+ this.app.editor.adjustHeight()
996
+ },
997
+ _sanitize (str) {
998
+ return str.replace(/[&<>"']/g, char => {
999
+ switch (char) {
1000
+ case '&': return '&amp;'
1001
+ case '<': return '&lt;'
1002
+ case '>': return '&gt;'
1003
+ case '"': return '&quot;'
1004
+ case "'": return '&#39;'
1005
+ }
1006
+ })
1007
+ },
1008
+ _parseMarkdown (markdown) {
1009
+ let inCodeBlock = false
1010
+ let result = ''
1011
+ const lines = markdown.split('\n')
1012
+ const tags = this.opts.get('replaceTags')
1013
+
1014
+ // code parsing
1015
+ for (const line of lines) {
1016
+ if (line.startsWith('```')) {
1017
+ inCodeBlock = !inCodeBlock
1018
+ result += inCodeBlock ? this._replaceCodeLine(line) : '</code></pre>'
1019
+ } else if (line.startsWith('####_')) {
1020
+ result += line
1021
+ } else {
1022
+ result += inCodeBlock ? this._escapeHtml(line) + '\n' : `<p>${this._escapeHtml(line)}</p>`
1023
+ }
1024
+ }
1057
1025
 
1058
- return result;
1059
- },
1060
- _replaceCodeLine(line) {
1061
- return line.replace(/\`\`\`(([^\s]+))?/gm, function(match, p1, p2) {
1062
- let classAttribute = p2 ? ' class="' + p2 + '"' : '';
1063
- return '<pre' + classAttribute + '><code>';
1064
- });
1065
- },
1066
- _escapeHtml(text) {
1067
- const htmlEntities = {
1068
- '&': '&amp;',
1069
- '<': '&lt;',
1070
- '>': '&gt;',
1071
- '"': '&quot;',
1072
- "'": '&#39;',
1073
- };
1074
-
1075
- return text.replace(/[&<>"']/g, (match) => htmlEntities[match]);
1026
+ // bold/italic parsing
1027
+ const bTag = (tags.b) ? tags.b : 'b'
1028
+ const iTag = (tags.i) ? tags.i : 'i'
1029
+
1030
+ result = result.replace(/\*\*\_(.*?)\_\*\*/g, '<' + bTag + '><' + iTag + '>$1</' + iTag + '></' + bTag + '>')
1031
+ result = result.replace(/\*\*(.*?)\*\*/g, '<' + bTag + '>$1</' + bTag + '>')
1032
+ result = result.replace(/\*(.*?)\*/g, '<' + iTag + '>$1</' + iTag + '>')
1033
+
1034
+ return result
1035
+ },
1036
+ _replaceCodeLine (line) {
1037
+ return line.replace(/\`\`\`(([^\s]+))?/gm, function (match, p1, p2) {
1038
+ const classAttribute = p2 ? ' class="' + p2 + '"' : ''
1039
+ return '<pre' + classAttribute + '><code>'
1040
+ })
1041
+ },
1042
+ _escapeHtml (text) {
1043
+ const htmlEntities = {
1044
+ '&': '&amp;',
1045
+ '<': '&lt;',
1046
+ '>': '&gt;',
1047
+ '"': '&quot;',
1048
+ "'": '&#39;'
1076
1049
  }
1077
- });
1050
+
1051
+ return text.replace(/[&<>"']/g, (match) => htmlEntities[match])
1052
+ }
1053
+ })