json-object-editor 0.10.660 → 0.10.662
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.
- package/CHANGELOG.md +10 -0
- package/css/joe-styles.css +1 -1
- package/css/joe.css +2 -2
- package/css/joe.min.css +1 -1
- package/docs/JOE_AI_Overview.md +73 -17
- package/docs/React_Form_Integration_Example.md +299 -0
- package/docs/React_Form_Integration_Strategy.md +5 -6
- package/dummy +9 -1
- package/js/joe-ai.js +26 -0
- package/js/joe-react-form.js +608 -0
- package/js/joe.js +1 -1
- package/package.json +1 -1
- package/readme.md +8 -0
- package/server/fields/core.js +48 -1
- package/server/modules/MCP.js +4 -0
- package/server/modules/Sites.js +28 -2
- package/server/plugins/chatgpt.js +169 -21
- package/server/plugins/formBuilder.js +98 -0
- package/server/schemas/ai_assistant.js +43 -44
- package/server/schemas/ai_prompt.js +4 -58
- package/server/schemas/include.js +8 -3
- package/server/schemas/page.js +6 -0
package/server/fields/core.js
CHANGED
|
@@ -170,7 +170,7 @@ var fields = {
|
|
|
170
170
|
return status.color||'';
|
|
171
171
|
}
|
|
172
172
|
},
|
|
173
|
-
tags:{type:'group',icon:'tag',hidden:function(obj){
|
|
173
|
+
tags:{type:'group',icon:'tag',reloadable:true,hidden:function(obj){
|
|
174
174
|
let tags = _joe.Data.tag.filter(function(tag){
|
|
175
175
|
return !(tag.datasets.indexOf(item.itemtype) == -1);
|
|
176
176
|
});
|
|
@@ -542,6 +542,53 @@ var fields = {
|
|
|
542
542
|
4.1-nano is best for lightweight classification or routing logic where speed and cost matter more than depth.`,
|
|
543
543
|
default: "gpt-5-mini",
|
|
544
544
|
},
|
|
545
|
+
mcp_enabled:{
|
|
546
|
+
type:'boolean',
|
|
547
|
+
display:'Enable MCP tools',
|
|
548
|
+
default:false,
|
|
549
|
+
comment:'When true, this surface may call JOE MCP tools (read-only by default unless toolset/custom overrides it).'
|
|
550
|
+
},
|
|
551
|
+
mcp_toolset:{
|
|
552
|
+
type:'select',
|
|
553
|
+
display:'MCP Toolset',
|
|
554
|
+
values:[
|
|
555
|
+
{value:'read-only',name:'Read-only (safe defaults)'},
|
|
556
|
+
{value:'minimal',name:'Minimal (small safe subset)'},
|
|
557
|
+
{value:'all',name:'All MCP tools'},
|
|
558
|
+
{value:'custom',name:'Custom selection'}
|
|
559
|
+
],
|
|
560
|
+
default:'read-only',
|
|
561
|
+
rerender:'mcp_selected_tools',
|
|
562
|
+
comment:'Choose which MCP toolset to expose when MCP tools are enabled.'
|
|
563
|
+
},
|
|
564
|
+
mcp_selected_tools:{
|
|
565
|
+
type:'select',
|
|
566
|
+
display:'Custom MCP tools (names)',
|
|
567
|
+
isArray:true,
|
|
568
|
+
allowMultiple:true,
|
|
569
|
+
values:function(doc){
|
|
570
|
+
try{
|
|
571
|
+
return (window._joe && _joe.Ai && Array.isArray(_joe.Ai.mcpToolNames)) ? _joe.Ai.mcpToolNames : [];
|
|
572
|
+
}catch(e){
|
|
573
|
+
return [];
|
|
574
|
+
}
|
|
575
|
+
},
|
|
576
|
+
hidden:function(doc){
|
|
577
|
+
return !doc || doc.mcp_toolset !== 'custom';
|
|
578
|
+
},
|
|
579
|
+
comment:'Multi-select from MCP manifest tool names. Only used when MCP Toolset = custom.'
|
|
580
|
+
},
|
|
581
|
+
mcp_instructions_mode:{
|
|
582
|
+
type:'select',
|
|
583
|
+
display:'MCP Instructions Mode',
|
|
584
|
+
values:[
|
|
585
|
+
{value:'auto',name:'Auto (short per-tool instructions)'},
|
|
586
|
+
{value:'full',name:'Full MCP instructions block'},
|
|
587
|
+
{value:'none',name:'No MCP instructions (tools only)'}
|
|
588
|
+
],
|
|
589
|
+
default:'auto',
|
|
590
|
+
comment:'Controls whether and how MCP tool instructions are appended to system text.'
|
|
591
|
+
},
|
|
545
592
|
objectChat:{
|
|
546
593
|
type:'button',
|
|
547
594
|
display:'Start Chat',
|
package/server/modules/MCP.js
CHANGED
|
@@ -1019,6 +1019,10 @@ MCP.buildToolInstructions = function(toolNames, mode){
|
|
|
1019
1019
|
if (m === 'none') { return ''; }
|
|
1020
1020
|
|
|
1021
1021
|
const lines = [];
|
|
1022
|
+
// Visual section break so humans (and logs/debug views) can easily
|
|
1023
|
+
// spot where the MCP tool list begins inside a larger system prompt.
|
|
1024
|
+
// This is purely presentational and does not affect tool behavior.
|
|
1025
|
+
lines.push('----- JOE MCP tools -----');
|
|
1022
1026
|
lines.push('You can call the following JOE MCP tools when helpful:');
|
|
1023
1027
|
toolNames.forEach(function(name){
|
|
1024
1028
|
const desc = MCP.descriptions && MCP.descriptions[name] || '';
|
package/server/modules/Sites.js
CHANGED
|
@@ -48,6 +48,9 @@ function getInclude(req,res,next){
|
|
|
48
48
|
case 'css':
|
|
49
49
|
res.set('Content-Type', 'text/css');
|
|
50
50
|
break;
|
|
51
|
+
case 'json':
|
|
52
|
+
res.set('Content-Type', 'application/json');
|
|
53
|
+
break;
|
|
51
54
|
}
|
|
52
55
|
res.send(content);
|
|
53
56
|
}
|
|
@@ -536,8 +539,31 @@ var routeWithoutSlash = route.startsWith('/') ? route.slice(1) : route;
|
|
|
536
539
|
page.content = Editor.wrap(page.content,'jpe-content',{name:'page content'})
|
|
537
540
|
}
|
|
538
541
|
|
|
539
|
-
|
|
540
|
-
|
|
542
|
+
// First, process page content with fillTemplate to resolve variables like ${INCLUDES}, ${this.PAGE.name}, etc.
|
|
543
|
+
// This is needed because these variables are INSIDE the page content
|
|
544
|
+
var pageContent = content.PAGE.content || '';
|
|
545
|
+
pageContent = pageContent.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
|
546
|
+
|
|
547
|
+
// Preserve newlines before fillTemplate strips them
|
|
548
|
+
// Replace newlines with a temporary marker that we'll restore later
|
|
549
|
+
var newlineMarker = '__JOE_NEWLINE__' + Date.now() + '__';
|
|
550
|
+
var pageContentWithMarkers = pageContent.replace(/\n/g, newlineMarker);
|
|
551
|
+
|
|
552
|
+
// Process page content with fillTemplate to resolve template variables
|
|
553
|
+
var processedPageContent = fillTemplate(pageContentWithMarkers, content);
|
|
554
|
+
|
|
555
|
+
// Restore newlines after fillTemplate processing
|
|
556
|
+
processedPageContent = processedPageContent.replace(new RegExp(newlineMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), '\n');
|
|
557
|
+
|
|
558
|
+
// Use a placeholder for ${this.PAGE.content} in layout template
|
|
559
|
+
var placeholder = '__JOE_PAGE_CONTENT_PLACEHOLDER__' + Date.now() + '__';
|
|
560
|
+
var template = layout.template.replace(/\$\{this\.PAGE\.content\}/g, placeholder);
|
|
561
|
+
|
|
562
|
+
// Process layout template with fillTemplate (which will strip newlines from layout, but our placeholder remains)
|
|
563
|
+
var html = fillTemplate(template, content);
|
|
564
|
+
|
|
565
|
+
// Replace placeholder with processed page content (which has newlines preserved)
|
|
566
|
+
html = html.replace(new RegExp(placeholder.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'g'), processedPageContent);
|
|
541
567
|
if(editor){
|
|
542
568
|
html = html.replace('</head>',Editor.styles+'</head>')
|
|
543
569
|
.replace('</body>',Editor.gui(layout)+Editor.gui(page)+Editor.menu
|
|
@@ -19,7 +19,28 @@ function ChatGPT() {
|
|
|
19
19
|
var self = this;
|
|
20
20
|
this.async ={};
|
|
21
21
|
function coloredLog(message){
|
|
22
|
-
|
|
22
|
+
try{
|
|
23
|
+
// Only emit verbose plugin logs in non‑production environments.
|
|
24
|
+
// This keeps consoles clean in production while preserving rich
|
|
25
|
+
// traces (assistant resolution, MCP config, systemText) during
|
|
26
|
+
// local/dev debugging.
|
|
27
|
+
var env = null;
|
|
28
|
+
if (typeof JOE !== 'undefined' && JOE && JOE.webconfig && JOE.webconfig.env){
|
|
29
|
+
env = JOE.webconfig.env;
|
|
30
|
+
} else if (typeof process !== 'undefined' && process.env && process.env.NODE_ENV){
|
|
31
|
+
env = process.env.NODE_ENV;
|
|
32
|
+
}
|
|
33
|
+
if (env && env.toLowerCase() === 'production'){
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
console.log(JOE.Utils.color('[chatgpt]', 'plugin', false), message);
|
|
37
|
+
}catch(_e){
|
|
38
|
+
// If anything goes wrong determining env, default to logging so
|
|
39
|
+
// that development debugging is not silently broken.
|
|
40
|
+
try{
|
|
41
|
+
console.log('[chatgpt]', message);
|
|
42
|
+
}catch(__e){}
|
|
43
|
+
}
|
|
23
44
|
}
|
|
24
45
|
//xx -setup and send a test prompt to chatgpt
|
|
25
46
|
//xx get the api key from joe settings
|
|
@@ -58,6 +79,23 @@ function ChatGPT() {
|
|
|
58
79
|
const summary = JOE.Schemas && JOE.Schemas.summary && JOE.Schemas.summary[name];
|
|
59
80
|
return { full, summary };
|
|
60
81
|
}
|
|
82
|
+
function buildMcpToolsFromConfig(cfg) {
|
|
83
|
+
if (!cfg || !cfg.mcp_enabled) {
|
|
84
|
+
return { tools: null, names: [] };
|
|
85
|
+
}
|
|
86
|
+
try {
|
|
87
|
+
const names = MCP.getToolNamesForToolset(
|
|
88
|
+
cfg.mcp_toolset || 'read-only',
|
|
89
|
+
Array.isArray(cfg.mcp_selected_tools) ? cfg.mcp_selected_tools : null
|
|
90
|
+
);
|
|
91
|
+
const defs = MCP.getToolDefinitions(names);
|
|
92
|
+
return { tools: defs, names: names };
|
|
93
|
+
} catch (e) {
|
|
94
|
+
console.warn('[chatgpt] buildMcpToolsFromConfig failed', e);
|
|
95
|
+
return { tools: null, names: [] };
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
61
99
|
/**
|
|
62
100
|
* callMCPTool
|
|
63
101
|
*
|
|
@@ -270,29 +308,32 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
|
|
|
270
308
|
const attachmentsMode = opts.attachments_mode || null;
|
|
271
309
|
const openaiFileIds = opts.openai_file_ids || null;
|
|
272
310
|
|
|
273
|
-
//
|
|
274
|
-
//
|
|
275
|
-
|
|
311
|
+
// Debug/trace: log the effective system instructions going into this
|
|
312
|
+
// Responses+tools call. This helps verify assistant + MCP instructions
|
|
313
|
+
// wiring across prompts, assists, autofill, and widget chat.
|
|
314
|
+
try{
|
|
315
|
+
coloredLog('runWithTools systemText:\n' + systemText);
|
|
316
|
+
}catch(_e){}
|
|
317
|
+
|
|
318
|
+
// Normalize tools: manual assistant.tools plus optional MCP tools
|
|
319
|
+
let manualTools = null;
|
|
276
320
|
if (assistant && assistant.tools) {
|
|
277
321
|
if (Array.isArray(assistant.tools)) {
|
|
278
|
-
|
|
322
|
+
manualTools = assistant.tools;
|
|
279
323
|
} else if (typeof assistant.tools === 'string') {
|
|
280
324
|
try {
|
|
281
325
|
const parsed = JSON.parse(assistant.tools);
|
|
282
326
|
if (Array.isArray(parsed)) {
|
|
283
|
-
|
|
327
|
+
manualTools = parsed;
|
|
284
328
|
}
|
|
285
329
|
} catch (e) {
|
|
286
330
|
console.error('[chatgpt] Failed to parse assistant.tools JSON', e);
|
|
287
331
|
}
|
|
288
332
|
}
|
|
289
333
|
}
|
|
290
|
-
//
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
// { type:'function', name:'x', description:'...', parameters:{...} }
|
|
294
|
-
if (Array.isArray(tools)) {
|
|
295
|
-
tools = tools.map(function (t) {
|
|
334
|
+
// Flatten any Assistants-style function definitions
|
|
335
|
+
if (Array.isArray(manualTools)) {
|
|
336
|
+
manualTools = manualTools.map(function (t) {
|
|
296
337
|
if (t && t.type === 'function' && t.function && !t.name) {
|
|
297
338
|
const fn = t.function || {};
|
|
298
339
|
return {
|
|
@@ -306,6 +347,26 @@ function shrinkUnderstandObjectMessagesForTokens(messages) {
|
|
|
306
347
|
});
|
|
307
348
|
}
|
|
308
349
|
|
|
350
|
+
// Merge manual tools with MCP tools (manual wins on name collisions).
|
|
351
|
+
let tools = null;
|
|
352
|
+
const mergedByName = {};
|
|
353
|
+
const mcp = buildMcpToolsFromConfig(assistant || {});
|
|
354
|
+
const mcpTools = Array.isArray(mcp.tools) ? mcp.tools : null;
|
|
355
|
+
|
|
356
|
+
if (Array.isArray(manualTools) || Array.isArray(mcpTools)) {
|
|
357
|
+
tools = [];
|
|
358
|
+
(manualTools || []).forEach(function(t){
|
|
359
|
+
if (!t) { return; }
|
|
360
|
+
if (t.name) { mergedByName[t.name] = true; }
|
|
361
|
+
tools.push(t);
|
|
362
|
+
});
|
|
363
|
+
(mcpTools || []).forEach(function(t){
|
|
364
|
+
if (!t || !t.name) { return; }
|
|
365
|
+
if (mergedByName[t.name]) { return; }
|
|
366
|
+
tools.push(t);
|
|
367
|
+
});
|
|
368
|
+
}
|
|
369
|
+
|
|
309
370
|
// No tools configured – do a simple single Responses call.
|
|
310
371
|
if (!tools) {
|
|
311
372
|
const resp = await openai.responses.create({
|
|
@@ -1096,6 +1157,24 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1096
1157
|
return { error: "Prompt not found." };
|
|
1097
1158
|
}
|
|
1098
1159
|
|
|
1160
|
+
// If this prompt run is associated with a JOE ai_assistant, log which
|
|
1161
|
+
// assistant is being used so we can debug "which agent handled this?"
|
|
1162
|
+
try{
|
|
1163
|
+
const aiAssistantId = data.ai_assistant_id || null;
|
|
1164
|
+
if (aiAssistantId) {
|
|
1165
|
+
let asst = null;
|
|
1166
|
+
try{
|
|
1167
|
+
// Prefer explicit ai_assistant schema lookup, then fallback
|
|
1168
|
+
// to a generic get in case datasets are flattened.
|
|
1169
|
+
asst = $J.get(aiAssistantId,'ai_assistant') || $J.get(aiAssistantId);
|
|
1170
|
+
}catch(_e){}
|
|
1171
|
+
const label = asst && (asst.name || asst.title || asst.info || asst._id) || aiAssistantId;
|
|
1172
|
+
coloredLog('[prompt] executeJOEAiPrompt using ai_assistant: '
|
|
1173
|
+
+ label + ' [' + aiAssistantId + ']'
|
|
1174
|
+
+ ' for prompt: ' + (prompt.name || prompt._id));
|
|
1175
|
+
}
|
|
1176
|
+
}catch(_e){}
|
|
1177
|
+
|
|
1099
1178
|
let instructions = prompt.instructions || "";
|
|
1100
1179
|
let finalInstructions=instructions;
|
|
1101
1180
|
let finalInput='';
|
|
@@ -1705,17 +1784,59 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1705
1784
|
}
|
|
1706
1785
|
}
|
|
1707
1786
|
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1787
|
+
// Resolve the JOE ai_assistant driving this conversation. We support
|
|
1788
|
+
// both the modern flow (ai_assistant_id / convo.assistant, which are
|
|
1789
|
+
// JOE cuid references) and the legacy OpenAI Assistants flow
|
|
1790
|
+
// (assistant_id / convo.assistant_id, which are OpenAI ids).
|
|
1791
|
+
var assistantObj = null;
|
|
1792
|
+
var joeAssistantId = body.ai_assistant_id || convo.assistant || null; // JOE cuid
|
|
1793
|
+
if (joeAssistantId) {
|
|
1794
|
+
try{
|
|
1795
|
+
// Prefer a direct lookup via the ai_assistant schema, but fall
|
|
1796
|
+
// back to scanning the in-memory dataset if needed. In some
|
|
1797
|
+
// server contexts $J.get may not be wired for ai_assistant yet,
|
|
1798
|
+
// while JOE.Data.ai_assistant is available.
|
|
1799
|
+
assistantObj = $J.get(joeAssistantId,'ai_assistant') || $J.get(joeAssistantId) || null;
|
|
1800
|
+
}catch(_e){}
|
|
1801
|
+
if (!assistantObj && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
|
|
1802
|
+
assistantObj = JOE.Data.ai_assistant.find(function(a){
|
|
1803
|
+
return a && (a._id === joeAssistantId);
|
|
1717
1804
|
}) || null;
|
|
1718
1805
|
}
|
|
1806
|
+
}
|
|
1807
|
+
const assistantId = body.assistant_id || convo.assistant_id || (assistantObj && assistantObj.assistant_id) || null;
|
|
1808
|
+
// Legacy fallback: if we only have an OpenAI assistant_id, try to
|
|
1809
|
+
// locate the JOE ai_assistant by that id.
|
|
1810
|
+
if (!assistantObj && assistantId && JOE && JOE.Data && Array.isArray(JOE.Data.ai_assistant)) {
|
|
1811
|
+
assistantObj = JOE.Data.ai_assistant.find(function (a) {
|
|
1812
|
+
return a && a.assistant_id === assistantId;
|
|
1813
|
+
}) || null;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Log which ai_assistant (if any) is being used for this widget
|
|
1817
|
+
// conversation so we can easily confirm the active agent when
|
|
1818
|
+
// debugging object chat vs AI Hub behavior.
|
|
1819
|
+
try{
|
|
1820
|
+
coloredLog('[widget] assistant resolution: '
|
|
1821
|
+
+ 'convo.assistant=' + String(convo.assistant || '')
|
|
1822
|
+
+ ' convo.assistant_id=' + String(convo.assistant_id || '')
|
|
1823
|
+
+ ' body.ai_assistant_id=' + String(body.ai_assistant_id || '')
|
|
1824
|
+
+ ' body.assistant_id=' + String(body.assistant_id || '')
|
|
1825
|
+
+ ' resolvedJoe=' + String(assistantObj && assistantObj._id || ''));
|
|
1826
|
+
if (assistantObj) {
|
|
1827
|
+
var asstLabel = assistantObj.name || assistantObj.title || assistantObj.info || assistantObj._id || assistantId;
|
|
1828
|
+
coloredLog('[widget] widgetMessage using ai_assistant: '
|
|
1829
|
+
+ asstLabel + ' [' + (assistantObj._id || assistantId || '') + ']'
|
|
1830
|
+
+ ' source=' + String(convo.source || 'widget')
|
|
1831
|
+
+ ' convo=' + String(convo._id || ''));
|
|
1832
|
+
} else if (assistantId) {
|
|
1833
|
+
coloredLog('[widget] widgetMessage assistant_id (no JOE ai_assistant found): '
|
|
1834
|
+
+ assistantId
|
|
1835
|
+
+ ' source=' + String(convo.source || 'widget')
|
|
1836
|
+
+ ' convo=' + String(convo._id || ''));
|
|
1837
|
+
}
|
|
1838
|
+
}catch(_e){}
|
|
1839
|
+
|
|
1719
1840
|
const openai = newClient();
|
|
1720
1841
|
const model = (assistantObj && assistantObj.ai_model) || convo.model || body.model || "gpt-5.1";
|
|
1721
1842
|
|
|
@@ -1741,6 +1862,33 @@ this.executeJOEAiPrompt = async function(data, req, res) {
|
|
|
1741
1862
|
}
|
|
1742
1863
|
}catch(_e){ /* non-fatal */ }
|
|
1743
1864
|
|
|
1865
|
+
// Append MCP tool instructions for assistants that have MCP enabled,
|
|
1866
|
+
// using the same helper as other MCP-aware surfaces.
|
|
1867
|
+
try{
|
|
1868
|
+
if (assistantObj && assistantObj.mcp_enabled) {
|
|
1869
|
+
const mcpCfg = {
|
|
1870
|
+
mcp_enabled: assistantObj.mcp_enabled,
|
|
1871
|
+
mcp_toolset: assistantObj.mcp_toolset,
|
|
1872
|
+
mcp_selected_tools: assistantObj.mcp_selected_tools,
|
|
1873
|
+
mcp_instructions_mode: assistantObj.mcp_instructions_mode || 'auto'
|
|
1874
|
+
};
|
|
1875
|
+
const mcp = buildMcpToolsFromConfig(mcpCfg);
|
|
1876
|
+
coloredLog('[widget] MCP config for assistant '
|
|
1877
|
+
+ String(assistantObj._id || '') + ': enabled=' + String(mcpCfg.mcp_enabled)
|
|
1878
|
+
+ ' toolset=' + String(mcpCfg.mcp_toolset || '')
|
|
1879
|
+
+ ' names=' + JSON.stringify(mcp.names || []));
|
|
1880
|
+
if (mcp.names && mcp.names.length) {
|
|
1881
|
+
const txt = MCP.buildToolInstructions(mcp.names, mcpCfg.mcp_instructions_mode || 'auto');
|
|
1882
|
+
if (txt) {
|
|
1883
|
+
coloredLog('[widget] appending MCP instructions block to systemText');
|
|
1884
|
+
systemText = (systemText || '') + '\n\n' + txt;
|
|
1885
|
+
}
|
|
1886
|
+
}
|
|
1887
|
+
} else {
|
|
1888
|
+
coloredLog('[widget] MCP disabled for this assistant or assistant missing; no MCP block appended.');
|
|
1889
|
+
}
|
|
1890
|
+
}catch(_e){ /* non-fatal */ }
|
|
1891
|
+
|
|
1744
1892
|
// Build the messages array for the model. We deliberately separate
|
|
1745
1893
|
// the stored `convo.messages` from the model-facing payload so we
|
|
1746
1894
|
// can annotate the latest user turn with uploaded_files metadata
|
|
@@ -335,6 +335,104 @@ function FormBuilder(){
|
|
|
335
335
|
}
|
|
336
336
|
}//end bodycontent
|
|
337
337
|
|
|
338
|
+
/**
|
|
339
|
+
* Get form definition JSON from an include
|
|
340
|
+
* GET /API/plugin/formBuilder/definition?include_id={id}
|
|
341
|
+
* GET /API/plugin/formBuilder/definition?formid={form_id}&field=json_definition_include
|
|
342
|
+
* GET /API/plugin/formBuilder/definition?formid={form_id}&pageid={page_id} (auto-finds JSON from page includes)
|
|
343
|
+
*/
|
|
344
|
+
this.definition = function(data, req, res) {
|
|
345
|
+
var includeId = data.include_id;
|
|
346
|
+
var formId = data.formid;
|
|
347
|
+
var pageId = data.pageid;
|
|
348
|
+
var includeField = data.field || 'json_definition_include';
|
|
349
|
+
|
|
350
|
+
// If formid provided, look up the include from form's meta, page includes, or specified field
|
|
351
|
+
if (formId && !includeId) {
|
|
352
|
+
var form = JOE.Cache.findByID('form', formId);
|
|
353
|
+
if (!form) {
|
|
354
|
+
return res.status(404).json({errors: 'form not found', failedat: 'formBuilder'});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// First try: Check if form has a reference to include in meta or a direct field
|
|
358
|
+
if (form.meta && form.meta[includeField]) {
|
|
359
|
+
includeId = form.meta[includeField];
|
|
360
|
+
} else if (form[includeField]) {
|
|
361
|
+
includeId = form[includeField];
|
|
362
|
+
}
|
|
363
|
+
// Second try: If pageid provided, find JSON include from page's includes
|
|
364
|
+
else if (pageId) {
|
|
365
|
+
var page = JOE.Cache.findByID('page', pageId);
|
|
366
|
+
if (page && page.includes && Array.isArray(page.includes)) {
|
|
367
|
+
for (var i = 0; i < page.includes.length; i++) {
|
|
368
|
+
var inc = JOE.Cache.findByID('include', page.includes[i]);
|
|
369
|
+
if (inc && inc.filetype === 'json') {
|
|
370
|
+
includeId = inc._id;
|
|
371
|
+
break;
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (!includeId) {
|
|
378
|
+
return res.status(404).json({errors: 'form does not have json definition include reference. Set it in form.meta.json_definition_include or add a JSON include to the page', failedat: 'formBuilder'});
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
if (!includeId) {
|
|
383
|
+
return res.status(400).json({errors: 'include_id or formid required', failedat: 'formBuilder'});
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
var include = JOE.Cache.findByID('include', includeId);
|
|
387
|
+
if (!include) {
|
|
388
|
+
return res.status(404).json({errors: 'include not found', failedat: 'formBuilder'});
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
if (include.filetype !== 'json') {
|
|
392
|
+
return res.status(400).json({errors: 'include is not a JSON file', failedat: 'formBuilder'});
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
var content = include.content;
|
|
396
|
+
|
|
397
|
+
// If fillTemplate is enabled, process template variables
|
|
398
|
+
if (include.fillTemplate) {
|
|
399
|
+
var payload = {
|
|
400
|
+
WEBCONFIG: JOE.webconfig,
|
|
401
|
+
SETTING: JOE.Cache.settings,
|
|
402
|
+
REQUEST: req,
|
|
403
|
+
FORM: formId ? JOE.Cache.findByID('form', formId) : null
|
|
404
|
+
};
|
|
405
|
+
try {
|
|
406
|
+
var fillTemplate = require('../modules/Sites.js').fillTemplate || function(str, data) {
|
|
407
|
+
return str.replace(/\$\{this\.([^}]+)\}/g, function(match, path) {
|
|
408
|
+
var parts = path.split('.');
|
|
409
|
+
var val = data;
|
|
410
|
+
for (var i = 0; i < parts.length; i++) {
|
|
411
|
+
if (val && typeof val === 'object') {
|
|
412
|
+
val = val[parts[i]];
|
|
413
|
+
} else {
|
|
414
|
+
return match;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
return val != null ? val : match;
|
|
418
|
+
});
|
|
419
|
+
};
|
|
420
|
+
content = fillTemplate(content, payload);
|
|
421
|
+
} catch(e) {
|
|
422
|
+
console.error('[formBuilder.definition] template fill error:', e);
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
// Parse and return JSON
|
|
427
|
+
try {
|
|
428
|
+
var jsonData = typeof content === 'string' ? JSON.parse(content) : content;
|
|
429
|
+
res.set('Content-Type', 'application/json');
|
|
430
|
+
return res.json(jsonData);
|
|
431
|
+
} catch(e) {
|
|
432
|
+
return res.status(500).json({errors: 'invalid JSON: ' + e.message, failedat: 'formBuilder'});
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
|
|
338
436
|
return self;
|
|
339
437
|
}
|
|
340
438
|
module.exports = new FormBuilder();
|
|
@@ -42,6 +42,11 @@ var schema = {
|
|
|
42
42
|
{ name:'tags', type:'string', isArray:true, isReference:true, targetSchema:'tag' },
|
|
43
43
|
{ name:'status', type:'string', isReference:true, targetSchema:'status' },
|
|
44
44
|
{ name:'last_synced', type:'string', format:'date-time' },
|
|
45
|
+
// MCP configuration for assistants (mirrors ai_prompt / ai_response)
|
|
46
|
+
{ name:'mcp_enabled', type:'boolean' },
|
|
47
|
+
{ name:'mcp_toolset', type:'string' },
|
|
48
|
+
{ name:'mcp_selected_tools', type:'string', isArray:true },
|
|
49
|
+
{ name:'mcp_instructions_mode', type:'string' },
|
|
45
50
|
{ name:'joeUpdated', type:'string', format:'date-time' },
|
|
46
51
|
{ name:'created', type:'string', format:'date-time' }
|
|
47
52
|
]
|
|
@@ -64,6 +69,7 @@ var schema = {
|
|
|
64
69
|
(new Set(schemas)).map(function(schema){
|
|
65
70
|
subs.push({name:schema,filter:{datasets:{$in:[schema]}}})
|
|
66
71
|
});
|
|
72
|
+
subs = subs.concat({name:'mcp_enabled',filter:{mcp_enabled:true}});
|
|
67
73
|
return subs;
|
|
68
74
|
},
|
|
69
75
|
stripeColor: function(ai_assistant) {
|
|
@@ -353,30 +359,35 @@ var schema = {
|
|
|
353
359
|
"itemtype",
|
|
354
360
|
{ section_end: "system" },
|
|
355
361
|
{ sidebar_start: "right", collapsed: false },
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
362
|
+
{ section_start:'workflow' },
|
|
363
|
+
"status",
|
|
364
|
+
"tags",
|
|
365
|
+
"last_synced",
|
|
366
|
+
{
|
|
367
|
+
name: "sync_button",
|
|
368
|
+
type: "button",
|
|
369
|
+
display: "Sync to OpenAI",
|
|
370
|
+
method: "syncAssistantToOpenAI",
|
|
371
|
+
color: "green",
|
|
372
|
+
title: "Sync this Assistant with OpenAI"
|
|
373
|
+
},
|
|
374
|
+
{
|
|
375
|
+
name: "set_default_assistant",
|
|
376
|
+
type: "button",
|
|
377
|
+
display: "Set as Default Assistant",
|
|
378
|
+
method: "setAsDefaultAssistant",
|
|
379
|
+
color: "orange",
|
|
380
|
+
hidden:function(ai_assistant){
|
|
381
|
+
try{
|
|
382
|
+
var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
|
|
383
|
+
var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
|
|
384
|
+
return !!(existing && existing.value === ai_assistant._id);
|
|
385
|
+
}catch(e){
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
},
|
|
389
|
+
title: "Mark this assistant as the DEFAULT_AI_ASSISTANT setting.",
|
|
390
|
+
locked:function(ai_assistant){
|
|
380
391
|
try{
|
|
381
392
|
var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
|
|
382
393
|
var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
|
|
@@ -384,27 +395,15 @@ var schema = {
|
|
|
384
395
|
}catch(e){
|
|
385
396
|
return false;
|
|
386
397
|
}
|
|
387
|
-
},
|
|
388
|
-
title: "Mark this assistant as the DEFAULT_AI_ASSISTANT setting.",
|
|
389
|
-
locked:function(ai_assistant){
|
|
390
|
-
try{
|
|
391
|
-
var settings = (_joe && _joe.Data && _joe.Data.setting) || [];
|
|
392
|
-
var existing = settings.where({name:'DEFAULT_AI_ASSISTANT'})[0] || null;
|
|
393
|
-
return !!(existing && existing.value === ai_assistant._id);
|
|
394
|
-
}catch(e){
|
|
395
|
-
return false;
|
|
396
398
|
}
|
|
397
|
-
|
|
398
|
-
},
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
// title: "Click to test button functionality",
|
|
406
|
-
// schema: "ai_assistant" // <- schema to pull the method from (optional for now, we lock it anyway)
|
|
407
|
-
// },
|
|
399
|
+
},
|
|
400
|
+
{ section_end:'workflow' },
|
|
401
|
+
{ section_start:'mcp', display:'MCP Tools', collapsed:true },
|
|
402
|
+
'mcp_enabled',
|
|
403
|
+
'mcp_toolset',
|
|
404
|
+
'mcp_selected_tools',
|
|
405
|
+
'mcp_instructions_mode',
|
|
406
|
+
{ section_end:'mcp' },
|
|
408
407
|
{ sidebar_end: "right" },
|
|
409
408
|
|
|
410
409
|
];
|