diff --git a/README.md b/README.md index 2a0ab72..926f57a 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,9 @@ https://github.com/ZgDaniel/cc-web 给我装! ## 更新记录 +- **v1.2.1** + - 修复 `AskUserQuestion` 交互选项在 Web 端不显示的问题:后端保留完整结构化参数并前端按问题/选项渲染。 + - 新增交互选项快捷填充:点击选项即可把对应答案插入输入框,便于快速确认并发送。 - **v1.2** - 修复消息中包含代码块时可能触发的页面横向溢出问题:窗口不再被长代码撑宽,代码块可在块内横向滚动。 - 优化移动端输入体验:手机浏览器回车键默认换行,不再直接发送消息;消息发送改为手动点击发送按钮。 diff --git a/public/app.js b/public/app.js index 81f3fb0..acac859 100644 --- a/public/app.js +++ b/public/app.js @@ -410,9 +410,16 @@ for (const tc of m.toolCalls) { const details = document.createElement('details'); details.className = 'tool-call'; - const contentText = tc.result || (typeof tc.input === 'string' ? tc.input : (tc.input ? JSON.stringify(tc.input, null, 2) : '')); - details.innerHTML = ` ${escapeHtml(tc.name)} -
${escapeHtml(contentText)}
`; + details.dataset.toolName = tc.name || ''; + if (tc.name === 'AskUserQuestion') details.open = true; + + const summary = document.createElement('summary'); + summary.innerHTML = ` ${escapeHtml(tc.name)}`; + details.appendChild(summary); + + const displayInput = tc.name === 'AskUserQuestion' ? tc.input : (tc.result || tc.input); + details.appendChild(buildToolContentElement(tc.name, displayInput)); + bubble.insertBefore(details, bubble.firstChild); } } @@ -421,6 +428,99 @@ scrollToBottom(); } + function normalizeAskUserInput(input) { + if (input === null || input === undefined) return null; + if (typeof input === 'string') { + const trimmed = input.trim(); + if (!trimmed) return null; + try { + return JSON.parse(trimmed); + } catch { + return null; + } + } + return input; + } + + function extractAskUserQuestions(input) { + const parsed = normalizeAskUserInput(input); + if (!parsed || !Array.isArray(parsed.questions)) return []; + return parsed.questions; + } + + function appendAskOptionToInput(question, option) { + const header = (question?.header || '').trim() || '问题'; + const line = `【${header}】${option?.label || ''}`; + const current = msgInput.value.trim(); + msgInput.value = current ? `${current}\n${line}` : line; + autoResize(); + msgInput.focus(); + } + + function createAskUserQuestionView(questions) { + const wrapper = document.createElement('div'); + wrapper.className = 'ask-user-question'; + + questions.forEach((q, idx) => { + const card = document.createElement('div'); + card.className = 'ask-question-card'; + + const header = document.createElement('div'); + header.className = 'ask-question-header'; + header.textContent = `${idx + 1}. ${q.header || '问题'}`; + card.appendChild(header); + + const body = document.createElement('div'); + body.className = 'ask-question-text'; + body.textContent = q.question || ''; + card.appendChild(body); + + if (Array.isArray(q.options) && q.options.length > 0) { + const opts = document.createElement('div'); + opts.className = 'ask-question-options'; + q.options.forEach((opt, i) => { + const item = document.createElement('button'); + item.type = 'button'; + item.className = 'ask-option-item'; + item.addEventListener('click', () => appendAskOptionToInput(q, opt)); + + const title = document.createElement('div'); + title.className = 'ask-option-label'; + title.textContent = `${i + 1}. ${opt.label || ''}`; + item.appendChild(title); + + if (opt.description) { + const desc = document.createElement('div'); + desc.className = 'ask-option-desc'; + desc.textContent = opt.description; + item.appendChild(desc); + } + + opts.appendChild(item); + }); + card.appendChild(opts); + } + + wrapper.appendChild(card); + }); + + return wrapper; + } + + function buildToolContentElement(name, input) { + if (name === 'AskUserQuestion') { + const questions = extractAskUserQuestions(input); + if (questions.length > 0) { + return createAskUserQuestionView(questions); + } + } + const inputStr = typeof input === 'string' ? input : (input ? JSON.stringify(input, null, 2) : ''); + const content = document.createElement('div'); + content.className = 'tool-call-content'; + content.textContent = inputStr; + return content; + } + function appendToolCall(toolUseId, name, input, done) { const streamEl = document.getElementById('streaming-msg'); if (!streamEl) return; @@ -430,11 +530,14 @@ const details = document.createElement('details'); details.className = 'tool-call'; details.id = `tool-${toolUseId}`; - const inputStr = typeof input === 'string' ? input : (input ? JSON.stringify(input, null, 2) : ''); - details.innerHTML = ` - ${escapeHtml(name)} -
${escapeHtml(inputStr)}
- `; + details.dataset.toolName = name || ''; + if (name === 'AskUserQuestion') details.open = true; + + const summary = document.createElement('summary'); + summary.innerHTML = ` ${escapeHtml(name)}`; + details.appendChild(summary); + details.appendChild(buildToolContentElement(name, input)); + bubble.appendChild(details); scrollToBottom(); } @@ -445,6 +548,9 @@ const icon = el.querySelector('.tool-call-icon'); if (icon) { icon.classList.remove('running'); icon.classList.add('done'); } if (result) { + if (el.dataset.toolName === 'AskUserQuestion') { + return; + } const content = el.querySelector('.tool-call-content'); if (content) content.textContent = result; } diff --git a/public/style.css b/public/style.css index 4b1edc6..3b5fb83 100644 --- a/public/style.css +++ b/public/style.css @@ -571,6 +571,63 @@ body { font-family: 'SF Mono', monospace; } +/* AskUserQuestion preview */ +.ask-user-question { + padding: 10px 12px; + background: var(--bg-primary); + display: flex; + flex-direction: column; + gap: 10px; +} +.ask-question-card { + border: 1px solid var(--border-color); + border-radius: 10px; + background: #fff; + padding: 10px; +} +.ask-question-header { + font-size: 12px; + font-weight: 700; + color: var(--accent); + margin-bottom: 6px; +} +.ask-question-text { + font-size: 13px; + line-height: 1.5; + color: var(--text-primary); + margin-bottom: 8px; +} +.ask-question-options { + display: flex; + flex-direction: column; + gap: 6px; +} +.ask-option-item { + border: 1px solid var(--border-color); + border-radius: 8px; + padding: 8px; + background: var(--bg-secondary); + width: 100%; + text-align: left; + cursor: pointer; + transition: background 0.12s, border-color 0.12s; +} +.ask-option-item:hover { + background: var(--accent-light); + border-color: var(--accent); +} +.ask-option-label { + font-size: 12px; + font-weight: 600; + color: var(--text-primary); +} +.ask-option-desc { + font-size: 12px; + color: var(--text-secondary); + line-height: 1.45; + margin-top: 4px; +} + /* Typing indicator */ .typing-indicator { display: inline-flex; diff --git a/server.js b/server.js index 7a1a090..2797d87 100644 --- a/server.js +++ b/server.js @@ -1155,7 +1155,8 @@ function processClaudeEvent(entry, event, sessionId) { entry.fullText += block.text; wsSend(entry.ws, { type: 'text_delta', text: block.text }); } else if (block.type === 'tool_use') { - const tc = { name: block.name, id: block.id, input: truncateObj(block.input, 500), done: false }; + const toolInput = sanitizeToolInput(block.name, block.input); + const tc = { name: block.name, id: block.id, input: toolInput, done: false }; entry.toolCalls.push(tc); wsSend(entry.ws, { type: 'tool_start', name: block.name, toolUseId: block.id, input: tc.input }); } else if (block.type === 'tool_result') { @@ -1202,6 +1203,29 @@ function truncateObj(obj, maxLen) { return s.slice(0, maxLen) + '...'; } +function safeJsonParse(input) { + if (input === null || input === undefined) return input; + if (typeof input !== 'string') return input; + const trimmed = input.trim(); + if (!trimmed) return input; + if (!((trimmed.startsWith('{') && trimmed.endsWith('}')) || (trimmed.startsWith('[') && trimmed.endsWith(']')))) { + return input; + } + try { + return JSON.parse(trimmed); + } catch { + return input; + } +} + +function sanitizeToolInput(toolName, input) { + const parsed = safeJsonParse(input); + if (toolName === 'AskUserQuestion') { + return parsed; + } + return truncateObj(parsed, 500); +} + // === Startup === recoverProcesses();