@things-factory/board-ui 10.1.46 → 10.1.50

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.
@@ -15,6 +15,7 @@ import { PageView, FontController } from '@operato/shell';
15
15
  import { hasPrivilege } from '@things-factory/auth-base/dist-client/index.js';
16
16
  import { applyScenePatchVerbose, dispatchSceneAction, dispatchSceneEditOp, findSceneComponent } from '@operato/scene-ops';
17
17
  import { client, gqlContext, subscribe } from '@operato/graphql';
18
+ import { clearAssistantSurface, provideAssistantSurface } from '@things-factory/ai-assistant/dist-client/index.js';
18
19
  import { i18next } from '@operato/i18n';
19
20
  import { OxPrompt } from '@operato/popup/ox-prompt.js';
20
21
  import { provider } from '@operato/board/board-provider.js';
@@ -77,10 +78,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
77
78
  this.overlay = null;
78
79
  this.scene = null;
79
80
  this.componentGroupList = BoardModeller.groups;
80
- /** 보드의 모든 ChatSession (탭으로 표시). */
81
- this.chatSessions = [];
82
- /** AI 채팅 패널 열림 상태 (기본 닫힘) */
83
- this.aiPanelOpen = false;
84
81
  /**
85
82
  * 적용된 patch 별 inverse op 시퀀스 — Revert 기능용.
86
83
  *
@@ -95,67 +92,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
95
92
  this._syncedModel = null;
96
93
  this.board = null;
97
94
  this._fontCtrl = new FontController(this);
98
- /** `@` mention popup 의 도메인 사용자 후보 — chat 의 userProvider prop 으로 wire.
99
- * query 변할 때마다 호출되므로 server query (`domainUsersForMention`) 를 그대로 위임. */
100
- this.fetchDomainUsersForMention = async (query) => {
101
- try {
102
- const result = await client.query({
103
- query: gql `
104
- query DomainUsersForMention($query: String, $limit: Int) {
105
- domainUsersForMention(query: $query, limit: $limit) {
106
- id
107
- name
108
- email
109
- }
110
- }
111
- `,
112
- variables: { query: query || null, limit: 50 },
113
- context: gqlContext(),
114
- fetchPolicy: 'cache-first'
115
- });
116
- return result.data?.domainUsersForMention ?? [];
117
- }
118
- catch (ex) {
119
- // 조용히 — chat panel 이 빈 결과로 처리
120
- console.warn('[board-modeller-page] fetchDomainUsersForMention failed:', ex);
121
- return [];
122
- }
123
- };
124
- /** 채팅 패널의 탭 클릭 — 활성 세션 전환. */
125
- this.onChatSessionSwitch = (e) => {
126
- const newId = e.detail?.sessionId;
127
- if (typeof newId === 'string' && newId !== this.chatSessionId) {
128
- this.chatSessionId = newId;
129
- }
130
- };
131
- /** 채팅 패널의 "+" 버튼 — 새 세션 생성 후 활성으로 전환. */
132
- this.onChatSessionCreate = async () => {
133
- if (!this.boardId)
134
- return;
135
- try {
136
- const created = await client.mutate({
137
- mutation: gql `
138
- mutation CreateChatSession($boardId: String!, $name: String) {
139
- createChatSession(boardId: $boardId, name: $name) {
140
- id
141
- name
142
- createdAt
143
- }
144
- }
145
- `,
146
- variables: { boardId: this.boardId, name: null },
147
- context: gqlContext()
148
- });
149
- const s = created.data?.createChatSession;
150
- if (s) {
151
- this.chatSessions = [...this.chatSessions, s];
152
- this.chatSessionId = s.id;
153
- }
154
- }
155
- catch (ex) {
156
- notifyError(ex);
157
- }
158
- };
159
95
  /**
160
96
  * AI 채팅 mention popup (`#`) 검색 위임 — things-scene SceneSearchEngine 으로 wrap.
161
97
  *
@@ -374,54 +310,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
374
310
  flex: 1;
375
311
  }
376
312
 
377
- /*
378
- * The assistant's door and its column, on the scheme's own surfaces. The door is a quiet
379
- * tonal pill on the canvas, like the save FAB beside it; open, it takes the pressed tone.
380
- */
381
- .ai-toggle {
382
- position: absolute;
383
- right: 12px;
384
- bottom: 12px;
385
- z-index: 10;
386
- display: inline-flex;
387
- align-items: center;
388
- gap: 6px;
389
- height: 36px;
390
- padding: 0 14px 0 11px;
391
- border: 1px solid var(--md-sys-color-outline-variant);
392
- border-radius: 999px;
393
- background: var(--md-sys-color-surface-container-high);
394
- color: var(--md-sys-color-on-surface);
395
- font: 500 0.8rem/1 var(--theme-font, Roboto, sans-serif);
396
- cursor: pointer;
397
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
398
- transition: background 0.15s, transform 0.1s;
399
- }
400
- .ai-toggle md-icon {
401
- --md-icon-size: 18px;
402
- color: var(--md-sys-color-primary);
403
- font-variation-settings: 'FILL' 1;
404
- }
405
- .ai-toggle:hover { background: var(--md-sys-color-surface-container-highest); }
406
- .ai-toggle:active { transform: scale(0.97); }
407
- .ai-toggle.open {
408
- border-color: transparent;
409
- background: var(--md-sys-color-secondary-container);
410
- color: var(--md-sys-color-on-secondary-container);
411
- }
412
- .ai-toggle.open md-icon { color: inherit; font-variation-settings: 'FILL' 0; }
413
-
414
- .ai-panel {
415
- flex: 0 0 340px;
416
- display: flex;
417
- min-width: 0;
418
- border-left: 1px solid var(--md-sys-color-outline-variant);
419
- background: var(--md-sys-color-surface-container-low);
420
- color: var(--md-sys-color-on-surface);
421
- }
422
-
423
- ox-assistant-chat { flex: 1; }
424
-
425
313
  ox-oops-note {
426
314
  display: block;
427
315
  position: absolute;
@@ -430,14 +318,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
430
318
  transform: translate(-50%, -50%);
431
319
  }
