You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

modal.tsx 9.4KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236
  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 PromptEditor from '@/app/components/base/prompt-editor'
  12. import type { OpeningStatement } from '@/app/components/base/features/types'
  13. import { getInputKeys } from '@/app/components/base/block-input'
  14. import type { PromptVariable } from '@/models/debug'
  15. import type { InputVar } from '@/app/components/workflow/types'
  16. import { getNewVar } from '@/utils/var'
  17. import cn from '@/utils/classnames'
  18. import { noop } from 'lodash-es'
  19. type OpeningSettingModalProps = {
  20. data: OpeningStatement
  21. onSave: (newState: OpeningStatement) => void
  22. onCancel: () => void
  23. promptVariables?: PromptVariable[]
  24. workflowVariables?: InputVar[]
  25. onAutoAddPromptVariable?: (variable: PromptVariable[]) => void
  26. }
  27. const MAX_QUESTION_NUM = 10
  28. const OpeningSettingModal = ({
  29. data,
  30. onSave,
  31. onCancel,
  32. promptVariables = [],
  33. workflowVariables = [],
  34. onAutoAddPromptVariable,
  35. }: OpeningSettingModalProps) => {
  36. const { t } = useTranslation()
  37. const [tempValue, setTempValue] = useState(data?.opening_statement || '')
  38. useEffect(() => {
  39. setTempValue(data.opening_statement || '')
  40. }, [data.opening_statement])
  41. const [tempSuggestedQuestions, setTempSuggestedQuestions] = useState(data.suggested_questions || [])
  42. const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
  43. const [notIncludeKeys, setNotIncludeKeys] = useState<string[]>([])
  44. const handleSave = useCallback((ignoreVariablesCheck?: boolean) => {
  45. if (!ignoreVariablesCheck) {
  46. const keys = getInputKeys(tempValue)
  47. const promptKeys = promptVariables.map(item => item.key)
  48. const workflowVariableKeys = workflowVariables.map(item => item.variable)
  49. let notIncludeKeys: string[] = []
  50. if (promptKeys.length === 0 && workflowVariables.length === 0) {
  51. if (keys.length > 0)
  52. notIncludeKeys = keys
  53. }
  54. else {
  55. if (workflowVariables.length > 0)
  56. notIncludeKeys = keys.filter(key => !workflowVariableKeys.includes(key))
  57. else notIncludeKeys = keys.filter(key => !promptKeys.includes(key))
  58. }
  59. if (notIncludeKeys.length > 0) {
  60. setNotIncludeKeys(notIncludeKeys)
  61. showConfirmAddVar()
  62. return
  63. }
  64. }
  65. const newOpening = produce(data, (draft) => {
  66. if (draft) {
  67. draft.opening_statement = tempValue
  68. draft.suggested_questions = tempSuggestedQuestions
  69. }
  70. })
  71. onSave(newOpening)
  72. }, [data, onSave, promptVariables, workflowVariables, showConfirmAddVar, tempSuggestedQuestions, tempValue])
  73. const cancelAutoAddVar = useCallback(() => {
  74. hideConfirmAddVar()
  75. handleSave(true)
  76. }, [handleSave, hideConfirmAddVar])
  77. const autoAddVar = useCallback(() => {
  78. onAutoAddPromptVariable?.(notIncludeKeys.map(key => getNewVar(key, 'string')))
  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='flex shrink-0 space-x-0.5 text-xs font-medium leading-[18px] 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 h-px w-0 grow' />
  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 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',
  113. focusID === index && 'border-components-input-border-active bg-components-input-bg-active hover:border-components-input-border-active hover:bg-components-input-bg-active',
  114. deletingID === index && 'border-components-input-border-destructive bg-state-destructive-hover hover:border-components-input-border-destructive hover:bg-state-destructive-hover',
  115. )}
  116. key={index}
  117. >
  118. <RiDraggable className='handle h-4 w-4 cursor-grab text-text-quaternary' />
  119. <input
  120. type="input"
  121. value={question || ''}
  122. placeholder={t('appDebug.openingStatement.openingQuestionPlaceholder') as string}
  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. <PromptEditor
  175. value={tempValue}
  176. onChange={setTempValue}
  177. placeholder={t('appDebug.openingStatement.placeholder') as string}
  178. variableBlock={{
  179. show: true,
  180. variables: [
  181. // Prompt variables
  182. ...promptVariables.map(item => ({
  183. name: item.name || item.key,
  184. value: item.key,
  185. })),
  186. // Workflow variables
  187. ...workflowVariables.map(item => ({
  188. name: item.variable,
  189. value: item.variable,
  190. })),
  191. ],
  192. }}
  193. />
  194. {renderQuestions()}
  195. </div>
  196. </div>
  197. <div className='flex items-center justify-end'>
  198. <Button
  199. onClick={onCancel}
  200. className='mr-2'
  201. >
  202. {t('common.operation.cancel')}
  203. </Button>
  204. <Button
  205. variant='primary'
  206. onClick={() => handleSave()}
  207. >
  208. {t('common.operation.save')}
  209. </Button>
  210. </div>
  211. {isShowConfirmAddVar && (
  212. <ConfirmAddVar
  213. varNameArr={notIncludeKeys}
  214. onConfirm={autoAddVar}
  215. onCancel={cancelAutoAddVar}
  216. onHide={hideConfirmAddVar}
  217. />
  218. )}
  219. </Modal>
  220. )
  221. }
  222. export default OpeningSettingModal