@things-factory/board-ui 10.0.0-beta.6 → 10.0.0-beta.64
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/apptools/favorite-tool.js +5 -5
- package/dist-client/apptools/favorite-tool.js.map +1 -1
- package/dist-client/board-list/board-tile-list.d.ts +6 -1
- package/dist-client/board-list/board-tile-list.js +291 -44
- package/dist-client/board-list/board-tile-list.js.map +1 -1
- package/dist-client/board-list/group-bar.js +3 -3
- package/dist-client/board-list/group-bar.js.map +1 -1
- package/dist-client/board-list/play-group-bar.d.ts +0 -1
- package/dist-client/board-list/play-group-bar.js +3 -6
- package/dist-client/board-list/play-group-bar.js.map +1 -1
- package/dist-client/board-provider.js +20 -8
- package/dist-client/board-provider.js.map +1 -1
- package/dist-client/data-grist/board-editor.js +4 -4
- package/dist-client/data-grist/board-editor.js.map +1 -1
- package/dist-client/data-grist/board-renderer.js +4 -4
- package/dist-client/data-grist/board-renderer.js.map +1 -1
- package/dist-client/graphql/board-template.js +1 -1
- package/dist-client/graphql/board-template.js.map +1 -1
- package/dist-client/graphql/board.d.ts +1 -0
- package/dist-client/graphql/board.js +28 -2
- package/dist-client/graphql/board.js.map +1 -1
- package/dist-client/graphql/group.js +1 -1
- package/dist-client/graphql/group.js.map +1 -1
- package/dist-client/graphql/play-group.js +3 -3
- package/dist-client/graphql/play-group.js.map +1 -1
- package/dist-client/pages/attachment-list-page.d.ts +16 -0
- package/dist-client/pages/attachment-list-page.js +63 -2
- package/dist-client/pages/attachment-list-page.js.map +1 -1
- package/dist-client/pages/board-list-page.d.ts +23 -3
- package/dist-client/pages/board-list-page.js +165 -77
- package/dist-client/pages/board-list-page.js.map +1 -1
- package/dist-client/pages/board-modeller-page.d.ts +122 -0
- package/dist-client/pages/board-modeller-page.js +705 -54
- package/dist-client/pages/board-modeller-page.js.map +1 -1
- package/dist-client/pages/board-player-by-name-page.js.map +1 -1
- package/dist-client/pages/board-player-page.js +14 -26
- package/dist-client/pages/board-player-page.js.map +1 -1
- package/dist-client/pages/board-viewer-by-name-page.d.ts +8 -1
- package/dist-client/pages/board-viewer-by-name-page.js +9 -1
- package/dist-client/pages/board-viewer-by-name-page.js.map +1 -1
- package/dist-client/pages/board-viewer-page.d.ts +2 -1
- package/dist-client/pages/board-viewer-page.js +52 -48
- package/dist-client/pages/board-viewer-page.js.map +1 -1
- package/dist-client/pages/play-list-page.d.ts +0 -1
- package/dist-client/pages/play-list-page.js +26 -33
- package/dist-client/pages/play-list-page.js.map +1 -1
- package/dist-client/pages/printable-board-viewer-page.js +2 -2
- package/dist-client/pages/printable-board-viewer-page.js.map +1 -1
- package/dist-client/route.d.ts +1 -1
- package/dist-client/route.js.map +1 -1
- package/dist-client/setting-let/board-view-setting-let.js +1 -1
- package/dist-client/setting-let/board-view-setting-let.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/notify-helper.d.ts +7 -0
- package/dist-client/utils/notify-helper.js +28 -0
- package/dist-client/utils/notify-helper.js.map +1 -0
- package/dist-client/utils/query-utils.d.ts +1 -0
- package/dist-client/utils/query-utils.js +20 -0
- package/dist-client/utils/query-utils.js.map +1 -0
- package/dist-client/viewparts/board-basic-info.js +9 -13
- package/dist-client/viewparts/board-basic-info.js.map +1 -1
- package/dist-client/viewparts/board-template-info.d.ts +0 -1
- package/dist-client/viewparts/board-template-info.js +5 -13
- package/dist-client/viewparts/board-template-info.js.map +1 -1
- package/dist-client/viewparts/board-versions.js +1 -1
- package/dist-client/viewparts/board-versions.js.map +1 -1
- package/dist-client/viewparts/group-info-basic.js +2 -2
- package/dist-client/viewparts/group-info-basic.js.map +1 -1
- package/dist-client/viewparts/group-info-import.js +2 -2
- package/dist-client/viewparts/group-info-import.js.map +1 -1
- package/dist-client/viewparts/link-builder.js +1 -1
- package/dist-client/viewparts/link-builder.js.map +1 -1
- package/dist-client/viewparts/play-group-info-basic.js +2 -2
- package/dist-client/viewparts/play-group-info-basic.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +5 -4
- package/translations/en.json +3 -29
- package/translations/ja.json +3 -29
- package/translations/ko.json +3 -29
- package/translations/ms.json +3 -29
- package/translations/zh.json +3 -29
|
@@ -2,19 +2,83 @@ import { __decorate, __metadata } from "tslib";
|
|
|
2
2
|
import './things-scene-components.import';
|
|
3
3
|
import '@operato/board/ox-board-modeller.js';
|
|
4
4
|
import '@operato/oops';
|
|
5
|
+
import '@things-factory/board-ai';
|
|
5
6
|
import gql from 'graphql-tag';
|
|
6
|
-
import { css, html } from 'lit';
|
|
7
|
-
import { customElement, property, query } from 'lit/decorators.js';
|
|
7
|
+
import { css, html, nothing } from 'lit';
|
|
8
|
+
import { customElement, property, query, state } from 'lit/decorators.js';
|
|
8
9
|
import { BoardModeller } from '@operato/board/ox-board-modeller.js';
|
|
10
|
+
import { LoadTracker } from '@hatiolab/things-scene';
|
|
9
11
|
import { OxPropertyEditor } from '@operato/property-editor';
|
|
10
12
|
import { PageView, FontController } from '@operato/shell';
|
|
11
13
|
import { hasPrivilege } from '@things-factory/auth-base/dist-client/index.js';
|
|
14
|
+
import { applyBoardEditPatchVerbose, mergeComponent } from '@things-factory/board-ai';
|
|
12
15
|
import { client, gqlContext } from '@operato/graphql';
|
|
13
16
|
import { i18next } from '@operato/i18n';
|
|
14
17
|
import { OxPrompt } from '@operato/popup/ox-prompt.js';
|
|
15
18
|
import { provider } from '../board-provider.js';
|
|
16
19
|
import components from './things-scene-components-with-tools.import';
|
|
20
|
+
import { notify, notifyError } from '../utils/notify-helper.js';
|
|
17
21
|
const NOOP = () => { };
|
|
22
|
+
/** 서버 PatchEntry.reverted=true 플래그 영속용 mutation. */
|
|
23
|
+
const REVERT_PATCH_MUTATION = gql `
|
|
24
|
+
mutation RevertPatch($patchId: String!) {
|
|
25
|
+
revertPatch(patchId: $patchId)
|
|
26
|
+
}
|
|
27
|
+
`;
|
|
28
|
+
/**
|
|
29
|
+
* AI 의 BoardEditOp 가 지정한 대상 (id 또는 refid) 으로 things-scene component 검색.
|
|
30
|
+
*
|
|
31
|
+
* id (사용자/AI 가 명시적으로 설정한 string identifier) 와
|
|
32
|
+
* refid (things-scene 이 모든 컴포넌트에 자동 발급하는 numeric identifier) 는
|
|
33
|
+
* 서로 별개 개념이고 AI 도 둘을 구별해서 보낸다 — 이 함수는 호스트 측에서 둘 중
|
|
34
|
+
* 어느 쪽으로든 lookup 가능하게 통합. refid 가 universal 이므로 우선.
|
|
35
|
+
*/
|
|
36
|
+
export function findSceneComponent(scene, target) {
|
|
37
|
+
if (!scene)
|
|
38
|
+
return null;
|
|
39
|
+
if (typeof target.refid === 'number') {
|
|
40
|
+
const byRefid = scene.rootContainer?.refidIndexMap?.get(target.refid);
|
|
41
|
+
if (byRefid)
|
|
42
|
+
return byRefid;
|
|
43
|
+
}
|
|
44
|
+
if (typeof target.id === 'string' && target.id.length > 0) {
|
|
45
|
+
return scene.findById?.(target.id) ?? null;
|
|
46
|
+
}
|
|
47
|
+
return null;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* default 위에 override 를 깊게 merge.
|
|
51
|
+
* - override 의 키 값이 undefined 가 아닌 경우 우선 적용
|
|
52
|
+
* - override 의 nested object 는 default 와 deep merge
|
|
53
|
+
* - array / primitive 는 override 가 그대로 우선
|
|
54
|
+
*/
|
|
55
|
+
function mergeDefaultsDeep(defaults, override) {
|
|
56
|
+
if (defaults === null || defaults === undefined)
|
|
57
|
+
return override;
|
|
58
|
+
if (override === null || override === undefined)
|
|
59
|
+
return defaults;
|
|
60
|
+
if (typeof defaults !== 'object' || typeof override !== 'object')
|
|
61
|
+
return override;
|
|
62
|
+
if (Array.isArray(defaults) || Array.isArray(override))
|
|
63
|
+
return override;
|
|
64
|
+
const out = { ...defaults };
|
|
65
|
+
for (const key of Object.keys(override)) {
|
|
66
|
+
const dv = defaults[key];
|
|
67
|
+
const ov = override[key];
|
|
68
|
+
if (dv !== null &&
|
|
69
|
+
ov !== null &&
|
|
70
|
+
typeof dv === 'object' &&
|
|
71
|
+
typeof ov === 'object' &&
|
|
72
|
+
!Array.isArray(dv) &&
|
|
73
|
+
!Array.isArray(ov)) {
|
|
74
|
+
out[key] = mergeDefaultsDeep(dv, ov);
|
|
75
|
+
}
|
|
76
|
+
else if (ov !== undefined) {
|
|
77
|
+
out[key] = ov;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
18
82
|
let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
19
83
|
constructor() {
|
|
20
84
|
super();
|
|
@@ -26,15 +90,199 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
|
26
90
|
this.overlay = null;
|
|
27
91
|
this.scene = null;
|
|
28
92
|
this.componentGroupList = BoardModeller.groups;
|
|
93
|
+
/** AI 채팅 패널 열림 상태 (기본 닫힘) */
|
|
94
|
+
this.aiPanelOpen = false;
|
|
95
|
+
/**
|
|
96
|
+
* 적용된 patch 별 inverse op 시퀀스 — Revert 기능용.
|
|
97
|
+
*
|
|
98
|
+
* Map<patchId, inverseOps[]>: 각 patch 적용 시점에 계산해 저장. revert 클릭
|
|
99
|
+
* 시 역순 in-place 적용. 페이지 reload 시 손실 — reload 후 revert 시도는
|
|
100
|
+
* notify 로 안내 (메모리 외 영속은 별개 작업).
|
|
101
|
+
*/
|
|
102
|
+
this.patchInverses = new Map();
|
|
29
103
|
this.board = null;
|
|
30
104
|
this._fontCtrl = new FontController(this);
|
|
105
|
+
/**
|
|
106
|
+
* 채팅에서 patch 도착 — things-scene 에 in-place 적용.
|
|
107
|
+
*
|
|
108
|
+
* 핵심: `this.model = next` 로 모델을 통째 교체하면 ox-scene-viewer 가 scene 을
|
|
109
|
+
* dispose → 재생성 → undo 히스토리/dirty 플래그가 모두 초기화된다.
|
|
110
|
+
* 따라서 일반 add/modify/remove 는 scene 의 in-place mutation API 로 적용 →
|
|
111
|
+
* commander 가 자동으로 snapshot push → CMD+Z 와 저장 안내 팝업이 정상 동작.
|
|
112
|
+
*
|
|
113
|
+
* 'replace' op (보드 전체 교체) 만은 부득이 wholesale 경로로 빠진다 — 이 경우는
|
|
114
|
+
* 사용자에게 명시적으로 안내 후 진행.
|
|
115
|
+
*/
|
|
116
|
+
this.onBoardEditPatch = (e) => {
|
|
117
|
+
const { patch, patchId } = e.detail || {};
|
|
118
|
+
if (!patch)
|
|
119
|
+
return;
|
|
120
|
+
const scene = this.scene;
|
|
121
|
+
if (!scene) {
|
|
122
|
+
notifyError(new Error('Scene not initialized'));
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
const hasReplace = (patch.ops || []).some((op) => op.op === 'replace');
|
|
126
|
+
if (hasReplace) {
|
|
127
|
+
this.applyPatchWholesale(patch, patchId);
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
try {
|
|
131
|
+
const applied = [];
|
|
132
|
+
const missed = [];
|
|
133
|
+
// 각 op 의 inverse — patch 적용 직전/직후 scene 상태 기준으로 계산해 누적.
|
|
134
|
+
// 나중에 onBoardEditRevert 가 역순 in-place 적용으로 복원.
|
|
135
|
+
const inverseOps = [];
|
|
136
|
+
for (const op of patch.ops) {
|
|
137
|
+
switch (op.op) {
|
|
138
|
+
case 'add': {
|
|
139
|
+
// normalize → things-scene 의 default 와 merge (gauge.value 등 누락 보강)
|
|
140
|
+
const normalized = this.normalizeComponent(op.component);
|
|
141
|
+
// add 의 inverse 는 새로 발급되는 refid 를 알아야 함 — scene.add 직후
|
|
142
|
+
// 새 컴포넌트의 refid 를 캡처해 remove inverse 생성.
|
|
143
|
+
const prevRefids = new Set(this.collectAllRefids(scene));
|
|
144
|
+
// scene.add 는 내부적으로 CommandMigrate 를 commander 에 execute → 자동 snapshot
|
|
145
|
+
// 두 번째 인자는 boundsOrOffset — 빈 객체면 model 자체의 좌표 그대로 사용.
|
|
146
|
+
scene.add(normalized, {});
|
|
147
|
+
// 새로 발급된 refid 식별
|
|
148
|
+
const newRefids = this.collectAllRefids(scene).filter(r => !prevRefids.has(r));
|
|
149
|
+
for (const refid of newRefids) {
|
|
150
|
+
inverseOps.push({ op: 'remove', refid });
|
|
151
|
+
}
|
|
152
|
+
applied.push(op);
|
|
153
|
+
break;
|
|
154
|
+
}
|
|
155
|
+
case 'remove': {
|
|
156
|
+
const target = findSceneComponent(scene, { refid: op.refid });
|
|
157
|
+
if (!target || !target.parent) {
|
|
158
|
+
missed.push(op);
|
|
159
|
+
break;
|
|
160
|
+
}
|
|
161
|
+
// 삭제 직전 model 캡처 → inverse 는 add(savedModel)
|
|
162
|
+
const savedModel = JSON.parse(JSON.stringify(target.model));
|
|
163
|
+
// scene.remove() 는 selected 를 사용하므로 일시적으로 대상으로 set.
|
|
164
|
+
const prevSelected = scene.selected ?? [];
|
|
165
|
+
scene.selected = [target];
|
|
166
|
+
scene.remove(); // CommandMigrate 통한 정상 경로 → snapshot 자동
|
|
167
|
+
// 선택 복원 — 삭제 대상은 제외
|
|
168
|
+
scene.selected = prevSelected.filter((c) => c !== target);
|
|
169
|
+
inverseOps.push({ op: 'add', component: savedModel });
|
|
170
|
+
applied.push(op);
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
case 'modify': {
|
|
174
|
+
const target = findSceneComponent(scene, { refid: op.refid });
|
|
175
|
+
if (!target) {
|
|
176
|
+
missed.push(op);
|
|
177
|
+
break;
|
|
178
|
+
}
|
|
179
|
+
// 변경 전 patch 가 건드린 키만 보관 — inverse 는 같은 키들을 원본 값으로 modify
|
|
180
|
+
const oldValues = this.captureOldKeys(target.model, op.patch);
|
|
181
|
+
// op.patch 의 nested 값 보존 위해 model 위에 deep-merge (apply-patch 와 동일 정책)
|
|
182
|
+
const merged = mergeComponent(target.model, op.patch);
|
|
183
|
+
target.set(merged);
|
|
184
|
+
// change 는 자동 snapshot 하지 않으므로 commander.execute 호출 → snapshot push
|
|
185
|
+
scene.commander?.execute(null, false);
|
|
186
|
+
inverseOps.push({ op: 'modify', refid: op.refid, patch: oldValues });
|
|
187
|
+
applied.push(op);
|
|
188
|
+
break;
|
|
189
|
+
}
|
|
190
|
+
case 'modifyBoard': {
|
|
191
|
+
// 보드 root 속성 (fillStyle / width / height / name 등) 변경.
|
|
192
|
+
// scene.root 는 model_layer — 보드 JSON 의 최상위. 자식 components 와 별개.
|
|
193
|
+
const root = scene.root;
|
|
194
|
+
if (!root || typeof root.set !== 'function') {
|
|
195
|
+
missed.push(op);
|
|
196
|
+
break;
|
|
197
|
+
}
|
|
198
|
+
const cleanPatch = { ...(op.patch || {}) };
|
|
199
|
+
delete cleanPatch.components; // 자식 변경은 별도 op
|
|
200
|
+
const oldValues = this.captureOldKeys(root.model, cleanPatch);
|
|
201
|
+
const merged = mergeComponent(root.model, cleanPatch);
|
|
202
|
+
root.set(merged);
|
|
203
|
+
scene.commander?.execute(null, false);
|
|
204
|
+
inverseOps.push({ op: 'modifyBoard', patch: oldValues });
|
|
205
|
+
applied.push(op);
|
|
206
|
+
break;
|
|
207
|
+
}
|
|
208
|
+
// 'replace' 는 위에서 분기됨 — 여기 도달 안 함
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
// patch 단위로 inverse 보관 — Revert 시 역순 적용
|
|
212
|
+
if (patchId && inverseOps.length > 0) {
|
|
213
|
+
this.patchInverses.set(patchId, inverseOps);
|
|
214
|
+
}
|
|
215
|
+
if (missed.length > 0) {
|
|
216
|
+
const missedDesc = missed
|
|
217
|
+
.map((op) => {
|
|
218
|
+
if (op.op === 'modify' || op.op === 'remove')
|
|
219
|
+
return `refid=${op.refid}`;
|
|
220
|
+
if (op.op === 'modifyBoard')
|
|
221
|
+
return 'modifyBoard (scene root unavailable)';
|
|
222
|
+
return op.op;
|
|
223
|
+
})
|
|
224
|
+
.join(', ');
|
|
225
|
+
notify('warn', `AI 가 ${missed.length}개의 변경을 시도했지만 적용되지 않았습니다: ${missedDesc}`);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
catch (ex) {
|
|
229
|
+
notifyError(ex);
|
|
230
|
+
}
|
|
231
|
+
};
|
|
232
|
+
/**
|
|
233
|
+
* 채팅에서 board-edit-revert 도착 — 누적된 inverse 를 역순 in-place 적용.
|
|
234
|
+
*
|
|
235
|
+
* - 메모리상 patchInverses Map 만 본다. 페이지 reload 후엔 inverse 가 없어
|
|
236
|
+
* 되돌릴 수 없음 (사용자에게 안내).
|
|
237
|
+
* - 성공 시 서버 revertPatch mutation 호출 → PatchEntry.reverted=true 영속.
|
|
238
|
+
* - in-place 적용이라 undo/dirty/selection 보존 (chatViaTools 의 모든 강점 그대로).
|
|
239
|
+
*/
|
|
240
|
+
this.onBoardEditRevert = async (e) => {
|
|
241
|
+
const { patchId } = e.detail || {};
|
|
242
|
+
if (!patchId)
|
|
243
|
+
return;
|
|
244
|
+
const inverses = this.patchInverses.get(patchId);
|
|
245
|
+
if (!inverses || inverses.length === 0) {
|
|
246
|
+
notify('warn', '이 변경은 자동 되돌릴 수 없습니다 (페이지 새로고침 이후 적용된 변경은 메모리에 inverse 가 없습니다).');
|
|
247
|
+
return;
|
|
248
|
+
}
|
|
249
|
+
const scene = this.scene;
|
|
250
|
+
if (!scene) {
|
|
251
|
+
notifyError(new Error('Scene not initialized'));
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
try {
|
|
255
|
+
// 역순 적용 — 마지막 op 부터 되돌림. 단일 patch 안에서도 순서 의미 있음
|
|
256
|
+
// (예: add 후 modify 한 시퀀스를 되돌리려면 modify(원본) 후 remove)
|
|
257
|
+
const reversed = [...inverses].reverse();
|
|
258
|
+
const reverseFakePatch = {
|
|
259
|
+
ops: reversed,
|
|
260
|
+
summary: 'revert',
|
|
261
|
+
confidence: 1
|
|
262
|
+
};
|
|
263
|
+
// hasReplace 면 wholesale, 그렇지 않으면 in-place — 일반 onBoardEditPatch 와 동일
|
|
264
|
+
const hasReplace = reversed.some(op => op.op === 'replace');
|
|
265
|
+
if (hasReplace) {
|
|
266
|
+
this.applyPatchWholesale(reverseFakePatch);
|
|
267
|
+
}
|
|
268
|
+
else {
|
|
269
|
+
// in-place 직접 적용 — 새 inverse 누적은 안 함 (revert 의 revert 는 redo, 별개 기능)
|
|
270
|
+
this.applyInverseOpsInPlace(scene, reversed);
|
|
271
|
+
}
|
|
272
|
+
this.patchInverses.delete(patchId);
|
|
273
|
+
await this.recordRevertOnServer(patchId);
|
|
274
|
+
}
|
|
275
|
+
catch (ex) {
|
|
276
|
+
notifyError(ex);
|
|
277
|
+
}
|
|
278
|
+
};
|
|
31
279
|
components.forEach(({ templates = [], groups = [] }) => {
|
|
32
280
|
groups.forEach(group => BoardModeller.registerGroup(group));
|
|
33
281
|
BoardModeller.registerTemplate(templates);
|
|
34
282
|
});
|
|
35
283
|
/* 컴포넌트에서 정의된 에디터들을 MODELLER_EDITORS에 등록 */
|
|
36
|
-
|
|
37
|
-
for (
|
|
284
|
+
const addedEditors = {};
|
|
285
|
+
for (const component in components) {
|
|
38
286
|
let { editors } = components[component];
|
|
39
287
|
editors &&
|
|
40
288
|
editors.forEach(editor => {
|
|
@@ -48,16 +296,53 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
|
48
296
|
css `
|
|
49
297
|
:host {
|
|
50
298
|
display: flex;
|
|
51
|
-
flex-direction:
|
|
299
|
+
flex-direction: row;
|
|
52
300
|
|
|
53
301
|
overflow: hidden;
|
|
54
302
|
position: relative;
|
|
55
303
|
}
|
|
56
304
|
|
|
305
|
+
.modeller-area {
|
|
306
|
+
flex: 1;
|
|
307
|
+
min-width: 0;
|
|
308
|
+
display: flex;
|
|
309
|
+
flex-direction: column;
|
|
310
|
+
position: relative;
|
|
311
|
+
}
|
|
312
|
+
|
|
57
313
|
ox-board-modeller {
|
|
58
314
|
flex: 1;
|
|
59
315
|
}
|
|
60
316
|
|
|
317
|
+
.ai-toggle {
|
|
318
|
+
position: absolute;
|
|
319
|
+
right: 12px;
|
|
320
|
+
bottom: 12px;
|
|
321
|
+
z-index: 10;
|
|
322
|
+
padding: 8px 14px;
|
|
323
|
+
border: 0;
|
|
324
|
+
border-radius: 24px;
|
|
325
|
+
background: #2563eb;
|
|
326
|
+
color: #fff;
|
|
327
|
+
font: 500 13px/1.4 system-ui, -apple-system, sans-serif;
|
|
328
|
+
cursor: pointer;
|
|
329
|
+
box-shadow: 0 4px 12px rgba(37, 99, 235, 0.35);
|
|
330
|
+
transition: background 0.15s, transform 0.1s;
|
|
331
|
+
}
|
|
332
|
+
.ai-toggle:hover { background: #1d4ed8; }
|
|
333
|
+
.ai-toggle:active { transform: scale(0.97); }
|
|
334
|
+
.ai-toggle.open { background: #475569; }
|
|
335
|
+
.ai-toggle.open:hover { background: #334155; }
|
|
336
|
+
|
|
337
|
+
.ai-panel {
|
|
338
|
+
flex: 0 0 380px;
|
|
339
|
+
display: flex;
|
|
340
|
+
border-left: 1px solid #e2e8f0;
|
|
341
|
+
background: #ffffff;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
ox-board-ai-chat { flex: 1; }
|
|
345
|
+
|
|
61
346
|
ox-oops-note {
|
|
62
347
|
display: block;
|
|
63
348
|
position: absolute;
|
|
@@ -65,11 +350,119 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
|
65
350
|
top: 50%;
|
|
66
351
|
transform: translate(-50%, -50%);
|
|
67
352
|
}
|
|
353
|
+
|
|
354
|
+
@media (max-width: 900px) {
|
|
355
|
+
.ai-panel {
|
|
356
|
+
flex-basis: 100%;
|
|
357
|
+
position: absolute;
|
|
358
|
+
inset: 0;
|
|
359
|
+
z-index: 20;
|
|
360
|
+
}
|
|
361
|
+
}
|
|
68
362
|
`
|
|
69
363
|
]; }
|
|
364
|
+
get knownTypes() {
|
|
365
|
+
if (!this._knownTypesCache) {
|
|
366
|
+
const types = new Set();
|
|
367
|
+
for (const group of BoardModeller.groups || []) {
|
|
368
|
+
for (const t of group.templates || []) {
|
|
369
|
+
if (t && typeof t.type === 'string')
|
|
370
|
+
types.add(t.type);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
this._knownTypesCache = Array.from(types).sort();
|
|
374
|
+
}
|
|
375
|
+
return this._knownTypesCache;
|
|
376
|
+
}
|
|
377
|
+
get knownCategories() {
|
|
378
|
+
if (!this._knownCategoriesCache) {
|
|
379
|
+
const cats = new Set();
|
|
380
|
+
for (const group of BoardModeller.groups || []) {
|
|
381
|
+
for (const t of group.templates || []) {
|
|
382
|
+
if (t && typeof t.group === 'string')
|
|
383
|
+
cats.add(t.group);
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
this._knownCategoriesCache = Array.from(cats).sort();
|
|
387
|
+
}
|
|
388
|
+
return this._knownCategoriesCache;
|
|
389
|
+
}
|
|
390
|
+
get componentSchemas() {
|
|
391
|
+
if (!this._componentSchemasCache) {
|
|
392
|
+
const out = [];
|
|
393
|
+
const NON_PROPS = new Set(['type', 'id', 'name', 'class']);
|
|
394
|
+
// 좌표/크기/경로 관련 키들 — type 마다 다른 조합으로 사용
|
|
395
|
+
const GEOMETRY_KEYS = new Set([
|
|
396
|
+
'left',
|
|
397
|
+
'top',
|
|
398
|
+
'right',
|
|
399
|
+
'bottom',
|
|
400
|
+
'x',
|
|
401
|
+
'y',
|
|
402
|
+
'cx',
|
|
403
|
+
'cy',
|
|
404
|
+
'width',
|
|
405
|
+
'height',
|
|
406
|
+
'rx',
|
|
407
|
+
'ry',
|
|
408
|
+
'radius',
|
|
409
|
+
'rotation',
|
|
410
|
+
'translate',
|
|
411
|
+
'scale',
|
|
412
|
+
'points',
|
|
413
|
+
'path',
|
|
414
|
+
'vertices',
|
|
415
|
+
'verticles',
|
|
416
|
+
'startX',
|
|
417
|
+
'startY',
|
|
418
|
+
'endX',
|
|
419
|
+
'endY',
|
|
420
|
+
'startPoint',
|
|
421
|
+
'endPoint'
|
|
422
|
+
]);
|
|
423
|
+
for (const group of BoardModeller.groups || []) {
|
|
424
|
+
for (const t of group.templates || []) {
|
|
425
|
+
if (!t || typeof t.type !== 'string')
|
|
426
|
+
continue;
|
|
427
|
+
const properties = {};
|
|
428
|
+
const geometryKeys = [];
|
|
429
|
+
const model = t.model || {};
|
|
430
|
+
for (const key of Object.keys(model)) {
|
|
431
|
+
if (NON_PROPS.has(key))
|
|
432
|
+
continue;
|
|
433
|
+
if (GEOMETRY_KEYS.has(key)) {
|
|
434
|
+
geometryKeys.push(key);
|
|
435
|
+
continue;
|
|
436
|
+
}
|
|
437
|
+
const v = model[key];
|
|
438
|
+
if (v && typeof v === 'object' && !Array.isArray(v)) {
|
|
439
|
+
properties[key] = Object.fromEntries(Object.keys(v)
|
|
440
|
+
.slice(0, 8)
|
|
441
|
+
.map(k => [k, null]));
|
|
442
|
+
}
|
|
443
|
+
else if (Array.isArray(v)) {
|
|
444
|
+
properties[key] = [];
|
|
445
|
+
}
|
|
446
|
+
else {
|
|
447
|
+
properties[key] = v;
|
|
448
|
+
}
|
|
449
|
+
}
|
|
450
|
+
out.push({
|
|
451
|
+
type: t.type,
|
|
452
|
+
description: t.description || undefined,
|
|
453
|
+
group: t.group || undefined,
|
|
454
|
+
geometryKeys: geometryKeys.length > 0 ? geometryKeys : undefined,
|
|
455
|
+
properties: Object.keys(properties).length > 0 ? properties : undefined
|
|
456
|
+
});
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
this._componentSchemasCache = out;
|
|
460
|
+
}
|
|
461
|
+
return this._componentSchemasCache;
|
|
462
|
+
}
|
|
70
463
|
get context() {
|
|
71
464
|
return {
|
|
72
|
-
title: this.board ? this.boardName : this.preparing ? 'Fetching board...' : '
|
|
465
|
+
title: this.board ? this.boardName : this.preparing ? 'Fetching board...' : '',
|
|
73
466
|
help: 'board-modeller/modeller',
|
|
74
467
|
widebleed: true
|
|
75
468
|
};
|
|
@@ -88,9 +481,14 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
|
88
481
|
return;
|
|
89
482
|
}
|
|
90
483
|
try {
|
|
484
|
+
// 이전 보드 완전히 클리어
|
|
485
|
+
this.board = null;
|
|
486
|
+
this.model = null;
|
|
487
|
+
this._loadTracker = new LoadTracker();
|
|
488
|
+
this._loadTracker.setPhase('fetch');
|
|
91
489
|
this.preparing = true;
|
|
92
490
|
this.updateContext();
|
|
93
|
-
|
|
491
|
+
const response = await client.query({
|
|
94
492
|
query: gql `
|
|
95
493
|
query FetchBoardById($id: String!) {
|
|
96
494
|
board(id: $id) {
|
|
@@ -103,10 +501,11 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
|
103
501
|
variables: { id: this.boardId },
|
|
104
502
|
context: gqlContext()
|
|
105
503
|
});
|
|
106
|
-
|
|
504
|
+
this._loadTracker?.setPhase('parse');
|
|
505
|
+
const board = response.data.board;
|
|
107
506
|
if (!board) {
|
|
108
507
|
this.board = null;
|
|
109
|
-
throw 'board not found';
|
|
508
|
+
throw new Error('board not found');
|
|
110
509
|
}
|
|
111
510
|
this.board = {
|
|
112
511
|
...board,
|
|
@@ -116,27 +515,247 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
|
116
515
|
this.model = {
|
|
117
516
|
...this.board.model
|
|
118
517
|
};
|
|
518
|
+
// AI 협력 세션 — 패널이 열릴 때 lazy 로드 (보드 단순 열기로는 세션 자동 생성 X)
|
|
519
|
+
this.chatSessionId = undefined;
|
|
119
520
|
}
|
|
120
521
|
catch (ex) {
|
|
121
|
-
|
|
122
|
-
detail: {
|
|
123
|
-
level: 'error',
|
|
124
|
-
message: ex,
|
|
125
|
-
ex
|
|
126
|
-
}
|
|
127
|
-
}));
|
|
522
|
+
notifyError(ex);
|
|
128
523
|
}
|
|
129
524
|
finally {
|
|
130
525
|
this.preparing = false;
|
|
131
526
|
this.updateContext();
|
|
132
527
|
}
|
|
133
528
|
}
|
|
529
|
+
/** 패널 토글 시 호출 — 열 때만 chatSession 보장 */
|
|
530
|
+
async toggleAIPanel() {
|
|
531
|
+
const willOpen = !this.aiPanelOpen;
|
|
532
|
+
if (willOpen && this.boardId && !this.chatSessionId) {
|
|
533
|
+
await this.ensureChatSession();
|
|
534
|
+
}
|
|
535
|
+
this.aiPanelOpen = willOpen;
|
|
536
|
+
}
|
|
537
|
+
/** boardId 로 ChatSession 조회 또는 생성 (idempotent). */
|
|
538
|
+
async ensureChatSession() {
|
|
539
|
+
if (!this.boardId || this.chatSessionId)
|
|
540
|
+
return;
|
|
541
|
+
try {
|
|
542
|
+
const found = await client.query({
|
|
543
|
+
query: gql `
|
|
544
|
+
query ChatSessionByBoard($boardId: String!) {
|
|
545
|
+
chatSessionByBoard(boardId: $boardId) { id }
|
|
546
|
+
}
|
|
547
|
+
`,
|
|
548
|
+
variables: { boardId: this.boardId },
|
|
549
|
+
context: gqlContext(),
|
|
550
|
+
fetchPolicy: 'network-only'
|
|
551
|
+
});
|
|
552
|
+
if (found.data?.chatSessionByBoard?.id) {
|
|
553
|
+
this.chatSessionId = found.data.chatSessionByBoard.id;
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
const created = await client.mutate({
|
|
557
|
+
mutation: gql `
|
|
558
|
+
mutation StartBoardAISession($boardId: String!) {
|
|
559
|
+
startBoardAISession(boardId: $boardId) { id }
|
|
560
|
+
}
|
|
561
|
+
`,
|
|
562
|
+
variables: { boardId: this.boardId },
|
|
563
|
+
context: gqlContext()
|
|
564
|
+
});
|
|
565
|
+
this.chatSessionId = created.data?.startBoardAISession?.id;
|
|
566
|
+
}
|
|
567
|
+
catch (ex) {
|
|
568
|
+
notifyError(ex);
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
/**
|
|
572
|
+
* 모델러의 selected (things-scene Component 인스턴스 배열) 에서 refid 추출.
|
|
573
|
+
*
|
|
574
|
+
* refid 는 things-scene 이 모든 컴포넌트에 자동 발급하는 universal numeric handle.
|
|
575
|
+
* AI 의 selection 표현은 항상 refid 기반 (id 는 데이터 바인딩 이름이며 unique 가
|
|
576
|
+
* 아니라 targeting 에 부적합).
|
|
577
|
+
*/
|
|
578
|
+
extractSelectedRefids() {
|
|
579
|
+
const sel = this.selected || [];
|
|
580
|
+
return sel
|
|
581
|
+
.map((c) => {
|
|
582
|
+
if (!c || typeof c.get !== 'function')
|
|
583
|
+
return null;
|
|
584
|
+
const refid = c.get('refid');
|
|
585
|
+
return typeof refid === 'number' && Number.isFinite(refid) ? refid : null;
|
|
586
|
+
})
|
|
587
|
+
.filter((x) => x !== null);
|
|
588
|
+
}
|
|
589
|
+
/**
|
|
590
|
+
* 라이브 보드 모델 — things-scene 캔버스가 들고 있는 가장 최신 상태의 deep clone.
|
|
591
|
+
*
|
|
592
|
+
* 사용자 수작업 편집은 scene.model 에만 반영되고 this.model property 에는 자동
|
|
593
|
+
* 동기화되지 않는다. AI 한테 보드 상태를 넘기거나 patch 를 적용할 때는 반드시
|
|
594
|
+
* 이쪽을 쓴다.
|
|
595
|
+
*
|
|
596
|
+
* id 와 refid 는 별개 필드 — 컴포넌트는 model.id (선택, 사용자/AI 가 명시 설정)
|
|
597
|
+
* 와 model.refid (필수, things-scene 자동 발급) 를 둘 다 가질 수 있고 AI 가
|
|
598
|
+
* 양쪽을 구별해서 다룬다 (tool 인자가 분리됨). 따라서 여기서는 model 을 mutate
|
|
599
|
+
* 하지 않고 raw deep clone 만 반환.
|
|
600
|
+
*/
|
|
601
|
+
getLiveBoard() {
|
|
602
|
+
const sceneModel = this.scene?.model;
|
|
603
|
+
if (sceneModel) {
|
|
604
|
+
return JSON.parse(JSON.stringify(sceneModel));
|
|
605
|
+
}
|
|
606
|
+
return this.model;
|
|
607
|
+
}
|
|
608
|
+
/**
|
|
609
|
+
* 'replace' op 적용 — scene 통째로 재생성 (undo 히스토리 초기화).
|
|
610
|
+
*
|
|
611
|
+
* AI 가 보드 전체 재구성을 요청한 경우. 사용자에게 history 손실 안내.
|
|
612
|
+
* board-import 데이터 wholesale 적용 등 드문 케이스에 한정.
|
|
613
|
+
*
|
|
614
|
+
* Revert 지원 — replace 직전 보드 model 을 inverse 로 보관 (또 다른 replace).
|
|
615
|
+
*/
|
|
616
|
+
applyPatchWholesale(patch, patchId) {
|
|
617
|
+
try {
|
|
618
|
+
const baseBoard = this.getLiveBoard();
|
|
619
|
+
const inverseOps = baseBoard
|
|
620
|
+
? [{ op: 'replace', board: JSON.parse(JSON.stringify(baseBoard)) }]
|
|
621
|
+
: [];
|
|
622
|
+
const report = applyBoardEditPatchVerbose(baseBoard, patch);
|
|
623
|
+
const next = report.board;
|
|
624
|
+
if (report.applied.length === 0)
|
|
625
|
+
return;
|
|
626
|
+
const normalized = (next.components || []).map((c) => this.normalizeComponent(c));
|
|
627
|
+
this.model = {
|
|
628
|
+
...(baseBoard || {}),
|
|
629
|
+
...(next.width !== undefined && { width: next.width }),
|
|
630
|
+
...(next.height !== undefined && { height: next.height }),
|
|
631
|
+
components: normalized
|
|
632
|
+
};
|
|
633
|
+
if (patchId && inverseOps.length > 0) {
|
|
634
|
+
this.patchInverses.set(patchId, inverseOps);
|
|
635
|
+
}
|
|
636
|
+
notify('info', '보드 전체가 교체되어 실행 취소(undo) 히스토리가 초기화되었습니다.');
|
|
637
|
+
}
|
|
638
|
+
catch (ex) {
|
|
639
|
+
notifyError(ex);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
/**
|
|
643
|
+
* scene 의 모든 컴포넌트 refid 수집 — add 의 inverse 계산용.
|
|
644
|
+
*
|
|
645
|
+
* scene.add 직전/직후 호출해 차집합으로 새 발급된 refid 식별.
|
|
646
|
+
*/
|
|
647
|
+
collectAllRefids(scene) {
|
|
648
|
+
const refids = [];
|
|
649
|
+
const map = scene?.rootContainer?.refidIndexMap;
|
|
650
|
+
if (map && typeof map.forEach === 'function') {
|
|
651
|
+
map.forEach((_, refid) => refids.push(refid));
|
|
652
|
+
}
|
|
653
|
+
return refids;
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* patch 가 변경하려는 키들에 대해 model 의 현재 값을 deep-clone 으로 캡처.
|
|
657
|
+
* inverse modify / modifyBoard op 의 patch 로 사용.
|
|
658
|
+
*/
|
|
659
|
+
captureOldKeys(model, patch) {
|
|
660
|
+
const out = {};
|
|
661
|
+
for (const k of Object.keys(patch || {})) {
|
|
662
|
+
const v = model?.[k];
|
|
663
|
+
out[k] = v === undefined ? null : JSON.parse(JSON.stringify(v));
|
|
664
|
+
}
|
|
665
|
+
return out;
|
|
666
|
+
}
|
|
667
|
+
/**
|
|
668
|
+
* inverse op 시퀀스를 things-scene 위에 in-place 적용 (snapshot/undo 보존).
|
|
669
|
+
* onBoardEditPatch 의 add/remove/modify/modifyBoard 분기와 동일 패턴이지만
|
|
670
|
+
* inverse 누적은 하지 않음 (revert 자체가 revert 가능하지 않은 단방향 동작).
|
|
671
|
+
*/
|
|
672
|
+
applyInverseOpsInPlace(scene, ops) {
|
|
673
|
+
for (const op of ops) {
|
|
674
|
+
switch (op.op) {
|
|
675
|
+
case 'add': {
|
|
676
|
+
const normalized = this.normalizeComponent(op.component);
|
|
677
|
+
scene.add(normalized, {});
|
|
678
|
+
break;
|
|
679
|
+
}
|
|
680
|
+
case 'remove': {
|
|
681
|
+
const target = findSceneComponent(scene, { refid: op.refid });
|
|
682
|
+
if (!target || !target.parent)
|
|
683
|
+
break;
|
|
684
|
+
const prevSelected = scene.selected ?? [];
|
|
685
|
+
scene.selected = [target];
|
|
686
|
+
scene.remove();
|
|
687
|
+
scene.selected = prevSelected.filter((c) => c !== target);
|
|
688
|
+
break;
|
|
689
|
+
}
|
|
690
|
+
case 'modify': {
|
|
691
|
+
const target = findSceneComponent(scene, { refid: op.refid });
|
|
692
|
+
if (!target)
|
|
693
|
+
break;
|
|
694
|
+
const merged = mergeComponent(target.model, op.patch);
|
|
695
|
+
target.set(merged);
|
|
696
|
+
scene.commander?.execute(null, false);
|
|
697
|
+
break;
|
|
698
|
+
}
|
|
699
|
+
case 'modifyBoard': {
|
|
700
|
+
const root = scene.root;
|
|
701
|
+
if (!root || typeof root.set !== 'function')
|
|
702
|
+
break;
|
|
703
|
+
const cleanPatch = { ...(op.patch || {}) };
|
|
704
|
+
delete cleanPatch.components;
|
|
705
|
+
const merged = mergeComponent(root.model, cleanPatch);
|
|
706
|
+
root.set(merged);
|
|
707
|
+
scene.commander?.execute(null, false);
|
|
708
|
+
break;
|
|
709
|
+
}
|
|
710
|
+
// 'replace' 는 호출자가 wholesale 분기로 보냄
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
/** 서버 PatchEntry.reverted=true 영속 — fail 해도 UI 는 이미 되돌렸으니 warn 만. */
|
|
715
|
+
async recordRevertOnServer(patchId) {
|
|
716
|
+
try {
|
|
717
|
+
await client.mutate({ mutation: REVERT_PATCH_MUTATION, variables: { patchId } });
|
|
718
|
+
}
|
|
719
|
+
catch (e) {
|
|
720
|
+
console.warn('[board-modeller] revertPatch server persist failed:', e?.message ?? e);
|
|
721
|
+
}
|
|
722
|
+
}
|
|
723
|
+
/**
|
|
724
|
+
* AI 가 만든 컴포넌트를 things-scene 이 기대하는 완전한 형태로 normalize.
|
|
725
|
+
* BoardModeller 에 등록된 template.model (default 값) 위에 AI 응답을 덮어쓴다.
|
|
726
|
+
* 이 단계 없이 things-scene 에 넣으면 필수 속성 누락 시 render 에서 crash 가능
|
|
727
|
+
* (예: gauge-circle 의 value 가 undefined → toString() TypeError).
|
|
728
|
+
*/
|
|
729
|
+
normalizeComponent(c) {
|
|
730
|
+
if (!c || typeof c !== 'object' || typeof c.type !== 'string')
|
|
731
|
+
return c;
|
|
732
|
+
const template = this.findTemplateByType(c.type);
|
|
733
|
+
if (!template || !template.model)
|
|
734
|
+
return c;
|
|
735
|
+
return mergeDefaultsDeep(template.model, c);
|
|
736
|
+
}
|
|
737
|
+
findTemplateByType(type) {
|
|
738
|
+
for (const group of BoardModeller.groups || []) {
|
|
739
|
+
for (const t of group.templates || []) {
|
|
740
|
+
if (t && t.type === type)
|
|
741
|
+
return t;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return undefined;
|
|
745
|
+
}
|
|
134
746
|
async updated(changes) {
|
|
135
747
|
if (changes.has('boardId')) {
|
|
136
|
-
|
|
137
|
-
|
|
748
|
+
try {
|
|
749
|
+
if (await hasPrivilege({ privilege: 'mutation', category: 'board' })) {
|
|
750
|
+
this.refresh();
|
|
751
|
+
}
|
|
752
|
+
else {
|
|
753
|
+
this.boardId = null;
|
|
754
|
+
this.model = null;
|
|
755
|
+
this.modeller?.close();
|
|
756
|
+
}
|
|
138
757
|
}
|
|
139
|
-
|
|
758
|
+
catch {
|
|
140
759
|
this.boardId = null;
|
|
141
760
|
this.model = null;
|
|
142
761
|
this.modeller?.close();
|
|
@@ -149,46 +768,81 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
|
149
768
|
}
|
|
150
769
|
else {
|
|
151
770
|
this.boardId = null;
|
|
771
|
+
this.board = null;
|
|
152
772
|
this.model = null;
|
|
773
|
+
this.preparing = false;
|
|
153
774
|
this.modeller?.close();
|
|
775
|
+
this.updateContext();
|
|
154
776
|
}
|
|
155
777
|
}
|
|
156
778
|
render() {
|
|
157
|
-
|
|
158
|
-
return
|
|
159
|
-
|
|
779
|
+
const oops = !this.preparing && !this.model && this.oopsNote;
|
|
780
|
+
return html `
|
|
781
|
+
<div class="modeller-area">
|
|
782
|
+
${oops
|
|
783
|
+
? html `<ox-oops-note icon=${oops.icon} title=${oops.title} description=${oops.description}></ox-oops-note>`
|
|
160
784
|
: html `
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
785
|
+
<ox-board-modeller
|
|
786
|
+
.mode=${this.mode}
|
|
787
|
+
.loadTracker=${this._loadTracker}
|
|
788
|
+
@mode-changed=${e => {
|
|
164
789
|
this.mode = e.detail.value;
|
|
165
790
|
}}
|
|
166
|
-
|
|
167
|
-
|
|
791
|
+
.model=${this.model}
|
|
792
|
+
@model-changed=${e => {
|
|
168
793
|
this.model = e.detail.value;
|
|
169
794
|
}}
|
|
170
|
-
|
|
171
|
-
|
|
795
|
+
.scene=${this.scene}
|
|
796
|
+
@scene-changed=${e => {
|
|
172
797
|
this.scene = e.detail.value;
|
|
173
798
|
}}
|
|
174
|
-
|
|
175
|
-
|
|
799
|
+
.selected=${this.selected}
|
|
800
|
+
@selected-changed=${e => {
|
|
176
801
|
this.selected = e.detail.value;
|
|
177
802
|
}}
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
803
|
+
.provider=${provider}
|
|
804
|
+
@save-model=${e => this.saveBoard()}
|
|
805
|
+
.componentGroupList=${this.componentGroupList}
|
|
806
|
+
.fonts=${this._fontCtrl.fonts}
|
|
807
|
+
.hideProperty=${this.hideProperty}
|
|
808
|
+
>
|
|
809
|
+
</ox-board-modeller>
|
|
810
|
+
`}
|
|
811
|
+
${this.boardId
|
|
812
|
+
? html `
|
|
813
|
+
<button
|
|
814
|
+
class="ai-toggle ${this.aiPanelOpen ? 'open' : ''}"
|
|
815
|
+
title="${i18next.t('text.ai_assistant') || 'AI Assistant'}"
|
|
816
|
+
@click=${this.toggleAIPanel}>
|
|
817
|
+
${this.aiPanelOpen ? '✕ AI' : '✨ AI'}
|
|
818
|
+
</button>
|
|
819
|
+
`
|
|
820
|
+
: nothing}
|
|
821
|
+
</div>
|
|
822
|
+
${this.aiPanelOpen
|
|
823
|
+
? html `
|
|
824
|
+
<div class="ai-panel">
|
|
825
|
+
<ox-board-ai-chat
|
|
826
|
+
.sessionId=${this.chatSessionId}
|
|
827
|
+
.currentBoard=${this.model}
|
|
828
|
+
.boardProvider=${() => this.getLiveBoard()}
|
|
829
|
+
.knownTypes=${this.knownTypes}
|
|
830
|
+
.categories=${this.knownCategories}
|
|
831
|
+
.componentSchemas=${this.componentSchemas}
|
|
832
|
+
.selectedRefids=${this.extractSelectedRefids()}
|
|
833
|
+
@board-edit-patch=${this.onBoardEditPatch}
|
|
834
|
+
@board-edit-revert=${this.onBoardEditRevert}>
|
|
835
|
+
</ox-board-ai-chat>
|
|
836
|
+
</div>
|
|
837
|
+
`
|
|
838
|
+
: nothing}
|
|
839
|
+
`;
|
|
186
840
|
}
|
|
187
841
|
async updateBoard() {
|
|
188
842
|
try {
|
|
189
843
|
this.preparing = true;
|
|
190
|
-
|
|
191
|
-
|
|
844
|
+
const { id, name, description, groupId } = this.board;
|
|
845
|
+
const model = JSON.stringify(this.scene.model);
|
|
192
846
|
await client.mutate({
|
|
193
847
|
mutation: gql `
|
|
194
848
|
mutation UpdateBoard($id: String!, $patch: BoardPatch!) {
|
|
@@ -203,21 +857,10 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
|
|
|
203
857
|
},
|
|
204
858
|
context: gqlContext()
|
|
205
859
|
});
|
|
206
|
-
|
|
207
|
-
detail: {
|
|
208
|
-
level: 'info',
|
|
209
|
-
message: i18next.t('text.saved')
|
|
210
|
-
}
|
|
211
|
-
}));
|
|
860
|
+
notify('info', i18next.t('text.saved'));
|
|
212
861
|
}
|
|
213
862
|
catch (ex) {
|
|
214
|
-
|
|
215
|
-
detail: {
|
|
216
|
-
level: 'error',
|
|
217
|
-
message: ex,
|
|
218
|
-
ex: ex
|
|
219
|
-
}
|
|
220
|
-
}));
|
|
863
|
+
notifyError(ex);
|
|
221
864
|
}
|
|
222
865
|
finally {
|
|
223
866
|
this.preparing = false;
|
|
@@ -284,6 +927,14 @@ __decorate([
|
|
|
284
927
|
property({ type: Boolean }),
|
|
285
928
|
__metadata("design:type", Boolean)
|
|
286
929
|
], BoardModellerPage.prototype, "preparing", void 0);
|
|
930
|
+
__decorate([
|
|
931
|
+
state(),
|
|
932
|
+
__metadata("design:type", String)
|
|
933
|
+
], BoardModellerPage.prototype, "chatSessionId", void 0);
|
|
934
|
+
__decorate([
|
|
935
|
+
state(),
|
|
936
|
+
__metadata("design:type", Object)
|
|
937
|
+
], BoardModellerPage.prototype, "aiPanelOpen", void 0);
|
|
287
938
|
__decorate([
|
|
288
939
|
query('ox-board-modeller'),
|
|
289
940
|
__metadata("design:type", BoardModeller)
|