@clowder-ai/companion 0.1.0-alpha.12 → 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.
- package/README.md +9 -0
- package/manifest.json +2 -2
- package/package.json +5 -3
- package/plugin.yaml +2 -2
- package/renderer/call-transcript-view.mjs +238 -0
- package/renderer/client.mjs +6 -1
- package/renderer/companion-identity.mjs +4 -3
- package/renderer/conversation.mjs +127 -23
- package/renderer/decision-view.mjs +6 -8
- package/renderer/errors.mjs +4 -4
- package/renderer/index.html +105 -22
- package/renderer/native-work-motion.mjs +15 -0
- package/renderer/peer.mjs +1 -1
- package/renderer/pet-controls.mjs +71 -25
- package/renderer/pet-motion.mjs +17 -23
- package/renderer/recent-bubble.mjs +16 -3
- package/renderer/settings-controller.mjs +171 -0
- package/renderer/settings-view.mjs +234 -0
- package/renderer/style.css +136 -20
- package/renderer/surface.mjs +161 -54
- package/renderer/transcript-view.mjs +19 -1
package/renderer/surface.mjs
CHANGED
|
@@ -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 => {
|
|
20
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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,15 +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 => {
|
|
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'); }
|
|
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'); }
|
|
57
74
|
if (kind === 'stop') void conversation.end();
|
|
58
75
|
if (kind === 'mute') conversation.muteMic();
|
|
59
76
|
if (kind === 'speaker') conversation.muteSpeaker();
|
|
60
77
|
if (kind === 'share') toggleScreen();
|
|
61
78
|
if (kind === 'documents') void conversation.documents(conversation.identity?.documentsAllowed === false);
|
|
62
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
|
+
}
|
|
63
86
|
if (kind === 'play' && conversation.identity?.skin === 'xianxian-codex') {
|
|
64
87
|
void controls.show('none');
|
|
65
88
|
motion.signal('play');
|
|
@@ -73,14 +96,21 @@ if (!window.clowderCompanion) {
|
|
|
73
96
|
...frame, frameId: crypto.randomUUID(), sourceLabel: '所选画面', observedAt: Date.now(),
|
|
74
97
|
}), screenStop: () => client.screenClose(),
|
|
75
98
|
}, value => {
|
|
99
|
+
const wasActive = sharing || pendingScreen;
|
|
76
100
|
sharing = value.sharing; pendingScreen = value.pending === true;
|
|
77
|
-
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
|
+
}
|
|
78
106
|
$('share-badge').hidden = !sharing && !pendingScreen;
|
|
79
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);
|
|
80
110
|
});
|
|
81
111
|
function toggleScreen() {
|
|
82
112
|
if (conversation.phase !== 'talking') {
|
|
83
|
-
status('
|
|
113
|
+
status('先开始语音通话,连上后可选择窗口或屏幕');
|
|
84
114
|
$('status').hidden = false;
|
|
85
115
|
void controls.show('actions');
|
|
86
116
|
return;
|
|
@@ -90,35 +120,79 @@ if (!window.clowderCompanion) {
|
|
|
90
120
|
const conversation = new CompanionConversation({
|
|
91
121
|
client, createPeer: callback => new VoicePeer(callback, client), stopScreen: () => screen.stop(),
|
|
92
122
|
transcript: event => {
|
|
93
|
-
if (event.type === 'turn-done') {
|
|
94
|
-
|
|
95
|
-
|
|
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
|
+
}
|
|
96
136
|
},
|
|
97
137
|
render(value) {
|
|
98
138
|
const showFailure = value.failed && previousPhase !== 'idle' && value.phase === 'idle';
|
|
99
139
|
const justConnected = previousPhase !== 'talking' && value.phase === 'talking';
|
|
100
|
-
if (previousPhase !== 'idle' && value.phase === 'idle') {
|
|
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
|
+
}
|
|
101
144
|
previousPhase = value.phase;
|
|
102
145
|
const active = value.phase !== 'idle';
|
|
103
|
-
|
|
146
|
+
const receiveOnly = value.audioMode === 'receive_only';
|
|
147
|
+
const canReceiveOnly = value.identity?.audio?.supportedModes?.includes('receive_only') === true;
|
|
148
|
+
const retryReceiveOnly = value.failed && value.retryMode === 'receive_only' && canReceiveOnly;
|
|
149
|
+
callTranscript.setCall(value.callId);
|
|
150
|
+
void controls.setAmbient(ambientPanel(active));
|
|
104
151
|
$('begin').hidden = active; $('begin').disabled = !value.identity;
|
|
105
|
-
|
|
106
|
-
|
|
152
|
+
$('listen').hidden = active || !canReceiveOnly; $('listen').disabled = !value.identity;
|
|
153
|
+
label('begin', value.failed && !retryReceiveOnly ? '重试语音通话' : '语音通话');
|
|
154
|
+
label('listen', retryReceiveOnly ? '重试只听' : '只听');
|
|
155
|
+
$('begin').className = retryReceiveOnly ? '' : 'primary';
|
|
156
|
+
$('listen').className = retryReceiveOnly ? 'primary' : '';
|
|
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;
|
|
107
161
|
$('active-actions').hidden = !active;
|
|
108
162
|
$('share').disabled = value.phase !== 'talking';
|
|
109
163
|
$('compose').querySelector('button').disabled = !value.identity || value.phase === 'connecting';
|
|
110
164
|
status(value.message ?? '');
|
|
111
|
-
$('status').hidden = !value.message || /^(
|
|
112
|
-
$('
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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);
|
|
116
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');
|
|
117
184
|
label('speaker', value.silent ? '开启播音' : '关闭播音');
|
|
118
185
|
const identity = value.identity;
|
|
119
186
|
const companionIdentity = currentCompanionIdentity(identity);
|
|
120
|
-
|
|
121
|
-
|
|
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);
|
|
192
|
+
motion.setContext({ skin: identity?.skin, phase: value.phase,
|
|
193
|
+
nativeActivity: identity?.nativeActivity ?? 'none', muted: value.muted || receiveOnly });
|
|
194
|
+
motion.setBehaviorEnabled(identity?.behaviorEnabled === true);
|
|
195
|
+
motion.syncNativeSnapshot(nativeSnapshot);
|
|
122
196
|
if (justConnected) motion.signal('connected');
|
|
123
197
|
if (showFailure) motion.signal('failed');
|
|
124
198
|
if (identity) {
|
|
@@ -126,7 +200,8 @@ if (!window.clowderCompanion) {
|
|
|
126
200
|
const companionLabel = companionIdentity
|
|
127
201
|
? `${companionIdentity.title} · ${companionIdentity.partnerLabel}` : identity.displayName;
|
|
128
202
|
$('chat-name').textContent = threadTitle ?? companionLabel; $('menu-name').textContent = companionLabel;
|
|
129
|
-
$('
|
|
203
|
+
$('call-message').placeholder = `发消息给${identity.displayName}…`;
|
|
204
|
+
$('pet').setAttribute('aria-label', `${companionLabel}:点击或右键打开名片,拖动移动`);
|
|
130
205
|
$('documents').querySelector('.menu-status').textContent = identity.documentsAllowed ? '开启' : '暂停';
|
|
131
206
|
$('documents').setAttribute('aria-pressed', String(identity.documentsAllowed));
|
|
132
207
|
if (companionIdentity) {
|
|
@@ -139,7 +214,24 @@ if (!window.clowderCompanion) {
|
|
|
139
214
|
: `${identity.duty.displayName} · ${live} · 绑定载体 ${identity.carrier.displayName}`;
|
|
140
215
|
}
|
|
141
216
|
}
|
|
142
|
-
if (showFailure) void controls.show('
|
|
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);
|
|
143
235
|
},
|
|
144
236
|
});
|
|
145
237
|
async function readHistory() {
|
|
@@ -147,39 +239,49 @@ if (!window.clowderCompanion) {
|
|
|
147
239
|
loading = true;
|
|
148
240
|
try {
|
|
149
241
|
const history = await client.readConversation();
|
|
150
|
-
|
|
151
|
-
if (observedHistory && assistantId && assistantId !== lastAssistantId) {
|
|
152
|
-
resultPreviewUntil = Date.now() + 12_000;
|
|
153
|
-
}
|
|
154
|
-
observedHistory = true;
|
|
155
|
-
lastAssistantId = assistantId;
|
|
156
|
-
$('history').textContent = history.hasMore ? '更早聊天 ↗' : '完整聊天 ↗';
|
|
242
|
+
$('history').textContent = history.hasMore ? '在 Cat Café 中查看更早聊天 ↗' : '在 Cat Café 中查看完整聊天 ↗';
|
|
157
243
|
threadTitle = history.threadTitle;
|
|
158
244
|
$('chat-name').textContent = threadTitle;
|
|
159
245
|
bubble.load(history.messages);
|
|
160
|
-
void controls.setAmbient(
|
|
246
|
+
void controls.setAmbient(ambientPanel(conversation.phase !== 'idle'));
|
|
161
247
|
const serialized = JSON.stringify(history.messages);
|
|
162
248
|
if (serialized !== latestHistory) { latestHistory = serialized; transcript.load(history.messages); }
|
|
163
249
|
} catch { $('chat-status').textContent = '聊天记录暂未更新 · 正在说的话仍会显示'; }
|
|
164
250
|
finally { loading = false; }
|
|
165
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
|
+
}
|
|
166
266
|
function showDecisionBadge(page) {
|
|
267
|
+
latestDecisionPage = page;
|
|
167
268
|
const badge = decisionBadge(page);
|
|
168
|
-
$('pending-badge').hidden = !badge.visible;
|
|
269
|
+
$('pending-badge').hidden = !badge.visible || settingsValues?.proactivePolicy !== 'quiet-badge';
|
|
169
270
|
$('pending-count').textContent = badge.label;
|
|
170
271
|
$('pending-badge').title = badge.title;
|
|
171
272
|
$('pending-badge').setAttribute('aria-label', badge.title);
|
|
172
273
|
$('menu-pending').textContent = badge.label;
|
|
274
|
+
$('menu-pending').dataset.state = badge.state;
|
|
173
275
|
}
|
|
174
276
|
async function readDecisions(more = false) {
|
|
175
277
|
if (decisionLoading) return;
|
|
176
278
|
decisionLoading = true;
|
|
177
|
-
if (!more && controls.panel === 'decisions') $('decision-status').textContent = '
|
|
279
|
+
if (!more && controls.panel === 'decisions') $('decision-status').textContent = '正在读取待办…';
|
|
178
280
|
try {
|
|
179
281
|
const page = await client.readDecisions(more ? decisionOffset : 0, 10);
|
|
180
282
|
if (page.status !== 'available') throw new Error('Decision source unavailable');
|
|
181
283
|
showDecisionBadge(page);
|
|
182
|
-
motion.setPendingDecision(page.
|
|
284
|
+
motion.setPendingDecision((page.approvals?.length ?? 0) + (page.otherNeedsMe?.length ?? 0) > 0);
|
|
183
285
|
if (controls.panel !== 'decisions') return;
|
|
184
286
|
const list = $('decision-list');
|
|
185
287
|
if (!more) { list.replaceChildren(); list.scrollTop = 0; }
|
|
@@ -206,17 +308,18 @@ if (!window.clowderCompanion) {
|
|
|
206
308
|
}
|
|
207
309
|
}
|
|
208
310
|
decisionOffset = page.page.offset + page.page.limit;
|
|
209
|
-
|
|
311
|
+
const badge = decisionBadge(page);
|
|
312
|
+
$('decision-reload').textContent = '刷新';
|
|
313
|
+
$('decision-status').textContent = badge.state === 'empty'
|
|
210
314
|
? '现在没有待你处理的事项'
|
|
211
|
-
:
|
|
315
|
+
: '已显示读取到的待办 · 暂无总数';
|
|
212
316
|
$('decision-more').hidden = !page.page.hasMoreApprovals && !page.page.hasMoreNeedsMe;
|
|
213
317
|
} catch {
|
|
214
318
|
showDecisionBadge(undefined);
|
|
215
319
|
motion.setPendingDecision(null);
|
|
216
|
-
if (controls.panel === 'decisions') $('decision-status').textContent = '
|
|
320
|
+
if (controls.panel === 'decisions') $('decision-status').textContent = '待办暂不可读 · 请稍后刷新';
|
|
217
321
|
} finally { decisionLoading = false; }
|
|
218
322
|
}
|
|
219
|
-
$('call-badge').onclick = () => void conversation.end();
|
|
220
323
|
$('share-badge').onclick = toggleScreen;
|
|
221
324
|
$('pending-badge').onclick = () => void controls.show('decisions');
|
|
222
325
|
$('decision-reload').onclick = () => void readDecisions();
|
|
@@ -224,24 +327,28 @@ if (!window.clowderCompanion) {
|
|
|
224
327
|
$('history').onclick = () => void client.openConversation().then(result => {
|
|
225
328
|
if (result.delivery === 'unconfirmed') status('打开聊天尚未确认 · 请从 Clowder 查看');
|
|
226
329
|
}).catch(error => status(explainError(error)));
|
|
227
|
-
|
|
228
|
-
event.preventDefault(); const
|
|
330
|
+
async function submitMessage(event, inputId) {
|
|
331
|
+
event.preventDefault(); const input = $(inputId); const text = input.value;
|
|
229
332
|
if (await conversation.send(text)) {
|
|
230
333
|
motion.signal('received');
|
|
231
|
-
if (
|
|
334
|
+
if (input.value === text) input.value = '';
|
|
232
335
|
latestHistory = undefined; void readHistory();
|
|
233
336
|
}
|
|
234
|
-
}
|
|
337
|
+
}
|
|
338
|
+
$('compose').onsubmit = event => submitMessage(event, 'message');
|
|
339
|
+
$('call-compose').onsubmit = event => submitMessage(event, 'call-message');
|
|
340
|
+
$('call-unread').onclick = () => callTranscript.scrollToBottom();
|
|
235
341
|
const unsubscribe = client.subscribe(event => {
|
|
236
342
|
if (event.kind === 'media-stopped') void conversation.hostStopped(event.reason);
|
|
237
343
|
if (event.kind === 'view-dismiss') controls.dismiss();
|
|
238
344
|
});
|
|
239
345
|
const statusMonitor = setInterval(() => void conversation.refresh(), 1000);
|
|
240
346
|
const historyMonitor = setInterval(() => void readHistory(), 3000);
|
|
347
|
+
const transcriptMonitor = setInterval(() => void readCallTranscript(), 1000);
|
|
241
348
|
const decisionMonitor = setInterval(() => { if (controls.panel !== 'decisions') void readDecisions(); }, 15000);
|
|
242
|
-
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');
|
|
243
350
|
window.addEventListener('beforeunload', () => {
|
|
244
|
-
clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(decisionMonitor);
|
|
351
|
+
clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(transcriptMonitor); clearInterval(decisionMonitor);
|
|
245
352
|
unsubscribe(); nativeTravel.close(); motion.close(); void conversation.end();
|
|
246
353
|
});
|
|
247
354
|
}
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { historicalCompanionIdentity } from './companion-identity.mjs';
|
|
2
|
+
|
|
1
3
|
export class TranscriptView {
|
|
2
4
|
constructor(container) {
|
|
3
5
|
this.container = container;
|
|
@@ -57,7 +59,23 @@ export class TranscriptView {
|
|
|
57
59
|
const row = previous.get(message.id) ?? this.container.ownerDocument.createElement('p');
|
|
58
60
|
row.className = message.role; row.dataset.messageId = message.id;
|
|
59
61
|
if (row.textContent !== message.text) row.textContent = message.text;
|
|
60
|
-
|
|
62
|
+
const identity = historicalCompanionIdentity(message.companionIdentity, message.name);
|
|
63
|
+
row.dataset.author = identity.authorLabel;
|
|
64
|
+
if (identity.companionKnown) {
|
|
65
|
+
row.dataset.companion = identity.partnerLabel;
|
|
66
|
+
row.dataset.companionLive = identity.liveLabel;
|
|
67
|
+
row.dataset.companionDeep = identity.deepLabel;
|
|
68
|
+
row.dataset.companionAvatar = identity.avatarCatId;
|
|
69
|
+
row.dataset.companionSkin = identity.skin;
|
|
70
|
+
row.title = [identity.authorLabel, identity.partnerLabel, identity.liveLabel, identity.deepLabel].join('\n');
|
|
71
|
+
row.setAttribute('aria-label', [identity.authorLabel, message.text,
|
|
72
|
+
identity.partnerLabel, identity.liveLabel, identity.deepLabel].join('。'));
|
|
73
|
+
} else {
|
|
74
|
+
for (const key of ['companion', 'companionLive', 'companionDeep', 'companionAvatar', 'companionSkin'])
|
|
75
|
+
delete row.dataset[key];
|
|
76
|
+
row.title = identity.authorLabel;
|
|
77
|
+
row.removeAttribute?.('aria-label');
|
|
78
|
+
}
|
|
61
79
|
this.historyRows.set(message.id, row);
|
|
62
80
|
return row;
|
|
63
81
|
});
|