Browse Source

Feat/attachments (#9526)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
zxhlyh 6 months ago
parent
commit
7a1d6fe509
100 changed files with 1534 additions and 2877 deletions
  1. 1 1
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx
  2. 1 1
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx
  3. 3 3
      web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx
  4. 9 2
      web/app/(commonLayout)/apps/Apps.tsx
  5. 1 1
      web/app/(commonLayout)/datasets/Container.tsx
  6. 3 3
      web/app/components/app/annotation/empty-element.tsx
  7. 13 19
      web/app/components/app/annotation/filter.tsx
  8. 3 3
      web/app/components/app/annotation/index.tsx
  9. 15 16
      web/app/components/app/annotation/list.tsx
  10. 0 6
      web/app/components/app/annotation/style.module.css
  11. 86 0
      web/app/components/app/app-publisher/features-wrapper.tsx
  12. 3 18
      web/app/components/app/configuration/base/feature-panel/index.tsx
  13. 1 1
      web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx
  14. 5 2
      web/app/components/app/configuration/config-var/config-modal/field.tsx
  15. 72 23
      web/app/components/app/configuration/config-var/config-modal/index.tsx
  16. 2 2
      web/app/components/app/configuration/config-var/config-string/index.tsx
  17. 1 1
      web/app/components/app/configuration/config-var/index.tsx
  18. 10 4
      web/app/components/app/configuration/config-var/select-type-item/index.tsx
  19. 0 40
      web/app/components/app/configuration/config-var/select-type-item/style.module.css
  20. 79 37
      web/app/components/app/configuration/config-vision/index.tsx
  21. 113 104
      web/app/components/app/configuration/config-vision/param-config-content.tsx
  22. 3 3
      web/app/components/app/configuration/config-vision/param-config.tsx
  23. 0 40
      web/app/components/app/configuration/config-vision/radio-group/index.tsx
  24. 0 24
      web/app/components/app/configuration/config-vision/radio-group/style.module.css
  25. 0 220
      web/app/components/app/configuration/config-voice/param-config-content.tsx
  26. 0 41
      web/app/components/app/configuration/config-voice/param-config.tsx
  27. 1 1
      web/app/components/app/configuration/config/agent/agent-tools/index.tsx
  28. 20 6
      web/app/components/app/configuration/config/automatic/get-automatic-res.tsx
  29. 0 40
      web/app/components/app/configuration/config/feature/add-feature-btn/index.tsx
  30. 0 52
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/index.tsx
  31. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.png
  32. 0 150
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.svg
  33. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citations-and-attributions-preview@2x.png
  34. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/conversation-opener-preview@2x.png
  35. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this-preview@2x.png
  36. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.png
  37. 0 188
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.svg
  38. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/next-question-suggestion-preview@2x.png
  39. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-statement.png
  40. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-suggestion-preview@2x.png
  41. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text-preview@2x.png
  42. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.png
  43. 0 100
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.svg
  44. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.png
  45. 0 163
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.svg
  46. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-assistant@2x.png
  47. BIN
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-completion@2x.png
  48. 0 41
      web/app/components/app/configuration/config/feature/choose-feature/feature-item/style.module.css
  49. 0 172
      web/app/components/app/configuration/config/feature/choose-feature/index.tsx
  50. 0 31
      web/app/components/app/configuration/config/feature/feature-group/index.tsx
  51. 2 229
      web/app/components/app/configuration/config/index.tsx
  52. 1 1
      web/app/components/app/configuration/dataset-config/index.tsx
  53. 6 4
      web/app/components/app/configuration/dataset-config/settings-modal/index.tsx
  54. 109 0
      web/app/components/app/configuration/debug/chat-user-input.tsx
  55. 26 6
      web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx
  56. 28 17
      web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx
  57. 10 15
      web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx
  58. 37 8
      web/app/components/app/configuration/debug/debug-with-single-model/index.tsx
  59. 119 86
      web/app/components/app/configuration/debug/index.tsx
  60. 0 25
      web/app/components/app/configuration/features/chat-group/citation/index.tsx
  61. 0 65
      web/app/components/app/configuration/features/chat-group/index.tsx
  62. 0 25
      web/app/components/app/configuration/features/chat-group/speech-to-text/index.tsx
  63. 0 34
      web/app/components/app/configuration/features/chat-group/suggested-questions-after-answer/index.tsx
  64. 0 55
      web/app/components/app/configuration/features/chat-group/text-to-speech/index.tsx
  65. 229 174
      web/app/components/app/configuration/index.tsx
  66. 126 164
      web/app/components/app/configuration/prompt-value-panel/index.tsx
  67. 13 0
      web/app/components/app/configuration/prompt-value-panel/utils.ts
  68. 0 80
      web/app/components/app/configuration/toolbox/moderation/index.tsx
  69. 0 46
      web/app/components/app/configuration/toolbox/score-slider/index.tsx
  70. 1 1
      web/app/components/app/configuration/tools/external-data-tool-modal.tsx
  71. 6 4
      web/app/components/app/create-app-modal/index.tsx
  72. 2 2
      web/app/components/app/create-from-dsl-modal/index.tsx
  73. 3 2
      web/app/components/app/duplicate-modal/index.tsx
  74. 2 2
      web/app/components/app/log-annotation/index.tsx
  75. 38 40
      web/app/components/app/log/filter.tsx
  76. 5 5
      web/app/components/app/log/index.tsx
  77. 38 30
      web/app/components/app/log/list.tsx
  78. 0 6
      web/app/components/app/log/style.module.css
  79. 19 9
      web/app/components/app/overview/settings/index.tsx
  80. 0 5
      web/app/components/app/overview/settings/style.module.css
  81. 4 0
      web/app/components/app/store.ts
  82. 6 4
      web/app/components/app/switch-app-modal/index.tsx
  83. 2 2
      web/app/components/app/text-generate/item/index.tsx
  84. 21 10
      web/app/components/app/text-generate/item/result-tab.tsx
  85. 3 3
      web/app/components/app/workflow-log/detail.tsx
  86. 26 38
      web/app/components/app/workflow-log/filter.tsx
  87. 6 6
      web/app/components/app/workflow-log/index.tsx
  88. 34 30
      web/app/components/app/workflow-log/list.tsx
  89. 0 6
      web/app/components/app/workflow-log/style.module.css
  90. 2 2
      web/app/components/base/button/add-button.tsx
  91. 27 33
      web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
  92. 3 2
      web/app/components/base/chat/chat-with-history/config-panel/form-input.tsx
  93. 34 4
      web/app/components/base/chat/chat-with-history/config-panel/form.tsx
  94. 2 0
      web/app/components/base/chat/chat-with-history/context.tsx
  95. 48 14
      web/app/components/base/chat/chat-with-history/hooks.tsx
  96. 2 0
      web/app/components/base/chat/chat-with-history/index.tsx
  97. 13 16
      web/app/components/base/chat/chat/answer/agent-content.tsx
  98. 11 2
      web/app/components/base/chat/chat/answer/basic-content.tsx
  99. 25 6
      web/app/components/base/chat/chat/answer/index.tsx
  100. 0 0
      web/app/components/base/chat/chat/answer/operation.tsx

+ 1 - 1
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/annotations/page.tsx

@@ -1,6 +1,6 @@
 import React from 'react'
 import Main from '@/app/components/app/log-annotation'
-import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
+import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
 
 export type IProps = {
   params: { appId: string }

+ 1 - 1
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/logs/page.tsx

@@ -1,6 +1,6 @@
 import React from 'react'
 import Main from '@/app/components/app/log-annotation'
-import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
+import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
 
 const Logs = async () => {
   return (

+ 3 - 3
web/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/field.tsx

@@ -2,6 +2,7 @@
 import type { FC } from 'react'
 import React from 'react'
 import cn from '@/utils/classnames'
+import Input from '@/app/components/base/input'
 
 type Props = {
   className?: string
@@ -28,11 +29,10 @@ const Field: FC<Props> = ({
         <div className={cn(labelClassName, 'flex items-center h-[18px] text-[13px] font-medium text-gray-900')}>{label} </div>
         {isRequired && <span className='ml-0.5 text-xs font-semibold text-[#D92D20]'>*</span>}
       </div>
-      <input
-        type='text'
+      <Input
         value={value}
         onChange={e => onChange(e.target.value)}
-        className='flex h-9 w-full py-1 px-2 rounded-lg text-xs leading-normal bg-gray-100 caret-primary-600 hover:bg-gray-100 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none focus:bg-gray-50 placeholder:text-gray-400'
+        className='h-9'
         placeholder={placeholder}
       />
     </div>

+ 9 - 2
web/app/(commonLayout)/apps/Apps.tsx

@@ -21,7 +21,7 @@ import { NEED_REFRESH_APP_LIST_KEY } from '@/config'
 import { CheckModal } from '@/hooks/use-pay'
 import TabSliderNew from '@/app/components/base/tab-slider-new'
 import { useTabSearchParams } from '@/hooks/use-tab-searchparams'
-import SearchInput from '@/app/components/base/search-input'
+import Input from '@/app/components/base/input'
 import { useStore as useTagStore } from '@/app/components/base/tag-management/store'
 import TagManagementModal from '@/app/components/base/tag-management'
 import TagFilter from '@/app/components/base/tag-management/filter'
@@ -133,7 +133,14 @@ const Apps = () => {
         />
         <div className='flex items-center gap-2'>
           <TagFilter type='app' value={tagFilterValue} onChange={handleTagsChange} />
-          <SearchInput className='w-[200px]' value={keywords} onChange={handleKeywordsChange} />
+          <Input
+            showLeftIcon
+            showClearIcon
+            wrapperClassName='w-[200px]'
+            value={keywords}
+            onChange={e => handleKeywordsChange(e.target.value)}
+            onClear={() => handleKeywordsChange('')}
+          />
         </div>
       </div>
       <nav className='grid content-start grid-cols-1 gap-4 px-12 pt-2 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 grow shrink-0'>

+ 1 - 1
web/app/(commonLayout)/datasets/Container.tsx

@@ -14,11 +14,11 @@ import DatasetFooter from './DatasetFooter'
 import ApiServer from './ApiServer'
 import Doc from './Doc'
 import TabSliderNew from '@/app/components/base/tab-slider-new'
-import SearchInput from '@/app/components/base/search-input'
 import TagManagementModal from '@/app/components/base/tag-management'
 import TagFilter from '@/app/components/base/tag-management/filter'
 import Button from '@/app/components/base/button'
 import { ApiConnectionMod } from '@/app/components/base/icons/src/vender/solid/development'
+import SearchInput from '@/app/components/base/search-input'
 
 // Services
 import { fetchDatasetApiBaseUrl } from '@/service/datasets'

+ 3 - 3
web/app/components/app/annotation/empty-element.tsx

@@ -14,9 +14,9 @@ const EmptyElement: FC = () => {
 
   return (
     <div className='flex items-center justify-center h-full'>
-      <div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
-        <span className='text-gray-700 font-semibold'>{t('appAnnotation.noData.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
-        <div className='mt-2 text-gray-500 text-sm font-normal'>
+      <div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
+        <span className='text-text-secondary system-md-semibold'>{t('appAnnotation.noData.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
+        <div className='mt-2 text-text-tertiary system-sm-regular'>
           {t('appAnnotation.noData.description')}
         </div>
       </div>

+ 13 - 19
web/app/components/app/annotation/filter.tsx

@@ -2,10 +2,8 @@
 import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
-import {
-  MagnifyingGlassIcon,
-} from '@heroicons/react/24/solid'
 import useSWR from 'swr'
+import Input from '@/app/components/base/input'
 import { fetchAnnotationsCount } from '@/service/log'
 
 export type QueryParam = {
@@ -31,22 +29,18 @@ const Filter: FC<IFilterProps> = ({
   if (!data)
     return null
   return (
-    <div className='flex justify-between flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
-      <div className="relative">
-        <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
-          <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
-        </div>
-        <input
-          type="text"
-          name="query"
-          className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
-          placeholder={t('common.operation.search') as string}
-          value={queryParams.keyword}
-          onChange={(e) => {
-            setQueryParams({ ...queryParams, keyword: e.target.value })
-          }}
-        />
-      </div>
+    <div className='flex justify-between flex-row flex-wrap gap-2 items-center mb-2'>
+      <Input
+        wrapperClassName='w-[200px]'
+        showLeftIcon
+        showClearIcon
+        value={queryParams.keyword}
+        placeholder={t('common.operation.search')!}
+        onChange={(e) => {
+          setQueryParams({ ...queryParams, keyword: e.target.value })
+        }}
+        onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
+      />
       {children}
     </div>
   )

+ 3 - 3
web/app/components/app/annotation/index.tsx

@@ -19,7 +19,7 @@ import Switch from '@/app/components/base/switch'
 import { addAnnotation, delAnnotation, fetchAnnotationConfig as doFetchAnnotationConfig, editAnnotation, fetchAnnotationList, queryAnnotationJobStatus, updateAnnotationScore, updateAnnotationStatus } from '@/service/annotation'
 import Loading from '@/app/components/base/loading'
 import { APP_PAGE_LIMIT } from '@/config'
-import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
+import ConfigParamModal from '@/app/components/base/features/new-feature-panel/annotation-reply/config-param-modal'
 import type { AnnotationReplyConfig } from '@/models/debug'
 import { sleep } from '@/utils'
 import { useProviderContext } from '@/context/provider-context'
@@ -152,8 +152,8 @@ const Annotation: FC<Props> = ({
 
   return (
     <div className='flex flex-col h-full'>
-      <p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
-      <div className='grow flex flex-col py-4 '>
+      <p className='text-text-tertiary system-sm-regular'>{t('appLog.description')}</p>
+      <div className='flex flex-col py-4 flex-1'>
         <Filter appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams}>
           <div className='flex items-center space-x-2'>
             {isChatApp && (

+ 15 - 16
web/app/components/app/annotation/list.tsx

@@ -4,7 +4,6 @@ import React from 'react'
 import { useTranslation } from 'react-i18next'
 import { RiDeleteBinLine } from '@remixicon/react'
 import { Edit02 } from '../../base/icons/src/vender/line/general'
-import s from './style.module.css'
 import type { AnnotationItem } from './type'
 import RemoveAnnotationConfirmModal from './remove-annotation-confirm-modal'
 import cn from '@/utils/classnames'
@@ -27,21 +26,21 @@ const List: FC<Props> = ({
   const [showConfirmDelete, setShowConfirmDelete] = React.useState(false)
   return (
     <div className='overflow-x-auto'>
-      <table className={cn(s.logTable, 'w-full min-w-[440px] border-collapse border-0 text-sm')} >
-        <thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
-          <tr className='uppercase'>
-            <td className='whitespace-nowrap'>{t('appAnnotation.table.header.question')}</td>
-            <td className='whitespace-nowrap'>{t('appAnnotation.table.header.answer')}</td>
-            <td className='whitespace-nowrap'>{t('appAnnotation.table.header.createdAt')}</td>
-            <td className='whitespace-nowrap'>{t('appAnnotation.table.header.hits')}</td>
-            <td className='whitespace-nowrap w-[96px]'>{t('appAnnotation.table.header.actions')}</td>
+      <table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
+        <thead className='system-xs-medium-uppercase text-text-tertiary'>
+          <tr>
+            <td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.question')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.answer')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.createdAt')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appAnnotation.table.header.hits')}</td>
+            <td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap w-[96px]'>{t('appAnnotation.table.header.actions')}</td>
           </tr>
         </thead>
-        <tbody className="text-gray-500">
+        <tbody className="text-text-secondary system-sm-regular">
           {list.map(item => (
             <tr
               key={item.id}
-              className={'border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer'}
+              className='border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer'
               onClick={
                 () => {
                   onView(item)
@@ -49,16 +48,16 @@ const List: FC<Props> = ({
               }
             >
               <td
-                className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
+                className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
                 title={item.question}
               >{item.question}</td>
               <td
-                className='whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
+                className='p-3 pr-2 whitespace-nowrap overflow-hidden text-ellipsis max-w-[250px]'
                 title={item.answer}
               >{item.answer}</td>
-              <td>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
-              <td>{item.hit_count}</td>
-              <td className='w-[96px]' onClick={e => e.stopPropagation()}>
+              <td className='p-3 pr-2'>{formatTime(item.created_at, t('appLog.dateTimeFormat') as string)}</td>
+              <td className='p-3 pr-2'>{item.hit_count}</td>
+              <td className='w-[96px] p-3 pr-2' onClick={e => e.stopPropagation()}>
                 {/* Actions */}
                 <div className='flex space-x-2 text-gray-500'>
                   <div

+ 0 - 6
web/app/components/app/annotation/style.module.css

@@ -1,9 +1,3 @@
-.logTable td {
-  padding: 7px 8px;
-  box-sizing: border-box;
-  max-width: 200px;
-}
-
 .pagination li {
   list-style: none;
 }

+ 86 - 0
web/app/components/app/app-publisher/features-wrapper.tsx

@@ -0,0 +1,86 @@
+import React, { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import produce from 'immer'
+import type { AppPublisherProps } from '@/app/components/app/app-publisher'
+import Confirm from '@/app/components/base/confirm'
+import AppPublisher from '@/app/components/app/app-publisher'
+import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
+import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
+import type { FileUpload } from '@/app/components/base/features/types'
+import { Resolution } from '@/types/app'
+import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
+import { SupportUploadFileTypes } from '@/app/components/workflow/types'
+
+type Props = Omit<AppPublisherProps, 'onPublish'> & {
+  onPublish?: (modelAndParameter?: ModelAndParameter, features?: any) => Promise<any> | any
+  publishedConfig?: any
+  resetAppConfig?: () => void
+}
+
+const FeaturesWrappedAppPublisher = (props: Props) => {
+  const { t } = useTranslation()
+  const features = useFeatures(s => s.features)
+  const featuresStore = useFeaturesStore()
+  const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
+  const handleConfirm = useCallback(() => {
+    props.resetAppConfig?.()
+    const {
+      features,
+      setFeatures,
+    } = featuresStore!.getState()
+    const newFeatures = produce(features, (draft) => {
+      draft.moreLikeThis = props.publishedConfig.modelConfig.more_like_this || { enabled: false }
+      draft.opening = {
+        enabled: !!props.publishedConfig.modelConfig.opening_statement,
+        opening_statement: props.publishedConfig.modelConfig.opening_statement || '',
+        suggested_questions: props.publishedConfig.modelConfig.suggested_questions || [],
+      }
+      draft.moderation = props.publishedConfig.modelConfig.sensitive_word_avoidance || { enabled: false }
+      draft.speech2text = props.publishedConfig.modelConfig.speech_to_text || { enabled: false }
+      draft.text2speech = props.publishedConfig.modelConfig.text_to_speech || { enabled: false }
+      draft.suggested = props.publishedConfig.modelConfig.suggested_questions_after_answer || { enabled: false }
+      draft.citation = props.publishedConfig.modelConfig.retriever_resource || { enabled: false }
+      draft.annotationReply = props.publishedConfig.modelConfig.annotation_reply || { enabled: false }
+      draft.file = {
+        image: {
+          detail: props.publishedConfig.modelConfig.file_upload?.image?.detail || Resolution.high,
+          enabled: !!props.publishedConfig.modelConfig.file_upload?.image?.enabled,
+          number_limits: props.publishedConfig.modelConfig.file_upload?.image?.number_limits || 3,
+          transfer_methods: props.publishedConfig.modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
+        },
+        enabled: !!(props.publishedConfig.modelConfig.file_upload?.enabled || props.publishedConfig.modelConfig.file_upload?.image?.enabled),
+        allowed_file_types: props.publishedConfig.modelConfig.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
+        allowed_file_extensions: props.publishedConfig.modelConfig.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
+        allowed_file_upload_methods: props.publishedConfig.modelConfig.file_upload?.allowed_file_upload_methods || props.publishedConfig.modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
+        number_limits: props.publishedConfig.modelConfig.file_upload?.number_limits || props.publishedConfig.modelConfig.file_upload?.image?.number_limits || 3,
+      } as FileUpload
+    })
+    setFeatures(newFeatures)
+    setRestoreConfirmOpen(false)
+  }, [featuresStore, props])
+
+  const handlePublish = useCallback((modelAndParameter?: ModelAndParameter) => {
+    return props.onPublish?.(modelAndParameter, features)
+  }, [features, props])
+
+  return (
+    <>
+      <AppPublisher {...{
+        ...props,
+        onPublish: handlePublish,
+        onRestore: () => setRestoreConfirmOpen(true),
+      }}/>
+      {restoreConfirmOpen && (
+        <Confirm
+          title={t('appDebug.resetConfig.title')}
+          content={t('appDebug.resetConfig.message')}
+          isShow={restoreConfirmOpen}
+          onConfirm={handleConfirm}
+          onCancel={() => setRestoreConfirmOpen(false)}
+        />
+      )}
+    </>
+  )
+}
+
+export default FeaturesWrappedAppPublisher

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

@@ -2,7 +2,6 @@
 import type { FC, ReactNode } from 'react'
 import React from 'react'
 import cn from '@/utils/classnames'
-import ParamsConfig from '@/app/components/app/configuration/config-voice/param-config'
 
 export type IFeaturePanelProps = {
   className?: string
@@ -10,10 +9,8 @@ export type IFeaturePanelProps = {
   title: ReactNode
   headerRight?: ReactNode
   hasHeaderBottomBorder?: boolean
-  isFocus?: boolean
   noBodySpacing?: boolean
   children?: ReactNode
-  isShowTextToSpeech?: boolean
 }
 
 const FeaturePanel: FC<IFeaturePanelProps> = ({
@@ -22,32 +19,20 @@ const FeaturePanel: FC<IFeaturePanelProps> = ({
   title,
   headerRight,
   hasHeaderBottomBorder,
-  isFocus,
   noBodySpacing,
   children,
-  isShowTextToSpeech,
 }) => {
   return (
-    <div
-      className={cn(className, isFocus && 'border border-[#2D0DEE]', 'rounded-xl bg-gray-50 pt-2 pb-3', noBodySpacing && '!pb-0')}
-      style={isFocus
-        ? {
-          boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
-        }
-        : {}}
-    >
+    <div className={cn('rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn pb-3', noBodySpacing && '!pb-0', className)}>
       {/* Header */}
-      <div className={cn('pb-2 px-3', hasHeaderBottomBorder && 'border-b border-gray-100')}>
+      <div className={cn('px-3 pt-2', hasHeaderBottomBorder && 'border-b border-divider-subtle')}>
         <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-6 h-6'>{headerIcon}</div>}
-            <div className='text-sm font-semibold text-gray-800'>{title}</div>
+            <div className='text-text-secondary system-sm-semibold'>{title}</div>
           </div>
           <div className='flex gap-2 items-center'>
             {headerRight && <div>{headerRight}</div>}
-            {isShowTextToSpeech && <div className='flex items-center'>
-              <ParamsConfig />
-            </div>}
           </div>
         </div>
       </div>

+ 1 - 1
web/app/components/app/configuration/config-prompt/conversation-history/history-panel.tsx

@@ -23,7 +23,7 @@ const HistoryPanel: FC<Props> = ({
 
   return (
     <Panel
-      className='mt-3'
+      className='mt-2'
       title={
         <div className='flex items-center gap-2'>
           <div>{t('appDebug.feature.conversationHistory.title')}</div>

+ 5 - 2
web/app/components/app/configuration/config-var/config-modal/field.tsx

@@ -1,19 +1,22 @@
 'use client'
 import type { FC } from 'react'
 import React from 'react'
+import cn from '@/utils/classnames'
 
 type Props = {
+  className?: string
   title: string
   children: JSX.Element
 }
 
 const Field: FC<Props> = ({
+  className,
   title,
   children,
 }) => {
   return (
-    <div>
-      <div className='leading-8 text-[13px] font-medium text-gray-700'>{title}</div>
+    <div className={cn(className)}>
+      <div className='text-text-secondary system-sm-semibold leading-8'>{title}</div>
       <div>{children}</div>
     </div>
   )

+ 72 - 23
web/app/components/app/configuration/config-var/config-modal/index.tsx

@@ -3,18 +3,23 @@ import type { FC } from 'react'
 import React, { useCallback, useEffect, useRef, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
+import produce from 'immer'
 import ModalFoot from '../modal-foot'
 import ConfigSelect from '../config-select'
 import ConfigString from '../config-string'
 import SelectTypeItem from '../select-type-item'
 import Field from './field'
+import Input from '@/app/components/base/input'
 import Toast from '@/app/components/base/toast'
 import { checkKeys, getNewVarInWorkflow } from '@/utils/var'
 import ConfigContext from '@/context/debug-configuration'
-import type { InputVar, MoreInfo } from '@/app/components/workflow/types'
+import type { InputVar, MoreInfo, UploadFileSetting } from '@/app/components/workflow/types'
 import Modal from '@/app/components/base/modal'
-import Switch from '@/app/components/base/switch'
-import { ChangeType, InputVarType } from '@/app/components/workflow/types'
+import { ChangeType, InputVarType, SupportUploadFileTypes } from '@/app/components/workflow/types'
+import FileUploadSetting from '@/app/components/workflow/nodes/_base/components/file-upload-setting'
+import Checkbox from '@/app/components/base/checkbox'
+import { DEFAULT_FILE_UPLOAD_SETTING } from '@/app/components/workflow/constants'
+import { DEFAULT_VALUE_MAX_LEN } from '@/config'
 
 const TEXT_MAX_LENGTH = 256
 
@@ -25,16 +30,16 @@ export type IConfigModalProps = {
   varKeys?: string[]
   onClose: () => void
   onConfirm: (newValue: InputVar, moreInfo?: MoreInfo) => void
+  supportFile?: boolean
 }
 
-const inputClassName = 'w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200'
-
 const ConfigModal: FC<IConfigModalProps> = ({
   isCreate,
   payload,
   isShow,
   onClose,
   onConfirm,
+  supportFile,
 }) => {
   const { modelConfig } = useContext(ConfigContext)
   const { t } = useTranslation()
@@ -48,8 +53,8 @@ const ConfigModal: FC<IConfigModalProps> = ({
   }, [isShow])
 
   const isStringInput = type === InputVarType.textInput || type === InputVarType.paragraph
-  const checkVariableName = useCallback((value: string) => {
-    const { isValid, errorMessageKey } = checkKeys([value], false)
+  const checkVariableName = useCallback((value: string, canBeEmpty?: boolean) => {
+    const { isValid, errorMessageKey } = checkKeys([value], canBeEmpty)
     if (!isValid) {
       Toast.notify({
         type: 'error',
@@ -72,9 +77,28 @@ const ConfigModal: FC<IConfigModalProps> = ({
     }
   }, [])
 
+  const handleTypeChange = useCallback((type: InputVarType) => {
+    return () => {
+      const newPayload = produce(tempPayload, (draft) => {
+        draft.type = type
+        if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
+          (Object.keys(DEFAULT_FILE_UPLOAD_SETTING)).forEach((key) => {
+            if (key !== 'max_length')
+              (draft as any)[key] = (DEFAULT_FILE_UPLOAD_SETTING as any)[key]
+          })
+          if (type === InputVarType.multiFiles)
+            draft.max_length = DEFAULT_FILE_UPLOAD_SETTING.max_length
+        }
+        if (type === InputVarType.paragraph)
+          draft.max_length = DEFAULT_VALUE_MAX_LEN
+      })
+      setTempPayload(newPayload)
+    }
+  }, [tempPayload])
+
   const handleVarKeyBlur = useCallback((e: any) => {
     const varName = e.target.value
-    if (!checkVariableName(varName) || tempPayload.label)
+    if (!checkVariableName(varName, true) || tempPayload.label)
       return
 
     setTempPayload((prev) => {
@@ -113,7 +137,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
     if (isStringInput || type === InputVarType.number) {
       onConfirm(tempPayload, moreInfo)
     }
-    else {
+    else if (type === InputVarType.select) {
       if (options?.length === 0) {
         Toast.notify({ type: 'error', message: t('appDebug.variableConfig.errorMsg.atLeastOneOption') })
         return
@@ -133,6 +157,22 @@ const ConfigModal: FC<IConfigModalProps> = ({
       }
       onConfirm(tempPayload, moreInfo)
     }
+    else if ([InputVarType.singleFile, InputVarType.multiFiles].includes(type)) {
+      if (tempPayload.allowed_file_types?.length === 0) {
+        const errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('appDebug.variableConfig.file.supportFileTypes') })
+        Toast.notify({ type: 'error', message: errorMessages })
+        return
+      }
+      if (tempPayload.allowed_file_types?.includes(SupportUploadFileTypes.custom) && !tempPayload.allowed_file_extensions?.length) {
+        const errorMessages = t('workflow.errorMsg.fieldRequired', { field: t('appDebug.variableConfig.file.custom.name') })
+        Toast.notify({ type: 'error', message: errorMessages })
+        return
+      }
+      onConfirm(tempPayload, moreInfo)
+    }
+    else {
+      onConfirm(tempPayload, moreInfo)
+    }
   }
 
   return (
@@ -145,18 +185,20 @@ const ConfigModal: FC<IConfigModalProps> = ({
         <div className='space-y-2'>
 
           <Field title={t('appDebug.variableConfig.fieldType')}>
-            <div className='flex space-x-2'>
-              <SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={() => handlePayloadChange('type')(InputVarType.textInput)} />
-              <SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={() => handlePayloadChange('type')(InputVarType.paragraph)} />
-              <SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={() => handlePayloadChange('type')(InputVarType.select)} />
-              <SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={() => handlePayloadChange('type')(InputVarType.number)} />
+            <div className='grid grid-cols-3 gap-2'>
+              <SelectTypeItem type={InputVarType.textInput} selected={type === InputVarType.textInput} onClick={handleTypeChange(InputVarType.textInput)} />
+              <SelectTypeItem type={InputVarType.paragraph} selected={type === InputVarType.paragraph} onClick={handleTypeChange(InputVarType.paragraph)} />
+              <SelectTypeItem type={InputVarType.select} selected={type === InputVarType.select} onClick={handleTypeChange(InputVarType.select)} />
+              <SelectTypeItem type={InputVarType.number} selected={type === InputVarType.number} onClick={handleTypeChange(InputVarType.number)} />
+              {supportFile && <>
+                <SelectTypeItem type={InputVarType.singleFile} selected={type === InputVarType.singleFile} onClick={handleTypeChange(InputVarType.singleFile)} />
+                <SelectTypeItem type={InputVarType.multiFiles} selected={type === InputVarType.multiFiles} onClick={handleTypeChange(InputVarType.multiFiles)} />
+              </>}
             </div>
           </Field>
 
           <Field title={t('appDebug.variableConfig.varName')}>
-            <input
-              type='text'
-              className={inputClassName}
+            <Input
               value={variable}
               onChange={e => handlePayloadChange('variable')(e.target.value)}
               onBlur={handleVarKeyBlur}
@@ -164,9 +206,7 @@ const ConfigModal: FC<IConfigModalProps> = ({
             />
           </Field>
           <Field title={t('appDebug.variableConfig.labelName')}>
-            <input
-              type='text'
-              className={inputClassName}
+            <Input
               value={label as string}
               onChange={e => handlePayloadChange('label')(e.target.value)}
               placeholder={t('appDebug.variableConfig.inputPlaceholder')!}
@@ -185,9 +225,18 @@ const ConfigModal: FC<IConfigModalProps> = ({
             </Field>
           )}
 
-          <Field title={t('appDebug.variableConfig.required')}>
-            <Switch defaultValue={tempPayload.required} onChange={handlePayloadChange('required')} />
-          </Field>
+          {[InputVarType.singleFile, InputVarType.multiFiles].includes(type) && (
+            <FileUploadSetting
+              payload={tempPayload as UploadFileSetting}
+              onChange={(p: UploadFileSetting) => setTempPayload(p as InputVar)}
+              isMultiple={type === InputVarType.multiFiles}
+            />
+          )}
+
+          <div className='!mt-5 flex items-center h-6 space-x-2'>
+            <Checkbox checked={tempPayload.required} onCheck={() => handlePayloadChange('required')(!tempPayload.required)} />
+            <span className='text-text-secondary system-sm-semibold'>{t('appDebug.variableConfig.required')}</span>
+          </div>
         </div>
       </div>
       <ModalFoot

+ 2 - 2
web/app/components/app/configuration/config-var/config-string/index.tsx

@@ -1,6 +1,7 @@
 'use client'
 import type { FC } from 'react'
 import React, { useEffect } from 'react'
+import Input from '@/app/components/base/input'
 
 export type IConfigStringProps = {
   value: number | undefined
@@ -21,7 +22,7 @@ const ConfigString: FC<IConfigStringProps> = ({
 
   return (
     <div>
-      <input
+      <Input
         type="number"
         max={maxLength}
         min={1}
@@ -36,7 +37,6 @@ const ConfigString: FC<IConfigStringProps> = ({
 
           onChange(value)
         }}
-        className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-100 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
       />
     </div>
   )

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

@@ -272,7 +272,7 @@ const ConfigVar: FC<IConfigVarProps> = ({ promptVariables, readonly, onPromptVar
   }
   return (
     <Panel
-      className="mt-4"
+      className="mt-2"
       headerIcon={
         <VarIcon className='w-4 h-4 text-primary-500' />
       }

+ 10 - 4
web/app/components/app/configuration/config-var/select-type-item/index.tsx

@@ -2,7 +2,6 @@
 import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
-import s from './style.module.css'
 import cn from '@/utils/classnames'
 import type { InputVarType } from '@/app/components/workflow/types'
 import InputVarTypeIcon from '@/app/components/workflow/nodes/_base/components/input-var-type-icon'
@@ -12,23 +11,30 @@ export type ISelectTypeItemProps = {
   onClick: () => void
 }
 
+const i18nFileTypeMap: Record<string, string> = {
+  'file': 'single-file',
+  'file-list': 'multi-files',
+}
+
 const SelectTypeItem: FC<ISelectTypeItemProps> = ({
   type,
   selected,
   onClick,
 }) => {
   const { t } = useTranslation()
-  const typeName = t(`appDebug.variableConfig.${type}`)
+  const typeName = t(`appDebug.variableConfig.${i18nFileTypeMap[type] || type}`)
 
   return (
     <div
-      className={cn(s.item, selected && s.selected, 'space-y-1')}
+      className={cn(
+        'flex flex-col justify-center items-center h-[58px] rounded-lg border border-components-option-card-option-border bg-components-option-card-option-bg space-y-1',
+        selected ? 'border-[1.5px] border-components-option-card-option-selected-border bg-components-option-card-option-selected-bg shadow-xs system-xs-medium' : ' hover:border-components-option-card-option-border-hover hover:bg-components-option-card-option-bg-hover hover:shadow-xs cursor-pointer system-xs-regular')}
       onClick={onClick}
     >
       <div className='shrink-0'>
         <InputVarTypeIcon type={type} className='w-5 h-5' />
       </div>
-      <span className={cn(s.text)}>{typeName}</span>
+      <span>{typeName}</span>
     </div>
   )
 }

+ 0 - 40
web/app/components/app/configuration/config-var/select-type-item/style.module.css

@@ -1,40 +0,0 @@
-.item {
-  display: flex;
-  flex-direction: column;
-  justify-content: center;
-  align-items: center;
-  height: 58px;
-  width: 98px;
-  border-radius: 8px;
-  border: 1px solid #EAECF0;
-  box-shadow: 0px 1px 2px rgba(16, 24, 40, 0.05);
-  background-color: #fff;
-  cursor: pointer;
-}
-
-.item:not(.selected):hover {
-  border-color: #B2CCFF;
-  background-color: #F5F8FF;
-  box-shadow: 0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06);
-}
-
-.item.selected {
-  color: #155EEF;
-  border-color: #528BFF;
-  background-color: #F5F8FF;
-  box-shadow: 0px 1px 3px rgba(16, 24, 40, 0.1), 0px 1px 2px rgba(16, 24, 40, 0.06);
-}
-
-.text {
-  font-size: 13px;
-  color: #667085;
-  font-weight: 500;
-}
-
-.item.selected .text {
-  color: #155EEF;
-}
-
-.item:not(.selected):hover {
-  color: #344054;
-}

+ 79 - 37
web/app/components/app/configuration/config-vision/index.tsx

@@ -1,61 +1,103 @@
 'use client'
 import type { FC } from 'react'
-import React from 'react'
+import React, { useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
+import produce from 'immer'
 import { useContext } from 'use-context-selector'
-import Panel from '../base/feature-panel'
 import ParamConfig from './param-config'
+import { Vision } from '@/app/components/base/icons/src/vender/features'
 import Tooltip from '@/app/components/base/tooltip'
-import Switch from '@/app/components/base/switch'
-import { Eye } from '@/app/components/base/icons/src/vender/solid/general'
+// import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
 import ConfigContext from '@/context/debug-configuration'
+// import { Resolution } from '@/types/app'
+import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
+import Switch from '@/app/components/base/switch'
+import type { FileUpload } from '@/app/components/base/features/types'
 
 const ConfigVision: FC = () => {
   const { t } = useTranslation()
-  const {
-    isShowVisionConfig,
-    visionConfig,
-    setVisionConfig,
-  } = useContext(ConfigContext)
+  const { isShowVisionConfig } = useContext(ConfigContext)
+  const file = useFeatures(s => s.features.file)
+  const featuresStore = useFeaturesStore()
+
+  const handleChange = useCallback((data: FileUpload) => {
+    const {
+      features,
+      setFeatures,
+    } = featuresStore!.getState()
+
+    const newFeatures = produce(features, (draft) => {
+      draft.file = {
+        ...draft.file,
+        enabled: data.enabled,
+        image: {
+          enabled: data.enabled,
+          detail: data.image?.detail,
+          transfer_methods: data.image?.transfer_methods,
+          number_limits: data.image?.number_limits,
+        },
+      }
+    })
+    setFeatures(newFeatures)
+  }, [featuresStore])
 
   if (!isShowVisionConfig)
     return null
 
-  return (<>
-    <Panel
-      className="mt-4"
-      headerIcon={
-        <Eye className='w-4 h-4 text-[#6938EF]'/>
-      }
-      title={
-        <div className='flex items-center'>
-          <div className='mr-1'>{t('appDebug.vision.name')}</div>
+  return (
+    <div className='mt-2 flex items-center gap-2 p-2 rounded-xl border-t-[0.5px] border-l-[0.5px] bg-background-section-burn'>
+      <div className='shrink-0 p-1'>
+        <div className='p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-indigo-indigo-600'>
+          <Vision className='w-4 h-4 text-text-primary-on-surface' />
+        </div>
+      </div>
+      <div className='grow flex items-center'>
+        <div className='mr-1 text-text-secondary system-sm-semibold'>{t('appDebug.vision.name')}</div>
+        <Tooltip
+          popupContent={
+            <div className='w-[180px]' >
+              {t('appDebug.vision.description')}
+            </div>
+          }
+        />
+      </div>
+      <div className='shrink-0 flex items-center'>
+        {/* <div className='mr-2 flex items-center gap-0.5'>
+          <div className='text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.vision.visionSettings.resolution')}</div>
           <Tooltip
             popupContent={
               <div className='w-[180px]' >
-                {t('appDebug.vision.description')}
+                {t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
+                  <div key={item}>{item}</div>
+                ))}
               </div>
             }
           />
-        </div>
-      }
-      headerRight={
-        <div className='flex items-center'>
-          <ParamConfig />
-          <div className='ml-4 mr-3 w-[1px] h-3.5 bg-gray-200'></div>
-          <Switch
-            defaultValue={visionConfig.enabled}
-            onChange={value => setVisionConfig({
-              ...visionConfig,
-              enabled: value,
-            })}
-            size='md'
+        </div> */}
+        {/* <div className='flex items-center gap-1'>
+          <OptionCard
+            title={t('appDebug.vision.visionSettings.high')}
+            selected={file?.image?.detail === Resolution.high}
+            onSelect={() => handleChange(Resolution.high)}
           />
-        </div>
-      }
-      noBodySpacing
-    />
-  </>
+          <OptionCard
+            title={t('appDebug.vision.visionSettings.low')}
+            selected={file?.image?.detail === Resolution.low}
+            onSelect={() => handleChange(Resolution.low)}
+          />
+        </div> */}
+        <ParamConfig />
+        <div className='ml-1 mr-3 w-[1px] h-3.5 bg-divider-subtle'></div>
+        <Switch
+          defaultValue={file?.enabled}
+          onChange={value => handleChange({
+            ...(file || {}),
+            enabled: value,
+          })}
+          size='md'
+        />
+      </div>
+    </div>
   )
 }
 export default React.memo(ConfigVision)

+ 113 - 104
web/app/components/app/configuration/config-vision/param-config-content.tsx

@@ -1,130 +1,139 @@
 'use client'
 import type { FC } from 'react'
-import React from 'react'
-import { useContext } from 'use-context-selector'
+import React, { useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
-import RadioGroup from './radio-group'
-import ConfigContext from '@/context/debug-configuration'
+import produce from 'immer'
+import OptionCard from '@/app/components/workflow/nodes/_base/components/option-card'
 import { Resolution, TransferMethod } from '@/types/app'
 import ParamItem from '@/app/components/base/param-item'
 import Tooltip from '@/app/components/base/tooltip'
+import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
+import type { FileUpload } from '@/app/components/base/features/types'
 
 const MIN = 1
 const MAX = 6
 const ParamConfigContent: FC = () => {
   const { t } = useTranslation()
+  const file = useFeatures(s => s.features.file)
+  const featuresStore = useFeaturesStore()
 
-  const {
-    visionConfig,
-    setVisionConfig,
-  } = useContext(ConfigContext)
+  const handleChange = useCallback((data: FileUpload) => {
+    const {
+      features,
+      setFeatures,
+    } = featuresStore!.getState()
 
-  const transferMethod = (() => {
-    if (!visionConfig.transfer_methods || visionConfig.transfer_methods.length === 2)
-      return TransferMethod.all
-
-    return visionConfig.transfer_methods[0]
-  })()
+    const newFeatures = produce(features, (draft) => {
+      draft.file = {
+        ...draft.file,
+        allowed_file_upload_methods: data.allowed_file_upload_methods,
+        number_limits: data.number_limits,
+        image: {
+          enabled: data.enabled,
+          detail: data.image?.detail,
+          transfer_methods: data.allowed_file_upload_methods,
+          number_limits: data.number_limits,
+        },
+      }
+    })
+    setFeatures(newFeatures)
+  }, [featuresStore])
 
   return (
     <div>
-      <div>
-        <div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
-        <div className='pt-3 space-y-6'>
-          <div>
-            <div className='mb-2 flex items-center  space-x-1'>
-              <div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.resolution')}</div>
-              <Tooltip
-                popupContent={
-                  <div className='w-[180px]' >
-                    {t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
-                      <div key={item}>{item}</div>
-                    ))}
-                  </div>
-                }
-              />
-            </div>
-            <RadioGroup
-              className='space-x-3'
-              options={[
-                {
-                  label: t('appDebug.vision.visionSettings.high'),
-                  value: Resolution.high,
-                },
-                {
-                  label: t('appDebug.vision.visionSettings.low'),
-                  value: Resolution.low,
-                },
-              ]}
-              value={visionConfig.detail}
-              onChange={(value: Resolution) => {
-                setVisionConfig({
-                  ...visionConfig,
-                  detail: value,
-                })
-              }}
+      <div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.title')}</div>
+      <div className='pt-3 space-y-6'>
+        <div>
+          <div className='mb-2 flex items-center  space-x-1'>
+            <div className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.resolution')}</div>
+            <Tooltip
+              popupContent={
+                <div className='w-[180px]' >
+                  {t('appDebug.vision.visionSettings.resolutionTooltip').split('\n').map(item => (
+                    <div key={item}>{item}</div>
+                  ))}
+                </div>
+              }
             />
           </div>
-          <div>
-            <div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
-            <RadioGroup
-              className='space-x-3'
-              options={[
-                {
-                  label: t('appDebug.vision.visionSettings.both'),
-                  value: TransferMethod.all,
-                },
-                {
-                  label: t('appDebug.vision.visionSettings.localUpload'),
-                  value: TransferMethod.local_file,
-                },
-                {
-                  label: t('appDebug.vision.visionSettings.url'),
-                  value: TransferMethod.remote_url,
-                },
-              ]}
-              value={transferMethod}
-              onChange={(value: TransferMethod) => {
-                if (value === TransferMethod.all) {
-                  setVisionConfig({
-                    ...visionConfig,
-                    transfer_methods: [TransferMethod.remote_url, TransferMethod.local_file],
-                  })
-                  return
-                }
-                setVisionConfig({
-                  ...visionConfig,
-                  transfer_methods: [value],
-                })
-              }}
+          <div className='flex items-center gap-1'>
+            <OptionCard
+              className='grow'
+              title={t('appDebug.vision.visionSettings.high')}
+              selected={file?.image?.detail === Resolution.high}
+              onSelect={() => handleChange({
+                ...file,
+                image: { detail: Resolution.high },
+              })}
+            />
+            <OptionCard
+              className='grow'
+              title={t('appDebug.vision.visionSettings.low')}
+              selected={file?.image?.detail === Resolution.low}
+              onSelect={() => handleChange({
+                ...file,
+                image: { detail: Resolution.low },
+              })}
             />
           </div>
-          <div>
-            <ParamItem
-              id='upload_limit'
-              className=''
-              name={t('appDebug.vision.visionSettings.uploadLimit')}
-              noTooltip
-              {...{
-                default: 2,
-                step: 1,
-                min: MIN,
-                max: MAX,
-              }}
-              value={visionConfig.number_limits}
-              enable={true}
-              onChange={(_key: string, value: number) => {
-                if (!value)
-                  return
-
-                setVisionConfig({
-                  ...visionConfig,
-                  number_limits: value,
-                })
-              }}
+        </div>
+        <div>
+          <div className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.vision.visionSettings.uploadMethod')}</div>
+          <div className='flex items-center gap-1'>
+            <OptionCard
+              className='grow'
+              title={t('appDebug.vision.visionSettings.both')}
+              selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.local_file) && !!file?.allowed_file_upload_methods?.includes(TransferMethod.remote_url)}
+              onSelect={() => handleChange({
+                ...file,
+                allowed_file_upload_methods: [TransferMethod.local_file, TransferMethod.remote_url],
+              })}
+            />
+            <OptionCard
+              className='grow'
+              title={t('appDebug.vision.visionSettings.localUpload')}
+              selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.local_file) && file?.allowed_file_upload_methods?.length === 1}
+              onSelect={() => handleChange({
+                ...file,
+                allowed_file_upload_methods: [TransferMethod.local_file],
+              })}
+            />
+            <OptionCard
+              className='grow'
+              title={t('appDebug.vision.visionSettings.url')}
+              selected={!!file?.allowed_file_upload_methods?.includes(TransferMethod.remote_url) && file?.allowed_file_upload_methods?.length === 1}
+              onSelect={() => handleChange({
+                ...file,
+                allowed_file_upload_methods: [TransferMethod.remote_url],
+              })}
             />
           </div>
         </div>
+        <div>
+          <ParamItem
+            id='upload_limit'
+            className=''
+            name={t('appDebug.vision.visionSettings.uploadLimit')}
+            noTooltip
+            {...{
+              default: 2,
+              step: 1,
+              min: MIN,
+              max: MAX,
+            }}
+            value={file?.number_limits || 3}
+            enable={true}
+            onChange={(_key: string, value: number) => {
+              if (!value)
+                return
+
+              handleChange({
+                ...file,
+                number_limits: value,
+              })
+            }}
+          />
+        </div>
       </div>
     </div>
   )

+ 3 - 3
web/app/components/app/configuration/config-vision/param-config.tsx

@@ -2,7 +2,7 @@
 import type { FC } from 'react'
 import { memo, useState } from 'react'
 import { useTranslation } from 'react-i18next'
-import VoiceParamConfig from './param-config-content'
+import ParamConfigContent from './param-config-content'
 import cn from '@/utils/classnames'
 import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
 import {
@@ -25,14 +25,14 @@ const ParamsConfig: FC = () => {
       }}
     >
       <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')}>
+        <div className={cn('flex items-center rounded-md h-7 px-3 space-x-1 text-text-tertiary cursor-pointer hover:bg-gray-200', open && 'bg-gray-200')}>
           <Settings01 className='w-3.5 h-3.5 ' />
           <div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
         </div>
       </PortalToFollowElemTrigger>
       <PortalToFollowElemContent style={{ zIndex: 50 }}>
         <div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
-          <VoiceParamConfig />
+          <ParamConfigContent />
         </div>
       </PortalToFollowElemContent>
     </PortalToFollowElem>

+ 0 - 40
web/app/components/app/configuration/config-vision/radio-group/index.tsx

@@ -1,40 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import s from './style.module.css'
-import cn from '@/utils/classnames'
-
-type OPTION = {
-  label: string
-  value: any
-}
-
-type Props = {
-  className?: string
-  options: OPTION[]
-  value: any
-  onChange: (value: any) => void
-}
-
-const RadioGroup: FC<Props> = ({
-  className = '',
-  options,
-  value,
-  onChange,
-}) => {
-  return (
-    <div className={cn(className, 'flex')}>
-      {options.map(item => (
-        <div
-          key={item.value}
-          className={cn(s.item, item.value === value && s.checked)}
-          onClick={() => onChange(item.value)}
-        >
-          <div className={s.radio}></div>
-          <div className='text-[13px] font-medium text-gray-900'>{item.label}</div>
-        </div>
-      ))}
-    </div>
-  )
-}
-export default React.memo(RadioGroup)

+ 0 - 24
web/app/components/app/configuration/config-vision/radio-group/style.module.css

@@ -1,24 +0,0 @@
-.item {
-  @apply grow flex items-center h-8 px-2.5 rounded-lg bg-gray-25 border border-gray-100 cursor-pointer space-x-2;
-}
-
-.item:hover {
-  background-color: #ffffff;
-  border-color: #B2CCFF;
-  box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
-}
-
-.item.checked {
-  background-color: #ffffff;
-  border-color: #528BFF;
-  box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.06), 0px 1px 3px 0px rgba(16, 24, 40, 0.10);
-}
-
-.radio {
-  @apply w-4 h-4 border-[2px] border-gray-200 rounded-full;
-}
-
-.item.checked .radio {
-  border-width: 5px;
-  border-color: #155eef;
-}

+ 0 - 220
web/app/components/app/configuration/config-voice/param-config-content.tsx

@@ -1,220 +0,0 @@
-'use client'
-import useSWR from 'swr'
-import type { FC } from 'react'
-import { useContext } from 'use-context-selector'
-import React, { Fragment } from 'react'
-import { usePathname } from 'next/navigation'
-import { useTranslation } from 'react-i18next'
-import { Listbox, Transition } from '@headlessui/react'
-import { CheckIcon, ChevronDownIcon } from '@heroicons/react/20/solid'
-import classNames from '@/utils/classnames'
-import RadioGroup from '@/app/components/app/configuration/config-vision/radio-group'
-import type { Item } from '@/app/components/base/select'
-import ConfigContext from '@/context/debug-configuration'
-import { fetchAppVoices } from '@/service/apps'
-import Tooltip from '@/app/components/base/tooltip'
-import { languages } from '@/i18n/language'
-import { TtsAutoPlay } from '@/types/app'
-const VoiceParamConfig: FC = () => {
-  const { t } = useTranslation()
-  const pathname = usePathname()
-  const matched = pathname.match(/\/app\/([^/]+)/)
-  const appId = (matched?.length && matched[1]) ? matched[1] : ''
-
-  const {
-    textToSpeechConfig,
-    setTextToSpeechConfig,
-  } = useContext(ConfigContext)
-
-  let languageItem = languages.find(item => item.value === textToSpeechConfig.language)
-  const localLanguagePlaceholder = languageItem?.name || t('common.placeholder.select')
-  if (languages && !languageItem && languages.length > 0)
-    languageItem = languages[0]
-  const language = languageItem?.value
-  const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
-  let voiceItem = voiceItems?.find(item => item.value === textToSpeechConfig.voice)
-  if (voiceItems && !voiceItem && voiceItems.length > 0)
-    voiceItem = voiceItems[0]
-
-  const localVoicePlaceholder = voiceItem?.name || t('common.placeholder.select')
-
-  return (
-    <div>
-      <div>
-        <div className='leading-6 text-base font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.title')}</div>
-        <div className='pt-3 space-y-6'>
-          <div>
-            <div className='mb-2 flex items-center  space-x-1'>
-              <div
-                className='leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.language')}</div>
-              <Tooltip
-                popupContent={
-                  <div className='w-[180px]'>
-                    {t('appDebug.voice.voiceSettings.resolutionTooltip').split('\n').map(item => (
-                      <div key={item}>{item}</div>
-                    ))}
-                  </div>
-                }
-              />
-            </div>
-            <Listbox
-              value={languageItem}
-              onChange={(value: Item) => {
-                setTextToSpeechConfig({
-                  ...textToSpeechConfig,
-                  language: String(value.value),
-                })
-              }}
-            >
-              <div className={'relative h-9'}>
-                <Listbox.Button
-                  className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
-                  <span className={classNames('block truncate text-left', !languageItem?.name && 'text-gray-400')}>
-                    {languageItem?.name ? t(`common.voice.language.${languageItem?.value.replace('-', '')}`) : localLanguagePlaceholder}
-                  </span>
-                  <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
-                    <ChevronDownIcon
-                      className="h-5 w-5 text-gray-400"
-                      aria-hidden="true"
-                    />
-                  </span>
-                </Listbox.Button>
-                <Transition
-                  as={Fragment}
-                  leave="transition ease-in duration-100"
-                  leaveFrom="opacity-100"
-                  leaveTo="opacity-0"
-                >
-
-                  <Listbox.Options
-                    className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
-                    {languages.map((item: Item) => (
-                      <Listbox.Option
-                        key={item.value}
-                        className={({ active }) =>
-                          `relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
-                          }`
-                        }
-                        value={item}
-                        disabled={false}
-                      >
-                        {({ /* active, */ selected }) => (
-                          <>
-                            <span
-                              className={classNames('block', selected && 'font-normal')}>{t(`common.voice.language.${(item.value).toString().replace('-', '')}`)}</span>
-                            {(selected || item.value === textToSpeechConfig.language) && (
-                              <span
-                                className={classNames(
-                                  'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
-                                )}
-                              >
-                                <CheckIcon className="h-5 w-5" aria-hidden="true" />
-                              </span>
-                            )}
-                          </>
-                        )}
-                      </Listbox.Option>
-                    ))}
-                  </Listbox.Options>
-                </Transition>
-              </div>
-            </Listbox>
-          </div>
-          <div>
-            <div
-              className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.voice')}</div>
-            <Listbox
-              value={voiceItem ?? {}}
-              disabled={!languageItem}
-              onChange={(value: Item) => {
-                if (!value.value)
-                  return
-                setTextToSpeechConfig({
-                  ...textToSpeechConfig,
-                  voice: String(value.value),
-                })
-              }}
-            >
-              <div className={'relative h-9'}>
-                <Listbox.Button
-                  className={'w-full h-full rounded-lg border-0 bg-gray-100 py-1.5 pl-3 pr-10 sm:text-sm sm:leading-6 focus-visible:outline-none focus-visible:bg-gray-200 group-hover:bg-gray-200 cursor-pointer'}>
-                  <span
-                    className={classNames('block truncate text-left', !voiceItem?.name && 'text-gray-400')}>{voiceItem?.name ?? localVoicePlaceholder}</span>
-                  <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
-                    <ChevronDownIcon
-                      className="h-5 w-5 text-gray-400"
-                      aria-hidden="true"
-                    />
-                  </span>
-                </Listbox.Button>
-                <Transition
-                  as={Fragment}
-                  leave="transition ease-in duration-100"
-                  leaveFrom="opacity-100"
-                  leaveTo="opacity-0"
-                >
-
-                  <Listbox.Options
-                    className="absolute z-10 mt-1 px-1 max-h-60 w-full overflow-auto rounded-md bg-white py-1 text-base shadow-lg border-gray-200 border-[0.5px] focus:outline-none sm:text-sm">
-                    {voiceItems?.map((item: Item) => (
-                      <Listbox.Option
-                        key={item.value}
-                        className={({ active }) =>
-                          `relative cursor-pointer select-none py-2 pl-3 pr-9 rounded-lg hover:bg-gray-100 text-gray-700 ${active ? 'bg-gray-100' : ''
-                          }`
-                        }
-                        value={item}
-                        disabled={false}
-                      >
-                        {({ /* active, */ selected }) => (
-                          <>
-                            <span className={classNames('block', selected && 'font-normal')}>{item.name}</span>
-                            {(selected || item.value === textToSpeechConfig.voice) && (
-                              <span
-                                className={classNames(
-                                  'absolute inset-y-0 right-0 flex items-center pr-4 text-gray-700',
-                                )}
-                              >
-                                <CheckIcon className="h-5 w-5" aria-hidden="true" />
-                              </span>
-                            )}
-                          </>
-                        )}
-                      </Listbox.Option>
-                    ))}
-                  </Listbox.Options>
-                </Transition>
-              </div>
-            </Listbox>
-          </div>
-          <div>
-            <div
-              className='mb-2 leading-[18px] text-[13px] font-semibold text-gray-800'>{t('appDebug.voice.voiceSettings.autoPlay')}</div>
-            <RadioGroup
-              className='space-x-3'
-              options={[
-                {
-                  label: t('appDebug.voice.voiceSettings.autoPlayEnabled'),
-                  value: TtsAutoPlay.enabled,
-                },
-                {
-                  label: t('appDebug.voice.voiceSettings.autoPlayDisabled'),
-                  value: TtsAutoPlay.disabled,
-                },
-              ]}
-              value={textToSpeechConfig.autoPlay ? textToSpeechConfig.autoPlay : TtsAutoPlay.disabled}
-              onChange={(value: TtsAutoPlay) => {
-                setTextToSpeechConfig({
-                  ...textToSpeechConfig,
-                  autoPlay: value,
-                })
-              }}
-            />
-          </div>
-        </div>
-      </div>
-    </div>
-  )
-}
-
-export default React.memo(VoiceParamConfig)

+ 0 - 41
web/app/components/app/configuration/config-voice/param-config.tsx

@@ -1,41 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import { memo, useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import VoiceParamConfig from './param-config-content'
-import cn from '@/utils/classnames'
-import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
-import {
-  PortalToFollowElem,
-  PortalToFollowElemContent,
-  PortalToFollowElemTrigger,
-} from '@/app/components/base/portal-to-follow-elem'
-
-const ParamsConfig: FC = () => {
-  const { t } = useTranslation()
-  const [open, setOpen] = useState(false)
-
-  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')}>
-          <Settings01 className='w-3.5 h-3.5 ' />
-          <div className='ml-1 leading-[18px] text-xs font-medium '>{t('appDebug.voice.settings')}</div>
-        </div>
-      </PortalToFollowElemTrigger>
-      <PortalToFollowElemContent style={{ zIndex: 50 }}>
-        <div className='w-80 sm:w-[412px] p-4 bg-white rounded-lg border-[0.5px] border-gray-200 shadow-lg space-y-3'>
-          <VoiceParamConfig />
-        </div>
-      </PortalToFollowElemContent>
-    </PortalToFollowElem>
-  )
-}
-export default memo(ParamsConfig)

+ 1 - 1
web/app/components/app/configuration/config/agent/agent-tools/index.tsx

@@ -58,7 +58,7 @@ const AgentTools: FC = () => {
   return (
     <>
       <Panel
-        className="mt-4"
+        className="mt-2"
         noBodySpacing={tools.length === 0}
         headerIcon={
           <RiHammerFill className='w-4 h-4 text-primary-500' />

+ 20 - 6
web/app/components/app/configuration/config/automatic/get-automatic-res.tsx

@@ -18,16 +18,17 @@ import cn from 'classnames'
 import s from './style.module.css'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
+import Textarea from '@/app/components/base/textarea'
 import Toast from '@/app/components/base/toast'
 import { generateRule } from '@/service/debug'
 import ConfigPrompt from '@/app/components/app/configuration/config-prompt'
 import type { Model } from '@/types/app'
 import { AppType } from '@/types/app'
 import ConfigVar from '@/app/components/app/configuration/config-var'
-import OpeningStatement from '@/app/components/app/configuration/features/chat-group/opening-statement'
 import GroupName from '@/app/components/app/configuration/base/group-name'
 import Loading from '@/app/components/base/loading'
 import Confirm from '@/app/components/base/confirm'
+import { LoveMessage } from '@/app/components/base/icons/src/vender/features'
 
 // type
 import type { AutomaticRes } from '@/service/debug'
@@ -212,7 +213,11 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
           <div className='mt-6'>
             <div className='text-[0px]'>
               <div className='mb-2 leading-5 text-sm font-medium text-gray-900'>{t('appDebug.generate.instruction')}</div>
-              <textarea className="w-full h-[200px] overflow-y-auto px-3 py-2 text-sm bg-gray-50 rounded-lg" placeholder={t('appDebug.generate.instructionPlaceHolder') as string} value={instruction} onChange={e => setInstruction(e.target.value)} />
+              <Textarea
+                className="h-[200px] resize-none"
+                placeholder={t('appDebug.generate.instructionPlaceHolder') as string}
+                value={instruction}
+                onChange={e => setInstruction(e.target.value)} />
             </div>
 
             <div className='mt-5 flex justify-end'>
@@ -257,10 +262,19 @@ const GetAutomaticRes: FC<IGetAutomaticResProps> = ({
                   {(mode !== AppType.completion && res?.opening_statement) && (
                     <div className='mt-7'>
                       <GroupName name={t('appDebug.feature.groupChat.title')} />
-                      <OpeningStatement
-                        value={res?.opening_statement || ''}
-                        readonly
-                      />
+                      <div
+                        className='mb-1 p-3 border-t-[0.5px] border-l-[0.5px] border-effects-highlight rounded-xl bg-background-section-burn'
+                      >
+                        <div className='mb-2 flex items-center gap-2'>
+                          <div className='shrink-0 p-1 rounded-lg border-[0.5px] border-divider-subtle shadow-xs bg-util-colors-blue-light-blue-light-500'>
+                            <LoveMessage className='w-4 h-4 text-text-primary-on-surface' />
+                          </div>
+                          <div className='grow flex items-center text-text-secondary system-sm-semibold'>
+                            {t('appDebug.feature.conversationOpener.title')}
+                          </div>
+                        </div>
+                        <div className='min-h-8 text-text-tertiary system-xs-regular'>{res.opening_statement}</div>
+                      </div>
                     </div>
                   )}
                 </>

+ 0 - 40
web/app/components/app/configuration/config/feature/add-feature-btn/index.tsx

@@ -1,40 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import { PlusIcon } from '@heroicons/react/24/solid'
-
-export type IAddFeatureBtnProps = {
-  toBottomHeight: number
-  onClick: () => void
-}
-
-const ITEM_HEIGHT = 48
-
-const AddFeatureBtn: FC<IAddFeatureBtnProps> = ({
-  toBottomHeight,
-  onClick,
-}) => {
-  const { t } = useTranslation()
-  return (
-    <div
-      className='absolute z-[9] left-0 right-0 flex justify-center pb-4'
-      style={{
-        top: toBottomHeight - ITEM_HEIGHT,
-        background: 'linear-gradient(180deg, rgba(255, 255, 255, 0.00) 0%, #FFF 100%)',
-      }}
-    >
-      <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>
-  )
-}
-export default React.memo(AddFeatureBtn)

+ 0 - 52
web/app/components/app/configuration/config/feature/choose-feature/feature-item/index.tsx

@@ -1,52 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import s from './style.module.css'
-import cn from '@/utils/classnames'
-import Switch from '@/app/components/base/switch'
-
-export type IFeatureItemProps = {
-  icon: React.ReactNode
-  previewImgClassName?: string
-  title: string
-  description: string
-  value: boolean
-  onChange: (value: boolean) => void
-}
-
-const FeatureItem: FC<IFeatureItemProps> = ({
-  icon,
-  previewImgClassName,
-  title,
-  description,
-  value,
-  onChange,
-}) => {
-  return (
-    <div className={cn(s.wrap, 'relative flex justify-between p-3 rounded-xl border border-transparent bg-gray-50 hover:border-gray-200  cursor-pointer')}>
-      <div className='flex space-x-3 mr-2'>
-        {/* icon */}
-        <div
-          className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg border border-gray-200 bg-white'
-          style={{
-            boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
-          }}
-        >
-          {icon}
-        </div>
-        <div>
-          <div className='text-sm font-semibold text-gray-800'>{title}</div>
-          <div className='text-xs font-normal text-gray-500'>{description}</div>
-        </div>
-      </div>
-
-      <Switch onChange={onChange} defaultValue={value} />
-      {
-        previewImgClassName && (
-          <div className={cn(s.preview, s[previewImgClassName])}>
-          </div>)
-      }
-    </div>
-  )
-}
-export default React.memo(FeatureItem)

BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.png


File diff suppressed because it is too large
+ 0 - 150
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citation.svg


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/citations-and-attributions-preview@2x.png


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/conversation-opener-preview@2x.png


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this-preview@2x.png


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.png


File diff suppressed because it is too large
+ 0 - 188
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/more-like-this.svg


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/next-question-suggestion-preview@2x.png


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-statement.png


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/opening-suggestion-preview@2x.png


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text-preview@2x.png


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.png


File diff suppressed because it is too large
+ 0 - 100
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/speech-to-text.svg


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.png


File diff suppressed because it is too large
+ 0 - 163
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/suggested-questions-after-answer.svg


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-assistant@2x.png


BIN
web/app/components/app/configuration/config/feature/choose-feature/feature-item/preview-imgs/text-to-audio-preview-completion@2x.png


+ 0 - 41
web/app/components/app/configuration/config/feature/choose-feature/feature-item/style.module.css

@@ -1,41 +0,0 @@
-.preview {
-  display: none;
-  position: absolute;
-  top: 0;
-  left: 100%;
-  transform: translate(32px, -54px);
-  width: 280px;
-  height: 360px;
-  background: center center no-repeat;
-  background-size: contain;
-  border-radius: 8px;
-}
-
-.wrap:hover .preview {
-  display: block;
-}
-
-.openingStatementPreview {
-  background-image: url(./preview-imgs/opening-statement.png);
-}
-
-.suggestedQuestionsAfterAnswerPreview {
-  background-image: url(./preview-imgs/suggested-questions-after-answer.png);
-}
-
-.moreLikeThisPreview {
-  background-image: url(./preview-imgs/more-like-this.png);
-}
-
-.speechToTextPreview {
-  background-image: url(./preview-imgs/speech-to-text.png);
-}
-
-.textToSpeechPreview {
-  @apply shadow-lg rounded-lg;
-  background-image: url(./preview-imgs/text-to-audio-preview-assistant@2x.png);
-}
-
-.citationPreview {
-  background-image: url(./preview-imgs/citation.png);
-}

File diff suppressed because it is too large
+ 0 - 172
web/app/components/app/configuration/config/feature/choose-feature/index.tsx


+ 0 - 31
web/app/components/app/configuration/config/feature/feature-group/index.tsx

@@ -1,31 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import GroupName from '@/app/components/app/configuration/base/group-name'
-
-export type IFeatureGroupProps = {
-  title: string
-  description?: string
-  children: React.ReactNode
-}
-
-const FeatureGroup: FC<IFeatureGroupProps> = ({
-  title,
-  description,
-  children,
-}) => {
-  return (
-    <div className='mb-6'>
-      <div className='mb-2'>
-        <GroupName name={title} />
-        {description && (
-          <div className='text-xs font-normal text-gray-500'>{description}</div>
-        )}
-      </div>
-      <div className='space-y-2'>
-        {children}
-      </div>
-    </div>
-  )
-}
-export default React.memo(FeatureGroup)

+ 2 - 229
web/app/components/app/configuration/config/index.tsx

@@ -1,70 +1,33 @@
 'use client'
 import type { FC } from 'react'
-import React, { useRef } from 'react'
+import React from 'react'
 import { useContext } from 'use-context-selector'
 import produce from 'immer'
-import { useBoolean, useScroll } from 'ahooks'
 import { useFormattingChangedDispatcher } from '../debug/hooks'
 import DatasetConfig from '../dataset-config'
-import ChatGroup from '../features/chat-group'
-import ExperienceEnhanceGroup from '../features/experience-enhance-group'
-import Toolbox from '../toolbox'
 import HistoryPanel from '../config-prompt/conversation-history/history-panel'
 import ConfigVision from '../config-vision'
-import useAnnotationConfig from '../toolbox/annotation/use-annotation-config'
-import AddFeatureBtn from './feature/add-feature-btn'
-import ChooseFeature from './feature/choose-feature'
-import useFeature from './feature/use-feature'
 import AgentTools from './agent/agent-tools'
 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 CitationConfig, type ModelConfig, type ModerationConfig, type MoreLikeThisConfig, type PromptVariable, type SpeechToTextConfig, type SuggestedQuestionsAfterAnswerConfig, type TextToSpeechConfig } from '@/models/debug'
+import { type ModelConfig, type PromptVariable } from '@/models/debug'
 import type { AppType } from '@/types/app'
 import { ModelModeType } from '@/types/app'
-import { useModalContext } from '@/context/modal-context'
-import ConfigParamModal from '@/app/components/app/configuration/toolbox/annotation/config-param-modal'
-import AnnotationFullModal from '@/app/components/billing/annotation-full/modal'
-import { useDefaultModel } from '@/app/components/header/account-setting/model-provider-page/hooks'
-import { ModelTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
 
 const Config: FC = () => {
   const {
-    appId,
     mode,
     isAdvancedMode,
     modelModeType,
     isAgent,
-    // canReturnToSimpleMode,
-    // setPromptMode,
     hasSetBlockStatus,
     showHistoryModal,
-    introduction,
-    setIntroduction,
-    suggestedQuestions,
-    setSuggestedQuestions,
     modelConfig,
     setModelConfig,
     setPrevPromptConfig,
-    moreLikeThisConfig,
-    setMoreLikeThisConfig,
-    suggestedQuestionsAfterAnswerConfig,
-    setSuggestedQuestionsAfterAnswerConfig,
-    speechToTextConfig,
-    setSpeechToTextConfig,
-    textToSpeechConfig,
-    setTextToSpeechConfig,
-    citationConfig,
-    setCitationConfig,
-    annotationConfig,
-    setAnnotationConfig,
-    moderationConfig,
-    setModerationConfig,
   } = useContext(ConfigContext)
   const isChatApp = ['advanced-chat', 'agent-chat', 'chat'].includes(mode)
-  const { data: speech2textDefaultModel } = useDefaultModel(ModelTypeEnum.speech2text)
-  const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.tts)
-  const { setShowModerationSettingModal } = useModalContext()
   const formattingChangedDispatcher = useFormattingChangedDispatcher()
 
   const promptTemplate = modelConfig.configs.prompt_template
@@ -90,138 +53,11 @@ const Config: FC = () => {
     setModelConfig(newModelConfig)
   }
 
-  const [showChooseFeature, {
-    setTrue: showChooseFeatureTrue,
-    setFalse: showChooseFeatureFalse,
-  }] = useBoolean(false)
-  const { featureConfig, handleFeatureChange } = useFeature({
-    introduction,
-    setIntroduction,
-    moreLikeThis: moreLikeThisConfig.enabled,
-    setMoreLikeThis: (value) => {
-      setMoreLikeThisConfig(produce(moreLikeThisConfig, (draft: MoreLikeThisConfig) => {
-        draft.enabled = value
-      }))
-    },
-    suggestedQuestionsAfterAnswer: suggestedQuestionsAfterAnswerConfig.enabled,
-    setSuggestedQuestionsAfterAnswer: (value) => {
-      setSuggestedQuestionsAfterAnswerConfig(produce(suggestedQuestionsAfterAnswerConfig, (draft: SuggestedQuestionsAfterAnswerConfig) => {
-        draft.enabled = value
-      }))
-      formattingChangedDispatcher()
-    },
-    speechToText: speechToTextConfig.enabled,
-    setSpeechToText: (value) => {
-      setSpeechToTextConfig(produce(speechToTextConfig, (draft: SpeechToTextConfig) => {
-        draft.enabled = value
-      }))
-    },
-    textToSpeech: textToSpeechConfig.enabled,
-    setTextToSpeech: (value) => {
-      setTextToSpeechConfig(produce(textToSpeechConfig, (draft: TextToSpeechConfig) => {
-        draft.enabled = value
-        draft.voice = textToSpeechConfig?.voice
-        draft.language = textToSpeechConfig?.language
-      }))
-    },
-    citation: citationConfig.enabled,
-    setCitation: (value) => {
-      setCitationConfig(produce(citationConfig, (draft: CitationConfig) => {
-        draft.enabled = value
-      }))
-      formattingChangedDispatcher()
-    },
-    annotation: annotationConfig.enabled,
-    setAnnotation: async (value) => {
-      if (value) {
-        // eslint-disable-next-line @typescript-eslint/no-use-before-define
-        setIsShowAnnotationConfigInit(true)
-      }
-      else {
-        // eslint-disable-next-line @typescript-eslint/no-use-before-define
-        await handleDisableAnnotation(annotationConfig.embedding_model)
-      }
-    },
-    moderation: moderationConfig.enabled,
-    setModeration: (value) => {
-      setModerationConfig(produce(moderationConfig, (draft: ModerationConfig) => {
-        draft.enabled = value
-      }))
-      if (value && !moderationConfig.type) {
-        setShowModerationSettingModal({
-          payload: {
-            enabled: true,
-            type: 'keywords',
-            config: {
-              keywords: '',
-              inputs_config: {
-                enabled: true,
-                preset_response: '',
-              },
-            },
-          },
-          onSaveCallback: setModerationConfig,
-          onCancelCallback: () => {
-            setModerationConfig(produce(moderationConfig, (draft: ModerationConfig) => {
-              draft.enabled = false
-              showChooseFeatureTrue()
-            }))
-          },
-        })
-        showChooseFeatureFalse()
-      }
-    },
-  })
-
-  const {
-    handleEnableAnnotation,
-    setScore,
-    handleDisableAnnotation,
-    isShowAnnotationConfigInit,
-    setIsShowAnnotationConfigInit,
-    isShowAnnotationFullModal,
-    setIsShowAnnotationFullModal,
-  } = useAnnotationConfig({
-    appId,
-    annotationConfig,
-    setAnnotationConfig,
-  })
-
-  const hasChatConfig = isChatApp && (featureConfig.openingStatement || featureConfig.suggestedQuestionsAfterAnswer || (featureConfig.speechToText && !!speech2textDefaultModel) || (featureConfig.textToSpeech && !!text2speechDefaultModel) || featureConfig.citation)
-  const hasCompletionConfig = !isChatApp && (moreLikeThisConfig.enabled || (featureConfig.textToSpeech && !!text2speechDefaultModel))
-
-  const hasToolbox = moderationConfig.enabled || featureConfig.annotation
-
-  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
-        ref={wrapRef}
         className="grow h-0 relative px-6 pb-[50px] overflow-y-auto"
       >
-        <AddFeatureBtn toBottomHeight={toBottomHeight} onClick={showChooseFeatureTrue} />
-        {showChooseFeature && (
-          <ChooseFeature
-            isShow={showChooseFeature}
-            onClose={showChooseFeatureFalse}
-            isChatApp={isChatApp}
-            config={featureConfig}
-            onChange={handleFeatureChange}
-            showSpeechToTextItem={!!speech2textDefaultModel}
-            showTextToSpeechItem={!!text2speechDefaultModel}
-          />
-        )}
-
         {/* Template */}
         <ConfigPrompt
           mode={mode as AppType}
@@ -253,69 +89,6 @@ const Config: FC = () => {
             onShowEditModal={showHistoryModal}
           />
         )}
-
-        {/* ChatConfig */}
-        {
-          hasChatConfig && (
-            <ChatGroup
-              isShowOpeningStatement={featureConfig.openingStatement}
-              openingStatementConfig={
-                {
-                  value: introduction,
-                  onChange: setIntroduction,
-                  suggestedQuestions,
-                  onSuggestedQuestionsChange: setSuggestedQuestions,
-                }
-              }
-              isShowSuggestedQuestionsAfterAnswer={featureConfig.suggestedQuestionsAfterAnswer}
-              isShowTextToSpeech={featureConfig.textToSpeech && !!text2speechDefaultModel}
-              isShowSpeechText={featureConfig.speechToText && !!speech2textDefaultModel}
-              isShowCitation={featureConfig.citation}
-            />
-          )
-        }
-
-        {/* Text Generation config */}{
-          hasCompletionConfig && (
-            <ExperienceEnhanceGroup
-              isShowMoreLike={moreLikeThisConfig.enabled}
-              isShowTextToSpeech={featureConfig.textToSpeech && !!text2speechDefaultModel}
-            />
-          )
-        }
-
-        {/* Toolbox */}
-        {
-          hasToolbox && (
-            <Toolbox
-              showModerationSettings={moderationConfig.enabled}
-              showAnnotation={isChatApp && featureConfig.annotation}
-              onEmbeddingChange={handleEnableAnnotation}
-              onScoreChange={setScore}
-            />
-          )
-        }
-
-        <ConfigParamModal
-          appId={appId}
-          isInit
-          isShow={isShowAnnotationConfigInit}
-          onHide={() => {
-            setIsShowAnnotationConfigInit(false)
-            showChooseFeatureTrue()
-          }}
-          onSave={async (embeddingModel, score) => {
-            await handleEnableAnnotation(embeddingModel, score)
-            setIsShowAnnotationConfigInit(false)
-          }}
-          annotationConfig={annotationConfig}
-        />
-        {isShowAnnotationFullModal && (
-          <AnnotationFullModal
-            show={isShowAnnotationFullModal}
-            onHide={() => setIsShowAnnotationFullModal(false)}
-          />
-        )}
       </div>
     </>
   )

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

@@ -70,7 +70,7 @@ const DatasetConfig: FC = () => {
 
   return (
     <FeaturePanel
-      className='mt-3'
+      className='mt-2'
       headerIcon={Icon}
       title={t('appDebug.feature.dataSet.title')}
       headerRight={

+ 6 - 4
web/app/components/app/configuration/dataset-config/settings-modal/index.tsx

@@ -10,6 +10,8 @@ import cn from '@/utils/classnames'
 import IndexMethodRadio from '@/app/components/datasets/settings/index-method-radio'
 import Divider from '@/app/components/base/divider'
 import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
+import Textarea from '@/app/components/base/textarea'
 import type { DataSet } from '@/models/datasets'
 import { useToastContext } from '@/app/components/base/toast'
 import { updateDatasetSetting } from '@/service/datasets'
@@ -204,10 +206,10 @@ const SettingsModal: FC<SettingsModalProps> = ({
           <div className={labelClass}>
             <div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.name')}</div>
           </div>
-          <input
+          <Input
             value={localeCurrentDataset.name}
             onChange={e => handleValueChange('name', e.target.value)}
-            className='block px-3 w-full h-9 bg-gray-100 rounded-lg text-sm text-gray-900 outline-none appearance-none'
+            className='block h-9'
             placeholder={t('datasetSettings.form.namePlaceholder') || ''}
           />
         </div>
@@ -216,10 +218,10 @@ const SettingsModal: FC<SettingsModalProps> = ({
             <div className='text-text-secondary system-sm-semibold'>{t('datasetSettings.form.desc')}</div>
           </div>
           <div className='w-full'>
-            <textarea
+            <Textarea
               value={localeCurrentDataset.description || ''}
               onChange={e => handleValueChange('description', e.target.value)}
-              className='block px-3 py-2 w-full h-[88px] rounded-lg bg-gray-100 text-sm outline-none appearance-none resize-none'
+              className='resize-none'
               placeholder={t('datasetSettings.form.descPlaceholder') || ''}
             />
             <a className='mt-2 flex items-center h-[18px] px-3 text-xs text-gray-500' href="https://docs.dify.ai/features/datasets#how-to-write-a-good-dataset-description" target='_blank' rel='noopener noreferrer'>

+ 109 - 0
web/app/components/app/configuration/debug/chat-user-input.tsx

@@ -0,0 +1,109 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { useContext } from 'use-context-selector'
+import ConfigContext from '@/context/debug-configuration'
+import Input from '@/app/components/base/input'
+import Select from '@/app/components/base/select'
+import Textarea from '@/app/components/base/textarea'
+import { DEFAULT_VALUE_MAX_LEN } from '@/config'
+import type { Inputs } from '@/models/debug'
+import cn from '@/utils/classnames'
+
+type Props = {
+  inputs: Inputs
+}
+
+const ChatUserInput = ({
+  inputs,
+}: Props) => {
+  const { t } = useTranslation()
+  const { modelConfig, setInputs } = useContext(ConfigContext)
+
+  const promptVariables = modelConfig.configs.prompt_variables.filter(({ key, name }) => {
+    return key && key?.trim() && name && name?.trim()
+  })
+
+  const promptVariableObj = (() => {
+    const obj: Record<string, boolean> = {}
+    promptVariables.forEach((input) => {
+      obj[input.key] = true
+    })
+    return obj
+  })()
+
+  const handleInputValueChange = (key: string, value: string) => {
+    if (!(key in promptVariableObj))
+      return
+
+    const newInputs = { ...inputs }
+    promptVariables.forEach((input) => {
+      if (input.key === key)
+        newInputs[key] = value
+    })
+    setInputs(newInputs)
+  }
+
+  if (!promptVariables.length)
+    return null
+
+  return (
+    <div className={cn('bg-components-panel-on-panel-item-bg rounded-xl border-[0.5px] border-components-panel-border-subtle shadow-xs z-[1]')}>
+      <div className='px-4 pt-3 pb-4'>
+        {promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
+          <div
+            key={key}
+            className='mb-4 last-of-type:mb-0'
+          >
+            <div>
+              <div className='h-6 mb-1 flex items-center gap-1 text-text-secondary system-sm-semibold'>
+                <div className='truncate'>{name || key}</div>
+                {!required && <span className='text-text-tertiary system-xs-regular'>{t('workflow.panel.optional')}</span>}
+              </div>
+              <div className='grow'>
+                {type === 'string' && (
+                  <Input
+                    value={inputs[key] ? `${inputs[key]}` : ''}
+                    onChange={(e) => { handleInputValueChange(key, e.target.value) }}
+                    placeholder={name}
+                    autoFocus={index === 0}
+                    maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
+                  />
+                )}
+                {type === 'paragraph' && (
+                  <Textarea
+                    className='grow h-[120px]'
+                    placeholder={name}
+                    value={inputs[key] ? `${inputs[key]}` : ''}
+                    onChange={(e) => { handleInputValueChange(key, e.target.value) }}
+                  />
+                )}
+                {type === 'select' && (
+                  <Select
+                    className='w-full'
+                    defaultValue={inputs[key] as string}
+                    onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
+                    items={(options || []).map(i => ({ name: i, value: i }))}
+                    allowSearch={false}
+                    bgClassName='bg-gray-50'
+                  />
+                )}
+                {type === 'number' && (
+                  <Input
+                    type='number'
+                    value={inputs[key] ? `${inputs[key]}` : ''}
+                    onChange={(e) => { handleInputValueChange(key, e.target.value) }}
+                    placeholder={name}
+                    autoFocus={index === 0}
+                    maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
+                  />
+                )}
+              </div>
+            </div>
+          </div>
+        ))}
+      </div>
+    </div>
+  )
+}
+
+export default ChatUserInput

+ 26 - 6
web/app/components/app/configuration/debug/debug-with-multiple-model/chat-item.tsx

@@ -16,7 +16,7 @@ import {
 import Chat from '@/app/components/base/chat/chat'
 import { useChat } from '@/app/components/base/chat/chat/hooks'
 import { useDebugConfigurationContext } from '@/context/debug-configuration'
-import type { OnSend } from '@/app/components/base/chat/types'
+import type { ChatConfig, OnSend } from '@/app/components/base/chat/types'
 import { useEventEmitterContextContext } from '@/context/event-emitter'
 import { useProviderContext } from '@/context/provider-context'
 import {
@@ -27,6 +27,8 @@ import {
 import Avatar from '@/app/components/base/avatar'
 import { useAppContext } from '@/context/app-context'
 import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import { useFeatures } from '@/app/components/base/features/hooks'
+import type { InputForm } from '@/app/components/base/chat/chat/type'
 
 type ChatItemProps = {
   modelAndParameter: ModelAndParameter
@@ -39,11 +41,29 @@ const ChatItem: FC<ChatItemProps> = ({
     modelConfig,
     appId,
     inputs,
-    visionConfig,
     collectionList,
   } = useDebugConfigurationContext()
   const { textGenerationModelList } = useProviderContext()
-  const config = useConfigFromDebugContext()
+  const features = useFeatures(s => s.features)
+  const configTemplate = useConfigFromDebugContext()
+  const config = useMemo(() => {
+    return {
+      ...configTemplate,
+      more_like_this: features.moreLikeThis,
+      opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '',
+      suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
+      sensitive_word_avoidance: features.moderation,
+      speech_to_text: features.speech2text,
+      text_to_speech: features.text2speech,
+      file_upload: features.file,
+      suggested_questions_after_answer: features.suggested,
+      retriever_resource: features.citation,
+      annotation_reply: features.annotationReply,
+    } as ChatConfig
+  }, [configTemplate, features])
+  const inputsForm = useMemo(() => {
+    return modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
+  }, [modelConfig.configs.prompt_variables])
   const {
     chatList,
     chatListRef,
@@ -55,7 +75,7 @@ const ChatItem: FC<ChatItemProps> = ({
     config,
     {
       inputs,
-      promptVariables: modelConfig.configs.prompt_variables,
+      inputsForm,
     },
     [],
     taskId => stopChatMessageResponding(appId, taskId),
@@ -84,7 +104,7 @@ const ChatItem: FC<ChatItemProps> = ({
       parent_message_id: chatListRef.current.at(-1)?.id || null,
     }
 
-    if (visionConfig.enabled && files?.length && supportVision)
+    if ((config.file_upload as any).enabled && files?.length && supportVision)
       data.files = files
 
     handleSend(
@@ -95,7 +115,7 @@ const ChatItem: FC<ChatItemProps> = ({
         onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
       },
     )
-  }, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, visionConfig.enabled, chatListRef])
+  }, [appId, config, handleSend, inputs, modelAndParameter, textGenerationModelList, chatListRef])
 
   const { eventEmitter } = useEventEmitterContextContext()
   eventEmitter?.useSubscription((v: any) => {

+ 28 - 17
web/app/components/app/configuration/debug/debug-with-multiple-model/index.tsx

@@ -12,24 +12,30 @@ import {
 } from './context'
 import type { DebugWithMultipleModelContextType } from './context'
 import { useEventEmitterContextContext } from '@/context/event-emitter'
-import ChatInput from '@/app/components/base/chat/chat/chat-input'
-import type { VisionFile } from '@/app/components/base/chat/types'
+import ChatInputArea from '@/app/components/base/chat/chat/chat-input-area'
 import { useDebugConfigurationContext } from '@/context/debug-configuration'
+import { useFeatures } from '@/app/components/base/features/hooks'
+import { useStore as useAppStore } from '@/app/components/app/store'
+import type { FileEntity } from '@/app/components/base/file-uploader/types'
+import type { InputForm } from '@/app/components/base/chat/chat/type'
 
 const DebugWithMultipleModel = () => {
   const {
     mode,
-    speechToTextConfig,
-    visionConfig,
+    inputs,
+    modelConfig,
   } = useDebugConfigurationContext()
+  const speech2text = useFeatures(s => s.features.speech2text)
+  const file = useFeatures(s => s.features.file)
   const {
     multipleModelConfigs,
     checkCanSend,
   } = useDebugWithMultipleModelContext()
+
   const { eventEmitter } = useEventEmitterContextContext()
   const isChatMode = mode === 'chat' || mode === 'agent-chat'
 
-  const handleSend = useCallback((message: string, files?: VisionFile[]) => {
+  const handleSend = useCallback((message: string, files?: FileEntity[]) => {
     if (checkCanSend && !checkCanSend())
       return
 
@@ -92,6 +98,9 @@ const DebugWithMultipleModel = () => {
     }
   }, [twoLine, threeLine, fourLine])
 
+  const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
+  const inputsForm = modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
+
   return (
     <div className='flex flex-col h-full'>
       <div
@@ -121,18 +130,20 @@ const DebugWithMultipleModel = () => {
           ))
         }
       </div>
-      {
-        isChatMode && (
-          <div className='shrink-0 pb-4 px-6'>
-            <ChatInput
-              onSend={handleSend}
-              speechToTextConfig={speechToTextConfig}
-              visionConfig={visionConfig}
-              noSpacing
-            />
-          </div>
-        )
-      }
+      {isChatMode && (
+        <div className='shrink-0 pb-0 px-6'>
+          <ChatInputArea
+            showFeatureBar
+            showFileUpload={false}
+            onFeatureBarClick={setShowAppConfigureFeaturesModal}
+            onSend={handleSend}
+            speechToTextConfig={speech2text as any}
+            visionConfig={file}
+            inputs={inputs}
+            inputsForm={inputsForm}
+          />
+        </div>
+      )}
     </div>
   )
 }

+ 10 - 15
web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx

@@ -13,6 +13,7 @@ import { promptVariablesToUserInputsForm } from '@/utils/model-config'
 import { TransferMethod } from '@/app/components/base/chat/types'
 import { useEventEmitterContextContext } from '@/context/event-emitter'
 import { useProviderContext } from '@/context/provider-context'
+import { useFeatures } from '@/app/components/base/features/hooks'
 
 type TextGenerationItemProps = {
   modelAndParameter: ModelAndParameter
@@ -30,16 +31,14 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
     introduction,
     suggestedQuestionsAfterAnswerConfig,
     citationConfig,
-    moderationConfig,
     externalDataToolsConfig,
     chatPromptConfig,
     completionPromptConfig,
     dataSets,
     datasetConfigs,
-    visionConfig,
-    moreLikeThisConfig,
   } = useDebugConfigurationContext()
   const { textGenerationModelList } = useProviderContext()
+  const features = useFeatures(s => s.features)
   const postDatasets = dataSets.map(({ id }) => ({
     dataset: {
       enabled: true,
@@ -54,18 +53,16 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
     completion_prompt_config: isAdvancedMode ? completionPromptConfig : {},
     user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
     dataset_query_variable: contextVar || '',
+    // features
+    more_like_this: features.moreLikeThis as any,
+    sensitive_word_avoidance: features.moderation as any,
+    text_to_speech: features.text2speech as any,
+    file_upload: features.file as any,
     opening_statement: introduction,
-    suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
     speech_to_text: speechToTextConfig,
+    suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
     retriever_resource: citationConfig,
-    sensitive_word_avoidance: moderationConfig,
     external_data_tools: externalDataToolsConfig,
-    more_like_this: moreLikeThisConfig,
-    text_to_speech: {
-      enabled: false,
-      voice: '',
-      language: '',
-    },
     agent_mode: {
       enabled: false,
       tools: [],
@@ -76,9 +73,6 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
         datasets: [...postDatasets],
       } as any,
     },
-    file_upload: {
-      image: visionConfig,
-    },
   }
   const {
     completion,
@@ -106,7 +100,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
       model_config: configData,
     }
 
-    if (visionConfig.enabled && files && files?.length > 0) {
+    if ((config.file_upload as any).enabled && files && files?.length > 0) {
       data.files = files.map((item) => {
         if (item.transfer_method === TransferMethod.local_file) {
           return {
@@ -146,6 +140,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
       isLoading={!completion && isResponding}
       isResponding={isResponding}
       isInstalledApp={false}
+      siteInfo={null}
       messageId={messageId}
       isError={false}
       onRetry={() => { }}

+ 37 - 8
web/app/components/app/configuration/debug/debug-with-single-model/index.tsx

@@ -12,7 +12,7 @@ import {
 import Chat from '@/app/components/base/chat/chat'
 import { useChat } from '@/app/components/base/chat/chat/hooks'
 import { useDebugConfigurationContext } from '@/context/debug-configuration'
-import type { ChatItem, OnSend } from '@/app/components/base/chat/types'
+import type { ChatConfig, ChatItem, OnSend } from '@/app/components/base/chat/types'
 import { useProviderContext } from '@/context/provider-context'
 import {
   fetchConversationMessages,
@@ -22,7 +22,10 @@ import {
 import Avatar from '@/app/components/base/avatar'
 import { useAppContext } from '@/context/app-context'
 import { ModelFeatureEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
+import { useStore as useAppStore } from '@/app/components/app/store'
+import { useFeatures } from '@/app/components/base/features/hooks'
 import { getLastAnswer } from '@/app/components/base/chat/utils'
+import type { InputForm } from '@/app/components/base/chat/chat/type'
 
 type DebugWithSingleModelProps = {
   checkCanSend?: () => boolean
@@ -38,12 +41,31 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
     modelConfig,
     appId,
     inputs,
-    visionConfig,
     collectionList,
     completionParams,
+    // isShowVisionConfig,
   } = useDebugConfigurationContext()
   const { textGenerationModelList } = useProviderContext()
-  const config = useConfigFromDebugContext()
+  const features = useFeatures(s => s.features)
+  const configTemplate = useConfigFromDebugContext()
+  const config = useMemo(() => {
+    return {
+      ...configTemplate,
+      more_like_this: features.moreLikeThis,
+      opening_statement: features.opening?.enabled ? (features.opening?.opening_statement || '') : '',
+      suggested_questions: features.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
+      sensitive_word_avoidance: features.moderation,
+      speech_to_text: features.speech2text,
+      text_to_speech: features.text2speech,
+      file_upload: features.file,
+      suggested_questions_after_answer: features.suggested,
+      retriever_resource: features.citation,
+      annotation_reply: features.annotationReply,
+    } as ChatConfig
+  }, [configTemplate, features])
+  const inputsForm = useMemo(() => {
+    return modelConfig.configs.prompt_variables.filter(item => item.type !== 'api').map(item => ({ ...item, label: item.name, variable: item.key })) as InputForm[]
+  }, [modelConfig.configs.prompt_variables])
   const {
     chatList,
     chatListRef,
@@ -60,7 +82,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
     config,
     {
       inputs,
-      promptVariables: modelConfig.configs.prompt_variables,
+      inputsForm,
     },
     [],
     taskId => stopChatMessageResponding(appId, taskId),
@@ -91,7 +113,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
       parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
     }
 
-    if (visionConfig.enabled && files?.length && supportVision)
+    if ((config.file_upload as any)?.enabled && files?.length && supportVision)
       data.files = files
 
     handleSend(
@@ -102,7 +124,7 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
         onGetSuggestedQuestions: (responseItemId, getAbortController) => fetchSuggestedQuestions(appId, responseItemId, getAbortController),
       },
     )
-  }, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList, visionConfig.enabled])
+  }, [chatListRef, appId, checkCanSend, completionParams, config, handleSend, inputs, modelConfig, textGenerationModelList])
 
   const doRegenerate = useCallback((chatItem: ChatItem) => {
     const index = chatList.findIndex(item => item.id === chatItem.id)
@@ -134,15 +156,22 @@ const DebugWithSingleModel = forwardRef<DebugWithSingleModelRefType, DebugWithSi
     }
   }, [handleRestart])
 
+  const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
+
   return (
     <Chat
       config={config}
       chatList={chatList}
       isResponding={isResponding}
-      chatContainerClassName='p-6'
-      chatFooterClassName='px-6 pt-10 pb-4'
+      chatContainerClassName='px-3 pt-6'
+      chatFooterClassName='px-3 pt-10 pb-0'
+      showFeatureBar
+      showFileUpload={false}
+      onFeatureBarClick={setShowAppConfigureFeaturesModal}
       suggestedQuestions={suggestedQuestions}
       onSend={doSend}
+      inputs={inputs}
+      inputsForm={inputsForm}
       onRegenerate={doRegenerate}
       onStopResponding={handleStop}
       showPromptLog

File diff suppressed because it is too large
+ 119 - 86
web/app/components/app/configuration/debug/index.tsx


+ 0 - 25
web/app/components/app/configuration/features/chat-group/citation/index.tsx

@@ -1,25 +0,0 @@
-'use client'
-import React, { type FC } from 'react'
-import { useTranslation } from 'react-i18next'
-import Panel from '@/app/components/app/configuration/base/feature-panel'
-import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
-
-const Citation: FC = () => {
-  const { t } = useTranslation()
-
-  return (
-    <Panel
-      title={
-        <div className='flex items-center gap-2'>
-          <div>{t('appDebug.feature.citation.title')}</div>
-        </div>
-      }
-      headerIcon={<Citations className='w-4 h-4 text-[#FD853A]' />}
-      headerRight={
-        <div className='text-xs text-gray-500'>{t('appDebug.feature.citation.resDes')}</div>
-      }
-      noBodySpacing
-    />
-  )
-}
-export default React.memo(Citation)

+ 0 - 65
web/app/components/app/configuration/features/chat-group/index.tsx

@@ -1,65 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import GroupName from '../../base/group-name'
-import type { IOpeningStatementProps } from './opening-statement'
-import OpeningStatement from './opening-statement'
-import SuggestedQuestionsAfterAnswer from './suggested-questions-after-answer'
-import SpeechToText from './speech-to-text'
-import TextToSpeech from './text-to-speech'
-import Citation from './citation'
-/*
-* Include
-* 1. Conversation Opener
-* 2. Opening Suggestion
-* 3. Next question suggestion
-*/
-type ChatGroupProps = {
-  isShowOpeningStatement: boolean
-  openingStatementConfig: IOpeningStatementProps
-  isShowSuggestedQuestionsAfterAnswer: boolean
-  isShowSpeechText: boolean
-  isShowTextToSpeech: boolean
-  isShowCitation: boolean
-}
-const ChatGroup: FC<ChatGroupProps> = ({
-  isShowOpeningStatement,
-  openingStatementConfig,
-  isShowSuggestedQuestionsAfterAnswer,
-  isShowSpeechText,
-  isShowTextToSpeech,
-  isShowCitation,
-}) => {
-  const { t } = useTranslation()
-
-  return (
-    <div className='mt-7'>
-      <GroupName name={t('appDebug.feature.groupChat.title')} />
-      <div className='space-y-3'>
-        {isShowOpeningStatement && (
-          <OpeningStatement {...openingStatementConfig} />
-        )}
-        {isShowSuggestedQuestionsAfterAnswer && (
-          <SuggestedQuestionsAfterAnswer />
-        )}
-        {
-          isShowTextToSpeech && (
-            <TextToSpeech />
-          )
-        }
-        {
-          isShowSpeechText && (
-            <SpeechToText />
-          )
-        }
-        {
-          isShowCitation && (
-            <Citation />
-          )
-        }
-      </div>
-    </div>
-  )
-}
-export default React.memo(ChatGroup)

+ 0 - 25
web/app/components/app/configuration/features/chat-group/speech-to-text/index.tsx

@@ -1,25 +0,0 @@
-'use client'
-import React, { type FC } from 'react'
-import { useTranslation } from 'react-i18next'
-import Panel from '@/app/components/app/configuration/base/feature-panel'
-import { Microphone01 } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
-
-const SpeechToTextConfig: FC = () => {
-  const { t } = useTranslation()
-
-  return (
-    <Panel
-      title={
-        <div className='flex items-center gap-2'>
-          <div>{t('appDebug.feature.speechToText.title')}</div>
-        </div>
-      }
-      headerIcon={<Microphone01 className='w-4 h-4 text-[#7839EE]' />}
-      headerRight={
-        <div className='text-xs text-gray-500'>{t('appDebug.feature.speechToText.resDes')}</div>
-      }
-      noBodySpacing
-    />
-  )
-}
-export default React.memo(SpeechToTextConfig)

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

@@ -1,34 +0,0 @@
-'use client'
-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'
-
-const SuggestedQuestionsAfterAnswer: FC = () => {
-  const { t } = useTranslation()
-
-  return (
-    <Panel
-      title={
-        <div className='flex items-center gap-1'>
-          <div>{t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}</div>
-          <Tooltip
-            popupContent={
-              <div className='w-[180px]'>
-                {t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
-              </div>
-            }
-          />
-        </div>
-      }
-      headerIcon={<SuggestedQuestionsAfterAnswerIcon />}
-      headerRight={
-        <div className='text-xs text-gray-500'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.resDes')}</div>
-      }
-      noBodySpacing
-    />
-  )
-}
-export default React.memo(SuggestedQuestionsAfterAnswer)

+ 0 - 55
web/app/components/app/configuration/features/chat-group/text-to-speech/index.tsx

@@ -1,55 +0,0 @@
-'use client'
-import useSWR from 'swr'
-import React, { type FC } from 'react'
-import { useTranslation } from 'react-i18next'
-import { useContext } from 'use-context-selector'
-import { usePathname } from 'next/navigation'
-import Panel from '@/app/components/app/configuration/base/feature-panel'
-import { Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
-import ConfigContext from '@/context/debug-configuration'
-import { languages } from '@/i18n/language'
-import { fetchAppVoices } from '@/service/apps'
-import AudioBtn from '@/app/components/base/audio-btn'
-
-const TextToSpeech: FC = () => {
-  const { t } = useTranslation()
-  const {
-    textToSpeechConfig,
-  } = useContext(ConfigContext)
-
-  const pathname = usePathname()
-  const matched = pathname.match(/\/app\/([^/]+)/)
-  const appId = (matched?.length && matched[1]) ? matched[1] : ''
-  const language = textToSpeechConfig.language
-  const languageInfo = languages.find(i => i.value === textToSpeechConfig.language)
-
-  const voiceItems = useSWR({ appId, language }, fetchAppVoices).data
-  const voiceItem = voiceItems?.find(item => item.value === textToSpeechConfig.voice)
-
-  return (
-    <Panel
-      title={
-        <div className='flex items-center'>
-          <div>{t('appDebug.feature.textToSpeech.title')}</div>
-        </div>
-      }
-      headerIcon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
-      headerRight={
-        <div className='text-xs text-gray-500 inline-flex items-center gap-2'>
-          {languageInfo && (`${languageInfo?.name} - `)}{voiceItem?.name ?? t('appDebug.voice.defaultDisplay')}
-          { languageInfo?.example && (
-            <AudioBtn
-              value={languageInfo?.example}
-              isAudition
-              voice={textToSpeechConfig.voice}
-              noCache
-            />
-          )}
-        </div>
-      }
-      noBodySpacing
-      isShowTextToSpeech
-    />
-  )
-}
-export default React.memo(TextToSpeech)

+ 229 - 174
web/app/components/app/configuration/index.tsx

@@ -1,6 +1,6 @@
 'use client'
 import type { FC } from 'react'
-import React, { useEffect, useMemo, useRef, useState } from 'react'
+import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
 import { usePathname } from 'next/navigation'
@@ -9,17 +9,17 @@ import { useBoolean, useGetState } from 'ahooks'
 import { clone, isEqual } from 'lodash-es'
 import { CodeBracketIcon } from '@heroicons/react/20/solid'
 import { useShallow } from 'zustand/react/shallow'
-import Button from '../../base/button'
-import Loading from '../../base/loading'
-import AppPublisher from '../app-publisher'
-import AgentSettingButton from './config/agent-setting-button'
-import useAdvancedPromptConfig from './hooks/use-advanced-prompt-config'
-import EditHistoryModal from './config-prompt/conversation-history/edit-modal'
+import AgentSettingButton from '@/app/components/app/configuration/config/agent-setting-button'
+import useAdvancedPromptConfig from '@/app/components/app/configuration/hooks/use-advanced-prompt-config'
+import EditHistoryModal from '@/app/components/app/configuration/config-prompt/conversation-history/edit-modal'
 import {
   useDebugWithSingleOrMultipleModel,
   useFormattingChangedDispatcher,
-} from './debug/hooks'
-import type { ModelAndParameter } from './debug/types'
+} from '@/app/components/app/configuration/debug/hooks'
+import type { ModelAndParameter } from '@/app/components/app/configuration/debug/types'
+import Button from '@/app/components/base/button'
+import Loading from '@/app/components/base/loading'
+import AppPublisher from '@/app/components/app/app-publisher/features-wrapper'
 import type {
   AnnotationReplyConfig,
   DatasetConfigs,
@@ -61,6 +61,11 @@ import {
   getMultipleRetrievalConfig,
   getSelectedDatasetsMode,
 } from '@/app/components/workflow/nodes/knowledge-retrieval/utils'
+import { FeaturesProvider } from '@/app/components/base/features'
+import type { Features as FeaturesData, FileUpload } from '@/app/components/base/features/types'
+import { FILE_EXTS } from '@/app/components/base/prompt-editor/constants'
+import { SupportUploadFileTypes } from '@/app/components/workflow/types'
+import NewFeaturePanel from '@/app/components/base/features/new-feature-panel'
 
 type PublishConfig = {
   modelConfig: ModelConfig
@@ -70,10 +75,13 @@ type PublishConfig = {
 const Configuration: FC = () => {
   const { t } = useTranslation()
   const { notify } = useContext(ToastContext)
-  const { appDetail, setAppSiderbarExpand } = useAppStore(useShallow(state => ({
+  const { appDetail, showAppConfigureFeaturesModal, setAppSiderbarExpand, setShowAppConfigureFeaturesModal } = useAppStore(useShallow(state => ({
     appDetail: state.appDetail,
     setAppSiderbarExpand: state.setAppSiderbarExpand,
+    showAppConfigureFeaturesModal: state.showAppConfigureFeaturesModal,
+    setShowAppConfigureFeaturesModal: state.setShowAppConfigureFeaturesModal,
   })))
+  const latestPublishedAt = useMemo(() => appDetail?.model_config.updated_at, [appDetail])
   const [formattingChanged, setFormattingChanged] = useState(false)
   const { setShowAccountSettingModal } = useModalContext()
   const [hasFetchedDetail, setHasFetchedDetail] = useState(false)
@@ -84,7 +92,6 @@ const Configuration: FC = () => {
   const [mode, setMode] = useState('')
   const [publishedConfig, setPublishedConfig] = useState<PublishConfig | null>(null)
 
-  const modalConfig = useMemo(() => appDetail?.model_config || {} as BackendModelConfig, [appDetail])
   const [conversationId, setConversationId] = useState<string | null>('')
 
   const media = useBreakpoints()
@@ -158,13 +165,16 @@ const Configuration: FC = () => {
       prompt_template: '',
       prompt_variables: [] as PromptVariable[],
     },
-    opening_statement: '',
     more_like_this: null,
-    suggested_questions_after_answer: null,
+    opening_statement: '',
+    suggested_questions: [],
+    sensitive_word_avoidance: null,
     speech_to_text: null,
     text_to_speech: null,
+    file_upload: null,
+    suggested_questions_after_answer: null,
     retriever_resource: null,
-    sensitive_word_avoidance: null,
+    annotation_reply: null,
     dataSets: [],
     agentConfig: DEFAULT_AGENT_SETTING,
   })
@@ -274,7 +284,7 @@ const Configuration: FC = () => {
     setModelConfig(_publishedConfig.modelConfig)
     setCompletionParams(_publishedConfig.completionParams)
     setDataSets(modelConfig.dataSets || [])
-    // feature
+    // reset feature
     setIntroduction(modelConfig.opening_statement!)
     setMoreLikeThisConfig(modelConfig.more_like_this || {
       enabled: false,
@@ -422,6 +432,48 @@ const Configuration: FC = () => {
 
   const isShowVisionConfig = !!currModel?.features?.includes(ModelFeatureEnum.vision)
 
+  // *** web app features ***
+  const featuresData: FeaturesData = useMemo(() => {
+    return {
+      moreLikeThis: modelConfig.more_like_this || { enabled: false },
+      opening: {
+        enabled: !!modelConfig.opening_statement,
+        opening_statement: modelConfig.opening_statement || '',
+        suggested_questions: modelConfig.suggested_questions || [],
+      },
+      moderation: modelConfig.sensitive_word_avoidance || { enabled: false },
+      speech2text: modelConfig.speech_to_text || { enabled: false },
+      text2speech: modelConfig.text_to_speech || { enabled: false },
+      file: {
+        image: {
+          detail: modelConfig.file_upload?.image?.detail || Resolution.high,
+          enabled: !!modelConfig.file_upload?.image?.enabled,
+          number_limits: modelConfig.file_upload?.image?.number_limits || 3,
+          transfer_methods: modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
+        },
+        enabled: !!(modelConfig.file_upload?.enabled || modelConfig.file_upload?.image?.enabled),
+        allowed_file_types: modelConfig.file_upload?.allowed_file_types || [SupportUploadFileTypes.image],
+        allowed_file_extensions: modelConfig.file_upload?.allowed_file_extensions || FILE_EXTS[SupportUploadFileTypes.image].map(ext => `.${ext}`),
+        allowed_file_upload_methods: modelConfig.file_upload?.allowed_file_upload_methods || modelConfig.file_upload?.image?.transfer_methods || ['local_file', 'remote_url'],
+        number_limits: modelConfig.file_upload?.number_limits || modelConfig.file_upload?.image?.number_limits || 3,
+      } as FileUpload,
+      suggested: modelConfig.suggested_questions_after_answer || { enabled: false },
+      citation: modelConfig.retriever_resource || { enabled: false },
+      annotationReply: modelConfig.annotation_reply || { enabled: false },
+    }
+  }, [modelConfig])
+  const handleFeaturesChange = useCallback((flag: any) => {
+    setShowAppConfigureFeaturesModal(true)
+    if (flag)
+      formattingChangedDispatcher()
+  }, [formattingChangedDispatcher, setShowAppConfigureFeaturesModal])
+  const handleAddPromptVariable = useCallback((variable: PromptVariable[]) => {
+    const newModelConfig = produce(modelConfig, (draft: ModelConfig) => {
+      draft.configs.prompt_variables = variable
+    })
+    setModelConfig(newModelConfig)
+  }, [modelConfig])
+
   useEffect(() => {
     (async () => {
       const collectionList = await fetchCollectionList()
@@ -514,13 +566,16 @@ const Configuration: FC = () => {
                 modelConfig.dataset_query_variable,
               ),
             },
-            opening_statement: modelConfig.opening_statement,
             more_like_this: modelConfig.more_like_this,
-            suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
+            opening_statement: modelConfig.opening_statement,
+            suggested_questions: modelConfig.suggested_questions,
+            sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
             speech_to_text: modelConfig.speech_to_text,
             text_to_speech: modelConfig.text_to_speech,
+            file_upload: modelConfig.file_upload,
+            suggested_questions_after_answer: modelConfig.suggested_questions_after_answer,
             retriever_resource: modelConfig.retriever_resource,
-            sensitive_word_avoidance: modelConfig.sensitive_word_avoidance,
+            annotation_reply: modelConfig.annotation_reply,
             external_data_tools: modelConfig.external_data_tools,
             dataSets: datasets || [],
             // eslint-disable-next-line multiline-ternary
@@ -589,7 +644,7 @@ const Configuration: FC = () => {
     else { return promptEmpty }
   })()
   const contextVarEmpty = mode === AppType.completion && dataSets.length > 0 && !hasSetContextVar
-  const onPublish = async (modelAndParameter?: ModelAndParameter) => {
+  const onPublish = async (modelAndParameter?: ModelAndParameter, features?: FeaturesData) => {
     const modelId = modelAndParameter?.model || modelConfig.model_id
     const promptTemplate = modelConfig.configs.prompt_template
     const promptVariables = modelConfig.configs.prompt_variables
@@ -630,14 +685,16 @@ const Configuration: FC = () => {
       completion_prompt_config: {},
       user_input_form: promptVariablesToUserInputsForm(promptVariables),
       dataset_query_variable: contextVar || '',
-      opening_statement: introduction || '',
-      suggested_questions: suggestedQuestions || [],
-      more_like_this: moreLikeThisConfig,
-      suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
-      speech_to_text: speechToTextConfig,
-      text_to_speech: textToSpeechConfig,
-      retriever_resource: citationConfig,
-      sensitive_word_avoidance: moderationConfig,
+      //  features
+      more_like_this: features?.moreLikeThis as any,
+      opening_statement: features?.opening?.enabled ? (features.opening?.opening_statement || '') : '',
+      suggested_questions: features?.opening?.enabled ? (features.opening?.suggested_questions || []) : [],
+      sensitive_word_avoidance: features?.moderation as any,
+      speech_to_text: features?.speech2text as any,
+      text_to_speech: features?.text2speech as any,
+      file_upload: features?.file as any,
+      suggested_questions_after_answer: features?.suggested as any,
+      retriever_resource: features?.citation as any,
       agent_mode: {
         ...modelConfig.agentConfig,
         strategy: isFunctionCall ? AgentStrategy.functionCall : AgentStrategy.react,
@@ -654,9 +711,6 @@ const Configuration: FC = () => {
           datasets: [...postDatasets],
         } as any,
       },
-      file_upload: {
-        image: visionConfig,
-      },
     }
 
     if (isAdvancedMode) {
@@ -684,12 +738,6 @@ const Configuration: FC = () => {
     return true
   }
 
-  const [restoreConfirmOpen, setRestoreConfirmOpen] = useState(false)
-  const resetAppConfig = () => {
-    syncToPublishedConfig(publishedConfig!)
-    setRestoreConfirmOpen(false)
-  }
-
   const [showUseGPT4Confirm, setShowUseGPT4Confirm] = useState(false)
 
   const {
@@ -788,153 +836,160 @@ const Configuration: FC = () => {
       setRerankSettingModalOpen,
     }}
     >
-      <>
-        <div className="flex flex-col h-full">
-          <div className='relative flex grow h-[200px] pt-14'>
-            {/* Header */}
-            <div className='absolute top-0 left-0 w-full bg-white h-14'>
-              <div className='flex items-center justify-between px-6 h-14'>
-                <div className='flex items-center'>
-                  <div className='text-base font-semibold leading-6 text-gray-900'>{t('appDebug.orchestrate')}</div>
-                  <div className='flex items-center h-[14px] space-x-1 text-xs'>
-                    {isAdvancedMode && (
-                      <div className='ml-1 flex items-center h-5 px-1.5 border border-gray-100 rounded-md text-[11px] font-medium text-gray-500 uppercase'>{t('appDebug.promptMode.advanced')}</div>
-                    )}
+      <FeaturesProvider features={featuresData}>
+        <>
+          <div className="flex flex-col h-full">
+            <div className='relative flex grow h-[200px] pt-14'>
+              {/* Header */}
+              <div className='absolute top-0 left-0 w-full bg-white h-14'>
+                <div className='flex items-center justify-between px-6 h-14'>
+                  <div className='flex items-center'>
+                    <div className='text-base font-semibold leading-6 text-gray-900'>{t('appDebug.orchestrate')}</div>
+                    <div className='flex items-center h-[14px] space-x-1 text-xs'>
+                      {isAdvancedMode && (
+                        <div className='ml-1 flex items-center h-5 px-1.5 border border-gray-100 rounded-md text-[11px] font-medium text-gray-500 uppercase'>{t('appDebug.promptMode.advanced')}</div>
+                      )}
+                    </div>
                   </div>
-                </div>
-                <div className='flex items-center'>
-                  {/* Agent Setting */}
-                  {isAgent && (
-                    <AgentSettingButton
-                      isChatModel={modelConfig.mode === ModelModeType.chat}
-                      agentConfig={modelConfig.agentConfig}
-
-                      isFunctionCall={isFunctionCall}
-                      onAgentSettingChange={(config) => {
-                        const nextConfig = produce(modelConfig, (draft: ModelConfig) => {
-                          draft.agentConfig = config
-                        })
-                        setModelConfig(nextConfig)
-                      }}
-                    />
-                  )}
-                  {/* Model and Parameters */}
-                  {!debugWithMultipleModel && (
-                    <>
-                      <ModelParameterModal
-                        isAdvancedMode={isAdvancedMode}
-                        mode={mode}
-                        provider={modelConfig.provider}
-                        completionParams={completionParams}
-                        modelId={modelConfig.model_id}
-                        setModel={setModel as any}
-                        onCompletionParamsChange={(newParams: FormValue) => {
-                          setCompletionParams(newParams)
+                  <div className='flex items-center'>
+                    {/* Agent Setting */}
+                    {isAgent && (
+                      <AgentSettingButton
+                        isChatModel={modelConfig.mode === ModelModeType.chat}
+                        agentConfig={modelConfig.agentConfig}
+
+                        isFunctionCall={isFunctionCall}
+                        onAgentSettingChange={(config) => {
+                          const nextConfig = produce(modelConfig, (draft: ModelConfig) => {
+                            draft.agentConfig = config
+                          })
+                          setModelConfig(nextConfig)
                         }}
-                        debugWithMultipleModel={debugWithMultipleModel}
-                        onDebugWithMultipleModelChange={handleDebugWithMultipleModelChange}
                       />
-                      <div className='mx-2 w-[1px] h-[14px] bg-gray-200'></div>
-                    </>
-                  )}
-                  {isMobile && (
-                    <Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
-                      <span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
-                      <CodeBracketIcon className="w-4 h-4 text-gray-500" />
-                    </Button>
-                  )}
-                  <AppPublisher {...{
-                    publishDisabled: cannotPublish,
-                    publishedAt: (modalConfig.created_at || 0) * 1000,
-                    debugWithMultipleModel,
-                    multipleModelConfigs,
-                    onPublish,
-                    onRestore: () => setRestoreConfirmOpen(true),
-                  }} />
+                    )}
+                    {/* Model and Parameters */}
+                    {!debugWithMultipleModel && (
+                      <>
+                        <ModelParameterModal
+                          isAdvancedMode={isAdvancedMode}
+                          mode={mode}
+                          provider={modelConfig.provider}
+                          completionParams={completionParams}
+                          modelId={modelConfig.model_id}
+                          setModel={setModel as any}
+                          onCompletionParamsChange={(newParams: FormValue) => {
+                            setCompletionParams(newParams)
+                          }}
+                          debugWithMultipleModel={debugWithMultipleModel}
+                          onDebugWithMultipleModelChange={handleDebugWithMultipleModelChange}
+                        />
+                        <div className='mx-2 w-[1px] h-[14px] bg-gray-200'></div>
+                      </>
+                    )}
+                    {isMobile && (
+                      <Button className='!h-8 !text-[13px] font-medium' onClick={showDebugPanel}>
+                        <span className='mr-1'>{t('appDebug.operation.debugConfig')}</span>
+                        <CodeBracketIcon className="w-4 h-4 text-gray-500" />
+                      </Button>
+                    )}
+                    <AppPublisher {...{
+                      publishDisabled: cannotPublish,
+                      publishedAt: (latestPublishedAt || 0) * 1000,
+                      debugWithMultipleModel,
+                      multipleModelConfigs,
+                      onPublish,
+                      publishedConfig: publishedConfig!,
+                      resetAppConfig: () => syncToPublishedConfig(publishedConfig!),
+                    }} />
+                  </div>
                 </div>
               </div>
-            </div>
-            <div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
-              <Config />
-            </div>
-            {!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
-              <div className='flex flex-col h-0 border-t border-l grow rounded-tl-2xl bg-gray-50 '>
-                <Debug
-                  isAPIKeySet={isAPIKeySet}
-                  onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
-                  inputs={inputs}
-                  modelParameterParams={{
-                    setModel: setModel as any,
-                    onCompletionParamsChange: setCompletionParams,
-                  }}
-                  debugWithMultipleModel={debugWithMultipleModel}
-                  multipleModelConfigs={multipleModelConfigs}
-                  onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
-                />
+              <div className={`w-full sm:w-1/2 shrink-0 flex flex-col h-full ${debugWithMultipleModel && 'max-w-[560px]'}`}>
+                <Config />
               </div>
-            </div>}
+              {!isMobile && <div className="relative flex flex-col w-1/2 h-full overflow-y-auto grow " style={{ borderColor: 'rgba(0, 0, 0, 0.02)' }}>
+                <div className='grow flex flex-col border-t-[0.5px] border-l-[0.5px] rounded-tl-2xl border-components-panel-border bg-chatbot-bg '>
+                  <Debug
+                    isAPIKeySet={isAPIKeySet}
+                    onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
+                    inputs={inputs}
+                    modelParameterParams={{
+                      setModel: setModel as any,
+                      onCompletionParamsChange: setCompletionParams,
+                    }}
+                    debugWithMultipleModel={debugWithMultipleModel}
+                    multipleModelConfigs={multipleModelConfigs}
+                    onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
+                  />
+                </div>
+              </div>}
+            </div>
           </div>
-        </div>
-        {restoreConfirmOpen && (
-          <Confirm
-            title={t('appDebug.resetConfig.title')}
-            content={t('appDebug.resetConfig.message')}
-            isShow={restoreConfirmOpen}
-            onConfirm={resetAppConfig}
-            onCancel={() => setRestoreConfirmOpen(false)}
-          />
-        )}
-        {showUseGPT4Confirm && (
-          <Confirm
-            title={t('appDebug.trailUseGPT4Info.title')}
-            content={t('appDebug.trailUseGPT4Info.description')}
-            isShow={showUseGPT4Confirm}
-            onConfirm={() => {
-              setShowAccountSettingModal({ payload: 'provider' })
-              setShowUseGPT4Confirm(false)
-            }}
-            onCancel={() => setShowUseGPT4Confirm(false)}
-          />
-        )}
-
-        {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()
-            }}
-          />
-        )}
-        {isMobile && (
-          <Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
-            <Debug
-              isAPIKeySet={isAPIKeySet}
-              onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
-              inputs={inputs}
-              modelParameterParams={{
-                setModel: setModel as any,
-                onCompletionParamsChange: setCompletionParams,
+          {showUseGPT4Confirm && (
+            <Confirm
+              title={t('appDebug.trailUseGPT4Info.title')}
+              content={t('appDebug.trailUseGPT4Info.description')}
+              isShow={showUseGPT4Confirm}
+              onConfirm={() => {
+                setShowAccountSettingModal({ payload: 'provider' })
+                setShowUseGPT4Confirm(false)
               }}
-              debugWithMultipleModel={debugWithMultipleModel}
-              multipleModelConfigs={multipleModelConfigs}
-              onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
+              onCancel={() => setShowUseGPT4Confirm(false)}
+            />
+          )}
+
+          {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()
+              }}
+            />
+          )}
+          {isMobile && (
+            <Drawer showClose isOpen={isShowDebugPanel} onClose={hideDebugPanel} mask footer={null} panelClassname='!bg-gray-50'>
+              <Debug
+                isAPIKeySet={isAPIKeySet}
+                onSetting={() => setShowAccountSettingModal({ payload: 'provider' })}
+                inputs={inputs}
+                modelParameterParams={{
+                  setModel: setModel as any,
+                  onCompletionParamsChange: setCompletionParams,
+                }}
+                debugWithMultipleModel={debugWithMultipleModel}
+                multipleModelConfigs={multipleModelConfigs}
+                onMultipleModelConfigsChange={handleMultipleModelConfigsChange}
+              />
+            </Drawer>
+          )}
+          {showAppConfigureFeaturesModal && (
+            <NewFeaturePanel
+              show
+              inWorkflow={false}
+              showFileUpload={false}
+              isChatMode={mode !== 'completion'}
+              disabled={false}
+              onChange={handleFeaturesChange}
+              onClose={() => setShowAppConfigureFeaturesModal(false)}
+              promptVariables={modelConfig.configs.prompt_variables}
+              onAutoAddPromptVariable={handleAddPromptVariable}
             />
-          </Drawer>
-        )}
-      </>
+          )}
+        </>
+      </FeaturesProvider>
     </ConfigContext.Provider>
   )
 }

+ 126 - 164
web/app/components/app/configuration/prompt-value-panel/index.tsx

@@ -1,24 +1,27 @@
 'use client'
 import type { FC } from 'react'
-import React, { useState } from 'react'
+import React, { useMemo, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useContext } from 'use-context-selector'
 import {
   RiArrowDownSLine,
-  RiArrowRightLine,
+  RiArrowRightSLine,
+  RiPlayLargeFill,
 } from '@remixicon/react'
-import {
-  PlayIcon,
-} from '@heroicons/react/24/solid'
 import ConfigContext from '@/context/debug-configuration'
-import type { Inputs, PromptVariable } from '@/models/debug'
+import type { Inputs } from '@/models/debug'
 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'
+import Input from '@/app/components/base/input'
+import Textarea from '@/app/components/base/textarea'
 import Tooltip from '@/app/components/base/tooltip'
 import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
+import FeatureBar from '@/app/components/base/features/new-feature-panel/feature-bar'
 import type { VisionFile, VisionSettings } from '@/types/app'
+import { DEFAULT_VALUE_MAX_LEN } from '@/config'
+import { useStore as useAppStore } from '@/app/components/app/store'
+import cn from '@/utils/classnames'
 
 export type IPromptValuePanelProps = {
   appType: AppType
@@ -42,15 +45,15 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
     return key && key?.trim() && name && name?.trim()
   })
 
-  const promptVariableObj = (() => {
+  const promptVariableObj = useMemo(() => {
     const obj: Record<string, boolean> = {}
     promptVariables.forEach((input) => {
       obj[input.key] = true
     })
     return obj
-  })()
+  }, [promptVariables])
 
-  const canNotRun = (() => {
+  const canNotRun = useMemo(() => {
     if (mode !== AppType.completion)
       return true
 
@@ -61,19 +64,8 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
     }
 
     else { return !modelConfig.configs.prompt_template }
-  })()
-  const renderRunButton = () => {
-    return (
-      <Button
-        variant="primary"
-        disabled={canNotRun}
-        onClick={() => onSend && onSend()}
-        className="w-[80px] !h-8">
-        <PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
-        <span className='uppercase text-[13px]'>{t('appDebug.inputs.run')}</span>
-      </Button>
-    )
-  }
+  }, [chatPromptConfig.prompt, completionPromptConfig.prompt?.text, isAdvancedMode, mode, modelConfig.configs.prompt_template, modelModeType])
+
   const handleInputValueChange = (key: string, value: string) => {
     if (!(key in promptVariableObj))
       return
@@ -94,159 +86,129 @@ const PromptValuePanel: FC<IPromptValuePanelProps> = ({
     setInputs(newInputs)
   }
 
+  const setShowAppConfigureFeaturesModal = useAppStore(s => s.setShowAppConfigureFeaturesModal)
+
   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)',
-    }}>
-      <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)}>
-            {
-              userInputFieldCollapse
-                ? <RiArrowRightLine className='w-3 h-3 text-gray-300' />
-                : <RiArrowDownSLine className='w-3 h-3 text-gray-300' />
-            }
-            <div className='text-xs font-medium text-gray-800 uppercase'>{t('appDebug.inputs.userInputField')}</div>
+    <>
+      <div className='relative z-[1] mx-3 border-[0.5px] bg-components-panel-on-panel-item-bg border-components-panel-border-subtle rounded-xl shadow-md'>
+        <div className={cn('px-4 pt-3', userInputFieldCollapse ? 'pb-3' : 'pb-1')}>
+          <div className='flex items-center gap-0.5 py-0.5 cursor-pointer' onClick={() => setUserInputFieldCollapse(!userInputFieldCollapse)}>
+            <div className='text-text-secondary system-md-semibold-uppercase'>{t('appDebug.inputs.userInputField')}</div>
+            {userInputFieldCollapse && <RiArrowRightSLine className='w-4 h-4 text-text-secondary'/>}
+            {!userInputFieldCollapse && <RiArrowDownSLine className='w-4 h-4 text-text-secondary'/>}
           </div>
-          {appType === AppType.completion && promptVariables.length > 0 && !userInputFieldCollapse && (
-            <div className="mt-1 text-xs leading-normal text-gray-500">{t('appDebug.inputs.completionVarTip')}</div>
+          {!userInputFieldCollapse && (
+            <div className='mt-1 text-text-tertiary system-xs-regular'>{t('appDebug.inputs.completionVarTip')}</div>
           )}
         </div>
-        {!userInputFieldCollapse && (
-          <>
-            {
-              promptVariables.length > 0
-                ? (
-                  <div className="space-y-3 ">
-                    {promptVariables.map(({ key, name, type, options, max_length, required }) => (
-                      <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'
-                            defaultValue={inputs[key] as string}
-                            onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
-                            items={(options || []).map(i => ({ name: i, value: i }))}
-                            allowSearch={false}
-                            bgClassName='bg-gray-50'
-                          />
-                        )
-                        }
-                        {type === 'string' && (
-                          <input
-                            className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
-                            placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-                            type="text"
-                            value={inputs[key] ? `${inputs[key]}` : ''}
-                            onChange={(e) => { handleInputValueChange(key, e.target.value) }}
-                            maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
-                          />
-                        )}
-                        {type === 'paragraph' && (
-                          <textarea
-                            className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-[120px] bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
-                            placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-                            value={inputs[key] ? `${inputs[key]}` : ''}
-                            onChange={(e) => { handleInputValueChange(key, e.target.value) }}
-                          />
-                        )}
-                        {type === 'number' && (
-                          <input
-                            className="w-full px-3 text-sm leading-9 text-gray-900 border-0 rounded-lg grow h-9 bg-gray-50 focus:outline-none focus:ring-1 focus:ring-inset focus:ring-gray-200"
-                            placeholder={`${name}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-                            type="number"
-                            value={inputs[key] ? `${inputs[key]}` : ''}
-                            onChange={(e) => { handleInputValueChange(key, e.target.value) }}
-                          />
-                        )}
-                      </div>
-                    ))}
+        {!userInputFieldCollapse && promptVariables.length > 0 && (
+          <div className='px-4 pt-3 pb-4'>
+            {promptVariables.map(({ key, name, type, options, max_length, required }, index) => (
+              <div
+                key={key}
+                className='mb-4 last-of-type:mb-0'
+              >
+                <div>
+                  <div className='h-6 mb-1 flex items-center gap-1 text-text-secondary system-sm-semibold'>
+                    <div className='truncate'>{name || key}</div>
+                    {!required && <span className='text-text-tertiary system-xs-regular'>{t('workflow.panel.optional')}</span>}
                   </div>
-                )
-                : (
-                  <div className='text-xs text-gray-500'>{t('appDebug.inputs.noVar')}</div>
-                )
-            }
-            {
-              appType === AppType.completion && visionConfig?.enabled && (
-                <div className="mt-3 xl:flex justify-between">
-                  <div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div>
                   <div className='grow'>
-                    <TextGenerationImageUploader
-                      settings={visionConfig}
-                      onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
-                        type: 'image',
-                        transfer_method: fileItem.type,
-                        url: fileItem.url,
-                        upload_file_id: fileItem.fileId,
-                      })))}
-                    />
+                    {type === 'string' && (
+                      <Input
+                        value={inputs[key] ? `${inputs[key]}` : ''}
+                        onChange={(e) => { handleInputValueChange(key, e.target.value) }}
+                        placeholder={name}
+                        autoFocus={index === 0}
+                        maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
+                      />
+                    )}
+                    {type === 'paragraph' && (
+                      <Textarea
+                        className='grow h-[120px]'
+                        placeholder={name}
+                        value={inputs[key] ? `${inputs[key]}` : ''}
+                        onChange={(e) => { handleInputValueChange(key, e.target.value) }}
+                      />
+                    )}
+                    {type === 'select' && (
+                      <Select
+                        className='w-full'
+                        defaultValue={inputs[key] as string}
+                        onSelect={(i) => { handleInputValueChange(key, i.value as string) }}
+                        items={(options || []).map(i => ({ name: i, value: i }))}
+                        allowSearch={false}
+                        bgClassName='bg-gray-50'
+                      />
+                    )}
+                    {type === 'number' && (
+                      <Input
+                        type='number'
+                        value={inputs[key] ? `${inputs[key]}` : ''}
+                        onChange={(e) => { handleInputValueChange(key, e.target.value) }}
+                        placeholder={name}
+                        autoFocus={index === 0}
+                        maxLength={max_length || DEFAULT_VALUE_MAX_LEN}
+                      />
+                    )}
                   </div>
                 </div>
-              )
-            }
-          </>
-        )
-        }
-      </div>
-
-      {
-        appType === AppType.completion && (
-          <div>
-            <div className="mt-5 border-b border-gray-100"></div>
-            <div className="flex justify-between mt-4 px-4">
+              </div>
+            ))}
+            {visionConfig?.enabled && (
+              <div className="mt-3 xl:flex justify-between">
+                <div className="mr-1 py-2 shrink-0 w-[120px] text-sm text-gray-900">{t('common.imageUploader.imageUpload')}</div>
+                <div className='grow'>
+                  <TextGenerationImageUploader
+                    settings={visionConfig}
+                    onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
+                      type: 'image',
+                      transfer_method: fileItem.type,
+                      url: fileItem.url,
+                      upload_file_id: fileItem.fileId,
+                    })))}
+                  />
+                </div>
+              </div>
+            )}
+          </div>
+        )}
+        {!userInputFieldCollapse && (
+          <div className='flex justify-between p-4 pt-3 border-t border-divider-subtle'>
+            <Button className='w-[72px]' onClick={onClear}>{t('common.operation.clear')}</Button>
+            {canNotRun && (
+              <Tooltip popupContent={t('appDebug.otherError.promptNoBeEmpty')} needsDelay>
+                <Button
+                  variant="primary"
+                  disabled={canNotRun}
+                  onClick={() => onSend && onSend()}
+                  className="w-[96px]">
+                  <RiPlayLargeFill className="shrink-0 w-4 h-4 mr-0.5" aria-hidden="true" />
+                  {t('appDebug.inputs.run')}
+                </Button>
+              </Tooltip>
+            )}
+            {!canNotRun && (
               <Button
-                onClick={onClear}
-                disabled={false}
-              >
-                <span className='text-[13px]'>{t('common.operation.clear')}</span>
+                variant="primary"
+                disabled={canNotRun}
+                onClick={() => onSend && onSend()}
+                className="w-[96px]">
+                <RiPlayLargeFill className="shrink-0 w-4 h-4 mr-0.5" aria-hidden="true" />
+                {t('appDebug.inputs.run')}
               </Button>
-
-              {canNotRun
-                ? (<Tooltip
-                  popupContent={t('appDebug.otherError.promptNoBeEmpty')}
-                  needsDelay
-                >
-                  {renderRunButton()}
-                </Tooltip>)
-                : renderRunButton()}
-            </div>
+            )}
           </div>
-        )
-      }
-    </div>
+        )}
+      </div>
+      <div className='mx-3'>
+        <FeatureBar
+          showFileUpload={false}
+          isChatMode={appType !== AppType.completion}
+          onFeatureBarClick={setShowAppConfigureFeaturesModal} />
+      </div>
+    </>
   )
 }
 
 export default React.memo(PromptValuePanel)
-
-function replaceStringWithValuesWithFormat(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
-  return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
-    const name = inputs[key]
-    if (name) { // has set value
-      return `<div class='inline-block px-1 rounded-md text-gray-900' style='background: rgba(16, 24, 40, 0.1)'>${name}</div>`
-    }
-
-    const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
-    return `<div class='inline-block px-1 rounded-md text-gray-500' style='background: rgba(16, 24, 40, 0.05)'>${valueObj ? valueObj.name : match}</div>`
-  })
-}
-
-export function replaceStringWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
-  return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
-    const name = inputs[key]
-    if (name) { // has set value
-      return name
-    }
-
-    const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
-    return valueObj ? `{{${valueObj.name}}}` : match
-  })
-}
-
-// \n -> br
-function format(str: string) {
-  return str.replaceAll('\n', '<br>')
-}

+ 13 - 0
web/app/components/app/configuration/prompt-value-panel/utils.ts

@@ -0,0 +1,13 @@
+import type { PromptVariable } from '@/models/debug'
+
+export function replaceStringWithValues(str: string, promptVariables: PromptVariable[], inputs: Record<string, any>) {
+  return str.replace(/\{\{([^}]+)\}\}/g, (match, key) => {
+    const name = inputs[key]
+    if (name) { // has set value
+      return name
+    }
+
+    const valueObj: PromptVariable | undefined = promptVariables.find(v => v.key === key)
+    return valueObj ? `{{${valueObj.name}}}` : match
+  })
+}

+ 0 - 80
web/app/components/app/configuration/toolbox/moderation/index.tsx

@@ -1,80 +0,0 @@
-import { useTranslation } from 'react-i18next'
-import useSWR from 'swr'
-import { useContext } from 'use-context-selector'
-import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
-import { Settings01 } from '@/app/components/base/icons/src/vender/line/general'
-import { useModalContext } from '@/context/modal-context'
-import ConfigContext from '@/context/debug-configuration'
-import { fetchCodeBasedExtensionList } from '@/service/common'
-import I18n from '@/context/i18n'
-const Moderation = () => {
-  const { t } = useTranslation()
-  const { setShowModerationSettingModal } = useModalContext()
-  const { locale } = useContext(I18n)
-  const {
-    moderationConfig,
-    setModerationConfig,
-  } = useContext(ConfigContext)
-  const { data: codeBasedExtensionList } = useSWR(
-    '/code-based-extension?module=moderation',
-    fetchCodeBasedExtensionList,
-  )
-
-  const handleOpenModerationSettingModal = () => {
-    setShowModerationSettingModal({
-      payload: moderationConfig,
-      onSaveCallback: setModerationConfig,
-    })
-  }
-
-  const renderInfo = () => {
-    let prefix = ''
-    let suffix = ''
-    if (moderationConfig.type === 'openai_moderation')
-      prefix = t('appDebug.feature.moderation.modal.provider.openai')
-    else if (moderationConfig.type === 'keywords')
-      prefix = t('appDebug.feature.moderation.modal.provider.keywords')
-    else if (moderationConfig.type === 'api')
-      prefix = t('common.apiBasedExtension.selector.title')
-    else
-      prefix = codeBasedExtensionList?.data.find(item => item.name === moderationConfig.type)?.label[locale] || ''
-
-    if (moderationConfig.config?.inputs_config?.enabled && moderationConfig.config?.outputs_config?.enabled)
-      suffix = t('appDebug.feature.moderation.allEnabled')
-    else if (moderationConfig.config?.inputs_config?.enabled)
-      suffix = t('appDebug.feature.moderation.inputEnabled')
-    else if (moderationConfig.config?.outputs_config?.enabled)
-      suffix = t('appDebug.feature.moderation.outputEnabled')
-
-    return `${prefix} · ${suffix}`
-  }
-
-  return (
-    <div className='flex items-center px-3 h-12 bg-gray-50 rounded-xl overflow-hidden'>
-      <div className='shrink-0 flex items-center justify-center mr-1 w-6 h-6'>
-        <FileSearch02 className='shrink-0 w-4 h-4 text-[#039855]' />
-      </div>
-      <div className='shrink-0 mr-2 whitespace-nowrap text-sm text-gray-800 font-semibold'>
-        {t('appDebug.feature.moderation.title')}
-      </div>
-      <div
-        className='grow block w-0 text-right text-xs text-gray-500 truncate'
-        title={renderInfo()}>
-        {renderInfo()}
-      </div>
-      <div className='shrink-0 ml-4 mr-1 w-[1px] h-3.5 bg-gray-200'></div>
-      <div
-        className={`
-          shrink-0 flex items-center px-3 h-7 cursor-pointer rounded-md
-          text-xs text-gray-700 font-medium hover:bg-gray-200
-        `}
-        onClick={handleOpenModerationSettingModal}
-      >
-        <Settings01 className='mr-[5px] w-3.5 h-3.5' />
-        {t('common.operation.settings')}
-      </div>
-    </div>
-  )
-}
-
-export default Moderation

+ 0 - 46
web/app/components/app/configuration/toolbox/score-slider/index.tsx

@@ -1,46 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import Slider from '@/app/components/app/configuration/toolbox/score-slider/base-slider'
-
-type Props = {
-  className?: string
-  value: number
-  onChange: (value: number) => void
-}
-
-const ScoreSlider: FC<Props> = ({
-  className,
-  value,
-  onChange,
-}) => {
-  const { t } = useTranslation()
-
-  return (
-    <div className={className}>
-      <div className='h-[1px] mt-[14px]'>
-        <Slider
-          max={100}
-          min={80}
-          step={1}
-          value={value}
-          onChange={onChange}
-        />
-      </div>
-      <div className='mt-[10px] flex justify-between items-center leading-4 text-xs font-normal '>
-        <div className='flex space-x-1 text-[#00A286]'>
-          <div>0.8</div>
-          <div>·</div>
-          <div>{t('appDebug.feature.annotation.scoreThreshold.easyMatch')}</div>
-        </div>
-        <div className='flex space-x-1 text-[#0057D8]'>
-          <div>1.0</div>
-          <div>·</div>
-          <div>{t('appDebug.feature.annotation.scoreThreshold.accurateMatch')}</div>
-        </div>
-      </div>
-    </div>
-  )
-}
-export default React.memo(ScoreSlider)

+ 1 - 1
web/app/components/app/configuration/tools/external-data-tool-modal.tsx

@@ -3,7 +3,7 @@ import { useState } from 'react'
 import useSWR from 'swr'
 import { useContext } from 'use-context-selector'
 import { useTranslation } from 'react-i18next'
-import FormGeneration from '../toolbox/moderation/form-generation'
+import FormGeneration from '@/app/components/base/features/new-feature-panel/moderation/form-generation'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
 import EmojiPicker from '@/app/components/base/emoji-picker'

+ 6 - 4
web/app/components/app/create-app-modal/index.tsx

@@ -19,6 +19,8 @@ import type { AppMode } from '@/types/app'
 import { createApp } from '@/service/apps'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
+import Textarea from '@/app/components/base/textarea'
 import AppIcon from '@/app/components/base/app-icon'
 import AppsFull from '@/app/components/billing/apps-full-in-dialog'
 import { AiText, ChatBot, CuteRobot } from '@/app/components/base/icons/src/vender/solid/communication'
@@ -275,11 +277,11 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
             size='large' className='cursor-pointer'
             onClick={() => { setShowAppIconPicker(true) }}
           />
-          <input
+          <Input
             value={name}
             onChange={e => setName(e.target.value)}
             placeholder={t('app.newApp.appNamePlaceholder') || ''}
-            className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
+            className='grow h-10'
           />
         </div>
         {showAppIconPicker && <AppIconPicker
@@ -295,8 +297,8 @@ const CreateAppModal = ({ show, onSuccess, onClose }: CreateAppDialogProps) => {
       {/* description */}
       <div className='pt-2 px-8'>
         <div className='py-2 text-sm font-medium leading-[20px] text-gray-900'>{t('app.newApp.captionDescription')}</div>
-        <textarea
-          className='w-full px-3 py-2 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs h-[80px] resize-none'
+        <Textarea
+          className='resize-none'
           placeholder={t('app.newApp.appDescriptionPlaceholder') || ''}
           value={description}
           onChange={e => setDescription(e.target.value)}

+ 2 - 2
web/app/components/app/create-from-dsl-modal/index.tsx

@@ -8,6 +8,7 @@ import { useTranslation } from 'react-i18next'
 import { RiCloseLine } from '@remixicon/react'
 import Uploader from './uploader'
 import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
 import Modal from '@/app/components/base/modal'
 import { ToastContext } from '@/app/components/base/toast'
 import {
@@ -171,9 +172,8 @@ const CreateFromDSLModal = ({ show, onSuccess, onClose, activeTab = CreateFromDS
           currentTab === CreateFromDSLModalTab.FROM_URL && (
             <div>
               <div className='mb-1 system-md-semibold leading6'>DSL URL</div>
-              <input
+              <Input
                 placeholder={t('app.importFromDSLUrlPlaceholder') || ''}
-                className='px-2 w-full h-8 border border-components-input-border-active bg-components-input-bg-active rounded-lg outline-none appearance-none placeholder:text-components-input-text-placeholder system-sm-regular'
                 value={dslUrlValue}
                 onChange={e => setDslUrlValue(e.target.value)}
               />

+ 3 - 2
web/app/components/app/duplicate-modal/index.tsx

@@ -6,6 +6,7 @@ import s from './style.module.css'
 import cn from '@/utils/classnames'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
 import Toast from '@/app/components/base/toast'
 import AppIcon from '@/app/components/base/app-icon'
 import { useProviderContext } from '@/context/provider-context'
@@ -87,10 +88,10 @@ const DuplicateAppModal = ({
               background={appIcon.type === 'image' ? undefined : appIcon.background}
               imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
             />
-            <input
+            <Input
               value={name}
               onChange={e => setName(e.target.value)}
-              className='h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg grow'
+              className='h-10'
             />
           </div>
           {isAppsFull && <AppsFull loc='app-duplicate-create' />}

+ 2 - 2
web/app/components/app/log-annotation/index.tsx

@@ -8,7 +8,7 @@ import Log from '@/app/components/app/log'
 import WorkflowLog from '@/app/components/app/workflow-log'
 import Annotation from '@/app/components/app/annotation'
 import Loading from '@/app/components/base/loading'
-import { PageType } from '@/app/components/app/configuration/toolbox/annotation/type'
+import { PageType } from '@/app/components/base/features/new-feature-panel/annotation-reply/type'
 import TabSlider from '@/app/components/base/tab-slider-plain'
 import { useStore as useAppStore } from '@/app/components/app/store'
 
@@ -41,7 +41,7 @@ const LogAnnotation: FC<Props> = ({
   }
 
   return (
-    <div className='pt-4 px-6 h-full flex flex-col'>
+    <div className='pt-3 px-6 h-full flex flex-col'>
       {appDetail.mode !== 'workflow' && (
         <TabSlider
           className='shrink-0'

+ 38 - 40
web/app/components/app/log/filter.tsx

@@ -2,14 +2,13 @@
 import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
-import {
-  MagnifyingGlassIcon,
-} from '@heroicons/react/24/solid'
 import useSWR from 'swr'
 import dayjs from 'dayjs'
+import { RiCalendarLine } from '@remixicon/react'
 import quarterOfYear from 'dayjs/plugin/quarterOfYear'
 import type { QueryParam } from './index'
-import { SimpleSelect } from '@/app/components/base/select'
+import Chip from '@/app/components/base/chip'
+import Input from '@/app/components/base/input'
 import Sort from '@/app/components/base/sort'
 import { fetchAnnotationsCount } from '@/service/log'
 dayjs.extend(quarterOfYear)
@@ -41,45 +40,44 @@ const Filter: FC<IFilterProps> = ({ isChatMode, appId, queryParams, setQueryPara
   if (!data)
     return null
   return (
-    <div className='flex flex-row flex-wrap gap-2 items-center mb-4 text-gray-900 text-base'>
-      <SimpleSelect
+    <div className='flex flex-row flex-wrap gap-2 items-center mb-2'>
+      <Chip
+        className='min-w-[150px]'
+        panelClassName='w-[270px]'
+        leftIcon={<RiCalendarLine className='h-4 w-4 text-text-secondary' />}
+        value={queryParams.period || 7}
+        onSelect={(item) => {
+          setQueryParams({ ...queryParams, period: item.value as string })
+        }}
+        onClear={() => setQueryParams({ ...queryParams, period: 7 })}
         items={TIME_PERIOD_LIST.map(item => ({ value: item.value, name: t(`appLog.filter.period.${item.name}`) }))}
-        className='mt-0 !w-40'
+      />
+      <Chip
+        className='min-w-[150px]'
+        panelClassName='w-[270px]'
+        showLeftIcon={false}
+        value={queryParams.annotation_status || 'all'}
         onSelect={(item) => {
-          setQueryParams({ ...queryParams, period: item.value })
+          setQueryParams({ ...queryParams, annotation_status: item.value as string })
+        }}
+        onClear={() => setQueryParams({ ...queryParams, annotation_status: 'all' })}
+        items={[
+          { value: 'all', name: t('appLog.filter.annotation.all') },
+          { value: 'annotated', name: t('appLog.filter.annotation.annotated', { count: data?.count }) },
+          { value: 'not_annotated', name: t('appLog.filter.annotation.not_annotated') },
+        ]}
+      />
+      <Input
+        wrapperClassName='w-[200px]'
+        showLeftIcon
+        showClearIcon
+        value={queryParams.keyword}
+        placeholder={t('common.operation.search')!}
+        onChange={(e) => {
+          setQueryParams({ ...queryParams, keyword: e.target.value })
         }}
-        defaultValue={queryParams.period} />
-      <div className="relative rounded-md">
-        <SimpleSelect
-          defaultValue={'all'}
-          className='!w-[300px]'
-          onSelect={
-            (item) => {
-              if (!item.value)
-                return
-              setQueryParams({ ...queryParams, annotation_status: item.value as string })
-            }
-          }
-          items={[{ value: 'all', name: t('appLog.filter.annotation.all') },
-            { value: 'annotated', name: t('appLog.filter.annotation.annotated', { count: data?.count }) },
-            { value: 'not_annotated', name: t('appLog.filter.annotation.not_annotated') }]}
-        />
-      </div>
-      <div className="relative">
-        <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
-          <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
-        </div>
-        <input
-          type="text"
-          name="query"
-          className="block w-[180px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
-          placeholder={t('common.operation.search')!}
-          value={queryParams.keyword}
-          onChange={(e) => {
-            setQueryParams({ ...queryParams, keyword: e.target.value })
-          }}
-        />
-      </div>
+        onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
+      />
       {isChatMode && (
         <>
           <div className='w-px h-3.5 bg-divider-regular'></div>

+ 5 - 5
web/app/components/app/log/index.tsx

@@ -40,12 +40,12 @@ const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
   const pathSegments = pathname.split('/')
   pathSegments.pop()
   return <div className='flex items-center justify-center h-full'>
-    <div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
-      <span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
-      <div className='mt-2 text-gray-500 text-sm font-normal'>
+    <div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
+      <span className='text-text-secondary system-md-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
+      <div className='mt-2 text-text-tertiary system-sm-regular'>
         <Trans
           i18nKey="appLog.table.empty.element.content"
-          components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' rel='noopener noreferrer' /> }}
+          components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-util-colors-blue-blue-600' />, testLink: <Link href={appUrl} className='text-util-colors-blue-blue-600' target='_blank' rel='noopener noreferrer' /> }}
         />
       </div>
     </div>
@@ -103,7 +103,7 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
 
   return (
     <div className='flex flex-col h-full'>
-      <p className='flex text-sm font-normal text-gray-500'>{t('appLog.description')}</p>
+      <p className='text-text-tertiary system-sm-regular'>{t('appLog.description')}</p>
       <div className='flex flex-col py-4 flex-1'>
         <Filter isChatMode={isChatMode} appId={appDetail.id} queryParams={queryParams} setQueryParams={setQueryParams} />
         {total === undefined

+ 38 - 30
web/app/components/app/log/list.tsx

@@ -17,11 +17,10 @@ import { createContext, useContext } from 'use-context-selector'
 import { useShallow } from 'zustand/react/shallow'
 import { useTranslation } from 'react-i18next'
 import { UUID_NIL } from '../../base/chat/constants'
-import s from './style.module.css'
 import VarPanel from './var-panel'
 import cn from '@/utils/classnames'
 import type { FeedbackFunc, FeedbackType, IChatItem, SubmitAnnotationFunc } from '@/app/components/base/chat/chat/type'
-import type { Annotation, ChatConversationFullDetailResponse, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationFullDetailResponse, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
+import type { Annotation, ChatConversationGeneralDetail, ChatConversationsResponse, ChatMessage, ChatMessagesRequest, CompletionConversationGeneralDetail, CompletionConversationsResponse, LogAnnotation } from '@/models/log'
 import type { App } from '@/types/app'
 import Loading from '@/app/components/base/loading'
 import Drawer from '@/app/components/base/drawer'
@@ -42,6 +41,7 @@ import { useAppContext } from '@/context/app-context'
 import useTimestamp from '@/hooks/use-timestamp'
 import Tooltip from '@/app/components/base/tooltip'
 import { CopyIcon } from '@/app/components/base/copy-icon'
+import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
 
 dayjs.extend(utc)
 dayjs.extend(timezone)
@@ -83,6 +83,7 @@ const PARAM_MAP = {
 }
 
 function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId: string, timezone: string, format: string) {
+  const answerFiles = item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || []
   newChatList.push({
     id: item.id,
     content: item.answer,
@@ -91,7 +92,7 @@ function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId:
     adminFeedback: item.feedbacks.find((item: any) => item.from_source === 'admin'), // admin feedback
     feedbackDisabled: false,
     isAnswer: true,
-    message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
+    message_files: getProcessedFilesFromResponse(answerFiles.map((item: any) => ({ ...item, related_id: item.id }))),
     log: [
       ...item.message,
       ...(item.message[item.message.length - 1]?.role !== 'assistant'
@@ -138,11 +139,12 @@ function appendQAToChatList(newChatList: IChatItem[], item: any, conversationId:
     })(),
     parentMessageId: `question-${item.id}`,
   })
+  const questionFiles = item.message_files?.filter((file: any) => file.belongs_to === 'user') || []
   newChatList.push({
     id: `question-${item.id}`,
     content: item.inputs.query || item.inputs.default_input || item.query, // text generation: item.inputs.query; chat: item.query
     isAnswer: false,
-    message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
+    message_files: getProcessedFilesFromResponse(questionFiles.map((item: any) => ({ ...item, related_id: item.id }))),
     parentMessageId: item.parent_message_id || undefined,
   })
 }
@@ -173,13 +175,13 @@ const getFormattedChatList = (messages: ChatMessage[], conversationId: string, t
 // const displayedParams = CompletionParams.slice(0, -2)
 const validatedParams = ['temperature', 'top_p', 'presence_penalty', 'frequency_penalty']
 
-type IDetailPanel<T> = {
+type IDetailPanel = {
   detail: any
   onFeedback: FeedbackFunc
   onSubmitAnnotation: SubmitAnnotationFunc
 }
 
-function DetailPanel<T extends ChatConversationFullDetailResponse | CompletionConversationFullDetailResponse>({ detail, onFeedback }: IDetailPanel<T>) {
+function DetailPanel({ detail, onFeedback }: IDetailPanel) {
   const { userProfile: { timezone } } = useAppContext()
   const { formatTime } = useTimestamp()
   const { onClose, appDetail } = useContext(DrawerContext)
@@ -597,7 +599,7 @@ const CompletionConversationDetailComp: FC<{ appId?: string; conversationId?: st
   if (!conversationDetail)
     return null
 
-  return <DetailPanel<CompletionConversationFullDetailResponse>
+  return <DetailPanel
     detail={conversationDetail}
     onFeedback={handleFeedback}
     onSubmitAnnotation={handleAnnotation}
@@ -640,7 +642,7 @@ const ChatConversationDetailComp: FC<{ appId?: string; conversationId?: string }
   if (!conversationDetail)
     return null
 
-  return <DetailPanel<ChatConversationFullDetailResponse>
+  return <DetailPanel
     detail={conversationDetail}
     onFeedback={handleFeedback}
     onSubmitAnnotation={handleAnnotation}
@@ -666,13 +668,13 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
     return (
       <Tooltip
         popupContent={
-          <span className='text-xs text-gray-500 inline-flex items-center'>
+          <span className='text-xs text-text-tertiary inline-flex items-center'>
             <RiEditFill className='w-3 h-3 mr-1' />{`${t('appLog.detail.annotationTip', { user: annotation?.account?.name })} ${formatTime(annotation?.created_at || dayjs().unix(), 'MM-DD hh:mm A')}`}
           </span>
         }
         popupClassName={(isHighlight && !isChatMode) ? '' : '!hidden'}
       >
-        <div className={cn(isEmptyStyle ? 'text-gray-400' : 'text-gray-700', !isHighlight ? '' : 'bg-orange-100', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
+        <div className={cn(isEmptyStyle ? 'text-text-quaternary' : 'text-text-secondary', !isHighlight ? '' : 'bg-orange-100', 'system-sm-regular overflow-hidden text-ellipsis whitespace-nowrap')}>
           {value || '-'}
         </div>
       </Tooltip>
@@ -690,40 +692,46 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
 
   return (
     <div className='overflow-x-auto'>
-      <table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
-        <thead className="h-8 leading-8 border-b border-gray-200 text-gray-500 font-bold">
+      <table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
+        <thead className='system-xs-medium-uppercase text-text-tertiary'>
           <tr>
-            <td className='w-[1.375rem] whitespace-nowrap'></td>
-            <td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
-            <td className='whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.updatedTime')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.time')}</td>
+            <td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'></td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.summary') : t('appLog.table.header.input')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.endUser')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{isChatMode ? t('appLog.table.header.messageCount') : t('appLog.table.header.output')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.userRate')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.adminRate')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.updatedTime')}</td>
+            <td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.time')}</td>
           </tr>
         </thead>
-        <tbody className="text-gray-500">
+        <tbody className="text-text-secondary system-sm-regular">
           {logs.data.map((log: any) => {
             const endUser = log.from_end_user_session_id || log.from_account_name
             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}
-              className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentConversation?.id !== log.id ? '' : 'bg-gray-50'}`}
+              className={cn('border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer', currentConversation?.id !== log.id ? '' : 'bg-background-default-hover')}
               onClick={() => {
                 setShowDrawer(true)
                 setCurrentConversation(log)
               }}>
-              <td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
-              <td style={{ maxWidth: isChatMode ? 300 : 200 }}>
+              <td className='h-4'>
+                {!log.read_at && (
+                  <div className='p-3 pr-0.5 flex items-center'>
+                    <span className='inline-block bg-util-colors-blue-blue-500 h-1.5 w-1.5 rounded'></span>
+                  </div>
+                )}
+              </td>
+              <td className='p-3 pr-2 w-[160px]' style={{ maxWidth: isChatMode ? 300 : 200 }}>
                 {renderTdValue(leftValue || t('appLog.table.empty.noChat'), !leftValue, isChatMode && log.annotated)}
               </td>
-              <td>{renderTdValue(endUser || defaultValue, !endUser)}</td>
-              <td style={{ maxWidth: isChatMode ? 100 : 200 }}>
+              <td className='p-3 pr-2'>{renderTdValue(endUser || defaultValue, !endUser)}</td>
+              <td className='p-3 pr-2' style={{ maxWidth: isChatMode ? 100 : 200 }}>
                 {renderTdValue(rightValue === 0 ? 0 : (rightValue || t('appLog.table.empty.noOutput')), !rightValue, !isChatMode && !!log.annotation?.content, log.annotation)}
               </td>
-              <td>
+              <td className='p-3 pr-2'>
                 {(!log.user_feedback_stats.like && !log.user_feedback_stats.dislike)
                   ? renderTdValue(defaultValue, true)
                   : <>
@@ -732,7 +740,7 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
                   </>
                 }
               </td>
-              <td>
+              <td className='p-3 pr-2'>
                 {(!log.admin_feedback_stats.like && !log.admin_feedback_stats.dislike)
                   ? renderTdValue(defaultValue, true)
                   : <>
@@ -741,8 +749,8 @@ const ConversationList: FC<IConversationList> = ({ logs, appDetail, onRefresh })
                   </>
                 }
               </td>
-              <td className='w-[160px]'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
-              <td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
+              <td className='w-[160px] p-3 pr-2'>{formatTime(log.updated_at, t('appLog.dateTimeFormat') as string)}</td>
+              <td className='w-[160px] p-3 pr-2'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
             </tr>
           })}
         </tbody>

+ 0 - 6
web/app/components/app/log/style.module.css

@@ -1,9 +1,3 @@
-.logTable td {
-  padding: 7px 8px;
-  box-sizing: border-box;
-  max-width: 200px;
-}
-
 .pagination li {
   list-style: none;
 }

+ 19 - 9
web/app/components/app/overview/settings/index.tsx

@@ -8,6 +8,8 @@ import { useContextSelector } from 'use-context-selector'
 import s from './style.module.css'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
+import Textarea from '@/app/components/base/textarea'
 import AppIcon from '@/app/components/base/app-icon'
 import Switch from '@/app/components/base/switch'
 import { SimpleSelect } from '@/app/components/base/select'
@@ -183,6 +185,10 @@ const SettingsModal: FC<ISettingsModalProps> = ({
     }
   }
 
+  const onDesChange = (value: string) => {
+    setInputInfo(item => ({ ...item, desc: value }))
+  }
+
   return (
     <>
       <Modal
@@ -201,7 +207,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
             background={appIcon.type === 'image' ? undefined : appIcon.background}
             imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
           />
-          <input className={`flex-grow rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
+          <Input
+            className='grow h-10'
             value={inputInfo.title}
             onChange={onChange('title')}
             placeholder={t('app.appNamePlaceholder') || ''}
@@ -209,11 +216,10 @@ const SettingsModal: FC<ISettingsModalProps> = ({
         </div>
         <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900 `}>{t(`${prefixSettings}.webDesc`)}</div>
         <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.webDescTip`)}</p>
-        <textarea
-          rows={3}
-          className={`mt-2 pt-2 pb-2 px-3 rounded-lg bg-gray-100 w-full ${s.settingsTip} text-gray-900`}
+        <Textarea
+          className='mt-2'
           value={inputInfo.desc}
-          onChange={onChange('desc')}
+          onChange={e => onDesChange(e.target.value)}
           placeholder={t(`${prefixSettings}.webDescPlaceholder`) as string}
         />
         {isChatBot && (
@@ -249,7 +255,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
 
         {isChat && <> <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.chatColorTheme`)}</div>
           <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.chatColorThemeDesc`)}</p>
-          <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
+          <Input
+            className='mt-2 h-10'
             value={inputInfo.chatColorTheme ?? ''}
             onChange={onChange('chatColorTheme')}
             placeholder='E.g #A020F0'
@@ -283,7 +290,8 @@ const SettingsModal: FC<ISettingsModalProps> = ({
         {isShowMore && <>
           <hr className='w-full mt-6' />
           <div className={`mt-6 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.copyright`)}</div>
-          <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
+          <Input
+            className='mt-2 h-10'
             value={inputInfo.copyright}
             onChange={onChange('copyright')}
             placeholder={t(`${prefixSettings}.more.copyRightPlaceholder`) as string}
@@ -295,14 +303,16 @@ const SettingsModal: FC<ISettingsModalProps> = ({
               components={{ privacyPolicyLink: <Link href={'https://docs.dify.ai/user-agreement/privacy-policy'} target='_blank' rel='noopener noreferrer' className='text-primary-600' /> }}
             />
           </p>
-          <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
+          <Input
+            className='mt-2 h-10'
             value={inputInfo.privacyPolicy}
             onChange={onChange('privacyPolicy')}
             placeholder={t(`${prefixSettings}.more.privacyPolicyPlaceholder`) as string}
           />
           <div className={`mt-8 font-medium ${s.settingTitle} text-gray-900`}>{t(`${prefixSettings}.more.customDisclaimer`)}</div>
           <p className={`mt-1 ${s.settingsTip} text-gray-500`}>{t(`${prefixSettings}.more.customDisclaimerTip`)}</p>
-          <input className={`w-full mt-2 rounded-lg h-10 box-border px-3 ${s.projectName} bg-gray-100`}
+          <Input
+            className='mt-2 h-10'
             value={inputInfo.customDisclaimer}
             onChange={onChange('customDisclaimer')}
             placeholder={t(`${prefixSettings}.more.customDisclaimerPlaceholder`) as string}

+ 0 - 5
web/app/components/app/overview/settings/style.module.css

@@ -15,9 +15,4 @@
 .policy {
 	font-size: 0.75rem;
 	line-height: 1.125rem;
-}
-
-.projectName {
-	font-size: 0.875rem;
-	line-height: 2.5rem;
 }

+ 4 - 0
web/app/components/app/store.ts

@@ -10,6 +10,7 @@ type State = {
   showPromptLogModal: boolean
   showAgentLogModal: boolean
   showMessageLogModal: boolean
+  showAppConfigureFeaturesModal: boolean
 }
 
 type Action = {
@@ -20,6 +21,7 @@ type Action = {
   setShowPromptLogModal: (showPromptLogModal: boolean) => void
   setShowAgentLogModal: (showAgentLogModal: boolean) => void
   setShowMessageLogModal: (showMessageLogModal: boolean) => void
+  setShowAppConfigureFeaturesModal: (showAppConfigureFeaturesModal: boolean) => void
 }
 
 export const useStore = create<State & Action>(set => ({
@@ -47,4 +49,6 @@ export const useStore = create<State & Action>(set => ({
       }
     }
   }),
+  showAppConfigureFeaturesModal: false,
+  setShowAppConfigureFeaturesModal: showAppConfigureFeaturesModal => set(() => ({ showAppConfigureFeaturesModal })),
 }))

+ 6 - 4
web/app/components/app/switch-app-modal/index.tsx

@@ -8,7 +8,9 @@ import { RiCloseLine } from '@remixicon/react'
 import AppIconPicker from '../../base/app-icon-picker'
 import s from './style.module.css'
 import cn from '@/utils/classnames'
+import Checkbox from '@/app/components/base/checkbox'
 import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
 import Modal from '@/app/components/base/modal'
 import Confirm from '@/app/components/base/confirm'
 import { ToastContext } from '@/app/components/base/toast'
@@ -121,11 +123,11 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
               background={appIcon.type === 'image' ? undefined : appIcon.background}
               imageUrl={appIcon.type === 'image' ? appIcon.url : undefined}
             />
-            <input
+            <Input
               value={name}
               onChange={e => setName(e.target.value)}
               placeholder={t('app.newApp.appNamePlaceholder') || ''}
-              className='grow h-10 px-3 text-sm font-normal bg-gray-100 rounded-lg border border-transparent outline-none appearance-none caret-primary-600 placeholder:text-gray-400 hover:bg-gray-50 hover:border hover:border-gray-300 focus:bg-gray-50 focus:border focus:border-gray-300 focus:shadow-xs'
+              className='grow h-10'
             />
           </div>
           {showAppIconPicker && <AppIconPicker
@@ -144,8 +146,8 @@ const SwitchAppModal = ({ show, appDetail, inAppDetail = false, onSuccess, onClo
         {isAppsFull && <AppsFull loc='app-switch' />}
         <div className='pt-6 flex justify-between items-center'>
           <div className='flex items-center'>
-            <input id="removeOriginal" type="checkbox" checked={removeOriginal} onChange={() => setRemoveOriginal(!removeOriginal)} className="w-4 h-4 rounded border-gray-300 text-blue-700 cursor-pointer focus:ring-blue-700" />
-            <label htmlFor="removeOriginal" className="ml-2 text-sm leading-5 text-gray-700 cursor-pointer">{t('app.removeOriginal')}</label>
+            <Checkbox className='shrink-0' checked={removeOriginal} onCheck={() => setRemoveOriginal(!removeOriginal)} />
+            <div className="ml-2 text-sm leading-5 text-gray-700 cursor-pointer" onClick={() => setRemoveOriginal(!removeOriginal)}>{t('app.removeOriginal')}</div>
           </div>
           <div className='flex items-center'>
             <Button className='mr-2' onClick={onClose}>{t('app.newApp.Cancel')}</Button>

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

@@ -22,8 +22,8 @@ import { 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 AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
 import { fetchTextGenerationMessage } from '@/service/debug'
-import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
 import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
 import { useStore as useAppStore } from '@/app/components/app/store'
 import WorkflowProcessItem from '@/app/components/base/chat/chat/answer/workflow-process'
@@ -307,7 +307,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
             <div className={`flex ${contentClassName}`}>
               <div className='grow w-0'>
                 {siteInfo && siteInfo.show_workflow_steps && workflowProcessData && (
-                  <WorkflowProcessItem grayBg hideInfo data={workflowProcessData} expand={workflowProcessData.expand} hideProcessDetail={hideProcessDetail} />
+                  <WorkflowProcessItem data={workflowProcessData} expand={workflowProcessData.expand} hideProcessDetail={hideProcessDetail} />
                 )}
                 {workflowProcessData && !isError && (
                   <ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} />

+ 21 - 10
web/app/components/app/text-generate/item/result-tab.tsx

@@ -4,12 +4,11 @@ import {
 } from 'react'
 import { useTranslation } from 'react-i18next'
 import cn from '@/utils/classnames'
-// import Loading from '@/app/components/base/loading'
 import { Markdown } from '@/app/components/base/markdown'
 import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
 import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
 import type { WorkflowProcess } from '@/app/components/base/chat/types'
-// import { WorkflowRunningStatus } from '@/app/components/workflow/types'
+import { FileList } from '@/app/components/base/file-uploader'
 
 const ResultTab = ({
   data,
@@ -56,16 +55,28 @@ const ResultTab = ({
       )}
       <div className={cn('grow bg-white')}>
         {currentTab === 'RESULT' && (
-          <Markdown content={data?.resultText || ''} />
+          <>
+            <Markdown content={data?.resultText || ''} />
+            {!!data?.files?.length && (
+              <FileList
+                files={data?.files}
+                showDeleteAction={false}
+                showDownloadAction
+                canPreview
+              />
+            )}
+          </>
         )}
         {currentTab === 'DETAIL' && content && (
-          <CodeEditor
-            readOnly
-            title={<div>JSON OUTPUT</div>}
-            language={CodeLanguage.json}
-            value={content}
-            isJSONStringifyBeauty
-          />
+          <div className='mt-1'>
+            <CodeEditor
+              readOnly
+              title={<div>JSON OUTPUT</div>}
+              language={CodeLanguage.json}
+              value={content}
+              isJSONStringifyBeauty
+            />
+          </div>
         )}
       </div>
     </div>

+ 3 - 3
web/app/components/app/workflow-log/detail.tsx

@@ -13,11 +13,11 @@ const DetailPanel: FC<ILogDetail> = ({ runID, onClose }) => {
   const { t } = useTranslation()
 
   return (
-    <div className='grow relative flex flex-col py-3'>
+    <div className='grow relative flex flex-col pt-3'>
       <span className='absolute right-3 top-4 p-1 cursor-pointer z-20' onClick={onClose}>
-        <RiCloseLine className='w-4 h-4 text-gray-500' />
+        <RiCloseLine className='w-4 h-4 text-text-tertiary' />
       </span>
-      <h1 className='shrink-0 px-4 py-1 text-md font-semibold text-gray-900'>{t('appLog.runDetail.workflowTitle')}</h1>
+      <h1 className='shrink-0 px-4 py-1 text-text-primary system-xl-semibold'>{t('appLog.runDetail.workflowTitle')}</h1>
       <Run runID={runID}/>
     </div>
   )

+ 26 - 38
web/app/components/app/workflow-log/filter.tsx

@@ -2,11 +2,9 @@
 import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
-import {
-  MagnifyingGlassIcon,
-} from '@heroicons/react/24/solid'
 import type { QueryParam } from './index'
-import { SimpleSelect } from '@/app/components/base/select'
+import Chip from '@/app/components/base/chip'
+import Input from '@/app/components/base/input'
 
 type IFilterProps = {
   queryParams: QueryParam
@@ -16,40 +14,30 @@ type IFilterProps = {
 const Filter: FC<IFilterProps> = ({ queryParams, setQueryParams }: IFilterProps) => {
   const { t } = useTranslation()
   return (
-    <div className='flex flex-row flex-wrap gap-y-2 gap-x-4 items-center mb-4 text-gray-900 text-base'>
-      <div className="relative rounded-md">
-        <SimpleSelect
-          defaultValue={'all'}
-          className='!min-w-[100px]'
-          onSelect={
-            (item) => {
-              if (!item.value)
-                return
-              setQueryParams({ ...queryParams, status: item.value as string })
-            }
-          }
-          items={[{ value: 'all', name: 'All' },
-            { value: 'succeeded', name: 'Success' },
-            { value: 'failed', name: 'Fail' },
-            { value: 'stopped', name: 'Stop' },
-          ]}
-        />
-      </div>
-      <div className="relative">
-        <div className="pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
-          <MagnifyingGlassIcon className="h-5 w-5 text-gray-400" aria-hidden="true" />
-        </div>
-        <input
-          type="text"
-          name="query"
-          className="block w-[240px] bg-gray-100 shadow-sm rounded-md border-0 py-1.5 pl-10 text-gray-900 placeholder:text-gray-400 focus:ring-1 focus:ring-inset focus:ring-gray-200 focus-visible:outline-none sm:text-sm sm:leading-6"
-          placeholder={t('common.operation.search')!}
-          value={queryParams.keyword}
-          onChange={(e) => {
-            setQueryParams({ ...queryParams, keyword: e.target.value })
-          }}
-        />
-      </div>
+    <div className='flex flex-row flex-wrap gap-2 mb-2'>
+      <Chip
+        value={queryParams.status || 'all'}
+        onSelect={(item) => {
+          setQueryParams({ ...queryParams, status: item.value as string })
+        }}
+        onClear={() => setQueryParams({ ...queryParams, status: 'all' })}
+        items={[{ value: 'all', name: 'All' },
+          { value: 'succeeded', name: 'Success' },
+          { value: 'failed', name: 'Fail' },
+          { value: 'stopped', name: 'Stop' },
+        ]}
+      />
+      <Input
+        wrapperClassName='w-[200px]'
+        showLeftIcon
+        showClearIcon
+        value={queryParams.keyword}
+        placeholder={t('common.operation.search')!}
+        onChange={(e) => {
+          setQueryParams({ ...queryParams, keyword: e.target.value })
+        }}
+        onClear={() => setQueryParams({ ...queryParams, keyword: '' })}
+      />
     </div>
   )
 }

+ 6 - 6
web/app/components/app/workflow-log/index.tsx

@@ -36,12 +36,12 @@ const EmptyElement: FC<{ appUrl: string }> = ({ appUrl }) => {
   const pathSegments = pathname.split('/')
   pathSegments.pop()
   return <div className='flex items-center justify-center h-full'>
-    <div className='bg-gray-50 w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
-      <span className='text-gray-700 font-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
-      <div className='mt-2 text-gray-500 text-sm font-normal'>
+    <div className='bg-background-section-burn w-[560px] h-fit box-border px-5 py-4 rounded-2xl'>
+      <span className='text-text-secondary system-md-semibold'>{t('appLog.table.empty.element.title')}<ThreeDotsIcon className='inline relative -top-3 -left-1.5' /></span>
+      <div className='mt-2 text-text-tertiary system-sm-regular'>
         <Trans
           i18nKey="appLog.table.empty.element.content"
-          components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-primary-600' />, testLink: <Link href={appUrl} className='text-primary-600' target='_blank' rel='noopener noreferrer' /> }}
+          components={{ shareLink: <Link href={`${pathSegments.join('/')}/overview`} className='text-util-colors-blue-blue-600' />, testLink: <Link href={appUrl} className='text-util-colors-blue-blue-600' target='_blank' rel='noopener noreferrer' /> }}
         />
       </div>
     </div>
@@ -75,8 +75,8 @@ const Logs: FC<ILogsProps> = ({ appDetail }) => {
 
   return (
     <div className='flex flex-col h-full'>
-      <h1 className='text-md font-semibold text-gray-900'>{t('appLog.workflowTitle')}</h1>
-      <p className='flex text-sm font-normal text-gray-500'>{t('appLog.workflowSubtitle')}</p>
+      <h1 className='text-text-primary system-xl-semibold'>{t('appLog.workflowTitle')}</h1>
+      <p className='text-text-tertiary system-sm-regular'>{t('appLog.workflowSubtitle')}</p>
       <div className='flex flex-col py-4 flex-1'>
         <Filter queryParams={queryParams} setQueryParams={setQueryParams} />
         {/* workflow log */}

+ 34 - 30
web/app/components/app/workflow-log/list.tsx

@@ -2,7 +2,7 @@
 import type { FC } from 'react'
 import React, { useState } from 'react'
 import { useTranslation } from 'react-i18next'
-import s from './style.module.css'
+// import s from './style.module.css'
 import DetailPanel from './detail'
 import cn from '@/utils/classnames'
 import type { WorkflowAppLogDetail, WorkflowLogsResponse } from '@/models/log'
@@ -34,33 +34,33 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
   const statusTdRender = (status: string) => {
     if (status === 'succeeded') {
       return (
-        <div className='inline-flex items-center gap-1'>
+        <div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
           <Indicator color={'green'} />
-          <span>Success</span>
+          <span className='text-util-colors-green-green-600'>Success</span>
         </div>
       )
     }
     if (status === 'failed') {
       return (
-        <div className='inline-flex items-center gap-1'>
+        <div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
           <Indicator color={'red'} />
-          <span className='text-red-600'>Fail</span>
+          <span className='text-util-colors-red-red-600'>Fail</span>
         </div>
       )
     }
     if (status === 'stopped') {
       return (
-        <div className='inline-flex items-center gap-1'>
+        <div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
           <Indicator color={'yellow'} />
-          <span>Stop</span>
+          <span className='text-util-colors-warning-warning-600'>Stop</span>
         </div>
       )
     }
     if (status === 'running') {
       return (
-        <div className='inline-flex items-center gap-1'>
+        <div className='inline-flex items-center gap-1 system-xs-semibold-uppercase'>
           <Indicator color={'blue'} />
-          <span className='text-primary-600'>Running</span>
+          <span className='text-util-colors-blue-light-blue-light-600'>Running</span>
         </div>
       )
     }
@@ -77,43 +77,47 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
 
   return (
     <div className='overflow-x-auto'>
-      <table className={`w-full min-w-[440px] border-collapse border-0 text-sm mt-3 ${s.logTable}`}>
-        <thead className="h-8 !pl-3 py-2 leading-[18px] border-b border-gray-200 text-xs text-gray-500 font-medium">
+      <table className={cn('mt-2 w-full min-w-[440px] border-collapse border-0')}>
+        <thead className='system-xs-medium-uppercase text-text-tertiary'>
           <tr>
-            <td className='w-[1.375rem] whitespace-nowrap'></td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.startTime')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.status')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.runtime')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.tokens')}</td>
-            <td className='whitespace-nowrap'>{t('appLog.table.header.user')}</td>
-            {/* <td className='whitespace-nowrap'>{t('appLog.table.header.version')}</td> */}
+            <td className='pl-2 pr-1 w-5 rounded-l-lg bg-background-section-burn whitespace-nowrap'></td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.startTime')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.status')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.runtime')}</td>
+            <td className='pl-3 py-1.5 bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.tokens')}</td>
+            <td className='pl-3 py-1.5 rounded-r-lg bg-background-section-burn whitespace-nowrap'>{t('appLog.table.header.user')}</td>
           </tr>
         </thead>
-        <tbody className="text-gray-700 text-[13px]">
+        <tbody className="text-text-secondary system-sm-regular">
           {logs.data.map((log: WorkflowAppLogDetail) => {
             const endUser = log.created_by_end_user ? log.created_by_end_user.session_id : log.created_by_account ? log.created_by_account.name : defaultValue
             return <tr
               key={log.id}
-              className={`border-b border-gray-200 h-8 hover:bg-gray-50 cursor-pointer ${currentLog?.id !== log.id ? '' : 'bg-gray-50'}`}
+              className={cn('border-b border-divider-subtle hover:bg-background-default-hover cursor-pointer', currentLog?.id !== log.id ? '' : 'bg-background-default-hover')}
               onClick={() => {
                 setCurrentLog(log)
                 setShowDrawer(true)
               }}>
-              <td className='text-center align-middle'>{!log.read_at && <span className='inline-block bg-[#3F83F8] h-1.5 w-1.5 rounded'></span>}</td>
-              <td className='w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
-              <td>{statusTdRender(log.workflow_run.status)}</td>
-              <td>
+              <td className='h-4'>
+                {!log.read_at && (
+                  <div className='p-3 pr-0.5 flex items-center'>
+                    <span className='inline-block bg-util-colors-blue-blue-500 h-1.5 w-1.5 rounded'></span>
+                  </div>
+                )}
+              </td>
+              <td className='p-3 pr-2 w-[160px]'>{formatTime(log.created_at, t('appLog.dateTimeFormat') as string)}</td>
+              <td className='p-3 pr-2'>{statusTdRender(log.workflow_run.status)}</td>
+              <td className='p-3 pr-2'>
                 <div className={cn(
-                  log.workflow_run.elapsed_time === 0 && 'text-gray-400',
+                  log.workflow_run.elapsed_time === 0 && 'text-text-quaternary',
                 )}>{`${log.workflow_run.elapsed_time.toFixed(3)}s`}</div>
               </td>
-              <td>{log.workflow_run.total_tokens}</td>
-              <td>
-                <div className={cn(endUser === defaultValue ? 'text-gray-400' : 'text-gray-700', 'text-sm overflow-hidden text-ellipsis whitespace-nowrap')}>
+              <td className='p-3 pr-2'>{log.workflow_run.total_tokens}</td>
+              <td className='p-3 pr-2'>
+                <div className={cn(endUser === defaultValue ? 'text-text-quaternary' : 'text-text-secondary', 'overflow-hidden text-ellipsis whitespace-nowrap')}>
                   {endUser}
                 </div>
               </td>
-              {/* <td>VERSION</td> */}
             </tr>
           })}
         </tbody>
@@ -123,7 +127,7 @@ const WorkflowAppLogList: FC<ILogs> = ({ logs, appDetail, onRefresh }) => {
         onClose={onCloseDrawer}
         mask={isMobile}
         footer={null}
-        panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[600px] rounded-xl border border-gray-200'
+        panelClassname='mt-16 mx-2 sm:mr-2 mb-3 !p-0 !max-w-[600px] rounded-xl border border-components-panel-border'
       >
         <DetailPanel onClose={onCloseDrawer} runID={currentLog?.workflow_run.id || ''} />
       </Drawer>

+ 0 - 6
web/app/components/app/workflow-log/style.module.css

@@ -1,9 +1,3 @@
-.logTable td {
-  padding: 7px 8px;
-  box-sizing: border-box;
-  max-width: 200px;
-}
-
 .pagination li {
   list-style: none;
 }

+ 2 - 2
web/app/components/base/button/add-button.tsx

@@ -14,8 +14,8 @@ const AddButton: FC<Props> = ({
   onClick,
 }) => {
   return (
-    <div className={cn(className, 'p-1 rounded-md cursor-pointer hover:bg-gray-200 select-none')} onClick={onClick}>
-      <RiAddLine className='w-4 h-4 text-gray-500' />
+    <div className={cn(className, 'p-1 rounded-md cursor-pointer hover:bg-state-base-hover select-none')} onClick={onClick}>
+      <RiAddLine className='w-4 h-4 text-text-tertiary' />
     </div>
   )
 }

+ 27 - 33
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx

@@ -15,7 +15,6 @@ import {
   getUrl,
   stopChatMessageResponding,
 } from '@/service/share'
-import AnswerIcon from '@/app/components/base/answer-icon'
 
 const ChatWrapper = () => {
   const {
@@ -56,7 +55,7 @@ const ChatWrapper = () => {
     appConfig,
     {
       inputs: (currentConversationId ? currentConversationItem?.inputs : newConversationInputs) as any,
-      promptVariables: inputsForms,
+      inputsForm: inputsForms,
     },
     appPrevChatList,
     taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
@@ -65,19 +64,18 @@ const ChatWrapper = () => {
   useEffect(() => {
     if (currentChatInstanceRef.current)
       currentChatInstanceRef.current.handleStop = handleStop
+  // eslint-disable-next-line react-hooks/exhaustive-deps
   }, [])
 
   const doSend: OnSend = useCallback((message, files, last_answer) => {
     const data: any = {
       query: message,
+      files,
       inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
       conversation_id: currentConversationId,
       parent_message_id: last_answer?.id || getLastAnswer(chatListRef.current)?.id || null,
     }
 
-    if (appConfig?.file_upload?.image.enabled && files?.length)
-      data.files = files
-
     handleSend(
       getUrl('chat-messages', isInstalledApp, appId || ''),
       data,
@@ -152,35 +150,31 @@ const ChatWrapper = () => {
     isMobile,
   ])
 
-  const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
-    ? <AnswerIcon
-      iconType={appData.site.icon_type}
-      icon={appData.site.icon}
-      background={appData.site.icon_background}
-      imageUrl={appData.site.icon_url}
-    />
-    : null
-
   return (
-    <Chat
-      appData={appData}
-      config={appConfig}
-      chatList={chatList}
-      isResponding={isResponding}
-      chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-full ${isMobile && 'px-4'}`}
-      chatFooterClassName='pb-4'
-      chatFooterInnerClassName={`mx-auto w-full max-w-full ${isMobile && 'px-4'}`}
-      onSend={doSend}
-      onRegenerate={doRegenerate}
-      onStopResponding={handleStop}
-      chatNode={chatNode}
-      allToolIcons={appMeta?.tool_icons || {}}
-      onFeedback={handleFeedback}
-      suggestedQuestions={suggestedQuestions}
-      answerIcon={answerIcon}
-      hideProcessDetail
-      themeBuilder={themeBuilder}
-    />
+    <div
+      className='h-full bg-chatbot-bg overflow-hidden'
+    >
+      <Chat
+        appData={appData}
+        config={appConfig}
+        chatList={chatList}
+        isResponding={isResponding}
+        chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
+        chatFooterClassName='pb-4'
+        chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}
+        onSend={doSend}
+        inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
+        inputsForm={inputsForms}
+        onRegenerate={doRegenerate}
+        onStopResponding={handleStop}
+        chatNode={chatNode}
+        allToolIcons={appMeta?.tool_icons || {}}
+        onFeedback={handleFeedback}
+        suggestedQuestions={suggestedQuestions}
+        hideProcessDetail
+        themeBuilder={themeBuilder}
+      />
+    </div>
   )
 }
 

+ 3 - 2
web/app/components/base/chat/chat-with-history/config-panel/form-input.tsx

@@ -1,6 +1,7 @@
 import type { FC } from 'react'
 import { useTranslation } from 'react-i18next'
 import { memo } from 'react'
+import Textarea from '@/app/components/base/textarea'
 
 type InputProps = {
   form: any
@@ -23,9 +24,9 @@ const FormInput: FC<InputProps> = ({
 
   if (type === 'paragraph') {
     return (
-      <textarea
+      <Textarea
         value={value}
-        className='grow h-[104px] rounded-lg bg-gray-100 px-2.5 py-2 outline-none appearance-none resize-none'
+        className='resize-none'
         onChange={e => onChange(variable, e.target.value)}
         placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
       />

+ 34 - 4
web/app/components/base/chat/chat-with-history/config-panel/form.tsx

@@ -1,24 +1,26 @@
-import { useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
 import { useChatWithHistoryContext } from '../context'
 import Input from './form-input'
 import { PortalSelect } from '@/app/components/base/select'
+import { InputVarType } from '@/app/components/workflow/types'
+import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
 
 const Form = () => {
   const { t } = useTranslation()
   const {
     inputsForms,
     newConversationInputs,
+    newConversationInputsRef,
     handleNewConversationInputsChange,
     isMobile,
   } = useChatWithHistoryContext()
 
-  const handleFormChange = useCallback((variable: string, value: string) => {
+  const handleFormChange = (variable: string, value: any) => {
     handleNewConversationInputsChange({
-      ...newConversationInputs,
+      ...newConversationInputsRef.current,
       [variable]: value,
     })
-  }, [newConversationInputs, handleNewConversationInputsChange])
+  }
 
   const renderField = (form: any) => {
     const {
@@ -48,6 +50,34 @@ const Form = () => {
         />
       )
     }
+    if (form.type === InputVarType.singleFile) {
+      return (
+        <FileUploaderInAttachmentWrapper
+          value={newConversationInputs[variable] ? [newConversationInputs[variable]] : []}
+          onChange={files => handleFormChange(variable, files[0])}
+          fileConfig={{
+            allowed_file_types: form.allowed_file_types,
+            allowed_file_extensions: form.allowed_file_extensions,
+            allowed_file_upload_methods: form.allowed_file_upload_methods,
+            number_limits: 1,
+          }}
+        />
+      )
+    }
+    if (form.type === InputVarType.multiFiles) {
+      return (
+        <FileUploaderInAttachmentWrapper
+          value={newConversationInputs[variable]}
+          onChange={files => handleFormChange(variable, files)}
+          fileConfig={{
+            allowed_file_types: form.allowed_file_types,
+            allowed_file_extensions: form.allowed_file_extensions,
+            allowed_file_upload_methods: form.allowed_file_upload_methods,
+            number_limits: form.max_length,
+          }}
+        />
+      )
+    }
 
     return (
       <PortalSelect

+ 2 - 0
web/app/components/base/chat/chat-with-history/context.tsx

@@ -30,6 +30,7 @@ export type ChatWithHistoryContextValue = {
   conversationList: AppConversationData['data']
   showConfigPanelBeforeChat: boolean
   newConversationInputs: Record<string, any>
+  newConversationInputsRef: RefObject<Record<string, any>>
   handleNewConversationInputsChange: (v: Record<string, any>) => void
   inputsForms: any[]
   handleNewConversation: () => void
@@ -57,6 +58,7 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
   conversationList: [],
   showConfigPanelBeforeChat: false,
   newConversationInputs: {},
+  newConversationInputsRef: { current: {} },
   handleNewConversationInputsChange: () => {},
   inputsForms: [],
   handleNewConversation: () => {},

+ 48 - 14
web/app/components/base/chat/chat-with-history/hooks.tsx

@@ -37,6 +37,8 @@ import type {
 import { useToastContext } from '@/app/components/base/toast'
 import { changeLanguage } from '@/i18n/i18next-config'
 import { useAppFavicon } from '@/hooks/use-app-favicon'
+import { InputVarType } from '@/app/components/workflow/types'
+import { TransferMethod } from '@/types/app'
 
 export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
   const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
@@ -127,7 +129,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
     setNewConversationInputs(newInputs)
   }, [])
   const inputsForms = useMemo(() => {
-    return (appParams?.user_input_form || []).filter((item: any) => item.paragraph || item.select || item['text-input'] || item.number).map((item: any) => {
+    return (appParams?.user_input_form || []).filter((item: any) => !item.external_data_tool).map((item: any) => {
       if (item.paragraph) {
         return {
           ...item.paragraph,
@@ -147,6 +149,20 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
         }
       }
 
+      if (item['file-list']) {
+        return {
+          ...item['file-list'],
+          type: 'file-list',
+        }
+      }
+
+      if (item.file) {
+        return {
+          ...item.file,
+          type: 'file',
+        }
+      }
+
       return {
         ...item['text-input'],
         type: 'text-input',
@@ -206,21 +222,38 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
 
   const { notify } = useToastContext()
   const checkInputsRequired = useCallback((silent?: boolean) => {
-    if (inputsForms.length) {
-      for (let i = 0; i < inputsForms.length; i += 1) {
-        const item = inputsForms[i]
-
-        if (item.required && !newConversationInputsRef.current[item.variable]) {
-          if (!silent) {
-            notify({
-              type: 'error',
-              message: t('appDebug.errorMessage.valueOfVarRequired', { key: item.variable }),
-            })
-          }
+    let hasEmptyInput = ''
+    let fileIsUploading = false
+    const requiredVars = inputsForms.filter(({ required }) => required)
+    if (requiredVars.length) {
+      requiredVars.forEach(({ variable, label, type }) => {
+        if (hasEmptyInput)
+          return
+
+        if (fileIsUploading)
           return
+
+        if (!newConversationInputsRef.current[variable] && !silent)
+          hasEmptyInput = label as string
+
+        if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && newConversationInputsRef.current[variable] && !silent) {
+          const files = newConversationInputsRef.current[variable]
+          if (Array.isArray(files))
+            fileIsUploading = files.find(item => item.transferMethod === TransferMethod.local_file && !item.uploadedId)
+          else
+            fileIsUploading = files.transferMethod === TransferMethod.local_file && !files.uploadedId
         }
-      }
-      return true
+      })
+    }
+
+    if (hasEmptyInput) {
+      notify({ type: 'error', message: t('appDebug.errorMessage.valueOfVarRequired', { key: hasEmptyInput }) })
+      return false
+    }
+
+    if (fileIsUploading) {
+      notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
+      return
     }
 
     return true
@@ -377,6 +410,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
     setShowConfigPanelBeforeChat,
     setShowNewConversationItemInList,
     newConversationInputs,
+    newConversationInputsRef,
     handleNewConversationInputsChange,
     inputsForms,
     handleNewConversation,

+ 2 - 0
web/app/components/base/chat/chat-with-history/index.tsx

@@ -125,6 +125,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
     conversationList,
     showConfigPanelBeforeChat,
     newConversationInputs,
+    newConversationInputsRef,
     handleNewConversationInputsChange,
     inputsForms,
     handleNewConversation,
@@ -158,6 +159,7 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
       conversationList,
       showConfigPanelBeforeChat,
       newConversationInputs,
+      newConversationInputsRef,
       handleNewConversationInputsChange,
       inputsForms,
       handleNewConversation,

+ 13 - 16
web/app/components/base/chat/chat/answer/agent-content.tsx

@@ -2,41 +2,32 @@ import type { FC } from 'react'
 import { memo } from 'react'
 import type {
   ChatItem,
-  VisionFile,
 } from '../../types'
 import { Markdown } from '@/app/components/base/markdown'
 import Thought from '@/app/components/base/chat/chat/thought'
-import ImageGallery from '@/app/components/base/image-gallery'
-import type { Emoji } from '@/app/components/tools/types'
+import { FileList } from '@/app/components/base/file-uploader'
+import { getProcessedFilesFromResponse } from '@/app/components/base/file-uploader/utils'
 
 type AgentContentProps = {
   item: ChatItem
   responding?: boolean
-  allToolIcons?: Record<string, string | Emoji>
 }
 const AgentContent: FC<AgentContentProps> = ({
   item,
   responding,
-  allToolIcons,
 }) => {
   const {
     annotation,
     agent_thoughts,
   } = item
 
-  const getImgs = (list?: VisionFile[]) => {
-    if (!list)
-      return []
-    return list.filter(file => file.type === 'image' && file.belongs_to === 'assistant')
-  }
-
   if (annotation?.logAnnotation)
     return <Markdown content={annotation?.logAnnotation.content || ''} />
 
   return (
     <div>
       {agent_thoughts?.map((thought, index) => (
-        <div key={index}>
+        <div key={index} className='px-2 py-1'>
           {thought.thought && (
             <Markdown content={thought.thought} />
           )}
@@ -45,14 +36,20 @@ const AgentContent: FC<AgentContentProps> = ({
           {!!thought.tool && (
             <Thought
               thought={thought}
-              allToolIcons={allToolIcons || {}}
               isFinished={!!thought.observation || !responding}
             />
           )}
 
-          {getImgs(thought.message_files).length > 0 && (
-            <ImageGallery srcs={getImgs(thought.message_files).map(file => file.url)} />
-          )}
+          {
+            !!thought.message_files?.length && (
+              <FileList
+                files={getProcessedFilesFromResponse(thought.message_files.map((item: any) => ({ ...item, related_id: item.id })))}
+                showDeleteAction={false}
+                showDownloadAction={true}
+                canPreview={true}
+              />
+            )
+          }
         </div>
       ))}
     </div>

+ 11 - 2
web/app/components/base/chat/chat/answer/basic-content.tsx

@@ -2,6 +2,7 @@ import type { FC } from 'react'
 import { memo } from 'react'
 import type { ChatItem } from '../../types'
 import { Markdown } from '@/app/components/base/markdown'
+import cn from '@/utils/classnames'
 
 type BasicContentProps = {
   item: ChatItem
@@ -15,9 +16,17 @@ const BasicContent: FC<BasicContentProps> = ({
   } = item
 
   if (annotation?.logAnnotation)
-    return <Markdown content={annotation?.logAnnotation.content || ''} />
+    return <Markdown content={annotation?.logAnnotation.content || ''} className='px-2 py-1' />
 
-  return <Markdown content={content} className={`${item.isError && '!text-[#F04438]'}`} />
+  return (
+    <Markdown
+      className={cn(
+        'px-2 py-1',
+        item.isError && '!text-[#F04438]',
+      )}
+      content={content}
+    />
+  )
 }
 
 export default memo(BasicContent)

+ 25 - 6
web/app/components/base/chat/chat/answer/index.tsx

@@ -18,10 +18,10 @@ import { AnswerTriangle } from '@/app/components/base/icons/src/vender/solid/gen
 import LoadingAnim from '@/app/components/base/chat/chat/loading-anim'
 import Citation from '@/app/components/base/chat/chat/citation'
 import { EditTitle } from '@/app/components/app/annotation/edit-annotation-modal/edit-item'
-import type { Emoji } from '@/app/components/tools/types'
 import type { AppData } from '@/models/share'
 import AnswerIcon from '@/app/components/base/answer-icon'
 import cn from '@/utils/classnames'
+import { FileList } from '@/app/components/base/file-uploader'
 
 type AnswerProps = {
   item: ChatItem
@@ -30,7 +30,6 @@ type AnswerProps = {
   config?: ChatConfig
   answerIcon?: ReactNode
   responding?: boolean
-  allToolIcons?: Record<string, string | Emoji>
   showPromptLog?: boolean
   chatAnswerContainerInner?: string
   hideProcessDetail?: boolean
@@ -44,7 +43,6 @@ const Answer: FC<AnswerProps> = ({
   config,
   answerIcon,
   responding,
-  allToolIcons,
   showPromptLog,
   chatAnswerContainerInner,
   hideProcessDetail,
@@ -59,6 +57,8 @@ const Answer: FC<AnswerProps> = ({
     more,
     annotation,
     workflowProcess,
+    allFiles,
+    message_files,
   } = item
   const hasAgentThoughts = !!agent_thoughts?.length
 
@@ -135,7 +135,6 @@ const Answer: FC<AnswerProps> = ({
                 <WorkflowProcess
                   data={workflowProcess}
                   item={item}
-                  hideInfo
                   hideProcessDetail={hideProcessDetail}
                 />
               )
@@ -146,7 +145,6 @@ const Answer: FC<AnswerProps> = ({
                 <WorkflowProcess
                   data={workflowProcess}
                   item={item}
-                  hideInfo
                   hideProcessDetail={hideProcessDetail}
                 />
               )
@@ -168,7 +166,28 @@ const Answer: FC<AnswerProps> = ({
                 <AgentContent
                   item={item}
                   responding={responding}
-                  allToolIcons={allToolIcons}
+                />
+              )
+            }
+            {
+              !!allFiles?.length && (
+                <FileList
+                  className='my-1'
+                  files={allFiles}
+                  showDeleteAction={false}
+                  showDownloadAction
+                  canPreview
+                />
+              )
+            }
+            {
+              !!message_files?.length && (
+                <FileList
+                  className='my-1'
+                  files={message_files}
+                  showDeleteAction={false}
+                  showDownloadAction
+                  canPreview
                 />
               )
             }

+ 0 - 0
web/app/components/base/chat/chat/answer/operation.tsx


Some files were not shown because too many files changed in this diff