@wendongfly/myhi 1.3.167 → 1.3.169
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/dist/chat.html +144 -1
- package/dist/index.html +29 -7
- package/dist/index.js +1 -1
- package/dist/index.min.js +128 -128
- package/package.json +1 -1
package/dist/chat.html
CHANGED
|
@@ -210,6 +210,22 @@
|
|
|
210
210
|
#img-preview .img-remove:hover { background: var(--red); color: #fff; }
|
|
211
211
|
#input-box.focused { border-color: var(--blue); }
|
|
212
212
|
#input-box.disabled { opacity: 0.5; pointer-events: none; }
|
|
213
|
+
|
|
214
|
+
/* @ 文件引用补全浮层(挂在 body 上,fixed 定位在输入框上方) */
|
|
215
|
+
#mention-box {
|
|
216
|
+
position: fixed; z-index: 95; display: none;
|
|
217
|
+
background: var(--surface); border: 1px solid var(--border); border-radius: 10px;
|
|
218
|
+
box-shadow: 0 8px 30px rgba(0,0,0,0.45);
|
|
219
|
+
max-height: 42vh; overflow-y: auto; overscroll-behavior: contain;
|
|
220
|
+
font-size: 0.8rem; padding: 0.25rem;
|
|
221
|
+
}
|
|
222
|
+
#mention-box .mi { display: flex; align-items: center; gap: 0.45rem; padding: 0.45rem 0.6rem; border-radius: 7px; cursor: pointer; }
|
|
223
|
+
#mention-box .mi.on { background: var(--surface3); }
|
|
224
|
+
#mention-box .mi .m-ico { flex-shrink: 0; }
|
|
225
|
+
#mention-box .mi .m-dir { color: var(--muted); font-size: 0.68rem; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; flex-shrink: 1; }
|
|
226
|
+
#mention-box .mi .m-name { color: var(--text); white-space: nowrap; }
|
|
227
|
+
#mention-box .mi .m-hint { color: var(--muted); font-size: 0.66rem; margin-left: auto; padding-left: 0.5rem; flex-shrink: 0; }
|
|
228
|
+
#mention-box .mi-empty { padding: 0.55rem 0.7rem; color: var(--muted); font-size: 0.75rem; }
|
|
213
229
|
#cmd-input { display: block; width: 100%; background: transparent; color: var(--text); border: none; padding: 0.6rem 0.75rem 0.3rem; font-family: 'SF Mono', 'Cascadia Code', 'Consolas', monospace; font-size: 0.85rem; outline: none; resize: none; line-height: 1.5; max-height: 140px; overflow-y: auto; }
|
|
214
230
|
#cmd-input::placeholder { color: var(--dimmer); }
|
|
215
231
|
@media (pointer: coarse) { #cmd-input { font-size: 1rem; padding: 0.75rem 0.75rem 0.35rem; line-height: 1.55; } }
|
|
@@ -2871,7 +2887,110 @@
|
|
|
2871
2887
|
cmdInput.focus();
|
|
2872
2888
|
}
|
|
2873
2889
|
|
|
2890
|
+
// ── @ 文件引用补全 ─────────────────────────────────────────
|
|
2891
|
+
// 输入框里打出 @词(@ 后无空白)时浮出文件搜索层:递归搜会话目录、
|
|
2892
|
+
// 最近修改优先;选中把 @词 替换成真实相对路径。「让 AI 读某个文件」
|
|
2893
|
+
// 不再需要手打文件名。也可在 📄 文件面板点「引用」达到同样效果。
|
|
2894
|
+
const _mention = { open: false, start: -1, items: [], idx: 0 };
|
|
2895
|
+
const _mentionBox = document.createElement('div');
|
|
2896
|
+
_mentionBox.id = 'mention-box';
|
|
2897
|
+
document.body.appendChild(_mentionBox);
|
|
2898
|
+
let _mentionTimer = null;
|
|
2899
|
+
|
|
2900
|
+
// 光标处是否正打着一个 @词 —— 是则返回 { start }(@ 的下标)
|
|
2901
|
+
function mentionToken() {
|
|
2902
|
+
const pos = cmdInput.selectionStart ?? cmdInput.value.length;
|
|
2903
|
+
const val = cmdInput.value;
|
|
2904
|
+
const from = val.lastIndexOf('@', Math.max(0, pos - 1));
|
|
2905
|
+
if (from < 0) return null;
|
|
2906
|
+
if (from > 0 && /[A-Za-z0-9._%-]/.test(val[from - 1])) return null; // 邮箱等普通文本不触发
|
|
2907
|
+
if (/[\s\r\n]/.test(val.slice(from + 1, pos))) return null; // @ 后已有空白 = 早结束了
|
|
2908
|
+
return { start: from };
|
|
2909
|
+
}
|
|
2910
|
+
|
|
2911
|
+
function mentionClose() {
|
|
2912
|
+
_mention.open = false;
|
|
2913
|
+
_mention.items = [];
|
|
2914
|
+
_mentionBox.style.display = 'none';
|
|
2915
|
+
}
|
|
2916
|
+
|
|
2917
|
+
function mentionRowHTML(path, i) {
|
|
2918
|
+
const cut = path.lastIndexOf('/');
|
|
2919
|
+
const dir = cut >= 0 ? path.slice(0, cut + 1) : '';
|
|
2920
|
+
const name = cut >= 0 ? path.slice(cut + 1) : path;
|
|
2921
|
+
return `<div class="mi ${i === _mention.idx ? 'on' : ''}" data-i="${i}"><span class="m-ico">${isEditableFile(name) ? '📝' : '📄'}</span><span class="m-dir">${escHtml(dir)}</span><span class="m-name">${escHtml(name)}</span><span class="m-hint">引用</span></div>`;
|
|
2922
|
+
}
|
|
2923
|
+
|
|
2924
|
+
function mentionRender() {
|
|
2925
|
+
_mentionBox.innerHTML = _mention.items.length
|
|
2926
|
+
? _mention.items.map((f, i) => mentionRowHTML(f, i)).join('')
|
|
2927
|
+
: '<div class="mi-empty">没有匹配的文件</div>';
|
|
2928
|
+
const r = cmdInput.getBoundingClientRect();
|
|
2929
|
+
_mentionBox.style.left = Math.max(8, r.left) + 'px';
|
|
2930
|
+
_mentionBox.style.width = Math.max(r.width, 240) + 'px';
|
|
2931
|
+
_mentionBox.style.bottom = (window.innerHeight - r.top + 6) + 'px';
|
|
2932
|
+
_mentionBox.style.display = 'block';
|
|
2933
|
+
const on = _mentionBox.querySelector('.mi.on');
|
|
2934
|
+
if (on) on.scrollIntoView({ block: 'nearest' });
|
|
2935
|
+
}
|
|
2936
|
+
|
|
2937
|
+
function mentionSearch(token) {
|
|
2938
|
+
clearTimeout(_mentionTimer);
|
|
2939
|
+
_mentionTimer = setTimeout(async () => {
|
|
2940
|
+
try {
|
|
2941
|
+
const r = await fetch(`/api/files/${SESSION_ID}/search?q=${encodeURIComponent(token)}`);
|
|
2942
|
+
const d = await r.json();
|
|
2943
|
+
if (!_mention.open || !d.ok) return;
|
|
2944
|
+
_mention.items = d.files || [];
|
|
2945
|
+
_mention.idx = 0;
|
|
2946
|
+
mentionRender();
|
|
2947
|
+
} catch { mentionClose(); }
|
|
2948
|
+
}, 140);
|
|
2949
|
+
}
|
|
2950
|
+
|
|
2951
|
+
function mentionUpdate() {
|
|
2952
|
+
const t = mentionToken();
|
|
2953
|
+
if (!t) return mentionClose();
|
|
2954
|
+
_mention.open = true;
|
|
2955
|
+
_mention.start = t.start;
|
|
2956
|
+
mentionSearch(cmdInput.value.slice(t.start + 1, cmdInput.selectionStart ?? cmdInput.value.length));
|
|
2957
|
+
}
|
|
2958
|
+
|
|
2959
|
+
function mentionPick(path) {
|
|
2960
|
+
const pos = cmdInput.selectionStart ?? cmdInput.value.length;
|
|
2961
|
+
const before = cmdInput.value.slice(0, _mention.start);
|
|
2962
|
+
const after = cmdInput.value.slice(pos);
|
|
2963
|
+
cmdInput.value = before + path + ' ' + after;
|
|
2964
|
+
const caret = (before + path + ' ').length;
|
|
2965
|
+
cmdInput.setSelectionRange(caret, caret);
|
|
2966
|
+
mentionClose();
|
|
2967
|
+
cmdInput.style.height = 'auto';
|
|
2968
|
+
cmdInput.style.height = Math.min(cmdInput.scrollHeight, 200) + 'px';
|
|
2969
|
+
cmdInput.focus();
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
cmdInput.addEventListener('input', (e) => { if (!e.isComposing) mentionUpdate(); });
|
|
2973
|
+
cmdInput.addEventListener('keyup', (e) => { if (!['ArrowUp','ArrowDown','Escape','Enter'].includes(e.key)) mentionUpdate(); });
|
|
2974
|
+
cmdInput.addEventListener('blur', () => setTimeout(mentionClose, 120)); // 等候选中的 pointerdown 先落地
|
|
2975
|
+
_mentionBox.addEventListener('pointerdown', (e) => {
|
|
2976
|
+
const row = e.target.closest('.mi[data-i]');
|
|
2977
|
+
if (row) { e.preventDefault(); mentionPick(_mention.items[Number(row.dataset.i)]); }
|
|
2978
|
+
});
|
|
2979
|
+
window.addEventListener('resize', () => { if (_mention.open) mentionClose(); });
|
|
2980
|
+
|
|
2874
2981
|
cmdInput.addEventListener('keydown', (e) => {
|
|
2982
|
+
if (_mention.open) {
|
|
2983
|
+
if (e.key === 'ArrowDown' && _mention.items.length) {
|
|
2984
|
+
e.preventDefault(); _mention.idx = (_mention.idx + 1) % _mention.items.length; mentionRender(); return;
|
|
2985
|
+
}
|
|
2986
|
+
if (e.key === 'ArrowUp' && _mention.items.length) {
|
|
2987
|
+
e.preventDefault(); _mention.idx = (_mention.idx - 1 + _mention.items.length) % _mention.items.length; mentionRender(); return;
|
|
2988
|
+
}
|
|
2989
|
+
if ((e.key === 'Enter' || e.key === 'Tab') && _mention.items.length && !e.isComposing && e.keyCode !== 229) {
|
|
2990
|
+
e.preventDefault(); mentionPick(_mention.items[_mention.idx]); return; // 补全层开着时回车=选中,不是发送
|
|
2991
|
+
}
|
|
2992
|
+
if (e.key === 'Escape') { e.preventDefault(); mentionClose(); return; } // 先关补全,别误触中断 AI
|
|
2993
|
+
}
|
|
2875
2994
|
if (e.key === 'Escape') {
|
|
2876
2995
|
e.preventDefault();
|
|
2877
2996
|
if (currentSession?.mode === 'agent' && workState !== 'idle') {
|
|
@@ -2992,13 +3111,14 @@
|
|
|
2992
3111
|
for (const e of entries) {
|
|
2993
3112
|
const rel = fpCurrentPath ? fpCurrentPath + '/' + e.name : e.name;
|
|
2994
3113
|
if (e.isDir) {
|
|
2995
|
-
html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name">${escHtml(e.name)}</span></div>`;
|
|
3114
|
+
html += `<div class="fp-item" onclick="fpNav('${escHtml(rel)}')"><span class="fp-icon">📁</span><span class="fp-name">${escHtml(e.name)}</span><span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}/')" title="把目录路径插入消息框">引用</span></div>`;
|
|
2996
3115
|
} else {
|
|
2997
3116
|
const editable = isEditableFile(e.name);
|
|
2998
3117
|
html += `<div class="fp-item" ${editable ? `style="cursor:pointer" onclick="openFileEditor('${escAttr(rel)}')" title="点击在线编辑"` : ''}>
|
|
2999
3118
|
<span class="fp-icon">${editable ? '📝' : '📄'}</span>
|
|
3000
3119
|
<span class="fp-name">${escHtml(e.name)}</span>
|
|
3001
3120
|
<span class="fp-size">${fpFormatSize(e.size)}</span>
|
|
3121
|
+
<span class="fp-dl" onclick="event.stopPropagation();fpRefer('${escAttr(rel)}')" title="把文件路径插入消息框,让 AI 读这个文件">引用</span>
|
|
3002
3122
|
${editable ? `<span class="fp-dl" onclick="event.stopPropagation();openFileEditor('${escAttr(rel)}')" title="在线编辑">编辑</span>` : ''}
|
|
3003
3123
|
<a class="fp-dl" href="/api/files/${SESSION_ID}/download?path=${encodeURIComponent(rel)}" download title="下载" onclick="event.stopPropagation()">下载</a>
|
|
3004
3124
|
</div>`;
|
|
@@ -3012,6 +3132,29 @@
|
|
|
3012
3132
|
|
|
3013
3133
|
window.fpNav = function(path) { fpCurrentPath = path; fpLoadFiles(); };
|
|
3014
3134
|
|
|
3135
|
+
// 把文本插进消息框光标处(前面没空白就补一个,后面留一个空格方便接着打字)
|
|
3136
|
+
function insertIntoInput(text) {
|
|
3137
|
+
const ta = cmdInput;
|
|
3138
|
+
const s = ta.selectionStart ?? ta.value.length;
|
|
3139
|
+
const e = ta.selectionEnd ?? ta.value.length;
|
|
3140
|
+
const before = ta.value.slice(0, s);
|
|
3141
|
+
const after = ta.value.slice(e);
|
|
3142
|
+
const sep = before && !/\s$/.test(before) ? ' ' : '';
|
|
3143
|
+
ta.value = before + sep + text + ' ' + after;
|
|
3144
|
+
const pos = (before + sep + text + ' ').length;
|
|
3145
|
+
ta.setSelectionRange(pos, pos);
|
|
3146
|
+
ta.style.height = 'auto';
|
|
3147
|
+
ta.style.height = Math.min(ta.scrollHeight, 200) + 'px';
|
|
3148
|
+
ta.focus();
|
|
3149
|
+
}
|
|
3150
|
+
|
|
3151
|
+
// 文件面板「引用」:把相对路径插进消息框 —— 「让 AI 读某个文件」不用再手打文件名。
|
|
3152
|
+
// 引用完关掉面板,直接回到输入框继续写要求。
|
|
3153
|
+
window.fpRefer = function(rel) {
|
|
3154
|
+
closeFilePanel();
|
|
3155
|
+
insertIntoInput(rel);
|
|
3156
|
+
};
|
|
3157
|
+
|
|
3015
3158
|
function fpFormatSize(bytes) {
|
|
3016
3159
|
if (bytes == null) return '';
|
|
3017
3160
|
if (bytes < 1024) return bytes + ' B';
|
package/dist/index.html
CHANGED
|
@@ -81,6 +81,28 @@
|
|
|
81
81
|
#conn-dot.on { background: var(--green); box-shadow: 0 0 6px var(--green); }
|
|
82
82
|
#conn-dot.off { background: var(--red); }
|
|
83
83
|
|
|
84
|
+
/* 顶栏右侧按钮组:空间不够时整组换行,但按钮自身永不收缩——
|
|
85
|
+
否则中文标签被挤成一字一行竖排(手机端反馈「不美观」的主因) */
|
|
86
|
+
.hd-actions {
|
|
87
|
+
display: flex;
|
|
88
|
+
align-items: center;
|
|
89
|
+
gap: 0.6rem;
|
|
90
|
+
flex-wrap: wrap;
|
|
91
|
+
justify-content: flex-end;
|
|
92
|
+
}
|
|
93
|
+
.hd-btn {
|
|
94
|
+
background: none; border: 1px solid var(--border); color: var(--muted);
|
|
95
|
+
font-size: 0.75rem; padding: 0.3rem 0.6rem; border-radius: 6px; cursor: pointer;
|
|
96
|
+
width: auto; white-space: nowrap; flex-shrink: 0;
|
|
97
|
+
}
|
|
98
|
+
/* 手机(≤540px):藏用户名、文字标签只留图标,一行放下且不憋屈 */
|
|
99
|
+
@media (max-width: 540px) {
|
|
100
|
+
#user-name { display: none; }
|
|
101
|
+
.hd-actions { gap: 0.4rem; }
|
|
102
|
+
.hd-btn { padding: 0.32rem 0.48rem; }
|
|
103
|
+
.hd-btn .hd-txt { display: none; }
|
|
104
|
+
}
|
|
105
|
+
|
|
84
106
|
/* ── Session list ── */
|
|
85
107
|
#list {
|
|
86
108
|
flex: 1;
|
|
@@ -456,14 +478,14 @@
|
|
|
456
478
|
<div id="app">
|
|
457
479
|
<div id="header">
|
|
458
480
|
<div class="logo">my<span>hi</span></div>
|
|
459
|
-
<div
|
|
481
|
+
<div class="hd-actions">
|
|
460
482
|
<span id="user-name" style="font-size:0.8rem;color:var(--muted)"></span>
|
|
461
|
-
<button id="grid-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)"
|
|
462
|
-
<button id="appearance-btn" onclick="openAppearance()" title="外观"
|
|
463
|
-
<button id="doctor-btn" onclick="openDoctor()"
|
|
464
|
-
<button id="usage-btn" onclick="showUsage()"
|
|
465
|
-
<button id="passwd-btn" onclick="openPasswdSheet()" title="修改我的密码" style="display:none
|
|
466
|
-
<button id="logout-btn" onclick="doLogout()" style="display:none
|
|
483
|
+
<button id="grid-btn" class="hd-btn" onclick="location.href='/grid'" title="分屏工作台(多任务并排)">▦ 分屏</button>
|
|
484
|
+
<button id="appearance-btn" class="hd-btn" onclick="openAppearance()" title="外观">🎨</button>
|
|
485
|
+
<button id="doctor-btn" class="hd-btn" onclick="openDoctor()" title="会话体检">🩺<span class="hd-txt"> 体检</span></button>
|
|
486
|
+
<button id="usage-btn" class="hd-btn" onclick="showUsage()" title="用量">📊<span class="hd-txt"> 用量</span></button>
|
|
487
|
+
<button id="passwd-btn" class="hd-btn" onclick="openPasswdSheet()" title="修改我的密码" style="display:none">🔑<span class="hd-txt"> 改密码</span></button>
|
|
488
|
+
<button id="logout-btn" class="hd-btn" onclick="doLogout()" style="display:none">退出</button>
|
|
467
489
|
<div id="conn-dot"></div>
|
|
468
490
|
</div>
|
|
469
491
|
</div>
|