@things-factory/board-ui 10.1.47 → 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.
@@ -23,26 +23,11 @@ export declare class BoardModellerPage extends PageView {
23
23
  componentGroupList?: any[];
24
24
  propertyEditor: any;
25
25
  preparing?: boolean;
26
- /** AI 협력 세션 id — 현재 활성 세션. boardId 변경 시 자동 ensure. 다중 세션 시 활성 1개. */
27
- chatSessionId?: string;
28
- /** 보드의 모든 ChatSession (탭으로 표시). */
29
- chatSessions: Array<{
30
- id: string;
31
- name?: string;
32
- createdAt?: string;
33
- }>;
34
- /** AI 채팅 패널 열림 상태 (기본 닫힘) */
35
- aiPanelOpen: boolean;
36
26
  /**
37
- * The conversation the AI panel stands in — the mode the app server registered as the default for
38
- * a board (ADR-0061 decision 7). The server then owns persona, tool categories and model. Absent
39
- * when the app registered none: the panel keeps its older, undeclared behaviour.
27
+ * The AI conversation open for this board - the framework's dock owns it (design/ai/shared-dock.md) and tells
28
+ * this page through the board surface's `sessionChanged`. Direct edits are recorded under it (C3).
40
29
  */
41
- aiMode?: {
42
- kind: string;
43
- target?: string;
44
- };
45
- private aiModeResolved;
30
+ private chatSessionId?;
46
31
  /**
47
32
  * 적용된 patch 별 inverse op 시퀀스 — Revert 기능용.
48
33
  *
@@ -84,23 +69,6 @@ export declare class BoardModellerPage extends PageView {
84
69
  description: string;
85
70
  };
86
71
  refresh(): Promise<void>;
87
- /** 패널 토글 시 호출 — 열 때만 chatSession 보장 (세션 리스트 로드 + 활성 세션 결정) */
88
- private toggleAIPanel;
89
- /** Ask the server which conversation a board opens in. Once per page; the target follows the board. */
90
- private resolveAIMode;
91
- private onAssistantModeChange;
92
- /** 보드의 모든 ChatSession 목록을 로드 + 활성 세션 보장.
93
- * - 세션이 0 개면 startBoardAISession 으로 첫 세션 생성 (단일 세션 UX 호환)
94
- * - 활성 세션 미설정 시 첫 번째 세션을 활성으로
95
- * - 이미 활성 세션 set 돼있고 list 에 포함되면 유지 */
96
- private ensureChatSession;
97
- /** `@` mention popup 의 도메인 사용자 후보 — chat 의 userProvider prop 으로 wire.
98
- * query 변할 때마다 호출되므로 server query (`domainUsersForMention`) 를 그대로 위임. */
99
- private fetchDomainUsersForMention;
100
- /** 채팅 패널의 탭 클릭 — 활성 세션 전환. */
101
- private onChatSessionSwitch;
102
- /** 채팅 패널의 "+" 버튼 — 새 세션 생성 후 활성으로 전환. */
103
- private onChatSessionCreate;
104
72
  /**
105
73
  * 모델러의 selected (things-scene Component 인스턴스 배열) 에서 refid 추출.
106
74
  *
@@ -194,6 +162,13 @@ export declare class BoardModellerPage extends PageView {
194
162
  private findTemplateByType;
195
163
  updated(changes: any): Promise<void>;
196
164
  disconnectedCallback(): void;
165
+ /**
166
+ * This board, told to the framework's AI dock (design/ai/shared-dock.md §3). The dock owns the
167
+ * conversation - sessions, modes, the manager - and hands the chat's scene edits back to this page.
168
+ * Decided 2026-09-25: the modeller no longer carries its own "AI" button and panel; the band's forum
169
+ * button opens the same dock as every other screen.
170
+ */
171
+ private provideAssistantBoard;
197
172
  pageUpdated(changes: any, lifecycle: any): void;
198
173
  render(): import("lit-html").TemplateResult<1>;
199
174
  updateBoard(): Promise<void>;
@@ -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,11 +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
- this.aiModeResolved = false;
85
81
  /**
86
82
  * 적용된 patch 별 inverse op 시퀀스 — Revert 기능용.
87
83
  *
@@ -96,67 +92,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
96
92
  this._syncedModel = null;
97
93
  this.board = null;
98
94
  this._fontCtrl = new FontController(this);
99
- /** `@` mention popup 의 도메인 사용자 후보 — chat 의 userProvider prop 으로 wire.
100
- * query 변할 때마다 호출되므로 server query (`domainUsersForMention`) 를 그대로 위임. */
101
- this.fetchDomainUsersForMention = async (query) => {
102
- try {
103
- const result = await client.query({
104
- query: gql `
105
- query DomainUsersForMention($query: String, $limit: Int) {
106
- domainUsersForMention(query: $query, limit: $limit) {
107
- id
108
- name
109
- email
110
- }
111
- }
112
- `,
113
- variables: { query: query || null, limit: 50 },
114
- context: gqlContext(),
115
- fetchPolicy: 'cache-first'
116
- });
117
- return result.data?.domainUsersForMention ?? [];
118
- }
119
- catch (ex) {
120
- // 조용히 — chat panel 이 빈 결과로 처리
121
- console.warn('[board-modeller-page] fetchDomainUsersForMention failed:', ex);
122
- return [];
123
- }
124
- };
125
- /** 채팅 패널의 탭 클릭 — 활성 세션 전환. */
126
- this.onChatSessionSwitch = (e) => {
127
- const newId = e.detail?.sessionId;
128
- if (typeof newId === 'string' && newId !== this.chatSessionId) {
129
- this.chatSessionId = newId;
130
- }
131
- };
132
- /** 채팅 패널의 "+" 버튼 — 새 세션 생성 후 활성으로 전환. */
133
- this.onChatSessionCreate = async () => {
134
- if (!this.boardId)
135
- return;
136
- try {
137
- const created = await client.mutate({
138
- mutation: gql `
139
- mutation CreateChatSession($boardId: String!, $name: String) {
140
- createChatSession(boardId: $boardId, name: $name) {
141
- id
142
- name
143
- createdAt
144
- }
145
- }
146
- `,
147
- variables: { boardId: this.boardId, name: null },
148
- context: gqlContext()
149
- });
150
- const s = created.data?.createChatSession;
151
- if (s) {
152
- this.chatSessions = [...this.chatSessions, s];
153
- this.chatSessionId = s.id;
154
- }
155
- }
156
- catch (ex) {
157
- notifyError(ex);
158
- }
159
- };
160
95
  /**
161
96
  * AI 채팅 mention popup (`#`) 검색 위임 — things-scene SceneSearchEngine 으로 wrap.
162
97
  *
@@ -375,54 +310,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
375
310
  flex: 1;
376
311
  }
377
312
 
378
- /*
379
- * The assistant's door and its column, on the scheme's own surfaces. The door is a quiet
380
- * tonal pill on the canvas, like the save FAB beside it; open, it takes the pressed tone.
381
- */
382
- .ai-toggle {
383
- position: absolute;
384
- right: 12px;
385
- bottom: 12px;
386
- z-index: 10;
387
- display: inline-flex;
388
- align-items: center;
389
- gap: 6px;
390
- height: 36px;
391
- padding: 0 14px 0 11px;
392
- border: 1px solid var(--md-sys-color-outline-variant);
393
- border-radius: 999px;
394
- background: var(--md-sys-color-surface-container-high);
395
- color: var(--md-sys-color-on-surface);
396
- font: 500 0.8rem/1 var(--theme-font, Roboto, sans-serif);
397
- cursor: pointer;
398
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.12);
399
- transition: background 0.15s, transform 0.1s;
400
- }
401
- .ai-toggle md-icon {
402
- --md-icon-size: 18px;
403
- color: var(--md-sys-color-primary);
404
- font-variation-settings: 'FILL' 1;
405
- }
406
- .ai-toggle:hover { background: var(--md-sys-color-surface-container-highest); }
407
- .ai-toggle:active { transform: scale(0.97); }
408
- .ai-toggle.open {
409
- border-color: transparent;
410
- background: var(--md-sys-color-secondary-container);
411
- color: var(--md-sys-color-on-secondary-container);
412
- }
413
- .ai-toggle.open md-icon { color: inherit; font-variation-settings: 'FILL' 0; }
414
-
415
- .ai-panel {
416
- flex: 0 0 340px;
417
- display: flex;
418
- min-width: 0;
419
- border-left: 1px solid var(--md-sys-color-outline-variant);
420
- background: var(--md-sys-color-surface-container-low);
421
- color: var(--md-sys-color-on-surface);
422
- }
423
-
424
- ox-assistant-chat { flex: 1; }
425
-
426
313
  ox-oops-note {
427
314
  display: block;
428
315
  position: absolute;
@@ -431,14 +318,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
431
318
  transform: translate(-50%, -50%);
432
319
  }
