mirror of
https://github.com/qingchencloud/clawpanel.git
synced 2026-07-09 14:31:57 +08:00
feat: IME-aware chat input, message copy button, Git path scanning
- Fix IME composition issue: Enter during Chinese/Japanese/Korean input method composition no longer prematurely sends messages (assistant.js) Uses e.isComposing + keyCode 229 guard on keydown handler - Add one-click copy button to chat message bubbles (both chat.js and assistant.js), with hover-reveal animation and checkmark feedback - Add 'copy' icon to SVG icon library (Lucide style) - Add CSS for msg-copy-btn in chat.css and assistant.css - Implement scan_git_paths Rust command: scans common Git installation locations on Windows/macOS/Linux (Program Files, Scoop, Chocolatey, GitHub Desktop, VS Code, MSYS2, Homebrew, Xcode CLT, etc.) - Register scan_git_paths in lib.rs, tauri-api.js, dev-api.js - Add scan button + results UI in settings.js Git path section - Add i18n keys: gitScan, gitScanning, gitScanEmpty, gitScanUse
This commit is contained in:
@@ -2431,10 +2431,10 @@ function renderMessages() {
|
||||
? `<img class="ast-msg-img" src="${img.dataUrl}" alt="${escHtml(img.name)}" style="max-width:${Math.min(img.width || 300, 300)}px" loading="lazy"/>`
|
||||
: `<div class="ast-msg-img-loading" data-db-id="${img.dbId || ''}"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="24" height="24"><rect x="3" y="3" width="18" height="18" rx="2"/><circle cx="8.5" cy="8.5" r="1.5"/><polyline points="21 15 16 10 5 21"/></svg><span>${escHtml(img.name || t('assistant.image'))}</span></div>`
|
||||
).join('')}</div>` : ''
|
||||
return `<div class="ast-msg ast-msg-user" data-msg-idx="${idx}"><div class="ast-msg-bubble ast-msg-bubble-user">${imagesHtml}${textPart ? escHtml(textPart) : ''}</div></div>`
|
||||
return `<div class="ast-msg ast-msg-user" data-msg-idx="${idx}"><div class="ast-msg-bubble ast-msg-bubble-user">${imagesHtml}${textPart ? escHtml(textPart) : ''}</div><div class="ast-msg-meta"><button class="msg-copy-btn" title="${t('common.copy')}">${icon('copy', 12)}</button></div></div>`
|
||||
} else if (m.role === 'assistant') {
|
||||
const toolHtml = renderToolBlocks(m.toolHistory)
|
||||
return `<div class="ast-msg ast-msg-ai" data-msg-idx="${idx}">${toolHtml}<div class="ast-msg-bubble ast-msg-bubble-ai">${renderMarkdown(m.content)}</div></div>`
|
||||
return `<div class="ast-msg ast-msg-ai" data-msg-idx="${idx}">${toolHtml}<div class="ast-msg-bubble ast-msg-bubble-ai">${renderMarkdown(m.content)}</div><div class="ast-msg-meta"><button class="msg-copy-btn" title="${t('common.copy')}">${icon('copy', 12)}</button></div></div>`
|
||||
}
|
||||
return ''
|
||||
}).join('')
|
||||
@@ -4067,6 +4067,23 @@ export async function render() {
|
||||
|
||||
// ── 事件绑定 ──
|
||||
|
||||
// 复制按钮(事件委托)
|
||||
_messagesEl.addEventListener('click', (e) => {
|
||||
const copyBtn = e.target.closest('.msg-copy-btn')
|
||||
if (!copyBtn) return
|
||||
e.stopPropagation()
|
||||
const msgWrap = copyBtn.closest('.ast-msg')
|
||||
const bubble = msgWrap?.querySelector('.ast-msg-bubble')
|
||||
if (bubble) {
|
||||
const text = bubble.innerText || bubble.textContent || ''
|
||||
navigator.clipboard.writeText(text.trim()).then(() => {
|
||||
copyBtn.classList.add('copied')
|
||||
copyBtn.innerHTML = icon('check', 12)
|
||||
setTimeout(() => { copyBtn.classList.remove('copied'); copyBtn.innerHTML = icon('copy', 12) }, 1500)
|
||||
}).catch(() => {})
|
||||
}
|
||||
})
|
||||
|
||||
// 右键调试菜单(事件委托)
|
||||
_messagesEl.addEventListener('contextmenu', (e) => {
|
||||
const msgEl = e.target.closest('[data-msg-idx]')
|
||||
@@ -4084,9 +4101,9 @@ export async function render() {
|
||||
}
|
||||
})
|
||||
|
||||
// Enter 发送,Shift+Enter 换行
|
||||
// Enter 发送,Shift+Enter 换行;IME 选词中不发送
|
||||
_textarea.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) {
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && e.keyCode !== 229) {
|
||||
e.preventDefault()
|
||||
if (!_textarea.value.trim() && _pendingImages.length === 0) return
|
||||
sendMessage(_textarea.value)
|
||||
|
||||
@@ -394,7 +394,7 @@ function bindEvents(page) {
|
||||
})
|
||||
|
||||
_textarea.addEventListener('keydown', (e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); sendMessage() }
|
||||
if (e.key === 'Enter' && !e.shiftKey && !e.isComposing && e.keyCode !== 229) { e.preventDefault(); sendMessage() }
|
||||
if (e.key === 'Escape') hideCmdPanel()
|
||||
})
|
||||
|
||||
@@ -536,7 +536,24 @@ function bindEvents(page) {
|
||||
_autoScrollEnabled = true
|
||||
scrollToBottom(true)
|
||||
})
|
||||
_messagesEl.addEventListener('click', () => hideCmdPanel())
|
||||
_messagesEl.addEventListener('click', (e) => {
|
||||
const copyBtn = e.target.closest('.msg-copy-btn')
|
||||
if (copyBtn) {
|
||||
e.stopPropagation()
|
||||
const msgWrap = copyBtn.closest('.msg')
|
||||
const bubble = msgWrap?.querySelector('.msg-bubble')
|
||||
if (bubble) {
|
||||
const text = bubble.innerText || bubble.textContent || ''
|
||||
navigator.clipboard.writeText(text.trim()).then(() => {
|
||||
copyBtn.classList.add('copied')
|
||||
copyBtn.innerHTML = svgIcon('check', 12)
|
||||
setTimeout(() => { copyBtn.classList.remove('copied'); copyBtn.innerHTML = svgIcon('copy', 12) }, 1500)
|
||||
}).catch(() => {})
|
||||
}
|
||||
return
|
||||
}
|
||||
hideCmdPanel()
|
||||
})
|
||||
}
|
||||
|
||||
async function loadModelOptions(showToast = false) {
|
||||
@@ -1733,6 +1750,7 @@ function handleChatEvent(payload) {
|
||||
parts.push(`<span class="meta-sep">·</span><span class="msg-tokens">${tokenStr}</span>`)
|
||||
}
|
||||
}
|
||||
parts.push(`<button class="msg-copy-btn" title="${t('common.copy')}">${svgIcon('copy', 12)}</button>`)
|
||||
meta.innerHTML = parts.join('')
|
||||
wrapper.appendChild(meta)
|
||||
}
|
||||
@@ -2335,7 +2353,7 @@ function appendUserMessage(text, attachments = [], msgTime) {
|
||||
|
||||
const meta = document.createElement('div')
|
||||
meta.className = 'msg-meta'
|
||||
meta.innerHTML = `<span class="msg-time">${formatTime(msgTime || new Date())}</span>`
|
||||
meta.innerHTML = `<span class="msg-time">${formatTime(msgTime || new Date())}</span><button class="msg-copy-btn" title="${t('common.copy')}">${svgIcon('copy', 12)}</button>`
|
||||
|
||||
wrap.appendChild(bubble)
|
||||
wrap.appendChild(meta)
|
||||
@@ -2362,7 +2380,7 @@ function appendAiMessage(text, msgTime, images, videos, audios, files, tools) {
|
||||
|
||||
const meta = document.createElement('div')
|
||||
meta.className = 'msg-meta'
|
||||
meta.innerHTML = `<span class="msg-time">${formatTime(msgTime || new Date())}</span>`
|
||||
meta.innerHTML = `<span class="msg-time">${formatTime(msgTime || new Date())}</span><button class="msg-copy-btn" title="${t('common.copy')}">${svgIcon('copy', 12)}</button>`
|
||||
|
||||
wrap.appendChild(bubble)
|
||||
wrap.appendChild(meta)
|
||||
|
||||
@@ -471,6 +471,13 @@ function bindEvents(page) {
|
||||
case 'reset-git-path':
|
||||
await handleResetGitPath(page)
|
||||
break
|
||||
case 'scan-git-paths':
|
||||
await handleScanGitPaths(page)
|
||||
break
|
||||
case 'use-scanned-git':
|
||||
page.querySelector('[data-name="git-path"]').value = btn.dataset.gitPath || ''
|
||||
await handleSaveGitPath(page)
|
||||
break
|
||||
case 'bind-cli':
|
||||
await handleBindCli(page, btn.dataset.path)
|
||||
break
|
||||
@@ -587,7 +594,9 @@ async function loadGitPath(page) {
|
||||
<input class="input" data-name="git-path" value="${escapeHtml(customValue)}" placeholder="${t('settings.gitPathPlaceholder')}" style="flex:1;min-width:200px">
|
||||
<button class="btn btn-primary btn-sm" data-action="save-git-path">${t('common.save')}</button>
|
||||
<button class="btn btn-secondary btn-sm" data-action="reset-git-path">${t('settings.resetDefault')}</button>
|
||||
<button class="btn btn-secondary btn-sm" data-action="scan-git-paths">${t('settings.gitScan')}</button>
|
||||
</div>
|
||||
<div id="git-scan-results"></div>
|
||||
</div>`
|
||||
} catch (e) {
|
||||
bar.innerHTML = `<div class="stat-card" style="padding:16px;color:var(--error)">${e}</div>`
|
||||
@@ -613,6 +622,29 @@ async function handleSaveGitPath(page) {
|
||||
await loadGitPath(page)
|
||||
}
|
||||
|
||||
async function handleScanGitPaths(page) {
|
||||
const container = page.querySelector('#git-scan-results')
|
||||
if (!container) return
|
||||
container.innerHTML = `<div style="margin-top:10px;font-size:12px;color:var(--text-secondary)">${t('settings.gitScanning')}</div>`
|
||||
try {
|
||||
const results = await api.scanGitPaths()
|
||||
if (!results || results.length === 0) {
|
||||
container.innerHTML = `<div style="margin-top:10px;font-size:12px;color:var(--text-tertiary)">${t('settings.gitScanEmpty')}</div>`
|
||||
return
|
||||
}
|
||||
container.innerHTML = `<div style="margin-top:10px;display:flex;flex-direction:column;gap:6px">${results.map(r =>
|
||||
`<div style="display:flex;align-items:center;gap:8px;font-size:12px;padding:6px 8px;background:var(--bg-tertiary);border-radius:var(--radius-sm)">
|
||||
<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap" title="${escapeHtml(r.path)}">${escapeHtml(r.path)}</span>
|
||||
<span style="color:var(--text-tertiary);flex-shrink:0">${escapeHtml(r.version || '')}</span>
|
||||
<span class="badge" style="font-size:10px;flex-shrink:0">${escapeHtml(r.source)}</span>
|
||||
<button class="btn btn-primary btn-sm" style="padding:2px 8px;font-size:11px" data-action="use-scanned-git" data-git-path="${escapeHtml(r.path)}">${t('settings.gitScanUse')}</button>
|
||||
</div>`
|
||||
).join('')}</div>`
|
||||
} catch (e) {
|
||||
container.innerHTML = `<div style="margin-top:10px;font-size:12px;color:var(--error)">${e}</div>`
|
||||
}
|
||||
}
|
||||
|
||||
async function handleResetGitPath(page) {
|
||||
const cfg = await api.readPanelConfig()
|
||||
delete cfg.gitPath
|
||||
|
||||
Reference in New Issue
Block a user