json-object-editor 0.10.625 ā 0.10.633
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 +6 -0
- package/_www/ai-widget-test.html +367 -0
- package/_www/mcp-test.html +10 -1
- package/css/joe-styles.css +11 -3
- package/css/joe.css +12 -4
- package/css/joe.min.css +1 -1
- package/docs/joe_agent_custom_gpt_instructions_v_3.md +9 -0
- package/dummy +10 -0
- package/img/svgs/ai_assistant.svg +1 -0
- package/img/svgs/ai_assistant_white.svg +1 -0
- package/js/JsonObjectEditor.jquery.craydent.js +34 -3
- package/js/joe-ai.js +784 -52
- package/js/joe.js +52 -21
- package/js/joe.min.js +1 -1
- package/package.json +1 -1
- package/readme.md +8 -1
- package/server/apps/aihub.js +97 -0
- package/server/fields/core.js +4 -1
- package/server/modules/MCP.js +233 -2
- package/server/modules/Server.js +1 -46
- package/server/plugins/auth.js +34 -30
- package/server/plugins/chatgpt-assistants.js +70 -35
- package/server/plugins/chatgpt.js +560 -44
- package/server/schemas/ai_assistant.js +149 -1
- package/server/schemas/ai_conversation.js +14 -1
- package/server/schemas/ai_widget_conversation.js +133 -14
- package/server/schemas/project.js +27 -3
- package/server/schemas/task.js +6 -3
package/js/joe-ai.js
CHANGED
|
@@ -6,6 +6,27 @@
|
|
|
6
6
|
Ai.default_ai = null; // Default AI assistant object
|
|
7
7
|
// ========== COMPONENTS ==========
|
|
8
8
|
|
|
9
|
+
// Simple markdown -> HTML helper used by chat UI components.
|
|
10
|
+
// Uses global `marked` and `DOMPurify` when available, falls back to
|
|
11
|
+
// basic escaping + <br> conversion otherwise.
|
|
12
|
+
function renderMarkdownSafe(text) {
|
|
13
|
+
const raw = text || '';
|
|
14
|
+
if (typeof window !== 'undefined' && window.marked && window.DOMPurify) {
|
|
15
|
+
try {
|
|
16
|
+
const html = window.marked.parse(raw);
|
|
17
|
+
return window.DOMPurify.sanitize(html);
|
|
18
|
+
} catch (e) {
|
|
19
|
+
console.error('[joe-ai] markdown render error', e);
|
|
20
|
+
// fall through to plain escape
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
return raw
|
|
24
|
+
.replace(/&/g, '&')
|
|
25
|
+
.replace(/</g, '<')
|
|
26
|
+
.replace(/>/g, '>')
|
|
27
|
+
.replace(/\n/g, '<br>');
|
|
28
|
+
}
|
|
29
|
+
|
|
9
30
|
class JoeAIChatbox extends HTMLElement {
|
|
10
31
|
constructor() {
|
|
11
32
|
super();
|
|
@@ -168,7 +189,31 @@
|
|
|
168
189
|
this.thread_id = convo.thread_id;
|
|
169
190
|
this.user = $J.get(convo.user);
|
|
170
191
|
}
|
|
171
|
-
|
|
192
|
+
|
|
193
|
+
// If there is no assistant reply yet, inject a local greeting based on context.
|
|
194
|
+
// This still shows even if we have a single PLATFORM/system card from context.
|
|
195
|
+
const hasAssistantReply = Array.isArray(this.messages) &&
|
|
196
|
+
this.messages.some(m => m.role === 'assistant');
|
|
197
|
+
if (!hasAssistantReply) {
|
|
198
|
+
try {
|
|
199
|
+
const user = this.user || (convo.user && $J.get(convo.user)) || {};
|
|
200
|
+
const contextId = (convo.context_objects && convo.context_objects[0]) || null;
|
|
201
|
+
const ctxObj = contextId ? $J.get(contextId) : null;
|
|
202
|
+
const uname = user.name || 'there';
|
|
203
|
+
let target = 'this conversation';
|
|
204
|
+
if (ctxObj) {
|
|
205
|
+
const label = ctxObj.name || ctxObj.title || ctxObj.label || ctxObj._id;
|
|
206
|
+
target = `${label} (${ctxObj.itemtype || 'item'})`;
|
|
207
|
+
}
|
|
208
|
+
const greeting = `Hi ${uname}, Iām ready to help you with ${target}. What would you like to explore or change?`;
|
|
209
|
+
this.messages.push({
|
|
210
|
+
role: 'assistant',
|
|
211
|
+
content: greeting,
|
|
212
|
+
created_at: Math.floor(Date.now()/1000)
|
|
213
|
+
});
|
|
214
|
+
} catch(_e){}
|
|
215
|
+
}
|
|
216
|
+
|
|
172
217
|
return {conversation:convo,messages:this.messages};
|
|
173
218
|
} catch (err) {
|
|
174
219
|
console.error("Chatbox load error:", err);
|
|
@@ -297,18 +342,27 @@
|
|
|
297
342
|
body: JSON.stringify({
|
|
298
343
|
conversation_id: this.conversation_id,
|
|
299
344
|
content: message,
|
|
300
|
-
assistant_id: this.selected_assistant_id||Ai.default_ai?Ai.default_ai.value:null
|
|
345
|
+
assistant_id: this.selected_assistant_id || (Ai.default_ai ? Ai.default_ai.value : null)
|
|
301
346
|
})
|
|
302
347
|
}).then(res => res.json());
|
|
303
348
|
|
|
304
|
-
if (response.success && response.runObj) {
|
|
305
|
-
|
|
306
|
-
|
|
349
|
+
if (response.success && (response.run_id || (response.runObj && response.runObj.id))) {
|
|
350
|
+
// Use IDs from server so we don't depend on cache or stale conversation data
|
|
351
|
+
const threadId = response.thread_id || this.conversation.thread_id;
|
|
352
|
+
this.conversation.thread_id = threadId;
|
|
353
|
+
this.currentRunId = response.run_id || (response.runObj && response.runObj.id);
|
|
354
|
+
|
|
355
|
+
// Reload conversation (name, info, etc.), but preserve the known thread_id
|
|
356
|
+
await this.loadConversation();
|
|
357
|
+
if (!this.conversation.thread_id && threadId) {
|
|
358
|
+
this.conversation.thread_id = threadId;
|
|
359
|
+
}
|
|
360
|
+
|
|
307
361
|
this.UI.content.update(); // update messages
|
|
308
362
|
this.startPolling(); // šø start watching for assistant reply!
|
|
309
363
|
input.value = '';
|
|
310
364
|
} else {
|
|
311
|
-
alert('Failed to send message.');
|
|
365
|
+
alert((response.error||'Failed to send message.')+' - '+response.message);
|
|
312
366
|
}
|
|
313
367
|
} catch (err) {
|
|
314
368
|
console.error('Send message error:', err);
|
|
@@ -325,27 +379,56 @@
|
|
|
325
379
|
|
|
326
380
|
// Insert thinking message
|
|
327
381
|
this.showThinkingMessage();
|
|
328
|
-
|
|
382
|
+
|
|
383
|
+
// Baseline: timestamp of the latest assistant message we have so far
|
|
384
|
+
const lastAssistantTs = (this.messages || [])
|
|
385
|
+
.filter(m => m && m.role === 'assistant' && m.created_at)
|
|
386
|
+
.reduce((max, m) => Math.max(max, m.created_at), 0);
|
|
387
|
+
|
|
388
|
+
let attempts = 0;
|
|
329
389
|
this.pollingInterval = setInterval(async () => {
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
if (run.status === 'completed') {
|
|
334
|
-
const resThread = await fetch(`/API/plugin/chatgpt-assistants/getThreadMessages?thread_id=${this.conversation.thread_id}`);
|
|
335
|
-
//const activeAssistant = this.conversation.assistants.find(a => a.openai_id === run.assistant_id);
|
|
390
|
+
attempts++;
|
|
391
|
+
try {
|
|
392
|
+
const resThread = await fetch(`/API/plugin/chatgpt-assistants/getThreadMessages?thread_id=${this.conversation.thread_id}&polling=true`);
|
|
336
393
|
const threadMessages = await resThread.json();
|
|
337
|
-
|
|
394
|
+
|
|
338
395
|
if (threadMessages?.messages) {
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
396
|
+
const msgs = threadMessages.messages;
|
|
397
|
+
const latestAssistantTs = msgs
|
|
398
|
+
.filter(m => m && m.role === 'assistant' && m.created_at)
|
|
399
|
+
.reduce((max, m) => Math.max(max, m.created_at), 0);
|
|
400
|
+
|
|
401
|
+
// When we see a newer assistant message than we had before, treat it as the reply.
|
|
402
|
+
if (latestAssistantTs && latestAssistantTs > lastAssistantTs) {
|
|
403
|
+
this.messages = msgs;
|
|
404
|
+
this.UI.content.update(msgs);
|
|
405
|
+
|
|
406
|
+
clearInterval(this.pollingInterval);
|
|
407
|
+
this.pollingInterval = null;
|
|
408
|
+
this.hideThinkingMessage();
|
|
409
|
+
this.UI.textarea.disabled = false;
|
|
410
|
+
this.UI.sendButton.disabled = false;
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
342
413
|
}
|
|
343
|
-
|
|
414
|
+
|
|
415
|
+
if (attempts > 60) { // ~2 minutes
|
|
416
|
+
console.warn('Thread polling timeout for assistant reply');
|
|
417
|
+
clearInterval(this.pollingInterval);
|
|
418
|
+
this.pollingInterval = null;
|
|
419
|
+
this.hideThinkingMessage();
|
|
420
|
+
this.UI.textarea.disabled = false;
|
|
421
|
+
this.UI.sendButton.disabled = false;
|
|
422
|
+
alert('Timed out waiting for assistant response.');
|
|
423
|
+
}
|
|
424
|
+
} catch (err) {
|
|
425
|
+
console.error('Polling error (thread messages):', err);
|
|
344
426
|
clearInterval(this.pollingInterval);
|
|
345
427
|
this.pollingInterval = null;
|
|
346
428
|
this.hideThinkingMessage();
|
|
347
429
|
this.UI.textarea.disabled = false;
|
|
348
430
|
this.UI.sendButton.disabled = false;
|
|
431
|
+
alert('Error while checking assistant response.');
|
|
349
432
|
}
|
|
350
433
|
}, 2000);
|
|
351
434
|
}
|
|
@@ -417,17 +500,33 @@
|
|
|
417
500
|
this.conversation_id = this.getAttribute('conversation_id') || null;
|
|
418
501
|
this.assistant_id = this.getAttribute('assistant_id') || null;
|
|
419
502
|
this.ai_assistant_id = this.getAttribute('ai_assistant_id') || null;
|
|
503
|
+
this.user_id = this.getAttribute('user_id') || null;
|
|
420
504
|
this.model = this.getAttribute('model') || null;
|
|
505
|
+
this.assistant_color = this.getAttribute('assistant_color') || null;
|
|
506
|
+
this.user_color = this.getAttribute('user_color') || null;
|
|
507
|
+
// Optional persistence key so widgets inside dynamic layouts (like capp cards)
|
|
508
|
+
// can restore their conversation after re-render or resize.
|
|
509
|
+
this.persist_key =
|
|
510
|
+
this.getAttribute('persist_key') ||
|
|
511
|
+
(this.getAttribute('source') ? ('joe-ai-widget:' + this.getAttribute('source')) : null);
|
|
421
512
|
this.messages = [];
|
|
422
513
|
this._ui = {};
|
|
423
514
|
}
|
|
424
515
|
|
|
425
516
|
connectedCallback() {
|
|
426
517
|
this.renderShell();
|
|
518
|
+
// If we don't have an explicit conversation_id but a persisted one exists,
|
|
519
|
+
// restore it before deciding what to load.
|
|
520
|
+
this.restoreFromStorageIfNeeded();
|
|
521
|
+
// Lazy conversation creation:
|
|
522
|
+
// - If a conversation_id is provided, load its history.
|
|
523
|
+
// - Otherwise, do NOT create a conversation until the user actually
|
|
524
|
+
// sends a message. `sendMessage` will call startConversation() on
|
|
525
|
+
// demand when needed.
|
|
427
526
|
if (this.conversation_id) {
|
|
428
527
|
this.loadHistory();
|
|
429
528
|
} else {
|
|
430
|
-
this.
|
|
529
|
+
this.setStatus('online');
|
|
431
530
|
}
|
|
432
531
|
}
|
|
433
532
|
|
|
@@ -435,13 +534,44 @@
|
|
|
435
534
|
return this.endpoint || '';
|
|
436
535
|
}
|
|
437
536
|
|
|
537
|
+
persistState() {
|
|
538
|
+
if (!this.persist_key || typeof window === 'undefined' || !window.localStorage) return;
|
|
539
|
+
try {
|
|
540
|
+
const payload = {
|
|
541
|
+
conversation_id: this.conversation_id,
|
|
542
|
+
assistant_id: this.assistant_id,
|
|
543
|
+
model: this.model,
|
|
544
|
+
assistant_color: this.assistant_color
|
|
545
|
+
};
|
|
546
|
+
window.localStorage.setItem(this.persist_key, JSON.stringify(payload));
|
|
547
|
+
} catch (_e) { /* ignore storage errors */ }
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
restoreFromStorageIfNeeded() {
|
|
551
|
+
if (this.conversation_id || !this.persist_key || typeof window === 'undefined' || !window.localStorage) return;
|
|
552
|
+
try {
|
|
553
|
+
const raw = window.localStorage.getItem(this.persist_key);
|
|
554
|
+
if (!raw) return;
|
|
555
|
+
const data = JSON.parse(raw);
|
|
556
|
+
if (!data || !data.conversation_id) return;
|
|
557
|
+
this.conversation_id = data.conversation_id;
|
|
558
|
+
this.setAttribute('conversation_id', this.conversation_id);
|
|
559
|
+
this.assistant_id = data.assistant_id || this.assistant_id;
|
|
560
|
+
this.model = data.model || this.model;
|
|
561
|
+
this.assistant_color = data.assistant_color || this.assistant_color;
|
|
562
|
+
} catch (_e) { /* ignore */ }
|
|
563
|
+
}
|
|
564
|
+
|
|
438
565
|
renderShell() {
|
|
439
566
|
const style = document.createElement('style');
|
|
440
567
|
style.textContent = `
|
|
441
568
|
:host {
|
|
442
569
|
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
443
570
|
display: block;
|
|
444
|
-
|
|
571
|
+
/* Allow the widget to grow/shrink with its container (cards, sidebars, etc.) */
|
|
572
|
+
width: 100%;
|
|
573
|
+
height: 100%;
|
|
574
|
+
max-width: none;
|
|
445
575
|
border: 1px solid #ddd;
|
|
446
576
|
border-radius: 8px;
|
|
447
577
|
box-shadow: 0 2px 6px rgba(0,0,0,0.08);
|
|
@@ -471,7 +601,9 @@
|
|
|
471
601
|
}
|
|
472
602
|
.messages {
|
|
473
603
|
padding: 10px;
|
|
474
|
-
|
|
604
|
+
/* Fill remaining space between header and footer */
|
|
605
|
+
flex: 1 1 auto;
|
|
606
|
+
min-height: 0;
|
|
475
607
|
overflow-y: auto;
|
|
476
608
|
background: #f5f7fa;
|
|
477
609
|
font-size: 13px;
|
|
@@ -488,17 +620,24 @@
|
|
|
488
620
|
.bubble {
|
|
489
621
|
display: inline-block;
|
|
490
622
|
padding: 6px 8px;
|
|
491
|
-
border-radius:
|
|
623
|
+
border-radius: 5px;
|
|
492
624
|
line-height: 1.4;
|
|
625
|
+
max-width: 100%;
|
|
626
|
+
overflow-x: auto;
|
|
493
627
|
}
|
|
494
628
|
.user .bubble {
|
|
495
|
-
background: #2563eb;
|
|
629
|
+
background: var(--joe-ai-user-bg, #2563eb);
|
|
496
630
|
color: #fff;
|
|
497
631
|
}
|
|
498
632
|
.assistant .bubble {
|
|
499
|
-
background: #e5e7eb;
|
|
633
|
+
background: var(--joe-ai-assistant-bg, #e5e7eb);
|
|
500
634
|
color: #111827;
|
|
501
635
|
}
|
|
636
|
+
.msg.assistant.tools-used .bubble {
|
|
637
|
+
background: #fef3c7;
|
|
638
|
+
color: #92400e;
|
|
639
|
+
font-size: 11px;
|
|
640
|
+
}
|
|
502
641
|
.footer {
|
|
503
642
|
border-top: 1px solid #e5e7eb;
|
|
504
643
|
padding: 6px;
|
|
@@ -562,22 +701,72 @@
|
|
|
562
701
|
this.sendMessage();
|
|
563
702
|
}
|
|
564
703
|
});
|
|
704
|
+
this.applyThemeColors();
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
applyThemeColors() {
|
|
708
|
+
if (!this.shadowRoot || !this.shadowRoot.host) return;
|
|
709
|
+
const host = this.shadowRoot.host;
|
|
710
|
+
if (this.assistant_color) {
|
|
711
|
+
host.style.setProperty('--joe-ai-assistant-bg', this.assistant_color);
|
|
712
|
+
}
|
|
713
|
+
if (this.user_color) {
|
|
714
|
+
host.style.setProperty('--joe-ai-user-bg', this.user_color);
|
|
715
|
+
}
|
|
565
716
|
}
|
|
566
717
|
|
|
567
718
|
setStatus(text) {
|
|
568
|
-
if (this._ui.status)
|
|
719
|
+
if (!this._ui.status) return;
|
|
720
|
+
const cid = this.conversation_id || '';
|
|
721
|
+
// Show conversation id when available, e.g. "cuid123 - online"
|
|
722
|
+
this._ui.status.textContent = cid ? (cid + ' - ' + text) : text;
|
|
569
723
|
}
|
|
570
724
|
|
|
571
725
|
renderMessages() {
|
|
572
726
|
if (!this._ui.messages) return;
|
|
573
727
|
this._ui.messages.innerHTML = (this.messages || []).map(m => {
|
|
574
728
|
const role = m.role || 'assistant';
|
|
575
|
-
const
|
|
576
|
-
|
|
729
|
+
const extra = m.meta === 'tools_used' ? ' tools-used' : '';
|
|
730
|
+
const html = renderMarkdownSafe(m.content || '');
|
|
731
|
+
return `<div class="msg ${role}${extra}"><div class="bubble">${html}</div></div>`;
|
|
577
732
|
}).join('');
|
|
578
733
|
this._ui.messages.scrollTop = this._ui.messages.scrollHeight;
|
|
579
734
|
}
|
|
580
735
|
|
|
736
|
+
// Resolve the logical user for this widget instance.
|
|
737
|
+
// Priority:
|
|
738
|
+
// 1) Explicit user_id attribute on the element (fetch /API/item/user/_id/:id)
|
|
739
|
+
// 2) Page-level window._aiWidgetUser (used by ai-widget-test.html)
|
|
740
|
+
// 3) null (caller can still force a user_color theme)
|
|
741
|
+
async resolveUser() {
|
|
742
|
+
if (this._resolvedUser) {
|
|
743
|
+
return this._resolvedUser;
|
|
744
|
+
}
|
|
745
|
+
// 1) If the hosting page passed a user_id attribute, look up that user via JOE.
|
|
746
|
+
if (this.user_id) {
|
|
747
|
+
try {
|
|
748
|
+
const res = await fetch(
|
|
749
|
+
this.apiBase + '/API/item/user/_id/' + encodeURIComponent(this.user_id),
|
|
750
|
+
{ credentials: 'include' }
|
|
751
|
+
);
|
|
752
|
+
const data = await res.json();
|
|
753
|
+
const u = (data && data.item) || null;
|
|
754
|
+
if (u && u._id) {
|
|
755
|
+
this._resolvedUser = u;
|
|
756
|
+
return u;
|
|
757
|
+
}
|
|
758
|
+
} catch (e) {
|
|
759
|
+
console.error('JoeAIWidget.resolveUser: failed to load user by id', this.user_id, e);
|
|
760
|
+
}
|
|
761
|
+
}
|
|
762
|
+
// 2) Fall back to a page-global user (ai-widget-test.html populates this).
|
|
763
|
+
if (typeof window !== 'undefined' && window._aiWidgetUser) {
|
|
764
|
+
this._resolvedUser = window._aiWidgetUser;
|
|
765
|
+
return this._resolvedUser;
|
|
766
|
+
}
|
|
767
|
+
return null;
|
|
768
|
+
}
|
|
769
|
+
|
|
581
770
|
async startConversation() {
|
|
582
771
|
try {
|
|
583
772
|
this.setStatus('connectingā¦');
|
|
@@ -586,27 +775,48 @@
|
|
|
586
775
|
ai_assistant_id: this.getAttribute('ai_assistant_id') || undefined,
|
|
587
776
|
source: this.getAttribute('source') || 'widget'
|
|
588
777
|
};
|
|
589
|
-
|
|
778
|
+
// Resolve the effective user for this widget and pass id/name/color
|
|
779
|
+
// explicitly to the server. This works for:
|
|
780
|
+
// - ai-widget-test.html (which sets window._aiWidgetUser)
|
|
781
|
+
// - JOE pages or external sites that pass a user_id attribute
|
|
782
|
+
try {
|
|
783
|
+
const globalUser = await this.resolveUser();
|
|
784
|
+
if (globalUser) {
|
|
785
|
+
payload.user_id = globalUser._id;
|
|
786
|
+
payload.user_name = globalUser.fullname || globalUser.name;
|
|
787
|
+
if (globalUser.color) { payload.user_color = globalUser.color; }
|
|
788
|
+
} else if (this.user_color) {
|
|
789
|
+
payload.user_color = this.user_color;
|
|
790
|
+
}
|
|
791
|
+
} catch (_e) { /* ignore */ }
|
|
792
|
+
const resp = await fetch(this.apiBase + '/API/plugin/chatgpt/widgetStart', {
|
|
590
793
|
method: 'POST',
|
|
591
794
|
headers: { 'Content-Type': 'application/json' },
|
|
592
795
|
body: JSON.stringify(payload)
|
|
593
796
|
}).then(r => r.json());
|
|
594
797
|
|
|
595
798
|
if (!resp || resp.success !== true) {
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
799
|
+
const msg = (resp && (resp.error || resp.message)) || 'Failed to start conversation';
|
|
800
|
+
this.setStatus('error: ' + msg);
|
|
801
|
+
this.messages.push({
|
|
802
|
+
role:'assistant',
|
|
803
|
+
content:'[Error starting conversation: '+msg+']',
|
|
804
|
+
created_at:new Date().toISOString()
|
|
805
|
+
});
|
|
806
|
+
this.renderMessages();
|
|
807
|
+
console.error('widgetStart error', { payload, response: resp });
|
|
601
808
|
return;
|
|
602
809
|
}
|
|
603
810
|
this.conversation_id = resp.conversation_id;
|
|
604
811
|
this.setAttribute('conversation_id', this.conversation_id);
|
|
605
812
|
this.assistant_id = resp.assistant_id || this.assistant_id;
|
|
606
813
|
this.model = resp.model || this.model;
|
|
814
|
+
this.assistant_color = resp.assistant_color || this.assistant_color;
|
|
815
|
+
this.applyThemeColors();
|
|
607
816
|
this.messages = [];
|
|
608
817
|
this.renderMessages();
|
|
609
818
|
this.setStatus('online');
|
|
819
|
+
this.persistState();
|
|
610
820
|
} catch (e) {
|
|
611
821
|
console.error('widgetStart exception', e);
|
|
612
822
|
this.setStatus('error');
|
|
@@ -616,17 +826,23 @@
|
|
|
616
826
|
async loadHistory() {
|
|
617
827
|
try {
|
|
618
828
|
this.setStatus('loadingā¦');
|
|
619
|
-
|
|
620
|
-
.
|
|
829
|
+
const resp = await fetch(
|
|
830
|
+
this.apiBase + '/API/plugin/chatgpt/widgetHistory?conversation_id=' +
|
|
831
|
+
encodeURIComponent(this.conversation_id)
|
|
832
|
+
).then(r => r.json());
|
|
621
833
|
if (!resp || resp.success !== true) {
|
|
834
|
+
console.warn('widgetHistory non-success response', resp);
|
|
622
835
|
this.setStatus('online');
|
|
623
836
|
return;
|
|
624
837
|
}
|
|
625
838
|
this.assistant_id = resp.assistant_id || this.assistant_id;
|
|
626
839
|
this.model = resp.model || this.model;
|
|
840
|
+
this.assistant_color = resp.assistant_color || this.assistant_color;
|
|
841
|
+
this.applyThemeColors();
|
|
627
842
|
this.messages = resp.messages || [];
|
|
628
843
|
this.renderMessages();
|
|
629
844
|
this.setStatus('online');
|
|
845
|
+
this.persistState();
|
|
630
846
|
} catch (e) {
|
|
631
847
|
console.error('widgetHistory exception', e);
|
|
632
848
|
this.setStatus('online');
|
|
@@ -658,6 +874,16 @@
|
|
|
658
874
|
assistant_id: this.assistant_id || undefined,
|
|
659
875
|
model: this.model || undefined
|
|
660
876
|
};
|
|
877
|
+
try {
|
|
878
|
+
const globalUser = await this.resolveUser();
|
|
879
|
+
if (globalUser) {
|
|
880
|
+
payload.user_id = globalUser._id;
|
|
881
|
+
payload.user_name = globalUser.fullname || globalUser.name;
|
|
882
|
+
if (globalUser.color) { payload.user_color = globalUser.color; }
|
|
883
|
+
} else if (this.user_color) {
|
|
884
|
+
payload.user_color = this.user_color;
|
|
885
|
+
}
|
|
886
|
+
} catch (_e) { /* ignore */ }
|
|
661
887
|
const resp = await fetch(this.apiBase + '/API/plugin/chatgpt/widgetMessage', {
|
|
662
888
|
method: 'POST',
|
|
663
889
|
headers: { 'Content-Type': 'application/json' },
|
|
@@ -665,17 +891,20 @@
|
|
|
665
891
|
}).then(r => r.json());
|
|
666
892
|
|
|
667
893
|
if (!resp || resp.success !== true) {
|
|
668
|
-
const msg = (resp && resp.error) || 'Failed to send message';
|
|
669
|
-
console.error('widgetMessage error', resp);
|
|
894
|
+
const msg = (resp && (resp.error || resp.message)) || 'Failed to send message';
|
|
895
|
+
console.error('widgetMessage error', { payload, response: resp });
|
|
670
896
|
this.messages.push({ role:'assistant', content:'[Error: '+msg+']', created_at:new Date().toISOString() });
|
|
671
897
|
this.renderMessages();
|
|
672
898
|
this.setStatus('error: ' + msg);
|
|
673
899
|
} else {
|
|
674
900
|
this.assistant_id = resp.assistant_id || this.assistant_id;
|
|
675
901
|
this.model = resp.model || this.model;
|
|
902
|
+
this.assistant_color = resp.assistant_color || this.assistant_color;
|
|
903
|
+
this.applyThemeColors();
|
|
676
904
|
this.messages = resp.messages || this.messages;
|
|
677
905
|
this.renderMessages();
|
|
678
906
|
this.setStatus('online');
|
|
907
|
+
this.persistState();
|
|
679
908
|
}
|
|
680
909
|
} catch (e) {
|
|
681
910
|
console.error('widgetMessage exception', e);
|
|
@@ -690,6 +919,447 @@
|
|
|
690
919
|
customElements.define('joe-ai-widget', JoeAIWidget);
|
|
691
920
|
}
|
|
692
921
|
|
|
922
|
+
// ---------- Assistant picker: small selector component for joe-ai-widget ----------
|
|
923
|
+
class JoeAIAssistantPicker extends HTMLElement {
|
|
924
|
+
constructor() {
|
|
925
|
+
super();
|
|
926
|
+
this.attachShadow({ mode: 'open' });
|
|
927
|
+
this._ui = {};
|
|
928
|
+
this._assistants = [];
|
|
929
|
+
this._defaultId = null;
|
|
930
|
+
}
|
|
931
|
+
|
|
932
|
+
connectedCallback() {
|
|
933
|
+
this.renderShell();
|
|
934
|
+
this.init();
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
get widget() {
|
|
938
|
+
const targetId = this.getAttribute('for_widget');
|
|
939
|
+
if (targetId) {
|
|
940
|
+
return document.getElementById(targetId);
|
|
941
|
+
}
|
|
942
|
+
// Fallback: nearest joe-ai-widget in the same card/container
|
|
943
|
+
return this.closest('capp-card,div')?.querySelector('joe-ai-widget') || null;
|
|
944
|
+
}
|
|
945
|
+
|
|
946
|
+
renderShell() {
|
|
947
|
+
const style = document.createElement('style');
|
|
948
|
+
style.textContent = `
|
|
949
|
+
:host {
|
|
950
|
+
display: block;
|
|
951
|
+
margin-bottom: 6px;
|
|
952
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
953
|
+
font-size: 12px;
|
|
954
|
+
}
|
|
955
|
+
.row {
|
|
956
|
+
display: flex;
|
|
957
|
+
align-items: center;
|
|
958
|
+
justify-content: space-between;
|
|
959
|
+
gap: 6px;
|
|
960
|
+
}
|
|
961
|
+
label {
|
|
962
|
+
color: #374151;
|
|
963
|
+
}
|
|
964
|
+
select {
|
|
965
|
+
margin-left: 4px;
|
|
966
|
+
padding: 2px 4px;
|
|
967
|
+
font-size: 12px;
|
|
968
|
+
}
|
|
969
|
+
.hint {
|
|
970
|
+
font-size: 11px;
|
|
971
|
+
color: #6b7280;
|
|
972
|
+
margin-left: 4px;
|
|
973
|
+
}
|
|
974
|
+
`;
|
|
975
|
+
const wrapper = document.createElement('div');
|
|
976
|
+
wrapper.className = 'row';
|
|
977
|
+
wrapper.innerHTML = `
|
|
978
|
+
<div>
|
|
979
|
+
<label>Assistant:</label>
|
|
980
|
+
<select id="assistant-select"></select>
|
|
981
|
+
<span id="assistant-hint" class="hint"></span>
|
|
982
|
+
</div>
|
|
983
|
+
<div>
|
|
984
|
+
<button id="new-convo" style="font-size:11px;padding:2px 8px;">New conversation</button>
|
|
985
|
+
</div>
|
|
986
|
+
`;
|
|
987
|
+
this.shadowRoot.innerHTML = '';
|
|
988
|
+
this.shadowRoot.appendChild(style);
|
|
989
|
+
this.shadowRoot.appendChild(wrapper);
|
|
990
|
+
this._ui.select = this.shadowRoot.getElementById('assistant-select');
|
|
991
|
+
this._ui.hint = this.shadowRoot.getElementById('assistant-hint');
|
|
992
|
+
this._ui.newConvo = this.shadowRoot.getElementById('new-convo');
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
async init() {
|
|
996
|
+
const select = this._ui.select;
|
|
997
|
+
const hint = this._ui.hint;
|
|
998
|
+
const widget = this.widget;
|
|
999
|
+
if (!select || !widget) return;
|
|
1000
|
+
|
|
1001
|
+
try {
|
|
1002
|
+
const [assistantsResp, settingsResp] = await Promise.all([
|
|
1003
|
+
fetch('/API/item/ai_assistant', { credentials: 'include' }).then(r => r.json()),
|
|
1004
|
+
fetch('/API/item/setting', { credentials: 'include' }).then(r => r.json())
|
|
1005
|
+
]);
|
|
1006
|
+
this._assistants = Array.isArray(assistantsResp.item) ? assistantsResp.item : [];
|
|
1007
|
+
const settings = Array.isArray(settingsResp.item) ? settingsResp.item : [];
|
|
1008
|
+
const def = settings.find(s => s && s.name === 'DEFAULT_AI_ASSISTANT');
|
|
1009
|
+
this._defaultId = def && def.value;
|
|
1010
|
+
} catch (e) {
|
|
1011
|
+
console.error('[joe-ai] AssistantPicker init error', e);
|
|
1012
|
+
this._assistants = [];
|
|
1013
|
+
}
|
|
1014
|
+
|
|
1015
|
+
select.innerHTML = '';
|
|
1016
|
+
const noneOption = document.createElement('option');
|
|
1017
|
+
noneOption.value = '';
|
|
1018
|
+
noneOption.textContent = 'None (model only)';
|
|
1019
|
+
select.appendChild(noneOption);
|
|
1020
|
+
|
|
1021
|
+
this._assistants.forEach(a => {
|
|
1022
|
+
const opt = document.createElement('option');
|
|
1023
|
+
opt.value = a._id;
|
|
1024
|
+
opt.textContent = a.name || a.title || a._id;
|
|
1025
|
+
if (a._id === this._defaultId) {
|
|
1026
|
+
opt.textContent += ' (default)';
|
|
1027
|
+
}
|
|
1028
|
+
select.appendChild(opt);
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
let initialId = '';
|
|
1032
|
+
if (widget.getAttribute('ai_assistant_id')) {
|
|
1033
|
+
initialId = widget.getAttribute('ai_assistant_id');
|
|
1034
|
+
hint && (hint.textContent = 'Using assistant from widget attribute.');
|
|
1035
|
+
} else if (this._defaultId && this._assistants.some(a => a && a._id === this._defaultId)) {
|
|
1036
|
+
initialId = this._defaultId;
|
|
1037
|
+
hint && (hint.textContent = 'Using DEFAULT_AI_ASSISTANT.');
|
|
1038
|
+
} else {
|
|
1039
|
+
hint && (hint.textContent = this._assistants.length
|
|
1040
|
+
? 'No default; using model only.'
|
|
1041
|
+
: 'No assistants defined; using model only.');
|
|
1042
|
+
}
|
|
1043
|
+
select.value = initialId;
|
|
1044
|
+
this.applyAssistantToWidget(widget, initialId);
|
|
1045
|
+
|
|
1046
|
+
select.addEventListener('change', () => {
|
|
1047
|
+
this.applyAssistantToWidget(widget, select.value);
|
|
1048
|
+
});
|
|
1049
|
+
|
|
1050
|
+
if (this._ui.newConvo) {
|
|
1051
|
+
this._ui.newConvo.addEventListener('click', () => {
|
|
1052
|
+
this.startNewConversation(widget);
|
|
1053
|
+
});
|
|
1054
|
+
}
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1057
|
+
startNewConversation(widget) {
|
|
1058
|
+
try {
|
|
1059
|
+
if (!widget) return;
|
|
1060
|
+
// Clear persisted state and conversation id so a fresh widgetStart will happen
|
|
1061
|
+
if (widget.persist_key && typeof window !== 'undefined' && window.localStorage) {
|
|
1062
|
+
try { window.localStorage.removeItem(widget.persist_key); } catch (_e) { /* ignore */ }
|
|
1063
|
+
}
|
|
1064
|
+
widget.removeAttribute('conversation_id');
|
|
1065
|
+
widget.conversation_id = null;
|
|
1066
|
+
if (Array.isArray(widget.messages)) {
|
|
1067
|
+
widget.messages = [];
|
|
1068
|
+
}
|
|
1069
|
+
if (typeof widget.renderMessages === 'function') {
|
|
1070
|
+
widget.renderMessages();
|
|
1071
|
+
}
|
|
1072
|
+
if (typeof widget.setStatus === 'function') {
|
|
1073
|
+
widget.setStatus('online');
|
|
1074
|
+
}
|
|
1075
|
+
} catch (e) {
|
|
1076
|
+
console.error('[joe-ai] error starting new conversation from picker', e);
|
|
1077
|
+
}
|
|
1078
|
+
}
|
|
1079
|
+
|
|
1080
|
+
applyAssistantToWidget(widget, assistantId) {
|
|
1081
|
+
try {
|
|
1082
|
+
if (!widget) return;
|
|
1083
|
+
const val = assistantId || '';
|
|
1084
|
+
if (val) {
|
|
1085
|
+
widget.setAttribute('ai_assistant_id', val);
|
|
1086
|
+
} else {
|
|
1087
|
+
widget.removeAttribute('ai_assistant_id');
|
|
1088
|
+
}
|
|
1089
|
+
// Reset conversation and clear persisted state so a new chat starts
|
|
1090
|
+
if (widget.persist_key && typeof window !== 'undefined' && window.localStorage) {
|
|
1091
|
+
try { window.localStorage.removeItem(widget.persist_key); } catch (_e) { /* ignore */ }
|
|
1092
|
+
}
|
|
1093
|
+
widget.removeAttribute('conversation_id');
|
|
1094
|
+
widget.conversation_id = null;
|
|
1095
|
+
if (typeof widget.setStatus === 'function') {
|
|
1096
|
+
widget.setStatus('online');
|
|
1097
|
+
}
|
|
1098
|
+
} catch (e) {
|
|
1099
|
+
console.error('[joe-ai] error applying assistant selection', e);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
if (!customElements.get('joe-ai-assistant-picker')) {
|
|
1105
|
+
customElements.define('joe-ai-assistant-picker', JoeAIAssistantPicker);
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// ---------- Conversation list: clickable ai_widget_conversation list ----------
|
|
1109
|
+
class JoeAIConversationList extends HTMLElement {
|
|
1110
|
+
constructor() {
|
|
1111
|
+
super();
|
|
1112
|
+
this.attachShadow({ mode: 'open' });
|
|
1113
|
+
this._ui = {};
|
|
1114
|
+
this._assistants = [];
|
|
1115
|
+
}
|
|
1116
|
+
|
|
1117
|
+
connectedCallback() {
|
|
1118
|
+
this.renderShell();
|
|
1119
|
+
this.refreshConversations();
|
|
1120
|
+
}
|
|
1121
|
+
|
|
1122
|
+
get widget() {
|
|
1123
|
+
const targetId = this.getAttribute('for_widget');
|
|
1124
|
+
if (targetId) {
|
|
1125
|
+
return document.getElementById(targetId);
|
|
1126
|
+
}
|
|
1127
|
+
return this.closest('capp-card,div')?.querySelector('joe-ai-widget') || null;
|
|
1128
|
+
}
|
|
1129
|
+
|
|
1130
|
+
renderShell() {
|
|
1131
|
+
const style = document.createElement('style');
|
|
1132
|
+
style.textContent = `
|
|
1133
|
+
:host {
|
|
1134
|
+
display: block;
|
|
1135
|
+
font-family: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
1136
|
+
font-size: 12px;
|
|
1137
|
+
}
|
|
1138
|
+
.container {
|
|
1139
|
+
display: flex;
|
|
1140
|
+
flex-direction: column;
|
|
1141
|
+
height: 100%;
|
|
1142
|
+
}
|
|
1143
|
+
.header {
|
|
1144
|
+
display: flex;
|
|
1145
|
+
align-items: center;
|
|
1146
|
+
justify-content: space-between;
|
|
1147
|
+
margin-bottom: 4px;
|
|
1148
|
+
font-weight: 600;
|
|
1149
|
+
}
|
|
1150
|
+
.status {
|
|
1151
|
+
font-size: 11px;
|
|
1152
|
+
margin-bottom: 4px;
|
|
1153
|
+
}
|
|
1154
|
+
.row {
|
|
1155
|
+
margin-bottom: 2px;
|
|
1156
|
+
}
|
|
1157
|
+
.row.chips {
|
|
1158
|
+
display: flex;
|
|
1159
|
+
gap: 4px;
|
|
1160
|
+
flex-wrap: wrap;
|
|
1161
|
+
}
|
|
1162
|
+
.chip {
|
|
1163
|
+
display: inline-block;
|
|
1164
|
+
padding: 1px 6px;
|
|
1165
|
+
border-radius: 999px;
|
|
1166
|
+
font-size: 10px;
|
|
1167
|
+
line-height: 1.4;
|
|
1168
|
+
white-space: nowrap;
|
|
1169
|
+
}
|
|
1170
|
+
.row.link a {
|
|
1171
|
+
font-size: 11px;
|
|
1172
|
+
}
|
|
1173
|
+
ul {
|
|
1174
|
+
list-style: none;
|
|
1175
|
+
padding: 0;
|
|
1176
|
+
margin: 0;
|
|
1177
|
+
flex: 1 1 auto;
|
|
1178
|
+
overflow-y: auto;
|
|
1179
|
+
}
|
|
1180
|
+
li {
|
|
1181
|
+
cursor: pointer;
|
|
1182
|
+
padding: 3px 0;
|
|
1183
|
+
border-bottom: 1px solid #e5e7eb;
|
|
1184
|
+
}
|
|
1185
|
+
li:hover {
|
|
1186
|
+
background: #f9fafb;
|
|
1187
|
+
}
|
|
1188
|
+
`;
|
|
1189
|
+
const wrapper = document.createElement('div');
|
|
1190
|
+
wrapper.className = 'container';
|
|
1191
|
+
wrapper.innerHTML = `
|
|
1192
|
+
<div class="header">
|
|
1193
|
+
<span>Widget Conversations</span>
|
|
1194
|
+
<button id="refresh" style="font-size:11px;padding:2px 6px;">Refresh</button>
|
|
1195
|
+
</div>
|
|
1196
|
+
<div id="status" class="status"></div>
|
|
1197
|
+
<ul id="list"></ul>
|
|
1198
|
+
`;
|
|
1199
|
+
this.shadowRoot.innerHTML = '';
|
|
1200
|
+
this.shadowRoot.appendChild(style);
|
|
1201
|
+
this.shadowRoot.appendChild(wrapper);
|
|
1202
|
+
this._ui.status = this.shadowRoot.getElementById('status');
|
|
1203
|
+
this._ui.list = this.shadowRoot.getElementById('list');
|
|
1204
|
+
this._ui.refresh = this.shadowRoot.getElementById('refresh');
|
|
1205
|
+
this._ui.refresh.addEventListener('click', () => this.refreshConversations());
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
async refreshConversations() {
|
|
1209
|
+
const statusEl = this._ui.status;
|
|
1210
|
+
const listEl = this._ui.list;
|
|
1211
|
+
if (!statusEl || !listEl) return;
|
|
1212
|
+
statusEl.textContent = 'Loading conversations...';
|
|
1213
|
+
listEl.innerHTML = '';
|
|
1214
|
+
try {
|
|
1215
|
+
// Load assistants once for name/color lookup
|
|
1216
|
+
if (!this._assistants || !this._assistants.length) {
|
|
1217
|
+
try {
|
|
1218
|
+
const aRes = await fetch('/API/item/ai_assistant', { credentials: 'include' });
|
|
1219
|
+
const aData = await aRes.json();
|
|
1220
|
+
this._assistants = Array.isArray(aData.item) ? aData.item : [];
|
|
1221
|
+
} catch (_e) {
|
|
1222
|
+
this._assistants = [];
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
const res = await fetch('/API/item/ai_widget_conversation', { credentials: 'include' });
|
|
1227
|
+
const data = await res.json();
|
|
1228
|
+
let items = Array.isArray(data.item) ? data.item : [];
|
|
1229
|
+
const allItems = items.slice();
|
|
1230
|
+
const sourceFilter = this.getAttribute('source');
|
|
1231
|
+
let filtered = false;
|
|
1232
|
+
if (sourceFilter) {
|
|
1233
|
+
items = items.filter(c => c.source === sourceFilter);
|
|
1234
|
+
filtered = true;
|
|
1235
|
+
}
|
|
1236
|
+
if (!items.length) {
|
|
1237
|
+
if (filtered && allItems.length) {
|
|
1238
|
+
// Fallback: no conversations for this source; show all instead.
|
|
1239
|
+
items = allItems;
|
|
1240
|
+
statusEl.textContent = 'No conversations for source "' + sourceFilter + '". Showing all widget conversations.';
|
|
1241
|
+
} else {
|
|
1242
|
+
statusEl.textContent = 'No widget conversations found.';
|
|
1243
|
+
return;
|
|
1244
|
+
}
|
|
1245
|
+
}
|
|
1246
|
+
items.sort((a, b) => {
|
|
1247
|
+
const da = a.last_message_at || a.joeUpdated || a.created || '';
|
|
1248
|
+
const db = b.last_message_at || b.joeUpdated || b.created || '';
|
|
1249
|
+
return db.localeCompare(da);
|
|
1250
|
+
});
|
|
1251
|
+
statusEl.textContent = 'Click a conversation to resume it.';
|
|
1252
|
+
items.forEach(c => {
|
|
1253
|
+
const li = document.createElement('li');
|
|
1254
|
+
li.dataset.id = c._id;
|
|
1255
|
+
|
|
1256
|
+
const ts = c.last_message_at || c.joeUpdated || c.created || '';
|
|
1257
|
+
const prettyTs = (typeof _joe !== 'undefined' && _joe.Utils && typeof _joe.Utils.prettyPrintDTS === 'function')
|
|
1258
|
+
? _joe.Utils.prettyPrintDTS(ts)
|
|
1259
|
+
: ts;
|
|
1260
|
+
|
|
1261
|
+
// 1. Conversation title: name or pretty date-time
|
|
1262
|
+
const title = c.name || prettyTs || c._id;
|
|
1263
|
+
|
|
1264
|
+
// Helper to choose readable foreground for a hex bg
|
|
1265
|
+
function textColorForBg(hex) {
|
|
1266
|
+
if (!hex || typeof hex !== 'string' || !/^#?[0-9a-fA-F]{6}$/.test(hex)) return '#000';
|
|
1267
|
+
const h = hex[0] === '#' ? hex.slice(1) : hex;
|
|
1268
|
+
const n = parseInt(h, 16);
|
|
1269
|
+
const r = (n >> 16) & 0xff;
|
|
1270
|
+
const g = (n >> 8) & 0xff;
|
|
1271
|
+
const b = n & 0xff;
|
|
1272
|
+
const luminance = r * 0.299 + g * 0.587 + b * 0.114;
|
|
1273
|
+
return luminance > 186 ? '#000' : '#fff';
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
// 2. User and assistant colored chiclets
|
|
1277
|
+
const userLabel = c.user_name || c.user || '';
|
|
1278
|
+
const userColor = c.user_color || '';
|
|
1279
|
+
let userChip = '';
|
|
1280
|
+
if (userLabel) {
|
|
1281
|
+
const bg = userColor || '#4b5563';
|
|
1282
|
+
const fg = textColorForBg(bg);
|
|
1283
|
+
userChip = `<span class="chip user" style="background:${bg};color:${fg};">${userLabel}</span>`;
|
|
1284
|
+
}
|
|
1285
|
+
|
|
1286
|
+
let asstName = '';
|
|
1287
|
+
let asstColor = '';
|
|
1288
|
+
const asstId = c.assistant;
|
|
1289
|
+
const asstOpenAIId = c.assistant_id;
|
|
1290
|
+
if (this._assistants && this._assistants.length) {
|
|
1291
|
+
const asst = this._assistants.find(a =>
|
|
1292
|
+
(asstId && a && a._id === asstId) ||
|
|
1293
|
+
(asstOpenAIId && a && a.assistant_id === asstOpenAIId)
|
|
1294
|
+
);
|
|
1295
|
+
if (asst) {
|
|
1296
|
+
asstName = asst.name || asst.title || asst.assistant_id || asst._id || 'Assistant';
|
|
1297
|
+
asstColor = asst.assistant_color || asst.color || c.assistant_color || '';
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
if (!asstName && (asstOpenAIId || asstId)) {
|
|
1301
|
+
asstName = asstOpenAIId || asstId;
|
|
1302
|
+
asstColor = c.assistant_color || '';
|
|
1303
|
+
}
|
|
1304
|
+
let asstChip = '';
|
|
1305
|
+
if (asstName) {
|
|
1306
|
+
const bg = asstColor || '#2563eb';
|
|
1307
|
+
const fg = textColorForBg(bg);
|
|
1308
|
+
asstChip = `<span class="chip assistant" style="background:${bg};color:${fg};">${asstName}</span>`;
|
|
1309
|
+
}
|
|
1310
|
+
|
|
1311
|
+
// 3. Source and # of messages
|
|
1312
|
+
const msgCount = Array.isArray(c.messages) ? c.messages.length : 0;
|
|
1313
|
+
const src = c.source || '';
|
|
1314
|
+
const metaParts = [];
|
|
1315
|
+
if (src) metaParts.push(src);
|
|
1316
|
+
metaParts.push(msgCount + ' msgs');
|
|
1317
|
+
const metaLine = metaParts.join(' Ā· ');
|
|
1318
|
+
|
|
1319
|
+
// 4. Link to the widget conversation in JOE
|
|
1320
|
+
let href = '#/ai_widget_conversation/' + encodeURIComponent(c._id);
|
|
1321
|
+
try {
|
|
1322
|
+
if (window && window.location && typeof window.location.href === 'string') {
|
|
1323
|
+
href = window.location.href.replace(window.location.hash, href);
|
|
1324
|
+
}
|
|
1325
|
+
} catch (_e) { /* ignore */ }
|
|
1326
|
+
|
|
1327
|
+
li.innerHTML = ''
|
|
1328
|
+
+ `<div class="row title">${title}</div>`
|
|
1329
|
+
+ `<div class="row chips">${userChip}${asstChip}</div>`
|
|
1330
|
+
+ `<div class="row meta">${metaLine}</div>`
|
|
1331
|
+
+ `<div class="row link"><a href="${href}" target="_blank" rel="noopener" onclick="event.stopPropagation();">open in JOE</a></div>`;
|
|
1332
|
+
|
|
1333
|
+
li.addEventListener('click', () => {
|
|
1334
|
+
this.applyConversationToWidget(this.widget, c._id);
|
|
1335
|
+
});
|
|
1336
|
+
listEl.appendChild(li);
|
|
1337
|
+
});
|
|
1338
|
+
} catch (e) {
|
|
1339
|
+
console.error('[joe-ai] error loading widget conversations', e);
|
|
1340
|
+
statusEl.textContent = 'Error loading conversations; see console.';
|
|
1341
|
+
}
|
|
1342
|
+
}
|
|
1343
|
+
|
|
1344
|
+
applyConversationToWidget(widget, conversationId) {
|
|
1345
|
+
if (!widget || !conversationId) return;
|
|
1346
|
+
widget.setAttribute('conversation_id', conversationId);
|
|
1347
|
+
widget.conversation_id = conversationId;
|
|
1348
|
+
if (typeof widget.loadHistory === 'function') {
|
|
1349
|
+
widget.loadHistory();
|
|
1350
|
+
} else if (widget.connectedCallback) {
|
|
1351
|
+
widget.connectedCallback();
|
|
1352
|
+
}
|
|
1353
|
+
if (typeof widget.persistState === 'function') {
|
|
1354
|
+
widget.persistState();
|
|
1355
|
+
}
|
|
1356
|
+
}
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1359
|
+
if (!customElements.get('joe-ai-conversation-list')) {
|
|
1360
|
+
customElements.define('joe-ai-conversation-list', JoeAIConversationList);
|
|
1361
|
+
}
|
|
1362
|
+
|
|
693
1363
|
|
|
694
1364
|
//**YES**
|
|
695
1365
|
Ai.spawnChatHelper = async function(object_id,user_id=_joe.User._id,conversation_id) {
|
|
@@ -709,6 +1379,7 @@
|
|
|
709
1379
|
newChat = true;
|
|
710
1380
|
if(response.error){
|
|
711
1381
|
console.error('ā Failed to create conversation:', response.error);
|
|
1382
|
+
alert('Failed to create conversation: '+response.message);
|
|
712
1383
|
return;
|
|
713
1384
|
}
|
|
714
1385
|
}
|
|
@@ -718,6 +1389,7 @@
|
|
|
718
1389
|
Ai.getDefaultAssistant = function() {
|
|
719
1390
|
|
|
720
1391
|
Ai.default_ai = Ai.default_ai ||_joe.Data.setting.where({name:'DEFAULT_AI_ASSISTANT'})[0]||false;
|
|
1392
|
+
if(!Ai.default_ai){alert('No default AI assistant found');}
|
|
721
1393
|
return Ai.default_ai;
|
|
722
1394
|
}
|
|
723
1395
|
// ========== HELPERS ==========
|
|
@@ -903,29 +1575,61 @@
|
|
|
903
1575
|
}
|
|
904
1576
|
};
|
|
905
1577
|
|
|
906
|
-
// ---------- Autofill (
|
|
907
|
-
// Usage
|
|
908
|
-
Ai.populateField
|
|
1578
|
+
// ---------- Autofill (Responses) ----------
|
|
1579
|
+
// Usage from schema: add `ai:{ prompt:'...' }` to a field. Core will render a
|
|
1580
|
+
// button that calls `_joe.Ai.populateField('field_name')`. You can also call
|
|
1581
|
+
// `_joe.Ai.populateField(dom, { prompt:'...' })` manually from a button.
|
|
1582
|
+
Ai.populateField = async function(target, options = {}){
|
|
909
1583
|
try{
|
|
910
1584
|
const obj = _joe.current && _joe.current.object;
|
|
911
1585
|
const schema = _joe.current && _joe.current.schema && _joe.current.schema.__schemaname;
|
|
912
1586
|
if(!obj || !schema){ return alert('No current object/schema found'); }
|
|
913
1587
|
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1588
|
+
let dom = null;
|
|
1589
|
+
let explicitField = null;
|
|
1590
|
+
if (typeof target === 'string') {
|
|
1591
|
+
explicitField = target;
|
|
1592
|
+
} else {
|
|
1593
|
+
dom = target;
|
|
1594
|
+
}
|
|
1595
|
+
|
|
1596
|
+
let fields = [];
|
|
1597
|
+
if (options.fields && options.fields.length) {
|
|
1598
|
+
fields = options.fields;
|
|
1599
|
+
} else if (explicitField) {
|
|
1600
|
+
fields = [explicitField];
|
|
1601
|
+
} else if (dom) {
|
|
1602
|
+
const parentField = $(dom).parents('.joe-object-field');
|
|
1603
|
+
const inferredField = parentField.data('name');
|
|
1604
|
+
if (inferredField) { fields = [inferredField]; }
|
|
1605
|
+
}
|
|
1606
|
+
if(!fields.length){ return alert('No target field detected. Configure ai.fields or pass a field name.'); }
|
|
1607
|
+
|
|
1608
|
+
// If no prompt was passed explicitly, try to pick it up from the field's
|
|
1609
|
+
// schema-level ai config.
|
|
1610
|
+
let prompt = options.prompt || '';
|
|
1611
|
+
if (!prompt && fields.length === 1 && _joe && typeof _joe.getField === 'function') {
|
|
1612
|
+
try{
|
|
1613
|
+
const fd = _joe.getField(fields[0]);
|
|
1614
|
+
if (fd && fd.ai && fd.ai.prompt) {
|
|
1615
|
+
prompt = fd.ai.prompt;
|
|
1616
|
+
}
|
|
1617
|
+
}catch(_e){ /* ignore */ }
|
|
1618
|
+
}
|
|
918
1619
|
|
|
919
1620
|
// UI feedback
|
|
920
|
-
|
|
921
|
-
dom.
|
|
922
|
-
|
|
1621
|
+
let originalHtml = null;
|
|
1622
|
+
if (dom && dom.innerHTML != null) {
|
|
1623
|
+
originalHtml = dom.innerHTML;
|
|
1624
|
+
dom.disabled = true;
|
|
1625
|
+
dom.innerHTML = (options.loadingLabel || 'Thinking...');
|
|
1626
|
+
}
|
|
923
1627
|
|
|
924
1628
|
const payload = {
|
|
925
1629
|
object_id: obj._id,
|
|
926
1630
|
schema: schema,
|
|
927
1631
|
fields: fields,
|
|
928
|
-
prompt:
|
|
1632
|
+
prompt: prompt || '',
|
|
929
1633
|
assistant_id: options.assistant_id || undefined,
|
|
930
1634
|
allow_web: !!options.allowWeb,
|
|
931
1635
|
save_history: !!options.saveHistory,
|
|
@@ -953,18 +1657,46 @@
|
|
|
953
1657
|
}
|
|
954
1658
|
}
|
|
955
1659
|
|
|
956
|
-
dom
|
|
957
|
-
|
|
1660
|
+
if (dom && originalHtml != null) {
|
|
1661
|
+
dom.disabled = false;
|
|
1662
|
+
dom.innerHTML = originalHtml;
|
|
1663
|
+
}
|
|
958
1664
|
}catch(e){
|
|
959
1665
|
console.error('populateField error', e);
|
|
960
1666
|
alert('Error running AI autofill');
|
|
961
|
-
try{
|
|
1667
|
+
try{
|
|
1668
|
+
if (dom) { dom.disabled = false; }
|
|
1669
|
+
}catch(_e){}
|
|
962
1670
|
}
|
|
963
1671
|
};
|
|
964
1672
|
|
|
965
1673
|
Ai.applyAutofillPatch = function(patch = {}){
|
|
966
1674
|
Object.keys(patch).forEach(function(fname){
|
|
967
|
-
|
|
1675
|
+
try {
|
|
1676
|
+
const $container = $(`.joe-object-field[data-name="${fname}"]`);
|
|
1677
|
+
if(!$container.length){ return; }
|
|
1678
|
+
const $el = $container.find('.joe-field').eq(0);
|
|
1679
|
+
if(!$el.length){ return; }
|
|
1680
|
+
|
|
1681
|
+
// Determine current value (text fields, selects, rich editors, etc.)
|
|
1682
|
+
let currentVal = '';
|
|
1683
|
+
if ($el.is('select') || $el.is('input,textarea')) {
|
|
1684
|
+
currentVal = $el.val();
|
|
1685
|
+
} else {
|
|
1686
|
+
currentVal = $el.text() || '';
|
|
1687
|
+
}
|
|
1688
|
+
const isBlank = !currentVal || String(currentVal).trim() === '';
|
|
1689
|
+
|
|
1690
|
+
if (!isBlank) {
|
|
1691
|
+
const label = fname;
|
|
1692
|
+
const ok = window.confirm(`Replace existing value for "${label}" with AI suggestion?`);
|
|
1693
|
+
if (!ok) { return; }
|
|
1694
|
+
}
|
|
1695
|
+
|
|
1696
|
+
Ai._setFieldValue(fname, patch[fname]);
|
|
1697
|
+
} catch (e) {
|
|
1698
|
+
console.warn('applyAutofillPatch error for field', fname, e);
|
|
1699
|
+
}
|
|
968
1700
|
});
|
|
969
1701
|
};
|
|
970
1702
|
|