v1.2.1: 修复交互选项渲染并增强答复输入体验
修复 AskUserQuestion 选项在 Web 端无法展示的问题,补齐前后端结构化数据处理,并支持点击选项一键填充输入框,提升交互效率。
This commit is contained in:
@@ -22,6 +22,9 @@ https://github.com/ZgDaniel/cc-web 给我装!
|
||||
|
||||
## 更新记录
|
||||
|
||||
- **v1.2.1**
|
||||
- 修复 `AskUserQuestion` 交互选项在 Web 端不显示的问题:后端保留完整结构化参数并前端按问题/选项渲染。
|
||||
- 新增交互选项快捷填充:点击选项即可把对应答案插入输入框,便于快速确认并发送。
|
||||
- **v1.2**
|
||||
- 修复消息中包含代码块时可能触发的页面横向溢出问题:窗口不再被长代码撑宽,代码块可在块内横向滚动。
|
||||
- 优化移动端输入体验:手机浏览器回车键默认换行,不再直接发送消息;消息发送改为手动点击发送按钮。
|
||||
|
||||
122
public/app.js
122
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 = `<summary><span class="tool-call-icon done"></span> ${escapeHtml(tc.name)}</summary>
|
||||
<div class="tool-call-content">${escapeHtml(contentText)}</div>`;
|
||||
details.dataset.toolName = tc.name || '';
|
||||
if (tc.name === 'AskUserQuestion') details.open = true;
|
||||
|
||||
const summary = document.createElement('summary');
|
||||
summary.innerHTML = `<span class="tool-call-icon done"></span> ${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 = `
|
||||
<summary><span class="tool-call-icon ${done ? 'done' : 'running'}"></span> ${escapeHtml(name)}</summary>
|
||||
<div class="tool-call-content">${escapeHtml(inputStr)}</div>
|
||||
`;
|
||||
details.dataset.toolName = name || '';
|
||||
if (name === 'AskUserQuestion') details.open = true;
|
||||
|
||||
const summary = document.createElement('summary');
|
||||
summary.innerHTML = `<span class="tool-call-icon ${done ? 'done' : 'running'}"></span> ${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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
26
server.js
26
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();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user