@things-factory/board-ai 10.1.46 → 10.1.47
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/dist-client/server/service/assistant.js +2 -0
- package/dist-client/server/service/assistant.js.map +1 -1
- package/dist-client/server/service/types.d.ts +6 -0
- package/dist-client/server/service/types.js.map +1 -1
- package/dist-client/server/service/validation/tool-validation.d.ts +1 -1
- package/dist-client/server/service/validation/tool-validation.js +27 -1
- package/dist-client/server/service/validation/tool-validation.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-server/service/assistant.js +2 -0
- package/dist-server/service/assistant.js.map +1 -1
- package/dist-server/service/binding-tools.d.ts +23 -0
- package/dist-server/service/binding-tools.js +269 -0
- package/dist-server/service/binding-tools.js.map +1 -0
- package/dist-server/service/board-ai-resolver.d.ts +5 -0
- package/dist-server/service/board-ai-resolver.js +55 -24
- package/dist-server/service/board-ai-resolver.js.map +1 -1
- package/dist-server/service/index.d.ts +1 -0
- package/dist-server/service/index.js +1 -0
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/types.d.ts +6 -0
- package/dist-server/service/types.js.map +1 -1
- package/dist-server/service/validation/tool-validation.d.ts +1 -1
- package/dist-server/service/validation/tool-validation.js +27 -1
- package/dist-server/service/validation/tool-validation.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/service/assistant.ts +2 -0
- package/server/service/binding-tools.ts +293 -0
- package/server/service/board-ai-resolver.ts +33 -6
- package/server/service/index.ts +1 -0
- package/server/service/types.ts +6 -0
- package/server/service/validation/tool-validation.ts +15 -2
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Data binding as first-class board tools — `getBindings`, `bindData`, `unbindData`.
|
|
3
|
+
*
|
|
4
|
+
* ── Why these exist ──────────────────────────────────────────────────────────
|
|
5
|
+
* A binding was reachable only through `modifyComponentByRefid` with a raw `mappings` array. The
|
|
6
|
+
* model had to know the mapping shape, where the mapping lives (on the component that HAS the
|
|
7
|
+
* data, not on the one that shows it), that a target is a selector, and that a `#id` selector
|
|
8
|
+
* needs the target to carry an id. It guessed, and a wrong guess is silent: the patch applies,
|
|
9
|
+
* nothing moves on the board, no error anywhere.
|
|
10
|
+
*
|
|
11
|
+
* These tools take refids and plain words and do the bookkeeping here:
|
|
12
|
+
* - the mapping is appended to the SOURCE's `mappings` (same target + property replaces);
|
|
13
|
+
* - a target given by refid gets an `id` if it has none, and the mapping points at `#id`;
|
|
14
|
+
* - the rule's parameter lands where the engine reads it (`mapping.param`).
|
|
15
|
+
*
|
|
16
|
+
* The engine side is `things-scene/src/components/data/data.ts` (spreading) and `evaluator.ts`
|
|
17
|
+
* (value / map / range / eval). The property panel writes the same shape.
|
|
18
|
+
*
|
|
19
|
+
* Writes return scene-edit `modify` ops, so they go through the same preview → apply → revert →
|
|
20
|
+
* save path as every other board edit. Nothing here touches the database.
|
|
21
|
+
*/
|
|
22
|
+
import { type ToolCategory } from '@things-factory/ai-client-base';
|
|
23
|
+
export declare const boardBindingToolsCategory: ToolCategory;
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.boardBindingToolsCategory = void 0;
|
|
4
|
+
/**
|
|
5
|
+
* Data binding as first-class board tools — `getBindings`, `bindData`, `unbindData`.
|
|
6
|
+
*
|
|
7
|
+
* ── Why these exist ──────────────────────────────────────────────────────────
|
|
8
|
+
* A binding was reachable only through `modifyComponentByRefid` with a raw `mappings` array. The
|
|
9
|
+
* model had to know the mapping shape, where the mapping lives (on the component that HAS the
|
|
10
|
+
* data, not on the one that shows it), that a target is a selector, and that a `#id` selector
|
|
11
|
+
* needs the target to carry an id. It guessed, and a wrong guess is silent: the patch applies,
|
|
12
|
+
* nothing moves on the board, no error anywhere.
|
|
13
|
+
*
|
|
14
|
+
* These tools take refids and plain words and do the bookkeeping here:
|
|
15
|
+
* - the mapping is appended to the SOURCE's `mappings` (same target + property replaces);
|
|
16
|
+
* - a target given by refid gets an `id` if it has none, and the mapping points at `#id`;
|
|
17
|
+
* - the rule's parameter lands where the engine reads it (`mapping.param`).
|
|
18
|
+
*
|
|
19
|
+
* The engine side is `things-scene/src/components/data/data.ts` (spreading) and `evaluator.ts`
|
|
20
|
+
* (value / map / range / eval). The property panel writes the same shape.
|
|
21
|
+
*
|
|
22
|
+
* Writes return scene-edit `modify` ops, so they go through the same preview → apply → revert →
|
|
23
|
+
* save path as every other board edit. Nothing here touches the database.
|
|
24
|
+
*/
|
|
25
|
+
const ai_client_base_1 = require("@things-factory/ai-client-base");
|
|
26
|
+
const component_tree_1 = require("./component-tree");
|
|
27
|
+
const RULES = ['value', 'map', 'range', 'eval'];
|
|
28
|
+
/* The write builder receives the board's top-level components; wrap them so the tree helpers work. */
|
|
29
|
+
const boardOf = (components) => ({ components: [...(components ?? [])] });
|
|
30
|
+
function idFor(component, refid, taken) {
|
|
31
|
+
const base = String(component?.type || 'component')
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
34
|
+
.replace(/^-+|-+$/g, '') || 'component';
|
|
35
|
+
let id = `${base}-${refid}`;
|
|
36
|
+
for (let n = 2; taken.has(id); n++)
|
|
37
|
+
id = `${base}-${refid}-${n}`;
|
|
38
|
+
return id;
|
|
39
|
+
}
|
|
40
|
+
/** What a mapping means, in the words the model and the person use. */
|
|
41
|
+
function describeMapping(m, index, idToRefids) {
|
|
42
|
+
const target = (m.target || '(self)').trim();
|
|
43
|
+
const refids = target.startsWith('#') ? idToRefids.get(target.slice(1)) : undefined;
|
|
44
|
+
return {
|
|
45
|
+
index,
|
|
46
|
+
target,
|
|
47
|
+
...(refids ? { targetRefids: refids } : {}),
|
|
48
|
+
property: m.property,
|
|
49
|
+
accessor: m.accessor || undefined,
|
|
50
|
+
rule: m.rule || 'value',
|
|
51
|
+
...(m.param !== undefined ? { param: m.param } : {}),
|
|
52
|
+
...(m.partial ? { partial: true } : {}),
|
|
53
|
+
...(m.ndnsp ? { ndnsp: true } : {})
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
const getBindings = {
|
|
57
|
+
kind: 'read',
|
|
58
|
+
name: 'getBindings',
|
|
59
|
+
/* Answers from the board the surface sent; nothing behind it to gate. */
|
|
60
|
+
doors: [],
|
|
61
|
+
description: 'List the data bindings on the board (or on one component). A binding lives on the component that holds the data ' +
|
|
62
|
+
'(the source) and spreads a value into a property of a target. Returns, per source component: refid, id, type, ' +
|
|
63
|
+
'whether it holds initial data, and its mappings with target selector, resolved target refids, property, accessor, rule and rule parameter. ' +
|
|
64
|
+
'Also lists components that already have an id (usable as targets without changes).',
|
|
65
|
+
schema: {
|
|
66
|
+
type: 'object',
|
|
67
|
+
properties: {
|
|
68
|
+
refid: { type: 'number', description: 'Only this component. Omit for every component that has bindings.' }
|
|
69
|
+
}
|
|
70
|
+
},
|
|
71
|
+
builder: (args, context) => {
|
|
72
|
+
const flat = (0, component_tree_1.collectAllComponents)(context?.board);
|
|
73
|
+
const idToRefids = new Map();
|
|
74
|
+
for (const f of flat) {
|
|
75
|
+
const id = f.component?.id;
|
|
76
|
+
if (id && typeof f.refid === 'number')
|
|
77
|
+
idToRefids.set(id, [...(idToRefids.get(id) ?? []), f.refid]);
|
|
78
|
+
}
|
|
79
|
+
const only = typeof args?.refid === 'number' ? args.refid : undefined;
|
|
80
|
+
const sources = flat
|
|
81
|
+
.filter(f => (only === undefined ? Array.isArray(f.component?.mappings) && f.component.mappings.length > 0 : f.refid === only))
|
|
82
|
+
.map(f => {
|
|
83
|
+
const c = f.component;
|
|
84
|
+
const mappings = Array.isArray(c.mappings) ? c.mappings : [];
|
|
85
|
+
return {
|
|
86
|
+
refid: f.refid,
|
|
87
|
+
id: c.id || undefined,
|
|
88
|
+
type: c.type,
|
|
89
|
+
hasInitialData: c.data !== undefined && c.data !== null && c.data !== '',
|
|
90
|
+
mappings: mappings.map((m, i) => describeMapping(m, i, idToRefids))
|
|
91
|
+
};
|
|
92
|
+
});
|
|
93
|
+
if (only !== undefined && sources.length === 0) {
|
|
94
|
+
return { error: `No component with refid ${only} on this board.` };
|
|
95
|
+
}
|
|
96
|
+
return {
|
|
97
|
+
sources,
|
|
98
|
+
componentsWithId: [...idToRefids.entries()].map(([id, refids]) => ({ id, refids }))
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
const bindData = {
|
|
103
|
+
kind: 'write',
|
|
104
|
+
name: 'bindData',
|
|
105
|
+
/* Proposes an edit the person saves through the board's own door — offer it only to those who can. */
|
|
106
|
+
doors: ['Mutation.updateBoard'],
|
|
107
|
+
description: 'Bind data from a source component to a property of a target component. Use this for every data binding - never write ' +
|
|
108
|
+
'`mappings` through modifyComponentByRefid. The binding is stored on the SOURCE (the component that holds or receives the data, ' +
|
|
109
|
+
'e.g. a data source, or the component itself). Give the target as targetRefid (an id is assigned to it if missing) or as a selector. ' +
|
|
110
|
+
'rule "value" passes the value through; "map" translates exact values ({"ON":"#2e7d32","OFF":"#c62828","default":"#9e9e9e"}); ' +
|
|
111
|
+
'"range" maps numeric ranges written "from~to", open ends allowed ({"~50":"green","50~80":"orange","80~":"red"}); ' +
|
|
112
|
+
'"eval" runs a short expression over `value` - use only when map or range cannot express it. ' +
|
|
113
|
+
'An existing binding with the same target and property on that source is replaced.',
|
|
114
|
+
schema: {
|
|
115
|
+
type: 'object',
|
|
116
|
+
properties: {
|
|
117
|
+
sourceRefid: { type: 'number', description: 'Component whose data is spread (where the binding is stored).' },
|
|
118
|
+
targetRefid: {
|
|
119
|
+
type: 'number',
|
|
120
|
+
description: 'Component that receives the value. Omit (or equal to sourceRefid) to bind into the source itself.'
|
|
121
|
+
},
|
|
122
|
+
targetSelector: {
|
|
123
|
+
type: 'string',
|
|
124
|
+
description: 'Instead of targetRefid: a selector for many targets, e.g. ".alarm" (class), "#line-3" (id), "(self)".'
|
|
125
|
+
},
|
|
126
|
+
property: {
|
|
127
|
+
type: 'string',
|
|
128
|
+
description: 'Property path on the target, e.g. "value", "text", "fillStyle", "strokeStyle", "hidden", "data", "rotation".'
|
|
129
|
+
},
|
|
130
|
+
accessor: {
|
|
131
|
+
type: 'string',
|
|
132
|
+
description: 'Path into the source data, e.g. "temperature" or "items[0].qty". Omit to pass the whole data.'
|
|
133
|
+
},
|
|
134
|
+
rule: { type: 'string', enum: RULES, description: 'How the value is turned into the property value. Default "value".' },
|
|
135
|
+
map: { type: 'object', description: 'rule "map": exact value -> result, optional "default".' },
|
|
136
|
+
range: { type: 'object', description: 'rule "range": "from~to" -> result, open ends allowed, optional "default".' },
|
|
137
|
+
eval: { type: 'string', description: 'rule "eval": a short JavaScript expression over `value` that returns the property value.' },
|
|
138
|
+
partial: { type: 'boolean', description: 'Merge an object value into the property instead of replacing it.' },
|
|
139
|
+
skipEmpty: { type: 'boolean', description: 'Do not spread empty values (null, undefined, "") - keeps the last value.' }
|
|
140
|
+
},
|
|
141
|
+
required: ['sourceRefid', 'property']
|
|
142
|
+
},
|
|
143
|
+
builder: (args, context) => {
|
|
144
|
+
const errors = [];
|
|
145
|
+
const board = boardOf(context?.components);
|
|
146
|
+
const sourceRefid = args?.sourceRefid;
|
|
147
|
+
const property = typeof args?.property === 'string' ? args.property.trim() : '';
|
|
148
|
+
const rule = RULES.includes(args?.rule) ? args.rule : 'value';
|
|
149
|
+
const source = typeof sourceRefid === 'number' ? (0, component_tree_1.findComponentByRefid)(board, sourceRefid)?.component : undefined;
|
|
150
|
+
if (!source)
|
|
151
|
+
errors.push(`sourceRefid ${sourceRefid} is not a component on this board.`);
|
|
152
|
+
if (!property)
|
|
153
|
+
errors.push('property is required (the property path on the target, e.g. "value").');
|
|
154
|
+
if (typeof args?.targetRefid === 'number' && args?.targetSelector) {
|
|
155
|
+
errors.push('Give either targetRefid or targetSelector, not both.');
|
|
156
|
+
}
|
|
157
|
+
let param = undefined;
|
|
158
|
+
if (rule === 'map') {
|
|
159
|
+
if (!args?.map || typeof args.map !== 'object' || Array.isArray(args.map))
|
|
160
|
+
errors.push('rule "map" needs a "map" object.');
|
|
161
|
+
else
|
|
162
|
+
param = args.map;
|
|
163
|
+
}
|
|
164
|
+
else if (rule === 'range') {
|
|
165
|
+
if (!args?.range || typeof args.range !== 'object' || Array.isArray(args.range))
|
|
166
|
+
errors.push('rule "range" needs a "range" object with "from~to" keys.');
|
|
167
|
+
else {
|
|
168
|
+
const bad = Object.keys(args.range).filter(k => k !== 'default' && !k.includes('~'));
|
|
169
|
+
if (bad.length)
|
|
170
|
+
errors.push(`range keys must be written "from~to" (open ends allowed): ${bad.join(', ')}`);
|
|
171
|
+
param = args.range;
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
else if (rule === 'eval') {
|
|
175
|
+
if (typeof args?.eval !== 'string' || !args.eval.trim())
|
|
176
|
+
errors.push('rule "eval" needs an "eval" expression.');
|
|
177
|
+
else
|
|
178
|
+
param = args.eval;
|
|
179
|
+
}
|
|
180
|
+
const ops = [];
|
|
181
|
+
let target = '(self)';
|
|
182
|
+
if (typeof args?.targetSelector === 'string' && args.targetSelector.trim()) {
|
|
183
|
+
target = args.targetSelector.trim();
|
|
184
|
+
}
|
|
185
|
+
else if (typeof args?.targetRefid === 'number' && args.targetRefid !== sourceRefid) {
|
|
186
|
+
const found = (0, component_tree_1.findComponentByRefid)(board, args.targetRefid)?.component;
|
|
187
|
+
if (!found) {
|
|
188
|
+
errors.push(`targetRefid ${args.targetRefid} is not a component on this board.`);
|
|
189
|
+
}
|
|
190
|
+
else if (found.id) {
|
|
191
|
+
target = `#${found.id}`;
|
|
192
|
+
}
|
|
193
|
+
else {
|
|
194
|
+
const taken = new Set((0, component_tree_1.collectAllComponents)(board)
|
|
195
|
+
.map(f => f.component?.id)
|
|
196
|
+
.filter(Boolean));
|
|
197
|
+
const id = idFor(found, args.targetRefid, taken);
|
|
198
|
+
ops.push({ op: 'modify', refid: args.targetRefid, patch: { id } });
|
|
199
|
+
target = `#${id}`;
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
if (errors.length)
|
|
203
|
+
return { ops: [], errors };
|
|
204
|
+
const mapping = {
|
|
205
|
+
target,
|
|
206
|
+
property,
|
|
207
|
+
rule,
|
|
208
|
+
...(typeof args?.accessor === 'string' && args.accessor.trim() ? { accessor: args.accessor.trim() } : {}),
|
|
209
|
+
...(param !== undefined ? { param } : {}),
|
|
210
|
+
...(args?.partial ? { partial: true } : {}),
|
|
211
|
+
...(args?.skipEmpty ? { ndnsp: true } : {})
|
|
212
|
+
};
|
|
213
|
+
const existing = Array.isArray(source.mappings) ? source.mappings : [];
|
|
214
|
+
const same = existing.findIndex(m => (m?.target || '(self)').trim() === target && (m?.property || '') === property);
|
|
215
|
+
const mappings = same >= 0 ? existing.map((m, i) => (i === same ? mapping : m)) : [...existing, mapping];
|
|
216
|
+
ops.unshift({ op: 'modify', refid: sourceRefid, patch: { mappings } });
|
|
217
|
+
return { ops };
|
|
218
|
+
}
|
|
219
|
+
};
|
|
220
|
+
const unbindData = {
|
|
221
|
+
kind: 'write',
|
|
222
|
+
name: 'unbindData',
|
|
223
|
+
doors: ['Mutation.updateBoard'],
|
|
224
|
+
description: 'Remove a data binding from a source component - by its index (from getBindings) or by target selector and property. ' +
|
|
225
|
+
'Leaves the target component unchanged.',
|
|
226
|
+
schema: {
|
|
227
|
+
type: 'object',
|
|
228
|
+
properties: {
|
|
229
|
+
sourceRefid: { type: 'number', description: 'Component the binding is stored on.' },
|
|
230
|
+
index: { type: 'number', description: 'Mapping index as returned by getBindings.' },
|
|
231
|
+
target: { type: 'string', description: 'Instead of index: the mapping target selector, e.g. "#gauge-12".' },
|
|
232
|
+
property: { type: 'string', description: 'Instead of index: the mapping property, with target.' }
|
|
233
|
+
},
|
|
234
|
+
required: ['sourceRefid']
|
|
235
|
+
},
|
|
236
|
+
builder: (args, context) => {
|
|
237
|
+
const board = boardOf(context?.components);
|
|
238
|
+
const source = typeof args?.sourceRefid === 'number' ? (0, component_tree_1.findComponentByRefid)(board, args.sourceRefid)?.component : undefined;
|
|
239
|
+
if (!source)
|
|
240
|
+
return { ops: [], errors: [`sourceRefid ${args?.sourceRefid} is not a component on this board.`] };
|
|
241
|
+
const existing = Array.isArray(source.mappings) ? source.mappings : [];
|
|
242
|
+
let at = -1;
|
|
243
|
+
if (typeof args?.index === 'number')
|
|
244
|
+
at = args.index;
|
|
245
|
+
else if (typeof args?.target === 'string' || typeof args?.property === 'string')
|
|
246
|
+
at = existing.findIndex(m => (args.target === undefined || (m?.target || '(self)').trim() === String(args.target).trim()) &&
|
|
247
|
+
(args.property === undefined || m?.property === args.property));
|
|
248
|
+
if (at < 0 || at >= existing.length) {
|
|
249
|
+
return { ops: [], errors: [`No such binding on refid ${args.sourceRefid} (it has ${existing.length}). Call getBindings first.`] };
|
|
250
|
+
}
|
|
251
|
+
return { ops: [{ op: 'modify', refid: args.sourceRefid, patch: { mappings: existing.filter((_, i) => i !== at) } }] };
|
|
252
|
+
}
|
|
253
|
+
};
|
|
254
|
+
exports.boardBindingToolsCategory = {
|
|
255
|
+
name: 'board-binding',
|
|
256
|
+
description: 'Data binding on the open board: read, add, replace and remove mappings.',
|
|
257
|
+
guidance: [
|
|
258
|
+
'Data binding rules:',
|
|
259
|
+
'- For any request to connect, bind, link, drive or "show" data on components, use bindData. Never write `mappings` with modifyComponentByRefid.',
|
|
260
|
+
'- Call getBindings before changing or explaining bindings, so you speak from what is on the board.',
|
|
261
|
+
'- A binding is stored on the SOURCE - the component whose data is spread (a data source, or the component itself). Pick the target by refid.',
|
|
262
|
+
'- Prefer rule "map" for states and "range" for thresholds; use "eval" only when neither can express it.',
|
|
263
|
+
'- After a bindData or unbindData call, say which source feeds which target property and with what rule. The person reviews and saves; do not say it is saved.'
|
|
264
|
+
].join('\n'),
|
|
265
|
+
specs: [getBindings, bindData, unbindData]
|
|
266
|
+
};
|
|
267
|
+
// Registered on load, like board-import's category.
|
|
268
|
+
(0, ai_client_base_1.registerToolCategory)(exports.boardBindingToolsCategory);
|
|
269
|
+
//# sourceMappingURL=binding-tools.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"binding-tools.js","sourceRoot":"","sources":["../../server/service/binding-tools.ts"],"names":[],"mappings":";;;AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,mEAAuG;AAEvG,qDAA6E;AAG7E,MAAM,KAAK,GAAW,CAAC,OAAO,EAAE,KAAK,EAAE,OAAO,EAAE,MAAM,CAAC,CAAA;AAavD,sGAAsG;AACtG,MAAM,OAAO,GAAG,CAAC,UAA0C,EAAE,EAAE,CAAC,CAAC,EAAE,UAAU,EAAE,CAAC,GAAG,CAAC,UAAU,IAAI,EAAE,CAAC,CAAC,EAAE,CAAQ,CAAA;AAEhH,SAAS,KAAK,CAAC,SAAc,EAAE,KAAa,EAAE,KAAkB;IAC9D,MAAM,IAAI,GACR,MAAM,CAAC,SAAS,EAAE,IAAI,IAAI,WAAW,CAAC;SACnC,WAAW,EAAE;SACb,OAAO,CAAC,aAAa,EAAE,GAAG,CAAC;SAC3B,OAAO,CAAC,UAAU,EAAE,EAAE,CAAC,IAAI,WAAW,CAAA;IAC3C,IAAI,EAAE,GAAG,GAAG,IAAI,IAAI,KAAK,EAAE,CAAA;IAC3B,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE;QAAE,EAAE,GAAG,GAAG,IAAI,IAAI,KAAK,IAAI,CAAC,EAAE,CAAA;IAChE,OAAO,EAAE,CAAA;AACX,CAAC;AAED,uEAAuE;AACvE,SAAS,eAAe,CAAC,CAAU,EAAE,KAAa,EAAE,UAAiC;IACnF,MAAM,MAAM,GAAG,CAAC,CAAC,CAAC,MAAM,IAAI,QAAQ,CAAC,CAAC,IAAI,EAAE,CAAA;IAC5C,MAAM,MAAM,GAAG,MAAM,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,UAAU,CAAC,GAAG,CAAC,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAA;IACnF,OAAO;QACL,KAAK;QACL,MAAM;QACN,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,YAAY,EAAE,MAAM,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QAC3C,QAAQ,EAAE,CAAC,CAAC,QAAQ;QACpB,QAAQ,EAAE,CAAC,CAAC,QAAQ,IAAI,SAAS;QACjC,IAAI,EAAE,CAAC,CAAC,IAAI,IAAI,OAAO;QACvB,GAAG,CAAC,CAAC,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACpD,GAAG,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;QACvC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;KACpC,CAAA;AACH,CAAC;AAED,MAAM,WAAW,GAAa;IAC5B,IAAI,EAAE,MAAM;IACZ,IAAI,EAAE,aAAa;IACnB,yEAAyE;IACzE,KAAK,EAAE,EAAE;IACT,WAAW,EACT,kHAAkH;QAClH,gHAAgH;QAChH,6IAA6I;QAC7I,oFAAoF;IACtF,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,kEAAkE,EAAE;SAC3G;KACF;IACD,OAAO,EAAE,CAAC,IAAS,EAAE,OAAY,EAAE,EAAE;QACnC,MAAM,IAAI,GAAG,IAAA,qCAAoB,EAAC,OAAO,EAAE,KAAK,CAAC,CAAA;QACjD,MAAM,UAAU,GAAG,IAAI,GAAG,EAAoB,CAAA;QAC9C,KAAK,MAAM,CAAC,IAAI,IAAI,EAAE,CAAC;YACrB,MAAM,EAAE,GAAI,CAAC,CAAC,SAAiB,EAAE,EAAE,CAAA;YACnC,IAAI,EAAE,IAAI,OAAO,CAAC,CAAC,KAAK,KAAK,QAAQ;gBAAE,UAAU,CAAC,GAAG,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,UAAU,CAAC,GAAG,CAAC,EAAE,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAA;QACrG,CAAC;QAED,MAAM,IAAI,GAAG,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAA;QACrE,MAAM,OAAO,GAAG,IAAI;aACjB,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAE,CAAC,CAAC,SAAiB,EAAE,QAAQ,CAAC,IAAK,CAAC,CAAC,SAAiB,CAAC,QAAQ,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,KAAK,IAAI,CAAC,CAAC;aAChJ,GAAG,CAAC,CAAC,CAAC,EAAE;YACP,MAAM,CAAC,GAAQ,CAAC,CAAC,SAAS,CAAA;YAC1B,MAAM,QAAQ,GAAc,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;YACvE,OAAO;gBACL,KAAK,EAAE,CAAC,CAAC,KAAK;gBACd,EAAE,EAAE,CAAC,CAAC,EAAE,IAAI,SAAS;gBACrB,IAAI,EAAE,CAAC,CAAC,IAAI;gBACZ,cAAc,EAAE,CAAC,CAAC,IAAI,KAAK,SAAS,IAAI,CAAC,CAAC,IAAI,KAAK,IAAI,IAAI,CAAC,CAAC,IAAI,KAAK,EAAE;gBACxE,QAAQ,EAAE,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,eAAe,CAAC,CAAC,EAAE,CAAC,EAAE,UAAU,CAAC,CAAC;aACpE,CAAA;QACH,CAAC,CAAC,CAAA;QAEJ,IAAI,IAAI,KAAK,SAAS,IAAI,OAAO,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC/C,OAAO,EAAE,KAAK,EAAE,2BAA2B,IAAI,iBAAiB,EAAE,CAAA;QACpE,CAAC;QAED,OAAO;YACL,OAAO;YACP,gBAAgB,EAAE,CAAC,GAAG,UAAU,CAAC,OAAO,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,MAAM,CAAC,EAAE,EAAE,CAAC,CAAC,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,CAAC;SACpF,CAAA;IACH,CAAC;CACF,CAAA;AAED,MAAM,QAAQ,GAAa;IACzB,IAAI,EAAE,OAAO;IACb,IAAI,EAAE,UAAU;IAChB,sGAAsG;IACtG,KAAK,EAAE,CAAC,sBAAsB,CAAC;IAC/B,WAAW,EACT,uHAAuH;QACvH,iIAAiI;QACjI,sIAAsI;QACtI,+HAA+H;QAC/H,mHAAmH;QACnH,8FAA8F;QAC9F,mFAAmF;IACrF,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,+DAA+D,EAAE;YAC7G,WAAW,EAAE;gBACX,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,mGAAmG;aACjH;YACD,cAAc,EAAE;gBACd,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,uGAAuG;aACrH;YACD,QAAQ,EAAE;gBACR,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,8GAA8G;aAC5H;YACD,QAAQ,EAAE;gBACR,IAAI,EAAE,QAAQ;gBACd,WAAW,EAAE,+FAA+F;aAC7G;YACD,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,KAAK,EAAE,WAAW,EAAE,mEAAmE,EAAE;YACvH,GAAG,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,wDAAwD,EAAE;YAC9F,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,2EAA2E,EAAE;YACnH,IAAI,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,0FAA0F,EAAE;YACjI,OAAO,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,kEAAkE,EAAE;YAC7G,SAAS,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,0EAA0E,EAAE;SACxH;QACD,QAAQ,EAAE,CAAC,aAAa,EAAE,UAAU,CAAC;KACtC;IACD,OAAO,EAAE,CAAC,IAAS,EAAE,OAAY,EAAE,EAAE;QACnC,MAAM,MAAM,GAAa,EAAE,CAAA;QAC3B,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;QAC1C,MAAM,WAAW,GAAG,IAAI,EAAE,WAAW,CAAA;QACrC,MAAM,QAAQ,GAAG,OAAO,IAAI,EAAE,QAAQ,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAA;QAC/E,MAAM,IAAI,GAAS,KAAK,CAAC,QAAQ,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAA;QAEnE,MAAM,MAAM,GAAG,OAAO,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAA,qCAAoB,EAAC,KAAK,EAAE,WAAW,CAAC,EAAE,SAAS,CAAC,CAAC,CAAC,SAAS,CAAA;QAChH,IAAI,CAAC,MAAM;YAAE,MAAM,CAAC,IAAI,CAAC,eAAe,WAAW,oCAAoC,CAAC,CAAA;QACxF,IAAI,CAAC,QAAQ;YAAE,MAAM,CAAC,IAAI,CAAC,uEAAuE,CAAC,CAAA;QACnG,IAAI,OAAO,IAAI,EAAE,WAAW,KAAK,QAAQ,IAAI,IAAI,EAAE,cAAc,EAAE,CAAC;YAClE,MAAM,CAAC,IAAI,CAAC,sDAAsD,CAAC,CAAA;QACrE,CAAC;QAED,IAAI,KAAK,GAAQ,SAAS,CAAA;QAC1B,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;YACnB,IAAI,CAAC,IAAI,EAAE,GAAG,IAAI,OAAO,IAAI,CAAC,GAAG,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,GAAG,CAAC;gBAAE,MAAM,CAAC,IAAI,CAAC,kCAAkC,CAAC,CAAA;;gBACrH,KAAK,GAAG,IAAI,CAAC,GAAG,CAAA;QACvB,CAAC;aAAM,IAAI,IAAI,KAAK,OAAO,EAAE,CAAC;YAC5B,IAAI,CAAC,IAAI,EAAE,KAAK,IAAI,OAAO,IAAI,CAAC,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,OAAO,CAAC,IAAI,CAAC,KAAK,CAAC;gBAC7E,MAAM,CAAC,IAAI,CAAC,0DAA0D,CAAC,CAAA;iBACpE,CAAC;gBACJ,MAAM,GAAG,GAAG,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,SAAS,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAA;gBACpF,IAAI,GAAG,CAAC,MAAM;oBAAE,MAAM,CAAC,IAAI,CAAC,6DAA6D,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;gBAC1G,KAAK,GAAG,IAAI,CAAC,KAAK,CAAA;YACpB,CAAC;QACH,CAAC;aAAM,IAAI,IAAI,KAAK,MAAM,EAAE,CAAC;YAC3B,IAAI,OAAO,IAAI,EAAE,IAAI,KAAK,QAAQ,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE;gBAAE,MAAM,CAAC,IAAI,CAAC,yCAAyC,CAAC,CAAA;;gBAC1G,KAAK,GAAG,IAAI,CAAC,IAAI,CAAA;QACxB,CAAC;QAED,MAAM,GAAG,GAAU,EAAE,CAAA;QACrB,IAAI,MAAM,GAAG,QAAQ,CAAA;QAErB,IAAI,OAAO,IAAI,EAAE,cAAc,KAAK,QAAQ,IAAI,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,EAAE,CAAC;YAC3E,MAAM,GAAG,IAAI,CAAC,cAAc,CAAC,IAAI,EAAE,CAAA;QACrC,CAAC;aAAM,IAAI,OAAO,IAAI,EAAE,WAAW,KAAK,QAAQ,IAAI,IAAI,CAAC,WAAW,KAAK,WAAW,EAAE,CAAC;YACrF,MAAM,KAAK,GAAG,IAAA,qCAAoB,EAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,SAAgB,CAAA;YAC7E,IAAI,CAAC,KAAK,EAAE,CAAC;gBACX,MAAM,CAAC,IAAI,CAAC,eAAe,IAAI,CAAC,WAAW,oCAAoC,CAAC,CAAA;YAClF,CAAC;iBAAM,IAAI,KAAK,CAAC,EAAE,EAAE,CAAC;gBACpB,MAAM,GAAG,IAAI,KAAK,CAAC,EAAE,EAAE,CAAA;YACzB,CAAC;iBAAM,CAAC;gBACN,MAAM,KAAK,GAAG,IAAI,GAAG,CACnB,IAAA,qCAAoB,EAAC,KAAK,CAAC;qBACxB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAE,CAAC,CAAC,SAAiB,EAAE,EAAE,CAAC;qBAClC,MAAM,CAAC,OAAO,CAAC,CACnB,CAAA;gBACD,MAAM,EAAE,GAAG,KAAK,CAAC,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,CAAC,CAAA;gBAChD,GAAG,CAAC,IAAI,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;gBAClE,MAAM,GAAG,IAAI,EAAE,EAAE,CAAA;YACnB,CAAC;QACH,CAAC;QAED,IAAI,MAAM,CAAC,MAAM;YAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAA;QAE7C,MAAM,OAAO,GAAY;YACvB,MAAM;YACN,QAAQ;YACR,IAAI;YACJ,GAAG,CAAC,OAAO,IAAI,EAAE,QAAQ,KAAK,QAAQ,IAAI,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,QAAQ,EAAE,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzG,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YACzC,GAAG,CAAC,IAAI,EAAE,OAAO,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC3C,GAAG,CAAC,IAAI,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SAC5C,CAAA;QAED,MAAM,QAAQ,GAAc,KAAK,CAAC,OAAO,CAAE,MAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAE,MAAc,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;QACnG,MAAM,IAAI,GAAG,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,MAAM,IAAI,QAAQ,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,IAAI,CAAC,CAAC,EAAE,QAAQ,IAAI,EAAE,CAAC,KAAK,QAAQ,CAAC,CAAA;QACnH,MAAM,QAAQ,GAAG,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,GAAG,QAAQ,EAAE,OAAO,CAAC,CAAA;QAExG,GAAG,CAAC,OAAO,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,WAAW,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,EAAE,CAAC,CAAA;QACtE,OAAO,EAAE,GAAG,EAAE,CAAA;IAChB,CAAC;CACF,CAAA;AAED,MAAM,UAAU,GAAa;IAC3B,IAAI,EAAE,OAAO;IACb,IAAI,EAAE,YAAY;IAClB,KAAK,EAAE,CAAC,sBAAsB,CAAC;IAC/B,WAAW,EACT,sHAAsH;QACtH,wCAAwC;IAC1C,MAAM,EAAE;QACN,IAAI,EAAE,QAAQ;QACd,UAAU,EAAE;YACV,WAAW,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,qCAAqC,EAAE;YACnF,KAAK,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,2CAA2C,EAAE;YACnF,MAAM,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,kEAAkE,EAAE;YAC3G,QAAQ,EAAE,EAAE,IAAI,EAAE,QAAQ,EAAE,WAAW,EAAE,sDAAsD,EAAE;SAClG;QACD,QAAQ,EAAE,CAAC,aAAa,CAAC;KAC1B;IACD,OAAO,EAAE,CAAC,IAAS,EAAE,OAAY,EAAE,EAAE;QACnC,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,EAAE,UAAU,CAAC,CAAA;QAC1C,MAAM,MAAM,GAAG,OAAO,IAAI,EAAE,WAAW,KAAK,QAAQ,CAAC,CAAC,CAAE,IAAA,qCAAoB,EAAC,KAAK,EAAE,IAAI,CAAC,WAAW,CAAC,EAAE,SAAiB,CAAC,CAAC,CAAC,SAAS,CAAA;QACpI,IAAI,CAAC,MAAM;YAAE,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,eAAe,IAAI,EAAE,WAAW,oCAAoC,CAAC,EAAE,CAAA;QAE/G,MAAM,QAAQ,GAAc,KAAK,CAAC,OAAO,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAA;QACjF,IAAI,EAAE,GAAG,CAAC,CAAC,CAAA;QACX,IAAI,OAAO,IAAI,EAAE,KAAK,KAAK,QAAQ;YAAE,EAAE,GAAG,IAAI,CAAC,KAAK,CAAA;aAC/C,IAAI,OAAO,IAAI,EAAE,MAAM,KAAK,QAAQ,IAAI,OAAO,IAAI,EAAE,QAAQ,KAAK,QAAQ;YAC7E,EAAE,GAAG,QAAQ,CAAC,SAAS,CACrB,CAAC,CAAC,EAAE,CACF,CAAC,IAAI,CAAC,MAAM,KAAK,SAAS,IAAI,CAAC,CAAC,EAAE,MAAM,IAAI,QAAQ,CAAC,CAAC,IAAI,EAAE,KAAK,MAAM,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,IAAI,EAAE,CAAC;gBAC5F,CAAC,IAAI,CAAC,QAAQ,KAAK,SAAS,IAAI,CAAC,EAAE,QAAQ,KAAK,IAAI,CAAC,QAAQ,CAAC,CACjE,CAAA;QAEH,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,IAAI,QAAQ,CAAC,MAAM,EAAE,CAAC;YACpC,OAAO,EAAE,GAAG,EAAE,EAAE,EAAE,MAAM,EAAE,CAAC,4BAA4B,IAAI,CAAC,WAAW,YAAY,QAAQ,CAAC,MAAM,4BAA4B,CAAC,EAAE,CAAA;QACnI,CAAC;QAED,OAAO,EAAE,GAAG,EAAE,CAAC,EAAE,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,IAAI,CAAC,WAAW,EAAE,KAAK,EAAE,EAAE,QAAQ,EAAE,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,EAAE,CAAC,EAAE,EAAE,CAAC,EAAE,CAAA;IACvH,CAAC;CACF,CAAA;AAEY,QAAA,yBAAyB,GAAiB;IACrD,IAAI,EAAE,eAAe;IACrB,WAAW,EAAE,yEAAyE;IACtF,QAAQ,EAAE;QACR,qBAAqB;QACrB,iJAAiJ;QACjJ,oGAAoG;QACpG,8IAA8I;QAC9I,yGAAyG;QACzG,+JAA+J;KAChK,CAAC,IAAI,CAAC,IAAI,CAAC;IACZ,KAAK,EAAE,CAAC,WAAW,EAAE,QAAQ,EAAE,UAAU,CAAC;CAC3C,CAAA;AAED,oDAAoD;AACpD,IAAA,qCAAoB,EAAC,iCAAyB,CAAC,CAAA","sourcesContent":["/**\n * Data binding as first-class board tools — `getBindings`, `bindData`, `unbindData`.\n *\n * ── Why these exist ──────────────────────────────────────────────────────────\n * A binding was reachable only through `modifyComponentByRefid` with a raw `mappings` array. The\n * model had to know the mapping shape, where the mapping lives (on the component that HAS the\n * data, not on the one that shows it), that a target is a selector, and that a `#id` selector\n * needs the target to carry an id. It guessed, and a wrong guess is silent: the patch applies,\n * nothing moves on the board, no error anywhere.\n *\n * These tools take refids and plain words and do the bookkeeping here:\n * - the mapping is appended to the SOURCE's `mappings` (same target + property replaces);\n * - a target given by refid gets an `id` if it has none, and the mapping points at `#id`;\n * - the rule's parameter lands where the engine reads it (`mapping.param`).\n *\n * The engine side is `things-scene/src/components/data/data.ts` (spreading) and `evaluator.ts`\n * (value / map / range / eval). The property panel writes the same shape.\n *\n * Writes return scene-edit `modify` ops, so they go through the same preview → apply → revert →\n * save path as every other board edit. Nothing here touches the database.\n */\nimport { registerToolCategory, type ToolCategory, type ToolSpec } from '@things-factory/ai-client-base'\n\nimport { collectAllComponents, findComponentByRefid } from './component-tree'\n\ntype Rule = 'value' | 'map' | 'range' | 'eval'\nconst RULES: Rule[] = ['value', 'map', 'range', 'eval']\n\ninterface Mapping {\n source?: string\n target?: string\n property?: string\n accessor?: string\n rule?: Rule\n param?: any\n partial?: boolean\n ndnsp?: boolean\n}\n\n/* The write builder receives the board's top-level components; wrap them so the tree helpers work. */\nconst boardOf = (components: ReadonlyArray<any> | undefined) => ({ components: [...(components ?? [])] }) as any\n\nfunction idFor(component: any, refid: number, taken: Set<string>): string {\n const base =\n String(component?.type || 'component')\n .toLowerCase()\n .replace(/[^a-z0-9]+/g, '-')\n .replace(/^-+|-+$/g, '') || 'component'\n let id = `${base}-${refid}`\n for (let n = 2; taken.has(id); n++) id = `${base}-${refid}-${n}`\n return id\n}\n\n/** What a mapping means, in the words the model and the person use. */\nfunction describeMapping(m: Mapping, index: number, idToRefids: Map<string, number[]>) {\n const target = (m.target || '(self)').trim()\n const refids = target.startsWith('#') ? idToRefids.get(target.slice(1)) : undefined\n return {\n index,\n target,\n ...(refids ? { targetRefids: refids } : {}),\n property: m.property,\n accessor: m.accessor || undefined,\n rule: m.rule || 'value',\n ...(m.param !== undefined ? { param: m.param } : {}),\n ...(m.partial ? { partial: true } : {}),\n ...(m.ndnsp ? { ndnsp: true } : {})\n }\n}\n\nconst getBindings: ToolSpec = {\n kind: 'read',\n name: 'getBindings',\n /* Answers from the board the surface sent; nothing behind it to gate. */\n doors: [],\n description:\n 'List the data bindings on the board (or on one component). A binding lives on the component that holds the data ' +\n '(the source) and spreads a value into a property of a target. Returns, per source component: refid, id, type, ' +\n 'whether it holds initial data, and its mappings with target selector, resolved target refids, property, accessor, rule and rule parameter. ' +\n 'Also lists components that already have an id (usable as targets without changes).',\n schema: {\n type: 'object',\n properties: {\n refid: { type: 'number', description: 'Only this component. Omit for every component that has bindings.' }\n }\n },\n builder: (args: any, context: any) => {\n const flat = collectAllComponents(context?.board)\n const idToRefids = new Map<string, number[]>()\n for (const f of flat) {\n const id = (f.component as any)?.id\n if (id && typeof f.refid === 'number') idToRefids.set(id, [...(idToRefids.get(id) ?? []), f.refid])\n }\n\n const only = typeof args?.refid === 'number' ? args.refid : undefined\n const sources = flat\n .filter(f => (only === undefined ? Array.isArray((f.component as any)?.mappings) && (f.component as any).mappings.length > 0 : f.refid === only))\n .map(f => {\n const c: any = f.component\n const mappings: Mapping[] = Array.isArray(c.mappings) ? c.mappings : []\n return {\n refid: f.refid,\n id: c.id || undefined,\n type: c.type,\n hasInitialData: c.data !== undefined && c.data !== null && c.data !== '',\n mappings: mappings.map((m, i) => describeMapping(m, i, idToRefids))\n }\n })\n\n if (only !== undefined && sources.length === 0) {\n return { error: `No component with refid ${only} on this board.` }\n }\n\n return {\n sources,\n componentsWithId: [...idToRefids.entries()].map(([id, refids]) => ({ id, refids }))\n }\n }\n}\n\nconst bindData: ToolSpec = {\n kind: 'write',\n name: 'bindData',\n /* Proposes an edit the person saves through the board's own door — offer it only to those who can. */\n doors: ['Mutation.updateBoard'],\n description:\n 'Bind data from a source component to a property of a target component. Use this for every data binding - never write ' +\n '`mappings` through modifyComponentByRefid. The binding is stored on the SOURCE (the component that holds or receives the data, ' +\n 'e.g. a data source, or the component itself). Give the target as targetRefid (an id is assigned to it if missing) or as a selector. ' +\n 'rule \"value\" passes the value through; \"map\" translates exact values ({\"ON\":\"#2e7d32\",\"OFF\":\"#c62828\",\"default\":\"#9e9e9e\"}); ' +\n '\"range\" maps numeric ranges written \"from~to\", open ends allowed ({\"~50\":\"green\",\"50~80\":\"orange\",\"80~\":\"red\"}); ' +\n '\"eval\" runs a short expression over `value` - use only when map or range cannot express it. ' +\n 'An existing binding with the same target and property on that source is replaced.',\n schema: {\n type: 'object',\n properties: {\n sourceRefid: { type: 'number', description: 'Component whose data is spread (where the binding is stored).' },\n targetRefid: {\n type: 'number',\n description: 'Component that receives the value. Omit (or equal to sourceRefid) to bind into the source itself.'\n },\n targetSelector: {\n type: 'string',\n description: 'Instead of targetRefid: a selector for many targets, e.g. \".alarm\" (class), \"#line-3\" (id), \"(self)\".'\n },\n property: {\n type: 'string',\n description: 'Property path on the target, e.g. \"value\", \"text\", \"fillStyle\", \"strokeStyle\", \"hidden\", \"data\", \"rotation\".'\n },\n accessor: {\n type: 'string',\n description: 'Path into the source data, e.g. \"temperature\" or \"items[0].qty\". Omit to pass the whole data.'\n },\n rule: { type: 'string', enum: RULES, description: 'How the value is turned into the property value. Default \"value\".' },\n map: { type: 'object', description: 'rule \"map\": exact value -> result, optional \"default\".' },\n range: { type: 'object', description: 'rule \"range\": \"from~to\" -> result, open ends allowed, optional \"default\".' },\n eval: { type: 'string', description: 'rule \"eval\": a short JavaScript expression over `value` that returns the property value.' },\n partial: { type: 'boolean', description: 'Merge an object value into the property instead of replacing it.' },\n skipEmpty: { type: 'boolean', description: 'Do not spread empty values (null, undefined, \"\") - keeps the last value.' }\n },\n required: ['sourceRefid', 'property']\n },\n builder: (args: any, context: any) => {\n const errors: string[] = []\n const board = boardOf(context?.components)\n const sourceRefid = args?.sourceRefid\n const property = typeof args?.property === 'string' ? args.property.trim() : ''\n const rule: Rule = RULES.includes(args?.rule) ? args.rule : 'value'\n\n const source = typeof sourceRefid === 'number' ? findComponentByRefid(board, sourceRefid)?.component : undefined\n if (!source) errors.push(`sourceRefid ${sourceRefid} is not a component on this board.`)\n if (!property) errors.push('property is required (the property path on the target, e.g. \"value\").')\n if (typeof args?.targetRefid === 'number' && args?.targetSelector) {\n errors.push('Give either targetRefid or targetSelector, not both.')\n }\n\n let param: any = undefined\n if (rule === 'map') {\n if (!args?.map || typeof args.map !== 'object' || Array.isArray(args.map)) errors.push('rule \"map\" needs a \"map\" object.')\n else param = args.map\n } else if (rule === 'range') {\n if (!args?.range || typeof args.range !== 'object' || Array.isArray(args.range))\n errors.push('rule \"range\" needs a \"range\" object with \"from~to\" keys.')\n else {\n const bad = Object.keys(args.range).filter(k => k !== 'default' && !k.includes('~'))\n if (bad.length) errors.push(`range keys must be written \"from~to\" (open ends allowed): ${bad.join(', ')}`)\n param = args.range\n }\n } else if (rule === 'eval') {\n if (typeof args?.eval !== 'string' || !args.eval.trim()) errors.push('rule \"eval\" needs an \"eval\" expression.')\n else param = args.eval\n }\n\n const ops: any[] = []\n let target = '(self)'\n\n if (typeof args?.targetSelector === 'string' && args.targetSelector.trim()) {\n target = args.targetSelector.trim()\n } else if (typeof args?.targetRefid === 'number' && args.targetRefid !== sourceRefid) {\n const found = findComponentByRefid(board, args.targetRefid)?.component as any\n if (!found) {\n errors.push(`targetRefid ${args.targetRefid} is not a component on this board.`)\n } else if (found.id) {\n target = `#${found.id}`\n } else {\n const taken = new Set(\n collectAllComponents(board)\n .map(f => (f.component as any)?.id)\n .filter(Boolean)\n )\n const id = idFor(found, args.targetRefid, taken)\n ops.push({ op: 'modify', refid: args.targetRefid, patch: { id } })\n target = `#${id}`\n }\n }\n\n if (errors.length) return { ops: [], errors }\n\n const mapping: Mapping = {\n target,\n property,\n rule,\n ...(typeof args?.accessor === 'string' && args.accessor.trim() ? { accessor: args.accessor.trim() } : {}),\n ...(param !== undefined ? { param } : {}),\n ...(args?.partial ? { partial: true } : {}),\n ...(args?.skipEmpty ? { ndnsp: true } : {})\n }\n\n const existing: Mapping[] = Array.isArray((source as any).mappings) ? (source as any).mappings : []\n const same = existing.findIndex(m => (m?.target || '(self)').trim() === target && (m?.property || '') === property)\n const mappings = same >= 0 ? existing.map((m, i) => (i === same ? mapping : m)) : [...existing, mapping]\n\n ops.unshift({ op: 'modify', refid: sourceRefid, patch: { mappings } })\n return { ops }\n }\n}\n\nconst unbindData: ToolSpec = {\n kind: 'write',\n name: 'unbindData',\n doors: ['Mutation.updateBoard'],\n description:\n 'Remove a data binding from a source component - by its index (from getBindings) or by target selector and property. ' +\n 'Leaves the target component unchanged.',\n schema: {\n type: 'object',\n properties: {\n sourceRefid: { type: 'number', description: 'Component the binding is stored on.' },\n index: { type: 'number', description: 'Mapping index as returned by getBindings.' },\n target: { type: 'string', description: 'Instead of index: the mapping target selector, e.g. \"#gauge-12\".' },\n property: { type: 'string', description: 'Instead of index: the mapping property, with target.' }\n },\n required: ['sourceRefid']\n },\n builder: (args: any, context: any) => {\n const board = boardOf(context?.components)\n const source = typeof args?.sourceRefid === 'number' ? (findComponentByRefid(board, args.sourceRefid)?.component as any) : undefined\n if (!source) return { ops: [], errors: [`sourceRefid ${args?.sourceRefid} is not a component on this board.`] }\n\n const existing: Mapping[] = Array.isArray(source.mappings) ? source.mappings : []\n let at = -1\n if (typeof args?.index === 'number') at = args.index\n else if (typeof args?.target === 'string' || typeof args?.property === 'string')\n at = existing.findIndex(\n m =>\n (args.target === undefined || (m?.target || '(self)').trim() === String(args.target).trim()) &&\n (args.property === undefined || m?.property === args.property)\n )\n\n if (at < 0 || at >= existing.length) {\n return { ops: [], errors: [`No such binding on refid ${args.sourceRefid} (it has ${existing.length}). Call getBindings first.`] }\n }\n\n return { ops: [{ op: 'modify', refid: args.sourceRefid, patch: { mappings: existing.filter((_, i) => i !== at) } }] }\n }\n}\n\nexport const boardBindingToolsCategory: ToolCategory = {\n name: 'board-binding',\n description: 'Data binding on the open board: read, add, replace and remove mappings.',\n guidance: [\n 'Data binding rules:',\n '- For any request to connect, bind, link, drive or \"show\" data on components, use bindData. Never write `mappings` with modifyComponentByRefid.',\n '- Call getBindings before changing or explaining bindings, so you speak from what is on the board.',\n '- A binding is stored on the SOURCE - the component whose data is spread (a data source, or the component itself). Pick the target by refid.',\n '- Prefer rule \"map\" for states and \"range\" for thresholds; use \"eval\" only when neither can express it.',\n '- After a bindData or unbindData call, say which source feeds which target property and with what rule. The person reviews and saves; do not say it is saved.'\n ].join('\\n'),\n specs: [getBindings, bindData, unbindData]\n}\n\n// Registered on load, like board-import's category.\nregisterToolCategory(boardBindingToolsCategory)\n"]}
|
|
@@ -7,6 +7,10 @@ declare class BoardAIMentionPickInput {
|
|
|
7
7
|
token: string;
|
|
8
8
|
refid: number;
|
|
9
9
|
}
|
|
10
|
+
export declare class BoardAIModeInput {
|
|
11
|
+
kind: string;
|
|
12
|
+
target?: string;
|
|
13
|
+
}
|
|
10
14
|
declare class BoardAIChatInput {
|
|
11
15
|
sessionId?: string;
|
|
12
16
|
messages: BoardAILLMMessageInput[];
|
|
@@ -14,6 +18,7 @@ declare class BoardAIChatInput {
|
|
|
14
18
|
scopes?: string[];
|
|
15
19
|
hostContext?: any;
|
|
16
20
|
truncateAfterMessageId?: string;
|
|
21
|
+
mode?: BoardAIModeInput;
|
|
17
22
|
toolCategories?: string[];
|
|
18
23
|
boardTools?: boolean;
|
|
19
24
|
requireGroundingTools?: boolean;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.BoardAIChatResolver = void 0;
|
|
3
|
+
exports.BoardAIChatResolver = exports.BoardAIModeInput = void 0;
|
|
4
4
|
const tslib_1 = require("tslib");
|
|
5
5
|
/**
|
|
6
6
|
* GraphQL resolver — AI 주도 보드 모델링 (chat).
|
|
@@ -22,6 +22,7 @@ const ai_assistant_2 = require("@things-factory/ai-assistant");
|
|
|
22
22
|
const ai_assistant_3 = require("@things-factory/ai-assistant");
|
|
23
23
|
const ai_assistant_4 = require("@things-factory/ai-assistant");
|
|
24
24
|
const ai_assistant_5 = require("@things-factory/ai-assistant");
|
|
25
|
+
const ai_assistant_6 = require("@things-factory/ai-assistant");
|
|
25
26
|
/**
|
|
26
27
|
* 모델에 넘길 최근 메시지 수 상한.
|
|
27
28
|
*
|
|
@@ -37,13 +38,13 @@ const LLM_HISTORY_MAX_TURNS = 40;
|
|
|
37
38
|
* 상한을 넘긴 뒤 매 턴 요약을 만들면 대화마다 LLM 호출이 두 번씩 붙는다. 10 개씩 모아 접는다.
|
|
38
39
|
*/
|
|
39
40
|
const LLM_SUMMARY_THRESHOLD = 10;
|
|
40
|
-
const ai_assistant_6 = require("@things-factory/ai-assistant");
|
|
41
41
|
const ai_assistant_7 = require("@things-factory/ai-assistant");
|
|
42
42
|
const ai_assistant_8 = require("@things-factory/ai-assistant");
|
|
43
|
+
const ai_assistant_9 = require("@things-factory/ai-assistant");
|
|
43
44
|
const patch_entry_js_1 = require("./patch-entry/patch-entry.js");
|
|
44
45
|
const board_patch_subscription_js_1 = require("./patch-entry/board-patch-subscription.js");
|
|
45
|
-
const ai_assistant_9 = require("@things-factory/ai-assistant");
|
|
46
46
|
const ai_assistant_10 = require("@things-factory/ai-assistant");
|
|
47
|
+
const ai_assistant_11 = require("@things-factory/ai-assistant");
|
|
47
48
|
const mention_marker_js_1 = require("./mention-marker.js");
|
|
48
49
|
let BoardAILLMMessageInput = class BoardAILLMMessageInput {
|
|
49
50
|
};
|
|
@@ -73,6 +74,20 @@ tslib_1.__decorate([
|
|
|
73
74
|
BoardAIMentionPickInput = tslib_1.__decorate([
|
|
74
75
|
(0, type_graphql_1.InputType)()
|
|
75
76
|
], BoardAIMentionPickInput);
|
|
77
|
+
let BoardAIModeInput = class BoardAIModeInput {
|
|
78
|
+
};
|
|
79
|
+
exports.BoardAIModeInput = BoardAIModeInput;
|
|
80
|
+
tslib_1.__decorate([
|
|
81
|
+
(0, type_graphql_1.Field)({ description: 'A mode kind the app server registered, e.g. "board.modeller".' }),
|
|
82
|
+
tslib_1.__metadata("design:type", String)
|
|
83
|
+
], BoardAIModeInput.prototype, "kind", void 0);
|
|
84
|
+
tslib_1.__decorate([
|
|
85
|
+
(0, type_graphql_1.Field)({ nullable: true, description: 'What the conversation is about, e.g. the board id.' }),
|
|
86
|
+
tslib_1.__metadata("design:type", String)
|
|
87
|
+
], BoardAIModeInput.prototype, "target", void 0);
|
|
88
|
+
exports.BoardAIModeInput = BoardAIModeInput = tslib_1.__decorate([
|
|
89
|
+
(0, type_graphql_1.InputType)({ description: 'Which registered conversation this board chat is, and what it is about.' })
|
|
90
|
+
], BoardAIModeInput);
|
|
76
91
|
let BoardAIChatInput = class BoardAIChatInput {
|
|
77
92
|
};
|
|
78
93
|
tslib_1.__decorate([
|
|
@@ -113,6 +128,13 @@ tslib_1.__decorate([
|
|
|
113
128
|
}),
|
|
114
129
|
tslib_1.__metadata("design:type", String)
|
|
115
130
|
], BoardAIChatInput.prototype, "truncateAfterMessageId", void 0);
|
|
131
|
+
tslib_1.__decorate([
|
|
132
|
+
(0, type_graphql_1.Field)(() => BoardAIModeInput, {
|
|
133
|
+
nullable: true,
|
|
134
|
+
description: 'The registered conversation this is (ADR-0061 decision 7). When given, the server takes the persona, tool categories, model and limits from the mode the app registered, and ignores toolCategories / model / temperature / maxTokens / requireGroundingTools sent by the caller. Omit for the older behaviour.'
|
|
135
|
+
}),
|
|
136
|
+
tslib_1.__metadata("design:type", BoardAIModeInput)
|
|
137
|
+
], BoardAIChatInput.prototype, "mode", void 0);
|
|
116
138
|
tslib_1.__decorate([
|
|
117
139
|
(0, type_graphql_1.Field)(() => [String], {
|
|
118
140
|
nullable: true,
|
|
@@ -285,9 +307,17 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
285
307
|
if (!base) {
|
|
286
308
|
throw new Error('No AI client configured. Set config.aiClient = { provider: "anthropic" | "gemini", model, apiKey } or call setDefaultAIClient(...).');
|
|
287
309
|
}
|
|
310
|
+
/*
|
|
311
|
+
* A declared conversation decides its own authority (ADR-0061 decision 7): with a mode, the
|
|
312
|
+
* categories, model and limits come from what the app server registered, not from the request.
|
|
313
|
+
* An unknown kind is refused here, before anything is saved, in the person's language.
|
|
314
|
+
*/
|
|
315
|
+
const modeSettings = input.mode?.kind
|
|
316
|
+
? (0, ai_assistant_2.resolveModeSettings)({ kind: input.mode.kind, target: input.mode.target }, (0, ai_assistant_2.requestTranslator)(context))
|
|
317
|
+
: undefined;
|
|
288
318
|
const { domain, user } = context.state;
|
|
289
319
|
const sessionRepo = (0, shell_1.getRepository)(ai_assistant_1.ChatSession);
|
|
290
|
-
const messageRepo = (0, shell_1.getRepository)(
|
|
320
|
+
const messageRepo = (0, shell_1.getRepository)(ai_assistant_3.ChatMessage);
|
|
291
321
|
const patchRepo = (0, shell_1.getRepository)(patch_entry_js_1.PatchEntry);
|
|
292
322
|
// ── Phase A: LLM 호출 전 짧은 DB 쓰기 ─────────────────────────
|
|
293
323
|
let session;
|
|
@@ -323,16 +353,16 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
323
353
|
* 훨씬 유용하다. 사용자가 직접 바꾼 이름은 여기서 건드리지 않는다(이름이 있으면 그대로).
|
|
324
354
|
* best-effort: 실패해도 대화를 막지 않는다(목록 정렬이 한 칸 늦을 뿐). */
|
|
325
355
|
try {
|
|
326
|
-
const patch = (0,
|
|
356
|
+
const patch = (0, ai_assistant_7.activityPatch)(enriched);
|
|
327
357
|
if (!session.name)
|
|
328
|
-
patch.name = (0,
|
|
358
|
+
patch.name = (0, ai_assistant_7.autoSessionName)(enriched);
|
|
329
359
|
await sessionRepo.update(session.id, patch);
|
|
330
360
|
}
|
|
331
361
|
catch {
|
|
332
362
|
/* noop */
|
|
333
363
|
}
|
|
334
364
|
/* 내가 참여한 세션 활동 방송 — 창을 닫아 둔 다른 참여자의 배지·목록을 위해. */
|
|
335
|
-
(0,
|
|
365
|
+
(0, ai_assistant_8.publishSessionActivity)({
|
|
336
366
|
domainId: domain.id,
|
|
337
367
|
sessionId: session.id,
|
|
338
368
|
kind: 'message',
|
|
@@ -343,7 +373,7 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
343
373
|
});
|
|
344
374
|
// C4: 사용자 메시지를 같은 세션 구독자에게 방송 (그룹챗 실시간). best-effort.
|
|
345
375
|
// 자기 발신 에코는 클라이언트가 message.id 로 dedup.
|
|
346
|
-
(0,
|
|
376
|
+
(0, ai_assistant_9.publishChatMessage)({ domainId: domain.id, sessionId: session.id, message: saved });
|
|
347
377
|
// C4: @user 멘션 → 세션 참여자(member) 자동 등록 (초대). best-effort — 실패해도 chat 흐름 무영향.
|
|
348
378
|
// enriched content 의 {userId:UUID} 마커에서 추출 → 기존 참여자/자기 제외한 신규만 member 로 등록.
|
|
349
379
|
try {
|
|
@@ -375,11 +405,11 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
375
405
|
order: { createdAt: 'ASC', id: 'ASC' }
|
|
376
406
|
});
|
|
377
407
|
/* 이번 턴에 상한 밖으로 밀려난 것이 있는가 — 화면이 "대화가 길어졌다" 를 알릴 근거(사실만). */
|
|
378
|
-
const capped = (0,
|
|
408
|
+
const capped = (0, ai_assistant_5.splitByCap)(rows.map(row => ({ id: row.id, role: row.role, content: row.content ?? '' })), LLM_HISTORY_MAX_TURNS);
|
|
379
409
|
if (capped.dropped.length > 0) {
|
|
380
410
|
historyFolded = { omitted: capped.dropped.length, summarized: !!session.lastSummary };
|
|
381
411
|
}
|
|
382
|
-
llmMessages = (0,
|
|
412
|
+
llmMessages = (0, ai_assistant_4.buildLlmHistory)(rows.map(row => ({
|
|
383
413
|
id: row.id,
|
|
384
414
|
role: row.role,
|
|
385
415
|
content: (0, mention_marker_js_1.stripMentionRefids)(row.content ?? ''),
|
|
@@ -405,18 +435,19 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
405
435
|
const r = await assistant.chat(llmMessages, input.currentBoard ?? undefined, {
|
|
406
436
|
scopes: input.scopes,
|
|
407
437
|
hostContext: input.hostContext ?? undefined,
|
|
408
|
-
|
|
409
|
-
|
|
438
|
+
surfacePrompt: modeSettings?.systemPrompt,
|
|
439
|
+
/* 대화면별 도구 노출 — 모드가 있으면 서버 선언(빈 목록 = 외부 도구 없음), 없으면 호출자 값(미지정 = 전부). */
|
|
440
|
+
toolCategories: modeSettings ? modeSettings.toolCategories ?? [] : input.toolCategories ?? undefined,
|
|
410
441
|
boardTools: await (0, board_tool_rights_js_1.effectiveBoardTools)(input.boardTools, context),
|
|
411
|
-
requireGroundingTools: input.requireGroundingTools ?? undefined,
|
|
442
|
+
requireGroundingTools: modeSettings ? modeSettings.requireToolOnFirstTurn : input.requireGroundingTools ?? undefined,
|
|
412
443
|
knownTypes: input.knownTypes,
|
|
413
444
|
categories: input.categories,
|
|
414
445
|
selectedRefids: input.selectedRefids,
|
|
415
446
|
mentionPicks: input.mentions,
|
|
416
447
|
componentSchemas: Array.isArray(input.componentSchemas) ? input.componentSchemas : undefined,
|
|
417
|
-
model: input.model,
|
|
418
|
-
temperature: input.temperature ?? undefined,
|
|
419
|
-
maxTokens: input.maxTokens ?? undefined,
|
|
448
|
+
model: modeSettings ? modeSettings.model : input.model,
|
|
449
|
+
temperature: modeSettings ? modeSettings.temperature : input.temperature ?? undefined,
|
|
450
|
+
maxTokens: modeSettings ? modeSettings.maxTokens : input.maxTokens ?? undefined,
|
|
420
451
|
// 외부 도메인 (board-import 등) 의 read/external tool builder 가 server-side
|
|
421
452
|
// 의존성 (도메인 / 유저 / 트랜잭션) 을 사용할 수 있도록 ResolverContext.state 전달.
|
|
422
453
|
// board-ai 코어 자체는 이 값을 알지 않고 dispatch 단계에서 closure 로만 흘림.
|
|
@@ -465,9 +496,9 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
465
496
|
});
|
|
466
497
|
assistantMessageId = aiMsg.id;
|
|
467
498
|
// C4: AI 응답도 같은 세션 구독자에게 방송 (트리거한 본인 포함 — id dedup). best-effort.
|
|
468
|
-
(0,
|
|
499
|
+
(0, ai_assistant_9.publishChatMessage)({ domainId: domain.id, sessionId: session.id, message: aiMsg });
|
|
469
500
|
/* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 "새 소식 없음" 으로 보인다. */
|
|
470
|
-
(0,
|
|
501
|
+
(0, ai_assistant_8.publishSessionActivity)({
|
|
471
502
|
domainId: domain.id,
|
|
472
503
|
sessionId: session.id,
|
|
473
504
|
kind: 'message',
|
|
@@ -480,7 +511,7 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
480
511
|
aiClientId: base.id,
|
|
481
512
|
updater: user,
|
|
482
513
|
/* AI 응답도 세션 활동이다 — 이걸 빼면 다른 참여자에게 "새 소식 없음"으로 보인다. */
|
|
483
|
-
...(0,
|
|
514
|
+
...(0, ai_assistant_7.activityPatch)(enrichedReply)
|
|
484
515
|
});
|
|
485
516
|
/* ── Phase D: 이력 요약 접기 ────────────────────────────────────────────
|
|
486
517
|
* 응답을 **돌려준 뒤** 갱신한다 — 사용자 대기 시간에 요약용 LLM 호출을 얹지 않는다.
|
|
@@ -533,14 +564,14 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
533
564
|
senderId: row.creator?.id,
|
|
534
565
|
senderName: row.creator?.name || row.creator?.username
|
|
535
566
|
}));
|
|
536
|
-
const { patch } = await (0,
|
|
567
|
+
const { patch } = await (0, ai_assistant_6.foldHistorySummary)({
|
|
537
568
|
rows: history,
|
|
538
569
|
rowsStartAfterSummary: startsAfterSummary,
|
|
539
570
|
stored,
|
|
540
571
|
maxTurns: LLM_HISTORY_MAX_TURNS,
|
|
541
572
|
threshold: LLM_SUMMARY_THRESHOLD,
|
|
542
573
|
summarize: content => client.chat([{ role: 'user', content }], {
|
|
543
|
-
systemPrompt: (0,
|
|
574
|
+
systemPrompt: (0, ai_assistant_5.summaryInstruction)(),
|
|
544
575
|
maxTokens: 700,
|
|
545
576
|
/* 요약은 창작이 아니다 — 흔들림을 줄인다. */
|
|
546
577
|
temperature: 0
|
|
@@ -573,7 +604,7 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
573
604
|
* id 로 가른다(원래 정렬이 이미 그 규약이다).
|
|
574
605
|
*/
|
|
575
606
|
async _historyForFold(sessionId, stored) {
|
|
576
|
-
const repo = (0, shell_1.getRepository)(
|
|
607
|
+
const repo = (0, shell_1.getRepository)(ai_assistant_3.ChatMessage);
|
|
577
608
|
const session = { id: sessionId };
|
|
578
609
|
const order = { createdAt: 'ASC', id: 'ASC' };
|
|
579
610
|
const relations = { creator: true };
|
|
@@ -604,7 +635,7 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
604
635
|
const mentioned = (0, mention_marker_js_1.extractMentionUserIds)(content);
|
|
605
636
|
if (mentioned.length === 0)
|
|
606
637
|
return;
|
|
607
|
-
const repo = (0, shell_1.getRepository)(
|
|
638
|
+
const repo = (0, shell_1.getRepository)(ai_assistant_10.ChatSessionParticipant);
|
|
608
639
|
const existing = await repo.find({
|
|
609
640
|
where: { session: { id: sessionId }, domain: { id: domainId } },
|
|
610
641
|
withDeleted: true
|
|
@@ -639,7 +670,7 @@ let BoardAIChatResolver = class BoardAIChatResolver {
|
|
|
639
670
|
_notifyInvited(notify, userIds, boardId, inviter) {
|
|
640
671
|
if (typeof notify !== 'function')
|
|
641
672
|
return;
|
|
642
|
-
const payload = (0,
|
|
673
|
+
const payload = (0, ai_assistant_11.buildInviteNotification)(userIds, boardId, inviter);
|
|
643
674
|
if (!payload)
|
|
644
675
|
return;
|
|
645
676
|
Promise.resolve(notify(payload)).catch(() => {
|