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**
|
- **v1.2**
|
||||||
- 修复消息中包含代码块时可能触发的页面横向溢出问题:窗口不再被长代码撑宽,代码块可在块内横向滚动。
|
- 修复消息中包含代码块时可能触发的页面横向溢出问题:窗口不再被长代码撑宽,代码块可在块内横向滚动。
|
||||||
- 优化移动端输入体验:手机浏览器回车键默认换行,不再直接发送消息;消息发送改为手动点击发送按钮。
|
- 优化移动端输入体验:手机浏览器回车键默认换行,不再直接发送消息;消息发送改为手动点击发送按钮。
|
||||||
|
|||||||
122
public/app.js
122
public/app.js
@@ -410,9 +410,16 @@
|
|||||||
for (const tc of m.toolCalls) {
|
for (const tc of m.toolCalls) {
|
||||||
const details = document.createElement('details');
|
const details = document.createElement('details');
|
||||||
details.className = 'tool-call';
|
details.className = 'tool-call';
|
||||||
const contentText = tc.result || (typeof tc.input === 'string' ? tc.input : (tc.input ? JSON.stringify(tc.input, null, 2) : ''));
|
details.dataset.toolName = tc.name || '';
|
||||||
details.innerHTML = `<summary><span class="tool-call-icon done"></span> ${escapeHtml(tc.name)}</summary>
|
if (tc.name === 'AskUserQuestion') details.open = true;
|
||||||
<div class="tool-call-content">${escapeHtml(contentText)}</div>`;
|
|
||||||
|
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);
|
bubble.insertBefore(details, bubble.firstChild);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -421,6 +428,99 @@
|
|||||||
scrollToBottom();
|
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) {
|
function appendToolCall(toolUseId, name, input, done) {
|
||||||
const streamEl = document.getElementById('streaming-msg');
|
const streamEl = document.getElementById('streaming-msg');
|
||||||
if (!streamEl) return;
|
if (!streamEl) return;
|
||||||
@@ -430,11 +530,14 @@
|
|||||||
const details = document.createElement('details');
|
const details = document.createElement('details');
|
||||||
details.className = 'tool-call';
|
details.className = 'tool-call';
|
||||||
details.id = `tool-${toolUseId}`;
|
details.id = `tool-${toolUseId}`;
|
||||||
const inputStr = typeof input === 'string' ? input : (input ? JSON.stringify(input, null, 2) : '');
|
details.dataset.toolName = name || '';
|
||||||
details.innerHTML = `
|
if (name === 'AskUserQuestion') details.open = true;
|
||||||
<summary><span class="tool-call-icon ${done ? 'done' : 'running'}"></span> ${escapeHtml(name)}</summary>
|
|
||||||
<div class="tool-call-content">${escapeHtml(inputStr)}</div>
|
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);
|
bubble.appendChild(details);
|
||||||
scrollToBottom();
|
scrollToBottom();
|
||||||
}
|
}
|
||||||
@@ -445,6 +548,9 @@
|
|||||||
const icon = el.querySelector('.tool-call-icon');
|
const icon = el.querySelector('.tool-call-icon');
|
||||||
if (icon) { icon.classList.remove('running'); icon.classList.add('done'); }
|
if (icon) { icon.classList.remove('running'); icon.classList.add('done'); }
|
||||||
if (result) {
|
if (result) {
|
||||||
|
if (el.dataset.toolName === 'AskUserQuestion') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
const content = el.querySelector('.tool-call-content');
|
const content = el.querySelector('.tool-call-content');
|
||||||
if (content) content.textContent = result;
|
if (content) content.textContent = result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -571,6 +571,63 @@ body {
|
|||||||
font-family: 'SF Mono', monospace;
|
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 */
|
||||||
.typing-indicator {
|
.typing-indicator {
|
||||||
display: inline-flex;
|
display: inline-flex;
|
||||||
|
|||||||
26
server.js
26
server.js
@@ -1155,7 +1155,8 @@ function processClaudeEvent(entry, event, sessionId) {
|
|||||||
entry.fullText += block.text;
|
entry.fullText += block.text;
|
||||||
wsSend(entry.ws, { type: 'text_delta', text: block.text });
|
wsSend(entry.ws, { type: 'text_delta', text: block.text });
|
||||||
} else if (block.type === 'tool_use') {
|
} 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);
|
entry.toolCalls.push(tc);
|
||||||
wsSend(entry.ws, { type: 'tool_start', name: block.name, toolUseId: block.id, input: tc.input });
|
wsSend(entry.ws, { type: 'tool_start', name: block.name, toolUseId: block.id, input: tc.input });
|
||||||
} else if (block.type === 'tool_result') {
|
} else if (block.type === 'tool_result') {
|
||||||
@@ -1202,6 +1203,29 @@ function truncateObj(obj, maxLen) {
|
|||||||
return s.slice(0, 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 ===
|
// === Startup ===
|
||||||
recoverProcesses();
|
recoverProcesses();
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user