index.tsx 9.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252
  1. 'use client'
  2. import type { FC } from 'react'
  3. import React, { useState } from 'react'
  4. import { useTranslation } from 'react-i18next'
  5. import { useContext } from 'use-context-selector'
  6. import {
  7. RiArrowDownSLine,
  8. RiArrowRightLine,
  9. } from '@remixicon/react'
  10. import {
  11. PlayIcon,
  12. } from '@heroicons/react/24/solid'
  13. import ConfigContext from '@/context/debug-configuration'
  14. import type { Inputs, PromptVariable } from '@/models/debug'
  15. import { AppType, ModelModeType } from '@/types/app'
  16. import Select from '@/app/components/base/select'
  17. import { DEFAULT_VALUE_MAX_LEN } from '@/config'
  18. import Button from '@/app/components/base/button'
  19. import Tooltip from '@/app/components/base/tooltip-plus'
  20. import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
  21. import type { VisionFile, VisionSettings } from '@/types/app'
  22. export type IPromptValuePanelProps = {
  23. appType: AppType
  24. onSend?: () => void
  25. inputs: Inputs
  26. visionConfig: VisionSettings
  27. onVisionFilesChange: (files: VisionFile[]) => void
  28. }
  29. const PromptValuePanel: FC<IPromptValuePanelProps> = ({
  30. appType,
  31. onSend,
  32. inputs,
  33. visionConfig,
  34. onVisionFilesChange,
  35. }) => {
  36. const { t } = useTranslation()
  37. const { modelModeType, modelConfig, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
  38. const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
  39. const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
  40. return key && key?.trim() && name && name?.trim()
  41. })
  42. const promptVariableObj = (() => {
  43. const obj: Record<string, boolean> = {}
  44. promptVariables.forEach((input) => {
  45. obj[input.key] = true
  46. })
  47. return obj
  48. })()
  49. const canNotRun = (() => {
  50. if (mode !== AppType.completion)
  51. return true
  52. if (isAdvancedMode) {
  53. if (modelModeType === ModelModeType.chat)
  54. return chatPromptConfig.prompt.every(({ text }) => !text)
  55. return !completionPromptConfig.prompt?.text
  56. }
  57. else { return !modelConfig.configs.prompt_template }
  58. })()
  59. const renderRunButton = () => {
  60. return (
  61. <Button
  62. variant="primary"
  63. disabled={canNotRun}
  64. onClick={() => onSend && onSend()}
  65. className="w-[80px] !h-8">
  66. <PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
  67. <span className='uppercase text-[13px]'>{t('appDebug.inputs.run')}</span>
  68. </Button>
  69. )
  70. }
  71. const handleInputValueChange = (key: string, value: string) => {
  72. if (!(key in promptVariableObj))
  73. return
  74. const newInputs = { ...inputs }
  75. promptVariables.forEach((input) => {
  76. if (input.key === key)
  77. newInputs[key] = value
  78. })
  79. setInputs(newInputs)
  80. }
  81. const onClear = () => {
  82. const newInputs: Record<string, any> = {}
  83. promptVariables.forEach((item) => {
  84. newInputs[item.key] = ''
  85. })
  86. setInputs(newInputs)
  87. }
  88. return (
  89. <div className="pb-3 border border-gray-200 bg-white rounded-xl" style={{
  90. boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
  91. }}>
  92. <div className={'mt-3 px-4 bg-white'}>
  93. <div className={
  94. `${!userInputFieldCollapse && 'mb-2'}`
  95. }>
  96. <div className='flex items-center space-x-1 cursor-pointer' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
  97. {
  98. userInputFieldCollapse
  99. ? <RiArrowRightLine className='w-3 h-3 text-gray-300' />
  100. : <RiArrowDownSLine className='w-3 h-3 text-gray-300' />
  101. }
  102. <div className='text-xs font-medium text-gray-800 uppercase'>{t('appDebug.inputs.userInputField')}</div>
  103. </div>
  104. {appType === AppType.completion && promptVariables.length > 0 && !userInputFieldCollapse && (
  105. <div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
  106. )}
  107. </div>
  108. {!userInputFieldCollapse && (
  109. <>
  110. {
  111. promptVariables.length > 0
  112. ? (
  113. <div className="space-y-3 ">
  114. {promptVariables.map(({ key, name, type, options, max_length, required }) => (
  115. <div key={key} className="xl:flex justify-between">
  116. <div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
  117. {type === 'select' && (
  118. <Select
  119. className='w-full'
  120. defaultValue={inputs[key] as string}
  121. onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
  122. items={(options || []).map(i => ({ name: i, value: i }))}
  123. allowSearch={false}
  124. bgClassName='bg-gray-50'
  125. />
  126. )
  127. }
  128. {type === 'string' && (
  129. <input
  130. className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
  131. placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  132. type="text"
  133. value={inputs[key] ? `${inputs[key]}` : ''}
  134. onChange={(e) => { handleInputValueChange(key, e.target.value) }}
  135. maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
  136. />
  137. )}
  138. {type === 'paragraph' && (
  139. <textarea
  140. className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-[120px] bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
  141. placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  142. value={inputs[key] ? `${inputs[key]}` : ''}
  143. onChange={(e) => { handleInputValueChange(key, e.target.value) }}
  144. />
  145. )}
  146. {type === 'number' && (
  147. <input
  148. className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
  149. placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
  150. type="number"
  151. value={inputs[key] ? `${inputs[key]}` : ''}
  152. onChange={(e) => { handleInputValueChange(key, e.target.value) }}
  153. />
  154. )}
  155. </div>
  156. ))}
  157. </div>
  158. )
  159. : (
  160. <div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
  161. )
  162. }
  163. {
  164. appType === AppType.completion && visionConfig?.enabled && (
  165. <div className="mt-3 xl:flex justify-between">
  166. <div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div>
  167. <div className='grow'>
  168. <TextGenerationImageUploader
  169. settings={visionConfig}
  170. onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
  171. type: 'image',
  172. transfer_method: fileItem.type,
  173. url: fileItem.url,
  174. upload_file_id: fileItem.fileId,
  175. })))}
  176. />
  177. </div>
  178. </div>
  179. )
  180. }
  181. </>
  182. )
  183. }
  184. </div>
  185. {
  186. appType === AppType.completion && (
  187. <div>
  188. <div className="mt-5 border-b border-gray-100"></div>
  189. <div className="flex justify-between mt-4 px-4">
  190. <Button
  191. className='!h-8 !p-3'
  192. onClick={onClear}
  193. disabled={false}
  194. >
  195. <span className='text-[13px]'>{t('common.operation.clear')}</span>
  196. </Button>
  197. {canNotRun
  198. ? (<Tooltip
  199. popupContent={t('appDebug.otherError.promptNoBeEmpty')}
  200. >
  201. {renderRunButton()}
  202. </Tooltip>)
  203. : renderRunButton()}
  204. </div>
  205. </div>
  206. )
  207. }
  208. </div>
  209. )
  210. }
  211. export default React.memo(PromptValuePanel)
  212. function replaceStringWithValuesWithFormat(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
  213. return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
  214. const name = inputs[key]
  215. if (name) { // has set value
  216. return `<div class='inline-block px-1 rounded-md text-gray-900' style='background: rgba(16, 24, 40, 0.1)'>${name}</div>`
  217. }
  218. const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
  219. return `<div class='inline-block px-1 rounded-md text-gray-500' style='background: rgba(16, 24, 40, 0.05)'>${valueObj ? valueObj.name : match}</div>`
  220. })
  221. }
  222. export function replaceStringWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
  223. return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
  224. const name = inputs[key]
  225. if (name) { // has set value
  226. return name
  227. }
  228. const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
  229. return valueObj ? `{{${valueObj.name}}}` : match
  230. })
  231. }
  232. // \n -> br
  233. function format(str: string) {
  234. return str.replaceAll('\n', '<br>')
  235. }