json-object-editor 0.10.657 → 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.
@@ -0,0 +1,570 @@
1
+ import React, { useMemo, useState } from "react";
2
+
3
+ // Harmonious Wellness Questionnaire – interactive mockup
4
+ // - Multi-step sections
5
+ // - Conditional visibility
6
+ // - Basic required validation
7
+ // - Review + JSON export
8
+
9
+ const FORM = {
10
+ formName: "Harmonious Wellness Health Questionnaire",
11
+ version: "2026-01-04",
12
+ sections: [
13
+ {
14
+ id: "demographics",
15
+ title: "Demographics",
16
+ description: "Basic identifying and contact details.",
17
+ fields: [
18
+ { id: "first_name", label: "First Name", type: "text", required: true, placeholder: "First name" },
19
+ { id: "last_name", label: "Last Name", type: "text", required: true, placeholder: "Last name" },
20
+ {
21
+ id: "gender",
22
+ label: "Gender",
23
+ type: "select",
24
+ required: true,
25
+ options: [
26
+ { value: "female", label: "Female" },
27
+ { value: "male", label: "Male" },
28
+ { value: "nonbinary", label: "Non-binary" },
29
+ { value: "prefer_not_to_say", label: "Prefer not to say" }
30
+ ]
31
+ },
32
+ { id: "age", label: "Age", type: "number", required: true, min: 0, max: 120 },
33
+ { id: "height", label: "Height", type: "text", required: true, placeholder: "e.g., 5'8\" or 173 cm" },
34
+ { id: "weight", label: "Weight", type: "text", required: true, placeholder: "e.g., 165 lb or 75 kg" },
35
+ { id: "email", label: "Email Address", type: "email", required: true, placeholder: "name@email.com" },
36
+ { id: "location_time_zone", label: "Location / Time Zone", type: "text", required: true, placeholder: "City, State and Time Zone" },
37
+ { id: "phone_number", label: "Phone Number", type: "phone", required: true, placeholder: "(555) 555-5555" },
38
+ {
39
+ id: "preferred_communication_method",
40
+ label: "Preferred Communication Method",
41
+ type: "select",
42
+ required: true,
43
+ options: [
44
+ { value: "text", label: "Text" },
45
+ { value: "email", label: "Email" },
46
+ { value: "phone", label: "Phone Call" }
47
+ ]
48
+ }
49
+ ]
50
+ },
51
+ {
52
+ id: "primary_concerns_history",
53
+ title: "Primary Concerns and Medical History",
54
+ description: "Your main concerns and key medical background.",
55
+ fields: [
56
+ {
57
+ id: "main_health_concerns",
58
+ label: "Main Health Concerns you'd like support on",
59
+ type: "textarea",
60
+ required: true,
61
+ placeholder: "Briefly describe your top concerns."
62
+ },
63
+ {
64
+ id: "diagnosed_medical_conditions",
65
+ label: "Diagnosed Medical Conditions",
66
+ type: "textarea",
67
+ required: true,
68
+ placeholder: "List diagnoses or type “None”."
69
+ },
70
+ {
71
+ id: "current_medications",
72
+ label: "Current Medications",
73
+ type: "text",
74
+ required: true,
75
+ placeholder: "List medications or type “None”."
76
+ },
77
+ {
78
+ id: "current_supplements",
79
+ label: "Current Supplements",
80
+ type: "text",
81
+ required: true,
82
+ placeholder: "List supplements or type “None”."
83
+ },
84
+ {
85
+ id: "past_major_surgeries",
86
+ label: "Past Major Surgeries",
87
+ type: "text",
88
+ required: true,
89
+ placeholder: "List surgeries or type “N/A”."
90
+ },
91
+ {
92
+ id: "pregnant_or_breastfeeding",
93
+ label: "Pregnant or Breastfeeding",
94
+ type: "boolean",
95
+ required: true,
96
+ visibility: { whenAll: [{ field: "gender", op: "eq", value: "female" }] }
97
+ },
98
+ { id: "gallbladder_removed", label: "Gallbladder Removed", type: "boolean", required: true },
99
+ {
100
+ id: "history_of_heart_issues",
101
+ label: "History of Heart Issues (Arrhythmia, Stent, Pacemaker, etc.)",
102
+ type: "text",
103
+ required: true,
104
+ placeholder: "Describe or type “N/A”."
105
+ },
106
+ {
107
+ id: "cancer_history_explain",
108
+ label: "Cancer History (Please explain)",
109
+ type: "text",
110
+ required: true,
111
+ visibility: {
112
+ whenAny: [
113
+ { field: "diagnosed_medical_conditions", op: "contains", value: "cancer" },
114
+ { field: "diagnosed_medical_conditions", op: "contains", value: "Cancer" }
115
+ ]
116
+ },
117
+ placeholder: "Type and location, or “N/A”."
118
+ },
119
+ {
120
+ id: "diabetes_type",
121
+ label: "Diabetes Type",
122
+ type: "select",
123
+ required: true,
124
+ options: [
125
+ { value: "none", label: "None" },
126
+ { value: "type_1", label: "Type 1" },
127
+ { value: "type_2", label: "Type 2" },
128
+ { value: "unsure", label: "Unsure" }
129
+ ]
130
+ },
131
+ {
132
+ id: "known_allergies_food_environmental",
133
+ label: "Known Allergies (Food/Environmental)",
134
+ type: "text",
135
+ required: true,
136
+ placeholder: "List allergies or type “N/A”."
137
+ }
138
+ ]
139
+ },
140
+ {
141
+ id: "gi_digestion",
142
+ title: "Digestion and Elimination",
143
+ description: "Digestive function, reactions, and stool patterns.",
144
+ fields: [
145
+ { id: "overall_digestion", label: "Overall Digestion", type: "scale", required: true, min: 0, max: 5 },
146
+ { id: "bloating_after_meals", label: "Bloating After Meals", type: "scale", required: true, min: 0, max: 5 },
147
+ { id: "reaction_to_fats", label: "Reaction to Fats", type: "scale", required: true, min: 0, max: 5 },
148
+ { id: "gas_frequency", label: "Gas Frequency", type: "scale", required: false, min: 0, max: 5 },
149
+ { id: "constipation_tendency", label: "Constipation Tendency", type: "scale", required: true, min: 0, max: 5 },
150
+ { id: "diarrhea_tendency", label: "Diarrhea Tendency", type: "scale", required: true, min: 0, max: 5 },
151
+ {
152
+ id: "stool_form_bristol",
153
+ label: "Stool Form (Bristol Scale)",
154
+ type: "select",
155
+ required: true,
156
+ options: [
157
+ { value: "1", label: "Type 1 (hard lumps)" },
158
+ { value: "2", label: "Type 2 (lumpy sausage)" },
159
+ { value: "3", label: "Type 3 (cracked sausage)" },
160
+ { value: "4", label: "Type 4 (smooth sausage)" },
161
+ { value: "5", label: "Type 5 (soft blobs)" },
162
+ { value: "6", label: "Type 6 (mushy)" },
163
+ { value: "7", label: "Type 7 (watery)" }
164
+ ]
165
+ },
166
+ {
167
+ id: "stool_frequency",
168
+ label: "Stool Frequency",
169
+ type: "select",
170
+ required: true,
171
+ options: [
172
+ { value: "0_1", label: "0–1 per day" },
173
+ { value: "1_2", label: "1–2 per day" },
174
+ { value: "2_3", label: "2–3 per day" },
175
+ { value: "3_plus", label: "3+ per day" }
176
+ ]
177
+ },
178
+ { id: "food_sensitivities", label: "Food Sensitivities", type: "text", required: false, placeholder: "List foods or type “N/A”." },
179
+ { id: "acid_reflux_heartburn", label: "Acid Reflux / Heartburn", type: "scale", required: true, min: 0, max: 5 }
180
+ ]
181
+ },
182
+ {
183
+ id: "hormones_repro",
184
+ title: "Hormones and Reproductive Health",
185
+ description: "Hormonal symptoms and sex-specific health signals.",
186
+ fields: [
187
+ { id: "hormonal_imbalance_symptoms", label: "Hormonal Imbalance Symptoms", type: "scale", required: true, min: 0, max: 5 },
188
+ { id: "temperature_sensitivity", label: "Temperature Sensitivity", type: "scale", required: true, min: 0, max: 5 },
189
+ { id: "hot_flashes_night_sweats", label: "Hot Flashes / Night Sweats", type: "boolean", required: true },
190
+ {
191
+ id: "libido_level",
192
+ label: "Libido Level",
193
+ type: "select",
194
+ required: true,
195
+ options: [
196
+ { value: "low", label: "Low" },
197
+ { value: "moderate", label: "Moderate" },
198
+ { value: "high", label: "High" }
199
+ ]
200
+ },
201
+ {
202
+ id: "cycle_regularity",
203
+ label: "Cycle Regularity",
204
+ type: "boolean",
205
+ required: true,
206
+ visibility: { whenAll: [{ field: "gender", op: "eq", value: "female" }] }
207
+ },
208
+ {
209
+ id: "pms_severity",
210
+ label: "PMS Severity",
211
+ type: "scale",
212
+ required: true,
213
+ min: 0,
214
+ max: 5,
215
+ visibility: { whenAll: [{ field: "gender", op: "eq", value: "female" }] }
216
+ },
217
+ {
218
+ id: "menstrual_pain",
219
+ label: "Menstrual Pain",
220
+ type: "scale",
221
+ required: true,
222
+ min: 0,
223
+ max: 5,
224
+ visibility: { whenAll: [{ field: "gender", op: "eq", value: "female" }] }
225
+ },
226
+ {
227
+ id: "prostate_symptoms",
228
+ label: "Prostate Symptoms",
229
+ type: "boolean",
230
+ required: true,
231
+ visibility: { whenAll: [{ field: "gender", op: "eq", value: "male" }] }
232
+ },
233
+ {
234
+ id: "erectile_function_concerns",
235
+ label: "Erectile Function Concerns",
236
+ type: "boolean",
237
+ required: true,
238
+ visibility: { whenAll: [{ field: "gender", op: "eq", value: "male" }] }
239
+ }
240
+ ]
241
+ },
242
+ {
243
+ id: "vitals_diet_family",
244
+ title: "Vitals, Diet, and Family History",
245
+ description: "Key anchors grouped and easy to reference.",
246
+ fields: [
247
+ { id: "blood_pressure_right", label: "Blood Pressure Right", type: "text", required: true, placeholder: "e.g., 120/80 or type “N/A”" },
248
+ { id: "blood_pressure_left", label: "Blood Pressure Left", type: "text", required: true, placeholder: "e.g., 120/80 or type “N/A”" },
249
+ { id: "urine_ph", label: "Urine pH", type: "number", required: true, min: 0, max: 14, step: 0.1, placeholder: "e.g., 6.5" },
250
+ {
251
+ id: "daily_diet_breakfast_lunch_dinner_snacks",
252
+ label: "What does your current daily diet consist of? (Breakfast/Lunch/Dinner/Snacks)",
253
+ type: "textarea",
254
+ required: true,
255
+ placeholder: "List typical breakfast, lunch, dinner, and snacks."
256
+ },
257
+ {
258
+ id: "family_history_all",
259
+ label: "Please list all known health concerns for each family member (Mother/Father/Grandparents/Siblings). Leave blank if you aren’t sure.",
260
+ type: "textarea",
261
+ required: true,
262
+ placeholder: "Mother: ... Father: ... Grandparents: ... Siblings: ..."
263
+ }
264
+ ]
265
+ }
266
+ ]
267
+ };
268
+
269
+ function clsx(...xs) {
270
+ return xs.filter(Boolean).join(" ");
271
+ }
272
+
273
+ function evalCondition(cond, values) {
274
+ const v = values?.[cond.field];
275
+ switch (cond.op) {
276
+ case "eq":
277
+ return v === cond.value;
278
+ case "neq":
279
+ return v !== cond.value;
280
+ case "contains":
281
+ return typeof v === "string" ? v.toLowerCase().includes(String(cond.value).toLowerCase()) : false;
282
+ case "gt":
283
+ return Number(v) > Number(cond.value);
284
+ case "gte":
285
+ return Number(v) >= Number(cond.value);
286
+ case "lt":
287
+ return Number(v) < Number(cond.value);
288
+ case "lte":
289
+ return Number(v) <= Number(cond.value);
290
+ case "truthy":
291
+ return Boolean(v);
292
+ default:
293
+ return false;
294
+ }
295
+ }
296
+
297
+ function isVisible(field, values) {
298
+ const vis = field.visibility;
299
+ if (!vis) return true;
300
+ if (vis.whenAll) return vis.whenAll.every((c) => evalCondition(c, values));
301
+ if (vis.whenAny) return vis.whenAny.some((c) => evalCondition(c, values));
302
+ return true;
303
+ }
304
+
305
+ function Field({ field, value, onChange, error }) {
306
+ const common = {
307
+ id: field.id,
308
+ name: field.id,
309
+ className:
310
+ "w-full rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm focus:border-slate-400 focus:outline-none focus:ring-2 focus:ring-slate-200",
311
+ value: value ?? "",
312
+ onChange: (e) => onChange(field.id, e.target.value)
313
+ };
314
+
315
+ return (
316
+ <div className="space-y-1">
317
+ <div className="flex items-start justify-between gap-3">
318
+ <label htmlFor={field.id} className="text-sm font-medium text-slate-900">
319
+ {field.label}
320
+ {field.required ? <span className="ml-1 text-rose-600">*</span> : null}
321
+ </label>
322
+ </div>
323
+
324
+ {field.type === "textarea" ? (
325
+ <textarea {...common} rows={4} placeholder={field.placeholder || ""} />
326
+ ) : field.type === "select" ? (
327
+ <select
328
+ {...common}
329
+ value={value ?? ""}
330
+ onChange={(e) => onChange(field.id, e.target.value)}
331
+ >
332
+ <option value="">Select…</option>
333
+ {field.options?.map((o) => (
334
+ <option key={o.value} value={o.value}>
335
+ {o.label}
336
+ </option>
337
+ ))}
338
+ </select>
339
+ ) : field.type === "boolean" ? (
340
+ <div className="flex items-center gap-3">
341
+ <label className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm">
342
+ <input
343
+ type="radio"
344
+ name={field.id}
345
+ checked={value === true}
346
+ onChange={() => onChange(field.id, true)}
347
+ />
348
+ Yes
349
+ </label>
350
+ <label className="inline-flex items-center gap-2 rounded-xl border border-slate-200 bg-white px-3 py-2 text-sm shadow-sm">
351
+ <input
352
+ type="radio"
353
+ name={field.id}
354
+ checked={value === false}
355
+ onChange={() => onChange(field.id, false)}
356
+ />
357
+ No
358
+ </label>
359
+ </div>
360
+ ) : field.type === "scale" ? (
361
+ <div className="space-y-2">
362
+ <input
363
+ type="range"
364
+ min={field.min ?? 0}
365
+ max={field.max ?? 5}
366
+ step={1}
367
+ value={value ?? 0}
368
+ onChange={(e) => onChange(field.id, Number(e.target.value))}
369
+ className="w-full"
370
+ />
371
+ <div className="flex items-center justify-between text-xs text-slate-500">
372
+ <span>{field.minLabel ?? field.min ?? 0}</span>
373
+ <span className="rounded-lg bg-slate-100 px-2 py-1 text-slate-700">{String(value ?? 0)}</span>
374
+ <span>{field.maxLabel ?? field.max ?? 5}</span>
375
+ </div>
376
+ </div>
377
+ ) : (
378
+ <input
379
+ {...common}
380
+ type={field.type === "number" ? "number" : field.type === "email" ? "email" : "text"}
381
+ min={field.min}
382
+ max={field.max}
383
+ step={field.step}
384
+ placeholder={field.placeholder || ""}
385
+ />
386
+ )}
387
+
388
+ {error ? <p className="text-xs text-rose-600">{error}</p> : null}
389
+ </div>
390
+ );
391
+ }
392
+
393
+ function validateSection(section, values) {
394
+ const errors = {};
395
+ for (const f of section.fields) {
396
+ if (!isVisible(f, values)) continue;
397
+ if (!f.required) continue;
398
+
399
+ const v = values[f.id];
400
+ const empty = v === undefined || v === null || v === "";
401
+ if (f.type === "boolean") {
402
+ if (v !== true && v !== false) errors[f.id] = "Please select Yes or No.";
403
+ } else if (f.type === "scale") {
404
+ if (v === undefined || v === null) errors[f.id] = "Please choose a value.";
405
+ } else if (empty) {
406
+ errors[f.id] = "This field is required.";
407
+ }
408
+ }
409
+ return errors;
410
+ }
411
+
412
+ export default function App() {
413
+ const [step, setStep] = useState(0);
414
+ const [values, setValues] = useState({});
415
+ const [errors, setErrors] = useState({});
416
+ const [showReview, setShowReview] = useState(false);
417
+
418
+ const sections = FORM.sections;
419
+ const current = sections[step];
420
+
421
+ const visibleFields = useMemo(() => {
422
+ return current.fields.filter((f) => isVisible(f, values));
423
+ }, [current, values]);
424
+
425
+ function setValue(id, v) {
426
+ setValues((prev) => ({ ...prev, [id]: v }));
427
+ setErrors((prev) => {
428
+ if (!prev[id]) return prev;
429
+ const next = { ...prev };
430
+ delete next[id];
431
+ return next;
432
+ });
433
+ }
434
+
435
+ function next() {
436
+ const e = validateSection(current, values);
437
+ setErrors(e);
438
+ if (Object.keys(e).length) return;
439
+
440
+ if (step < sections.length - 1) {
441
+ setStep(step + 1);
442
+ window.scrollTo({ top: 0, behavior: "smooth" });
443
+ } else {
444
+ setShowReview(true);
445
+ window.scrollTo({ top: 0, behavior: "smooth" });
446
+ }
447
+ }
448
+
449
+ function back() {
450
+ if (showReview) {
451
+ setShowReview(false);
452
+ return;
453
+ }
454
+ setStep(Math.max(0, step - 1));
455
+ window.scrollTo({ top: 0, behavior: "smooth" });
456
+ }
457
+
458
+ return (
459
+ <div className="min-h-screen bg-gradient-to-b from-slate-50 to-white">
460
+ <div className="mx-auto max-w-3xl px-4 py-8">
461
+ <header className="mb-6">
462
+ <div className="flex flex-col gap-2">
463
+ <h1 className="text-2xl font-semibold text-slate-900">{FORM.formName}</h1>
464
+ <p className="text-sm text-slate-600">
465
+ Version {FORM.version} • Interactive mockup with conditional logic
466
+ </p>
467
+ </div>
468
+ <div className="mt-4 h-2 w-full overflow-hidden rounded-full bg-slate-100">
469
+ <div
470
+ className="h-full rounded-full bg-slate-900 transition-all"
471
+ style={{ width: `${showReview ? 100 : ((step + 1) / sections.length) * 100}%` }}
472
+ />
473
+ </div>
474
+ <div className="mt-2 flex items-center justify-between text-xs text-slate-500">
475
+ <span>
476
+ {showReview ? "Review" : `Section ${step + 1} of ${sections.length}`}
477
+ </span>
478
+ <span>{showReview ? "Ready" : current.title}</span>
479
+ </div>
480
+ </header>
481
+
482
+ <main className="rounded-2xl border border-slate-200 bg-white p-5 shadow-sm">
483
+ {!showReview ? (
484
+ <>
485
+ <div className="mb-4">
486
+ <h2 className="text-lg font-semibold text-slate-900">{current.title}</h2>
487
+ <p className="mt-1 text-sm text-slate-600">{current.description}</p>
488
+ </div>
489
+
490
+ <div className="grid gap-4">
491
+ {visibleFields.map((f) => (
492
+ <Field
493
+ key={f.id}
494
+ field={f}
495
+ value={values[f.id]}
496
+ onChange={setValue}
497
+ error={errors[f.id]}
498
+ />
499
+ ))}
500
+ </div>
501
+
502
+ <div className="mt-6 flex items-center justify-between">
503
+ <button
504
+ onClick={back}
505
+ disabled={step === 0}
506
+ className={clsx(
507
+ "rounded-xl px-4 py-2 text-sm font-medium",
508
+ step === 0
509
+ ? "cursor-not-allowed bg-slate-100 text-slate-400"
510
+ : "bg-slate-100 text-slate-800 hover:bg-slate-200"
511
+ )}
512
+ >
513
+ Back
514
+ </button>
515
+ <button
516
+ onClick={next}
517
+ className="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800"
518
+ >
519
+ {step === sections.length - 1 ? "Review" : "Next"}
520
+ </button>
521
+ </div>
522
+
523
+ <div className="mt-4 rounded-xl bg-slate-50 p-3 text-xs text-slate-600">
524
+ Tip: Set <strong>Gender</strong> to Female to reveal pregnancy and cycle questions, or Male to reveal prostate-related questions.
525
+ Add the word <strong>cancer</strong> in “Diagnosed Medical Conditions” to reveal the Cancer History field.
526
+ </div>
527
+ </>
528
+ ) : (
529
+ <>
530
+ <div className="mb-4">
531
+ <h2 className="text-lg font-semibold text-slate-900">Review & Export</h2>
532
+ <p className="mt-1 text-sm text-slate-600">
533
+ This is the payload your system can ingest after submission.
534
+ </p>
535
+ </div>
536
+
537
+ <div className="rounded-xl border border-slate-200 bg-slate-50 p-3">
538
+ <pre className="max-h-[55vh] overflow-auto text-xs text-slate-800">
539
+ {JSON.stringify(values, null, 2)}
540
+ </pre>
541
+ </div>
542
+
543
+ <div className="mt-6 flex items-center justify-between">
544
+ <button
545
+ onClick={back}
546
+ className="rounded-xl bg-slate-100 px-4 py-2 text-sm font-medium text-slate-800 hover:bg-slate-200"
547
+ >
548
+ Back
549
+ </button>
550
+ <button
551
+ onClick={() => {
552
+ navigator.clipboard.writeText(JSON.stringify(values, null, 2));
553
+ alert("Copied JSON to clipboard.");
554
+ }}
555
+ className="rounded-xl bg-slate-900 px-4 py-2 text-sm font-medium text-white hover:bg-slate-800"
556
+ >
557
+ Copy JSON
558
+ </button>
559
+ </div>
560
+ </>
561
+ )}
562
+ </main>
563
+
564
+ <footer className="mt-6 text-xs text-slate-500">
565
+ Mockup for internal testing. Not production-hardened. Conditional logic and structure are designed to be reusable.
566
+ </footer>
567
+ </div>
568
+ </div>
569
+ );
570
+ }
package/readme.md CHANGED
@@ -4,8 +4,16 @@
4
4
  JOE is software that allows you to manage data models via JSON objects. There are two flavors, the client-side version and nodejs server platform.
