feat: add code generator (#9051)

Co-authored-by: crazywoola <100913391+crazywoola@users.noreply.github.com>
This commit is contained in:
Kota-Yamaguchi
2024-10-22 22:57:54 +09:00
committed by GitHub
parent 0e965b6529
commit a7ee51e5d8
12 changed files with 470 additions and 2 deletions

View File

@@ -0,0 +1,200 @@
import type { FC } from 'react'
import React from 'react'
import cn from 'classnames'
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 { ModelModeType } from '@/types/app'
import type { AppType, Model } from '@/types/app'
import Modal from '@/app/components/base/modal'
import Button from '@/app/components/base/button'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import Toast from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import Confirm from '@/app/components/base/confirm'
import type { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
export type IGetCodeGeneratorResProps = {
mode: AppType
isShow: boolean
codeLanguages: CodeLanguage
onClose: () => void
onFinished: (res: CodeGenRes) => void
}
export const GetCodeGeneratorResModal: FC<IGetCodeGeneratorResProps> = (
{
mode,
isShow,
codeLanguages,
onClose,
onFinished,
},
) => {
const { t } = useTranslation()
const [instruction, setInstruction] = React.useState<string>('')
const [isLoading, { setTrue: setLoadingTrue, setFalse: setLoadingFalse }] = useBoolean(false)
const [res, setRes] = React.useState<CodeGenRes | null>(null)
const isValid = () => {
if (instruction.trim() === '') {
Toast.notify({
type: 'error',
message: t('common.errorMsg.fieldRequired', {
field: t('appDebug.code.instruction'),
}),
})
return false
}
return true
}
const model: Model = {
provider: 'openai',
name: 'gpt-4o-mini',
mode: ModelModeType.chat,
completion_params: {
temperature: 0.7,
max_tokens: 0,
top_p: 0,
echo: false,
stop: [],
presence_penalty: 0,
frequency_penalty: 0,
},
}
const isInLLMNode = true
const onGenerate = async () => {
if (!isValid())
return
if (isLoading)
return
setLoadingTrue()
try {
const { error, ...res } = await generateRuleCode({
instruction,
model_config: model,
no_variable: !!isInLLMNode,
code_language: languageMap[codeLanguages] || 'javascript',
})
setRes(res)
if (error) {
Toast.notify({
type: 'error',
message: error,
})
}
}
finally {
setLoadingFalse()
}
}
const [showConfirmOverwrite, setShowConfirmOverwrite] = React.useState(false)
const renderLoading = (
<div className='w-0 grow flex flex-col items-center justify-center h-full space-y-3'>
<Loading />
<div className='text-[13px] text-gray-400'>{t('appDebug.codegen.loading')}</div>
</div>
)
return (
<Modal
isShow={isShow}
onClose={onClose}
className='!p-0 min-w-[1140px]'
closable
>
<div className='relative flex h-[680px] flex-wrap'>
<div className='w-[570px] shrink-0 p-8 h-full overflow-y-auto border-r border-gray-100'>
<div className='mb-8'>
<div className={'leading-[28px] text-lg font-bold'}>{t('appDebug.codegen.title')}</div>
<div className='mt-1 text-[13px] font-normal text-gray-500'>{t('appDebug.codegen.description')}</div>
</div>
<div className='mt-6'>
<div className='text-[0px]'>
<div className='mb-2 leading-5 text-sm font-medium text-gray-900'>{t('appDebug.codegen.instruction')}</div>
<textarea
className="w-full h-[200px] overflow-y-auto px-3 py-2 text-sm bg-gray-50 rounded-lg"
placeholder={t('appDebug.codegen.instructionPlaceholder') || ''}
value={instruction}
onChange={e => setInstruction(e.target.value)}
/>
</div>
<div className='mt-5 flex justify-end'>
<Button
className='flex space-x-1'
variant='primary'
onClick={onGenerate}
disabled={isLoading}
>
<Generator className='w-4 h-4 text-white' />
<span className='text-xs font-semibold text-white'>{t('appDebug.codegen.generate')}</span>
</Button>
</div>
</div>
</div>
{isLoading && renderLoading}
{(!isLoading && res) && (
<div className='w-0 grow p-6 pb-0 h-full'>
<div className='shrink-0 mb-3 leading-[160%] text-base font-semibold text-gray-800'>{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-gray-900'>{t('appDebug.codegen.generatedCode')}</h3>
<pre className='p-4 bg-gray-50 rounded-lg overflow-x-auto'>
<code className={`language-${res.language}`}>
{res.code}
</code>
</pre>
</div>
)}
{res?.error && (
<div className='mt-4 p-4 bg-red-50 rounded-lg'>
<p className='text-sm text-red-600'>{res.error}</p>
</div>
)}
</>
)}
</div>
<div className='flex justify-end py-4 bg-white'>
<Button onClick={onClose}>{t('common.operation.cancel')}</Button>
<Button variant='primary' className='ml-2' onClick={() => {
setShowConfirmOverwrite(true)
}}>{t('appDebug.codegen.apply')}</Button>
</div>
</div>
)}
</div>
{showConfirmOverwrite && (
<Confirm
title={t('appDebug.codegen.overwriteConfirmTitle')}
content={t('appDebug.codegen.overwriteConfirmMessage')}
isShow={showConfirmOverwrite}
onConfirm={() => {
setShowConfirmOverwrite(false)
onFinished(res!)
}}
onCancel={() => setShowConfirmOverwrite(false)}
/>
)}
</Modal>
)
}
export default React.memo(GetCodeGeneratorResModal)

