json-object-editor
Version:
JOE the Json Object Editor | Platform Edition
403 lines (373 loc) • 19.9 kB
JavaScript
// Joe AI Assistant Schema
// This schema is used to manage AI Assistants within Joe, including syncing with OpenAI.
var schema = {
title: "Ai Assistant | ${name}",
display: "Ai Assist",
info: "An AI Assistant configuration linked to OpenAI, managed within Joe.",
// Curated summary for agents and tools
summary:{
description:'Configuration record for an AI assistant connected to OpenAI.',
purpose:'Use ai_assistant to define the model, instructions, tools (MCP-backed), color, and OpenAI assistant_id for chat experiences in JOE (in-app chatbox and embeddable widget). One ai_assistant typically maps to a single OpenAI assistant and can be marked as the default via the DEFAULT_AI_ASSISTANT setting.',
labelField:'name',
defaultSort:{ field:'joeUpdated', dir:'desc' },
searchableFields:['name','info','assistant_id','ai_model','tags','datasets'],
allowedSorts:['joeUpdated','created','name','last_synced'],
relationships:{
outbound:[
{ field:'status', targetSchema:'status', cardinality:'one' },
{ field:'datasets', targetSchema:'<schemaName>', cardinality:'many' },
{ field:'files', targetSchema:'file', cardinality:'many' }
],
inbound:{ graphRef:'server/relationships.graph.json' }
},
joeManagedFields:['created','joeUpdated'],
fields:[
{ name:'_id', type:'string', required:true },
{ name:'itemtype', type:'string', required:true, const:'ai_assistant' },
{ name:'name', type:'string', required:true },
{ name:'info', type:'string' },
{ name:'ai_model', type:'string' },
{ name:'assistant_id', type:'string' },
{ name:'openai_assistant_version', type:'string' },
{ name:'file_search_enabled', type:'boolean' },
{ name:'code_interpreter_enabled', type:'boolean' },
{ name:'assistant_thinking_text', type:'string' },
{ name:'assistant_color', type:'string' },
{ name:'instructions', type:'string' },
{ name:'tools', type:'string' },
{ name:'files', type:'string', isArray:true, isReference:true, targetSchema:'file' },
{ name:'file_ids', type:'string', isArray:true },
{ name:'datasets', type:'string', isArray:true },
{ name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' },
{ name:'status', type:'string', isReference:true, targetSchema:'status' },
{ name:'last_synced', type:'string', format:'date-time' },
{ name:'joeUpdated', type:'string', format:'date-time' },
{ name:'created', type:'string', format:'date-time' }
]
},
menuicon: `<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1024 1024">
<path d="M294.49 618.18v64.49c0 22.42.95 35.46 2.84 39.12 2.4 4.94 6.93 8.52 12.29 9.71 9.18 2.13 18.6 3.01 28.02 2.64h17.31c18.81 0 28.22 6.04 28.22 18.11 0 13.96-9.93 22.51-29.8 25.64-24.41 2.74-48.98 3.87-73.55 3.37-29.87 0-51.01-5.79-63.44-17.38-12.42-11.59-18.66-31.41-18.7-59.47v-81.08c.85-11.33-1.15-22.68-5.81-33.04-5.69-7.66-14.07-12.88-23.46-14.6-11.09-2.78-18.54-13.19-17.58-24.58-1.07-11.43 6.42-21.9 17.58-24.58 9.41-1.95 17.77-7.32 23.46-15.07 4.7-10.35 6.7-21.71 5.81-33.04v-80.88c0-27.62 6.21-47.22 18.63-58.81 12.42-11.59 33.57-17.36 63.44-17.31 26.13-.73 52.27.66 78.17 4.16 6.92 1.05 13.37 4.16 18.5 8.92 4.27 4.08 6.66 9.75 6.61 15.66 0 12.07-9.41 18.11-28.22 18.11h-17.84c-16.04 0-27.05 1.72-33.04 5.15-5.92 3.96-9.36 10.72-9.05 17.84v92.71c-.35 31.23-8.72 52.03-25.11 62.38-1.29.88-2.06 2.34-2.05 3.9 0 2.42 2.93 6.06 8.79 10.9 6.54 5.96 11.3 13.63 13.74 22.14 3.19 11.34 4.62 23.11 4.23 34.89Zm435.01-134.4v-64.23c0-22.42-.95-35.46-2.84-39.12-2.4-4.94-6.93-8.52-12.29-9.71-9.18-2.13-18.6-3.01-28.02-2.64h-17.18c-18.81 0-28.22-6.04-28.22-18.11 0-13.96 9.93-22.42 29.8-25.37 24.42-2.73 48.98-3.86 73.55-3.37 29.87 0 51.01 5.79 63.44 17.38 12.42 11.59 18.63 31.19 18.63 58.81v81.28c-.88 11.33 1.12 22.69 5.81 33.04 5.75 7.62 14.1 12.87 23.46 14.74 11.16 2.71 18.62 13.22 17.51 24.65.74 9.18-4.03 17.93-12.16 22.27-4.09 1.86-8.33 3.34-12.69 4.43-8.51 2.69-15.35 9.08-18.63 17.38-2.71 9.15-3.85 18.69-3.37 28.22v81.01c0 27.8-6.21 47.47-18.63 59.01-12.42 11.54-33.57 17.33-63.44 17.38-26.13.74-52.27-.65-78.17-4.16-6.92-1.05-13.37-4.16-18.5-8.92-4.31-4.17-6.7-9.93-6.61-15.93 0-12.07 9.41-18.11 28.22-18.11 2.25 0 8.19.18 17.84.53 11.25.56 22.5-1.19 33.04-5.15 4.1-2.12 7.02-5.98 7.93-10.51 1.33-11.91 1.82-23.9 1.45-35.88v-64.49c-.51-12.88 1.38-25.74 5.55-37.93 4.19-9.77 11.07-18.15 19.82-24.18 1.29-.88 2.06-2.34 2.05-3.9 0-2.42-2.93-6.06-8.79-10.9-6.55-5.96-11.31-13.63-13.74-22.14-3.43-11.46-5.05-23.39-4.82-35.35Z"/>
<rect width="74.5" height="189" x="409.75" y="456.75" rx="33.94" ry="33.94"/>
<rect width="74.5" height="189" x="539.75" y="456.75" rx="33.94" ry="33.94"/>
</svg>`,
listView:{
title: '<joe-title>${name}</joe-title><joe-subtitle>${info}</joe-subtitle><joe-subtitle>${assistant_id}</joe-subtitle>',
listWindowTitle: 'Ai Assistants'
},
subsets:function(){
var schemas = [];
var subs = [];
_joe.current.list.map(function(asst){
schemas = schemas.concat(asst.datasets||[]);
});
(new Set(schemas)).map(function(schema){
subs.push({name:schema,filter:{datasets:{$in:[schema]}}})
});
return subs;
},
stripeColor: function(ai_assistant) {
//use the stripe color from ai_assistant status object if it has one
if (ai_assistant.status){
var status = _joe.Cache.get(ai_assistant.status);
if (status && status.color) {
return {color:status.color,title:status.name};
}
}
},
bgColor: function(ai_assistant) {
//if it's the default assistant, use goldenrod color
let def_id = _joe.Data.setting.where({name:'DEFAULT_AI_ASSISTANT'})[0]||false;
if (def_id && def_id.value == ai_assistant._id) {
return {color:'#DAA520'};
}
},
methods: {
syncAssistantToOpenAI: function(currentObject, event) {
var $button = $(event.target).closest('joe-button');
var originalHTML = $button.html();
if (!currentObject || !currentObject._id) {
alert("No valid record to sync.");
return;
}
const url = `/API/plugin/chatgpt-assistants/syncAssistantToOpenAI?_id=${currentObject._id}`;
// Disable button and show spinner
$button.prop('disabled', true);
$button.html(`<svg class="joe-loading-spinner" viewBox="0 0 50 50" xmlns="http://www.w3.org/2000/svg"><circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="5"/></svg> Syncing...`);
fetch(url)
.then(response => response.json())
.then(response => {
if (response && response.success) {
console.log("Assistant synced successfully:", response);
// 🔄 Refresh the object after sync
_joe.Object.getFromServer(currentObject._id, { goto: true }, function(success) {
if (success) {
console.log("Assistant reloaded into Joe UI.");
//_joe.updateObject(null,null,true);
} else {
console.warn("Failed to reload Assistant object.");
}
$button.prop('disabled', false);
$button.html(originalHTML);
});
} else {
alert("Sync failed. See console for details.");
console.error(response);
$button.prop('disabled', false);
$button.html(originalHTML);
}
})
.catch(err => {
alert("Error syncing Assistant. See console.");
console.error(err);
$button.prop('disabled', false);
$button.html(originalHTML);
});
},
loadMCPTolsIntoAssistant: async function(currentObject, event){
try{
var $button = $(event.target).closest('joe-button');
var originalHTML = $button.html();
$button.prop('disabled', true);
$button.html('Loading MCP tools...');
var base = location.origin.replace(/\/$/,'');
var manifestUrl = base + '/.well-known/mcp/manifest.json';
const manifest = await fetch(manifestUrl).then(r=>{
if(!r.ok){ throw new Error('HTTP '+r.status); }
return r.json();
});
var tools = (manifest.tools||[]).map(function(t){
return {
type:'function',
function:{
name: t.name,
description: t.description || '',
parameters: t.params || { type:'object' }
}
};
});
var json = JSON.stringify(tools, null, 2);
// Update current object and visible tools field via core helper
currentObject.tools = json;
if (window._joe && _joe.Fields && typeof _joe.Fields.set === 'function') {
_joe.Fields.set('tools', json);
} else {
// Fallback to direct DOM update if helper is unavailable
var $field = $('.joe-object-field[data-name="tools"]').find('.joe-field').eq(0);
if($field && $field.length){
$field.val(json);
$field.trigger('input').trigger('change');
}
}
$button.prop('disabled', false);
$button.html(originalHTML);
(_joe.toast && _joe.toast('Loaded '+tools.length+' MCP tools into assistant.')) || alert('Loaded '+tools.length+' MCP tools.');
}catch(e){
console.error('loadMCPTolsIntoAssistant error', e);
alert('Failed to load MCP tools: '+(e.message||e));
try{
var $button = $(event.target).closest('joe-button');
$button.prop('disabled', false);
}catch(_e){}
}
},
setAsDefaultAssistant: async function(currentObject, event){
try{
if(!currentObject || !currentObject._id){
alert('No assistant loaded'); return;
}
var $button = $(event.target).closest('joe-button');
var originalHTML = $button.html();
$button.prop('disabled', true);
$button.html('Setting default...');
// Find existing DEFAULT_AI_ASSISTANT setting (if any)
var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
// Build querystring for /API/save
var params = new URLSearchParams();
params.set('itemtype','setting');
params.set('name','DEFAULT_AI_ASSISTANT');
params.set('setting_type', (existing && existing.setting_type) || 'text');
params.set('info', (existing && existing.info) || 'Default ai_assistant used for chats and widgets.');
params.set('value', currentObject._id);
var url;
if(existing && existing._id){
url = '/API/save/' + encodeURIComponent(existing._id) + '?' + params.toString();
}else{
url = '/API/save/?' + params.toString();
}
const resp = await fetch(url, {
method: 'GET',
credentials: 'include'
}).then(r => r.json());
if(!resp || resp.status !== 'success'){
const msg = (resp && resp.error) || 'Save failed';
throw new Error(msg);
}
var saved = resp.results || resp.object || null;
if(saved){
if(existing){
Object.assign(existing, saved);
}else{
_joe.Data.setting = _joe.Data.setting || [];
_joe.Data.setting.push(saved);
}
}
$button.prop('disabled', false);
$button.html(originalHTML);
(_joe.toast && _joe.toast('Default assistant set.')) || alert('Default assistant set.');
}catch(e){
console.error('setAsDefaultAssistant error', e);
alert('Failed to set default assistant: '+(e.message||e));
try{
var $button = $(event.target).closest('joe-button');
$button.prop('disabled', false);
}catch(_e){}
}
}
},
fields: function() {
return [
"name:text:40%", // Assistant Name
"info:text:60%", // Assistant Subtitle (optional)
{ section_start: "openai_config", display: "OpenAI" },
'ai_model',
{ name: "assistant_id", type: "text", display: "OpenAI Assistant ID", locked: true,
comment: function(ai_assistant) {
if (!ai_assistant.assistant_id) {
return 'No Assistant created yet. Save to generate a Playground link.';
}
return `<a href="https://platform.openai.com/playground/assistants?assistant=${ai_assistant.assistant_id}" target="_blank" style="color: #3498db;">View this Assistant in OpenAI Playground</a>`;
}
},
{ name: "openai_assistant_version", type: "text", default: "v1", hidden: true },
{
name: "file_search_enabled",
type: "boolean",
display: "Enable File Search",
default: false,
width:"50%"
},
{
name: "code_interpreter_enabled",
type: "boolean",
display: "Enable Code Interpreter",
default: false,
width:"50%"
},
{ section_end: "openai_config" },
{ section_start: "instructions", display: "instructions" },
{
name: "assistant_thinking_text",
type: "text",
display: "Assistant Thinking Text",
info: "What the user should see while the Assistant is thinking.",
placeholder: "e.g. 'Assistant is preparing a response...'"
},
{ name: "assistant_color", type: "color", display: "Assistant Color", default: "teal" },
{ name: "instructions", type: "wysiwyg" }, // Full TinyMCE editor
{ section_end: "instructions" },
{ section_start: "tools", display: "Function Tools",
collapsed:function(asst) { return !asst.tools; }
},
{
name: "tools",
type: "code",
display: "Tool Definitions (JSON)",
comment: '<div>Paste or edit full JSON function definitions here:</div><div><a href="https://platform.openai.com/docs/assistants/tools/function-calling" target="_blank" style="color: #3498db;">View OpenAI Function Calling Documentation</a></div><pre>{<br/> "type": "function",<br/> "function": {<br/> "name": "your_function",<br/> "description": "Describe what your function does.",<br/> "parameters": {<br/> "type": "object",<br/> "properties": {<br/> "theme": {<br/> "type": "string",<br/> "description": "The theme to suggest (e.g., hope, perseverance)."<br/> }<br/> }<br/> }<br/> }<br/>}</pre><br/>'
,
height: "300px"
},
{
name: "load_mcp_tools",
type: "button",
display: "Load MCP Tools",
method: "loadMCPTolsIntoAssistant",
color: "blue",
title: "Fetch MCP tools from this JOE instance and populate the tools JSON array."
},
{ section_end: "tools" },
{ section_start: "files" },
{
name: "files",
type: "group",
display: "Attached Files",
values: function() {
return _joe.getDataset("file");
},
template: "${name}",
idprop: "_id"
},
{
name: "file_ids",
type: "content",
display: "File IDs (auto populated on sync)",
comment: "Auto-managed list of OpenAI file_ids attached to this Assistant."
},
{ section_end: "files" },
{section_start:'categorization',collapsed:true},
'datasets',
{section_end:'categorization'},
{ section_start: "system", collapsed: true },
"_id",
"created",
"itemtype",
{ section_end: "system" },
{ sidebar_start: "right", collapsed: false },
"tags",
"status",
{
name: "last_synced",
type: "date",
display: "Last Synced",
locked:true,
comment: "Last time this Assistant was updated at OpenAI."
},
{
name: "sync_button",
type: "button",
display: "Sync to OpenAI",
method: "syncAssistantToOpenAI",
color: "green",
title: "Sync this Assistant with OpenAI"
},
{
name: "set_default_assistant",
type: "button",
display: "Set as Default Assistant",
method: "setAsDefaultAssistant",
color: "orange",
hidden:function(ai_assistant){
try{
var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
return !!(existing && existing.value === ai_assistant._id);
}catch(e){
return false;
}
},
title: "Mark this assistant as the DEFAULT_AI_ASSISTANT setting.",
locked:function(ai_assistant){
try{
var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
return !!(existing && existing.value === ai_assistant._id);
}catch(e){
return false;
}
}
},
// {
// name: "test_button",
// type: "button",
// display: "Test Button",
// method: "testButtonClick", // <- the method name you want to call
// color: "orange", // <- Joe color class (joe-orange-button)
// title: "Click to test button functionality",
// schema: "ai_assistant" // <- schema to pull the method from (optional for now, we lock it anyway)
// },
{ sidebar_end: "right" },
];
},
idprop: "_id"
};
module.exports = schema;