json-object-editor 0.10.625 → 0.10.632

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.
@@ -8,7 +8,7 @@ var schema = {
8
8
  // Curated summary for agents and tools
9
9
  summary:{
10
10
  description:'Configuration record for an AI assistant connected to OpenAI.',
11
- purpose:'Use to store instructions, default model, tools, and file attachments for assistants used by JOE chat UIs and widgets. One ai_assistant typically maps to a single OpenAI assistant_id.',
11
+ 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.',
12
12
  labelField:'name',
13
13
  defaultSort:{ field:'joeUpdated', dir:'desc' },
14
14
  searchableFields:['name','info','assistant_id','ai_model','tags','datasets'],
@@ -33,6 +33,7 @@ var schema = {
33
33
  { name:'file_search_enabled', type:'boolean' },
34
34
  { name:'code_interpreter_enabled', type:'boolean' },
35
35
  { name:'assistant_thinking_text', type:'string' },
36
+ { name:'assistant_color', type:'string' },
36
37
  { name:'instructions', type:'string' },
37
38
  { name:'tools', type:'string' },
38
39
  { name:'files', type:'string', isArray:true, isReference:true, targetSchema:'file' },
@@ -129,6 +130,117 @@ var schema = {
129
130
  $button.prop('disabled', false);
130
131
  $button.html(originalHTML);
131
132
  });
133
+ },
134
+ loadMCPTolsIntoAssistant: async function(currentObject, event){
135
+ try{
136
+ var $button = $(event.target).closest('joe-button');
137
+ var originalHTML = $button.html();
138
+ $button.prop('disabled', true);
139
+ $button.html('Loading MCP tools...');
140
+
141
+ var base = location.origin.replace(/\/$/,'');
142
+ var manifestUrl = base + '/.well-known/mcp/manifest.json';
143
+ const manifest = await fetch(manifestUrl).then(r=>{
144
+ if(!r.ok){ throw new Error('HTTP '+r.status); }
145
+ return r.json();
146
+ });
147
+ var tools = (manifest.tools||[]).map(function(t){
148
+ return {
149
+ type:'function',
150
+ function:{
151
+ name: t.name,
152
+ description: t.description || '',
153
+ parameters: t.params || { type:'object' }
154
+ }
155
+ };
156
+ });
157
+ var json = JSON.stringify(tools, null, 2);
158
+
159
+ // Update current object and visible tools field via core helper
160
+ currentObject.tools = json;
161
+ if (window._joe && _joe.Fields && typeof _joe.Fields.set === 'function') {
162
+ _joe.Fields.set('tools', json);
163
+ } else {
164
+ // Fallback to direct DOM update if helper is unavailable
165
+ var $field = $('.joe-object-field[data-name="tools"]').find('.joe-field').eq(0);
166
+ if($field && $field.length){
167
+ $field.val(json);
168
+ $field.trigger('input').trigger('change');
169
+ }
170
+ }
171
+
172
+ $button.prop('disabled', false);
173
+ $button.html(originalHTML);
174
+ (_joe.toast && _joe.toast('Loaded '+tools.length+' MCP tools into assistant.')) || alert('Loaded '+tools.length+' MCP tools.');
175
+ }catch(e){
176
+ console.error('loadMCPTolsIntoAssistant error', e);
177
+ alert('Failed to load MCP tools: '+(e.message||e));
178
+ try{
179
+ var $button = $(event.target).closest('joe-button');
180
+ $button.prop('disabled', false);
181
+ }catch(_e){}
182
+ }
183
+ },
184
+ setAsDefaultAssistant: async function(currentObject, event){
185
+ try{
186
+ if(!currentObject || !currentObject._id){
187
+ alert('No assistant loaded'); return;
188
+ }
189
+ var $button = $(event.target).closest('joe-button');
190
+ var originalHTML = $button.html();
191
+ $button.prop('disabled', true);
192
+ $button.html('Setting default...');
193
+
194
+ // Find existing DEFAULT_AI_ASSISTANT setting (if any)
195
+ var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
196
+ var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
197
+
198
+ // Build querystring for /API/save
199
+ var params = new URLSearchParams();
200
+ params.set('itemtype','setting');
201
+ params.set('name','DEFAULT_AI_ASSISTANT');
202
+ params.set('setting_type', (existing && existing.setting_type) || 'text');
203
+ params.set('info', (existing && existing.info) || 'Default ai_assistant used for chats and widgets.');
204
+ params.set('value', currentObject._id);
205
+
206
+ var url;
207
+ if(existing && existing._id){
208
+ url = '/API/save/' + encodeURIComponent(existing._id) + '?' + params.toString();
209
+ }else{
210
+ url = '/API/save/?' + params.toString();
211
+ }
212
+
213
+ const resp = await fetch(url, {
214
+ method: 'GET',
215
+ credentials: 'include'
216
+ }).then(r => r.json());
217
+
218
+ if(!resp || resp.status !== 'success'){
219
+ const msg = (resp && resp.error) || 'Save failed';
220
+ throw new Error(msg);
221
+ }
222
+
223
+ var saved = resp.results || resp.object || null;
224
+ if(saved){
225
+ if(existing){
226
+ Object.assign(existing, saved);
227
+ }else{
228
+ _joe.Data.setting = _joe.Data.setting || [];
229
+ _joe.Data.setting.push(saved);
230
+ }
231
+ }
232
+
233
+ $button.prop('disabled', false);
234
+ $button.html(originalHTML);
235
+ (_joe.toast && _joe.toast('Default assistant set.')) || alert('Default assistant set.');
236
+ }catch(e){
237
+ console.error('setAsDefaultAssistant error', e);
238
+ alert('Failed to set default assistant: '+(e.message||e));
239
+ try{
240
+ var $button = $(event.target).closest('joe-button');
241
+ $button.prop('disabled', false);
242
+ }catch(_e){}
243
+ }
132
244
  }
133
245
  },
