Przeglądaj źródła

Feat/attachments (#9526)

Co-authored-by: Joel <iamjoel007@gmail.com>
Co-authored-by: JzoNg <jzongcode@gmail.com>
zxhlyh 6 miesięcy temu
rodzic
commit
7a1d6fe509
100 zmienionych plików z 1606 dodań i 2404 usunięć
  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 62
      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 43
      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 20
      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 48
      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. 1 1
      web/app/components/app/configuration/tools/external-data-tool-modal.tsx
  70. 6 4
      web/app/components/app/create-app-modal/index.tsx
  71. 2 2
      web/app/components/app/create-from-dsl-modal/index.tsx
  72. 3 2
      web/app/components/app/duplicate-modal/index.tsx
  73. 2 2
      web/app/components/app/log-annotation/index.tsx
  74. 38 40
      web/app/components/app/log/filter.tsx
  75. 5 5
      web/app/components/app/log/index.tsx
  76. 38 30
      web/app/components/app/log/list.tsx
  77. 0 6
      web/app/components/app/log/style.module.css
  78. 19 9
      web/app/components/app/overview/settings/index.tsx
  79. 0 5
      web/app/components/app/overview/settings/style.module.css
  80. 4 0
      web/app/components/app/store.ts
  81. 6 4
      web/app/components/app/switch-app-modal/index.tsx
  82. 2 2
      web/app/components/app/text-generate/item/index.tsx
  83. 21 10
      web/app/components/app/text-generate/item/result-tab.tsx
  84. 3 3
      web/app/components/app/workflow-log/detail.tsx
  85. 26 38
      web/app/components/app/workflow-log/filter.tsx
  86. 6 6
      web/app/components/app/workflow-log/index.tsx
  87. 34 30
      web/app/components/app/workflow-log/list.tsx
  88. 0 6
      web/app/components/app/workflow-log/style.module.css
  89. 2 2
      web/app/components/base/button/add-button.tsx
  90. 27 33
      web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
  91. 3 2
      web/app/components/base/chat/chat-with-history/config-panel/form-input.tsx
  92. 34 4
      web/app/components/base/chat/chat-with-history/config-panel/form.tsx
  93. 2 0
      web/app/components/base/chat/chat-with-history/context.tsx
  94. 48 14
      web/app/components/base/chat/chat-with-history/hooks.tsx
  95. 2 0
      web/app/components/base/chat/chat-with-history/index.tsx
  96. 13 16
      web/app/components/base/chat/chat/answer/agent-content.tsx
  97. 11 2
      web/app/components/base/chat/chat/answer/basic-content.tsx
  98. 25 6
      web/app/components/base/chat/chat/answer/index.tsx
  99. 1 1
      web/app/components/base/chat/chat/answer/operation.tsx
  100. 71 0
      web/app/components/base/chat/chat/answer/tool-detail.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


Plik diff jest za duży
+ 0 - 62
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


Plik diff jest za duży
+ 0 - 43
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


Plik diff jest za duży
+ 0 - 20
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


Plik diff jest za duży
+ 0 - 48
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);
-}

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

