json-object-editor 0.10.610 → 0.10.623

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.
package/js/joe-ai.js CHANGED
@@ -408,6 +408,288 @@
408
408
  }
409
409
  customElements.define('joe-object', JoeObject);
410
410
 
411
+ // ---------- Joe AI Widget: embeddable chat for any site ----------
412
+ class JoeAIWidget extends HTMLElement {
413
+ constructor() {
414
+ super();
415
+ this.attachShadow({ mode: 'open' });
416
+ this.endpoint = this.getAttribute('endpoint') || ''; // base URL to JOE; default same origin
417
+ this.conversation_id = this.getAttribute('conversation_id') || null;
418
+ this.assistant_id = this.getAttribute('assistant_id') || null;
419
+ this.ai_assistant_id = this.getAttribute('ai_assistant_id') || null;
420
+ this.model = this.getAttribute('model') || null;
421
+ this.messages = [];
422
+ this._ui = {};
423
+ }
424
+
425
+ connectedCallback() {
426
+ this.renderShell();
427
+ if (this.conversation_id) {
428
+ this.loadHistory();
429
+ } else {
430
+ this.startConversation();
431
+ }
432
+ }
433
+
434
+ get apiBase() {
435
+ return this.endpoint || '';
436
+ }
437
+
438
+ renderShell() {
439
+ const style = document.createElement('style');
440
+ style.textContent = `
441
+ :host {
442
+ font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
443
+ display: block;
444
+ max-width: 420px;
445
+ border: 1px solid #ddd;
446
+ border-radius: 8px;
447
+ box-shadow: 0 2px 6px rgba(0,0,0,0.08);
448
+ overflow: hidden;
449
+ background: #fff;
450
+ }
451
+ .wrapper {
452
+ display: flex;
453
+ flex-direction: column;
454
+ height: 100%;
455
+ }
456
+ .header {
457
+ padding: 8px 12px;
458
+ background: #1f2933;
459
+ color: #f9fafb;
460
+ font-size: 13px;
461
+ display: flex;
462
+ align-items: center;
463
+ justify-content: space-between;
464
+ }
465
+ .title {
466
+ font-weight: 600;
467
+ }
468
+ .status {
469
+ font-size: 11px;
470
+ opacity: 0.8;
471
+ }
472
+ .messages {
473
+ padding: 10px;
474
+ height: 260px;
475
+ overflow-y: auto;
476
+ background: #f5f7fa;
477
+ font-size: 13px;
478
+ }
479
+ .msg {
480
+ margin-bottom: 8px;
481
+ max-width: 90%;
482
+ clear: both;
483
+ }
484
+ .msg.user {
485
+ text-align: right;
486
+ margin-left: auto;
487
+ }
488
+ .bubble {
489
+ display: inline-block;
490
+ padding: 6px 8px;
491
+ border-radius: 10px;
492
+ line-height: 1.4;
493
+ }
494
+ .user .bubble {
495
+ background: #2563eb;
496
+ color: #fff;
497
+ }
498
+ .assistant .bubble {
499
+ background: #e5e7eb;
500
+ color: #111827;
501
+ }
502
+ .footer {
503
+ border-top: 1px solid #e5e7eb;
504
+ padding: 6px;
505
+ display: flex;
506
+ gap: 6px;
507
+ align-items: center;
508
+ }
509
+ textarea {
510
+ flex: 1;
511
+ resize: none;
512
+ border-radius: 6px;
513
+ border: 1px solid #d1d5db;
514
+ padding: 6px 8px;
515
+ font-size: 13px;
516
+ min-height: 34px;
517
+ max-height: 80px;
518
+ }
519
+ button {
520
+ border-radius: 6px;
521
+ border: none;
522
+ background: #2563eb;
523
+ color: #fff;
524
+ padding: 6px 10px;
525
+ font-size: 13px;
526
+ cursor: pointer;
527
+ white-space: nowrap;
528
+ }
529
+ button:disabled {
530
+ opacity: 0.6;
531
+ cursor: default;
532
+ }
533
+ `;
534
+
535
+ const wrapper = document.createElement('div');
536
+ wrapper.className = 'wrapper';
537
+ wrapper.innerHTML = `
538
+ <div class="header">
539
+ <div class="title">${this.getAttribute('title') || 'AI Assistant'}</div>
540
+ <div class="status" id="status">connecting…</div>
541
+ </div>
542
+ <div class="messages" id="messages"></div>
543
+ <div class="footer">
544
+ <textarea id="input" placeholder="${this.getAttribute('placeholder') || 'Ask me anything…'}"></textarea>
545
+ <button id="send">Send</button>
546
+ </div>
547
+ `;
548
+
549
+ this.shadowRoot.innerHTML = '';
550
+ this.shadowRoot.appendChild(style);
551
+ this.shadowRoot.appendChild(wrapper);
552
+
553
+ this._ui.messages = this.shadowRoot.getElementById('messages');
554
+ this._ui.status = this.shadowRoot.getElementById('status');
555
+ this._ui.input = this.shadowRoot.getElementById('input');
556
+ this._ui.send = this.shadowRoot.getElementById('send');
557
+
558
+ this._ui.send.addEventListener('click', () => this.sendMessage());
559
+ this._ui.input.addEventListener('keydown', (e) => {
560
+ if (e.key === 'Enter' && !e.shiftKey) {
561
+ e.preventDefault();
562
+ this.sendMessage();
563
+ }
564
+ });
565
+ }
566
+
567
+ setStatus(text) {
568
+ if (this._ui.status) this._ui.status.textContent = text;
569
+ }
570
+
571
+ renderMessages() {
572
+ if (!this._ui.messages) return;
573
+ this._ui.messages.innerHTML = (this.messages || []).map(m => {
574
+ const role = m.role || 'assistant';
575
+ const safe = (m.content || '').replace(/</g, '&lt;').replace(/>/g, '&gt;');
576
+ return `<div class="msg ${role}"><div class="bubble">${safe}</div></div>`;
577
+ }).join('');
578
+ this._ui.messages.scrollTop = this._ui.messages.scrollHeight;
579
+ }
580
+
581
+ async startConversation() {
582
+ try {
583
+ this.setStatus('connecting…');
584
+ const payload = {
585
+ model: this.model || undefined,
586
+ ai_assistant_id: this.getAttribute('ai_assistant_id') || undefined,
587
+ source: this.getAttribute('source') || 'widget'
588
+ };
589
+ const resp = await fetch(this.apiBase + '/API/plugin/chatgpt/widgetStart', {
590
+ method: 'POST',
591
+ headers: { 'Content-Type': 'application/json' },
592
+ body: JSON.stringify(payload)
593
+ }).then(r => r.json());
594
+
595
+ if (!resp || resp.success !== true) {
596
+ const msg = (resp && resp.error) || 'Failed to start conversation';
597
+ this.setStatus('error: ' + msg);
598
+ this.messages.push({ role:'assistant', content:'[Error starting conversation: '+msg+']', created_at:new Date().toISOString() });
599
+ this.renderMessages();
600
+ console.error('widgetStart error', resp);
601
+ return;
602
+ }
603
+ this.conversation_id = resp.conversation_id;
604
+ this.setAttribute('conversation_id', this.conversation_id);
605
+ this.assistant_id = resp.assistant_id || this.assistant_id;
606
+ this.model = resp.model || this.model;
607
+ this.messages = [];
608
+ this.renderMessages();
609
+ this.setStatus('online');
610
+ } catch (e) {
611
+ console.error('widgetStart exception', e);
612
+ this.setStatus('error');
613
+ }
614
+ }
615
+
616
+ async loadHistory() {
617
+ try {
618
+ this.setStatus('loading…');
619
+ const resp = await fetch(this.apiBase + '/API/plugin/chatgpt/widgetHistory?conversation_id=' + encodeURIComponent(this.conversation_id))
620
+ .then(r => r.json());
621
+ if (!resp || resp.success !== true) {
622
+ this.setStatus('online');
623
+ return;
624
+ }
625
+ this.assistant_id = resp.assistant_id || this.assistant_id;
626
+ this.model = resp.model || this.model;
627
+ this.messages = resp.messages || [];
628
+ this.renderMessages();
629
+ this.setStatus('online');
630
+ } catch (e) {
631
+ console.error('widgetHistory exception', e);
632
+ this.setStatus('online');
633
+ }
634
+ }
635
+
636
+ async sendMessage() {
637
+ const input = this._ui.input;
638
+ if (!input) return;
639
+ const text = input.value.trim();
640
+ if (!text) return;
641
+ if (!this.conversation_id) {
642
+ await this.startConversation();
643
+ if (!this.conversation_id) return;
644
+ }
645
+ input.value = '';
646
+ this._ui.send.disabled = true;
647
+
648
+ const userMsg = { role: 'user', content: text, created_at: new Date().toISOString() };
649
+ this.messages.push(userMsg);
650
+ this.renderMessages();
651
+ this.setStatus('thinking…');
652
+
653
+ try {
654
+ const payload = {
655
+ conversation_id: this.conversation_id,
656
+ content: text,
657
+ role: 'user',
658
+ assistant_id: this.assistant_id || undefined,
659
+ model: this.model || undefined
660
+ };
661
+ const resp = await fetch(this.apiBase + '/API/plugin/chatgpt/widgetMessage', {
662
+ method: 'POST',
663
+ headers: { 'Content-Type': 'application/json' },
664
+ body: JSON.stringify(payload)
665
+ }).then(r => r.json());
666
+
667
+ if (!resp || resp.success !== true) {
668
+ const msg = (resp && resp.error) || 'Failed to send message';
669
+ console.error('widgetMessage error', resp);
670
+ this.messages.push({ role:'assistant', content:'[Error: '+msg+']', created_at:new Date().toISOString() });
671
+ this.renderMessages();
672
+ this.setStatus('error: ' + msg);
673
+ } else {
674
+ this.assistant_id = resp.assistant_id || this.assistant_id;
675
+ this.model = resp.model || this.model;
676
+ this.messages = resp.messages || this.messages;
677
+ this.renderMessages();
678
+ this.setStatus('online');
679
+ }
680
+ } catch (e) {
681
+ console.error('widgetMessage exception', e);
682
+ this.setStatus('error');
683
+ } finally {
684
+ this._ui.send.disabled = false;
685
+ }
686
+ }
687
+ }
688
+
689
+ if (!customElements.get('joe-ai-widget')) {
690
+ customElements.define('joe-ai-widget', JoeAIWidget);
691
+ }
692
+
411
693
 
