<img src="http://joe.craydent.com/JsonObjectEditor/img/svgs/joe_banner_o.svg"/>

# Json Object Editor
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.



## Architecture & Mental Model (Server)

- Global JOE
  - Created in `server/init.js`; exposes subsystems like `JOE.Utils`, `JOE.Schemas`, `JOE.Storage`, `JOE.Mongo`/`JOE.MySQL`, `JOE.Cache`, `JOE.Apps`, `JOE.Server` (Express), `JOE.Sites`, `JOE.io` (Socket), and `JOE.auth`.
- Init pattern
  - Modules can optionally export `init()`; `init.js` loads/watches modules and calls `init()` after `JOE.Server` is ready. This enables hot-reload and keeps `Server.js` thin.
- Schemas & events
  - `JOE.Schemas` loads from `server/schemas/` and app schema dir into `JOE.Schemas.schema` with names in `JOE.Schemas.schemaList`. Raw copies live in `JOE.Schemas.raw_schemas` for event hooks.
  - `JOE.Storage.save()` triggers schema events (`create`, `save`, `status`, `delete`) via `JOE.Schemas.events()` and writes history documents to `_history`.
- Storage & cache
  - `JOE.Storage.load(collection, query, cb)` chooses backend per schema `storage.type` (`mongo`, `mysql`, `file`, `api`), with Mongo/file fallback.
  - `JOE.Storage.save(item, collection, cb, { user, history })` emits `item_updated` to sockets, records history, and fires events.
  - `JOE.Cache.update(cb, collections)` populates `JOE.Data`, flattens a `list`, and builds `lookup`. Use `JOE.Cache.findByID(collection, idOrCsv)` for fast lookups; `JOE.Cache.search(query)` for in-memory filtering.
- Auth
  - `JOE.auth` middleware checks cookie token or Basic Auth; many API routes (and `/mcp`) are protected.
- Shorthand `$J`
  - Server and client convenience: `$J.get(_id)`, `$J.search(query)`, `$J.schema(name)`.
  - On server, provided by `server/modules/UniversalShorthand.js` and assigned to `global.$J` in `init.js`.
- MCP overview
  - Manifest: `/.well-known/mcp/manifest.json`; JSON-RPC: `POST /mcp` (auth-protected). Tools map to real JOE APIs (`Schemas`, `Storage`, `Cache`) and sanitize sensitive fields.

## MCP routes and local testing

- Endpoints
  - Manifest (public): `GET /.well-known/mcp/manifest.json`
  - JSON-RPC (auth): `POST /mcp`
  - Privacy (public): `/privacy` (uses Setting `PRIVACY_CONTACT` for contact email)
  - Terms (public): `/terms`

- Auth
  - If users exist, `POST /mcp` requires cookie or Basic Auth (same as other APIs). If no users configured, it is effectively open.

- Test pages
  - JOE ships several debug/test pages in `/_www/`:
    - `mcp-test.html` - Simple MCP tool tester
    - `mcp-schemas.html` - Schema health and summary viewer
    - `mcp-prompt.html` - MCP prompt/instructions viewer
    - `matrix.html` - Interactive schema relationship visualization with D3.js force-directed graph
  - Access via JOE path: `http://localhost:<PORT>/JsonObjectEditor/_www/<page>.html`
  - If your host app serves its own `_www`, pages can also be available at the root (fallback) if running with the updated server that mounts JOE’s `_www` as a secondary static directory. Then: `http://localhost:<PORT>/<page>.html`

- Tools
  - `listSchemas(name?)`, `getSchema(name)`
  - `getObject(_id, itemtype?)` (supports optional `flatten` and `depth`)
  - `search` (exact): unified tool for cache and storage
    - Params: `{ itemtype?, query?, ids?, source?: 'cache'|'storage', limit?, flatten?, depth? }`
    - Defaults to cache across all collections; add `itemtype` to filter; set `source:"storage"` to query a specific collection in the DB. Runtime accepts legacy alias `schema` (maps to `itemtype`). Use `fuzzySearch` for typo-tolerant free text.
  - `fuzzySearch` (typo-tolerant free text across weighted fields)
    - Params: `{ itemtype?, q, filters?, fields?, threshold?, limit?, offset?, highlight?, minQueryLength? }`
    - Defaults: `fields` resolved from schema `searchables` (plural) if present; otherwise weights `name:0.6, info:0.3, description:0.1`. `threshold:0.5`, `limit:50`, `minQueryLength:2`.
    - Returns: `{ items, count }`. Each item may include `_score` (0..1) and `_matches` when `highlight` is true.
  - `saveObject({ object })`
  - `saveObjects({ objects, stopOnError?, concurrency? })`
    - Batch save with per-item history/events. Defaults: `stopOnError=false`, `concurrency=5`.
    - Each object must include `itemtype`; `_id` and `joeUpdated` are set when omitted. `created` is set on first save when missing.
    - Return shape: `{ results, errors, saved, failed }`.

