modal.tsx 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223
  1. import React, { useCallback, useEffect, useState } from 'react'
  2. import { useTranslation } from 'react-i18next'
  3. import { useBoolean } from 'ahooks'
  4. import produce from 'immer'
  5. import { ReactSortable } from 'react-sortablejs'
  6. import { RiAddLine, RiAsterisk, RiCloseLine, RiDeleteBinLine, RiDraggable } from '@remixicon/react'
  7. import Modal from '@/app/components/base/modal'
  8. import Button from '@/app/components/base/button'
  9. import Divider from '@/app/components/base/divider'
  10. import ConfirmAddVar from '@/app/components/app/configuration/config-prompt/confirm-add-var'
  11. import type { OpeningStatement } from '@/app/components/base/features/types'
  12. import { getInputKeys } from '@/app/components/base/block-input'
  13. import type { PromptVariable } from '@/models/debug'
  14. import type { InputVar } from '@/app/components/workflow/types'
  15. import { getNewVar } from '@/utils/var'
  16. import cn from '@/utils/classnames'
  17. type OpeningSettingModalProps = {
  18. data: OpeningStatement
  19. onSave: (newState: OpeningStatement) => void
  20. onCancel: () => void
  21. promptVariables?: PromptVariable[]
  22. workflowVariables?: InputVar[]
  23. onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
  24. }
  25. const MAX_QUESTION_NUM = 10
  26. const OpeningSettingModal = ({
  27. data,
  28. onSave,
  29. onCancel,
  30. promptVariables = [],
  31. workflowVariables = [],
  32. onAutoAddPromptVariable,
  33. }: OpeningSettingModalProps) => {
  34. const { t } = useTranslation()
  35. const [tempValue, setTempValue] = useState(data?.opening_statement || '')
  36. useEffect(() => {
  37. setTempValue(data.opening_statement || '')
  38. }, [data.opening_statement])
  39. const [tempSuggestedQuestions, setTempSuggestedQuestions] = useState(data.suggested_questions || [])
  40. const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
  41. const [notIncludeKeys, setNotIncludeKeys] = useState<string[]>([])
  42. const handleSave = useCallback((ignoreVariablesCheck?: boolean) => {
  43. if (!ignoreVariablesCheck) {
  44. const keys = getInputKeys(tempValue)
  45. const promptKeys = promptVariables.map(item => item.key)
  46. const workflowVariableKeys = workflowVariables.map(item => item.variable)
  47. let notIncludeKeys: string[] = []
  48. if (promptKeys.length === 0 && workflowVariables.length === 0) {
  49. if (keys.length > 0)
  50. notIncludeKeys = keys
  51. }
  52. else {
  53. if (workflowVariables.length > 0)
  54. notIncludeKeys = keys.filter(key => !workflowVariableKeys.includes(key))
  55. else notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
  56. }
  57. if (notIncludeKeys.length > 0) {
  58. setNotIncludeKeys(notIncludeKeys)
  59. showConfirmAddVar()
  60. return
  61. }
  62. }
  63. const newOpening = produce(data, (draft) => {
  64. if (draft) {
  65. draft.opening_statement = tempValue
  66. draft.suggested_questions = tempSuggestedQuestions
  67. }
  68. })
  69. onSave(newOpening)
  70. }, [data, onSave, promptVariables, workflowVariables, showConfirmAddVar, tempSuggestedQuestions, tempValue])
  71. const cancelAutoAddVar = useCallback(() => {
  72. hideConfirmAddVar()
  73. handleSave(true)
  74. }, [handleSave, hideConfirmAddVar])
  75. const autoAddVar = useCallback(() => {
  76. onAutoAddPromptVariable?.([
  77. ...notIncludeKeys.map(key => getNewVar(key, 'string')),
  78. ])
  79. hideConfirmAddVar()
  80. handleSave(true)
  81. }, [handleSave, hideConfirmAddVar, notIncludeKeys, onAutoAddPromptVariable])
  82. const [focusID, setFocusID] = useState<number | null>(null)
  83. const [deletingID, setDeletingID] = useState<number | null>(null)
  84. const renderQuestions = () => {
  85. return (
  86. <div>
  87. <div className='flex items-center py-2'>
  88. <div className='shrink-0 flex space-x-0.5 leading-[18px] text-xs font-medium text-text-tertiary'>
  89. <div className='uppercase'>{t('appDebug.openingStatement.openingQuestion')}</div>
  90. <div>·</div>
  91. <div>{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}</div>
  92. </div>
  93. <Divider bgStyle='gradient' className='ml-3 grow w-0 h-px'/>
  94. </div>
  95. <ReactSortable
  96. className="space-y-1"
  97. list={tempSuggestedQuestions.map((name, index) => {
  98. return {
  99. id: index,
  100. name,
  101. }
  102. })}
  103. setList={list => setTempSuggestedQuestions(list.map(item => item.name))}
  104. handle='.handle'
  105. ghostClass="opacity-50"
  106. animation={150}
  107. >
  108. {tempSuggestedQuestions.map((question, index) => {
  109. return (
  110. <div
  111. className={cn(
  112. 'group relative rounded-lg border border-components-panel-border-subtle bg-components-panel-on-panel-item-bg flex items-center pl-2.5 hover:bg-components-panel-on-panel-item-bg-hover',
  113. focusID === index && 'border-components-input-border-active hover:border-components-input-border-active bg-components-input-bg-active hover:bg-components-input-bg-active',
  114. deletingID === index && 'border-components-input-border-destructive hover:border-components-input-border-destructive bg-state-destructive-hover hover:bg-state-destructive-hover',
  115. )}
  116. key={index}
  117. >
  118. <RiDraggable className='handle w-4 h-4 text-text-quaternary cursor-grab' />
  119. <input
  120. type="input"
  121. value={question || ''}
  122. onChange={(e) => {
  123. const value = e.target.value
  124. setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => {
  125. if (index === i)
  126. return value
  127. return item
  128. }))
  129. }}
  130. className={'w-full overflow-x-auto pl-1.5 pr-8 text-sm leading-9 text-text-secondary border-0 grow h-9 bg-transparent focus:outline-none cursor-pointer rounded-lg'}
  131. onFocus={() => setFocusID(index)}
  132. onBlur={() => setFocusID(null)}
  133. />
  134. <div
  135. className='block absolute top-1/2 translate-y-[-50%] right-1.5 p-1 rounded-md cursor-pointer text-text-tertiary hover:bg-state-destructive-hover hover:text-text-destructive'
  136. onClick={() => {
  137. setTempSuggestedQuestions(tempSuggestedQuestions.filter((_, i) => index !== i))
  138. }}
  139. onMouseEnter={() => setDeletingID(index)}
  140. onMouseLeave={() => setDeletingID(null)}
  141. >
  142. <RiDeleteBinLine className='w-3.5 h-3.5' />
  143. </div>
  144. </div>
  145. )
  146. })}</ReactSortable>
  147. {tempSuggestedQuestions.length < MAX_QUESTION_NUM && (
  148. <div
  149. onClick={() => { setTempSuggestedQuestions([...tempSuggestedQuestions, '']) }}
  150. className='mt-1 flex items-center h-9 px-3 gap-2 rounded-lg cursor-pointer text-components-button-tertiary-text bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover'>
  151. <RiAddLine className='w-4 h-4' />
  152. <div className='system-sm-medium text-[13px]'>{t('appDebug.variableConfig.addOption')}</div>
  153. </div>
  154. )}
  155. </div>
  156. )
  157. }
  158. return (
  159. <Modal
  160. isShow
  161. onClose={() => { }}
  162. className='!p-6 !mt-14 !max-w-none !w-[640px] !bg-components-panel-bg-blur'
  163. >
  164. <div className='flex items-center justify-between mb-6'>
  165. <div className='text-text-primary title-2xl-semi-bold'>{t('appDebug.feature.conversationOpener.title')}</div>
  166. <div className='p-1 cursor-pointer' onClick={onCancel}><RiCloseLine className='w-4 h-4 text-text-tertiary'/></div>
  167. </div>
  168. <div className='flex gap-2 mb-8'>
  169. <div className='shrink-0 mt-1.5 w-8 h-8 p-1.5 rounded-lg border-components-panel-border bg-util-colors-orange-dark-orange-dark-500'>
  170. <RiAsterisk className='w-5 h-5 text-text-primary-on-surface' />
  171. </div>
  172. <div className='grow p-3 bg-chat-bubble-bg rounded-2xl border-t border-divider-subtle shadow-xs'>
  173. <textarea
  174. value={tempValue}
  175. rows={3}
  176. onChange={e => setTempValue(e.target.value)}
  177. className="w-full px-0 text-text-secondary system-md-regular border-0 bg-transparent focus:outline-none"
  178. placeholder={t('appDebug.openingStatement.placeholder') as string}
  179. />
  180. {renderQuestions()}
  181. </div>
  182. </div>
  183. <div className='flex items-center justify-end'>
  184. <Button
  185. onClick={onCancel}
  186. className='mr-2'
  187. >
  188. {t('common.operation.cancel')}
  189. </Button>
  190. <Button
  191. variant='primary'
  192. onClick={() => handleSave()}
  193. >
  194. {t('common.operation.save')}
  195. </Button>
  196. </div>
  197. {isShowConfirmAddVar && (
  198. <ConfirmAddVar
  199. varNameArr={notIncludeKeys}
  200. onConfirm={autoAddVar}
  201. onCancel={cancelAutoAddVar}
  202. onHide={hideConfirmAddVar}
  203. />
  204. )}
  205. </Modal>
  206. )
  207. }
  208. export default OpeningSettingModal