Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

modal.tsx 9.0KB

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