Jelajahi Sumber

feat: advanced prompt (#1330)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
Co-authored-by: Gillian97 <jinling.sunshine@gmail.com>
zxhlyh 1 tahun lalu
induk
melakukan
5b9858a8a3
100 mengubah file dengan 4437 tambahan dan 377 penghapusan
  1. 14 1
      web/app/components/app/chat/index.tsx
  2. 70 0
      web/app/components/app/chat/log/index.tsx
  3. 17 5
      web/app/components/app/chat/question/index.tsx
  4. 1 0
      web/app/components/app/chat/type.ts
  5. 1 1
      web/app/components/app/configuration/base/feature-panel/index.tsx
  6. 52 23
      web/app/components/app/configuration/config-model/index.tsx
  7. 29 0
      web/app/components/app/configuration/config-model/model-mode-type-label.tsx
  8. 43 24
      web/app/components/app/configuration/config-model/param-item.tsx
  9. 185 0
      web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx
  10. 12 11
      web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx
  11. 59 0
      web/app/components/app/configuration/config-prompt/conversation-histroy/edit-modal.tsx
  12. 50 0
      web/app/components/app/configuration/config-prompt/conversation-histroy/history-panel.tsx
  13. 35 39
      web/app/components/app/configuration/config-prompt/index.tsx
  14. 50 0
      web/app/components/app/configuration/config-prompt/message-type-selector.tsx
  15. 95 0
      web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx
  16. 8 0
      web/app/components/app/configuration/config-prompt/style.module.css
  17. 1 3
      web/app/components/app/configuration/config-var/index.tsx
  18. 3 4
      web/app/components/app/configuration/config/automatic/automatic-btn.tsx
  19. 21 12
      web/app/components/app/configuration/config/feature/add-feature-btn/index.tsx
  20. 41 33
      web/app/components/app/configuration/config/index.tsx
  21. 8 37
      web/app/components/app/configuration/dataset-config/index.tsx
  22. 181 0
      web/app/components/app/configuration/dataset-config/params-config/index.tsx
  23. 1 0
      web/app/components/app/configuration/dataset-config/select-dataset/index.tsx
  24. 53 4
      web/app/components/app/configuration/debug/index.tsx
  25. 4 4
      web/app/components/app/configuration/features/chat-group/suggested-questions-after-answer/index.tsx
  26. 176 0
      web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts
  27. 273 20
      web/app/components/app/configuration/index.tsx
  28. 35 0
      web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx
  29. 19 59
      web/app/components/app/configuration/prompt-value-panel/index.tsx
  30. 14 0
      web/app/components/app/configuration/style.module.css
  31. 83 45
      web/app/components/app/log/list.tsx
  32. 48 0
      web/app/components/app/log/var-panel.tsx
  33. 0 1
      web/app/components/app/overview/settings/index.tsx
  34. 42 5
      web/app/components/app/text-generate/item/index.tsx
  35. 12 46
      web/app/components/base/block-input/index.tsx
  36. 39 0
      web/app/components/base/copy-feedback/index.tsx
  37. 3 0
      web/app/components/base/icons/assets/vender/line/arrows/chevron-selector-vertical.svg
  38. 3 0
      web/app/components/base/icons/assets/vender/line/arrows/flip-backward.svg
  39. 9 0
      web/app/components/base/icons/assets/vender/line/development/variable.svg
  40. 3 0
      web/app/components/base/icons/assets/vender/line/files/clipboard-check.svg
  41. 5 0
      web/app/components/base/icons/assets/vender/line/files/file-02.svg
  42. 5 0
      web/app/components/base/icons/assets/vender/line/general/settings-04.svg
  43. 3 0
      web/app/components/base/icons/assets/vender/line/mediaAndDevices/sliders-h.svg
  44. 8 0
      web/app/components/base/icons/assets/vender/solid/files/file-05.svg
  45. 5 0
      web/app/components/base/icons/assets/vender/solid/files/folder.svg
  46. 5 0
      web/app/components/base/icons/assets/vender/solid/general/message-clock-circle.svg
  47. 14 0
      web/app/components/base/icons/assets/vender/solid/users/user-edit-02.svg
  48. 29 0
      web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json
  49. 16 0
      web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.tsx
  50. 29 0
      web/app/components/base/icons/src/vender/line/arrows/FlipBackward.json
  51. 16 0
      web/app/components/base/icons/src/vender/line/arrows/FlipBackward.tsx
  52. 2 0
      web/app/components/base/icons/src/vender/line/arrows/index.ts
  53. 62 0
      web/app/components/base/icons/src/vender/line/development/Variable.json
  54. 16 0
      web/app/components/base/icons/src/vender/line/development/Variable.tsx
  55. 1 0
      web/app/components/base/icons/src/vender/line/development/index.ts
  56. 29 0
      web/app/components/base/icons/src/vender/line/files/ClipboardCheck.json
  57. 16 0
      web/app/components/base/icons/src/vender/line/files/ClipboardCheck.tsx
  58. 39 0
      web/app/components/base/icons/src/vender/line/files/File02.json
  59. 16 0
      web/app/components/base/icons/src/vender/line/files/File02.tsx
  60. 2 0
      web/app/components/base/icons/src/vender/line/files/index.ts
  61. 39 0
      web/app/components/base/icons/src/vender/line/general/Settings04.json
  62. 16 0
      web/app/components/base/icons/src/vender/line/general/Settings04.tsx
  63. 1 0
      web/app/components/base/icons/src/vender/line/general/index.ts
  64. 29 0
      web/app/components/base/icons/src/vender/line/mediaAndDevices/SlidersH.json
  65. 16 0
      web/app/components/base/icons/src/vender/line/mediaAndDevices/SlidersH.tsx
  66. 1 0
      web/app/components/base/icons/src/vender/line/mediaAndDevices/index.ts
  67. 55 0
      web/app/components/base/icons/src/vender/solid/files/File05.json
  68. 16 0
      web/app/components/base/icons/src/vender/solid/files/File05.tsx
  69. 38 0
      web/app/components/base/icons/src/vender/solid/files/Folder.json
  70. 16 0
      web/app/components/base/icons/src/vender/solid/files/Folder.tsx
  71. 2 0
      web/app/components/base/icons/src/vender/solid/files/index.ts
  72. 36 0
      web/app/components/base/icons/src/vender/solid/general/MessageClockCircle.json
  73. 16 0
      web/app/components/base/icons/src/vender/solid/general/MessageClockCircle.tsx
  74. 1 0
      web/app/components/base/icons/src/vender/solid/general/index.ts
  75. 92 0
      web/app/components/base/icons/src/vender/solid/users/UserEdit02.json
  76. 16 0
      web/app/components/base/icons/src/vender/solid/users/UserEdit02.tsx
  77. 1 0
      web/app/components/base/icons/src/vender/solid/users/index.ts
  78. 24 0
      web/app/components/base/prompt-editor/constants.tsx
  79. 184 0
      web/app/components/base/prompt-editor/hooks.ts
  80. 216 0
      web/app/components/base/prompt-editor/index.tsx
  81. 218 0
      web/app/components/base/prompt-editor/plugins/component-picker.tsx
  82. 61 0
      web/app/components/base/prompt-editor/plugins/context-block-replacement-block.tsx
  83. 97 0
      web/app/components/base/prompt-editor/plugins/context-block/component.tsx
  84. 73 0
      web/app/components/base/prompt-editor/plugins/context-block/index.tsx
  85. 90 0
      web/app/components/base/prompt-editor/plugins/context-block/node.tsx
  86. 52 0
      web/app/components/base/prompt-editor/plugins/custom-text/node.tsx
  87. 61 0
      web/app/components/base/prompt-editor/plugins/history-block-replacement-block.tsx
  88. 90 0
      web/app/components/base/prompt-editor/plugins/history-block/component.tsx
  89. 73 0
      web/app/components/base/prompt-editor/plugins/history-block/index.tsx
  90. 90 0
      web/app/components/base/prompt-editor/plugins/history-block/node.tsx
  91. 36 0
      web/app/components/base/prompt-editor/plugins/on-blur-block.tsx
  92. 13 0
      web/app/components/base/prompt-editor/plugins/placeholder.tsx
  93. 59 0
      web/app/components/base/prompt-editor/plugins/query-block-replacement-block.tsx
  94. 33 0
      web/app/components/base/prompt-editor/plugins/query-block/component.tsx
  95. 62 0
      web/app/components/base/prompt-editor/plugins/query-block/index.tsx
  96. 59 0
      web/app/components/base/prompt-editor/plugins/query-block/node.tsx
  97. 19 0
      web/app/components/base/prompt-editor/plugins/tree-view.tsx
  98. 45 0
      web/app/components/base/prompt-editor/plugins/variable-block/index.tsx
  99. 228 0
      web/app/components/base/prompt-editor/plugins/variable-picker.tsx
  100. 52 0
      web/app/components/base/prompt-editor/plugins/variable-value-block/index.tsx

+ 14 - 1
web/app/components/app/chat/index.tsx

@@ -53,6 +53,7 @@ export type IChatProps = {
   isShowConfigElem?: boolean
   dataSets?: DataSet[]
   isShowCitationHitInfo?: boolean
+  isShowPromptLog?: boolean
 }
 
 const Chat: FC<IChatProps> = ({
@@ -81,6 +82,7 @@ const Chat: FC<IChatProps> = ({
   isShowConfigElem,
   dataSets,
   isShowCitationHitInfo,
+  isShowPromptLog,
 }) => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
@@ -186,7 +188,18 @@ const Chat: FC<IChatProps> = ({
               isShowCitationHitInfo={isShowCitationHitInfo}
             />
           }
-          return <Question key={item.id} id={item.id} content={item.content} more={item.more} useCurrentUserAvatar={useCurrentUserAvatar} />
+          return (
+            <Question
+              key={item.id}
+              id={item.id}
+              content={item.content}
+              more={item.more}
+              useCurrentUserAvatar={useCurrentUserAvatar}
+              item={item}
+              isShowPromptLog={isShowPromptLog}
+              isResponsing={isResponsing}
+            />
+          )
         })}
       </div>
       {

+ 70 - 0
web/app/components/app/chat/log/index.tsx

@@ -0,0 +1,70 @@
+import type { Dispatch, FC, ReactNode, RefObject, SetStateAction } from 'react'
+import { useEffect, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { File02 } from '@/app/components/base/icons/src/vender/line/files'
+import PromptLogModal from '@/app/components/base/prompt-log-modal'
+import Tooltip from '@/app/components/base/tooltip'
+
+export type LogData = {
+  role: string
+  text: string
+}
+
+type LogProps = {
+  containerRef: RefObject<HTMLElement>
+  log: LogData[]
+  children?: (v: Dispatch<SetStateAction<boolean>>) => ReactNode
+}
+const Log: FC<LogProps> = ({
+  containerRef,
+  children,
+  log,
+}) => {
+  const { t } = useTranslation()
+  const [showModal, setShowModal] = useState(false)
+  const [width, setWidth] = useState(0)
+
+  const adjustModalWidth = () => {
+    if (containerRef.current)
+      setWidth(document.body.clientWidth - (containerRef.current?.clientWidth + 56 + 16))
+  }
+
+  useEffect(() => {
+    adjustModalWidth()
+  }, [])
+
+  return (
+    <>
+      {
+        children
+          ? children(setShowModal)
+          : (
+            <Tooltip selector='prompt-log-modal-trigger' content={t('common.operation.log') || ''}>
+              <div className={`
+                hidden absolute -left-[14px] -top-[14px] group-hover:block w-7 h-7
+                p-0.5 rounded-lg border-[0.5px] border-gray-100 bg-white shadow-md cursor-pointer
+              `}>
+                <div
+                  className='flex items-center justify-center rounded-md w-full h-full hover:bg-gray-100'
+                  onClick={() => setShowModal(true)}
+                >
+                  <File02 className='w-4 h-4 text-gray-500' />
+                </div>
+              </div>
+            </Tooltip>
+          )
+      }
+      {
+        showModal && (
+          <PromptLogModal
+            width={width}
+            log={log}
+            onCancel={() => setShowModal(false)}
+          />
+        )
+      }
+    </>
+  )
+}
+
+export default Log

+ 17 - 5
web/app/components/app/chat/question/index.tsx

@@ -1,22 +1,34 @@
 'use client'
 import type { FC } from 'react'
-import React from 'react'
+import React, { useRef } from 'react'
 import { useContext } from 'use-context-selector'
 import s from '../style.module.css'
 import type { IChatItem } from '../type'
+import Log from '../log'
 import MoreInfo from '../more-info'
 import AppContext from '@/context/app-context'
 import { Markdown } from '@/app/components/base/markdown'
 
-type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'>
+type IQuestionProps = Pick<IChatItem, 'id' | 'content' | 'more' | 'useCurrentUserAvatar'> & {
+  isShowPromptLog?: boolean
+  item: IChatItem
+  isResponsing?: boolean
+}
 
-const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar }) => {
+const Question: FC<IQuestionProps> = ({ id, content, more, useCurrentUserAvatar, isShowPromptLog, item, isResponsing }) => {
   const { userProfile } = useContext(AppContext)
   const userName = userProfile?.name
+  const ref = useRef(null)
+
   return (
-    <div className='flex items-start justify-end' key={id}>
+    <div className={`flex items-start justify-end ${isShowPromptLog && 'first-of-type:pt-[14px]'}`} key={id} ref={ref}>
       <div className={s.questionWrapWrap}>
-        <div className={`${s.question} relative text-sm text-gray-900`}>
+        <div className={`${s.question} group relative text-sm text-gray-900`}>
+          {
+            isShowPromptLog && !isResponsing && (
+              <Log log={item.log!} containerRef={ref} />
+            )
+          }
           <div
             className={'mr-2 py-3 px-4 bg-blue-500 rounded-tl-2xl rounded-b-2xl'}
           >

+ 1 - 0
web/app/components/app/chat/type.ts

@@ -66,6 +66,7 @@ export type IChatItem = {
   annotation?: Annotation
   useCurrentUserAvatar?: boolean
   isOpeningStatement?: boolean
+  log?: { role: string; text: string }[]
 }
 
 export type MessageEnd = {

+ 1 - 1
web/app/components/app/configuration/base/feature-panel/index.tsx

@@ -37,7 +37,7 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
       <div className={cn('pb-2 px-3', hasHeaderBottomBorder && 'border-b border-gray-100')}>
         <div className='flex justify-between items-center h-8'>
           <div className='flex items-center space-x-1 shrink-0'>
-            {headerIcon && <div className='flex items-center justify-center w-4 h-4'>{headerIcon}</div>}
+            {headerIcon && <div className='flex items-center justify-center w-6 h-6'>{headerIcon}</div>}
             <div className='text-sm font-semibold text-gray-800'>{title}</div>
           </div>
           <div>

+ 52 - 23
web/app/components/app/configuration/config-model/index.tsx

@@ -4,11 +4,13 @@ import React, { useEffect, useState } from 'react'
 import cn from 'classnames'
 import { useTranslation } from 'react-i18next'
 import { useBoolean, useClickAway, useGetState } from 'ahooks'
-import { Cog8ToothIcon, InformationCircleIcon } from '@heroicons/react/24/outline'
+import { InformationCircleIcon } from '@heroicons/react/24/outline'
 import produce from 'immer'
 import ParamItem from './param-item'
 import ModelIcon from './model-icon'
 import ModelName from './model-name'
+import ModelModeTypeLabel from './model-mode-type-label'
+import { SlidersH } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
 import Radio from '@/app/components/base/radio'
 import Panel from '@/app/components/base/panel'
 import type { CompletionParams } from '@/models/debug'
@@ -25,21 +27,23 @@ import Loading from '@/app/components/base/loading'
 import ModelSelector from '@/app/components/header/account-setting/model-page/model-selector'
 import { ModelType, ProviderEnum } from '@/app/components/header/account-setting/model-page/declarations'
 import { useProviderContext } from '@/context/provider-context'
-
+import type { ModelModeType } from '@/types/app'
 export type IConfigModelProps = {
+  isAdvancedMode: boolean
   mode: string
   modelId: string
   provider: ProviderEnum
-  setModelId: (id: string, provider: ProviderEnum) => void
+  setModel: (model: { id: string; provider: ProviderEnum; mode: ModelModeType }) => void
   completionParams: CompletionParams
   onCompletionParamsChange: (newParams: CompletionParams) => void
   disabled: boolean
 }
 
 const ConfigModel: FC<IConfigModelProps> = ({
+  isAdvancedMode,
   modelId,
   provider,
-  setModelId,
+  setModel,
   completionParams,
   onCompletionParamsChange,
   disabled,
@@ -56,6 +60,8 @@ const ConfigModel: FC<IConfigModelProps> = ({
   const hasEnableParams = currParams && Object.keys(currParams).some(key => currParams[key].enabled)
   const allSupportParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty', 'max_tokens']
   const currSupportParams = currParams ? allSupportParams.filter(key => currParams[key].enabled) : allSupportParams
+  if (isAdvancedMode)
+    currSupportParams.push('stop')
 
   useEffect(() => {
     (async () => {
@@ -115,11 +121,15 @@ const ConfigModel: FC<IConfigModelProps> = ({
     return adjustedValue
   }
 
-  const handleSelectModel = (id: string, nextProvider = ProviderEnum.openai) => {
+  const handleSelectModel = ({ id, provider: nextProvider, mode }: { id: string; provider: ProviderEnum; mode: ModelModeType }) => {
     return async () => {
       const prevParamsRule = getAllParams()[provider]?.[modelId]
 
-      setModelId(id, nextProvider)
+      setModel({
+        id,
+        provider: nextProvider || ProviderEnum.openai,
+        mode,
+      })
 
       await ensureModelParamLoaded(nextProvider, id)
 
@@ -211,16 +221,26 @@ const ConfigModel: FC<IConfigModelProps> = ({
     setToneId(matchToneId(completionParams))
   }, [completionParams])
 
-  const handleParamChange = (key: string, value: number) => {
-    const currParamsRule = getAllParams()[provider]?.[modelId]
-    let notOutRangeValue = parseFloat((value || 0).toFixed(2))
-    notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue)
-    notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue)
+  const handleParamChange = (key: string, value: number | string[]) => {
+    if (value === undefined)
+      return
 
-    onCompletionParamsChange({
-      ...completionParams,
-      [key]: notOutRangeValue,
-    })
+    if (key === 'stop') {
+      onCompletionParamsChange({
+        ...completionParams,
+        [key]: value as string[],
+      })
+    }
+    else {
+      const currParamsRule = getAllParams()[provider]?.[modelId]
+      let notOutRangeValue = parseFloat((value as number).toFixed(2))
+      notOutRangeValue = Math.max(currParamsRule[key].min, notOutRangeValue)
+      notOutRangeValue = Math.min(currParamsRule[key].max, notOutRangeValue)
+      onCompletionParamsChange({
+        ...completionParams,
+        [key]: notOutRangeValue,
+      })
+    }
   }
   const ableStyle = 'bg-indigo-25 border-[#2A87F5] cursor-pointer'
   const diabledStyle = 'bg-[#FFFCF5] border-[#F79009]'
@@ -228,7 +248,7 @@ const ConfigModel: FC<IConfigModelProps> = ({
   const getToneIcon = (toneId: number) => {
     const className = 'w-[14px] h-[14px]'
     const res = ({
-      1: <Brush01 className={className}/>,
+      1: <Brush01 className={className} />,
       2: <Scales02 className={className} />,
       3: <Target04 className={className} />,
       4: <Sliders02 className={className} />,
@@ -249,17 +269,19 @@ const ConfigModel: FC<IConfigModelProps> = ({
   return (
     <div className='relative' ref={configContentRef}>
       <div
-        className={cn('flex items-center border h-8 px-2.5 space-x-2 rounded-lg', disabled ? diabledStyle : ableStyle)}
+        className={cn('flex items-center border h-8 px-2 space-x-2 rounded-lg', disabled ? diabledStyle : ableStyle)}
         onClick={() => !disabled && toogleShowConfig()}
       >
         <ModelIcon
+          className='!w-5 !h-5'
           modelId={modelId}
           providerName={provider}
         />
         <div className='text-[13px] text-gray-900 font-medium'>
           <ModelName modelId={selectedModel.name} modelDisplayName={currModel?.model_display_name} />
         </div>
-        {disabled ? <InformationCircleIcon className='w-3.5 h-3.5 text-[#F79009]' /> : <Cog8ToothIcon className='w-3.5 h-3.5 text-gray-500' />}
+        {isAdvancedMode && <ModelModeTypeLabel type={currModel?.model_mode as ModelModeType} isHighlight />}
+        {disabled ? <InformationCircleIcon className='w-4 h-4 text-[#F79009]' /> : <SlidersH className='w-4 h-4 text-indigo-600' />}
       </div>
       {isShowConfig && (
         <Panel
@@ -282,6 +304,8 @@ const ConfigModel: FC<IConfigModelProps> = ({
             <div className="flex items-center justify-between my-5 h-9">
               <div>{t('appDebug.modelConfig.model')}</div>
               <ModelSelector
+                isShowModelModeType={isAdvancedMode}
+                isShowAddModel
                 popClassName='right-0'
                 triggerIconSmall
                 value={{
@@ -290,7 +314,11 @@ const ConfigModel: FC<IConfigModelProps> = ({
                 }}
                 modelType={ModelType.textGeneration}
                 onChange={(model) => {
-                  handleSelectModel(model.model_name, model.model_provider.provider_name as ProviderEnum)()
+                  handleSelectModel({
+                    id: model.model_name,
+                    provider: model.model_provider.provider_name as ProviderEnum,
+                    mode: model.model_mode,
+                  })()
                 }}
               />
             </div>
@@ -343,20 +371,21 @@ const ConfigModel: FC<IConfigModelProps> = ({
 
             {/* Params */}
             <div className={cn(hasEnableParams && 'mt-4', 'space-y-4', !allParams[provider]?.[modelId] && 'flex items-center min-h-[200px]')}>
-              {allParams[provider]?.[modelId]
+              {(allParams[provider]?.[modelId])
                 ? (
                   currSupportParams.map(key => (<ParamItem
                     key={key}
                     id={key}
-                    name={t(`common.model.params.${key}`)}
-                    tip={t(`common.model.params.${key}Tip`)}
+                    name={t(`common.model.params.${key === 'stop' ? 'stop_sequences' : key}`)}
+                    tip={t(`common.model.params.${key === 'stop' ? 'stop_sequences' : key}Tip`)}
                     {...currParams[key] as any}
                     value={(completionParams as any)[key] as any}
                     onChange={handleParamChange}
+                    inputType={key === 'stop' ? 'inputTag' : 'slider'}
                   />))
                 )
                 : (
-                  <Loading type='area'/>
+                  <Loading type='area' />
                 )}
             </div>
           </div>

+ 29 - 0
web/app/components/app/configuration/config-model/model-mode-type-label.tsx

@@ -0,0 +1,29 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
+import type { ModelModeType } from '@/types/app'
+
+type Props = {
+  className?: string
+  type: ModelModeType
+  isHighlight?: boolean
+}
+
+const ModelModeTypeLabel: FC<Props> = ({
+  className,
+  type,
+  isHighlight,
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <div
+      className={cn(className, isHighlight ? 'border-indigo-300 text-indigo-600' : 'border-gray-300 text-gray-500', 'flex items-center h-4 px-1 border  rounded text-xs font-semibold uppercase')}
+    >
+      {t(`appDebug.modelConfig.modeType.${type}`)}
+    </div>
+  )
+}
+export default React.memo(ModelModeTypeLabel)

+ 43 - 24
web/app/components/app/configuration/config-model/param-item.tsx

@@ -1,8 +1,10 @@
 'use client'
 import type { FC } from 'react'
 import React, { useEffect } from 'react'
+import { useTranslation } from 'react-i18next'
 import Tooltip from '@/app/components/base/tooltip'
 import Slider from '@/app/components/base/slider'
+import TagInput from '@/app/components/base/tag-input'
 
 export const getFitPrecisionValue = (num: number, precision: number | null) => {
   if (!precision || !(`${num}`).includes('.'))
@@ -19,16 +21,19 @@ export type IParamIteProps = {
   id: string
   name: string
   tip: string
-  value: number
+  value: number | string[]
   step?: number
   min?: number
   max: number
   precision: number | null
-  onChange: (key: string, value: number) => void
+  onChange: (key: string, value: number | string[]) => void
+  inputType?: 'inputTag' | 'slider'
 }
 
 const TIMES_TEMPLATE = '1000000000000'
-const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, precision, value, onChange }) => {
+const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max, precision, value, inputType, onChange }) => {
+  const { t } = useTranslation()
+
   const getToIntTimes = (num: number) => {
     if (precision)
       return parseInt(TIMES_TEMPLATE.slice(0, precision + 1), 10)
@@ -45,30 +50,44 @@ const ParamItem: FC<IParamIteProps> = ({ id, name, tip, step = 0.1, min = 0, max
   }, [value, precision])
   return (
     <div className="flex items-center justify-between">
-      <div className="flex items-center">
-        <span className="mr-[6px] text-gray-500 text-[13px] font-medium">{name}</span>
-        {/* Give tooltip different tip to avoiding hide bug */}
-        <Tooltip htmlContent={<div className="w-[200px] whitespace-pre-wrap">{tip}</div>} position='top' selector={`param-name-tooltip-${id}`}>
-          <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-            <path d="M8.66667 10.6667H8V8H7.33333M8 5.33333H8.00667M14 8C14 8.78793 13.8448 9.56815 13.5433 10.2961C13.2417 11.0241 12.7998 11.6855 12.2426 12.2426C11.6855 12.7998 11.0241 13.2417 10.2961 13.5433C9.56815 13.8448 8.78793 14 8 14C7.21207 14 6.43185 13.8448 5.7039 13.5433C4.97595 13.2417 4.31451 12.7998 3.75736 12.2426C3.20021 11.6855 2.75825 11.0241 2.45672 10.2961C2.15519 9.56815 2 8.78793 2 8C2 6.4087 2.63214 4.88258 3.75736 3.75736C4.88258 2.63214 6.4087 2 8 2C9.5913 2 11.1174 2.63214 12.2426 3.75736C13.3679 4.88258 14 6.4087 14 8Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
-          </svg>
-        </Tooltip>
+      <div className="flex flex-col flex-shrink-0">
+        <div className="flex items-center">
+          <span className="mr-[6px] text-gray-500 text-[13px] font-medium">{name}</span>
+          {/* Give tooltip different tip to avoiding hide bug */}
+          <Tooltip htmlContent={<div className="w-[200px] whitespace-pre-wrap">{tip}</div>} position='top' selector={`param-name-tooltip-${id}`}>
+            <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+              <path d="M8.66667 10.6667H8V8H7.33333M8 5.33333H8.00667M14 8C14 8.78793 13.8448 9.56815 13.5433 10.2961C13.2417 11.0241 12.7998 11.6855 12.2426 12.2426C11.6855 12.7998 11.0241 13.2417 10.2961 13.5433C9.56815 13.8448 8.78793 14 8 14C7.21207 14 6.43185 13.8448 5.7039 13.5433C4.97595 13.2417 4.31451 12.7998 3.75736 12.2426C3.20021 11.6855 2.75825 11.0241 2.45672 10.2961C2.15519 9.56815 2 8.78793 2 8C2 6.4087 2.63214 4.88258 3.75736 3.75736C4.88258 2.63214 6.4087 2 8 2C9.5913 2 11.1174 2.63214 12.2426 3.75736C13.3679 4.88258 14 6.4087 14 8Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
+            </svg>
+          </Tooltip>
+        </div>
+        {inputType === 'inputTag' && <div className="text-gray-400 text-xs font-normal">{t('common.model.params.stop_sequencesPlaceholder')}</div>}
       </div>
       <div className="flex items-center">
-        <div className="mr-4 w-[120px]">
-          <Slider value={value * times} min={min * times} max={max * times} onChange={(value) => {
-            onChange(id, value / times)
-          }} />
-        </div>
-        <input type="number" min={min} max={max} step={step} className="block w-[64px] h-9 leading-9 rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900  placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600" value={value} onChange={(e) => {
-          let value = getFitPrecisionValue(isNaN(parseFloat(e.target.value)) ? min : parseFloat(e.target.value), precision)
-          if (value < min)
-            value = min
+        {inputType === 'inputTag'
+          ? <TagInput
+            items={(value ?? []) as string[]}
+            onChange={newSequences => onChange(id, newSequences)}
+            customizedConfirmKey='Tab'
+          />
+          : (
+            <>
+              <div className="mr-4 w-[120px]">
+                <Slider value={value * times} min={min * times} max={max * times} onChange={(value) => {
+                  onChange(id, value / times)
+                }} />
+              </div>
+              <input type="number" min={min} max={max} step={step} className="block w-[64px] h-9 leading-9 rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900  placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600" value={value} onChange={(e) => {
+                let value = getFitPrecisionValue(isNaN(parseFloat(e.target.value)) ? min : parseFloat(e.target.value), precision)
+                if (value < min)
+                  value = min
 
-          if (value > max)
-            value = max
-          onChange(id, value)
-        }} />
+                if (value > max)
+                  value = max
+                onChange(id, value)
+              }} />
+            </>
+          )
+        }
       </div>
     </div>
   )

+ 185 - 0
web/app/components/app/configuration/config-prompt/advanced-prompt-input.tsx

@@ -0,0 +1,185 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import copy from 'copy-to-clipboard'
+import cn from 'classnames'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import { useBoolean } from 'ahooks'
+import produce from 'immer'
+import s from './style.module.css'
+import MessageTypeSelector from './message-type-selector'
+import ConfirmAddVar from './confirm-add-var'
+import type { PromptRole, PromptVariable } from '@/models/debug'
+import { HelpCircle, Trash03 } from '@/app/components/base/icons/src/vender/line/general'
+import { Clipboard, ClipboardCheck } from '@/app/components/base/icons/src/vender/line/files'
+import Tooltip from '@/app/components/base/tooltip'
+import PromptEditor from '@/app/components/base/prompt-editor'
+import ConfigContext from '@/context/debug-configuration'
+import { getNewVar, getVars } from '@/utils/var'
+import { AppType } from '@/types/app'
+
+type Props = {
+  type: PromptRole
+  isChatMode: boolean
+  value: string
+  onTypeChange: (value: PromptRole) => void
+  onChange: (value: string) => void
+  canDelete: boolean
+  onDelete: () => void
+  promptVariables: PromptVariable[]
+}
+
+const AdvancedPromptInput: FC<Props> = ({
+  type,
+  isChatMode,
+  value,
+  onChange,
+  onTypeChange,
+  canDelete,
+  onDelete,
+  promptVariables,
+}) => {
+  const { t } = useTranslation()
+
+  const {
+    mode,
+    hasSetBlockStatus,
+    modelConfig,
+    setModelConfig,
+    conversationHistoriesRole,
+    showHistoryModal,
+    dataSets,
+    showSelectDataSet,
+  } = useContext(ConfigContext)
+  const isChatApp = mode === AppType.chat
+  const [isCopied, setIsCopied] = React.useState(false)
+
+  const promptVariablesObj = (() => {
+    const obj: Record<string, boolean> = {}
+    promptVariables.forEach((item) => {
+      obj[item.key] = true
+    })
+    return obj
+  })()
+  const [newPromptVariables, setNewPromptVariables] = React.useState<PromptVariable[]>(promptVariables)
+  const [isShowConfirmAddVar, { setTrue: showConfirmAddVar, setFalse: hideConfirmAddVar }] = useBoolean(false)
+  const handlePromptChange = (newValue: string) => {
+    if (value === newValue)
+      return
+    onChange(newValue)
+  }
+  const handleBlur = () => {
+    const keys = getVars(value)
+    const newPromptVariables = keys.filter(key => !(key in promptVariablesObj)).map(key => getNewVar(key))
+    if (newPromptVariables.length > 0) {
+      setNewPromptVariables(newPromptVariables)
+      showConfirmAddVar()
+    }
+  }
+
+  const handleAutoAdd = (isAdd: boolean) => {
+    return () => {
+      if (isAdd) {
+        const newModelConfig = produce(modelConfig, (draft) => {
+          draft.configs.prompt_variables = [...draft.configs.prompt_variables, ...newPromptVariables]
+        })
+        setModelConfig(newModelConfig)
+      }
+      hideConfirmAddVar()
+    }
+  }
+
+  const editorHeight = isChatMode ? 'h-[200px]' : 'h-[508px]'
+
+  return (
+    <div className={`relative ${s.gradientBorder}`}>
+      <div className='rounded-xl bg-white'>
+        <div className={cn(s.boxHeader, 'flex justify-between items-center h-11 pt-2 pr-3 pb-1 pl-4 rounded-tl-xl rounded-tr-xl bg-white hover:shadow-xs')}>
+          {isChatMode
+            ? (
+              <MessageTypeSelector value={type} onChange={onTypeChange} />
+            )
+            : (
+              <div className='flex items-center space-x-1'>
+
+                <div className='text-sm font-semibold uppercase text-indigo-800'>{t('appDebug.pageTitle.line1')}
+                </div>
+                <Tooltip
+                  htmlContent={<div className='w-[180px]'>
+                    {t('appDebug.promptTip')}
+                  </div>}
+                  selector='config-prompt-tooltip'>
+                  <HelpCircle className='w-[14px] h-[14px] text-indigo-400' />
+                </Tooltip>
+              </div>)}
+          <div className={cn(s.optionWrap, 'items-center space-x-1')}>
+            {canDelete && (
+              <Trash03 onClick={onDelete} className='h-6 w-6 p-1 text-gray-500 cursor-pointer' />
+            )}
+            {!isCopied
+              ? (
+                <Clipboard className='h-6 w-6 p-1 text-gray-500 cursor-pointer' onClick={() => {
+                  copy(value)
+                  setIsCopied(true)
+                }} />
+              )
+              : (
+                <ClipboardCheck className='h-6 w-6 p-1 text-gray-500' />
+              )}
+
+          </div>
+        </div>
+        <div className={cn(editorHeight, 'px-4 min-h-[102px] overflow-y-auto text-sm text-gray-700')}>
+          <PromptEditor
+            className={editorHeight}
+            value={value}
+            contextBlock={{
+              selectable: !hasSetBlockStatus.context,
+              datasets: dataSets.map(item => ({
+                id: item.id,
+                name: item.name,
+                type: item.data_source_type,
+              })),
+              onAddContext: showSelectDataSet,
+            }}
+            variableBlock={{
+              variables: modelConfig.configs.prompt_variables.map(item => ({
+                name: item.name,
+                value: item.key,
+              })),
+            }}
+            historyBlock={{
+              show: !isChatMode && isChatApp,
+              selectable: !hasSetBlockStatus.history,
+              history: {
+                user: conversationHistoriesRole?.user_prefix,
+                assistant: conversationHistoriesRole?.assistant_prefix,
+              },
+              onEditRole: showHistoryModal,
+            }}
+            queryBlock={{
+              show: !isChatMode && isChatApp,
+              selectable: !hasSetBlockStatus.query,
+            }}
+            onChange={handlePromptChange}
+            onBlur={handleBlur}
+          />
+        </div>
+        <div className='pl-4 pb-2 flex'>
+          <div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{value.length}</div>
+        </div>
+      </div>
+
+      {isShowConfirmAddVar && (
+        <ConfirmAddVar
+          varNameArr={newPromptVariables.map(v => v.name)}
+          onConfrim={handleAutoAdd(true)}
+          onCancel={handleAutoAdd(false)}
+          onHide={hideConfirmAddVar}
+        />
+      )}
+    </div>
+  )
+}
+export default React.memo(AdvancedPromptInput)

+ 12 - 11
web/app/components/app/configuration/config-prompt/confirm-add-var/index.tsx

@@ -1,11 +1,11 @@
 'use client'
-import React, { FC, useRef } from 'react'
+import type { FC } from 'react'
+import React, { useRef } from 'react'
 import { useTranslation } from 'react-i18next'
-import Button from '@/app/components/base/button'
-import { useClickAway } from 'ahooks'
 import VarHighlight from '../../base/var-highlight'
+import Button from '@/app/components/base/button'
 
-export interface IConfirmAddVarProps {
+export type IConfirmAddVarProps = {
   varNameArr: string[]
   onConfrim: () => void
   onCancel: () => void
@@ -28,19 +28,20 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
 }) => {
   const { t } = useTranslation()
   const mainContentRef = useRef<HTMLDivElement>(null)
-  useClickAway(() => {
-    onHide()
-  }, mainContentRef)
+  // new prompt editor blur trigger click...
+  // useClickAway(() => {
+  //   onHide()
+  // }, mainContentRef)
   return (
     <div className='absolute inset-0  flex items-center justify-center rounded-xl'
       style={{
-        backgroundColor: 'rgba(35, 56, 118, 0.2)'
+        backgroundColor: 'rgba(35, 56, 118, 0.2)',
       }}>
       <div
         ref={mainContentRef}
         className='w-[420px] rounded-xl bg-gray-50 p-6'
         style={{
-          boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)'
+          boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
         }}
       >
         <div className='flex items-start space-x-3'>
@@ -48,13 +49,13 @@ const ConfirmAddVar: FC<IConfirmAddVarProps> = ({
             className='shrink-0 flex items-center justify-center h-10 w-10 rounded-xl border border-gray-100'
             style={{
               backgroundColor: 'rgba(255, 255, 255, 0.9)',
-              boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)'
+              boxShadow: '0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03)',
             }}
           >{VarIcon}</div>
           <div className='grow-1'>
             <div className='text-sm font-medium text-gray-900'>{t('appDebug.autoAddVar')}</div>
             <div className='flex flex-wrap mt-[15px] max-h-[66px] overflow-y-auto px-1 space-x-1'>
-              {varNameArr.map((name) => (
+              {varNameArr.map(name => (
                 <VarHighlight key={name} name={name} />
               ))}
             </div>

+ 59 - 0
web/app/components/app/configuration/config-prompt/conversation-histroy/edit-modal.tsx

@@ -0,0 +1,59 @@
+'use client'
+import type { FC } from 'react'
+import React, { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import Modal from '@/app/components/base/modal'
+import type { ConversationHistoriesRole } from '@/models/debug'
+import Button from '@/app/components/base/button'
+type Props = {
+  isShow: boolean
+  saveLoading: boolean
+  data: ConversationHistoriesRole
+  onClose: () => void
+  onSave: (data: any) => void
+}
+
+const EditModal: FC<Props> = ({
+  isShow,
+  saveLoading,
+  data,
+  onClose,
+  onSave,
+}) => {
+  const { t } = useTranslation()
+  const [tempData, setTempData] = useState(data)
+  return (
+    <Modal
+      title={t('appDebug.feature.conversationHistory.editModal.title')}
+      isShow={isShow}
+      onClose={onClose}
+      wrapperClassName='!z-[101]'
+    >
+      <div className={'mt-6 font-medium text-sm leading-[21px] text-gray-900'}>{t('appDebug.feature.conversationHistory.editModal.userPrefix')}</div>
+      <input className={'mt-2 w-full rounded-lg h-10 box-border px-3 text-sm leading-10 bg-gray-100'}
+        value={tempData.user_prefix}
+        onChange={e => setTempData({
+          ...tempData,
+          user_prefix: e.target.value,
+        })}
+      />
+
+      <div className={'mt-6 font-medium text-sm leading-[21px] text-gray-900'}>{t('appDebug.feature.conversationHistory.editModal.assistantPrefix')}</div>
+      <input className={'mt-2 w-full rounded-lg h-10 box-border px-3 text-sm leading-10 bg-gray-100'}
+        value={tempData.assistant_prefix}
+        onChange={e => setTempData({
+          ...tempData,
+          assistant_prefix: e.target.value,
+        })}
+        placeholder={t('common.chat.conversationNamePlaceholder') || ''}
+      />
+
+      <div className='mt-10 flex justify-end'>
+        <Button className='mr-2 flex-shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
+        <Button type='primary' className='flex-shrink-0' onClick={() => onSave(tempData)} loading={saveLoading}>{t('common.operation.save')}</Button>
+      </div>
+    </Modal>
+  )
+}
+
+export default React.memo(EditModal)

+ 50 - 0
web/app/components/app/configuration/config-prompt/conversation-histroy/history-panel.tsx

@@ -0,0 +1,50 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import OperationBtn from '@/app/components/app/configuration/base/operation-btn'
+import Panel from '@/app/components/app/configuration/base/feature-panel'
+import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
+
+type Props = {
+  showWarning: boolean
+  onShowEditModal: () => void
+}
+
+const HistoryPanel: FC<Props> = ({
+  showWarning,
+  onShowEditModal,
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <Panel
+      className='mt-3'
+      title={
+        <div className='flex items-center gap-2'>
+          <div>{t('appDebug.feature.conversationHistory.title')}</div>
+        </div>
+      }
+      headerIcon={
+        <div className='p-1 rounded-md bg-white shadow-xs'>
+          <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />
+        </div>}
+      headerRight={
+        <div className='flex items-center'>
+          <div className='text-xs text-gray-500'>{t('appDebug.feature.conversationHistory.description')}</div>
+          <div className='ml-3 w-[1px] h-[14px] bg-gray-200'></div>
+          <OperationBtn type="edit" onClick={onShowEditModal} />
+        </div>
+      }
+      noBodySpacing
+    >
+      {showWarning && (
+        <div className='flex justify-between py-2 px-3 rounded-b-xl bg-[#FFFAEB] text-xs text-gray-700'>
+          {/* <div>{t('appDebug.feature.conversationHistory.tip')} <a href="https://docs.dify.ai/getting-started/readme" target='_blank' className='text-[#155EEF]'>{t('appDebug.feature.conversationHistory.learnMore')}</a></div> */}
+          <div>{t('appDebug.feature.conversationHistory.tip')}</div>
+        </div>
+      )}
+    </Panel>
+  )
+}
+export default React.memo(HistoryPanel)

File diff ditekan karena terlalu besar
+ 35 - 39
web/app/components/app/configuration/config-prompt/index.tsx


+ 50 - 0
web/app/components/app/configuration/config-prompt/message-type-selector.tsx

@@ -0,0 +1,50 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useBoolean, useClickAway } from 'ahooks'
+import cn from 'classnames'
+import { PromptRole } from '@/models/debug'
+import { ChevronSelectorVertical } from '@/app/components/base/icons/src/vender/line/arrows'
+type Props = {
+  value: PromptRole
+  onChange: (value: PromptRole) => void
+}
+
+const allTypes = [PromptRole.system, PromptRole.user, PromptRole.assistant]
+const MessageTypeSelector: FC<Props> = ({
+  value,
+  onChange,
+}) => {
+  const [showOption, { setFalse: setHide, toggle: toggleShow }] = useBoolean(false)
+  const ref = React.useRef(null)
+  useClickAway(() => {
+    setHide()
+  }, ref)
+  return (
+    <div className='relative left-[-8px]' ref={ref}>
+      <div
+        onClick={toggleShow}
+        className={cn(showOption && 'bg-indigo-100', 'flex items-center h-7 pl-1.5 pr-1 space-x-0.5 rounded-lg cursor-pointer text-indigo-800')}>
+        <div className='text-sm font-semibold uppercase'>{value}</div>
+        <ChevronSelectorVertical className='w-3 h-3 ' />
+      </div>
+      {showOption && (
+        <div className='absolute z-10 top-[30px] p-1 border border-gray-200 shadow-lg rounded-lg bg-white'>
+          {allTypes.map(type => (
+            <div
+              key={type}
+              onClick={() => {
+                setHide()
+                onChange(type)
+              }}
+              className='flex items-center h-9 min-w-[44px] px-3 rounded-lg cursor-pointer text-sm font-medium text-gray-700 uppercase hover:bg-gray-50'
+            >{type}</div>
+          ))
+          }
+        </div>
+      )
+      }
+    </div>
+  )
+}
+export default React.memo(MessageTypeSelector)

File diff ditekan karena terlalu besar
+ 95 - 0
web/app/components/app/configuration/config-prompt/simple-prompt-input.tsx


+ 8 - 0
web/app/components/app/configuration/config-prompt/style.module.css

@@ -12,4 +12,12 @@
   border-radius: 12px;
   padding: 2px;
   box-sizing: border-box;
+}
+
+.optionWrap {
+  display: none;
+}
+
+.boxHeader:hover .optionWrap {
+  display: flex;
 }

+ 1 - 3
web/app/components/app/configuration/config-var/index.tsx

@@ -73,7 +73,6 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
           delete newItem.max_length
           delete newItem.options
         }
-        console.log(newItem)
         return newItem
       }
 
@@ -175,8 +174,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
             <Tooltip htmlContent={<div className='w-[180px]'>
               {t('appDebug.variableTip')}
             </div>} selector='config-var-tooltip'>
-              <HelpCircle className='w-3.5 h-3.5 text-gray-400'/>
-
+              <HelpCircle className='w-[14px] h-[14px] text-gray-400' />
             </Tooltip>
           )}
         </div>

+ 3 - 4
web/app/components/app/configuration/config/automatic/automatic-btn.tsx

@@ -2,7 +2,6 @@
 import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
-import Button from '@/app/components/base/button'
 
 export type IAutomaticBtnProps = {
   onClick: () => void
@@ -22,12 +21,12 @@ const AutomaticBtn: FC<IAutomaticBtnProps> = ({
   const { t } = useTranslation()
 
   return (
-    <Button className='flex space-x-2 items-center !h-8'
+    <div className='flex px-3 space-x-2 items-center !h-8 cursor-pointer'
       onClick={onClick}
     >
       {leftIcon}
-      <span className='text-xs font-semibold text-primary-600 uppercase'>{t('appDebug.operation.automatic')}</span>
-    </Button>
+      <span className='text-xs font-semibold text-indigo-600 uppercase'>{t('appDebug.operation.automatic')}</span>
+    </div>
   )
 }
 export default React.memo(AutomaticBtn)

+ 21 - 12
web/app/components/app/configuration/config/feature/add-feature-btn/index.tsx

@@ -1,30 +1,39 @@
 'use client'
-import React, { FC } from 'react'
+import type { FC } from 'react'
+import React from 'react'
 import { useTranslation } from 'react-i18next'
 import { PlusIcon } from '@heroicons/react/24/solid'
 
-export interface IAddFeatureBtnProps {
+export type IAddFeatureBtnProps = {
+  toBottomHeight: number
   onClick: () => void
 }
 
+const ITEM_HEIGHT = 48
+
 const AddFeatureBtn: FC<IAddFeatureBtnProps> = ({
-  onClick
+  toBottomHeight,
+  onClick,
 }) => {
   const { t } = useTranslation()
   return (
     <div
-      className='
-        flex items-center h-8 space-x-2 px-3
-        border border-primary-100 rounded-lg bg-primary-25 hover:bg-primary-50 cursor-pointer
-        text-xs font-semibold text-primary-600 uppercase 
-      '
+      className='absolute z-[9] left-0 right-0 flex justify-center pb-4'
       style={{
-        boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
+        top: toBottomHeight - ITEM_HEIGHT,
+        background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%)',
       }}
-      onClick={onClick}
     >
-      <PlusIcon className='w-4 h-4 font-semibold' />
-      <div>{t('appDebug.operation.addFeature')}</div>
+      <div
+        className='flex items-center h-8 space-x-2 px-3
+        border border-primary-100 rounded-lg bg-primary-25 hover:bg-primary-50 cursor-pointer
+        text-xs font-semibold text-primary-600 uppercase
+      '
+        onClick={onClick}
+      >
+        <PlusIcon className='w-4 h-4 font-semibold' />
+        <div>{t('appDebug.operation.addFeature')}</div>
+      </div>
     </div>
   )
 }

+ 41 - 33
web/app/components/app/configuration/config/index.tsx

@@ -1,29 +1,32 @@
 'use client'
 import type { FC } from 'react'
-import React from 'react'
+import React, { useRef } from 'react'
 import { useContext } from 'use-context-selector'
 import produce from 'immer'
-import { useBoolean } from 'ahooks'
+import { useBoolean, useScroll } from 'ahooks'
 import DatasetConfig from '../dataset-config'
 import ChatGroup from '../features/chat-group'
 import ExperienceEnchanceGroup from '../features/experience-enchance-group'
 import Toolbox from '../toolbox'
+import HistoryPanel from '../config-prompt/conversation-histroy/history-panel'
 import AddFeatureBtn from './feature/add-feature-btn'
-import AutomaticBtn from './automatic/automatic-btn'
-import type { AutomaticRes } from './automatic/get-automatic-res'
-import GetAutomaticResModal from './automatic/get-automatic-res'
 import ChooseFeature from './feature/choose-feature'
 import useFeature from './feature/use-feature'
+import AdvancedModeWaring from '@/app/components/app/configuration/prompt-mode/advanced-mode-waring'
 import ConfigContext from '@/context/debug-configuration'
 import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
 import ConfigVar from '@/app/components/app/configuration/config-var'
 import type { PromptVariable } from '@/models/debug'
-import { AppType } from '@/types/app'
+import { AppType, ModelModeType } from '@/types/app'
 import { useProviderContext } from '@/context/provider-context'
-
 const Config: FC = () => {
   const {
     mode,
+    isAdvancedMode,
+    modelModeType,
+    canReturnToSimpleMode,
+    hasSetBlockStatus,
+    showHistoryModal,
     introduction,
     setIntroduction,
     modelConfig,
@@ -44,6 +47,7 @@ const Config: FC = () => {
 
   const promptTemplate = modelConfig.configs.prompt_template
   const promptVariables = modelConfig.configs.prompt_variables
+  // simple mode
   const handlePromptChange = (newTemplate: string, newVariables: PromptVariable[]) => {
     const newModelConfig = produce(modelConfig, (draft) => {
       draft.configs.prompt_template = newTemplate
@@ -101,26 +105,29 @@ const Config: FC = () => {
   const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || featureConfig.citation)
   const hasToolbox = false
 
-  const [showAutomatic, { setTrue: showAutomaticTrue, setFalse: showAutomaticFalse }] = useBoolean(false)
-  const handleAutomaticRes = (res: AutomaticRes) => {
-    const newModelConfig = produce(modelConfig, (draft) => {
-      draft.configs.prompt_template = res.prompt
-      draft.configs.prompt_variables = res.variables.map(key => ({ key, name: key, type: 'string', required: true }))
-    })
-    setModelConfig(newModelConfig)
-    setPrevPromptConfig(modelConfig.configs)
-    if (mode === AppType.chat)
-      setIntroduction(res.opening_statement)
-    showAutomaticFalse()
-  }
+  const wrapRef = useRef<HTMLDivElement>(null)
+  const wrapScroll = useScroll(wrapRef)
+  const toBottomHeight = (() => {
+    if (!wrapRef.current)
+      return 999
+    const elem = wrapRef.current
+    const { clientHeight } = elem
+    const value = (wrapScroll?.top || 0) + clientHeight
+    return value
+  })()
+
   return (
     <>
-      <div className="pb-[20px]">
-        <div className='flex justify-between items-center mb-4'>
-          <AddFeatureBtn onClick={showChooseFeatureTrue} />
-          <AutomaticBtn onClick={showAutomaticTrue}/>
-        </div>
-
+      <div
+        ref={wrapRef}
+        className="relative px-6 pb-[50px] overflow-y-auto h-full"
+      >
+        <AddFeatureBtn toBottomHeight={toBottomHeight} onClick={showChooseFeatureTrue} />
+        {
+          (isAdvancedMode && canReturnToSimpleMode) && (
+            <AdvancedModeWaring />
+          )
+        }
         {showChooseFeature && (
           <ChooseFeature
             isShow={showChooseFeature}
@@ -131,14 +138,7 @@ const Config: FC = () => {
             showSpeechToTextItem={!!speech2textDefaultModel}
           />
         )}
-        {showAutomatic && (
-          <GetAutomaticResModal
-            mode={mode as AppType}
-            isShow={showAutomatic}
-            onClose={showAutomaticFalse}
-            onFinished={handleAutomaticRes}
-          />
-        )}
+
         {/* Template */}
         <ConfigPrompt
           mode={mode as AppType}
@@ -156,6 +156,14 @@ const Config: FC = () => {
         {/* Dataset */}
         <DatasetConfig />
 
+        {/* Chat History */}
+        {isAdvancedMode && isChatApp && modelModeType === ModelModeType.completion && (
+          <HistoryPanel
+            showWarning={!hasSetBlockStatus.history}
+            onShowEditModal={showHistoryModal}
+          />
+        )}
+
         {/* ChatConifig */}
         {
           hasChatConfig && (

+ 8 - 37
web/app/components/app/configuration/dataset-config/index.tsx

@@ -3,16 +3,13 @@ import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
-import { useBoolean } from 'ahooks'
-import { isEqual } from 'lodash-es'
 import produce from 'immer'
 import FeaturePanel from '../base/feature-panel'
 import OperationBtn from '../base/operation-btn'
 import CardItem from './card-item'
-import SelectDataSet from './select-dataset'
+import ParamsConfig from './params-config'
 import ContextVar from './context-var'
 import ConfigContext from '@/context/debug-configuration'
-import type { DataSet } from '@/models/datasets'
 import { AppType } from '@/types/app'
 
 const Icon = (
@@ -31,35 +28,12 @@ const DatasetConfig: FC = () => {
     setFormattingChanged,
     modelConfig,
     setModelConfig,
+    showSelectDataSet,
   } = useContext(ConfigContext)
   const selectedIds = dataSet.map(item => item.id)
 
   const hasData = dataSet.length > 0
-  const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
-  const handleSelect = (data: DataSet[]) => {
-    if (isEqual(data.map(item => item.id), dataSet.map(item => item.id))) {
-      hideSelectDataSet()
-      return
-    }
 
-    setFormattingChanged(true)
-    if (data.find(item => !item.name)) { // has not loaded selected dataset
-      const newSelected = produce(data, (draft) => {
-        data.forEach((item, index) => {
-          if (!item.name) { // not fetched database
-            const newItem = dataSet.find(i => i.id === item.id)
-            if (newItem)
-              draft[index] = newItem
-          }
-        })
-      })
-      setDataSet(newSelected)
-    }
-    else {
-      setDataSet(data)
-    }
-    hideSelectDataSet()
-  }
   const onRemove = (id: string) => {
     setDataSet(dataSet.filter(item => item.id !== id))
     setFormattingChanged(true)
@@ -89,7 +63,12 @@ const DatasetConfig: FC = () => {
       className='mt-3'
       headerIcon={Icon}
       title={t('appDebug.feature.dataSet.title')}
-      headerRight={<OperationBtn type="add" onClick={showSelectDataSet} />}
+      headerRight={
+        <div className='flex items-center gap-1'>
+          <ParamsConfig />
+          <OperationBtn type="add" onClick={showSelectDataSet} />
+        </div>
+      }
       hasHeaderBottomBorder={!hasData}
       noBodySpacing
     >
@@ -120,14 +99,6 @@ const DatasetConfig: FC = () => {
         />
       )}
 
-      {isShowSelectDataSet && (
-        <SelectDataSet
-          isShow={isShowSelectDataSet}
-          onClose={hideSelectDataSet}
-          selectedIds={selectedIds}
-          onSelect={handleSelect}
-        />
-      )}
     </FeaturePanel>
   )
 }

+ 181 - 0
web/app/components/app/configuration/dataset-config/params-config/index.tsx

@@ -0,0 +1,181 @@
+'use client'
+import type { FC } from 'react'
+import { memo, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import cn from 'classnames'
+import { HelpCircle, Settings04 } from '@/app/components/base/icons/src/vender/line/general'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import Tooltip from '@/app/components/base/tooltip-plus'
+import Slider from '@/app/components/base/slider'
+import Switch from '@/app/components/base/switch'
+import ConfigContext from '@/context/debug-configuration'
+
+// TODO
+const PARAMS_KEY = [
+  'top_k',
+  'score_threshold',
+]
+const PARAMS = {
+  top_k: {
+    default: 2,
+    step: 1,
+    min: 1,
+    max: 10,
+  },
+  score_threshold: {
+    default: 0.7,
+    step: 0.01,
+    min: 0,
+    max: 1,
+  },
+} as any
+
+export type IParamItemProps = {
+  id: string
+  name: string
+  tip: string
+  value: number
+  enable: boolean
+  step?: number
+  min?: number
+  max: number
+  onChange: (key: string, value: number) => void
+  onSwitchChange: (key: string, enable: boolean) => void
+}
+
+const ParamItem: FC<IParamItemProps> = ({ id, name, tip, step = 0.1, min = 0, max, value, enable, onChange, onSwitchChange }) => {
+  return (
+    <div>
+      <div className="flex items-center justify-between">
+        <div className="flex items-center">
+          {id === 'score_threshold' && (
+            <Switch
+              size='md'
+              defaultValue={enable}
+              onChange={async (val) => {
+                onSwitchChange(id, val)
+              }}
+            />
+          )}
+          <span className="mx-1 text-gray-800 text-[13px] leading-[18px] font-medium">{name}</span>
+          <Tooltip popupContent={<div className="w-[200px]">{tip}</div>}>
+            <HelpCircle className='w-[14px] h-[14px] text-gray-400' />
+          </Tooltip>
+        </div>
+        <div className="flex items-center"></div>
+      </div>
+      <div className="mt-2 flex items-center justify-between">
+        <div className="flex items-center h-7">
+          <div className="w-[148px]">
+            <Slider
+              disabled={!enable}
+              value={max < 5 ? value * 100 : value}
+              min={min < 1 ? min * 100 : min}
+              max={max < 5 ? max * 100 : max}
+              onChange={value => onChange(id, value / (max < 5 ? 100 : 1))}
+            />
+          </div>
+        </div>
+        <div className="flex items-center">
+          <input disabled={!enable} type="number" min={min} max={max} step={step} className="block w-[48px] h-7 text-xs leading-[18px] rounded-lg border-0 pl-1 pl py-1.5 bg-gray-50 text-gray-900  placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-primary-600 disabled:opacity-60" value={value} onChange={(e) => {
+            const value = parseFloat(e.target.value)
+            if (value < min || value > max)
+              return
+
+            onChange(id, value)
+          }} />
+        </div>
+      </div>
+    </div>
+  )
+}
+
+const ParamsConfig: FC = () => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+  const {
+    datasetConfigs,
+    setDatasetConfigs,
+  } = useContext(ConfigContext)
+
+  const handleParamChange = (key: string, value: number) => {
+    let notOutRangeValue = parseFloat(value.toFixed(2))
+    notOutRangeValue = Math.max(PARAMS[key].min, notOutRangeValue)
+    notOutRangeValue = Math.min(PARAMS[key].max, notOutRangeValue)
+    if (key === 'top_k') {
+      setDatasetConfigs({
+        ...datasetConfigs,
+        top_k: notOutRangeValue,
+      })
+    }
+    else if (key === 'score_threshold') {
+      setDatasetConfigs({
+        ...datasetConfigs,
+        [key]: {
+          enable: datasetConfigs.score_threshold.enable,
+          value: notOutRangeValue,
+        },
+      })
+    }
+  }
+
+  const handleSwitch = (key: string, enable: boolean) => {
+    if (key === 'top_k')
+      return
+
+    setDatasetConfigs({
+      ...datasetConfigs,
+      [key]: {
+        enable,
+        value: (datasetConfigs as any)[key].value,
+      },
+    })
+  }
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={{
+        mainAxis: 4,
+      }}
+    >
+      <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
+        <div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-gray-700 cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
+          <Settings04 className="w-[14px] h-[14px]" />
+          <div className='text-xs font-medium'>
+            {t('appDebug.datasetConfig.params')}
+          </div>
+        </div>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent style={{ zIndex: 50 }}>
+        <div className='w-[240px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
+          {PARAMS_KEY.map((key: string) => {
+            const currentValue = key === 'top_k' ? datasetConfigs[key] : (datasetConfigs as any)[key].value
+            const currentEnableState = key === 'top_k' ? true : (datasetConfigs as any)[key].enable
+            return (
+              <ParamItem
+                key={key}
+                id={key}
+                name={t(`appDebug.datasetConfig.${key}`)}
+                tip={t(`appDebug.datasetConfig.${key}Tip`)}
+                {...PARAMS[key]}
+                value={currentValue}
+                enable={currentEnableState}
+                onChange={handleParamChange}
+                onSwitchChange={handleSwitch}
+              />
+            )
+          })}
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+export default memo(ParamsConfig)

+ 1 - 0
web/app/components/app/configuration/dataset-config/select-dataset/index.tsx

@@ -94,6 +94,7 @@ const SelectDataSet: FC<ISelectDataSetProps> = ({
       isShow={isShow}
       onClose={onClose}
       className='w-[400px]'
+      wrapperClassName='!z-[101]'
       title={t('appDebug.feature.dataSet.selectTitle')}
     >
       {!loaded && (

+ 53 - 4
web/app/components/app/configuration/debug/index.tsx

@@ -11,7 +11,7 @@ import HasNotSetAPIKEY from '../base/warning-mask/has-not-set-api'
 import FormattingChanged from '../base/warning-mask/formatting-changed'
 import GroupName from '../base/group-name'
 import CannotQueryDataset from '../base/warning-mask/cannot-query-dataset'
-import { AppType } from '@/types/app'
+import { AppType, ModelModeType } from '@/types/app'
 import PromptValuePanel, { replaceStringWithValues } from '@/app/components/app/configuration/prompt-value-panel'
 import type { IChatItem } from '@/app/components/app/chat/type'
 import Chat from '@/app/components/app/chat'
@@ -37,6 +37,12 @@ const Debug: FC<IDebug> = ({
   const {
     appId,
     mode,
+    modelModeType,
+    hasSetBlockStatus,
+    isAdvancedMode,
+    promptMode,
+    chatPromptConfig,
+    completionPromptConfig,
     introduction,
     suggestedQuestionsAfterAnswerConfig,
     speechToTextConfig,
@@ -53,6 +59,7 @@ const Debug: FC<IDebug> = ({
     modelConfig,
     completionParams,
     hasSetContextVar,
+    datasetConfigs,
   } = useContext(ConfigContext)
   const { speech2textDefaultModel } = useProviderContext()
   const [chatList, setChatList, getChatList] = useGetState<IChatItem[]>([])
@@ -120,6 +127,18 @@ const Debug: FC<IDebug> = ({
   }
 
   const checkCanSend = () => {
+    if (isAdvancedMode && mode === AppType.chat) {
+      if (modelModeType === ModelModeType.completion) {
+        if (!hasSetBlockStatus.history) {
+          notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 })
+          return false
+        }
+        if (!hasSetBlockStatus.query) {
+          notify({ type: 'error', message: t('appDebug.otherError.queryNoBeEmpty'), duration: 3000 })
+          return false
+        }
+      }
+    }
     let hasEmptyInput = ''
     const requiredVars = modelConfig.configs.prompt_variables.filter(({ key, name, required }) => {
       const res = (!key || !key.trim()) || (!name || !name.trim()) || (required || required === undefined || required === null)
@@ -155,11 +174,15 @@ const Debug: FC<IDebug> = ({
         id,
       },
     }))
+    const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
 
     const postModelConfig: BackendModelConfig = {
-      pre_prompt: modelConfig.configs.prompt_template,
+      pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
+      prompt_type: promptMode,
+      chat_prompt_config: {},
+      completion_prompt_config: {},
       user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
-      dataset_query_variable: '',
+      dataset_query_variable: contextVar || '',
       opening_statement: introduction,
       more_like_this: {
         enabled: false,
@@ -174,8 +197,15 @@ const Debug: FC<IDebug> = ({
       model: {
         provider: modelConfig.provider,
         name: modelConfig.model_id,
+        mode: modelConfig.mode,
         completion_params: completionParams as any,
       },
+      dataset_configs: datasetConfigs,
+    }
+
+    if (isAdvancedMode) {
+      postModelConfig.chat_prompt_config = chatPromptConfig
+      postModelConfig.completion_prompt_config = completionPromptConfig
     }
 
     const data = {
@@ -254,6 +284,11 @@ const Debug: FC<IDebug> = ({
           setChatList(produce(getChatList(), (draft) => {
             const index = draft.findIndex(item => item.id === responseItem.id)
             if (index !== -1) {
+              const requestion = draft[index - 1]
+              draft[index - 1] = {
+                ...requestion,
+                log: newResponseItem.message,
+              }
               draft[index] = {
                 ...draft[index],
                 more: {
@@ -326,7 +361,10 @@ const Debug: FC<IDebug> = ({
     const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
 
     const postModelConfig: BackendModelConfig = {
-      pre_prompt: modelConfig.configs.prompt_template,
+      pre_prompt: !isAdvancedMode ? modelConfig.configs.prompt_template : '',
+      prompt_type: promptMode,
+      chat_prompt_config: {},
+      completion_prompt_config: {},
       user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
       dataset_query_variable: contextVar || '',
       opening_statement: introduction,
@@ -341,8 +379,15 @@ const Debug: FC<IDebug> = ({
       model: {
         provider: modelConfig.provider,
         name: modelConfig.model_id,
+        mode: modelConfig.mode,
         completion_params: completionParams as any,
       },
+      dataset_configs: datasetConfigs,
+    }
+
+    if (isAdvancedMode) {
+      postModelConfig.chat_prompt_config = chatPromptConfig
+      postModelConfig.completion_prompt_config = completionPromptConfig
     }
 
     const data = {
@@ -413,6 +458,7 @@ const Debug: FC<IDebug> = ({
                   isShowSpeechToText={speechToTextConfig.enabled && !!speech2textDefaultModel}
                   isShowCitation={citationConfig.enabled}
                   isShowCitationHitInfo
+                  isShowPromptLog
                 />
               </div>
             </div>
@@ -427,8 +473,11 @@ const Debug: FC<IDebug> = ({
                 className="mt-2"
                 content={completionRes}
                 isLoading={!completionRes && isResponsing}
+                isResponsing={isResponsing}
                 isInstalledApp={false}
                 messageId={messageId}
+                isError={false}
+                onRetry={() => { }}
               />
             )}
           </div>

+ 4 - 4
web/app/components/app/configuration/features/chat-group/suggested-questions-after-answer/index.tsx

@@ -1,9 +1,11 @@
 'use client'
-import React, { FC } from 'react'
+import type { FC } from 'react'
+import React from 'react'
 import { useTranslation } from 'react-i18next'
 import Panel from '@/app/components/app/configuration/base/feature-panel'
 import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
 import Tooltip from '@/app/components/base/tooltip'
+import { HelpCircle } from '@/app/components/base/icons/src/vender/line/general'
 
 const SuggestedQuestionsAfterAnswer: FC = () => {
   const { t } = useTranslation()
@@ -16,9 +18,7 @@ const SuggestedQuestionsAfterAnswer: FC = () => {
           <Tooltip htmlContent={<div className='w-[180px]'>
             {t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
           </div>} selector='suggestion-question-tooltip'>
-            <svg width="16" height="17" viewBox="0 0 16 17" fill="none" xmlns="http://www.w3.org/2000/svg">
-              <path d="M8.66667 11.1667H8V8.5H7.33333M8 5.83333H8.00667M14 8.5C14 9.28793 13.8448 10.0681 13.5433 10.7961C13.2417 11.5241 12.7998 12.1855 12.2426 12.7426C11.6855 13.2998 11.0241 13.7417 10.2961 14.0433C9.56815 14.3448 8.78793 14.5 8 14.5C7.21207 14.5 6.43185 14.3448 5.7039 14.0433C4.97595 13.7417 4.31451 13.2998 3.75736 12.7426C3.20021 12.1855 2.75825 11.5241 2.45672 10.7961C2.15519 10.0681 2 9.28793 2 8.5C2 6.9087 2.63214 5.38258 3.75736 4.25736C4.88258 3.13214 6.4087 2.5 8 2.5C9.5913 2.5 11.1174 3.13214 12.2426 4.25736C13.3679 5.38258 14 6.9087 14 8.5Z" stroke="#9CA3AF" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
-            </svg>
+            <HelpCircle className='w-[14px] h-[14px] text-gray-400' />
           </Tooltip>
         </div>
       }

+ 176 - 0
web/app/components/app/configuration/hooks/use-advanced-prompt-config.ts

@@ -0,0 +1,176 @@
+import { useState } from 'react'
+import { clone } from 'lodash-es'
+import produce from 'immer'
+import type { ChatPromptConfig, CompletionPromptConfig, ConversationHistoriesRole, PromptItem } from '@/models/debug'
+import { PromptMode } from '@/models/debug'
+import { AppType, ModelModeType } from '@/types/app'
+import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
+import { PRE_PROMPT_PLACEHOLDER_TEXT, checkHasContextBlock, checkHasHistoryBlock, checkHasQueryBlock } from '@/app/components/base/prompt-editor/constants'
+import { fetchPromptTemplate } from '@/service/debug'
+
+type Param = {
+  appMode: string
+  modelModeType: ModelModeType
+  modelName: string
+  promptMode: PromptMode
+  prePrompt: string
+  onUserChangedPrompt: () => void
+  hasSetDataSet: boolean
+}
+
+const useAdvancedPromptConfig = ({
+  appMode,
+  modelModeType,
+  modelName,
+  promptMode,
+  prePrompt,
+  onUserChangedPrompt,
+  hasSetDataSet,
+}: Param) => {
+  const isAdvancedPrompt = promptMode === PromptMode.advanced
+  const [chatPromptConfig, setChatPromptConfig] = useState<ChatPromptConfig>(clone(DEFAULT_CHAT_PROMPT_CONFIG))
+  const [completionPromptConfig, setCompletionPromptConfig] = useState<CompletionPromptConfig>(clone(DEFAULT_COMPLETION_PROMPT_CONFIG))
+
+  const currentAdvancedPrompt = (() => {
+    if (!isAdvancedPrompt)
+      return []
+
+    return (modelModeType === ModelModeType.chat) ? chatPromptConfig.prompt : completionPromptConfig.prompt
+  })()
+
+  const setCurrentAdvancedPrompt = (prompt: PromptItem | PromptItem[], isUserChanged?: boolean) => {
+    if (!isAdvancedPrompt)
+      return
+
+    if (modelModeType === ModelModeType.chat) {
+      setChatPromptConfig({
+        ...chatPromptConfig,
+        prompt: prompt as PromptItem[],
+      })
+    }
+    else {
+      setCompletionPromptConfig({
+        ...completionPromptConfig,
+        prompt: prompt as PromptItem,
+      })
+    }
+    if (isUserChanged)
+      onUserChangedPrompt()
+  }
+
+  const setConversationHistoriesRole = (conversationHistoriesRole: ConversationHistoriesRole) => {
+    setCompletionPromptConfig({
+      ...completionPromptConfig,
+      conversation_histories_role: conversationHistoriesRole,
+    })
+  }
+
+  const hasSetBlockStatus = (() => {
+    if (!isAdvancedPrompt) {
+      return {
+        context: checkHasContextBlock(prePrompt),
+        history: false,
+        query: false,
+      }
+    }
+    if (modelModeType === ModelModeType.chat) {
+      return {
+        context: !!chatPromptConfig.prompt.find(p => checkHasContextBlock(p.text)),
+        history: false,
+        query: !!chatPromptConfig.prompt.find(p => checkHasQueryBlock(p.text)),
+      }
+    }
+    else {
+      const prompt = completionPromptConfig.prompt.text
+      return {
+        context: checkHasContextBlock(prompt),
+        history: checkHasHistoryBlock(prompt),
+        query: checkHasQueryBlock(prompt),
+      }
+    }
+  })()
+
+  /* prompt: simple to advanced process, or chat model to completion model
+  * 1. migrate prompt
+  * 2. change promptMode to advanced
+  */
+  const migrateToDefaultPrompt = async (isMigrateToCompetition?: boolean, toModelModeType?: ModelModeType) => {
+    const mode = modelModeType
+    const toReplacePrePrompt = prePrompt || ''
+    if (!isAdvancedPrompt) {
+      const { chat_prompt_config, completion_prompt_config } = await fetchPromptTemplate({
+        appMode,
+        mode,
+        modelName,
+        hasSetDataSet,
+      })
+      if (modelModeType === ModelModeType.chat) {
+        const newPromptConfig = produce(chat_prompt_config, (draft) => {
+          draft.prompt = draft.prompt.map((p) => {
+            return {
+              ...p,
+              text: p.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt),
+            }
+          })
+        })
+        setChatPromptConfig(newPromptConfig)
+      }
+
+      else {
+        const newPromptConfig = produce(completion_prompt_config, (draft) => {
+          draft.prompt.text = draft.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
+        })
+        setCompletionPromptConfig(newPromptConfig)
+      }
+      return
+    }
+
+    if (isMigrateToCompetition) {
+      const { completion_prompt_config, chat_prompt_config } = await fetchPromptTemplate({
+        appMode,
+        mode: toModelModeType as ModelModeType,
+        modelName,
+        hasSetDataSet,
+      })
+
+      if (toModelModeType === ModelModeType.completion) {
+        const newPromptConfig = produce(completion_prompt_config, (draft) => {
+          if (!completionPromptConfig.prompt.text)
+            draft.prompt.text = draft.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
+
+          else
+            draft.prompt.text = completionPromptConfig.prompt.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt)
+
+          if (appMode === AppType.chat && completionPromptConfig.conversation_histories_role.assistant_prefix && completionPromptConfig.conversation_histories_role.user_prefix)
+            draft.conversation_histories_role = completionPromptConfig.conversation_histories_role
+        })
+        setCompletionPromptConfig(newPromptConfig)
+      }
+      else {
+        const newPromptConfig = produce(chat_prompt_config, (draft) => {
+          draft.prompt = draft.prompt.map((p) => {
+            return {
+              ...p,
+              text: p.text.replace(PRE_PROMPT_PLACEHOLDER_TEXT, toReplacePrePrompt),
+            }
+          })
+        })
+        setChatPromptConfig(newPromptConfig)
+      }
+    }
+  }
+
+  return {
+    chatPromptConfig,
+    setChatPromptConfig,
+    completionPromptConfig,
+    setCompletionPromptConfig,
+    currentAdvancedPrompt,
+    setCurrentAdvancedPrompt,
+    hasSetBlockStatus,
+    setConversationHistoriesRole,
+    migrateToDefaultPrompt,
+  }
+}
+
+export default useAdvancedPromptConfig

+ 273 - 20
web/app/components/app/configuration/index.tsx

@@ -7,9 +7,13 @@ import { usePathname } from 'next/navigation'
 import produce from 'immer'
 import { useBoolean } from 'ahooks'
 import cn from 'classnames'
+import { clone, isEqual } from 'lodash-es'
 import Button from '../../base/button'
 import Loading from '../../base/loading'
-import type { CompletionParams, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, PromptVariable } from '@/models/debug'
+import s from './style.module.css'
+import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
+import EditHistoryModal from './config-prompt/conversation-histroy/edit-modal'
+import type { CompletionParams, DatasetConfigs, Inputs, ModelConfig, MoreLikeThisConfig, PromptConfig, PromptVariable } from '@/models/debug'
 import type { DataSet } from '@/models/datasets'
 import type { ModelConfig as BackendModelConfig } from '@/types/app'
 import ConfigContext from '@/context/debug-configuration'
@@ -24,7 +28,11 @@ import { promptVariablesToUserInputsForm, userInputsFormToPromptVariables } from
 import { fetchDatasets } from '@/service/datasets'
 import AccountSetting from '@/app/components/header/account-setting'
 import { useProviderContext } from '@/context/provider-context'
-import { AppType } from '@/types/app'
+import { AppType, ModelModeType } from '@/types/app'
+import { FlipBackward } from '@/app/components/base/icons/src/vender/line/arrows'
+import { PromptMode } from '@/models/debug'
+import { DEFAULT_CHAT_PROMPT_CONFIG, DEFAULT_COMPLETION_PROMPT_CONFIG } from '@/config'
+import SelectDataSet from '@/app/components/app/configuration/dataset-config/select-dataset'
 
 type PublichConfig = {
   modelConfig: ModelConfig
@@ -44,6 +52,7 @@ const Configuration: FC = () => {
   const [publishedConfig, setPublishedConfig] = useState<PublichConfig | null>(null)
 
   const [conversationId, setConversationId] = useState<string | null>('')
+
   const [introduction, setIntroduction] = useState<string>('')
   const [controlClearChatMessage, setControlClearChatMessage] = useState(0)
   const [prevPromptConfig, setPrevPromptConfig] = useState<PromptConfig>({
@@ -75,6 +84,7 @@ const Configuration: FC = () => {
   const [modelConfig, doSetModelConfig] = useState<ModelConfig>({
     provider: ProviderEnum.openai,
     model_id: 'gpt-3.5-turbo',
+    mode: ModelModeType.unset,
     configs: {
       prompt_template: '',
       prompt_variables: [] as PromptVariable[],
@@ -87,21 +97,52 @@ const Configuration: FC = () => {
     dataSets: [],
   })
 
+  const [datasetConfigs, setDatasetConfigs] = useState<DatasetConfigs>({
+    top_k: 2,
+    score_threshold: {
+      enable: false,
+      value: 0.7,
+    },
+  })
+
   const setModelConfig = (newModelConfig: ModelConfig) => {
     doSetModelConfig(newModelConfig)
   }
 
-  const setModelId = (modelId: string, provider: ProviderEnum) => {
-    const newModelConfig = produce(modelConfig, (draft: any) => {
-      draft.provider = provider
-      draft.model_id = modelId
-    })
-    setModelConfig(newModelConfig)
-  }
+  const modelModeType = modelConfig.mode
 
   const [dataSets, setDataSets] = useState<DataSet[]>([])
   const contextVar = modelConfig.configs.prompt_variables.find(item => item.is_context_var)?.key
   const hasSetContextVar = !!contextVar
+  const [isShowSelectDataSet, { setTrue: showSelectDataSet, setFalse: hideSelectDataSet }] = useBoolean(false)
+  const selectedIds = dataSets.map(item => item.id)
+  const handleSelect = (data: DataSet[]) => {
+    if (isEqual(data.map(item => item.id), dataSets.map(item => item.id))) {
+      hideSelectDataSet()
+      return
+    }
+
+    setFormattingChanged(true)
+    if (data.find(item => !item.name)) { // has not loaded selected dataset
+      const newSelected = produce(data, (draft) => {
+        data.forEach((item, index) => {
+          if (!item.name) { // not fetched database
+            const newItem = dataSets.find(i => i.id === item.id)
+            if (newItem)
+              draft[index] = newItem
+          }
+        })
+      })
+      setDataSets(newSelected)
+    }
+    else {
+      setDataSets(data)
+    }
+    hideSelectDataSet()
+  }
+
+  const [isShowHistoryModal, { setTrue: showHistoryModal, setFalse: hideHistoryModal }] = useBoolean(false)
+
   const syncToPublishedConfig = (_publishedConfig: PublichConfig) => {
     const modelConfig = _publishedConfig.modelConfig
     setModelConfig(_publishedConfig.modelConfig)
@@ -140,14 +181,101 @@ const Configuration: FC = () => {
       return quota_used === quota_limit
     })
 
+  // Fill old app data missing model mode.
+  useEffect(() => {
+    if (hasFetchedDetail && !modelModeType) {
+      const mode = textGenerationModelList.find(({ model_name }) => model_name === modelConfig.model_id)?.model_mode
+      if (mode) {
+        const newModelConfig = produce(modelConfig, (draft) => {
+          draft.mode = mode
+        })
+        setModelConfig(newModelConfig)
+      }
+    }
+  }, [textGenerationModelList, hasFetchedDetail])
+
   const hasSetAPIKEY = hasSetCustomAPIKEY || !isTrailFinished
 
   const [isShowSetAPIKey, { setTrue: showSetAPIKey, setFalse: hideSetAPIkey }] = useBoolean()
+  const [promptMode, doSetPromptMode] = useState(PromptMode.advanced)
+  const isAdvancedMode = promptMode === PromptMode.advanced
+  const [canReturnToSimpleMode, setCanReturnToSimpleMode] = useState(true)
+  const setPromptMode = async (mode: PromptMode) => {
+    if (mode === PromptMode.advanced) {
+      // eslint-disable-next-line @typescript-eslint/no-use-before-define
+      await migrateToDefaultPrompt()
+      setCanReturnToSimpleMode(true)
+    }
+
+    doSetPromptMode(mode)
+  }
+
+  const {
+    chatPromptConfig,
+    setChatPromptConfig,
+    completionPromptConfig,
+    setCompletionPromptConfig,
+    currentAdvancedPrompt,
+    setCurrentAdvancedPrompt,
+    hasSetBlockStatus,
+    setConversationHistoriesRole,
+    migrateToDefaultPrompt,
+  } = useAdvancedPromptConfig({
+    appMode: mode,
+    modelName: modelConfig.model_id,
+    promptMode,
+    modelModeType,
+    prePrompt: modelConfig.configs.prompt_template,
+    hasSetDataSet: dataSets.length > 0,
+    onUserChangedPrompt: () => {
+      setCanReturnToSimpleMode(false)
+    },
+  })
+
+  const setModel = async ({
+    id: modelId,
+    provider,
+    mode: modeMode,
+  }: { id: string; provider: ProviderEnum; mode: ModelModeType }) => {
+    if (isAdvancedMode) {
+      const appMode = mode
+
+      if (modeMode === ModelModeType.completion) {
+        if (appMode === AppType.chat) {
+          if (!completionPromptConfig.prompt.text || !completionPromptConfig.conversation_histories_role.assistant_prefix || !completionPromptConfig.conversation_histories_role.user_prefix)
+            await migrateToDefaultPrompt(true, ModelModeType.completion)
+        }
+        else {
+          if (!completionPromptConfig.prompt.text)
+            await migrateToDefaultPrompt(true, ModelModeType.completion)
+        }
+      }
+      if (modeMode === ModelModeType.chat) {
+        if (chatPromptConfig.prompt.length === 0)
+          await migrateToDefaultPrompt(true, ModelModeType.chat)
+      }
+    }
+    const newModelConfig = produce(modelConfig, (draft) => {
+      draft.provider = provider
+      draft.model_id = modelId
+      draft.mode = modeMode
+    })
+
+    setModelConfig(newModelConfig)
+  }
 
   useEffect(() => {
     fetchAppDetail({ url: '/apps', id: appId }).then(async (res) => {
       setMode(res.mode)
       const modelConfig = res.model_config
+      const promptMode = modelConfig.prompt_type === PromptMode.advanced ? PromptMode.advanced : PromptMode.simple
+      doSetPromptMode(promptMode)
+      if (promptMode === PromptMode.advanced) {
+        setChatPromptConfig(modelConfig.chat_prompt_config || clone(DEFAULT_CHAT_PROMPT_CONFIG) as any)
+        setCompletionPromptConfig(modelConfig.completion_prompt_config || clone(DEFAULT_COMPLETION_PROMPT_CONFIG) as any)
+        setCanReturnToSimpleMode(false)
+      }
+
       const model = res.model_config.model
 
       let datasets: any = null
@@ -177,6 +305,7 @@ const Configuration: FC = () => {
         modelConfig: {
           provider: model.provider,
           model_id: model.name,
+          mode: model.mode,
           configs: {
             prompt_template: modelConfig.pre_prompt,
             prompt_variables: userInputsFormToPromptVariables(modelConfig.user_input_form, modelConfig.dataset_query_variable),
@@ -197,10 +326,38 @@ const Configuration: FC = () => {
     })
   }, [appId])
 
-  const promptEmpty = mode === AppType.completion && !modelConfig.configs.prompt_template
+  const promptEmpty = (() => {
+    if (mode === AppType.chat)
+      return false
+
+    if (isAdvancedMode) {
+      if (modelModeType === ModelModeType.chat)
+        return chatPromptConfig.prompt.every(({ text }) => !text)
+
+      else
+        return !completionPromptConfig.prompt.text
+    }
+
+    else { return !modelConfig.configs.prompt_template }
+  })()
+  const cannotPublish = (() => {
+    if (mode === AppType.chat) {
+      if (!isAdvancedMode)
+        return false
+
+      if (modelModeType === ModelModeType.completion) {
+        if (!hasSetBlockStatus.history || !hasSetBlockStatus.query)
+          return true
+
+        return false
+      }
+
+      return false
+    }
+    else { return promptEmpty }
+  })()
   const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
-  const cannotPublish = promptEmpty || contextVarEmpty
-  const saveAppConfig = async () => {
+  const handlePublish = async (isSilence?: boolean) => {
     const modelId = modelConfig.model_id
     const promptTemplate = modelConfig.configs.prompt_template
     const promptVariables = modelConfig.configs.prompt_variables
@@ -209,6 +366,18 @@ const Configuration: FC = () => {
       notify({ type: 'error', message: t('appDebug.otherError.promptNoBeEmpty'), duration: 3000 })
       return
     }
+    if (isAdvancedMode && mode === AppType.chat) {
+      if (modelModeType === ModelModeType.completion) {
+        if (!hasSetBlockStatus.history) {
+          notify({ type: 'error', message: t('appDebug.otherError.historyNoBeEmpty'), duration: 3000 })
+          return
+        }
+        if (!hasSetBlockStatus.query) {
+          notify({ type: 'error', message: t('appDebug.otherError.queryNoBeEmpty'), duration: 3000 })
+          return
+        }
+      }
+    }
     if (contextVarEmpty) {
       notify({ type: 'error', message: t('appDebug.feature.dataSet.queryVariable.contextVarNotEmpty'), duration: 3000 })
       return
@@ -222,7 +391,11 @@ const Configuration: FC = () => {
 
     // new model config data struct
     const data: BackendModelConfig = {
-      pre_prompt: promptTemplate,
+      // Simple Mode prompt
+      pre_prompt: !isAdvancedMode ? promptTemplate : '',
+      prompt_type: promptMode,
+      chat_prompt_config: {},
+      completion_prompt_config: {},
       user_input_form: promptVariablesToUserInputsForm(promptVariables),
       dataset_query_variable: contextVar || '',
       opening_statement: introduction || '',
@@ -237,8 +410,15 @@ const Configuration: FC = () => {
       model: {
         provider: modelConfig.provider,
         name: modelId,
+        mode: modelConfig.mode,
         completion_params: completionParams as any,
       },
+      dataset_configs: datasetConfigs,
+    }
+
+    if (isAdvancedMode) {
+      data.chat_prompt_config = chatPromptConfig
+      data.completion_prompt_config = completionPromptConfig
     }
 
     await updateAppModelConfig({ url: `/apps/${appId}/model-config`, body: data })
@@ -254,7 +434,11 @@ const Configuration: FC = () => {
       modelConfig: newModelConfig,
       completionParams,
     })
-    notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
+    if (!isSilence)
+      notify({ type: 'success', message: t('common.api.success'), duration: 3000 })
+
+    setCanReturnToSimpleMode(false)
+    return true
   }
 
   const [showConfirm, setShowConfirm] = useState(false)
@@ -278,6 +462,20 @@ const Configuration: FC = () => {
       hasSetAPIKEY,
       isTrailFinished,
       mode,
+      modelModeType,
+      promptMode,
+      isAdvancedMode,
+      setPromptMode,
+      canReturnToSimpleMode,
+      setCanReturnToSimpleMode,
+      chatPromptConfig,
+      completionPromptConfig,
+      currentAdvancedPrompt,
+      setCurrentAdvancedPrompt,
+      conversationHistoriesRole: completionPromptConfig.conversation_histories_role,
+      showHistoryModal,
+      setConversationHistoriesRole,
+      hasSetBlockStatus,
       conversationId,
       introduction,
       setIntroduction,
@@ -304,23 +502,56 @@ const Configuration: FC = () => {
       setCompletionParams,
       modelConfig,
       setModelConfig,
+      showSelectDataSet,
       dataSets,
       setDataSets,
+      datasetConfigs,
+      setDatasetConfigs,
       hasSetContextVar,
     }}
     >
       <>
         <div className="flex flex-col h-full">
-          <div className='flex items-center justify-between px-6 border-b shrink-0 h-14 boder-gray-100'>
-            <div className='text-xl text-gray-900'>{t('appDebug.pageTitle')}</div>
+          <div className='flex items-center justify-between px-6 shrink-0 h-14'>
+            <div>
+              <div className='italic text-base font-bold text-gray-900 leading-[18px]'>{t('appDebug.pageTitle.line1')}</div>
+              <div className='flex items-center h-6 space-x-1 text-xs'>
+                <div className='text-gray-500 font-medium italic'>{t('appDebug.pageTitle.line2')}</div>
+                {/* modelModeType missing can not load template */}
+                {(!isAdvancedMode && modelModeType) && (
+                  <div
+                    onClick={() => setPromptMode(PromptMode.advanced)}
+                    className={'cursor-pointer text-indigo-600'}
+                  >
+                    {t('appDebug.promptMode.simple')}
+                  </div>
+                )}
+                {isAdvancedMode && (
+                  <div className='flex items-center space-x-2'>
+                    <div className={`${s.advancedPromptMode} italic text-indigo-600`}>{t('appDebug.promptMode.advanced')}</div>
+                    {canReturnToSimpleMode && (
+                      <div
+                        onClick={() => setPromptMode(PromptMode.simple)}
+                        className='flex items-center h-6 px-2 bg-indigo-600 shadow-xs border border-gray-200 rounded-lg text-white text-xs font-semibold cursor-pointer space-x-1'
+                      >
+                        <FlipBackward className='w-3 h-3 text-white'/>
+                        <div className='text-xs font-semibold uppercase'>{t('appDebug.promptMode.switchBack')}</div>
+                      </div>
+                    )}
+                  </div>
+                )}
+              </div>
+            </div>
+
             <div className='flex items-center'>
               {/* Model and Parameters */}
               <ConfigModel
+                isAdvancedMode={isAdvancedMode}
                 mode={mode}
                 provider={modelConfig.provider as ProviderEnum}
                 completionParams={completionParams}
                 modelId={modelConfig.model_id}
-                setModelId={setModelId}
+                setModel={setModel}
                 onCompletionParamsChange={(newParams: CompletionParams) => {
                   setCompletionParams(newParams)
                 }}
@@ -328,14 +559,14 @@ const Configuration: FC = () => {
               />
               <div className='mx-3 w-[1px] h-[14px] bg-gray-200'></div>
               <Button onClick={() => setShowConfirm(true)} className='shrink-0 mr-2 w-[70px] !h-8 !text-[13px] font-medium'>{t('appDebug.operation.resetConfig')}</Button>
-              <Button type='primary' onClick={saveAppConfig} className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}>{t('appDebug.operation.applyConfig')}</Button>
+              <Button type='primary' onClick={() => handlePublish(false)} className={cn(cannotPublish && '!bg-primary-200 !cursor-not-allowed', 'shrink-0 w-[70px] !h-8 !text-[13px] font-medium')}>{t('appDebug.operation.applyConfig')}</Button>
             </div>
           </div>
           <div className='flex grow h-[200px]'>
-            <div className="w-[574px] shrink-0 h-full overflow-y-auto border-r border-gray-100 py-4 px-6">
+            <div className="w-1/2 min-w-[560px] shrink-0">
               <Config />
             </div>
-            <div className="relative grow h-full overflow-y-auto  py-4 px-6 bg-gray-50 flex flex-col">
+            <div className="relative w-1/2  grow h-full overflow-y-auto  py-4 px-6 bg-gray-50 flex flex-col rounded-tl-2xl border-t border-l" style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
               <Debug hasSetAPIKEY={hasSetAPIKEY} onSetting={showSetAPIKey} />
             </div>
           </div>
@@ -373,6 +604,28 @@ const Configuration: FC = () => {
         {isShowSetAPIKey && <AccountSetting activeTab="provider" onCancel={async () => {
           hideSetAPIkey()
         }} />}
+
+        {isShowSelectDataSet && (
+          <SelectDataSet
+            isShow={isShowSelectDataSet}
+            onClose={hideSelectDataSet}
+            selectedIds={selectedIds}
+            onSelect={handleSelect}
+          />
+        )}
+
+        {isShowHistoryModal && (
+          <EditHistoryModal
+            isShow={isShowHistoryModal}
+            saveLoading={false}
+            onClose={hideHistoryModal}
+            data={completionPromptConfig.conversation_histories_role}
+            onSave={(data) => {
+              setConversationHistoriesRole(data)
+              hideHistoryModal()
+            }}
+          />
+        )}
       </>
     </ConfigContext.Provider>
   )

+ 35 - 0
web/app/components/app/configuration/prompt-mode/advanced-mode-waring.tsx

@@ -0,0 +1,35 @@
+'use client'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+
+const AdvancedModeWarning: FC = () => {
+  const { t } = useTranslation()
+  const [show, setShow] = React.useState(true)
+  if (!show)
+    return null
+  return (
+    <div className='mb-3 py-3 px-4 border border-[#FEF0C7] rounded-xl bg-[#FFFAEB]' >
+      <div className='mb-2 text-xs leading-[18px] font-bold text-[#DC6803]'>{t('appDebug.promptMode.advancedWarning.title')}</div>
+      <div className='flex justify-between items-center'>
+        <div className='text-xs leading-[18px] '>
+          <span className='text-gray-700'>{t('appDebug.promptMode.advancedWarning.description')}</span>
+          {/* TODO: Doc link */}
+          {/* <a
+            className='font-medium text-[#155EEF]'
+            href='https://docs.dify.ai/getting-started/readme'
+            target='_blank'
+          >
+            {t('appDebug.promptMode.advancedWarning.learnMore')}
+          </a> */}
+        </div>
+
+        <div
+          className='flex items-center h-6 px-2 rounded-md bg-[#fff] border border-gray-200 shadow-xs text-xs font-medium text-primary-600 cursor-pointer'
+          onClick={() => setShow(false)}
+        >{t('appDebug.promptMode.advancedWarning.ok')}</div>
+      </div>
+    </div>
+  )
+}
+export default React.memo(AdvancedModeWarning)

+ 19 - 59
web/app/components/app/configuration/prompt-value-panel/index.tsx

@@ -6,10 +6,9 @@ import { useContext } from 'use-context-selector'
 import {
   PlayIcon,
 } from '@heroicons/react/24/solid'
-import { BracketsX as VarIcon } from '@/app/components/base/icons/src/vender/line/development'
 import ConfigContext from '@/context/debug-configuration'
 import type { PromptVariable } from '@/models/debug'
-import { AppType } from '@/types/app'
+import { AppType, ModelModeType } from '@/types/app'
 import Select from '@/app/components/base/select'
 import { DEFAULT_VALUE_MAX_LEN } from '@/config'
 import Button from '@/app/components/base/button'
@@ -21,23 +20,13 @@ export type IPromptValuePanelProps = {
   onSend?: () => void
 }
 
-const starIcon = (
-  <svg width="12" height="12" viewBox="0 0 12 12" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M2.75 1C2.75 0.723858 2.52614 0.5 2.25 0.5C1.97386 0.5 1.75 0.723858 1.75 1V1.75H1C0.723858 1.75 0.5 1.97386 0.5 2.25C0.5 2.52614 0.723858 2.75 1 2.75H1.75V3.5C1.75 3.77614 1.97386 4 2.25 4C2.52614 4 2.75 3.77614 2.75 3.5V2.75H3.5C3.77614 2.75 4 2.52614 4 2.25C4 1.97386 3.77614 1.75 3.5 1.75H2.75V1Z" fill="#444CE7" />
-    <path d="M2.75 8.5C2.75 8.22386 2.52614 8 2.25 8C1.97386 8 1.75 8.22386 1.75 8.5V9.25H1C0.723858 9.25 0.5 9.47386 0.5 9.75C0.5 10.0261 0.723858 10.25 1 10.25H1.75V11C1.75 11.2761 1.97386 11.5 2.25 11.5C2.52614 11.5 2.75 11.2761 2.75 11V10.25H3.5C3.77614 10.25 4 10.0261 4 9.75C4 9.47386 3.77614 9.25 3.5 9.25H2.75V8.5Z" fill="#444CE7" />
-    <path d="M6.96667 1.32051C6.8924 1.12741 6.70689 1 6.5 1C6.29311 1 6.10759 1.12741 6.03333 1.32051L5.16624 3.57494C5.01604 3.96546 4.96884 4.078 4.90428 4.1688C4.8395 4.2599 4.7599 4.3395 4.6688 4.40428C4.578 4.46884 4.46546 4.51604 4.07494 4.66624L1.82051 5.53333C1.62741 5.60759 1.5 5.79311 1.5 6C1.5 6.20689 1.62741 6.39241 1.82051 6.46667L4.07494 7.33376C4.46546 7.48396 4.578 7.53116 4.6688 7.59572C4.7599 7.6605 4.8395 7.7401 4.90428 7.8312C4.96884 7.922 5.01604 8.03454 5.16624 8.42506L6.03333 10.6795C6.1076 10.8726 6.29311 11 6.5 11C6.70689 11 6.89241 10.8726 6.96667 10.6795L7.83376 8.42506C7.98396 8.03454 8.03116 7.922 8.09572 7.8312C8.1605 7.7401 8.2401 7.6605 8.3312 7.59572C8.422 7.53116 8.53454 7.48396 8.92506 7.33376L11.1795 6.46667C11.3726 6.39241 11.5 6.20689 11.5 6C11.5 5.79311 11.3726 5.60759 11.1795 5.53333L8.92506 4.66624C8.53454 4.51604 8.422 4.46884 8.3312 4.40428C8.2401 4.3395 8.1605 4.2599 8.09572 4.1688C8.03116 4.078 7.98396 3.96546 7.83376 3.57494L6.96667 1.32051Z" fill="#444CE7" />
-  </svg>
-)
-
 const PromptValuePanel: FC<IPromptValuePanelProps> = ({
   appType,
   onSend,
 }) => {
   const { t } = useTranslation()
-  const { modelConfig, inputs, setInputs, mode } = useContext(ConfigContext)
-  const [promptPreviewCollapse, setPromptPreviewCollapse] = useState(false)
+  const { modelModeType, modelConfig, inputs, setInputs, mode, isAdvancedMode, completionPromptConfig, chatPromptConfig } = useContext(ConfigContext)
   const [userInputFieldCollapse, setUserInputFieldCollapse] = useState(false)
-  const promptTemplate = modelConfig.configs.prompt_template
   const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
     return key && key?.trim() && name && name?.trim()
   })
@@ -50,7 +39,18 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
     return obj
   })()
 
-  const canNotRun = mode === AppType.completion && !modelConfig.configs.prompt_template
+  const canNotRun = (() => {
+    if (mode !== AppType.completion)
+      return true
+
+    if (isAdvancedMode) {
+      if (modelModeType === ModelModeType.chat)
+        return chatPromptConfig.prompt.every(({ text }) => !text)
+      return !completionPromptConfig.prompt.text
+    }
+
+    else { return !modelConfig.configs.prompt_template }
+  })()
   const renderRunButton = () => {
     return (
       <Button
@@ -83,61 +83,21 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
     setInputs(newInputs)
   }
 
-  const promptPreview = (
-    <div className='py-3 rounded-t-xl bg-indigo-25'>
-      <div className="px-4">
-        <div className="flex items-center space-x-1 cursor-pointer" onClick={() => setPromptPreviewCollapse(!promptPreviewCollapse)}>
-          {starIcon}
-          <div className="text-xs font-medium text-indigo-600 uppercase">{t('appDebug.inputs.previewTitle')}</div>
-          {
-            promptPreviewCollapse
-              ? <ChevronRight className='w-3 h-3 text-gray-700' />
-              : <ChevronDown className='w-3 h-3 text-gray-700' />
-          }
-        </div>
-        {
-          !promptPreviewCollapse && (
-            <div className='mt-2  leading-normal'>
-              {
-                (promptTemplate && promptTemplate?.trim())
-                  ? (
-                    <div
-                      className="max-h-48 overflow-y-auto text-sm text-gray-700 break-all"
-                      dangerouslySetInnerHTML={{
-                        __html: format(replaceStringWithValuesWithFormat(promptTemplate.replace(/</g, '&lt;').replace(/>/g, '&gt;'), promptVariables, inputs)),
-                      }}
-                    >
-                    </div>
-                  )
-                  : (
-                    <div className='text-xs text-gray-500'>{t('appDebug.inputs.noPrompt')}</div>
-                  )
-              }
-            </div>
-          )
-        }
-      </div>
-    </div>
-  )
-
   return (
     <div className="pb-3 border border-gray-200 bg-white rounded-xl" style={{
       boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
     }}>
-      {promptPreview}
-
       <div className={'mt-3 px-4 bg-white'}>
         <div className={
           `${!userInputFieldCollapse && 'mb-2'}`
         }>
           <div className='flex items-center space-x-1 cursor-pointer' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
-            <div className='flex items-center justify-center w-4 h-4'><VarIcon className='w-4 h-4 text-primary-500'/></div>
-            <div className='text-xs font-medium text-gray-800'>{t('appDebug.inputs.userInputField')}</div>
             {
               userInputFieldCollapse
-                ? <ChevronRight className='w-3 h-3 text-gray-700' />
-                : <ChevronDown className='w-3 h-3 text-gray-700' />
+                ? <ChevronRight className='w-3 h-3 text-gray-300' />
+                : <ChevronDown className='w-3 h-3 text-gray-300' />
             }
+            <div className='text-xs font-medium text-gray-800 uppercase'>{t('appDebug.inputs.userInputField')}</div>
           </div>
           {appType === AppType.completion && promptVariables.length > 0 && !userInputFieldCollapse && (
             <div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
@@ -150,8 +110,8 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
                 ? (
                   <div className="space-y-3 ">
                     {promptVariables.map(({ key, name, type, options, max_length, required }) => (
-                      <div key={key} className="flex justify-between">
-                        <div className="mr-1 pt-2 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
+                      <div key={key} className="xl:flex justify-between">
+                        <div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{name || key}</div>
                         {type === 'select' && (
                           <Select
                             className='w-full'

+ 14 - 0
web/app/components/app/configuration/style.module.css

@@ -0,0 +1,14 @@
+.advancedPromptMode {
+  position: relative;
+}
+
+.advancedPromptMode::before {
+  content: '';
+  position: absolute;
+  bottom: 0;
+  left: -1px;
+  width: 100%;
+  height: 3px;
+  background-color: rgba(68, 76, 231, 0.18);
+  transform: skewX(-30deg);
+}

+ 83 - 45
web/app/components/app/log/list.tsx

@@ -9,14 +9,14 @@ import {
   InformationCircleIcon,
   XMarkIcon,
 } from '@heroicons/react/24/outline'
-import { SparklesIcon } from '@heroicons/react/24/solid'
 import { get } from 'lodash-es'
 import InfiniteScroll from 'react-infinite-scroll-component'
 import dayjs from 'dayjs'
 import { createContext, useContext } from 'use-context-selector'
-import classNames from 'classnames'
 import { useTranslation } from 'react-i18next'
+import cn from 'classnames'
 import s from './style.module.css'
+import VarPanel from './var-panel'
 import { randomString } from '@/utils'
 import { EditIconSolid } from '@/app/components/app/chat/icon-component'
 import type { FeedbackFunc, Feedbacktype, IChatItem, SubmitAnnotationFunc } from '@/app/components/app/chat/type'
@@ -32,6 +32,8 @@ import { fetchChatConversationDetail, fetchChatMessages, fetchCompletionConversa
 import { TONE_LIST } from '@/config'
 import ModelIcon from '@/app/components/app/configuration/config-model/model-icon'
 import ModelName from '@/app/components/app/configuration/config-model/model-name'
+import ModelModeTypeLabel from '@/app/components/app/configuration/config-model/model-mode-type-label'
+import { ModelModeType } from '@/types/app'
 
 type IConversationList = {
   logs?: ChatConversationsResponse | CompletionConversationsResponse
@@ -78,6 +80,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
       id: `question-${item.id}`,
       content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
       isAnswer: false,
+      log: item.message as any,
     })
 
     newChatList.push({
@@ -102,7 +105,7 @@ const getFormattedChatList = (messages: ChatMessage[]) => {
 const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
 
 type IDetailPanel<T> = {
-  detail: T
+  detail: any
   onFeedback: FeedbackFunc
   onSubmitAnnotation: SubmitAnnotationFunc
 }
@@ -112,7 +115,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
   const { t } = useTranslation()
   const [items, setItems] = React.useState<IChatItem[]>([])
   const [hasMore, setHasMore] = useState(true)
-
+  const [varValues, setVarValues] = useState<Record<string, string>>({})
   const fetchData = async () => {
     try {
       if (!hasMore)
@@ -128,6 +131,10 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
         url: `/apps/${appDetail?.id}/chat-messages`,
         params,
       })
+      if (messageRes.data.length > 0) {
+        const varValues = messageRes.data[0].inputs
+        setVarValues(varValues)
+      }
       const newItems = [...getFormattedChatList(messageRes.data), ...items]
       if (messageRes.has_more === false && detail?.model_config?.configs?.introduction) {
         newItems.unshift({
@@ -153,7 +160,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
 
   const isChatMode = appDetail?.mode === 'chat'
 
-  const targetTone = TONE_LIST.find((item) => {
+  const targetTone = TONE_LIST.find((item: any) => {
     let res = true
     validatedParams.forEach((param) => {
       res = item.config?.[param] === detail.model_config?.configs?.completion_params?.[param]
@@ -161,53 +168,81 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
     return res
   })?.name ?? 'custom'
 
+  const modelName = (detail.model_config as any).model.name
+  const provideName = (detail.model_config as any).model.provider as any
+  const varList = (detail.model_config as any).user_input_form.map((item: any) => {
+    const itemContent = item[Object.keys(item)[0]]
+    return {
+      label: itemContent.variable,
+      value: varValues[itemContent.variable],
+    }
+  })
+
+  const getParamValue = (param: string) => {
+    const value = detail?.model_config.model?.completion_params?.[param] || '-'
+    if (param === 'stop') {
+      if (!value || value.length === 0)
+        return '-'
+
+      return value.join(',')
+    }
+
+    return value
+  }
   return (<div className='rounded-xl border-[0.5px] border-gray-200 h-full flex flex-col overflow-auto'>
     {/* Panel Header */}
     <div className='border-b border-gray-100 py-4 px-6 flex items-center justify-between'>
-      <div className='flex-1'>
-        <span className='text-gray-500 text-[10px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</span>
-        <div className='text-gray-800 text-sm'>{isChatMode ? detail.id : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
+      <div>
+        <div className='text-gray-500 text-[10px] leading-[14px]'>{isChatMode ? t('appLog.detail.conversationId') : t('appLog.detail.time')}</div>
+        <div className='text-gray-700 text-[13px] leading-[18px]'>{isChatMode ? detail.id?.split('-').slice(-1)[0] : dayjs.unix(detail.created_at).format(t('appLog.dateTimeFormat') as string)}</div>
       </div>
-      <div className='mr-2 bg-gray-50 py-1.5 px-2.5 rounded-lg flex items-center text-[13px]'>
-        <ModelIcon
-          className={classNames('mr-1.5', 'w-5 h-5')}
-          modelId={detail.model_config.model.name}
-          providerName={detail.model_config.model.provider}
-        />
-        <ModelName modelId={detail.model_config.model.name} modelDisplayName={detail.model_config.model.name} />
-      </div>
-      <Popover
-        position='br'
-        className='!w-[280px]'
-        btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
-        btnElement={<>
-          <span className='text-[13px]'>{targetTone}</span>
-          <InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
-        </>}
-        htmlContent={<div className='w-[280px]'>
-          <div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
-            <span>Tone of responses</span>
-            <div>{targetTone}</div>
+      <div className='flex items-center'>
+        <div
+          className={cn('mr-2 flex items-center border h-8 px-2 space-x-2 rounded-lg bg-indigo-25 border-[#2A87F5]')}
+        >
+          <ModelIcon
+            className='!w-5 !h-5'
+            modelId={modelName}
+            providerName={provideName}
+          />
+          <div className='text-[13px] text-gray-900 font-medium'>
+            <ModelName modelId={modelName} modelDisplayName={modelName} />
           </div>
-          {['temperature', 'top_p', 'presence_penalty', 'max_tokens'].map((param: string, index: number) => {
-            return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
-              <span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
-              <span className='text-gray-800 font-medium text-xs'>{detail?.model_config.model?.completion_params?.[param] || '-'}</span>
+          <ModelModeTypeLabel type={ModelModeType.chat} isHighlight />
+        </div>
+        <Popover
+          position='br'
+          className='!w-[280px]'
+          btnClassName='mr-4 !bg-gray-50 !py-1.5 !px-2.5 border-none font-normal'
+          btnElement={<>
+            <span className='text-[13px]'>{targetTone}</span>
+            <InformationCircleIcon className='h-4 w-4 text-gray-800 ml-1.5' />
+          </>}
+          htmlContent={<div className='w-[280px]'>
+            <div className='flex justify-between py-2 px-4 font-medium text-sm text-gray-700'>
+              <span>Tone of responses</span>
+              <div>{targetTone}</div>
             </div>
-          })}
-        </div>}
-      />
-      <div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
-        <XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
+            {['temperature', 'top_p', 'presence_penalty', 'max_tokens', 'stop'].map((param: string, index: number) => {
+              return <div className='flex justify-between py-2 px-4 bg-gray-50' key={index}>
+                <span className='text-xs text-gray-700'>{PARAM_MAP[param as keyof typeof PARAM_MAP]}</span>
+                <span className='text-gray-800 font-medium text-xs'>{getParamValue(param)}</span>
+              </div>
+            })}
+          </div>}
+        />
+        <div className='w-6 h-6 rounded-lg flex items-center justify-center hover:cursor-pointer hover:bg-gray-100'>
+          <XMarkIcon className='w-4 h-4 text-gray-500' onClick={onClose} />
+        </div>
       </div>
+
     </div>
     {/* Panel Body */}
-    <div className='bg-gray-50 border border-gray-100 px-4 py-3 mx-6 my-4 rounded-lg'>
-      <div className='text-gray-500 text-xs flex items-center'>
-        <SparklesIcon className='h-3 w-3 mr-1' />{isChatMode ? t('appLog.detail.promptTemplateBeforeChat') : t('appLog.detail.promptTemplate')}
+    {varList.length > 0 && (
+      <div className='px-6 pt-4 pb-2'>
+        <VarPanel varList={varList} />
       </div>
-      <div className='text-gray-700 font-medium text-sm mt-2'>{detail.model_config?.pre_prompt || emptyText}</div>
-    </div>
+    )}
     {!isChatMode
       ? <div className="px-2.5 py-4">
         <Chat
@@ -216,6 +251,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
           onFeedback={onFeedback}
           onSubmitAnnotation={onSubmitAnnotation}
           displayScene='console'
+          isShowPromptLog
         />
       </div>
       : items.length < 8
@@ -226,6 +262,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
             onFeedback={onFeedback}
             onSubmitAnnotation={onSubmitAnnotation}
             displayScene='console'
+            isShowPromptLog
           />
         </div>
         : <div
@@ -265,6 +302,7 @@ function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionCo
               onFeedback={onFeedback}
               onSubmitAnnotation={onSubmitAnnotation}
               displayScene='console'
+              isShowPromptLog
             />
           </InfiniteScroll>
         </div>
@@ -382,7 +420,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
         className={(isHighlight && !isChatMode) ? '' : '!hidden'}
         selector={`highlight-${randomString(16)}`}
       >
-        <div className={classNames(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
+        <div className={cn(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
           {value || '-'}
         </div>
       </Tooltip>
@@ -413,9 +451,9 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
           </tr>
         </thead>
         <tbody className="text-gray-500">
-          {logs.data.map((log) => {
+          {logs.data.map((log: any) => {
             const endUser = log.from_end_user_session_id
-            const leftValue = get(log, isChatMode ? 'summary' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
+            const leftValue = get(log, isChatMode ? 'name' : 'message.inputs.query') || (!isChatMode ? (get(log, 'message.query') || get(log, 'message.inputs.default_input')) : '') || ''
             const rightValue = get(log, isChatMode ? 'message_count' : 'message.answer')
             return <tr
               key={log.id}

+ 48 - 0
web/app/components/app/log/var-panel.tsx

@@ -0,0 +1,48 @@
+'use client'
+import { useBoolean } from 'ahooks'
+import type { FC } from 'react'
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { ChevronDown, ChevronRight } from '@/app/components/base/icons/src/vender/line/arrows'
+
+type Props = {
+  varList: { label: string; value: string }[]
+}
+
+const VarPanel: FC<Props> = ({
+  varList,
+}) => {
+  const { t } = useTranslation()
+  const [isCollapse, { toggle: toggleCollapse }] = useBoolean(false)
+  return (
+    <div className='rounded-xl border border-color-indigo-100 bg-indigo-25'>
+      <div
+        className='flex items-center h-6 pl-2 py-6 space-x-1 cursor-pointer'
+        onClick={toggleCollapse}
+      >
+        {
+          isCollapse
+            ? <ChevronRight className='w-3 h-3 text-gray-300' />
+            : <ChevronDown className='w-3 h-3 text-gray-300' />
+        }
+        <div className='text-sm font-semibold text-indigo-800 uppercase'>{t('appLog.detail.variables')}</div>
+      </div>
+      {!isCollapse && (
+        <div className='px-6 pb-3'>
+          {varList.map(({ label, value }, index) => (
+            <div key={index} className='flex py-1 leading-[18px] text-[13px]'>
+              <div className='shrink-0 w-[128px] flex text-primary-600'>
+                <span className='shrink-0 opacity-60'>{'{{'}</span>
+                <span className='truncate'>{label}</span>
+                <span className='shrink-0 opacity-60'>{'}}'}</span>
+              </div>
+              <div className='pl-2.5 break-all'>{value}</div>
+            </div>
+          ))}
+        </div>
+      )}
+
+    </div>
+  )
+}
+export default React.memo(VarPanel)

+ 0 - 1
web/app/components/app/overview/settings/index.tsx

@@ -165,7 +165,6 @@ const SettingsModal: FC<ISettingsModalProps> = ({
         </div>
         {showEmojiPicker && <EmojiPicker
           onSelect={(icon, icon_background) => {
-            console.log(icon, icon_background)
             setEmoji({ icon, icon_background })
             setShowEmojiPicker(false)
           }}

+ 42 - 5
web/app/components/app/text-generate/item/index.tsx

@@ -1,21 +1,24 @@
 'use client'
-import type { FC } from 'react'
-import React, { useEffect, useState } from 'react'
+import type { Dispatch, FC, SetStateAction } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import cn from 'classnames'
 import copy from 'copy-to-clipboard'
+import { useParams } from 'next/navigation'
 import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
 import { useBoolean } from 'ahooks'
 import { HashtagIcon } from '@heroicons/react/24/solid'
+import PromptLog from '@/app/components/app/chat/log'
 import { Markdown } from '@/app/components/base/markdown'
 import Loading from '@/app/components/base/loading'
 import Toast from '@/app/components/base/toast'
 import type { Feedbacktype } from '@/app/components/app/chat/type'
 import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
-import { Clipboard } from '@/app/components/base/icons/src/vender/line/files'
+import { Clipboard, File02 } from '@/app/components/base/icons/src/vender/line/files'
 import { Bookmark } from '@/app/components/base/icons/src/vender/line/general'
 import { Stars02 } from '@/app/components/base/icons/src/vender/line/weather'
 import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
+import { fetchTextGenerationMessge } from '@/service/debug'
 const MAX_DEPTH = 3
 export type IGenerationItemProps = {
   className?: string
@@ -23,7 +26,9 @@ export type IGenerationItemProps = {
   onRetry: () => void
   content: string
   messageId?: string | null
+  conversationId?: string
   isLoading?: boolean
+  isResponsing?: boolean
   isInWebApp?: boolean
   moreLikeThis?: boolean
   depth?: number
@@ -64,6 +69,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
   content,
   messageId,
   isLoading,
+  isResponsing,
   moreLikeThis,
   isInWebApp = false,
   feedback,
@@ -77,14 +83,16 @@ const GenerationItem: FC<IGenerationItemProps> = ({
   controlClearMoreLikeThis,
 }) => {
   const { t } = useTranslation()
+  const params = useParams()
   const isTop = depth === 1
-
+  const ref = useRef(null)
   const [completionRes, setCompletionRes] = useState('')
   const [childMessageId, setChildMessageId] = useState<string | null>(null)
   const hasChild = !!childMessageId
   const [childFeedback, setChildFeedback] = useState<Feedbacktype>({
     rating: null,
   })
+  const [promptLog, setPromptLog] = useState<{ role: string; text: string }[]>([])
 
   const handleFeedback = async (childFeedback: Feedbacktype) => {
     await updateFeedback({ url: `/messages/${childMessageId}/feedbacks`, body: { rating: childFeedback.rating } }, isInstalledApp, installedAppId)
@@ -150,8 +158,17 @@ const GenerationItem: FC<IGenerationItemProps> = ({
       setChildMessageId(null)
   }, [isLoading])
 
+  const handleOpenLogModal = async (setModal: Dispatch<SetStateAction<boolean>>) => {
+    const data = await fetchTextGenerationMessge({
+      appId: params.appId,
+      messageId: messageId!,
+    })
+    setPromptLog(data.message as any || [])
+    setModal(true)
+  }
+
   return (
-    <div className={cn(className, isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0')}
+    <div ref={ref} className={cn(className, isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-white' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0')}
       style={isTop
         ? {
           boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
@@ -186,6 +203,26 @@ const GenerationItem: FC<IGenerationItemProps> = ({
 
             <div className='flex items-center justify-between mt-3'>
               <div className='flex items-center'>
+                {
+                  !isInWebApp && !isInstalledApp && !isResponsing && (
+                    <PromptLog
+                      log={promptLog}
+                      containerRef={ref}
+                    >
+                      {
+                        showModal => (
+                          <SimpleBtn
+                            isDisabled={isError || !messageId}
+                            className={cn(isMobile && '!px-1.5', 'space-x-1 mr-2')}
+                            onClick={() => handleOpenLogModal(showModal)}>
+                            <File02 className='w-3.5 h-3.5' />
+                            {!isMobile && <div>{t('common.operation.log')}</div>}
+                          </SimpleBtn>
+                        )
+                      }
+                    </PromptLog>
+                  )
+                }
                 <SimpleBtn
                   isDisabled={isError || !messageId}
                   className={cn(isMobile && '!px-1.5', 'space-x-1')}

+ 12 - 46
web/app/components/base/block-input/index.tsx

@@ -4,9 +4,8 @@ import type { ChangeEvent, FC } from 'react'
 import React, { useCallback, useEffect, useRef, useState } from 'react'
 import classNames from 'classnames'
 import { useTranslation } from 'react-i18next'
-import Toast from '../toast'
 import { varHighlightHTML } from '../../app/configuration/base/var-highlight'
-import Button from '@/app/components/base/button'
+import Toast from '../toast'
 import { checkKeys } from '@/utils/var'
 
 // regex to match the {{}} and replace it with a span
@@ -65,7 +64,7 @@ const BlockInput: FC<IBlockInputProps> = ({
   }, [isEditing])
 
   const style = classNames({
-    'block px-4 py-1 w-full h-full text-sm text-gray-900 outline-0 border-0 break-all': true,
+    'block px-4 py-2 w-full h-full text-sm text-gray-900 outline-0 border-0 break-all': true,
     'block-input--editing': isEditing,
   })
 
@@ -76,9 +75,8 @@ const BlockInput: FC<IBlockInputProps> = ({
     .replace(/\n/g, '<br />')
 
   // Not use useCallback. That will cause out callback get old data.
-  const handleSubmit = () => {
+  const handleSubmit = (value: string) => {
     if (onConfirm) {
-      const value = currentValue
       const keys = getInputKeys(value)
       const { isValid, errorKey, errorMessageKey } = checkKeys(keys)
       if (!isValid) {
@@ -89,17 +87,13 @@ const BlockInput: FC<IBlockInputProps> = ({
         return
       }
       onConfirm(value, keys)
-      setIsEditing(false)
     }
   }
 
-  const handleCancel = useCallback(() => {
-    setIsEditing(false)
-    setCurrentValue(value)
-  }, [value])
-
   const onValueChange = useCallback((e: ChangeEvent<HTMLTextAreaElement>) => {
-    setCurrentValue(e.target.value)
+    const value = e.target.value
+    setCurrentValue(value)
+    handleSubmit(value)
   }, [])
 
   // Prevent rerendering caused cursor to jump to the start of the contentEditable element
@@ -117,18 +111,16 @@ const BlockInput: FC<IBlockInputProps> = ({
   const textAreaContent = (
     <div className={classNames(readonly ? 'max-h-[180px] pb-5' : 'h-[180px]', ' overflow-y-auto')} onClick={() => !readonly && setIsEditing(true)}>
       {isEditing
-        ? <div className='h-full px-4 py-1'>
+        ? <div className='h-full px-4 py-2'>
           <textarea
             ref={contentEditableRef}
-            className={classNames(editAreaClassName, 'block w-full h-full absolut3e resize-none')}
+            className={classNames(editAreaClassName, 'block w-full h-full resize-none')}
             placeholder={placeholder}
             onChange={onValueChange}
             value={currentValue}
             onBlur={() => {
               blur()
-              if (!isContentChanged)
-                setIsEditing(false)
-
+              setIsEditing(false)
               // click confirm also make blur. Then outter value is change. So below code has problem.
               // setTimeout(() => {
               //   handleCancel()
@@ -140,38 +132,12 @@ const BlockInput: FC<IBlockInputProps> = ({
     </div>)
 
   return (
-    <div className={classNames('block-input w-full overflow-y-auto border-none rounded-lg')}>
+    <div className={classNames('block-input w-full overflow-y-auto bg-white border-none rounded-xl')}>
       {textAreaContent}
       {/* footer */}
       {!readonly && (
-        <div className='flex item-center h-14 px-4'>
-          {isContentChanged
-            ? (
-              <div className='flex items-center justify-between w-full'>
-                <div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue?.length}</div>
-                <div className='flex space-x-2'>
-                  <Button
-                    onClick={handleCancel}
-                    className='w-20 !h-8 !text-[13px]'
-                  >
-                    {t('common.operation.cancel')}
-                  </Button>
-                  <Button
-                    onClick={handleSubmit}
-                    type="primary"
-                    className='w-20 !h-8 !text-[13px]'
-                  >
-                    {t('common.operation.confirm')}
-                  </Button>
-                </div>
-
-              </div>
-            )
-            : (
-              <p className="leading-5 text-xs text-gray-500">
-                {t('appDebug.promptTip')}
-              </p>
-            )}
+        <div className='pl-4 pb-2 flex'>
+          <div className="h-[18px] leading-[18px] px-1 rounded-md bg-gray-100 text-xs text-gray-500">{currentValue?.length}</div>
         </div>
       )}
 

+ 39 - 0
web/app/components/base/copy-feedback/index.tsx

@@ -4,6 +4,7 @@ import { useTranslation } from 'react-i18next'
 import { debounce } from 'lodash-es'
 import copy from 'copy-to-clipboard'
 import Tooltip from '../tooltip'
+import TooltipPlus from '../tooltip-plus'
 import copyStyle from './style.module.css'
 
 type Props = {
@@ -54,3 +55,41 @@ const CopyFeedback = ({ content, selectorId, className }: Props) => {
 }
 
 export default CopyFeedback
+
+export const CopyFeedbackNew = ({ content, className }: Pick<Props, 'className' | 'content'>) => {
+  const { t } = useTranslation()
+  const [isCopied, setIsCopied] = useState<boolean>(false)
+
+  const onClickCopy = debounce(() => {
+    copy(content)
+    setIsCopied(true)
+  }, 100)
+
+  const onMouseLeave = debounce(() => {
+    setIsCopied(false)
+  }, 100)
+
+  return (
+    <TooltipPlus
+      popupContent={
+        (isCopied
+          ? t(`${prefixEmbedded}.copied`)
+          : t(`${prefixEmbedded}.copy`)) || ''
+      }
+    >
+      <div
+        className={`w-8 h-8 cursor-pointer hover:bg-gray-100 rounded-lg ${
+          className ?? ''
+        }`}
+        onMouseLeave={onMouseLeave}
+      >
+        <div
+          onClick={onClickCopy}
+          className={`w-full h-full ${copyStyle.copyIcon} ${
+            isCopied ? copyStyle.copied : ''
+          }`}
+        ></div>
+      </div>
+    </TooltipPlus>
+  )
+}

+ 3 - 0
web/app/components/base/icons/assets/vender/line/arrows/chevron-selector-vertical.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M7 15L12 20L17 15M7 9L12 4L17 9" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 3 - 0
web/app/components/base/icons/assets/vender/line/arrows/flip-backward.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 9H16.5C18.9853 9 21 11.0147 21 13.5C21 15.9853 18.9853 18 16.5 18H12M3 9L7 5M3 9L7 13" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 9 - 0
web/app/components/base/icons/assets/vender/line/development/variable.svg

@@ -0,0 +1,9 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="variable">
+<g id="Solid">
+<path d="M13.8686 1.70487C13.7055 1.37481 13.3056 1.23952 12.9756 1.40268C12.6455 1.56585 12.5102 1.9657 12.6734 2.29576C13.5225 4.01329 14.0003 5.94969 14.0003 8.00031C14.0003 10.0509 13.5225 11.9873 12.6734 13.7049C12.5102 14.0349 12.6455 14.4348 12.9756 14.5979C13.3056 14.7611 13.7055 14.6258 13.8686 14.2958C14.8066 12.3984 15.3336 10.2602 15.3336 8.00031C15.3336 5.74041 14.8066 3.60221 13.8686 1.70487Z" fill="#2970FF"/>
+<path d="M3.32724 2.29576C3.49041 1.9657 3.35511 1.56585 3.02506 1.40268C2.695 1.23952 2.29515 1.37481 2.13198 1.70487C1.19401 3.60221 0.666992 5.74041 0.666992 8.00031C0.666992 10.2602 1.19401 12.3984 2.13198 14.2958C2.29515 14.6258 2.695 14.7611 3.02506 14.5979C3.35511 14.4348 3.49041 14.0349 3.32724 13.7049C2.47815 11.9873 2.00033 10.0509 2.00033 8.00031C2.00033 5.94969 2.47815 4.01329 3.32724 2.29576Z" fill="#2970FF"/>
+<path d="M9.33274 5.84142C9.74245 5.36093 10.3415 5.0835 10.973 5.0835H11.0328C11.4009 5.0835 11.6994 5.38197 11.6994 5.75016C11.6994 6.11835 11.4009 6.41683 11.0328 6.41683H10.973C10.7333 6.41683 10.5046 6.52209 10.3473 6.70653L8.78729 8.53612L9.28122 10.2739C9.29182 10.3112 9.32425 10.3335 9.35733 10.3335H10.2867C10.6549 10.3335 10.9534 10.632 10.9534 11.0002C10.9534 11.3684 10.6549 11.6668 10.2867 11.6668H9.35733C8.72419 11.6668 8.17111 11.2451 7.99868 10.6385L7.74768 9.75536L6.7641 10.9089C6.35439 11.3894 5.75537 11.6668 5.12387 11.6668H5.06409C4.6959 11.6668 4.39742 11.3684 4.39742 11.0002C4.39742 10.632 4.6959 10.3335 5.06409 10.3335H5.12387C5.36357 10.3335 5.59225 10.2282 5.74952 10.0438L7.30963 8.21412L6.81573 6.47639C6.80513 6.43909 6.7727 6.41683 6.73962 6.41683H5.81022C5.44203 6.41683 5.14355 6.11835 5.14355 5.75016C5.14355 5.38197 5.44203 5.0835 5.81022 5.0835H6.73962C7.37276 5.0835 7.92584 5.5052 8.09826 6.11186L8.34924 6.99487L9.33274 5.84142Z" fill="#2970FF"/>
+</g>
+</g>
+</svg>

+ 3 - 0
web/app/components/base/icons/assets/vender/line/files/clipboard-check.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M16 4C16.93 4 17.395 4 17.7765 4.10222C18.8117 4.37962 19.6204 5.18827 19.8978 6.22354C20 6.60504 20 7.07003 20 8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V8C4 7.07003 4 6.60504 4.10222 6.22354C4.37962 5.18827 5.18827 4.37962 6.22354 4.10222C6.60504 4 7.07003 4 8 4M9 15L11 17L15.5 12.5M9.6 6H14.4C14.9601 6 15.2401 6 15.454 5.89101C15.6422 5.79513 15.7951 5.64215 15.891 5.45399C16 5.24008 16 4.96005 16 4.4V3.6C16 3.03995 16 2.75992 15.891 2.54601C15.7951 2.35785 15.6422 2.20487 15.454 2.10899C15.2401 2 14.9601 2 14.4 2H9.6C9.03995 2 8.75992 2 8.54601 2.10899C8.35785 2.20487 8.20487 2.35785 8.10899 2.54601C8 2.75992 8 3.03995 8 3.6V4.4C8 4.96005 8 5.24008 8.10899 5.45399C8.20487 5.64215 8.35785 5.79513 8.54601 5.89101C8.75992 6 9.03995 6 9.6 6Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/line/files/file-02.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon">
+<path id="Icon_2" d="M9.33366 7.3335H5.33366M6.66699 10.0002H5.33366M10.667 4.66683H5.33366M13.3337 4.5335V11.4668C13.3337 12.5869 13.3337 13.147 13.1157 13.5748C12.9239 13.9511 12.618 14.2571 12.2416 14.4488C11.8138 14.6668 11.2538 14.6668 10.1337 14.6668H5.86699C4.74689 14.6668 4.18683 14.6668 3.75901 14.4488C3.38269 14.2571 3.07673 13.9511 2.88498 13.5748C2.66699 13.147 2.66699 12.5869 2.66699 11.4668V4.5335C2.66699 3.41339 2.66699 2.85334 2.88498 2.42552C3.07673 2.04919 3.38269 1.74323 3.75901 1.55148C4.18683 1.3335 4.74689 1.3335 5.86699 1.3335H10.1337C11.2538 1.3335 11.8138 1.3335 12.2416 1.55148C12.618 1.74323 12.9239 2.04919 13.1157 2.42552C13.3337 2.85334 13.3337 3.41339 13.3337 4.5335Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/line/general/settings-04.svg

@@ -0,0 +1,5 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Left Icon">
+<path id="Icon" d="M1.75 4.6665L8.75 4.6665M8.75 4.6665C8.75 5.633 9.5335 6.4165 10.5 6.4165C11.4665 6.4165 12.25 5.633 12.25 4.6665C12.25 3.70001 11.4665 2.9165 10.5 2.9165C9.5335 2.9165 8.75 3.70001 8.75 4.6665ZM5.25 9.33317L12.25 9.33317M5.25 9.33317C5.25 10.2997 4.4665 11.0832 3.5 11.0832C2.5335 11.0832 1.75 10.2997 1.75 9.33317C1.75 8.36667 2.5335 7.58317 3.5 7.58317C4.4665 7.58317 5.25 8.36667 5.25 9.33317Z" stroke="#344054" stroke-width="1.25" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 3 - 0
web/app/components/base/icons/assets/vender/line/mediaAndDevices/sliders-h.svg

@@ -0,0 +1,3 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M3 5H9M9 5C9 6.10457 9.89543 7 11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5ZM17 5L21 5M3 12H9M17 12H21M17 12C17 10.8954 16.1046 10 15 10C13.8954 10 13 10.8954 13 12C13 13.1046 13.8954 14 15 14C16.1046 14 17 13.1046 17 12ZM3 19H7M7 19C7 20.1046 7.89543 21 9 21C10.1046 21 11 20.1046 11 19C11 17.8954 10.1046 17 9 17C7.89543 17 7 17.8954 7 19ZM15 19H21" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

+ 8 - 0
web/app/components/base/icons/assets/vender/solid/files/file-05.svg

@@ -0,0 +1,8 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="file-05">
+<g id="Solid">
+<path fill-rule="evenodd" clip-rule="evenodd" d="M8.66667 1.34356C8.66667 1.32602 8.66667 1.31725 8.66591 1.30135C8.65018 0.972168 8.3607 0.682824 8.03151 0.667251C8.01562 0.666499 8.0104 0.666501 8.00001 0.666504H5.8391C5.30248 0.666497 4.85957 0.666491 4.49878 0.695968C4.12405 0.726585 3.77958 0.792295 3.45603 0.957155C2.95426 1.21282 2.54631 1.62077 2.29065 2.12253C2.12579 2.44609 2.06008 2.79056 2.02946 3.16529C1.99999 3.52608 1.99999 3.96899 2 4.50562V11.494C1.99999 12.0307 1.99999 12.4736 2.02946 12.8344C2.06008 13.2091 2.12579 13.5536 2.29065 13.8771C2.54631 14.3789 2.95426 14.7869 3.45603 15.0425C3.77958 15.2074 4.12405 15.2731 4.49878 15.3037C4.85958 15.3332 5.30248 15.3332 5.83912 15.3332H10.1609C10.6975 15.3332 11.1404 15.3332 11.5012 15.3037C11.8759 15.2731 12.2204 15.2074 12.544 15.0425C13.0457 14.7869 13.4537 14.3789 13.7093 13.8771C13.8742 13.5536 13.9399 13.2091 13.9705 12.8344C14 12.4736 14 12.0307 14 11.4941V6.66646C14 6.65611 14 6.65093 13.9993 6.63505C13.9837 6.30583 13.6943 6.01631 13.3651 6.0006C13.3492 5.99985 13.3405 5.99985 13.323 5.99985L10.3787 5.99985C10.2105 5.99987 10.0466 5.99989 9.90785 5.98855C9.75545 5.9761 9.57563 5.94672 9.39468 5.85452C9.1438 5.72669 8.93983 5.52272 8.81199 5.27183C8.7198 5.09088 8.69042 4.91106 8.67797 4.75867C8.66663 4.61989 8.66665 4.45603 8.66667 4.28778L8.66667 1.34356ZM5.33333 8.6665C4.96514 8.6665 4.66667 8.96498 4.66667 9.33317C4.66667 9.70136 4.96514 9.99984 5.33333 9.99984H10.6667C11.0349 9.99984 11.3333 9.70136 11.3333 9.33317C11.3333 8.96498 11.0349 8.6665 10.6667 8.6665H5.33333ZM5.33333 11.3332C4.96514 11.3332 4.66667 11.6316 4.66667 11.9998C4.66667 12.368 4.96514 12.6665 5.33333 12.6665H9.33333C9.70152 12.6665 10 12.368 10 11.9998C10 11.6316 9.70152 11.3332 9.33333 11.3332H5.33333Z" fill="#6938EF"/>
+<path d="M12.6053 4.6665C12.8011 4.6665 12.8989 4.6665 12.9791 4.61735C13.0923 4.54794 13.16 4.3844 13.129 4.25526C13.107 4.16382 13.0432 4.10006 12.9155 3.97253L10.694 1.75098C10.5664 1.62333 10.5027 1.5595 10.4112 1.53752C10.2821 1.50648 10.1186 1.57417 10.0492 1.6874C10 1.76757 10 1.86545 10 2.0612L10 4.13315C10 4.31982 10 4.41316 10.0363 4.48446C10.0683 4.54718 10.1193 4.59818 10.182 4.63014C10.2533 4.66647 10.3466 4.66647 10.5333 4.66647L12.6053 4.6665Z" fill="#6938EF"/>
+</g>
+</g>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/solid/files/folder.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon">
+<path id="Solid" fill-rule="evenodd" clip-rule="evenodd" d="M0.666993 4.10794C0.666981 3.75652 0.666972 3.45333 0.687374 3.20362C0.708908 2.94006 0.756452 2.67791 0.884981 2.42566C1.07673 2.04933 1.38269 1.74337 1.75901 1.55163C2.01127 1.4231 2.27341 1.37555 2.53698 1.35402C2.78669 1.33362 3.08986 1.33363 3.4413 1.33364L6.0981 1.33357C6.4938 1.33304 6.84179 1.33258 7.16176 1.44295C7.44201 1.53961 7.69726 1.69737 7.90905 1.9048C8.15086 2.14164 8.30607 2.45309 8.48257 2.80725L9.07895 4.00016H11.4945C12.0312 4.00015 12.4741 4.00015 12.8349 4.02963C13.2096 4.06024 13.5541 4.12595 13.8776 4.29081C14.3794 4.54648 14.7873 4.95442 15.043 5.45619C15.2079 5.77975 15.2736 6.12421 15.3042 6.49895C15.3337 6.85974 15.3337 7.30264 15.3337 7.83928V10.8277C15.3337 11.3644 15.3337 11.8073 15.3042 12.168C15.2736 12.5428 15.2079 12.8872 15.043 13.2108C14.7873 13.7126 14.3794 14.1205 13.8776 14.3762C13.5541 14.541 13.2096 14.6068 12.8349 14.6374C12.4741 14.6668 12.0312 14.6668 11.4945 14.6668H4.50614C3.9695 14.6668 3.52657 14.6668 3.16578 14.6374C2.79104 14.6068 2.44658 14.541 2.12302 14.3762C1.62125 14.1205 1.2133 13.7126 0.957643 13.2108C0.792782 12.8872 0.727073 12.5428 0.696456 12.168C0.666978 11.8073 0.666985 11.3643 0.666993 10.8277V4.10794ZM6.01519 2.66697C6.54213 2.66697 6.64658 2.67567 6.727 2.70341C6.82041 2.73563 6.9055 2.78822 6.97609 2.85736C7.03687 2.91688 7.09136 3.00642 7.32701 3.47773L7.58823 4.00016L2.00038 4.00016C2.00067 3.69017 2.00271 3.47827 2.01628 3.3122C2.03108 3.13109 2.05619 3.06394 2.07299 3.03098C2.13691 2.90554 2.23889 2.80355 2.36433 2.73964C2.3973 2.72284 2.46444 2.69772 2.64555 2.68292C2.83444 2.66749 3.08263 2.66697 3.46699 2.66697H6.01519Z" fill="#444CE7"/>
+</g>
+</svg>

+ 5 - 0
web/app/components/base/icons/assets/vender/solid/general/message-clock-circle.svg

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="message-clock-circle">
+<path id="Solid" d="M1.33301 8.00016C1.33301 4.31826 4.31778 1.3335 7.99967 1.3335C11.6816 1.3335 14.6663 4.31826 14.6663 8.00016C14.6663 11.6821 11.6816 14.6668 7.99967 14.6668C7.11413 14.6668 6.26734 14.4938 5.49248 14.1791C5.42249 14.1507 5.38209 14.1344 5.35225 14.1231L5.34304 14.1197L5.33987 14.1202C5.31527 14.1235 5.28173 14.129 5.21771 14.1397L2.82667 14.5382C2.71958 14.5561 2.59976 14.5761 2.4957 14.5839C2.38225 14.5925 2.20175 14.5955 2.01101 14.5137C1.77521 14.4125 1.5873 14.2246 1.48616 13.9888C1.40435 13.7981 1.40733 13.6176 1.41589 13.5041C1.42375 13.4001 1.44375 13.2803 1.46163 13.1732L1.86015 10.7821C1.87082 10.7181 1.87634 10.6846 1.87967 10.66L1.8801 10.6568L1.87669 10.6476C1.86549 10.6178 1.84914 10.5773 1.82071 10.5074C1.50602 9.7325 1.33301 8.88571 1.33301 8.00016ZM7.99967 5.3335C7.99967 4.96531 7.7012 4.66683 7.33301 4.66683C6.96482 4.66683 6.66634 4.96531 6.66634 5.3335V8.66683C6.66634 9.03502 6.96482 9.3335 7.33301 9.3335H10.6663C11.0345 9.3335 11.333 9.03502 11.333 8.66683C11.333 8.29864 11.0345 8.00016 10.6663 8.00016H7.99967V5.3335Z" fill="#DD2590"/>
+</g>
+</svg>

+ 14 - 0
web/app/components/base/icons/assets/vender/solid/users/user-edit-02.svg

@@ -0,0 +1,14 @@
+<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="user-edit 2" clip-path="url(#clip0_10419_49994)">
+<g id="Group">
+<path id="Vector" d="M5.83333 6.41667C7.60525 6.41667 9.04167 4.98025 9.04167 3.20833C9.04167 1.43642 7.60525 0 5.83333 0C4.06142 0 2.625 1.43642 2.625 3.20833C2.625 4.98025 4.06142 6.41667 5.83333 6.41667Z" fill="#FD853A"/>
+<path id="Vector_2" d="M5.90917 13.2465L6.78417 10.6221C6.85533 10.4086 6.97725 10.2114 7.1365 10.0522L8.79083 8.39783C7.92225 7.88391 6.91308 7.5835 5.83333 7.5835C2.61683 7.5835 0 10.2003 0 13.4168C0 13.7394 0.261333 14.0002 0.583333 14.0002H5.86717C5.817 13.7546 5.82575 13.4962 5.90917 13.2465Z" fill="#FD853A"/>
+<path id="Vector_3" d="M13.5524 7.44766C12.9562 6.85208 11.9856 6.85208 11.39 7.44766L7.96057 10.8771C7.92849 10.9092 7.90457 10.9482 7.88999 10.9908L7.01499 13.6158C6.97999 13.7208 7.0074 13.8363 7.08557 13.9145C7.14099 13.9705 7.21565 13.9997 7.29207 13.9997C7.32299 13.9997 7.3539 13.9944 7.38424 13.9851L10.0092 13.1101C10.0524 13.0961 10.0915 13.0716 10.123 13.0395L13.5524 9.61008C14.148 9.0145 14.148 8.04383 13.5524 7.44766Z" fill="#FD853A"/>
+</g>
+</g>
+<defs>
+<clipPath id="clip0_10419_49994">
+<rect width="14" height="14" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 29 - 0
web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.json

@@ -0,0 +1,29 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "24",
+			"viewBox": "0 0 24 24",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M7 15L12 20L17 15M7 9L12 4L17 9",
+					"stroke": "currentColor",
+					"stroke-width": "2",
+					"stroke-linecap": "round",
+					"stroke-linejoin": "round"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "ChevronSelectorVertical"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/arrows/ChevronSelectorVertical.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ChevronSelectorVertical.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'ChevronSelectorVertical'
+
+export default Icon

+ 29 - 0
web/app/components/base/icons/src/vender/line/arrows/FlipBackward.json

@@ -0,0 +1,29 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "24",
+			"viewBox": "0 0 24 24",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M3 9H16.5C18.9853 9 21 11.0147 21 13.5C21 15.9853 18.9853 18 16.5 18H12M3 9L7 5M3 9L7 13",
+					"stroke": "currentColor",
+					"stroke-width": "2",
+					"stroke-linecap": "round",
+					"stroke-linejoin": "round"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "FlipBackward"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/arrows/FlipBackward.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './FlipBackward.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'FlipBackward'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/vender/line/arrows/index.ts

@@ -3,5 +3,7 @@ export { default as ArrowUpRight } from './ArrowUpRight'
 export { default as ChevronDownDouble } from './ChevronDownDouble'
 export { default as ChevronDown } from './ChevronDown'
 export { default as ChevronRight } from './ChevronRight'
+export { default as ChevronSelectorVertical } from './ChevronSelectorVertical'
+export { default as FlipBackward } from './FlipBackward'
 export { default as RefreshCcw01 } from './RefreshCcw01'
 export { default as RefreshCw05 } from './RefreshCw05'

+ 62 - 0
web/app/components/base/icons/src/vender/line/development/Variable.json

@@ -0,0 +1,62 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "variable"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Solid"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M13.8686 1.70487C13.7055 1.37481 13.3056 1.23952 12.9756 1.40268C12.6455 1.56585 12.5102 1.9657 12.6734 2.29576C13.5225 4.01329 14.0003 5.94969 14.0003 8.00031C14.0003 10.0509 13.5225 11.9873 12.6734 13.7049C12.5102 14.0349 12.6455 14.4348 12.9756 14.5979C13.3056 14.7611 13.7055 14.6258 13.8686 14.2958C14.8066 12.3984 15.3336 10.2602 15.3336 8.00031C15.3336 5.74041 14.8066 3.60221 13.8686 1.70487Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M3.32724 2.29576C3.49041 1.9657 3.35511 1.56585 3.02506 1.40268C2.695 1.23952 2.29515 1.37481 2.13198 1.70487C1.19401 3.60221 0.666992 5.74041 0.666992 8.00031C0.666992 10.2602 1.19401 12.3984 2.13198 14.2958C2.29515 14.6258 2.695 14.7611 3.02506 14.5979C3.35511 14.4348 3.49041 14.0349 3.32724 13.7049C2.47815 11.9873 2.00033 10.0509 2.00033 8.00031C2.00033 5.94969 2.47815 4.01329 3.32724 2.29576Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M9.33274 5.84142C9.74245 5.36093 10.3415 5.0835 10.973 5.0835H11.0328C11.4009 5.0835 11.6994 5.38197 11.6994 5.75016C11.6994 6.11835 11.4009 6.41683 11.0328 6.41683H10.973C10.7333 6.41683 10.5046 6.52209 10.3473 6.70653L8.78729 8.53612L9.28122 10.2739C9.29182 10.3112 9.32425 10.3335 9.35733 10.3335H10.2867C10.6549 10.3335 10.9534 10.632 10.9534 11.0002C10.9534 11.3684 10.6549 11.6668 10.2867 11.6668H9.35733C8.72419 11.6668 8.17111 11.2451 7.99868 10.6385L7.74768 9.75536L6.7641 10.9089C6.35439 11.3894 5.75537 11.6668 5.12387 11.6668H5.06409C4.6959 11.6668 4.39742 11.3684 4.39742 11.0002C4.39742 10.632 4.6959 10.3335 5.06409 10.3335H5.12387C5.36357 10.3335 5.59225 10.2282 5.74952 10.0438L7.30963 8.21412L6.81573 6.47639C6.80513 6.43909 6.7727 6.41683 6.73962 6.41683H5.81022C5.44203 6.41683 5.14355 6.11835 5.14355 5.75016C5.14355 5.38197 5.44203 5.0835 5.81022 5.0835H6.73962C7.37276 5.0835 7.92584 5.5052 8.09826 6.11186L8.34924 6.99487L9.33274 5.84142Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Variable"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/development/Variable.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Variable.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Variable'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/development/index.ts

@@ -3,3 +3,4 @@ export { default as Container } from './Container'
 export { default as Database01 } from './Database01'
 export { default as Database03 } from './Database03'
 export { default as PuzzlePiece01 } from './PuzzlePiece01'
+export { default as Variable } from './Variable'

+ 29 - 0
web/app/components/base/icons/src/vender/line/files/ClipboardCheck.json

@@ -0,0 +1,29 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "24",
+			"viewBox": "0 0 24 24",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M16 4C16.93 4 17.395 4 17.7765 4.10222C18.8117 4.37962 19.6204 5.18827 19.8978 6.22354C20 6.60504 20 7.07003 20 8V17.2C20 18.8802 20 19.7202 19.673 20.362C19.3854 20.9265 18.9265 21.3854 18.362 21.673C17.7202 22 16.8802 22 15.2 22H8.8C7.11984 22 6.27976 22 5.63803 21.673C5.07354 21.3854 4.6146 20.9265 4.32698 20.362C4 19.7202 4 18.8802 4 17.2V8C4 7.07003 4 6.60504 4.10222 6.22354C4.37962 5.18827 5.18827 4.37962 6.22354 4.10222C6.60504 4 7.07003 4 8 4M9 15L11 17L15.5 12.5M9.6 6H14.4C14.9601 6 15.2401 6 15.454 5.89101C15.6422 5.79513 15.7951 5.64215 15.891 5.45399C16 5.24008 16 4.96005 16 4.4V3.6C16 3.03995 16 2.75992 15.891 2.54601C15.7951 2.35785 15.6422 2.20487 15.454 2.10899C15.2401 2 14.9601 2 14.4 2H9.6C9.03995 2 8.75992 2 8.54601 2.10899C8.35785 2.20487 8.20487 2.35785 8.10899 2.54601C8 2.75992 8 3.03995 8 3.6V4.4C8 4.96005 8 5.24008 8.10899 5.45399C8.20487 5.64215 8.35785 5.79513 8.54601 5.89101C8.75992 6 9.03995 6 9.6 6Z",
+					"stroke": "currentColor",
+					"stroke-width": "2",
+					"stroke-linecap": "round",
+					"stroke-linejoin": "round"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "ClipboardCheck"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/files/ClipboardCheck.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ClipboardCheck.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'ClipboardCheck'
+
+export default Icon

+ 39 - 0
web/app/components/base/icons/src/vender/line/files/File02.json

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon_2",
+							"d": "M9.33366 7.3335H5.33366M6.66699 10.0002H5.33366M10.667 4.66683H5.33366M13.3337 4.5335V11.4668C13.3337 12.5869 13.3337 13.147 13.1157 13.5748C12.9239 13.9511 12.618 14.2571 12.2416 14.4488C11.8138 14.6668 11.2538 14.6668 10.1337 14.6668H5.86699C4.74689 14.6668 4.18683 14.6668 3.75901 14.4488C3.38269 14.2571 3.07673 13.9511 2.88498 13.5748C2.66699 13.147 2.66699 12.5869 2.66699 11.4668V4.5335C2.66699 3.41339 2.66699 2.85334 2.88498 2.42552C3.07673 2.04919 3.38269 1.74323 3.75901 1.55148C4.18683 1.3335 4.74689 1.3335 5.86699 1.3335H10.1337C11.2538 1.3335 11.8138 1.3335 12.2416 1.55148C12.618 1.74323 12.9239 2.04919 13.1157 2.42552C13.3337 2.85334 13.3337 3.41339 13.3337 4.5335Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "File02"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/files/File02.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './File02.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'File02'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/vender/line/files/index.ts

@@ -1,2 +1,4 @@
+export { default as ClipboardCheck } from './ClipboardCheck'
 export { default as Clipboard } from './Clipboard'
+export { default as File02 } from './File02'
 export { default as FilePlus02 } from './FilePlus02'

+ 39 - 0
web/app/components/base/icons/src/vender/line/general/Settings04.json

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "14",
+			"height": "14",
+			"viewBox": "0 0 14 14",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Left Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M1.75 4.6665L8.75 4.6665M8.75 4.6665C8.75 5.633 9.5335 6.4165 10.5 6.4165C11.4665 6.4165 12.25 5.633 12.25 4.6665C12.25 3.70001 11.4665 2.9165 10.5 2.9165C9.5335 2.9165 8.75 3.70001 8.75 4.6665ZM5.25 9.33317L12.25 9.33317M5.25 9.33317C5.25 10.2997 4.4665 11.0832 3.5 11.0832C2.5335 11.0832 1.75 10.2997 1.75 9.33317C1.75 8.36667 2.5335 7.58317 3.5 7.58317C4.4665 7.58317 5.25 8.36667 5.25 9.33317Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.25",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Settings04"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/general/Settings04.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Settings04.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Settings04'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/general/index.ts

@@ -13,6 +13,7 @@ export { default as LogOut01 } from './LogOut01'
 export { default as Pin02 } from './Pin02'
 export { default as Plus } from './Plus'
 export { default as SearchLg } from './SearchLg'
+export { default as Settings04 } from './Settings04'
 export { default as Target04 } from './Target04'
 export { default as Trash03 } from './Trash03'
 export { default as XClose } from './XClose'

+ 29 - 0
web/app/components/base/icons/src/vender/line/mediaAndDevices/SlidersH.json

@@ -0,0 +1,29 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "24",
+			"viewBox": "0 0 24 24",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "path",
+				"attributes": {
+					"d": "M3 5H9M9 5C9 6.10457 9.89543 7 11 7C12.1046 7 13 6.10457 13 5C13 3.89543 12.1046 3 11 3C9.89543 3 9 3.89543 9 5ZM17 5L21 5M3 12H9M17 12H21M17 12C17 10.8954 16.1046 10 15 10C13.8954 10 13 10.8954 13 12C13 13.1046 13.8954 14 15 14C16.1046 14 17 13.1046 17 12ZM3 19H7M7 19C7 20.1046 7.89543 21 9 21C10.1046 21 11 20.1046 11 19C11 17.8954 10.1046 17 9 17C7.89543 17 7 17.8954 7 19ZM15 19H21",
+					"stroke": "currentColor",
+					"stroke-width": "2",
+					"stroke-linecap": "round",
+					"stroke-linejoin": "round"
+				},
+				"children": []
+			}
+		]
+	},
+	"name": "SlidersH"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/mediaAndDevices/SlidersH.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './SlidersH.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'SlidersH'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/line/mediaAndDevices/index.ts

@@ -1 +1,2 @@
 export { default as Microphone01 } from './Microphone01'
+export { default as SlidersH } from './SlidersH'

+ 55 - 0
web/app/components/base/icons/src/vender/solid/files/File05.json

@@ -0,0 +1,55 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "file-05"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Solid"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"fill-rule": "evenodd",
+									"clip-rule": "evenodd",
+									"d": "M8.66667 1.34356C8.66667 1.32602 8.66667 1.31725 8.66591 1.30135C8.65018 0.972168 8.3607 0.682824 8.03151 0.667251C8.01562 0.666499 8.0104 0.666501 8.00001 0.666504H5.8391C5.30248 0.666497 4.85957 0.666491 4.49878 0.695968C4.12405 0.726585 3.77958 0.792295 3.45603 0.957155C2.95426 1.21282 2.54631 1.62077 2.29065 2.12253C2.12579 2.44609 2.06008 2.79056 2.02946 3.16529C1.99999 3.52608 1.99999 3.96899 2 4.50562V11.494C1.99999 12.0307 1.99999 12.4736 2.02946 12.8344C2.06008 13.2091 2.12579 13.5536 2.29065 13.8771C2.54631 14.3789 2.95426 14.7869 3.45603 15.0425C3.77958 15.2074 4.12405 15.2731 4.49878 15.3037C4.85958 15.3332 5.30248 15.3332 5.83912 15.3332H10.1609C10.6975 15.3332 11.1404 15.3332 11.5012 15.3037C11.8759 15.2731 12.2204 15.2074 12.544 15.0425C13.0457 14.7869 13.4537 14.3789 13.7093 13.8771C13.8742 13.5536 13.9399 13.2091 13.9705 12.8344C14 12.4736 14 12.0307 14 11.4941V6.66646C14 6.65611 14 6.65093 13.9993 6.63505C13.9837 6.30583 13.6943 6.01631 13.3651 6.0006C13.3492 5.99985 13.3405 5.99985 13.323 5.99985L10.3787 5.99985C10.2105 5.99987 10.0466 5.99989 9.90785 5.98855C9.75545 5.9761 9.57563 5.94672 9.39468 5.85452C9.1438 5.72669 8.93983 5.52272 8.81199 5.27183C8.7198 5.09088 8.69042 4.91106 8.67797 4.75867C8.66663 4.61989 8.66665 4.45603 8.66667 4.28778L8.66667 1.34356ZM5.33333 8.6665C4.96514 8.6665 4.66667 8.96498 4.66667 9.33317C4.66667 9.70136 4.96514 9.99984 5.33333 9.99984H10.6667C11.0349 9.99984 11.3333 9.70136 11.3333 9.33317C11.3333 8.96498 11.0349 8.6665 10.6667 8.6665H5.33333ZM5.33333 11.3332C4.96514 11.3332 4.66667 11.6316 4.66667 11.9998C4.66667 12.368 4.96514 12.6665 5.33333 12.6665H9.33333C9.70152 12.6665 10 12.368 10 11.9998C10 11.6316 9.70152 11.3332 9.33333 11.3332H5.33333Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M12.6053 4.6665C12.8011 4.6665 12.8989 4.6665 12.9791 4.61735C13.0923 4.54794 13.16 4.3844 13.129 4.25526C13.107 4.16382 13.0432 4.10006 12.9155 3.97253L10.694 1.75098C10.5664 1.62333 10.5027 1.5595 10.4112 1.53752C10.2821 1.50648 10.1186 1.57417 10.0492 1.6874C10 1.76757 10 1.86545 10 2.0612L10 4.13315C10 4.31982 10 4.41316 10.0363 4.48446C10.0683 4.54718 10.1193 4.59818 10.182 4.63014C10.2533 4.66647 10.3466 4.66647 10.5333 4.66647L12.6053 4.6665Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "File05"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/files/File05.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './File05.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'File05'
+
+export default Icon

+ 38 - 0
web/app/components/base/icons/src/vender/solid/files/Folder.json

@@ -0,0 +1,38 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Solid",
+							"fill-rule": "evenodd",
+							"clip-rule": "evenodd",
+							"d": "M0.666993 4.10794C0.666981 3.75652 0.666972 3.45333 0.687374 3.20362C0.708908 2.94006 0.756452 2.67791 0.884981 2.42566C1.07673 2.04933 1.38269 1.74337 1.75901 1.55163C2.01127 1.4231 2.27341 1.37555 2.53698 1.35402C2.78669 1.33362 3.08986 1.33363 3.4413 1.33364L6.0981 1.33357C6.4938 1.33304 6.84179 1.33258 7.16176 1.44295C7.44201 1.53961 7.69726 1.69737 7.90905 1.9048C8.15086 2.14164 8.30607 2.45309 8.48257 2.80725L9.07895 4.00016H11.4945C12.0312 4.00015 12.4741 4.00015 12.8349 4.02963C13.2096 4.06024 13.5541 4.12595 13.8776 4.29081C14.3794 4.54648 14.7873 4.95442 15.043 5.45619C15.2079 5.77975 15.2736 6.12421 15.3042 6.49895C15.3337 6.85974 15.3337 7.30264 15.3337 7.83928V10.8277C15.3337 11.3644 15.3337 11.8073 15.3042 12.168C15.2736 12.5428 15.2079 12.8872 15.043 13.2108C14.7873 13.7126 14.3794 14.1205 13.8776 14.3762C13.5541 14.541 13.2096 14.6068 12.8349 14.6374C12.4741 14.6668 12.0312 14.6668 11.4945 14.6668H4.50614C3.9695 14.6668 3.52657 14.6668 3.16578 14.6374C2.79104 14.6068 2.44658 14.541 2.12302 14.3762C1.62125 14.1205 1.2133 13.7126 0.957643 13.2108C0.792782 12.8872 0.727073 12.5428 0.696456 12.168C0.666978 11.8073 0.666985 11.3643 0.666993 10.8277V4.10794ZM6.01519 2.66697C6.54213 2.66697 6.64658 2.67567 6.727 2.70341C6.82041 2.73563 6.9055 2.78822 6.97609 2.85736C7.03687 2.91688 7.09136 3.00642 7.32701 3.47773L7.58823 4.00016L2.00038 4.00016C2.00067 3.69017 2.00271 3.47827 2.01628 3.3122C2.03108 3.13109 2.05619 3.06394 2.07299 3.03098C2.13691 2.90554 2.23889 2.80355 2.36433 2.73964C2.3973 2.72284 2.46444 2.69772 2.64555 2.68292C2.83444 2.66749 3.08263 2.66697 3.46699 2.66697H6.01519Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Folder"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/files/Folder.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Folder.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'Folder'
+
+export default Icon

+ 2 - 0
web/app/components/base/icons/src/vender/solid/files/index.ts

@@ -0,0 +1,2 @@
+export { default as File05 } from './File05'
+export { default as Folder } from './Folder'

+ 36 - 0
web/app/components/base/icons/src/vender/solid/general/MessageClockCircle.json

@@ -0,0 +1,36 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "message-clock-circle"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Solid",
+							"d": "M1.33301 8.00016C1.33301 4.31826 4.31778 1.3335 7.99967 1.3335C11.6816 1.3335 14.6663 4.31826 14.6663 8.00016C14.6663 11.6821 11.6816 14.6668 7.99967 14.6668C7.11413 14.6668 6.26734 14.4938 5.49248 14.1791C5.42249 14.1507 5.38209 14.1344 5.35225 14.1231L5.34304 14.1197L5.33987 14.1202C5.31527 14.1235 5.28173 14.129 5.21771 14.1397L2.82667 14.5382C2.71958 14.5561 2.59976 14.5761 2.4957 14.5839C2.38225 14.5925 2.20175 14.5955 2.01101 14.5137C1.77521 14.4125 1.5873 14.2246 1.48616 13.9888C1.40435 13.7981 1.40733 13.6176 1.41589 13.5041C1.42375 13.4001 1.44375 13.2803 1.46163 13.1732L1.86015 10.7821C1.87082 10.7181 1.87634 10.6846 1.87967 10.66L1.8801 10.6568L1.87669 10.6476C1.86549 10.6178 1.84914 10.5773 1.82071 10.5074C1.50602 9.7325 1.33301 8.88571 1.33301 8.00016ZM7.99967 5.3335C7.99967 4.96531 7.7012 4.66683 7.33301 4.66683C6.96482 4.66683 6.66634 4.96531 6.66634 5.3335V8.66683C6.66634 9.03502 6.96482 9.3335 7.33301 9.3335H10.6663C11.0345 9.3335 11.333 9.03502 11.333 8.66683C11.333 8.29864 11.0345 8.00016 10.6663 8.00016H7.99967V5.3335Z",
+							"fill": "currentColor"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "MessageClockCircle"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/general/MessageClockCircle.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './MessageClockCircle.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'MessageClockCircle'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/solid/general/index.ts

@@ -1,5 +1,6 @@
 export { default as CheckCircle } from './CheckCircle'
 export { default as CheckDone01 } from './CheckDone01'
 export { default as Download02 } from './Download02'
+export { default as MessageClockCircle } from './MessageClockCircle'
 export { default as Target04 } from './Target04'
 export { default as XCircle } from './XCircle'

+ 92 - 0
web/app/components/base/icons/src/vender/solid/users/UserEdit02.json

@@ -0,0 +1,92 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "14",
+			"height": "14",
+			"viewBox": "0 0 14 14",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "user-edit 2",
+					"clip-path": "url(#clip0_10419_49994)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Group"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"id": "Vector",
+									"d": "M5.83333 6.41667C7.60525 6.41667 9.04167 4.98025 9.04167 3.20833C9.04167 1.43642 7.60525 0 5.83333 0C4.06142 0 2.625 1.43642 2.625 3.20833C2.625 4.98025 4.06142 6.41667 5.83333 6.41667Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"id": "Vector_2",
+									"d": "M5.90917 13.2465L6.78417 10.6221C6.85533 10.4086 6.97725 10.2114 7.1365 10.0522L8.79083 8.39783C7.92225 7.88391 6.91308 7.5835 5.83333 7.5835C2.61683 7.5835 0 10.2003 0 13.4168C0 13.7394 0.261333 14.0002 0.583333 14.0002H5.86717C5.817 13.7546 5.82575 13.4962 5.90917 13.2465Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"id": "Vector_3",
+									"d": "M13.5524 7.44766C12.9562 6.85208 11.9856 6.85208 11.39 7.44766L7.96057 10.8771C7.92849 10.9092 7.90457 10.9482 7.88999 10.9908L7.01499 13.6158C6.97999 13.7208 7.0074 13.8363 7.08557 13.9145C7.14099 13.9705 7.21565 13.9997 7.29207 13.9997C7.32299 13.9997 7.3539 13.9944 7.38424 13.9851L10.0092 13.1101C10.0524 13.0961 10.0915 13.0716 10.123 13.0395L13.5524 9.61008C14.148 9.0145 14.148 8.04383 13.5524 7.44766Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_10419_49994"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "14",
+									"height": "14",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "UserEdit02"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/users/UserEdit02.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './UserEdit02.json'
+import IconBase from '@/app/components/base/icons/IconBase'
+import type { IconBaseProps, IconData } from '@/app/components/base/icons/IconBase'
+
+const Icon = React.forwardRef<React.MutableRefObject<SVGElement>, Omit<IconBaseProps, 'data'>>((
+  props,
+  ref,
+) => <IconBase {...props} ref={ref} data={data as IconData} />)
+
+Icon.displayName = 'UserEdit02'
+
+export default Icon

+ 1 - 0
web/app/components/base/icons/src/vender/solid/users/index.ts

@@ -1,2 +1,3 @@
 export { default as User01 } from './User01'
+export { default as UserEdit02 } from './UserEdit02'
 export { default as Users01 } from './Users01'

+ 24 - 0
web/app/components/base/prompt-editor/constants.tsx

@@ -0,0 +1,24 @@
+export const CONTEXT_PLACEHOLDER_TEXT = '{{#context#}}'
+export const HISTORY_PLACEHOLDER_TEXT = '{{#histories#}}'
+export const QUERY_PLACEHOLDER_TEXT = '{{#query#}}'
+export const PRE_PROMPT_PLACEHOLDER_TEXT = '{{#pre_prompt#}}'
+export const UPDATE_DATASETS_EVENT_EMITTER = 'prompt-editor-context-block-update-datasets'
+export const UPDATE_HISTORY_EVENT_EMITTER = 'prompt-editor-history-block-update-role'
+
+export const checkHasContextBlock = (text: string) => {
+  if (!text)
+    return false
+  return text.includes(CONTEXT_PLACEHOLDER_TEXT)
+}
+
+export const checkHasHistoryBlock = (text: string) => {
+  if (!text)
+    return false
+  return text.includes(HISTORY_PLACEHOLDER_TEXT)
+}
+
+export const checkHasQueryBlock = (text: string) => {
+  if (!text)
+    return false
+  return text.includes(QUERY_PLACEHOLDER_TEXT)
+}

+ 184 - 0
web/app/components/base/prompt-editor/hooks.ts

@@ -0,0 +1,184 @@
+import {
+  useCallback,
+  useEffect,
+  useRef,
+  useState,
+} from 'react'
+import type { Dispatch, RefObject, SetStateAction } from 'react'
+import type {
+  Klass,
+  LexicalCommand,
+  LexicalEditor,
+  TextNode,
+} from 'lexical'
+import {
+  $getNodeByKey,
+  $getSelection,
+  $isDecoratorNode,
+  $isNodeSelection,
+  COMMAND_PRIORITY_LOW,
+  KEY_BACKSPACE_COMMAND,
+  KEY_DELETE_COMMAND,
+} from 'lexical'
+import type { EntityMatch } from '@lexical/text'
+import {
+  mergeRegister,
+} from '@lexical/utils'
+import { useLexicalNodeSelection } from '@lexical/react/useLexicalNodeSelection'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { $isContextBlockNode } from './plugins/context-block/node'
+import { DELETE_CONTEXT_BLOCK_COMMAND } from './plugins/context-block'
+import { $isHistoryBlockNode } from './plugins/history-block/node'
+import { DELETE_HISTORY_BLOCK_COMMAND } from './plugins/history-block'
+import { $isQueryBlockNode } from './plugins/query-block/node'
+import { DELETE_QUERY_BLOCK_COMMAND } from './plugins/query-block'
+import type { CustomTextNode } from './plugins/custom-text/node'
+import { registerLexicalTextEntity } from './utils'
+
+export type UseSelectOrDeleteHanlder = (nodeKey: string, command?: LexicalCommand<undefined>) => [RefObject<HTMLDivElement>, boolean]
+export const useSelectOrDelete: UseSelectOrDeleteHanlder = (nodeKey: string, command?: LexicalCommand<undefined>) => {
+  const ref = useRef<HTMLDivElement>(null)
+  const [editor] = useLexicalComposerContext()
+  const [isSelected, setSelected, clearSelection] = useLexicalNodeSelection(nodeKey)
+
+  const handleDelete = useCallback(
+    (event: KeyboardEvent) => {
+      const selection = $getSelection()
+      const nodes = selection?.getNodes()
+      if (
+        !isSelected
+        && nodes?.length === 1
+        && (
+          ($isContextBlockNode(nodes[0]) && command === DELETE_CONTEXT_BLOCK_COMMAND)
+          || ($isHistoryBlockNode(nodes[0]) && command === DELETE_HISTORY_BLOCK_COMMAND)
+          || ($isQueryBlockNode(nodes[0]) && command === DELETE_QUERY_BLOCK_COMMAND)
+        )
+      )
+        editor.dispatchCommand(command, undefined)
+
+      if (isSelected && $isNodeSelection(selection)) {
+        event.preventDefault()
+        const node = $getNodeByKey(nodeKey)
+        if ($isDecoratorNode(node)) {
+          if (command)
+            editor.dispatchCommand(command, undefined)
+
+          node.remove()
+        }
+      }
+
+      return false
+    },
+    [isSelected, nodeKey, command, editor],
+  )
+
+  const handleSelect = useCallback((e: MouseEvent) => {
+    e.stopPropagation()
+    clearSelection()
+    setSelected(true)
+  }, [setSelected, clearSelection])
+
+  useEffect(() => {
+    const ele = ref.current
+
+    if (ele)
+      ele.addEventListener('click', handleSelect)
+
+    return () => {
+      if (ele)
+        ele.removeEventListener('click', handleSelect)
+    }
+  }, [handleSelect])
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerCommand(
+        KEY_DELETE_COMMAND,
+        handleDelete,
+        COMMAND_PRIORITY_LOW,
+      ),
+      editor.registerCommand(
+        KEY_BACKSPACE_COMMAND,
+        handleDelete,
+        COMMAND_PRIORITY_LOW,
+      ),
+    )
+  }, [editor, clearSelection, handleDelete])
+
+  return [ref, isSelected]
+}
+
+export type UseTriggerHandler = () => [RefObject<HTMLDivElement>, boolean, Dispatch<SetStateAction<boolean>>]
+export const useTrigger: UseTriggerHandler = () => {
+  const triggerRef = useRef<HTMLDivElement>(null)
+  const [open, setOpen] = useState(false)
+  const handleOpen = useCallback((e: MouseEvent) => {
+    e.stopPropagation()
+    setOpen(v => !v)
+  }, [])
+
+  useEffect(() => {
+    const trigger = triggerRef.current
+    if (trigger)
+      trigger.addEventListener('click', handleOpen)
+
+    return () => {
+      if (trigger)
+        trigger.removeEventListener('click', handleOpen)
+    }
+  }, [handleOpen])
+
+  return [triggerRef, open, setOpen]
+}
+
+export function useLexicalTextEntity<T extends TextNode>(
+  getMatch: (text: string) => null | EntityMatch,
+  targetNode: Klass<T>,
+  createNode: (textNode: CustomTextNode) => T,
+) {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    return mergeRegister(...registerLexicalTextEntity(editor, getMatch, targetNode, createNode))
+  }, [createNode, editor, getMatch, targetNode])
+}
+
+export type MenuTextMatch = {
+  leadOffset: number
+  matchingString: string
+  replaceableString: string
+}
+export type TriggerFn = (
+  text: string,
+  editor: LexicalEditor,
+) => MenuTextMatch | null
+export const PUNCTUATION = '\\.,\\+\\*\\?\\$\\@\\|#{}\\(\\)\\^\\-\\[\\]\\\\/!%\'"~=<>_:;'
+export function useBasicTypeaheadTriggerMatch(
+  trigger: string,
+  { minLength = 1, maxLength = 75 }: { minLength?: number; maxLength?: number },
+): TriggerFn {
+  return useCallback(
+    (text: string) => {
+      const validChars = `[^${trigger}${PUNCTUATION}\\s]`
+      const TypeaheadTriggerRegex = new RegExp(
+        `([^${trigger}]|^)(`
+          + `[${trigger}]`
+          + `((?:${validChars}){0,${maxLength}})`
+          + ')$',
+      )
+      const match = TypeaheadTriggerRegex.exec(text)
+      if (match !== null) {
+        const maybeLeadingWhitespace = match[1]
+        const matchingString = match[3]
+        if (matchingString.length >= minLength) {
+          return {
+            leadOffset: match.index + maybeLeadingWhitespace.length,
+            matchingString,
+            replaceableString: match[2],
+          }
+        }
+      }
+      return null
+    },
+    [maxLength, minLength, trigger],
+  )
+}

+ 216 - 0
web/app/components/base/prompt-editor/index.tsx

@@ -0,0 +1,216 @@
+'use client'
+
+import type { FC } from 'react'
+import { useEffect } from 'react'
+import type {
+  EditorState,
+} from 'lexical'
+import {
+  $getRoot,
+  TextNode,
+} from 'lexical'
+import { LexicalComposer } from '@lexical/react/LexicalComposer'
+import { RichTextPlugin } from '@lexical/react/LexicalRichTextPlugin'
+import { ContentEditable } from '@lexical/react/LexicalContentEditable'
+import LexicalErrorBoundary from '@lexical/react/LexicalErrorBoundary'
+import { OnChangePlugin } from '@lexical/react/LexicalOnChangePlugin'
+// import TreeView from './plugins/tree-view'
+import Placeholder from './plugins/placeholder'
+import ComponentPicker from './plugins/component-picker'
+import VariablePicker from './plugins/variable-picker'
+import ContextBlock from './plugins/context-block'
+import { ContextBlockNode } from './plugins/context-block/node'
+import ContextBlockReplacementBlock from './plugins/context-block-replacement-block'
+import HistoryBlock from './plugins/history-block'
+import { HistoryBlockNode } from './plugins/history-block/node'
+import HistoryBlockReplacementBlock from './plugins/history-block-replacement-block'
+import QueryBlock from './plugins/query-block'
+import { QueryBlockNode } from './plugins/query-block/node'
+import QueryBlockReplacementBlock from './plugins/query-block-replacement-block'
+import VariableBlock from './plugins/variable-block'
+import VariableValueBlock from './plugins/variable-value-block'
+import { VariableValueBlockNode } from './plugins/variable-value-block/node'
+import { CustomTextNode } from './plugins/custom-text/node'
+import OnBlurBlock from './plugins/on-blur-block'
+import { textToEditorState } from './utils'
+import type { Dataset } from './plugins/context-block'
+import type { RoleName } from './plugins/history-block'
+import type { Option } from './plugins/variable-picker'
+import {
+  UPDATE_DATASETS_EVENT_EMITTER,
+  UPDATE_HISTORY_EVENT_EMITTER,
+} from './constants'
+import { useEventEmitterContextContext } from '@/context/event-emitter'
+
+export type PromptEditorProps = {
+  className?: string
+  value?: string
+  editable?: boolean
+  onChange?: (text: string) => void
+  onBlur?: () => void
+  contextBlock?: {
+    selectable?: boolean
+    datasets: Dataset[]
+    onInsert?: () => void
+    onDelete?: () => void
+    onAddContext: () => void
+  }
+  variableBlock?: {
+    selectable?: boolean
+    variables: Option[]
+  }
+  historyBlock?: {
+    show?: boolean
+    selectable?: boolean
+    history: RoleName
+    onInsert?: () => void
+    onDelete?: () => void
+    onEditRole: () => void
+  }
+  queryBlock?: {
+    show?: boolean
+    selectable?: boolean
+    onInsert?: () => void
+    onDelete?: () => void
+  }
+}
+
+const PromptEditor: FC<PromptEditorProps> = ({
+  className,
+  value,
+  editable = true,
+  onChange,
+  onBlur,
+  contextBlock = {
+    selectable: true,
+    datasets: [],
+    onAddContext: () => {},
+    onInsert: () => {},
+    onDelete: () => {},
+  },
+  historyBlock = {
+    show: true,
+    selectable: true,
+    history: {
+      user: '',
+      assistant: '',
+    },
+    onEditRole: () => {},
+    onInsert: () => {},
+    onDelete: () => {},
+  },
+  variableBlock = {
+    variables: [],
+  },
+  queryBlock = {
+    show: true,
+    selectable: true,
+    onInsert: () => {},
+    onDelete: () => {},
+  },
+}) => {
+  const { eventEmitter } = useEventEmitterContextContext()
+  const initialConfig = {
+    namespace: 'prompt-editor',
+    nodes: [
+      CustomTextNode,
+      {
+        replace: TextNode,
+        with: (node: TextNode) => new CustomTextNode(node.__text),
+      },
+      ContextBlockNode,
+      HistoryBlockNode,
+      QueryBlockNode,
+      VariableValueBlockNode,
+    ],
+    editorState: value ? textToEditorState(value as string) : null,
+    onError: (error: Error) => {
+      throw error
+    },
+  }
+
+  const handleEditorChange = (editorState: EditorState) => {
+    const text = editorState.read(() => $getRoot().getTextContent())
+    if (onChange)
+      onChange(text.replaceAll('\n\n', '\n'))
+  }
+
+  useEffect(() => {
+    eventEmitter?.emit({
+      type: UPDATE_DATASETS_EVENT_EMITTER,
+      payload: contextBlock.datasets,
+    } as any)
+  }, [eventEmitter, contextBlock.datasets])
+  useEffect(() => {
+    eventEmitter?.emit({
+      type: UPDATE_HISTORY_EVENT_EMITTER,
+      payload: historyBlock.history,
+    } as any)
+  }, [eventEmitter, historyBlock.history])
+
+  return (
+    <LexicalComposer initialConfig={{ ...initialConfig, editable }}>
+      <div className='relative'>
+        <RichTextPlugin
+          contentEditable={<ContentEditable className={`${className} outline-none text-sm text-gray-700 leading-6`} />}
+          placeholder={<Placeholder />}
+          ErrorBoundary={LexicalErrorBoundary}
+        />
+        <ComponentPicker
+          contextDisabled={!contextBlock.selectable}
+          historyDisabled={!historyBlock.selectable}
+          historyShow={historyBlock.show}
+          queryDisabled={!queryBlock.selectable}
+          queryShow={queryBlock.show}
+        />
+        <VariablePicker items={variableBlock.variables} />
+        <ContextBlock
+          datasets={contextBlock.datasets}
+          onAddContext={contextBlock.onAddContext}
+          onInsert={contextBlock.onInsert}
+          onDelete={contextBlock.onDelete}
+        />
+        <ContextBlockReplacementBlock
+          datasets={contextBlock.datasets}
+          onAddContext={contextBlock.onAddContext}
+          onInsert={contextBlock.onInsert}
+        />
+        <VariableBlock />
+        {
+          historyBlock.show && (
+            <>
+              <HistoryBlock
+                roleName={historyBlock.history}
+                onEditRole={historyBlock.onEditRole}
+                onInsert={historyBlock.onInsert}
+                onDelete={historyBlock.onDelete}
+              />
+              <HistoryBlockReplacementBlock
+                roleName={historyBlock.history}
+                onEditRole={historyBlock.onEditRole}
+                onInsert={historyBlock.onInsert}
+              />
+            </>
+          )
+        }
+        {
+          queryBlock.show && (
+            <>
+              <QueryBlock
+                onInsert={queryBlock.onInsert}
+                onDelete={queryBlock.onDelete}
+              />
+              <QueryBlockReplacementBlock />
+            </>
+          )
+        }
+        <VariableValueBlock />
+        <OnChangePlugin onChange={handleEditorChange} />
+        <OnBlurBlock onBlur={onBlur} />
+        {/* <TreeView /> */}
+      </div>
+    </LexicalComposer>
+  )
+}
+
+export default PromptEditor

+ 218 - 0
web/app/components/base/prompt-editor/plugins/component-picker.tsx

@@ -0,0 +1,218 @@
+import type { FC } from 'react'
+import { useCallback } from 'react'
+import ReactDOM from 'react-dom'
+import { useTranslation } from 'react-i18next'
+import type { TextNode } from 'lexical'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import {
+  LexicalTypeaheadMenuPlugin,
+  MenuOption,
+} from '@lexical/react/LexicalTypeaheadMenuPlugin'
+import { useBasicTypeaheadTriggerMatch } from '../hooks'
+import { INSERT_CONTEXT_BLOCK_COMMAND } from './context-block'
+import { INSERT_VARIABLE_BLOCK_COMMAND } from './variable-block'
+import { INSERT_HISTORY_BLOCK_COMMAND } from './history-block'
+import { INSERT_QUERY_BLOCK_COMMAND } from './query-block'
+import { File05 } from '@/app/components/base/icons/src/vender/solid/files'
+import { Variable } from '@/app/components/base/icons/src/vender/line/development'
+import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
+import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
+
+class ComponentPickerOption extends MenuOption {
+  title: string
+  icon?: JSX.Element
+  keywords: Array<string>
+  keyboardShortcut?: string
+  desc: string
+  onSelect: (queryString: string) => void
+  disabled?: boolean
+
+  constructor(
+    title: string,
+    options: {
+      icon?: JSX.Element
+      keywords?: Array<string>
+      keyboardShortcut?: string
+      desc: string
+      onSelect: (queryString: string) => void
+      disabled?: boolean
+    },
+  ) {
+    super(title)
+    this.title = title
+    this.keywords = options.keywords || []
+    this.icon = options.icon
+    this.keyboardShortcut = options.keyboardShortcut
+    this.desc = options.desc
+    this.onSelect = options.onSelect.bind(this)
+    this.disabled = options.disabled
+  }
+}
+
+type ComponentPickerMenuItemProps = {
+  isSelected: boolean
+  onClick: () => void
+  onMouseEnter: () => void
+  option: ComponentPickerOption
+}
+const ComponentPickerMenuItem: FC<ComponentPickerMenuItemProps> = ({
+  isSelected,
+  onClick,
+  onMouseEnter,
+  option,
+}) => {
+  const { t } = useTranslation()
+
+  return (
+    <div
+      key={option.key}
+      className={`
+        flex items-center px-3 py-1.5 rounded-lg 
+        ${isSelected && !option.disabled && '!bg-gray-50'}
+        ${option.disabled ? 'cursor-not-allowed opacity-30' : 'hover:bg-gray-50 cursor-pointer'}
+      `}
+      tabIndex={-1}
+      ref={option.setRefElement}
+      onMouseEnter={onMouseEnter}
+      onClick={onClick}>
+      <div className='flex items-center justify-center mr-2 w-8 h-8 rounded-lg border border-gray-100'>
+        {option.icon}
+      </div>
+      <div className='grow'>
+        <div className='flex items-center justify-between h-5 text-sm text-gray-900'>
+          {option.title}
+          <span className='text-xs text-gray-400'>{option.disabled && t('common.promptEditor.existed')}</span>
+        </div>
+        <div className='text-xs text-gray-500'>{option.desc}</div>
+      </div>
+    </div>
+  )
+}
+
+type ComponentPickerProps = {
+  contextDisabled?: boolean
+  historyDisabled?: boolean
+  queryDisabled?: boolean
+  historyShow?: boolean
+  queryShow?: boolean
+}
+const ComponentPicker: FC<ComponentPickerProps> = ({
+  contextDisabled,
+  historyDisabled,
+  queryDisabled,
+  historyShow,
+  queryShow,
+}) => {
+  const { t } = useTranslation()
+  const [editor] = useLexicalComposerContext()
+  const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('/', {
+    minLength: 0,
+    maxLength: 0,
+  })
+
+  const options = [
+    new ComponentPickerOption(t('common.promptEditor.context.item.title'), {
+      desc: t('common.promptEditor.context.item.desc'),
+      icon: <File05 className='w-4 h-4 text-[#6938EF]' />,
+      onSelect: () => {
+        if (contextDisabled)
+          return
+        editor.dispatchCommand(INSERT_CONTEXT_BLOCK_COMMAND, undefined)
+      },
+      disabled: contextDisabled,
+    }),
+    new ComponentPickerOption(t('common.promptEditor.variable.item.title'), {
+      desc: t('common.promptEditor.variable.item.desc'),
+      icon: <Variable className='w-4 h-4 text-[#2970FF]' />,
+      onSelect: () => {
+        editor.dispatchCommand(INSERT_VARIABLE_BLOCK_COMMAND, undefined)
+      },
+    }),
+    ...historyShow
+      ? [
+        new ComponentPickerOption(t('common.promptEditor.history.item.title'), {
+          desc: t('common.promptEditor.history.item.desc'),
+          icon: <MessageClockCircle className='w-4 h-4 text-[#DD2590]' />,
+          onSelect: () => {
+            if (historyDisabled)
+              return
+            editor.dispatchCommand(INSERT_HISTORY_BLOCK_COMMAND, undefined)
+          },
+          disabled: historyDisabled,
+        }),
+      ]
+      : [],
+    ...queryShow
+      ? [
+        new ComponentPickerOption(t('common.promptEditor.query.item.title'), {
+          desc: t('common.promptEditor.query.item.desc'),
+          icon: <UserEdit02 className='w-4 h-4 text-[#FD853A]' />,
+          onSelect: () => {
+            if (queryDisabled)
+              return
+            editor.dispatchCommand(INSERT_QUERY_BLOCK_COMMAND, undefined)
+          },
+          disabled: queryDisabled,
+        }),
+      ]
+      : [],
+  ]
+
+  const onSelectOption = useCallback(
+    (
+      selectedOption: ComponentPickerOption,
+      nodeToRemove: TextNode | null,
+      closeMenu: () => void,
+      matchingString: string,
+    ) => {
+      editor.update(() => {
+        if (nodeToRemove)
+          nodeToRemove.remove()
+
+        selectedOption.onSelect(matchingString)
+        closeMenu()
+      })
+    },
+    [editor],
+  )
+
+  return (
+    <LexicalTypeaheadMenuPlugin
+      options={options}
+      onQueryChange={() => {}}
+      onSelectOption={onSelectOption}
+      menuRenderFn={(
+        anchorElementRef,
+        { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
+      ) =>
+        (anchorElementRef.current && options.length)
+          ? ReactDOM.createPortal(
+            <div className='mt-[25px] p-1 w-[400px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
+              {options.map((option, i: number) => (
+                <ComponentPickerMenuItem
+                  isSelected={selectedIndex === i}
+                  onClick={() => {
+                    if (option.disabled)
+                      return
+                    setHighlightedIndex(i)
+                    selectOptionAndCleanUp(option)
+                  }}
+                  onMouseEnter={() => {
+                    if (option.disabled)
+                      return
+                    setHighlightedIndex(i)
+                  }}
+                  key={option.key}
+                  option={option}
+                />
+              ))}
+            </div>,
+            anchorElementRef.current,
+          )
+          : null}
+      triggerFn={checkForTriggerMatch}
+    />
+  )
+}
+
+export default ComponentPicker

+ 61 - 0
web/app/components/base/prompt-editor/plugins/context-block-replacement-block.tsx

@@ -0,0 +1,61 @@
+import type { FC } from 'react'
+import {
+  useCallback,
+  useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../utils'
+import { CONTEXT_PLACEHOLDER_TEXT } from '../constants'
+import {
+  $createContextBlockNode,
+  ContextBlockNode,
+} from './context-block/node'
+import type { ContextBlockProps } from './context-block/index'
+import { CustomTextNode } from './custom-text/node'
+
+const REGEX = new RegExp(CONTEXT_PLACEHOLDER_TEXT)
+
+const ContextBlockReplacementBlock: FC<ContextBlockProps> = ({
+  datasets,
+  onAddContext,
+  onInsert,
+}) => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    if (!editor.hasNodes([ContextBlockNode]))
+      throw new Error('ContextBlockNodePlugin: ContextBlockNode not registered on editor')
+  }, [editor])
+
+  const createContextBlockNode = useCallback((): ContextBlockNode => {
+    if (onInsert)
+      onInsert()
+    return $applyNodeReplacement($createContextBlockNode(datasets, onAddContext))
+  }, [datasets, onAddContext, onInsert])
+
+  const getMatch = useCallback((text: string) => {
+    const matchArr = REGEX.exec(text)
+
+    if (matchArr === null)
+      return null
+
+    const startOffset = matchArr.index
+    const endOffset = startOffset + CONTEXT_PLACEHOLDER_TEXT.length
+    return {
+      end: endOffset,
+      start: startOffset,
+    }
+  }, [])
+
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createContextBlockNode)),
+    )
+  }, [])
+
+  return null
+}
+
+export default ContextBlockReplacementBlock

+ 97 - 0
web/app/components/base/prompt-editor/plugins/context-block/component.tsx

@@ -0,0 +1,97 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useSelectOrDelete, useTrigger } from '../../hooks'
+import { UPDATE_DATASETS_EVENT_EMITTER } from '../../constants'
+import type { Dataset } from './index'
+import { DELETE_CONTEXT_BLOCK_COMMAND } from './index'
+import { File05, Folder } from '@/app/components/base/icons/src/vender/solid/files'
+import { Plus } from '@/app/components/base/icons/src/vender/line/general'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import { useEventEmitterContextContext } from '@/context/event-emitter'
+
+type ContextBlockComponentProps = {
+  nodeKey: string
+  datasets?: Dataset[]
+  onAddContext: () => void
+}
+
+const ContextBlockComponent: FC<ContextBlockComponentProps> = ({
+  nodeKey,
+  datasets = [],
+  onAddContext,
+}) => {
+  const { t } = useTranslation()
+  const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_CONTEXT_BLOCK_COMMAND)
+  const [triggerRef, open, setOpen] = useTrigger()
+  const { eventEmitter } = useEventEmitterContextContext()
+  const [localDatasets, setLocalDatasets] = useState<Dataset[]>(datasets)
+
+  eventEmitter?.useSubscription((v: any) => {
+    if (v?.type === UPDATE_DATASETS_EVENT_EMITTER)
+      setLocalDatasets(v.payload)
+  })
+
+  return (
+    <div className={`
+      group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent bg-[#F4F3FF] text-[#6938EF] rounded-[5px] hover:bg-[#EBE9FE]
+      ${open ? 'bg-[#EBE9FE]' : 'bg-[#F4F3FF]'}
+      ${isSelected && '!border-[#9B8AFB]'}
+    `} ref={ref}>
+      <File05 className='mr-1 w-[14px] h-[14px]' />
+      <div className='mr-1 text-xs font-medium'>{t('common.promptEditor.context.item.title')}</div>
+      <PortalToFollowElem
+        open={open}
+        onOpenChange={setOpen}
+        placement='bottom-end'
+        offset={{
+          mainAxis: 3,
+          alignmentAxis: -147,
+        }}
+      >
+        <PortalToFollowElemTrigger ref={triggerRef}>
+          <div className={`
+            flex items-center justify-center w-[18px] h-[18px] text-[11px] font-semibold rounded cursor-pointer
+            ${open ? 'bg-[#6938EF] text-white' : 'bg-white/50 group-hover:bg-white group-hover:shadow-xs'}
+          `}>{localDatasets.length}</div>
+        </PortalToFollowElemTrigger>
+        <PortalToFollowElemContent style={{ zIndex: 100 }}>
+          <div className='w-[360px] bg-white rounded-xl shadow-lg'>
+            <div className='p-4'>
+              <div className='mb-2 text-xs font-medium text-gray-500'>
+                {t('common.promptEditor.context.modal.title', { num: localDatasets.length })}
+              </div>
+              <div className='max-h-[270px] overflow-y-auto'>
+                {
+                  localDatasets.map(dataset => (
+                    <div key={dataset.id} className='flex items-center h-8'>
+                      <div className='flex items-center justify-center shrink-0 mr-2 w-6 h-6 bg-[#F5F8FF] rounded-md border-[0.5px] border-[#EAECF5]'>
+                        <Folder className='w-4 h-4 text-[#444CE7]' />
+                      </div>
+                      <div className='text-sm text-gray-800 truncate' title=''>{dataset.name}</div>
+                    </div>
+                  ))
+                }
+              </div>
+              <div className='flex items-center h-8 text-[#155EEF] cursor-pointer' onClick={onAddContext}>
+                <div className='shrink-0 flex justify-center items-center mr-2 w-6 h-6 rounded-md border-[0.5px] border-gray-100'>
+                  <Plus className='w-[14px] h-[14px]' />
+                </div>
+                <div className='text-[13px] font-medium' title=''>{t('common.promptEditor.context.modal.add')}</div>
+              </div>
+            </div>
+            <div className='px-4 py-3 text-xs text-gray-500 bg-gray-50 border-t-[0.5px] border-gray-50 rounded-b-xl'>
+              {t('common.promptEditor.context.modal.footer')}
+            </div>
+          </div>
+        </PortalToFollowElemContent>
+      </PortalToFollowElem>
+    </div>
+  )
+}
+
+export default ContextBlockComponent

+ 73 - 0
web/app/components/base/prompt-editor/plugins/context-block/index.tsx

@@ -0,0 +1,73 @@
+import type { FC } from 'react'
+import { useEffect } from 'react'
+import {
+  $insertNodes,
+  COMMAND_PRIORITY_EDITOR,
+  createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import {
+  $createContextBlockNode,
+  ContextBlockNode,
+} from './node'
+
+export const INSERT_CONTEXT_BLOCK_COMMAND = createCommand('INSERT_CONTEXT_BLOCK_COMMAND')
+export const DELETE_CONTEXT_BLOCK_COMMAND = createCommand('DELETE_CONTEXT_BLOCK_COMMAND')
+
+export type Dataset = {
+  id: string
+  name: string
+  type: string
+}
+
+export type ContextBlockProps = {
+  datasets: Dataset[]
+  onAddContext: () => void
+  onInsert?: () => void
+  onDelete?: () => void
+}
+const ContextBlock: FC<ContextBlockProps> = ({
+  datasets,
+  onAddContext,
+  onInsert,
+  onDelete,
+}) => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    if (!editor.hasNodes([ContextBlockNode]))
+      throw new Error('ContextBlockPlugin: ContextBlock not registered on editor')
+
+    return mergeRegister(
+      editor.registerCommand(
+        INSERT_CONTEXT_BLOCK_COMMAND,
+        () => {
+          const contextBlockNode = $createContextBlockNode(datasets, onAddContext)
+
+          $insertNodes([contextBlockNode])
+
+          if (onInsert)
+            onInsert()
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+      editor.registerCommand(
+        DELETE_CONTEXT_BLOCK_COMMAND,
+        () => {
+          if (onDelete)
+            onDelete()
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+    )
+  }, [editor, datasets, onAddContext, onInsert, onDelete])
+
+  return null
+}
+
+export default ContextBlock

+ 90 - 0
web/app/components/base/prompt-editor/plugins/context-block/node.tsx

@@ -0,0 +1,90 @@
+import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import ContextBlockComponent from './component'
+import type { Dataset } from './index'
+
+export type SerializedNode = SerializedLexicalNode & { datasets: Dataset[]; onAddContext: () => void }
+
+export class ContextBlockNode extends DecoratorNode<JSX.Element> {
+  __datasets: Dataset[]
+  __onAddContext: () => void
+
+  static getType(): string {
+    return 'context-block'
+  }
+
+  static clone(node: ContextBlockNode): ContextBlockNode {
+    return new ContextBlockNode(node.__datasets, node.__onAddContext)
+  }
+
+  isInline(): boolean {
+    return true
+  }
+
+  constructor(datasets: Dataset[], onAddContext: () => void, key?: NodeKey) {
+    super(key)
+
+    this.__datasets = datasets
+    this.__onAddContext = onAddContext
+  }
+
+  createDOM(): HTMLElement {
+    const div = document.createElement('div')
+    div.classList.add('inline-flex', 'items-center', 'align-middle')
+    return div
+  }
+
+  updateDOM(): false {
+    return false
+  }
+
+  decorate(): JSX.Element {
+    return (
+      <ContextBlockComponent
+        nodeKey={this.getKey()}
+        datasets={this.getDatasets()}
+        onAddContext={this.getOnAddContext()}
+      />
+    )
+  }
+
+  getDatasets(): Dataset[] {
+    const self = this.getLatest()
+
+    return self.__datasets
+  }
+
+  getOnAddContext(): () => void {
+    const self = this.getLatest()
+
+    return self.__onAddContext
+  }
+
+  static importJSON(serializedNode: SerializedNode): ContextBlockNode {
+    const node = $createContextBlockNode(serializedNode.datasets, serializedNode.onAddContext)
+
+    return node
+  }
+
+  exportJSON(): SerializedNode {
+    return {
+      type: 'context-block',
+      version: 1,
+      datasets: this.getDatasets(),
+      onAddContext: this.getOnAddContext(),
+    }
+  }
+
+  getTextContent(): string {
+    return '{{#context#}}'
+  }
+}
+export function $createContextBlockNode(datasets: Dataset[], onAddContext: () => void): ContextBlockNode {
+  return new ContextBlockNode(datasets, onAddContext)
+}
+
+export function $isContextBlockNode(
+  node: ContextBlockNode | LexicalNode | null | undefined,
+): boolean {
+  return node instanceof ContextBlockNode
+}

+ 52 - 0
web/app/components/base/prompt-editor/plugins/custom-text/node.tsx

@@ -0,0 +1,52 @@
+import type { EditorConfig, NodeKey, SerializedTextNode } from 'lexical'
+import { $createTextNode, TextNode } from 'lexical'
+
+export class CustomTextNode extends TextNode {
+  static getType() {
+    return 'custom-text'
+  }
+
+  static clone(node: CustomTextNode) {
+    return new CustomTextNode(node.__text, node.__key)
+  }
+
+  constructor(text: string, key?: NodeKey) {
+    super(text, key)
+  }
+
+  createDOM(config: EditorConfig) {
+    const dom = super.createDOM(config)
+    dom.classList.add('align-middle')
+    return dom
+  }
+
+  static importJSON(serializedNode: SerializedTextNode): TextNode {
+    const node = $createTextNode(serializedNode.text)
+    node.setFormat(serializedNode.format)
+    node.setDetail(serializedNode.detail)
+    node.setMode(serializedNode.mode)
+    node.setStyle(serializedNode.style)
+    return node
+  }
+
+  exportJSON(): SerializedTextNode {
+    return {
+      detail: this.getDetail(),
+      format: this.getFormat(),
+      mode: this.getMode(),
+      style: this.getStyle(),
+      text: this.getTextContent(),
+      type: 'custom-text',
+      version: 1,
+    }
+  }
+
+  isSimpleText() {
+    return (
+      (this.__type === 'text' || this.__type === 'custom-text') && this.__mode === 0)
+  }
+}
+
+export function $createCustomTextNode(text: string): CustomTextNode {
+  return new CustomTextNode(text)
+}

+ 61 - 0
web/app/components/base/prompt-editor/plugins/history-block-replacement-block.tsx

@@ -0,0 +1,61 @@
+import type { FC } from 'react'
+import {
+  useCallback,
+  useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../utils'
+import { HISTORY_PLACEHOLDER_TEXT } from '../constants'
+import {
+  $createHistoryBlockNode,
+  HistoryBlockNode,
+} from './history-block/node'
+import type { HistoryBlockProps } from './history-block/index'
+import { CustomTextNode } from './custom-text/node'
+
+const REGEX = new RegExp(HISTORY_PLACEHOLDER_TEXT)
+
+const HistoryBlockReplacementBlock: FC<HistoryBlockProps> = ({
+  roleName,
+  onEditRole,
+  onInsert,
+}) => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    if (!editor.hasNodes([HistoryBlockNode]))
+      throw new Error('HistoryBlockNodePlugin: HistoryBlockNode not registered on editor')
+  }, [editor])
+
+  const createHistoryBlockNode = useCallback((): HistoryBlockNode => {
+    if (onInsert)
+      onInsert()
+    return $applyNodeReplacement($createHistoryBlockNode(roleName, onEditRole))
+  }, [roleName, onEditRole, onInsert])
+
+  const getMatch = useCallback((text: string) => {
+    const matchArr = REGEX.exec(text)
+
+    if (matchArr === null)
+      return null
+
+    const startOffset = matchArr.index
+    const endOffset = startOffset + HISTORY_PLACEHOLDER_TEXT.length
+    return {
+      end: endOffset,
+      start: startOffset,
+    }
+  }, [])
+
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createHistoryBlockNode)),
+    )
+  }, [])
+
+  return null
+}
+
+export default HistoryBlockReplacementBlock

+ 90 - 0
web/app/components/base/prompt-editor/plugins/history-block/component.tsx

@@ -0,0 +1,90 @@
+import type { FC } from 'react'
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useSelectOrDelete, useTrigger } from '../../hooks'
+import { UPDATE_HISTORY_EVENT_EMITTER } from '../../constants'
+import type { RoleName } from './index'
+import { DELETE_HISTORY_BLOCK_COMMAND } from './index'
+import { DotsHorizontal } from '@/app/components/base/icons/src/vender/line/general'
+import { MessageClockCircle } from '@/app/components/base/icons/src/vender/solid/general'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import { useEventEmitterContextContext } from '@/context/event-emitter'
+
+type HistoryBlockComponentProps = {
+  nodeKey: string
+  roleName?: RoleName
+  onEditRole: () => void
+}
+
+const HistoryBlockComponent: FC<HistoryBlockComponentProps> = ({
+  nodeKey,
+  roleName = { user: '', assistant: '' },
+  onEditRole,
+}) => {
+  const { t } = useTranslation()
+  const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_HISTORY_BLOCK_COMMAND)
+  const [triggerRef, open, setOpen] = useTrigger()
+  const { eventEmitter } = useEventEmitterContextContext()
+  const [localRoleName, setLocalRoleName] = useState<RoleName>(roleName)
+
+  eventEmitter?.useSubscription((v: any) => {
+    if (v?.type === UPDATE_HISTORY_EVENT_EMITTER)
+      setLocalRoleName(v.payload)
+  })
+
+  return (
+    <div className={`
+      group inline-flex items-center pl-1 pr-0.5 h-6 border border-transparent text-[#DD2590] rounded-[5px] hover:bg-[#FCE7F6]
+      ${open ? 'bg-[#FCE7F6]' : 'bg-[#FDF2FA]'}
+      ${isSelected && '!border-[#F670C7]'}
+    `} ref={ref}>
+      <MessageClockCircle className='mr-1 w-[14px] h-[14px]' />
+      <div className='mr-1 text-xs font-medium'>{t('common.promptEditor.history.item.title')}</div>
+      <PortalToFollowElem
+        open={open}
+        onOpenChange={setOpen}
+        placement='top-end'
+        offset={{
+          mainAxis: 4,
+          alignmentAxis: -148,
+        }}
+      >
+        <PortalToFollowElemTrigger ref={triggerRef}>
+          <div className={`
+            flex items-center justify-center w-[18px] h-[18px] rounded cursor-pointer
+            ${open ? 'bg-[#DD2590] text-white' : 'bg-white/50 group-hover:bg-white group-hover:shadow-xs'}
+          `}>
+            <DotsHorizontal className='w-3 h-3' />
+          </div>
+        </PortalToFollowElemTrigger>
+        <PortalToFollowElemContent style={{ zIndex: 100 }}>
+          <div className='w-[360px] bg-white rounded-xl shadow-lg'>
+            <div className='p-4'>
+              <div className='mb-2 text-xs font-medium text-gray-500'>{t('common.promptEditor.history.modal.title')}</div>
+              <div className='flex items-center text-sm text-gray-700'>
+                <div className='mr-1 w-20 text-xs font-semibold'>{localRoleName?.user}</div>
+                {t('common.promptEditor.history.modal.user')}
+              </div>
+              <div className='flex items-center text-sm text-gray-700'>
+                <div className='mr-1 w-20 text-xs font-semibold'>{localRoleName?.assistant}</div>
+                {t('common.promptEditor.history.modal.assistant')}
+              </div>
+            </div>
+            <div
+              className='px-4 py-3 text-xs text-[#155EEF] border-t border-black/5 rounded-b-xl cursor-pointer'
+              onClick={onEditRole}
+            >
+              {t('common.promptEditor.history.modal.edit')}
+            </div>
+          </div>
+        </PortalToFollowElemContent>
+      </PortalToFollowElem>
+    </div>
+  )
+}
+
+export default HistoryBlockComponent

+ 73 - 0
web/app/components/base/prompt-editor/plugins/history-block/index.tsx

@@ -0,0 +1,73 @@
+import type { FC } from 'react'
+import { useEffect } from 'react'
+import {
+  $insertNodes,
+  COMMAND_PRIORITY_EDITOR,
+  createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import {
+  $createHistoryBlockNode,
+  HistoryBlockNode,
+} from './node'
+
+export const INSERT_HISTORY_BLOCK_COMMAND = createCommand('INSERT_HISTORY_BLOCK_COMMAND')
+export const DELETE_HISTORY_BLOCK_COMMAND = createCommand('DELETE_HISTORY_BLOCK_COMMAND')
+
+export type RoleName = {
+  user: string
+  assistant: string
+}
+
+export type HistoryBlockProps = {
+  roleName: RoleName
+  onEditRole: () => void
+  onInsert?: () => void
+  onDelete?: () => void
+}
+
+const HistoryBlock: FC<HistoryBlockProps> = ({
+  roleName,
+  onEditRole,
+  onInsert,
+  onDelete,
+}) => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    if (!editor.hasNodes([HistoryBlockNode]))
+      throw new Error('HistoryBlockPlugin: HistoryBlock not registered on editor')
+
+    return mergeRegister(
+      editor.registerCommand(
+        INSERT_HISTORY_BLOCK_COMMAND,
+        () => {
+          const historyBlockNode = $createHistoryBlockNode(roleName, onEditRole)
+
+          $insertNodes([historyBlockNode])
+
+          if (onInsert)
+            onInsert()
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+      editor.registerCommand(
+        DELETE_HISTORY_BLOCK_COMMAND,
+        () => {
+          if (onDelete)
+            onDelete()
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+    )
+  }, [editor, roleName, onEditRole, onInsert, onDelete])
+
+  return null
+}
+
+export default HistoryBlock

+ 90 - 0
web/app/components/base/prompt-editor/plugins/history-block/node.tsx

@@ -0,0 +1,90 @@
+import type { LexicalNode, NodeKey, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import HistoryBlockComponent from './component'
+import type { RoleName } from './index'
+
+export type SerializedNode = SerializedLexicalNode & { roleName: RoleName; onEditRole: () => void }
+
+export class HistoryBlockNode extends DecoratorNode<JSX.Element> {
+  __roleName: RoleName
+  __onEditRole: () => void
+
+  static getType(): string {
+    return 'history-block'
+  }
+
+  static clone(node: HistoryBlockNode): HistoryBlockNode {
+    return new HistoryBlockNode(node.__roleName, node.__onEditRole)
+  }
+
+  constructor(roleName: RoleName, onEditRole: () => void, key?: NodeKey) {
+    super(key)
+
+    this.__roleName = roleName
+    this.__onEditRole = onEditRole
+  }
+
+  isInline(): boolean {
+    return true
+  }
+
+  createDOM(): HTMLElement {
+    const div = document.createElement('div')
+    div.classList.add('inline-flex', 'items-center', 'align-middle')
+    return div
+  }
+
+  updateDOM(): false {
+    return false
+  }
+
+  decorate(): JSX.Element {
+    return (
+      <HistoryBlockComponent
+        nodeKey={this.getKey()}
+        roleName={this.getRoleName()}
+        onEditRole={this.getOnEditRole()}
+      />
+    )
+  }
+
+  getRoleName(): RoleName {
+    const self = this.getLatest()
+
+    return self.__roleName
+  }
+
+  getOnEditRole(): () => void {
+    const self = this.getLatest()
+
+    return self.__onEditRole
+  }
+
+  static importJSON(serializedNode: SerializedNode): HistoryBlockNode {
+    const node = $createHistoryBlockNode(serializedNode.roleName, serializedNode.onEditRole)
+
+    return node
+  }
+
+  exportJSON(): SerializedNode {
+    return {
+      type: 'history-block',
+      version: 1,
+      roleName: this.getRoleName(),
+      onEditRole: this.getOnEditRole,
+    }
+  }
+
+  getTextContent(): string {
+    return '{{#histories#}}'
+  }
+}
+export function $createHistoryBlockNode(roleName: RoleName, onEditRole: () => void): HistoryBlockNode {
+  return new HistoryBlockNode(roleName, onEditRole)
+}
+
+export function $isHistoryBlockNode(
+  node: HistoryBlockNode | LexicalNode | null | undefined,
+): node is HistoryBlockNode {
+  return node instanceof HistoryBlockNode
+}

+ 36 - 0
web/app/components/base/prompt-editor/plugins/on-blur-block.tsx

@@ -0,0 +1,36 @@
+import type { FC } from 'react'
+import { useEffect } from 'react'
+import {
+  BLUR_COMMAND,
+  COMMAND_PRIORITY_EDITOR,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+
+type OnBlurBlockProps = {
+  onBlur?: () => void
+}
+const OnBlurBlock: FC<OnBlurBlockProps> = ({
+  onBlur,
+}) => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerCommand(
+        BLUR_COMMAND,
+        () => {
+          if (onBlur)
+            onBlur()
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+    )
+  }, [editor, onBlur])
+
+  return null
+}
+
+export default OnBlurBlock

+ 13 - 0
web/app/components/base/prompt-editor/plugins/placeholder.tsx

@@ -0,0 +1,13 @@
+import { useTranslation } from 'react-i18next'
+
+const Placeholder = () => {
+  const { t } = useTranslation()
+
+  return (
+    <div className='absolute top-0 left-0 h-full w-full text-sm text-gray-300 select-none pointer-events-none leading-6'>
+      {t('common.promptEditor.placeholder')}
+    </div>
+  )
+}
+
+export default Placeholder

+ 59 - 0
web/app/components/base/prompt-editor/plugins/query-block-replacement-block.tsx

@@ -0,0 +1,59 @@
+import type { FC } from 'react'
+import {
+  useCallback,
+  useEffect,
+} from 'react'
+import { $applyNodeReplacement } from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { decoratorTransform } from '../utils'
+import { QUERY_PLACEHOLDER_TEXT } from '../constants'
+import {
+  $createQueryBlockNode,
+  QueryBlockNode,
+} from './query-block/node'
+import type { QueryBlockProps } from './query-block/index'
+import { CustomTextNode } from './custom-text/node'
+
+const REGEX = new RegExp(QUERY_PLACEHOLDER_TEXT)
+
+const QueryBlockReplacementBlock: FC<QueryBlockProps> = ({
+  onInsert,
+}) => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    if (!editor.hasNodes([QueryBlockNode]))
+      throw new Error('QueryBlockNodePlugin: QueryBlockNode not registered on editor')
+  }, [editor])
+
+  const createQueryBlockNode = useCallback((): QueryBlockNode => {
+    if (onInsert)
+      onInsert()
+    return $applyNodeReplacement($createQueryBlockNode())
+  }, [onInsert])
+
+  const getMatch = useCallback((text: string) => {
+    const matchArr = REGEX.exec(text)
+
+    if (matchArr === null)
+      return null
+
+    const startOffset = matchArr.index
+    const endOffset = startOffset + QUERY_PLACEHOLDER_TEXT.length
+    return {
+      end: endOffset,
+      start: startOffset,
+    }
+  }, [])
+
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerNodeTransform(CustomTextNode, textNode => decoratorTransform(textNode, getMatch, createQueryBlockNode)),
+    )
+  }, [])
+
+  return null
+}
+
+export default QueryBlockReplacementBlock

+ 33 - 0
web/app/components/base/prompt-editor/plugins/query-block/component.tsx

@@ -0,0 +1,33 @@
+import type { FC } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useSelectOrDelete } from '../../hooks'
+import { DELETE_QUERY_BLOCK_COMMAND } from './index'
+import { UserEdit02 } from '@/app/components/base/icons/src/vender/solid/users'
+
+type QueryBlockComponentProps = {
+  nodeKey: string
+}
+
+const QueryBlockComponent: FC<QueryBlockComponentProps> = ({
+  nodeKey,
+}) => {
+  const { t } = useTranslation()
+  const [ref, isSelected] = useSelectOrDelete(nodeKey, DELETE_QUERY_BLOCK_COMMAND)
+
+  return (
+    <div
+      className={`
+        inline-flex items-center pl-1 pr-0.5 h-6 bg-[#FFF6ED] border border-transparent rounded-[5px] hover:bg-[#FFEAD5]
+        ${isSelected && '!border-[#FD853A]'}
+      `}
+      ref={ref}
+    >
+      <UserEdit02 className='mr-1 w-[14px] h-[14px] text-[#FD853A]' />
+      <div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'{{'}</div>
+      <div className='text-xs font-medium text-[#EC4A0A]'>{t('common.promptEditor.query.item.title')}</div>
+      <div className='text-xs font-medium text-[#EC4A0A] opacity-60'>{'}}'}</div>
+    </div>
+  )
+}
+
+export default QueryBlockComponent

+ 62 - 0
web/app/components/base/prompt-editor/plugins/query-block/index.tsx

@@ -0,0 +1,62 @@
+import type { FC } from 'react'
+import { useEffect } from 'react'
+import {
+  $insertNodes,
+  COMMAND_PRIORITY_EDITOR,
+  createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import {
+  $createQueryBlockNode,
+  QueryBlockNode,
+} from './node'
+
+export const INSERT_QUERY_BLOCK_COMMAND = createCommand('INSERT_QUERY_BLOCK_COMMAND')
+export const DELETE_QUERY_BLOCK_COMMAND = createCommand('DELETE_QUERY_BLOCK_COMMAND')
+
+export type QueryBlockProps = {
+  onInsert?: () => void
+  onDelete?: () => void
+}
+const QueryBlock: FC<QueryBlockProps> = ({
+  onInsert,
+  onDelete,
+}) => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    if (!editor.hasNodes([QueryBlockNode]))
+      throw new Error('QueryBlockPlugin: QueryBlock not registered on editor')
+
+    return mergeRegister(
+      editor.registerCommand(
+        INSERT_QUERY_BLOCK_COMMAND,
+        () => {
+          const contextBlockNode = $createQueryBlockNode()
+
+          $insertNodes([contextBlockNode])
+          if (onInsert)
+            onInsert()
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+      editor.registerCommand(
+        DELETE_QUERY_BLOCK_COMMAND,
+        () => {
+          if (onDelete)
+            onDelete()
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+    )
+  }, [editor, onInsert, onDelete])
+
+  return null
+}
+
+export default QueryBlock

+ 59 - 0
web/app/components/base/prompt-editor/plugins/query-block/node.tsx

@@ -0,0 +1,59 @@
+import type { LexicalNode, SerializedLexicalNode } from 'lexical'
+import { DecoratorNode } from 'lexical'
+import QueryBlockComponent from './component'
+
+export type SerializedNode = SerializedLexicalNode
+
+export class QueryBlockNode extends DecoratorNode<JSX.Element> {
+  static getType(): string {
+    return 'query-block'
+  }
+
+  static clone(): QueryBlockNode {
+    return new QueryBlockNode()
+  }
+
+  isInline(): boolean {
+    return true
+  }
+
+  createDOM(): HTMLElement {
+    const div = document.createElement('div')
+    div.classList.add('inline-flex', 'items-center', 'align-middle')
+    return div
+  }
+
+  updateDOM(): false {
+    return false
+  }
+
+  decorate(): JSX.Element {
+    return <QueryBlockComponent nodeKey={this.getKey()} />
+  }
+
+  static importJSON(): QueryBlockNode {
+    const node = $createQueryBlockNode()
+
+    return node
+  }
+
+  exportJSON(): SerializedNode {
+    return {
+      type: 'query-block',
+      version: 1,
+    }
+  }
+
+  getTextContent(): string {
+    return '{{#query#}}'
+  }
+}
+export function $createQueryBlockNode(): QueryBlockNode {
+  return new QueryBlockNode()
+}
+
+export function $isQueryBlockNode(
+  node: QueryBlockNode | LexicalNode | null | undefined,
+): node is QueryBlockNode {
+  return node instanceof QueryBlockNode
+}

+ 19 - 0
web/app/components/base/prompt-editor/plugins/tree-view.tsx

@@ -0,0 +1,19 @@
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { TreeView } from '@lexical/react/LexicalTreeView'
+
+const TreeViewPlugin = () => {
+  const [editor] = useLexicalComposerContext()
+  return (
+    <TreeView
+      viewClassName="tree-view-output"
+      treeTypeButtonClassName="debug-treetype-button"
+      timeTravelPanelClassName="debug-timetravel-panel"
+      timeTravelButtonClassName="debug-timetravel-button"
+      timeTravelPanelSliderClassName="debug-timetravel-panel-slider"
+      timeTravelPanelButtonClassName="debug-timetravel-panel-button"
+      editor={editor}
+    />
+  )
+}
+
+export default TreeViewPlugin

+ 45 - 0
web/app/components/base/prompt-editor/plugins/variable-block/index.tsx

@@ -0,0 +1,45 @@
+import { useEffect } from 'react'
+import {
+  $insertNodes,
+  COMMAND_PRIORITY_EDITOR,
+  createCommand,
+} from 'lexical'
+import { mergeRegister } from '@lexical/utils'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { CustomTextNode } from '../custom-text/node'
+
+export const INSERT_VARIABLE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_BLOCK_COMMAND')
+export const INSERT_VARIABLE_VALUE_BLOCK_COMMAND = createCommand('INSERT_VARIABLE_VALUE_BLOCK_COMMAND')
+
+const VariableBlock = () => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    return mergeRegister(
+      editor.registerCommand(
+        INSERT_VARIABLE_BLOCK_COMMAND,
+        () => {
+          const textNode = new CustomTextNode('{')
+          $insertNodes([textNode])
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+      editor.registerCommand(
+        INSERT_VARIABLE_VALUE_BLOCK_COMMAND,
+        (value: string) => {
+          const textNode = new CustomTextNode(value)
+          $insertNodes([textNode])
+
+          return true
+        },
+        COMMAND_PRIORITY_EDITOR,
+      ),
+    )
+  }, [editor])
+
+  return null
+}
+
+export default VariableBlock

+ 228 - 0
web/app/components/base/prompt-editor/plugins/variable-picker.tsx

@@ -0,0 +1,228 @@
+import type { FC } from 'react'
+import { useCallback, useMemo, useState } from 'react'
+import ReactDOM from 'react-dom'
+import { useTranslation } from 'react-i18next'
+import { $insertNodes, type TextNode } from 'lexical'
+import {
+  LexicalTypeaheadMenuPlugin,
+  MenuOption,
+} from '@lexical/react/LexicalTypeaheadMenuPlugin'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useBasicTypeaheadTriggerMatch } from '../hooks'
+import { INSERT_VARIABLE_VALUE_BLOCK_COMMAND } from './variable-block'
+import { $createCustomTextNode } from './custom-text/node'
+import { BracketsX } from '@/app/components/base/icons/src/vender/line/development'
+
+class VariablePickerOption extends MenuOption {
+  title: string
+  icon?: JSX.Element
+  keywords: Array<string>
+  keyboardShortcut?: string
+  onSelect: (queryString: string) => void
+
+  constructor(
+    title: string,
+    options: {
+      icon?: JSX.Element
+      keywords?: Array<string>
+      keyboardShortcut?: string
+      onSelect: (queryString: string) => void
+    },
+  ) {
+    super(title)
+    this.title = title
+    this.keywords = options.keywords || []
+    this.icon = options.icon
+    this.keyboardShortcut = options.keyboardShortcut
+    this.onSelect = options.onSelect.bind(this)
+  }
+}
+
+type VariablePickerMenuItemProps = {
+  isSelected: boolean
+  onClick: () => void
+  onMouseEnter: () => void
+  option: VariablePickerOption
+  queryString: string | null
+}
+const VariablePickerMenuItem: FC<VariablePickerMenuItemProps> = ({
+  isSelected,
+  onClick,
+  onMouseEnter,
+  option,
+  queryString,
+}) => {
+  const title = option.title
+  let before = title
+  let middle = ''
+  let after = ''
+
+  if (queryString) {
+    const regex = new RegExp(queryString, 'i')
+    const match = regex.exec(option.title)
+
+    if (match) {
+      before = title.substring(0, match.index)
+      middle = match[0]
+      after = title.substring(match.index + match[0].length)
+    }
+  }
+
+  return (
+    <div
+      key={option.key}
+      className={`
+        flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
+        ${isSelected && 'bg-primary-50'}
+      `}
+      tabIndex={-1}
+      ref={option.setRefElement}
+      onMouseEnter={onMouseEnter}
+      onClick={onClick}>
+      <div className='mr-2'>
+        {option.icon}
+      </div>
+      <div className='text-[13px] text-gray-900'>
+        {before}
+        <span className='text-[#2970FF]'>{middle}</span>
+        {after}
+      </div>
+    </div>
+  )
+}
+
+export type Option = {
+  value: string
+  name: string
+}
+
+type VariablePickerProps = {
+  items?: Option[]
+}
+const VariablePicker: FC<VariablePickerProps> = ({
+  items = [],
+}) => {
+  const { t } = useTranslation()
+  const [editor] = useLexicalComposerContext()
+  const checkForTriggerMatch = useBasicTypeaheadTriggerMatch('{', {
+    minLength: 0,
+    maxLength: 6,
+  })
+  const [queryString, setQueryString] = useState<string | null>(null)
+
+  const options = useMemo(() => {
+    const baseOptions = items.map((item) => {
+      return new VariablePickerOption(item.value, {
+        icon: <BracketsX className='w-[14px] h-[14px] text-[#2970FF]' />,
+        onSelect: () => {
+          editor.dispatchCommand(INSERT_VARIABLE_VALUE_BLOCK_COMMAND, `{{${item.value}}}`)
+        },
+      })
+    })
+    if (!queryString)
+      return baseOptions
+
+    const regex = new RegExp(queryString, 'i')
+
+    return baseOptions.filter(option => regex.test(option.title) || option.keywords.some(keyword => regex.test(keyword)))
+  }, [editor, queryString, items])
+
+  const newOption = new VariablePickerOption(t('common.promptEditor.variable.modal.add'), {
+    icon: <BracketsX className='mr-2 w-[14px] h-[14px] text-[#2970FF]' />,
+    onSelect: () => {
+      editor.update(() => {
+        const prefixNode = $createCustomTextNode('{{')
+        const suffixNode = $createCustomTextNode('}}')
+        $insertNodes([prefixNode, suffixNode])
+        prefixNode.select()
+      })
+    },
+  })
+
+  const onSelectOption = useCallback(
+    (
+      selectedOption: VariablePickerOption,
+      nodeToRemove: TextNode | null,
+      closeMenu: () => void,
+      matchingString: string,
+    ) => {
+      editor.update(() => {
+        if (nodeToRemove)
+          nodeToRemove.remove()
+
+        selectedOption.onSelect(matchingString)
+        closeMenu()
+      })
+    },
+    [editor],
+  )
+
+  const mergedOptions = [...options, newOption]
+
+  return (
+    <LexicalTypeaheadMenuPlugin
+      options={mergedOptions}
+      onQueryChange={setQueryString}
+      onSelectOption={onSelectOption}
+      menuRenderFn={(
+        anchorElementRef,
+        { selectedIndex, selectOptionAndCleanUp, setHighlightedIndex },
+      ) =>
+        (anchorElementRef.current && mergedOptions.length)
+          ? ReactDOM.createPortal(
+            <div className='mt-[25px] w-[240px] bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg'>
+              {
+                !!options.length && (
+                  <>
+                    <div className='p-1'>
+                      {options.map((option, i: number) => (
+                        <VariablePickerMenuItem
+                          isSelected={selectedIndex === i}
+                          onClick={() => {
+                            setHighlightedIndex(i)
+                            selectOptionAndCleanUp(option)
+                          }}
+                          onMouseEnter={() => {
+                            setHighlightedIndex(i)
+                          }}
+                          key={option.key}
+                          option={option}
+                          queryString={queryString}
+                        />
+                      ))}
+                    </div>
+                    <div className='h-[1px] bg-gray-100' />
+                  </>
+                )
+              }
+              <div className='p-1'>
+                <div
+                  className={`
+                    flex items-center px-3 h-6 rounded-md hover:bg-primary-50 cursor-pointer
+                    ${selectedIndex === options.length && 'bg-primary-50'}
+                  `}
+                  ref={newOption.setRefElement}
+                  tabIndex={-1}
+                  onClick={() => {
+                    setHighlightedIndex(options.length)
+                    selectOptionAndCleanUp(newOption)
+                  }}
+                  onMouseEnter={() => {
+                    setHighlightedIndex(options.length)
+                  }}
+                  key={newOption.key}
+                >
+                  {newOption.icon}
+                  <div className='text-[13px] text-gray-900'>{newOption.title}</div>
+                </div>
+              </div>
+            </div>,
+            anchorElementRef.current,
+          )
+          : null}
+      triggerFn={checkForTriggerMatch}
+    />
+  )
+}
+
+export default VariablePicker

+ 52 - 0
web/app/components/base/prompt-editor/plugins/variable-value-block/index.tsx

@@ -0,0 +1,52 @@
+import {
+  useCallback,
+  useEffect,
+} from 'react'
+import type { TextNode } from 'lexical'
+import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
+import { useLexicalTextEntity } from '../../hooks'
+import {
+  $createVariableValueBlockNode,
+  VariableValueBlockNode,
+} from './node'
+import { getHashtagRegexString } from './utils'
+
+const REGEX = new RegExp(getHashtagRegexString(), 'i')
+
+const VariableValueBlock = () => {
+  const [editor] = useLexicalComposerContext()
+
+  useEffect(() => {
+    if (!editor.hasNodes([VariableValueBlockNode]))
+      throw new Error('VariableValueBlockPlugin: VariableValueNode not registered on editor')
+  }, [editor])
+
+  const createVariableValueBlockNode = useCallback((textNode: TextNode): VariableValueBlockNode => {
+    return $createVariableValueBlockNode(textNode.getTextContent())
+  }, [])
+
+  const getVariableValueMatch = useCallback((text: string) => {
+    const matchArr = REGEX.exec(text)
+
+    if (matchArr === null)
+      return null
+
+    const hashtagLength = matchArr[3].length + 4
+    const startOffset = matchArr.index
+    const endOffset = startOffset + hashtagLength
+    return {
+      end: endOffset,
+      start: startOffset,
+    }
+  }, [])
+
+  useLexicalTextEntity<VariableValueBlockNode>(
+    getVariableValueMatch,
+    VariableValueBlockNode,
+    createVariableValueBlockNode,
+  )
+
+  return null
+}
+
+export default VariableValueBlock

Beberapa file tidak ditampilkan karena terlalu banyak file yang berubah dalam diff ini