UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

517 lines (435 loc) 19.5 kB
(function(){ // Define the joeAI namespace const Ai = {}; const self = this; Ai._openChats = {}; // Conversation ID -> element Ai.default_ai = null; // Default AI assistant ID // ========== COMPONENTS ========== class JoeAIChatbox extends HTMLElement { constructor() { super(); this.attachShadow({ mode: 'open' }); this.messages = []; } connectedCallback() { this.conversation_id = this.getAttribute('conversation_id'); this.selected_assistant_id = Ai.default_ai?Ai.default_ai.value:null; this.ui = {}; if (!this.conversation_id) { this.renderError("Missing conversation_id"); return; } this.loadConversation(); this.getAllAssistants(); } async getAllAssistants() { const res = await fetch('/API/item/ai_assistant'); const result = await res.json(); if (result.error) { console.error("Error fetching assistants:", result.error); } this.allAssistants = {}; if (Array.isArray(result.item)) { result.item.map(a => { if (a._id) { this.allAssistants[a._id] = a; } if (a.openai_id) { this.allAssistants[a.openai_id] = a; // Optional dual-key if you prefer } }); } } async loadConversation() { try { const res = await fetch(`/API/object/ai_conversation/_id/${this.conversation_id}`); const convo = await res.json(); if (!convo || convo.error) { this.renderError("Conversation not found."); return; } this.conversation = convo; this.messages = []; if (convo.thread_id) { const resThread = await fetch(`/API/plugin/chatgpt-assistants/getThreadMessages?thread_id=${convo.thread_id}`); const threadMessages = await resThread.json(); this.messages = threadMessages?.messages || []; } this.render(); } catch (err) { console.error("Chatbox load error:", err); this.renderError("Error loading conversation."); } } getAssistantInfo(id){ const assistant = this.allAssistants[id]; if (assistant) { return assistant; } else { console.warn("Assistant not found:", id); return null; } } render() { const convoName = this.conversation.name || "Untitled Conversation"; const convoInfo = this.conversation.info || ""; const chatMessages = this.messages.map(msg => this.renderMessage(msg)).reverse().join(''); const assistantOptions = (this.conversation.assistants || []).map(a => { const meta = this.getAssistantInfo(a) || {}; const label = meta.name || a.name || a.title || 'Assistant'; const value = meta._id; const selected = value === this.selected_assistant_id ? 'selected' : ''; return `<option value="${value}" ${selected}>${label}</option>`; }).join(''); const assistantSelect = ` <label-select-wrapper> <label class="assistant-select-label" title="joe ai assistants">${_joe && _joe.SVG.icon.assistant}</label> <select id="assistant-select"> ${assistantOptions} </select> </label-select-wrapper> `; // Inject external CSS const styleLink = document.createElement('link'); styleLink.setAttribute('rel', 'stylesheet'); styleLink.setAttribute('href', '/JsonObjectEditor/css/joe-ai.css'); // Adjust path as needed this.shadowRoot.appendChild(styleLink); // Build inner HTML in a wrapper const wrapper = document.createElement('div'); wrapper.className = 'chatbox'; wrapper.innerHTML = ` <div class="close-btn" title="Close Chatbox" >${_joe.SVG.icon.close}</div> <div class="header"> <h2>${convoName}</h2> <p>${convoInfo}</p> ${assistantSelect} </div> <div class="messages">${chatMessages}</div> <div class="inputRow"> <textarea id="chat-input" type="text" placeholder="Type a message..."></textarea> <button id="send-button">Send</button> </div> `; this.shadowRoot.appendChild(wrapper); const messagesDiv = this.shadowRoot.querySelector('.messages'); messagesDiv.scrollTop = messagesDiv.scrollHeight; this.shadowRoot.getElementById('send-button').addEventListener('click', () => this.sendMessage()); // Wire up the close button this.shadowRoot.querySelector('.close-btn').addEventListener('click', () => { //this.closeChat() this.closest('joe-ai-chatbox').closeChat(); }); this.ui.assistant_select = this.shadowRoot.querySelector('#assistant-select'); this.ui.assistant_select?.addEventListener('change', (e) => { this.selected_assistant_id = e.target.value; }); this.selected_assistant_id = this.ui.assistant_select?.value || null; } renderMessage(msg) { const role = msg.role || 'user'; const classes = `message ${role}`; let contentText = ''; if (Array.isArray(msg.content)) { // OpenAI style: array of parts contentText = msg.content.map(part => { if (part.type === 'text' && part.text && part.text.value) { return part.text.value; } return ''; }).join('\n'); } else if (typeof msg.content === 'string') { contentText = msg.content; } else { contentText = '[Unsupported message format]'; } // Build timestamp const createdAt = msg.created_at ? new Date(msg.created_at * 1000) : null; // OpenAI sends timestamps in seconds const timestamp = createdAt ? createdAt.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }) : ''; return ` <div class="${classes}"> <div class="meta"> <participant-name>${role.toUpperCase()}</participant-name> ${timestamp ? `<span class="timestamp">${timestamp}</span>` : ''}</div> <div class="content">${contentText}</div> </div> `; } renderError(message) { this.shadowRoot.innerHTML = `<div style="color:red;">${message}</div>`; } async getResponse(conversation_id,content,role,assistant_id){ const response = await fetch('/API/plugin/chatgpt-assistants/addMessage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversation_id: conversation_id, content: content, role: role||'system', assistant_id: assistant_id||Ai.default_ai?Ai.default_ai.value:null }) }).then(res => res.json()); } async sendMessage() {//** */ const input = this.shadowRoot.getElementById('chat-input'); const message = input.value.trim(); if (!message) return; input.disabled = true; this.shadowRoot.getElementById('send-button').disabled = true; try { const response = await fetch('/API/plugin/chatgpt-assistants/addMessage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversation_id: this.conversation_id, content: message, assistant_id: this.selected_assistant_id||Ai.default_ai?Ai.default_ai.value:null }) }).then(res => res.json()); if (response.success && response.runObj) { this.currentRunId = response.runObj.id; // Store the run ID for polling await this.loadConversation(); // reload messages this.startPolling(); // 🌸 start watching for assistant reply! input.value = ''; } else { alert('Failed to send message.'); } } catch (err) { console.error('Send message error:', err); alert('Error sending message.'); } input.disabled = false; this.shadowRoot.getElementById('send-button').disabled = false; } startPolling() { if (this.pollingInterval) return; // Already polling // Insert thinking message this.showThinkingMessage(); this.pollingInterval = setInterval(async () => { const runRes = await fetch(`/API/plugin/chatgpt-assistants/getRunStatus?thread_id=${this.conversation.thread_id}&run_id=${this.currentRunId}`); const run = await runRes.json(); if (run.status === 'completed') { const resThread = await fetch(`/API/plugin/chatgpt-assistants/getThreadMessages?thread_id=${this.conversation.thread_id}`); //const activeAssistant = this.conversation.assistants.find(a => a.openai_id === run.assistant_id); const threadMessages = await resThread.json(); if (threadMessages?.messages) { this.messages = threadMessages.messages; //this.render(); } clearInterval(this.pollingInterval); this.pollingInterval = null; this.hideThinkingMessage(); } }, 2000); } showThinkingMessage() { const messagesDiv = this.shadowRoot.querySelector('.messages'); if (!messagesDiv) return; // Pull assistant thinking text const assistant = this.getAssistantInfo(this.selected_assistant_id); const thinkingText = assistant?.assistant_thinking_text || 'Assistant is thinking...'; const div = document.createElement('div'); div.className = 'thinking-message'; div.textContent = thinkingText; div.id = 'thinking-message'; messagesDiv.appendChild(div); messagesDiv.scrollTop = messagesDiv.scrollHeight; } hideThinkingMessage() { const existing = this.shadowRoot.querySelector('#thinking-message'); if (existing) existing.remove(); } closeChat() { // Remove the element this.remove(); // Clean up from open chat registry if possible if (_joe && _joe.Ai && _joe.Ai._openChats) { delete _joe.Ai._openChats[this.conversation_id]; } } } customElements.define('joe-ai-chatbox', JoeAIChatbox); //**YES** Ai.spawnChatHelper = async function(object_id,user_id=_joe.User._id,conversation_id) { //if not conversation_id, create a new one let convo_id = conversation_id; var newChat = false; if(!convo_id){ const response = await fetch('/API/plugin/chatgpt-assistants/createConversation', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ object_id, user_id }) }).then(res => res.json()); convo_id = response?.conversation?._id; newChat = true; if(response.error){ console.error('❌ Failed to create conversation:', response.error); return; } } await _joe.Ai.spawnContextualChat(convo_id,{object_id,newChat}); } // ========== HELPERS ========== Ai.spawnContextualChat = async function(conversationId, options = {}) { if (!conversationId) { console.warn("Missing conversation ID for chat spawn."); return; } Ai.default_ai = _joe.Data.setting.where({name:'DEFAULT_AI_ASSISTANT'})[0]||false; // 1. Check if chat already open if (Ai._openChats[conversationId]) { console.log("Chatbox already open for", conversationId); Ai._openChats[conversationId].scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } try { const flattened = _joe.Object.flatten(options.object_id); if (options.newChat) { // 2. Prepare context const contextInstructions = _joe.Ai.generateContextInstructions(flattened,options.object_id); // 3. Inject context into backend const contextResult = await fetch('/API/plugin/chatgpt-assistants/addMessage', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ conversation_id: conversationId, role: 'system', content: contextInstructions, assistant_id: Ai.default_ai.value }) }).then(res => res.json()); if (!contextResult || contextResult.error) { console.error('❌ Failed to inject context:', contextResult?.error); return; } } // 4. Create new chatbox const chat = document.createElement('joe-ai-chatbox'); chat.setAttribute('conversation_id', conversationId); const screenWidth = window.innerWidth; if(screenWidth <= 768){ chat.setAttribute('mobile', 'true'); chat.style.width = 'auto'; chat.style.left = '0px'; } else{ chat.setAttribute('mobile', 'false'); chat.style.width = options.width || '640px'; chat.style.left = 'auto'; } // Apply styles //chat.style.height = options.height || '420px'; chat.style.bottom = options.bottom || '50px'; chat.style.right = options.right || '0px'; chat.style.top = options.top || '100px'; chat.style.position = 'fixed'; chat.style.zIndex = '10000'; chat.style.background = '#efefef'; chat.style.border = '1px solid #fff'; chat.style.borderRadius = '8px'; chat.style.boxShadow = '0px 1px 4px rgba(0, 0, 0, 0.3)'; chat.style.padding = '5px'; chat.style.margin = '5px'; document.body.appendChild(chat); // 5. Track it Ai._openChats[conversationId] = chat; if (options.newChat) { // 6. Show soft local UI message _joe.Ai.injectSystemMessage(conversationId, `Context injected: ${flattened.name || flattened.title || 'Object'} (${flattened._id})`); } } catch (err) { console.error('❌ spawnChat context injection failed:', err); } }; /* Ai.spawnChat = function(conversationId, options = {}) { if (!conversationId) { console.warn("Missing conversation ID for chat spawn."); return; } // 1. Check if chatbox already open if (Ai._openChats[conversationId]) { console.log("Chatbox already open for", conversationId); Ai._openChats[conversationId].scrollIntoView({ behavior: 'smooth', block: 'center' }); return; } // 2. Create new chatbox const chat = document.createElement('joe-ai-chatbox'); chat.setAttribute('conversation_id', conversationId); const flattened = _joe.Object.flatten(); const contextInstructions = _joe.Ai.generateContextInstructions(flattened); // Actually inject into AI backend (for assistant awareness) if you have that later // For now: silently show a soft system bubble _joe.Ai.injectSystemMessage(conversationId, `Context injected: ${flattened.name || flattened.title || 'Object'} (${flattened._id})`); // Apply styles chat.style.width = options.width || '400px'; chat.style.height = options.height || '420px'; chat.style.bottom = options.bottom || '20px'; chat.style.right = options.right || '20px'; chat.style.position = 'fixed'; chat.style.zIndex = '10000'; chat.style.background = '#efefef'; chat.style.border = '1px solid #fff'; chat.style.borderRadius = '8px'; chat.style.boxShadow = '0px 2px 10px rgba(0,0,0,0.1)'; chat.style.padding = '5px'; document.body.appendChild(chat); // 3. Track it Ai._openChats[conversationId] = chat; return chat; // 4. Optionally clean up when chatbox is removed (if you wire close buttons later) }; */ Ai.generateContextInstructions = function(flattenedObj,object_id) { if (!flattenedObj) return ''; let context = `{{{BEGIN_OBJECT:${object_id}}}}`+ "Context: You are assisting the user with the following object:\n\n"; context += JSON.stringify(flattenedObj, null, 2) + "\n\n"; // for (const [key, value] of Object.entries(flattenedObj)) { // if (typeof value === 'object' && value !== null) { // context += `- ${key}: (linked object)\n`; // for (const [subkey, subval] of Object.entries(value)) { // context += ` • ${subkey}: ${subval}\n`; // } // } else { // context += `- ${key}: ${value}\n`; // } // } context += `\nAlways refer to this context when answering questions or completing tasks related to this object.\n`+ `{{{END_OBJECT:${object_id}}}}`; return context; }; Ai.injectSystemMessage = async function(conversationId, text) { if (!conversationId || !text) return; try { // Create a system-style message object const messageObj = { conversation_id: conversationId, role: 'joe', content: text, created: new Date().toISOString() }; // You could either push this directly into chatbox if loaded, or update server messages if you have backend ready const chatbox = document.querySelector(`joe-ai-chatbox[conversation_id="${conversationId}"]`); if (chatbox) { if (!chatbox.messages) { chatbox.messages = []; } chatbox.messages.push(messageObj); // Optionally trigger a soft re-render of chatbox if needed if (typeof chatbox.renderMessages === 'function') { chatbox.renderMessages(); } } } catch (err) { console.error("❌ injectSystemMessage failed:", err); } }; // Attach AI to _joe if (window._joe) { _joe.Ai = Ai; } else { console.warn('joeAI.js loaded before _joe was ready.'); } })();