json-object-editor 0.10.425 → 0.10.430

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,517 @@
1
+ (function(){
2
+ // Define the joeAI namespace
3
+ const Ai = {};
4
+ const self = this;
5
+ Ai._openChats = {}; // Conversation ID -> element
6
+ Ai.default_ai = null; // Default AI assistant ID
7
+ // ========== COMPONENTS ==========
8
+
9
+ class JoeAIChatbox extends HTMLElement {
10
+ constructor() {
11
+ super();
12
+
13
+
14
+ this.attachShadow({ mode: 'open' });
15
+ this.messages = [];
16
+
17
+ }
18
+
19
+ connectedCallback() {
20
+ this.conversation_id = this.getAttribute('conversation_id');
21
+
22
+ this.selected_assistant_id = Ai.default_ai?Ai.default_ai.value:null;
23
+ this.ui = {};
24
+ if (!this.conversation_id) {
25
+ this.renderError("Missing conversation_id");
26
+ return;
27
+ }
28
+ this.loadConversation();
29
+ this.getAllAssistants();
30
+
31
+ }
32
+ async getAllAssistants() {
33
+ const res = await fetch('/API/item/ai_assistant');
34
+ const result = await res.json();
35
+ if (result.error) {
36
+ console.error("Error fetching assistants:", result.error);
37
+ }
38
+
39
+ this.allAssistants = {};
40
+
41
+ if (Array.isArray(result.item)) {
42
+ result.item.map(a => {
43
+ if (a._id) {
44
+ this.allAssistants[a._id] = a;
45
+ }
46
+ if (a.openai_id) {
47
+ this.allAssistants[a.openai_id] = a; // Optional dual-key if you prefer
48
+ }
49
+ });
50
+ }
51
+
52
+ }
53
+ async loadConversation() {
54
+ try {
55
+ const res = await fetch(`/API/object/ai_conversation/_id/${this.conversation_id}`);
56
+ const convo = await res.json();
57
+
58
+ if (!convo || convo.error) {
59
+ this.renderError("Conversation not found.");
60
+ return;
61
+ }
62
+ this.conversation = convo;
63
+ this.messages = [];
64
+
65
+ if (convo.thread_id) {
66
+ const resThread = await fetch(`/API/plugin/chatgpt-assistants/getThreadMessages?thread_id=${convo.thread_id}`);
67
+ const threadMessages = await resThread.json();
68
+ this.messages = threadMessages?.messages || [];
69
+ }
70
+
71
+ this.render();
72
+ } catch (err) {
73
+ console.error("Chatbox load error:", err);
74
+ this.renderError("Error loading conversation.");
75
+ }
76
+ }
77
+ getAssistantInfo(id){
78
+ const assistant = this.allAssistants[id];
79
+ if (assistant) {
80
+ return assistant;
81
+ } else {
82
+ console.warn("Assistant not found:", id);
83
+ return null;
84
+ }
85
+ }
86
+ render() {
87
+ const convoName = this.conversation.name || "Untitled Conversation";
88
+ const convoInfo = this.conversation.info || "";
89
+
90
+ const chatMessages = this.messages.map(msg => this.renderMessage(msg)).reverse().join('');
91
+
92
+ const assistantOptions = (this.conversation.assistants || []).map(a => {
93
+ const meta = this.getAssistantInfo(a) || {};
94
+
95
+ const label = meta.name || a.name || a.title || 'Assistant';
96
+ const value = meta._id;
97
+ const selected = value === this.selected_assistant_id ? 'selected' : '';
98
+
99
+ return `<option value="${value}" ${selected}>${label}</option>`;
100
+ }).join('');
101
+
102
+ const assistantSelect = `
103
+ <label-select-wrapper>
104
+ <label class="assistant-select-label" title="joe ai assistants">${_joe && _joe.SVG.icon.assistant}</label>
105
+ <select id="assistant-select">
106
+ ${assistantOptions}
107
+ </select>
108
+ </label-select-wrapper>
109
+ `;
110
+
111
+ // Inject external CSS
112
+ const styleLink = document.createElement('link');
113
+ styleLink.setAttribute('rel', 'stylesheet');
114
+ styleLink.setAttribute('href', '/JsonObjectEditor/css/joe-ai.css'); // Adjust path as needed
115
+ this.shadowRoot.appendChild(styleLink);
116
+
117
+ // Build inner HTML in a wrapper
118
+ const wrapper = document.createElement('div');
119
+ wrapper.className = 'chatbox';
120
+ wrapper.innerHTML = `
121
+ <div class="close-btn" title="Close Chatbox" >${_joe.SVG.icon.close}</div>
122
+ <div class="header">
123
+ <h2>${convoName}</h2>
124
+ <p>${convoInfo}</p>
125
+ ${assistantSelect}
126
+ </div>
127
+ <div class="messages">${chatMessages}</div>
128
+ <div class="inputRow">
129
+ <textarea id="chat-input" type="text" placeholder="Type a message..."></textarea>
130
+ <button id="send-button">Send</button>
131
+ </div>
132
+ `;
133
+
134
+ this.shadowRoot.appendChild(wrapper);
135
+
136
+ const messagesDiv = this.shadowRoot.querySelector('.messages');
137
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
138
+ this.shadowRoot.getElementById('send-button').addEventListener('click', () => this.sendMessage());
139
+
140
+ // Wire up the close button
141
+ this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => {
142
+ //this.closeChat()
143
+ this.closest('joe-ai-chatbox').closeChat();
144
+ });
145
+ this.ui.assistant_select = this.shadowRoot.querySelector('#assistant-select');
146
+ this.ui.assistant_select?.addEventListener('change', (e) => {
147
+ this.selected_assistant_id = e.target.value;
148
+ });
149
+ this.selected_assistant_id = this.ui.assistant_select?.value || null;
150
+
151
+ }
152
+
153
+ renderMessage(msg) {
154
+ const role = msg.role || 'user';
155
+ const classes = `message ${role}`;
156
+
157
+ let contentText = '';
158
+
159
+ if (Array.isArray(msg.content)) {
160
+ // OpenAI style: array of parts
161
+ contentText = msg.content.map(part => {
162
+ if (part.type === 'text' && part.text && part.text.value) {
163
+ return part.text.value;
164
+ }
165
+ return '';
166
+ }).join('\n');
167
+ } else if (typeof msg.content === 'string') {
168
+ contentText = msg.content;
169
+ } else {
170
+ contentText = '[Unsupported message format]';
171
+ }
172
+
173
+ // Build timestamp
174
+ const createdAt = msg.created_at ? new Date(msg.created_at * 1000) : null; // OpenAI sends timestamps in seconds
175
+ const timestamp = createdAt ? createdAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : '';
176
+
177
+ return `
178
+ <div class="${classes}">
179
+ <div class="meta">
180
+ <participant-name>${role.toUpperCase()}</participant-name>
181
+ ${timestamp ? `<span class="timestamp">${timestamp}</span>` : ''}</div>
182
+ <div class="content">${contentText}</div>
183
+ </div>
184
+ `;
185
+ }
186
+
187
+ renderError(message) {
188
+ this.shadowRoot.innerHTML = `<div style="color:red;">${message}</div>`;
189
+ }
190
+
191
+
192
+ async getResponse(conversation_id,content,role,assistant_id){
193
+ const response = await fetch('/API/plugin/chatgpt-assistants/addMessage', {
194
+ method: 'POST',
195
+ headers: { 'Content-Type': 'application/json' },
196
+ body: JSON.stringify({
197
+ conversation_id: conversation_id,
198
+ content: content,
199
+ role: role||'system',
200
+ assistant_id: assistant_id||Ai.default_ai?Ai.default_ai.value:null
201
+ })
202
+ }).then(res => res.json());
203
+ }
204
+
205
+ async sendMessage() {//** */
206
+ const input = this.shadowRoot.getElementById('chat-input');
207
+ const message = input.value.trim();
208
+ if (!message) return;
209
+
210
+ input.disabled = true;
211
+ this.shadowRoot.getElementById('send-button').disabled = true;
212
+
213
+ try {
214
+ const response = await fetch('/API/plugin/chatgpt-assistants/addMessage', {
215
+ method: 'POST',
216
+ headers: { 'Content-Type': 'application/json' },
217
+ body: JSON.stringify({
218
+ conversation_id: this.conversation_id,
219
+ content: message,
220
+ assistant_id: this.selected_assistant_id||Ai.default_ai?Ai.default_ai.value:null
221
+ })
222
+ }).then(res => res.json());
223
+
224
+ if (response.success && response.runObj) {
225
+ this.currentRunId = response.runObj.id; // Store the run ID for polling
226
+ await this.loadConversation(); // reload messages
227
+ this.startPolling(); // 🌸 start watching for assistant reply!
228
+ input.value = '';
229
+ } else {
230
+ alert('Failed to send message.');
231
+ }
232
+ } catch (err) {
233
+ console.error('Send message error:', err);
234
+ alert('Error sending message.');
235
+ }
236
+
237
+
238
+ input.disabled = false;
239
+ this.shadowRoot.getElementById('send-button').disabled = false;
240
+ }
241
+
242
+ startPolling() {
243
+ if (this.pollingInterval) return; // Already polling
244
+
245
+ // Insert thinking message
246
+ this.showThinkingMessage();
247
+
248
+ this.pollingInterval = setInterval(async () => {
249
+ const runRes = await fetch(`/API/plugin/chatgpt-assistants/getRunStatus?thread_id=${this.conversation.thread_id}&run_id=${this.currentRunId}`);
250
+ const run = await runRes.json();
251
+
252
+ if (run.status === 'completed') {
253
+ const resThread = await fetch(`/API/plugin/chatgpt-assistants/getThreadMessages?thread_id=${this.conversation.thread_id}`);
254
+ //const activeAssistant = this.conversation.assistants.find(a => a.openai_id === run.assistant_id);
255
+ const threadMessages = await resThread.json();
256
+
257
+ if (threadMessages?.messages) {
258
+ this.messages = threadMessages.messages;
259
+ //this.render();
260
+ }
261
+
262
+ clearInterval(this.pollingInterval);
263
+ this.pollingInterval = null;
264
+ this.hideThinkingMessage();
265
+ }
266
+ }, 2000);
267
+ }
268
+ showThinkingMessage() {
269
+ const messagesDiv = this.shadowRoot.querySelector('.messages');
270
+ if (!messagesDiv) return;
271
+
272
+ // Pull assistant thinking text
273
+ const assistant = this.getAssistantInfo(this.selected_assistant_id);
274
+ const thinkingText = assistant?.assistant_thinking_text || 'Assistant is thinking...';
275
+
276
+ const div = document.createElement('div');
277
+ div.className = 'thinking-message';
278
+ div.textContent = thinkingText;
279
+ div.id = 'thinking-message';
280
+ messagesDiv.appendChild(div);
281
+
282
+ messagesDiv.scrollTop = messagesDiv.scrollHeight;
283
+ }
284
+
285
+ hideThinkingMessage() {
286
+ const existing = this.shadowRoot.querySelector('#thinking-message');
287
+ if (existing) existing.remove();
288
+ }
289
+ closeChat() {
290
+ // Remove the element
291
+ this.remove();
292
+
293
+ // Clean up from open chat registry if possible
294
+ if (_joe && _joe.Ai && _joe.Ai._openChats) {
295
+ delete _joe.Ai._openChats[this.conversation_id];
296
+ }
297
+ }
298
+ }
299
+
300
+ customElements.define('joe-ai-chatbox', JoeAIChatbox);
301
+
302
+ //**YES**
303
+ Ai.spawnChatHelper = async function(object_id,user_id=_joe.User._id,conversation_id) {
304
+ //if not conversation_id, create a new one
305
+ let convo_id = conversation_id;
306
+ var newChat = false;
307
+ if(!convo_id){
308
+ const response = await fetch('/API/plugin/chatgpt-assistants/createConversation', {
309
+ method: 'POST',
310
+ headers: { 'Content-Type': 'application/json' },
311
+ body: JSON.stringify({
312
+ object_id,
313
+ user_id
314
+ })
315
+ }).then(res => res.json());
316
+ convo_id = response?.conversation?._id;
317
+ newChat = true;
318
+ if(response.error){
319
+ console.error('❌ Failed to create conversation:', response.error);
320
+ return;
321
+ }
322
+ }
323
+
324
+ await _joe.Ai.spawnContextualChat(convo_id,{object_id,newChat});
325
+ }
326
+ // ========== HELPERS ==========
327
+ Ai.spawnContextualChat = async function(conversationId, options = {}) {
328
+ if (!conversationId) {
329
+ console.warn("Missing conversation ID for chat spawn.");
330
+ return;
331
+ }
332
+ Ai.default_ai = _joe.Data.setting.where({name:'DEFAULT_AI_ASSISTANT'})[0]||false;
333
+
334
+ // 1. Check if chat already open
335
+ if (Ai._openChats[conversationId]) {
336
+ console.log("Chatbox already open for", conversationId);
337
+ Ai._openChats[conversationId].scrollIntoView({ behavior: 'smooth', block: 'center' });
338
+ return;
339
+ }
340
+
341
+ try {
342
+ const flattened = _joe.Object.flatten(options.object_id);
343
+ if (options.newChat) {
344
+ // 2. Prepare context
345
+
346
+ const contextInstructions = _joe.Ai.generateContextInstructions(flattened,options.object_id);
347
+
348
+ // 3. Inject context into backend
349
+ const contextResult = await fetch('/API/plugin/chatgpt-assistants/addMessage', {
350
+ method: 'POST',
351
+ headers: { 'Content-Type': 'application/json' },
352
+ body: JSON.stringify({
353
+ conversation_id: conversationId,
354
+ role: 'system',
355
+ content: contextInstructions,
356
+ assistant_id: Ai.default_ai.value
357
+ })
358
+ }).then(res => res.json());
359
+
360
+ if (!contextResult || contextResult.error) {
361
+ console.error('❌ Failed to inject context:', contextResult?.error);
362
+ return;
363
+ }
364
+ }
365
+ // 4. Create new chatbox
366
+ const chat = document.createElement('joe-ai-chatbox');
367
+ chat.setAttribute('conversation_id', conversationId);
368
+ const screenWidth = window.innerWidth;
369
+ if(screenWidth <= 768){
370
+ chat.setAttribute('mobile', 'true');
371
+ chat.style.width = 'auto';
372
+ chat.style.left = '0px';
373
+ }
374
+ else{
375
+ chat.setAttribute('mobile', 'false');
376
+ chat.style.width = options.width || '640px';
377
+ chat.style.left = 'auto';
378
+ }
379
+ // Apply styles
380
+
381
+ //chat.style.height = options.height || '420px';
382
+ chat.style.bottom = options.bottom || '50px';
383
+ chat.style.right = options.right || '0px';
384
+ chat.style.top = options.top || '100px';
385
+ chat.style.position = 'fixed';
386
+ chat.style.zIndex = '10000';
387
+ chat.style.background = '#efefef';
388
+ chat.style.border = '1px solid #fff';
389
+ chat.style.borderRadius = '8px';
390
+ chat.style.boxShadow = '0px 1px 4px rgba(0, 0, 0, 0.3)';
391
+ chat.style.padding = '5px';
392
+ chat.style.margin = '5px';
393
+
394
+ document.body.appendChild(chat);
395
+
396
+ // 5. Track it
397
+ Ai._openChats[conversationId] = chat;
398
+
399
+ if (options.newChat) {
400
+ // 6. Show soft local UI message
401
+ _joe.Ai.injectSystemMessage(conversationId, `Context injected: ${flattened.name || flattened.title || 'Object'} (${flattened._id})`);
402
+ }
403
+ } catch (err) {
404
+ console.error('❌ spawnChat context injection failed:', err);
405
+ }
406
+ };
407
+
408
+ /* Ai.spawnChat = function(conversationId, options = {}) {
409
+ if (!conversationId) {
410
+ console.warn("Missing conversation ID for chat spawn.");
411
+ return;
412
+ }
413
+
414
+ // 1. Check if chatbox already open
415
+ if (Ai._openChats[conversationId]) {
416
+ console.log("Chatbox already open for", conversationId);
417
+ Ai._openChats[conversationId].scrollIntoView({ behavior: 'smooth', block: 'center' });
418
+ return;
419
+ }
420
+
421
+ // 2. Create new chatbox
422
+ const chat = document.createElement('joe-ai-chatbox');
423
+ chat.setAttribute('conversation_id', conversationId);
424
+
425
+ const flattened = _joe.Object.flatten();
426
+ const contextInstructions = _joe.Ai.generateContextInstructions(flattened);
427
+
428
+ // Actually inject into AI backend (for assistant awareness) if you have that later
429
+ // For now: silently show a soft system bubble
430
+ _joe.Ai.injectSystemMessage(conversationId, `Context injected: ${flattened.name || flattened.title || 'Object'} (${flattened._id})`);
431
+
432
+
433
+ // Apply styles
434
+ chat.style.width = options.width || '400px';
435
+ chat.style.height = options.height || '420px';
436
+ chat.style.bottom = options.bottom || '20px';
437
+ chat.style.right = options.right || '20px';
438
+ chat.style.position = 'fixed';
439
+ chat.style.zIndex = '10000';
440
+ chat.style.background = '#efefef';
441
+ chat.style.border = '1px solid #fff';
442
+ chat.style.borderRadius = '8px';
443
+ chat.style.boxShadow = '0px 2px 10px rgba(0,0,0,0.1)';
444
+ chat.style.padding = '5px';
445
+
446
+ document.body.appendChild(chat);
447
+
448
+ // 3. Track it
449
+ Ai._openChats[conversationId] = chat;
450
+ return chat;
451
+ // 4. Optionally clean up when chatbox is removed (if you wire close buttons later)
452
+ };
453
+ */
454
+ Ai.generateContextInstructions = function(flattenedObj,object_id) {
455
+ if (!flattenedObj) return '';
456
+
457
+ let context = `{{{BEGIN_OBJECT:${object_id}}}}`+
458
+ "Context: You are assisting the user with the following object:\n\n";
459
+
460
+ context += JSON.stringify(flattenedObj, null, 2) + "\n\n";
461
+ // for (const [key, value] of Object.entries(flattenedObj)) {
462
+ // if (typeof value === 'object' && value !== null) {
463
+ // context += `- ${key}: (linked object)\n`;
464
+ // for (const [subkey, subval] of Object.entries(value)) {
465
+ // context += ` • ${subkey}: ${subval}\n`;
466
+ // }
467
+ // } else {
468
+ // context += `- ${key}: ${value}\n`;
469
+ // }
470
+ // }
471
+
472
+ context += `\nAlways refer to this context when answering questions or completing tasks related to this object.\n`+
473
+ `{{{END_OBJECT:${object_id}}}}`;
474
+
475
+ return context;
476
+ };
477
+
478
+ Ai.injectSystemMessage = async function(conversationId, text) {
479
+ if (!conversationId || !text) return;
480
+
481
+ try {
482
+ // Create a system-style message object
483
+ const messageObj = {
484
+ conversation_id: conversationId,
485
+ role: 'joe',
486
+ content: text,
487
+ created: new Date().toISOString()
488
+ };
489
+
490
+ // You could either push this directly into chatbox if loaded, or update server messages if you have backend ready
491
+ const chatbox = document.querySelector(`joe-ai-chatbox[conversation_id="${conversationId}"]`);
492
+ if (chatbox) {
493
+ if (!chatbox.messages) {
494
+ chatbox.messages = [];
495
+ }
496
+ chatbox.messages.push(messageObj);
497
+
498
+ // Optionally trigger a soft re-render of chatbox if needed
499
+ if (typeof chatbox.renderMessages === 'function') {
500
+ chatbox.renderMessages();
501
+ }
502
+ }
503
+ } catch (err) {
504
+ console.error("❌ injectSystemMessage failed:", err);
505
+ }
506
+ };
507
+
508
+
509
+ // Attach AI to _joe
510
+ if (window._joe) {
511
+ _joe.Ai = Ai;
512
+ } else {
513
+ console.warn('joeAI.js loaded before _joe was ready.');
514
+ }
515
+
516
+ })();
517
+