@@ -1,172 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { useTranslation } from 'react-i18next'
-import FeatureGroup from '../feature-group'
-import MoreLikeThisIcon from '../../../base/icons/more-like-this-icon'
-import FeatureItem from './feature-item'
-import Modal from '@/app/components/base/modal'
-import SuggestedQuestionsAfterAnswerIcon from '@/app/components/app/configuration/base/icons/suggested-questions-after-answer-icon'
-import { Microphone01, Speaker } from '@/app/components/base/icons/src/vender/solid/mediaAndDevices'
-import { Citations } from '@/app/components/base/icons/src/vender/solid/editor'
-import { FileSearch02 } from '@/app/components/base/icons/src/vender/solid/files'
-import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
-type IConfig = {
-  openingStatement: boolean
-  moreLikeThis: boolean
-  suggestedQuestionsAfterAnswer: boolean
-  speechToText: boolean
-  textToSpeech: boolean
-  citation: boolean
-  moderation: boolean
-  annotation: boolean
-}
-
-export type IChooseFeatureProps = {
-  isShow: boolean
-  onClose: () => void
-  config: IConfig
-  isChatApp: boolean
-  onChange: (key: string, value: boolean) => void
-  showTextToSpeechItem?: boolean
-  showSpeechToTextItem?: boolean
-}
-
-const OpeningStatementIcon = (
-  <svg width="15" height="13" viewBox="0 0 15 13" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path fillRule="evenodd" clipRule="evenodd" d="M8.33328 0.333252C4.83548 0.333252 1.99995 3.16878 1.99995 6.66659C1.99995 7.37325 2.11594 8.05419 2.33045 8.6906C2.36818 8.80254 2.39039 8.86877 2.40482 8.91762L2.40955 8.93407L2.40705 8.93928C2.38991 8.97462 2.36444 9.02207 2.31681 9.11025L1.21555 11.1486C1.1473 11.2749 1.07608 11.4066 1.02711 11.5212C0.978424 11.6351 0.899569 11.844 0.938369 12.0916C0.98385 12.3819 1.15471 12.6375 1.40556 12.7905C1.61957 12.9211 1.84276 12.9281 1.96659 12.9267C2.09117 12.9252 2.24012 12.9098 2.3829 12.895L5.81954 12.5397C5.87458 12.534 5.90335 12.5311 5.92443 12.5295L5.92715 12.5293L5.93539 12.5322C5.96129 12.5415 5.99642 12.555 6.05705 12.5784C6.76435 12.8509 7.53219 12.9999 8.33328 12.9999C11.8311 12.9999 14.6666 10.1644 14.6666 6.66659C14.6666 3.16878 11.8311 0.333252 8.33328 0.333252ZM5.97966 4.7214C6.73118 4.08722 7.73139 4.27352 8.3312 4.96609C8.931 4.27352 9.9183 4.09389 10.6827 4.7214C11.4472 5.34892 11.5401 6.41591 10.9499 7.16596C10.5843 7.63065 9.66655 8.47935 9.02117 9.05789C8.78411 9.2704 8.66558 9.37666 8.52332 9.41947C8.40129 9.4562 8.2611 9.4562 8.13907 9.41947C7.99682 9.37666 7.87829 9.2704 7.64122 9.05789C6.99584 8.47935 6.07814 7.63065 5.71251 7.16596C5.12234 6.41591 5.22815 5.35559 5.97966 4.7214Z" fill="#DD2590" />
-  </svg>
-)
-
-const ChooseFeature: FC<IChooseFeatureProps> = ({
-  isShow,
-  onClose,
-  isChatApp,
-  config,
-  onChange,
-  showTextToSpeechItem,
-  showSpeechToTextItem,
-}) => {
-  const { t } = useTranslation()
-  return (
-    <Modal
-      isShow={isShow}
-      onClose={onClose}
-      className='w-[400px]'
-      title={t('appDebug.operation.addFeature')}
-      closable
-      overflowVisible
-    >
-      <div className='pt-5 pb-10'>
-        {/* Chat Feature */}
-        {isChatApp && (
-          <FeatureGroup
-            title={t('appDebug.feature.groupChat.title')}
-            description={t('appDebug.feature.groupChat.description') as string}
-          >
-            <>
-              <FeatureItem
-                icon={OpeningStatementIcon}
-                previewImgClassName='openingStatementPreview'
-                title={t('appDebug.feature.conversationOpener.title')}
-                description={t('appDebug.feature.conversationOpener.description')}
-                value={config.openingStatement}
-                onChange={value => onChange('openingStatement', value)}
-              />
-              <FeatureItem
-                icon={<SuggestedQuestionsAfterAnswerIcon />}
-                previewImgClassName='suggestedQuestionsAfterAnswerPreview'
-                title={t('appDebug.feature.suggestedQuestionsAfterAnswer.title')}
-                description={t('appDebug.feature.suggestedQuestionsAfterAnswer.description')}
-                value={config.suggestedQuestionsAfterAnswer}
-                onChange={value => onChange('suggestedQuestionsAfterAnswer', value)}
-              />
-              {
-                showTextToSpeechItem && (
-                  <FeatureItem
-                    icon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
-                    previewImgClassName='textToSpeechPreview'
-                    title={t('appDebug.feature.textToSpeech.title')}
-                    description={t('appDebug.feature.textToSpeech.description')}
-                    value={config.textToSpeech}
-                    onChange={value => onChange('textToSpeech', value)}
-                  />
-                )
-              }
-              {
-                showSpeechToTextItem && (
-                  <FeatureItem
-                    icon={<Microphone01 className='w-4 h-4 text-[#7839EE]' />}
-                    previewImgClassName='speechToTextPreview'
-                    title={t('appDebug.feature.speechToText.title')}
-                    description={t('appDebug.feature.speechToText.description')}
-                    value={config.speechToText}
-                    onChange={value => onChange('speechToText', value)}
-                  />
-                )
-              }
-              <FeatureItem
-                icon={<Citations className='w-4 h-4 text-[#FD853A]' />}
-                previewImgClassName='citationPreview'
-                title={t('appDebug.feature.citation.title')}
-                description={t('appDebug.feature.citation.description')}
-                value={config.citation}
-                onChange={value => onChange('citation', value)}
-              />
-            </>
-          </FeatureGroup>
-        )}
-
-        {/* Text Generation Feature */}
-        {!isChatApp && (
-          <FeatureGroup title={t('appDebug.feature.groupExperience.title')}>
-            <>
-              <FeatureItem
-                icon={<MoreLikeThisIcon />}
-                previewImgClassName='moreLikeThisPreview'
-                title={t('appDebug.feature.moreLikeThis.title')}
-                description={t('appDebug.feature.moreLikeThis.description')}
-                value={config.moreLikeThis}
-                onChange={value => onChange('moreLikeThis', value)}
-              />
-              {
-                showTextToSpeechItem && (
-                  <FeatureItem
-                    icon={<Speaker className='w-4 h-4 text-[#7839EE]' />}
-                    previewImgClassName='textToSpeechPreview'
-                    title={t('appDebug.feature.textToSpeech.title')}
-                    description={t('appDebug.feature.textToSpeech.description')}
-                    value={config.textToSpeech}
-                    onChange={value => onChange('textToSpeech', value)}
-                  />
-                )
-              }
-            </>
-          </FeatureGroup>
-        )}
-        <FeatureGroup title={t('appDebug.feature.toolbox.title')}>
-          <>
-            <FeatureItem
-              icon={<FileSearch02 className='w-4 h-4 text-[#039855]' />}
-              previewImgClassName=''
-              title={t('appDebug.feature.moderation.title')}
-              description={t('appDebug.feature.moderation.description')}
-              value={config.moderation}
-              onChange={value => onChange('moderation', value)}
-            />
-            {isChatApp && (
-              <FeatureItem
-                icon={<MessageFast className='w-4 h-4 text-[#444CE7]' />}
-                title={t('appDebug.feature.annotation.title')}
-                description={t('appDebug.feature.annotation.description')}
-                value={config.annotation}
-                onChange={value => onChange('annotation', value)}
-              />
-            )}
-          </>
-        </FeatureGroup>
-      </div>
-    </Modal>
-  )
-}
-export default React.memo(ChooseFeature)

