UNPKG

json-object-editor

Version:

JOE the Json Object Editor | Platform Edition

368 lines (334 loc) 15.6 kB
<!doctype html> <html> <head> <meta charset="utf-8"> <title>JOE AI Widget Test</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <style> body{font-family:system-ui,-apple-system,Segoe UI,Roboto,Ubuntu,Cantarell,Noto Sans,sans-serif;margin:20px;background:#f3f4f6;} h1{margin-top:0;} .small{font-size:13px;color:#6b7280;margin-bottom:16px; box-sizing: border-box;} .container{max-width:85%;margin:0 auto;background:#fff;border-radius:12px;box-shadow:0 4px 14px rgba(0,0,0,0.06);padding:16px;} code{background:#e5e7eb;border-radius:4px;padding:2px 4px;font-size:12px;} joe-ai-widget#widget { float: left; width: calc(100% - 300px); } div#ai-convo-panel { float: left; } </style> </head> <body> <div id="mcp-nav"></div> <script src="/JsonObjectEditor/_www/mcp-nav.js"></script> <!-- Optional markdown + sanitizer libs used by joe-ai.js when present --> <script src="https://cdn.jsdelivr.net/npm/marked@12.0.2/marked.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/dompurify@3.1.6/dist/purify.min.js"></script> <div class="container"> <h1>AI Widget Test</h1> <div class="small"> This page mounts <code>&lt;joe-ai-widget&gt;</code> and sends messages through <code>/API/plugin/chatgpt/widget*</code> using the OpenAI Responses API. It will use the <code>DEFAULT_AI_ASSISTANT</code> if configured, or fall back to model-only. </div> <div style="margin-bottom:8px; display:flex; align-items:center; justify-content:space-between; gap:8px;"> <div> <label for="ai-assistant-select" style="font-size:12px;color:#374151;">Assistant:</label> <select id="ai-assistant-select" style="margin-left:4px;padding:2px 4px;font-size:12px;"></select> <span id="ai-assistant-hint" style="font-size:11px;color:#6b7280;margin-left:4px;"></span> </div> <div id="ai-user-info" style="font-size:11px;color:#4b5563;text-align:right;white-space:nowrap;"></div> </div> <div id="ai-tools-panel" class="small" style="margin-bottom:8px; max-height:40px; overflow:auto; background:#f9fafb; border:1px solid #e5e7eb; border-radius:6px; padding:6px;"> <div style="font-weight:600;margin-bottom:4px;">Tools for selected assistant</div> <div id="ai-tools-summary" style="margin-bottom:4px;"></div> <pre id="ai-tools-json" style="white-space:pre-wrap;font-size:11px;margin:0;"></pre> </div> <div id="ai-convo-panel" class="small" style="margin-bottom:8px; max-height:200px; overflow:auto; background:#f9fafb; border:1px solid #e5e7eb; border-radius:6px; padding:6px;"> <div style="display:flex;align-items:center;justify-content:space-between;margin-bottom:4px;"> <span style="font-weight:600;">Widget Conversations</span> <button id="ai-convo-refresh" style="font-size:11px;padding:2px 6px;">Refresh</button> </div> <div id="ai-convo-status" style="margin-bottom:4px;"></div> <ul id="ai-convo-list" style="list-style:none;padding:0;margin:0;"></ul> </div> <div id="ai-convo-debug" class="small" style="margin-bottom:8px; max-height:260px; overflow:auto; background:#f9fafb; border:1px solid #e5e7eb; border-radius:6px; padding:6px;"> <div style="font-weight:600;margin-bottom:4px;">Selected Conversation (raw ai_widget_conversation)</div> <div id="ai-convo-debug-status" style="margin-bottom:4px;"></div> <pre id="ai-convo-debug-json" style="white-space:pre-wrap;font-size:11px;margin:0;"></pre> </div> <joe-ai-widget id="widget" title="JOE AI Assistant"></joe-ai-widget> <div style="clear:both;"></div> </div> <script src="/JsonObjectEditor/js/joe-ai.js"></script> <script> (function(){ function $(id){ return document.getElementById(id); } // Simple contrast helper: choose black/white text for a hex background. function textColorForBg(hex){ if (!hex || typeof hex !== 'string' || !/^#?[0-9a-fA-F]{6}$/.test(hex)) return '#000'; const h = hex[0] === '#' ? hex.slice(1) : hex; const n = parseInt(h, 16); const r = (n >> 16) & 0xff; const g = (n >> 8) & 0xff; const b = n & 0xff; const luminance = r * 0.299 + g * 0.587 + b * 0.114; return luminance > 186 ? '#000' : '#fff'; } async function fetchJSON(url){ const res = await fetch(url, { credentials: 'include' }); const text = await res.text(); try{ return JSON.parse(text); }catch(e){ console.error('[ai-widget-test] Bad JSON from', url, '->', text); throw e; } } async function initAssistantSelect(){ const select = $('ai-assistant-select'); const hint = $('ai-assistant-hint'); const widget = $('widget'); const userInfoEl = $('ai-user-info'); const convoStatus = $('ai-convo-status'); const convoList = $('ai-convo-list'); const convoRefresh = $('ai-convo-refresh'); const convoDebugStatus = $('ai-convo-debug-status'); const convoDebugJson = $('ai-convo-debug-json'); if (!select || !widget) { return; } const toolsSummaryEl = $('ai-tools-summary'); const toolsJsonEl = $('ai-tools-json'); let assistants = []; let currentUser = null; let defaultId = null; // Load current user (for color/id) from JOE auth API try{ const uResp = await fetchJSON('/API/user/current'); currentUser = (uResp && uResp.user) || null; // Expose to joe-ai.js so the widget can pick it up. window._aiWidgetUser = currentUser; if (currentUser && userInfoEl){ var uname = currentUser.fullname || currentUser.name || '(no name)'; var uid = currentUser._id || '(no _id)'; userInfoEl.textContent = 'User: ' + uname + ' [' + uid + ']'; } }catch(e){ console.error('[ai-widget-test] error loading current user', e); } // Load assistants try{ const resp = await fetchJSON('/API/item/ai_assistant'); assistants = Array.isArray(resp.item) ? resp.item : []; }catch(e){ console.error('[ai-widget-test] error loading assistants', e); } // Load settings to find DEFAULT_AI_ASSISTANT try{ const sResp = await fetchJSON('/API/item/setting'); const settings = Array.isArray(sResp.item) ? sResp.item : []; const def = settings.find(function(s){ return s && s.name === 'DEFAULT_AI_ASSISTANT'; }); defaultId = def && def.value; }catch(e){ console.error('[ai-widget-test] error loading settings', e); } const params = new URLSearchParams(location.search); const overrideId = params.get('assistant_id') || params.get('assistant') || ''; // Build options select.innerHTML = ''; const noneOption = document.createElement('option'); noneOption.value = ''; noneOption.textContent = 'None (model only)'; select.appendChild(noneOption); assistants.forEach(function(a){ const opt = document.createElement('option'); opt.value = a._id; opt.textContent = a.name || a.title || a._id; if (a._id === defaultId){ opt.textContent += ' (default)'; } select.appendChild(opt); }); // Decide initial selection let initialId = ''; if (overrideId && assistants.some(function(a){ return a._id === overrideId; })){ initialId = overrideId; if (hint){ hint.textContent = 'Using assistant from query string.'; } } else if (defaultId && assistants.some(function(a){ return a._id === defaultId; })){ initialId = defaultId; if (hint){ hint.textContent = 'Using DEFAULT_AI_ASSISTANT.'; } } else { initialId = ''; if (hint){ hint.textContent = assistants.length ? 'No default; using model only.' : 'No assistants defined; using model only.'; } } select.value = initialId; // Render tools + apply initial assistant to widget and restart conversation renderToolsForAssistant(assistants, select.value, toolsSummaryEl, toolsJsonEl); applyAssistantToWidget(widget, select.value); select.addEventListener('change', function(){ renderToolsForAssistant(assistants, select.value, toolsSummaryEl, toolsJsonEl); applyAssistantToWidget(widget, select.value); }); // Load initial conversation list and wire refresh button async function refreshConversations(){ if (!convoStatus || !convoList) { return; } try{ convoStatus.textContent = 'Loading conversations...'; convoList.innerHTML = ''; const resp = await fetchJSON('/API/item/ai_widget_conversation'); const items = Array.isArray(resp.item) ? resp.item : []; if (!items.length){ convoStatus.textContent = 'No widget conversations found.'; return; } // Sort by last_message_at or created desc items.sort(function(a,b){ var da = a.last_message_at || a.joeUpdated || a.created || ''; var db = b.last_message_at || b.joeUpdated || b.created || ''; return db.localeCompare(da); }); convoStatus.textContent = 'Click a conversation to resume it.'; items.forEach(function(c){ var li = document.createElement('li'); li.style.cursor = 'pointer'; li.style.padding = '2px 0'; li.style.borderBottom = '1px solid #e5e7eb'; li.dataset.id = c._id; var label = c.name || (c.source ? ('['+c.source+']') : '') || c._id; var ts = c.last_message_at || c.joeUpdated || c.created || ''; var msgCount = Array.isArray(c.messages) ? c.messages.length : 0; var userLabel = c.user_name || c.user || ''; var userColor = c.user_color || ''; var userChip = ''; if (userLabel){ if (userColor){ var fg = textColorForBg(userColor); userChip = ' <span style="margin-left:4px;padding:1px 6px;border-radius:10px;background:'+userColor+';color:'+fg+';">'+userLabel+'</span>'; } else { userChip = ' <span style="margin-left:4px;color:#111827;">'+userLabel+'</span>'; } } li.innerHTML = '<strong>'+label+'</strong>' + (ts ? ' <span style="color:#6b7280;">'+ts+'</span>' : '') + userChip + ' <span style="color:#4b5563;">['+msgCount+' msgs]</span>'; li.addEventListener('click', function(){ try{ applyConversationToWidget(widget, c._id); loadConversationDebug(c._id); }catch(e){ console.error('[ai-widget-test] error applying conversation', e); } }); convoList.appendChild(li); }); }catch(e){ console.error('[ai-widget-test] error loading conversations', e); convoStatus.textContent = 'Error loading conversations; see console.'; } } if (convoRefresh){ convoRefresh.addEventListener('click', function(){ refreshConversations(); }); } // Initial load refreshConversations(); } async function loadConversationDebug(conversationId){ if (!conversationId || !document.getElementById('ai-convo-debug-json')) return; const statusEl = document.getElementById('ai-convo-debug-status'); const preEl = document.getElementById('ai-convo-debug-json'); try{ statusEl.textContent = 'Loading conversation '+conversationId+'...'; preEl.textContent = ''; const resp = await fetchJSON('/API/object/ai_widget_conversation/_id/' + encodeURIComponent(conversationId)); if (!resp || resp.error){ statusEl.textContent = (resp && resp.error) || 'Conversation not found.'; return; } statusEl.textContent = 'ai_widget_conversation '+conversationId; preEl.textContent = JSON.stringify(resp, null, 2); }catch(e){ console.error('[ai-widget-test] error loading conversation debug', e); statusEl.textContent = 'Error loading conversation; see console.'; } } function renderToolsForAssistant(assistants, assistantId, summaryEl, jsonEl){ if (!summaryEl || !jsonEl) { return; } const id = assistantId || ''; const asst = assistants.find(function(a){ return a && a._id === id; }) || null; if (!asst) { summaryEl.textContent = 'No assistant selected; no tools.'; jsonEl.textContent = ''; return; } let tools = []; try{ if (Array.isArray(asst.tools)) { tools = asst.tools; } else if (typeof asst.tools === 'string' && asst.tools.trim()) { // Many JOE schemas store codeeditor JSON as a string; try to parse. tools = JSON.parse(asst.tools); } }catch(e){ console.error('[ai-widget-test] error parsing assistant.tools JSON', e, asst.tools); summaryEl.textContent = 'Error parsing tools JSON for this assistant. See console.'; jsonEl.textContent = String(asst.tools || ''); return; } if (!tools.length) { summaryEl.textContent = 'This assistant has no tools configured.'; jsonEl.textContent = ''; return; } const names = tools .map(function(t){ return t && t.function && t.function.name || t.name || '[unnamed]'; }) .join(', '); summaryEl.textContent = 'Configured tools: ' + names; jsonEl.textContent = JSON.stringify(tools, null, 2); } function applyAssistantToWidget(widget, assistantId){ try{ if (!widget) { return; } const val = assistantId || ''; if (val){ widget.setAttribute('ai_assistant_id', val); } else { widget.removeAttribute('ai_assistant_id'); } // Reset conversation so the widget starts fresh with new assistant. // Do NOT create a new conversation here; the widget will lazily // create one on first user message via startConversation(). widget.removeAttribute('conversation_id'); widget.conversation_id = null; }catch(e){ console.error('[ai-widget-test] error applying assistant selection', e); } } function applyConversationToWidget(widget, conversationId){ if (!widget || !conversationId) { return; } widget.setAttribute('conversation_id', conversationId); widget.conversation_id = conversationId; // Let the widget load the existing history instead of starting a new convo if (typeof widget.loadHistory === 'function'){ widget.loadHistory(); } else if (widget.connectedCallback){ widget.connectedCallback(); } } if (document.readyState === 'loading'){ document.addEventListener('DOMContentLoaded', initAssistantSelect); } else { initAssistantSelect(); } })(); </script> </body> </html>