v1.2.1: 修复交互选项渲染并增强答复输入体验

修复 AskUserQuestion 选项在 Web 端无法展示的问题,补齐前后端结构化数据处理,并支持点击选项一键填充输入框,提升交互效率。
This commit is contained in:
Daniel
2026-03-09 12:36:04 +00:00
parent e3337c8d1b
commit b24a3c74b2
4 changed files with 199 additions and 9 deletions

View File

@@ -22,6 +22,9 @@ https://github.com/ZgDaniel/cc-web 给我装!
## 更新记录
- **v1.2.1**
- 修复 `AskUserQuestion` 交互选项在 Web 端不显示的问题:后端保留完整结构化参数并前端按问题/选项渲染。
- 新增交互选项快捷填充:点击选项即可把对应答案插入输入框,便于快速确认并发送。
- **v1.2**
- 修复消息中包含代码块时可能触发的页面横向溢出问题:窗口不再被长代码撑宽,代码块可在块内横向滚动。
- 优化移动端输入体验:手机浏览器回车键默认换行,不再直接发送消息;消息发送改为手动点击发送按钮。

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -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();