index.tsx 9.3 KB

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