From c4249f94deffe6923c0beff57c02fa9b9e23ebf0 Mon Sep 17 00:00:00 2001 From: zhsama Date: Wed, 21 Jan 2026 20:49:12 +0800 Subject: [PATCH] feat: Add suggested questions to context generate modal --- .../components/code-generator-button.tsx | 16 +- .../context-generate-modal/index.tsx | 151 ++++++++++++++++-- .../mixed-variable-text-input/index.tsx | 7 + web/i18n/en-US/workflow.json | 1 + web/i18n/ja-JP/workflow.json | 7 + web/i18n/zh-Hans/workflow.json | 19 +++ web/i18n/zh-Hant/workflow.json | 7 + web/service/debug.ts | 29 ++++ 8 files changed, 222 insertions(+), 15 deletions(-) diff --git a/web/app/components/workflow/nodes/_base/components/code-generator-button.tsx b/web/app/components/workflow/nodes/_base/components/code-generator-button.tsx index b022e3fac9..df5cb55b79 100644 --- a/web/app/components/workflow/nodes/_base/components/code-generator-button.tsx +++ b/web/app/components/workflow/nodes/_base/components/code-generator-button.tsx @@ -1,10 +1,11 @@ 'use client' import type { FC } from 'react' import type { CodeLanguage } from '../../code/types' +import type { ContextGenerateModalHandle } from '../../tool/components/context-generate-modal' import type { GenRes } from '@/service/debug' import { useBoolean } from 'ahooks' import * as React from 'react' -import { useCallback, useMemo } from 'react' +import { useCallback, useMemo, useRef } from 'react' import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res' import { ActionButton } from '@/app/components/base/action-button' import { Generator } from '@/app/components/base/icons/src/vender/other' @@ -32,6 +33,7 @@ const CodeGenerateBtn: FC = ({ }) => { const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false) const nodes = useStore(s => s.nodes) + const contextGenerateModalRef = useRef(null) const handleAutomaticRes = useCallback((res: GenRes) => { onGenerated?.(res.modified) showAutomaticFalse() @@ -64,11 +66,20 @@ const CodeGenerateBtn: FC = ({ } }, [nodeId, nodes, parseExtractorNodeId]) + const handleOpenAutomatic = useCallback(() => { + showAutomaticTrue() + if (!contextGenerateConfig) + return + setTimeout(() => { + contextGenerateModalRef.current?.onOpen() + }, 0) + }, [contextGenerateConfig, showAutomaticTrue]) + return (
@@ -76,6 +87,7 @@ const CodeGenerateBtn: FC = ({ contextGenerateConfig ? ( void +} + const minCodeHeight = 80 const minOutputHeight = 80 const splitHandleHeight = 4 @@ -93,13 +97,13 @@ const mapOutputsToResponse = (outputs?: OutputVar) => { return next } -const ContextGenerateModal: FC = ({ +const ContextGenerateModal = forwardRef(({ isShow, onClose, toolNodeId, paramKey, codeNodeId, -}) => { +}, ref) => { const { t, i18n } = useTranslation() const configsMap = useHooksStore(s => s.configsMap) const nodes = useStore(s => s.nodes) @@ -149,7 +153,22 @@ const ContextGenerateModal: FC = ({ { defaultValue: [] }, ) + const [suggestedQuestions, setSuggestedQuestions] = useSessionStorageState( + `${storageKey}-suggested-questions`, + { defaultValue: [] }, + ) + const [hasFetchedSuggestions, setHasFetchedSuggestions] = useSessionStorageState( + `${storageKey}-suggested-questions-fetched`, + { defaultValue: false }, + ) + const [isFetchingSuggestions, { setTrue: setFetchingSuggestionsTrue, setFalse: setFetchingSuggestionsFalse }] = useBoolean(false) + const suggestedQuestionsAbortControllerRef = useRef(null) + const language = useMemo(() => (i18n.language || 'en-US').replace('-', '_'), [i18n.language]) + const promptLanguage = useMemo(() => { + const matched = languages.find(item => item.value === i18n.language) + return matched?.prompt_name || 'English' + }, [i18n.language]) const [inputValue, setInputValue] = useState('') const [isGenerating, { setTrue: setGeneratingTrue, setFalse: setGeneratingFalse }] = useBoolean(false) const [modelOverride, setModelOverride] = useState(() => { @@ -213,6 +232,8 @@ const ContextGenerateModal: FC = ({ const hasHistory = (versions?.length ?? 0) > 0 || promptMessageCount > 0 const isInitView = !isGenerating && !hasHistory const defaultAssistantMessage = t('nodes.tool.contextGenerate.defaultAssistantMessage', { ns: 'workflow' }) + const shouldShowSuggestedSkeleton = isInitView && !hasFetchedSuggestions + const suggestedQuestionsSafe = suggestedQuestions ?? [] const suggestedSkeletonItems = useMemo(() => ([ 0, 1, @@ -255,6 +276,95 @@ const ContextGenerateModal: FC = ({ clearVersions() }, [clearVersions, isGenerating, setPromptMessages]) + const handleSuggestedQuestionClick = useCallback((question: string) => { + setInputValue(question) + }, []) + + const handleFetchSuggestedQuestions = useCallback(async () => { + if (!flowId || !toolNodeId || !paramKey) + return + if (!model.name || !model.provider) + return + if (hasFetchedSuggestions || isFetchingSuggestions || !isInitView) + return + + setFetchingSuggestionsTrue() + let shouldMarkFetched = true + suggestedQuestionsAbortControllerRef.current?.abort() + try { + const response = await fetchContextGenerateSuggestedQuestions({ + workflow_id: flowId, + node_id: toolNodeId, + parameter_name: paramKey, + language: promptLanguage, + model_config: { + provider: model.provider, + name: model.name, + completion_params: model.completion_params, + }, + }, (abortController) => { + suggestedQuestionsAbortControllerRef.current = abortController + }) + + if (response.error) { + shouldMarkFetched = false + Toast.notify({ + type: 'error', + message: t('modal.errors.networkError', { ns: 'pluginTrigger' }), + }) + setSuggestedQuestions([]) + return + } + + const nextQuestions = (response.questions || []).filter(question => question && question.trim()) + setSuggestedQuestions(nextQuestions) + } + catch (error) { + if (String(error).includes('AbortError')) { + shouldMarkFetched = false + return + } + shouldMarkFetched = false + Toast.notify({ + type: 'error', + message: t('modal.errors.networkError', { ns: 'pluginTrigger' }), + }) + setSuggestedQuestions([]) + } + finally { + if (shouldMarkFetched) + setHasFetchedSuggestions(true) + setFetchingSuggestionsFalse() + } + }, [ + flowId, + hasFetchedSuggestions, + isFetchingSuggestions, + isInitView, + model.completion_params, + model.name, + model.provider, + paramKey, + promptLanguage, + setFetchingSuggestionsFalse, + setFetchingSuggestionsTrue, + setHasFetchedSuggestions, + setSuggestedQuestions, + t, + toolNodeId, + ]) + + const handleCloseModal = useCallback(() => { + suggestedQuestionsAbortControllerRef.current?.abort() + onClose() + }, [onClose]) + + useImperativeHandle(ref, () => ({ + onOpen: () => { + void handleFetchSuggestedQuestions() + }, + }), [handleFetchSuggestedQuestions]) + const renderModelTrigger = useCallback((params: TriggerProps) => { const label = params.currentModel?.label ? renderI18nObject(params.currentModel.label, language) @@ -396,8 +506,8 @@ const ContextGenerateModal: FC = ({ }) if (closeOnApply) - onClose() - }, [codeNodeData, codeNodeId, current, handleNodeDataUpdateWithSyncDraft, onClose]) + handleCloseModal() + }, [codeNodeData, codeNodeId, current, handleCloseModal, handleNodeDataUpdateWithSyncDraft]) const handleRun = useCallback(() => { if (!codeNodeId) @@ -475,7 +585,7 @@ const ContextGenerateModal: FC = ({ return ( = ({
- + + {t('nodes.tool.contextGenerate.suggestedQuestionsTitle', { ns: 'workflow' })} +
- {suggestedSkeletonItems.map(item => ( + {shouldShowSuggestedSkeleton && suggestedSkeletonItems.map(item => (
))} + {!shouldShowSuggestedSkeleton && suggestedQuestionsSafe.map((question, index) => ( + + ))}
@@ -716,7 +839,7 @@ const ContextGenerateModal: FC = ({ > {isInitView && (
- +
@@ -804,7 +927,7 @@ const ContextGenerateModal: FC = ({ {t('nodes.tool.contextGenerate.apply', { ns: 'workflow' })}
- +
@@ -897,6 +1020,8 @@ const ContextGenerateModal: FC = ({ ) -} +}) + +ContextGenerateModal.displayName = 'ContextGenerateModal' export default React.memo(ContextGenerateModal) diff --git a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx index 4d751a5a51..71c7751e50 100644 --- a/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/mixed-variable-text-input/index.tsx @@ -1,3 +1,4 @@ +import type { ContextGenerateModalHandle } from '../context-generate-modal' import type { DetectedAgent } from './hooks' import type { AgentNode, WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types' import type { StrategyDetail, StrategyPluginDetail } from '@/app/components/plugins/types' @@ -13,6 +14,7 @@ import { memo, useCallback, useMemo, + useRef, useState, } from 'react' import { useTranslation } from 'react-i18next' @@ -91,6 +93,7 @@ const MixedVariableTextInput = ({ const { handleSyncWorkflowDraft } = useNodesSyncDraft() const [isSubGraphModalOpen, setIsSubGraphModalOpen] = useState(false) const [isContextGenerateModalOpen, setIsContextGenerateModalOpen] = useState(false) + const contextGenerateModalRef = useRef(null) const nodesByIdMap = useMemo(() => { return availableNodes.reduce((acc, node) => { @@ -319,6 +322,9 @@ const MixedVariableTextInput = ({ onChange?.(assemblePlaceholder, VarKindTypeEnum.mixed, null) setControlPromptEditorRerenderKey(Date.now()) setIsContextGenerateModalOpen(true) + setTimeout(() => { + contextGenerateModalRef.current?.onOpen() + }, 0) return [extractorNodeId, 'result'] }, [assembleExtractorNodeId, assemblePlaceholder, ensureAssembleExtractorNode, onChange, paramKey, setControlPromptEditorRerenderKey, toolNodeId]) @@ -439,6 +445,7 @@ const MixedVariableTextInput = ({ )} {toolNodeId && paramKey && ( { }) } +export const fetchContextGenerateSuggestedQuestions = ( + body: ContextGenerateSuggestedQuestionsRequest, + getAbortController?: (abortController: AbortController) => void, +) => { + return post('/context-generate/suggested-questions', { + body, + }, { + getAbortController, + silent: true, + }) +} + export const fetchModelParams = (providerName: string, modelId: string) => { return get(`workspaces/current/model-providers/${providerName}/models/parameter-rules`, { params: {