fix(chat): 澄清问题卡片 UX 优化 — 去悬空引用 + 默认展开
Some checks failed
CI / Lint & TypeCheck (push) Has been cancelled
CI / Unit Tests (push) Has been cancelled
CI / Build Frontend (push) Has been cancelled
CI / Rust Check (push) Has been cancelled
CI / Security Scan (push) Has been cancelled
CI / E2E Tests (push) Has been cancelled

- 提示词增加 ask_clarification 引用规则,避免 LLM 在文本中生成
  "以下信息"/"比如:"等悬空引用短语
- 新增 stripDanglingClarificationRef 前端安全网,当消息包含
  ask_clarification 工具调用时自动移除末尾悬空引用
- 澄清卡片默认展开,让用户直接看到选项无需额外点击
This commit is contained in:
iven
2026-04-23 19:21:10 +08:00
parent d7dbdf8600
commit 10497362bb
3 changed files with 29 additions and 2 deletions

View File

@@ -426,6 +426,7 @@ impl Kernel {
prompt.push_str("- Provide clear options when possible\n");
prompt.push_str("- Include brief context about why you're asking\n");
prompt.push_str("- After receiving clarification, proceed immediately\n");
prompt.push_str("- CRITICAL: When calling ask_clarification, do NOT repeat the options in your text response. The options will be shown in a dedicated card above your reply. Simply greet the user and briefly explain why you need clarification — avoid phrases like \"以下信息\" or \"the following options\" that imply a list follows in your text\n");
prompt
}

View File

@@ -665,6 +665,28 @@ function stripToolNarration(content: string): string {
return result || content;
}
/**
* Strip dangling clarification references from text when ask_clarification tool was called.
* When the LLM calls ask_clarification, it often ends its text with phrases like
* "比如:" / "以下信息" / "以下选项" that reference the tool output — but the tool output
* is rendered in a separate ClarificationCard, so these become confusing dead-end sentences.
*/
function stripDanglingClarificationRef(text: string, hasClarificationTool: boolean): string {
if (!hasClarificationTool || !text) return text;
// Match trailing dangling references in Chinese and English
const patterns = [
/[,]\s*可以(?:提供以下|告诉我更多细节,)?(?:信息|选项|方向|细节|分类|类型)[:]\s*$/,
/[,]\s*比如[:]\s*$/,
/[,]\s*(?:例如|譬如|如以下)[:]\s*$/,
/,\s*(?:for example|such as|like|the following)[:]?\s*$/i,
];
for (const pat of patterns) {
const stripped = text.replace(pat, '');
if (stripped !== text) return stripped;
}
return text;
}
function MessageBubble({ message, onRetry }: { message: Message; setInput?: (text: string) => void; onRetry?: () => void }) {
if (message.role === 'tool') {
return null;
@@ -749,7 +771,10 @@ function MessageBubble({ message, onRetry }: { message: Message; setInput?: (tex
? (isUser
? message.content
: <StreamingText
content={stripToolNarration(message.content)}
content={stripDanglingClarificationRef(
stripToolNarration(message.content),
toolCallSteps?.some(s => s.toolName === 'ask_clarification') ?? false,
)}
isStreaming={!!message.streaming}
className="text-gray-700 dark:text-gray-200"
/>

View File

@@ -166,7 +166,8 @@ interface ToolStepRowProps {
}
function ToolStepRow({ step, isActive, showConnector }: ToolStepRowProps) {
const [expanded, setExpanded] = useState(false);
// Clarification cards default to expanded so users see options immediately
const [expanded, setExpanded] = useState(step.toolName === 'ask_clarification');
const Icon = getToolIcon(step.toolName);
const label = getToolLabel(step.toolName);
const isRunning = step.status === 'running';