json-object-editor 0.10.521 → 0.10.622

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.443
3
+ * json-object-editor - v0.10.610
4
4
  * Created by: Corey Hadden
5
5
  *
6
6
  * -------------------------------------------------------- */
@@ -4859,8 +4859,9 @@ this.renderHTMLContent = function(specs){
4859
4859
  $(this).removeClass('editing');
4860
4860
  })
4861
4861
  if(!stopEditing){
4862
- $(templateDom).parents('.joe-objectlist-row').addClass('editing');
4863
- $(templateDom).parents('.joe-objectlist-row').find('.joe-text-field,.joe-integer-field,.joe-number-field,select,textarea').eq(0)[0].focus();
4862
+ var $row = $(templateDom).parents('.joe-objectlist-row');
4863
+ $row.addClass('editing');
4864
+ $row.find('.joe-text-field,.joe-integer-field,.joe-number-field,select,textarea').eq(0)[0].focus();
4864
4865
  }
4865
4866
  }
4866
4867
 
@@ -8255,12 +8256,28 @@ Field Rendering Helpers
8255
8256
  //find all tds
8256
8257
  var ol_array_obj = {};
8257
8258
  $(this).find('td').each(function(i){
8258
- subInput = $(this).find('input,select,textarea');
8259
- subVal = subInput.val();
8260
- switch(subInput.attr('type')){
8261
- case 'checkbox':
8262
- subVal = subInput.is(':checked');
8263
- break;
8259
+ var $cell = $(this);
8260
+ var $groupCheckboxes = $cell.find('.joe-group-item-checkbox input[type="checkbox"]');
8261
+ var $radios = $cell.find('input[type="radio"]');
8262
+ var $checkboxes = $cell.find('input[type="checkbox"]');
8263
+
8264
+ if ($groupCheckboxes.length) {
8265
+ // Checkbox group inside objectList: collect checked values as an array
8266
+ subVal = $groupCheckboxes.filter(':checked').map(function(){ return $(this).val(); }).get();
8267
+ } else if ($radios.length) {
8268
+ // Single-select group (radio): get selected value or null
8269
+ var r = $radios.filter(':checked').val();
8270
+ subVal = (typeof r === 'undefined') ? null : r;
8271
+ } else if ($checkboxes.length === 1) {
8272
+ // Single boolean checkbox
8273
+ subVal = $checkboxes.is(':checked');
8274
+ } else if ($checkboxes.length > 1) {
8275
+ // Multiple checkboxes without explicit group wrapper: treat as array
8276
+ subVal = $checkboxes.filter(':checked').map(function(){ return $(this).val(); }).get();
8277
+ } else {
8278
+ // Fallback for inputs/selects/textarea
8279
+ subInput = $cell.find('input,select,textarea');
8280
+ subVal = subInput.val();
8264
8281
  }
8265
8282
  ol_array_obj[subprops[i]] = subVal;
8266
8283
  });
@@ -9685,7 +9702,8 @@ logit(intent)
9685
9702
  var field = data.field;
9686
9703
  var bucket = field.bucketname || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9687
9704
  if(!bucket){
9688
- callback(err,file.base64);
9705
+ var errMsg = 'AWS bucket not configured';
9706
+ callback && callback(errMsg);
9689
9707
  return;
9690
9708
  }
9691
9709
  var filename = file.filename;
@@ -9703,72 +9721,47 @@ logit(intent)
9703
9721
  Key:Key,
9704
9722
  base64:file.base64,
9705
9723
  extension:file.extension,
9706
- ACL:'public-read'
9724
+ contentType:file.type,
9725
+ ACL:(field.hasOwnProperty('ACL') ? field.ACL : 'public-read')
9707
9726
  },
