@clowder-ai/companion 0.1.0-alpha.13 → 0.1.0-alpha.15
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/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 -0
- package/renderer/conversation.mjs +86 -16
- package/renderer/decision-view.mjs +111 -17
- package/renderer/errors.mjs +4 -4
- package/renderer/index.html +104 -23
- package/renderer/native-work-motion.mjs +15 -0
- 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 +135 -21
- package/renderer/surface.mjs +190 -64
package/renderer/surface.mjs
CHANGED
|
@@ -4,35 +4,50 @@ 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
|
-
import { decisionBadge, decisionRows } from './decision-view.mjs';
|
|
13
|
-
import { normalizeNativeWork } from './native-work-motion.mjs';
|
|
14
|
+
import { decisionBadge, decisionPresentation, decisionRows } from './decision-view.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
|
|
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; };
|
|
47
|
+
const decisionReadBasis = page => page?.version === 1
|
|
48
|
+
? [page.status, page.totalCount ?? 'unknown', ...['approvals', 'needsMe']
|
|
49
|
+
.flatMap(key => [page.sources?.[key]?.status, page.sources?.[key]?.coverage])].join('|')
|
|
50
|
+
: page ? 'legacy' : null;
|
|
36
51
|
if (!window.clowderCompanion) {
|
|
37
52
|
$('actions').hidden = false; $('status').hidden = false;
|
|
38
53
|
status('请从 Clowder 的聊聊入口打开猫猫球');
|
|
@@ -41,7 +56,7 @@ if (!window.clowderCompanion) {
|
|
|
41
56
|
nativeTravel.start();
|
|
42
57
|
const client = createCompanionClient(window.clowderCompanion);
|
|
43
58
|
const controls = bindPetControls(client, {
|
|
44
|
-
error: error => status(explainError(error)),
|
|
59
|
+
error: (error, context) => status(context?.panel === 'settings' && error?.code === 'invalid_request' ? '这个 Host 版本还不支持猫旁设置。原来的设置入口会继续保留。' : explainError(error)),
|
|
45
60
|
moved: (dx, dy) => {
|
|
46
61
|
if (dx !== 'stop') { didMove = true; nativeTravel.setManual(true); motion.move(dx, dy); return; }
|
|
47
62
|
nativeTravel.setManual(false);
|
|
@@ -51,16 +66,27 @@ if (!window.clowderCompanion) {
|
|
|
51
66
|
void conversation.refresh();
|
|
52
67
|
setTimeout(() => motion.setDockedEdge(detectDockedEdge(window, $('pet').getBoundingClientRect())), 100);
|
|
53
68
|
},
|
|
54
|
-
changed: panel => {
|
|
69
|
+
changed: panel => {
|
|
70
|
+
if (panel === 'chat') void readHistory();
|
|
71
|
+
if (panel === 'transcript') void readCallTranscript();
|
|
72
|
+
if (panel === 'decisions') void readDecisions();
|
|
73
|
+
if (panel === 'settings') void settingsView?.open();
|
|
74
|
+
},
|
|
55
75
|
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'); }
|
|
76
|
+
if (kind === 'begin') { transcript.reset(); callTranscript.reset(); bubble.reset(); callBubble.clear(); void conversation.begin(); void controls.show('none'); }
|
|
77
|
+
if (kind === 'listen') { transcript.reset(); callTranscript.reset(); bubble.reset(); callBubble.clear(); void conversation.begin('receive_only'); void controls.show('none'); }
|
|
58
78
|
if (kind === 'stop') void conversation.end();
|
|
59
79
|
if (kind === 'mute') conversation.muteMic();
|
|
60
80
|
if (kind === 'speaker') conversation.muteSpeaker();
|
|
61
81
|
if (kind === 'share') toggleScreen();
|
|
62
82
|
if (kind === 'documents') void conversation.documents(conversation.identity?.documentsAllowed === false);
|
|
63
83
|
if (kind === 'hide') void client.hide().catch(error => status(explainError(error)));
|
|
84
|
+
if (kind === 'transcript-expand') {
|
|
85
|
+
const expanded = $('transcript').dataset.expanded !== 'true';
|
|
86
|
+
$('transcript').dataset.expanded = String(expanded);
|
|
87
|
+
setTexts(['transcript-expand-label'], expanded ? '收起' : '展开');
|
|
88
|
+
void controls.show('transcript');
|
|
89
|
+
}
|
|
64
90
|
if (kind === 'play' && conversation.identity?.skin === 'xianxian-codex') {
|
|
65
91
|
void controls.show('none');
|
|
66
92
|
motion.signal('play');
|
|
@@ -74,14 +100,21 @@ if (!window.clowderCompanion) {
|
|
|
74
100
|
...frame, frameId: crypto.randomUUID(), sourceLabel: '所选画面', observedAt: Date.now(),
|
|
75
101
|
}), screenStop: () => client.screenClose(),
|
|
76
102
|
}, value => {
|
|
103
|
+
const wasActive = sharing || pendingScreen;
|
|
77
104
|
sharing = value.sharing; pendingScreen = value.pending === true;
|
|
78
|
-
for (const id of ['share', 'menu-share']) label(id, sharing ? '停止共享' : pendingScreen ? '取消选屏' : '共享画面');
|
|
105
|
+
for (const id of ['share', 'menu-share', 'chat-share', 'settings-share']) label(id, sharing ? '停止共享' : pendingScreen ? '取消选屏' : '共享画面');
|
|
106
|
+
for (const prefix of ['call', 'menu', 'chat', 'transcript', 'settings']) {
|
|
107
|
+
$(`${prefix}-share-context`).hidden = !sharing;
|
|
108
|
+
$(`${prefix}-share-target`).textContent = sharing ? `共享中:${value.label}` : '';
|
|
109
|
+
}
|
|
79
110
|
$('share-badge').hidden = !sharing && !pendingScreen;
|
|
80
111
|
$('share-badge').title = sharing ? `正在共享:${value.label},点此停止` : '正在选屏,点此取消';
|
|
112
|
+
$('share-badge').setAttribute('aria-label', sharing ? `停止共享:${value.label}` : '取消选屏');
|
|
113
|
+
if (value.message && value.message !== '未共享屏幕' && (wasActive || pendingScreen)) status(value.message);
|
|
81
114
|
});
|
|
82
115
|
function toggleScreen() {
|
|
83
116
|
if (conversation.phase !== 'talking') {
|
|
84
|
-
status('
|
|
117
|
+
status('先开始语音通话,连上后可选择窗口或屏幕');
|
|
85
118
|
$('status').hidden = false;
|
|
86
119
|
void controls.show('actions');
|
|
87
120
|
return;
|
|
@@ -91,46 +124,79 @@ if (!window.clowderCompanion) {
|
|
|
91
124
|
const conversation = new CompanionConversation({
|
|
92
125
|
client, createPeer: callback => new VoicePeer(callback, client), stopScreen: () => screen.stop(),
|
|
93
126
|
transcript: event => {
|
|
94
|
-
if (event.type === 'turn-done') {
|
|
95
|
-
|
|
96
|
-
|
|
127
|
+
if (event.type === 'turn-done') {
|
|
128
|
+
transcript.finish(event); callTranscript.finish(event); bubble.finish(event.role); callBubble.finish(event.role);
|
|
129
|
+
void readHistory(); void readCallTranscript();
|
|
130
|
+
} else if (event.type === 'typed') {
|
|
131
|
+
callTranscript.appendTyped(event);
|
|
132
|
+
void readHistory(); void readCallTranscript();
|
|
133
|
+
} else {
|
|
134
|
+
transcript.append(event.role, event.text, true);
|
|
135
|
+
callTranscript.appendLive(event);
|
|
136
|
+
const speaker = event.role === 'assistant' ? conversation.identity?.displayName ?? '猫猫' : '语音';
|
|
137
|
+
bubble.append(event.role, event.text, speaker); callBubble.append(event.role, event.text, speaker);
|
|
138
|
+
void controls.setAmbient('actions');
|
|
139
|
+
}
|
|
97
140
|
},
|
|
98
141
|
render(value) {
|
|
99
142
|
const showFailure = value.failed && previousPhase !== 'idle' && value.phase === 'idle';
|
|
100
143
|
const justConnected = previousPhase !== 'talking' && value.phase === 'talking';
|
|
101
|
-
if (previousPhase !== 'idle' && value.phase === 'idle') {
|
|
144
|
+
if (previousPhase !== 'idle' && value.phase === 'idle') {
|
|
145
|
+
transcript.reset(); callTranscript.reset(); bubble.reset(); callBubble.clear(); void readHistory();
|
|
146
|
+
if (controls.panel === 'transcript') void controls.show('menu');
|
|
147
|
+
}
|
|
102
148
|
previousPhase = value.phase;
|
|
103
149
|
const active = value.phase !== 'idle';
|
|
104
150
|
const receiveOnly = value.audioMode === 'receive_only';
|
|
105
151
|
const canReceiveOnly = value.identity?.audio?.supportedModes?.includes('receive_only') === true;
|
|
106
152
|
const retryReceiveOnly = value.failed && value.retryMode === 'receive_only' && canReceiveOnly;
|
|
107
|
-
|
|
153
|
+
callTranscript.setCall(value.callId);
|
|
154
|
+
void controls.setAmbient(ambientPanel(active));
|
|
108
155
|
$('begin').hidden = active; $('begin').disabled = !value.identity;
|
|
109
156
|
$('listen').hidden = active || !canReceiveOnly; $('listen').disabled = !value.identity;
|
|
110
|
-
label('begin', value.failed && !retryReceiveOnly ? '
|
|
157
|
+
label('begin', value.failed && !retryReceiveOnly ? '重试语音通话' : '语音通话');
|
|
111
158
|
label('listen', retryReceiveOnly ? '重试只听' : '只听');
|
|
112
159
|
$('begin').className = retryReceiveOnly ? '' : 'primary';
|
|
113
160
|
$('listen').className = retryReceiveOnly ? 'primary' : '';
|
|
114
161
|
$('mic').hidden = value.phase !== 'talking' || receiveOnly; $('speaker').hidden = !active;
|
|
162
|
+
$('chat-mic').hidden = value.phase !== 'talking' || receiveOnly;
|
|
163
|
+
$('transcript-mic').hidden = value.phase !== 'talking' || receiveOnly;
|
|
164
|
+
$('settings-mic').hidden = value.phase !== 'talking' || receiveOnly;
|
|
115
165
|
$('active-actions').hidden = !active;
|
|
116
166
|
$('share').disabled = value.phase !== 'talking';
|
|
117
167
|
$('compose').querySelector('button').disabled = !value.identity || value.phase === 'connecting';
|
|
118
168
|
status(value.message ?? '');
|
|
119
|
-
$('status').hidden = !value.message || /^(
|
|
120
|
-
$('
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
const
|
|
124
|
-
:
|
|
125
|
-
: value.muted ? '
|
|
126
|
-
|
|
169
|
+
$('status').hidden = !value.message || /^(点击|正在听|只听模式|只听已结束|语音通话已结束|已发送)/.test(value.message);
|
|
170
|
+
$('menu-status').hidden = !value.message;
|
|
171
|
+
const callState = value.recovering ? '连接不稳定' : value.phase === 'connecting' ? '正在连接'
|
|
172
|
+
: receiveOnly ? '只听中' : active ? '通话中' : '';
|
|
173
|
+
const microphoneState = receiveOnly ? '麦克风未启用'
|
|
174
|
+
: value.phase === 'connecting' ? '正在请求麦克风'
|
|
175
|
+
: value.recovering ? `正在等待原连接恢复 · ${value.muted ? '麦克风已静音' : '麦克风已开启'}`
|
|
176
|
+
: value.muted ? '麦克风已静音' : active ? '麦克风已开启' : '';
|
|
177
|
+
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';
|
|
178
|
+
setTexts(['call-state', 'chat-call-state', 'transcript-call-state', 'menu-call-state', 'settings-call-state'], callState);
|
|
179
|
+
setTexts(['call-mic-state', 'chat-call-mic-state', 'transcript-call-mic-state', 'menu-call-mic-state', 'settings-call-mic-state'], microphoneState);
|
|
180
|
+
setHidden(['chat-call-context', 'transcript-call-context', 'menu-call-context', 'settings-call-context'], !active);
|
|
127
181
|
label('mic', value.muted ? '取消静音' : '静音'); $('mic').setAttribute('aria-pressed', String(value.muted));
|
|
182
|
+
$('mic-icon').setAttribute('href', value.muted ? '#i-mic-off' : '#i-mic');
|
|
183
|
+
$('chat-mic').textContent = value.muted ? '取消静音' : '静音';
|
|
184
|
+
$('settings-mic').textContent = value.muted ? '取消静音' : '静音';
|
|
185
|
+
$('transcript-mic').setAttribute('aria-label', value.muted ? '取消静音' : '静音');
|
|
186
|
+
$('transcript-mic').setAttribute('aria-pressed', String(value.muted));
|
|
187
|
+
$('transcript-mic-icon').setAttribute('href', value.muted ? '#i-mic-off' : '#i-mic');
|
|
128
188
|
label('speaker', value.silent ? '开启播音' : '关闭播音');
|
|
129
189
|
const identity = value.identity;
|
|
130
190
|
const companionIdentity = currentCompanionIdentity(identity);
|
|
191
|
+
const nativeSnapshot = normalizeNativeWork(identity?.nativeWork);
|
|
192
|
+
const workLabel = nativeWorkLabel(nativeSnapshot, identity?.nativeActivity);
|
|
193
|
+
const workState = workLabel ? `${identity.displayName}${workLabel}` : '';
|
|
194
|
+
setTexts(['call-work', 'chat-call-work', 'transcript-call-work', 'menu-call-work', 'settings-call-work'], workState);
|
|
195
|
+
setHidden(['call-work', 'chat-call-work', 'transcript-call-work', 'menu-call-work', 'settings-call-work'], !workState);
|
|
131
196
|
motion.setContext({ skin: identity?.skin, phase: value.phase,
|
|
132
197
|
nativeActivity: identity?.nativeActivity ?? 'none', muted: value.muted || receiveOnly });
|
|
133
|
-
motion.
|
|
198
|
+
motion.setBehaviorEnabled(identity?.behaviorEnabled === true);
|
|
199
|
+
motion.syncNativeSnapshot(nativeSnapshot);
|
|
134
200
|
if (justConnected) motion.signal('connected');
|
|
135
201
|
if (showFailure) motion.signal('failed');
|
|
136
202
|
if (identity) {
|
|
@@ -138,7 +204,8 @@ if (!window.clowderCompanion) {
|
|
|
138
204
|
const companionLabel = companionIdentity
|
|
139
205
|
? `${companionIdentity.title} · ${companionIdentity.partnerLabel}` : identity.displayName;
|
|
140
206
|
$('chat-name').textContent = threadTitle ?? companionLabel; $('menu-name').textContent = companionLabel;
|
|
141
|
-
$('
|
|
207
|
+
$('call-message').placeholder = `发消息给${identity.displayName}…`;
|
|
208
|
+
$('pet').setAttribute('aria-label', `${companionLabel}:点击或右键打开名片,拖动移动`);
|
|
142
209
|
$('documents').querySelector('.menu-status').textContent = identity.documentsAllowed ? '开启' : '暂停';
|
|
143
210
|
$('documents').setAttribute('aria-pressed', String(identity.documentsAllowed));
|
|
144
211
|
if (companionIdentity) {
|
|
@@ -151,7 +218,24 @@ if (!window.clowderCompanion) {
|
|
|
151
218
|
: `${identity.duty.displayName} · ${live} · 绑定载体 ${identity.carrier.displayName}`;
|
|
152
219
|
}
|
|
153
220
|
}
|
|
154
|
-
if (showFailure) void controls.show('
|
|
221
|
+
if (showFailure) void controls.show('menu');
|
|
222
|
+
},
|
|
223
|
+
});
|
|
224
|
+
settingsView = new SettingsView({
|
|
225
|
+
document,
|
|
226
|
+
client,
|
|
227
|
+
close: () => void controls.show('menu'),
|
|
228
|
+
callActive: () => conversation.active,
|
|
229
|
+
expectHostStop: () => conversation.expectHostStop(),
|
|
230
|
+
settleExpectedHostStop: callStopped => conversation.settleExpectedHostStop(callStopped),
|
|
231
|
+
layoutChanged: () => { if (controls.panel === 'settings') controls.remeasure?.(); },
|
|
232
|
+
onValues(values) {
|
|
233
|
+
settingsValues = values;
|
|
234
|
+
document.documentElement?.style?.setProperty?.('--pet-scale', String(values.ballSize / 72));
|
|
235
|
+
$('anchor').dataset.ballSize = String(values.ballSize);
|
|
236
|
+
motion.setBehaviorEnabled(values.behaviorEnabled);
|
|
237
|
+
$('documents').hidden = true;
|
|
238
|
+
if (latestDecisionPage) showDecisionBadge(latestDecisionPage);
|
|
155
239
|
},
|
|
156
240
|
});
|
|
157
241
|
async function readHistory() {
|
|
@@ -159,49 +243,78 @@ if (!window.clowderCompanion) {
|
|
|
159
243
|
loading = true;
|
|
160
244
|
try {
|
|
161
245
|
const history = await client.readConversation();
|
|
162
|
-
|
|
163
|
-
if (observedHistory && assistantId && assistantId !== lastAssistantId) {
|
|
164
|
-
resultPreviewUntil = Date.now() + 12_000;
|
|
165
|
-
}
|
|
166
|
-
observedHistory = true;
|
|
167
|
-
lastAssistantId = assistantId;
|
|
168
|
-
$('history').textContent = history.hasMore ? '更早聊天 ↗' : '完整聊天 ↗';
|
|
246
|
+
$('history').textContent = history.hasMore ? '在 Cat Café 中查看更早聊天 ↗' : '在 Cat Café 中查看完整聊天 ↗';
|
|
169
247
|
threadTitle = history.threadTitle;
|
|
170
248
|
$('chat-name').textContent = threadTitle;
|
|
171
249
|
bubble.load(history.messages);
|
|
172
|
-
void controls.setAmbient(
|
|
250
|
+
void controls.setAmbient(ambientPanel(conversation.phase !== 'idle'));
|
|
173
251
|
const serialized = JSON.stringify(history.messages);
|
|
174
252
|
if (serialized !== latestHistory) { latestHistory = serialized; transcript.load(history.messages); }
|
|
175
253
|
} catch { $('chat-status').textContent = '聊天记录暂未更新 · 正在说的话仍会显示'; }
|
|
176
254
|
finally { loading = false; }
|
|
177
255
|
}
|
|
256
|
+
async function readCallTranscript() {
|
|
257
|
+
if (transcriptLoading || !conversation.active || !conversation.callId) return;
|
|
258
|
+
transcriptLoading = true;
|
|
259
|
+
const callId = conversation.callId;
|
|
260
|
+
try {
|
|
261
|
+
const reply = await client.readTranscript();
|
|
262
|
+
if (!conversation.active || conversation.callId !== callId) return;
|
|
263
|
+
callTranscript.load(reply);
|
|
264
|
+
callBubble.loadTranscript(reply.rows, conversation.identity?.displayName);
|
|
265
|
+
$('call-transcript-status').textContent = reply.hasMore ? '这里只显示这次通话最近的内容' : '';
|
|
266
|
+
} catch {
|
|
267
|
+
if (conversation.active && conversation.callId === callId) $('call-transcript-status').textContent = '字幕暂未更新 · 实时内容仍会继续显示';
|
|
268
|
+
} finally { transcriptLoading = false; }
|
|
269
|
+
}
|
|
178
270
|
function showDecisionBadge(page) {
|
|
271
|
+
latestDecisionPage = page;
|
|
179
272
|
const badge = decisionBadge(page);
|
|
180
|
-
$('pending-badge').hidden = !badge.visible;
|
|
273
|
+
$('pending-badge').hidden = !badge.visible || settingsValues?.proactivePolicy !== 'quiet-badge';
|
|
181
274
|
$('pending-count').textContent = badge.label;
|
|
182
275
|
$('pending-badge').title = badge.title;
|
|
183
276
|
$('pending-badge').setAttribute('aria-label', badge.title);
|
|
184
277
|
$('menu-pending').textContent = badge.label;
|
|
278
|
+
$('menu-pending').dataset.state = badge.state;
|
|
185
279
|
}
|
|
186
280
|
async function readDecisions(more = false) {
|
|
187
281
|
if (decisionLoading) return;
|
|
188
282
|
decisionLoading = true;
|
|
189
|
-
if (!more && controls.panel === 'decisions') $('decision-status').textContent = '
|
|
283
|
+
if (!more && controls.panel === 'decisions') $('decision-status').textContent = '正在读取待办…';
|
|
190
284
|
try {
|
|
285
|
+
const previousReadBasis = decisionReadBasis(latestDecisionPage);
|
|
191
286
|
const page = await client.readDecisions(more ? decisionOffset : 0, 10);
|
|
192
|
-
|
|
287
|
+
const presentation = decisionPresentation(page);
|
|
288
|
+
const changedRead = more && presentation.renderRows && previousReadBasis !== null
|
|
289
|
+
&& decisionReadBasis(page) !== previousReadBasis;
|
|
193
290
|
showDecisionBadge(page);
|
|
194
|
-
motion.setPendingDecision(
|
|
291
|
+
motion.setPendingDecision(presentation.pending);
|
|
195
292
|
if (controls.panel !== 'decisions') return;
|
|
196
293
|
const list = $('decision-list');
|
|
197
|
-
if (!more) { list.replaceChildren(); list.scrollTop = 0; }
|
|
198
|
-
|
|
294
|
+
if (!more || changedRead) { list.replaceChildren(); list.scrollTop = 0; }
|
|
295
|
+
const retainedRows = more && !presentation.renderRows && list.children.length > 0;
|
|
296
|
+
const knownVariantRefs = new Set(Array.from(list.children)
|
|
297
|
+
.map(item => item.dataset.variantRef).filter(Boolean));
|
|
298
|
+
for (const row of presentation.renderRows && !changedRead ? decisionRows(page) : []) {
|
|
299
|
+
if (row.variantRef && knownVariantRefs.has(row.variantRef)) continue;
|
|
199
300
|
const item = document.createElement('li');
|
|
301
|
+
if (row.variantRef) { item.dataset.variantRef = row.variantRef; knownVariantRefs.add(row.variantRef); }
|
|
200
302
|
const title = document.createElement('strong');
|
|
201
303
|
const meta = document.createElement('span');
|
|
202
304
|
title.textContent = row.title; meta.textContent = row.meta;
|
|
203
305
|
item.append(title, meta); list.append(item);
|
|
204
|
-
if (row.
|
|
306
|
+
if (row.navigation) {
|
|
307
|
+
const button = document.createElement('button');
|
|
308
|
+
button.type = 'button'; button.textContent = row.navigation.label;
|
|
309
|
+
button.onclick = async () => {
|
|
310
|
+
try {
|
|
311
|
+
const result = await client.openDecision(row.navigation.variantRef, row.navigation.target);
|
|
312
|
+
$('decision-status').textContent = result.delivery === 'requested'
|
|
313
|
+
? '已请求打开对应事项' : '打开对应事项尚未确认 · 请从 Cat Café 查看';
|
|
314
|
+
} catch { $('decision-status').textContent = '暂时无法打开对应事项 · 请稍后重试'; }
|
|
315
|
+
};
|
|
316
|
+
item.append(button);
|
|
317
|
+
} else if (row.previewable) {
|
|
205
318
|
const button = document.createElement('button');
|
|
206
319
|
button.type = 'button'; button.textContent = '查看确认演练';
|
|
207
320
|
button.onclick = async () => {
|
|
@@ -217,18 +330,27 @@ if (!window.clowderCompanion) {
|
|
|
217
330
|
item.append(button);
|
|
218
331
|
}
|
|
219
332
|
}
|
|
220
|
-
decisionOffset = page.page.offset + page.page.limit;
|
|
221
|
-
$('decision-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
333
|
+
decisionOffset = changedRead ? 0 : page.page.offset + page.page.limit;
|
|
334
|
+
$('decision-reload').textContent = changedRead ? '刷新' : presentation.reloadLabel;
|
|
335
|
+
$('decision-status').textContent = changedRead && page.totalCount !== 0
|
|
336
|
+
? '待办已变化 · 请刷新查看最新列表'
|
|
337
|
+
: retainedRows
|
|
338
|
+
? `${presentation.badge.state === 'authentication' ? '待办需要登录' : '待办暂不可读'} · 以下为上次读到的内容`
|
|
339
|
+
: presentation.message;
|
|
340
|
+
$('decision-more').hidden = changedRead || !presentation.hasMore;
|
|
225
341
|
} catch {
|
|
226
342
|
showDecisionBadge(undefined);
|
|
227
343
|
motion.setPendingDecision(null);
|
|
228
|
-
if (controls.panel === 'decisions')
|
|
344
|
+
if (controls.panel === 'decisions') {
|
|
345
|
+
const retained = $('decision-list').children.length > 0;
|
|
346
|
+
$('decision-status').textContent = retained
|
|
347
|
+
? '待办暂不可读 · 以下为上次读到的内容'
|
|
348
|
+
: '待办暂不可读 · 请稍后刷新';
|
|
349
|
+
$('decision-reload').textContent = '重试';
|
|
350
|
+
$('decision-more').hidden = true;
|
|
351
|
+
}
|
|
229
352
|
} finally { decisionLoading = false; }
|
|
230
353
|
}
|
|
231
|
-
$('call-badge').onclick = () => void conversation.end();
|
|
232
354
|
$('share-badge').onclick = toggleScreen;
|
|
233
355
|
$('pending-badge').onclick = () => void controls.show('decisions');
|
|
234
356
|
$('decision-reload').onclick = () => void readDecisions();
|
|
@@ -236,24 +358,28 @@ if (!window.clowderCompanion) {
|
|
|
236
358
|
$('history').onclick = () => void client.openConversation().then(result => {
|
|
237
359
|
if (result.delivery === 'unconfirmed') status('打开聊天尚未确认 · 请从 Clowder 查看');
|
|
238
360
|
}).catch(error => status(explainError(error)));
|
|
239
|
-
|
|
240
|
-
event.preventDefault(); const
|
|
361
|
+
async function submitMessage(event, inputId) {
|
|
362
|
+
event.preventDefault(); const input = $(inputId); const text = input.value;
|
|
241
363
|
if (await conversation.send(text)) {
|
|
242
364
|
motion.signal('received');
|
|
243
|
-
if (
|
|
365
|
+
if (input.value === text) input.value = '';
|
|
244
366
|
latestHistory = undefined; void readHistory();
|
|
245
367
|
}
|
|
246
|
-
}
|
|
368
|
+
}
|
|
369
|
+
$('compose').onsubmit = event => submitMessage(event, 'message');
|
|
370
|
+
$('call-compose').onsubmit = event => submitMessage(event, 'call-message');
|
|
371
|
+
$('call-unread').onclick = () => callTranscript.scrollToBottom();
|
|
247
372
|
const unsubscribe = client.subscribe(event => {
|
|
248
373
|
if (event.kind === 'media-stopped') void conversation.hostStopped(event.reason);
|
|
249
374
|
if (event.kind === 'view-dismiss') controls.dismiss();
|
|
250
375
|
});
|
|
251
376
|
const statusMonitor = setInterval(() => void conversation.refresh(), 1000);
|
|
252
377
|
const historyMonitor = setInterval(() => void readHistory(), 3000);
|
|
378
|
+
const transcriptMonitor = setInterval(() => void readCallTranscript(), 1000);
|
|
253
379
|
const decisionMonitor = setInterval(() => { if (controls.panel !== 'decisions') void readDecisions(); }, 15000);
|
|
254
|
-
void conversation.refresh(); void readHistory(); void readDecisions(); void controls.show('none');
|
|
380
|
+
void conversation.refresh(); void readHistory(); void readDecisions(); void settingsView.controller.load(); void controls.show('none');
|
|
255
381
|
window.addEventListener('beforeunload', () => {
|
|
256
|
-
clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(decisionMonitor);
|
|
382
|
+
clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(transcriptMonitor); clearInterval(decisionMonitor);
|
|
257
383
|
unsubscribe(); nativeTravel.close(); motion.close(); void conversation.end();
|
|
258
384
|
});
|
|
259
385
|
}
|