@things-factory/ai-assistant 10.1.208 → 10.1.212
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/client/components/assistant-chat.ts +238 -9
- package/client/components/assistant-dock.ts +9 -4
- package/client/components/assistant-invite.ts +138 -0
- package/client/components/assistant-session-manager.ts +53 -12
- package/client/components/proposal-request.test.ts +10 -0
- package/client/components/roster-identity.test.ts +2 -12
- package/client/components/roster-identity.ts +0 -6
- package/client/utils/assistant-session-actions.ts +3 -1
- package/client/utils/assistant-session-admin.ts +9 -2
- package/client/utils/assistant-session-controller.ts +1 -1
- package/client/utils/assistant-typing.test.ts +33 -0
- package/client/utils/assistant-typing.ts +53 -0
- package/dist-client/components/assistant-chat.d.ts +28 -0
- package/dist-client/components/assistant-chat.js +259 -8
- package/dist-client/components/assistant-chat.js.map +1 -1
- package/dist-client/components/assistant-dock.js +12 -4
- package/dist-client/components/assistant-dock.js.map +1 -1
- package/dist-client/components/assistant-invite.d.ts +39 -0
- package/dist-client/components/assistant-invite.js +150 -0
- package/dist-client/components/assistant-invite.js.map +1 -0
- package/dist-client/components/assistant-session-manager.d.ts +5 -1
- package/dist-client/components/assistant-session-manager.js +40 -10
- package/dist-client/components/assistant-session-manager.js.map +1 -1
- package/dist-client/components/proposal-request.test.js +8 -0
- package/dist-client/components/proposal-request.test.js.map +1 -1
- package/dist-client/components/roster-identity.d.ts +0 -2
- package/dist-client/components/roster-identity.js +0 -7
- package/dist-client/components/roster-identity.js.map +1 -1
- package/dist-client/components/roster-identity.test.js +2 -13
- package/dist-client/components/roster-identity.test.js.map +1 -1
- package/dist-client/tsconfig.tsbuildinfo +1 -1
- package/dist-client/utils/assistant-session-actions.d.ts +1 -0
- package/dist-client/utils/assistant-session-actions.js +3 -1
- package/dist-client/utils/assistant-session-actions.js.map +1 -1
- package/dist-client/utils/assistant-session-admin.d.ts +5 -2
- package/dist-client/utils/assistant-session-admin.js +3 -0
- package/dist-client/utils/assistant-session-admin.js.map +1 -1
- package/dist-client/utils/assistant-session-controller.d.ts +1 -1
- package/dist-client/utils/assistant-session-controller.js.map +1 -1
- package/dist-client/utils/assistant-typing.d.ts +20 -0
- package/dist-client/utils/assistant-typing.js +48 -0
- package/dist-client/utils/assistant-typing.js.map +1 -0
- package/dist-client/utils/assistant-typing.test.d.ts +1 -0
- package/dist-client/utils/assistant-typing.test.js +30 -0
- package/dist-client/utils/assistant-typing.test.js.map +1 -0
- package/dist-server/service/assistant-chat-resolver.js +5 -12
- package/dist-server/service/assistant-chat-resolver.js.map +1 -1
- package/dist-server/service/assistant-mode/index.d.ts +1 -1
- package/dist-server/service/assistant-mode/index.js +2 -1
- package/dist-server/service/assistant-mode/index.js.map +1 -1
- package/dist-server/service/assistant-mode/mode-categories.js +9 -2
- package/dist-server/service/assistant-mode/mode-categories.js.map +1 -1
- package/dist-server/service/assistant-mode/screen-rules.d.ts +6 -0
- package/dist-server/service/assistant-mode/screen-rules.js +10 -3
- package/dist-server/service/assistant-mode/screen-rules.js.map +1 -1
- package/dist-server/service/chat-message/chat-message-room.js +3 -1
- package/dist-server/service/chat-message/chat-message-room.js.map +1 -1
- package/dist-server/service/chat-message/chat-typing-subscription.d.ts +10 -0
- package/dist-server/service/chat-message/chat-typing-subscription.js +68 -0
- package/dist-server/service/chat-message/chat-typing-subscription.js.map +1 -0
- package/dist-server/service/chat-message/chat-typing.d.ts +28 -0
- package/dist-server/service/chat-message/chat-typing.js +40 -0
- package/dist-server/service/chat-message/chat-typing.js.map +1 -0
- package/dist-server/service/chat-session/attachment-tool-result.d.ts +1 -0
- package/dist-server/service/chat-session/attachment-tool-result.js +6 -0
- package/dist-server/service/chat-session/attachment-tool-result.js.map +1 -1
- package/dist-server/service/chat-session/attachment-tools.js +2 -2
- package/dist-server/service/chat-session/attachment-tools.js.map +1 -1
- package/dist-server/service/chat-session/chat-session.d.ts +2 -1
- package/dist-server/service/chat-session/chat-session.js +1 -1
- package/dist-server/service/chat-session/chat-session.js.map +1 -1
- package/dist-server/service/chat-session/session-activity-broadcast.d.ts +12 -0
- package/dist-server/service/chat-session/session-activity-broadcast.js +15 -0
- package/dist-server/service/chat-session/session-activity-broadcast.js.map +1 -1
- package/dist-server/service/chat-session/session-activity-publish.d.ts +2 -0
- package/dist-server/service/chat-session/session-activity-publish.js +5 -2
- package/dist-server/service/chat-session/session-activity-publish.js.map +1 -1
- package/dist-server/service/chat-session/session-visibility.d.ts +17 -2
- package/dist-server/service/chat-session/session-visibility.js +28 -3
- package/dist-server/service/chat-session/session-visibility.js.map +1 -1
- package/dist-server/service/chat-session/session-write-gate.d.ts +1 -1
- package/dist-server/service/chat-session/session-write-gate.js +1 -1
- package/dist-server/service/chat-session/session-write-gate.js.map +1 -1
- package/dist-server/service/chat-session-participant/index.d.ts +1 -0
- package/dist-server/service/chat-session-participant/index.js +4 -1
- package/dist-server/service/chat-session-participant/index.js.map +1 -1
- package/dist-server/service/chat-session-participant/invite-notification.d.ts +16 -1
- package/dist-server/service/chat-session-participant/invite-notification.js +34 -5
- package/dist-server/service/chat-session-participant/invite-notification.js.map +1 -1
- package/dist-server/service/chat-session-participant/mention-invite.d.ts +30 -4
- package/dist-server/service/chat-session-participant/mention-invite.js +78 -31
- package/dist-server/service/chat-session-participant/mention-invite.js.map +1 -1
- package/dist-server/service/chat-session-participant/read-roster.js +5 -2
- package/dist-server/service/chat-session-participant/read-roster.js.map +1 -1
- package/dist-server/service/chat-session-resolver.d.ts +27 -0
- package/dist-server/service/chat-session-resolver.js +226 -25
- package/dist-server/service/chat-session-resolver.js.map +1 -1
- package/dist-server/service/data-entry/entry-target.d.ts +6 -0
- package/dist-server/service/data-entry/entry-target.js.map +1 -1
- package/dist-server/service/data-entry/entry-tools.js +2 -2
- package/dist-server/service/data-entry/entry-tools.js.map +1 -1
- package/dist-server/service/data-entry/entry-update.js +1 -1
- package/dist-server/service/data-entry/entry-update.js.map +1 -1
- package/dist-server/service/data-read/list-read.d.ts +10 -0
- package/dist-server/service/data-read/list-read.js +12 -0
- package/dist-server/service/data-read/list-read.js.map +1 -1
- package/dist-server/service/data-read/read-target.d.ts +5 -0
- package/dist-server/service/data-read/read-target.js.map +1 -1
- package/dist-server/service/index.d.ts +4 -2
- package/dist-server/service/index.js +9 -1
- package/dist-server/service/index.js.map +1 -1
- package/dist-server/service/testing/conversation-script.d.ts +1 -1
- package/dist-server/service/testing/conversation-script.js.map +1 -1
- package/dist-server/service/testing/mode-conformance.js +2 -0
- package/dist-server/service/testing/mode-conformance.js.map +1 -1
- package/dist-server/tsconfig.tsbuildinfo +1 -1
- package/package.json +6 -6
- package/server/service/assistant-chat-resolver.ts +7 -12
- package/server/service/assistant-mode/index.ts +1 -1
- package/server/service/assistant-mode/mode-categories.ts +11 -3
- package/server/service/assistant-mode/mode-front.test.ts +53 -1
- package/server/service/assistant-mode/screen-rules.test.ts +7 -1
- package/server/service/assistant-mode/screen-rules.ts +11 -2
- package/server/service/chat-message/chat-message-room.test.ts +7 -2
- package/server/service/chat-message/chat-message-room.ts +3 -1
- package/server/service/chat-message/chat-typing-subscription.ts +52 -0
- package/server/service/chat-message/chat-typing.test.ts +45 -0
- package/server/service/chat-message/chat-typing.ts +48 -0
- package/server/service/chat-session/attachment-tool-result.test.ts +9 -1
- package/server/service/chat-session/attachment-tool-result.ts +7 -0
- package/server/service/chat-session/attachment-tools.ts +3 -3
- package/server/service/chat-session/chat-session.ts +3 -2
- package/server/service/chat-session/group-room.test.ts +233 -0
- package/server/service/chat-session/session-activity-broadcast.ts +20 -0
- package/server/service/chat-session/session-activity-publish.ts +6 -1
- package/server/service/chat-session/session-activity-receivers.test.ts +26 -0
- package/server/service/chat-session/session-visibility.test.ts +32 -1
- package/server/service/chat-session/session-visibility.ts +33 -4
- package/server/service/chat-session/session-write-gate.test.ts +7 -1
- package/server/service/chat-session/session-write-gate.ts +2 -2
- package/server/service/chat-session-participant/index.ts +2 -0
- package/server/service/chat-session-participant/invite-notification.test.ts +36 -1
- package/server/service/chat-session-participant/invite-notification.ts +43 -5
- package/server/service/chat-session-participant/mention-invite.test.ts +49 -1
- package/server/service/chat-session-participant/mention-invite.ts +82 -28
- package/server/service/chat-session-participant/read-roster.test.ts +13 -4
- package/server/service/chat-session-participant/read-roster.ts +6 -3
- package/server/service/chat-session-resolver.ts +204 -27
- package/server/service/data-entry/data-entry-update.test.ts +8 -0
- package/server/service/data-entry/entry-target.ts +6 -0
- package/server/service/data-entry/entry-tools.ts +2 -2
- package/server/service/data-entry/entry-update.ts +1 -1
- package/server/service/data-read/list-missing.test.ts +49 -0
- package/server/service/data-read/list-read.ts +18 -0
- package/server/service/data-read/read-target.ts +5 -0
- package/server/service/index.ts +6 -1
- package/server/service/testing/conversation-script.ts +1 -1
- package/server/service/testing/mode-conformance.ts +1 -0
- package/test/assistant-dock.test.ts +5 -1
- package/test/assistant-session-manager.test.ts +4 -2
- package/test/open-doors.test.ts +10 -5
- package/translations/en.json +29 -4
- package/translations/es.json +29 -4
- package/translations/ja.json +29 -4
- package/translations/ko.json +30 -5
- package/translations/ms.json +29 -4
- package/translations/zh.json +29 -4
|
@@ -13,7 +13,8 @@ import {
|
|
|
13
13
|
sessionActions,
|
|
14
14
|
type AssistantSessionAdmin,
|
|
15
15
|
type AssistantSessionEntry,
|
|
16
|
-
type AssistantSessionFilters
|
|
16
|
+
type AssistantSessionFilters,
|
|
17
|
+
type SessionVisibility
|
|
17
18
|
} from '../utils/assistant-session-admin.js'
|
|
18
19
|
|
|
19
20
|
const PAGE = 30
|
|
@@ -34,7 +35,7 @@ const t = (key: string, defaultValue: string, options: Record<string, unknown> =
|
|
|
34
35
|
* with the toolbar's own events and does not open anything itself:
|
|
35
36
|
* assistant-session-select { sessionId } open that conversation
|
|
36
37
|
* assistant-session-reset start a new one
|
|
37
|
-
* assistant-session-changed { sessionId, change: 'renamed' | 'hidden' | 'unhidden' | 'removed' | 'visibility' }
|
|
38
|
+
* assistant-session-changed { sessionId, change: 'renamed' | 'hidden' | 'unhidden' | 'removed' | 'visibility' | 'left' }
|
|
38
39
|
* a row changed - the host refreshes its tabs; if the open
|
|
39
40
|
* conversation was removed or hidden, it moves off it
|
|
40
41
|
*
|
|
@@ -459,7 +460,7 @@ export class AssistantSessionManager extends LitElement {
|
|
|
459
460
|
void this.act(entry, hidden ? 'hidden' : 'unhidden', () => this.admin.hide(entry.id, hidden), t('hide-refused', 'this conversation could not be hidden.'))
|
|
460
461
|
}
|
|
461
462
|
|
|
462
|
-
private setVisibility(entry: AssistantSessionEntry, visibility:
|
|
463
|
+
private setVisibility(entry: AssistantSessionEntry, visibility: SessionVisibility) {
|
|
463
464
|
void this.act(
|
|
464
465
|
entry,
|
|
465
466
|
'visibility',
|
|
@@ -497,8 +498,9 @@ export class AssistantSessionManager extends LitElement {
|
|
|
497
498
|
</select>
|
|
498
499
|
<select aria-label=${t('filter-visibility', 'Visibility')} .value=${this.visibility}
|
|
499
500
|
@change=${(e: Event) => this.refilter(() => (this.visibility = (e.target as HTMLSelectElement).value as any))}>
|
|
500
|
-
<option value="all">${t('filter-any-visibility', '
|
|
501
|
+
<option value="all">${t('filter-any-visibility', 'Anyone')}</option>
|
|
501
502
|
<option value="open">${t('filter-open', 'Public')}</option>
|
|
503
|
+
<option value="group">${t('filter-group', 'Group')}</option>
|
|
502
504
|
<option value="private">${t('filter-private', 'Private')}</option>
|
|
503
505
|
</select>
|
|
504
506
|
<label><input type="checkbox" .checked=${this.unreadOnly}
|
|
@@ -529,7 +531,7 @@ export class AssistantSessionManager extends LitElement {
|
|
|
529
531
|
private renderRow(entry: AssistantSessionEntry): TemplateResult {
|
|
530
532
|
const name = entry.name || t('untitled', 'Untitled conversation')
|
|
531
533
|
const actions = sessionActions(entry)
|
|
532
|
-
const
|
|
534
|
+
const isOpen = !entry.visibility || entry.visibility === 'open'
|
|
533
535
|
|
|
534
536
|
if (this.renamingId === entry.id) {
|
|
535
537
|
return html`<div class="row">
|
|
@@ -548,13 +550,13 @@ export class AssistantSessionManager extends LitElement {
|
|
|
548
550
|
}
|
|
549
551
|
|
|
550
552
|
return html`<div
|
|
551
|
-
class="row ${entry.unread ? 'unread' : ''} ${entry.hidden ? 'hidden' : ''} ${entry.id === this.sessionId ? 'current' : ''} ${
|
|
553
|
+
class="row ${entry.unread ? 'unread' : ''} ${entry.hidden ? 'hidden' : ''} ${entry.id === this.sessionId ? 'current' : ''} ${isOpen ? 'open-audience' : ''}"
|
|
552
554
|
>
|
|
553
555
|
<span class="dot"></span>
|
|
554
556
|
<button class="open-row" aria-current=${entry.id === this.sessionId ? 'true' : 'false'} @click=${() => this.emit('assistant-session-select', { sessionId: entry.id, kind: entry.kind })}>
|
|
555
557
|
<div class="title">${name}</div>
|
|
556
558
|
<div class="prev">${entry.lastMessagePreview || ''}</div>
|
|
557
|
-
<span class="meta"
|
|
559
|
+
<span class="meta">${this.renderAudience(entry.visibility)}${entry.lastMessageAt && Number.isFinite(Date.parse(entry.lastMessageAt)) ? html` · <time datetime=${entry.lastMessageAt}>${new Intl.DateTimeFormat(i18next.language || 'en', { month: 'short', day: 'numeric', hour: '2-digit', minute: '2-digit' }).format(new Date(entry.lastMessageAt))}</time>` : nothing}${entry.mine ? html` · ${t('filter-mine', 'Started by me')}` : nothing}</span>
|
|
558
560
|
</button>
|
|
559
561
|
<button
|
|
560
562
|
class="more-menu"
|
|
@@ -571,16 +573,21 @@ export class AssistantSessionManager extends LitElement {
|
|
|
571
573
|
private openRowMenu(event: MouseEvent, entry: AssistantSessionEntry, name: string) {
|
|
572
574
|
event.stopPropagation()
|
|
573
575
|
const actions = sessionActions(entry)
|
|
574
|
-
const
|
|
576
|
+
const current = entry.visibility || 'open'
|
|
575
577
|
const at = (event.currentTarget as HTMLElement).getBoundingClientRect()
|
|
576
578
|
OxPopupMenu.open({
|
|
577
579
|
template: html`
|
|
578
580
|
${actions.rename ? html`<ox-popup-menuitem label=${t('rename', 'Rename')} @select=${() => (this.renamingId = entry.id)}></ox-popup-menuitem>` : nothing}
|
|
579
|
-
${actions.setVisibility
|
|
580
|
-
?
|
|
581
|
-
? html`<ox-popup-menuitem label=${t('make-open', 'Make public')} @select=${() => this.askOpen(entry, name)}></ox-popup-menuitem>`
|
|
582
|
-
: html`<ox-popup-menuitem label=${t('make-private', 'Make private')} @select=${() => this.setVisibility(entry, 'private')}></ox-popup-menuitem>`
|
|
581
|
+
${actions.setVisibility && current !== 'open'
|
|
582
|
+
? html`<ox-popup-menuitem label=${t('make-open', 'Make public')} @select=${() => this.askOpen(entry, name)}></ox-popup-menuitem>`
|
|
583
583
|
: nothing}
|
|
584
|
+
${actions.setVisibility && current !== 'group'
|
|
585
|
+
? html`<ox-popup-menuitem label=${t('make-group', 'Make it a group')} @select=${() => this.setVisibility(entry, 'group')}></ox-popup-menuitem>`
|
|
586
|
+
: nothing}
|
|
587
|
+
${actions.setVisibility && current !== 'private'
|
|
588
|
+
? html`<ox-popup-menuitem label=${t('make-private', 'Make private')} @select=${() => this.askPrivate(entry, name)}></ox-popup-menuitem>`
|
|
589
|
+
: nothing}
|
|
590
|
+
${actions.leave ? html`<ox-popup-menuitem label=${t('leave', 'Leave')} @select=${() => this.askLeave(entry, name)}></ox-popup-menuitem>` : nothing}
|
|
584
591
|
<ox-popup-menuitem
|
|
585
592
|
label=${actions.unhide ? t('unhide', 'Show in my list') : t('hide', 'Hide from my list')}
|
|
586
593
|
@select=${() => this.setHidden(entry, !entry.hidden)}></ox-popup-menuitem>
|
|
@@ -620,4 +627,38 @@ export class AssistantSessionManager extends LitElement {
|
|
|
620
627
|
)
|
|
621
628
|
if (ok) this.setVisibility(entry, 'open')
|
|
622
629
|
}
|
|
630
|
+
|
|
631
|
+
/* Private is the owner's alone - the people in it leave, and coming back is being invited again. */
|
|
632
|
+
private async askPrivate(entry: AssistantSessionEntry, name: string) {
|
|
633
|
+
const ok = await this.ask(
|
|
634
|
+
t('make-private', 'Make private'),
|
|
635
|
+
t('confirm-private', 'Make "{name}" private? Only you will read it, and everyone else in it leaves.', { name }),
|
|
636
|
+
t('make-private', 'Make private')
|
|
637
|
+
)
|
|
638
|
+
if (ok) this.setVisibility(entry, 'private')
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
private async askLeave(entry: AssistantSessionEntry, name: string) {
|
|
642
|
+
const ok = await this.ask(
|
|
643
|
+
t('leave', 'Leave'),
|
|
644
|
+
entry.visibility === 'open'
|
|
645
|
+
? t('confirm-leave-open', 'Leave "{name}"? It stops coming to your list.', { name })
|
|
646
|
+
: t('confirm-leave', 'Leave "{name}"? You will not read it again unless its owner brings you back.', { name }),
|
|
647
|
+
t('leave', 'Leave')
|
|
648
|
+
)
|
|
649
|
+
if (ok) void this.act(entry, 'left', () => this.admin.leave(entry.id), t('leave-refused', 'you could not leave this conversation.'))
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
/** Who reads a conversation, on its row: the same three words wherever a conversation is listed. */
|
|
653
|
+
private renderAudience(visibility?: string) {
|
|
654
|
+
const v = visibility || 'open'
|
|
655
|
+
const icon = v === 'open' ? 'public' : v === 'group' ? 'group' : 'lock'
|
|
656
|
+
const label =
|
|
657
|
+
v === 'open'
|
|
658
|
+
? t('audience-open', 'Everyone in this domain')
|
|
659
|
+
: v === 'group'
|
|
660
|
+
? t('audience-group', 'Invited people')
|
|
661
|
+
: t('audience-private', 'Only me')
|
|
662
|
+
return html`<span class="audience ${v}"><md-icon aria-hidden="true">${icon}</md-icon>${label}</span>`
|
|
663
|
+
}
|
|
623
664
|
}
|
|
@@ -50,3 +50,13 @@ it('★ a stored answer brings its cards back after a refresh', () => {
|
|
|
50
50
|
it('the chat reads stored proposals from history and from the broadcast', () => {
|
|
51
51
|
expect(chat.split('proposalsJson').length - 1).toBeGreaterThanOrEqual(2)
|
|
52
52
|
})
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
it('a failed page-context handoff never sends the request into the previous conversation', () => {
|
|
56
|
+
const take = body(dock, 'private async takeAsk()')
|
|
57
|
+
expect(take.indexOf('if (!id) return')).toBeGreaterThan(take.indexOf('await this.startCarried(ask)'))
|
|
58
|
+
expect(take.indexOf('if (!id) return')).toBeLessThan(take.indexOf('this.asking ='))
|
|
59
|
+
const carry = body(dock, 'private async startCarried(')
|
|
60
|
+
expect(carry).toContain('if (created.errors?.length) throw')
|
|
61
|
+
expect(carry).toContain('if (attached.errors?.length) throw')
|
|
62
|
+
})
|
|
@@ -1,15 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
describe('roster identity
|
|
3
|
-
it('does not claim denial when participant identities are missing or still loading',()=>{
|
|
4
|
-
expect(rosterInviteState([{role:'owner'}],'me',true)).toBe('checking')
|
|
5
|
-
expect(rosterInviteState([], 'me',false)).toBe('checking')
|
|
6
|
-
expect(rosterInviteState([], undefined,true)).toBe('checking')
|
|
7
|
-
})
|
|
8
|
-
it('uses the current person role rather than another member or a stale loading state',()=>{
|
|
9
|
-
for(const role of ['owner','member'])expect(rosterInviteState([{role,user:{id:'me'}}],'me',true)).toBe('allowed')
|
|
10
|
-
expect(rosterInviteState([{role:'viewer',user:{id:'me'}},{role:'owner',user:{id:'other'}}],'me',true)).toBe('denied')
|
|
11
|
-
expect(rosterInviteState([{role:'member',user:{id:'other'}}],'me',true)).toBe('denied')
|
|
12
|
-
})
|
|
1
|
+
import { rosterName } from './roster-identity'
|
|
2
|
+
describe('roster identity',()=>{
|
|
13
3
|
it('shows a recognizable full name, then username, then an explicit unavailable label',()=>{
|
|
14
4
|
expect(rosterName({role:'member',user:{id:'u',name:' 정연희 '}},'Unavailable')).toBe('정연희')
|
|
15
5
|
expect(rosterName({role:'member',user:{id:'u',username:'yeonhee'}},'Unavailable')).toBe('yeonhee')
|
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
export interface RosterParticipant { role: string; user?: { id: string; name?: string; username?: string } }
|
|
2
|
-
/** Missing identity/loading is not evidence that a person lacks membership. Server gates remain authoritative. */
|
|
3
|
-
export function rosterInviteState(rows: readonly RosterParticipant[], userId: string | undefined, loaded: boolean): 'checking' | 'allowed' | 'denied' {
|
|
4
|
-
if (!loaded || !userId || rows.some(row => !row.user?.id)) return 'checking'
|
|
5
|
-
const role = rows.find(row => row.user?.id === userId)?.role
|
|
6
|
-
return role === 'owner' || role === 'member' ? 'allowed' : 'denied'
|
|
7
|
-
}
|
|
8
2
|
export function rosterName(person: RosterParticipant, fallback: string): string {
|
|
9
3
|
return person.user?.name?.trim() || person.user?.username?.trim() || fallback
|
|
10
4
|
}
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* hide/unhide anyone, for their own list only (reversible)
|
|
7
7
|
* delete the creator (`mine`) (not reversible from a screen)
|
|
8
8
|
* visibility the owner (`owner`) (opening shows what is already written)
|
|
9
|
+
* leave a participant who is not the owner (the owner deletes or makes it private instead)
|
|
9
10
|
*/
|
|
10
11
|
export function sessionActions(entry: { participant?: boolean; hidden?: boolean; mine?: boolean; owner?: boolean }) {
|
|
11
12
|
return {
|
|
@@ -13,6 +14,7 @@ export function sessionActions(entry: { participant?: boolean; hidden?: boolean;
|
|
|
13
14
|
hide: !entry.hidden,
|
|
14
15
|
unhide: !!entry.hidden,
|
|
15
16
|
remove: !!entry.mine,
|
|
16
|
-
setVisibility: !!entry.owner
|
|
17
|
+
setVisibility: !!entry.owner,
|
|
18
|
+
leave: !!entry.participant && !entry.owner
|
|
17
19
|
}
|
|
18
20
|
}
|
|
@@ -17,9 +17,11 @@ export { sessionActions } from './assistant-session-actions.js'
|
|
|
17
17
|
* hide/unhide anyone, for their own list only (reversible)
|
|
18
18
|
* delete the creator (`mine`) (not reversible from a screen)
|
|
19
19
|
* visibility the owner (`owner`) (opening shows what is already written)
|
|
20
|
+
* leave a member, never the owner (in a group, leaving ends reading here)
|
|
20
21
|
*/
|
|
21
22
|
|
|
22
|
-
|
|
23
|
+
/** Who reads it: everyone in the domain, the people the owner brought in, or the owner alone (ADR-0073 decision 3). */
|
|
24
|
+
export type SessionVisibility = 'open' | 'group' | 'private'
|
|
23
25
|
|
|
24
26
|
/** One row of `chatSessionInbox`, as the list draws it. */
|
|
25
27
|
export interface AssistantSessionEntry {
|
|
@@ -48,7 +50,7 @@ export interface AssistantSessionFilters {
|
|
|
48
50
|
/** A kind (mode) to list; empty lists every kind on the anchor. */
|
|
49
51
|
kind?: string
|
|
50
52
|
whose?: 'all' | 'mine' | 'participant'
|
|
51
|
-
visibility?: 'all' |
|
|
53
|
+
visibility?: 'all' | SessionVisibility
|
|
52
54
|
includeHidden?: boolean
|
|
53
55
|
unreadOnly?: boolean
|
|
54
56
|
}
|
|
@@ -77,6 +79,7 @@ export interface AssistantSessionAdmin {
|
|
|
77
79
|
hide(sessionId: string, hidden: boolean): Promise<boolean>
|
|
78
80
|
remove(sessionId: string): Promise<boolean>
|
|
79
81
|
setVisibility(sessionId: string, visibility: SessionVisibility): Promise<boolean>
|
|
82
|
+
leave(sessionId: string): Promise<boolean>
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
/** The page size the server allows at most (`chatSessionInbox` clamps to 200). */
|
|
@@ -163,5 +166,9 @@ export const assistantSessionAdmin: AssistantSessionAdmin = {
|
|
|
163
166
|
{ id: sessionId, v: visibility },
|
|
164
167
|
'setSessionVisibility'
|
|
165
168
|
)
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
leave(sessionId) {
|
|
172
|
+
return mutate(gql`mutation ($id: String!) { leaveAISession(sessionId: $id) }`, { id: sessionId }, 'leaveAISession')
|
|
166
173
|
}
|
|
167
174
|
}
|
|
@@ -11,7 +11,7 @@ export interface AssistantSession {
|
|
|
11
11
|
* 칸이 서기 전에 난 세션에는 값이 없다. 그때는 지금 대화로 두고, 서버가 다음 턴에 찍는다.
|
|
12
12
|
*/
|
|
13
13
|
kind?: string
|
|
14
|
-
/** Who reads it: 'open' (everyone in the domain) or 'private' (
|
|
14
|
+
/** Who reads it: 'open' (everyone in the domain), 'group' (the people the owner brought in) or 'private' (the owner alone). The dock draws an open one apart. */
|
|
15
15
|
visibility?: string
|
|
16
16
|
}
|
|
17
17
|
export interface AssistantSessionTarget {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { TYPING_SAY_EVERY_MS, TYPING_SHOW_MS, TypingRoom } from './assistant-typing'
|
|
2
|
+
|
|
3
|
+
describe('typing in a conversation', () => {
|
|
4
|
+
it('says it with words in the box, not on every key', () => {
|
|
5
|
+
const room = new TypingRoom()
|
|
6
|
+
expect(room.shouldSay('', 0)).toBe(false)
|
|
7
|
+
expect(room.shouldSay('h', 10_000)).toBe(true)
|
|
8
|
+
expect(room.shouldSay('he', 10_000 + TYPING_SAY_EVERY_MS - 1)).toBe(false)
|
|
9
|
+
expect(room.shouldSay('hel', 10_000 + TYPING_SAY_EVERY_MS)).toBe(true)
|
|
10
|
+
})
|
|
11
|
+
|
|
12
|
+
it('★ a notice fades without the next one', () => {
|
|
13
|
+
const room = new TypingRoom()
|
|
14
|
+
room.heard({ userId: 'u2', name: 'Kim' }, 0)
|
|
15
|
+
expect(room.typing(TYPING_SHOW_MS - 1)).toEqual(['Kim'])
|
|
16
|
+
expect(room.typing(TYPING_SHOW_MS)).toEqual([])
|
|
17
|
+
})
|
|
18
|
+
|
|
19
|
+
it('★ the message ends it at once', () => {
|
|
20
|
+
const room = new TypingRoom()
|
|
21
|
+
room.heard({ userId: 'u2', name: 'Kim' }, 0)
|
|
22
|
+
room.heard({ userId: 'u3', name: 'Lee' }, 0)
|
|
23
|
+
expect(room.spoke('u2')).toBe(true)
|
|
24
|
+
expect(room.typing(1)).toEqual(['Lee'])
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
it('a new notice from the same person keeps one line for them', () => {
|
|
28
|
+
const room = new TypingRoom()
|
|
29
|
+
room.heard({ userId: 'u2', name: 'Kim' }, 0)
|
|
30
|
+
room.heard({ userId: 'u2', name: 'Kim' }, TYPING_SHOW_MS - 1)
|
|
31
|
+
expect(room.typing(TYPING_SHOW_MS + 1)).toEqual(['Kim'])
|
|
32
|
+
})
|
|
33
|
+
})
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/*
|
|
2
|
+
* "Someone is typing" on the client - when to say it, and whose to show. Pure, so it is tested alone
|
|
3
|
+
* (`assistant-typing.test.ts`); the chat sends and receives, this keeps the times.
|
|
4
|
+
*
|
|
5
|
+
* Nothing is stored on the server, so a notice stands only until the next would have come: the typist says it
|
|
6
|
+
* every few seconds while the words change, the room lets it fade a little after the last one, and a message
|
|
7
|
+
* from that person ends it at once - the words arrived.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/** How often a typist says it while typing. */
|
|
11
|
+
export const TYPING_SAY_EVERY_MS = 3_000
|
|
12
|
+
|
|
13
|
+
/** How long a notice stands without the next - the server's `TYPING_NOTICE_MS`. */
|
|
14
|
+
export const TYPING_SHOW_MS = 6_000
|
|
15
|
+
|
|
16
|
+
export interface TypingNotice {
|
|
17
|
+
userId: string
|
|
18
|
+
name: string
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export class TypingRoom {
|
|
22
|
+
private saidAt = 0
|
|
23
|
+
private heardFrom = new Map<string, { name: string; until: number }>()
|
|
24
|
+
|
|
25
|
+
/** Should the typist say it now? Only with words in the box, and not more often than every few seconds. */
|
|
26
|
+
shouldSay(text: string, now = Date.now()): boolean {
|
|
27
|
+
if (!text.trim()) return false
|
|
28
|
+
if (now - this.saidAt < TYPING_SAY_EVERY_MS) return false
|
|
29
|
+
this.saidAt = now
|
|
30
|
+
return true
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
heard(notice: TypingNotice, now = Date.now()): void {
|
|
34
|
+
if (!notice?.userId) return
|
|
35
|
+
this.heardFrom.set(notice.userId, { name: notice.name, until: now + TYPING_SHOW_MS })
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** That person's message arrived - they are not typing it any more. */
|
|
39
|
+
spoke(userId: string | undefined): boolean {
|
|
40
|
+
return !!userId && this.heardFrom.delete(userId)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The names typing now, in the order they started; faded ones are dropped. */
|
|
44
|
+
typing(now = Date.now()): string[] {
|
|
45
|
+
for (const [id, one] of this.heardFrom) if (one.until <= now) this.heardFrom.delete(id)
|
|
46
|
+
return [...this.heardFrom.values()].map(one => one.name)
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
clear(): void {
|
|
50
|
+
this.saidAt = 0
|
|
51
|
+
this.heardFrom.clear()
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import './assistant-invite.js';
|
|
1
2
|
/**
|
|
2
3
|
* <ox-assistant-chat> — AI 도우미 채팅 컴포넌트 (Lit).
|
|
3
4
|
*
|
|
@@ -41,6 +42,8 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
41
42
|
/** 영속 세션 id. 없으면 ad-hoc (메시지 영속 안 됨). */
|
|
42
43
|
sessionId?: string;
|
|
43
44
|
prepareSession?: (adopt: (id: string) => void) => Promise<string | undefined>;
|
|
45
|
+
/** Who reads this conversation - 'open' · 'group' · 'private'. The host passes what its list says; the roster follows it. */
|
|
46
|
+
visibility?: string;
|
|
44
47
|
private adoptingSession;
|
|
45
48
|
/**
|
|
46
49
|
* 보드의 모든 ChatSession 리스트 — 다중 세션 활성화 시 탭 UI 로 표시.
|
|
@@ -310,6 +313,15 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
310
313
|
/** C4 presence/roster — 세션 참여자 목록. lastSeenAt 으로 online 판정. */
|
|
311
314
|
private participants;
|
|
312
315
|
private rosterLoaded;
|
|
316
|
+
/** May I bring someone in here - the server's answer (`mayInviteToAISession`); unknown until the roster is read. */
|
|
317
|
+
private mayInvite?;
|
|
318
|
+
/** The name box under the roster is open. */
|
|
319
|
+
private inviting;
|
|
320
|
+
/** Who is typing now, by name - kept by `typingRoom`, redrawn on each notice and as they fade. */
|
|
321
|
+
private typingNames;
|
|
322
|
+
private typingRoom;
|
|
323
|
+
private _typingSub?;
|
|
324
|
+
private _typingTimer?;
|
|
313
325
|
/** roster online 재평가 트리거 — heartbeat tick 마다 갱신(시간 경과 반영). */
|
|
314
326
|
private _presenceTick;
|
|
315
327
|
private _heartbeatTimer?;
|
|
@@ -390,6 +402,9 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
390
402
|
private _startPresence;
|
|
391
403
|
private _stopPresence;
|
|
392
404
|
private _startMessageSubscription;
|
|
405
|
+
/** The room's "typing" line - notices of the others, which fade by themselves (`TypingRoom`). */
|
|
406
|
+
private _startTypingSubscription;
|
|
407
|
+
private _refreshTyping;
|
|
393
408
|
private _stopMessageSubscription;
|
|
394
409
|
/** 서버 ChatMessage → ChatLine. */
|
|
395
410
|
private _msgToLine;
|
|
@@ -494,7 +509,20 @@ export declare class OxAssistantChat extends LitElement {
|
|
|
494
509
|
private renderAgentPicker;
|
|
495
510
|
private renderSessionTabs;
|
|
496
511
|
/** C4 roster — 세션 참여자 + online 표시. 참여자 2명 미만이면 숨김(혼자면 의미 없음). */
|
|
512
|
+
/**
|
|
513
|
+
* Who is in the room. A group shows it always - its people are the point of it, and the owner brings them in
|
|
514
|
+
* here; any other room once someone else has joined. The buttons follow what the server said this person may
|
|
515
|
+
* do (`mayInviteToAISession`, the owner's role); the doors decide again.
|
|
516
|
+
*/
|
|
497
517
|
private renderRoster;
|
|
518
|
+
/** Bring someone in - the door seats them and tells them (`inviteToAISession`). */
|
|
519
|
+
private _invite;
|
|
520
|
+
/** Send a member out - in a group they no longer read or write here. Bringing them back is inviting them again. */
|
|
521
|
+
private _removeParticipant;
|
|
522
|
+
/** Tell the others I am typing - only where someone else could hear it, and not on every key (`TypingRoom`). */
|
|
523
|
+
private _sayTyping;
|
|
524
|
+
/** "Kim is typing…" - over the box, while their notices keep coming. */
|
|
525
|
+
private renderTyping;
|
|
498
526
|
private _onSessionTabClick;
|
|
499
527
|
private _onSessionCreateClick;
|
|
500
528
|
private renderEmpty;
|