+ 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

+ 119 - 86
web/app/components/app/configuration/debug/index.tsx

@@ -3,10 +3,12 @@ import type { FC } from 'react'
 import useSWR from 'swr'
 import { useTranslation } from 'react-i18next'
 import React, { useCallback, useEffect, useRef, useState } from 'react'
-import { setAutoFreeze } from 'immer'
+import produce, { setAutoFreeze } from 'immer'
 import { useBoolean } from 'ahooks'
 import {
   RiAddLine,
+  RiEqualizer2Line,
+  RiSparklingFill,
 } from '@remixicon/react'
 import { useContext } from 'use-context-selector'
 import { useShallow } from 'zustand/react/shallow'
@@ -23,12 +25,16 @@ import {
   APP_CHAT_WITH_MULTIPLE_MODEL_RESTART,
 } from './types'
 import { AppType, ModelModeType, TransferMethod } from '@/types/app'
+import ChatUserInput from '@/app/components/app/configuration/debug/chat-user-input'
 import PromptValuePanel from '@/app/components/app/configuration/prompt-value-panel'
 import ConfigContext from '@/context/debug-configuration'
 import { ToastContext } from '@/app/components/base/toast'
 import { sendCompletionMessage } from '@/service/debug'
 import Button from '@/app/components/base/button'
