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