@clowder-ai/companion 0.1.0-alpha.29 → 0.1.0-alpha.30
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 +7 -2
- package/manifest.json +3 -3
- package/package.json +3 -3
- package/plugin.yaml +3 -3
- package/renderer/app-assist-controller.mjs +125 -0
- package/renderer/call-duration.mjs +59 -0
- package/renderer/client.mjs +4 -1
- package/renderer/conversation.mjs +6 -3
- package/renderer/index.html +15 -8
- package/renderer/pet-controls.mjs +43 -7
- package/renderer/recent-bubble.mjs +4 -2
- package/renderer/settings-view.mjs +9 -2
- package/renderer/style.css +30 -20
- package/renderer/surface.mjs +26 -5
package/README.md
CHANGED
|
@@ -26,9 +26,9 @@ 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.6.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. Hosts below 1.
|
|
31
|
+
owns bounded drag gestures and transparent-area hit testing. Hosts below 1.6
|
|
32
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.
|
|
@@ -55,6 +55,11 @@ revision and route. If the canonical thread changes, the old unconfirmed input
|
|
|
55
55
|
stays visible as invalidated local text and is never replayed into the new scope.
|
|
56
56
|
The legacy text and conversation methods remain unchanged for older pairs.
|
|
57
57
|
|
|
58
|
+
Bridge 1.6 separates passive call speech from the bounded nameplate and control
|
|
59
|
+
panel, and adds an optional application-assistance projection. Assistance is
|
|
60
|
+
off by default; settling a prompt only opens the ordinary menu and grants no
|
|
61
|
+
media, screen-selection or call authority.
|
|
62
|
+
|
|
58
63
|
Xianxian uses one layered sitting body or one VP9 clip at a time. Expiring Host
|
|
59
64
|
facts with task, result and event identities select tool work, workspace fetch,
|
|
60
65
|
screen reading and applied-result delivery. Native travel is independent: the
|
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.30",
|
|
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.6.0",
|
|
41
41
|
"surface": {
|
|
42
42
|
"entrypoint": "renderer/index.html",
|
|
43
|
-
"integrity": "sha256-
|
|
43
|
+
"integrity": "sha256-dR1cqLYEPsY28TJ6ureljiV5CP23wubn70G7pePPkNg="
|
|
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.30",
|
|
4
4
|
"description": "The desktop voice body for your Clowder companion",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,8 +27,8 @@
|
|
|
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.29",
|
|
31
|
+
"@clowder-ai/plugin-sdk": "0.1.0-beta.25",
|
|
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.30",
|
|
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.6.0",
|
|
41
41
|
"surface": {
|
|
42
42
|
"entrypoint": "renderer/index.html",
|
|
43
|
-
"integrity": "sha256-
|
|
43
|
+
"integrity": "sha256-dR1cqLYEPsY28TJ6ureljiV5CP23wubn70G7pePPkNg="
|
|
44
44
|
},
|
|
45
45
|
"presentation": {
|
|
46
46
|
"width": 300,
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
const ACTIONS = new Set(['dismiss', 'timeout', 'accept']);
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Renderer projection only. The Host owns offer creation, expiry and clearing;
|
|
5
|
+
* this controller merely keeps stale reads and replies from reviving UI or
|
|
6
|
+
* turning an old accept into a new action.
|
|
7
|
+
*/
|
|
8
|
+
export class AppAssistController {
|
|
9
|
+
constructor({
|
|
10
|
+
client,
|
|
11
|
+
changed = () => {},
|
|
12
|
+
accepted = () => {},
|
|
13
|
+
scheduleTimeout = setTimeout,
|
|
14
|
+
cancelTimeout = clearTimeout,
|
|
15
|
+
timeoutMs = 15_000,
|
|
16
|
+
}) {
|
|
17
|
+
this.client = client;
|
|
18
|
+
this.changed = changed;
|
|
19
|
+
this.accepted = accepted;
|
|
20
|
+
this.scheduleTimeout = scheduleTimeout;
|
|
21
|
+
this.cancelTimeout = cancelTimeout;
|
|
22
|
+
this.timeoutMs = timeoutMs;
|
|
23
|
+
this.operation = 0;
|
|
24
|
+
this.timer = null;
|
|
25
|
+
this.timerOfferRef = null;
|
|
26
|
+
this.state = {
|
|
27
|
+
scopeRef: null,
|
|
28
|
+
revision: -1,
|
|
29
|
+
status: null,
|
|
30
|
+
offer: null,
|
|
31
|
+
hiddenRef: null,
|
|
32
|
+
pending: null,
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
publish(patch) {
|
|
37
|
+
this.state = { ...this.state, ...patch };
|
|
38
|
+
this.changed(this.state);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
clearTimer() {
|
|
42
|
+
if (this.timer !== null) this.cancelTimeout(this.timer);
|
|
43
|
+
this.timer = null;
|
|
44
|
+
this.timerOfferRef = null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
armTimer(offerRef) {
|
|
48
|
+
if (this.timer !== null && this.timerOfferRef === offerRef) return;
|
|
49
|
+
this.clearTimer();
|
|
50
|
+
this.timerOfferRef = offerRef;
|
|
51
|
+
this.timer = this.scheduleTimeout(() => {
|
|
52
|
+
this.timer = null;
|
|
53
|
+
this.timerOfferRef = null;
|
|
54
|
+
if (this.state.offer?.offerRef === offerRef) void this.settle('timeout');
|
|
55
|
+
}, this.timeoutMs);
|
|
56
|
+
this.timer?.unref?.();
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
consume(snapshot) {
|
|
60
|
+
if (!snapshot || typeof snapshot.scopeRef !== 'string' || !Number.isSafeInteger(snapshot.revision)) return false;
|
|
61
|
+
if (this.state.scopeRef !== null && snapshot.scopeRef !== this.state.scopeRef) return false;
|
|
62
|
+
if (snapshot.revision <= this.state.revision) return false;
|
|
63
|
+
|
|
64
|
+
const scopeRef = this.state.scopeRef ?? snapshot.scopeRef;
|
|
65
|
+
const incomingRef = snapshot.offer?.offerRef ?? null;
|
|
66
|
+
const sameHiddenOffer = incomingRef !== null && incomingRef === this.state.hiddenRef;
|
|
67
|
+
const hiddenRef = incomingRef === null || (this.state.hiddenRef && incomingRef !== this.state.hiddenRef)
|
|
68
|
+
? null
|
|
69
|
+
: this.state.hiddenRef;
|
|
70
|
+
const offer = sameHiddenOffer ? null : snapshot.offer;
|
|
71
|
+
const pending = incomingRef && this.state.pending?.offerRef !== incomingRef ? null : this.state.pending;
|
|
72
|
+
this.publish({ scopeRef, revision: snapshot.revision, status: snapshot.status, offer, hiddenRef, pending });
|
|
73
|
+
if (offer) this.armTimer(offer.offerRef);
|
|
74
|
+
else this.clearTimer();
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
async settle(action) {
|
|
79
|
+
if (!ACTIONS.has(action) || !this.state.offer || !this.state.scopeRef) return false;
|
|
80
|
+
const pending = {
|
|
81
|
+
id: ++this.operation,
|
|
82
|
+
scopeRef: this.state.scopeRef,
|
|
83
|
+
revision: this.state.revision,
|
|
84
|
+
offerRef: this.state.offer.offerRef,
|
|
85
|
+
action,
|
|
86
|
+
};
|
|
87
|
+
this.clearTimer();
|
|
88
|
+
this.publish({ offer: null, hiddenRef: pending.offerRef, pending });
|
|
89
|
+
|
|
90
|
+
let reply;
|
|
91
|
+
try {
|
|
92
|
+
reply = await this.client.settleAppAssist(pending.scopeRef, pending.offerRef, action);
|
|
93
|
+
} catch {
|
|
94
|
+
if (this.state.pending?.id === pending.id) this.publish({ pending: null });
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
if (this.state.pending?.id !== pending.id) return false;
|
|
98
|
+
const matching = reply?.kind === 'app-assist-settlement'
|
|
99
|
+
&& reply.scopeRef === pending.scopeRef
|
|
100
|
+
&& reply.offerRef === pending.offerRef
|
|
101
|
+
&& reply.action === pending.action
|
|
102
|
+
&& Number.isSafeInteger(reply.revision)
|
|
103
|
+
&& reply.revision > pending.revision
|
|
104
|
+
&& reply.revision >= this.state.revision
|
|
105
|
+
&& this.state.status === 'ready';
|
|
106
|
+
if (!matching || reply.outcome !== 'settled') {
|
|
107
|
+
this.publish({ pending: null });
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
this.publish({ revision: reply.revision, hiddenRef: null, pending: null });
|
|
112
|
+
if (action === 'accept') this.accepted({
|
|
113
|
+
scopeRef: reply.scopeRef,
|
|
114
|
+
revision: reply.revision,
|
|
115
|
+
offerRef: reply.offerRef,
|
|
116
|
+
});
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
close() {
|
|
121
|
+
this.operation++;
|
|
122
|
+
this.clearTimer();
|
|
123
|
+
this.state = { ...this.state, offer: null, pending: null };
|
|
124
|
+
}
|
|
125
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
function formatDuration(elapsedMs) {
|
|
2
|
+
const seconds = Math.max(0, Math.floor(elapsedMs / 1000));
|
|
3
|
+
const hours = Math.floor(seconds / 3600);
|
|
4
|
+
const minutes = Math.floor((seconds % 3600) / 60);
|
|
5
|
+
const remainder = seconds % 60;
|
|
6
|
+
const clock = `${String(minutes).padStart(2, '0')}:${String(remainder).padStart(2, '0')}`;
|
|
7
|
+
return hours ? `${hours}:${clock}` : clock;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** Presentation clock anchored only after the current media connection is confirmed. */
|
|
11
|
+
export class CallDuration {
|
|
12
|
+
constructor(node, {
|
|
13
|
+
now = Date.now,
|
|
14
|
+
schedule = callback => {
|
|
15
|
+
const handle = setInterval(callback, 1000);
|
|
16
|
+
handle.unref?.();
|
|
17
|
+
return handle;
|
|
18
|
+
},
|
|
19
|
+
cancel = handle => clearInterval(handle),
|
|
20
|
+
} = {}) {
|
|
21
|
+
Object.assign(this, { node, now, schedule, cancel });
|
|
22
|
+
this.startedAt = null;
|
|
23
|
+
this.timer = undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
set(startedAt) {
|
|
27
|
+
startedAt = Number.isFinite(startedAt) ? startedAt : null;
|
|
28
|
+
if (startedAt === this.startedAt) {
|
|
29
|
+
if (startedAt !== null) this.render();
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
this.clear();
|
|
33
|
+
this.startedAt = startedAt;
|
|
34
|
+
this.node.hidden = startedAt === null;
|
|
35
|
+
if (startedAt === null) {
|
|
36
|
+
this.node.textContent = '';
|
|
37
|
+
return;
|
|
38
|
+
}
|
|
39
|
+
this.render();
|
|
40
|
+
this.timer = this.schedule(() => this.render());
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
render() {
|
|
44
|
+
if (this.startedAt === null) return;
|
|
45
|
+
this.node.textContent = formatDuration(this.now() - this.startedAt);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
clear() {
|
|
49
|
+
if (this.timer === undefined) return;
|
|
50
|
+
this.cancel(this.timer);
|
|
51
|
+
this.timer = undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
close() {
|
|
55
|
+
this.clear();
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export { formatDuration };
|
package/renderer/client.mjs
CHANGED
|
@@ -95,12 +95,15 @@ export function createCompanionClient(bridge) {
|
|
|
95
95
|
openDecision: (variantRef, target) => invoke({ kind: 'decision.open', variantRef, target }, 'navigation'),
|
|
96
96
|
inspectF221: (proposalId) => invoke({ kind: 'f221.inspect', proposalId }, 'decision-trial'),
|
|
97
97
|
resize: (expanded) => invoke({ kind: 'view.resize', expanded }, 'ok'),
|
|
98
|
-
layout: (panel, width, height) => invoke(
|
|
98
|
+
layout: (panel, width, height, call) => invoke(call === undefined
|
|
99
|
+
? { kind: 'view.layout', panel, width, height }
|
|
100
|
+
: { kind: 'view.layout', panel, width, height, call }, 'layout'),
|
|
99
101
|
drag: (phase) => invoke({ kind: 'view.drag', phase }, 'ok'),
|
|
100
102
|
hide: () => invoke({ kind: 'view.hide' }, 'ok'),
|
|
101
103
|
resetPosition: () => invoke({ kind: 'view.reset' }, 'ok'),
|
|
102
104
|
readSettings: () => invoke({ kind: 'settings.read' }, 'settings'),
|
|
103
105
|
updateSetting: (field, value) => invoke({ kind: 'settings.update', field, value }, 'settings-update'),
|
|
106
|
+
settleAppAssist: (scopeRef, offerRef, action) => invoke({ kind: 'app-assist.settle', scopeRef, offerRef, action }, 'app-assist-settlement'),
|
|
104
107
|
disableCompanion: () => invoke({ kind: 'companion.disable' }, 'companion-lifecycle'),
|
|
105
108
|
readConversation,
|
|
106
109
|
readConversationPage,
|
|
@@ -3,8 +3,8 @@ import { explainError } from './errors.mjs';
|
|
|
3
3
|
/** Local controls only. Durable messages, identity and preferences belong to the Host. */
|
|
4
4
|
export class CompanionConversation {
|
|
5
5
|
constructor({ client, createPeer, render, transcript, delivery = () => {}, stopScreen,
|
|
6
|
-
uuid = () => crypto.randomUUID() }) {
|
|
7
|
-
Object.assign(this, { client, createPeer, render, transcript, delivery, stopScreen, uuid });
|
|
6
|
+
uuid = () => crypto.randomUUID(), now = Date.now }) {
|
|
7
|
+
Object.assign(this, { client, createPeer, render, transcript, delivery, stopScreen, uuid, now });
|
|
8
8
|
this.active = false;
|
|
9
9
|
this.generation = 0;
|
|
10
10
|
this.phase = 'idle';
|
|
@@ -20,7 +20,7 @@ export class CompanionConversation {
|
|
|
20
20
|
this.message = message;
|
|
21
21
|
this.render({ phase: this.phase, identity: this.identity, audioMode: this.audioMode, retryMode: this.retryMode,
|
|
22
22
|
muted: this.muted, silent: this.silent, failed: this.failed, recovering: this.recovering,
|
|
23
|
-
callId: this.callId, message });
|
|
23
|
+
callId: this.callId, connectedAt: this.connectedAt ?? null, message });
|
|
24
24
|
}
|
|
25
25
|
current(generation) { return this.active && generation === this.generation; }
|
|
26
26
|
listening() {
|
|
@@ -69,6 +69,7 @@ export class CompanionConversation {
|
|
|
69
69
|
this.recovering = false;
|
|
70
70
|
this.phase = 'connecting';
|
|
71
71
|
this.callId = null;
|
|
72
|
+
this.connectedAt = null;
|
|
72
73
|
this.expectedHostStop = undefined;
|
|
73
74
|
const generation = ++this.generation;
|
|
74
75
|
// Must reach the isolated preload while the click still has user activation.
|
|
@@ -82,6 +83,7 @@ export class CompanionConversation {
|
|
|
82
83
|
if (event.type === 'connected') {
|
|
83
84
|
if (this.callId && this.callId !== event.callId) return;
|
|
84
85
|
this.callId = event.callId;
|
|
86
|
+
this.connectedAt ??= this.now();
|
|
85
87
|
this.recovering = false;
|
|
86
88
|
clearTimeout(this.deadline); this.phase = 'talking'; this.show(this.listening());
|
|
87
89
|
}
|
|
@@ -130,6 +132,7 @@ export class CompanionConversation {
|
|
|
130
132
|
this.phase = 'idle';
|
|
131
133
|
this.recovering = false;
|
|
132
134
|
this.callId = null;
|
|
135
|
+
this.connectedAt = null;
|
|
133
136
|
this.show(message);
|
|
134
137
|
await Promise.allSettled([peer?.close(), this.stopScreen()]);
|
|
135
138
|
}
|
package/renderer/index.html
CHANGED
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
</svg>
|
|
18
18
|
<main id="desktop" aria-label="猫猫球">
|
|
19
19
|
<div id="anchor">
|
|
20
|
-
<button id="pet" aria-label="猫猫:点击或右键打开名片,拖动移动" aria-expanded="false" aria-controls="actions menu chat transcript settings decisions">
|
|
20
|
+
<button id="pet" aria-label="猫猫:点击或右键打开名片,拖动移动" aria-expanded="false" aria-controls="actions menu chat transcript settings decisions app-assist">
|
|
21
21
|
<svg id="living-sit" viewBox="0 0 600 600" aria-hidden="true" hidden>
|
|
22
22
|
<defs>
|
|
23
23
|
<mask id="living-eye-left" maskUnits="userSpaceOnUse" x="0" y="0" width="600" height="600"><image href="./skins/xianxian-living/layers/whiteL.webp" width="600" height="600"/></mask>
|
|
@@ -47,15 +47,21 @@
|
|
|
47
47
|
<section id="bubble" class="floating" aria-label="最新消息" hidden>
|
|
48
48
|
<p id="bubble-first" hidden></p><p id="bubble-second" hidden></p>
|
|
49
49
|
</section>
|
|
50
|
+
<section id="app-assist" class="floating app-assist" aria-label="应用旁的轻提示" data-candidate="true" hidden>
|
|
51
|
+
<p>你打开了 <strong id="app-assist-name"></strong>。要我一起看吗?</p>
|
|
52
|
+
<div><button id="app-assist-dismiss" type="button">暂不</button><button id="app-assist-start" class="primary" type="button">打开通话控制</button></div>
|
|
53
|
+
</section>
|
|
54
|
+
<section id="call-bubble" class="floating call-recent" aria-label="通话中的最新话语" hidden>
|
|
55
|
+
<p id="call-bubble-first" hidden></p><p id="call-bubble-second" hidden></p>
|
|
56
|
+
</section>
|
|
50
57
|
<section id="actions" class="floating call-bar" aria-label="当前通话" hidden>
|
|
51
|
-
<div class="call-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
<small class="call-recent-label">最新消息</small>
|
|
57
|
-
<p id="call-bubble-first" hidden></p><p id="call-bubble-second" hidden></p>
|
|
58
|
+
<div class="call-header">
|
|
59
|
+
<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>
|
|
60
|
+
<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>
|
|
61
|
+
<p id="call-work" class="call-work" hidden></p>
|
|
62
|
+
<time id="call-duration" class="call-duration" aria-label="通话时长" hidden></time>
|
|
58
63
|
</div>
|
|
64
|
+
<div id="call-share-context" class="call-share-context" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><strong id="call-share-target"></strong><small> · 仅此窗口</small></div>
|
|
59
65
|
<div id="active-actions" class="action-row call-actions">
|
|
60
66
|
<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>
|
|
61
67
|
<button data-action="share" id="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
@@ -185,6 +191,7 @@
|
|
|
185
191
|
<button id="settings-look-open" class="settings-row" type="button" data-setting-control data-drilldown><span>外观</span><span id="settings-look-value" class="settings-value">—</span></button>
|
|
186
192
|
<button id="settings-behavior" class="settings-row setting-switch" type="button" role="switch" aria-checked="false" data-setting-control><span><strong>自主活动</strong><small>空闲时自行走动</small></span><i aria-hidden="true"></i></button>
|
|
187
193
|
<button id="settings-proactive" class="settings-row setting-switch" type="button" role="switch" aria-checked="false" data-setting-control><span><strong>主动提示</strong><small>有待办时在猫旁显示数量</small></span><i aria-hidden="true"></i></button>
|
|
194
|
+
<button id="settings-app-assist" class="settings-row setting-switch" type="button" role="switch" aria-checked="false" data-setting-control><span><strong>应用旁问一句</strong><small>只看应用名字;每个应用每天最多一次</small></span><i aria-hidden="true"></i></button>
|
|
188
195
|
<button id="settings-documents" class="settings-row setting-switch" type="button" role="switch" aria-checked="false" data-setting-control><span><strong>资料查询</strong><small>允许陪伴者在通话中查询家里的资料</small></span><i aria-hidden="true"></i></button>
|
|
189
196
|
<footer class="settings-footer"><button id="settings-reset" type="button" data-setting-control>重置位置</button><button id="settings-disable-open" type="button" data-setting-control>停用桌面猫猫球…</button></footer>
|
|
190
197
|
</div>
|
|
@@ -4,7 +4,9 @@ export function bindPetControls(client, {
|
|
|
4
4
|
}) {
|
|
5
5
|
const $ = id => document.getElementById(id);
|
|
6
6
|
const body = $('pet');
|
|
7
|
-
const panelIds = ['actions', 'menu', 'chat', 'bubble', 'decisions', 'transcript', 'settings'];
|
|
7
|
+
const panelIds = ['actions', 'menu', 'chat', 'bubble', 'app-assist', 'decisions', 'transcript', 'settings'];
|
|
8
|
+
const callBubble = $('call-bubble');
|
|
9
|
+
const managedIds = [...panelIds, 'call-bubble'];
|
|
8
10
|
const desiredSizes = new WeakMap();
|
|
9
11
|
let panel = 'none', ambientPanel = 'none', layoutRevision = 0, layoutScheduled = false, drag, suppressClick = false;
|
|
10
12
|
const measure = node => {
|
|
@@ -21,6 +23,24 @@ export function bindPetControls(client, {
|
|
|
21
23
|
node.style.maxHeight = appliedMaxHeight;
|
|
22
24
|
return desired;
|
|
23
25
|
};
|
|
26
|
+
const hasCallBubble = () => callBubble?.querySelector('p:not([hidden])') !== null;
|
|
27
|
+
const callLayout = next => {
|
|
28
|
+
if (next !== 'actions') {
|
|
29
|
+
if (callBubble) callBubble.hidden = true;
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
const presentation = $('actions').dataset.presentation === 'controls' ? 'controls' : 'lite';
|
|
33
|
+
const call = { presentation };
|
|
34
|
+
if (callBubble && hasCallBubble()) {
|
|
35
|
+
callBubble.hidden = false;
|
|
36
|
+
call.bubble = measure(callBubble);
|
|
37
|
+
} else if (callBubble) callBubble.hidden = true;
|
|
38
|
+
return call;
|
|
39
|
+
};
|
|
40
|
+
const sizeKey = (desired, call) => [
|
|
41
|
+
desired.width, desired.height, call?.presentation ?? '',
|
|
42
|
+
call?.bubble?.width ?? '', call?.bubble?.height ?? '',
|
|
43
|
+
].join(':');
|
|
24
44
|
async function show(next) {
|
|
25
45
|
if (next === 'none' && ambientPanel !== 'none') next = ambientPanel;
|
|
26
46
|
const previousPanel = panel;
|
|
@@ -28,14 +48,24 @@ export function bindPetControls(client, {
|
|
|
28
48
|
panel = next;
|
|
29
49
|
for (const id of panelIds) $(id).hidden = id !== next;
|
|
30
50
|
body.setAttribute('aria-expanded', String(!['none', 'bubble'].includes(next)));
|
|
51
|
+
const call = callLayout(next);
|
|
31
52
|
const desired = measure(node);
|
|
32
|
-
if (node) desiredSizes.set(node,
|
|
53
|
+
if (node) desiredSizes.set(node, sizeKey(desired, call));
|
|
33
54
|
const revision = ++layoutRevision;
|
|
34
55
|
try {
|
|
35
|
-
const placed = await client.layout(next, desired.width, desired.height);
|
|
56
|
+
const placed = await client.layout(next, desired.width, desired.height, call);
|
|
36
57
|
if (revision !== layoutRevision) return;
|
|
37
58
|
$('anchor').style.left = `${placed.pet.x}px`; $('anchor').style.top = `${placed.pet.y}px`;
|
|
38
59
|
if (node) { node.style.left = `${placed.panel.x}px`; node.style.top = `${placed.panel.y}px`; node.style.maxHeight = `${placed.panel.height}px`; }
|
|
60
|
+
if (callBubble) {
|
|
61
|
+
const visible = next === 'actions' && call?.bubble && placed.bubble;
|
|
62
|
+
callBubble.hidden = !visible;
|
|
63
|
+
if (visible) {
|
|
64
|
+
callBubble.style.left = `${placed.bubble.x}px`;
|
|
65
|
+
callBubble.style.top = `${placed.bubble.y}px`;
|
|
66
|
+
callBubble.style.maxHeight = `${placed.bubble.height}px`;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
39
69
|
if (previousPanel !== next) {
|
|
40
70
|
if (next === 'chat') {
|
|
41
71
|
const mode = $('chat').dataset.replyMode;
|
|
@@ -54,6 +84,7 @@ export function bindPetControls(client, {
|
|
|
54
84
|
if (revision !== layoutRevision) return;
|
|
55
85
|
panel = previousPanel;
|
|
56
86
|
for (const id of panelIds) $(id).hidden = id !== panel;
|
|
87
|
+
if (callBubble) callBubble.hidden = panel !== 'actions' || !hasCallBubble();
|
|
57
88
|
body.setAttribute('aria-expanded', String(!['none', 'bubble'].includes(panel)));
|
|
58
89
|
error(cause, { panel: next });
|
|
59
90
|
}
|
|
@@ -65,8 +96,9 @@ export function bindPetControls(client, {
|
|
|
65
96
|
layoutScheduled = false;
|
|
66
97
|
const node = $(panel);
|
|
67
98
|
if (!node || node.hidden) return;
|
|
99
|
+
const call = callLayout(panel);
|
|
68
100
|
const desired = measure(node);
|
|
69
|
-
const key =
|
|
101
|
+
const key = sizeKey(desired, call);
|
|
70
102
|
if (desiredSizes.get(node) === key) return;
|
|
71
103
|
void show(panel);
|
|
72
104
|
});
|
|
@@ -123,20 +155,24 @@ export function bindPetControls(client, {
|
|
|
123
155
|
if (!current || current.hidden) return;
|
|
124
156
|
// Root visibility is already handled by show(). Descendant visibility and
|
|
125
157
|
// text/row mutations are content demand and may require a larger surface.
|
|
126
|
-
if (records.some(record => record.type !== 'attributes' || !
|
|
158
|
+
if (records.some(record => record.type !== 'attributes' || !managedIds.includes(record.target.id))) remeasure();
|
|
127
159
|
});
|
|
128
160
|
for (const id of panelIds) observer.observe($(id), {
|
|
129
161
|
subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['hidden'],
|
|
130
162
|
});
|
|
163
|
+
if (callBubble) observer.observe(callBubble, {
|
|
164
|
+
subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['hidden'],
|
|
165
|
+
});
|
|
131
166
|
window.addEventListener('beforeunload', () => observer.disconnect());
|
|
132
167
|
return {
|
|
133
168
|
get panel() { return panel; }, show, remeasure,
|
|
134
169
|
setAmbient(value) {
|
|
135
170
|
const next = value === true ? 'bubble' : value === false ? 'none' : value;
|
|
136
|
-
|
|
171
|
+
const ambientPanels = ['none', 'bubble', 'actions', 'app-assist'];
|
|
172
|
+
if (!ambientPanels.includes(next)) return Promise.resolve();
|
|
137
173
|
if (ambientPanel === next) return Promise.resolve();
|
|
138
174
|
ambientPanel = next;
|
|
139
|
-
return
|
|
175
|
+
return ambientPanels.includes(panel) ? show('none') : Promise.resolve();
|
|
140
176
|
},
|
|
141
177
|
dismiss() { if (drag) suppressClick = true; void show('none'); },
|
|
142
178
|
};
|
|
@@ -6,8 +6,9 @@ function referenceKey(reference) {
|
|
|
6
6
|
}
|
|
7
7
|
|
|
8
8
|
export class RecentBubble {
|
|
9
|
-
constructor(rows) {
|
|
9
|
+
constructor(rows, { showSpeaker = true } = {}) {
|
|
10
10
|
this.rows = rows;
|
|
11
|
+
this.showSpeaker = showSpeaker;
|
|
11
12
|
this.history = [];
|
|
12
13
|
this.live = new Map();
|
|
13
14
|
this.rowReferences = new WeakMap();
|
|
@@ -58,7 +59,7 @@ export class RecentBubble {
|
|
|
58
59
|
for (const [index, row] of this.rows.entries()) {
|
|
59
60
|
const entry = entries[index];
|
|
60
61
|
row.hidden = !entry;
|
|
61
|
-
const speaker = entry ? `${String(entry.name).slice(0, 28)}:` : '';
|
|
62
|
+
const speaker = entry && this.showSpeaker ? `${String(entry.name).slice(0, 28)}:` : '';
|
|
62
63
|
const message = entry ? String(entry.text).slice(-120) : '';
|
|
63
64
|
row.textContent = `${speaker}${message}`;
|
|
64
65
|
if (entry && row.ownerDocument?.createElement && typeof row.replaceChildren === 'function') {
|
|
@@ -66,6 +67,7 @@ export class RecentBubble {
|
|
|
66
67
|
const messageNode = row.ownerDocument.createElement('span');
|
|
67
68
|
const messageText = row.ownerDocument.createElement('bdi');
|
|
68
69
|
speakerNode.className = 'recent-speaker';
|
|
70
|
+
speakerNode.hidden = !speaker;
|
|
69
71
|
messageNode.className = 'recent-message';
|
|
70
72
|
messageText.className = 'recent-message-text';
|
|
71
73
|
messageText.dir = 'auto';
|
|
@@ -14,6 +14,7 @@ const fieldLabels = {
|
|
|
14
14
|
proactivePolicy: '主动提示',
|
|
15
15
|
personaTone: '人设基调',
|
|
16
16
|
householdReadsAllowed: '资料查询',
|
|
17
|
+
appAssistEnabled: '应用旁问一句',
|
|
17
18
|
};
|
|
18
19
|
|
|
19
20
|
export class SettingsView {
|
|
@@ -44,6 +45,9 @@ export class SettingsView {
|
|
|
44
45
|
this.$('settings-proactive').onclick = () => void this.controller.requestUpdate(
|
|
45
46
|
'proactivePolicy', this.controller.state.values?.proactivePolicy === 'quiet-badge' ? 'ambient' : 'quiet-badge',
|
|
46
47
|
);
|
|
48
|
+
this.$('settings-app-assist').onclick = () => void this.controller.requestUpdate(
|
|
49
|
+
'appAssistEnabled', !this.controller.state.values?.appAssistEnabled,
|
|
50
|
+
);
|
|
47
51
|
this.$('settings-documents').onclick = () => {
|
|
48
52
|
const value = !this.controller.state.values?.householdReadsAllowed;
|
|
49
53
|
void this.controller.requestUpdate('householdReadsAllowed', value, { confirm: this.callActive() });
|
|
@@ -123,6 +127,7 @@ export class SettingsView {
|
|
|
123
127
|
this.$('settings-look-value').textContent = skinLabels[values.skin] ?? values.skin;
|
|
124
128
|
this.setToggle('settings-behavior', values.behaviorEnabled);
|
|
125
129
|
this.setToggle('settings-proactive', values.proactivePolicy === 'quiet-badge');
|
|
130
|
+
this.setToggle('settings-app-assist', values.appAssistEnabled);
|
|
126
131
|
this.setToggle('settings-documents', values.householdReadsAllowed);
|
|
127
132
|
this.$('settings-size').value = String(values.ballSize);
|
|
128
133
|
this.$('settings-tone').value = values.personaTone;
|
|
@@ -139,7 +144,8 @@ export class SettingsView {
|
|
|
139
144
|
if (field === 'skin') return skinLabels[value] ?? value;
|
|
140
145
|
if (field === 'dutyCatProfileId')
|
|
141
146
|
return state.companions?.find(item => item.catProfileId === value)?.displayName ?? '已保存的陪伴者';
|
|
142
|
-
if (field === 'behaviorEnabled' || field === 'householdReadsAllowed'
|
|
147
|
+
if (field === 'behaviorEnabled' || field === 'householdReadsAllowed' || field === 'appAssistEnabled')
|
|
148
|
+
return value ? '开启' : '关闭';
|
|
143
149
|
if (field === 'proactivePolicy') return value === 'quiet-badge' ? '开启' : '关闭';
|
|
144
150
|
if (field === 'ballSize') return `${value}`;
|
|
145
151
|
return value;
|
|
@@ -169,12 +175,13 @@ export class SettingsView {
|
|
|
169
175
|
|
|
170
176
|
renderSettlement(state) {
|
|
171
177
|
for (const id of ['settings-partner-open', 'settings-look-open', 'settings-behavior',
|
|
172
|
-
'settings-proactive', 'settings-documents', 'settings-tone-field', 'settings-size-field'])
|
|
178
|
+
'settings-proactive', 'settings-app-assist', 'settings-documents', 'settings-tone-field', 'settings-size-field'])
|
|
173
179
|
delete this.$(id).dataset.settlement;
|
|
174
180
|
if (state.notice?.kind !== 'unconfirmed') return;
|
|
175
181
|
const ids = {
|
|
176
182
|
dutyCatProfileId: 'settings-partner-open', skin: 'settings-look-open',
|
|
177
183
|
behaviorEnabled: 'settings-behavior', proactivePolicy: 'settings-proactive',
|
|
184
|
+
appAssistEnabled: 'settings-app-assist',
|
|
178
185
|
householdReadsAllowed: 'settings-documents', personaTone: 'settings-tone-field',
|
|
179
186
|
ballSize: 'settings-size-field',
|
|
180
187
|
};
|
package/renderer/style.css
CHANGED
|
@@ -78,6 +78,16 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
78
78
|
#bubble p { margin: 0; display: flex; min-width: 0; font-size: 11px; line-height: 1.45; overflow: hidden; white-space: nowrap; }
|
|
79
79
|
#bubble p + p { margin-top: 4px; }
|
|
80
80
|
#bubble [data-role="user"] { color: var(--cafe-text-secondary); }
|
|
81
|
+
.app-assist { width: 320px; padding: 12px; }
|
|
82
|
+
.app-assist p { margin: 0; color: var(--desktop-body); font-size: 13px; line-height: 1.5; }
|
|
83
|
+
.app-assist p strong { color: var(--desktop-ink); font-weight: 600; }
|
|
84
|
+
.app-assist > div { display: flex; justify-content: flex-end; gap: 6px; margin-top: 9px; }
|
|
85
|
+
.app-assist > div button { min-height: 30px; padding: 5px 10px; font-size: 12px; }
|
|
86
|
+
#call-bubble { width: 320px; max-height: 80px; overflow: hidden; padding: 9px 11px; pointer-events: none; }
|
|
87
|
+
#call-bubble:not(:has(p:not([hidden]))) { display: none; }
|
|
88
|
+
#call-bubble p { margin: 0; display: flex; min-width: 0; overflow: hidden; white-space: nowrap; font-size: 12px; line-height: 1.45; }
|
|
89
|
+
#call-bubble p + p { margin-top: 3px; }
|
|
90
|
+
#call-bubble p[data-role="user"] { color: var(--desktop-muted); }
|
|
81
91
|
.floating button { border: 0; background: transparent; border-radius: 7px; padding: 8px 11px; font-weight: 400; }
|
|
82
92
|
.floating button:hover:not(:disabled) { background: var(--cafe-surface); }
|
|
83
93
|
.floating .primary { background: var(--desktop-primary); color: white; }
|
|
@@ -85,24 +95,22 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
85
95
|
.floating .destructive { background: var(--desktop-danger); color: white; font-weight: 500; }
|
|
86
96
|
.floating .destructive:hover:not(:disabled) { background: #942c24; }
|
|
87
97
|
.floating .destructive.compact { min-width: 72px; padding: 6px 12px; }
|
|
88
|
-
#actions {
|
|
89
|
-
#actions[data-presentation="lite"] { width:
|
|
90
|
-
#actions[data-presentation="lite"] > :not(.call-
|
|
91
|
-
#actions[data-presentation="lite"] .call-
|
|
98
|
+
#actions { width: 412px; overflow: hidden; padding: 10px; }
|
|
99
|
+
#actions[data-presentation="lite"] { width: max-content; max-width: 320px; border: 0; background: transparent; box-shadow: none; padding: 0; overflow: visible; }
|
|
100
|
+
#actions[data-presentation="lite"] > :not(.call-header) { display: none !important; }
|
|
101
|
+
#actions[data-presentation="lite"] .call-header { min-height: 29px; width: fit-content; border: 1px solid #fff3; border-radius: 8px; background: var(--desktop-ink); box-shadow: 0 3px 8px #1817152e; color: var(--desktop-paper); padding: 5px 10px; }
|
|
102
|
+
#actions[data-presentation="lite"] .call-header > :not(.call-status-line) { display: none !important; }
|
|
92
103
|
#actions[data-presentation="lite"] .call-status-line strong { color: inherit; font-size: 12px; }
|
|
93
104
|
#actions[data-presentation="lite"] .call-status-line span:not(.status-dot) { color: #d8d1c6; font-size: 11px; }
|
|
94
|
-
|
|
95
|
-
#actions[data-presentation="lite"] .call-recent:has(p:not([hidden])) { display: block; }
|
|
96
|
-
#actions[data-presentation="lite"] .call-recent-label { display: none; }
|
|
97
|
-
#actions[data-presentation="lite"] .call-recent p { margin-top: 0; }
|
|
105
|
+
.call-header { min-width: 0; min-height: 24px; display: flex; align-items: center; gap: 7px; }
|
|
98
106
|
.action-row { display: flex; align-items: center; gap: 4px; }
|
|
99
|
-
.action-row button { display: flex; align-items: center; justify-content: center; gap:
|
|
107
|
+
.action-row button { display: flex; align-items: center; justify-content: center; gap: 5px; min-height: 28px; height: 28px; padding: 4px 8px; font-size: 12px; white-space: nowrap; }
|
|
100
108
|
.action-row button .icon { width: 16px; height: 16px; }
|
|
101
|
-
#active-actions {
|
|
102
|
-
#lite-call-compose { display: none; align-items: center; gap:
|
|
109
|
+
#active-actions { margin-top: 6px; }
|
|
110
|
+
#lite-call-compose { display: none; align-items: center; gap: 7px; margin-top: 6px; }
|
|
103
111
|
#actions[data-presentation="controls"] #lite-call-compose { display: flex; }
|
|
104
|
-
#lite-call-message { min-width: 0; height:
|
|
105
|
-
#lite-call-compose button { height:
|
|
112
|
+
#lite-call-message { min-width: 0; height: 30px; flex: 1; border: 1px solid var(--desktop-line-strong); border-radius: 7px; background: var(--desktop-paper); color: var(--desktop-ink); padding: 5px 9px; }
|
|
113
|
+
#lite-call-compose button { height: 30px; min-height: 30px; padding: 5px 11px; }
|
|
106
114
|
#status { font-size: 12px; line-height: 1.45; margin: 8px 0 0; color: var(--desktop-muted); }
|
|
107
115
|
.call-status-line { min-height: 24px; display: flex; align-items: center; gap: 7px; white-space: nowrap; }
|
|
108
116
|
.call-status-line strong { font-size: 13px; font-weight: 600; }
|
|
@@ -139,6 +147,10 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
139
147
|
to { opacity: 0; transform: scale(3); }
|
|
140
148
|
}
|
|
141
149
|
.call-meeting { min-width: 0; margin: 3px 0 0 15px; display: flex; align-items: center; gap: 5px; color: var(--desktop-muted); font-size: 12px; line-height: 1.35; }
|
|
150
|
+
#actions .call-meeting { margin: 0; overflow: hidden; flex: 0 1 auto; }
|
|
151
|
+
#actions .call-work { min-width: 0; margin: 0; overflow: hidden; flex: 0 1 auto; text-overflow: ellipsis; white-space: nowrap; }
|
|
152
|
+
#actions .call-work::before { content: "· "; }
|
|
153
|
+
.call-duration { margin-left: auto; flex: none; color: var(--desktop-muted); font-size: 11px; font-variant-numeric: tabular-nums; }
|
|
142
154
|
.call-context > .call-meeting { grid-column: 1 / -1; margin: 0; }
|
|
143
155
|
.meeting-status-dot { width: 7px; height: 7px; flex: none; border-radius: 999px; background: var(--desktop-success); }
|
|
144
156
|
.call-meeting[data-tone="quiet"] .meeting-status-dot { background: var(--desktop-muted); }
|
|
@@ -146,21 +158,19 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
146
158
|
.meeting-label { flex: none; white-space: nowrap; }
|
|
147
159
|
.meeting-source { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
148
160
|
.call-work { margin: 3px 0 0 15px; color: var(--desktop-muted); font-size: 12px; }
|
|
149
|
-
.call-share-context { display: flex; align-items: center; gap:
|
|
161
|
+
.call-share-context { min-width: 0; display: flex; align-items: center; gap: 5px; margin-top: 4px; color: var(--desktop-muted); white-space: nowrap; }
|
|
162
|
+
.call-share-context .icon { width: 14px; height: 14px; }
|
|
163
|
+
.call-share-context strong { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
164
|
+
.call-share-context small { flex: none; }
|
|
150
165
|
.call-share-context > span, .call-share-line > span { display: grid; gap: 1px; min-width: 0; }
|
|
151
166
|
.call-share-context strong, .call-share-line strong { overflow: hidden; color: var(--desktop-body); font-size: 12px; font-weight: 500; text-overflow: ellipsis; white-space: nowrap; }
|
|
152
167
|
.call-share-context small, .call-share-line small { color: var(--desktop-muted); font-size: 11px; }
|
|
153
|
-
.call-recent { margin-top: 8px; padding-top: 8px; border-top: 1px solid var(--desktop-line); }
|
|
154
|
-
.call-recent-label { color: var(--desktop-muted); font-size: 11px; letter-spacing: .05em; }
|
|
155
|
-
.call-recent p { margin: 4px 0 0; display: flex; min-width: 0; overflow: hidden; white-space: nowrap; font-size: 12px; line-height: 1.45; }
|
|
156
168
|
.recent-speaker { flex: none; }
|
|
157
169
|
.recent-message { min-width: 0; flex: 1 1 auto; overflow: hidden; direction: rtl; text-align: left; text-overflow: ellipsis; white-space: nowrap; }
|
|
158
170
|
.recent-message[data-direction="rtl"] { direction: ltr; }
|
|
159
171
|
.recent-message-text { unicode-bidi: isolate; }
|
|
160
|
-
.call-recent p[data-role="user"] { color: var(--desktop-muted); }
|
|
161
172
|
.call-actions { justify-content: flex-end; }
|
|
162
|
-
.call-actions #
|
|
163
|
-
.call-actions #end { min-width: 88px; margin-left: auto; }
|
|
173
|
+
.call-actions #end { min-width: 60px; margin-left: auto; }
|
|
164
174
|
.action-unread { min-width: 17px; height: 17px; display: inline-grid; place-items: center; border-radius: 999px; padding: 0 5px; background: #f2dcc8; color: #7a3f2e; font-size: 10px; font-weight: 700; line-height: 1; }
|
|
165
175
|
#menu { width: 340px; max-height: 500px; display: flex; flex-direction: column; overflow: hidden; padding: 8px; }
|
|
166
176
|
.menu-scroll { min-height: 0; flex: 1 1 auto; overflow-x: hidden; overflow-y: auto; }
|
package/renderer/surface.mjs
CHANGED
|
@@ -22,6 +22,8 @@ import { MeetingStatus, renderMeetingStatus } from './meeting-state.mjs';
|
|
|
22
22
|
import { VisibleTextPresentation } from './visible-text-receipt.mjs';
|
|
23
23
|
import { ConversationSurface } from './conversation-surface.mjs';
|
|
24
24
|
import { CallControlsPresentation } from './call-controls-presentation.mjs';
|
|
25
|
+
import { CallDuration } from './call-duration.mjs';
|
|
26
|
+
import { AppAssistController } from './app-assist-controller.mjs';
|
|
25
27
|
const $ = id => document.getElementById(id);
|
|
26
28
|
const label = (id, text) => { $(id).querySelector('.label').textContent = text; };
|
|
27
29
|
const status = text => {
|
|
@@ -37,7 +39,8 @@ const callTranscript = new CallTranscriptView($('call-transcript-log'), {
|
|
|
37
39
|
},
|
|
38
40
|
});
|
|
39
41
|
const bubble = new RecentBubble([$('bubble-first'), $('bubble-second')]);
|
|
40
|
-
const callBubble = new RecentBubble([$('call-bubble-first'), $('call-bubble-second')]);
|
|
42
|
+
const callBubble = new RecentBubble([$('call-bubble-first'), $('call-bubble-second')], { showSpeaker: false });
|
|
43
|
+
const callDuration = new CallDuration($('call-duration'));
|
|
41
44
|
const meetingStatus = new MeetingStatus();
|
|
42
45
|
const foldedChatUnread = new FoldedChatUnread(count => {
|
|
43
46
|
$('call-chat-unread').hidden = count === 0;
|
|
@@ -51,10 +54,11 @@ const nativeTravel = new NativeWindowTravel({
|
|
|
51
54
|
changed: value => motion.syncTravel(value),
|
|
52
55
|
});
|
|
53
56
|
let sharing = false, pendingScreen = false, transcriptLoading = false, previousPhase = 'idle';
|
|
57
|
+
let appAssistOffer = null;
|
|
54
58
|
let didMove = false, settingsView, settingsValues, decisionPanel, workView, callControlsPresentation;
|
|
55
59
|
let previousControlPanel = 'none';
|
|
56
60
|
// F229 stays quiet: a history ID change has no provenance for proactive text.
|
|
57
|
-
const ambientPanel = active => active ? 'actions' : 'none';
|
|
61
|
+
const ambientPanel = active => active ? 'actions' : appAssistOffer ? 'app-assist' : 'none';
|
|
58
62
|
const setTexts = (ids, text) => { for (const id of ids) $(id).textContent = text; };
|
|
59
63
|
const setHidden = (ids, hidden) => { for (const id of ids) $(id).hidden = hidden; };
|
|
60
64
|
if (!window.clowderCompanion) {
|
|
@@ -135,6 +139,19 @@ if (!window.clowderCompanion) {
|
|
|
135
139
|
accepted: () => { motion.signal('received'); void conversationSurface.readHistory(); },
|
|
136
140
|
changed: () => controls.remeasure(),
|
|
137
141
|
});
|
|
142
|
+
const appAssist = new AppAssistController({
|
|
143
|
+
client,
|
|
144
|
+
changed: value => {
|
|
145
|
+
appAssistOffer = value.offer;
|
|
146
|
+
$('app-assist-name').textContent = appAssistOffer?.appName ?? '';
|
|
147
|
+
void controls.setAmbient(ambientPanel(previousPhase !== 'idle'));
|
|
148
|
+
},
|
|
149
|
+
accepted: () => {
|
|
150
|
+
status('提示已收起。请先点“语音通话”,连上后再点“共享画面”。');
|
|
151
|
+
$('menu-status').hidden = false;
|
|
152
|
+
void controls.show('menu');
|
|
153
|
+
},
|
|
154
|
+
});
|
|
138
155
|
const screen = new ScreenShare({
|
|
139
156
|
screenRequest: () => client.screenPick(), screenStart: (selectionId, name) => client.screenOpen(selectionId, name),
|
|
140
157
|
screenFrame: (selectionId, frame) => client.screenFrame(selectionId, {
|
|
@@ -193,7 +210,9 @@ if (!window.clowderCompanion) {
|
|
|
193
210
|
const canReceiveOnly = value.identity?.audio?.supportedModes?.includes('receive_only') === true;
|
|
194
211
|
const retryReceiveOnly = value.failed && value.retryMode === 'receive_only' && canReceiveOnly;
|
|
195
212
|
visibleText.setCall(value.callId);
|
|
213
|
+
callDuration.set(value.connectedAt);
|
|
196
214
|
callControlsPresentation.setActive(active);
|
|
215
|
+
if (value.identity?.appAssist) appAssist.consume(value.identity.appAssist);
|
|
197
216
|
void controls.setAmbient(ambientPanel(active));
|
|
198
217
|
$('begin').hidden = active; $('begin').disabled = !value.identity;
|
|
199
218
|
$('listen').hidden = active || !canReceiveOnly; $('listen').disabled = !value.identity;
|
|
@@ -212,7 +231,7 @@ if (!window.clowderCompanion) {
|
|
|
212
231
|
$('compose').querySelector('button').disabled = !value.identity || value.phase === 'connecting';
|
|
213
232
|
callControlsPresentation.setComposerState(value.identity, value.phase === 'connecting');
|
|
214
233
|
status(value.message ?? '');
|
|
215
|
-
$('status').hidden = !value.message || /^(
|
|
234
|
+
$('status').hidden = !value.message || /^(点击|正在听|麦克风已静音|只听模式|只听已结束|语音通话已结束|已发送)/.test(value.message);
|
|
216
235
|
$('menu-status').hidden = !value.message;
|
|
217
236
|
const callState = value.recovering ? '连接不稳定' : value.phase === 'connecting' ? '正在连接'
|
|
218
237
|
: receiveOnly ? '只听中' : active ? '通话中' : '';
|
|
@@ -278,6 +297,8 @@ if (!window.clowderCompanion) {
|
|
|
278
297
|
if (showFailure) void controls.show('menu');
|
|
279
298
|
},
|
|
280
299
|
});
|
|
300
|
+
$('app-assist-dismiss').onclick = () => { void appAssist.settle('dismiss'); };
|
|
301
|
+
$('app-assist-start').onclick = () => { void appAssist.settle('accept'); };
|
|
281
302
|
conversationSurface = new ConversationSurface({
|
|
282
303
|
document, client, controls, bubble, transcript,
|
|
283
304
|
getCallPhase: () => conversation.phase,
|
|
@@ -344,7 +365,7 @@ if (!window.clowderCompanion) {
|
|
|
344
365
|
const decisionMonitor = setInterval(() => { if (controls.panel !== 'decisions') void decisionPanel.read(); }, 15000), workMonitor = setInterval(() => { if (controls.panel === 'menu') void workView.load(); }, 15000);
|
|
345
366
|
void conversation.refresh(); conversationSurface.start(); void decisionPanel.read(); void settingsView.controller.load(); void controls.show('none');
|
|
346
367
|
window.addEventListener('beforeunload', () => {
|
|
347
|
-
clearInterval(statusMonitor); clearInterval(transcriptMonitor); clearInterval(decisionMonitor); clearInterval(workMonitor); conversationSurface.close(); callControlsPresentation.close();
|
|
348
|
-
unsubscribe(); visibleText.close(); nativeTravel.close(); motion.close(); void conversation.end();
|
|
368
|
+
clearInterval(statusMonitor); clearInterval(transcriptMonitor); clearInterval(decisionMonitor); clearInterval(workMonitor); conversationSurface.close(); callControlsPresentation.close(); callDuration.close();
|
|
369
|
+
unsubscribe(); appAssist.close(); visibleText.close(); nativeTravel.close(); motion.close(); void conversation.end();
|
|
349
370
|
});
|
|
350
371
|
}
|