feat: Add suggested questions to context generate modal

This commit is contained in:
zhsama
2026-01-21 20:49:12 +08:00
parent d7ccea8ac5
commit c4249f94de
8 changed files with 222 additions and 15 deletions

View File

@@ -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<Props> = ({
}) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const nodes = useStore(s => s.nodes)
const contextGenerateModalRef = useRef<ContextGenerateModalHandle>(null)
const handleAutomaticRes = useCallback((res: GenRes) => {
onGenerated?.(res.modified)
showAutomaticFalse()
@@ -64,11 +66,20 @@ const CodeGenerateBtn: FC<Props> = ({
}
}, [nodeId, nodes, parseExtractorNodeId])
const handleOpenAutomatic = useCallback(() => {
showAutomaticTrue()
if (!contextGenerateConfig)
return
setTimeout(() => {
contextGenerateModalRef.current?.onOpen()
}, 0)
}, [contextGenerateConfig, showAutomaticTrue])
return (
<div className={cn(className)}>
<ActionButton
className="hover:bg-[#155EFF]/8"
onClick={showAutomaticTrue}
onClick={handleOpenAutomatic}
>
<Generator className="h-4 w-4 text-primary-600" />
</ActionButton>
@@ -76,6 +87,7 @@ const CodeGenerateBtn: FC<Props> = ({
contextGenerateConfig
? (
<ContextGenerateModal
ref={contextGenerateModalRef}
isShow={showAutomatic}
onClose={showAutomaticFalse}
toolNodeId={contextGenerateConfig.toolNodeId}

View File

@@ -1,5 +1,4 @@
'use client'
import type { FC } from 'react'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { TriggerProps } from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal/trigger'
import type { CodeNodeType, OutputVar } from '@/app/components/workflow/nodes/code/types'
@@ -9,7 +8,7 @@ import { RiArrowDownSLine, RiArrowRightLine, RiCheckLine, RiCloseLine, RiRefresh
import { useEventListener, useSessionStorageState, useSize } from 'ahooks'
import useBoolean from 'ahooks/lib/useBoolean'
import * as React from 'react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { forwardRef, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import ActionButton from '@/app/components/base/action-button'
import Button from '@/app/components/base/button'
@@ -32,7 +31,8 @@ import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import { useStore, useWorkflowStore } from '@/app/components/workflow/store'
import { NodeRunningStatus, VarType } from '@/app/components/workflow/types'
import { renderI18nObject } from '@/i18n-config'
import { generateContext } from '@/service/debug'
import { languages } from '@/i18n-config/language'
import { fetchContextGenerateSuggestedQuestions, generateContext } from '@/service/debug'
import { AppModeEnum } from '@/types/app'
import { cn } from '@/utils/classnames'
import useContextGenData from './use-context-gen-data'
@@ -49,6 +49,10 @@ type ContextGenerateChatMessage = ContextGenerateMessage & {
durationMs?: number
}
export type ContextGenerateModalHandle = {
onOpen: () => void
}
const minCodeHeight = 80
const minOutputHeight = 80
const splitHandleHeight = 4
@@ -93,13 +97,13 @@ const mapOutputsToResponse = (outputs?: OutputVar) => {
return next
}
const ContextGenerateModal: FC<Props> = ({
const ContextGenerateModal = forwardRef<ContextGenerateModalHandle, Props>(({
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<Props> = ({
{ defaultValue: [] },
)
const [suggestedQuestions, setSuggestedQuestions] = useSessionStorageState<string[]>(
`${storageKey}-suggested-questions`,
{ defaultValue: [] },
)
const [hasFetchedSuggestions, setHasFetchedSuggestions] = useSessionStorageState<boolean>(
`${storageKey}-suggested-questions-fetched`,
{ defaultValue: false },
)
const [isFetchingSuggestions, { setTrue: setFetchingSuggestionsTrue, setFalse: setFetchingSuggestionsFalse }] = useBoolean(false)
const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(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<Model | null>(() => {
@@ -213,6 +232,8 @@ const ContextGenerateModal: FC<Props> = ({
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<Props> = ({
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<Props> = ({
})
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<Props> = ({
return (
<Modal
isShow={isShow}
onClose={onClose}
onClose={handleCloseModal}
className={cn(
'max-w-[calc(100vw-32px)] border-[0.5px] border-components-panel-border bg-background-body !p-0 shadow-xl shadow-shadow-shadow-5',
isInitView ? 'w-[1280px]' : 'w-[1200px]',
@@ -565,15 +675,28 @@ const ContextGenerateModal: FC<Props> = ({
</div>
<div className="flex flex-col gap-px px-2">
<div className="flex items-center px-3 pb-2 pt-4">
<SkeletonRectangle className="h-3 w-20" />
<span className="text-xs font-semibold uppercase text-text-tertiary">
{t('nodes.tool.contextGenerate.suggestedQuestionsTitle', { ns: 'workflow' })}
</span>
</div>
<div className="flex flex-col gap-1 px-3">
{suggestedSkeletonItems.map(item => (
{shouldShowSuggestedSkeleton && suggestedSkeletonItems.map(item => (
<SkeletonRow key={item} className="py-1">
<div className="h-4 w-4 rounded-sm bg-divider-subtle opacity-60" />
<SkeletonRectangle className="h-3 w-[260px]" />
</SkeletonRow>
))}
{!shouldShowSuggestedSkeleton && suggestedQuestionsSafe.map((question, index) => (
<button
key={`${question}-${index}`}
type="button"
className="flex items-start gap-2 rounded-lg px-2 py-1 text-left text-sm text-text-secondary transition hover:bg-state-base-hover"
onClick={() => handleSuggestedQuestionClick(question)}
>
<span className="mt-1 h-1.5 w-1.5 shrink-0 rounded-full bg-divider-regular" />
<span className="flex-1 whitespace-pre-wrap">{question}</span>
</button>
))}
</div>
</div>
</div>
@@ -716,7 +839,7 @@ const ContextGenerateModal: FC<Props> = ({
>
{isInitView && (
<div className="flex h-10 items-center justify-end px-3 py-1">
<ActionButton size="m" className="!h-8 !w-8" onClick={onClose}>
<ActionButton size="m" className="!h-8 !w-8" onClick={handleCloseModal}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</ActionButton>
</div>
@@ -804,7 +927,7 @@ const ContextGenerateModal: FC<Props> = ({
{t('nodes.tool.contextGenerate.apply', { ns: 'workflow' })}
</Button>
<div className="mx-1 h-4 w-px bg-divider-regular" />
<ActionButton size="m" className="!h-8 !w-8" onClick={onClose}>
<ActionButton size="m" className="!h-8 !w-8" onClick={handleCloseModal}>
<RiCloseLine className="h-4 w-4 text-text-tertiary" />
</ActionButton>
</div>
@@ -897,6 +1020,8 @@ const ContextGenerateModal: FC<Props> = ({
</div>
</Modal>
)
}
})
ContextGenerateModal.displayName = 'ContextGenerateModal'
export default React.memo(ContextGenerateModal)

View File

@@ -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<ContextGenerateModalHandle>(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 && (
<ContextGenerateModal
ref={contextGenerateModalRef}
isShow={isContextGenerateModalOpen}
onClose={handleCloseContextGenerateModal}
toolNodeId={toolNodeId}

View File

@@ -805,6 +805,7 @@
"nodes.tool.contextGenerate.run": "Run",
"nodes.tool.contextGenerate.running": "Running",
"nodes.tool.contextGenerate.subtitle": "Assemble multiple variables into one from previous nodes",
"nodes.tool.contextGenerate.suggestedQuestionsTitle": "Suggested Questions",
"nodes.tool.contextGenerate.title": "Assemble Variables",
"nodes.tool.inputVars": "Input Variables",
"nodes.tool.insertPlaceholder1": "Type or press",

View File

@@ -770,13 +770,20 @@
"nodes.tool.contextGenerate.apply": "適用",
"nodes.tool.contextGenerate.code": "コード",
"nodes.tool.contextGenerate.codeBlock": "コードブロック",
"nodes.tool.contextGenerate.codeLanguage.javascript": "JavaScript",
"nodes.tool.contextGenerate.codeLanguage.python3": "Python 3",
"nodes.tool.contextGenerate.defaultAssistantMessage": "完了しました。確認してください。",
"nodes.tool.contextGenerate.generatedCode": "生成されたコード",
"nodes.tool.contextGenerate.generating": "生成中...",
"nodes.tool.contextGenerate.initPlaceholder": "上流ノードの変数の組み立て方を説明し、'/' を押して変数を挿入してください。",
"nodes.tool.contextGenerate.inputPlaceholder": "変更を依頼...",
"nodes.tool.contextGenerate.output": "出力",
"nodes.tool.contextGenerate.resizeHandle": "サイズ調整ハンドル",
"nodes.tool.contextGenerate.rightSidePlaceholder": "左側に指示を入力してください。生成されたコードがここに表示されます。",
"nodes.tool.contextGenerate.run": "実行",
"nodes.tool.contextGenerate.running": "実行中",
"nodes.tool.contextGenerate.subtitle": "前のードの複数変数を1つにまとめる",
"nodes.tool.contextGenerate.suggestedQuestionsTitle": "おすすめ質問",
"nodes.tool.contextGenerate.title": "変数を組み立てる",
"nodes.tool.inputVars": "入力変数",
"nodes.tool.insertPlaceholder1": "タイプするか押してください",

View File

@@ -780,6 +780,25 @@
"nodes.tool.agentPlaceholder": "告诉我 {{paramKey}}...",
"nodes.tool.assembleVariables": "组装变量",
"nodes.tool.authorize": "授权",
"nodes.tool.contextGenerate.apply": "应用",
"nodes.tool.contextGenerate.code": "代码",
"nodes.tool.contextGenerate.codeBlock": "代码块",
"nodes.tool.contextGenerate.codeLanguage.javascript": "JavaScript",
"nodes.tool.contextGenerate.codeLanguage.python3": "Python 3",
"nodes.tool.contextGenerate.defaultAssistantMessage": "已完成,请检查。",
"nodes.tool.contextGenerate.generatedCode": "生成的代码",
"nodes.tool.contextGenerate.generating": "生成中...",
"nodes.tool.contextGenerate.initPlaceholder": "描述如何从上游节点组装变量,并按“/”插入变量。",
"nodes.tool.contextGenerate.inputPlaceholder": "输入修改需求...",
"nodes.tool.contextGenerate.instruction": "指令",
"nodes.tool.contextGenerate.output": "输出",
"nodes.tool.contextGenerate.resizeHandle": "调整大小把手",
"nodes.tool.contextGenerate.rightSidePlaceholder": "请在左侧输入指令。\n生成的代码将显示在这里。",
"nodes.tool.contextGenerate.run": "运行",
"nodes.tool.contextGenerate.running": "运行中",
"nodes.tool.contextGenerate.subtitle": "从之前的节点组装多个变量为一个",
"nodes.tool.contextGenerate.suggestedQuestionsTitle": "推荐问题",
"nodes.tool.contextGenerate.title": "组装变量",
"nodes.tool.inputVars": "输入变量",
"nodes.tool.insertPlaceholder1": "键入",
"nodes.tool.insertPlaceholder2": "插入变量",

View File

@@ -770,14 +770,21 @@
"nodes.tool.contextGenerate.apply": "套用",
"nodes.tool.contextGenerate.code": "程式碼",
"nodes.tool.contextGenerate.codeBlock": "程式碼區塊",
"nodes.tool.contextGenerate.codeLanguage.javascript": "JavaScript",
"nodes.tool.contextGenerate.codeLanguage.python3": "Python 3",
"nodes.tool.contextGenerate.defaultAssistantMessage": "我已完成,請查看。",
"nodes.tool.contextGenerate.generatedCode": "生成的程式碼",
"nodes.tool.contextGenerate.generating": "生成中...",
"nodes.tool.contextGenerate.initPlaceholder": "描述如何從上游節點組裝變數,並按「/」插入變數。",
"nodes.tool.contextGenerate.inputPlaceholder": "請求修改...",
"nodes.tool.contextGenerate.instruction": "指令",
"nodes.tool.contextGenerate.output": "輸出",
"nodes.tool.contextGenerate.resizeHandle": "調整大小把手",
"nodes.tool.contextGenerate.rightSidePlaceholder": "請在左側輸入指令。生成的程式碼將顯示在這裡。",
"nodes.tool.contextGenerate.run": "執行",
"nodes.tool.contextGenerate.running": "執行中",
"nodes.tool.contextGenerate.subtitle": "從之前的節點組裝多個變數為一個",
"nodes.tool.contextGenerate.suggestedQuestionsTitle": "推薦問題",
"nodes.tool.contextGenerate.title": "組裝變數",
"nodes.tool.inputVars": "輸入變數",
"nodes.tool.insertPlaceholder1": "輸入或按壓",

View File

@@ -58,6 +58,23 @@ export type ContextGenerateResponse = {
error: string
}
export type ContextGenerateSuggestedQuestionsRequest = {
workflow_id: string
node_id: string
parameter_name: string
language: string
model_config: {
provider: string
name: string
completion_params?: CompletionParams
}
}
export type ContextGenerateSuggestedQuestionsResponse = {
questions: string[]
error: string
}
export type TextGenerationMessageFile = FileEntity & {
belongs_to?: 'assistant' | 'user' | string
}
@@ -149,6 +166,18 @@ export const generateContext = (body: ContextGenerateRequest) => {
})
}
export const fetchContextGenerateSuggestedQuestions = (
body: ContextGenerateSuggestedQuestionsRequest,
getAbortController?: (abortController: AbortController) => void,
) => {
return post<ContextGenerateSuggestedQuestionsResponse>('/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: {