134
246
  fields: function() {
@@ -171,6 +283,7 @@ var schema = {
171
283
  info: "What the user should see while the Assistant is thinking.",
172
284
  placeholder: "e.g. 'Assistant is preparing a response...'"
173
285
  },
286
+ { name: "assistant_color", type: "color", display: "Assistant Color", default: "teal" },
174
287
  { name: "instructions", type: "wysiwyg" }, // Full TinyMCE editor
175
288
  { section_end: "instructions" },
176
289
 
@@ -188,6 +301,15 @@ var schema = {
188
301
 
189
302
  height: "300px"
190
303
  },
304
+ {
305
+ name: "load_mcp_tools",
306
+ type: "button",
307
+ display: "Load MCP Tools",
308
+ method: "loadMCPTolsIntoAssistant",
309
+ color: "blue",
310
+ title: "Fetch MCP tools from this JOE instance and populate the tools JSON array."
311
+ },
312
+
191
313
  { section_end: "tools" },
192
314
 
193
315
  { section_start: "files" },
@@ -234,6 +356,32 @@ var schema = {
234
356
  color: "green",
235
357
  title: "Sync this Assistant with OpenAI"
236
358
  },
359
+ {
360
+ name: "set_default_assistant",
361
+ type: "button",
362
+ display: "Set as Default Assistant",
363
+ method: "setAsDefaultAssistant",
364
+ color: "orange",
365
+ hidden:function(ai_assistant){
366
+ try{
367
+ var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
368
+ var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
369
+ return !!(existing && existing.value === ai_assistant._id);
370
+ }catch(e){
371
+ return false;
372
+ }
373
+ },
374
+ title: "Mark this assistant as the DEFAULT_AI_ASSISTANT setting.",
375
+ locked:function(ai_assistant){
376
+ try{
377
+ var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
378
+ var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
379
+ return !!(existing && existing.value === ai_assistant._id);
380
+ }catch(e){
381
+ return false;
382
+ }
383
+ }
384
+ },
237
385
  // {
238
386
  // name: "test_button",
239
387
  // type: "button",
@@ -39,7 +39,20 @@ var schema = {
39
39
  methods:{
40
40
  chatSpawner:async function(object_id){
41
41
  await _joe.Ai.spawnContextualChat(object_id);
42
- }
42
+ },
43
+ listConversations:function(bus,oneline){
44
+ let html ='';
45
+ let conversations = _joe.Data.ai_conversation.filter(co=>{
46
+ if(co?.context_objects?.includes(bus._id)) return true;
47
+ })
48
+
49
+ $c.sortBy(conversations,'!created');
50
+ let temp = oneline?null:'<joe-subtext>${RUN[_joe.Utils.prettyPrintDTS;${created}]}</joe-subtext><joe-subtitle>${name}</joe-subtitle>';
51
+ conversations.map(tct=>{
52
+ html+=_joe.renderFieldListItem(tct,temp,'ai_conversation');
53
+ })
54
+ return html;
55
+ },
43
56
  },
44
57
  listView:{
45
58
  title: function(chat){
@@ -1,18 +1,19 @@
1
1
  var schema = {
2
2
  title: "AI Widget Conversation | ${name||_id}",
3
- display: "AI Widget Conversation",
3
+ display: "AI Convo (new)",
4
4
  info: "Lightweight conversation log for external AI widgets.",
5
5
  summary:{
6
6
  description:'Lightweight, persistent conversation record for embeddable AI widgets.',
7
- purpose:'Use ai_widget_conversation to store model choice, assistant linkage, system instructions, and a compact message array for widget chats. Widgets read/write this schema via the chatgpt widgetStart/widgetMessage/widgetHistory endpoints.',
8
- wip:true,
7
+ purpose:'Use ai_widget_conversation to track widget chats, including model choice, linked ai_assistant, effective system instructions, and a compact messages array. External sites and test pages create/update these records via the chatgpt widgetStart/widgetMessage/widgetHistory endpoints.',
8
+ wip:false,
9
9
  labelField:'name',
10
10
  defaultSort:{ field:'last_message_at', dir:'desc' },
11
- searchableFields:['name','info','model','assistant_id','source','_id'],
11
+ searchableFields:['name','info','model','assistant_id','source','user_name','_id'],
12
12
  allowedSorts:['last_message_at','created','joeUpdated','name'],
13
13
  relationships:{
14
14
  outbound:[
15
15
  { field:'assistant', targetSchema:'ai_assistant', cardinality:'one' },
16
+ { field:'user', targetSchema:'user', cardinality:'one' },
16
17
  { field:'tags', targetSchema:'tag', cardinality:'many' }
17
18
  ],
18
19
  inbound:{ graphRef:'server/relationships.graph.json' }
@@ -30,34 +31,120 @@ var schema = {
30
31
  { name:'messages', type:'objectList' },
31
32
  { name:'last_message_at', type:'string', format:'date-time' },
32
33
  { name:'source', type:'string' },
34
+ { name:'user', type:'string', isReference:true, targetSchema:'user' },
35
+ { name:'user_name', type:'string' },
36
+ { name:'user_color', type:'string' },
37
+ { name:'assistant_color', type:'string' },
33
38
  { name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' },
34
39
  { name:'joeUpdated', type:'string', format:'date-time' },
35
40
  { name:'created', type:'string', format:'date-time' }
36
41
  ]
37
42
  },
38
43
  listView: {
39
- title: "<joe-title>${name||_id}</joe-title><joe-subtitle>${model}</joe-subtitle><joe-subtext>${last_message_at}</joe-subtext>",
44
+ title: function (convo) {
45
+ try{
46
+ var ts = convo.last_message_at || convo.joeUpdated || convo.created || '';
47
+ var prettyTs = (_joe && _joe.Utils && typeof _joe.Utils.prettyPrintDTS === 'function')
48
+ ? _joe.Utils.prettyPrintDTS(ts)
49
+ : ts;
50
+ var title = convo.name || prettyTs || convo._id;
51
+
52
+ function textColorForBg(hex){
53
+ if (!hex || typeof hex !== 'string' || !/^#?[0-9a-fA-F]{6}$/.test(hex)) return '#000';
54
+ var h = (hex[0] === '#') ? hex.slice(1) : hex;
55
+ var n = parseInt(h, 16);
56
+ var r = (n >> 16) & 0xff;
57
+ var g = (n >> 8) & 0xff;
58
+ var b = n & 0xff;
59
+ var luminance = r * 0.299 + g * 0.587 + b * 0.114;
60
+ return luminance > 186 ? '#000' : '#fff';
61
+ }
62
+
63
+ function chip(label,bg){
64
+ if(!label){ return ''; }
65
+ var fg = textColorForBg(bg);
66
+ return '<span style="display:inline-block;padding:1px 6px;border-radius:999px;font-size:10px;margin-right:4px;background:'+bg+';color:'+fg+';">'
67
+ + label + '</span>';
68
+ }
69
+
70
+ // User chip
71
+ var userName = '';
72
+ var userColor = convo.user_color || '';
73
+ if (convo.user){
74
+ var u = $J.get(convo.user,'user') || $J.get(convo.user);
75
+ if (u){
76
+ userName = u.fullname || u.name || convo.user_name || '';
77
+ userColor = u.color || userColor;
78
+ }
79
+ } else if (convo.user_name){
80
+ userName = convo.user_name;
81
+ }
82
+ var userChip = userName ? chip(userName, userColor || '#4b5563') : '';
83
+
84
+ // Assistant chip
85
+ var asstName = '';
86
+ var asstColor = convo.assistant_color || '';
87
+ if (convo.assistant && _joe && _joe.Data && _joe.Data.ai_assistant){
88
+ var a = $J.get(convo.assistant,'ai_assistant') ||
89
+ _joe.Data.ai_assistant.where({_id:convo.assistant})[0];
90
+ if (a){
91
+ asstName = a.name || a.title || a.assistant_id || a._id;
92
+ asstColor = a.assistant_color || a.color || asstColor;
93
+ }
94
+ }
95
+ if (!asstName && convo.assistant_id && _joe && _joe.Data && _joe.Data.ai_assistant){
96
+ var a2 = _joe.Data.ai_assistant.where({assistant_id:convo.assistant_id})[0];
97
+ if (a2){
98
+ asstName = a2.name || a2.title || a2.assistant_id || a2._id;
99
+ asstColor = a2.assistant_color || a2.color || asstColor;
100
+ }else{
101
+ asstName = convo.assistant_id;
102
+ }
103
+ }
104
+ var asstChip = asstName ? chip(asstName, asstColor || '#2563eb') : '';
105
+
106
+ return ''
107
+ + '<joe-title>'+title+'</joe-title>'
108
+ + '<joe-subtitle>'+userChip+asstChip+'</joe-subtitle>'
109
+ + (prettyTs ? '<joe-subtext>'+prettyTs+'</joe-subtext>' : '');
110
+ }catch(e){
111
+ return '<joe-title>'+(convo.name||convo._id)+'</joe-title>';
112
+ }
113
+ },
40
114
  listWindowTitle: "AI Widget Conversations"
41
115
  },
42
116
  fields: function () {
43
117
  return [
44
118
  "name",
45
119
  "info",
46
-
47
- { section_start: "conversation", display: "Conversation", collapsed: false },
120
+ {section_start:"participants"},
48
121
  {
49
- name: "model",
122
+ name: "user",
50
123
  type: "select",
51
- values: "ai_model",
52
- display: "Model",
53
- comment: "Default OpenAI model used for this conversation."
124
+ values: "user",
125
+ display: "User (JOE)",
126
+ comment: "User of this conversation.",
127
+ width: "50%",
128
+ blank:true
54
129
  },
55
130
  {
56
131
  name: "assistant",
57
- type: "objectReference",
132
+ type: "select",
58
133
  values: "ai_assistant",
59
134
  display: "Assistant (JOE)",
60
- comment: "Optional link to an ai_assistant config used by this widget."
135
+ comment: "Optional link to an ai_assistant config used by this widget.",
136
+ width: "50%",
137
+ blank:true
138
+ },
139
+ {section_end:"participants"},
140
+ { section_start: "conversation", display: "Conversation", collapsed: false },
141
+ {
142
+ name: "model",
143
+ type: "select",
144
+ values: "ai_model",
145
+ display: "Model",
146
+ comment: "Default OpenAI model used for this conversation.",
147
+ width: "50%"
61
148
  },
62
149
  {
63
150
  name: "assistant_id",
@@ -98,7 +185,39 @@ var schema = {
98
185
  "joeUpdated",
99
186
  "itemtype",
100
187
  { section_end: "system" }
101
- ];
188
+ ];
189
+ },
190
+
191
+ // Subsets: grouped quick-filters in the list submenu
192
+ subsets:function(){
193
+ var sets = [];
194
+ // Group by source (e.g., aihub_card, widget, etc.)
195
+ sets = sets.concat(
196
+ _joe.Filter.Options.getDatasetPropertyValues('ai_widget_conversation','source',{
197
+ group:'source'
198
+ })
199
+ );
200
+ return sets;
201
+ },
202
+
203
+ // Filters: sidebar filters
204
+ filters:function(){
205
+ var filters = [];
206
+
207
+ // Distinct users referenced by ai_widget_conversation.user
208
+ filters = filters.concat(
209
+ _joe.Filter.Options.datasetProperty('user','user',{
210
+ group:'user',
211
+ collapsed:true
212
+ }),
213
+ // Distinct assistants (JOE ai_assistant) referenced by this conversation
214
+ _joe.Filter.Options.datasetProperty('ai_assistant','assistant',{
215
+ group:'assistant',
216
+ collapsed:true
217
+ })
218
+ );
219
+
220
+ return filters;
102
221
  },
103
222
 
104
223
  idprop: "_id",
@@ -45,14 +45,15 @@ var project = function(){return{
45
45
  purpose:'Projects group tasks, people, and timelines. A project typically has one status and group, many members (users), and many tags. Tasks reference their owning project. Use projects for planning, tracking, and reporting across related tasks.',
46
46
  labelField:'name',
47
47
  defaultSort:{ field:'joeUpdated', dir:'desc' },
48
- searchableFields:['name','info','description','_id'],
48
+ searchableFields:['name','info','description','problem_statement','_id'],
49
49
  allowedSorts:['priority','status','name','joeUpdated','created','start_dt','end_dt'],
50
50
  relationships:{
51
51
  outbound:[
52
52
  { field:'status', targetSchema:'status', cardinality:'one' },
53
53
  { field:'group', targetSchema:'group', cardinality:'one' },
54
54
  { field:'members', targetSchema:'user', cardinality:'many' },
55
- { field:'tags', targetSchema:'tag', cardinality:'many' }
55
+ { field:'tags', targetSchema:'tag', cardinality:'many' },
56
+ { field:'aligned_goals', targetSchema:'goal', cardinality:'many' }
56
57
  ],
57
58
  inbound:{ graphRef:'server/relationships.graph.json' }
58
59
  },
@@ -63,12 +64,24 @@ var project = function(){return{
63
64
  { name:'name', type:'string', required:true },
64
65
  { name:'info', type:'string' },
65
66
  { name:'description', type:'string' },
67
+ { name:'problem_statement', type:'string' },
66
68
  { name:'status', type:'string', isReference:true, targetSchema:'status' },
67
69
  { name:'group', type:'string', isReference:true, targetSchema:'group' },
68
70
  { name:'members', type:'string', isArray:true, isReference:true, targetSchema:'user' },
69
71
  { name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' },
72
+ { name:'aligned_goals', type:'string', isArray:true, isReference:true, targetSchema:'goal' },
73
+ { name:'goals', type:'objectList' },
70
74
  { name:'start_dt', type:'string', format:'date-time' },
71
75
  { name:'end_dt', type:'string', format:'date-time' },
76
+ { name:'phases', type:'objectList' },
77
+ { name:'subtasks', type:'objectList' },
78
+ { name:'considerations_list', type:'objectList' },
79
+ { name:'deliverables', type:'objectList' },
80
+ { name:'key_features', type:'objectList' },
81
+ { name:'marketing_tactics', type:'objectList' },
82
+ { name:'links', type:'objectList' },
83
+ { name:'pricing', type:'objectList' },
84
+ { name:'files', type:'string', isArray:true },
72
85
  { name:'priority', type:'number', enumValues:[1,2,3,1000] },
73
86
  { name:'complete', type:'boolean' },
74
87
  { name:'joeUpdated', type:'string', format:'date-time', required:true },
@@ -77,7 +90,18 @@ var project = function(){return{
77
90
  },
78
91
  fields:function(){
79
92
  var fields=[
80
-
93
+ {sidebar_start:'left'},
94
+ {section_start:'ai'},
95
+ 'objectChat',
96
+ 'listConversations',
97
+ {name:'ai_summary',display:'AI Summary',type:'rendering',comment:'Summarize the project in a few sentences.',
98
+ 'ai':{
99
+ label:'summarize with ai',
100
+ prompt:'Summarize the project in a few sentences.'
101
+ }
102
+ },
103
+ {section_end:'ai'},
104
+ {sidebar_end:'left'},
81
105
  {section_start:'overview'},
82
106
  'name',
83
107
  {extend:'info',specs:{comment:'What\'s the problem solved or reason for this project?'}},
@@ -179,6 +179,7 @@ var task = function(){return{
179
179
  {sidebar_start:'left'},
180
180
  {section_start:'JAI',display:'JOE Ai'},
181
181
  "objectChat",
182
+ "listConversations",
182
183
  {section_end:'JAI'},
183
184
  {sidebar_end:'left'},
184
185
  {section_start:'overview'},