index.tsx 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315
  1. /* eslint-disable multiline-ternary */
  2. 'use client'
  3. import type { FC } from 'react'
  4. import React, { useEffect, useRef, useState } from 'react'
  5. import produce from 'immer'
  6. import cn from 'classnames'
  7. import {
  8. RiAddLine,
  9. RiDeleteBinLine,
  10. } from '@remixicon/react'
  11. import { useTranslation } from 'react-i18next'
  12. import { useBoolean } from 'ahooks'
  13. import { ReactSortable } from 'react-sortablejs'
  14. import {
  15. useFeatures,
  16. useFeaturesStore,
  17. } from '../../hooks'
  18. import type { OnFeaturesChange } from '../../types'
  19. import Panel from '@/app/components/app/configuration/base/feature-panel'
  20. import Button from '@/app/components/base/button'
  21. import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
  22. import { getInputKeys } from '@/app/components/base/block-input'
  23. import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
  24. import { getNewVar } from '@/utils/var'
  25. import { varHighlightHTML } from '@/app/components/app/configuration/base/var-highlight'
  26. import type { PromptVariable } from '@/models/debug'
  27. const MAX_QUESTION_NUM = 5
  28. export type OpeningStatementProps = {
  29. onChange?: OnFeaturesChange
  30. readonly?: boolean
  31. promptVariables?: PromptVariable[]
  32. onAutoAddPromptVariable: (variable: PromptVariable[]) => void
  33. }
  34. // regex to match the {{}} and replace it with a span
  35. const regex = /\{\{([^}]+)\}\}/g
  36. const OpeningStatement: FC<OpeningStatementProps> = ({
  37. onChange,
  38. readonly,
  39. promptVariables = [],
  40. onAutoAddPromptVariable,
  41. }) => {
  42. const { t } = useTranslation()
  43. const featureStore = useFeaturesStore()
  44. const openingStatement = useFeatures(s => s.features.opening)
  45. const value = openingStatement?.opening_statement || ''
  46. const suggestedQuestions = openingStatement?.suggested_questions || []
  47. const [notIncludeKeys, setNotIncludeKeys] = useState<string[]>([])
  48. const hasValue = !!(value || '').trim()
  49. const inputRef = useRef<HTMLTextAreaElement>(null)
  50. const [isFocus, { setTrue: didSetFocus, setFalse: setBlur }] = useBoolean(false)
  51. const setFocus = () => {
  52. didSetFocus()
  53. setTimeout(() => {
  54. const input = inputRef.current
  55. if (input) {
  56. input.focus()
  57. input.setSelectionRange(input.value.length, input.value.length)
  58. }
  59. }, 0)
  60. }
  61. const [tempValue, setTempValue] = useState(value)
  62. useEffect(() => {
  63. setTempValue(value || '')
  64. }, [value])
  65. const [tempSuggestedQuestions, setTempSuggestedQuestions] = useState(suggestedQuestions || [])
  66. const notEmptyQuestions = tempSuggestedQuestions.filter(question => !!question && question.trim())
  67. const coloredContent = (tempValue || '')
  68. .replace(/</g, '&lt;')
  69. .replace(/>/g, '&gt;')
  70. .replace(regex, varHighlightHTML({ name: '$1' })) // `<span class="${highLightClassName}">{{$1}}</span>`
  71. .replace(/\n/g, '<br />')
  72. const handleEdit = () => {
  73. if (readonly)
  74. return
  75. setFocus()
  76. }
  77. const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
  78. const handleCancel = () => {
  79. setBlur()
  80. setTempValue(value)
  81. setTempSuggestedQuestions(suggestedQuestions)
  82. }
  83. const handleConfirm = () => {
  84. const keys = getInputKeys(tempValue)
  85. const promptKeys = promptVariables.map(item => item.key)
  86. let notIncludeKeys: string[] = []
  87. if (promptKeys.length === 0) {
  88. if (keys.length > 0)
  89. notIncludeKeys = keys
  90. }
  91. else {
  92. notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
  93. }
  94. if (notIncludeKeys.length > 0) {
  95. setNotIncludeKeys(notIncludeKeys)
  96. showConfirmAddVar()
  97. return
  98. }
  99. setBlur()
  100. const { getState } = featureStore!
  101. const {
  102. features,
  103. setFeatures,
  104. } = getState()
  105. const newFeatures = produce(features, (draft) => {
  106. if (draft.opening) {
  107. draft.opening.opening_statement = tempValue
  108. draft.opening.suggested_questions = tempSuggestedQuestions
  109. }
  110. })
  111. setFeatures(newFeatures)
  112. if (onChange)
  113. onChange(newFeatures)
  114. }
  115. const cancelAutoAddVar = () => {
  116. const { getState } = featureStore!
  117. const {
  118. features,
  119. setFeatures,
  120. } = getState()
  121. const newFeatures = produce(features, (draft) => {
  122. if (draft.opening)
  123. draft.opening.opening_statement = tempValue
  124. })
  125. setFeatures(newFeatures)
  126. if (onChange)
  127. onChange(newFeatures)
  128. hideConfirmAddVar()
  129. setBlur()
  130. }
  131. const autoAddVar = () => {
  132. const { getState } = featureStore!
  133. const {
  134. features,
  135. setFeatures,
  136. } = getState()
  137. const newFeatures = produce(features, (draft) => {
  138. if (draft.opening)
  139. draft.opening.opening_statement = tempValue
  140. })
  141. setFeatures(newFeatures)
  142. if (onChange)
  143. onChange(newFeatures)
  144. onAutoAddPromptVariable([...notIncludeKeys.map(key => getNewVar(key, 'string'))])
  145. hideConfirmAddVar()
  146. setBlur()
  147. }
  148. const headerRight = !readonly ? (
  149. isFocus ? (
  150. <div className='flex items-center space-x-1'>
  151. <div className='px-3 leading-[18px] text-xs font-medium text-gray-700 cursor-pointer' onClick={handleCancel}>{t('common.operation.cancel')}</div>
  152. <Button className='!h-8 !px-3 text-xs' onClick={handleConfirm} variant="primary">{t('common.operation.save')}</Button>
  153. </div>
  154. ) : (
  155. <OperationBtn type='edit' actionName={hasValue ? '' : t('appDebug.openingStatement.writeOpener') as string} onClick={handleEdit} />
  156. )
  157. ) : null
  158. const renderQuestions = () => {
  159. return isFocus ? (
  160. <div>
  161. <div className='flex items-center py-2'>
  162. <div className='shrink-0 flex space-x-0.5 leading-[18px] text-xs font-medium text-gray-500'>
  163. <div className='uppercase'>{t('appDebug.openingStatement.openingQuestion')}</div>
  164. <div>·</div>
  165. <div>{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}</div>
  166. </div>
  167. <div className='ml-3 grow w-0 h-px bg-[#243, 244, 246]'></div>
  168. </div>
  169. <ReactSortable
  170. className="space-y-1"
  171. list={tempSuggestedQuestions.map((name, index) => {
  172. return {
  173. id: index,
  174. name,
  175. }
  176. })}
  177. setList={list => setTempSuggestedQuestions(list.map(item => item.name))}
  178. handle='.handle'
  179. ghostClass="opacity-50"
  180. animation={150}
  181. >
  182. {tempSuggestedQuestions.map((question, index) => {
  183. return (
  184. <div className='group relative rounded-lg border border-gray-200 flex items-center pl-2.5 hover:border-gray-300 hover:bg-white' key={index}>
  185. <div className='handle flex items-center justify-center w-4 h-4 cursor-grab'>
  186. <svg width="6" height="10" viewBox="0 0 6 10" fill="none" xmlns="http://www.w3.org/2000/svg">
  187. <path fillRule="evenodd" clipRule="evenodd" d="M1 2C1.55228 2 2 1.55228 2 1C2 0.447715 1.55228 0 1 0C0.447715 0 0 0.447715 0 1C0 1.55228 0.447715 2 1 2ZM1 6C1.55228 6 2 5.55228 2 5C2 4.44772 1.55228 4 1 4C0.447715 4 0 4.44772 0 5C0 5.55228 0.447715 6 1 6ZM6 1C6 1.55228 5.55228 2 5 2C4.44772 2 4 1.55228 4 1C4 0.447715 4.44772 0 5 0C5.55228 0 6 0.447715 6 1ZM5 6C5.55228 6 6 5.55228 6 5C6 4.44772 5.55228 4 5 4C4.44772 4 4 4.44772 4 5C4 5.55228 4.44772 6 5 6ZM2 9C2 9.55229 1.55228 10 1 10C0.447715 10 0 9.55229 0 9C0 8.44771 0.447715 8 1 8C1.55228 8 2 8.44771 2 9ZM5 10C5.55228 10 6 9.55229 6 9C6 8.44771 5.55228 8 5 8C4.44772 8 4 8.44771 4 9C4 9.55229 4.44772 10 5 10Z" fill="#98A2B3" />
  188. </svg>
  189. </div>
  190. <input
  191. type="input"
  192. value={question || ''}
  193. onChange={(e) => {
  194. const value = e.target.value
  195. setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => {
  196. if (index === i)
  197. return value
  198. return item
  199. }))
  200. }}
  201. className={'w-full overflow-x-auto pl-1.5 pr-8 text-sm leading-9 text-gray-900 border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer rounded-lg'}
  202. />
  203. <div
  204. className='block absolute top-1/2 translate-y-[-50%] right-1.5 p-1 rounded-md cursor-pointer hover:bg-[#FEE4E2] hover:text-[#D92D20]'
  205. onClick={() => {
  206. setTempSuggestedQuestions(tempSuggestedQuestions.filter((_, i) => index !== i))
  207. }}
  208. >
  209. <RiDeleteBinLine className='w-3.5 h-3.5' />
  210. </div>
  211. </div>
  212. )
  213. })}</ReactSortable>
  214. {tempSuggestedQuestions.length < MAX_QUESTION_NUM && (
  215. <div
  216. onClick={() => { setTempSuggestedQuestions([...tempSuggestedQuestions, '']) }}
  217. className='mt-1 flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-gray-400 bg-gray-100 hover:bg-gray-200'>
  218. <RiAddLine className='w-4 h-4' />
  219. <div className='text-gray-500 text-[13px]'>{t('appDebug.variableConig.addOption')}</div>
  220. </div>
  221. )}
  222. </div>
  223. ) : (
  224. <div className='mt-1.5 flex flex-wrap'>
  225. {notEmptyQuestions.map((question, index) => {
  226. return (
  227. <div key={index} className='mt-1 mr-1 max-w-full truncate last:mr-0 shrink-0 leading-8 items-center px-2.5 rounded-lg border border-gray-200 shadow-xs bg-white text-[13px] font-normal text-gray-900 cursor-pointer'>
  228. {question}
  229. </div>
  230. )
  231. })}
  232. </div>
  233. )
  234. }
  235. return (
  236. <Panel
  237. className={cn(isShowConfirmAddVar && 'h-[220px]', 'relative !bg-gray-25')}
  238. title={t('appDebug.openingStatement.title')}
  239. headerIcon={
  240. <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
  241. <path fillRule="evenodd" clipRule="evenodd" d="M8.33353 1.33301C4.83572 1.33301 2.00019 4.16854 2.00019 7.66634C2.00019 8.37301 2.11619 9.05395 2.3307 9.69036C2.36843 9.80229 2.39063 9.86853 2.40507 9.91738L2.40979 9.93383L2.40729 9.93903C2.39015 9.97437 2.36469 10.0218 2.31705 10.11L1.2158 12.1484C1.14755 12.2746 1.07633 12.4064 1.02735 12.5209C0.978668 12.6348 0.899813 12.8437 0.938613 13.0914C0.984094 13.3817 1.15495 13.6373 1.40581 13.7903C1.61981 13.9208 1.843 13.9279 1.96683 13.9264C2.09141 13.925 2.24036 13.9095 2.38314 13.8947L5.81978 13.5395C5.87482 13.5338 5.9036 13.5309 5.92468 13.5292L5.92739 13.529L5.93564 13.532C5.96154 13.5413 5.99666 13.5548 6.0573 13.5781C6.76459 13.8506 7.53244 13.9997 8.33353 13.9997C11.8313 13.9997 14.6669 11.1641 14.6669 7.66634C14.6669 4.16854 11.8313 1.33301 8.33353 1.33301ZM5.9799 5.72116C6.73142 5.08698 7.73164 5.27327 8.33144 5.96584C8.93125 5.27327 9.91854 5.09365 10.683 5.72116C11.4474 6.34867 11.5403 7.41567 10.9501 8.16572C10.5845 8.6304 9.6668 9.47911 9.02142 10.0576C8.78435 10.2702 8.66582 10.3764 8.52357 10.4192C8.40154 10.456 8.26134 10.456 8.13931 10.4192C7.99706 10.3764 7.87853 10.2702 7.64147 10.0576C6.99609 9.47911 6.07839 8.6304 5.71276 8.16572C5.12259 7.41567 5.22839 6.35534 5.9799 5.72116Z" fill="#E74694" />
  242. </svg>
  243. }
  244. headerRight={headerRight}
  245. hasHeaderBottomBorder={!hasValue}
  246. isFocus={isFocus}
  247. >
  248. <div className='text-gray-700 text-sm'>
  249. {(hasValue || (!hasValue && isFocus)) ? (
  250. <>
  251. {isFocus
  252. ? (
  253. <div>
  254. <textarea
  255. ref={inputRef}
  256. value={tempValue}
  257. rows={3}
  258. onChange={e => setTempValue(e.target.value)}
  259. className="w-full px-0 text-sm border-0 bg-transparent focus:outline-none "
  260. placeholder={t('appDebug.openingStatement.placeholder') as string}
  261. >
  262. </textarea>
  263. </div>
  264. )
  265. : (
  266. <div dangerouslySetInnerHTML={{
  267. __html: coloredContent,
  268. }}></div>
  269. )}
  270. {renderQuestions()}
  271. </>) : (
  272. <div className='pt-2 pb-1 text-xs text-gray-500'>{t('appDebug.openingStatement.noDataPlaceHolder')}</div>
  273. )}
  274. {isShowConfirmAddVar && (
  275. <ConfirmAddVar
  276. varNameArr={notIncludeKeys}
  277. onConfrim={autoAddVar}
  278. onCancel={cancelAutoAddVar}
  279. onHide={hideConfirmAddVar}
  280. />
  281. )}
  282. </div>
  283. </Panel>
  284. )
  285. }
  286. export default React.memo(OpeningStatement)