412
694
  //**YES**
413
695
  Ai.spawnChatHelper = async function(object_id,user_id=_joe.User._id,conversation_id) {
@@ -620,6 +902,123 @@
620
902
  console.error("❌ injectSystemMessage failed:", err);
621
903
  }
622
904
  };
905
+
906
+ // ---------- Autofill (Completions) ----------
907
+ // Usage in schema button: onclick: "_joe.Ai.populateField(this, { prompt: 'Write a short summary', saveHistory: true })"
908
+ Ai.populateField = async function(dom, options = {}){
909
+ try{
910
+ const obj = _joe.current && _joe.current.object;
911
+ const schema = _joe.current && _joe.current.schema && _joe.current.schema.__schemaname;
912
+ if(!obj || !schema){ return alert('No current object/schema found'); }
913
+
914
+ const parentField = $(dom).parents('.joe-object-field');
915
+ const inferredField = parentField.data('name');
916
+ const fields = (options.fields && options.fields.length) ? options.fields : (inferredField ? [inferredField] : []);
917
+ if(!fields.length){ return alert('No target field detected. Pass options.fields or place the button inside a field.'); }
918
+
919
+ // UI feedback
920
+ const originalHtml = dom.innerHTML;
921
+ dom.disabled = true;
922
+ dom.innerHTML = (options.loadingLabel || 'Thinking...');
923
+
924
+ const payload = {
925
+ object_id: obj._id,
926
+ schema: schema,
927
+ fields: fields,
928
+ prompt: options.prompt || '',
929
+ assistant_id: options.assistant_id || undefined,
930
+ allow_web: !!options.allowWeb,
931
+ save_history: !!options.saveHistory,
932
+ save_itemtype: options.saveItemtype || undefined,
933
+ model: options.model || undefined
934
+ };
935
+
936
+ const resp = await fetch('/API/plugin/chatgpt/autofill', {
937
+ method: 'POST',
938
+ headers: { 'Content-Type': 'application/json' },
939
+ body: JSON.stringify(payload)
940
+ }).then(r => r.json());
941
+
942
+ if(!resp || resp.success !== true){
943
+ console.error('AI autofill error:', resp && resp.error);
944
+ alert('AI autofill failed' + (resp && resp.error ? (': ' + resp.error) : ''));
945
+ }else{
946
+ const patch = resp.patch || {};
947
+ Ai.applyAutofillPatch(patch);
948
+ if(options.autoSave === true){
949
+ _joe.updateObject(null, null, true, null, options.skipValidation === true);
950
+ }
951
+ if(options.openChatAfter === true){
952
+ _joe.Ai.spawnChatHelper(obj._id);
953
+ }
954
+ }
955
+
956
+ dom.disabled = false;
957
+ dom.innerHTML = originalHtml;
958
+ }catch(e){
959
+ console.error('populateField error', e);
960
+ alert('Error running AI autofill');
961
+ try{ dom.disabled = false; }catch(_e){}
962
+ }
963
+ };
964
+
965
+ Ai.applyAutofillPatch = function(patch = {}){
966
+ Object.keys(patch).forEach(function(fname){
967
+ Ai._setFieldValue(fname, patch[fname]);
968
+ });
969
+ };
970
+
971
+ Ai._setFieldValue = function(fieldName, value){
972
+ const $container = $(`.joe-object-field[data-name="${fieldName}"]`);
973
+ if(!$container.length){ return false; }
974
+ const $el = $container.find('.joe-field').eq(0);
975
+ if(!$el.length){ return false; }
976
+
977
+ const ftype = $el.data('ftype');
978
+ try{
979
+ switch(ftype){
980
+ case 'tinymce': {
981
+ const editorId = $el.data('texteditor_id');
982
+ const ed = window.tinymce && editorId ? window.tinymce.get(editorId) : null;
983
+ if(ed){ ed.setContent(value || ''); }
984
+ else { $el.val(value || ''); }
985
+ break;
986
+ }
987
+ case 'ckeditor': {
988
+ const editorId = $el.data('ckeditor_id');
989
+ const ed = (window.CKEDITOR && CKEDITOR.instances) ? CKEDITOR.instances[editorId] : null;
990
+ if(ed){ ed.setData(value || ''); }
991
+ else { $el.val(value || ''); }
992
+ break;
993
+ }
994
+ case 'ace': {
995
+ const aceId = $el.data('ace_id');
996
+ const ed = (_joe && _joe.ace_editors) ? _joe.ace_editors[aceId] : null;
997
+ if(ed){ ed.setValue(value || '', -1); }
998
+ else { $el.val(value || ''); }
999
+ break;
1000
+ }
1001
+ default: {
1002
+ if($el.is('select')){
1003
+ $el.val(value);
1004
+ }else if($el.is('input,textarea')){
1005
+ $el.val(value);
1006
+ }else{
1007
+ // Fallback: try innerText/innerHTML for custom components
1008
+ $el.text(typeof value === 'string' ? value : JSON.stringify(value));
1009
+ }
1010
+ $el.trigger('input');
1011
+ $el.trigger('change');
1012
+ break;
1013
+ }
1014
+ }
1015
+ $container.addClass('changed');
1016
+ return true;
1017
+ }catch(e){
1018
+ console.warn('Failed to set field', fieldName, e);
1019
+ return false;
1020
+ }
1021
+ };
623
1022
 