5
5
 
6
6
 
7
+ ## What's new in 0.10.662 (brief)
8
+ - React Form Integration with JSON Definitions:
9
+ - **JSON includes**: `include` schema now supports `filetype: 'json'` for storing JSON form definitions (served at `/_include/{id}` with proper content-type).
10
+ - **Form-page linking**: Added `form` reference field to `page` schema to link pages to JOE forms.
11
+ - **Form definition API**: New `/API/plugin/formBuilder/definition` endpoint (merged from `formDefinition` plugin) serves JSON form definitions. Automatically finds JSON includes from form metadata or page includes.
12
+ - **React form renderer**: New `joe-react-form.js` client library renders multi-step React forms from JSON definitions with conditional visibility, validation, and submission to JOE's form submission system.
13
+ - **Page rendering fix**: Enhanced template variable processing to preserve newlines in page content, fixing JavaScript comment issues in `code` and `module` content types.
7
14
 
8
- ## What’s new in 0.10.657 (brief)
15
+
16
+ ## What’s new in 0.10.660 (brief)
9
17
  - MCP everywhere (prompts, autofill, widget):
10
18
  - **Prompts** (`ai_prompt`): new MCP config block (`mcp_enabled`, `mcp_toolset`, `mcp_selected_tools`, `mcp_instructions_mode`) lets you turn MCP tools on per‑prompt, pick a toolset (`read-only`, `minimal`, `all`, or `custom`), and auto‑generate short tool instructions.
