json-object-editor 0.10.610 → 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/CHANGELOG.md +8 -0
- package/_www/mcp-nav.js +1 -0
- package/app.js +26 -2
- package/css/joe-styles.css +1 -0
- package/css/joe.css +1 -0
- package/css/joe.min.css +1 -1
- package/js/JsonObjectEditor.jquery.craydent.js +27 -53
- package/js/joe-ai.js +399 -0
- package/js/joe.js +46 -70
- package/package.json +2 -2
- package/readme.md +139 -2
- package/server/app-config.js +2 -1
- package/server/fields/core.js +35 -17
- package/server/modules/Server.js +50 -0
- package/server/modules/Storage.js +7 -1
- package/server/modules/Utils.js +2 -1
- package/server/plugins/awsConnect.js +32 -6
- package/server/plugins/chatgpt-assistants.js +3 -3
- package/server/plugins/chatgpt-responses.js +45 -0
- package/server/plugins/chatgpt.js +729 -0
- package/server/schemas/ai_assistant.js +255 -0
- package/server/schemas/ai_conversation.js +34 -0
- package/server/schemas/ai_prompt.js +323 -0
- package/server/schemas/ai_response.js +217 -0
- package/server/schemas/ai_tool.js +30 -0
- package/server/schemas/ai_widget_conversation.js +110 -0
- package/server/schemas/task.js +5 -0
- package/server/webconfig.js +3 -1
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, '<').replace(/>/g, '>');
|
|
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
|