From: Codebuddy Date: Thu, 18 Jun 2026 08:39:34 +0000 (+0800) Subject: 粘贴改为右上角弹窗模式(避免发送自动提交问题) X-Git-Url: http://acesimba.cloud/gitweb/?a=commitdiff_plain;h=3125ecdbee443192f7ff1cc06c0c516f3becc082;p=codebuddy-web.git 粘贴改为右上角弹窗模式(避免发送自动提交问题) 变更: - 移除下方独立输入框,恢复 xterm 原生输入(两条绿线之间) - 右上角"列表"后加回"📋 粘贴"按钮 - 点击弹出 modal:textarea + 📎 + ✕ + 发送,样式复用豆包式输入框 - modal 支持文字/图片粘贴、图片拖拽、📎 选文件 - Ctrl+Enter 或点发送:内容进 xterm 输入行(不自动提交),modal 自动关闭 - ESC 关闭 modal - 发送逻辑只发内容字符流,不带 \r,用户自行回车提交 原因: 之前自动提交方案(\n / \r / bracketed paste 包裹)均无法让 codebuddy CLI 正确触发提交。改为"内容进输入行 + 用户回车"最可靠,符合终端交互本质。 --- diff --git a/frontend/index.html b/frontend/index.html index 4a3eac0..9f30e5c 100644 --- a/frontend/index.html +++ b/frontend/index.html @@ -59,29 +59,11 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color: } .top-bar .action-btn:hover { background: rgba(255,255,255,0.12); color: #fff; } -/* 终端页布局:xterm 输出区 + 独立输入区 */ +/* 终端页布局 */ #terminal-page { display: none; height: 100vh; flex-direction: column; } #terminal-container { flex: 1; overflow: hidden; padding: 4px; min-height: 0; } -/* 输入区(豆包式) */ -#input-bar { - flex-shrink: 0; border-top: 1px solid rgba(255,255,255,0.12); - background: rgba(0,0,0,0.35); padding: 10px 14px; -} -#input-bar .input-row { - display: flex; gap: 8px; align-items: flex-end; - background: rgba(255,255,255,0.06); border: 1px solid rgba(255,255,255,0.15); - border-radius: 12px; padding: 8px 10px; -} -#input-bar .input-row:focus-within { border-color: #4ecca3; } -#msg-input { - flex: 1; background: transparent; border: none; outline: none; color: #e0e0e0; - font-family: 'Cascadia Code', 'Fira Code', Menlo, Monaco, 'Courier New', monospace; - font-size: 14px; line-height: 1.5; resize: none; max-height: 160px; min-height: 24px; - padding: 4px 4px; -} -#msg-input::placeholder { color: #666; } -#input-bar .input-actions { display: flex; gap: 6px; align-items: center; flex-shrink: 0; } +/* 通用按钮样式(粘贴 modal 复用) */ .icon-btn { width: 32px; height: 32px; border-radius: 8px; border: 1px solid rgba(255,255,255,0.15); background: rgba(255,255,255,0.05); color: #ccc; font-size: 16px; cursor: pointer; @@ -94,16 +76,13 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color: font-size: 14px; font-weight: 600; cursor: pointer; } .send-btn:hover { opacity: 0.9; } -.send-btn:disabled { opacity: 0.4; cursor: not-allowed; } -#input-bar .input-hint { font-size: 11px; color: #666; margin-top: 4px; text-align: right; } -#input-bar .img-chips { display: flex; flex-wrap: wrap; gap: 6px; margin-bottom: 6px; } -#input-bar .img-chip { +.img-chip { display: inline-flex; align-items: center; gap: 6px; padding: 3px 8px; background: rgba(78,204,163,0.12); border: 1px solid rgba(78,204,163,0.3); border-radius: 6px; font-size: 12px; color: #4ecca3; } -#input-bar .img-chip .remove { cursor: pointer; opacity: 0.6; } -#input-bar .img-chip .remove:hover { opacity: 1; } +.img-chip .remove { cursor: pointer; opacity: 0.6; } +.img-chip .remove:hover { opacity: 1; } /* 任务列表页 */ #picker-page { display: none; flex-direction: column; height: 100vh; } @@ -184,22 +163,27 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color:
+
-
- -
- -
- - - + +
@@ -386,24 +370,17 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color: if (ws && ws.readyState === WebSocket.OPEN) ws.send(JSON.stringify({ type: 'input', data: text })); } - // ========== 输入区(方案 B:独立 textarea 输入框) ========== - const msgInput = document.getElementById('msg-input'); - const btnSend = document.getElementById('btn-send'); - const btnAttach = document.getElementById('btn-attach'); - const btnClear = document.getElementById('btn-clear'); - const fileInput = document.getElementById('file-input'); - const imgChips = document.getElementById('img-chips'); - const wsHint = document.getElementById('ws-hint'); + // ========== 粘贴 modal(右上角 📋 粘贴按钮触发) ========== + const pasteModal = document.getElementById('paste-modal'); + const pasteInput = document.getElementById('paste-input'); + const pasteSend = document.getElementById('paste-send'); + const pasteAttach = document.getElementById('paste-attach'); + const pasteClear = document.getElementById('paste-clear'); + const pasteFileInput = document.getElementById('paste-file-input'); + const pasteImgChips = document.getElementById('paste-img-chips'); // 待发送的图片列表:[{path, name}] let pendingImages = []; - // textarea 自适应高度 - function autoResize() { - msgInput.style.height = 'auto'; - msgInput.style.height = Math.min(160, msgInput.scrollHeight) + 'px'; - } - msgInput.addEventListener('input', autoResize); - // 上传图片到后端,返回 {path, name} 或抛错 async function uploadImage(blob) { const sizeKB = Math.max(1, Math.round(blob.size / 1024)); @@ -433,21 +410,34 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color: } // 渲染图片 chip 列表 - function renderImgChips() { - if (!pendingImages.length) { imgChips.style.display = 'none'; imgChips.innerHTML = ''; return; } - imgChips.style.display = 'flex'; - imgChips.innerHTML = pendingImages.map((img, i) => + function renderPasteImgChips() { + if (!pendingImages.length) { pasteImgChips.style.display = 'none'; pasteImgChips.innerHTML = ''; return; } + pasteImgChips.style.display = 'flex'; + pasteImgChips.innerHTML = pendingImages.map((img, i) => '🖼 ' + img.name + ' ✕' ).join(''); - imgChips.querySelectorAll('.remove').forEach(el => { - el.onclick = () => { pendingImages.splice(parseInt(el.dataset.i), 1); renderImgChips(); }; + pasteImgChips.querySelectorAll('.remove').forEach(el => { + el.onclick = () => { pendingImages.splice(parseInt(el.dataset.i), 1); renderPasteImgChips(); }; }); } + // 打开/关闭 modal + function openPasteModal() { + pasteInput.value = ''; + pendingImages = []; + renderPasteImgChips(); + pasteModal.style.display = 'flex'; + setTimeout(() => pasteInput.focus(), 0); + } + function closePasteModal() { + pasteModal.style.display = 'none'; + if (term && term.focus) term.focus(); + } + document.getElementById('btn-paste').onclick = openPasteModal; + // textarea 的 paste 事件:文字直接插入,图片上传后加 chip - msgInput.addEventListener('paste', async (e) => { + pasteInput.addEventListener('paste', async (e) => { if (!e.clipboardData) return; - // 优先检测图片 let imgBlob = null; if (e.clipboardData.items) { for (const item of e.clipboardData.items) { @@ -463,95 +453,75 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color: } if (imgBlob) { e.preventDefault(); - try { - const img = await uploadImage(imgBlob); - pendingImages.push(img); - renderImgChips(); - } catch (err) { - term.writeln('\x1b[31m✗ 图片上传失败: ' + err.message + '\x1b[0m'); - } + try { const img = await uploadImage(imgBlob); pendingImages.push(img); renderPasteImgChips(); } + catch (err) { term.writeln('\x1b[31m✗ 图片上传失败: ' + err.message + '\x1b[0m'); } return; } - // 文字:让浏览器默认插入 textarea(不拦截) + // 文字:浏览器默认插入 }); - // 拖拽图片到输入区 - msgInput.addEventListener('dragover', (e) => { e.preventDefault(); }); - msgInput.addEventListener('drop', async (e) => { + // 拖拽图片到 modal + pasteInput.addEventListener('dragover', (e) => { e.preventDefault(); }); + pasteInput.addEventListener('drop', async (e) => { if (!e.dataTransfer || !e.dataTransfer.files) return; const files = Array.from(e.dataTransfer.files).filter(f => f.type && f.type.startsWith('image/')); if (!files.length) return; e.preventDefault(); for (const f of files) { - try { - const img = await uploadImage(f); - pendingImages.push(img); - } catch (err) { - term.writeln('\x1b[31m✗ ' + f.name + ' 上传失败: ' + err.message + '\x1b[0m'); - } + try { const img = await uploadImage(f); pendingImages.push(img); } + catch (err) { term.writeln('\x1b[31m✗ ' + f.name + ' 上传失败: ' + err.message + '\x1b[0m'); } } - renderImgChips(); + renderPasteImgChips(); }); - // 📎 选择图片文件 - btnAttach.onclick = () => fileInput.click(); - fileInput.onchange = async () => { - if (!fileInput.files || !fileInput.files.length) return; - for (const f of fileInput.files) { + // 📎 选择图片 + pasteAttach.onclick = () => pasteFileInput.click(); + pasteFileInput.onchange = async () => { + if (!pasteFileInput.files || !pasteFileInput.files.length) return; + for (const f of pasteFileInput.files) { if (!f.type || !f.type.startsWith('image/')) continue; - try { - const img = await uploadImage(f); - pendingImages.push(img); - } catch (err) { - term.writeln('\x1b[31m✗ ' + f.name + ' 上传失败: ' + err.message + '\x1b[0m'); - } + try { const img = await uploadImage(f); pendingImages.push(img); } + catch (err) { term.writeln('\x1b[31m✗ ' + f.name + ' 上传失败: ' + err.message + '\x1b[0m'); } } - fileInput.value = ''; - renderImgChips(); + pasteFileInput.value = ''; + renderPasteImgChips(); }; - // ✕ 清空输入 - btnClear.onclick = () => { - msgInput.value = ''; + // ✕ 清空 + pasteClear.onclick = () => { + pasteInput.value = ''; pendingImages = []; - renderImgChips(); - autoResize(); - msgInput.focus(); + renderPasteImgChips(); + pasteInput.focus(); }; - // 发送:文字 + 图片路径标签 - function sendMessage() { + // 发送:把内容字符流发给 PTY(不加 \r),内容显示在 xterm 输入行, + // 用户自己按回车提交。多行内容用 \n 分隔,最后一行留空让用户继续输入。 + function sendPasteContent() { if (!ws || ws.readyState !== WebSocket.OPEN) { term.writeln('\x1b[31m✗ 未连接,无法发送\x1b[0m'); return; } - const text = msgInput.value; - let payload = ''; - // 先发图片标签 + const text = pasteInput.value; + let parts = []; for (const img of pendingImages) { - payload += '' + img.path + '\n'; + parts.push('' + img.path + ''); } - // 再发文字 - if (text) payload += text; - if (!payload) return; - payload += '\n'; // 回车确认提交 - sendInput(payload); - // 清空 - msgInput.value = ''; - pendingImages = []; - renderImgChips(); - autoResize(); - msgInput.focus(); + if (text) parts.push(text); + if (!parts.length) return; + const body = parts.join(' '); + // 只发内容,不带回车:内容进 xterm 输入行,用户自行回车提交 + sendInput(body); + closePasteModal(); } - btnSend.onclick = sendMessage; - msgInput.addEventListener('keydown', (e) => { + pasteSend.onclick = sendPasteContent; + pasteInput.addEventListener('keydown', (e) => { if (e.key === 'Enter' && (e.ctrlKey || e.metaKey)) { e.preventDefault(); - sendMessage(); + sendPasteContent(); + } else if (e.key === 'Escape') { + e.preventDefault(); + closePasteModal(); } }); - function setWsHint(connected) { - wsHint.textContent = connected ? '已连接' : '未连接'; - } - function initTerminal() { if (term) return; term = new Terminal({ @@ -585,8 +555,7 @@ html { font-size: 15px; } html, body { height: 100%; background: #1a1a2e; color: function setConnected(v) { statusDot.className = 'dot ' + (v ? 'connected' : 'disconnected'); statusText.textContent = v ? '已连接' : '未连接'; - setWsHint(v); - if (v) { try { msgInput.focus(); } catch(e){} } + if (v && term && term.focus) { try { term.focus(); } catch(e){} } } function connectWS() {