View File

@@ -0,0 +1,48 @@
'use client'
import type { FC } from 'react'
import React, { useCallback } from 'react'
import { useBoolean } from 'ahooks'
import cn from 'classnames'
import type { CodeLanguage } from '../../code/types'
import { Generator } from '@/app/components/base/icons/src/vender/other'
import { ActionButton } from '@/app/components/base/action-button'
import { AppType } from '@/types/app'
import type { CodeGenRes } from '@/service/debug'
import { GetCodeGeneratorResModal } from '@/app/components/app/configuration/config/code-generator/get-code-generator-res'
type Props = {
className?: string
onGenerated?: (prompt: string) => void
codeLanguages: CodeLanguage
}
const CodeGenerateBtn: FC<Props> = ({
className,
codeLanguages,
onGenerated,
}) => {
const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
const handleAutomaticRes = useCallback((res: CodeGenRes) => {
onGenerated?.(res.code)
showAutomaticFalse()
}, [onGenerated, showAutomaticFalse])
return (
<div className={cn(className)}>
<ActionButton
className='hover:bg-[#155EFF]/8'
onClick={showAutomaticTrue}>
<Generator className='w-4 h-4 text-primary-600' />
</ActionButton>
{showAutomatic && (
<GetCodeGeneratorResModal
mode={AppType.chat}
isShow={showAutomatic}
codeLanguages={codeLanguages}
onClose={showAutomaticFalse}
onFinished={handleAutomaticRes}
/>
)}
</div>
)
}
export default React.memo(CodeGenerateBtn)

View File

