@clowder-ai/companion 0.1.0-alpha.8 → 0.1.0-alpha.9
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 +10 -6
- package/manifest.json +2 -2
- package/package.json +3 -3
- package/plugin.yaml +2 -2
- package/renderer/index.html +1 -0
- package/renderer/living-body.mjs +121 -33
- package/renderer/living-edge.mjs +9 -3
- package/renderer/motion-facts.mjs +79 -0
- package/renderer/native-work-motion.mjs +24 -0
- package/renderer/pet-motion.mjs +233 -66
- package/renderer/skins/xianxian-living/clips/peek_fade.webm +0 -0
- package/renderer/skins/xianxian-living/clips/sleep.webm +0 -0
- package/renderer/skins/xianxian-living/clips/wake.webm +0 -0
- package/renderer/style.css +7 -2
- package/renderer/surface.mjs +11 -4
- package/source-lock.json +8 -4
package/README.md
CHANGED
|
@@ -24,12 +24,16 @@ Hosts reject this manifest before installation. Media admission and capture
|
|
|
24
24
|
remain in the trusted Host; this package receives neither streams nor provider
|
|
25
25
|
SDP. Hiding releases media; the existing Clowder **聊聊** entry restores the cat.
|
|
26
26
|
|
|
27
|
-
Xianxian uses one layered sitting body or one VP9 clip at a time.
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
27
|
+
Xianxian uses one layered sitting body or one VP9 clip at a time. Expiring Host
|
|
28
|
+
facts with task, result and event identities select tool work, workspace fetch,
|
|
29
|
+
screen reading, applied-result delivery and autonomous target movement.
|
|
30
|
+
Dragging instead freezes a visibly held candidate and cancels the old target;
|
|
31
|
+
it never renames the running clip. Pounce requires an explicit play gesture by
|
|
32
|
+
default; optional automatic play is bounded by idle and cooldown timers.
|
|
33
|
+
Reduced motion keeps semantic poses static and suppresses roaming and pounce.
|
|
34
|
+
Other cats retain their own skins. The v3 living assets and their hashes are
|
|
35
|
+
pinned in `source-lock.json`; the installable renderer carries all 21 assets
|
|
36
|
+
itself, including the mid-edge `peek_fade` variant.
|
|
33
37
|
|
|
34
38
|
The Host keeps a small transparent margin left of the pet hit target so the
|
|
35
39
|
pounce's tail is visible without turning empty space into a click target.
|
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.9",
|
|
4
4
|
"contractVersion": "0.1.0",
|
|
5
5
|
"name": "猫猫球",
|
|
6
6
|
"icon": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"bridgeVersion": "1.3.0",
|
|
41
41
|
"surface": {
|
|
42
42
|
"entrypoint": "renderer/index.html",
|
|
43
|
-
"integrity": "sha256-
|
|
43
|
+
"integrity": "sha256-x3ponGs/dhxPH5D1rT9JrLqJpFA6rxrgMjSImhksoHQ="
|
|
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.9",
|
|
4
4
|
"description": "The desktop voice body for your Clowder companion",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -26,8 +26,8 @@
|
|
|
26
26
|
"prepack": "pnpm build"
|
|
27
27
|
},
|
|
28
28
|
"devDependencies": {
|
|
29
|
-
"@clowder-ai/plugin-contract": "0.1.0-beta.
|
|
30
|
-
"@clowder-ai/plugin-sdk": "0.1.0-beta.
|
|
29
|
+
"@clowder-ai/plugin-contract": "0.1.0-beta.19",
|
|
30
|
+
"@clowder-ai/plugin-sdk": "0.1.0-beta.15"
|
|
31
31
|
},
|
|
32
32
|
"repository": {
|
|
33
33
|
"type": "git",
|
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.9",
|
|
4
4
|
"contractVersion": "0.1.0",
|
|
5
5
|
"name": "猫猫球",
|
|
6
6
|
"icon": {
|
|
@@ -40,7 +40,7 @@
|
|
|
40
40
|
"bridgeVersion": "1.3.0",
|
|
41
41
|
"surface": {
|
|
42
42
|
"entrypoint": "renderer/index.html",
|
|
43
|
-
"integrity": "sha256-
|
|
43
|
+
"integrity": "sha256-x3ponGs/dhxPH5D1rT9JrLqJpFA6rxrgMjSImhksoHQ="
|
|
44
44
|
},
|
|
45
45
|
"presentation": {
|
|
46
46
|
"width": 300,
|
package/renderer/index.html
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
</g>
|
|
34
34
|
</svg>
|
|
35
35
|
<video id="living-video" muted playsinline preload="metadata" aria-hidden="true" hidden></video>
|
|
36
|
+
<video id="living-video-transition" muted playsinline preload="metadata" aria-hidden="true" hidden></video>
|
|
36
37
|
</button>
|
|
37
38
|
<div id="indicators">
|
|
38
39
|
<button id="call-badge" hidden aria-label="结束语音" title="结束语音"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v4M8 22h8"/></svg></button>
|
package/renderer/living-body.mjs
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
|
-
// F317 living Xianxian package
|
|
2
|
-
// Values are the package-v1 CSS anchors scaled from its 290px stage to the Host pet.
|
|
1
|
+
// F317 living Xianxian package v3. Values are CSS anchors from its reviewed manifest.
|
|
3
2
|
const scale = 120 / 290;
|
|
4
3
|
const groundY = 10 + 262.5 * scale;
|
|
5
4
|
const clips = {
|
|
@@ -7,77 +6,166 @@ const clips = {
|
|
|
7
6
|
'running-right': { name: 'trot', size: [265, 236], ground: 234.8, centre: 132.5, loop: true },
|
|
8
7
|
running: { name: 'trot', size: [265, 236], ground: 234.8, centre: 132.5, loop: true },
|
|
9
8
|
play: { name: 'pounce', size: [304.8, 312.6], ground: 303.9, centre: 201.9, rate: 1.5 },
|
|
10
|
-
sleeping: { name: 'sleep', size: [
|
|
11
|
-
waking: { name: 'wake', size: [
|
|
9
|
+
sleeping: { name: 'sleep', size: [271.2, 168], ground: 161, centre: 133.2, loop: true },
|
|
10
|
+
waking: { name: 'wake', size: [400.2, 282.7], ground: 244.2, centre: 200.5,
|
|
11
|
+
endGround: 277.4, endCentre: 197.7, durationMs: 7_250, crossfadeMs: 250 },
|
|
12
12
|
peek: { name: 'peek', size: [153.5, 234.3], ground: 234.3, centre: 76.75, loop: true },
|
|
13
|
+
delivering: { name: 'carry', size: [344.3, 290.9], ground: 263, centre: 198.3 },
|
|
13
14
|
review: { name: 'carry', size: [344.3, 290.9], ground: 263, centre: 198.3 },
|
|
14
15
|
pending_decision: { name: 'wait', size: [197.9, 252.8], ground: 249.6, centre: 98.4, loop: true },
|
|
15
16
|
working: { name: 'think', size: [266.4, 256.9], ground: 253.7, centre: 132.1, loop: true },
|
|
17
|
+
fetching: { name: 'think', size: [266.4, 256.9], ground: 253.7, centre: 132.1, loop: true },
|
|
18
|
+
studying: { name: 'think', size: [266.4, 256.9], ground: 253.7, centre: 132.1, loop: true },
|
|
16
19
|
staged_thought: { name: 'think', size: [266.4, 256.9], ground: 253.7, centre: 132.1, loop: true },
|
|
17
20
|
};
|
|
18
21
|
export const isLivingClip = action => Object.hasOwn(clips, action);
|
|
19
22
|
const setHidden = (node, hidden) => {
|
|
20
|
-
|
|
21
|
-
// attribute; the renderer's [hidden] rule reads the attribute itself.
|
|
23
|
+
if (!node) return;
|
|
22
24
|
node.toggleAttribute?.('hidden', hidden);
|
|
23
25
|
node.hidden = hidden;
|
|
24
26
|
};
|
|
25
27
|
|
|
26
|
-
/** One visible body: a layered sit or one muted clip,
|
|
28
|
+
/** One visible body: a layered sit or one muted clip, with a bounded sleep/wake hand-off. */
|
|
27
29
|
export class LivingBody {
|
|
28
|
-
constructor({ root, sit, video }) {
|
|
30
|
+
constructor({ root, sit, video, transitionVideo, setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout }) {
|
|
29
31
|
this.root = root;
|
|
30
32
|
this.sit = sit;
|
|
31
33
|
this.video = video;
|
|
34
|
+
this.standby = transitionVideo;
|
|
35
|
+
this.setTimer = (callback, delay) => setTimer.call(globalThis, callback, delay);
|
|
36
|
+
this.clearTimer = id => clearTimer.call(globalThis, id);
|
|
32
37
|
this.action = '';
|
|
33
38
|
this.revision = 0;
|
|
34
39
|
}
|
|
40
|
+
|
|
35
41
|
show(action, { reducedMotion = false, onEnded = () => {} } = {}) {
|
|
36
42
|
const dockEdge = this.root.dataset.dockEdge;
|
|
37
|
-
|
|
43
|
+
const dockZone = this.root.dataset.dockZone;
|
|
44
|
+
if (this.action === action && this.reducedMotion === reducedMotion && this.dockEdge === dockEdge && this.dockZone === dockZone) return;
|
|
45
|
+
const previous = this.action;
|
|
38
46
|
this.action = action;
|
|
39
47
|
this.reducedMotion = reducedMotion;
|
|
40
48
|
this.dockEdge = dockEdge;
|
|
49
|
+
this.dockZone = dockZone;
|
|
41
50
|
const revision = ++this.revision;
|
|
42
|
-
|
|
43
|
-
this.root.dataset.pose = action
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
51
|
+
this.cancelFade();
|
|
52
|
+
this.root.dataset.pose = this.pose(action);
|
|
53
|
+
this.root.dataset.direction = action === 'running-left' || (action === 'peek' && dockEdge === 'left') ? 'left' : 'right';
|
|
54
|
+
const base = clips[action];
|
|
55
|
+
const clip = !reducedMotion && base
|
|
56
|
+
? { ...base, name: action === 'peek' && dockZone !== 'corner' ? 'peek_fade' : base.name }
|
|
57
|
+
: null;
|
|
48
58
|
if (!clip) {
|
|
49
|
-
|
|
59
|
+
this.hideVideos();
|
|
50
60
|
setHidden(this.sit, false);
|
|
51
61
|
return;
|
|
52
62
|
}
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
this.video
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
63
|
+
const crossfade = previous === 'sleeping' && action === 'waking' && this.standby;
|
|
64
|
+
if (crossfade) this.crossfade(clip, revision, onEnded);
|
|
65
|
+
else this.showClip(this.video, clip, revision, onEnded);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
pose(action) {
|
|
69
|
+
if (action === 'failed') return 'scared';
|
|
70
|
+
if (action === 'waiting') return 'listening';
|
|
71
|
+
if (['sleeping', 'pending_decision', 'held', 'working', 'fetching', 'studying'].includes(action))
|
|
72
|
+
return action === 'pending_decision' ? 'pending' : action;
|
|
73
|
+
return 'sit';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
showClip(video, clip, revision, onEnded) {
|
|
77
|
+
this.retireVideo(this.standby);
|
|
78
|
+
this.configure(video, clip, revision, onEnded);
|
|
79
|
+
video.style.opacity = '1';
|
|
80
|
+
setHidden(this.sit, true);
|
|
81
|
+
setHidden(video, false);
|
|
82
|
+
this.play(video, revision, onEnded);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
crossfade(clip, revision, onEnded) {
|
|
86
|
+
const outgoing = this.video;
|
|
87
|
+
const incoming = this.standby;
|
|
88
|
+
this.configure(incoming, clip, revision, onEnded);
|
|
89
|
+
incoming.style.transition = `opacity ${clip.crossfadeMs}ms linear`;
|
|
90
|
+
outgoing.style.transition = `opacity ${clip.crossfadeMs}ms linear`;
|
|
91
|
+
incoming.style.opacity = '0';
|
|
92
|
+
outgoing.style.opacity = '1';
|
|
64
93
|
setHidden(this.sit, true);
|
|
65
|
-
setHidden(
|
|
66
|
-
|
|
94
|
+
setHidden(outgoing, false);
|
|
95
|
+
setHidden(incoming, false);
|
|
96
|
+
this.play(incoming, revision, onEnded);
|
|
97
|
+
incoming.style.opacity = '1';
|
|
98
|
+
outgoing.style.opacity = '0';
|
|
99
|
+
this.video = incoming;
|
|
100
|
+
this.standby = outgoing;
|
|
101
|
+
this.fadeTimer = this.setTimer(() => {
|
|
102
|
+
this.fadeTimer = undefined;
|
|
67
103
|
if (revision !== this.revision) return;
|
|
68
|
-
|
|
104
|
+
this.retireVideo(outgoing);
|
|
105
|
+
}, clip.crossfadeMs);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
configure(video, clip, revision, onEnded) {
|
|
109
|
+
video.onended = null;
|
|
110
|
+
video.pause();
|
|
111
|
+
video.src = `./skins/xianxian-living/clips/${clip.name}.webm`;
|
|
112
|
+
video.loop = clip.loop === true;
|
|
113
|
+
video.playbackRate = clip.rate ?? 1;
|
|
114
|
+
video.style.width = `${clip.size[0] * scale}px`;
|
|
115
|
+
video.style.height = `${clip.size[1] * scale}px`;
|
|
116
|
+
const left = this.action === 'peek'
|
|
117
|
+
? (this.root.dataset.dockEdge === 'left' ? -32 : 128 - clip.size[0] * scale)
|
|
118
|
+
: 60 - clip.centre * scale;
|
|
119
|
+
const top = this.action === 'peek' ? 138 - clip.size[1] * scale : groundY - clip.ground * scale;
|
|
120
|
+
video.style.left = `${left}px`;
|
|
121
|
+
video.style.top = `${top}px`;
|
|
122
|
+
video.style.animation = clip.endGround
|
|
123
|
+
? `living-wake-anchor ${clip.durationMs}ms linear forwards` : 'none';
|
|
124
|
+
if (clip.endGround) {
|
|
125
|
+
video.style.setProperty?.('--living-left-start', `${left}px`);
|
|
126
|
+
video.style.setProperty?.('--living-top-start', `${top}px`);
|
|
127
|
+
video.style.setProperty?.('--living-left-end', `${60 - clip.endCentre * scale}px`);
|
|
128
|
+
video.style.setProperty?.('--living-top-end', `${groundY - clip.endGround * scale}px`);
|
|
129
|
+
}
|
|
130
|
+
try { video.currentTime = 0; } catch { /* New source is not yet seekable. */ }
|
|
131
|
+
video.onended = () => { if (revision === this.revision) onEnded(); };
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
play(video, revision, onEnded) {
|
|
135
|
+
void video.play().catch(() => {
|
|
136
|
+
if (revision !== this.revision) return;
|
|
137
|
+
this.hideVideos();
|
|
69
138
|
setHidden(this.sit, false);
|
|
70
139
|
onEnded();
|
|
71
140
|
});
|
|
72
141
|
}
|
|
142
|
+
|
|
143
|
+
hideVideos() {
|
|
144
|
+
for (const video of [this.video, this.standby]) {
|
|
145
|
+
this.retireVideo(video);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
retireVideo(video) {
|
|
150
|
+
if (!video) return;
|
|
151
|
+
video.onended = null;
|
|
152
|
+
video.pause();
|
|
153
|
+
setHidden(video, true);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
cancelFade() {
|
|
157
|
+
if (this.fadeTimer !== undefined) this.clearTimer(this.fadeTimer);
|
|
158
|
+
this.fadeTimer = undefined;
|
|
159
|
+
}
|
|
160
|
+
|
|
73
161
|
close() {
|
|
74
162
|
this.revision++;
|
|
75
|
-
this.
|
|
76
|
-
this.
|
|
163
|
+
this.cancelFade();
|
|
164
|
+
this.hideVideos();
|
|
77
165
|
}
|
|
166
|
+
|
|
78
167
|
hide() {
|
|
79
168
|
this.close();
|
|
80
|
-
setHidden(this.video, true);
|
|
81
169
|
setHidden(this.sit, true);
|
|
82
170
|
this.action = '';
|
|
83
171
|
}
|
package/renderer/living-edge.mjs
CHANGED
|
@@ -4,7 +4,13 @@ export function detectDockedEdge(win, petRect) {
|
|
|
4
4
|
const width = win.screen?.availWidth;
|
|
5
5
|
if (![win.screenX, left, width, petRect.left, petRect.width].every(Number.isFinite) || width <= 0) return null;
|
|
6
6
|
const petLeft = win.screenX + petRect.left;
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
return null;
|
|
7
|
+
const edge = petLeft - left <= 40 ? 'left'
|
|
8
|
+
: left + width - petLeft - petRect.width <= 40 ? 'right' : null;
|
|
9
|
+
if (!edge) return null;
|
|
10
|
+
const top = win.screen?.availTop;
|
|
11
|
+
const height = win.screen?.availHeight;
|
|
12
|
+
const vertical = [win.screenY, top, height, petRect.top, petRect.height];
|
|
13
|
+
const atBottom = vertical.every(Number.isFinite) && height > 0
|
|
14
|
+
&& top + height - (win.screenY + petRect.top + petRect.height) <= 40;
|
|
15
|
+
return { edge, zone: atBottom ? 'corner' : 'mid' };
|
|
10
16
|
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const validId = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
2
|
+
const validExpiry = (value, now) => Number.isFinite(value) && value > now;
|
|
3
|
+
const workKinds = new Set(['reasoning', 'tool', 'workspace', 'screen_reading']);
|
|
4
|
+
|
|
5
|
+
/** A bounded, expiring projection of Host-owned motion facts. */
|
|
6
|
+
export class MotionFacts {
|
|
7
|
+
constructor({ now, setTimer, clearTimer, changed }) {
|
|
8
|
+
this.now = now;
|
|
9
|
+
this.setTimer = setTimer;
|
|
10
|
+
this.clearTimer = clearTimer;
|
|
11
|
+
this.changed = changed;
|
|
12
|
+
this.snapshot = { work: null, delivery: null, travel: null };
|
|
13
|
+
this.seenResults = new Map();
|
|
14
|
+
this.suppressedTravel = new Map();
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
sync(value, suppressTravel = false) {
|
|
18
|
+
const now = this.now();
|
|
19
|
+
this.prune(now);
|
|
20
|
+
const input = value && typeof value === 'object' ? value : {};
|
|
21
|
+
const work = input.work;
|
|
22
|
+
const delivery = input.delivery;
|
|
23
|
+
const travel = input.travel;
|
|
24
|
+
this.snapshot = {
|
|
25
|
+
work: work && validId(work.taskId) && work.status === 'active' && workKinds.has(work.kind)
|
|
26
|
+
&& validExpiry(work.expiresAt, now)
|
|
27
|
+
? { taskId: work.taskId, kind: work.kind, expiresAt: work.expiresAt } : null,
|
|
28
|
+
delivery: delivery && validId(delivery.resultId) && delivery.status === 'applied'
|
|
29
|
+
&& validExpiry(delivery.expiresAt, now)
|
|
30
|
+
? { resultId: delivery.resultId, expiresAt: delivery.expiresAt } : null,
|
|
31
|
+
travel: travel && validId(travel.eventId) && travel.status === 'active' && Number.isFinite(travel.dx)
|
|
32
|
+
&& Number.isFinite(travel.dy) && Math.hypot(travel.dx, travel.dy) >= 1
|
|
33
|
+
&& validExpiry(travel.expiresAt, now) && !this.suppressedTravel.has(travel.eventId)
|
|
34
|
+
? { eventId: travel.eventId, dx: travel.dx, dy: travel.dy, expiresAt: travel.expiresAt } : null,
|
|
35
|
+
};
|
|
36
|
+
if (suppressTravel) this.suppressTravel();
|
|
37
|
+
this.scheduleExpiry();
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
consumeDelivery() {
|
|
41
|
+
const delivery = this.snapshot.delivery;
|
|
42
|
+
if (!delivery || this.seenResults.has(delivery.resultId)) return null;
|
|
43
|
+
this.seenResults.set(delivery.resultId, this.now() + 3_600_000);
|
|
44
|
+
return delivery;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
suppressTravel() {
|
|
48
|
+
if (!this.snapshot.travel) return;
|
|
49
|
+
this.suppressedTravel.set(this.snapshot.travel.eventId, this.now() + 3_600_000);
|
|
50
|
+
this.snapshot.travel = null;
|
|
51
|
+
this.scheduleExpiry();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
close() {
|
|
55
|
+
if (this.timer !== undefined) this.clearTimer(this.timer);
|
|
56
|
+
this.timer = undefined;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
scheduleExpiry() {
|
|
60
|
+
this.close();
|
|
61
|
+
const expiries = Object.values(this.snapshot).filter(Boolean).map(value => value.expiresAt);
|
|
62
|
+
if (!expiries.length) return;
|
|
63
|
+
this.timer = this.setTimer(() => {
|
|
64
|
+
this.timer = undefined;
|
|
65
|
+
const now = this.now();
|
|
66
|
+
for (const key of ['work', 'delivery', 'travel']) {
|
|
67
|
+
if (this.snapshot[key]?.expiresAt <= now) this.snapshot[key] = null;
|
|
68
|
+
}
|
|
69
|
+
this.scheduleExpiry();
|
|
70
|
+
this.changed();
|
|
71
|
+
}, Math.max(0, Math.min(...expiries) - this.now()));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
prune(now) {
|
|
75
|
+
for (const collection of [this.seenResults, this.suppressedTravel]) {
|
|
76
|
+
for (const [id, expiresAt] of collection) if (expiresAt <= now) collection.delete(id);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
const priority = { reasoning: 1, tool: 2, workspace_fetch: 3, workspace_dispatch: 4, screen_read: 5 };
|
|
2
|
+
const semantic = {
|
|
3
|
+
reasoning: 'reasoning',
|
|
4
|
+
tool: 'tool',
|
|
5
|
+
workspace_fetch: 'workspace',
|
|
6
|
+
workspace_dispatch: 'workspace',
|
|
7
|
+
screen_read: 'screen_reading',
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
/** Normalize the Host's bounded call snapshot; never infer work from chat text. */
|
|
11
|
+
export function normalizeNativeWork(value, now = Date.now()) {
|
|
12
|
+
if (!value || typeof value !== 'object' || !Array.isArray(value.active) || !Array.isArray(value.recent)) return null;
|
|
13
|
+
const active = value.active
|
|
14
|
+
.filter(entry => entry && semantic[entry.kind] && Number.isFinite(entry.expiresAt) && entry.expiresAt > now)
|
|
15
|
+
.sort((left, right) => priority[right.kind] - priority[left.kind] || right.startedAt - left.startedAt)[0];
|
|
16
|
+
const handed = [...value.recent].reverse().find(event => event?.kind === 'result'
|
|
17
|
+
&& event.phase === 'result_handed_to_voice' && typeof event.resultId === 'string'
|
|
18
|
+
&& event.resultId.length > 0 && Number.isFinite(event.expiresAt) && event.expiresAt > now);
|
|
19
|
+
return {
|
|
20
|
+
work: active ? { taskId: active.taskId, kind: semantic[active.kind], status: 'active', expiresAt: active.expiresAt } : null,
|
|
21
|
+
delivery: handed ? { resultId: handed.resultId, status: 'applied', expiresAt: handed.expiresAt } : null,
|
|
22
|
+
travel: null,
|
|
23
|
+
};
|
|
24
|
+
}
|
package/renderer/pet-motion.mjs
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { isLivingClip } from './living-body.mjs';
|
|
2
|
+
import { MotionFacts } from './motion-facts.mjs';
|
|
2
3
|
|
|
3
4
|
// F229 pet.json frame durations, plus the three F258 rows pinned in source-lock.json.
|
|
4
5
|
const atlas = {
|
|
@@ -17,10 +18,13 @@ const additional = {
|
|
|
17
18
|
working: { file: 'xianxian-working-row.png', frames: [520, 560, 520, 640, 520, 560, 520, 640] },
|
|
18
19
|
staged_thought: { file: 'xianxian-staged-thought-row.png', frames: [1200, 500, 700, 900, 1200, 500] },
|
|
19
20
|
};
|
|
20
|
-
const
|
|
21
|
+
const visualAlias = { fetching: 'working', studying: 'staged_thought', delivering: 'review', held: 'idle' };
|
|
22
|
+
const workAction = { reasoning: 'staged_thought', tool: 'working', workspace: 'fetching', screen_reading: 'studying' };
|
|
23
|
+
const signals = { connected: 'waving', received: 'jumping', failed: 'failed' };
|
|
21
24
|
const staticSkins = { 'ragdoll-v1': 'ragdoll-v1.png', 'yarn-ball': 'yarn-ball.png' };
|
|
25
|
+
const validId = value => typeof value === 'string' && value.length > 0 && value.length <= 256;
|
|
22
26
|
|
|
23
|
-
/**
|
|
27
|
+
/** Select one truthful body from Host facts and local, user-controlled gestures. */
|
|
24
28
|
export class PetMotion {
|
|
25
29
|
constructor(element, options = {}) {
|
|
26
30
|
this.element = element;
|
|
@@ -28,88 +32,206 @@ export class PetMotion {
|
|
|
28
32
|
const clearTimer = options.clearTimer ?? globalThis.clearTimeout;
|
|
29
33
|
this.setTimer = (callback, delay) => setTimer.call(globalThis, callback, delay);
|
|
30
34
|
this.clearTimer = id => clearTimer.call(globalThis, id);
|
|
35
|
+
this.now = options.now ?? Date.now;
|
|
31
36
|
this.livingBody = options.livingBody;
|
|
32
37
|
this.reducedMotion = options.reducedMotion ?? globalThis.matchMedia?.('(prefers-reduced-motion: reduce)').matches ?? false;
|
|
38
|
+
this.autoPlay = options.autoPlay === true;
|
|
39
|
+
this.autoPlayIdleMs = Math.max(90_000, options.autoPlayIdleMs ?? 90_000);
|
|
40
|
+
this.autoPlayCooldownMs = Math.max(180_000, options.autoPlayCooldownMs ?? 180_000);
|
|
41
|
+
this.lastPlayAt = Number.NEGATIVE_INFINITY;
|
|
33
42
|
this.skin = 'xianxian-codex';
|
|
34
43
|
this.phase = 'idle';
|
|
35
44
|
this.base = 'idle';
|
|
36
45
|
this.action = '';
|
|
37
46
|
this.frame = 0;
|
|
38
|
-
this.
|
|
47
|
+
this.dragging = false;
|
|
39
48
|
this.oneShot = false;
|
|
40
49
|
this.pendingDecision = false;
|
|
41
50
|
this.dockedEdge = null;
|
|
51
|
+
this.dockZone = null;
|
|
52
|
+
this.resting = false;
|
|
53
|
+
this.transient = null;
|
|
54
|
+
this.facts = new MotionFacts({ now: this.now, setTimer: this.setTimer, clearTimer: this.clearTimer,
|
|
55
|
+
changed: () => this.refresh() });
|
|
56
|
+
this.seenEvents = new Map();
|
|
42
57
|
}
|
|
58
|
+
|
|
43
59
|
setContext({ skin, phase, nativeActivity, muted }) {
|
|
44
|
-
|
|
45
|
-
|
|
60
|
+
const selectedSkin = ['xianxian-codex', 'yanyan-codex', ...Object.keys(staticSkins)].includes(skin)
|
|
61
|
+
? skin : 'xianxian-codex';
|
|
62
|
+
const wasSleeping = this.action === 'sleeping' || this.resting;
|
|
63
|
+
this.context = { skin: selectedSkin, phase, nativeActivity, muted };
|
|
46
64
|
const changedSkin = selectedSkin !== this.skin;
|
|
47
|
-
const changedPhase = phase !== this.phase;
|
|
48
65
|
this.skin = selectedSkin;
|
|
49
66
|
this.phase = phase;
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
this.
|
|
55
|
-
if (
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
if (waking) { this.oneShot = true; this.play('waking'); }
|
|
59
|
-
else if (this.oneShot && this.action === 'waking' && ['working', 'staged_thought', 'pending_decision'].includes(this.base)) {
|
|
60
|
-
this.oneShot = false;
|
|
61
|
-
this.play(this.base);
|
|
62
|
-
} else if (!this.moving && !this.oneShot && (changedSkin || changedBase || changedPhase || !this.action)) this.play(this.base, changedSkin);
|
|
63
|
-
if (phase === 'idle' && this.action === 'idle') this.armRest();
|
|
67
|
+
if (phase !== 'idle' || nativeActivity !== 'none') {
|
|
68
|
+
this.resting = false;
|
|
69
|
+
this.cancelIdleTimers();
|
|
70
|
+
}
|
|
71
|
+
if (nativeActivity !== 'none' && this.transient?.action !== 'failed') this.transient = null;
|
|
72
|
+
else if (wasSleeping && ['talking', 'connecting'].includes(phase) && !muted)
|
|
73
|
+
this.transient = { action: 'waking' };
|
|
74
|
+
this.refresh(changedSkin);
|
|
64
75
|
}
|
|
76
|
+
|
|
77
|
+
syncSnapshot(value) {
|
|
78
|
+
const now = this.now();
|
|
79
|
+
this.hasNativeWork = value !== null && value !== undefined;
|
|
80
|
+
for (const [id, expiresAt] of this.seenEvents) if (expiresAt <= now) this.seenEvents.delete(id);
|
|
81
|
+
this.facts.sync(value, this.dragging);
|
|
82
|
+
if (this.snapshot.work) {
|
|
83
|
+
this.resting = false;
|
|
84
|
+
this.cancelIdleTimers();
|
|
85
|
+
if (this.transient?.action !== 'failed') this.transient = null;
|
|
86
|
+
} else if (this.resting && (this.snapshot.delivery || this.snapshot.travel)) {
|
|
87
|
+
this.resting = false;
|
|
88
|
+
this.cancelRest();
|
|
89
|
+
this.transient = { action: 'waking' };
|
|
90
|
+
}
|
|
91
|
+
this.refresh();
|
|
92
|
+
}
|
|
93
|
+
|
|
65
94
|
setPendingDecision(value) {
|
|
66
|
-
|
|
67
|
-
this.pendingDecision
|
|
68
|
-
|
|
69
|
-
if (
|
|
70
|
-
this.
|
|
71
|
-
this.
|
|
95
|
+
const pending = value === true;
|
|
96
|
+
if (this.pendingDecision === pending) return;
|
|
97
|
+
this.pendingDecision = pending;
|
|
98
|
+
if (pending) {
|
|
99
|
+
this.resting = false;
|
|
100
|
+
this.transient = null;
|
|
101
|
+
this.cancelIdleTimers();
|
|
72
102
|
}
|
|
103
|
+
this.refresh();
|
|
73
104
|
}
|
|
105
|
+
|
|
74
106
|
setDockedEdge(value) {
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
107
|
+
const candidate = typeof value === 'string' ? { edge: value, zone: 'corner' } : value;
|
|
108
|
+
const edge = ['left', 'right'].includes(candidate?.edge) ? candidate.edge : null;
|
|
109
|
+
const zone = edge && ['corner', 'mid'].includes(candidate?.zone) ? candidate.zone : edge ? 'corner' : null;
|
|
110
|
+
if (this.dockedEdge === edge && this.dockZone === zone) return;
|
|
78
111
|
this.dockedEdge = edge;
|
|
79
|
-
|
|
80
|
-
if (
|
|
112
|
+
this.dockZone = zone;
|
|
113
|
+
if (edge) this.cancelIdleTimers();
|
|
114
|
+
this.refresh(this.action === 'peek');
|
|
81
115
|
}
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
if (kind === '
|
|
85
|
-
if (
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
116
|
+
|
|
117
|
+
signal(kind, eventId) {
|
|
118
|
+
if (kind === 'answered') return;
|
|
119
|
+
if (kind === 'connected' && this.living()) return;
|
|
120
|
+
if (kind === 'play') {
|
|
121
|
+
if (!this.canPlay()) return;
|
|
122
|
+
this.lastPlayAt = this.now();
|
|
123
|
+
this.resting = false;
|
|
124
|
+
this.cancelIdleTimers();
|
|
125
|
+
this.transient = { action: 'play' };
|
|
126
|
+
this.refresh();
|
|
127
|
+
return;
|
|
128
|
+
}
|
|
129
|
+
let requested = signals[kind];
|
|
130
|
+
if (kind === 'received' && (this.action === 'sleeping' || this.resting)) requested = 'waking';
|
|
131
|
+
if (!requested || this.dragging || staticSkins[this.skin]) return;
|
|
132
|
+
const now = this.now();
|
|
133
|
+
if (validId(eventId)) {
|
|
134
|
+
if (this.seenEvents.has(eventId)) return;
|
|
135
|
+
this.seenEvents.set(eventId, now + 3_600_000);
|
|
136
|
+
}
|
|
137
|
+
this.resting = false;
|
|
138
|
+
this.cancelIdleTimers();
|
|
139
|
+
this.transient = { action: requested };
|
|
140
|
+
this.refresh();
|
|
89
141
|
}
|
|
142
|
+
|
|
90
143
|
move(dx, dy) {
|
|
91
144
|
if (!Number.isFinite(dx) || !Number.isFinite(dy) || Math.hypot(dx, dy) < 6) return;
|
|
92
|
-
this.
|
|
93
|
-
this.
|
|
94
|
-
this.
|
|
95
|
-
this.
|
|
145
|
+
if (this.dragging) return;
|
|
146
|
+
this.dragging = true;
|
|
147
|
+
this.element.classList?.add('dragging');
|
|
148
|
+
this.element.dataset.interaction = 'held-candidate';
|
|
149
|
+
this.facts.suppressTravel();
|
|
150
|
+
this.resting = false;
|
|
151
|
+
this.transient = null;
|
|
152
|
+
this.cancelIdleTimers();
|
|
153
|
+
this.refresh();
|
|
96
154
|
}
|
|
155
|
+
|
|
97
156
|
stopMove() {
|
|
98
|
-
if (!this.
|
|
99
|
-
this.
|
|
100
|
-
this.
|
|
101
|
-
|
|
157
|
+
if (!this.dragging) return;
|
|
158
|
+
this.dragging = false;
|
|
159
|
+
this.element.classList?.remove('dragging');
|
|
160
|
+
delete this.element.dataset.interaction;
|
|
161
|
+
this.refresh();
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
close() {
|
|
165
|
+
this.cancelFrame();
|
|
166
|
+
this.cancelIdleTimers();
|
|
167
|
+
this.facts.close();
|
|
168
|
+
this.livingBody?.close();
|
|
102
169
|
}
|
|
103
|
-
close() { this.cancelFrame(); this.cancelRest(); this.livingBody?.close(); }
|
|
104
170
|
living() { return this.skin === 'xianxian-codex' && Boolean(this.livingBody); }
|
|
105
171
|
resolve(action) {
|
|
106
172
|
if (action === 'pending_decision' && !this.living()) return 'waiting';
|
|
107
173
|
if (action === 'peek' && !this.living()) return 'idle';
|
|
108
174
|
if (staticSkins[this.skin]) return 'idle';
|
|
109
|
-
|
|
110
|
-
|
|
175
|
+
const visual = visualAlias[action] ?? action;
|
|
176
|
+
if (this.skin !== 'xianxian-codex' && visual in additional)
|
|
177
|
+
return visual === 'sleeping' ? 'idle' : 'review';
|
|
111
178
|
return action;
|
|
112
179
|
}
|
|
180
|
+
|
|
181
|
+
refresh(force = false) {
|
|
182
|
+
const selection = this.select();
|
|
183
|
+
this.base = this.baseAction();
|
|
184
|
+
const action = this.resolve(selection.action);
|
|
185
|
+
const changed = action !== this.action;
|
|
186
|
+
if (changed || force || selection.oneShot !== this.oneShot) {
|
|
187
|
+
this.oneShot = selection.oneShot;
|
|
188
|
+
this.play(action, force);
|
|
189
|
+
}
|
|
190
|
+
if (action === 'idle') this.armRest();
|
|
191
|
+
else if (action !== 'sleeping') this.cancelRest();
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
select() {
|
|
195
|
+
if (this.dragging) return { action: 'held', oneShot: false };
|
|
196
|
+
if (this.transient?.action === 'failed') return { action: 'failed', oneShot: true };
|
|
197
|
+
if (this.pendingDecision) return { action: 'pending_decision', oneShot: false };
|
|
198
|
+
const work = this.activeWorkAction();
|
|
199
|
+
if (work) return { action: work, oneShot: false };
|
|
200
|
+
if (this.transient && this.transient.action !== 'play')
|
|
201
|
+
return { action: this.transient.action, oneShot: true };
|
|
202
|
+
const delivery = this.facts.consumeDelivery();
|
|
203
|
+
if (delivery) {
|
|
204
|
+
this.transient = { action: 'delivering', resultId: delivery.resultId };
|
|
205
|
+
return { action: 'delivering', oneShot: true };
|
|
206
|
+
}
|
|
207
|
+
if (!this.reducedMotion && this.snapshot.travel) {
|
|
208
|
+
const { dx, dy } = this.snapshot.travel;
|
|
209
|
+
return { action: Math.abs(dx) >= Math.abs(dy) ? dx < 0 ? 'running-left' : 'running-right' : 'running', oneShot: false };
|
|
210
|
+
}
|
|
211
|
+
if (['talking', 'connecting'].includes(this.phase) && !this.context?.muted)
|
|
212
|
+
return { action: 'waiting', oneShot: false };
|
|
213
|
+
if (this.transient?.action === 'play') return { action: 'play', oneShot: true };
|
|
214
|
+
if (this.dockedEdge) return { action: 'peek', oneShot: false };
|
|
215
|
+
if (this.resting) return { action: 'sleeping', oneShot: false };
|
|
216
|
+
return { action: 'idle', oneShot: false };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
baseAction() {
|
|
220
|
+
if (this.pendingDecision) return 'pending_decision';
|
|
221
|
+
const work = this.activeWorkAction();
|
|
222
|
+
if (work) return work;
|
|
223
|
+
if (['talking', 'connecting'].includes(this.phase) && !this.context?.muted) return 'waiting';
|
|
224
|
+
if (this.dockedEdge) return 'peek';
|
|
225
|
+
return 'idle';
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
activeWorkAction() {
|
|
229
|
+
if (this.snapshot.work) return workAction[this.snapshot.work.kind];
|
|
230
|
+
if (!this.hasNativeWork && this.context?.nativeActivity === 'tool_running') return 'working';
|
|
231
|
+
if (!this.hasNativeWork && this.context?.nativeActivity === 'reasoning') return 'staged_thought';
|
|
232
|
+
return null;
|
|
233
|
+
}
|
|
234
|
+
|
|
113
235
|
play(action, force = false) {
|
|
114
236
|
if (this.action === action && !force) return;
|
|
115
237
|
this.cancelFrame();
|
|
@@ -118,11 +240,13 @@ export class PetMotion {
|
|
|
118
240
|
this.paint();
|
|
119
241
|
this.scheduleFrame();
|
|
120
242
|
}
|
|
243
|
+
|
|
121
244
|
paint() {
|
|
122
245
|
const { style, dataset } = this.element;
|
|
123
246
|
dataset.skin = this.skin;
|
|
124
247
|
dataset.action = this.action;
|
|
125
248
|
dataset.dockEdge = this.dockedEdge ?? 'none';
|
|
249
|
+
dataset.dockZone = this.dockZone ?? 'none';
|
|
126
250
|
if (this.living()) {
|
|
127
251
|
style.backgroundImage = 'none';
|
|
128
252
|
this.livingBody.show(this.action, { reducedMotion: this.reducedMotion, onEnded: () => this.completeOneShot() });
|
|
@@ -136,27 +260,31 @@ export class PetMotion {
|
|
|
136
260
|
style.backgroundPosition = 'center';
|
|
137
261
|
return;
|
|
138
262
|
}
|
|
139
|
-
const
|
|
263
|
+
const visual = visualAlias[this.action] ?? this.action;
|
|
264
|
+
const extra = this.skin === 'xianxian-codex' ? additional[visual] : undefined;
|
|
140
265
|
style.backgroundImage = `url("./skins/${extra?.file ?? `${this.skin}.webp`}")`;
|
|
141
266
|
style.backgroundSize = extra ? '960px 130px' : '960px 1170px';
|
|
142
|
-
style.backgroundPosition = `${-120 * this.frame}px ${-130 * (atlas[
|
|
267
|
+
style.backgroundPosition = `${-120 * this.frame}px ${-130 * (atlas[visual]?.row ?? 0)}px`;
|
|
143
268
|
}
|
|
269
|
+
|
|
144
270
|
scheduleFrame() {
|
|
145
|
-
if (this.reducedMotion
|
|
271
|
+
if (this.reducedMotion) {
|
|
272
|
+
if (this.oneShot) this.frameTimer = this.setTimer(() => { this.frameTimer = undefined; this.completeOneShot(); }, 1_000);
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
if (staticSkins[this.skin]) return;
|
|
146
276
|
if (this.living()) {
|
|
147
277
|
if (isLivingClip(this.action) || !this.oneShot) return;
|
|
148
278
|
this.frameTimer = this.setTimer(() => { this.frameTimer = undefined; this.completeOneShot(); }, 700);
|
|
149
279
|
return;
|
|
150
280
|
}
|
|
151
|
-
const
|
|
281
|
+
const visual = visualAlias[this.action] ?? this.action;
|
|
282
|
+
const frames = additional[visual]?.frames ?? atlas[visual]?.frames;
|
|
152
283
|
if (!frames) return;
|
|
153
284
|
this.frameTimer = this.setTimer(() => {
|
|
154
285
|
this.frameTimer = undefined;
|
|
155
286
|
if (this.frame + 1 === frames.length && this.oneShot) {
|
|
156
|
-
this.
|
|
157
|
-
this.action = '';
|
|
158
|
-
this.play(this.base);
|
|
159
|
-
if (this.phase === 'idle') this.armRest();
|
|
287
|
+
this.completeOneShot();
|
|
160
288
|
return;
|
|
161
289
|
}
|
|
162
290
|
this.frame = (this.frame + 1) % frames.length;
|
|
@@ -164,20 +292,59 @@ export class PetMotion {
|
|
|
164
292
|
this.scheduleFrame();
|
|
165
293
|
}, frames[this.frame]);
|
|
166
294
|
}
|
|
167
|
-
|
|
295
|
+
|
|
296
|
+
cancelFrame() {
|
|
297
|
+
if (this.frameTimer !== undefined) this.clearTimer(this.frameTimer);
|
|
298
|
+
this.frameTimer = undefined;
|
|
299
|
+
}
|
|
300
|
+
|
|
168
301
|
completeOneShot() {
|
|
169
302
|
if (!this.oneShot) return;
|
|
170
303
|
this.oneShot = false;
|
|
304
|
+
this.transient = null;
|
|
171
305
|
this.action = '';
|
|
172
|
-
this.
|
|
173
|
-
|
|
306
|
+
this.refresh();
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
canPlay() {
|
|
310
|
+
return !this.reducedMotion && !this.dragging && !this.pendingDecision && this.phase === 'idle'
|
|
311
|
+
&& !this.activeWorkAction() && !this.snapshot.delivery && !this.snapshot.travel && !this.dockedEdge
|
|
312
|
+
&& !staticSkins[this.skin];
|
|
174
313
|
}
|
|
314
|
+
|
|
175
315
|
armRest() {
|
|
176
|
-
if (this.restTimer
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
316
|
+
if (this.restTimer === undefined && !this.resting && !this.pendingDecision && !this.dockedEdge) {
|
|
317
|
+
this.restTimer = this.setTimer(() => {
|
|
318
|
+
this.restTimer = undefined;
|
|
319
|
+
if (this.action === 'idle' && !this.dragging && !this.transient) {
|
|
320
|
+
this.resting = true;
|
|
321
|
+
this.refresh();
|
|
322
|
+
}
|
|
323
|
+
}, 45_000);
|
|
324
|
+
}
|
|
325
|
+
if (this.autoPlay && !this.reducedMotion && this.autoTimer === undefined) {
|
|
326
|
+
const delay = Math.max(this.autoPlayIdleMs, this.lastPlayAt + this.autoPlayCooldownMs - this.now());
|
|
327
|
+
this.autoTimer = this.setTimer(() => {
|
|
328
|
+
this.autoTimer = undefined;
|
|
329
|
+
if (!this.canPlay()) return;
|
|
330
|
+
this.lastPlayAt = this.now();
|
|
331
|
+
this.resting = false;
|
|
332
|
+
this.transient = { action: 'play' };
|
|
333
|
+
this.refresh();
|
|
334
|
+
}, delay);
|
|
335
|
+
}
|
|
181
336
|
}
|
|
182
|
-
|
|
337
|
+
|
|
338
|
+
cancelRest() {
|
|
339
|
+
if (this.restTimer !== undefined) this.clearTimer(this.restTimer);
|
|
340
|
+
this.restTimer = undefined;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
cancelIdleTimers() {
|
|
344
|
+
this.cancelRest();
|
|
345
|
+
if (this.autoTimer !== undefined) this.clearTimer(this.autoTimer);
|
|
346
|
+
this.autoTimer = undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
get snapshot() { return this.facts.snapshot; }
|
|
183
350
|
}
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/renderer/style.css
CHANGED
|
@@ -14,8 +14,9 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
14
14
|
#pet.dragging { cursor: grabbing; }
|
|
15
15
|
#pet[data-skin="xianxian-codex"] { position: relative; background-image: none; overflow: visible; }
|
|
16
16
|
#living-sit { position: absolute; left: 0; bottom: 10px; width: 120px; height: 120px; overflow: visible; pointer-events: none; }
|
|
17
|
-
#living-video { position: absolute; object-fit: fill; pointer-events: none; }
|
|
18
|
-
#pet[data-direction="left"] #living-video { transform: scaleX(-1); }
|
|
17
|
+
#living-video, #living-video-transition { position: absolute; object-fit: fill; pointer-events: none; }
|
|
18
|
+
#pet[data-direction="left"] #living-video, #pet[data-direction="left"] #living-video-transition { transform: scaleX(-1); }
|
|
19
|
+
#pet[data-action="held"] #living-sit { transform: translateY(-4px) scale(.98); transform-origin: 50% 80%; }
|
|
19
20
|
#living-sit [data-layer="tail"] { transform-box: view-box; transform-origin: 168px 456px; animation: living-tail 3.4s ease-in-out infinite; }
|
|
20
21
|
#living-sit [data-layer="head"] { transform-box: view-box; transform-origin: 300px 280px; animation: living-head 5.5s ease-in-out infinite; }
|
|
21
22
|
#living-sit [data-layer="ear-left"], #living-sit [data-layer="ear-right"] { transform-box: view-box; }
|
|
@@ -40,6 +41,10 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
40
41
|
@keyframes living-idle-ear-left { 0%, 68%, 76%, 100% { transform: rotate(0); } 71% { transform: rotate(-9deg); } 73% { transform: rotate(3deg); } }
|
|
41
42
|
@keyframes living-idle-ear-right { 0%, 30%, 39%, 100% { transform: rotate(0); } 33% { transform: rotate(8deg); } 36% { transform: rotate(-3deg); } }
|
|
42
43
|
@keyframes living-idle-blink { 0%, 24%, 30%, 74%, 80%, 100% { opacity: 0; } 25%, 29%, 75%, 79% { opacity: 1; } }
|
|
44
|
+
@keyframes living-wake-anchor {
|
|
45
|
+
from { left: var(--living-left-start); top: var(--living-top-start); }
|
|
46
|
+
to { left: var(--living-left-end); top: var(--living-top-end); }
|
|
47
|
+
}
|
|
43
48
|
#indicators { position: absolute; bottom: 0; right: 0; display: flex; gap: 4px; }
|
|
44
49
|
#indicators button { width: 32px; height: 32px; border: 1px solid #fff9; border-radius: 50%; display: grid; place-items: center; background: #236b59; color: white; box-shadow: 0 2px 5px #0003; }
|
|
45
50
|
#indicators button:hover { background: #ab4842; }
|
package/renderer/surface.mjs
CHANGED
|
@@ -10,13 +10,15 @@ import { LivingBody } from './living-body.mjs';
|
|
|
10
10
|
import { detectDockedEdge } from './living-edge.mjs';
|
|
11
11
|
import { bindPetControls } from './pet-controls.mjs';
|
|
12
12
|
import { decisionBadge, decisionRows } from './decision-view.mjs';
|
|
13
|
+
import { normalizeNativeWork } from './native-work-motion.mjs';
|
|
13
14
|
|
|
14
15
|
const $ = id => document.getElementById(id);
|
|
15
16
|
const label = (id, text) => { $(id).querySelector('.label').textContent = text; };
|
|
16
17
|
const status = text => { $('status').textContent = text; $('chat-status').textContent = text; $('announcement').textContent = text; };
|
|
17
18
|
const transcript = new TranscriptView($('transcript'));
|
|
18
19
|
const bubble = new RecentBubble([$('bubble-first'), $('bubble-second')]);
|
|
19
|
-
const motion = new PetMotion($('pet'), { livingBody: new LivingBody({ root: $('pet'), sit: $('living-sit'),
|
|
20
|
+
const motion = new PetMotion($('pet'), { livingBody: new LivingBody({ root: $('pet'), sit: $('living-sit'),
|
|
21
|
+
video: $('living-video'), transitionVideo: $('living-video-transition') }) });
|
|
20
22
|
let sharing = false, pendingScreen = false, loading = false, latestHistory, previousPhase = 'idle';
|
|
21
23
|
let observedHistory = false, lastAssistantId;
|
|
22
24
|
let resultPreviewUntil = 0;
|
|
@@ -38,6 +40,7 @@ if (!window.clowderCompanion) {
|
|
|
38
40
|
motion.stopMove();
|
|
39
41
|
if (!didMove) return;
|
|
40
42
|
didMove = false;
|
|
43
|
+
void conversation.refresh();
|
|
41
44
|
setTimeout(() => motion.setDockedEdge(detectDockedEdge(window, $('pet').getBoundingClientRect())), 100);
|
|
42
45
|
},
|
|
43
46
|
changed: panel => { if (panel === 'chat') void readHistory(); if (panel === 'decisions') void readDecisions(); },
|
|
@@ -79,7 +82,7 @@ if (!window.clowderCompanion) {
|
|
|
79
82
|
const conversation = new CompanionConversation({
|
|
80
83
|
client, createPeer: callback => new VoicePeer(callback, client), stopScreen: () => screen.stop(),
|
|
81
84
|
transcript: event => {
|
|
82
|
-
if (event.type === 'turn-done') { transcript.finish(event); bubble.finish(event.role);
|
|
85
|
+
if (event.type === 'turn-done') { transcript.finish(event); bubble.finish(event.role); void readHistory(); }
|
|
83
86
|
else if (event.typed) void readHistory();
|
|
84
87
|
else { transcript.append(event.role, event.text, true); bubble.append(event.role, event.text); void controls.setAmbient(true); }
|
|
85
88
|
},
|
|
@@ -106,6 +109,7 @@ if (!window.clowderCompanion) {
|
|
|
106
109
|
label('speaker', value.silent ? '开启播音' : '关闭播音');
|
|
107
110
|
const identity = value.identity;
|
|
108
111
|
motion.setContext({ skin: identity?.skin, phase: value.phase, nativeActivity: identity?.nativeActivity ?? 'none', muted: value.muted });
|
|
112
|
+
motion.syncSnapshot(normalizeNativeWork(identity?.nativeWork));
|
|
109
113
|
if (justConnected) motion.signal('connected');
|
|
110
114
|
if (showFailure) motion.signal('failed');
|
|
111
115
|
if (identity) {
|
|
@@ -114,7 +118,11 @@ if (!window.clowderCompanion) {
|
|
|
114
118
|
$('pet').setAttribute('aria-label', `${identity.displayName}:点击交流,右键更多,拖动移动`);
|
|
115
119
|
$('documents').querySelector('.menu-status').textContent = identity.documentsAllowed ? '开启' : '暂停';
|
|
116
120
|
$('documents').setAttribute('aria-pressed', String(identity.documentsAllowed));
|
|
117
|
-
|
|
121
|
+
const live = identity.liveTransport?.kind === 'gpt_live_v3'
|
|
122
|
+
? 'GPT Live · 实时型号未证实' : '实时载体未确认';
|
|
123
|
+
$('connection-scope').textContent = identity.duty.catId === identity.carrier.catId
|
|
124
|
+
? `${identity.duty.displayName} · ${live}`
|
|
125
|
+
: `${identity.duty.displayName} · ${live} · 绑定载体 ${identity.carrier.displayName}`;
|
|
118
126
|
}
|
|
119
127
|
if (showFailure) void controls.show('actions');
|
|
120
128
|
},
|
|
@@ -126,7 +134,6 @@ if (!window.clowderCompanion) {
|
|
|
126
134
|
const history = await client.readConversation();
|
|
127
135
|
const assistantId = history.messages.findLast(message => message.role === 'assistant')?.id;
|
|
128
136
|
if (observedHistory && assistantId && assistantId !== lastAssistantId) {
|
|
129
|
-
motion.signal('answered');
|
|
130
137
|
resultPreviewUntil = Date.now() + 12_000;
|
|
131
138
|
}
|
|
132
139
|
observedHistory = true;
|
package/source-lock.json
CHANGED
|
@@ -68,8 +68,8 @@
|
|
|
68
68
|
}
|
|
69
69
|
],
|
|
70
70
|
"livingSkin": {
|
|
71
|
-
"package": "xianxian-living package-
|
|
72
|
-
"manifestSha256": "
|
|
71
|
+
"package": "xianxian-living package-v3",
|
|
72
|
+
"manifestSha256": "a7434caa0cd559bfb8747a9d9391fb5219d4c4a68639b5b669008dd7c7b67aaf",
|
|
73
73
|
"creator": "opus55; keyframes by codex-astra; source videos by CVO",
|
|
74
74
|
"files": [
|
|
75
75
|
{
|
|
@@ -130,16 +130,20 @@
|
|
|
130
130
|
},
|
|
131
131
|
{
|
|
132
132
|
"destination": "assets/skins/xianxian-living/clips/sleep.webm",
|
|
133
|
-
"sha256": "
|
|
133
|
+
"sha256": "f53ae41cc7722128e68f4b5cf3df980be216a79f8cc4b8dff36fa0dc029dbca9"
|
|
134
134
|
},
|
|
135
135
|
{
|
|
136
136
|
"destination": "assets/skins/xianxian-living/clips/wake.webm",
|
|
137
|
-
"sha256": "
|
|
137
|
+
"sha256": "8821cb764d7ca085f4b30dec2f893f2515d383cc1f2053215e91b18b86753cd7"
|
|
138
138
|
},
|
|
139
139
|
{
|
|
140
140
|
"destination": "assets/skins/xianxian-living/clips/peek.webm",
|
|
141
141
|
"sha256": "adebecf89b724eec9852d097364e10bc328d006ce3bb3bd3f4b257c095d57a92"
|
|
142
142
|
},
|
|
143
|
+
{
|
|
144
|
+
"destination": "assets/skins/xianxian-living/clips/peek_fade.webm",
|
|
145
|
+
"sha256": "7880e695fa8afe783c4f1fcb4fc1c31c12d64d36f3d124dc4ef929ad09f7ffef"
|
|
146
|
+
},
|
|
143
147
|
{
|
|
144
148
|
"destination": "assets/skins/xianxian-living/clips/carry.webm",
|
|
145
149
|
"sha256": "4fa582ab6a18b538cfe815ab19e73da30c0da061ec661bb12e4060f8b3d2e600"
|