simple-prompt-input.tsx 9.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import { useBoolean } from 'ahooks'
  6. import {
  7. RiQuestionLine,
  8. } from '@remixicon/react'
  9. import produce from 'immer'
  10. import { useContext } from 'use-context-selector'
  11. import ConfirmAddVar from './confirm-add-var'
  12. import s from './style.module.css'
  13. import PromptEditorHeightResizeWrap from './prompt-editor-height-resize-wrap'
  14. import cn from '@/utils/classnames'
  15. import { type PromptVariable } from '@/models/debug'
  16. import Tooltip from '@/app/components/base/tooltip'
  17. import type { CompletionParams } from '@/types/app'
  18. import { AppType } from '@/types/app'
  19. import { getNewVar, getVars } from '@/utils/var'
  20. import AutomaticBtn from '@/app/components/app/configuration/config/automatic/automatic-btn'
  21. import type { AutomaticRes } from '@/service/debug'
  22. import GetAutomaticResModal from '@/app/components/app/configuration/config/automatic/get-automatic-res'
  23. import PromptEditor from '@/app/components/base/prompt-editor'
  24. import ConfigContext from '@/context/debug-configuration'
  25. import { useModalContext } from '@/context/modal-context'
  26. import type { ExternalDataTool } from '@/models/common'
  27. import { useToastContext } from '@/app/components/base/toast'
  28. import { useEventEmitterContextContext } from '@/context/event-emitter'
  29. import { ADD_EXTERNAL_DATA_TOOL } from '@/app/components/app/configuration/config-var'
  30. import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from '@/app/components/base/prompt-editor/plugins/variable-block'
  31. import { PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER } from '@/app/components/base/prompt-editor/plugins/update-block'
  32. import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
  33. export type ISimplePromptInput = {
  34. mode: AppType
  35. promptTemplate: string
  36. promptVariables: PromptVariable[]
  37. readonly?: boolean
  38. onChange?: (promp: string, promptVariables: PromptVariable[]) => void
  39. noTitle?: boolean
  40. gradientBorder?: boolean
  41. editorHeight?: number
  42. noResize?: boolean
  43. }
  44. const Prompt: FC<ISimplePromptInput> = ({
  45. mode,
  46. promptTemplate,
  47. promptVariables,
  48. readonly = false,
  49. onChange,
  50. noTitle,
  51. gradientBorder,
  52. editorHeight: initEditorHeight,
  53. noResize,
  54. }) => {
  55. const { t } = useTranslation()
  56. const media = useBreakpoints()
  57. const isMobile = media === MediaType.mobile
  58. const { eventEmitter } = useEventEmitterContextContext()
  59. const {
  60. modelConfig,
  61. completionParams,
  62. dataSets,
  63. setModelConfig,
  64. setPrevPromptConfig,
  65. setIntroduction,
  66. hasSetBlockStatus,
  67. showSelectDataSet,
  68. externalDataToolsConfig,
  69. isAgent,
  70. } = useContext(ConfigContext)
  71. const { notify } = useToastContext()
  72. const { setShowExternalDataToolModal } = useModalContext()
  73. const handleOpenExternalDataToolModal = () => {
  74. setShowExternalDataToolModal({
  75. payload: {},
  76. onSaveCallback: (newExternalDataTool: ExternalDataTool) => {
  77. eventEmitter?.emit({
  78. type: ADD_EXTERNAL_DATA_TOOL,
  79. payload: newExternalDataTool,
  80. } as any)
  81. eventEmitter?.emit({
  82. type: INSERT_VARIABLE_VALUE_BLOCK_COMMAND,
  83. payload: newExternalDataTool.variable,
  84. } as any)
  85. },
  86. onValidateBeforeSaveCallback: (newExternalDataTool: ExternalDataTool) => {
  87. for (let i = 0; i < promptVariables.length; i++) {
  88. if (promptVariables[i].key === newExternalDataTool.variable) {
  89. notify({ type: 'error', message: t('appDebug.varKeyError.keyAlreadyExists', { key: promptVariables[i].key }) })
  90. return false
  91. }
  92. }
  93. return true
  94. },
  95. })
  96. }
  97. const promptVariablesObj = (() => {
  98. const obj: Record<string, boolean> = {}
  99. promptVariables.forEach((item) => {
  100. obj[item.key] = true
  101. })
  102. return obj
  103. })()
  104. const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables)
  105. const [newTemplates, setNewTemplates] = React.useState('')
  106. const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
  107. const handleChange = (newTemplates: string, keys: string[]) => {
  108. const newPromptVariables = keys.filter(key => !(key in promptVariablesObj) && !externalDataToolsConfig.find(item => item.variable === key)).map(key => getNewVar(key, ''))
  109. if (newPromptVariables.length > 0) {
  110. setNewPromptVariables(newPromptVariables)
  111. setNewTemplates(newTemplates)
  112. showConfirmAddVar()
  113. return
  114. }
  115. onChange?.(newTemplates, [])
  116. }
  117. const handleAutoAdd = (isAdd: boolean) => {
  118. return () => {
  119. onChange?.(newTemplates, isAdd ? newPromptVariables : [])
  120. hideConfirmAddVar()
  121. }
  122. }
  123. const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
  124. const handleAutomaticRes = (res: AutomaticRes) => {
  125. // put eventEmitter in first place to prevent overwrite the configs.prompt_variables.But another problem is that prompt won't hight the prompt_variables.
  126. eventEmitter?.emit({
  127. type: PROMPT_EDITOR_UPDATE_VALUE_BY_EVENT_EMITTER,
  128. payload: res.prompt,
  129. } as any)
  130. const newModelConfig = produce(modelConfig, (draft) => {
  131. draft.configs.prompt_template = res.prompt
  132. draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true }))
  133. })
  134. setModelConfig(newModelConfig)
  135. setPrevPromptConfig(modelConfig.configs)
  136. if (mode !== AppType.completion)
  137. setIntroduction(res.opening_statement)
  138. showAutomaticFalse()
  139. }
  140. const minHeight = initEditorHeight || 228
  141. const [editorHeight, setEditorHeight] = useState(minHeight)
  142. return (
  143. <div className={cn((!readonly || gradientBorder) ? `${s.gradientBorder}` : 'bg-gray-50', ' relative shadow-md')}>
  144. <div className='rounded-xl bg-[#EEF4FF]'>
  145. {!noTitle && (
  146. <div className="flex justify-between items-center h-11 pl-3 pr-6">
  147. <div className="flex items-center space-x-1">
  148. <div className='h2'>{mode !== AppType.completion ? t('appDebug.chatSubTitle') : t('appDebug.completionSubTitle')}</div>
  149. {!readonly && (
  150. <Tooltip
  151. htmlContent={<div className='w-[180px]'>
  152. {t('appDebug.promptTip')}
  153. </div>}
  154. selector='config-prompt-tooltip'>
  155. <RiQuestionLine className='w-[14px] h-[14px] text-indigo-400' />
  156. </Tooltip>
  157. )}
  158. </div>
  159. <div className='flex items-center'>
  160. {!readonly && !isMobile && (
  161. <AutomaticBtn onClick={showAutomaticTrue} />
  162. )}
  163. </div>
  164. </div>
  165. )}
  166. <PromptEditorHeightResizeWrap
  167. className='px-4 pt-2 min-h-[228px] bg-white rounded-t-xl text-sm text-gray-700'
  168. height={editorHeight}
  169. minHeight={minHeight}
  170. onHeightChange={setEditorHeight}
  171. hideResize={noResize}
  172. footer={(
  173. <div className='pl-4 pb-2 flex bg-white rounded-b-xl'>
  174. <div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{promptTemplate.length}</div>
  175. </div>
  176. )}
  177. >
  178. <PromptEditor
  179. className='min-h-[210px]'
  180. compact
  181. value={promptTemplate}
  182. contextBlock={{
  183. show: false,
  184. selectable: !hasSetBlockStatus.context,
  185. datasets: dataSets.map(item => ({
  186. id: item.id,
  187. name: item.name,
  188. type: item.data_source_type,
  189. })),
  190. onAddContext: showSelectDataSet,
  191. }}
  192. variableBlock={{
  193. show: true,
  194. variables: modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({
  195. name: item.name,
  196. value: item.key,
  197. })),
  198. }}
  199. externalToolBlock={{
  200. show: true,
  201. externalTools: modelConfig.configs.prompt_variables.filter(item => item.type === 'api').map(item => ({
  202. name: item.name,
  203. variableName: item.key,
  204. icon: item.icon,
  205. icon_background: item.icon_background,
  206. })),
  207. onAddExternalTool: handleOpenExternalDataToolModal,
  208. }}
  209. historyBlock={{
  210. show: false,
  211. selectable: false,
  212. history: {
  213. user: '',
  214. assistant: '',
  215. },
  216. onEditRole: () => { },
  217. }}
  218. queryBlock={{
  219. show: false,
  220. selectable: !hasSetBlockStatus.query,
  221. }}
  222. onChange={(value) => {
  223. handleChange?.(value, [])
  224. }}
  225. onBlur={() => {
  226. handleChange(promptTemplate, getVars(promptTemplate))
  227. }}
  228. editable={!readonly}
  229. />
  230. </PromptEditorHeightResizeWrap>
  231. </div>
  232. {isShowConfirmAddVar && (
  233. <ConfirmAddVar
  234. varNameArr={newPromptVariables.map(v => v.name)}
  235. onConfrim={handleAutoAdd(true)}
  236. onCancel={handleAutoAdd(false)}
  237. onHide={hideConfirmAddVar}
  238. />
  239. )}
  240. {showAutomatic && (
  241. <GetAutomaticResModal
  242. mode={mode as AppType}
  243. model={
  244. {
  245. provider: modelConfig.provider,
  246. name: modelConfig.model_id,
  247. mode: modelConfig.mode,
  248. completion_params: completionParams as CompletionParams,
  249. }
  250. }
  251. isShow={showAutomatic}
  252. onClose={showAutomaticFalse}
  253. onFinished={handleAutomaticRes}
  254. />
  255. )}
  256. </div>
  257. )
  258. }
  259. export default React.memo(Prompt)