您最多选择25个主题 主题必须以字母或数字开头,可以包含连字符 (-),并且长度不得超过35个字符

modal.tsx 9.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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?.([
  79. ...notIncludeKeys.map(key => getNewVar(key, 'string')),
  80. ])
  81. hideConfirmAddVar()
  82. handleSave(true)
  83. }, [handleSave, hideConfirmAddVar, notIncludeKeys, onAutoAddPromptVariable])
  84. const [focusID, setFocusID] = useState<number | null>(null)
  85. const [deletingID, setDeletingID] = useState<number | null>(null)
  86. const renderQuestions = () => {
  87. return (
  88. <div>
  89. <div className='flex items-center py-2'>
  90. <div className='flex shrink-0 space-x-0.5 text-xs font-medium leading-[18px] text-text-tertiary'>
  91. <div className='uppercase'>{t('appDebug.openingStatement.openingQuestion')}</div>
  92. <div>·</div>
  93. <div>{tempSuggestedQuestions.length}/{MAX_QUESTION_NUM}</div>
  94. </div>
  95. <Divider bgStyle='gradient' className='ml-3 h-px w-0 grow' />
  96. </div>
  97. <ReactSortable
  98. className="space-y-1"
  99. list={tempSuggestedQuestions.map((name, index) => {
  100. return {
  101. id: index,
  102. name,
  103. }
  104. })}
  105. setList={list => setTempSuggestedQuestions(list.map(item => item.name))}
  106. handle='.handle'
  107. ghostClass="opacity-50"
  108. animation={150}
  109. >
  110. {tempSuggestedQuestions.map((question, index) => {
  111. return (
  112. <div
  113. className={cn(
  114. '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',
  115. focusID === index && 'border-components-input-border-active bg-components-input-bg-active hover:border-components-input-border-active hover:bg-components-input-bg-active',
  116. deletingID === index && 'border-components-input-border-destructive bg-state-destructive-hover hover:border-components-input-border-destructive hover:bg-state-destructive-hover',
  117. )}
  118. key={index}
  119. >
  120. <RiDraggable className='handle h-4 w-4 cursor-grab text-text-quaternary' />
  121. <input
  122. type="input"
  123. value={question || ''}
  124. placeholder={t('appDebug.openingStatement.openingQuestionPlaceholder') as string}
  125. onChange={(e) => {
  126. const value = e.target.value
  127. setTempSuggestedQuestions(tempSuggestedQuestions.map((item, i) => {
  128. if (index === i)
  129. return value
  130. return item
  131. }))
  132. }}
  133. 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'}
  134. onFocus={() => setFocusID(index)}
  135. onBlur={() => setFocusID(null)}
  136. />
  137. <div
  138. 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'
  139. onClick={() => {
  140. setTempSuggestedQuestions(tempSuggestedQuestions.filter((_, i) => index !== i))
  141. }}
  142. onMouseEnter={() => setDeletingID(index)}
  143. onMouseLeave={() => setDeletingID(null)}
  144. >
  145. <RiDeleteBinLine className='h-3.5 w-3.5' />
  146. </div>
  147. </div>
  148. )
  149. })}</ReactSortable>
  150. {tempSuggestedQuestions.length < MAX_QUESTION_NUM && (
  151. <div
  152. onClick={() => { setTempSuggestedQuestions([...tempSuggestedQuestions, '']) }}
  153. 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'>
  154. <RiAddLine className='h-4 w-4' />
  155. <div className='system-sm-medium text-[13px]'>{t('appDebug.variableConfig.addOption')}</div>
  156. </div>
  157. )}
  158. </div>
  159. )
  160. }
  161. return (
  162. <Modal
  163. isShow
  164. onClose={noop}
  165. className='!mt-14 !w-[640px] !max-w-none !bg-components-panel-bg-blur !p-6'
  166. >
  167. <div className='mb-6 flex items-center justify-between'>
  168. <div className='title-2xl-semi-bold text-text-primary'>{t('appDebug.feature.conversationOpener.title')}</div>
  169. <div className='cursor-pointer p-1' onClick={onCancel}><RiCloseLine className='h-4 w-4 text-text-tertiary' /></div>
  170. </div>
  171. <div className='mb-8 flex gap-2'>
  172. <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'>
  173. <RiAsterisk className='h-5 w-5 text-text-primary-on-surface' />
  174. </div>
  175. <div className='grow rounded-2xl border-t border-divider-subtle bg-chat-bubble-bg p-3 shadow-xs'>
  176. <PromptEditor
  177. value={tempValue}
  178. onChange={setTempValue}
  179. placeholder={t('appDebug.openingStatement.placeholder') as string}
  180. variableBlock={{
  181. show: true,
  182. variables: [
  183. // Prompt variables
  184. ...promptVariables.map(item => ({
  185. name: item.name || item.key,
  186. value: item.key,
  187. })),
  188. // Workflow variables
  189. ...workflowVariables.map(item => ({
  190. name: item.variable,
  191. value: item.variable,
  192. })),
  193. ],
  194. }}
  195. />
  196. {renderQuestions()}
  197. </div>
  198. </div>
  199. <div className='flex items-center justify-end'>
  200. <Button
  201. onClick={onCancel}
  202. className='mr-2'
  203. >
  204. {t('common.operation.cancel')}
  205. </Button>
  206. <Button
  207. variant='primary'
  208. onClick={() => handleSave()}
  209. >
  210. {t('common.operation.save')}
  211. </Button>
  212. </div>
  213. {isShowConfirmAddVar && (
  214. <ConfirmAddVar
  215. varNameArr={notIncludeKeys}
  216. onConfirm={autoAddVar}
  217. onCancel={cancelAutoAddVar}
  218. onHide={hideConfirmAddVar}
  219. />
  220. )}
  221. </Modal>
  222. )
  223. }
  224. export default OpeningSettingModal