-import type { ModelConfig as BackendModelConfig, VisionFile } from '@/types/app'
+import { RefreshCcw01 } from '@/app/components/base/icons/src/vender/line/arrows'
+import TooltipPlus from '@/app/components/base/tooltip'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
+import type { ModelConfig as BackendModelConfig, VisionFile, VisionSettings } from '@/types/app'
 import { promptVariablesToUserInputsForm } from '@/utils/model-config'
 import TextGeneration from '@/app/components/app/text-generate/item'
 import { IS_CE_EDITION } from '@/config'
@@ -42,6 +48,7 @@ import { useProviderContext } from '@/context/provider-context'
 import AgentLogModal from '@/app/components/base/agent-log-modal'
 import PromptLogModal from '@/app/components/base/prompt-log-modal'
 import { useStore as useAppStore } from '@/app/components/app/store'
+import { useFeatures, useFeaturesStore } from '@/app/components/base/features/hooks'
 
 type IDebug = {
   isAPIKeySet: boolean
@@ -77,8 +84,6 @@ const Debug: FC<IDebug> = ({
     speechToTextConfig,
     textToSpeechConfig,
     citationConfig,
-    moderationConfig,
-    moreLikeThisConfig,
     formattingChanged,
     setFormattingChanged,
     dataSets,
@@ -86,8 +91,6 @@ const Debug: FC<IDebug> = ({
     completionParams,
     hasSetContextVar,
     datasetConfigs,
-    visionConfig,
-    setVisionConfig,
   } = useContext(ConfigContext)
   const { eventEmitter } = useEventEmitterContextContext()
   const { data: text2speechDefaultModel } = useDefaultModel(ModelTypeEnum.textEmbedding)
@@ -175,7 +178,7 @@ const Debug: FC<IDebug> = ({
     }
 
     if (completionFiles.find(item => item.transfer_method === TransferMethod.local_file && !item.upload_file_id)) {
-      notify({ type: 'info', message: t('appDebug.errorMessage.waitForImgUpload') })
+      notify({ type: 'info', message: t('appDebug.errorMessage.waitForFileUpload') })
       return false
     }
     return !hasEmptyInput
@@ -195,6 +198,8 @@ const Debug: FC<IDebug> = ({
 
   const [completionRes, setCompletionRes] = useState('')
   const [messageId, setMessageId] = useState<string | null>(null)
+  const features = useFeatures(s => s.features)
+  const featuresStore = useFeaturesStore()
 
   const sendTextCompletion = async () => {
     if (isResponding) {
@@ -225,36 +230,30 @@ const Debug: FC<IDebug> = ({
       completion_prompt_config: {},
       user_input_form: promptVariablesToUserInputsForm(modelConfig.configs.prompt_variables),
       dataset_query_variable: contextVar || '',
-      opening_statement: introduction,
-      suggested_questions_after_answer: suggestedQuestionsAfterAnswerConfig,
-      speech_to_text: speechToTextConfig,
-      retriever_resource: citationConfig,
-      sensitive_word_avoidance: moderationConfig,
-      more_like_this: moreLikeThisConfig,
-      model: {
-        provider: modelConfig.provider,
-        name: modelConfig.model_id,
-        mode: modelConfig.mode,
-        completion_params: completionParams as any,
-      },
-      text_to_speech: {
-        enabled: false,
-        voice: '',
-        language: '',
-      },
-      agent_mode: {
-        enabled: false,
-        tools: [],
-      },
       dataset_configs: {
         ...datasetConfigs,
         datasets: {
           datasets: [...postDatasets],
         } as any,
       },
-      file_upload: {
-        image: visionConfig,
+      agent_mode: {
+        enabled: false,
+        tools: [],
+      },
+      model: {
+        provider: modelConfig.provider,
+        name: modelConfig.model_id,
+        mode: modelConfig.mode,
+        completion_params: completionParams as any,
       },
+      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,
+      retriever_resource: citationConfig,
     }
 
     if (isAdvancedMode) {
@@ -267,7 +266,7 @@ const Debug: FC<IDebug> = ({
       model_config: postModelConfig,
     }
 
-    if (visionConfig.enabled && completionFiles && completionFiles?.length > 0) {
+    if ((features.file as any).enabled && completionFiles && completionFiles?.length > 0) {
       data.files = completionFiles.map((item) => {
         if (item.transfer_method === TransferMethod.local_file) {
           return {
@@ -343,7 +342,7 @@ const Debug: FC<IDebug> = ({
     )
   }
 
-  const handleVisionConfigInMultipleModel = () => {
+  const handleVisionConfigInMultipleModel = useCallback(() => {
     if (debugWithMultipleModel && mode) {
       const supportedVision = multipleModelConfigs.some((modelConfig) => {
         const currentProvider = textGenerationModelList.find(modelItem => modelItem.provider === modelConfig.provider)
@@ -351,25 +350,24 @@ const Debug: FC<IDebug> = ({
 
         return currentModel?.features?.includes(ModelFeatureEnum.vision)
       })
-
-      if (supportedVision) {
-        setVisionConfig({
-          ...visionConfig,
-          enabled: true,
-        }, true)
-      }
-      else {
-        setVisionConfig({
-          ...visionConfig,
-          enabled: false,
-        }, true)
-      }
+      const {
+        features,
+        setFeatures,
+      } = featuresStore!.getState()
+
+      const newFeatures = produce(features, (draft) => {
+        draft.file = {
+          ...draft.file,
+          enabled: supportedVision,
+        }
+      })
+      setFeatures(newFeatures)
     }
-  }
+  }, [debugWithMultipleModel, featuresStore, mode, multipleModelConfigs, textGenerationModelList])
 
   useEffect(() => {
     handleVisionConfigInMultipleModel()
-  }, [multipleModelConfigs, mode])
+  }, [multipleModelConfigs, mode, handleVisionConfigInMultipleModel])
 
   const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
     currentLogItem: state.currentLogItem,
@@ -391,49 +389,74 @@ const Debug: FC<IDebug> = ({
     adjustModalWidth()
   }, [])
 
+  const [expanded, setExpanded] = useState(true)
+
   return (
     <>
-      <div className="shrink-0 pt-4 px-6">
-        <div className='flex items-center justify-between mb-2'>
-          <div className='h2 '>{t('appDebug.inputs.title')}</div>
+      <div className="shrink-0">
+        <div className='flex items-center justify-between px-4 pt-3 pb-2'>
+          <div className='text-text-primary system-xl-semibold'>{t('appDebug.inputs.title')}</div>
           <div className='flex items-center'>
             {
               debugWithMultipleModel
                 ? (
                   <>
                     <Button
-                      variant='secondary-accent'
+                      variant='ghost-accent'
                       onClick={() => onMultipleModelConfigsChange(true, [...multipleModelConfigs, { id: `${Date.now()}`, model: '', provider: '', parameters: {} }])}
                       disabled={multipleModelConfigs.length >= 4}
                     >
                       <RiAddLine className='mr-1 w-3.5 h-3.5' />
                       {t('common.modelProvider.addModel')}({multipleModelConfigs.length}/4)
                     </Button>
-                    <div className='mx-2 w-[1px] h-[14px] bg-gray-200' />
+                    <div className='mx-2 w-[1px] h-[14px] bg-divider-regular' />
                   </>
                 )
                 : null
             }
             {mode !== AppType.completion && (
-              <Button variant='secondary-accent' className='gap-1' onClick={clearConversation}>
-                <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
-                  <path d="M2.66663 2.66629V5.99963H3.05463M3.05463 5.99963C3.49719 4.90505 4.29041 3.98823 5.30998 3.39287C6.32954 2.7975 7.51783 2.55724 8.68861 2.70972C9.85938 2.8622 10.9465 3.39882 11.7795 4.23548C12.6126 5.07213 13.1445 6.16154 13.292 7.33296M3.05463 5.99963H5.99996M13.3333 13.333V9.99963H12.946M12.946 9.99963C12.5028 11.0936 11.7093 12.0097 10.6898 12.6045C9.67038 13.1993 8.48245 13.4393 7.31203 13.2869C6.1416 13.1344 5.05476 12.5982 4.22165 11.7621C3.38854 10.926 2.8562 9.83726 2.70796 8.66629M12.946 9.99963H9.99996" stroke="#1C64F2" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
-                </svg>
-                <span className='text-primary-600 text-[13px] font-semibold'>{t('common.operation.refresh')}</span>
-              </Button>
+              <>
+                <TooltipPlus
+                  popupContent={t('common.operation.refresh')}
+                >
+                  <ActionButton onClick={clearConversation}>
+                    <RefreshCcw01 className='w-4 h-4' />
+                  </ActionButton>
+                </TooltipPlus>
+                {varList.length > 0 && (
+                  <div className='relative ml-1 mr-2'>
+                    <TooltipPlus
+                      popupContent={t('workflow.panel.userInputField')}
+                    >
+                      <ActionButton state={expanded ? ActionButtonState.Active : undefined} onClick={() => setExpanded(!expanded)}>
+                        <RiEqualizer2Line className='w-4 h-4' />
+                      </ActionButton>
+                    </TooltipPlus>
+                    {expanded && <div className='absolute z-10 bottom-[-14px] right-[5px] w-3 h-3 bg-components-panel-on-panel-item-bg border-l-[0.5px] border-t-[0.5px] border-components-panel-border-subtle rotate-45' />}
+                  </div>
+                )}
+              </>
             )}
           </div>
         </div>
-        <PromptValuePanel
-          appType={mode as AppType}
-          onSend={handleSendTextCompletion}
-          inputs={inputs}
-          visionConfig={{
-            ...visionConfig,
-            image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
-          }}
-          onVisionFilesChange={setCompletionFiles}
-        />
+        {mode !== AppType.completion && expanded && (
+          <div className='mx-3'>
+            <ChatUserInput inputs={inputs} />
+          </div>
+        )}
+        {mode === AppType.completion && (
+          <PromptValuePanel
+            appType={mode as AppType}
+            onSend={handleSendTextCompletion}
+            inputs={inputs}
+            visionConfig={{
+              ...features.file! as VisionSettings,
+              transfer_methods: features.file!.allowed_file_upload_methods || [],
+              image_file_size_limit: fileUploadConfigResponse?.image_file_size_limit,
+            }}
+            onVisionFilesChange={setCompletionFiles}
+          />
+        )}
       </div>
       {
         debugWithMultipleModel && (
@@ -481,26 +504,36 @@ const Debug: FC<IDebug> = ({
             )}
             {/* Text  Generation */}
             {mode === AppType.completion && (
-              <div className="mt-6 px-6 pb-4">
-                <GroupName name={t('appDebug.result')} />
+              <>
                 {(completionRes || isResponding) && (
-                  <TextGeneration
-                    className="mt-2"
-                    content={completionRes}
-                    isLoading={!completionRes && isResponding}
-                    isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
-                    isResponding={isResponding}
-                    isInstalledApp={false}
-                    messageId={messageId}
-                    isError={false}
-                    onRetry={() => { }}
-                    supportAnnotation
-                    appId={appId}
-                    varList={varList}
-                    siteInfo={null}
-                  />
+                  <>
+                    <div className='mx-4 mt-3'><GroupName name={t('appDebug.result')} /></div>
+                    <div className='mx-3 mb-8'>
+                      <TextGeneration
+                        className="mt-2"
+                        content={completionRes}
+                        isLoading={!completionRes && isResponding}
+                        isShowTextToSpeech={textToSpeechConfig.enabled && !!text2speechDefaultModel}
+                        isResponding={isResponding}
+                        isInstalledApp={false}
+                        messageId={messageId}
+                        isError={false}
+                        onRetry={() => { }}
+                        supportAnnotation
+                        appId={appId}
+                        varList={varList}
+                        siteInfo={null}
+                      />
+                    </div>
+                  </>
                 )}
-              </div>
+                {!completionRes && !isResponding && (
+                  <div className='grow flex flex-col items-center justify-center gap-2'>
+                    <RiSparklingFill className='w-12 h-12 text-text-empty-state-icon' />
+                    <div className='text-text-quaternary system-sm-regular'>{t('appDebug.noResult')}</div>
+                  </div>
+                )}
+              </>
             )}
             {mode === AppType.completion && showPromptLogModal && (
               <PromptLogModal

+ 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

+ 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
                 />
               )
             }

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

@@ -12,7 +12,7 @@ import cn from '@/utils/classnames'
 import CopyBtn from '@/app/components/base/copy-btn'
 import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
 import AudioBtn from '@/app/components/base/audio-btn'
-import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
+import AnnotationCtrlBtn from '@/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
 import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
 import {
   ThumbsDown,

+ 71 - 0
web/app/components/base/chat/chat/answer/tool-detail.tsx

@@ -0,0 +1,71 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  RiArrowDownSLine,
+  RiArrowRightSLine,
+  RiHammerFill,
+  RiLoader2Line,
+} from '@remixicon/react'
+import type { ToolInfoInThought } from '../type'
+import cn from '@/utils/classnames'
+
+type ToolDetailProps = {
+  payload: ToolInfoInThought
+}
+const ToolDetail = ({
+  payload,
+}: ToolDetailProps) => {
+  const { t } = useTranslation()
+  const { name, label, input, isFinished, output } = payload
+  const toolLabel = name.startsWith('dataset_') ? t('dataset.knowledge') : label
+  const [expand, setExpand] = useState(false)
+
+  return (
+    <div
+      className={cn(
+        'rounded-xl',
+        !expand && 'border-l-[0.25px] border-components-panel-border bg-workflow-process-bg',
+        expand && 'border-[0.5px] border-components-panel-border-subtle bg-background-section-burn',
+      )}
+    >
+      <div
+        className={cn(
+          'flex items-center system-xs-medium text-text-tertiary px-2.5 py-2 cursor-pointer',
+          expand && 'pb-1.5',
+        )}
+        onClick={() => setExpand(!expand)}
+      >
+        {isFinished && <RiHammerFill className='mr-1 w-3.5 h-3.5' />}
+        {!isFinished && <RiLoader2Line className='mr-1 w-3.5 h-3.5 animate-spin' />}
+        {t(`tools.thought.${isFinished ? 'used' : 'using'}`)}
+        <div className='mx-1 text-text-secondary'>{toolLabel}</div>
+        {!expand && <RiArrowRightSLine className='w-4 h-4' />}
+        {expand && <RiArrowDownSLine className='ml-auto w-4 h-4' />}
+      </div>
+      {
+        expand && (
+          <>
+            <div className='mb-0.5 mx-1 rounded-[10px] bg-components-panel-on-panel-item-bg text-text-secondary'>
+              <div className='flex items-center justify-between px-2 pt-1 h-7 system-xs-semibold-uppercase'>
+                {t('tools.thought.requestTitle')}
+              </div>
+              <div className='pt-1 px-3 pb-2 code-xs-regular break-words'>
+                {input}
+              </div>
+            </div>
+            <div className='mx-1 mb-1 rounded-[10px] bg-components-panel-on-panel-item-bg text-text-secondary'>
+              <div className='flex items-center justify-between px-2 pt-1 h-7 system-xs-semibold-uppercase'>
+                {t('tools.thought.responseTitle')}
+              </div>
+              <div className='pt-1 px-3 pb-2 code-xs-regular break-words'>
+                {output}
+              </div>
+            </div>
+          </>
+        )
+      }
+    </div>
+  )
+}
+
+export default ToolDetail

Niektóre pliki nie zostały wyświetlone z powodu dużej ilości zmienionych plików