@things-factory/ai-assistant 10.1.208 → 10.1.210

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.
Files changed (167) hide show
  1. package/client/components/assistant-chat.ts +238 -9
  2. package/client/components/assistant-dock.ts +9 -4
  3. package/client/components/assistant-invite.ts +138 -0
  4. package/client/components/assistant-session-manager.ts +53 -12
  5. package/client/components/proposal-request.test.ts +10 -0
  6. package/client/components/roster-identity.test.ts +2 -12
  7. package/client/components/roster-identity.ts +0 -6
  8. package/client/utils/assistant-session-actions.ts +3 -1
  9. package/client/utils/assistant-session-admin.ts +9 -2
  10. package/client/utils/assistant-session-controller.ts +1 -1
  11. package/client/utils/assistant-typing.test.ts +33 -0
  12. package/client/utils/assistant-typing.ts +53 -0
  13. package/dist-client/components/assistant-chat.d.ts +28 -0
  14. package/dist-client/components/assistant-chat.js +259 -8
  15. package/dist-client/components/assistant-chat.js.map +1 -1
  16. package/dist-client/components/assistant-dock.js +12 -4
  17. package/dist-client/components/assistant-dock.js.map +1 -1
  18. package/dist-client/components/assistant-invite.d.ts +39 -0
  19. package/dist-client/components/assistant-invite.js +150 -0
  20. package/dist-client/components/assistant-invite.js.map +1 -0
  21. package/dist-client/components/assistant-session-manager.d.ts +5 -1
  22. package/dist-client/components/assistant-session-manager.js +40 -10
  23. package/dist-client/components/assistant-session-manager.js.map +1 -1
  24. package/dist-client/components/proposal-request.test.js +8 -0
  25. package/dist-client/components/proposal-request.test.js.map +1 -1
  26. package/dist-client/components/roster-identity.d.ts +0 -2
  27. package/dist-client/components/roster-identity.js +0 -7
  28. package/dist-client/components/roster-identity.js.map +1 -1
  29. package/dist-client/components/roster-identity.test.js +2 -13
  30. package/dist-client/components/roster-identity.test.js.map +1 -1
  31. package/dist-client/tsconfig.tsbuildinfo +1 -1
  32. package/dist-client/utils/assistant-session-actions.d.ts +1 -0
  33. package/dist-client/utils/assistant-session-actions.js +3 -1
  34. package/dist-client/utils/assistant-session-actions.js.map +1 -1
  35. package/dist-client/utils/assistant-session-admin.d.ts +5 -2
  36. package/dist-client/utils/assistant-session-admin.js +3 -0
  37. package/dist-client/utils/assistant-session-admin.js.map +1 -1
  38. package/dist-client/utils/assistant-session-controller.d.ts +1 -1
  39. package/dist-client/utils/assistant-session-controller.js.map +1 -1
  40. package/dist-client/utils/assistant-typing.d.ts +20 -0
  41. package/dist-client/utils/assistant-typing.js +48 -0
  42. package/dist-client/utils/assistant-typing.js.map +1 -0
  43. package/dist-client/utils/assistant-typing.test.d.ts +1 -0
  44. package/dist-client/utils/assistant-typing.test.js +30 -0
  45. package/dist-client/utils/assistant-typing.test.js.map +1 -0
  46. package/dist-server/service/assistant-chat-resolver.js +5 -12
  47. package/dist-server/service/assistant-chat-resolver.js.map +1 -1
  48. package/dist-server/service/assistant-mode/index.d.ts +1 -1
  49. package/dist-server/service/assistant-mode/index.js +2 -1
  50. package/dist-server/service/assistant-mode/index.js.map +1 -1
  51. package/dist-server/service/assistant-mode/mode-categories.js +9 -2
  52. package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
  53. package/dist-server/service/assistant-mode/screen-rules.d.ts +6 -0
  54. package/dist-server/service/assistant-mode/screen-rules.js +10 -3
  55. package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
  56. package/dist-server/service/chat-message/chat-message-room.js +3 -1
  57. package/dist-server/service/chat-message/chat-message-room.js.map +1 -1
  58. package/dist-server/service/chat-message/chat-typing-subscription.d.ts +10 -0
  59. package/dist-server/service/chat-message/chat-typing-subscription.js +68 -0
  60. package/dist-server/service/chat-message/chat-typing-subscription.js.map +1 -0
  61. package/dist-server/service/chat-message/chat-typing.d.ts +28 -0
  62. package/dist-server/service/chat-message/chat-typing.js +40 -0
  63. package/dist-server/service/chat-message/chat-typing.js.map +1 -0
  64. package/dist-server/service/chat-session/attachment-tool-result.d.ts +1 -0
  65. package/dist-server/service/chat-session/attachment-tool-result.js +6 -0
  66. package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -1
  67. package/dist-server/service/chat-session/attachment-tools.js +2 -2
  68. package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
  69. package/dist-server/service/chat-session/chat-session.d.ts +2 -1
  70. package/dist-server/service/chat-session/chat-session.js +1 -1
  71. package/dist-server/service/chat-session/chat-session.js.map +1 -1
  72. package/dist-server/service/chat-session/session-activity-broadcast.d.ts +12 -0
  73. package/dist-server/service/chat-session/session-activity-broadcast.js +15 -0
  74. package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -1
  75. package/dist-server/service/chat-session/session-activity-publish.d.ts +2 -0
  76. package/dist-server/service/chat-session/session-activity-publish.js +5 -2
  77. package/dist-server/service/chat-session/session-activity-publish.js.map +1 -1
  78. package/dist-server/service/chat-session/session-visibility.d.ts +17 -2
  79. package/dist-server/service/chat-session/session-visibility.js +28 -3
  80. package/dist-server/service/chat-session/session-visibility.js.map +1 -1
  81. package/dist-server/service/chat-session/session-write-gate.d.ts +1 -1
  82. package/dist-server/service/chat-session/session-write-gate.js +1 -1
  83. package/dist-server/service/chat-session/session-write-gate.js.map +1 -1
  84. package/dist-server/service/chat-session-participant/index.d.ts +1 -0
  85. package/dist-server/service/chat-session-participant/index.js +4 -1
  86. package/dist-server/service/chat-session-participant/index.js.map +1 -1
  87. package/dist-server/service/chat-session-participant/invite-notification.d.ts +16 -1
  88. package/dist-server/service/chat-session-participant/invite-notification.js +34 -5
  89. package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -1
  90. package/dist-server/service/chat-session-participant/mention-invite.d.ts +30 -4
  91. package/dist-server/service/chat-session-participant/mention-invite.js +78 -31
  92. package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -1
  93. package/dist-server/service/chat-session-participant/read-roster.js +5 -2
  94. package/dist-server/service/chat-session-participant/read-roster.js.map +1 -1
  95. package/dist-server/service/chat-session-resolver.d.ts +27 -0
  96. package/dist-server/service/chat-session-resolver.js +226 -25
  97. package/dist-server/service/chat-session-resolver.js.map +1 -1
  98. package/dist-server/service/data-entry/entry-target.d.ts +6 -0
  99. package/dist-server/service/data-entry/entry-target.js.map +1 -1
  100. package/dist-server/service/data-entry/entry-tools.js +2 -2
  101. package/dist-server/service/data-entry/entry-tools.js.map +1 -1
  102. package/dist-server/service/data-entry/entry-update.js +1 -1
  103. package/dist-server/service/data-entry/entry-update.js.map +1 -1
  104. package/dist-server/service/data-read/list-read.d.ts +10 -0
  105. package/dist-server/service/data-read/list-read.js +12 -0
  106. package/dist-server/service/data-read/list-read.js.map +1 -1
  107. package/dist-server/service/data-read/read-target.d.ts +5 -0
  108. package/dist-server/service/data-read/read-target.js.map +1 -1
  109. package/dist-server/service/index.d.ts +4 -2
  110. package/dist-server/service/index.js +9 -1
  111. package/dist-server/service/index.js.map +1 -1
  112. package/dist-server/service/testing/conversation-script.d.ts +1 -1
  113. package/dist-server/service/testing/conversation-script.js.map +1 -1
  114. package/dist-server/service/testing/mode-conformance.js +2 -0
  115. package/dist-server/service/testing/mode-conformance.js.map +1 -1
  116. package/dist-server/tsconfig.tsbuildinfo +1 -1
  117. package/package.json +2 -2
  118. package/server/service/assistant-chat-resolver.ts +7 -12
  119. package/server/service/assistant-mode/index.ts +1 -1
  120. package/server/service/assistant-mode/mode-categories.ts +11 -3
  121. package/server/service/assistant-mode/mode-front.test.ts +53 -1
  122. package/server/service/assistant-mode/screen-rules.test.ts +7 -1
  123. package/server/service/assistant-mode/screen-rules.ts +11 -2
  124. package/server/service/chat-message/chat-message-room.test.ts +7 -2
  125. package/server/service/chat-message/chat-message-room.ts +3 -1
  126. package/server/service/chat-message/chat-typing-subscription.ts +52 -0
  127. package/server/service/chat-message/chat-typing.test.ts +45 -0
  128. package/server/service/chat-message/chat-typing.ts +48 -0
  129. package/server/service/chat-session/attachment-tool-result.test.ts +9 -1
  130. package/server/service/chat-session/attachment-tool-result.ts +7 -0
  131. package/server/service/chat-session/attachment-tools.ts +3 -3
  132. package/server/service/chat-session/chat-session.ts +3 -2
  133. package/server/service/chat-session/group-room.test.ts +233 -0
  134. package/server/service/chat-session/session-activity-broadcast.ts +20 -0
  135. package/server/service/chat-session/session-activity-publish.ts +6 -1
  136. package/server/service/chat-session/session-activity-receivers.test.ts +26 -0
  137. package/server/service/chat-session/session-visibility.test.ts +32 -1
  138. package/server/service/chat-session/session-visibility.ts +33 -4
  139. package/server/service/chat-session/session-write-gate.test.ts +7 -1
  140. package/server/service/chat-session/session-write-gate.ts +2 -2
  141. package/server/service/chat-session-participant/index.ts +2 -0
  142. package/server/service/chat-session-participant/invite-notification.test.ts +36 -1
  143. package/server/service/chat-session-participant/invite-notification.ts +43 -5
  144. package/server/service/chat-session-participant/mention-invite.test.ts +49 -1
  145. package/server/service/chat-session-participant/mention-invite.ts +82 -28
  146. package/server/service/chat-session-participant/read-roster.test.ts +13 -4
  147. package/server/service/chat-session-participant/read-roster.ts +6 -3
  148. package/server/service/chat-session-resolver.ts +204 -27
  149. package/server/service/data-entry/data-entry-update.test.ts +8 -0
  150. package/server/service/data-entry/entry-target.ts +6 -0
  151. package/server/service/data-entry/entry-tools.ts +2 -2
  152. package/server/service/data-entry/entry-update.ts +1 -1
  153. package/server/service/data-read/list-missing.test.ts +49 -0
  154. package/server/service/data-read/list-read.ts +18 -0
  155. package/server/service/data-read/read-target.ts +5 -0
  156. package/server/service/index.ts +6 -1
  157. package/server/service/testing/conversation-script.ts +1 -1
  158. package/server/service/testing/mode-conformance.ts +1 -0
  159. package/test/assistant-dock.test.ts +5 -1
  160. package/test/assistant-session-manager.test.ts +4 -2
  161. package/test/open-doors.test.ts +10 -5
  162. package/translations/en.json +29 -4
  163. package/translations/es.json +29 -4
  164. package/translations/ja.json +29 -4
  165. package/translations/ko.json +29 -4
  166. package/translations/ms.json +29 -4
  167. package/translations/zh.json +29 -4