432
320
 
433
- @media (max-width: 900px) {
434
- .ai-panel {
435
- flex-basis: 100%;
436
- position: absolute;
437
- inset: 0;
438
- z-index: 20;
439
- }
440
- }
441
321
  `
442
322
  ]; }
443
323
  get knownTypes() {
@@ -598,14 +478,9 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
598
478
  this.model = {
599
479
  ...this.board.model
600
480
  };
601
- // AI 협력 세션 — 보드 전환 시 이전 보드의 세션 / 세션 리스트 정리 (chat panel 의
602
- // sessionId/sessions 변화 감지 → history 비우고 새로 로드). 패널 열린 채 보드 옮겼다면
603
- // 새 보드의 세션 리스트를 즉시 ensure (fire-and-forget — 에러는 ensureChatSession 처리).
481
+ /* Another board is another conversation - the dock reopens for the new surface. */
604
482
  this.chatSessionId = undefined;
605
- this.chatSessions = [];
606
- if (this.aiPanelOpen && this.boardId) {
607
- this.ensureChatSession();
608
- }
483
+ this.provideAssistantBoard();
609
484
  }
610
485
  catch (ex) {
611
486
  notifyError(ex);
@@ -615,66 +490,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
615
490
  this.updateContext();
616
491
  }
617
492
  }
618
- /** 패널 토글 시 호출 — 열 때만 chatSession 보장 (세션 리스트 로드 + 활성 세션 결정) */
619
- async toggleAIPanel() {
620
- const willOpen = !this.aiPanelOpen;
621
- if (willOpen && this.boardId) {
622
- await this.ensureChatSession();
623
- }
624
- this.aiPanelOpen = willOpen;
625
- }
626
- /** 보드의 모든 ChatSession 목록을 로드 + 활성 세션 보장.
627
- * - 세션이 0 개면 startBoardAISession 으로 첫 세션 생성 (단일 세션 UX 호환)
628
- * - 활성 세션 미설정 시 첫 번째 세션을 활성으로
629
- * - 이미 활성 세션 set 돼있고 list 에 포함되면 유지 */
630
- async ensureChatSession() {
631
- if (!this.boardId)
632
- return;
633
- try {
634
- const result = await client.query({
635
- query: gql `
636
- query ChatSessionsByBoard($boardId: String!) {
637
- chatSessionsByBoard(boardId: $boardId) {
638
- id
639
- name
640
- createdAt
641
- }
642
- }
643
- `,
644
- variables: { boardId: this.boardId },
645
- context: gqlContext(),
646
- fetchPolicy: 'network-only'
647
- });
648
- let sessions = result.data?.chatSessionsByBoard ?? [];
649
- if (sessions.length === 0) {
650
- // 첫 세션 ensure (idempotent)
651
- const created = await client.mutate({
652
- mutation: gql `
653
- mutation StartBoardAISession($boardId: String!) {
654
- startBoardAISession(boardId: $boardId) {
655
- id
656
- name
657
- createdAt
658
- }
659
- }
660
- `,
661
- variables: { boardId: this.boardId },
662
- context: gqlContext()
663
- });
664
- const s = created.data?.startBoardAISession;
665
- if (s)
666
- sessions = [s];
667
- }
668
- this.chatSessions = sessions;
669
- // 활성 세션 결정 — 기존 active 가 list 에 있으면 유지, 아니면 첫 번째
670
- if (!this.chatSessionId || !sessions.some(s => s.id === this.chatSessionId)) {
671
- this.chatSessionId = sessions[0]?.id;
672
- }
673
- }
674
- catch (ex) {
675
- notifyError(ex);
676
- }
677
- }
678
493
  /**
679
494
  * 모델러의 selected (things-scene Component 인스턴스 배열) 에서 refid 추출.
680
495
  *
@@ -889,6 +704,44 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
889
704
  disconnectedCallback() {
890
705
  super.disconnectedCallback();
891
706
  this._unsubscribeBoardPatches();
707
+ clearAssistantSurface(this);
708
+ }
709
+ /**
710
+ * This board, told to the framework's AI dock (design/ai/shared-dock.md §3). The dock owns the
711
+ * conversation - sessions, modes, the manager - and hands the chat's scene edits back to this page.
712
+ * Decided 2026-09-25: the modeller no longer carries its own "AI" button and panel; the band's forum
713
+ * button opens the same dock as every other screen.
714
+ */
715
+ provideAssistantBoard() {
716
+ if (!this.boardId)
717
+ return;
718
+ const boardId = this.boardId;
719
+ provideAssistantSurface(this, {
720
+ key: `board:${boardId}`,
721
+ subject: 'board',
722
+ icon: 'dashboard',
723
+ label: () => this.boardName || i18next.t('text.ai_assistant'),
724
+ detail: () => i18next.t('label.board', { defaultValue: 'board' }),
725
+ anchor: () => ({ anchorType: 'board', anchorId: boardId }),
726
+ sessionChanged: sessionId => (this.chatSessionId = sessionId || undefined),
727
+ board: {
728
+ chatEndpoint: 'boardAIChat',
729
+ currentBoard: () => this.model,
730
+ boardProvider: () => this.getLiveBoard(),
731
+ selectedRefids: () => this.extractSelectedRefids(),
732
+ knownTypes: () => this.knownTypes,
733
+ categories: () => this.knownCategories,
734
+ componentSchemas: () => this.componentSchemas,
735
+ searchProvider: this.searchSceneForMention,
736
+ applyPatch: (detail) => {
737
+ /* The modeller tells the person which ops missed itself, so the dock is told none to repeat. */
738
+ this.onSceneEditPatch(new CustomEvent('scene-edit-patch', { detail }));
739
+ return { applied: 0, missed: 0 };
740
+ },
741
+ revertPatch: (patchId) => this.onBoardEditRevert(new CustomEvent('board-edit-revert', { detail: { patchId } })),
742
+ applyAction: (detail) => this.onSceneActionExecute(new CustomEvent('scene-action-execute', { detail }))
743
+ }
744
+ });
892
745
  }
893
746
  pageUpdated(changes, lifecycle) {
894
747
  if (this.active) {
@@ -898,6 +751,7 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
898
751
  this.boardId = lifecycle.resourceId;
899
752
  }
900
753
  else {
754
+ clearAssistantSurface(this);
901
755
  this.boardId = null;
902
756
  this.board = null;
903
757
  this.model = null;
@@ -946,41 +800,7 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
946
800
  ${this.scene && boardPlacementProvider()
947
801
  ? html `<board-placement-tray .boardId=${this.boardId} .scene=${this.scene}></board-placement-tray>`
948
802
  : nothing}
949
- ${this.boardId
950
- ? html `
951
- <button
952
- class="ai-toggle ${this.aiPanelOpen ? 'open' : ''}"
953
- title="${i18next.t('text.ai_assistant') || 'AI Assistant'}"
954
- @click=${this.toggleAIPanel}>
955
- <md-icon>${this.aiPanelOpen ? 'close' : 'auto_awesome'}</md-icon>AI
956
- </button>
957
- `
958
- : nothing}
959
803
  </div>
960
- ${this.aiPanelOpen
961
- ? html `
962
- <div class="ai-panel">
963
- <ox-assistant-chat
964
- chat-endpoint="boardAIChat"
965
- .sessionId=${this.chatSessionId}
966
- .sessions=${this.chatSessions}
967
- .currentBoard=${this.model}
968
- .boardProvider=${() => this.getLiveBoard()}
969
- .searchProvider=${this.searchSceneForMention}
970
- .userProvider=${this.fetchDomainUsersForMention}
971
- .knownTypes=${this.knownTypes}
972
- .categories=${this.knownCategories}
973
- .componentSchemas=${this.componentSchemas}
974
- .selectedRefids=${this.extractSelectedRefids()}
975
- @session-switch=${this.onChatSessionSwitch}
976
- @session-create=${this.onChatSessionCreate}
977
- @scene-edit-patch=${this.onSceneEditPatch}
978
- @board-edit-revert=${this.onBoardEditRevert}
979
- @scene-action-execute=${this.onSceneActionExecute}>
980
- </ox-assistant-chat>
981
- </div>
982
- `
983
- : nothing}
984
804
  `;
985
805
  }
986
806
  async updateBoard() {
@@ -1118,18 +938,6 @@ __decorate([
1118
938
  property({ type: Boolean }),
1119
939
  __metadata("design:type", Boolean)
1120
940
  ], BoardModellerPage.prototype, "preparing", void 0);
1121
- __decorate([
1122
- state(),
1123
- __metadata("design:type", String)
1124
- ], BoardModellerPage.prototype, "chatSessionId", void 0);
1125
- __decorate([
1126
- state(),
1127
- __metadata("design:type", Array)
1128
- ], BoardModellerPage.prototype, "chatSessions", void 0);
1129
- __decorate([
1130
- state(),
1131
- __metadata("design:type", Object)
1132
- ], BoardModellerPage.prototype, "aiPanelOpen", void 0);
1133
941
  __decorate([
1134
942
  query('ox-board-modeller'),
1135
943
  __metadata("design:type", BoardModeller)