From 57d07312497fec2e0ed12994a60a9075033402bd Mon Sep 17 00:00:00 2001 From: wsp1911 Date: Wed, 25 Mar 2026 17:33:10 +0800 Subject: [PATCH] fix(toolcard): stream AskUserQuestion card params; avoid false parse error --- .../tool-cards/AskUserQuestionCard.scss | 13 +++ .../tool-cards/AskUserQuestionCard.tsx | 93 +++++++++++++------ src/web-ui/src/locales/en-US/flow-chat.json | 1 + src/web-ui/src/locales/zh-CN/flow-chat.json | 1 + 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.scss b/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.scss index 5ddc796d..d2d5f571 100644 --- a/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.scss +++ b/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.scss @@ -101,6 +101,19 @@ color: var(--color-warning); } +.params-loading-row { + display: flex; + align-items: center; + gap: 8px; + padding: 12px 14px; + font-size: 12px; + color: var(--color-text-secondary); +} + +.params-loading-text { + font-weight: 500; +} + /* ========== Question container ========== */ .questions-container { display: flex; diff --git a/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx b/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx index ba54a25e..5106fa41 100644 --- a/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx +++ b/src/web-ui/src/flow_chat/tool-cards/AskUserQuestionCard.tsx @@ -6,7 +6,7 @@ import React, { useState, useCallback, useMemo, useLayoutEffect, useRef } from 'react'; import { Loader2, AlertCircle, Send, ChevronDown, ChevronRight } from 'lucide-react'; import { useTranslation } from 'react-i18next'; -import type { ToolCardProps } from '../types/flow-chat'; +import type { FlowToolItem, ToolCardProps } from '../types/flow-chat'; import { toolAPI } from '@/infrastructure/api/service-api/ToolAPI'; import { createLogger } from '@/shared/utils/logger'; import { Button } from '@/component-library'; @@ -27,29 +27,53 @@ interface QuestionData { multiSelect: boolean; } +function normalizeQuestionsFromParams(input: unknown): QuestionData[] { + if (!input || typeof input !== 'object') return []; + const raw = input as Record; + const qs = raw.questions; + if (!Array.isArray(qs)) return []; + return qs.map((q: any) => ({ + question: q.question || '', + header: q.header || '', + options: Array.isArray(q.options) ? q.options : [], + multiSelect: Boolean(q.multiSelect), + })); +} + +/** Same source as FileOperationToolCard: partial JSON while streaming, then final toolCall.input. */ +function isAwaitingQuestionPayload( + questionsLength: number, + isParamsStreaming: boolean | undefined, + status: FlowToolItem['status'] +): boolean { + if (questionsLength > 0) return false; + if (isParamsStreaming) return true; + const s = status as string; + return ( + status === 'preparing' || + status === 'streaming' || + status === 'pending' || + s === 'receiving' + ); +} + export const AskUserQuestionCard: React.FC = ({ toolItem }) => { const { t } = useTranslation('flow-chat'); - const { status, toolCall, toolResult } = toolItem; - - const getQuestions = (): QuestionData[] => { - if (!toolCall?.input) return []; - const input = toolCall.input; - - if (input.questions && Array.isArray(input.questions)) { - return input.questions.map((q: any) => ({ - question: q.question || '', - header: q.header || '', - options: q.options || [], - multiSelect: q.multiSelect || false - })); - } - - return []; - }; + const { status, toolCall, toolResult, isParamsStreaming, partialParams } = toolItem; + + const paramsSource = partialParams || toolCall?.input; + const questions = useMemo( + () => normalizeQuestionsFromParams(paramsSource), + [partialParams, toolCall?.input] + ); - const questions = useMemo(() => getQuestions(), [toolCall?.input]); + const awaitingPayload = isAwaitingQuestionPayload( + questions.length, + isParamsStreaming, + status + ); const [answers, setAnswers] = useState>({}); const [otherInputs, setOtherInputs] = useState>({}); @@ -199,7 +223,7 @@ export const AskUserQuestionCard: React.FC = ({ value={option.label} checked={Array.isArray(answer) && answer.includes(option.label)} onChange={(e) => handleMultiChange(questionIndex, option.label, e.target.checked)} - disabled={isSubmitted || status === 'completed'} + disabled={isSubmitted || status === 'completed' || Boolean(isParamsStreaming)} /> @@ -211,7 +235,7 @@ export const AskUserQuestionCard: React.FC = ({ value={option.label} checked={answer === option.label} onChange={(e) => handleSingleChange(questionIndex, e.target.value)} - disabled={isSubmitted || status === 'completed'} + disabled={isSubmitted || status === 'completed' || Boolean(isParamsStreaming)} /> @@ -237,7 +261,7 @@ export const AskUserQuestionCard: React.FC = ({ handleMultiChange(questionIndex, 'Other', true); } }} - disabled={isSubmitted || status === 'completed'} + disabled={isSubmitted || status === 'completed' || Boolean(isParamsStreaming)} /> @@ -249,7 +273,7 @@ export const AskUserQuestionCard: React.FC = ({ value="Other" checked={false} onChange={() => handleSingleChange(questionIndex, 'Other')} - disabled={isSubmitted || status === 'completed'} + disabled={isSubmitted || status === 'completed' || Boolean(isParamsStreaming)} /> @@ -273,7 +297,7 @@ export const AskUserQuestionCard: React.FC = ({ handleMultiChange(questionIndex, 'Other', false); } }} - disabled={isSubmitted || status === 'completed'} + disabled={isSubmitted || status === 'completed' || Boolean(isParamsStreaming)} /> @@ -285,7 +309,7 @@ export const AskUserQuestionCard: React.FC = ({ value="Other" checked={true} onChange={() => {}} - disabled={isSubmitted || status === 'completed'} + disabled={isSubmitted || status === 'completed' || Boolean(isParamsStreaming)} /> @@ -296,7 +320,7 @@ export const AskUserQuestionCard: React.FC = ({ placeholder={t('toolCards.askUser.pleaseSpecify')} value={otherInput} onChange={(e) => handleOtherInputChange(questionIndex, e.target.value)} - disabled={isSubmitted || status === 'completed'} + disabled={isSubmitted || status === 'completed' || Boolean(isParamsStreaming)} autoFocus /> @@ -356,6 +380,21 @@ export const AskUserQuestionCard: React.FC = ({ return null; }; + if (awaitingPayload) { + return ( +
+
+ + {t('toolCards.askUser.loadingQuestions')} +
+
+ ); + } + if (questions.length === 0) { return (
@@ -382,7 +421,7 @@ export const AskUserQuestionCard: React.FC = ({ size="small" className="submit-button" onClick={handleSubmit} - disabled={!isAllAnswered() || isSubmitting} + disabled={!isAllAnswered() || isSubmitting || Boolean(isParamsStreaming)} isLoading={isSubmitting} title={!isAllAnswered() ? t('toolCards.askUser.answerAllBeforeSubmit') : ""} > diff --git a/src/web-ui/src/locales/en-US/flow-chat.json b/src/web-ui/src/locales/en-US/flow-chat.json index 441bdf3a..c4366564 100644 --- a/src/web-ui/src/locales/en-US/flow-chat.json +++ b/src/web-ui/src/locales/en-US/flow-chat.json @@ -494,6 +494,7 @@ "notAnswered": "Not answered", "timeout": "Timeout (10 minutes)", "parseError": "Unable to parse question data", + "loadingQuestions": "Receiving questions…", "other": "Other", "customInputHint": "Provide custom text input", "pleaseSpecify": "Please specify..." diff --git a/src/web-ui/src/locales/zh-CN/flow-chat.json b/src/web-ui/src/locales/zh-CN/flow-chat.json index 4890e157..2721c5e2 100644 --- a/src/web-ui/src/locales/zh-CN/flow-chat.json +++ b/src/web-ui/src/locales/zh-CN/flow-chat.json @@ -486,6 +486,7 @@ "notAnswered": "未回答", "timeout": "等待超时(10分钟)", "parseError": "无法解析问题数据", + "loadingQuestions": "正在接收问题…", "other": "其他", "customInputHint": "提供自定义文本", "pleaseSpecify": "请输入..."