@@ -1,7 +1,10 @@
1
1
  var OxAssistantChat_1;
2
2
  import { __decorate, __metadata } from "tslib";
3
3
  import { replyDestination, quotedReply } from './reply-message.js';
4
- import { rosterInviteState, rosterName } from './roster-identity';
4
+ import { rosterName } from './roster-identity';
5
+ import { notify } from '@operato/layout';
6
+ import './assistant-invite.js';
7
+ import { TypingRoom } from '../utils/assistant-typing.js';
5
8
  /**
6
9
  * <ox-assistant-chat> — AI 도우미 채팅 컴포넌트 (Lit).
7
10
  *
@@ -235,6 +238,7 @@ const CHAT_MESSAGE_SUBSCRIPTION = gql `
235
238
  // C4 presence/roster — 세션 참여자 목록 + 마지막 접속(online 판정용).
236
239
  const CHAT_PARTICIPANTS_QUERY = gql `
237
240
  query ChatSessionParticipants($sessionId: String!) {
241
+ mayInviteToAISession(sessionId: $sessionId)
238
242
  chatSessionParticipants(sessionId: $sessionId) {
239
243
  id
240
244
  role
@@ -243,10 +247,38 @@ const CHAT_PARTICIPANTS_QUERY = gql `
243
247
  id
244
248
  name
245
249
  username
250
+ email
246
251
  }
247
252
  }
248
253
  }
249
254
  `;
255
+ /*
256
+ * A group conversation (ADR-0073 decision 3) - bringing someone in and sending them out, and the room's
257
+ * "typing" line. The doors decide who may; the roster only offers what the server said this person may do.
258
+ */
259
+ const INVITE_MUTATION = gql `
260
+ mutation InviteToAISession($sessionId: String!, $userIds: [String!]!) {
261
+ inviteToAISession(sessionId: $sessionId, userIds: $userIds)
262
+ }
263
+ `;
264
+ const REMOVE_MUTATION = gql `
265
+ mutation RemoveFromAISession($sessionId: String!, $userId: String!) {
266
+ removeFromAISession(sessionId: $sessionId, userId: $userId)
267
+ }
268
+ `;
269
+ const SAY_TYPING_MUTATION = gql `
270
+ mutation SayTyping($sessionId: String!) {
271
+ sayTyping(sessionId: $sessionId)
272
+ }
273
+ `;
274
+ const CHAT_TYPING_SUBSCRIPTION = gql `
275
+ subscription ChatTyping($sessionId: String!) {
276
+ chatTyping(sessionId: $sessionId) {
277
+ userId
278
+ name
279
+ }
280
+ }
281
+ `;
250
282
  const TOUCH_PRESENCE_MUTATION = gql `
251
283
  mutation TouchPresence($sessionId: String!) {
252
284
  touchPresence(sessionId: $sessionId)
@@ -314,6 +346,11 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
314
346
  /** C4 presence/roster — 세션 참여자 목록. lastSeenAt 으로 online 판정. */
315
347
  this.participants = [];
316
348
  this.rosterLoaded = false;
349
+ /** The name box under the roster is open. */
350
+ this.inviting = false;
351
+ /** Who is typing now, by name - kept by `typingRoom`, redrawn on each notice and as they fade. */
352
+ this.typingNames = [];
353
+ this.typingRoom = new TypingRoom();
317
354
  /** roster online 재평가 트리거 — heartbeat tick 마다 갱신(시간 경과 반영). */
318
355
  this._presenceTick = 0;
319
356
  /** 이미 transcript 에 반영된 서버 메시지 id 집합 — 자기 에코·중복 방송 차단. */
@@ -850,6 +887,52 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
850
887
  .roster-chip.online .roster-dot {
851
888
  background: #22c55e;
852
889
  }
890
+ .roster-remove {
891
+ display: inline-flex;
892
+ border: 0;
893
+ padding: 0;
894
+ margin-left: 1px;
895
+ background: none;
896
+ color: inherit;
897
+ opacity: 0.6;
898
+ cursor: pointer;
899
+ border-radius: 50%;
900
+ }
901
+ .roster-remove:hover {
902
+ opacity: 1;
903
+ }
904
+ .roster-remove md-icon {
905
+ --md-icon-size: 11px;
906
+ }
907
+ .roster-invite {
908
+ display: inline-flex;
909
+ align-items: center;
910
+ gap: 2px;
911
+ padding: 1px 7px 1px 5px;
912
+ border: 1px solid var(--md-sys-color-outline-variant, #cbd5e1);
913
+ border-radius: 9px;
914
+ background: none;
915
+ color: inherit;
916
+ font: inherit;
917
+ font-size: 10px;
918
+ cursor: pointer;
919
+ }
920
+ .roster-invite:hover {
921
+ background: var(--md-sys-color-surface-container-high, #eef2f7);
922
+ }
923
+ .roster-invite md-icon {
924
+ --md-icon-size: 12px;
925
+ }
926
+ .typing-line .typing-dots { gap: 3px; }
927
+ .typing-line .typing-dots > span { width: 4px; height: 4px; }
928
+ .typing-line {
929
+ display: flex;
930
+ align-items: center;
931
+ gap: 6px;
932
+ margin: 0 2px 4px;
933
+ color: var(--md-sys-color-on-surface-variant, #475569);
934
+ font-size: 11.5px;
935
+ }
853
936
 
854
937
  /* ── Session tabs ─────────────────────────────────── */
855
938
  .session-tabs {
@@ -2560,6 +2643,10 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2560
2643
  this.input = '';
2561
2644
  this.participants = [];
2562
2645
  this.rosterLoaded = false;
2646
+ this.mayInvite = undefined;
2647
+ this.inviting = false;
2648
+ this.typingRoom.clear();
2649
+ this.typingNames = [];
2563
2650
  this.replyingTo = undefined;
2564
2651
  this._seenMessageIds.clear(); // 세션 전환 — 에코 dedup 컨텍스트 리셋
2565
2652
  if (this.sessionId) {
@@ -2629,6 +2716,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2629
2716
  this._stopMessageSubscription();
2630
2717
  if (!this.sessionId)
2631
2718
  return;
2719
+ this._startTypingSubscription();
2632
2720
  try {
2633
2721
  this._messageSub = client
2634
2722
  .subscribe({ query: CHAT_MESSAGE_SUBSCRIPTION, variables: { sessionId: this.sessionId } })
@@ -2643,7 +2731,49 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2643
2731
  /* 구독 미지원 환경 — 조용히 스킵 */
2644
2732
  }
2645
2733
  }
2734
+ /** The room's "typing" line - notices of the others, which fade by themselves (`TypingRoom`). */
2735
+ _startTypingSubscription() {
2736
+ try {
2737
+ this._typingSub = client
2738
+ .subscribe({ query: CHAT_TYPING_SUBSCRIPTION, variables: { sessionId: this.sessionId } })
2739
+ .subscribe({
2740
+ next: ({ data }) => {
2741
+ if (!data?.chatTyping)
2742
+ return;
2743
+ this.typingRoom.heard(data.chatTyping);
2744
+ this._refreshTyping();
2745
+ },
2746
+ error: () => {
2747
+ /* no typing line - the conversation itself is unaffected */
2748
+ }
2749
+ });
2750
+ }
2751
+ catch {
2752
+ /* subscriptions unavailable */
2753
+ }
2754
+ }
2755
+ _refreshTyping() {
2756
+ this.typingNames = this.typingRoom.typing();
2757
+ if (this.typingNames.length && !this._typingTimer) {
2758
+ this._typingTimer = setInterval(() => this._refreshTyping(), 1_000);
2759
+ }
2760
+ else if (!this.typingNames.length && this._typingTimer) {
2761
+ clearInterval(this._typingTimer);
2762
+ this._typingTimer = undefined;
2763
+ }
2764
+ }
2646
2765
  _stopMessageSubscription() {
2766
+ try {
2767
+ this._typingSub?.unsubscribe();
2768
+ }
2769
+ catch {
2770
+ /* already closed */
2771
+ }
2772
+ this._typingSub = undefined;
2773
+ if (this._typingTimer) {
2774
+ clearInterval(this._typingTimer);
2775
+ this._typingTimer = undefined;
2776
+ }
2647
2777
  if (this._messageSub) {
2648
2778
  try {
2649
2779
  this._messageSub.unsubscribe();
@@ -2663,6 +2793,9 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2663
2793
  * 자기 발신 메시지의 id 는 전송 reconcile 에서 이미 _seenMessageIds 에 등록됨.
2664
2794
  */
2665
2795
  _onRemoteMessage(m) {
2796
+ /* Their words arrived - they are not typing them any more. */
2797
+ if (this.typingRoom.spoke(m?.creator?.id))
2798
+ this._refreshTyping();
2666
2799
  const next = receiveRemoteMessage(this.lines, this._seenMessageIds, m);
2667
2800
  if (!next)
2668
2801
  return;
@@ -2687,6 +2820,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2687
2820
  if (this.sessionId !== sessionId)
2688
2821
  return;
2689
2822
  this.participants = result.data?.chatSessionParticipants ?? [];
2823
+ this.mayInvite = result.data?.mayInviteToAISession === true;
2690
2824
  this.rosterLoaded = true;
2691
2825
  }
2692
2826
  catch {
@@ -2967,6 +3101,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
2967
3101
  <button type="button" aria-label=${i18next.t('ai-assistant.button.cancel-reply')}
2968
3102
  @click=${() => this.cancelReply()}><md-icon>close</md-icon></button>
2969
3103
  </div>` : nothing}