9708
9727
  success:function(response){
9709
9728
  var url,filename;
9710
9729
  if(!response.error){
9711
9730
  logit(response);
9712
- var bucket = response.bucket || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9713
- var region = (_joe.Utils.Settings('AWS_S3CONFIG') ||{}).region;
9714
- // var prefix = 'https://s3.'+((region && region +'.')||'')+'amazonaws.com/'+bucket+'/';
9715
- // var url = prefix+response.Key;
9716
- var prefix = (_joe.Data.setting.where({name:'AWS_S3PREFIX'})[0] || {value:''}).value || 'https://s3.'+((region && region +'.')||'')+'amazonaws.com/'+bucket+'/';
9717
- var url = prefix+response.Key;
9731
+ // Prefer server-returned URL; fallback kept for legacy
9732
+ var url = response.url;
9733
+ if(!url){
9734
+ var bucket = response.bucket || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9735
+ var region = (_joe.Utils.Settings('AWS_S3CONFIG') ||{}).region;
9736
+ var prefix = (_joe.Data.setting.where({name:'AWS_S3PREFIX'})[0] || {value:''}).value || 'https://'+bucket+'.s3.'+((region)||'')+'.amazonaws.com/';
9737
+ url = prefix+response.Key;
9738
+ }
9718
9739
  var filename = response.Key.replace(self.current.object._id+'/','');
9719
- //TODO: change file status in list.
9720
-
9721
-
9722
- }
9723
-
9724
- callback(response.error,url,filename);
9725
- }
9726
- })
9727
- },
9728
- awsConnect:function(data,callback){
9729
- var bucket = data.field.bucketname || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9730
- if(!bucket){
9731
- callback(err,data.base64);
9732
- return;
9733
- }
9734
- var filename = _joe.current.object._id+data.extension;
9735
- var directory = _joe.current.object.itemtype;
9736
- var url = (field && self.propAsFuncOrValue(field.server_url)) ||
9737
- (location.port && '//'+__jsc.hostname+':'+__jsc.port+'/API/plugin/awsConnect/') ||
9738
- '//'+__jsc.hostname+'/API/plugin/awsConnect/';
9739
- var Key = directory+'/'+filename;
9740
- var field = data.field;
9741
- $.ajax({
9742
- url:url,
9743
- type:'POST',
9744
- data:{
9745
- Key:Key,
9746
- base64:data.base64,
9747
- extension:data.extension,
9748
- ACL:'public-read'
9749
- },
9750
- success:function(data){
9751
- if(!data.error){
9752
- logit(data);
9753
- var bucket = data.bucket || (_joe.Data.setting.where({name:'AWS_BUCKETNAME'})[0] || {value:''}).value;
9754
-
9755
- //var prefix = 'https://s3.amazonaws.com/'+bucket+'/';
9756
- var prefix = 'https://'+bucket+'.s3.amazonaws.com/';
9757
-
9758
- var url = prefix+data.Key;
9740
+ // If a url_field is specified, set it and rerender
9759
9741
  if(field.url_field){
9760
9742
  var nprop = {};
9761
9743
  _joe.current.object[field.url_field] = url;
9762
9744
  nprop[field.url_field] = url;
9763
- /*$('.joe-field[name="'+field.url_field+'"]').val(url)*/
9764
9745
  _joe.Fields.rerender(field.url_field,nprop);
9765
9746
  }
9766
- callback(null,url);
9767
- }else{
9768
- callback(data.error);
9747
+
9748
+
9769
9749
  }
9750
+
9751
+ callback(response.error,url,filename);
9752
+ },
9753
+ error:function(xhr,status,err){
9754
+ var message = (xhr && (xhr.responseJSON && (xhr.responseJSON.error || xhr.responseJSON.code) || xhr.responseText)) || err || status || 'Upload failed';
9755
+ callback && callback(message);
9770
9756
  }
9771
9757
  })
9758
+ },
9759
+ awsConnect:function(data,callback){
9760
+ // Backward-compat wrapper: delegate to awsFileUpload
9761
+ return _joe.SERVER.Plugins.awsFileUpload({
9762
+ file: { base64:data.base64, extension:data.extension, type:data.contentType, filename: (_joe.current.object._id+data.extension) },
9763
+ field: data.field
9764
+ }, callback);
9772
9765
  }
9773
9766
  },
9774
9767
  ready:{