624
1023
 
625
1024
  // Attach AI to _joe
package/js/joe.js CHANGED
@@ -1,6 +1,6 @@
1
1
  /* --------------------------------------------------------
2
2
  *
3
- * json-object-editor - v0.10.610
3
+ * json-object-editor - v0.10.623
4
4
  * Created by: Corey Hadden
5
5
  *
6
6
  * -------------------------------------------------------- */
@@ -9702,10 +9702,11 @@ logit(intent)
9702
9702
  var field = data.field;
9703
9703
  var bucket = field.bucketname || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9704
9704
  if(!bucket){
9705
- callback(err,file.base64);
9705
+ var errMsg = 'AWS bucket not configured';
9706
+ callback && callback(errMsg);
9706
9707
  return;
9707
9708
  }
9708
- var filename = file.filename;
9709
+ var filename = file.name;
9709
9710
  var directory = _joe.current.object._id;
9710
9711
  var url = (field && self.propAsFuncOrValue(field.server_url)) ||
9711
9712
  (location.port && '//'+__jsc.hostname+':'+__jsc.port+'/API/plugin/awsConnect/') ||
@@ -9720,72 +9721,45 @@ logit(intent)
9720
9721
  Key:Key,
9721
9722
  base64:file.base64,
9722
9723
  extension:file.extension,
