feat: enchance prompt and code (#23633)

Co-authored-by: stream <stream@dify.ai>
Co-authored-by: Stream <1542763342@qq.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
Joel
2025-08-18 12:29:12 +08:00
committed by GitHub
parent a7fe0e3f87
commit de9c5f10b3
66 changed files with 2654 additions and 275 deletions

View File

@@ -13,7 +13,7 @@ import Tooltip from '@/app/components/base/tooltip'
import { AppType } from '@/types/app'
import { getNewVar, getVars } from '@/utils/var'
import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn'
import type { AutomaticRes } from '@/service/debug'
import type { GenRes } from '@/service/debug'
import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
import PromptEditor from '@/app/components/base/prompt-editor'
import ConfigContext from '@/context/debug-configuration'
@@ -61,6 +61,7 @@ const Prompt: FC<ISimplePromptInput> = ({
const { eventEmitter } = useEventEmitterContextContext()
const {
appId,
modelConfig,
dataSets,
setModelConfig,
@@ -139,21 +140,21 @@ const Prompt: FC<ISimplePromptInput> = ({
}
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = (res: AutomaticRes) => {
const handleAutomaticRes = (res: GenRes) => {
// put eventEmitter in first place to prevent overwrite the configs.prompt_variables.But another problem is that prompt won't hight the prompt_variables.
eventEmitter?.emit({
type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
payload: res.prompt,
payload: res.modified,
} as any)
const newModelConfig = produce(modelConfig, (draft) => {
draft.configs.prompt_template = res.prompt
draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true }))
draft.configs.prompt_template = res.modified
draft.configs.prompt_variables = (res.variables || []).map(key => ({ key, name: key, type: 'string', required: true }))
})
setModelConfig(newModelConfig)
setPrevPromptConfig(modelConfig.configs)
if (mode !== AppType.completion) {
setIntroduction(res.opening_statement)
setIntroduction(res.opening_statement || '')
const newFeatures = produce(features, (draft) => {
draft.opening = {
...draft.opening,
@@ -272,10 +273,13 @@ const Prompt: FC<ISimplePromptInput> = ({
{showAutomatic && (
<GetAutomaticResModal
flowId={appId}
mode={mode as AppType}
isShow={showAutomatic}
onClose={showAutomaticFalse}
onFinished={handleAutomaticRes}
currentPrompt={promptTemplate}
isBasicMode
/>
)}
</div>

View File

@@ -2,7 +2,7 @@
import type { FC } from 'react'
import React, { useCallback, useEffect, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { useBoolean } from 'ahooks'
import { useBoolean, useSessionStorageState } from 'ahooks'
import {
RiDatabase2Line,
RiFileExcel2Line,
@@ -14,24 +14,18 @@ import {
RiTranslate,
RiUser2Line,
} from '@remixicon/react'
import cn from 'classnames'
import s from './style.module.css'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import Textarea from '@/app/components/base/textarea'
import Toast from '@/app/components/base/toast'
import { generateRule } from '@/service/debug'
import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
import { generateBasicAppFistTimeRule, generateRule } from '@/service/debug'
import type { CompletionParams, Model } from '@/types/app'
import { AppType } from '@/types/app'
import ConfigVar from '@/app/components/app/configuration/config-var'
import GroupName from '@/app/components/app/configuration/base/group-name'
import type { AppType } from '@/types/app'
import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
// type
import type { AutomaticRes } from '@/service/debug'
import type { GenRes } from '@/service/debug'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
@@ -39,13 +33,25 @@ import { ModelTypeEnum } from '@/app/components/header/account-setting/model-pro
import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
import type { ModelModeType } from '@/types/app'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import InstructionEditorInWorkflow from './instruction-editor-in-workflow'
import InstructionEditorInBasic from './instruction-editor'
import { GeneratorType } from './types'
import Result from './result'
import useGenData from './use-gen-data'
import IdeaOutput from './idea-output'
import ResPlaceholder from './res-placeholder'
import { useGenerateRuleTemplate } from '@/service/use-apps'
const i18nPrefix = 'appDebug.generate'
export type IGetAutomaticResProps = {
mode: AppType
isShow: boolean
onClose: () => void
onFinished: (res: AutomaticRes) => void
isInLLMNode?: boolean
onFinished: (res: GenRes) => void
flowId?: string
nodeId?: string
currentPrompt?: string
isBasicMode?: boolean
}
const TryLabel: FC<{
@@ -68,7 +74,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
mode,
isShow,
onClose,
isInLLMNode,
flowId,
nodeId,
currentPrompt,
isBasicMode,
onFinished,
}) => {
const { t } = useTranslation()
@@ -123,13 +132,27 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
},
]
const [instruction, setInstruction] = useState<string>('')
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}${isBasicMode ? '' : `-${nodeId}`}`)
const instruction = instructionFromSessionStorage || ''
const [ideaOutput, setIdeaOutput] = useState<string>('')
const [editorKey, setEditorKey] = useState(`${flowId}-0`)
const handleChooseTemplate = useCallback((key: string) => {
return () => {
const template = t(`appDebug.generate.template.${key}.instruction`)
setInstruction(template)
setEditorKey(`${flowId}-${Date.now()}`)
}
}, [t])
const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.prompt, isBasicMode)
useEffect(() => {
if (!instruction && instructionTemplate)
setInstruction(instructionTemplate.data)
setEditorKey(`${flowId}-${Date.now()}`)
}, [instructionTemplate])
const isValid = () => {
if (instruction.trim() === '') {
Toast.notify({
@@ -143,7 +166,10 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
return true
}
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
const [res, setRes] = useState<AutomaticRes | null>(null)
const storageKey = `${flowId}${isBasicMode ? '' : `-${nodeId}`}`
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
storageKey,
})
useEffect(() => {
if (defaultModel) {
@@ -170,16 +196,6 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
</div>
)
const renderNoData = (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='h-14 w-14 text-text-tertiary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.generate.noDataLine1')}</div>
<div>{t('appDebug.generate.noDataLine2')}</div>
</div>
</div>
)
const handleModelChange = useCallback((newValue: { modelId: string; provider: string; mode?: string; features?: string[] }) => {
const newModel = {
...model,
@@ -207,28 +223,59 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
return
setLoadingTrue()
try {
const { error, ...res } = await generateRule({
instruction,
model_config: model,
no_variable: !!isInLLMNode,
})
setRes(res)
if (error) {
Toast.notify({
type: 'error',
message: error,
let apiRes: GenRes
let hasError = false
if (isBasicMode || !currentPrompt) {
const { error, ...res } = await generateBasicAppFistTimeRule({
instruction,
model_config: model,
no_variable: false,
})
apiRes = {
...res,
modified: res.prompt,
} as GenRes
if (error) {
hasError = true
Toast.notify({
type: 'error',
message: error,
})
}
}
else {
const { error, ...res } = await generateRule({
flow_id: flowId,
node_id: nodeId,
current: currentPrompt,
instruction,
ideal_output: ideaOutput,
model_config: model,
})
apiRes = res
if (error) {
hasError = true
Toast.notify({
type: 'error',
message: error,
})
}
}
if (!hasError)
addVersion(apiRes)
}
finally {
setLoadingFalse()
}
}
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
const [isShowConfirmOverwrite, {
setTrue: showConfirmOverwrite,
setFalse: hideShowConfirmOverwrite,
}] = useBoolean(false)
const isShowAutoPromptResPlaceholder = () => {
return !isLoading && !res
return !isLoading && !current
}
return (
@@ -236,15 +283,14 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
isShow={isShow}
onClose={onClose}
className='min-w-[1140px] !p-0'
closable
>
<div className='flex h-[680px] flex-wrap'>
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'>
<div className='mb-8'>
<div className='mb-5'>
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.generate.title')}</div>
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.generate.description')}</div>
</div>
<div className='mb-8'>
<div>
<ModelParameterModal
popupClassName='!w-[520px]'
portalToFollowElemContentClassName='z-[1000]'
@@ -258,116 +304,99 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
hideDebugWithMultipleModel
/>
</div>
<div >
<div className='flex items-center'>
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
<div className='h-px grow' style={{
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
}}></div>
</div>
<div className='flex flex-wrap'>
{tryList.map(item => (
<TryLabel
key={item.key}
Icon={item.icon}
text={t(`appDebug.generate.template.${item.key}.name`)}
onClick={handleChooseTemplate(item.key)}
/>
))}
</div>
</div>
{/* inputs */}
<div className='mt-6'>
<div className='text-[0px]'>
<div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.generate.instruction')}</div>
<Textarea
className="h-[200px] resize-none"
placeholder={t('appDebug.generate.instructionPlaceHolder') as string}
value={instruction}
onChange={e => setInstruction(e.target.value)} />
{isBasicMode && (
<div className='mt-4'>
<div className='flex items-center'>
<div className='mr-3 shrink-0 text-xs font-semibold uppercase leading-[18px] text-text-tertiary'>{t('appDebug.generate.tryIt')}</div>
<div className='h-px grow' style={{
background: 'linear-gradient(to right, rgba(243, 244, 246, 1), rgba(243, 244, 246, 0))',
}}></div>
</div>
<div className='flex flex-wrap'>
{tryList.map(item => (
<TryLabel
key={item.key}
Icon={item.icon}
text={t(`appDebug.generate.template.${item.key}.name`)}
onClick={handleChooseTemplate(item.key)}
/>
))}
</div>
</div>
)}
<div className='mt-5 flex justify-end'>
{/* inputs */}
<div className='mt-4'>
<div>
<div className='system-sm-semibold-uppercase mb-1.5 text-text-secondary'>{t('appDebug.generate.instruction')}</div>
{isBasicMode ? (
<InstructionEditorInBasic
editorKey={editorKey}
generatorType={GeneratorType.prompt}
value={instruction}
onChange={setInstruction}
availableVars={[]}
availableNodes={[]}
isShowCurrentBlock={!!currentPrompt}
isShowLastRunBlock={false}
/>
) : (
<InstructionEditorInWorkflow
editorKey={editorKey}
generatorType={GeneratorType.prompt}
value={instruction}
onChange={setInstruction}
nodeId={nodeId || ''}
isShowCurrentBlock={!!currentPrompt}
/>
)}
</div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>
<div className='mt-7 flex justify-end space-x-2'>
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`)}</Button>
<Button
className='flex space-x-1'
variant='primary'
onClick={onGenerate}
disabled={isLoading}
>
<Generator className='h-4 w-4 text-white' />
<span className='text-xs font-semibold text-white'>{t('appDebug.generate.generate')}</span>
<Generator className='h-4 w-4' />
<span className='text-xs font-semibold'>{t('appDebug.generate.generate')}</span>
</Button>
</div>
</div>
</div>
{(!isLoading && res) && (
<div className='h-full w-0 grow p-6 pb-0'>
<div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
<ConfigPrompt
mode={mode}
promptTemplate={res?.prompt || ''}
promptVariables={[]}
readonly
noTitle={isInLLMNode}
gradientBorder
editorHeight={isInLLMNode ? 524 : 0}
noResize={isInLLMNode}
/>
{!isInLLMNode && (
<>
{(res?.variables?.length && res?.variables?.length > 0)
? (
<ConfigVar
promptVariables={res?.variables.map(key => ({ key, name: key, type: 'string', required: true })) || []}
readonly
/>
)
: ''}
{(mode !== AppType.completion && res?.opening_statement) && (
<div className='mt-7'>
<GroupName name={t('appDebug.feature.groupChat.title')} />
<div
className='mb-1 rounded-xl border-l-[0.5px] border-t-[0.5px] border-effects-highlight bg-background-section-burn p-3'
>
<div className='mb-2 flex items-center gap-2'>
<div className='shrink-0 rounded-lg border-[0.5px] border-divider-subtle bg-util-colors-blue-light-blue-light-500 p-1 shadow-xs'>
<LoveMessage className='h-4 w-4 text-text-primary-on-surface' />
</div>
<div className='system-sm-semibold flex grow items-center text-text-secondary'>
{t('appDebug.feature.conversationOpener.title')}
</div>
</div>
<div className='system-xs-regular min-h-8 text-text-tertiary'>{res.opening_statement}</div>
</div>
</div>
)}
</>
)}
</div>
<div className='flex justify-end bg-background-default py-4'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='ml-2' onClick={() => {
setShowConfirmOverwrite(true)
}}>{t('appDebug.generate.apply')}</Button>
</div>
{(!isLoading && current) && (
<div className='h-full w-0 grow bg-background-default-subtle p-6 pb-0'>
<Result
current={current!}
isBasicMode={isBasicMode}
nodeId={nodeId!}
currentVersionIndex={currentVersionIndex || 0}
setCurrentVersionIndex={setCurrentVersionIndex}
versions={versions || []}
onApply={showConfirmOverwrite}
generatorType={GeneratorType.prompt}
/>
</div>
)}
{isLoading && renderLoading}
{isShowAutoPromptResPlaceholder() && renderNoData}
{showConfirmOverwrite && (
{isShowAutoPromptResPlaceholder() && <ResPlaceholder />}
{isShowConfirmOverwrite && (
<Confirm
title={t('appDebug.generate.overwriteTitle')}
content={t('appDebug.generate.overwriteMessage')}
isShow={showConfirmOverwrite}
isShow
onConfirm={() => {
setShowConfirmOverwrite(false)
onFinished(res!)
hideShowConfirmOverwrite()
onFinished(current!)
}}
onCancel={() => setShowConfirmOverwrite(false)}
onCancel={hideShowConfirmOverwrite}
/>
)}
</div>

View File

@@ -0,0 +1,48 @@
'use client'
import { ArrowDownRoundFill } from '@/app/components/base/icons/src/vender/solid/general'
import { useBoolean } from 'ahooks'
import type { FC } from 'react'
import React from 'react'
import cn from '@/utils/classnames'
import Textarea from '@/app/components/base/textarea'
import { useTranslation } from 'react-i18next'
const i18nPrefix = 'appDebug.generate'
type Props = {
value: string
onChange: (value: string) => void
}
const IdeaOutput: FC<Props> = ({
value,
onChange,
}) => {
const { t } = useTranslation()
const [isFoldIdeaOutput, {
toggle: toggleFoldIdeaOutput,
}] = useBoolean(true)
return (
<div className='mt-4 text-[0px]'>
<div
className='mb-1.5 flex cursor-pointer items-center text-sm font-medium leading-5 text-text-primary'
onClick={toggleFoldIdeaOutput}
>
<div className='system-sm-semibold-uppercase mr-1 text-text-secondary'>{t(`${i18nPrefix}.idealOutput`)}</div>
<div className='system-xs-regular text-text-tertiary'>({t(`${i18nPrefix}.optional`)})</div>
<ArrowDownRoundFill className={cn('size text-text-quaternary', isFoldIdeaOutput && 'relative top-[1px] rotate-[-90deg]')} />
</div>
{!isFoldIdeaOutput && (
<Textarea
className="h-[80px]"
placeholder={t(`${i18nPrefix}.idealOutputPlaceholder`)}
value={value}
onChange={e => onChange(e.target.value)}
/>
)}
</div>
)
}
export default React.memo(IdeaOutput)

View File

@@ -0,0 +1,58 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import type { GeneratorType } from './types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import { VarType } from '@/app/components/workflow/types'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import InstructionEditor from './instruction-editor'
import { useWorkflowVariableType } from '@/app/components/workflow/hooks'
import { useWorkflowStore } from '@/app/components/workflow/store'
type Props = {
nodeId: string
value: string
editorKey: string
onChange: (text: string) => void
generatorType: GeneratorType
isShowCurrentBlock: boolean
}
const InstructionEditorInWorkflow: FC<Props> = ({
nodeId,
value,
editorKey,
onChange,
generatorType,
isShowCurrentBlock,
}) => {
const workflowStore = useWorkflowStore()
const filterVar = useCallback((payload: Var, selector: ValueSelector) => {
const { nodesWithInspectVars } = workflowStore.getState()
const nodeId = selector?.[0]
return !!nodesWithInspectVars.find(node => node.nodeId === nodeId) && payload.type !== VarType.file && payload.type !== VarType.arrayFile
}, [workflowStore])
const {
availableVars,
availableNodes,
} = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar,
})
const getVarType = useWorkflowVariableType()
return (
<InstructionEditor
value={value}
onChange={onChange}
editorKey={editorKey}
generatorType={generatorType}
availableVars={availableVars}
availableNodes={availableNodes}
getVarType={getVarType}
isShowCurrentBlock={isShowCurrentBlock}
isShowLastRunBlock
/>
)
}
export default React.memo(InstructionEditorInWorkflow)

View File

@@ -0,0 +1,117 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import PromptEditor from '@/app/components/base/prompt-editor'
import type { GeneratorType } from './types'
import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ValueSelector } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { PROMPT_EDITOR_INSERT_QUICKLY } from '@/app/components/base/prompt-editor/plugins/update-block'
import { useEventEmitterContextContext } from '@/context/event-emitter'
type Props = {
editorKey: string
value: string
onChange: (text: string) => void
generatorType: GeneratorType
availableVars: NodeOutPutVar[]
availableNodes: Node[]
getVarType?: (params: {
nodeId: string,
valueSelector: ValueSelector,
}) => Type
isShowCurrentBlock: boolean
isShowLastRunBlock: boolean
}
const i18nPrefix = 'appDebug.generate'
const InstructionEditor: FC<Props> = ({
editorKey,
generatorType,
value,
onChange,
availableVars,
availableNodes,
getVarType = () => Type.string,
isShowCurrentBlock,
isShowLastRunBlock,
}) => {
const { t } = useTranslation()
const { eventEmitter } = useEventEmitterContextContext()
const isCode = generatorType === 'code'
const placeholder = isCode ? <div className='system-sm-regular whitespace-break-spaces !leading-6 text-text-placeholder'>
{t(`${i18nPrefix}.codeGenInstructionPlaceHolderLine`)}
</div> : (
<div className='system-sm-regular text-text-placeholder'>
<div className='leading-6'>{t(`${i18nPrefix}.instructionPlaceHolderTitle`)}</div>
<div className='mt-2'>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine1`)}</div>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine2`)}</div>
<div>{t(`${i18nPrefix}.instructionPlaceHolderLine3`)}</div>
</div>
</div>
)
const handleInsertVariable = () => {
eventEmitter?.emit({ type: PROMPT_EDITOR_INSERT_QUICKLY, instanceId: editorKey } as any)
}
return (
<div className='relative'>
<PromptEditor
wrapperClassName='border !border-components-input-bg-normal bg-components-input-bg-normal hover:!border-components-input-bg-hover rounded-[10px] px-4 pt-3'
key={editorKey}
instanceId={editorKey}
placeholder={placeholder}
placeholderClassName='px-4 pt-3'
className={cn('min-h-[240px] pb-8')}
value={value}
workflowVariableBlock={{
show: true,
variables: availableVars,
getVarType,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
currentBlock={{
show: isShowCurrentBlock,
generatorType,
}}
errorMessageBlock={{
show: isCode,
}}
lastRunBlock={{
show: isShowLastRunBlock,
}}
onChange={onChange}
editable
isSupportFileVar={false}
/>
<div className='system-xs-regular absolute bottom-0 left-4 flex h-8 items-center space-x-0.5 text-components-input-text-placeholder'>
<span>{t('appDebug.generate.press')}</span>
<span className='system-kbd flex h-4 w-3.5 items-center justify-center rounded-[4px] bg-components-kbd-bg-gray text-text-placeholder'>/</span>
<span>{t('appDebug.generate.to')}</span>
<span onClick={handleInsertVariable} className='!ml-1 cursor-pointer hover:border-b hover:border-dotted hover:border-text-tertiary hover:text-text-tertiary'>{t('appDebug.generate.insertContext')}</span>
</div>
</div>
)
}
export default React.memo(InstructionEditor)

View File

@@ -0,0 +1,55 @@
'use client'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import type { FC } from 'react'
import React from 'react'
import PromptRes from './prompt-res'
import { Type } from '@/app/components/workflow/nodes/llm/types'
import { BlockEnum } from '@/app/components/workflow/types'
import { useTranslation } from 'react-i18next'
type Props = {
value: string
nodeId: string
}
const PromptResInWorkflow: FC<Props> = ({
value,
nodeId,
}) => {
const { t } = useTranslation()
const {
availableVars,
availableNodes,
} = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
filterVar: _payload => true,
})
return (
<PromptRes
value={value}
workflowVariableBlock={{
show: true,
variables: availableVars || [],
getVarType: () => Type.string,
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
width: node.width,
height: node.height,
position: node.position,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
>
</PromptRes>
)
}
export default React.memo(PromptResInWorkflow)

View File

@@ -0,0 +1,31 @@
'use client'
import PromptEditor from '@/app/components/base/prompt-editor'
import type { WorkflowVariableBlockType } from '@/app/components/base/prompt-editor/types'
import type { FC } from 'react'
import React, { useEffect } from 'react'
type Props = {
value: string
workflowVariableBlock: WorkflowVariableBlockType
}
const keyIdPrefix = 'prompt-res-editor'
const PromptRes: FC<Props> = ({
value,
workflowVariableBlock,
}) => {
const [editorKey, setEditorKey] = React.useState<string>('keyIdPrefix-0')
useEffect(() => {
setEditorKey(`${keyIdPrefix}-${Date.now()}`)
}, [value])
return (
<PromptEditor
key={editorKey}
value={value}
editable={false}
className='h-full bg-transparent pt-0'
workflowVariableBlock={workflowVariableBlock}
/>
)
}
export default React.memo(PromptRes)

View File

@@ -0,0 +1,54 @@
import { RiArrowDownSLine, RiSparklingFill } from '@remixicon/react'
import { useBoolean } from 'ahooks'
import React from 'react'
import cn from '@/utils/classnames'
import { Markdown } from '@/app/components/base/markdown'
import { useTranslation } from 'react-i18next'
import s from './style.module.css'
type Props = {
message: string
className?: string
}
const PromptToast = ({
message,
className,
}: Props) => {
const { t } = useTranslation()
const [isFold, {
toggle: toggleFold,
}] = useBoolean(false)
// const message = `
// list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1
// # h1
// **strong text** ~~strikethrough~~
// * list1list1list1list1list1list1list1list1list1list1list1list1list1list1list1
// * list2
// xxxx
// ## h2
// \`\`\`python
// print('Hello, World!')
// \`\`\`
// `
return (
<div className={cn('rounded-xl border-[0.5px] border-components-panel-border bg-background-section-burn pl-4 shadow-xs', className)}>
<div className='my-3 flex h-4 items-center justify-between pr-3'>
<div className='flex items-center space-x-1'>
<RiSparklingFill className='size-3.5 text-components-input-border-active-prompt-1' />
<span className={cn(s.optimizationNoteText, 'system-xs-semibold-uppercase')}>{t('appDebug.generate.optimizationNote')}</span>
</div>
<RiArrowDownSLine className={cn('size-4 cursor-pointer text-text-tertiary', isFold && 'rotate-[-90deg]')} onClick={toggleFold} />
</div>
{!isFold && (
<div className='pb-4 pr-4'>
<Markdown className="!text-sm" content={message} />
</div>
)}
</div>
)
}
export default PromptToast

View File

@@ -0,0 +1,18 @@
'use client'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
const ResPlaceholder: FC = () => {
const { t } = useTranslation()
return (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='size-8 text-text-quaternary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.generate.newNoDataLine1')}</div>
</div>
</div>
)
}
export default React.memo(ResPlaceholder)

View File

@@ -0,0 +1,97 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import { GeneratorType } from './types'
import PromptToast from './prompt-toast'
import Button from '@/app/components/base/button'
import VersionSelector from './version-selector'
import type { GenRes } from '@/service/debug'
import { RiClipboardLine } from '@remixicon/react'
import copy from 'copy-to-clipboard'
import Toast from '@/app/components/base/toast'
import CodeEditor from '@/app/components/workflow/nodes/llm/components/json-schema-config-modal/code-editor'
import PromptRes from './prompt-res'
import PromptResInWorkflow from './prompt-res-in-workflow'
type Props = {
isBasicMode?: boolean
nodeId?: string
current: GenRes
currentVersionIndex: number
setCurrentVersionIndex: (index: number) => void
versions: GenRes[]
onApply: () => void
generatorType: GeneratorType
}
const Result: FC<Props> = ({
isBasicMode,
nodeId,
current,
currentVersionIndex,
setCurrentVersionIndex,
versions,
onApply,
generatorType,
}) => {
const { t } = useTranslation()
const isGeneratorPrompt = generatorType === GeneratorType.prompt
return (
<div className='flex h-full flex-col'>
<div className='mb-3 flex shrink-0 items-center justify-between'>
<div>
<div className='shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.generate.resTitle')}</div>
<VersionSelector
versionLen={versions.length}
value={currentVersionIndex}
onChange={setCurrentVersionIndex}
/>
</div>
<div className='flex items-center space-x-2'>
<Button className='px-2' onClick={() => {
copy(current.modified)
Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
}}>
<RiClipboardLine className='h-4 w-4 text-text-secondary' />
</Button>
<Button variant='primary' onClick={onApply}>
{t('appDebug.generate.apply')}
</Button>
</div>
</div>
<div className='flex grow flex-col overflow-y-auto'>
{
current?.message && (
<PromptToast message={current.message} className='mb-3 shrink-0' />
)
}
<div className='grow pb-6'>
{isGeneratorPrompt ? (
isBasicMode ? (
<PromptRes
value={current?.modified}
workflowVariableBlock={{
show: false,
}}
/>
) : (<PromptResInWorkflow
value={current?.modified || ''}
nodeId={nodeId!}
/>)
) : (
<CodeEditor
editorWrapperClassName='h-full'
className='bg-transparent pt-0'
value={current?.modified}
readOnly
hideTopMenu
/>
)}
</div>
</div>
</div>
)
}
export default React.memo(Result)

View File

@@ -3,5 +3,11 @@
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
text-fill-color: transparent;
}
.optimizationNoteText {
background: linear-gradient(263deg, rgba(21, 90, 239, 0.95) -20.92%, rgba(11, 165, 236, 0.95) 87.04%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
background-clip: text;
}

View File

@@ -0,0 +1,4 @@
export enum GeneratorType {
prompt = 'prompt',
code = 'code',
}

View File

@@ -0,0 +1,36 @@
import type { GenRes } from '@/service/debug'
import { useSessionStorageState } from 'ahooks'
import { useCallback } from 'react'
type Params = {
storageKey: string
}
const keyPrefix = 'gen-data-'
const useGenData = ({ storageKey }: Params) => {
const [versions, setVersions] = useSessionStorageState<GenRes[]>(`${keyPrefix}${storageKey}-versions`, {
defaultValue: [],
})
const [currentVersionIndex, setCurrentVersionIndex] = useSessionStorageState<number>(`${keyPrefix}${storageKey}-version-index`, {
defaultValue: 0,
})
const current = versions?.[currentVersionIndex || 0]
const addVersion = useCallback((version: GenRes) => {
setCurrentVersionIndex(() => versions?.length || 0)
setVersions((prev) => {
return [...prev!, version]
})
}, [setVersions, setCurrentVersionIndex, versions?.length])
return {
versions,
addVersion,
currentVersionIndex,
setCurrentVersionIndex,
current,
}
}
export default useGenData

View File

@@ -0,0 +1,103 @@
import React, { useCallback } from 'react'
import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
import { useBoolean } from 'ahooks'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiCheckLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
type Option = {
label: string
value: number
}
type VersionSelectorProps = {
versionLen: number;
value: number;
onChange: (index: number) => void;
}
const VersionSelector: React.FC<VersionSelectorProps> = ({ versionLen, value, onChange }) => {
const { t } = useTranslation()
const [isOpen, {
setFalse: handleOpenFalse,
toggle: handleOpenToggle,
set: handleOpenSet,
}] = useBoolean(false)
const moreThanOneVersion = versionLen > 1
const handleOpen = useCallback((value: boolean) => {
if (moreThanOneVersion)
handleOpenSet(value)
}, [moreThanOneVersion, handleOpenToggle])
const handleToggle = useCallback(() => {
if (moreThanOneVersion)
handleOpenToggle()
}, [moreThanOneVersion, handleOpenToggle])
const versions = Array.from({ length: versionLen }, (_, index) => ({
label: `${t('appDebug.generate.version')} ${index + 1}${index === versionLen - 1 ? ` · ${t('appDebug.generate.latest')}` : ''}`,
value: index,
}))
const isLatest = value === versionLen - 1
return (
<PortalToFollowElem
placement={'bottom-start'}
offset={{
mainAxis: 4,
crossAxis: -12,
}}
open={isOpen}
onOpenChange={handleOpen}
>
<PortalToFollowElemTrigger
onClick={handleToggle}
asChild
>
<div className={cn('system-xs-medium flex items-center text-text-tertiary', isOpen && 'text-text-secondary', moreThanOneVersion && 'cursor-pointer')}>
<div>{t('appDebug.generate.version')} {value + 1}{isLatest && ` · ${t('appDebug.generate.latest')}`}</div>
{moreThanOneVersion && <RiArrowDownSLine className='size-3 ' />}
</div>
</PortalToFollowElemTrigger >
<PortalToFollowElemContent className={cn(
'z-[99]',
)}>
<div
className={cn(
'w-[208px] rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur p-1 shadow-lg',
)}
>
<div className={cn('system-xs-medium-uppercase flex h-[22px] items-center px-3 pl-3 text-text-tertiary')}>
{t('appDebug.generate.versions')}
</div>
{
versions.map(option => (
<div
key={option.value}
className={cn(
'system-sm-medium flex h-7 cursor-pointer items-center rounded-lg px-2 text-text-secondary hover:bg-state-base-hover',
)}
title={option.label}
onClick={() => {
onChange(option.value)
handleOpenFalse()
}}
>
<div className='mr-1 grow truncate px-1 pl-1'>
{option.label}
</div>
{
value === option.value && <RiCheckLine className='h-4 w-4 shrink-0 text-text-accent' />
}
</div>
))
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem >
)
}
export default VersionSelector

View File

@@ -1,16 +1,13 @@
import type { FC } from 'react'
import React, { useCallback, useEffect } from 'react'
import cn from 'classnames'
import React, { useCallback, useEffect, useState } from 'react'
import useBoolean from 'ahooks/lib/useBoolean'
import { useTranslation } from 'react-i18next'
import ConfigPrompt from '../../config-prompt'
import { languageMap } from '../../../../workflow/nodes/_base/components/editor/code-editor/index'
import { generateRuleCode } from '@/service/debug'
import type { CodeGenRes } from '@/service/debug'
import { generateRule } from '@/service/debug'
import type { GenRes } from '@/service/debug'
import type { ModelModeType } from '@/types/app'
import type { AppType, CompletionParams, Model } from '@/types/app'
import Modal from '@/app/components/base/modal'
import Textarea from '@/app/components/base/textarea'
import Button from '@/app/components/base/button'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import Toast from '@/app/components/base/toast'
@@ -21,17 +18,33 @@ import { useModelListAndDefaultModelAndCurrentProviderAndModel } from '@/app/com
import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import ModelParameterModal from '@/app/components/header/account-setting/model-provider-page/model-parameter-modal'
import type { FormValue } from '@/app/components/header/account-setting/model-provider-page/declarations'
import IdeaOutput from '../automatic/idea-output'
import { GeneratorType } from '../automatic/types'
import InstructionEditor from '../automatic/instruction-editor-in-workflow'
import useGenData from '../automatic/use-gen-data'
import Result from '../automatic/result'
import ResPlaceholder from '../automatic/res-placeholder'
import { useGenerateRuleTemplate } from '@/service/use-apps'
import { useSessionStorageState } from 'ahooks'
import s from '../automatic/style.module.css'
const i18nPrefix = 'appDebug.generate'
export type IGetCodeGeneratorResProps = {
flowId: string
nodeId: string
currentCode?: string
mode: AppType
isShow: boolean
codeLanguages: CodeLanguage
onClose: () => void
onFinished: (res: CodeGenRes) => void
onFinished: (res: GenRes) => void
}
export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
{
flowId,
nodeId,
currentCode,
mode,
isShow,
codeLanguages,
@@ -61,9 +74,25 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
const {
defaultModel,
} = useModelListAndDefaultModelAndCurrentProviderAndModel(ModelTypeEnum.textGeneration)
const [instruction, setInstruction] = React.useState<string>('')
const [instructionFromSessionStorage, setInstruction] = useSessionStorageState<string>(`improve-instruction-${flowId}-${nodeId}`)
const instruction = instructionFromSessionStorage || ''
const [ideaOutput, setIdeaOutput] = useState<string>('')
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
const [res, setRes] = React.useState<CodeGenRes | null>(null)
const storageKey = `${flowId}-${nodeId}`
const { addVersion, current, currentVersionIndex, setCurrentVersionIndex, versions } = useGenData({
storageKey,
})
const [editorKey, setEditorKey] = useState(`${flowId}-0`)
const { data: instructionTemplate } = useGenerateRuleTemplate(GeneratorType.code)
useEffect(() => {
if (!instruction && instructionTemplate)
setInstruction(instructionTemplate.data)
setEditorKey(`${flowId}-${Date.now()}`)
}, [instructionTemplate])
const isValid = () => {
if (instruction.trim() === '') {
Toast.notify({
@@ -97,7 +126,6 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
localStorage.setItem('auto-gen-model', JSON.stringify(newModel))
}, [model, setModel])
const isInLLMNode = true
const onGenerate = async () => {
if (!isValid())
return
@@ -105,25 +133,35 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
return
setLoadingTrue()
try {
const { error, ...res } = await generateRuleCode({
const { error, ...res } = await generateRule({
flow_id: flowId,
node_id: nodeId,
current: currentCode,
instruction,
model_config: model,
no_variable: !!isInLLMNode,
code_language: languageMap[codeLanguages] || 'javascript',
ideal_output: ideaOutput,
language: languageMap[codeLanguages] || 'javascript',
})
setRes(res)
if (error) {
Toast.notify({
type: 'error',
message: error,
})
}
else {
addVersion(res)
}
}
finally {
setLoadingFalse()
}
}
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
const [isShowConfirmOverwrite, {
setTrue: showConfirmOverwrite,
setFalse: hideShowConfirmOverwrite,
}] = useBoolean(false)
useEffect(() => {
if (defaultModel) {
@@ -155,30 +193,20 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
<div className='text-[13px] text-text-tertiary'>{t('appDebug.codegen.loading')}</div>
</div>
)
const renderNoData = (
<div className='flex h-full w-0 grow flex-col items-center justify-center space-y-3 px-8'>
<Generator className='h-14 w-14 text-text-tertiary' />
<div className='text-center text-[13px] font-normal leading-5 text-text-tertiary'>
<div>{t('appDebug.codegen.noDataLine1')}</div>
<div>{t('appDebug.codegen.noDataLine2')}</div>
</div>
</div>
)
return (
<Modal
isShow={isShow}
onClose={onClose}
className='min-w-[1140px] !p-0'
closable
>
<div className='relative flex h-[680px] flex-wrap'>
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-8'>
<div className='mb-8'>
<div className={'text-lg font-bold leading-[28px] text-text-primary'}>{t('appDebug.codegen.title')}</div>
<div className='h-full w-[570px] shrink-0 overflow-y-auto border-r border-divider-regular p-6'>
<div className='mb-5'>
<div className={`text-lg font-bold leading-[28px] ${s.textGradient}`}>{t('appDebug.codegen.title')}</div>
<div className='mt-1 text-[13px] font-normal text-text-tertiary'>{t('appDebug.codegen.description')}</div>
</div>
<div className='mb-8'>
<div className='mb-4'>
<ModelParameterModal
popupClassName='!w-[520px]'
portalToFollowElemContentClassName='z-[1000]'
@@ -194,84 +222,60 @@ export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
</div>
<div>
<div className='text-[0px]'>
<div className='mb-2 text-sm font-medium leading-5 text-text-primary'>{t('appDebug.codegen.instruction')}</div>
<Textarea
className="h-[200px] resize-none"
placeholder={t('appDebug.codegen.instructionPlaceholder') || ''}
<div className='system-sm-semibold-uppercase mb-1.5 text-text-secondary'>{t('appDebug.codegen.instruction')}</div>
<InstructionEditor
editorKey={editorKey}
value={instruction}
onChange={e => setInstruction(e.target.value)}
onChange={setInstruction}
nodeId={nodeId}
generatorType={GeneratorType.code}
isShowCurrentBlock={!!currentCode}
/>
</div>
<IdeaOutput
value={ideaOutput}
onChange={setIdeaOutput}
/>
<div className='mt-5 flex justify-end'>
<div className='mt-7 flex justify-end space-x-2'>
<Button onClick={onClose}>{t(`${i18nPrefix}.dismiss`)}</Button>
<Button
className='flex space-x-1'
variant='primary'
onClick={onGenerate}
disabled={isLoading}
>
<Generator className='h-4 w-4 text-white' />
<span className='text-xs font-semibold text-white'>{t('appDebug.codegen.generate')}</span>
<Generator className='h-4 w-4' />
<span className='text-xs font-semibold '>{t('appDebug.codegen.generate')}</span>
</Button>
</div>
</div>
</div>
{isLoading && renderLoading}
{!isLoading && !res && renderNoData}
{(!isLoading && res) && (
<div className='h-full w-0 grow p-6 pb-0'>
<div className='mb-3 shrink-0 text-base font-semibold leading-[160%] text-text-secondary'>{t('appDebug.codegen.resTitle')}</div>
<div className={cn('max-h-[555px] overflow-y-auto', !isInLLMNode && 'pb-2')}>
<ConfigPrompt
mode={mode}
promptTemplate={res?.code || ''}
promptVariables={[]}
readonly
noTitle={isInLLMNode}
gradientBorder
editorHeight={isInLLMNode ? 524 : 0}
noResize={isInLLMNode}
/>
{!isInLLMNode && (
<>
{res?.code && (
<div className='mt-4'>
<h3 className='mb-2 text-sm font-medium text-text-primary'>{t('appDebug.codegen.generatedCode')}</h3>
<pre className='overflow-x-auto rounded-lg bg-gray-50 p-4'>
<code className={`language-${res.language}`}>
{res.code}
</code>
</pre>
</div>
)}
{res?.error && (
<div className='mt-4 rounded-lg bg-red-50 p-4'>
<p className='text-sm text-red-600'>{res.error}</p>
</div>
)}
</>
)}
</div>
<div className='flex justify-end bg-background-default py-4'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='ml-2' onClick={() => {
setShowConfirmOverwrite(true)
}}>{t('appDebug.codegen.apply')}</Button>
</div>
{!isLoading && !current && <ResPlaceholder />}
{(!isLoading && current) && (
<div className='h-full w-0 grow bg-background-default-subtle p-6 pb-0'>
<Result
current={current!}
currentVersionIndex={currentVersionIndex || 0}
setCurrentVersionIndex={setCurrentVersionIndex}
versions={versions || []}
onApply={showConfirmOverwrite}
generatorType={GeneratorType.code}
/>
</div>
)}
</div>
{showConfirmOverwrite && (
{isShowConfirmOverwrite && (
<Confirm
title={t('appDebug.codegen.overwriteConfirmTitle')}
content={t('appDebug.codegen.overwriteConfirmMessage')}
isShow={showConfirmOverwrite}
isShow
onConfirm={() => {
setShowConfirmOverwrite(false)
onFinished(res!)
hideShowConfirmOverwrite()
onFinished(current!)
}}
onCancel={() => setShowConfirmOverwrite(false)}
onCancel={hideShowConfirmOverwrite}
/>
)}
</Modal>