@clowder-ai/companion 0.1.0-alpha.3 → 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/NOTICE +7 -2
- package/README.md +54 -5
- package/manifest.json +3 -3
- package/package.json +5 -3
- package/plugin.yaml +3 -3
- package/renderer/app-assist-controller.mjs +125 -0
- package/renderer/call-controls-presentation.mjs +179 -0
- package/renderer/call-duration.mjs +59 -0
- package/renderer/call-transcript-view.mjs +240 -0
- package/renderer/chat-unread.mjs +44 -0
- package/renderer/client.mjs +75 -3
- package/renderer/compact-size.mjs +8 -0
- package/renderer/companion-identity.mjs +42 -0
- package/renderer/conversation-draft.mjs +304 -0
- package/renderer/conversation-history.mjs +176 -0
- package/renderer/conversation-surface.mjs +202 -0
- package/renderer/conversation.mjs +155 -21
- package/renderer/decision-panel.mjs +122 -0
- package/renderer/decision-view.mjs +124 -0
- package/renderer/errors.mjs +4 -4
- package/renderer/index.html +202 -25
- package/renderer/living-body.mjs +147 -0
- package/renderer/living-edge.mjs +16 -0
- package/renderer/living-geometry.mjs +96 -0
- package/renderer/meeting-state.mjs +55 -0
- package/renderer/motion-facts.mjs +108 -0
- package/renderer/native-window-travel.mjs +89 -0
- package/renderer/native-work-motion.mjs +39 -0
- package/renderer/peer.mjs +1 -1
- package/renderer/pet-controls.mjs +139 -27
- package/renderer/pet-motion-assets.mjs +22 -0
- package/renderer/pet-motion.mjs +343 -0
- package/renderer/recent-bubble.mjs +93 -0
- package/renderer/reply-letter-controller.mjs +361 -0
- package/renderer/reply-letter-state.mjs +38 -0
- package/renderer/reply-letter-view.mjs +116 -0
- package/renderer/settings-controller.mjs +171 -0
- package/renderer/settings-view.mjs +241 -0
- package/renderer/skins/xianxian-codex.webp +0 -0
- package/renderer/skins/xianxian-living/clips/carry.webm +0 -0
- package/renderer/skins/xianxian-living/clips/peek.webm +0 -0
- package/renderer/skins/xianxian-living/clips/peek_fade.webm +0 -0
- package/renderer/skins/xianxian-living/clips/pounce.webm +0 -0
- package/renderer/skins/xianxian-living/clips/sleep.webm +0 -0
- package/renderer/skins/xianxian-living/clips/think.webm +0 -0
- package/renderer/skins/xianxian-living/clips/trot.webm +0 -0
- package/renderer/skins/xianxian-living/clips/wait.webm +0 -0
- package/renderer/skins/xianxian-living/clips/wake.webm +0 -0
- package/renderer/skins/xianxian-living/layers/body.webp +0 -0
- package/renderer/skins/xianxian-living/layers/closed.webp +0 -0
- package/renderer/skins/xianxian-living/layers/earL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/earR.webp +0 -0
- package/renderer/skins/xianxian-living/layers/head.webp +0 -0
- package/renderer/skins/xianxian-living/layers/irisL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/irisR.webp +0 -0
- package/renderer/skins/xianxian-living/layers/meta.json +88 -0
- package/renderer/skins/xianxian-living/layers/mouth.webp +0 -0
- package/renderer/skins/xianxian-living/layers/scared.webp +0 -0
- package/renderer/skins/xianxian-living/layers/tail.webp +0 -0
- package/renderer/skins/xianxian-living/layers/whiteL.webp +0 -0
- package/renderer/skins/xianxian-living/layers/whiteR.webp +0 -0
- package/renderer/skins/xianxian-sleeping-row.png +0 -0
- package/renderer/skins/xianxian-staged-thought-row.png +0 -0
- package/renderer/skins/xianxian-working-row.png +0 -0
- package/renderer/style.css +325 -27
- package/renderer/surface.mjs +309 -51
- package/renderer/transcript-view.mjs +117 -15
- package/renderer/visible-text-receipt.mjs +241 -0
- package/renderer/work-overview.mjs +95 -0
- package/source-lock.json +122 -2
package/renderer/index.html
CHANGED
|
@@ -4,52 +4,229 @@
|
|
|
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>
|
|
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>
|
|
7
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>
|
|
8
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>
|
|
9
11
|
<symbol id="i-screen" viewBox="0 0 24 24"><rect x="3" y="4" width="18" height="13" rx="2.5"/><path d="M8 21h8M12 17v4"/></symbol>
|
|
10
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>
|
|
11
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>
|
|
12
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>
|
|
13
17
|
</svg>
|
|
14
18
|
<main id="desktop" aria-label="猫猫球">
|
|
15
19
|
<div id="anchor">
|
|
16
|
-
<button id="pet" aria-label="
|
|
20
|
+
<button id="pet" aria-label="猫猫:点击或右键打开名片,拖动移动" aria-expanded="false" aria-controls="actions menu chat transcript settings decisions app-assist">
|
|
21
|
+
<svg id="living-sit" viewBox="0 0 600 600" aria-hidden="true" hidden>
|
|
22
|
+
<defs>
|
|
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>
|
|
24
|
+
<mask id="living-eye-right" maskUnits="userSpaceOnUse" x="0" y="0" width="600" height="600"><image href="./skins/xianxian-living/layers/whiteR.webp" width="600" height="600"/></mask>
|
|
25
|
+
</defs>
|
|
26
|
+
<g data-layer="tail"><image href="./skins/xianxian-living/layers/tail.webp" width="600" height="600"/></g>
|
|
27
|
+
<image href="./skins/xianxian-living/layers/body.webp" width="600" height="600"/>
|
|
28
|
+
<g data-layer="head">
|
|
29
|
+
<g data-layer="ear-left"><image href="./skins/xianxian-living/layers/earL.webp" width="600" height="600"/></g>
|
|
30
|
+
<g data-layer="ear-right"><image href="./skins/xianxian-living/layers/earR.webp" width="600" height="600"/></g>
|
|
31
|
+
<image href="./skins/xianxian-living/layers/head.webp" width="600" height="600"/>
|
|
32
|
+
<g mask="url(#living-eye-left)"><image data-layer="iris-left" href="./skins/xianxian-living/layers/irisL.webp" width="600" height="600"/></g>
|
|
33
|
+
<g mask="url(#living-eye-right)"><image data-layer="iris-right" href="./skins/xianxian-living/layers/irisR.webp" width="600" height="600"/></g>
|
|
34
|
+
<image data-layer="closed" href="./skins/xianxian-living/layers/closed.webp" width="600" height="600"/>
|
|
35
|
+
<image data-layer="mouth" href="./skins/xianxian-living/layers/mouth.webp" width="600" height="600"/>
|
|
36
|
+
<image data-layer="scared" href="./skins/xianxian-living/layers/scared.webp" width="600" height="600"/>
|
|
37
|
+
</g>
|
|
38
|
+
</svg>
|
|
39
|
+
<video id="living-video" muted playsinline preload="metadata" aria-hidden="true" hidden></video>
|
|
40
|
+
<video id="living-video-transition" muted playsinline preload="metadata" aria-hidden="true" hidden></video>
|
|
41
|
+
</button>
|
|
17
42
|
<div id="indicators">
|
|
18
|
-
<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>
|
|
19
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>
|
|
44
|
+
<button id="pending-badge" hidden aria-label="查看待决事项" title="查看待决事项"><span id="pending-count">?</span></button>
|
|
20
45
|
</div>
|
|
21
46
|
</div>
|
|
22
|
-
<section id="
|
|
23
|
-
<p id="
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
47
|
+
<section id="bubble" class="floating" aria-label="最新消息" hidden>
|
|
48
|
+
<p id="bubble-first" hidden></p><p id="bubble-second" hidden></p>
|
|
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>
|
|
57
|
+
<section id="actions" class="floating call-bar" aria-label="当前通话" hidden>
|
|
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>
|
|
28
63
|
</div>
|
|
29
|
-
<div id="
|
|
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>
|
|
65
|
+
<div id="active-actions" class="action-row call-actions">
|
|
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>
|
|
30
67
|
<button data-action="share" id="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
31
|
-
<button
|
|
68
|
+
<button data-action="transcript" aria-label="字幕" title="字幕"><svg class="icon" aria-hidden="true"><use href="#i-subtitles"/></svg><span>字幕</span></button>
|
|
69
|
+
<button id="call-chat" data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span><span id="call-chat-unread" class="action-unread" hidden></span></button>
|
|
70
|
+
<button id="end" class="destructive" data-action="stop"><span>挂断</span></button>
|
|
32
71
|
</div>
|
|
72
|
+
<form id="lite-call-compose"><input id="lite-call-message" aria-label="通话中给猫猫发消息" placeholder="发消息给猫猫…" maxlength="8000"><button type="submit" class="primary">发送</button></form>
|
|
73
|
+
<p id="status" role="status" hidden></p>
|
|
33
74
|
</section>
|
|
34
|
-
<section id="menu" class="floating" aria-label="
|
|
35
|
-
<
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
75
|
+
<section id="menu" class="floating name-card" aria-label="猫猫名片" hidden>
|
|
76
|
+
<div id="menu-call-context" class="call-context" hidden>
|
|
77
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="menu-call-state"></strong><span id="menu-call-mic-state"></span>
|
|
78
|
+
<small id="menu-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="menu-call-meeting-label" class="meeting-label"></span><span id="menu-call-meeting-source" class="meeting-source" hidden></span></small>
|
|
79
|
+
<small id="menu-call-work" hidden></small>
|
|
80
|
+
<div id="menu-call-actions" class="call-context-actions menu-call-actions">
|
|
81
|
+
<button id="menu-mic" data-action="mute" aria-label="静音"><svg class="icon" aria-hidden="true"><use id="menu-mic-icon" href="#i-mic"/></svg></button>
|
|
82
|
+
<button id="menu-call-share" data-action="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
83
|
+
<button id="menu-call-chat" data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span></button>
|
|
84
|
+
<button data-action="stop" class="destructive compact">挂断</button>
|
|
85
|
+
</div>
|
|
86
|
+
<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>
|
|
87
|
+
</div>
|
|
88
|
+
<div id="menu-scroll" class="menu-scroll">
|
|
89
|
+
<strong id="menu-name" class="menu-title">猫猫</strong>
|
|
90
|
+
<div id="name-card-actions" class="name-card-actions">
|
|
91
|
+
<button id="begin" class="primary" data-action="begin"><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">语音通话</span></button>
|
|
92
|
+
<button id="listen" data-action="listen" hidden><svg class="icon" aria-hidden="true"><use href="#i-listen"/></svg><span class="label">只听</span></button>
|
|
93
|
+
<button data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span></button>
|
|
94
|
+
</div>
|
|
95
|
+
<p id="menu-status" role="status" hidden></p>
|
|
96
|
+
<button data-action="decisions"><span aria-hidden="true">◇</span><span>待办</span><span id="menu-pending" class="menu-status">—</span></button>
|
|
97
|
+
<div id="work-overview" class="work-overview" hidden>
|
|
98
|
+
<section id="work-active-section" class="work-overview-section" hidden>
|
|
99
|
+
<header><span>进行中的工作</span><small id="work-scope"></small></header>
|
|
100
|
+
<div id="work-active-list" class="work-overview-list"></div>
|
|
101
|
+
</section>
|
|
102
|
+
<section id="work-delivery-section" class="work-overview-section" hidden>
|
|
103
|
+
<header><span>最近交付</span></header>
|
|
104
|
+
<div id="work-delivery-list" class="work-overview-list"></div>
|
|
105
|
+
</section>
|
|
106
|
+
<p id="work-notice" role="status"></p>
|
|
107
|
+
</div>
|
|
108
|
+
<button data-action="history"><svg class="icon" aria-hidden="true"><use href="#i-history"/></svg><span>打开聊天</span></button>
|
|
109
|
+
<button id="menu-play" data-action="play" hidden><span aria-hidden="true">✦</span><span>逗宪宪</span></button>
|
|
110
|
+
<button id="menu-share" data-action="share" title="语音通话连接后可共享画面" hidden><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
111
|
+
<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>
|
|
112
|
+
</div>
|
|
113
|
+
<footer class="menu-footer">
|
|
114
|
+
<button id="speaker" data-action="speaker" hidden><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">播音</span></button>
|
|
115
|
+
<button id="menu-settings" data-action="settings"><span aria-hidden="true">⚙</span><span>设置</span></button>
|
|
116
|
+
<button id="menu-size-toggle" data-action="size-toggle"><svg class="icon" aria-hidden="true"><use href="#i-expand"/></svg><span class="label">缩小</span></button>
|
|
117
|
+
<button id="menu-hide" data-action="hide" class="quiet-action" title="可从 Clowder 的聊聊入口叫回来"><svg class="icon" aria-hidden="true"><use href="#i-moon"/></svg><span id="menu-hide-label">隐藏</span></button>
|
|
118
|
+
</footer>
|
|
44
119
|
</section>
|
|
45
120
|
<section id="chat" class="floating" aria-label="和猫猫写几句" hidden>
|
|
46
|
-
<
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
121
|
+
<div id="chat-call-context" class="call-context" hidden>
|
|
122
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="chat-call-state"></strong><span id="chat-call-mic-state"></span>
|
|
123
|
+
<small id="chat-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="chat-call-meeting-label" class="meeting-label"></span><span id="chat-call-meeting-source" class="meeting-source" hidden></span></small>
|
|
124
|
+
<small id="chat-call-work" hidden></small>
|
|
125
|
+
<div class="call-context-actions call-context-inline-actions"><button id="chat-mic" data-action="mute">静音</button><button id="chat-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
126
|
+
<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>
|
|
127
|
+
</div>
|
|
128
|
+
<header id="chat-history-header"><strong id="chat-name">和猫猫写几句</strong>
|
|
129
|
+
<button id="chat-history-dismiss" data-action="dismiss" aria-label="收起文字">收起</button>
|
|
130
|
+
</header>
|
|
131
|
+
<div id="chat-transcript-shell" class="chat-transcript-shell" hidden>
|
|
132
|
+
<div id="chat-transcript" role="log" aria-live="polite"><p class="empty">想说什么,就写在这里。</p></div>
|
|
133
|
+
<button id="reply-history-close" class="reply-history-close" data-action="dismiss" aria-label="关闭历史" title="关闭历史" hidden>
|
|
134
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="m4 4 8 8m0-8-8 8"/></svg>
|
|
135
|
+
</button>
|
|
136
|
+
<button id="reply-return-latest" class="reply-return-latest" type="button" hidden>回到最新 ↓</button>
|
|
137
|
+
</div>
|
|
138
|
+
<div id="reply-sent" class="reply-sent" tabindex="-1" hidden>
|
|
139
|
+
<svg viewBox="0 0 36 27" aria-hidden="true"><rect x="1" y="1" width="34" height="25" rx="4"/><path d="M2.5 3.5 18 15 33.5 3.5"/></svg>
|
|
140
|
+
<span><strong id="reply-sent-status">已发送</strong><small id="reply-letter-text" hidden></small></span>
|
|
141
|
+
</div>
|
|
142
|
+
<article id="reply-card" class="reply-card" hidden tabindex="-1">
|
|
143
|
+
<div class="reply-head"><span id="reply-input-text" class="reply-input-text"></span>
|
|
144
|
+
<button id="reply-retain" class="reply-ring" type="button" aria-label="留住回复" title="留住回复">
|
|
145
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><circle class="reply-ring-track" cx="8" cy="8" r="6.5"/><circle class="reply-ring-progress" cx="8" cy="8" r="6.5"/></svg>
|
|
146
|
+
</button>
|
|
147
|
+
<button id="reply-close" class="reply-close" type="button" aria-label="关闭回复" title="关闭回复" hidden>
|
|
148
|
+
<svg viewBox="0 0 16 16" aria-hidden="true"><path d="m4 4 8 8m0-8-8 8"/></svg>
|
|
149
|
+
</button>
|
|
150
|
+
</div>
|
|
151
|
+
<div id="reply-body" class="reply-body"></div>
|
|
152
|
+
<small id="reply-body-status" class="reply-body-status" role="status"></small>
|
|
153
|
+
<button id="reply-history-open" class="reply-history-open" type="button">往上翻聊天记录</button>
|
|
154
|
+
</article>
|
|
155
|
+
<form id="compose"><textarea id="message" aria-label="给猫猫输入消息" placeholder="输入消息…" rows="1" maxlength="8000"></textarea><button type="submit" class="primary">发送</button></form>
|
|
156
|
+
<div id="chat-feedback" class="chat-feedback" hidden><small id="chat-status" role="status"></small><button id="reply-retry" type="button" hidden>↻ 重试</button></div>
|
|
157
|
+
<small id="chat-connection-status" role="status"></small>
|
|
158
|
+
<small id="chat-scope"><span id="connection-scope">语音与文字共用同一段聊天</span> <button id="history">在 Cat Café 中查看完整聊天 ↗</button></small>
|
|
159
|
+
</section>
|
|
160
|
+
<section id="transcript" class="floating call-transcript" data-expanded="false" aria-label="这次通话的字幕" hidden>
|
|
161
|
+
<div id="transcript-call-context" class="call-context transcript-call-context">
|
|
162
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="transcript-call-state"></strong><span id="transcript-call-mic-state"></span>
|
|
163
|
+
<small id="transcript-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="transcript-call-meeting-label" class="meeting-label"></span><span id="transcript-call-meeting-source" class="meeting-source" hidden></span></small>
|
|
164
|
+
<small id="transcript-call-work" hidden></small>
|
|
165
|
+
<div class="call-context-actions">
|
|
166
|
+
<button id="transcript-return" class="transcript-return" type="button" data-action="transcript-return"><span aria-hidden="true">‹</span><span>返回通话</span></button>
|
|
167
|
+
<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>
|
|
168
|
+
<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>
|
|
169
|
+
<button data-action="stop" class="destructive compact">挂断</button>
|
|
170
|
+
</div>
|
|
171
|
+
<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>
|
|
172
|
+
</div>
|
|
173
|
+
<div id="call-transcript-log" role="log" aria-live="polite"></div>
|
|
174
|
+
<button id="call-unread" class="new-content" type="button" hidden></button>
|
|
175
|
+
<form id="call-compose"><input id="call-message" aria-label="通话中给猫猫发消息" placeholder="发消息给猫猫…" maxlength="8000"><button type="submit" class="primary">发送</button></form>
|
|
176
|
+
<small id="call-transcript-status" role="status"></small>
|
|
177
|
+
<small class="call-transcript-foot">完整记录会留在 Cat Café 聊天中</small>
|
|
178
|
+
</section>
|
|
179
|
+
<section id="settings" class="floating settings-panel" aria-label="猫猫球设置" hidden>
|
|
180
|
+
<div id="settings-call-context" class="call-context" hidden>
|
|
181
|
+
<span class="status-dot" aria-hidden="true"></span><strong id="settings-call-state"></strong><span id="settings-call-mic-state"></span>
|
|
182
|
+
<small id="settings-call-meeting" class="call-meeting" hidden><span class="meeting-status-dot" aria-hidden="true"></span><span id="settings-call-meeting-label" class="meeting-label"></span><span id="settings-call-meeting-source" class="meeting-source" hidden></span></small>
|
|
183
|
+
<small id="settings-call-work" hidden></small>
|
|
184
|
+
<div class="call-context-actions call-context-inline-actions"><button id="settings-mic" data-action="mute">静音</button><button id="settings-share" data-action="share"><span class="label">共享画面</span></button><button data-action="stop" class="destructive compact">挂断</button></div>
|
|
185
|
+
<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>
|
|
186
|
+
</div>
|
|
187
|
+
<header class="settings-head"><button id="settings-back" type="button" aria-label="返回名片">‹</button><strong id="settings-title">设置</strong></header>
|
|
188
|
+
<p id="settings-loading" class="settings-loading" hidden>正在读取设置…</p>
|
|
189
|
+
<div id="settings-page-main" class="settings-page">
|
|
190
|
+
<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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
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>
|
|
197
|
+
</div>
|
|
198
|
+
<div id="settings-page-partner" class="settings-page" hidden>
|
|
199
|
+
<p class="settings-copy">在桌面陪伴和回复你的猫。更换陪伴者将结束当前语音通话。</p>
|
|
200
|
+
<div id="settings-roster" class="settings-roster"></div>
|
|
201
|
+
<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>
|
|
202
|
+
</div>
|
|
203
|
+
<div id="settings-page-look" class="settings-page" hidden>
|
|
204
|
+
<p class="settings-eyebrow">皮肤</p><p class="settings-copy">仅改变外观,不更换陪伴者。</p>
|
|
205
|
+
<div class="settings-skins">
|
|
206
|
+
<button id="settings-skin-xianxian-codex" type="button" data-setting-control>宪宪 v1</button>
|
|
207
|
+
<button id="settings-skin-yanyan-codex" type="button" data-setting-control>砚砚 v1</button>
|
|
208
|
+
<button id="settings-skin-ragdoll-v1" type="button" data-setting-control>布偶猫</button>
|
|
209
|
+
<button id="settings-skin-yarn-ball" type="button" data-setting-control>猫猫球</button>
|
|
210
|
+
</div>
|
|
211
|
+
<label id="settings-size-field" class="settings-field settings-size"><span>大小</span><div><small>小</small><input id="settings-size" type="range" min="43" max="192" step="1" data-setting-control><small>大</small></div></label>
|
|
212
|
+
</div>
|
|
213
|
+
<div id="settings-page-confirm" class="settings-page settings-confirm" hidden>
|
|
214
|
+
<strong id="settings-confirm-title"></strong><p id="settings-confirm-copy"></p>
|
|
215
|
+
<div><button id="settings-confirm-cancel" type="button">取消</button><button id="settings-confirm-apply" type="button" data-setting-control>确认更改</button></div>
|
|
216
|
+
</div>
|
|
217
|
+
<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>
|
|
218
|
+
<div id="settings-page-disabled" class="settings-page settings-empty" hidden><p>桌面猫猫球已停用。可在 Cat Café 的“设置 › 插件”中重新启用。</p></div>
|
|
219
|
+
<p id="settings-notice" class="settings-notice" role="status" hidden></p>
|
|
220
|
+
<div class="settings-notice-actions"><button id="settings-retry" type="button" hidden>重试</button><button id="settings-reread" type="button" hidden>重新读取</button></div>
|
|
51
221
|
</section>
|
|
52
222
|
<p id="announcement" class="sr-only" role="status"></p>
|
|
223
|
+
<section id="decisions" class="floating" aria-label="待办" hidden>
|
|
224
|
+
<header><strong>待办</strong><button id="decision-reload" type="button">刷新</button></header>
|
|
225
|
+
<p id="decision-status" role="status">正在读取待办…</p>
|
|
226
|
+
<ul id="decision-list"></ul>
|
|
227
|
+
<button id="decision-more" type="button" hidden>查看更多</button>
|
|
228
|
+
<small>这里只查看原处事项。要决定时请打开对应卡片。</small>
|
|
229
|
+
</section>
|
|
53
230
|
</main>
|
|
54
231
|
<script type="module" src="./surface.mjs"></script>
|
|
55
232
|
</body></html>
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { isLivingClip, livingClipFor, placeLivingClip } from './living-geometry.mjs';
|
|
2
|
+
export { isLivingClip } from './living-geometry.mjs';
|
|
3
|
+
const setHidden = (node, hidden) => {
|
|
4
|
+
if (!node) return;
|
|
5
|
+
node.toggleAttribute?.('hidden', hidden);
|
|
6
|
+
node.hidden = hidden;
|
|
7
|
+
};
|
|
8
|
+
|
|
9
|
+
/** One visible body: a layered sit or one muted clip, with a bounded sleep/wake hand-off. */
|
|
10
|
+
export class LivingBody {
|
|
11
|
+
constructor({ root, sit, video, transitionVideo, setTimer = globalThis.setTimeout, clearTimer = globalThis.clearTimeout }) {
|
|
12
|
+
this.root = root;
|
|
13
|
+
this.sit = sit;
|
|
14
|
+
this.video = video;
|
|
15
|
+
this.standby = transitionVideo;
|
|
16
|
+
this.setTimer = (callback, delay) => setTimer.call(globalThis, callback, delay);
|
|
17
|
+
this.clearTimer = id => clearTimer.call(globalThis, id);
|
|
18
|
+
this.action = '';
|
|
19
|
+
this.revision = 0;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
show(action, { reducedMotion = false, onEnded = () => {} } = {}) {
|
|
23
|
+
const dockEdge = this.root.dataset.dockEdge;
|
|
24
|
+
const dockZone = this.root.dataset.dockZone;
|
|
25
|
+
if (this.action === action && this.reducedMotion === reducedMotion && this.dockEdge === dockEdge && this.dockZone === dockZone) return;
|
|
26
|
+
const previous = this.action;
|
|
27
|
+
this.action = action;
|
|
28
|
+
this.reducedMotion = reducedMotion;
|
|
29
|
+
this.dockEdge = dockEdge;
|
|
30
|
+
this.dockZone = dockZone;
|
|
31
|
+
const revision = ++this.revision;
|
|
32
|
+
this.cancelFade();
|
|
33
|
+
this.root.dataset.pose = this.pose(action);
|
|
34
|
+
this.root.dataset.direction = action === 'running-left' || (action === 'peek' && dockEdge === 'left') ? 'left' : 'right';
|
|
35
|
+
const clip = !reducedMotion ? livingClipFor(action, dockZone) : null;
|
|
36
|
+
if (!clip) {
|
|
37
|
+
this.hideVideos();
|
|
38
|
+
setHidden(this.sit, false);
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
const crossfade = previous === 'sleeping' && action === 'waking' && this.standby;
|
|
42
|
+
if (crossfade) this.crossfade(clip, revision, onEnded);
|
|
43
|
+
else this.showClip(this.video, clip, revision, onEnded);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
pose(action) {
|
|
47
|
+
if (action === 'failed') return 'scared';
|
|
48
|
+
if (action === 'waiting') return 'listening';
|
|
49
|
+
if (['sleeping', 'pending_decision', 'held', 'working', 'fetching', 'studying'].includes(action))
|
|
50
|
+
return action === 'pending_decision' ? 'pending' : action;
|
|
51
|
+
return 'sit';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
showClip(video, clip, revision, onEnded) {
|
|
55
|
+
this.retireVideo(this.standby);
|
|
56
|
+
this.configure(video, clip, revision, onEnded);
|
|
57
|
+
video.style.opacity = '1';
|
|
58
|
+
setHidden(this.sit, true);
|
|
59
|
+
setHidden(video, false);
|
|
60
|
+
this.play(video, revision, onEnded);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
crossfade(clip, revision, onEnded) {
|
|
64
|
+
const outgoing = this.video;
|
|
65
|
+
const incoming = this.standby;
|
|
66
|
+
this.configure(incoming, clip, revision, onEnded);
|
|
67
|
+
incoming.style.transition = `opacity ${clip.crossfadeMs}ms linear`;
|
|
68
|
+
outgoing.style.transition = `opacity ${clip.crossfadeMs}ms linear`;
|
|
69
|
+
incoming.style.opacity = '0';
|
|
70
|
+
outgoing.style.opacity = '1';
|
|
71
|
+
setHidden(this.sit, true);
|
|
72
|
+
setHidden(outgoing, false);
|
|
73
|
+
setHidden(incoming, false);
|
|
74
|
+
this.play(incoming, revision, onEnded);
|
|
75
|
+
incoming.style.opacity = '1';
|
|
76
|
+
outgoing.style.opacity = '0';
|
|
77
|
+
this.video = incoming;
|
|
78
|
+
this.standby = outgoing;
|
|
79
|
+
this.fadeTimer = this.setTimer(() => {
|
|
80
|
+
this.fadeTimer = undefined;
|
|
81
|
+
if (revision !== this.revision) return;
|
|
82
|
+
this.retireVideo(outgoing);
|
|
83
|
+
}, clip.crossfadeMs);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
configure(video, clip, revision, onEnded) {
|
|
87
|
+
video.onended = null;
|
|
88
|
+
video.pause();
|
|
89
|
+
video.src = `./skins/xianxian-living/clips/${clip.name}.webm`;
|
|
90
|
+
video.loop = clip.loop === true;
|
|
91
|
+
video.playbackRate = clip.rate ?? 1;
|
|
92
|
+
const placement = placeLivingClip(this.action, clip, this.root.dataset.dockEdge);
|
|
93
|
+
video.style.width = `${placement.width}px`;
|
|
94
|
+
video.style.height = `${placement.height}px`;
|
|
95
|
+
video.style.left = `${placement.left}px`;
|
|
96
|
+
video.style.top = `${placement.top}px`;
|
|
97
|
+
video.style.animation = clip.endGround
|
|
98
|
+
? `living-wake-anchor ${clip.durationMs}ms linear forwards` : 'none';
|
|
99
|
+
if (clip.endGround) {
|
|
100
|
+
video.style.setProperty?.('--living-left-start', `${placement.left}px`);
|
|
101
|
+
video.style.setProperty?.('--living-top-start', `${placement.top}px`);
|
|
102
|
+
video.style.setProperty?.('--living-left-end', `${placement.endLeft}px`);
|
|
103
|
+
video.style.setProperty?.('--living-top-end', `${placement.endTop}px`);
|
|
104
|
+
}
|
|
105
|
+
try { video.currentTime = 0; } catch { /* New source is not yet seekable. */ }
|
|
106
|
+
video.onended = () => { if (revision === this.revision) onEnded(); };
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
play(video, revision, onEnded) {
|
|
110
|
+
void video.play().catch(() => {
|
|
111
|
+
if (revision !== this.revision) return;
|
|
112
|
+
this.hideVideos();
|
|
113
|
+
setHidden(this.sit, false);
|
|
114
|
+
onEnded();
|
|
115
|
+
});
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
hideVideos() {
|
|
119
|
+
for (const video of [this.video, this.standby]) {
|
|
120
|
+
this.retireVideo(video);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
retireVideo(video) {
|
|
125
|
+
if (!video) return;
|
|
126
|
+
video.onended = null;
|
|
127
|
+
video.pause();
|
|
128
|
+
setHidden(video, true);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
cancelFade() {
|
|
132
|
+
if (this.fadeTimer !== undefined) this.clearTimer(this.fadeTimer);
|
|
133
|
+
this.fadeTimer = undefined;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
close() {
|
|
137
|
+
this.revision++;
|
|
138
|
+
this.cancelFade();
|
|
139
|
+
this.hideVideos();
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
hide() {
|
|
143
|
+
this.close();
|
|
144
|
+
setHidden(this.sit, true);
|
|
145
|
+
this.action = '';
|
|
146
|
+
}
|
|
147
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/** Observe the Host window's current screen; never select or move a display. */
|
|
2
|
+
export function detectDockedEdge(win, petRect) {
|
|
3
|
+
const left = win.screen?.availLeft;
|
|
4
|
+
const width = win.screen?.availWidth;
|
|
5
|
+
if (![win.screenX, left, width, petRect.left, petRect.width].every(Number.isFinite) || width <= 0) return null;
|
|
6
|
+
const petLeft = win.screenX + petRect.left;
|
|
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' };
|
|
16
|
+
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// F317 living Xianxian package v3. All values are reviewed package CSS anchors;
|
|
2
|
+
// every clip uses one stage scale. content bounds are the union of decoded
|
|
3
|
+
// alpha pixels and exist to size the native transparent surface, not to apply
|
|
4
|
+
// per-action "looks right" scaling.
|
|
5
|
+
export const LIVING_GEOMETRY = Object.freeze({
|
|
6
|
+
pet: Object.freeze({ width: 120, height: 130 }),
|
|
7
|
+
stage: Object.freeze({ size: 290, centre: 145, ground: 262.5 }),
|
|
8
|
+
surface: Object.freeze({ left: -32, top: -18, right: 143, bottom: 138 }),
|
|
9
|
+
sitContent: Object.freeze({ left: 8.8, top: 3.4, right: 94.2, bottom: 109.2 }),
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
export const LIVING_SCALE = LIVING_GEOMETRY.pet.width / LIVING_GEOMETRY.stage.size;
|
|
13
|
+
export const LIVING_REFERENCE = Object.freeze({
|
|
14
|
+
centre: LIVING_GEOMETRY.stage.centre * LIVING_SCALE,
|
|
15
|
+
ground: LIVING_GEOMETRY.stage.ground * LIVING_SCALE,
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
const clip = (name, size, ground, centre, content, options = {}) => Object.freeze({
|
|
19
|
+
name, size: Object.freeze(size), ground, centre, content: Object.freeze(content), ...options,
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
const library = Object.freeze({
|
|
23
|
+
trot: clip('trot', [265, 236], 234.8, 132.5,
|
|
24
|
+
{ left: 1.5, top: 2, right: 263.5, bottom: 235.5 }, { loop: true }),
|
|
25
|
+
pounce: clip('pounce', [304.8, 312.6], 303.9, 201.9,
|
|
26
|
+
{ left: 3, top: 0, right: 302, bottom: 312 }, { rate: 1.5 }),
|
|
27
|
+
sleep: clip('sleep', [271.2, 168], 161, 133.2,
|
|
28
|
+
{ left: 2.5, top: 2.5, right: 268.5, bottom: 166.5 }, { loop: true }),
|
|
29
|
+
wake: clip('wake', [400.2, 282.7], 244.2, 200.5,
|
|
30
|
+
{ left: 3, top: 3, right: 397, bottom: 280.5 },
|
|
31
|
+
{ endGround: 277.4, endCentre: 197.7, durationMs: 7_250, crossfadeMs: 250 }),
|
|
32
|
+
peek: clip('peek', [153.5, 234.3], 234.3, 76.75,
|
|
33
|
+
{ left: 2.5, top: 2.5, right: 153, bottom: 234 }, { loop: true }),
|
|
34
|
+
peek_fade: clip('peek_fade', [153.5, 234.3], 234.3, 76.75,
|
|
35
|
+
{ left: 2.5, top: 2.5, right: 153, bottom: 231.5 }, { loop: true }),
|
|
36
|
+
carry: clip('carry', [344.3, 290.9], 263, 198.3,
|
|
37
|
+
{ left: 4, top: 4, right: 340, bottom: 288.5 }),
|
|
38
|
+
wait: clip('wait', [197.9, 252.8], 249.6, 98.4,
|
|
39
|
+
{ left: 4, top: 4, right: 194, bottom: 251 }, { loop: true }),
|
|
40
|
+
think: clip('think', [266.4, 256.9], 253.7, 132.1,
|
|
41
|
+
{ left: 3.5, top: 3.5, right: 262, bottom: 255 }, { loop: true }),
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
const actionClip = Object.freeze({
|
|
45
|
+
'running-left': 'trot',
|
|
46
|
+
'running-right': 'trot',
|
|
47
|
+
running: 'trot',
|
|
48
|
+
play: 'pounce',
|
|
49
|
+
sleeping: 'sleep',
|
|
50
|
+
waking: 'wake',
|
|
51
|
+
peek: 'peek',
|
|
52
|
+
delivering: 'carry',
|
|
53
|
+
review: 'carry',
|
|
54
|
+
pending_decision: 'wait',
|
|
55
|
+
working: 'think',
|
|
56
|
+
fetching: 'think',
|
|
57
|
+
studying: 'think',
|
|
58
|
+
staged_thought: 'think',
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
export const isLivingClip = action => Object.hasOwn(actionClip, action);
|
|
62
|
+
|
|
63
|
+
export function livingClipFor(action, dockZone) {
|
|
64
|
+
const name = action === 'peek' && dockZone !== 'corner' ? 'peek_fade' : actionClip[action];
|
|
65
|
+
return name ? library[name] : null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export function placeLivingClip(action, selected, dockEdge) {
|
|
69
|
+
const width = selected.size[0] * LIVING_SCALE;
|
|
70
|
+
const height = selected.size[1] * LIVING_SCALE;
|
|
71
|
+
const left = action === 'peek'
|
|
72
|
+
? (dockEdge === 'left' ? LIVING_GEOMETRY.surface.left : LIVING_GEOMETRY.surface.right - width)
|
|
73
|
+
: LIVING_REFERENCE.centre - selected.centre * LIVING_SCALE;
|
|
74
|
+
const top = action === 'peek'
|
|
75
|
+
? LIVING_GEOMETRY.surface.bottom - height
|
|
76
|
+
: LIVING_REFERENCE.ground - selected.ground * LIVING_SCALE;
|
|
77
|
+
return Object.freeze({
|
|
78
|
+
left, top, width, height,
|
|
79
|
+
endLeft: selected.endCentre === undefined
|
|
80
|
+
? undefined : LIVING_REFERENCE.centre - selected.endCentre * LIVING_SCALE,
|
|
81
|
+
endTop: selected.endGround === undefined
|
|
82
|
+
? undefined : LIVING_REFERENCE.ground - selected.endGround * LIVING_SCALE,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function livingContentRect(selected, placement, endpoint = 'start') {
|
|
87
|
+
const left = endpoint === 'end' ? placement.endLeft : placement.left;
|
|
88
|
+
const top = endpoint === 'end' ? placement.endTop : placement.top;
|
|
89
|
+
if (!Number.isFinite(left) || !Number.isFinite(top)) return null;
|
|
90
|
+
return Object.freeze({
|
|
91
|
+
left: left + selected.content.left * LIVING_SCALE,
|
|
92
|
+
top: top + selected.content.top * LIVING_SCALE,
|
|
93
|
+
right: left + selected.content.right * LIVING_SCALE,
|
|
94
|
+
bottom: top + selected.content.bottom * LIVING_SCALE,
|
|
95
|
+
});
|
|
96
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
export function meetingPresentation(meeting) {
|
|
2
|
+
if (!meeting || meeting.kind === 'idle') return null;
|
|
3
|
+
if (meeting.kind === 'running') {
|
|
4
|
+
if (meeting.paused) return { label: '会议记录已暂停', sourceLabel: meeting.sourceLabel, tone: 'quiet' };
|
|
5
|
+
return meeting.sharing
|
|
6
|
+
? { label: '听会中', sourceLabel: meeting.sourceLabel, tone: 'active' }
|
|
7
|
+
: { label: '会议记录中 · 未接入通话', sourceLabel: meeting.sourceLabel, tone: 'active' };
|
|
8
|
+
}
|
|
9
|
+
if (meeting.kind === 'stopped') return { label: '会议记录已停止', sourceLabel: null, tone: 'quiet' };
|
|
10
|
+
if (meeting.kind === 'unconfirmed') return { label: '会议记录状态未确认', sourceLabel: null, tone: 'warning' };
|
|
11
|
+
if (meeting.kind === 'needs_source') return { label: '需要确认会议所在的 App', sourceLabel: null, tone: 'warning' };
|
|
12
|
+
return null;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const meetingPrefixes = ['call', 'menu-call', 'chat-call', 'transcript-call', 'settings-call'];
|
|
16
|
+
export function renderMeetingStatus(document, presentation) {
|
|
17
|
+
for (const prefix of meetingPrefixes) {
|
|
18
|
+
const row = document.getElementById(`${prefix}-meeting`);
|
|
19
|
+
const source = document.getElementById(`${prefix}-meeting-source`);
|
|
20
|
+
row.hidden = presentation === null;
|
|
21
|
+
row.dataset.tone = presentation?.tone ?? '';
|
|
22
|
+
document.getElementById(`${prefix}-meeting-label`).textContent = presentation?.label ?? '';
|
|
23
|
+
source.textContent = presentation?.sourceLabel ? ` · ${presentation.sourceLabel}` : '';
|
|
24
|
+
source.hidden = !presentation?.sourceLabel;
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Keeps the stopped acknowledgement transient without hiding uncertain or active capture truth. */
|
|
29
|
+
export class MeetingStatus {
|
|
30
|
+
constructor({ now = () => Date.now() } = {}) {
|
|
31
|
+
this.now = now;
|
|
32
|
+
this.previousKind = null;
|
|
33
|
+
this.stoppedUntil = 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
update(meeting) {
|
|
37
|
+
const kind = meeting?.kind ?? 'idle';
|
|
38
|
+
if (kind === 'idle') {
|
|
39
|
+
this.previousKind = 'idle';
|
|
40
|
+
this.stoppedUntil = 0;
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
if (kind === 'stopped') {
|
|
44
|
+
if (this.previousKind !== 'stopped') {
|
|
45
|
+
this.stoppedUntil = this.previousKind !== null && this.previousKind !== 'idle'
|
|
46
|
+
? this.now() + 10_000 : 0;
|
|
47
|
+
}
|
|
48
|
+
this.previousKind = 'stopped';
|
|
49
|
+
return this.now() < this.stoppedUntil ? meetingPresentation(meeting) : null;
|
|
50
|
+
}
|
|
51
|
+
this.previousKind = kind;
|
|
52
|
+
this.stoppedUntil = 0;
|
|
53
|
+
return meetingPresentation(meeting);
|
|
54
|
+
}
|
|
55
|
+
}
|