3104
+ ${this.renderTyping()}
2970
3105
  ${this.renderInviteNotice()}
2971
3106
  ${this.addressesPeople
2972
3107
  ? html `<div class="send-destination" aria-live="polite">
@@ -3148,7 +3283,8 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3148
3283
  .map(([token]) => token);
3149
3284
  if (incoming.length === 0)
3150
3285
  return nothing;
3151
- const inviteState = rosterInviteState(this.participants, this._myUserId, this.rosterLoaded);
3286
+ /* What the server said this person may do here - no copy of the rule on the screen (`maySeat`). */
3287
+ const inviteState = !this.rosterLoaded || this.mayInvite === undefined ? 'checking' : this.mayInvite ? 'allowed' : 'denied';
3152
3288
  const mayInvite = inviteState === 'allowed';
3153
3289
  const names = incoming.map(token => `@${token}`).join(', ');
3154
3290
  return html `
@@ -3160,10 +3296,15 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3160
3296
  names,
3161
3297
  defaultValue: '{names} will join this conversation when you send'
3162
3298
  })
3163
- : i18next.t('ai-assistant.text.cannot-invite', {
3164
- names,
3165
- defaultValue: '{names} will not join — only the people already in this conversation can bring someone in'
3166
- })}
3299
+ : this.visibility === 'private'
3300
+ ? i18next.t('ai-assistant.text.cannot-invite-private', {
3301
+ names,
3302
+ defaultValue: '{names} will not join — a private conversation is yours alone. Make it a group to bring people in'
3303
+ })
3304
+ : i18next.t('ai-assistant.text.cannot-invite-group', {
3305
+ names,
3306
+ defaultValue: '{names} will not join — only the person who started this group can bring someone in'
3307
+ })}
3167
3308
  </span>
