@clowder-ai/companion 0.1.0-alpha.20 → 0.1.0-alpha.24
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 +12 -3
- package/manifest.json +3 -3
- package/package.json +4 -4
- package/plugin.yaml +3 -3
- package/renderer/call-transcript-view.mjs +3 -1
- package/renderer/chat-unread.mjs +44 -0
- package/renderer/client.mjs +9 -0
- package/renderer/compact-size.mjs +8 -0
- package/renderer/decision-panel.mjs +122 -0
- package/renderer/index.html +46 -21
- package/renderer/meeting-state.mjs +55 -0
- package/renderer/recent-bubble.mjs +19 -2
- package/renderer/style.css +66 -9
- package/renderer/surface.mjs +72 -108
- package/renderer/transcript-view.mjs +1 -0
- package/renderer/visible-text-receipt.mjs +241 -0
- package/renderer/work-overview.mjs +95 -0
package/README.md
CHANGED
|
@@ -26,12 +26,21 @@ borrowing today's selected companion.
|
|
|
26
26
|
|
|
27
27
|
## Host integration
|
|
28
28
|
|
|
29
|
-
This surface requires the additive desktop bridge **1.
|
|
29
|
+
This surface requires the additive desktop bridge **1.4.0**. The Host computes
|
|
30
30
|
placement within the display work area, keeps the cat as its stable anchor, and
|
|
31
|
-
owns bounded drag gestures and transparent-area hit testing.
|
|
32
|
-
|
|
31
|
+
owns bounded drag gestures and transparent-area hit testing. Hosts below 1.4
|
|
32
|
+
reject this manifest before installation. Media admission and capture
|
|
33
33
|
remain in the trusted Host; this package receives neither streams nor provider
|
|
34
34
|
SDP. Hiding releases media; the existing Clowder **聊聊** entry restores the cat.
|
|
35
|
+
Bridge 1.4 adds one untrusted first-visible-subtitle observation containing only
|
|
36
|
+
an epoch-millisecond time, the current call fence and a Host-correlatable source
|
|
37
|
+
reference. It does not claim compositor paint, an unoccluded window or reading.
|
|
38
|
+
Message and native-item references use the same bounded, single-line opaque ID
|
|
39
|
+
contract as the transcript surfaces that supply them. A Host
|
|
40
|
+
`invalid_request` rejects and remembers that candidate for the current call so
|
|
41
|
+
another current reference may be observed without retrying the rejected one.
|
|
42
|
+
Other failures retry after 1 and 2 seconds, then leave the call's
|
|
43
|
+
first-visible result unknown; later text never backfills a manufactured success.
|
|
35
44
|
|
|
36
45
|
Xianxian uses one layered sitting body or one VP9 clip at a time. Expiring Host
|
|
37
46
|
facts with task, result and event identities select tool work, workspace fetch,
|
package/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pluginId": "official.companion",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.24",
|
|
4
4
|
"contractVersion": "0.1.0",
|
|
5
5
|
"name": "猫猫球",
|
|
6
6
|
"icon": {
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"type": "desktop-window",
|
|
38
38
|
"id": "companion",
|
|
39
39
|
"role": "companion",
|
|
40
|
-
"bridgeVersion": "1.
|
|
40
|
+
"bridgeVersion": "1.4.0",
|
|
41
41
|
"surface": {
|
|
42
42
|
"entrypoint": "renderer/index.html",
|
|
43
|
-
"integrity": "sha256-
|
|
43
|
+
"integrity": "sha256-EFERDKvXnOw6Mbk1Kq7ZyX7w4FdswfX5W/B0w/KTLE8="
|
|
44
44
|
},
|
|
45
45
|
"presentation": {
|
|
46
46
|
"width": 300,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@clowder-ai/companion",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.24",
|
|
4
4
|
"description": "The desktop voice body for your Clowder companion",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -21,14 +21,14 @@
|
|
|
21
21
|
"scripts": {
|
|
22
22
|
"build": "node scripts/build.mjs",
|
|
23
23
|
"test": "node --test test/*.test.mjs",
|
|
24
|
-
"test:browser": "pnpm build && node --test test
|
|
24
|
+
"test:browser": "pnpm build && node --test test/*.browser.mjs",
|
|
25
25
|
"typecheck": "node scripts/check.mjs",
|
|
26
26
|
"lint": "node scripts/check.mjs",
|
|
27
27
|
"prepack": "pnpm build"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@clowder-ai/plugin-contract": "0.1.0-beta.
|
|
31
|
-
"@clowder-ai/plugin-sdk": "0.1.0-beta.
|
|
30
|
+
"@clowder-ai/plugin-contract": "0.1.0-beta.27",
|
|
31
|
+
"@clowder-ai/plugin-sdk": "0.1.0-beta.23",
|
|
32
32
|
"playwright": "1.61.1"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
package/plugin.yaml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pluginId": "official.companion",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.24",
|
|
4
4
|
"contractVersion": "0.1.0",
|
|
5
5
|
"name": "猫猫球",
|
|
6
6
|
"icon": {
|
|
@@ -37,10 +37,10 @@
|
|
|
37
37
|
"type": "desktop-window",
|
|
38
38
|
"id": "companion",
|
|
39
39
|
"role": "companion",
|
|
40
|
-
"bridgeVersion": "1.
|
|
40
|
+
"bridgeVersion": "1.4.0",
|
|
41
41
|
"surface": {
|
|
42
42
|
"entrypoint": "renderer/index.html",
|
|
43
|
-
"integrity": "sha256-
|
|
43
|
+
"integrity": "sha256-EFERDKvXnOw6Mbk1Kq7ZyX7w4FdswfX5W/B0w/KTLE8="
|
|
44
44
|
},
|
|
45
45
|
"presentation": {
|
|
46
46
|
"width": 300,
|
|
@@ -191,9 +191,11 @@ export class CallTranscriptView {
|
|
|
191
191
|
this.container.scrollTop = this.container.scrollHeight;
|
|
192
192
|
this.setUnread(0);
|
|
193
193
|
} else if (isNew) this.setUnread(this.unread + 1);
|
|
194
|
-
return
|
|
194
|
+
return row;
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
rowForMessage(messageId) { return this.durableRows.get(messageId); }
|
|
198
|
+
|
|
197
199
|
appendTyped({ receipt, text }) {
|
|
198
200
|
if (!this.callId || receipt?.delivery !== 'accepted' || receipt.callId !== this.callId) return false;
|
|
199
201
|
const pinned = this.nearBottom();
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Counts new assistant call-message identities only after the owner folds an opened chat. */
|
|
2
|
+
export class FoldedChatUnread {
|
|
3
|
+
constructor(changed = () => {}) {
|
|
4
|
+
this.changed = changed;
|
|
5
|
+
this.seen = new Set();
|
|
6
|
+
this.count = 0;
|
|
7
|
+
this.armed = false;
|
|
8
|
+
this.folded = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
publish(count) {
|
|
12
|
+
if (this.count === count) return;
|
|
13
|
+
this.count = count;
|
|
14
|
+
this.changed(count);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
open() {
|
|
18
|
+
this.armed = true;
|
|
19
|
+
this.folded = false;
|
|
20
|
+
this.publish(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fold() {
|
|
24
|
+
if (this.armed) this.folded = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
receive(message) {
|
|
28
|
+
const { messageId, role } = message ?? {};
|
|
29
|
+
if (!messageId || this.seen.has(messageId)) return;
|
|
30
|
+
this.seen.add(messageId);
|
|
31
|
+
if (role === 'assistant' && this.armed && this.folded) this.publish(this.count + 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
receiveMany(messages) {
|
|
35
|
+
for (const message of messages) this.receive(message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
reset() {
|
|
39
|
+
this.seen.clear();
|
|
40
|
+
this.armed = false;
|
|
41
|
+
this.folded = false;
|
|
42
|
+
this.publish(0);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/renderer/client.mjs
CHANGED
|
@@ -36,6 +36,15 @@ export function createCompanionClient(bridge) {
|
|
|
36
36
|
screenFrame: (selectionId, frame) => invoke({ kind: 'screen.frame', selectionId, frame }, 'ok'),
|
|
37
37
|
screenClose: () => invoke({ kind: 'screen.close' }, 'ok'),
|
|
38
38
|
openConversation: () => invoke({ kind: 'conversation.open' }, 'navigation'),
|
|
39
|
+
observePresentation: (observation) => invoke({
|
|
40
|
+
kind: 'presentation.observe',
|
|
41
|
+
event: observation.event,
|
|
42
|
+
observedAt: observation.observedAt,
|
|
43
|
+
callId: observation.callId,
|
|
44
|
+
reference: observation.reference,
|
|
45
|
+
}, 'ok'),
|
|
46
|
+
readWork: () => invoke({ kind: 'work.read' }, 'work'),
|
|
47
|
+
openWork: (entryRef) => invoke({ kind: 'work.open', entryRef }, 'navigation'),
|
|
39
48
|
readDecisions: (offset = 0, limit = 20) => invoke({ kind: 'decisions.read', offset, limit }, 'decisions'),
|
|
40
49
|
openDecision: (variantRef, target) => invoke({ kind: 'decision.open', variantRef, target }, 'navigation'),
|
|
41
50
|
inspectF221: (proposalId) => invoke({ kind: 'f221.inspect', proposalId }, 'decision-trial'),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const COMPACT_BALL_SIZE = 43;
|
|
2
|
+
export const DEFAULT_BALL_SIZE = 72;
|
|
3
|
+
|
|
4
|
+
export function compactSizeAction(ballSize) {
|
|
5
|
+
return Number(ballSize) <= COMPACT_BALL_SIZE
|
|
6
|
+
? { label: '放大', value: DEFAULT_BALL_SIZE }
|
|
7
|
+
: { label: '缩小', value: COMPACT_BALL_SIZE };
|
|
8
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { decisionBadge, decisionPresentation, decisionRows } from './decision-view.mjs';
|
|
2
|
+
|
|
3
|
+
const readBasis = page => page?.version === 1
|
|
4
|
+
? [page.status, page.totalCount ?? 'unknown', ...['approvals', 'needsMe']
|
|
5
|
+
.flatMap(key => [page.sources?.[key]?.status, page.sources?.[key]?.coverage])].join('|')
|
|
6
|
+
: page ? 'legacy' : null;
|
|
7
|
+
|
|
8
|
+
export const decisionNavigationText = delivery => delivery === 'applied' ? '已打开对应事项'
|
|
9
|
+
: delivery === 'queued' || delivery === 'requested' ? '已请求打开对应事项'
|
|
10
|
+
: delivery === 'blocked' ? '暂时无法打开对应事项 · 请稍后重试'
|
|
11
|
+
: '打开对应事项尚未确认 · 请从 Cat Café 查看';
|
|
12
|
+
|
|
13
|
+
export class DecisionPanel {
|
|
14
|
+
constructor({ document, client, controls, motion, proactivePolicy = () => undefined }) {
|
|
15
|
+
this.document = document;
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.controls = controls;
|
|
18
|
+
this.motion = motion;
|
|
19
|
+
this.proactivePolicy = proactivePolicy;
|
|
20
|
+
this.$ = id => document.getElementById(id);
|
|
21
|
+
this.loading = false;
|
|
22
|
+
this.offset = 0;
|
|
23
|
+
this.latestPage = undefined;
|
|
24
|
+
this.$('pending-badge').onclick = () => void controls.show('decisions');
|
|
25
|
+
this.$('decision-reload').onclick = () => void this.read();
|
|
26
|
+
this.$('decision-more').onclick = () => void this.read(true);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
showBadge(page) {
|
|
30
|
+
this.latestPage = page;
|
|
31
|
+
const badge = decisionBadge(page);
|
|
32
|
+
this.$('pending-badge').hidden = !badge.visible || this.proactivePolicy() !== 'quiet-badge';
|
|
33
|
+
this.$('pending-count').textContent = badge.label;
|
|
34
|
+
this.$('pending-badge').title = badge.title;
|
|
35
|
+
this.$('pending-badge').setAttribute('aria-label', badge.title);
|
|
36
|
+
this.$('menu-pending').textContent = badge.label;
|
|
37
|
+
this.$('menu-pending').dataset.state = badge.state;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
refreshBadge() {
|
|
41
|
+
if (this.latestPage) this.showBadge(this.latestPage);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
appendRow(list, row, knownVariantRefs) {
|
|
45
|
+
if (row.variantRef && knownVariantRefs.has(row.variantRef)) return;
|
|
46
|
+
const item = this.document.createElement('li');
|
|
47
|
+
if (row.variantRef) { item.dataset.variantRef = row.variantRef; knownVariantRefs.add(row.variantRef); }
|
|
48
|
+
const title = this.document.createElement('strong');
|
|
49
|
+
const meta = this.document.createElement('span');
|
|
50
|
+
title.textContent = row.title; meta.textContent = row.meta;
|
|
51
|
+
item.append(title, meta); list.append(item);
|
|
52
|
+
if (row.navigation) this.appendNavigation(item, row.navigation);
|
|
53
|
+
else if (row.previewable) this.appendTrial(item, row.proposalId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
appendNavigation(item, navigation) {
|
|
57
|
+
const button = this.document.createElement('button');
|
|
58
|
+
button.type = 'button'; button.textContent = navigation.label;
|
|
59
|
+
button.onclick = async () => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await this.client.openDecision(navigation.variantRef, navigation.target);
|
|
62
|
+
this.$('decision-status').textContent = decisionNavigationText(result.delivery);
|
|
63
|
+
} catch { this.$('decision-status').textContent = '暂时无法打开对应事项 · 请稍后重试'; }
|
|
64
|
+
};
|
|
65
|
+
item.append(button);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
appendTrial(item, proposalId) {
|
|
69
|
+
const button = this.document.createElement('button');
|
|
70
|
+
button.type = 'button'; button.textContent = '查看确认演练';
|
|
71
|
+
button.onclick = async () => {
|
|
72
|
+
try {
|
|
73
|
+
const result = await this.client.inspectF221(proposalId);
|
|
74
|
+
this.$('decision-status').textContent = result.status === 'trial_confirmed'
|
|
75
|
+
? '确认演练已完成;没有写回提案'
|
|
76
|
+
: result.status === 'stale' ? '提案已变化,请重新查看原处卡片'
|
|
77
|
+
: result.status === 'dismissed' ? '已取消演练;提案没有变化' : '确认演练暂不可用;请在原处处理';
|
|
78
|
+
} catch { this.$('decision-status').textContent = '确认演练暂不可用;请在原处处理'; }
|
|
79
|
+
};
|
|
80
|
+
item.append(button);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async read(more = false) {
|
|
84
|
+
if (this.loading) return;
|
|
85
|
+
this.loading = true;
|
|
86
|
+
if (!more && this.controls.panel === 'decisions') this.$('decision-status').textContent = '正在读取待办…';
|
|
87
|
+
try {
|
|
88
|
+
const previous = readBasis(this.latestPage);
|
|
89
|
+
const page = await this.client.readDecisions(more ? this.offset : 0, 10);
|
|
90
|
+
const presentation = decisionPresentation(page);
|
|
91
|
+
const changedRead = more && presentation.renderRows && previous !== null && readBasis(page) !== previous;
|
|
92
|
+
this.showBadge(page);
|
|
93
|
+
this.motion.setPendingDecision(presentation.pending);
|
|
94
|
+
if (this.controls.panel !== 'decisions') return;
|
|
95
|
+
const list = this.$('decision-list');
|
|
96
|
+
if (!more || changedRead) { list.replaceChildren(); list.scrollTop = 0; }
|
|
97
|
+
const retainedRows = more && !presentation.renderRows && list.children.length > 0;
|
|
98
|
+
const knownVariantRefs = new Set(Array.from(list.children).map(item => item.dataset.variantRef).filter(Boolean));
|
|
99
|
+
for (const row of presentation.renderRows && !changedRead ? decisionRows(page) : []) {
|
|
100
|
+
this.appendRow(list, row, knownVariantRefs);
|
|
101
|
+
}
|
|
102
|
+
this.offset = changedRead ? 0 : page.page.offset + page.page.limit;
|
|
103
|
+
this.$('decision-reload').textContent = changedRead ? '刷新' : presentation.reloadLabel;
|
|
104
|
+
this.$('decision-status').textContent = changedRead && page.totalCount !== 0
|
|
105
|
+
? '待办已变化 · 请刷新查看最新列表'
|
|
106
|
+
: retainedRows
|
|
107
|
+
? `${presentation.badge.state === 'authentication' ? '待办需要登录' : '待办暂不可读'} · 以下为上次读到的内容`
|
|
108
|
+
: presentation.message;
|
|
109
|
+
this.$('decision-more').hidden = changedRead || !presentation.hasMore;
|
|
110
|
+
} catch {
|
|
111
|
+
this.showBadge(undefined);
|
|
112
|
+
this.motion.setPendingDecision(null);
|
|
113
|
+
if (this.controls.panel === 'decisions') {
|
|
114
|
+
const retained = this.$('decision-list').children.length > 0;
|
|
115
|
+
this.$('decision-status').textContent = retained
|
|
116
|
+
? '待办暂不可读 · 以下为上次读到的内容' : '待办暂不可读 · 请稍后刷新';
|
|
117
|
+
this.$('decision-reload').textContent = '重试';
|
|
118
|
+
this.$('decision-more').hidden = true;
|
|
119
|
+
}
|
|
120
|
+
} finally { this.loading = false; }
|
|
121
|
+
}
|
|
122
|
+
}
|
package/renderer/index.html
CHANGED
|
@@ -49,6 +49,7 @@
|
|
|
49
49
|
</section>
|
|
50
50
|
<section id="actions" class="floating call-bar" aria-label="当前通话" hidden>
|
|
51
51
|
<div class="call-status-line"><span class="status-dot" aria-hidden="true"></span><strong id="call-state">通话中</strong><span id="call-mic-state">麦克风已开启</span></div>
|
|
52
|
+
<p id="call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="call-meeting-label" class="meeting-label"></span><span id="call-meeting-source" class="meeting-source" hidden></span></p>
|
|
52
53
|
<p id="call-work" class="call-work" hidden></p>
|
|
53
54
|
<div id="call-share-context" class="call-share-context" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="call-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
54
55
|
<div class="call-recent" aria-label="最新消息">
|
|
@@ -58,7 +59,8 @@
|
|
|
58
59
|
<div id="active-actions" class="action-row call-actions">
|
|
59
60
|
<button id="mic" data-action="mute"><svg class="icon" aria-hidden="true"><use id="mic-icon" href="#i-mic"/></svg><span class="label">静音</span></button>
|
|
60
61
|
<button data-action="share" id="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
61
|
-
<button data-action="transcript"><svg class="icon" aria-hidden="true"><use href="#i-subtitles"/></svg><span>字幕</span></button>
|
|
62
|
+
<button data-action="transcript" aria-label="字幕" title="字幕"><svg class="icon" aria-hidden="true"><use href="#i-subtitles"/></svg><span>字幕</span></button>
|
|
63
|
+
<button id="call-chat" data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span><span id="call-chat-unread" class="action-unread" hidden></span></button>
|
|
62
64
|
<button id="end" class="destructive" data-action="stop"><span>挂断</span></button>
|
|
63
65
|
</div>
|
|
64
66
|
<p id="status" role="status" hidden></p>
|
|
@@ -66,33 +68,54 @@
|
|
|
66
68
|
<section id="menu" class="floating name-card" aria-label="猫猫名片" hidden>
|
|
67
69
|
<div id="menu-call-context" class="call-context" hidden>
|
|
68
70
|
<span class="status-dot" aria-hidden="true"></span><strong id="menu-call-state"></strong><span id="menu-call-mic-state"></span>
|
|
71
|
+
<small id="menu-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="menu-call-meeting-label" class="meeting-label"></span><span id="menu-call-meeting-source" class="meeting-source" hidden></span></small>
|
|
69
72
|
<small id="menu-call-work" hidden></small>
|
|
70
|
-
<
|
|
73
|
+
<div id="menu-call-actions" class="call-context-actions menu-call-actions">
|
|
74
|
+
<button id="menu-mic" data-action="mute" aria-label="静音"><svg class="icon" aria-hidden="true"><use id="menu-mic-icon" href="#i-mic"/></svg></button>
|
|
75
|
+
<button id="menu-call-share" data-action="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
76
|
+
<button id="menu-call-chat" data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span></button>
|
|
77
|
+
<button data-action="stop" class="destructive compact">挂断</button>
|
|
78
|
+
</div>
|
|
71
79
|
<div id="menu-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="menu-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
72
80
|
</div>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
81
|
+
<div id="menu-scroll" class="menu-scroll">
|
|
82
|
+
<strong id="menu-name" class="menu-title">猫猫</strong>
|
|
83
|
+
<div id="name-card-actions" class="name-card-actions">
|
|
84
|
+
<button id="begin" class="primary" data-action="begin"><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">语音通话</span></button>
|
|
85
|
+
<button id="listen" data-action="listen" hidden><svg class="icon" aria-hidden="true"><use href="#i-listen"/></svg><span class="label">只听</span></button>
|
|
86
|
+
<button data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span></button>
|
|
87
|
+
</div>
|
|
88
|
+
<p id="menu-status" role="status" hidden></p>
|
|
89
|
+
<button data-action="decisions"><span aria-hidden="true">◇</span><span>待办</span><span id="menu-pending" class="menu-status">—</span></button>
|
|
90
|
+
<div id="work-overview" class="work-overview" hidden>
|
|
91
|
+
<section id="work-active-section" class="work-overview-section" hidden>
|
|
92
|
+
<header><span>进行中的工作</span><small id="work-scope"></small></header>
|
|
93
|
+
<div id="work-active-list" class="work-overview-list"></div>
|
|
94
|
+
</section>
|
|
95
|
+
<section id="work-delivery-section" class="work-overview-section" hidden>
|
|
96
|
+
<header><span>最近交付</span></header>
|
|
97
|
+
<div id="work-delivery-list" class="work-overview-list"></div>
|
|
98
|
+
</section>
|
|
99
|
+
<p id="work-notice" role="status"></p>
|
|
100
|
+
</div>
|
|
101
|
+
<button data-action="history"><svg class="icon" aria-hidden="true"><use href="#i-history"/></svg><span>打开聊天</span></button>
|
|
102
|
+
<button id="menu-play" data-action="play" hidden><span aria-hidden="true">✦</span><span>逗宪宪</span></button>
|
|
103
|
+
<button id="menu-share" data-action="share" title="语音通话连接后可共享画面" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
104
|
+
<button id="documents" data-action="documents" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-book"/></svg><span>资料查询</span><span class="menu-status">开启</span></button>
|
|
78
105
|
</div>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<button id="menu-settings" data-action="settings"><span aria-hidden="true">⚙</span><span>设置</span><span class="menu-status">›</span></button>
|
|
86
|
-
<button id="menu-share" data-action="share" title="语音通话连接后可共享画面"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
87
|
-
<button id="documents" data-action="documents" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-book"/></svg><span>资料查询</span><span class="menu-status">开启</span></button>
|
|
88
|
-
<div class="menu-divider" role="separator"></div>
|
|
89
|
-
<button data-action="hide" class="quiet-action" title="可从 Clowder 的聊聊入口叫回来"><svg class="icon" aria-hidden="true"><use href="#i-moon"/></svg><span>收起猫猫</span></button>
|
|
106
|
+
<footer class="menu-footer">
|
|
107
|
+
<button id="speaker" data-action="speaker" hidden><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">播音</span></button>
|
|
108
|
+
<button id="menu-settings" data-action="settings"><span aria-hidden="true">⚙</span><span>设置</span></button>
|
|
109
|
+
<button id="menu-size-toggle" data-action="size-toggle"><svg class="icon" aria-hidden="true"><use href="#i-expand"/></svg><span class="label">缩小</span></button>
|
|
110
|
+
<button id="menu-hide" data-action="hide" class="quiet-action" title="可从 Clowder 的聊聊入口叫回来"><svg class="icon" aria-hidden="true"><use href="#i-moon"/></svg><span id="menu-hide-label">隐藏</span></button>
|
|
111
|
+
</footer>
|
|
90
112
|
</section>
|
|
91
113
|
<section id="chat" class="floating" aria-label="和猫猫写几句" hidden>
|
|
92
114
|
<div id="chat-call-context" class="call-context" hidden>
|
|
93
115
|
<span class="status-dot" aria-hidden="true"></span><strong id="chat-call-state"></strong><span id="chat-call-mic-state"></span>
|
|
116
|
+
<small id="chat-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="chat-call-meeting-label" class="meeting-label"></span><span id="chat-call-meeting-source" class="meeting-source" hidden></span></small>
|
|
94
117
|
<small id="chat-call-work" hidden></small>
|
|
95
|
-
<div class="call-context-actions"><button id="chat-mic" data-action="mute">静音</button><button id="chat-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
118
|
+
<div class="call-context-actions call-context-inline-actions"><button id="chat-mic" data-action="mute">静音</button><button id="chat-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
96
119
|
<div id="chat-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="chat-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
97
120
|
</div>
|
|
98
121
|
<header><strong id="chat-name">和猫猫写几句</strong><button data-action="dismiss" aria-label="收起文字">收起</button></header>
|
|
@@ -104,6 +127,7 @@
|
|
|
104
127
|
<section id="transcript" class="floating call-transcript" data-expanded="false" aria-label="这次通话的字幕" hidden>
|
|
105
128
|
<div id="transcript-call-context" class="call-context transcript-call-context">
|
|
106
129
|
<span class="status-dot" aria-hidden="true"></span><strong id="transcript-call-state"></strong><span id="transcript-call-mic-state"></span>
|
|
130
|
+
<small id="transcript-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="transcript-call-meeting-label" class="meeting-label"></span><span id="transcript-call-meeting-source" class="meeting-source" hidden></span></small>
|
|
107
131
|
<small id="transcript-call-work" hidden></small>
|
|
108
132
|
<div class="call-context-actions">
|
|
109
133
|
<button id="transcript-return" class="transcript-return" type="button" data-action="transcript-return"><span aria-hidden="true">‹</span><span>返回通话</span></button>
|
|
@@ -122,8 +146,9 @@
|
|
|
122
146
|
<section id="settings" class="floating settings-panel" aria-label="猫猫球设置" hidden>
|
|
123
147
|
<div id="settings-call-context" class="call-context" hidden>
|
|
124
148
|
<span class="status-dot" aria-hidden="true"></span><strong id="settings-call-state"></strong><span id="settings-call-mic-state"></span>
|
|
149
|
+
<small id="settings-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="settings-call-meeting-label" class="meeting-label"></span><span id="settings-call-meeting-source" class="meeting-source" hidden></span></small>
|
|
125
150
|
<small id="settings-call-work" hidden></small>
|
|
126
|
-
<div class="call-context-actions"><button id="settings-mic" data-action="mute">静音</button><button id="settings-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
151
|
+
<div class="call-context-actions call-context-inline-actions"><button id="settings-mic" data-action="mute">静音</button><button id="settings-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
127
152
|
<div id="settings-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="settings-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
128
153
|
</div>
|
|
129
154
|
<header class="settings-head"><button id="settings-back" type="button" aria-label="返回名片">‹</button><strong id="settings-title">设置</strong></header>
|
|
@@ -149,7 +174,7 @@
|
|
|
149
174
|
<button id="settings-skin-ragdoll-v1" type="button" data-setting-control>布偶猫</button>
|
|
150
175
|
<button id="settings-skin-yarn-ball" type="button" data-setting-control>猫猫球</button>
|
|
151
176
|
</div>
|
|
152
|
-
<label id="settings-size-field" class="settings-field settings-size"><span>大小</span><div><small>小</small><input id="settings-size" type="range" min="
|
|
177
|
+
<label id="settings-size-field" class="settings-field settings-size"><span>大小</span><div><small>小</small><input id="settings-size" type="range" min="43" max="192" step="1" data-setting-control><small>大</small></div></label>
|
|
153
178
|
</div>
|
|
154
179
|
<div id="settings-page-confirm" class="settings-page settings-confirm" hidden>
|
|
155
180
|
<strong id="settings-confirm-title"></strong><p id="settings-confirm-copy"></p>
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function meetingPresentation(meeting) {
|
|
2
|
+
if (!meeting || meeting.kind === 'idle') return null;
|
|
3
|
+
if (meeting.kind === 'running') {
|
|
4
|
+
if (meeting.paused) return { label: '会议记录已暂停', sourceLabel: meeting.sourceLabel, tone: 'quiet' };
|
|
5
|
+
return meeting.sharing
|
|
6
|
+
? { label: '听会中', sourceLabel: meeting.sourceLabel, tone: 'active' }
|
|
7
|
+
: { label: '会议记录中 · 未接入通话', sourceLabel: meeting.sourceLabel, tone: 'active' };
|
|
8
|
+
}
|
|
9
|
+
if (meeting.kind === 'stopped') return { label: '会议记录已停止', sourceLabel: null, tone: 'quiet' };
|
|
10
|
+
if (meeting.kind === 'unconfirmed') return { label: '会议记录状态未确认', sourceLabel: null, tone: 'warning' };
|
|
11
|
+
if (meeting.kind === 'needs_source') return { label: '需要确认会议所在的 App', sourceLabel: null, tone: 'warning' };
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const meetingPrefixes = ['call', 'menu-call', 'chat-call', 'transcript-call', 'settings-call'];
|
|
16
|
+
export function renderMeetingStatus(document, presentation) {
|
|
17
|
+
for (const prefix of meetingPrefixes) {
|
|
18
|
+
const row = document.getElementById(`${prefix}-meeting`);
|
|
19
|
+
const source = document.getElementById(`${prefix}-meeting-source`);
|
|
20
|
+
row.hidden = presentation === null;
|
|
21
|
+
row.dataset.tone = presentation?.tone ?? '';
|
|
22
|
+
document.getElementById(`${prefix}-meeting-label`).textContent = presentation?.label ?? '';
|
|
23
|
+
source.textContent = presentation?.sourceLabel ? ` · ${presentation.sourceLabel}` : '';
|
|
24
|
+
source.hidden = !presentation?.sourceLabel;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Keeps the stopped acknowledgement transient without hiding uncertain or active capture truth. */
|
|
29
|
+
export class MeetingStatus {
|
|
30
|
+
constructor({ now = () => Date.now() } = {}) {
|
|
31
|
+
this.now = now;
|
|
32
|
+
this.previousKind = null;
|
|
33
|
+
this.stoppedUntil = 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
update(meeting) {
|
|
37
|
+
const kind = meeting?.kind ?? 'idle';
|
|
38
|
+
if (kind === 'idle') {
|
|
39
|
+
this.previousKind = 'idle';
|
|
40
|
+
this.stoppedUntil = 0;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (kind === 'stopped') {
|
|
44
|
+
if (this.previousKind !== 'stopped') {
|
|
45
|
+
this.stoppedUntil = this.previousKind !== null && this.previousKind !== 'idle'
|
|
46
|
+
? this.now() + 10_000 : 0;
|
|
47
|
+
}
|
|
48
|
+
this.previousKind = 'stopped';
|
|
49
|
+
return this.now() < this.stoppedUntil ? meetingPresentation(meeting) : null;
|
|
50
|
+
}
|
|
51
|
+
this.previousKind = kind;
|
|
52
|
+
this.stoppedUntil = 0;
|
|
53
|
+
return meetingPresentation(meeting);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -1,9 +1,16 @@
|
|
|
1
1
|
/** A compact projection of Host history plus the current unsaved voice caption. */
|
|
2
|
+
function referenceKey(reference) {
|
|
3
|
+
if (reference?.kind === 'message') return `message:${reference.messageId}`;
|
|
4
|
+
if (reference?.kind === 'native-item') return `native-item:${reference.nativeItemId}`;
|
|
5
|
+
return null;
|
|
6
|
+
}
|
|
7
|
+
|
|
2
8
|
export class RecentBubble {
|
|
3
9
|
constructor(rows) {
|
|
4
10
|
this.rows = rows;
|
|
5
11
|
this.history = [];
|
|
6
12
|
this.live = new Map();
|
|
13
|
+
this.rowReferences = new WeakMap();
|
|
7
14
|
}
|
|
8
15
|
load(messages) {
|
|
9
16
|
this.history = messages.slice(-2);
|
|
@@ -14,13 +21,16 @@ export class RecentBubble {
|
|
|
14
21
|
role: row.role,
|
|
15
22
|
text: row.text,
|
|
16
23
|
name: row.source?.kind === 'typed' ? '你' : row.role === 'assistant' ? companionName : '语音',
|
|
24
|
+
reference: { kind: 'message', messageId: row.messageId },
|
|
17
25
|
})));
|
|
18
26
|
}
|
|
19
|
-
append(role, text, name = role === 'user' ? '你' : '猫猫') {
|
|
27
|
+
append(role, text, name = role === 'user' ? '你' : '猫猫', reference) {
|
|
20
28
|
if (!['user', 'assistant'].includes(role) || typeof text !== 'string') return;
|
|
21
29
|
const previous = this.live.get(role);
|
|
22
|
-
this.live.set(role, { name, text: `${previous?.text ?? ''}${text}`.slice(-240)
|
|
30
|
+
this.live.set(role, { name, text: `${previous?.text ?? ''}${text}`.slice(-240),
|
|
31
|
+
reference: reference ?? previous?.reference });
|
|
23
32
|
this.render();
|
|
33
|
+
return reference ? this.rowsForReference(reference)[0] : this.rows.find(row => row.dataset?.role === role);
|
|
24
34
|
}
|
|
25
35
|
finish(role) {
|
|
26
36
|
this.live.delete(role);
|
|
@@ -38,6 +48,10 @@ export class RecentBubble {
|
|
|
38
48
|
hasContent() {
|
|
39
49
|
return this.history.length > 0 || this.live.size > 0;
|
|
40
50
|
}
|
|
51
|
+
rowsForReference(reference) {
|
|
52
|
+
const key = referenceKey(reference);
|
|
53
|
+
return key ? this.rows.filter(row => this.rowReferences.get(row) === key) : [];
|
|
54
|
+
}
|
|
41
55
|
render() {
|
|
42
56
|
const live = [...this.live].map(([role, value]) => ({ role, ...value }));
|
|
43
57
|
const entries = [...this.history.slice(-Math.max(0, 2 - live.length)), ...live].slice(-2);
|
|
@@ -69,6 +83,9 @@ export class RecentBubble {
|
|
|
69
83
|
}
|
|
70
84
|
row.dataset ??= {};
|
|
71
85
|
row.dataset.role = entry?.role ?? '';
|
|
86
|
+
const key = referenceKey(entry?.reference);
|
|
87
|
+
if (key) this.rowReferences.set(row, key);
|
|
88
|
+
else this.rowReferences.delete(row);
|
|
72
89
|
}
|
|
73
90
|
}
|
|
74
91
|
}
|