9723
- ACL:'public-read'
9724
+ contentType:file.type,
9725
+ ACL:(field.hasOwnProperty('ACL') ? field.ACL : 'public-read')
9724
9726
  },
9725
9727
  success:function(response){
9726
9728
  var url,filename;
9727
9729
  if(!response.error){
9728
9730
  logit(response);
9729
- var bucket = response.bucket || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9730
- var region = (_joe.Utils.Settings('AWS_S3CONFIG') ||{}).region;
9731
- // var prefix = 'https://s3.'+((region && region +'.')||'')+'amazonaws.com/'+bucket+'/';
9732
- // var url = prefix+response.Key;
9733
- var prefix = (_joe.Data.setting.where({name:'AWS_S3PREFIX'})[0] || {value:''}).value || 'https://s3.'+((region && region +'.')||'')+'amazonaws.com/'+bucket+'/';
9734
- var url = prefix+response.Key;
9731
+ var url = response.url;
9732
+ if(!url){
9733
+ var bucket = response.bucket || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9734
+ var region = (_joe.Utils.Settings('AWS_S3CONFIG') ||{}).region;
9735
+ var prefix = (_joe.Data.setting.where({name:'AWS_S3PREFIX'})[0] || {value:''}).value || 'https://'+bucket+'.s3.'+((region)||'')+'.amazonaws.com/';
9736
+ url = prefix+response.Key;
9737
+ }
9735
9738
  var filename = response.Key.replace(self.current.object._id+'/','');
9736
- //TODO: change file status in list.
9737
-
9738
-
9739
- }
9740
-
9741
- callback(response.error,url,filename);
9742
- }
9743
- })
9744
- },
9745
- awsConnect:function(data,callback){
9746
- var bucket = data.field.bucketname || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9747
- if(!bucket){
9748
- callback(err,data.base64);
9749
- return;
9750
- }
9751
- var filename = _joe.current.object._id+data.extension;
9752
- var directory = _joe.current.object.itemtype;
9753
- var url = (field && self.propAsFuncOrValue(field.server_url)) ||
9754
- (location.port && '//'+__jsc.hostname+':'+__jsc.port+'/API/plugin/awsConnect/') ||
9755
- '//'+__jsc.hostname+'/API/plugin/awsConnect/';
9756
- var Key = directory+'/'+filename;
9757
- var field = data.field;
9758
- $.ajax({
9759
- url:url,
9760
- type:'POST',
9761
- data:{
9762
- Key:Key,
9763
- base64:data.base64,
9764
- extension:data.extension,
9765
- ACL:'public-read'
9766
- },
9767
- success:function(data){
9768
- if(!data.error){
9769
- logit(data);
9770
- var bucket = data.bucket || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9771
-
9772
- //var prefix = 'https://s3.amazonaws.com/'+bucket+'/';
9773
- var prefix = 'https://'+bucket+'.s3.amazonaws.com/';
9774
-
9775
- var url = prefix+data.Key;
9776
9739
  if(field.url_field){
9777
9740
  var nprop = {};
9778
9741
  _joe.current.object[field.url_field] = url;
9779
9742
  nprop[field.url_field] = url;
9780
- /*$('.joe-field[name="'+field.url_field+'"]').val(url)*/
9781
9743
  _joe.Fields.rerender(field.url_field,nprop);
9782
9744
  }
9783
- callback(null,url);
9784
- }else{
9785
- callback(data.error);
9745
+
9746
+
9786
9747
  }
9748
+
9749
+ callback(response.error,url,filename);
9750
+ },
9751
+ error:function(xhr,status,err){
9752
+ var message = (xhr && (xhr.responseJSON && (xhr.responseJSON.error || xhr.responseJSON.code) || xhr.responseText)) || err || status || 'Upload failed';
9753
+ callback && callback(message);
9787
9754
  }
9788
9755
  })
9756
+ },
9757
+ awsConnect:function(data,callback){
9758
+ // Backward-compat wrapper: delegate to awsFileUpload
9759
+ return _joe.SERVER.Plugins.awsFileUpload({
9760
+ file: { base64:data.base64, extension:data.extension, type:data.contentType, name: (_joe.current.object._id+data.extension) },
9761
+ field: data.field
9762
+ }, callback);
9789
9763
  }
9790
9764
  },
9791
9765
  ready:{