@clowder-ai/companion 0.1.0-alpha.13 → 0.1.0-alpha.14

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.
@@ -4,35 +4,46 @@ import { explainError } from './errors.mjs';
4
4
  import { VoicePeer } from './peer.mjs';
5
5
  import { ScreenShare } from './screen-share.mjs';
6
6
  import { TranscriptView } from './transcript-view.mjs';
7
+ import { CallTranscriptView } from './call-transcript-view.mjs';
8
+ import { SettingsView } from './settings-view.mjs';
7
9
  import { RecentBubble } from './recent-bubble.mjs';
8
10
  import { PetMotion } from './pet-motion.mjs';
9
11
  import { LivingBody } from './living-body.mjs';
10
12
  import { detectDockedEdge } from './living-edge.mjs';
11
13
  import { bindPetControls } from './pet-controls.mjs';
12
14
  import { decisionBadge, decisionRows } from './decision-view.mjs';
13
- import { normalizeNativeWork } from './native-work-motion.mjs';
15
+ import { nativeWorkLabel, normalizeNativeWork } from './native-work-motion.mjs';
14
16
  import { NativeWindowTravel } from './native-window-travel.mjs';
15
17
  import { currentCompanionIdentity } from './companion-identity.mjs';
16
-
17
18
  const $ = id => document.getElementById(id);
18
19
  const label = (id, text) => { $(id).querySelector('.label').textContent = text; };
19
- const status = text => { $('status').textContent = text; $('chat-status').textContent = text; $('announcement').textContent = text; };
20
- const transcript = new TranscriptView($('transcript'));
20
+ const status = text => {
21
+ $('status').textContent = text;
22
+ $('menu-status').textContent = text;
23
+ $('chat-status').textContent = text;
24
+ $('announcement').textContent = text;
25
+ };
26
+ const transcript = new TranscriptView($('chat-transcript'));
27
+ const callTranscript = new CallTranscriptView($('call-transcript-log'), {
28
+ onUnread: count => {
29
+ $('call-unread').hidden = count === 0;
30
+ $('call-unread').textContent = count ? `${count} 条新消息` : '';
31
+ },
32
+ });
21
33
  const bubble = new RecentBubble([$('bubble-first'), $('bubble-second')]);
34
+ const callBubble = new RecentBubble([$('call-bubble-first'), $('call-bubble-second')]);
22
35
  const motion = new PetMotion($('pet'), { livingBody: new LivingBody({ root: $('pet'), sit: $('living-sit'),
23
36
  video: $('living-video'), transitionVideo: $('living-video-transition') }) });
24
37
  const nativeTravel = new NativeWindowTravel({
25
38
  readPosition: () => ({ x: window.screenX, y: window.screenY }),
26
39
  changed: value => motion.syncTravel(value),
27
40
  });