3168
3309
  </div>
3169
3310
  `;
@@ -3294,6 +3435,7 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3294
3435
  onTextareaInput(e) {
3295
3436
  const ta = e.target;
3296
3437
  this.input = ta.value;
3438
+ this._sayTyping();
3297
3439
  if (this.inputDebounceTimer)
3298
3440
  clearTimeout(this.inputDebounceTimer);
3299
3441
  this.inputDebounceTimer = setTimeout(() => {
@@ -3597,27 +3739,120 @@ let OxAssistantChat = class OxAssistantChat extends LitElement {
3597
3739
  `;
3598
3740
  }
3599
3741
  /** C4 roster — 세션 참여자 + online 표시. 참여자 2명 미만이면 숨김(혼자면 의미 없음). */
3742
+ /**
3743
+ * Who is in the room. A group shows it always - its people are the point of it, and the owner brings them in
3744
+ * here; any other room once someone else has joined. The buttons follow what the server said this person may
3745
+ * do (`mayInviteToAISession`, the owner's role); the doors decide again.
3746
+ */
3600
3747
  renderRoster() {
3601
- if (!this.sessionId || this.participants.length < 2)
3748
+ if (!this.sessionId)
3749
+ return nothing;
3750
+ const group = this.visibility === 'group';
3751
+ if (!group && this.participants.length < 2)
3602
3752
  return nothing;
3603
3753
  const online = this.participants.filter(p => this._isOnline(p.lastSeenAt)).length;
3754
+ /* Who I am is my email here, as everywhere in this chat - the credential carries no user id. */
3755
+ const myEmail = this._myEmail;
3756
+ const iOwn = !!myEmail && this.participants.some(p => p.role === 'owner' && p.user?.email === myEmail);
3757
+ const inviteLabel = i18next.t('ai-assistant.button.invite', { defaultValue: 'Invite' });
3604
3758
  return html `
3605
- <div class="roster" role="group" aria-label="participants">
3759
+ <div class="roster" role="group" aria-label=${i18next.t('ai-assistant.label.participants', { defaultValue: 'People in this conversation' })}>
3606
3760
  <span class="roster-count" title=${`${online} online / ${this.participants.length} total`}>
3607
3761
  <md-icon>group</md-icon>${online}/${this.participants.length}
3608
3762
  </span>
3609
3763
  ${this.participants.map(p => {
3610
3764
  const name = rosterName(p, String(i18next.t('ai-assistant.text.participant-unavailable')));
3611
3765
  const on = this._isOnline(p.lastSeenAt);
3766
+ const removable = iOwn && p.role === 'member' && !!p.user?.id;
3612
3767
  return html `<span
3613
3768
  class="roster-chip ${on ? 'online' : 'offline'}"
3614
3769
  title=${`${name} · ${p.role}${on ? ' · online' : ''}`}>
3615
3770
  <span class="roster-dot"></span><span class="roster-name">${name}</span>
3771
+ ${removable
3772
+ ? html `<button
3773
+ type="button"
3774
+ class="roster-remove"
3775
+ aria-label=${i18next.t('ai-assistant.button.remove-participant', { name, defaultValue: 'Send {name} out' })}
3776
+ title=${i18next.t('ai-assistant.button.remove-participant', { name, defaultValue: 'Send {name} out' })}
3777
+ @click=${() => this._removeParticipant(p.user.id, name)}><md-icon>close</md-icon></button>`
3778
+ : nothing}
3616
3779
  </span>`;
3617
3780
  })}
3781
+ ${this.mayInvite && !this.inviting
3782
+ ? html `<button type="button" class="roster-invite" @click=${() => (this.inviting = true)}>
3783
+ <md-icon>person_add</md-icon>${inviteLabel}
3784
+ </button>`
3785
+ : nothing}
3618
3786
  </div>
3787
+ ${this.inviting
3788
+ ? html `<ox-assistant-invite
3789
+ .search=${(query) => (this.userProvider ? Promise.resolve(this.userProvider(query)) : this.fetchDomainUsers(query))}
3790
+ .exclude=${this.participants.map(p => p.user?.id).filter(Boolean)}
3791
+ @assistant-invite-pick=${(e) => this._invite(e.detail.userId, e.detail.name)}
3792
+ @assistant-invite-close=${() => (this.inviting = false)}></ox-assistant-invite>`
3793
+ : nothing}
3619
3794
  `;
3620
3795
  }