11
19
  - **Autofill fields**: the same MCP keys are now supported under a field’s `ai` config so autofill runs can optionally call MCP tools with the same toolset/playlist model.
@@ -16,6 +24,10 @@ JOE is software that allows you to manage data models via JSON objects. There ar
16
24
  - Responses+tools (`runWithTools`) now attaches files on both the initial tool‑planning call and the final answer call, so MCP runs see the same attachments end‑to‑end.
17
25
  - History safety:
18
26
  - Hardened `JOE.Storage.save` history diffing to avoid a `craydent-object` edge case where comparing `null`/`undefined` values could throw on `.toString()`. This only affects `_history.changes`, not what is saved.
27
+ - Object chat unification:
28
+ - The `objectChat` field now launches a floating `<joe-ai-widget>` shell with `<joe-ai-assistant-picker>`, reusing the same AIHub chat stack for all schemas.
29
+ - `ai_widget_conversation` gained `scope_itemtype`/`scope_id` so each chat can be scoped to a specific object, plus `attached_openai_file_ids` and `attached_files_meta` to track which uploader files were attached.
30
+ - On the first turn of an object-scoped chat, the server preloads a slimmed `understandObject` snapshot for the scoped object and merges object files + assistant files into `openai_file_ids`, so the assistant immediately knows “which record this is” and can reason over its files.
19
31
 
20
32
  ## What’s new in 0.10.654 (brief)
21
33
  - OpenAI Files mirrored on S3 upload; uploader tiles show the `openai_file_id`. Retry upload is available per file.
@@ -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,13 +542,63 @@ 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',
548
595
  icon:'ai_assistant',
549
596
  onclick:function(object){
550
597
  if (!object || !object._id) return '';
551
- return `_joe.Ai.spawnChatHelper('${object._id}');`;
598
+ var itemtype = object.itemtype || (_joe.current && _joe.current.schema && _joe.current.schema.name) || '';
599
+ var name = object.name || object.title || '';
600
+ name = String(name || '').replace(/'/g,"\\'");
601
+ return `if(window._joe && _joe.Ai && typeof _joe.Ai.openObjectChatLauncher==='function'){_joe.Ai.openObjectChatLauncher('${object._id}','${itemtype}','${name}');}else{console.warn('Object chat widget not ready (joe-ai.js).');}`;
552
602
  },
553
603
 
554
604
  },
@@ -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] || '';