json-object-editor 0.10.654 → 0.10.660

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "json-object-editor",
3
- "version": "0.10.654",
3
+ "version": "0.10.660",
4
4
  "description": "JOE the Json Object Editor | Platform Edition",
5
5
  "main": "app.js",
6
6
  "scripts": {
@@ -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
@@ -5,6 +5,22 @@ JOE is software that allows you to manage data models via JSON objects. There ar
5
5
 
6
6
 
7
7
 
8
+ ## What’s new in 0.10.660 (brief)
9
+ - MCP everywhere (prompts, autofill, widget):
10
+ - **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
+ - **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.
12
+ - **Audit**: `ai_response` records MCP config and actual tool calls (`mcp_tools_used[]`) plus `used_openai_file_ids[]` so you can see which tools and files were used for any run.
13
+ - Uploader file roles + AI‑aware attachments:
14
+ - Uploader fields can define `file_roles` (e.g. `{ value:'transcript', label:'Intake Transcript', default:true }`) and JOE renders a per‑file role `<select>` that saves to `file_role` on each file object.
15
+ - `executeJOEAiPrompt` now sends a compact `uploaded_files[]` header (including `itemtype`, `field`, `filename`, `file_role`, and `openai_file_id`) alongside Responses input so prompts can reason about “transcript vs summary” sources while the OpenAI Files integration still handles raw content.
16
+ - 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
+ - History safety:
18
+ - 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.
19
+ - Object chat unification:
20
+ - 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.
21
+ - `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.
22
+ - 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.
23
+
8
24
  ## What’s new in 0.10.654 (brief)
9
25
  - OpenAI Files mirrored on S3 upload; uploader tiles show the `openai_file_id`. Retry upload is available per file.
10
26
  - Responses integration improvements:
@@ -220,6 +236,23 @@ JOE is software that allows you to manage data models via JSON objects. There ar
220
236
  fs.writeFileSync('downloaded.bin', buf);
221
237
  ```
222
238
 
239
+ ### File roles on uploader fields
240
+ - **Schema configuration**:
241
+ - Any uploader field can declare `file_roles` as an array of `{ value, label?, default? }` objects, for example:
242
+ - `{ value:'transcript', label:'Intake Transcript', default:true }`
243
+ - `{ value:'summary', label:'Intake Summary' }`
244
+ - `label` is optional; it falls back to `value`. At most one role should have `default:true`.
245
+ - **Runtime behavior**:
246
+ - JOE renders a role `<select>` next to each uploaded file with:
247
+ - A blank option, and one option per configured role.
248
+ - The select updates the file object’s `file_role` property in the parent object (e.g. `client.files[].file_role`).
249
+ - Existing uploads show the role selector on first render as long as `file_roles` is configured on the field.
250
+ - When OpenAI Files are enabled, uploader files still receive `openai_file_id`, `openai_purpose`, `openai_status`, and `openai_error` as before; `file_role` is an additional, JOE‑level label.
251
+ - **AI integration**:
252
+ - When running an AI prompt via `executeJOEAiPrompt`, JOE inspects referenced objects for uploader fields and builds an `uploaded_files[]` header:
253
+ - Each entry includes `{ itemtype, field, name, role, openai_file_id }`.
254
+ - This header is merged into the user input so prompts can explicitly reason about which files are “transcripts”, “summaries”, etc., while the actual file bytes are attached via Responses `input_file` parts / `file_search` tool resources.
255
+
223
256
  ### Related endpoints (server/plugins)
224
257
 
225
258
  - `POST /API/plugin/awsConnect` – S3 upload (and OpenAI mirror when configured)
@@ -731,4 +764,4 @@ To help you develop and debug the widget + plugin in your instance, JOE exposes
731
764
  - Added a Responses‑based tool runner for `<joe-ai-widget>` that wires `ai_assistant.tools` into MCP functions via `chatgpt.runWithTools`.
732
765
  - Enhanced widget UX: assistant/user bubble theming (using `assistant_color` and user `color`), inline “tools used this turn” meta messages, and markdown rendering for assistant replies.
733
766
  - Expanded the AI widget test page with an assistant picker, live tool JSON viewer, a clickable conversation history list (resume existing `ai_widget_conversation` threads), and safer user handling (widget conversations now store user id/name/color explicitly and OAuth token‑exchange errors from Google are surfaced clearly during login).
734
- - Added field-level AI autofill support: schemas can declare `ai` config on a field (e.g. `{ name:'ai_summary', type:'rendering', ai:{ prompt:'Summarize the project in a few sentences.' } }`), which renders an inline “AI” button that calls `_joe.Ai.populateField('ai_summary')` and posts to `/API/plugin/chatgpt/autofill` to compute a JSON `patch` and update the UI (with confirmation before overwriting non-empty values).
767
+ - Added field-level AI autofill support: schemas can declare `ai` config on a field (e.g. `{ name:'ai_summary', type:'rendering', ai:{ prompt:'Summarize the project in a few sentences.' } }`), which renders an inline “AI” button that calls `_joe.Ai.populateField('ai_summary')` and posts to `/API/plugin/chatgpt/autofill` to compute a JSON `patch` and update the UI (with confirmation before overwriting non-empty values).
@@ -548,7 +548,10 @@ var fields = {
548
548
  icon:'ai_assistant',
549
549
  onclick:function(object){
550
550
  if (!object || !object._id) return '';
551
- return `_joe.Ai.spawnChatHelper('${object._id}');`;
551
+ var itemtype = object.itemtype || (_joe.current && _joe.current.schema && _joe.current.schema.name) || '';
552
+ var name = object.name || object.title || '';
553
+ name = String(name || '').replace(/'/g,"\\'");
554
+ 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
555
  },
553
556
 
554
557
  },