@clowder-ai/companion 0.1.0-alpha.20 → 0.1.0-alpha.21
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 +3 -3
- package/plugin.yaml +2 -2
- package/renderer/chat-unread.mjs +44 -0
- package/renderer/client.mjs +2 -0
- package/renderer/compact-size.mjs +8 -0
- package/renderer/decision-panel.mjs +122 -0
- package/renderer/index.html +39 -19
- package/renderer/style.css +26 -6
- package/renderer/surface.mjs +56 -100
- package/renderer/work-overview.mjs +95 -0
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.21",
|
|
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-rfOwPKO1TWQFzz7oUnWghd+p9HnVRS6e0SmUX679ezY="
|
|
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.21",
|
|
4
4
|
"description": "The desktop voice body for your Clowder companion",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -27,8 +27,8 @@
|
|
|
27
27
|
"prepack": "pnpm build"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
|
-
"@clowder-ai/plugin-contract": "0.1.0-beta.
|
|
31
|
-
"@clowder-ai/plugin-sdk": "0.1.0-beta.
|
|
30
|
+
"@clowder-ai/plugin-contract": "0.1.0-beta.25",
|
|
31
|
+
"@clowder-ai/plugin-sdk": "0.1.0-beta.21",
|
|
32
32
|
"playwright": "1.61.1"
|
|
33
33
|
},
|
|
34
34
|
"repository": {
|
package/plugin.yaml
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"pluginId": "official.companion",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.21",
|
|
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-rfOwPKO1TWQFzz7oUnWghd+p9HnVRS6e0SmUX679ezY="
|
|
44
44
|
},
|
|
45
45
|
"presentation": {
|
|
46
46
|
"width": 300,
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Counts new assistant call-message identities only after the owner folds an opened chat. */
|
|
2
|
+
export class FoldedChatUnread {
|
|
3
|
+
constructor(changed = () => {}) {
|
|
4
|
+
this.changed = changed;
|
|
5
|
+
this.seen = new Set();
|
|
6
|
+
this.count = 0;
|
|
7
|
+
this.armed = false;
|
|
8
|
+
this.folded = false;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
publish(count) {
|
|
12
|
+
if (this.count === count) return;
|
|
13
|
+
this.count = count;
|
|
14
|
+
this.changed(count);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
open() {
|
|
18
|
+
this.armed = true;
|
|
19
|
+
this.folded = false;
|
|
20
|
+
this.publish(0);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
fold() {
|
|
24
|
+
if (this.armed) this.folded = true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
receive(message) {
|
|
28
|
+
const { messageId, role } = message ?? {};
|
|
29
|
+
if (!messageId || this.seen.has(messageId)) return;
|
|
30
|
+
this.seen.add(messageId);
|
|
31
|
+
if (role === 'assistant' && this.armed && this.folded) this.publish(this.count + 1);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
receiveMany(messages) {
|
|
35
|
+
for (const message of messages) this.receive(message);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
reset() {
|
|
39
|
+
this.seen.clear();
|
|
40
|
+
this.armed = false;
|
|
41
|
+
this.folded = false;
|
|
42
|
+
this.publish(0);
|
|
43
|
+
}
|
|
44
|
+
}
|
package/renderer/client.mjs
CHANGED
|
@@ -36,6 +36,8 @@ export function createCompanionClient(bridge) {
|
|
|
36
36
|
screenFrame: (selectionId, frame) => invoke({ kind: 'screen.frame', selectionId, frame }, 'ok'),
|
|
37
37
|
screenClose: () => invoke({ kind: 'screen.close' }, 'ok'),
|
|
38
38
|
openConversation: () => invoke({ kind: 'conversation.open' }, 'navigation'),
|
|
39
|
+
readWork: () => invoke({ kind: 'work.read' }, 'work'),
|
|
40
|
+
openWork: (entryRef) => invoke({ kind: 'work.open', entryRef }, 'navigation'),
|
|
39
41
|
readDecisions: (offset = 0, limit = 20) => invoke({ kind: 'decisions.read', offset, limit }, 'decisions'),
|
|
40
42
|
openDecision: (variantRef, target) => invoke({ kind: 'decision.open', variantRef, target }, 'navigation'),
|
|
41
43
|
inspectF221: (proposalId) => invoke({ kind: 'f221.inspect', proposalId }, 'decision-trial'),
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
export const COMPACT_BALL_SIZE = 43;
|
|
2
|
+
export const DEFAULT_BALL_SIZE = 72;
|
|
3
|
+
|
|
4
|
+
export function compactSizeAction(ballSize) {
|
|
5
|
+
return Number(ballSize) <= COMPACT_BALL_SIZE
|
|
6
|
+
? { label: '放大', value: DEFAULT_BALL_SIZE }
|
|
7
|
+
: { label: '缩小', value: COMPACT_BALL_SIZE };
|
|
8
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { decisionBadge, decisionPresentation, decisionRows } from './decision-view.mjs';
|
|
2
|
+
|
|
3
|
+
const readBasis = page => page?.version === 1
|
|
4
|
+
? [page.status, page.totalCount ?? 'unknown', ...['approvals', 'needsMe']
|
|
5
|
+
.flatMap(key => [page.sources?.[key]?.status, page.sources?.[key]?.coverage])].join('|')
|
|
6
|
+
: page ? 'legacy' : null;
|
|
7
|
+
|
|
8
|
+
export const decisionNavigationText = delivery => delivery === 'applied' ? '已打开对应事项'
|
|
9
|
+
: delivery === 'queued' || delivery === 'requested' ? '已请求打开对应事项'
|
|
10
|
+
: delivery === 'blocked' ? '暂时无法打开对应事项 · 请稍后重试'
|
|
11
|
+
: '打开对应事项尚未确认 · 请从 Cat Café 查看';
|
|
12
|
+
|
|
13
|
+
export class DecisionPanel {
|
|
14
|
+
constructor({ document, client, controls, motion, proactivePolicy = () => undefined }) {
|
|
15
|
+
this.document = document;
|
|
16
|
+
this.client = client;
|
|
17
|
+
this.controls = controls;
|
|
18
|
+
this.motion = motion;
|
|
19
|
+
this.proactivePolicy = proactivePolicy;
|
|
20
|
+
this.$ = id => document.getElementById(id);
|
|
21
|
+
this.loading = false;
|
|
22
|
+
this.offset = 0;
|
|
23
|
+
this.latestPage = undefined;
|
|
24
|
+
this.$('pending-badge').onclick = () => void controls.show('decisions');
|
|
25
|
+
this.$('decision-reload').onclick = () => void this.read();
|
|
26
|
+
this.$('decision-more').onclick = () => void this.read(true);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
showBadge(page) {
|
|
30
|
+
this.latestPage = page;
|
|
31
|
+
const badge = decisionBadge(page);
|
|
32
|
+
this.$('pending-badge').hidden = !badge.visible || this.proactivePolicy() !== 'quiet-badge';
|
|
33
|
+
this.$('pending-count').textContent = badge.label;
|
|
34
|
+
this.$('pending-badge').title = badge.title;
|
|
35
|
+
this.$('pending-badge').setAttribute('aria-label', badge.title);
|
|
36
|
+
this.$('menu-pending').textContent = badge.label;
|
|
37
|
+
this.$('menu-pending').dataset.state = badge.state;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
refreshBadge() {
|
|
41
|
+
if (this.latestPage) this.showBadge(this.latestPage);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
appendRow(list, row, knownVariantRefs) {
|
|
45
|
+
if (row.variantRef && knownVariantRefs.has(row.variantRef)) return;
|
|
46
|
+
const item = this.document.createElement('li');
|
|
47
|
+
if (row.variantRef) { item.dataset.variantRef = row.variantRef; knownVariantRefs.add(row.variantRef); }
|
|
48
|
+
const title = this.document.createElement('strong');
|
|
49
|
+
const meta = this.document.createElement('span');
|
|
50
|
+
title.textContent = row.title; meta.textContent = row.meta;
|
|
51
|
+
item.append(title, meta); list.append(item);
|
|
52
|
+
if (row.navigation) this.appendNavigation(item, row.navigation);
|
|
53
|
+
else if (row.previewable) this.appendTrial(item, row.proposalId);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
appendNavigation(item, navigation) {
|
|
57
|
+
const button = this.document.createElement('button');
|
|
58
|
+
button.type = 'button'; button.textContent = navigation.label;
|
|
59
|
+
button.onclick = async () => {
|
|
60
|
+
try {
|
|
61
|
+
const result = await this.client.openDecision(navigation.variantRef, navigation.target);
|
|
62
|
+
this.$('decision-status').textContent = decisionNavigationText(result.delivery);
|
|
63
|
+
} catch { this.$('decision-status').textContent = '暂时无法打开对应事项 · 请稍后重试'; }
|
|
64
|
+
};
|
|
65
|
+
item.append(button);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
appendTrial(item, proposalId) {
|
|
69
|
+
const button = this.document.createElement('button');
|
|
70
|
+
button.type = 'button'; button.textContent = '查看确认演练';
|
|
71
|
+
button.onclick = async () => {
|
|
72
|
+
try {
|
|
73
|
+
const result = await this.client.inspectF221(proposalId);
|
|
74
|
+
this.$('decision-status').textContent = result.status === 'trial_confirmed'
|
|
75
|
+
? '确认演练已完成;没有写回提案'
|
|
76
|
+
: result.status === 'stale' ? '提案已变化,请重新查看原处卡片'
|
|
77
|
+
: result.status === 'dismissed' ? '已取消演练;提案没有变化' : '确认演练暂不可用;请在原处处理';
|
|
78
|
+
} catch { this.$('decision-status').textContent = '确认演练暂不可用;请在原处处理'; }
|
|
79
|
+
};
|
|
80
|
+
item.append(button);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
async read(more = false) {
|
|
84
|
+
if (this.loading) return;
|
|
85
|
+
this.loading = true;
|
|
86
|
+
if (!more && this.controls.panel === 'decisions') this.$('decision-status').textContent = '正在读取待办…';
|
|
87
|
+
try {
|
|
88
|
+
const previous = readBasis(this.latestPage);
|
|
89
|
+
const page = await this.client.readDecisions(more ? this.offset : 0, 10);
|
|
90
|
+
const presentation = decisionPresentation(page);
|
|
91
|
+
const changedRead = more && presentation.renderRows && previous !== null && readBasis(page) !== previous;
|
|
92
|
+
this.showBadge(page);
|
|
93
|
+
this.motion.setPendingDecision(presentation.pending);
|
|
94
|
+
if (this.controls.panel !== 'decisions') return;
|
|
95
|
+
const list = this.$('decision-list');
|
|
96
|
+
if (!more || changedRead) { list.replaceChildren(); list.scrollTop = 0; }
|
|
97
|
+
const retainedRows = more && !presentation.renderRows && list.children.length > 0;
|
|
98
|
+
const knownVariantRefs = new Set(Array.from(list.children).map(item => item.dataset.variantRef).filter(Boolean));
|
|
99
|
+
for (const row of presentation.renderRows && !changedRead ? decisionRows(page) : []) {
|
|
100
|
+
this.appendRow(list, row, knownVariantRefs);
|
|
101
|
+
}
|
|
102
|
+
this.offset = changedRead ? 0 : page.page.offset + page.page.limit;
|
|
103
|
+
this.$('decision-reload').textContent = changedRead ? '刷新' : presentation.reloadLabel;
|
|
104
|
+
this.$('decision-status').textContent = changedRead && page.totalCount !== 0
|
|
105
|
+
? '待办已变化 · 请刷新查看最新列表'
|
|
106
|
+
: retainedRows
|
|
107
|
+
? `${presentation.badge.state === 'authentication' ? '待办需要登录' : '待办暂不可读'} · 以下为上次读到的内容`
|
|
108
|
+
: presentation.message;
|
|
109
|
+
this.$('decision-more').hidden = changedRead || !presentation.hasMore;
|
|
110
|
+
} catch {
|
|
111
|
+
this.showBadge(undefined);
|
|
112
|
+
this.motion.setPendingDecision(null);
|
|
113
|
+
if (this.controls.panel === 'decisions') {
|
|
114
|
+
const retained = this.$('decision-list').children.length > 0;
|
|
115
|
+
this.$('decision-status').textContent = retained
|
|
116
|
+
? '待办暂不可读 · 以下为上次读到的内容' : '待办暂不可读 · 请稍后刷新';
|
|
117
|
+
this.$('decision-reload').textContent = '重试';
|
|
118
|
+
this.$('decision-more').hidden = true;
|
|
119
|
+
}
|
|
120
|
+
} finally { this.loading = false; }
|
|
121
|
+
}
|
|
122
|
+
}
|
package/renderer/index.html
CHANGED
|
@@ -58,7 +58,8 @@
|
|
|
58
58
|
<div id="active-actions" class="action-row call-actions">
|
|
59
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>
|
|
60
60
|
<button data-action="share" id="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
61
|
-
<button data-action="transcript"><svg class="icon" aria-hidden="true"><use href="#i-subtitles"/></svg><span>字幕</span></button>
|
|
61
|
+
<button data-action="transcript" aria-label="字幕" title="字幕"><svg class="icon" aria-hidden="true"><use href="#i-subtitles"/></svg><span>字幕</span></button>
|
|
62
|
+
<button id="call-chat" data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span><span id="call-chat-unread" class="action-unread" hidden></span></button>
|
|
62
63
|
<button id="end" class="destructive" data-action="stop"><span>挂断</span></button>
|
|
63
64
|
</div>
|
|
64
65
|
<p id="status" role="status" hidden></p>
|
|
@@ -67,26 +68,45 @@
|
|
|
67
68
|
<div id="menu-call-context" class="call-context" hidden>
|
|
68
69
|
<span class="status-dot" aria-hidden="true"></span><strong id="menu-call-state"></strong><span id="menu-call-mic-state"></span>
|
|
69
70
|
<small id="menu-call-work" hidden></small>
|
|
70
|
-
<
|
|
71
|
+
<div id="menu-call-actions" class="call-context-actions menu-call-actions">
|
|
72
|
+
<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>
|
|
73
|
+
<button id="menu-call-share" data-action="share"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
74
|
+
<button id="menu-call-chat" data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span></button>
|
|
75
|
+
<button data-action="stop" class="destructive compact">挂断</button>
|
|
76
|
+
</div>
|
|
71
77
|
<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
78
|
</div>
|
|
73
|
-
<
|
|
74
|
-
|
|
75
|
-
<
|
|
76
|
-
|
|
77
|
-
|
|
79
|
+
<div id="menu-scroll" class="menu-scroll">
|
|
80
|
+
<strong id="menu-name" class="menu-title">猫猫</strong>
|
|
81
|
+
<div id="name-card-actions" class="name-card-actions">
|
|
82
|
+
<button id="begin" class="primary" data-action="begin"><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">语音通话</span></button>
|
|
83
|
+
<button id="listen" data-action="listen" hidden><svg class="icon" aria-hidden="true"><use href="#i-listen"/></svg><span class="label">只听</span></button>
|
|
84
|
+
<button data-action="write"><svg class="icon" aria-hidden="true"><use href="#i-keyboard"/></svg><span>聊天</span></button>
|
|
85
|
+
</div>
|
|
86
|
+
<p id="menu-status" role="status" hidden></p>
|
|
87
|
+
<button data-action="decisions"><span aria-hidden="true">◇</span><span>待办</span><span id="menu-pending" class="menu-status">—</span></button>
|
|
88
|
+
<div id="work-overview" class="work-overview" hidden>
|
|
89
|
+
<section id="work-active-section" class="work-overview-section" hidden>
|
|
90
|
+
<header><span>进行中的工作</span><small id="work-scope"></small></header>
|
|
91
|
+
<div id="work-active-list" class="work-overview-list"></div>
|
|
92
|
+
</section>
|
|
93
|
+
<section id="work-delivery-section" class="work-overview-section" hidden>
|
|
94
|
+
<header><span>最近交付</span></header>
|
|
95
|
+
<div id="work-delivery-list" class="work-overview-list"></div>
|
|
96
|
+
</section>
|
|
97
|
+
<p id="work-notice" role="status"></p>
|
|
98
|
+
</div>
|
|
99
|
+
<button data-action="history"><svg class="icon" aria-hidden="true"><use href="#i-history"/></svg><span>打开聊天</span></button>
|
|
100
|
+
<button id="menu-play" data-action="play" hidden><span aria-hidden="true">✦</span><span>逗宪宪</span></button>
|
|
101
|
+
<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>
|
|
102
|
+
<button id="documents" data-action="documents" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-book"/></svg><span>资料查询</span><span class="menu-status">开启</span></button>
|
|
78
103
|
</div>
|
|
79
|
-
<
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
<button id="menu-settings" data-action="settings"><span aria-hidden="true">⚙</span><span>设置</span><span class="menu-status">›</span></button>
|
|
86
|
-
<button id="menu-share" data-action="share" title="语音通话连接后可共享画面"><svg class="icon" aria-hidden="true"><use href="#i-screen"/></svg><span class="label">共享画面</span></button>
|
|
87
|
-
<button id="documents" data-action="documents" aria-pressed="true"><svg class="icon" aria-hidden="true"><use href="#i-book"/></svg><span>资料查询</span><span class="menu-status">开启</span></button>
|
|
88
|
-
<div class="menu-divider" role="separator"></div>
|
|
89
|
-
<button data-action="hide" class="quiet-action" title="可从 Clowder 的聊聊入口叫回来"><svg class="icon" aria-hidden="true"><use href="#i-moon"/></svg><span>收起猫猫</span></button>
|
|
104
|
+
<footer class="menu-footer">
|
|
105
|
+
<button id="speaker" data-action="speaker" hidden><svg class="icon" aria-hidden="true"><use href="#i-mic"/></svg><span class="label">播音</span></button>
|
|
106
|
+
<button id="menu-settings" data-action="settings"><span aria-hidden="true">⚙</span><span>设置</span></button>
|
|
107
|
+
<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>
|
|
108
|
+
<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>
|
|
109
|
+
</footer>
|
|
90
110
|
</section>
|
|
91
111
|
<section id="chat" class="floating" aria-label="和猫猫写几句" hidden>
|
|
92
112
|
<div id="chat-call-context" class="call-context" hidden>
|
|
@@ -149,7 +169,7 @@
|
|
|
149
169
|
<button id="settings-skin-ragdoll-v1" type="button" data-setting-control>布偶猫</button>
|
|
150
170
|
<button id="settings-skin-yarn-ball" type="button" data-setting-control>猫猫球</button>
|
|
151
171
|
</div>
|
|
152
|
-
<label id="settings-size-field" class="settings-field settings-size"><span>大小</span><div><small>小</small><input id="settings-size" type="range" min="
|
|
172
|
+
<label id="settings-size-field" class="settings-field settings-size"><span>大小</span><div><small>小</small><input id="settings-size" type="range" min="43" max="192" step="1" data-setting-control><small>大</small></div></label>
|
|
153
173
|
</div>
|
|
154
174
|
<div id="settings-page-confirm" class="settings-page settings-confirm" hidden>
|
|
155
175
|
<strong id="settings-confirm-title"></strong><p id="settings-confirm-copy"></p>
|
package/renderer/style.css
CHANGED
|
@@ -60,15 +60,17 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
60
60
|
from { left: var(--living-left-start); top: var(--living-top-start); }
|
|
61
61
|
to { left: var(--living-left-end); top: var(--living-top-end); }
|
|
62
62
|
}
|
|
63
|
-
#indicators { position: absolute;
|
|
64
|
-
#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; }
|
|
63
|
+
#indicators { position: absolute; inset: 0; pointer-events: none; }
|
|
64
|
+
#indicators button { position: absolute; width: 32px; height: 32px; pointer-events: auto; border: 1px solid #fff9; border-radius: 50%; display: grid; place-items: center; background: #236b59; color: white; box-shadow: 0 2px 5px #0003; }
|
|
65
65
|
#indicators button:hover { background: #ab4842; }
|
|
66
66
|
#indicators button[data-state="connecting"] { background: #a77b36; animation: pulse 1.8s infinite; }
|
|
67
67
|
#indicators button[data-state="muted"] { background: #5f6670; }
|
|
68
68
|
#indicators button[data-state="receive-only"] { background: #476dac; }
|
|
69
69
|
#indicators svg { width: 16px; height: 16px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; }
|
|
70
|
-
#share-badge { background: #476dac !important; }
|
|
71
|
-
#pending-badge { width: auto !important; min-width:
|
|
70
|
+
#share-badge { right: 0; bottom: 0; background: #476dac !important; }
|
|
71
|
+
#pending-badge { top: 0; right: 0; width: auto !important; min-width: 24px; height: 24px !important; padding: 0 7px; border: 2px solid white !important; border-radius: 999px !important; background: #f2dcc8 !important; color: #7a3f2e !important; font-size: 11px; font-weight: 700; white-space: nowrap; }
|
|
72
|
+
#pet[data-dock-edge="right"] + #indicators #pending-badge,
|
|
73
|
+
#pet[data-dock-edge="right"] + #indicators #share-badge { right: auto; left: 0; }
|
|
72
74
|
@keyframes pulse { 50% { opacity: .65; } }
|
|
73
75
|
.floating { position: absolute; z-index: 3; border-radius: 12px; border: 1px solid var(--desktop-line-strong); background: var(--desktop-paper); box-shadow: 0 14px 28px #18171526, 0 2px 5px #1817150f; }
|
|
74
76
|
.floating { max-height: 500px; overflow: auto; }
|
|
@@ -83,7 +85,7 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
83
85
|
.floating .destructive { background: var(--desktop-danger); color: white; font-weight: 500; }
|
|
84
86
|
.floating .destructive:hover:not(:disabled) { background: #942c24; }
|
|
85
87
|
.floating .destructive.compact { min-width: 72px; padding: 6px 12px; }
|
|
86
|
-
#actions { padding: 12px; width:
|
|
88
|
+
#actions { padding: 12px; width: 412px; overflow: hidden; }
|
|
87
89
|
.action-row { display: flex; align-items: center; gap: 4px; }
|
|
88
90
|
.action-row button { display: flex; align-items: center; justify-content: center; gap: 7px; min-height: 35px; font-size: 12px; white-space: nowrap; }
|
|
89
91
|
.action-row button .icon { width: 16px; height: 16px; }
|
|
@@ -108,8 +110,11 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
108
110
|
.recent-message-text { unicode-bidi: isolate; }
|
|
109
111
|
.call-recent p[data-role="user"] { color: var(--desktop-muted); }
|
|
110
112
|
.call-actions { justify-content: flex-end; }
|
|
113
|
+
.call-actions #mic .label, .call-actions [data-action="transcript"] > span { display: none; }
|
|
111
114
|
.call-actions #end { min-width: 88px; margin-left: auto; }
|
|
112
|
-
|
|
115
|
+
.action-unread { min-width: 17px; height: 17px; display: inline-grid; place-items: center; border-radius: 999px; padding: 0 5px; background: #f2dcc8; color: #7a3f2e; font-size: 10px; font-weight: 700; line-height: 1; }
|
|
116
|
+
#menu { width: 340px; max-height: 500px; display: flex; flex-direction: column; overflow: hidden; padding: 8px; }
|
|
117
|
+
.menu-scroll { min-height: 0; flex: 1 1 auto; overflow-x: hidden; overflow-y: auto; }
|
|
113
118
|
#menu .menu-title { display: block; padding: 7px 11px 6px; color: var(--cafe-text-muted); font-size: 10px; font-weight: 500; letter-spacing: .04em; }
|
|
114
119
|
#menu button { display: flex; align-items: center; gap: 10px; width: 100%; min-height: 33px; text-align: left; font-size: 12px; line-height: 1.4; }
|
|
115
120
|
#menu button .icon { color: var(--cafe-text-secondary); width: 16px; height: 16px; }
|
|
@@ -129,6 +134,21 @@ button:disabled { opacity: .45; cursor: default; }
|
|
|
129
134
|
.call-context-actions { display: flex; align-items: center; justify-content: flex-end; gap: 4px; }
|
|
130
135
|
.call-context-actions button { display: inline-flex; align-items: center; justify-content: center; gap: 5px; padding: 6px 9px; white-space: nowrap; }
|
|
131
136
|
#menu .call-context { margin: -8px -8px 5px; }
|
|
137
|
+
.menu-footer { flex: none; display: flex; align-items: center; gap: 2px; margin: 4px -8px -8px; border-top: 1px solid var(--desktop-line); padding: 4px; }
|
|
138
|
+
#menu .menu-footer button { width: auto; min-width: 38px; min-height: 33px; padding: 7px 8px; }
|
|
139
|
+
#menu .menu-footer #menu-hide { margin-left: auto; }
|
|
140
|
+
.menu-call-actions { grid-column: 1 / -1; width: 100%; }
|
|
141
|
+
.menu-call-actions button { width: auto !important; min-width: 34px !important; }
|
|
142
|
+
.menu-call-actions button:last-child { margin-left: auto; }
|
|
143
|
+
.work-overview { margin: 3px -8px; border-top: 1px solid var(--desktop-line); border-bottom: 1px solid var(--desktop-line); }
|
|
144
|
+
.work-overview-section + .work-overview-section { border-top: 1px solid var(--desktop-line); }
|
|
145
|
+
.work-overview-section header { display: flex; align-items: baseline; justify-content: space-between; gap: 8px; padding: 8px 11px 3px; color: var(--desktop-muted); font-size: 11px; }
|
|
146
|
+
.work-overview-section header small { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 10px; }
|
|
147
|
+
#menu .work-overview-row { display: grid; grid-template-columns: minmax(0, 1fr); gap: 1px; min-height: 43px; border-radius: 0; padding: 6px 11px; }
|
|
148
|
+
.work-overview-row strong, .work-overview-row span { min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
|
149
|
+
.work-overview-row strong { color: var(--desktop-ink); font-size: 12px; font-weight: 500; }
|
|
150
|
+
.work-overview-row span { color: var(--desktop-muted); font-size: 10px; }
|
|
151
|
+
#work-notice { margin: 0; padding: 3px 11px 7px; color: var(--desktop-muted); font-size: 10px; }
|
|
132
152
|
#chat { width: 400px; max-height: 500px; display: flex; flex-direction: column; overflow: hidden; }
|
|
133
153
|
#chat header { padding: 7px 9px 7px 15px; display: flex; align-items: center; justify-content: space-between; border-bottom: 1px solid var(--cafe-border-subtle); }
|
|
134
154
|
#chat header button { font-size: 12px; color: var(--cafe-text-secondary); }
|
package/renderer/surface.mjs
CHANGED
|
@@ -11,10 +11,13 @@ import { PetMotion } from './pet-motion.mjs';
|
|
|
11
11
|
import { LivingBody } from './living-body.mjs';
|
|
12
12
|
import { detectDockedEdge } from './living-edge.mjs';
|
|
13
13
|
import { bindPetControls } from './pet-controls.mjs';
|
|
14
|
-
import {
|
|
14
|
+
import { DecisionPanel } from './decision-panel.mjs';
|
|
15
15
|
import { nativeWorkLabel, normalizeNativeWork } from './native-work-motion.mjs';
|
|
16
16
|
import { NativeWindowTravel } from './native-window-travel.mjs';
|
|
17
17
|
import { currentCompanionIdentity } from './companion-identity.mjs';
|
|
18
|
+
import { FoldedChatUnread } from './chat-unread.mjs';
|
|
19
|
+
import { COMPACT_BALL_SIZE, compactSizeAction } from './compact-size.mjs';
|
|
20
|
+
import { WorkOverviewView } from './work-overview.mjs';
|
|
18
21
|
const $ = id => document.getElementById(id);
|
|
19
22
|
const label = (id, text) => { $(id).querySelector('.label').textContent = text; };
|
|
20
23
|
const status = text => {
|
|
@@ -32,6 +35,11 @@ const callTranscript = new CallTranscriptView($('call-transcript-log'), {
|
|
|
32
35
|
});
|
|
33
36
|
const bubble = new RecentBubble([$('bubble-first'), $('bubble-second')]);
|
|
34
37
|
const callBubble = new RecentBubble([$('call-bubble-first'), $('call-bubble-second')]);
|
|
38
|
+
const foldedChatUnread = new FoldedChatUnread(count => {
|
|
39
|
+
$('call-chat-unread').hidden = count === 0;
|
|
40
|
+
$('call-chat-unread').textContent = count ? String(count) : '';
|
|
41
|
+
$('call-chat').setAttribute('aria-label', count ? `聊天,${count} 条未读` : '聊天');
|
|
42
|
+
});
|
|
35
43
|
const motion = new PetMotion($('pet'), { livingBody: new LivingBody({ root: $('pet'), sit: $('living-sit'),
|
|
36
44
|
video: $('living-video'), transitionVideo: $('living-video-transition') }) });
|
|
37
45
|
const nativeTravel = new NativeWindowTravel({
|
|
@@ -39,15 +47,12 @@ const nativeTravel = new NativeWindowTravel({
|
|
|
39
47
|
changed: value => motion.syncTravel(value),
|
|
40
48
|
});
|
|
41
49
|
let sharing = false, pendingScreen = false, loading = false, transcriptLoading = false, latestHistory, previousPhase = 'idle';
|
|
42
|
-
let didMove = false,
|
|
50
|
+
let didMove = false, threadTitle, settingsView, settingsValues, decisionPanel, workView;
|
|
51
|
+
let previousControlPanel = 'none';
|
|
43
52
|
// F229 stays quiet: a history ID change has no provenance for proactive text.
|
|
44
53
|
const ambientPanel = active => active ? 'actions' : 'none';
|
|
45
54
|
const setTexts = (ids, text) => { for (const id of ids) $(id).textContent = text; };
|
|
46
55
|
const setHidden = (ids, hidden) => { for (const id of ids) $(id).hidden = hidden; };
|
|
47
|
-
const decisionReadBasis = page => page?.version === 1
|
|
48
|
-
? [page.status, page.totalCount ?? 'unknown', ...['approvals', 'needsMe']
|
|
49
|
-
.flatMap(key => [page.sources?.[key]?.status, page.sources?.[key]?.coverage])].join('|')
|
|
50
|
-
: page ? 'legacy' : null;
|
|
51
56
|
if (!window.clowderCompanion) {
|
|
52
57
|
$('actions').hidden = false; $('status').hidden = false;
|
|
53
58
|
status('请从 Clowder 的聊聊入口打开猫猫球');
|
|
@@ -67,10 +72,14 @@ if (!window.clowderCompanion) {
|
|
|
67
72
|
setTimeout(() => motion.setDockedEdge(detectDockedEdge(window, $('pet').getBoundingClientRect())), 100);
|
|
68
73
|
},
|
|
69
74
|
changed: panel => {
|
|
75
|
+
if (panel === 'chat') foldedChatUnread.open();
|
|
76
|
+
else if (previousControlPanel === 'chat' && conversation?.active) foldedChatUnread.fold();
|
|
77
|
+
previousControlPanel = panel;
|
|
70
78
|
if (panel === 'chat') void readHistory();
|
|
71
79
|
if (panel === 'transcript') void readCallTranscript();
|
|
72
|
-
if (panel === 'decisions') void
|
|
80
|
+
if (panel === 'decisions') void decisionPanel?.read();
|
|
73
81
|
if (panel === 'settings') void settingsView?.open();
|
|
82
|
+
if (panel === 'menu') void workView?.load();
|
|
74
83
|
},
|
|
75
84
|
action(kind) {
|
|
76
85
|
if (kind === 'begin') { transcript.reset(); callTranscript.reset(); bubble.reset(); callBubble.clear(); void conversation.begin(); void controls.show('none'); }
|
|
@@ -81,6 +90,10 @@ if (!window.clowderCompanion) {
|
|
|
81
90
|
if (kind === 'share') toggleScreen();
|
|
82
91
|
if (kind === 'documents') void conversation.documents(conversation.identity?.documentsAllowed === false);
|
|
83
92
|
if (kind === 'hide') void client.hide().catch(error => status(explainError(error)));
|
|
93
|
+
if (kind === 'size-toggle') {
|
|
94
|
+
if (!settingsValues) { status('大小设置仍在读取'); return; }
|
|
95
|
+
void settingsView.controller.requestUpdate('ballSize', compactSizeAction(settingsValues.ballSize).value);
|
|
96
|
+
}
|
|
84
97
|
if (kind === 'transcript-return') void controls.show('actions');
|
|
85
98
|
if (kind === 'transcript-expand') {
|
|
86
99
|
const expanded = $('transcript').dataset.expanded !== 'true';
|
|
@@ -103,7 +116,7 @@ if (!window.clowderCompanion) {
|
|
|
103
116
|
}, value => {
|
|
104
117
|
const wasActive = sharing || pendingScreen;
|
|
105
118
|
sharing = value.sharing; pendingScreen = value.pending === true;
|
|
106
|
-
for (const id of ['share', 'menu-share', 'chat-share', 'settings-share']) label(id, sharing ? '停止共享' : pendingScreen ? '取消选屏' : '共享画面');
|
|
119
|
+
for (const id of ['share', 'menu-share', 'menu-call-share', 'chat-share', 'settings-share']) label(id, sharing ? '停止共享' : pendingScreen ? '取消选屏' : '共享画面');
|
|
107
120
|
for (const prefix of ['call', 'menu', 'chat', 'transcript', 'settings']) {
|
|
108
121
|
$(`${prefix}-share-context`).hidden = !sharing;
|
|
109
122
|
$(`${prefix}-share-target`).textContent = sharing ? `共享中:${value.label}` : '';
|
|
@@ -144,6 +157,7 @@ if (!window.clowderCompanion) {
|
|
|
144
157
|
const justConnected = previousPhase !== 'talking' && value.phase === 'talking';
|
|
145
158
|
if (previousPhase !== 'idle' && value.phase === 'idle') {
|
|
146
159
|
transcript.reset(); callTranscript.reset(); bubble.reset(); callBubble.clear(); void readHistory();
|
|
160
|
+
foldedChatUnread.reset();
|
|
147
161
|
if (controls.panel === 'transcript') void controls.show('menu');
|
|
148
162
|
}
|
|
149
163
|
previousPhase = value.phase;
|
|
@@ -161,10 +175,12 @@ if (!window.clowderCompanion) {
|
|
|
161
175
|
$('listen').className = retryReceiveOnly ? 'primary' : '';
|
|
162
176
|
$('mic').hidden = value.phase !== 'talking' || receiveOnly; $('speaker').hidden = !active;
|
|
163
177
|
$('chat-mic').hidden = value.phase !== 'talking' || receiveOnly;
|
|
178
|
+
$('menu-mic').hidden = value.phase !== 'talking' || receiveOnly;
|
|
164
179
|
$('transcript-mic').hidden = value.phase !== 'talking' || receiveOnly;
|
|
165
180
|
$('settings-mic').hidden = value.phase !== 'talking' || receiveOnly;
|
|
166
181
|
$('active-actions').hidden = !active;
|
|
167
182
|
$('share').disabled = value.phase !== 'talking';
|
|
183
|
+
$('menu-call-share').disabled = value.phase !== 'talking';
|
|
168
184
|
$('compose').querySelector('button').disabled = !value.identity || value.phase === 'connecting';
|
|
169
185
|
status(value.message ?? '');
|
|
170
186
|
$('status').hidden = !value.message || /^(点击|正在听|只听模式|只听已结束|语音通话已结束|已发送)/.test(value.message);
|
|
@@ -180,13 +196,22 @@ if (!window.clowderCompanion) {
|
|
|
180
196
|
setTexts(['call-mic-state', 'chat-call-mic-state', 'transcript-call-mic-state', 'menu-call-mic-state', 'settings-call-mic-state'], microphoneState);
|
|
181
197
|
setHidden(['chat-call-context', 'transcript-call-context', 'menu-call-context', 'settings-call-context'], !active);
|
|
182
198
|
label('mic', value.muted ? '取消静音' : '静音'); $('mic').setAttribute('aria-pressed', String(value.muted));
|
|
199
|
+
$('mic').setAttribute('aria-label', value.muted ? '取消静音' : '静音');
|
|
183
200
|
$('mic-icon').setAttribute('href', value.muted ? '#i-mic-off' : '#i-mic');
|
|
184
201
|
$('chat-mic').textContent = value.muted ? '取消静音' : '静音';
|
|
202
|
+
$('menu-mic').setAttribute('aria-label', value.muted ? '取消静音' : '静音');
|
|
203
|
+
$('menu-mic').title = value.muted ? '取消静音' : '静音';
|
|
204
|
+
$('menu-mic').setAttribute('aria-pressed', String(value.muted));
|
|
205
|
+
$('menu-mic-icon').setAttribute('href', value.muted ? '#i-mic-off' : '#i-mic');
|
|
185
206
|
$('settings-mic').textContent = value.muted ? '取消静音' : '静音';
|
|
186
207
|
$('transcript-mic').setAttribute('aria-label', value.muted ? '取消静音' : '静音');
|
|
187
208
|
$('transcript-mic').setAttribute('aria-pressed', String(value.muted));
|
|
188
209
|
$('transcript-mic-icon').setAttribute('href', value.muted ? '#i-mic-off' : '#i-mic');
|
|
189
|
-
label('speaker',
|
|
210
|
+
label('speaker', '播音');
|
|
211
|
+
$('speaker').setAttribute('aria-pressed', String(!value.silent));
|
|
212
|
+
$('speaker').title = value.silent ? '开启播音' : '关闭播音';
|
|
213
|
+
$('name-card-actions').hidden = active;
|
|
214
|
+
$('menu-hide-label').textContent = active ? '结束通话并隐藏' : '隐藏';
|
|
190
215
|
const identity = value.identity;
|
|
191
216
|
const companionIdentity = currentCompanionIdentity(identity);
|
|
192
217
|
const nativeSnapshot = normalizeNativeWork(identity?.nativeWork);
|
|
@@ -231,14 +256,30 @@ if (!window.clowderCompanion) {
|
|
|
231
256
|
settleExpectedHostStop: callStopped => conversation.settleExpectedHostStop(callStopped),
|
|
232
257
|
layoutChanged: () => { if (controls.panel === 'settings') controls.remeasure?.(); },
|
|
233
258
|
onValues(values) {
|
|
259
|
+
const sizeChanged = settingsValues !== undefined && settingsValues.ballSize !== values.ballSize;
|
|
234
260
|
settingsValues = values;
|
|
235
261
|
document.documentElement?.style?.setProperty?.('--pet-scale', String(values.ballSize / 72));
|
|
236
262
|
$('anchor').dataset.ballSize = String(values.ballSize);
|
|
263
|
+
$('anchor').dataset.compact = String(values.ballSize <= COMPACT_BALL_SIZE);
|
|
264
|
+
label('menu-size-toggle', compactSizeAction(values.ballSize).label);
|
|
237
265
|
motion.setBehaviorEnabled(values.behaviorEnabled);
|
|
238
266
|
$('documents').hidden = true;
|
|
239
|
-
|
|
267
|
+
decisionPanel?.refreshBadge();
|
|
268
|
+
if (sizeChanged) void controls.show(controls.panel);
|
|
240
269
|
},
|
|
241
270
|
});
|
|
271
|
+
workView = new WorkOverviewView({
|
|
272
|
+
document,
|
|
273
|
+
client,
|
|
274
|
+
changed: () => { if (controls.panel === 'menu') controls.remeasure?.(); },
|
|
275
|
+
});
|
|
276
|
+
decisionPanel = new DecisionPanel({
|
|
277
|
+
document,
|
|
278
|
+
client,
|
|
279
|
+
controls,
|
|
280
|
+
motion,
|
|
281
|
+
proactivePolicy: () => settingsValues?.proactivePolicy,
|
|
282
|
+
});
|
|
242
283
|
async function readHistory() {
|
|
243
284
|
if (loading) return;
|
|
244
285
|
loading = true;
|
|
@@ -262,100 +303,14 @@ if (!window.clowderCompanion) {
|
|
|
262
303
|
const reply = await client.readTranscript();
|
|
263
304
|
if (!conversation.active || conversation.callId !== callId) return;
|
|
264
305
|
callTranscript.load(reply);
|
|
306
|
+
foldedChatUnread.receiveMany(reply.rows);
|
|
265
307
|
callBubble.loadTranscript(reply.rows, conversation.identity?.displayName);
|
|
266
308
|
$('call-transcript-status').textContent = reply.hasMore ? '这里只显示这次通话最近的内容' : '';
|
|
267
309
|
} catch {
|
|
268
310
|
if (conversation.active && conversation.callId === callId) $('call-transcript-status').textContent = '字幕暂未更新 · 实时内容仍会继续显示';
|
|
269
311
|
} finally { transcriptLoading = false; }
|
|
270
312
|
}
|
|
271
|
-
function showDecisionBadge(page) {
|
|
272
|
-
latestDecisionPage = page;
|
|
273
|
-
const badge = decisionBadge(page);
|
|
274
|
-
$('pending-badge').hidden = !badge.visible || settingsValues?.proactivePolicy !== 'quiet-badge';
|
|
275
|
-
$('pending-count').textContent = badge.label;
|
|
276
|
-
$('pending-badge').title = badge.title;
|
|
277
|
-
$('pending-badge').setAttribute('aria-label', badge.title);
|
|
278
|
-
$('menu-pending').textContent = badge.label;
|
|
279
|
-
$('menu-pending').dataset.state = badge.state;
|
|
280
|
-
}
|
|
281
|
-
async function readDecisions(more = false) {
|
|
282
|
-
if (decisionLoading) return;
|
|
283
|
-
decisionLoading = true;
|
|
284
|
-
if (!more && controls.panel === 'decisions') $('decision-status').textContent = '正在读取待办…';
|
|
285
|
-
try {
|
|
286
|
-
const previousReadBasis = decisionReadBasis(latestDecisionPage);
|
|
287
|
-
const page = await client.readDecisions(more ? decisionOffset : 0, 10);
|
|
288
|
-
const presentation = decisionPresentation(page);
|
|
289
|
-
const changedRead = more && presentation.renderRows && previousReadBasis !== null
|
|
290
|
-
&& decisionReadBasis(page) !== previousReadBasis;
|
|
291
|
-
showDecisionBadge(page);
|
|
292
|
-
motion.setPendingDecision(presentation.pending);
|
|
293
|
-
if (controls.panel !== 'decisions') return;
|
|
294
|
-
const list = $('decision-list');
|
|
295
|
-
if (!more || changedRead) { list.replaceChildren(); list.scrollTop = 0; }
|
|
296
|
-
const retainedRows = more && !presentation.renderRows && list.children.length > 0;
|
|
297
|
-
const knownVariantRefs = new Set(Array.from(list.children)
|
|
298
|
-
.map(item => item.dataset.variantRef).filter(Boolean));
|
|
299
|
-
for (const row of presentation.renderRows && !changedRead ? decisionRows(page) : []) {
|
|
300
|
-
if (row.variantRef && knownVariantRefs.has(row.variantRef)) continue;
|
|
301
|
-
const item = document.createElement('li');
|
|
302
|
-
if (row.variantRef) { item.dataset.variantRef = row.variantRef; knownVariantRefs.add(row.variantRef); }
|
|
303
|
-
const title = document.createElement('strong');
|
|
304
|
-
const meta = document.createElement('span');
|
|
305
|
-
title.textContent = row.title; meta.textContent = row.meta;
|
|
306
|
-
item.append(title, meta); list.append(item);
|
|
307
|
-
if (row.navigation) {
|
|
308
|
-
const button = document.createElement('button');
|
|
309
|
-
button.type = 'button'; button.textContent = row.navigation.label;
|
|
310
|
-
button.onclick = async () => {
|
|
311
|
-
try {
|
|
312
|
-
const result = await client.openDecision(row.navigation.variantRef, row.navigation.target);
|
|
313
|
-
$('decision-status').textContent = result.delivery === 'requested'
|
|
314
|
-
? '已请求打开对应事项' : '打开对应事项尚未确认 · 请从 Cat Café 查看';
|
|
315
|
-
} catch { $('decision-status').textContent = '暂时无法打开对应事项 · 请稍后重试'; }
|
|
316
|
-
};
|
|
317
|
-
item.append(button);
|
|
318
|
-
} else if (row.previewable) {
|
|
319
|
-
const button = document.createElement('button');
|
|
320
|
-
button.type = 'button'; button.textContent = '查看确认演练';
|
|
321
|
-
button.onclick = async () => {
|
|
322
|
-
try {
|
|
323
|
-
const result = await client.inspectF221(row.proposalId);
|
|
324
|
-
$('decision-status').textContent = result.status === 'trial_confirmed'
|
|
325
|
-
? '确认演练已完成;没有写回提案'
|
|
326
|
-
: result.status === 'stale' ? '提案已变化,请重新查看原处卡片'
|
|
327
|
-
: result.status === 'dismissed' ? '已取消演练;提案没有变化'
|
|
328
|
-
: '确认演练暂不可用;请在原处处理';
|
|
329
|
-
} catch { $('decision-status').textContent = '确认演练暂不可用;请在原处处理'; }
|
|
330
|
-
};
|
|
331
|
-
item.append(button);
|
|
332
|
-
}
|
|
333
|
-
}
|
|
334
|
-
decisionOffset = changedRead ? 0 : page.page.offset + page.page.limit;
|
|
335
|
-
$('decision-reload').textContent = changedRead ? '刷新' : presentation.reloadLabel;
|
|
336
|
-
$('decision-status').textContent = changedRead && page.totalCount !== 0
|
|
337
|
-
? '待办已变化 · 请刷新查看最新列表'
|
|
338
|
-
: retainedRows
|
|
339
|
-
? `${presentation.badge.state === 'authentication' ? '待办需要登录' : '待办暂不可读'} · 以下为上次读到的内容`
|
|
340
|
-
: presentation.message;
|
|
341
|
-
$('decision-more').hidden = changedRead || !presentation.hasMore;
|
|
342
|
-
} catch {
|
|
343
|
-
showDecisionBadge(undefined);
|
|
344
|
-
motion.setPendingDecision(null);
|
|
345
|
-
if (controls.panel === 'decisions') {
|
|
346
|
-
const retained = $('decision-list').children.length > 0;
|
|
347
|
-
$('decision-status').textContent = retained
|
|
348
|
-
? '待办暂不可读 · 以下为上次读到的内容'
|
|
349
|
-
: '待办暂不可读 · 请稍后刷新';
|
|
350
|
-
$('decision-reload').textContent = '重试';
|
|
351
|
-
$('decision-more').hidden = true;
|
|
352
|
-
}
|
|
353
|
-
} finally { decisionLoading = false; }
|
|
354
|
-
}
|
|
355
313
|
$('share-badge').onclick = toggleScreen;
|
|
356
|
-
$('pending-badge').onclick = () => void controls.show('decisions');
|
|
357
|
-
$('decision-reload').onclick = () => void readDecisions();
|
|
358
|
-
$('decision-more').onclick = () => void readDecisions(true);
|
|
359
314
|
$('history').onclick = () => void client.openConversation().then(result => {
|
|
360
315
|
if (result.delivery === 'unconfirmed') status('打开聊天尚未确认 · 请从 Clowder 查看');
|
|
361
316
|
}).catch(error => status(explainError(error)));
|
|
@@ -377,10 +332,11 @@ if (!window.clowderCompanion) {
|
|
|
377
332
|
const statusMonitor = setInterval(() => void conversation.refresh(), 1000);
|
|
378
333
|
const historyMonitor = setInterval(() => void readHistory(), 3000);
|
|
379
334
|
const transcriptMonitor = setInterval(() => void readCallTranscript(), 1000);
|
|
380
|
-
const decisionMonitor = setInterval(() => { if (controls.panel !== 'decisions') void
|
|
381
|
-
|
|
335
|
+
const decisionMonitor = setInterval(() => { if (controls.panel !== 'decisions') void decisionPanel.read(); }, 15000);
|
|
336
|
+
const workMonitor = setInterval(() => { if (controls.panel === 'menu') void workView.load(); }, 15000);
|
|
337
|
+
void conversation.refresh(); void readHistory(); void decisionPanel.read(); void settingsView.controller.load(); void controls.show('none');
|
|
382
338
|
window.addEventListener('beforeunload', () => {
|
|
383
|
-
clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(transcriptMonitor); clearInterval(decisionMonitor);
|
|
339
|
+
clearInterval(statusMonitor); clearInterval(historyMonitor); clearInterval(transcriptMonitor); clearInterval(decisionMonitor); clearInterval(workMonitor);
|
|
384
340
|
unsubscribe(); nativeTravel.close(); motion.close(); void conversation.end();
|
|
385
341
|
});
|
|
386
342
|
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
const activityLabel = activity => activity === 'waiting' ? '等待中' : '正在工作';
|
|
2
|
+
|
|
3
|
+
function deliveryMeta(artifact) {
|
|
4
|
+
if (!artifact) return '已交付';
|
|
5
|
+
const version = artifact.version ? `作品 ${artifact.version}` : '作品 · 版本未确认';
|
|
6
|
+
if (artifact.versionState === 'current') return `${version} · 当前版本`;
|
|
7
|
+
if (artifact.versionState === 'superseded') return `${version} · 已有更新版本`;
|
|
8
|
+
return `${version} · 版本关系未确认`;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function workOverviewPresentation(reply) {
|
|
12
|
+
const availableRows = reply?.status !== 'unavailable';
|
|
13
|
+
const active = availableRows ? (reply.active ?? []).map(entry => ({
|
|
14
|
+
entryRef: entry.entryRef,
|
|
15
|
+
title: entry.title,
|
|
16
|
+
meta: entry.actor ? `${entry.actor.displayName} · ${activityLabel(entry.activity)}` : activityLabel(entry.activity),
|
|
17
|
+
})) : [];
|
|
18
|
+
const deliveries = availableRows ? (reply.recentDeliveries ?? []).map(entry => ({
|
|
19
|
+
entryRef: entry.entryRef,
|
|
20
|
+
title: entry.title,
|
|
21
|
+
meta: deliveryMeta(entry.artifact),
|
|
22
|
+
})) : [];
|
|
23
|
+
return {
|
|
24
|
+
visible: active.length > 0 || deliveries.length > 0 || ['partial', 'unavailable'].includes(reply?.status),
|
|
25
|
+
scopeLabel: availableRows && reply?.scope?.label ? `仅当前项目 · ${reply.scope.label}` : '',
|
|
26
|
+
notice: reply?.status === 'partial' ? '仅部分读取' : reply?.status === 'unavailable' ? '暂不可用' : '',
|
|
27
|
+
active,
|
|
28
|
+
deliveries,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const navigationText = delivery => delivery === 'applied' ? '已打开原处'
|
|
33
|
+
: delivery === 'queued' || delivery === 'requested' ? '已请求打开原处'
|
|
34
|
+
: delivery === 'blocked' ? '原处暂时无法打开' : '打开原处的结果未确认';
|
|
35
|
+
|
|
36
|
+
/** Renders only bounded Host-issued rows; every navigation target remains opaque. */
|
|
37
|
+
export class WorkOverviewView {
|
|
38
|
+
constructor({ document, client, changed = () => {} }) {
|
|
39
|
+
this.document = document;
|
|
40
|
+
this.client = client;
|
|
41
|
+
this.changed = changed;
|
|
42
|
+
this.$ = id => document.getElementById(id);
|
|
43
|
+
this.loading = false;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
renderRows(listId, rows) {
|
|
47
|
+
const list = this.$(listId);
|
|
48
|
+
list.replaceChildren();
|
|
49
|
+
for (const row of rows) {
|
|
50
|
+
const button = this.document.createElement('button');
|
|
51
|
+
button.type = 'button';
|
|
52
|
+
button.className = 'work-overview-row';
|
|
53
|
+
const title = this.document.createElement('strong');
|
|
54
|
+
const meta = this.document.createElement('span');
|
|
55
|
+
title.textContent = row.title;
|
|
56
|
+
meta.textContent = row.meta;
|
|
57
|
+
button.append(title, meta);
|
|
58
|
+
button.onclick = () => void this.open(row.entryRef);
|
|
59
|
+
list.append(button);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
render(reply) {
|
|
64
|
+
const view = workOverviewPresentation(reply);
|
|
65
|
+
this.$('work-overview').hidden = !view.visible;
|
|
66
|
+
this.$('work-scope').textContent = view.scopeLabel;
|
|
67
|
+
this.$('work-notice').textContent = view.notice;
|
|
68
|
+
this.$('work-active-section').hidden = view.active.length === 0;
|
|
69
|
+
this.$('work-delivery-section').hidden = view.deliveries.length === 0;
|
|
70
|
+
this.renderRows('work-active-list', view.active);
|
|
71
|
+
this.renderRows('work-delivery-list', view.deliveries);
|
|
72
|
+
this.changed();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
async open(entryRef) {
|
|
76
|
+
try {
|
|
77
|
+
const reply = await this.client.openWork(entryRef);
|
|
78
|
+
this.$('work-notice').textContent = navigationText(reply.delivery);
|
|
79
|
+
} catch {
|
|
80
|
+
this.$('work-notice').textContent = '原处暂时无法打开';
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
async load() {
|
|
85
|
+
if (this.loading) return;
|
|
86
|
+
this.loading = true;
|
|
87
|
+
try {
|
|
88
|
+
this.render(await this.client.readWork());
|
|
89
|
+
} catch {
|
|
90
|
+
this.render({ status: 'unavailable', active: [], recentDeliveries: [] });
|
|
91
|
+
} finally {
|
|
92
|
+
this.loading = false;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|