@@ -2,6 +2,9 @@
import type { FC } from 'react'
import React, { useCallback, useRef, useState } from 'react'
import copy from 'copy-to-clipboard'
import ToggleExpandBtn from '../toggle-expand-btn'
import CodeGeneratorButton from '../code-generator-button'
import type { CodeLanguage } from '../../../code/types'
import Wrap from './wrap'
import cn from '@/utils/classnames'
import PromptEditorHeightResizeWrap from '@/app/components/app/configuration/config-prompt/prompt-editor-height-resize-wrap'
@@ -9,7 +12,6 @@ import {
Clipboard,
ClipboardCheck,
} from '@/app/components/base/icons/src/vender/line/files'
import ToggleExpandBtn from '@/app/components/workflow/nodes/_base/components/toggle-expand-btn'
import useToggleExpend from '@/app/components/workflow/nodes/_base/hooks/use-toggle-expend'
import type { FileEntity } from '@/app/components/base/file-uploader/types'
import FileListInLog from '@/app/components/base/file-uploader/file-list-in-log'
@@ -23,6 +25,8 @@ type Props = {
value: string
isFocus: boolean
isInNode?: boolean
onGenerated?: (prompt: string) => void
codeLanguages: CodeLanguage
fileList?: FileEntity[]
showFileList?: boolean
}
@@ -36,6 +40,8 @@ const Base: FC<Props> = ({
value,
isFocus,
isInNode,
onGenerated,
codeLanguages,
fileList = [],
showFileList,
}) => {
@@ -70,6 +76,9 @@ const Base: FC<Props> = ({
e.stopPropagation()
}}>
{headerRight}
<div className='ml-1'>
<CodeGeneratorButton onGenerated={onGenerated} codeLanguages={codeLanguages}/>
</div>
{!isCopied
? (
<Clipboard className='mx-1 w-3.5 h-3.5 text-gray-500 cursor-pointer' onClick={handleCopy} />
@@ -78,6 +87,7 @@ const Base: FC<Props> = ({
<ClipboardCheck className='mx-1 w-3.5 h-3.5 text-gray-500' />
)
}
<div className='ml-1'>
<ToggleExpandBtn isExpand={isExpand} onExpandChange={setIsExpand} />
</div>

View File

@@ -33,7 +33,7 @@ export type Props = {
showFileList?: boolean
}
const languageMap = {
export const languageMap = {
[CodeLanguage.javascript]: 'javascript',
[CodeLanguage.python3]: 'python',
[CodeLanguage.json]: 'json',
@@ -149,6 +149,9 @@ const CodeEditor: FC<Props> = ({
return isFocus ? 'focus-theme' : 'blur-theme'
})()
const handleGenerated = (code: string) => {
handleEditorChange(code)
}
const main = (
<>
@@ -200,6 +203,8 @@ const CodeEditor: FC<Props> = ({
isFocus={isFocus && !readOnly}
minHeight={minHeight}
isInNode={isInNode}
onGenerated={handleGenerated}
codeLanguages={language}
fileList={fileList}
showFileList={showFileList}
>

View File

@@ -219,6 +219,20 @@ const translation = {
manage: 'Manage',
},
},
codegen: {
title: 'Code Generator',
description: 'The Code Generator uses configured models to generate high-quality code based on your instructions. Please provide clear and detailed instructions.',
instruction: 'Instructions',
instructionPlaceholder: 'Enter detailed description of the code you want to generate.',
generate: 'Generate',
generatedCodeTitle: 'Generated Code',
loading: 'Generating code...',
apply: 'Apply',
applyChanges: 'Apply Changes',
resTitle: 'Generated Code',
overwriteConfirmTitle: 'Overwrite existing code?',
overwriteConfirmMessage: 'This action will overwrite the existing code. Do you want to continue?',
},
generate: {
title: 'Prompt Generator',
description: 'The Prompt Generator uses the configured model to optimize prompts for higher quality and better structure. Please write clear and detailed instructions.',

View File

@@ -199,6 +199,20 @@ const translation = {
},
},
},
codegen: {
title: 'コードジェネレーター',
description: 'コードジェネレーターは、設定されたモデルを使用して指示に基づいて高品質なコードを生成します。明確で詳細な指示を提供してください。',
instruction: '指示',
instructionPlaceholder: '生成したいコードの詳細な説明を入力してください。',
generate: '生成',
generatedCodeTitle: '生成されたコード',
loading: 'コードを生成中...',
apply: '適用',
applyChanges: '変更を適用',
resTitle: '生成されたコード',
overwriteConfirmTitle: '既存のコードを上書きしますか?',
overwriteConfirmMessage: 'この操作は既存のコードを上書きします。続行しますか?',
},
generate: {
title: 'プロンプト生成器',
description: 'プロンプト生成器は、設定済みのモデルを使って、高品質で構造的に優れたプロンプトを作成するための最適化を行います。具体的で詳細な指示をお書きください。',

View File

@@ -219,6 +219,20 @@ const translation = {
manage: '管理',
},
},
codegen: {
title: '代码生成器',
description: '代码生成器使用配置的模型根据您的指令生成高质量的代码。请提供清晰详细的说明。',
instruction: '指令',
instructionPlaceholder: '请输入您想要生成的代码的详细描述。',
generate: '生成',
generatedCodeTitle: '生成的代码',
loading: '正在生成代码...',
apply: '应用',
applyChanges: '应用更改',
resTitle: '生成的代码',
overwriteConfirmTitle: '是否覆盖现有代码?',
overwriteConfirmMessage: '此操作将覆盖现有代码。您确定要继续吗?',
},
generate: {
title: '提示词生成器',
description: '提示词生成器使用配置的模型来优化提示词,以获得更高的质量和更好的结构。请写出清晰详细的说明。',

View File

@@ -9,6 +9,11 @@ export type AutomaticRes = {
opening_statement: string
error?: string
}
export type CodeGenRes = {
code: string
language: string[]
error?: string
}
export const sendChatMessage = async (appId: string, body: Record<string, any>, { onData, onCompleted, onThought, onFile, onError, getAbortController, onMessageEnd, onMessageReplace }: {
onData: IOnData
@@ -71,6 +76,11 @@ export const generateRule = (body: Record<string, any>) => {
body,
})
}
export const generateRuleCode = (body: Record<string, any>) => {
return post<CodeGenRes>('/rule-code-generate', {
body,
})
}
export const fetchModelParams = (providerName: string, modelId: string) => {
return get(`workspaces/current/model-providers/${providerName}/models/parameter-rules`, {