28
- let sharing = false, pendingScreen = false, loading = false, latestHistory, previousPhase = 'idle';
29
- let observedHistory = false, lastAssistantId;
30
- let resultPreviewUntil = 0;
31
- let didMove = false;
32
- let decisionLoading = false, decisionOffset = 0;
33
- let threadTitle;
34
- const showAmbient = active => (active || Date.now() < resultPreviewUntil) && bubble.hasContent();
35
-
41
+ let sharing = false, pendingScreen = false, loading = false, transcriptLoading = false, latestHistory, previousPhase = 'idle';
42
+ let didMove = false, decisionLoading = false, decisionOffset = 0, threadTitle, settingsView, settingsValues, latestDecisionPage;
43
+ // F229 stays quiet: a history ID change has no provenance for proactive text.
44
+ const ambientPanel = active => active ? 'actions' : 'none';
45
+ const setTexts = (ids, text) => { for (const id of ids) $(id).textContent = text; };
46
+ const setHidden = (ids, hidden) => { for (const id of ids) $(id).hidden = hidden; };
36
47
  if (!window.clowderCompanion) {
37
48
  $('actions').hidden = false; $('status').hidden = false;
38
49
  status('请从 Clowder 的聊聊入口打开猫猫球');
@@ -41,7 +52,7 @@ if (!window.clowderCompanion) {
41
52
  nativeTravel.start();
42
53
  const client = createCompanionClient(window.clowderCompanion);
43
54
  const controls = bindPetControls(client, {
44
- error: error => status(explainError(error)),
55
+ error: (error, context) => status(context?.panel === 'settings' && error?.code === 'invalid_request' ? '这个 Host 版本还不支持猫旁设置。原来的设置入口会继续保留。' : explainError(error)),
45
56
  moved: (dx, dy) => {
46
57
  if (dx !== 'stop') { didMove = true; nativeTravel.setManual(true); motion.move(dx, dy); return; }
47
58
  nativeTravel.setManual(false);
@@ -51,16 +62,27 @@ if (!window.clowderCompanion) {
51
62
  void conversation.refresh();
52
63
  setTimeout(() => motion.setDockedEdge(detectDockedEdge(window, $('pet').getBoundingClientRect())), 100);
53
64
  },
54
- changed: panel => { if (panel === 'chat') void readHistory(); if (panel === 'decisions') void readDecisions(); },
65
+ changed: panel => {
66
+ if (panel === 'chat') void readHistory();
67
+ if (panel === 'transcript') void readCallTranscript();
68
+ if (panel === 'decisions') void readDecisions();
69
+ if (panel === 'settings') void settingsView?.open();
70
+ },
55
71
  action(kind) {
56
- if (kind === 'begin') { transcript.reset(); bubble.reset(); void conversation.begin(); void controls.show('none'); }
57
- if (kind === 'listen') { transcript.reset(); bubble.reset(); void conversation.begin('receive_only'); void controls.show('none'); }
72
+ if (kind === 'begin') { transcript.reset(); callTranscript.reset(); bubble.reset(); callBubble.clear(); void conversation.begin(); void controls.show('none'); }
73
+ if (kind === 'listen') { transcript.reset(); callTranscript.reset(); bubble.reset(); callBubble.clear(); void conversation.begin('receive_only'); void controls.show('none'); }
58
74
  if (kind === 'stop') void conversation.end();
59
75
  if (kind === 'mute') conversation.muteMic();
60
76
  if (kind === 'speaker') conversation.muteSpeaker();
61
77
  if (kind === 'share') toggleScreen();
62
78
  if (kind === 'documents') void conversation.documents(conversation.identity?.documentsAllowed === false);
63
79
  if (kind === 'hide') void client.hide().catch(error => status(explainError(error)));
80
+ if (kind === 'transcript-expand') {
81
+ const expanded = $('transcript').dataset.expanded !== 'true';
82
+ $('transcript').dataset.expanded = String(expanded);
83
+ setTexts(['transcript-expand-label'], expanded ? '收起' : '展开');
84
+ void controls.show('transcript');
85
+ }
64
86
  if (kind === 'play' && conversation.identity?.skin === 'xianxian-codex') {
65
87
  void controls.show('none');
66
88
  motion.signal('play');
@@ -74,14 +96,21 @@ if (!window.clowderCompanion) {
74
96
  ...frame, frameId: crypto.randomUUID(), sourceLabel: '所选画面', observedAt: Date.now(),
75
97
  }), screenStop: () => client.screenClose(),
76
98
  }, value => {
99
+ const wasActive = sharing || pendingScreen;
77
100
  sharing = value.sharing; pendingScreen = value.pending === true;
78
- for (const id of ['share', 'menu-share']) label(id, sharing ? '停止共享' : pendingScreen ? '取消选屏' : '共享画面');
101
+ for (const id of ['share', 'menu-share', 'chat-share', 'settings-share']) label(id, sharing ? '停止共享' : pendingScreen ? '取消选屏' : '共享画面');
102
+ for (const prefix of ['call', 'menu', 'chat', 'transcript', 'settings']) {
103
+ $(`${prefix}-share-context`).hidden = !sharing;
104
+ $(`${prefix}-share-target`).textContent = sharing ? `共享中:${value.label}` : '';
105
+ }
79
106
  $('share-badge').hidden = !sharing && !pendingScreen;
80
107
  $('share-badge').title = sharing ? `正在共享:${value.label},点此停止` : '正在选屏,点此取消';
108
+ $('share-badge').setAttribute('aria-label', sharing ? `停止共享:${value.label}` : '取消选屏');
109
+ if (value.message && value.message !== '未共享屏幕' && (wasActive || pendingScreen)) status(value.message);
81
110
  });
82
111
  function toggleScreen() {
83
112
  if (conversation.phase !== 'talking') {
84
- status('先点语音聊,连上后可选择窗口或屏幕');
113
+ status('先开始语音通话,连上后可选择窗口或屏幕');
85
114
  $('status').hidden = false;
86
115
  void controls.show('actions');
87
116
  return;
@@ -91,46 +120,79 @@ if (!window.clowderCompanion) {
91
120
  const conversation = new CompanionConversation({
92
121
  client, createPeer: callback => new VoicePeer(callback, client), stopScreen: () => screen.stop(),
93
122
  transcript: event => {
94
- if (event.type === 'turn-done') { transcript.finish(event); bubble.finish(event.role); void readHistory(); }
95
- else if (event.typed) void readHistory();
96
- else { transcript.append(event.role, event.text, true); bubble.append(event.role, event.text); void controls.setAmbient(true); }
123
+ if (event.type === 'turn-done') {
124
+ transcript.finish(event); callTranscript.finish(event); bubble.finish(event.role); callBubble.finish(event.role);
125
+ void readHistory(); void readCallTranscript();
126
+ } else if (event.type === 'typed') {
127
+ callTranscript.appendTyped(event);
128
+ void readHistory(); void readCallTranscript();
129
+ } else {
130
+ transcript.append(event.role, event.text, true);
131
+ callTranscript.appendLive(event);
132
+ const speaker = event.role === 'assistant' ? conversation.identity?.displayName ?? '猫猫' : '语音';
133
+ bubble.append(event.role, event.text, speaker); callBubble.append(event.role, event.text, speaker);
134
+ void controls.setAmbient('actions');
135
+ }
97
136
  },
98
137
  render(value) {
99
138
  const showFailure = value.failed && previousPhase !== 'idle' && value.phase === 'idle';
100
139
  const justConnected = previousPhase !== 'talking' && value.phase === 'talking';
101
- if (previousPhase !== 'idle' && value.phase === 'idle') { transcript.reset(); bubble.reset(); void readHistory(); }
140
+ if (previousPhase !== 'idle' && value.phase === 'idle') {
141
+ transcript.reset(); callTranscript.reset(); bubble.reset(); callBubble.clear(); void readHistory();
142
+ if (controls.panel === 'transcript') void controls.show('menu');
143
+ }
102
144
  previousPhase = value.phase;
103
145
  const active = value.phase !== 'idle';
104
146
  const receiveOnly = value.audioMode === 'receive_only';
105
147
  const canReceiveOnly = value.identity?.audio?.supportedModes?.includes('receive_only') === true;
106
148
  const retryReceiveOnly = value.failed && value.retryMode === 'receive_only' && canReceiveOnly;
107
- void controls.setAmbient(showAmbient(active));
149
+ callTranscript.setCall(value.callId);
150
+ void controls.setAmbient(ambientPanel(active));
108
151
  $('begin').hidden = active; $('begin').disabled = !value.identity;
109
152
  $('listen').hidden = active || !canReceiveOnly; $('listen').disabled = !value.identity;
110
- label('begin', value.failed && !retryReceiveOnly ? '重试语音' : '语音聊');
153
+ label('begin', value.failed && !retryReceiveOnly ? '重试语音通话' : '语音通话');
111
154
  label('listen', retryReceiveOnly ? '重试只听' : '只听');
112
155
  $('begin').className = retryReceiveOnly ? '' : 'primary';
113
156
  $('listen').className = retryReceiveOnly ? 'primary' : '';
114
157
  $('mic').hidden = value.phase !== 'talking' || receiveOnly; $('speaker').hidden = !active;
158
+ $('chat-mic').hidden = value.phase !== 'talking' || receiveOnly;
159
+ $('transcript-mic').hidden = value.phase !== 'talking' || receiveOnly;
160
+ $('settings-mic').hidden = value.phase !== 'talking' || receiveOnly;
115
161
  $('active-actions').hidden = !active;
116
162
  $('share').disabled = value.phase !== 'talking';
117
163
  $('compose').querySelector('button').disabled = !value.identity || value.phase === 'connecting';
118
164
  status(value.message ?? '');
119
- $('status').hidden = !value.message || /^(点|正在听|只听模式|只听已结束|语音已结束|已发送)/.test(value.message);
120
- $('call-badge').hidden = !active;
121
- $('call-badge').dataset.state = receiveOnly ? 'receive-only' : value.muted ? 'muted' : value.phase;
122
- $('call-badge-icon').setAttribute('href', receiveOnly ? '#i-listen' : '#i-mic');
123
- const callLabel = value.phase === 'connecting' ? (receiveOnly ? '正在连接只听,麦克风未启用,点此取消' : '正在连接,点此取消')
124
- : receiveOnly ? '只听进行中,麦克风未启用,点此结束'
125
- : value.muted ? '麦克风已静音,点此结束' : '语音进行中,点此结束';
126
- $('call-badge').title = callLabel; $('call-badge').setAttribute('aria-label', callLabel);
165
+ $('status').hidden = !value.message || /^(点击|正在听|只听模式|只听已结束|语音通话已结束|已发送)/.test(value.message);
166
+ $('menu-status').hidden = !value.message;
167
+ const callState = value.recovering ? '连接不稳定' : value.phase === 'connecting' ? '正在连接'
168
+ : receiveOnly ? '只听中' : active ? '通话中' : '';
169
+ const microphoneState = receiveOnly ? '麦克风未启用'
170
+ : value.phase === 'connecting' ? '正在请求麦克风'
171
+ : value.recovering ? `正在等待原连接恢复 · ${value.muted ? '麦克风已静音' : '麦克风已开启'}`
172
+ : value.muted ? '麦克风已静音' : active ? '麦克风已开启' : '';
173
+ for (const id of ['actions', 'chat-call-context', 'transcript-call-context', 'menu-call-context', 'settings-call-context']) $(id).dataset.connectionState = value.recovering ? 'recovering' : active ? 'connected' : 'idle';
174
+ setTexts(['call-state', 'chat-call-state', 'transcript-call-state', 'menu-call-state', 'settings-call-state'], callState);
175
+ setTexts(['call-mic-state', 'chat-call-mic-state', 'transcript-call-mic-state', 'menu-call-mic-state', 'settings-call-mic-state'], microphoneState);
176
+ setHidden(['chat-call-context', 'transcript-call-context', 'menu-call-context', 'settings-call-context'], !active);
127
177
  label('mic', value.muted ? '取消静音' : '静音'); $('mic').setAttribute('aria-pressed', String(value.muted));
178
+ $('mic-icon').setAttribute('href', value.muted ? '#i-mic-off' : '#i-mic');
179
+ $('chat-mic').textContent = value.muted ? '取消静音' : '静音';
180
+ $('settings-mic').textContent = value.muted ? '取消静音' : '静音';
181
+ $('transcript-mic').setAttribute('aria-label', value.muted ? '取消静音' : '静音');
182
+ $('transcript-mic').setAttribute('aria-pressed', String(value.muted));
183
+ $('transcript-mic-icon').setAttribute('href', value.muted ? '#i-mic-off' : '#i-mic');
128
184
  label('speaker', value.silent ? '开启播音' : '关闭播音');
129
185
  const identity = value.identity;
130
186
  const companionIdentity = currentCompanionIdentity(identity);
187
+ const nativeSnapshot = normalizeNativeWork(identity?.nativeWork);
188
+ const workLabel = nativeWorkLabel(nativeSnapshot, identity?.nativeActivity);
189
+ const workState = workLabel ? `${identity.displayName}${workLabel}` : '';
190
+ setTexts(['call-work', 'chat-call-work', 'transcript-call-work', 'menu-call-work', 'settings-call-work'], workState);
191
+ setHidden(['call-work', 'chat-call-work', 'transcript-call-work', 'menu-call-work', 'settings-call-work'], !workState);
131
192
  motion.setContext({ skin: identity?.skin, phase: value.phase,
132
193
  nativeActivity: identity?.nativeActivity ?? 'none', muted: value.muted || receiveOnly });
133
- motion.syncNativeSnapshot(normalizeNativeWork(identity?.nativeWork));
194
+ motion.setBehaviorEnabled(identity?.behaviorEnabled === true);
195
+ motion.syncNativeSnapshot(nativeSnapshot);
134
196
  if (justConnected) motion.signal('connected');
135
197
  if (showFailure) motion.signal('failed');
136
198
  if (identity) {
@@ -138,7 +200,8 @@ if (!window.clowderCompanion) {
138
200
  const companionLabel = companionIdentity
139
201
  ? `${companionIdentity.title} · ${companionIdentity.partnerLabel}` : identity.displayName;
140
202
  $('chat-name').textContent = threadTitle ?? companionLabel; $('menu-name').textContent = companionLabel;
141
- $('pet').setAttribute('aria-label', `${companionLabel}:点击交流,右键更多,拖动移动`);
203
+ $('call-message').placeholder = `发消息给${identity.displayName}…`;
204
+ $('pet').setAttribute('aria-label', `${companionLabel}:点击或右键打开名片,拖动移动`);
142
205
  $('documents').querySelector('.menu-status').textContent = identity.documentsAllowed ? '开启' : '暂停';
143
206
  $('documents').setAttribute('aria-pressed', String(identity.documentsAllowed));
144
207
  if (companionIdentity) {
@@ -151,7 +214,24 @@ if (!window.clowderCompanion) {
151
214
  : `${identity.duty.displayName} · ${live} · 绑定载体 ${identity.carrier.displayName}`;
152
215
  }
153
216
  }
154
- if (showFailure) void controls.show('actions');
217
+ if (showFailure) void controls.show('menu');
218
+ },
219
+ });
220
+ settingsView = new SettingsView({
221
+ document,
222
+ client,
223
+ close: () => void controls.show('menu'),
224
+ callActive: () => conversation.active,
225
+ expectHostStop: () => conversation.expectHostStop(),
226
+ settleExpectedHostStop: callStopped => conversation.settleExpectedHostStop(callStopped),
227
+ layoutChanged: () => { if (controls.panel === 'settings') controls.remeasure?.(); },
228
+ onValues(values) {
229
+ settingsValues = values;
230
+ document.documentElement?.style?.setProperty?.('--pet-scale', String(values.ballSize / 72));
231
+ $('anchor').dataset.ballSize = String(values.ballSize);
232
+ motion.setBehaviorEnabled(values.behaviorEnabled);
233
+ $('documents').hidden = true;
234
+ if (latestDecisionPage) showDecisionBadge(latestDecisionPage);
155
235
  },
156
236
  });
157
237
  async function readHistory() {
@@ -159,39 +239,49 @@ if (!window.clowderCompanion) {
159
239
  loading = true;
160
240
  try {
161
241
  const history = await client.readConversation();
162
- const assistantId = history.messages.findLast(message => message.role === 'assistant')?.id;
163
- if (observedHistory && assistantId && assistantId !== lastAssistantId) {
164
- resultPreviewUntil = Date.now() + 12_000;
165
- }
166
- observedHistory = true;
167
- lastAssistantId = assistantId;
168
- $('history').textContent = history.hasMore ? '更早聊天 ↗' : '完整聊天 ↗';
242
+ $('history').textContent = history.hasMore ? '在 Cat Café 中查看更早聊天 ↗' : '在 Cat Café 中查看完整聊天 ↗';
169
243
  threadTitle = history.threadTitle;
170
244
  $('chat-name').textContent = threadTitle;
171
245
  bubble.load(history.messages);
172
- void controls.setAmbient(showAmbient(conversation.phase !== 'idle'));
246
+ void controls.setAmbient(ambientPanel(conversation.phase !== 'idle'));
173
247
  const serialized = JSON.stringify(history.messages);
174
248
  if (serialized !== latestHistory) { latestHistory = serialized; transcript.load(history.messages); }
175
249
  } catch { $('chat-status').textContent = '聊天记录暂未更新 · 正在说的话仍会显示'; }
176
250
  finally { loading = false; }
177
251
  }
252
+ async function readCallTranscript() {
253
+ if (transcriptLoading || !conversation.active || !conversation.callId) return;
254
+ transcriptLoading = true;
255
+ const callId = conversation.callId;
256
+ try {
257
+ const reply = await client.readTranscript();
258
+ if (!conversation.active || conversation.callId !== callId) return;
259
+ callTranscript.load(reply);
260
+ callBubble.loadTranscript(reply.rows, conversation.identity?.displayName);
261
+ $('call-transcript-status').textContent = reply.hasMore ? '这里只显示这次通话最近的内容' : '';
262
+ } catch {
263
+ if (conversation.active && conversation.callId === callId) $('call-transcript-status').textContent = '字幕暂未更新 · 实时内容仍会继续显示';
264
+ } finally { transcriptLoading = false; }
265
+ }
178
266
  function showDecisionBadge(page) {
267
+ latestDecisionPage = page;
179
268
  const badge = decisionBadge(page);
180
- $('pending-badge').hidden = !badge.visible;
269
+ $('pending-badge').hidden = !badge.visible || settingsValues?.proactivePolicy !== 'quiet-badge';
181
270
  $('pending-count').textContent = badge.label;
182
271
  $('pending-badge').title = badge.title;
183
272
  $('pending-badge').setAttribute('aria-label', badge.title);
184
273
  $('menu-pending').textContent = badge.label;
274
+ $('menu-pending').dataset.state = badge.state;
185
275
  }
186
276
  async function readDecisions(more = false) {
187
277
  if (decisionLoading) return;
188
278
  decisionLoading = true;
189
- if (!more && controls.panel === 'decisions') $('decision-status').textContent = '正在读取待决事项…';
279
+ if (!more && controls.panel === 'decisions') $('decision-status').textContent = '正在读取待办…';
190
280
  try {
191
281
  const page = await client.readDecisions(more ? decisionOffset : 0, 10);
192
282
  if (page.status !== 'available') throw new Error('Decision source unavailable');
193
283
  showDecisionBadge(page);
194
- motion.setPendingDecision(page.approvalCount + page.otherNeedsMeCount > 0);
284
+ motion.setPendingDecision((page.approvals?.length ?? 0) + (page.otherNeedsMe?.length ?? 0) > 0);
195
285
  if (controls.panel !== 'decisions') return;
196
286
  const list = $('decision-list');
197
287
  if (!more) { list.replaceChildren(); list.scrollTop = 0; }
@@ -218,17 +308,18 @@ if (!window.clowderCompanion) {
218
308
  }
219
309
  }
220
310
  decisionOffset = page.page.offset + page.page.limit;
221
- $('decision-status').textContent = page.approvalCount + page.otherNeedsMeCount === 0
311
+ const badge = decisionBadge(page);
312
+ $('decision-reload').textContent = '刷新';
313
+ $('decision-status').textContent = badge.state === 'empty'
222
314
  ? '现在没有待你处理的事项'
223
- : `${page.approvalCount} 项审批事项,另有 ${page.otherNeedsMeCount} 项待处理`;
315
+ : '已显示读取到的待办 · 暂无总数';
224
316
  $('decision-more').hidden = !page.page.hasMoreApprovals && !page.page.hasMoreNeedsMe;
225
317
  } catch {
226
318
  showDecisionBadge(undefined);
227
319
  motion.setPendingDecision(null);
228
- if (controls.panel === 'decisions') $('decision-status').textContent = '待决事项暂不可读 · 请稍后刷新';
320
+ if (controls.panel === 'decisions') $('decision-status').textContent = '待办暂不可读 · 请稍后刷新';
229
321
  } finally { decisionLoading = false; }
230
322
  }
231
- $('call-badge').onclick = () => void conversation.end();
232
323
  $('share-badge').onclick = toggleScreen;
233
324
  $('pending-badge').onclick = () => void controls.show('decisions');
234
325
  $('decision-reload').onclick = () => void readDecisions();
@@ -236,24 +327,28 @@ if (!window.clowderCompanion) {
236
327
  $('history').onclick = () => void client.openConversation().then(result => {
237
328
  if (result.delivery === 'unconfirmed') status('打开聊天尚未确认 · 请从 Clowder 查看');
238
329
  }).catch(error => status(explainError(error)));
239
- $('compose').onsubmit = async event => {
240
- event.preventDefault(); const text = $('message').value;
330
+ async function submitMessage(event, inputId) {
331
+ event.preventDefault(); const input = $(inputId); const text = input.value;
241
332
  if (await conversation.send(text)) {
242
333
  motion.signal('received');
243
- if ($('message').value === text) $('message').value = '';
334
+ if (input.value === text) input.value = '';
244
335
  latestHistory = undefined; void readHistory();
245
336
  }
246
- };
337
+ }
338
+ $('compose').onsubmit = event => submitMessage(event, 'message');
339
+ $('call-compose').onsubmit = event => submitMessage(event, 'call-message');
340
+ $('call-unread').onclick = () => callTranscript.scrollToBottom();
247
341
  const unsubscribe = client.subscribe(event => {
248
342
  if (event.kind === 'media-stopped') void conversation.hostStopped(event.reason);
249
343
  if (event.kind === 'view-dismiss') controls.dismiss();
250
344
  });
251
345
  const statusMonitor = setInterval(() => void conversation.refresh(), 1000);
252
346
  const historyMonitor = setInterval(() => void readHistory(), 3000);
347
+ const transcriptMonitor = setInterval(() => void readCallTranscript(), 1000);
253
348
  const decisionMonitor = setInterval(() => { if (controls.panel !== 'decisions') void readDecisions(); }, 15000);
254
- void conversation.refresh(); void readHistory(); void readDecisions(); void controls.show('none');
349
+ void conversation.refresh(); void readHistory(); void readDecisions(); void settingsView.controller.load(); void controls.show('none');
255
350
  window.addEventListener('beforeunload', () => {
256
- clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(decisionMonitor);
351
+ clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(transcriptMonitor); clearInterval(decisionMonitor);
257
352
  unsubscribe(); nativeTravel.close(); motion.close(); void conversation.end();
258
353
  });
259
354
  }