@clowder-ai/companion 0.1.0-alpha.13 → 0.1.0-alpha.14
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/manifest.json +2 -2
- package/package.json +5 -3
- package/plugin.yaml +2 -2
- package/renderer/call-transcript-view.mjs +238 -0
- package/renderer/client.mjs +5 -0
- package/renderer/conversation.mjs +86 -16
- package/renderer/decision-view.mjs +6 -8
- package/renderer/errors.mjs +4 -4
- package/renderer/index.html +104 -23
- package/renderer/native-work-motion.mjs +15 -0
- package/renderer/pet-controls.mjs +71 -25
- package/renderer/pet-motion.mjs +17 -23
- package/renderer/recent-bubble.mjs +16 -3
- package/renderer/settings-controller.mjs +171 -0
- package/renderer/settings-view.mjs +234 -0
- package/renderer/style.css +135 -21
- package/renderer/surface.mjs +151 -56
package/renderer/index.html
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
<body>
|
|
5
5
|
<svg class="icon-definitions" aria-hidden="true">
|
|
6
6
|
<symbol id="i-mic" viewBox="0 0 24 24"><rect x="9" y="3" width="6" height="12" rx="3"/><path d="M5 11a7 7 0 0 0 14 0M12 18v3M8 21h8"/></symbol>
|
|
7
|
+
<symbol id="i-mic-off" viewBox="0 0 24 24"><path d="M9 9v2a3 3 0 0 0 4.8 2.4M15 10V6a3 3 0 0 0-5.7-1.3M5 11a7 7 0 0 0 11.2 5.6M19 11a7 7 0 0 1-.7 3M12 18v3M8 21h8M3 3l18 18"/></symbol>
|
|
7
8
|
<symbol id="i-listen" viewBox="0 0 24 24"><path d="M4 13v-1a8 8 0 0 1 16 0v1M4 13h3v7H6a2 2 0 0 1-2-2v-5ZM20 13h-3v7h1a2 2 0 0 0 2-2v-5Z"/></symbol>
|
|
8
9
|
<symbol id="i-keyboard" viewBox="0 0 24 24"><rect x="2.5" y="5" width="19" height="14" rx="3"/><path d="M6 9h.01M10 9h.01M14 9h.01M18 9h.01M6 12h.01M10 12h.01M14 12h.01M18 12h.01M7 15h10"/></symbol>
|
|
9
10
|
<symbol id="i-history" viewBox="0 0 24 24"><path d="M3 11a9 9 0 1 1 2.6 7.4M3 4v7h7M12 7v5l3 2"/></symbol>
|
|
@@ -11,10 +12,12 @@
|
|
|
11
12
|
<symbol id="i-book" viewBox="0 0 24 24"><path d="M12 6c-3-2-6-2-9-1v14c3-1 6-1 9 1 3-2 6-2 9-1V5c-3-1-6-1-9 1v14"/></symbol>
|
|
12
13
|
<symbol id="i-moon" viewBox="0 0 24 24"><path d="M20.5 14A8.5 8.5 0 0 1 10 3.5 8.5 8.5 0 1 0 20.5 14Z"/></symbol>
|
|
13
14
|
<symbol id="i-end" viewBox="0 0 24 24"><rect x="6" y="6" width="12" height="12" rx="3"/></symbol>
|
|
15
|
+
<symbol id="i-subtitles" viewBox="0 0 24 24"><rect x="2.5" y="4.5" width="19" height="15" rx="3"/><path d="M6 10h5M6 14h7M15 10h3M15 14h3"/></symbol>
|
|
16
|
+
<symbol id="i-expand" viewBox="0 0 24 24"><path d="M8 3H3v5M16 3h5v5M8 21H3v-5M16 21h5v-5"/></symbol>
|
|
14
17
|
</svg>
|
|
15
18
|
<main id="desktop" aria-label="猫猫球">
|
|
16
19
|
<div id="anchor">
|
|
17
|
-
<button id="pet" aria-label="
|
|
20
|
+
<button id="pet" aria-label="猫猫:点击或右键打开名片,拖动移动" aria-expanded="false" aria-controls="actions menu chat transcript settings decisions">
|
|
18
21
|
<svg id="living-sit" viewBox="0 0 600 600" aria-hidden="true" hidden>
|
|
19
22
|
<defs>
|
|
20
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>
|
|
@@ -37,51 +40,129 @@
|
|
|
37
40
|
<video id="living-video-transition" muted playsinline preload="metadata" aria-hidden="true" hidden></video>
|
|
38
41
|
</button>
|
|
39
42
|
<div id="indicators">
|
|
40
|
-
<button id="call-badge" hidden aria-label="结束语音" title="结束语音"><svg viewBox="0 0 24 24" aria-hidden="true"><use id="call-badge-icon" href="#i-mic"/></svg></button>
|
|
41
43
|
<button id="share-badge" hidden aria-label="停止共享" title="停止共享"><svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="4" width="18" height="13" rx="2"/><path d="M8 21h8M12 17v4"/></svg></button>
|
|
42
44
|
<button id="pending-badge" hidden aria-label="查看待决事项" title="查看待决事项"><span id="pending-count">?</span></button>
|
|
43
45
|
</div>
|
|
44
46
|
</div>
|
|
45
|
-
<section id="bubble" class="floating" aria-label="
|
|
47
|
+
<section id="bubble" class="floating" aria-label="最新消息" hidden>
|
|
46
48
|
<p id="bubble-first" hidden></p><p id="bubble-second" hidden></p>
|
|
47
49
|
</section>
|
|
48
|
-
<section id="actions" class="floating" aria-label="
|
|
49
|
-
<
|
|
50
|
-
<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
<
|
|
54
|
-
<
|
|
50
|
+
<section id="actions" class="floating call-bar" aria-label="当前通话" hidden>
|
|
51
|
+
<div class="call-status-line"><span class="status-dot" aria-hidden="true"></span><strong id="call-state">通话中</strong><span id="call-mic-state">麦克风已开启</span></div>
|
|
52
|
+
<p id="call-work" class="call-work" hidden></p>
|
|
53
|
+
<div id="call-share-context" class="call-share-context" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="call-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
54
|
+
<div class="call-recent" aria-label="最新消息">
|
|
55
|
+
<small class="call-recent-label">最新消息</small>
|
|
56
|
+
<p id="call-bubble-first" hidden></p><p id="call-bubble-second" hidden></p>
|
|
55
57
|
</div>
|
|
56
|
-
<div id="active-actions" class="action-row"
|
|
58
|
+
<div id="active-actions" class="action-row call-actions">
|
|
59
|
+
<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>
|
|
57
60
|
<button data-action="share" id="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
58
|
-
<button
|
|
61
|
+
<button data-action="transcript"><svg class="icon" aria-hidden="true"><use href="#i-subtitles"/></svg><span>字幕</span></button>
|
|
62
|
+
<button id="end" class="destructive" data-action="stop"><span>挂断</span></button>
|
|
59
63
|
</div>
|
|
64
|
+
<p id="status" role="status" hidden></p>
|
|
60
65
|
</section>
|
|
61
|
-
<section id="menu" class="floating" aria-label="
|
|
66
|
+
<section id="menu" class="floating name-card" aria-label="猫猫名片" hidden>
|
|
67
|
+
<div id="menu-call-context" class="call-context" hidden>
|
|
68
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="menu-call-state"></strong><span id="menu-call-mic-state"></span>
|
|
69
|
+
<small id="menu-call-work" hidden></small>
|
|
70
|
+
<button data-action="stop" class="destructive compact">挂断</button>
|
|
71
|
+
<div id="menu-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="menu-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
72
|
+
</div>
|
|
62
73
|
<strong id="menu-name" class="menu-title">猫猫</strong>
|
|
63
|
-
<
|
|
64
|
-
|
|
65
|
-
|
|
74
|
+
<div class="name-card-actions">
|
|
75
|
+
<button id="begin" class="primary" data-action="begin"><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">语音通话</span></button>
|
|
76
|
+
<button id="listen" data-action="listen" hidden><svg class="icon" aria-hidden="true"><use href="#i-listen"/></svg><span class="label">只听</span></button>
|
|
77
|
+
<button data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span></button>
|
|
78
|
+
</div>
|
|
79
|
+
<p id="menu-status" role="status" hidden></p>
|
|
80
|
+
<button data-action="decisions"><span aria-hidden="true">◇</span><span>待办</span><span id="menu-pending" class="menu-status">—</span></button>
|
|
81
|
+
<button data-action="history"><svg class="icon" aria-hidden="true"><use href="#i-history"/></svg><span>打开聊天</span></button>
|
|
66
82
|
<button id="menu-play" data-action="play" hidden><span aria-hidden="true">✦</span><span>逗宪宪</span></button>
|
|
67
83
|
<div class="menu-divider" role="separator"></div>
|
|
68
84
|
<button id="speaker" data-action="speaker" hidden><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">关闭播音</span></button>
|
|
69
|
-
<button id="menu-
|
|
85
|
+
<button id="menu-settings" data-action="settings"><span aria-hidden="true">⚙</span><span>设置</span><span class="menu-status">›</span></button>
|
|
86
|
+
<button id="menu-share" data-action="share" title="语音通话连接后可共享画面"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
70
87
|
<button id="documents" data-action="documents" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-book"/></svg><span>资料查询</span><span class="menu-status">开启</span></button>
|
|
71
88
|
<div class="menu-divider" role="separator"></div>
|
|
72
89
|
<button data-action="hide" class="quiet-action" title="可从 Clowder 的聊聊入口叫回来"><svg class="icon" aria-hidden="true"><use href="#i-moon"/></svg><span>收起猫猫</span></button>
|
|
73
90
|
</section>
|
|
74
91
|
<section id="chat" class="floating" aria-label="和猫猫写几句" hidden>
|
|
92
|
+
<div id="chat-call-context" class="call-context" hidden>
|
|
93
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="chat-call-state"></strong><span id="chat-call-mic-state"></span>
|
|
94
|
+
<small id="chat-call-work" hidden></small>
|
|
95
|
+
<div class="call-context-actions"><button id="chat-mic" data-action="mute">静音</button><button id="chat-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
96
|
+
<div id="chat-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="chat-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
97
|
+
</div>
|
|
75
98
|
<header><strong id="chat-name">和猫猫写几句</strong><button data-action="dismiss" aria-label="收起文字">收起</button></header>
|
|
76
|
-
<div id="transcript" role="log" aria-live="polite"><p class="empty">想说什么,就写在这里。</p></div>
|
|
77
|
-
<form id="compose"><textarea id="message" aria-label="
|
|
99
|
+
<div id="chat-transcript" role="log" aria-live="polite"><p class="empty">想说什么,就写在这里。</p></div>
|
|
100
|
+
<form id="compose"><textarea id="message" aria-label="给猫猫输入消息" placeholder="输入消息…" rows="2" maxlength="8000"></textarea><button type="submit" class="primary">发送</button></form>
|
|
78
101
|
<small id="chat-status" role="status"></small>
|
|
79
|
-
<small><span id="connection-scope"
|
|
102
|
+
<small><span id="connection-scope">语音与文字共用同一段聊天</span> <button id="history">在 Cat Café 中查看完整聊天 ↗</button></small>
|
|
103
|
+
</section>
|
|
104
|
+
<section id="transcript" class="floating call-transcript" data-expanded="false" aria-label="这次通话的字幕" hidden>
|
|
105
|
+
<div id="transcript-call-context" class="call-context transcript-call-context">
|
|
106
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="transcript-call-state"></strong><span id="transcript-call-mic-state"></span><span class="view-tag">字幕</span>
|
|
107
|
+
<small id="transcript-call-work" hidden></small>
|
|
108
|
+
<div class="call-context-actions">
|
|
109
|
+
<button id="transcript-mic" data-action="mute" aria-label="静音"><svg class="icon" aria-hidden="true"><use id="transcript-mic-icon" href="#i-mic"/></svg></button>
|
|
110
|
+
<button id="transcript-expand" data-action="transcript-expand"><svg class="icon" aria-hidden="true"><use href="#i-expand"/></svg><span id="transcript-expand-label">展开</span></button>
|
|
111
|
+
<button data-action="stop" class="destructive compact">挂断</button>
|
|
112
|
+
</div>
|
|
113
|
+
<div id="transcript-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="transcript-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
114
|
+
</div>
|
|
115
|
+
<div id="call-transcript-log" role="log" aria-live="polite"></div>
|
|
116
|
+
<button id="call-unread" class="new-content" type="button" hidden></button>
|
|
117
|
+
<form id="call-compose"><input id="call-message" aria-label="通话中给猫猫发消息" placeholder="发消息给猫猫…" maxlength="8000"><button type="submit" class="primary">发送</button></form>
|
|
118
|
+
<small id="call-transcript-status" role="status"></small>
|
|
119
|
+
<small class="call-transcript-foot">完整记录会留在 Cat Café 聊天中</small>
|
|
120
|
+
</section>
|
|
121
|
+
<section id="settings" class="floating settings-panel" aria-label="猫猫球设置" hidden>
|
|
122
|
+
<div id="settings-call-context" class="call-context" hidden>
|
|
123
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="settings-call-state"></strong><span id="settings-call-mic-state"></span>
|
|
124
|
+
<small id="settings-call-work" hidden></small>
|
|
125
|
+
<div class="call-context-actions"><button id="settings-mic" data-action="mute">静音</button><button id="settings-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
126
|
+
<div id="settings-share-context" class="call-share-line" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span><strong id="settings-share-target"></strong><small>仅共享此窗口</small></span></div>
|
|
127
|
+
</div>
|
|
128
|
+
<header class="settings-head"><button id="settings-back" type="button" aria-label="返回名片">‹</button><strong id="settings-title">设置</strong></header>
|
|
129
|
+
<p id="settings-loading" class="settings-loading" hidden>正在读取设置…</p>
|
|
130
|
+
<div id="settings-page-main" class="settings-page">
|
|
131
|
+
<button id="settings-partner-open" class="settings-row" type="button" data-setting-control data-drilldown><span>陪伴者</span><span id="settings-partner-value" class="settings-value">—</span></button>
|
|
132
|
+
<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>
|
|
133
|
+
<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>
|
|
134
|
+
<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>
|
|
135
|
+
<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>
|
|
136
|
+
<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>
|
|
137
|
+
</div>
|
|
138
|
+
<div id="settings-page-partner" class="settings-page" hidden>
|
|
139
|
+
<p class="settings-copy">在桌面陪伴和回复你的猫。更换陪伴者将结束当前语音通话。</p>
|
|
140
|
+
<div id="settings-roster" class="settings-roster"></div>
|
|
141
|
+
<label id="settings-tone-field" class="settings-field"><span>人设基调</span><textarea id="settings-tone" rows="2" maxlength="200" data-setting-control></textarea><small><span>适用于所有陪伴者 · 下次语音通话生效</span><span id="settings-tone-count">0 / 200</span></small></label>
|
|
142
|
+
</div>
|
|
143
|
+
<div id="settings-page-look" class="settings-page" hidden>
|
|
144
|
+
<p class="settings-eyebrow">皮肤</p><p class="settings-copy">仅改变外观,不更换陪伴者。</p>
|
|
145
|
+
<div class="settings-skins">
|
|
146
|
+
<button id="settings-skin-xianxian-codex" type="button" data-setting-control>宪宪 v1</button>
|
|
147
|
+
<button id="settings-skin-yanyan-codex" type="button" data-setting-control>砚砚 v1</button>
|
|
148
|
+
<button id="settings-skin-ragdoll-v1" type="button" data-setting-control>布偶猫</button>
|
|
149
|
+
<button id="settings-skin-yarn-ball" type="button" data-setting-control>猫猫球</button>
|
|
150
|
+
</div>
|
|
151
|
+
<label id="settings-size-field" class="settings-field settings-size"><span>大小</span><div><small>小</small><input id="settings-size" type="range" min="48" max="192" step="1" data-setting-control><small>大</small></div></label>
|
|
152
|
+
</div>
|
|
153
|
+
<div id="settings-page-confirm" class="settings-page settings-confirm" hidden>
|
|
154
|
+
<strong id="settings-confirm-title"></strong><p id="settings-confirm-copy"></p>
|
|
155
|
+
<div><button id="settings-confirm-cancel" type="button">取消</button><button id="settings-confirm-apply" type="button" data-setting-control>确认更改</button></div>
|
|
156
|
+
</div>
|
|
157
|
+
<div id="settings-page-unavailable" class="settings-page settings-empty" hidden><p id="settings-unavailable-copy"></p><button id="settings-unavailable-retry" type="button">重试</button></div>
|
|
158
|
+
<div id="settings-page-disabled" class="settings-page settings-empty" hidden><p>桌面猫猫球已停用。可在 Cat Café 的“设置 › 插件”中重新启用。</p></div>
|
|
159
|
+
<p id="settings-notice" class="settings-notice" role="status" hidden></p>
|
|
160
|
+
<div class="settings-notice-actions"><button id="settings-retry" type="button" hidden>重试</button><button id="settings-reread" type="button" hidden>重新读取</button></div>
|
|
80
161
|
</section>
|
|
81
162
|
<p id="announcement" class="sr-only" role="status"></p>
|
|
82
|
-
<section id="decisions" class="floating" aria-label="
|
|
83
|
-
<header><strong
|
|
84
|
-
<p id="decision-status" role="status"
|
|
163
|
+
<section id="decisions" class="floating" aria-label="待办" hidden>
|
|
164
|
+
<header><strong>待办</strong><button id="decision-reload" type="button">刷新</button></header>
|
|
165
|
+
<p id="decision-status" role="status">正在读取待办…</p>
|
|
85
166
|
<ul id="decision-list"></ul>
|
|
86
167
|
<button id="decision-more" type="button" hidden>查看更多</button>
|
|
87
168
|
<small>这里只查看原处事项。要决定时请打开对应卡片。</small>
|
|
@@ -22,3 +22,18 @@ export function normalizeNativeWork(value, now = Date.now()) {
|
|
|
22
22
|
travel: null,
|
|
23
23
|
};
|
|
24
24
|
}
|
|
25
|
+
|
|
26
|
+
const workLabels = {
|
|
27
|
+
reasoning: '正在思考',
|
|
28
|
+
tool: '正在处理',
|
|
29
|
+
workspace: '正在处理家里的事务',
|
|
30
|
+
screen_reading: '正在查看共享画面',
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
/** Product wording for the same bounded work fact used by body motion. */
|
|
34
|
+
export function nativeWorkLabel(snapshot, nativeActivity = 'none') {
|
|
35
|
+
const kind = snapshot
|
|
36
|
+
? snapshot.work?.kind
|
|
37
|
+
: nativeActivity === 'reasoning' ? 'reasoning' : nativeActivity === 'tool_running' ? 'tool' : null;
|
|
38
|
+
return workLabels[kind] ?? '';
|
|
39
|
+
}
|
|
@@ -2,30 +2,71 @@
|
|
|
2
2
|
export function bindPetControls(client, { action, error, changed, moved = () => {} }) {
|
|
3
3
|
const $ = id => document.getElementById(id);
|
|
4
4
|
const body = $('pet');
|
|
5
|
-
|
|
5
|
+
const panelIds = ['actions', 'menu', 'chat', 'bubble', 'decisions', 'transcript', 'settings'];
|
|
6
|
+
const desiredSizes = new WeakMap();
|
|
7
|
+
let panel = 'none', ambientPanel = 'none', layoutRevision = 0, layoutScheduled = false, drag, suppressClick = false;
|
|
8
|
+
const measure = node => {
|
|
9
|
+
if (!node) return { width: 120, height: 130 };
|
|
10
|
+
// Host placement is an applied cap, not the panel's desired geometry.
|
|
11
|
+
// Release it synchronously while measuring so CSS height and nested flex
|
|
12
|
+
// overflow can expose their natural demand, then restore it before paint.
|
|
13
|
+
const appliedMaxHeight = node.style.maxHeight;
|
|
14
|
+
node.style.maxHeight = '';
|
|
15
|
+
const desired = {
|
|
16
|
+
width: Math.min(420, Math.max(120, Math.ceil(Math.max(node.offsetWidth, node.scrollWidth)))),
|
|
17
|
+
height: Math.min(500, Math.max(32, Math.ceil(Math.max(node.offsetHeight, node.scrollHeight)))),
|
|
18
|
+
};
|
|
19
|
+
node.style.maxHeight = appliedMaxHeight;
|
|
20
|
+
return desired;
|
|
21
|
+
};
|
|
6
22
|
async function show(next) {
|
|
7
|
-
if (next === 'none' &&
|
|
8
|
-
|
|
23
|
+
if (next === 'none' && ambientPanel !== 'none') next = ambientPanel;
|
|
24
|
+
const previousPanel = panel;
|
|
25
|
+
const node = $(next);
|
|
9
26
|
panel = next;
|
|
10
|
-
for (const id of
|
|
27
|
+
for (const id of panelIds) $(id).hidden = id !== next;
|
|
11
28
|
body.setAttribute('aria-expanded', String(!['none', 'bubble'].includes(next)));
|
|
12
|
-
const
|
|
29
|
+
const desired = measure(node);
|
|
30
|
+
if (node) desiredSizes.set(node, `${desired.width}:${desired.height}`);
|
|
13
31
|
const revision = ++layoutRevision;
|
|
14
32
|
try {
|
|
15
|
-
const placed = await client.layout(next,
|
|
33
|
+
const placed = await client.layout(next, desired.width, desired.height);
|
|
16
34
|
if (revision !== layoutRevision) return;
|
|
17
35
|
$('anchor').style.left = `${placed.pet.x}px`; $('anchor').style.top = `${placed.pet.y}px`;
|
|
18
36
|
if (node) { node.style.left = `${placed.panel.x}px`; node.style.top = `${placed.panel.y}px`; node.style.maxHeight = `${placed.panel.height}px`; }
|
|
19
|
-
if (
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
37
|
+
if (previousPanel !== next) {
|
|
38
|
+
if (next === 'chat') $('message').focus({ preventScroll: true });
|
|
39
|
+
if (next === 'transcript') $('call-message').focus({ preventScroll: true });
|
|
40
|
+
if (next === 'settings') $('settings-back').focus({ preventScroll: true });
|
|
41
|
+
if (next === 'decisions') $('decision-reload').focus({ preventScroll: true });
|
|
42
|
+
if (next === 'menu') $('menu').querySelector('button').focus({ preventScroll: true });
|
|
43
|
+
changed(next);
|
|
44
|
+
}
|
|
45
|
+
} catch (cause) {
|
|
46
|
+
if (revision !== layoutRevision) return;
|
|
47
|
+
panel = previousPanel;
|
|
48
|
+
for (const id of panelIds) $(id).hidden = id !== panel;
|
|
49
|
+
body.setAttribute('aria-expanded', String(!['none', 'bubble'].includes(panel)));
|
|
50
|
+
error(cause, { panel: next });
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function remeasure() {
|
|
54
|
+
if (layoutScheduled) return;
|
|
55
|
+
layoutScheduled = true;
|
|
56
|
+
queueMicrotask(() => {
|
|
57
|
+
layoutScheduled = false;
|
|
58
|
+
const node = $(panel);
|
|
59
|
+
if (!node || node.hidden) return;
|
|
60
|
+
const desired = measure(node);
|
|
61
|
+
const key = `${desired.width}:${desired.height}`;
|
|
62
|
+
if (desiredSizes.get(node) === key) return;
|
|
63
|
+
void show(panel);
|
|
64
|
+
});
|
|
24
65
|
}
|
|
25
66
|
body.onclick = event => {
|
|
26
67
|
if (suppressClick) { suppressClick = false; return; }
|
|
27
68
|
if (event.detail > 1) return;
|
|
28
|
-
void show(panel === '
|
|
69
|
+
void show(panel === 'menu' ? 'none' : 'menu');
|
|
29
70
|
};
|
|
30
71
|
body.oncontextmenu = event => { event.preventDefault(); void show('menu'); };
|
|
31
72
|
body.onkeydown = event => {
|
|
@@ -58,26 +99,31 @@ export function bindPetControls(client, { action, error, changed, moved = () =>
|
|
|
58
99
|
document.querySelectorAll('[data-action]').forEach(button => button.onclick = () => {
|
|
59
100
|
const kind = button.dataset.action;
|
|
60
101
|
if (kind === 'write' || kind === 'history') { void show('chat'); return; }
|
|
102
|
+
if (kind === 'transcript') { void show('transcript'); return; }
|
|
103
|
+
if (kind === 'settings') { void show('settings'); return; }
|
|
61
104
|
if (kind === 'decisions') { void show('decisions'); return; }
|
|
62
105
|
if (kind === 'dismiss') { void show('none'); return; }
|
|
63
106
|
action(kind);
|
|
64
107
|
});
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
108
|
+
const observer = new MutationObserver(records => {
|
|
109
|
+
const current = $(panel);
|
|
110
|
+
if (!current || current.hidden) return;
|
|
111
|
+
// Root visibility is already handled by show(). Descendant visibility and
|
|
112
|
+
// text/row mutations are content demand and may require a larger surface.
|
|
113
|
+
if (records.some(record => record.type !== 'attributes' || !panelIds.includes(record.target.id))) remeasure();
|
|
114
|
+
});
|
|
115
|
+
for (const id of panelIds) observer.observe($(id), {
|
|
116
|
+
subtree: true, childList: true, characterData: true, attributes: true, attributeFilter: ['hidden'],
|
|
72
117
|
});
|
|
73
|
-
for (const id of ['actions', 'menu', 'chat', 'bubble', 'decisions']) observer.observe($(id));
|
|
74
118
|
window.addEventListener('beforeunload', () => observer.disconnect());
|
|
75
119
|
return {
|
|
76
|
-
get panel() { return panel; }, show,
|
|
77
|
-
setAmbient(
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
120
|
+
get panel() { return panel; }, show, remeasure,
|
|
121
|
+
setAmbient(value) {
|
|
122
|
+
const next = value === true ? 'bubble' : value === false ? 'none' : value;
|
|
123
|
+
if (!['none', 'bubble', 'actions'].includes(next)) return Promise.resolve();
|
|
124
|
+
if (ambientPanel === next) return Promise.resolve();
|
|
125
|
+
ambientPanel = next;
|
|
126
|
+
return ['none', 'bubble', 'actions'].includes(panel) ? show('none') : Promise.resolve();
|
|
81
127
|
},
|
|
82
128
|
dismiss() { if (drag) suppressClick = true; void show('none'); },
|
|
83
129
|
};
|
package/renderer/pet-motion.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { isLivingClip } from './living-body.mjs';
|
|
2
2
|
import { MotionFacts } from './motion-facts.mjs';
|
|
3
3
|
import { additional, atlas, signals, staticSkins, validId, visualAlias, workAction } from './pet-motion-assets.mjs';
|
|
4
|
-
|
|
5
4
|
/** Select one truthful body from Host facts and local, user-controlled gestures. */
|
|
6
5
|
export class PetMotion {
|
|
7
6
|
constructor(element, options = {}) {
|
|
@@ -51,15 +50,27 @@ export class PetMotion {
|
|
|
51
50
|
this.transient = { action: 'waking' };
|
|
52
51
|
this.refresh(changedSkin);
|
|
53
52
|
}
|
|
54
|
-
|
|
53
|
+
setBehaviorEnabled(enabled) {
|
|
54
|
+
const next = enabled === true;
|
|
55
|
+
if (this.autoPlay === next) return;
|
|
56
|
+
this.autoPlay = next;
|
|
57
|
+
if (!next) {
|
|
58
|
+
if (this.autoTimer !== undefined) this.clearTimer(this.autoTimer);
|
|
59
|
+
this.autoTimer = undefined;
|
|
60
|
+
if (this.transient?.automatic) {
|
|
61
|
+
this.transient = null;
|
|
62
|
+
this.refresh();
|
|
63
|
+
}
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
this.refresh();
|
|
67
|
+
}
|
|
55
68
|
syncSnapshot(value) {
|
|
56
69
|
this.syncFacts(value, false);
|
|
57
70
|
}
|
|
58
|
-
|
|
59
71
|
syncNativeSnapshot(value) {
|
|
60
72
|
this.syncFacts(value, true);
|
|
61
73
|
}
|
|
62
|
-
|
|
63
74
|
syncFacts(value, nativeOnly) {
|
|
64
75
|
const now = this.now();
|
|
65
76
|
this.hasNativeWork = value !== null && value !== undefined;
|
|
@@ -77,7 +88,6 @@ export class PetMotion {
|
|
|
77
88
|
}
|
|
78
89
|
this.refresh();
|
|
79
90
|
}
|
|
80
|
-
|
|
81
91
|
syncTravel(value) {
|
|
82
92
|
this.facts.syncTravel(value, this.dragging);
|
|
83
93
|
if (this.resting && this.snapshot.travel) {
|
|
@@ -87,7 +97,6 @@ export class PetMotion {
|
|
|
87
97
|
}
|
|
88
98
|
this.refresh();
|
|
89
99
|
}
|
|
90
|
-
|
|
91
100
|
setPendingDecision(value) {
|
|
92
101
|
const pending = value === true;
|
|
93
102
|
if (this.pendingDecision === pending) return;
|
|
@@ -99,7 +108,6 @@ export class PetMotion {
|
|
|
99
108
|
}
|
|
100
109
|
this.refresh();
|
|
101
110
|
}
|
|
102
|
-
|
|
103
111
|
setDockedEdge(value) {
|
|
104
112
|
const candidate = typeof value === 'string' ? { edge: value, zone: 'corner' } : value;
|
|
105
113
|
const edge = ['left', 'right'].includes(candidate?.edge) ? candidate.edge : null;
|
|
@@ -110,7 +118,6 @@ export class PetMotion {
|
|
|
110
118
|
if (edge) this.cancelIdleTimers();
|
|
111
119
|
this.refresh(this.action === 'peek');
|
|
112
120
|
}
|
|
113
|
-
|
|
114
121
|
signal(kind, eventId) {
|
|
115
122
|
if (kind === 'answered') return;
|
|
116
123
|
if (kind === 'connected' && this.living()) return;
|
|
@@ -119,7 +126,7 @@ export class PetMotion {
|
|
|
119
126
|
this.lastPlayAt = this.now();
|
|
120
127
|
this.resting = false;
|
|
121
128
|
this.cancelIdleTimers();
|
|
122
|
-
this.transient = { action: 'play' };
|
|
129
|
+
this.transient = { action: 'play', automatic: false };
|
|
123
130
|
this.refresh();
|
|
124
131
|
return;
|
|
125
132
|
}
|
|
@@ -136,7 +143,6 @@ export class PetMotion {
|
|
|
136
143
|
this.transient = { action: requested };
|
|
137
144
|
this.refresh();
|
|
138
145
|
}
|
|
139
|
-
|
|
140
146
|
move(dx, dy) {
|
|
141
147
|
if (!Number.isFinite(dx) || !Number.isFinite(dy) || Math.hypot(dx, dy) < 6) return;
|
|
142
148
|
if (this.dragging) return;
|
|
@@ -149,7 +155,6 @@ export class PetMotion {
|
|
|
149
155
|
this.cancelIdleTimers();
|
|
150
156
|
this.refresh();
|
|
151
157
|
}
|
|
152
|
-
|
|
153
158
|
stopMove() {
|
|
154
159
|
if (!this.dragging) return;
|
|
155
160
|
this.dragging = false;
|
|
@@ -157,7 +162,6 @@ export class PetMotion {
|
|
|
157
162
|
delete this.element.dataset.interaction;
|
|
158
163
|
this.refresh();
|
|
159
164
|
}
|
|
160
|
-
|
|
161
165
|
close() {
|
|
162
166
|
this.cancelFrame();
|
|
163
167
|
this.cancelIdleTimers();
|
|
@@ -174,7 +178,6 @@ export class PetMotion {
|
|
|
174
178
|
return visual === 'sleeping' ? 'idle' : 'review';
|
|
175
179
|
return action;
|
|
176
180
|
}
|
|
177
|
-
|
|
178
181
|
refresh(force = false) {
|
|
179
182
|
this.element.dataset.listening = String(this.listening());
|
|
180
183
|
const selection = this.select();
|
|
@@ -188,7 +191,6 @@ export class PetMotion {
|
|
|
188
191
|
if (action === 'idle') this.armRest();
|
|
189
192
|
else if (action !== 'sleeping') this.cancelRest();
|
|
190
193
|
}
|
|
191
|
-
|
|
192
194
|
select() {
|
|
193
195
|
if (this.dragging) return { action: 'held', oneShot: false };
|
|
194
196
|
if (this.transient?.action === 'failed') return { action: 'failed', oneShot: true };
|
|
@@ -212,7 +214,6 @@ export class PetMotion {
|
|
|
212
214
|
if (this.resting) return { action: 'sleeping', oneShot: false };
|
|
213
215
|
return { action: 'idle', oneShot: false };
|
|
214
216
|
}
|
|
215
|
-
|
|
216
217
|
baseAction() {
|
|
217
218
|
if (this.pendingDecision) return 'pending_decision';
|
|
218
219
|
const work = this.activeWorkAction();
|
|
@@ -223,14 +224,12 @@ export class PetMotion {
|
|
|
223
224
|
}
|
|
224
225
|
|
|
225
226
|
listening() { return ['talking', 'connecting'].includes(this.phase) && !this.context?.muted; }
|
|
226
|
-
|
|
227
227
|
activeWorkAction() {
|
|
228
228
|
if (this.snapshot.work) return workAction[this.snapshot.work.kind];
|
|
229
229
|
if (!this.hasNativeWork && this.context?.nativeActivity === 'tool_running') return 'working';
|
|
230
230
|
if (!this.hasNativeWork && this.context?.nativeActivity === 'reasoning') return 'staged_thought';
|
|
231
231
|
return null;
|
|
232
232
|
}
|
|
233
|
-
|
|
234
233
|
play(action, force = false) {
|
|
235
234
|
if (this.action === action && !force) return;
|
|
236
235
|
this.cancelFrame();
|
|
@@ -239,7 +238,6 @@ export class PetMotion {
|
|
|
239
238
|
this.paint();
|
|
240
239
|
this.scheduleFrame();
|
|
241
240
|
}
|
|
242
|
-
|
|
243
241
|
paint() {
|
|
244
242
|
const { style, dataset } = this.element;
|
|
245
243
|
dataset.skin = this.skin;
|
|
@@ -265,7 +263,6 @@ export class PetMotion {
|
|
|
265
263
|
style.backgroundSize = extra ? '960px 130px' : '960px 1170px';
|
|
266
264
|
style.backgroundPosition = `${-120 * this.frame}px ${-130 * (atlas[visual]?.row ?? 0)}px`;
|
|
267
265
|
}
|
|
268
|
-
|
|
269
266
|
scheduleFrame() {
|
|
270
267
|
if (this.reducedMotion) {
|
|
271
268
|
if (this.oneShot) this.frameTimer = this.setTimer(() => { this.frameTimer = undefined; this.completeOneShot(); }, 1_000);
|
|
@@ -291,12 +288,10 @@ export class PetMotion {
|
|
|
291
288
|
this.scheduleFrame();
|
|
292
289
|
}, frames[this.frame]);
|
|
293
290
|
}
|
|
294
|
-
|
|
295
291
|
cancelFrame() {
|
|
296
292
|
if (this.frameTimer !== undefined) this.clearTimer(this.frameTimer);
|
|
297
293
|
this.frameTimer = undefined;
|
|
298
294
|
}
|
|
299
|
-
|
|
300
295
|
completeOneShot() {
|
|
301
296
|
if (!this.oneShot) return;
|
|
302
297
|
this.oneShot = false;
|
|
@@ -304,7 +299,6 @@ export class PetMotion {
|
|
|
304
299
|
this.action = '';
|
|
305
300
|
this.refresh();
|
|
306
301
|
}
|
|
307
|
-
|
|
308
302
|
canPlay() {
|
|
309
303
|
return !this.reducedMotion && !this.dragging && !this.pendingDecision && this.phase === 'idle'
|
|
310
304
|
&& !this.activeWorkAction() && !this.snapshot.delivery && !this.snapshot.travel && !this.dockedEdge
|
|
@@ -328,7 +322,7 @@ export class PetMotion {
|
|
|
328
322
|
if (!this.canPlay()) return;
|
|
329
323
|
this.lastPlayAt = this.now();
|
|
330
324
|
this.resting = false;
|
|
331
|
-
this.transient = { action: 'play' };
|
|
325
|
+
this.transient = { action: 'play', automatic: true };
|
|
332
326
|
this.refresh();
|
|
333
327
|
}, delay);
|
|
334
328
|
}
|
|
@@ -9,9 +9,17 @@ export class RecentBubble {
|
|
|
9
9
|
this.history = messages.slice(-2);
|
|
10
10
|
this.render();
|
|
11
11
|
}
|
|
12
|
-
|
|
12
|
+
loadTranscript(rows, companionName = '猫猫') {
|
|
13
|
+
this.load(rows.map(row => ({
|
|
14
|
+
role: row.role,
|
|
15
|
+
text: row.text,
|
|
16
|
+
name: row.source?.kind === 'typed' ? '你' : row.role === 'assistant' ? companionName : '语音',
|
|
17
|
+
})));
|
|
18
|
+
}
|
|
19
|
+
append(role, text, name = role === 'user' ? '你' : '猫猫') {
|
|
13
20
|
if (!['user', 'assistant'].includes(role) || typeof text !== 'string') return;
|
|
14
|
-
|
|
21
|
+
const previous = this.live.get(role);
|
|
22
|
+
this.live.set(role, { name, text: `${previous?.text ?? ''}${text}`.slice(-240) });
|
|
15
23
|
this.render();
|
|
16
24
|
}
|
|
17
25
|
finish(role) {
|
|
@@ -22,11 +30,16 @@ export class RecentBubble {
|
|
|
22
30
|
this.live.clear();
|
|
23
31
|
this.render();
|
|
24
32
|
}
|
|
33
|
+
clear() {
|
|
34
|
+
this.history = [];
|
|
35
|
+
this.live.clear();
|
|
36
|
+
this.render();
|
|
37
|
+
}
|
|
25
38
|
hasContent() {
|
|
26
39
|
return this.history.length > 0 || this.live.size > 0;
|
|
27
40
|
}
|
|
28
41
|
render() {
|
|
29
|
-
const live = [...this.live].map(([role,
|
|
42
|
+
const live = [...this.live].map(([role, value]) => ({ role, ...value }));
|
|
30
43
|
const entries = [...this.history.slice(-Math.max(0, 2 - live.length)), ...live].slice(-2);
|
|
31
44
|
for (const [index, row] of this.rows.entries()) {
|
|
32
45
|
const entry = entries[index];
|