formstrap 0.4.7 → 0.4.9

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.
@@ -0,0 +1,1077 @@
1
+ /*jshint esversion: 6 */
2
+ 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 });
106
+ },
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);
118
+ },
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);
155
+ },
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;
190
+
191
+ // loading
192
+ this.$progress.html(this.defaults.spinner);
193
+
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();
205
+
206
+ let insertion = this.app.create('insertion');
207
+ let html = this.$preview.html();
208
+
209
+ if (this.promptType === 'image') {
210
+ const tag = this.opts.get('image.tag');
211
+ html = `<${tag}>${html}</${tag}>`;
212
+ }
213
+
214
+ // broadcast
215
+ let event = this.app.broadcast('ai.before.insert', { html: html });
216
+ html = event.get('html');
217
+
218
+ let $target = this.savedInstance ? this.savedInstance.getBlock() : this.$prompt;
219
+ let position = this.savedInstance ? 'after' : 'before';
220
+ let remove = this.savedInstance ? false : true;
221
+
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 = [];
227
+
228
+ // broadcast
229
+ this.app.broadcast('ai.insert', { nodes: inserted });
230
+ }.bind(this), 3);
231
+
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
+ }
243
+
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();
259
+
260
+ this.app.block.setTool(false);
261
+ this.$prompt.remove();
262
+ this.conversation = [];
263
+
264
+ if (this.isEvent) {
265
+ this.isEvent.close();
266
+ }
267
+
268
+ // broadcast
269
+ this.app.broadcast('ai.discard');
270
+ },
271
+
272
+ // =private
273
+
274
+ // get
275
+ _getTone(message) {
276
+ let tone = this.$select.val();
277
+ if (tone === '0' || tone === '1') {
278
+ return false;
279
+ }
280
+
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();
287
+
288
+ if (instances.length === 0 && instance) {
289
+ instances = [instance];
290
+ }
291
+
292
+ for (let i = 0; i < instances.length; i++) {
293
+ html = html + instances[i].getOuterHtml();
294
+ }
295
+
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();
302
+
303
+ if (instances.length === 0 && instance) {
304
+ instances = [instance];
305
+ }
306
+
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') ? '- ' : '';
312
+
313
+ text = text + prefix + instances[i].getPlainText() + '\n';
314
+ }
315
+ }
316
+
317
+ text = text.trim();
318
+ text = text.replace(/\n$/, '');
319
+
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();
328
+
329
+ $node = this._buildNode($node, { traverse: true });
330
+
331
+ return $node;
332
+ },
333
+ _getInsertedNode() {
334
+ let $node = this.dom('<div class="rx-inserted-node" style="white-space: pre-wrap;">');
335
+
336
+ $node.html(this.opts.get('ai.spinner'));
337
+ $node = this._buildNode($node, { traverse: false });
338
+
339
+ return $node;
340
+ },
341
+
342
+ // set
343
+ _setPrompt(text, html, prompt, empty) {
344
+ if (text === '' && empty !== true) return;
345
+
346
+ this.promptType = 'text';
347
+ this.promptText = text;
348
+ this.promptHtml = html;
349
+
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
+ };
355
+
356
+ // send
357
+ if (this.opts.is('ai.text.stream')) {
358
+ let $node = this._getInsertedNode();
359
+ $node.html(this.defaults.spinner);
360
+
361
+ this.app.dropdown.close();
362
+ this.app.context.close();
363
+
364
+ this._sendStream($node, messages);
365
+ }
366
+ else {
367
+ this._sendPrompt(request, '_insert');
368
+ }
369
+ },
370
+
371
+ // 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'));
412
+
413
+ let responseContent = '';
414
+ let source = this._createSource(serverurl, data);
415
+ this.isEvent = source;
416
+ this.currentIndex = 0;
417
+
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();
429
+
430
+ let message = event.data,
431
+ start = message.indexOf(': ', 'data') + 2,
432
+ data = message.slice(start, message.length);
433
+
434
+
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);
480
+ }
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
+ }
519
+
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
+ }
533
+
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
+ }
544
+ }
545
+
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
+ }
567
+
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');
577
+ }
578
+
579
+ // insert ai form
580
+ this._insertPrompt(this.$prompt, instance);
581
+
582
+ if (isMultiple) {
583
+ this.app.blocks.unset();
584
+ }
585
+ }
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
+
612
+ }
613
+
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
+ }
650
+
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;
668
+ }
669
+
670
+ // current prompt
671
+ this.$previewLabel.html(this._sanitize(value));
672
+ this.$textarea.val('');
673
+ this.$textarea.focus();
674
+
675
+ // complete
676
+ if (this.promptType === 'text') {
677
+ if (!response.choices) return this._error(response);
678
+
679
+ reply = response.choices[0].message.content;
680
+ this.conversation.push({ role: "assistant", content: reply });
681
+ html = this._parseReply(reply);
682
+
683
+ result = html;
684
+ this.$preview.html(html);
685
+ this.$insert.show();
686
+
687
+ // broadcast
688
+ let prompt = this.$previewLabel.text();
689
+ this.app.broadcast('ai.complete', { prompt: prompt, response: result });
690
+
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
+ }
728
+
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
+ },
744
+
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
+ },
769
+
770
+ // create
771
+ _createPrompt(params) {
772
+ params = Redactor.extend(true, {}, { image: false }, params);
773
+
774
+ // type
775
+ this.promptType = (params.image) ? 'image' : 'text';
776
+
777
+ let placeholder = this.lang.get('ai.placeholder-' + this.promptType);
778
+ let $editor = this.app.editor.getEditor();
779
+ $editor.find('.rx-ai-main').remove();
780
+
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">');
785
+
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">');
794
+
795
+ // footer
796
+ this._createPromptFooter($footer, $buttons);
797
+
798
+ this.$prompt.append(this.$label);
799
+ this.$prompt.append(this.$textarea);
800
+
801
+ $body.append(this.$progress);
802
+ $body.append(this.$previewLabel);
803
+ $body.append(this.$preview);
804
+ $body.append(this.$prompt);
805
+
806
+ $main.append($body);
807
+ $main.append($footer);
808
+
809
+ return $main;
810
+ },
811
+ _createPromptFooter($footer, $buttons) {
812
+
813
+ this._createTone(this.$select);
814
+ this._createSize(this.$size);
815
+ this._createPromptButtons($buttons);
816
+
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);
846
+ }
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
+
854
+
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
+ }
907
+ }
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();
946
+ }
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' });
959
+ }
960
+
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
+ }
979
+
980
+ // insert
981
+ let $current = current.getBlock();
982
+ $current[position]($prompt);
983
+
984
+ // scroll
985
+ elm.scrollTo($prompt);
986
+
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
+ },
993
+
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');
1015
+
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
+ }
1049
+
1050
+ // bold/italic parsing
1051
+ let bTag = (tags.b) ? tags.b : 'b';
1052
+ let iTag = (tags.i) ? tags.i : 'i';
1053
+
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 + '>');
1057
+
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]);
1076
+ }
1077
+ });