- Quick tests (PowerShell)
  - Prepare headers if using Basic Auth:
    ```powershell
    $pair = "user:pass"; $b64 = [Convert]::ToBase64String([Text.Encoding]::ASCII.GetBytes($pair))
    $h = @{ Authorization = "Basic $b64"; "Content-Type" = "application/json" }
    $base = "http://localhost:<PORT>"
    ```
  - Manifest:
    ```powershell
    Invoke-RestMethod "$base/.well-known/mcp/manifest.json"
    ```
  - listSchemas:
    ```powershell
    $body = @{ jsonrpc="2.0"; id="1"; method="listSchemas"; params=@{} } | ConvertTo-Json -Depth 6
    Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
    ```
  - getSchema:
    ```powershell
    $body = @{ jsonrpc="2.0"; id="2"; method="getSchema"; params=@{ name="<schemaName>" } } | ConvertTo-Json -Depth 6
    Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
    ```
  - search (cache default):
    ```powershell
    $body = @{ jsonrpc="2.0"; id="3"; method="search"; params=@{ query=@{ itemtype="<schemaName>" }; limit=10 } } | ConvertTo-Json -Depth 10
    Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
    ```
  - fuzzySearch (cache):
    ```powershell
    $body = @{ jsonrpc="2.0"; id="6"; method="fuzzySearch"; params=@{ itemtype="<schemaName>"; q="st paal"; threshold=0.35; limit=10 } } | ConvertTo-Json -Depth 10
    Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
    ```
  - search (storage):
    ```powershell
    $body = @{ jsonrpc="2.0"; id="4"; method="search"; params=@{ itemtype="<schemaName>"; source="storage"; query=@{ }; limit=10 } } | ConvertTo-Json -Depth 10
    Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
    ```
  - search (ids + flatten):
    ```powershell
    $body = @{ jsonrpc="2.0"; id="5"; method="search"; params=@{ itemtype="<schemaName>"; ids=@("<id1>","<id2>"); flatten=$true; depth=2 } } | ConvertTo-Json -Depth 10
    Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
    ```
  - saveObject:
    ```powershell
    $object = @{ itemtype="<schemaName>"; name="Test via MCP" }
    $body = @{ jsonrpc="2.0"; id="4"; method="saveObject"; params=@{ object=$object } } | ConvertTo-Json -Depth 10
    Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
    ```
  - saveObjects (batch):
    ```powershell
    $objs = @(
      @{ itemtype="<schemaName>"; name="Batch A" },
      @{ itemtype="<schemaName>"; name="Batch B" }
    )
    $body = @{ jsonrpc="2.0"; id="7"; method="saveObjects"; params=@{ objects=$objs; stopOnError=$false; concurrency=5 } } | ConvertTo-Json -Depth 10
    Invoke-RestMethod -Method Post -Uri "$base/mcp" -Headers $h -Body $body
    ```

- Quick tests (curl)
  - Manifest:
    ```bash
    curl -s http://localhost:<PORT>/.well-known/mcp/manifest.json | jq
    ```
  - listSchemas:
  - search (cache):
    ```bash
    curl -s -X POST http://localhost:<PORT>/mcp \
      -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":"3","method":"search","params":{"query":{"itemtype":"<schemaName>"},"limit":10}}' | jq
    ```
  - fuzzySearch:
    ```bash
    curl -s -X POST http://localhost:<PORT>/mcp \
      -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":"6","method":"fuzzySearch","params":{"itemtype":"<schemaName>","q":"st paal","threshold":0.35,"limit":10}}' | jq
    ```
  - search (storage):
    ```bash
    curl -s -X POST http://localhost:<PORT>/mcp \
      -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":"4","method":"search","params":{"itemtype":"<schemaName>","source":"storage","query":{},"limit":10}}' | jq
    ```
    ```bash
    curl -s -X POST http://localhost:<PORT>/mcp \
      -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":"1","method":"listSchemas","params":{}}' | jq
    ```
  - saveObjects (batch):
    ```bash
    curl -s -X POST http://localhost:<PORT>/mcp \
      -H 'Content-Type: application/json' \
      -d '{"jsonrpc":"2.0","id":"7","method":"saveObjects","params":{"objects":[{"itemtype":"<schemaName>","name":"Batch A"}],"stopOnError":false,"concurrency":5}}' | jq
    ```