433
320
 
434
- @media (max-width: 900px) {
435
- .ai-panel {
436
- flex-basis: 100%;
437
- position: absolute;
438
- inset: 0;
439
- z-index: 20;
440
- }
441
- }
442
321
  `
443
322
  ]; }
444
323
  get knownTypes() {
@@ -599,16 +478,9 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
599
478
  this.model = {
600
479
  ...this.board.model
601
480
  };
602
- // AI 협력 세션 — 보드 전환 시 이전 보드의 세션 / 세션 리스트 정리 (chat panel 의
603
- // sessionId/sessions 변화 감지 → history 비우고 새로 로드). 패널 열린 채 보드 옮겼다면
604
- // 새 보드의 세션 리스트를 즉시 ensure (fire-and-forget — 에러는 ensureChatSession 처리).
481
+ /* Another board is another conversation - the dock reopens for the new surface. */
605
482
  this.chatSessionId = undefined;
606
- this.chatSessions = [];
607
- if (this.aiMode)
608
- this.aiMode = { ...this.aiMode, target: this.boardId ?? undefined };
609
- if (this.aiPanelOpen && this.boardId) {
610
- this.ensureChatSession();
611
- }
483
+ this.provideAssistantBoard();
612
484
  }
613
485
  catch (ex) {
614
486
  notifyError(ex);
@@ -618,98 +490,6 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
618
490
  this.updateContext();
619
491
  }
620
492
  }
621
- /** 패널 토글 시 호출 — 열 때만 chatSession 보장 (세션 리스트 로드 + 활성 세션 결정) */
622
- async toggleAIPanel() {
623
- const willOpen = !this.aiPanelOpen;
624
- if (willOpen && this.boardId) {
625
- await Promise.all([this.ensureChatSession(), this.resolveAIMode()]);
626
- }
627
- this.aiPanelOpen = willOpen;
628
- }
629
- /** Ask the server which conversation a board opens in. Once per page; the target follows the board. */
630
- async resolveAIMode() {
631
- if (this.aiModeResolved)
632
- return;
633
- this.aiModeResolved = true;
634
- try {
635
- const result = await client.query({
636
- query: gql `
637
- query BoardAssistantModes($subject: String!) {
638
- assistantModes(subject: $subject) {
639
- kind
640
- isDefault
641
- }
642
- }
643
- `,
644
- variables: { subject: 'board' },
645
- context: gqlContext()
646
- });
647
- const modes = (result.data?.assistantModes ?? []);
648
- const chosen = modes.find(m => m.isDefault) ?? modes[0];
649
- this.aiMode = chosen ? { kind: chosen.kind, target: this.boardId ?? undefined } : undefined;
650
- }
651
- catch (e) {
652
- /* A server without ai-assistant's mode door: stay undeclared rather than break the panel. */
653
- this.aiMode = undefined;
654
- }
655
- }
656
- onAssistantModeChange(e) {
657
- const kind = e.detail?.kind;
658
- if (kind)
659
- this.aiMode = { kind, target: this.boardId ?? undefined };
660
- }
661
- /** 보드의 모든 ChatSession 목록을 로드 + 활성 세션 보장.
662
- * - 세션이 0 개면 startBoardAISession 으로 첫 세션 생성 (단일 세션 UX 호환)
663
- * - 활성 세션 미설정 시 첫 번째 세션을 활성으로
664
- * - 이미 활성 세션 set 돼있고 list 에 포함되면 유지 */
665
- async ensureChatSession() {
666
- if (!this.boardId)
667
- return;
668
- try {
669
- const result = await client.query({
670
- query: gql `
671
- query ChatSessionsByBoard($boardId: String!) {
672
- chatSessionsByBoard(boardId: $boardId) {
673
- id
674
- name
675
- createdAt
676
- }
677
- }
678
- `,
679
- variables: { boardId: this.boardId },
680
- context: gqlContext(),
681
- fetchPolicy: 'network-only'
682
- });
683
- let sessions = result.data?.chatSessionsByBoard ?? [];
684
- if (sessions.length === 0) {
685
- // 첫 세션 ensure (idempotent)
686
- const created = await client.mutate({
687
- mutation: gql `
688
- mutation StartBoardAISession($boardId: String!) {
689
- startBoardAISession(boardId: $boardId) {
690
- id
691
- name
692
- createdAt
693
- }
694
- }
695
- `,
696
- variables: { boardId: this.boardId },
697
- context: gqlContext()
698
- });
699
- const s = created.data?.startBoardAISession;
700
- if (s)
701
- sessions = [s];
702
- }
703
- this.chatSessions = sessions;
704
- // 활성 세션 결정 — 기존 active 가 list 에 있으면 유지, 아니면 첫 번째
705
- if (!this.chatSessionId || !sessions.some(s => s.id === this.chatSessionId)) {
706
- this.chatSessionId = sessions[0]?.id;
707
- }
708
- }
709
- catch (ex) {
710
- notifyError(ex);
711
- }
712
- }
713
493
  /**
714
494
  * 모델러의 selected (things-scene Component 인스턴스 배열) 에서 refid 추출.
715
495
  *
@@ -924,6 +704,44 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
924
704
  disconnectedCallback() {
925
705
  super.disconnectedCallback();
926
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
+ });
927
745
  }
928
746
  pageUpdated(changes, lifecycle) {
929
747
  if (this.active) {
@@ -933,6 +751,7 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
933
751
  this.boardId = lifecycle.resourceId;
934
752
  }
935
753
  else {
754
+ clearAssistantSurface(this);
936
755
  this.boardId = null;
937
756
  this.board = null;
938
757
  this.model = null;
@@ -981,44 +800,7 @@ let BoardModellerPage = class BoardModellerPage extends PageView {
981
800
  ${this.scene && boardPlacementProvider()
982
801
  ? html `<board-placement-tray .boardId=${this.boardId} .scene=${this.scene}></board-placement-tray>`
983
802
  : nothing}
984
- ${this.boardId
985
- ? html `
986
- <button
987
- class="ai-toggle ${this.aiPanelOpen ? 'open' : ''}"
988
- title="${i18next.t('text.ai_assistant') || 'AI Assistant'}"
989
- @click=${this.toggleAIPanel}>
990
- <md-icon>${this.aiPanelOpen ? 'close' : 'auto_awesome'}</md-icon>AI
991
- </button>
992
- `
993
- : nothing}
994
803
  </div>
995
- ${this.aiPanelOpen
996
- ? html `
997
- <div class="ai-panel">
998
- <ox-assistant-chat
999
- chat-endpoint="boardAIChat"
1000
- subject="board"
1001
- .mode=${this.aiMode}
1002
- @assistant-mode-change=${this.onAssistantModeChange}
1003
- .sessionId=${this.chatSessionId}
1004
- .sessions=${this.chatSessions}
1005
- .currentBoard=${this.model}
1006
- .boardProvider=${() => this.getLiveBoard()}
1007
- .searchProvider=${this.searchSceneForMention}
1008
- .userProvider=${this.fetchDomainUsersForMention}
1009
- .knownTypes=${this.knownTypes}
1010
- .categories=${this.knownCategories}
1011
- .componentSchemas=${this.componentSchemas}
1012
- .selectedRefids=${this.extractSelectedRefids()}
1013
- @session-switch=${this.onChatSessionSwitch}
1014
- @session-create=${this.onChatSessionCreate}
1015
- @scene-edit-patch=${this.onSceneEditPatch}
1016
- @board-edit-revert=${this.onBoardEditRevert}
1017
- @scene-action-execute=${this.onSceneActionExecute}>
1018
- </ox-assistant-chat>
1019
- </div>
1020
- `
1021
- : nothing}
1022
804
  `;
1023
805
  }
1024
806
  async updateBoard() {
@@ -1156,22 +938,6 @@ __decorate([
1156
938
  property({ type: Boolean }),
1157
939
  __metadata("design:type", Boolean)
1158
940
  ], BoardModellerPage.prototype, "preparing", void 0);
1159
- __decorate([
1160
- state(),
1161
- __metadata("design:type", String)
1162
- ], BoardModellerPage.prototype, "chatSessionId", void 0);
1163
- __decorate([
1164
- state(),
1165
- __metadata("design:type", Array)
1166
- ], BoardModellerPage.prototype, "chatSessions", void 0);
1167
- __decorate([
1168
- state(),
1169
- __metadata("design:type", Object)
1170
- ], BoardModellerPage.prototype, "aiPanelOpen", void 0);
1171
- __decorate([
1172
- state(),
1173
- __metadata("design:type", Object)
1174
- ], BoardModellerPage.prototype, "aiMode", void 0);
1175
941
  __decorate([
1176
942
  query('ox-board-modeller'),
1177
943
  __metadata("design:type", BoardModeller)