@wendongfly/myhi 1.3.188 → 1.3.190
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/bin/daemon.js +196 -170
- package/bin/install-check.js +25 -0
- package/dist/admin.html +30 -7
- package/dist/chat.html +82 -33
- package/dist/index.html +37 -6
- package/dist/index.js +1 -1
- package/dist/index.min.js +10 -15
- package/dist/lib/machine.js +8 -1
- package/dist/terminal.html +1 -1
- package/package.json +1 -1
package/dist/chat.html
CHANGED
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
<link rel="manifest" href="/manifest.json">
|
|
10
10
|
<link rel="apple-touch-icon" href="/icon.png">
|
|
11
11
|
<title>myhi 会话</title>
|
|
12
|
-
<script src="/lib/machine.js"></script>
|
|
12
|
+
<script src="/lib/machine.js" data-session></script>
|
|
13
13
|
<script src="/sio-client/socket.io.min.js"></script>
|
|
14
14
|
<script>
|
|
15
15
|
(function(){try{
|
|
@@ -202,6 +202,9 @@
|
|
|
202
202
|
.msg-raw:hover .raw-copy, .raw-copy.done { opacity: 1; }
|
|
203
203
|
.raw-copy:hover, .cp-btn:hover { color: var(--text); border-color: var(--blue); }
|
|
204
204
|
@media (hover: none) { .raw-copy { opacity: 0.85; } }
|
|
205
|
+
/* AI 工具的原始输出默认不显示(设置→聊天→显示 打开;终端会话的命令输出不受影响) */
|
|
206
|
+
body.hide-toolout .msg-raw.tool-out { display: none; }
|
|
207
|
+
.setting-toggle.off { opacity: 0.45; pointer-events: none; }
|
|
205
208
|
/* 消息里其他带框内容(代码块/表格/diff/工具详情…)同款复制按钮,见 attachCopyBtns */
|
|
206
209
|
.cpw, .cp-host { position: relative; }
|
|
207
210
|
.cp-btn { user-select: none; }
|
|
@@ -647,7 +650,8 @@
|
|
|
647
650
|
<label class="setting-toggle"><span>显示思考过程</span><input type="checkbox" id="set-thinking" checked></label>
|
|
648
651
|
<label class="setting-toggle"><span>工具调用默认折叠</span><input type="checkbox" id="set-collapse" checked></label>
|
|
649
652
|
<label class="setting-toggle"><span>思考过程默认折叠</span><input type="checkbox" id="set-thinkcollapse" checked></label>
|
|
650
|
-
<label class="setting-toggle"><span
|
|
653
|
+
<label class="setting-toggle"><span>显示工具输出内容(AI 执行命令/读文件的原始结果)</span><input type="checkbox" id="set-toolout"></label>
|
|
654
|
+
<label class="setting-toggle" id="row-rawclip"><span> └ 工具输出只显示前 8 行(可展开)</span><input type="checkbox" id="set-rawclip" checked></label>
|
|
651
655
|
</div>
|
|
652
656
|
<div class="setting-group" data-stab="chat" style="display:none">
|
|
653
657
|
<h4>输入</h4>
|
|
@@ -1209,7 +1213,7 @@
|
|
|
1209
1213
|
// 快捷栏:skVisible 逐按钮勾选(唯一显隐依据,null=默认全显);skMode 仅记录最接近的预设(高亮用)
|
|
1210
1214
|
const SK_LABELS = { draft:'📥 草稿', folder:'📂 文件夹', model:'模型', plan:'计划', compact:'压缩', resume:'恢复', clear:'清除', secrets:'🔑 密钥', guard:'🛡 护栏', followup:'🔄 跟进', git:'提交', memory:'记忆', skills:'技能', cost:'费用' };
|
|
1211
1215
|
const SK_NORMAL_PRESET = ['draft', 'folder', 'plan', 'clear', 'guard', 'memory', 'skills'];
|
|
1212
|
-
const DEFAULT_SETTINGS = { parseOutput: true, showThinking: true, collapseTools: true, thinkCollapsed: true, rawClip: true, enterMode: 'auto', skMode: null, skVisible: null };
|
|
1216
|
+
const DEFAULT_SETTINGS = { parseOutput: true, showThinking: true, collapseTools: true, thinkCollapsed: true, rawClip: true, showToolOut: false, enterMode: 'auto', skMode: null, skVisible: null };
|
|
1213
1217
|
let settings = loadSettings();
|
|
1214
1218
|
|
|
1215
1219
|
function loadSettings() { try { return { ...DEFAULT_SETTINGS, ...JSON.parse(localStorage.getItem('myhi_chat_settings')) }; } catch { return { ...DEFAULT_SETTINGS }; } }
|
|
@@ -1315,6 +1319,21 @@
|
|
|
1315
1319
|
let _historyLoading = false; // 防抖锁
|
|
1316
1320
|
let _userScrolledUp = false; // 用户已主动往上翻 → 关掉 trim 防被自动删
|
|
1317
1321
|
const _histFingerprints = new Set(); // 已渲染历史消息指纹(翻页重叠兜底去重,agent:history 重放时清空)
|
|
1322
|
+
// 回放里已画过的块(工具调用 id / 工具结果 id / 文本按 message.id)。AI 正在跑时进入会话(分屏盯任务最常见):
|
|
1323
|
+
// 服务端从 transcript 回放时,最新几条已经写进 transcript,紧接着同样的块又作为实时事件推过来 → 画两次。
|
|
1324
|
+
// 实时事件按这些 id 查重,回放过的就跳过;agent:history 重放时清空。
|
|
1325
|
+
const _replayedBlocks = new Set();
|
|
1326
|
+
function blockKey(block, msgId) {
|
|
1327
|
+
if (block.type === 'tool_use' && block.id) return 'tu:' + block.id;
|
|
1328
|
+
if (block.type === 'tool_result' && block.tool_use_id) return 'tr:' + block.tool_use_id;
|
|
1329
|
+
if (block.type === 'text' && block.text && msgId) return 'tx:' + msgId + ':' + block.text.slice(0, 80);
|
|
1330
|
+
return null;
|
|
1331
|
+
}
|
|
1332
|
+
// 实时块是否在回放里画过(画过返回 true,调用方跳过)
|
|
1333
|
+
function seenInReplay(block, msgId) {
|
|
1334
|
+
const k = blockKey(block, msgId);
|
|
1335
|
+
return !!k && _replayedBlocks.has(k);
|
|
1336
|
+
}
|
|
1318
1337
|
|
|
1319
1338
|
// 命令历史
|
|
1320
1339
|
const cmdHistory = [];
|
|
@@ -2080,6 +2099,9 @@
|
|
|
2080
2099
|
}
|
|
2081
2100
|
|
|
2082
2101
|
function renderHistoryMessage(msg, parent) {
|
|
2102
|
+
if (msg.type === 'assistant' && Array.isArray(msg.message?.content)) {
|
|
2103
|
+
for (const b of msg.message.content) { const k = blockKey(b, msg.message.id); if (k) _replayedBlocks.add(k); }
|
|
2104
|
+
} else if (msg.type === 'tool_result' && msg.id) _replayedBlocks.add('tr:' + msg.id);
|
|
2083
2105
|
if (subaRoute(msg)) return; // 子代理内部历史消息归入对应面板(_history 兜底路径才有)
|
|
2084
2106
|
const fp = histFingerprint(msg);
|
|
2085
2107
|
if (fp) {
|
|
@@ -2094,7 +2116,7 @@
|
|
|
2094
2116
|
if (!text) return;
|
|
2095
2117
|
// 与 live 一致:工具输出 = 工具卡后面的独立 raw 代码块(addRawOutput 同款)
|
|
2096
2118
|
const el = document.createElement('div');
|
|
2097
|
-
el.className = 'msg msg-raw';
|
|
2119
|
+
el.className = 'msg msg-raw tool-out';
|
|
2098
2120
|
const block = document.createElement('div');
|
|
2099
2121
|
block.className = 'code-block';
|
|
2100
2122
|
block.innerHTML = renderAnsiHtml(text);
|
|
@@ -2268,13 +2290,14 @@
|
|
|
2268
2290
|
}
|
|
2269
2291
|
}
|
|
2270
2292
|
|
|
2271
|
-
|
|
2293
|
+
// tool=true:AI 工具的输出(受「显示工具输出内容」开关控制);否则是终端会话的命令输出,始终显示
|
|
2294
|
+
function addRawOutput(raw, tool = false) {
|
|
2272
2295
|
endStream(); endToolGroup();
|
|
2273
2296
|
removeThinking();
|
|
2274
2297
|
if (!stripAnsi(raw).trim()) return;
|
|
2275
|
-
//
|
|
2298
|
+
// 合并到上一个同类 raw 输出块(避免 DOM 碎片化)
|
|
2276
2299
|
const last = chatArea.lastElementChild;
|
|
2277
|
-
if (last?.classList.contains('msg-raw')) {
|
|
2300
|
+
if (last?.classList.contains('msg-raw') && last.classList.contains('tool-out') === tool) {
|
|
2278
2301
|
const block = last.querySelector('.code-block');
|
|
2279
2302
|
if (block) {
|
|
2280
2303
|
block.innerHTML += renderAnsiHtml(raw);
|
|
@@ -2287,7 +2310,7 @@
|
|
|
2287
2310
|
}
|
|
2288
2311
|
}
|
|
2289
2312
|
const msg = document.createElement('div');
|
|
2290
|
-
msg.className = 'msg msg-raw';
|
|
2313
|
+
msg.className = 'msg msg-raw' + (tool ? ' tool-out' : '');
|
|
2291
2314
|
const block = document.createElement('div');
|
|
2292
2315
|
block.className = 'code-block';
|
|
2293
2316
|
block.innerHTML = renderAnsiHtml(raw);
|
|
@@ -2714,6 +2737,7 @@
|
|
|
2714
2737
|
case 'user': {
|
|
2715
2738
|
// 只有字符串 content 才可能是真人/草稿发言;claude 原生 user 事件带数组 content
|
|
2716
2739
|
// (tool_result 块)不是用户说话,别渲染成气泡
|
|
2740
|
+
if (msg.from && msg.from === socket.id) break; // 自己发的,sendCommand 时已经画过
|
|
2717
2741
|
if (msg.content && typeof msg.content === 'string') {
|
|
2718
2742
|
endStream();
|
|
2719
2743
|
const kind = syntheticUserKind(msg.content);
|
|
@@ -2743,6 +2767,7 @@
|
|
|
2743
2767
|
// 避免渲染完第一段 text 后置位标志,导致同事件内 [text, tool_use, text] 的第二段被跳过
|
|
2744
2768
|
const skipText = _streamedTextThisCall;
|
|
2745
2769
|
for (const block of msg.message.content) {
|
|
2770
|
+
if (seenInReplay(block, msg.message.id)) continue; // 进会话时回放里已经画过
|
|
2746
2771
|
if (block.type === 'thinking' && block.thinking) {
|
|
2747
2772
|
showThinking(block.thinking);
|
|
2748
2773
|
} else if (block.type === 'text' && block.text) {
|
|
@@ -2761,7 +2786,7 @@
|
|
|
2761
2786
|
}
|
|
2762
2787
|
} else if (block.type === 'tool_result') {
|
|
2763
2788
|
const content = typeof block.content === 'string' ? block.content : JSON.stringify(block.content);
|
|
2764
|
-
if (!subaTryComplete(block.tool_use_id, content)) addRawOutput(content);
|
|
2789
|
+
if (!subaTryComplete(block.tool_use_id, content)) addRawOutput(content, true);
|
|
2765
2790
|
}
|
|
2766
2791
|
}
|
|
2767
2792
|
}
|
|
@@ -2841,11 +2866,11 @@
|
|
|
2841
2866
|
// 独立工具结果消息。同时作为 call 边界兜底:如果某适配器没发 message_start,
|
|
2842
2867
|
// 这里重置 call 标志能保证后续 assistant 文本不被前一个 call 的标志误吃
|
|
2843
2868
|
_streamedTextThisCall = false;
|
|
2844
|
-
if (msg.content) {
|
|
2869
|
+
if (msg.content && !seenInReplay({ type: 'tool_result', tool_use_id: msg.tool_use_id })) {
|
|
2845
2870
|
const text = typeof msg.content === 'string' ? msg.content
|
|
2846
2871
|
: Array.isArray(msg.content) ? msg.content.map(b => b.text || '').join('\n')
|
|
2847
2872
|
: JSON.stringify(msg.content);
|
|
2848
|
-
if (text.trim() && !subaTryComplete(msg.tool_use_id, text)) addRawOutput(text);
|
|
2873
|
+
if (text.trim() && !subaTryComplete(msg.tool_use_id, text)) addRawOutput(text, true);
|
|
2849
2874
|
}
|
|
2850
2875
|
setWorkState('working');
|
|
2851
2876
|
break;
|
|
@@ -2878,6 +2903,7 @@
|
|
|
2878
2903
|
|
|
2879
2904
|
subaResetAll();
|
|
2880
2905
|
_histFingerprints.clear();
|
|
2906
|
+
_replayedBlocks.clear();
|
|
2881
2907
|
for (const msg of history) {
|
|
2882
2908
|
renderHistoryMessage(msg, chatArea);
|
|
2883
2909
|
trimMessages(); scrollToBottom();
|
|
@@ -4100,6 +4126,8 @@
|
|
|
4100
4126
|
document.getElementById('set-collapse').checked = settings.collapseTools;
|
|
4101
4127
|
document.getElementById('set-thinkcollapse').checked = settings.thinkCollapsed !== false;
|
|
4102
4128
|
document.getElementById('set-rawclip').checked = settings.rawClip !== false;
|
|
4129
|
+
document.getElementById('set-toolout').checked = !!settings.showToolOut;
|
|
4130
|
+
applyToolOut();
|
|
4103
4131
|
syncEnterModeUI();
|
|
4104
4132
|
syncSkModeUI();
|
|
4105
4133
|
}
|
|
@@ -4107,6 +4135,17 @@
|
|
|
4107
4135
|
document.getElementById('set-thinking').addEventListener('change', (e) => { settings.showThinking = e.target.checked; saveSettings(); });
|
|
4108
4136
|
document.getElementById('set-collapse').addEventListener('change', (e) => { settings.collapseTools = e.target.checked; saveSettings(); });
|
|
4109
4137
|
document.getElementById('set-thinkcollapse').addEventListener('change', (e) => { settings.thinkCollapsed = e.target.checked; saveSettings(); });
|
|
4138
|
+
// 工具输出显隐:关着时「只显示前 8 行」没意义,置灰
|
|
4139
|
+
function applyToolOut() {
|
|
4140
|
+
document.body.classList.toggle('hide-toolout', !settings.showToolOut);
|
|
4141
|
+
document.getElementById('row-rawclip')?.classList.toggle('off', !settings.showToolOut);
|
|
4142
|
+
}
|
|
4143
|
+
applyToolOut();
|
|
4144
|
+
document.getElementById('set-toolout').addEventListener('change', (e) => {
|
|
4145
|
+
settings.showToolOut = e.target.checked; saveSettings(); applyToolOut();
|
|
4146
|
+
// 刚显示出来的块要重新量一次高度(隐藏时量不出是否超过 8 行)
|
|
4147
|
+
if (settings.showToolOut) chatArea.querySelectorAll('.msg-raw.tool-out').forEach(m => rawClipSync(m));
|
|
4148
|
+
});
|
|
4110
4149
|
document.getElementById('set-rawclip').addEventListener('change', (e) => {
|
|
4111
4150
|
settings.rawClip = e.target.checked; saveSettings();
|
|
4112
4151
|
// 已渲染的输出块立即跟随新默认(用户手动点过的块也一并重置)
|
|
@@ -6242,38 +6281,48 @@
|
|
|
6242
6281
|
setTimeout(checkUpdate, 10 * 60 * 1000);
|
|
6243
6282
|
})();
|
|
6244
6283
|
|
|
6245
|
-
|
|
6284
|
+
// 等升级落地:轮询 /api/version(重启/停服期间连不上是正常的)。
|
|
6285
|
+
// 成功 = 版本变了(且等于目标);失败 = 服务端报了本次升级的失败原因;10 分钟兜底超时。
|
|
6286
|
+
async function waitUpgrade(from, target, onTick) {
|
|
6287
|
+
const t0 = Date.now();
|
|
6288
|
+
while (Date.now() - t0 < 10 * 60 * 1000) {
|
|
6289
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
6290
|
+
let v = null;
|
|
6291
|
+
try { const r = await fetch('/api/version', { cache: 'no-store' }); if (r.ok) v = await r.json(); } catch {}
|
|
6292
|
+
onTick?.(!!v, Math.round((Date.now() - t0) / 1000));
|
|
6293
|
+
if (!v) continue;
|
|
6294
|
+
if (v.current !== from && (!target || v.current === target)) return { ok: true, version: v.current };
|
|
6295
|
+
const last = v.upgrade?.last;
|
|
6296
|
+
if (!v.upgrade?.running && last && !last.ok && last.at >= t0 - 5000) return { ok: false, error: last.error };
|
|
6297
|
+
}
|
|
6298
|
+
return { ok: false, error: '等了 10 分钟仍未完成,请稍后刷新页面查看版本,或到服务器上查看 myhi log' };
|
|
6299
|
+
}
|
|
6300
|
+
window.doUpdate = async function(force) {
|
|
6246
6301
|
if (!_versionInfo?.updateAvailable) return;
|
|
6247
|
-
if (!confirm(`更新 myhi?\n当前: ${_versionInfo.current}\n最新: ${_versionInfo.latest}\n\n更新后服务将自动重启`)) return;
|
|
6302
|
+
if (!force && !confirm(`更新 myhi?\n当前: ${_versionInfo.current}\n最新: ${_versionInfo.latest}\n\n更新后服务将自动重启`)) return;
|
|
6248
6303
|
const badge = document.getElementById('update-badge');
|
|
6249
|
-
badge.textContent =
|
|
6250
|
-
|
|
6304
|
+
const setBadge = (t, c) => { badge.textContent = t; badge.style.color = c || 'var(--muted)'; };
|
|
6305
|
+
setBadge('⬆ 更新中...');
|
|
6251
6306
|
try {
|
|
6252
|
-
const resp = await fetch('/api/update', { method: 'POST' });
|
|
6253
|
-
const data = await resp.json();
|
|
6307
|
+
const resp = await fetch('/api/update', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ force: !!force }) });
|
|
6308
|
+
const data = await resp.json().catch(() => ({}));
|
|
6254
6309
|
if (!data.ok) {
|
|
6310
|
+
// 有任务正在执行:说清楚会打断谁,确认后强制升级
|
|
6311
|
+
if (data.busy?.length) {
|
|
6312
|
+
if (confirm(data.error + '\n\n仍然现在升级吗?')) return doUpdate(true);
|
|
6313
|
+
setBadge(`⬆ ${_versionInfo.latest}`, 'var(--yellow)'); return;
|
|
6314
|
+
}
|
|
6255
6315
|
addStatusMessage('更新失败:' + (data.error || '未知错误'));
|
|
6256
|
-
|
|
6257
|
-
badge.style.color = 'var(--red)';
|
|
6316
|
+
setBadge('⬆ 更新失败', 'var(--red)');
|
|
6258
6317
|
return;
|
|
6259
6318
|
}
|
|
6260
6319
|
addStatusMessage(data.message || '升级中...');
|
|
6261
|
-
|
|
6262
|
-
|
|
6263
|
-
|
|
6264
|
-
let retries = 0;
|
|
6265
|
-
const poll = setInterval(async () => {
|
|
6266
|
-
try {
|
|
6267
|
-
const r = await fetch('/api/version');
|
|
6268
|
-
if (r.ok) { clearInterval(poll); location.reload(); }
|
|
6269
|
-
} catch {}
|
|
6270
|
-
if (++retries > 30) { clearInterval(poll); badge.textContent = '⬆ 请手动刷新'; badge.style.color = 'var(--red)'; }
|
|
6271
|
-
}, 3000);
|
|
6272
|
-
}, 5000);
|
|
6320
|
+
const r = await waitUpgrade(data.from, data.target, (up, sec) => setBadge(up ? `⬆ 安装中 ${sec}s` : `⬆ 重启中 ${sec}s`));
|
|
6321
|
+
if (r.ok) { addStatusMessage(`已升级到 ${r.version},即将刷新`); setTimeout(() => location.reload(), 800); }
|
|
6322
|
+
else { addStatusMessage('更新失败:' + r.error); setBadge('⬆ 更新失败', 'var(--red)'); }
|
|
6273
6323
|
} catch (e) {
|
|
6274
6324
|
addStatusMessage('更新失败: ' + e.message);
|
|
6275
|
-
|
|
6276
|
-
badge.style.color = 'var(--yellow)';
|
|
6325
|
+
setBadge(`⬆ ${_versionInfo.latest}`, 'var(--yellow)');
|
|
6277
6326
|
}
|
|
6278
6327
|
};
|
|
6279
6328
|
|
package/dist/index.html
CHANGED
|
@@ -187,6 +187,13 @@
|
|
|
187
187
|
}
|
|
188
188
|
.empty-state svg { opacity: 0.3; }
|
|
189
189
|
.empty-state p { font-size: 0.95rem; }
|
|
190
|
+
.empty-new { background: var(--accent); color: #fff; border: none; border-radius: 10px; padding: 0.65rem 1.4rem; font-size: 0.92rem; font-weight: 600; cursor: pointer; box-shadow: 0 4px 16px rgba(124,58,237,0.35); }
|
|
191
|
+
.empty-new:hover { background: var(--accent2); }
|
|
192
|
+
.empty-state small { font-size: 0.72rem; opacity: 0.8; }
|
|
193
|
+
/* 搜索框旁的「+ 新建任务」:大屏上右下角 FAB 离内容太远容易看不到;手机上有 FAB 就够了 */
|
|
194
|
+
#new-btn { flex-shrink: 0; white-space: nowrap; background: var(--accent); color: #fff; border: none; border-radius: 8px; font-size: 0.8rem; font-weight: 600; padding: 0.42rem 0.85rem; cursor: pointer; }
|
|
195
|
+
#new-btn:hover { background: var(--accent2); }
|
|
196
|
+
@media (max-width: 540px) { #new-btn { display: none; } }
|
|
190
197
|
|
|
191
198
|
/* ── Session card ── */
|
|
192
199
|
.card {
|
|
@@ -283,6 +290,14 @@
|
|
|
283
290
|
z-index: 20;
|
|
284
291
|
}
|
|
285
292
|
#fab:active { background: var(--accent2); transform: scale(0.93); }
|
|
293
|
+
/* 首次引导关掉后:FAB 呼吸光圈 + 气泡指一下「点这里新建任务」,只出现一次 */
|
|
294
|
+
#fab.hint { animation: fabPulse 1.4s ease-out infinite; }
|
|
295
|
+
@keyframes fabPulse { 0% { box-shadow: 0 0 0 0 rgba(124,58,237,0.7), 0 4px 20px rgba(124,58,237,0.5); } 100% { box-shadow: 0 0 0 22px rgba(124,58,237,0), 0 4px 20px rgba(124,58,237,0.5); } }
|
|
296
|
+
#fab-tip { display: none; position: fixed; z-index: 21; right: max(5.2rem, calc(env(safe-area-inset-right) + 4.7rem)); bottom: max(1.85rem, calc(env(safe-area-inset-bottom) + 1.35rem));
|
|
297
|
+
background: var(--accent); color: #fff; font-size: 0.85rem; font-weight: 600; padding: 0.5rem 0.8rem; border-radius: 10px; white-space: nowrap; box-shadow: 0 4px 16px rgba(0,0,0,0.35); cursor: pointer; }
|
|
298
|
+
#fab-tip::after { content: ''; position: absolute; right: -6px; top: 50%; margin-top: -6px; border: 6px solid transparent; border-right: none; border-left-color: var(--accent); }
|
|
299
|
+
#fab-tip.show { display: block; animation: tipIn 0.3s ease; }
|
|
300
|
+
@keyframes tipIn { from { opacity: 0; transform: translateX(8px); } to { opacity: 1; transform: none; } }
|
|
286
301
|
|
|
287
302
|
/* 随手记 FAB:灵感常常在你不在任何会话页的时候冒出来,首页也得能记 */
|
|
288
303
|
#fab-note {
|
|
@@ -604,6 +619,7 @@
|
|
|
604
619
|
autocomplete="off" spellcheck="false"
|
|
605
620
|
style="flex:1;background:var(--bg,var(--bg));border:1px solid var(--border,var(--border));border-radius:8px;color:var(--text,var(--text));font-size:0.8rem;padding:0.4rem 0.7rem;outline:none">
|
|
606
621
|
<span id="search-count" style="font-size:0.72rem;color:var(--muted,var(--muted));white-space:nowrap"></span>
|
|
622
|
+
<button id="new-btn" onclick="openSheet()" title="新建任务(也可点右下角 +)">+ 新建任务</button>
|
|
607
623
|
</div>
|
|
608
624
|
<div id="group-seg" style="display:inline-flex;align-self:flex-start;background:var(--bg,var(--bg));border:1px solid var(--border,var(--border));border-radius:8px;overflow:hidden">
|
|
609
625
|
<button class="grp-btn" data-group="time" style="background:none;border:none;color:var(--muted,var(--muted));font-size:0.78rem;padding:0.35rem 0.8rem;cursor:pointer">最近</button>
|
|
@@ -619,13 +635,16 @@
|
|
|
619
635
|
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
|
620
636
|
<path d="M8 9l4 3-4 3M13 15h3"/>
|
|
621
637
|
</svg>
|
|
622
|
-
<p
|
|
638
|
+
<p>还没有任务</p>
|
|
639
|
+
<button class="empty-new" onclick="openSheet()">+ 新建第一个任务</button>
|
|
640
|
+
<small>以后随时点右下角的 + 新建</small>
|
|
623
641
|
</div>
|
|
624
642
|
</div>
|
|
625
643
|
</div>
|
|
626
644
|
|
|
627
645
|
<!-- FAB -->
|
|
628
|
-
<button id="fab" onclick="openSheet()">+</button>
|
|
646
|
+
<button id="fab" onclick="openSheet()" title="新建任务">+</button>
|
|
647
|
+
<div id="fab-tip" onclick="openSheet()">👉 点这里新建任务</div>
|
|
629
648
|
<button id="fab-note" onclick="openNoteSheet()" title="随手记:把想法先记进某个任务的草稿箱">📥</button>
|
|
630
649
|
|
|
631
650
|
<!-- 使用说明 / 快速上手(首次访问自动弹出,之后从顶栏 ❓ 打开)。桌面居中卡片,手机底部抽屉 -->
|
|
@@ -653,7 +672,7 @@
|
|
|
653
672
|
<div class="hs-tip">📱 手机浏览器打开同一地址即可(局域网 / Tailscale),建议「添加到主屏幕」当 App 用。</div>
|
|
654
673
|
<div class="hs-actions">
|
|
655
674
|
<button class="hs-ghost" onclick="closeHelpSheet()">我知道了</button>
|
|
656
|
-
<button class="hs-primary" onclick="closeHelpSheet(); openSheet();">+ 新建第一个任务</button>
|
|
675
|
+
<button class="hs-primary" onclick="closeHelpSheet(true); openSheet();">+ 新建第一个任务</button>
|
|
657
676
|
</div>
|
|
658
677
|
</div>
|
|
659
678
|
|
|
@@ -709,7 +728,7 @@
|
|
|
709
728
|
<div class="set-group-t">这台电脑 · 所有设备生效</div>
|
|
710
729
|
<div class="set-card">
|
|
711
730
|
<div class="set-item inp">
|
|
712
|
-
<div class="set-lbl"
|
|
731
|
+
<div class="set-lbl">本机显示名<small>首页 / 分屏的浏览器标签显示它,不改电脑主机名;留空用主机名</small></div>
|
|
713
732
|
<div class="set-ctl">
|
|
714
733
|
<input id="set-machine-name" class="set-inp" maxlength="32" autocomplete="off" data-save="saveMachineName">
|
|
715
734
|
<button class="set-btn" onclick="saveMachineName()">保存</button>
|
|
@@ -1224,7 +1243,9 @@
|
|
|
1224
1243
|
<rect x="2" y="4" width="20" height="16" rx="2"/>
|
|
1225
1244
|
<path d="M8 9l4 3-4 3M13 15h3"/>
|
|
1226
1245
|
</svg>
|
|
1227
|
-
<p
|
|
1246
|
+
<p>还没有任务</p>
|
|
1247
|
+
<button class="empty-new" onclick="openSheet()">+ 新建第一个任务</button>
|
|
1248
|
+
<small>以后随时点右下角的 + 新建</small>
|
|
1228
1249
|
</div>`;
|
|
1229
1250
|
|
|
1230
1251
|
function cardHTML(s) {
|
|
@@ -1422,11 +1443,21 @@
|
|
|
1422
1443
|
document.getElementById('help-backdrop').style.display = 'block';
|
|
1423
1444
|
document.getElementById('help-sheet').style.display = 'block';
|
|
1424
1445
|
};
|
|
1425
|
-
window.closeHelpSheet = function() {
|
|
1446
|
+
window.closeHelpSheet = function(creating) {
|
|
1426
1447
|
document.getElementById('help-backdrop').style.display = 'none';
|
|
1427
1448
|
document.getElementById('help-sheet').style.display = 'none';
|
|
1449
|
+
const first = !localStorage.getItem('myhi_onboarded');
|
|
1428
1450
|
localStorage.setItem('myhi_onboarded', '1'); // 看过一次就不再自动弹
|
|
1451
|
+
// 第一次看完引导点的是「我知道了 / ✕」:把用户的视线引到右下角 +(大屏上它离内容很远,容易看不到)
|
|
1452
|
+
if (first && !creating) showFabHint();
|
|
1429
1453
|
};
|
|
1454
|
+
function showFabHint() {
|
|
1455
|
+
const fab = document.getElementById('fab'), tip = document.getElementById('fab-tip');
|
|
1456
|
+
fab.classList.add('hint'); tip.classList.add('show');
|
|
1457
|
+
const done = () => { fab.classList.remove('hint'); tip.classList.remove('show'); document.removeEventListener('pointerdown', done, true); };
|
|
1458
|
+
setTimeout(() => document.addEventListener('pointerdown', done, true), 300); // 点任何地方(含 + 本身)就收起
|
|
1459
|
+
setTimeout(done, 12000);
|
|
1460
|
+
}
|
|
1430
1461
|
// 首次访问:自动弹快速上手,带着建第一个任务
|
|
1431
1462
|
if (!localStorage.getItem('myhi_onboarded') && !new URLSearchParams(location.search).get('embed')) {
|
|
1432
1463
|
setTimeout(openHelpSheet, 600);
|