- Troubleshooting
  - If you see a payload like `{ originalURL: "/...", site: "no site found" }`, the request hit the Sites catch-all. Ensure MCP routes are initialized before Sites (handled by default in `server/init.js` via `MCP.init()`), and use the correct URL: `/.well-known/mcp/manifest.json` or `/mcp`.
  - To update contact email on /privacy and /terms, set Setting `PRIVACY_CONTACT`.

## File uploads (S3)
- Uploader field options:
  - `allowmultiple: true|false` — allow selecting multiple files.
  - `url_field: 'image_url'` — on success, sets this property to the remote URL and rerenders that field.
  - `ACL: 'public-read'` — optional per-field ACL. When omitted, server currently defaults to `public-read` (temporary during migration).
- Flow:
  - Client posts `{ Key, base64, contentType, ACL? }` to `/API/plugin/awsConnect`.
  - Server uploads with AWS SDK v3 and returns `{ url, Key, bucket, etag }` (HTTP 200).
  - Client uses `response.url`; if `url_field` is set, it assigns and rerenders that field.
- Errors:
  - If bucket or region config is missing, server returns 400 with a clear message.
  - If the bucket has ACLs disabled, server returns 400: “Bucket has ACLs disabled… remove ACL or switch to presigned/proxy access.”

## SERVER/PLATFORM mode
	check port 2099
	/JOE/
	/JsonObjectEditor/docs.html
	*Should auto-open on start

Json Object Editor (Universal-esque software)
(requires connection to a mongo server for full functionality)



### JOE server instantiation (add to entry point js file)
	var joe = require('json-object-editor');
	or here's a custom example
	var joe = require('json-object-editor')({
		name:'name_your_joe' (JOE),
    	joedb:'local_custom_database_name' // {{mongoURL}}' if using mongolab or remote mongodb,
    	port:'4099', (2099)
    	socketPort:'4098', (2098)
    	sitesPort:'4100' (2100),
		clusters:1,
		hostname:'server name if not localhost'(localhost)
	});

## $J (universal) Shorthand JOE
	$J to access helper funtions on client and server. (callbacks are optional)
	$J.get(itemId,[callback])
	$J.schema(schemaname,[callback])

# Client-Side (front end only)

## js client only instantiation
	var specs = {
		fields:{
			species:{label:'Species',type:'select', values:['cat','dog','rat','thing'], onchange:adjustSchema},
			gender:{type:'select', values:['male','female']},
			legs:{label:'# of Legs',type:'int', onblur:logit},
			weight:{label:' Weight (lbs)',type:'number', onblur:logit},
			name:{label:' pet Name', onkeyup:logValue},
			//id:{label:'ID',type:'text', locked:true},
			id:{label:'ID',type:'guid'},
			
		//example of select that takes function (function is passed item)	
			animalLink:{label:'Link to other animal',type:'select', values:getAnimals},
			hiddenizer:{hidden:true}
		},
		schemas:{
			animal:animalschema,
			thing:thingschema			
		},
		container:string ('body'),
        compact:false,
        useBackButton:true,
		autosave:2000,
        listSubMenu:{filters:{}},
        useHashlink:true,
        title:'${itemtype} | ${display}'
	}
	var JOE = new JsonObjectEditor(specs);
	JOE.init();

##JOE CONFIG
##specs
- useBackButton:[false] if true, back button moves through joe panels when joe has history to go to (is open).
- useHashlink:[false], true or a template for hashlinks. 
    default template is '${schema_name}_${_id}'
	default server tempalte is '${schema_name}/${_id}'
##SCHEMA CONFIG
###fields
Properties for all Fields

- `label / display`: what the field should display as
    *If the field type is boolean, label controls checkbox/boolean label