3796
+ /** Bring someone in - the door seats them and tells them (`inviteToAISession`). */
3797
+ async _invite(userId, name) {
3798
+ const sessionId = this.sessionId;
3799
+ if (!sessionId || !userId)
3800
+ return;
3801
+ try {
3802
+ const result = await client.mutate({ mutation: INVITE_MUTATION, variables: { sessionId, userIds: [userId] } });
3803
+ if (result.errors?.length)
3804
+ throw new Error(result.errors[0].message);
3805
+ if (this.sessionId !== sessionId)
3806
+ return;
3807
+ this.inviting = false;
3808
+ notify({ level: 'info', message: i18next.t('ai-assistant.text.invited', { name, defaultValue: '{name} can now read and write here, and has been told.' }) });
3809
+ await this._loadParticipants();
3810
+ }
3811
+ catch (error) {
3812
+ this.errorMessage = error instanceof Error ? error.message : String(error);
3813
+ }
3814
+ }
3815
+ /** Send a member out - in a group they no longer read or write here. Bringing them back is inviting them again. */
3816
+ async _removeParticipant(userId, name) {
3817
+ const sessionId = this.sessionId;
3818
+ if (!sessionId)
3819
+ return;
3820
+ try {
3821
+ const result = await client.mutate({ mutation: REMOVE_MUTATION, variables: { sessionId, userId } });
3822
+ if (result.errors?.length)
3823
+ throw new Error(result.errors[0].message);
3824
+ if (this.sessionId !== sessionId)
3825
+ return;
3826
+ notify({ level: 'info', message: i18next.t('ai-assistant.text.removed-participant', { name, defaultValue: '{name} has left this conversation.' }) });
3827
+ await this._loadParticipants();
3828
+ }
3829
+ catch (error) {
3830
+ this.errorMessage = error instanceof Error ? error.message : String(error);
3831
+ }
3832
+ }
3833
+ /** Tell the others I am typing - only where someone else could hear it, and not on every key (`TypingRoom`). */
3834
+ _sayTyping() {
3835
+ const sessionId = this.sessionId;
3836
+ if (!sessionId || this.visibility === 'private')
3837
+ return;
3838
+ if (this.visibility !== 'open' && this.participants.length < 2)
3839
+ return;
3840
+ if (!this.typingRoom.shouldSay(this.input))
3841
+ return;
3842
+ client.mutate({ mutation: SAY_TYPING_MUTATION, variables: { sessionId } }).catch(() => {
3843
+ /* a missed notice is a line that does not show */
3844
+ });
3845
+ }
3846
+ /** "Kim is typing…" - over the box, while their notices keep coming. */
3847
+ renderTyping() {
3848
+ const names = this.typingNames;
3849
+ if (!names.length)
3850
+ return nothing;
3851
+ const who = names.length > 2 ? i18next.t('ai-assistant.text.typing-many', { count: names.length, defaultValue: '{count} people are typing' }) : i18next.t('ai-assistant.text.typing', { names: names.join(', '), defaultValue: '{names} typing' });
3852
+ return html `<div class="typing-line" role="status" aria-live="polite">
3853
+ <span class="typing-dots"><span></span><span></span><span></span></span><span>${who}</span>
3854
+ </div>`;
3855
+ }
3621
3856
  _onSessionTabClick(sessionId) {
3622
3857
  if (sessionId === this.sessionId)
3623
3858
  return;
@@ -4655,6 +4890,10 @@ __decorate([
4655
4890
  property({ attribute: false }),
4656
4891
  __metadata("design:type", Function)
4657
4892
  ], OxAssistantChat.prototype, "prepareSession", void 0);
4893
+ __decorate([
4894
+ property({ type: String }),
4895
+ __metadata("design:type", String)
4896
+ ], OxAssistantChat.prototype, "visibility", void 0);
4658
4897
  __decorate([
4659
4898
  property({ type: Array, attribute: false }),
4660
4899
  __metadata("design:type", Array)
@@ -4839,6 +5078,18 @@ __decorate([
4839
5078
  state(),
4840
5079
  __metadata("design:type", Object)
4841
5080
  ], OxAssistantChat.prototype, "rosterLoaded", void 0);
5081
+ __decorate([
5082
+ state(),
5083
+ __metadata("design:type", Boolean)
5084
+ ], OxAssistantChat.prototype, "mayInvite", void 0);
5085
+ __decorate([
5086
+ state(),
5087
+ __metadata("design:type", Object)
5088
+ ], OxAssistantChat.prototype, "inviting", void 0);
5089
+ __decorate([
5090
+ state(),
5091
+ __metadata("design:type", Array)
5092
+ ], OxAssistantChat.prototype, "typingNames", void 0);
4842
5093
  __decorate([
4843
5094
  state(),
4844
5095
  __metadata("design:type", Object)