json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
368 lines (334 loc) • 15.6 kB
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><joe-ai-widget></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>