- `value`: default value if not one in object
- `default`: default value for field || function(object)
- `type`: what type of field should JOE show
- `hidden`: boolean / function, value will be added (but unsees by user)
- `locked`: boolean
- `condition`: boolean
- `width`: used for layout control.
	- can use pixels or percentages (as string)
- `comment`: a commentthat shows up at the beginning of the field
- `tooltip`: hover/clickable tooltip that shows up next to name

**field types:**

- `rendering`: for css html and js
- `text`: default single line text.
	- autocomplete: boolean // obj of specs (template, idprop)
		- values:array of possibilities
	-maxlength:string
- `int`: integer field
- `number`: number (float) field
- `select`: select list. 
	- multiple(bool)
	- values(array of objects, [{value:"",name/display:""]), can be a function
		- disabled:boolean(func acceptable)
	- idprop: string of prop name
- `geo`: shows a map
	- *takes a string array "[lat,lon]"*
	- center:[lat,lon], center of map
	- zoom: zoom level (higher zooms in more)
	- returns "[lat,lon]"
-`image` : shows an image and HxW as th image url is typed in.
- `multisorter` : allows arrays of objects to be selected and sorted in right bin.
	- values(array of objects, [{value:"",name/display:""]), can be a function
- `content` : show content on in the editor
    - run: function to be run(current_object,field_properties)
    - template: html template for fillTemplate(template,current_object);
- `objectlist` : a table of objects with editable properties
    - properties: array of objects|strings for the object property names
        -name: value in object
        -display: header in objectList
    - max: integer, number or items that can be added. use zero for infinite.
    - hideHeadings: don't show table headings
- `objectReference` : a list of object ids
    - template
    - autocomplete_template
    - idprop
    - values
    - max(0 unlimited)
    - sortable(true)
- `code` :
	- language
	
- `boolean`:
	- label:controls checkbox label
- `preview` :
	-content: string or function(current joe object) to replace everything on page (template).
	-bodycontent: same as content, only replaces body content.
	-url: preview page if not the default one.
	- encoded: boolean, if pre uriencoded
**labels:**

- pass an object instead of a string to the fields array.


    {label:'Name of the following properties section'}

##page sections
    {section_start: 'SectionName',
       section_label:'Section Name with Labels',
       condition:function(item){
           return item.show;}
    },
    {section_end: 'CreativeBrief'}

- pass an object instead of a string to the fields array. these show up on the details view as anchors.


- Object Properties
    - `section_start`: name/id of section
    - `'section_label`:use instead of section_start for display name
    - `section_end`: name/id of section(str)
    - template: html template for fillTemplate(template,current_object);
    - Note: for fields, `condition` removes the field from the DOM while `hidden` only toggles visibility; sections use `condition` (and sidebar sections use `hidden`) differently than fields.
    
##page sidebar
    {sidebar_start: 'SectionName',
       sidebar_label:'Section Name with Labels',
       condition:function(item){
           return item.show;}
    },
    {sidebar_end: 'CreativeBrief'}

- pass an object instead of a string to the fields array. these show up on the details view as anchors.


- Object Properties
    - `sidebar_start`: name/id of sidebar
    - `sidebar_label`:use instead of sidebar_start for display name
    - `sidebar_end`: name/id of sidebar(str)
    - template: html template for fillTemplate(template,current_object);
    
###defaultProfile
overwrites the default profile

#schemas 

a list of schema objects that can configure the editor fields, these can be given properties that are delegated to all the corresponding fields.

	var animalschema = 
	{
		title:'Animal', *what shows as the panel header* 
		fields:['id','name','legs','species','weight','color','gender','animalLink'], *list of visible fields*
		_listID:'id', *the id for finding the object*
		_listTitle:'${name} ${species}', *how to display items in the list*
		menu:[array of menu buttons],
		listMenuTitle: (string) template forjoe window title in list view,
		listmenu:[array of menu buttons] (multi-edit and select all always show),
		/*callback:function(obj){
			alert(obj.name);
		},*/
		onblur:logit,
		
		hideNumbers:boolean *toggle list numbers*
		multipleCallback:function to be called after a multi-edit. passed list of edited items.
		onUpdate: callback for after update. passed single edited items.
		onMultipleUpdate:callback for after multi update.passed list of edited items.
                filters: array of objects
		checkChanges:(bool) whether or not to check for changes via interval and on leave
	}
##Table View
	- add tableView object to a schema;
	-cols = [strings||objects]
		-string is the name and value
		-display/header is the column title
		-property/name = object property 
###Pre-formating
you can preformat at the joe call or schema level. The data item will be affected by the passed function (which should return the preformated item). 

##menu##
an array of menu buttons

    //the default save button
    //this is the dom object, 
    //use _joe.current.object for current object
    condition:function(field,object) to call
    self = Joe object
    var __saveBtn__ = {name:'save',label:'Save', action:'_joe.updateObject(this);', css:'joe-save-button'};

##itemMenu##
as array of buttons for each item in list views
	- name
	- action (action string)
	- url (instead of js action)
	- condition

##itemExpander##
template or run for content to be shown under the main list item block.


###Addition properties
**Changing the schema on the fly?**

	_joe.resetSchema(new schema name);



**css (included) options**

- joe-left-button
- joe-right-button

##FIELDS

    {extend:'name',specs:{display:'Request Title'}},//extends the field 'name' with the specs provided.
##usage
### a | adding a new object

	_joe.show({},{schema:'animal',callback:addAnimal); 
	//or goJoe(object,specs)

	...
	function addAnimal(obj){
		animals.push(obj);
	}

### b | viewing a list of objects

	goJoe([array of objects],specs:{schema,subsets,subset})
	goJoe.show(animals,{schema:'animal',subsets:[{name:'Two-Legged',filter:{legs:2}}]});
	//use the specs property subset to pre-select a subset by name

**properties**

- _listWindowTitle: the title of the window (can be passed in with the schema);
- _listCount: added to the current object and can be used in the title.
- _listTitle:'${name} ${species}', *how to display items in the list*
- _icon: [str] template for a list item icon (standard min 50x50), 'http://www.icons.com/${itemname}', can be obj with width, height, url 
- listSubMenu:a function or object that represents the list submenu
- stripeColor:string or function that returns valid css color descriptor.
- bgColor:string or function that returns valid css color descriptor.
- subsets: name:string, filter:object 
- subMenu:a function or object that represents the single item submenu

- _listTemplate: html template that uses ${var} to write out the item properties for the list item.
	- standard css class `joe-panel-content-option`

### Helper shortcuts: subsets and stripes

Use these helpers to quickly generate subset/filter options and add per-item stripe colors.

```javascript
// In a schema definition
{
  // 1) Subsets from statuses of the current schema (auto colors)
  subsets: function () {
    return _joe.Filter.Options.status({
      group: 'status',        // optional grouping label
      collapsed: true,        // start group collapsed
      none: true              // include a "no status" option
      // color: 'stripecolor' // uncomment to render colored stripes in the menu
    });
  },

  // 2) Subsets for all distinct values of a property
  //    Example: recommendation_domain on 'recommendation' items
  //    (pass values to control order/allowlist)
  // subsets: () => _joe.Filter.Options.getDatasetPropertyValues(
  //   'recommendation', 'recommendation_domain', { group: 'domain', values: ['product','dietary','activity'] }
  // ),

  // 3) Subsets from another dataset (reference values)
  // subsets: () => _joe.Filter.Options.datasetProperty('user','members',{ group: 'assignees', collapsed: true }),

  // 4) Row stripe color by status (string or { color, title })
  stripeColor: function (item) {
    if (!item.status) return '';
    const s = _joe.getDataItem(item.status, 'status');
    return s && { color: s.color, title: s.name };
  }
}
```



###c | Conditional select that changes the item schema

	fields:{
		species:{label:'Species',type:'select', values:['cat','dog','rat','thing'], onchange:adjustSchema},
		[field_id]:{
			
			+label : STR
			+type : STR
			value : STR (default value)
			+values : ARRAY/FUNC (for select)
			
			//modifiers
			+hidden:BOOL/STRING(name of field that toggles this) //don't show, but value is passed
			+locked:BOOL // show, but uneditable
			//events
			+onchange : FUNC
			+onblur : FUNC
			+onkeypress : FUNC
			+rerender : STRING // name of field to rerender
		}
	}

	function adjustSchema(dom){
		var species = $(dom).val();
		if(species == "thing"){
			JOE.resetSchema('thing')
		}
		else{
			JOE.resetSchema('animal')
		
		}
	}

###d | duplicating an item

	//duplicates the currently active object (being edited)
	_joe.duplicateObject(specs);

**specs**

- `deletes`:array of properties to clear for new item
	- note that you will need to delete guid/id fields or the id will be the same.	



### e | exporting an object in pretty format json (or minified)
JOE.exportJSON = function(object,objvarname,minify)
		
##Useful Functions
_joe.reload(hideMessage,specs)
- use specs.overwreite object to extend reloaded object.
		
_joe.constructObjectFromFields()

## AI / Chat Integrations (OpenAI Responses + Assistants)

JOE ships a few plugins and schemas to help you connect OpenAI assistants and the new Responses API to your data model.

- **`chatgpt` plugin**
  - Central entry point for OpenAI calls (uses the `openai` Node SDK and the Responses API).
  - Looks up the OpenAI API key from the `setting` schema:
    - `OPENAI_API_KEY`: your secret key (can be a service‑account key, e.g. `sk-svcacct-...`).
  - Key methods:
    - **`autofill`** (`POST /API/plugin/chatgpt/autofill`):
      - Given `{ object_id, schema, fields, prompt?, assistant_id?, model? }`, calls `openai.responses.create(...)` and returns a JSON patch for the requested fields only.
      - Used by `_joe.Ai.populateField(...)` for schema‑driven “AI fill” buttons.
    - **`executeJOEAiPrompt`**:
      - Drives the `ai_prompt` → `ai_response` workflow using Responses API + optional helper functions.
    - **Widget‑focused methods**:
      - `widgetStart`, `widgetMessage`, `widgetHistory` for the embeddable AI widget (see below).

- **`chatgpt-assistants` plugin (legacy Assistants API)**
  - Manages the older `beta.assistants` / `beta.threads` API for JOE’s rich in‑app chat (`<joe-ai-chatbox>`).
  - Still used by the `ai_conversation` flow; new integrations should prefer the Responses‑based `chatgpt` plugin.

- **`chatgpt-tools` plugin**
  - Exposes JOE “AI tools” configured via the `ai_tool` schema.
  - Useful when you want OpenAI tools that call back into JOE (e.g. search, profile, etc.).

### AI Assistant schemas

- **`ai_assistant` schema**
  - Represents a JOE‑side “assistant config” that can be synced to an OpenAI assistant.
  - Key fields:
    - `ai_model`: default model (e.g. `gpt-4.1-mini`, `gpt-4o`).
    - `instructions`: system prompt for the assistant.
    - `assistant_id`: OpenAI assistant ID (`asst_...`) set by the sync button.
    - `file_search_enabled`, `code_interpreter_enabled`: toggle built‑in tools.
    - `tools`: JSON definition array for custom function tools.
    - `assistant_thinking_text`: what the UI shows while the assistant is “thinking”.
  - UI highlights the **default assistant**:
    - The assistant whose `_id` matches the `DEFAULT_AI_ASSISTANT` setting is given a golden stripe in list view.
  - Default assistant resolution:
    - The client helper `Ai.getDefaultAssistant()` in `joe-ai.js` looks up `_joe.Data.setting.where({ name: 'DEFAULT_AI_ASSISTANT' })[0]` and caches it.

- **`ai_widget_conversation` schema**
  - Lightweight conversation log used by the new embeddable widget (`<joe-ai-widget>`).
  - Fields:
    - `model`: model name used for the conversation (or inherited from `ai_assistant.ai_model`).
    - `assistant`: optional reference to the JOE `ai_assistant` used.
    - `assistant_id`: OpenAI assistant ID (`asst_...`) used by the Responses API.
    - `system`: effective system prompt.
    - `messages`: JSON array of `{ role, content, created_at }` for the widget chat.
    - `last_message_at`, `source`, `tags`, standard system fields.
  - This schema is what the widget endpoints in `chatgpt` read/write.

### Embeddable AI Widget (`<joe-ai-widget>`)

JOE includes a small web component, defined in `js/joe-ai.js`, that can be dropped into any web page and talks to your JOE instance via the `chatgpt` plugin.

- **Custom element**: `<joe-ai-widget>`
  - Attributes:
    - `endpoint` (optional): Base URL to the JOE instance. Defaults to same origin.  
      Example: `endpoint="https://my-joe.example.com"`.
    - `ai_assistant_id` (optional): `_id` of an `ai_assistant` document in JOE. If present, the server will load this assistant and use its `ai_model`, `instructions`, and `assistant_id` when starting the conversation.
    - `assistant_id` (optional): Direct OpenAI assistant ID (`asst_...`). If provided, the widget uses Responses API with `assistant_id` instead of a bare `model`.
    - `model` (optional): Model name to use when no assistant is supplied (e.g. `gpt-4.1-mini`).
    - `title` (optional): Header title text shown in the widget panel.
    - `placeholder` (optional): Input placeholder text.
    - `source` (optional): Arbitrary string stored on the conversation (`ai_widget_conversation.source`).
    - `conversation_id` (optional): Existing `ai_widget_conversation._id` to resume a saved chat.
  - Behavior:
    - On first attach:
      - If `conversation_id` is set → calls `GET /API/plugin/chatgpt/widgetHistory?conversation_id=...` and renders messages.
      - Otherwise → calls `POST /API/plugin/chatgpt/widgetStart` to create a new `ai_widget_conversation` and stores the returned `conversation_id` (and any `assistant_id`/`model`) as attributes.
    - On send:
      - Optimistically appends a `{ role:'user', content }` message in the UI.
      - Calls `POST /API/plugin/chatgpt/widgetMessage` with `{ conversation_id, content, role:'user', assistant_id?, model? }`.
      - Renders the updated `messages` and assistant reply from the response.

- **Server endpoints (`chatgpt` plugin)**:
  - `POST /API/plugin/chatgpt/widgetStart` → `chatgpt.widgetStart(data)`
    - Input: `{ model?, ai_assistant_id?, source? }`.
    - Behavior:
      - If `ai_assistant_id` is provided, loads that `ai_assistant` and seeds `model`, `assistant_id`, `system` from it.
      - Creates `ai_widget_conversation` with empty `messages` and timestamps.
      - Returns `{ success, conversation_id, model, assistant_id }`.
  - `GET /API/plugin/chatgpt/widgetHistory?conversation_id=...` → `chatgpt.widgetHistory(data)`
    - Input: `{ conversation_id }`.
    - Returns `{ success, conversation_id, model, assistant_id, messages }`.
  - `POST /API/plugin/chatgpt/widgetMessage` → `chatgpt.widgetMessage(data)`
    - Input: `{ conversation_id, content, role='user', assistant_id?, model? }`.
    - Behavior:
      - Loads `ai_widget_conversation`, appends a user message.
      - Calls `openai.responses.create`:
        - With `assistant_id` if available (preferred), or
        - With `model` and `instructions` (from `system`) otherwise.
      - Appends an assistant message, updates `last_message_at`, saves the conversation.
      - Returns `{ success, conversation_id, model, assistant_id, messages, last_message, usage }`.

### AI Widget Test Page

To help you develop and debug the widget + plugin in your instance, JOE exposes an auth‑protected test page, similar to the MCP test pages.

- **Routes** (both require standard JOE `auth`):
  - `/ai-widget-test.html`
  - `${JOE.webconfig.joepath}ai-widget-test.html` (e.g. `/JsonObjectEditor/ai-widget-test.html`).

- **Behavior** (`server/modules/Server.js`):
  - The route handler:
    - Reads `assistant_id` or `assistant` from the query string.
    - If not present, tries to resolve the default assistant via:
      - `JOE.Utils.Settings('DEFAULT_AI_ASSISTANT', { object: true })` and uses its `.value` as `ai_assistant_id`.
    - Renders a small HTML page that:
      - Includes the MCP nav (`_www/mcp-nav.js`) so you can jump between MCP and widget tests.
      - Mounts `<joe-ai-widget title="JOE AI Assistant" ai_assistant_id="...">` in a centered card.
      - Loads `js/joe-ai.js`, which defines both `<joe-ai-chatbox>` (for in‑app use) and `<joe-ai-widget>` (for embedding).

- **Shared nav link**:
  - The shared dev nav (`_www/mcp-nav.js`) includes an “AI Widget” link:

    ```html
    <a href="/ai-widget-test.html" target="ai_widget_test_win" rel="noopener">AI Widget</a>
    ```

  - This appears on MCP test/export/prompt pages and on the AI widget test page itself.

### AI / Widget Changelog (current work – `0.10.632`)

- Added a Responses‑based tool runner for `<joe-ai-widget>` that wires `ai_assistant.tools` into MCP functions via `chatgpt.runWithTools`.
- 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.
- 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).
- 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).