Przeglądaj źródła

Feat: web app dark mode (#14732)

KVOJJJin 1 miesiąc temu
rodzic
commit
d0d0bf570e
98 zmienionych plików z 2997 dodań i 2487 usunięć
  1. 4 1
      web/app/(shareLayout)/layout.tsx
  2. 1 1
      web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx
  3. 1 11
      web/app/components/app/configuration/debug/debug-with-multiple-model/text-generation-item.tsx
  4. 0 3
      web/app/components/app/configuration/debug/index.tsx
  5. 0 3
      web/app/components/app/log/list.tsx
  6. 0 26
      web/app/components/app/text-generate/index.tsx
  7. 182 267
      web/app/components/app/text-generate/item/index.tsx
  8. 31 68
      web/app/components/app/text-generate/item/result-tab.tsx
  9. 31 52
      web/app/components/app/text-generate/saved-items/index.tsx
  10. 14 23
      web/app/components/app/text-generate/saved-items/no-data/index.tsx
  11. 4 0
      web/app/components/base/action-button/index.css
  12. 3 0
      web/app/components/base/action-button/index.tsx
  13. 0 117
      web/app/components/base/audio-gallery/AudioPlayer.module.css
  14. 22 23
      web/app/components/base/audio-gallery/AudioPlayer.tsx
  15. 85 36
      web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
  16. 0 47
      web/app/components/base/chat/chat-with-history/config-panel/form-input.tsx
  17. 0 117
      web/app/components/base/chat/chat-with-history/config-panel/form.tsx
  18. 0 172
      web/app/components/base/chat/chat-with-history/config-panel/index.tsx
  19. 5 3
      web/app/components/base/chat/chat-with-history/context.tsx
  20. 126 38
      web/app/components/base/chat/chat-with-history/header-in-mobile.tsx
  21. 0 25
      web/app/components/base/chat/chat-with-history/header.tsx
  22. 151 0
      web/app/components/base/chat/chat-with-history/header/index.tsx
  23. 55 0
      web/app/components/base/chat/chat-with-history/header/mobile-operation-dropdown.tsx
  24. 73 0
      web/app/components/base/chat/chat-with-history/header/operation.tsx
  25. 20 14
      web/app/components/base/chat/chat-with-history/hooks.tsx
  26. 45 33
      web/app/components/base/chat/chat-with-history/index.tsx
  27. 118 0
      web/app/components/base/chat/chat-with-history/inputs-form/content.tsx
  28. 79 0
      web/app/components/base/chat/chat-with-history/inputs-form/index.tsx
  29. 48 0
      web/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown.tsx
  30. 88 55
      web/app/components/base/chat/chat-with-history/sidebar/index.tsx
  31. 11 11
      web/app/components/base/chat/chat-with-history/sidebar/item.tsx
  32. 14 20
      web/app/components/base/chat/chat-with-history/sidebar/list.tsx
  33. 101 0
      web/app/components/base/chat/chat-with-history/sidebar/operation.tsx
  34. 5 4
      web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx
  35. 1 1
      web/app/components/base/chat/chat/answer/index.tsx
  36. 1 1
      web/app/components/base/chat/chat/answer/more.tsx
  37. 63 116
      web/app/components/base/chat/chat/answer/operation.tsx
  38. 5 3
      web/app/components/base/chat/chat/answer/suggested-questions.tsx
  39. 7 20
      web/app/components/base/chat/chat/answer/workflow-process.tsx
  40. 3 0
      web/app/components/base/chat/chat/chat-input-area/index.tsx
  41. 5 5
      web/app/components/base/chat/chat/citation/index.tsx
  42. 12 12
      web/app/components/base/chat/chat/citation/popup.tsx
  43. 3 3
      web/app/components/base/chat/chat/citation/progress-tooltip.tsx
  44. 1 1
      web/app/components/base/chat/chat/citation/tooltip.tsx
  45. 2 1
      web/app/components/base/chat/chat/hooks.ts
  46. 6 0
      web/app/components/base/chat/chat/index.tsx
  47. 6 6
      web/app/components/base/chat/chat/log/index.tsx
  48. 12 21
      web/app/components/base/chat/chat/try-to-ask.tsx
  49. 89 24
      web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx
  50. 0 47
      web/app/components/base/chat/embedded-chatbot/config-panel/form-input.tsx
  51. 0 129
      web/app/components/base/chat/embedded-chatbot/config-panel/form.tsx
  52. 0 180
      web/app/components/base/chat/embedded-chatbot/config-panel/index.tsx
  53. 2 4
      web/app/components/base/chat/embedded-chatbot/context.tsx
  54. 0 56
      web/app/components/base/chat/embedded-chatbot/header.tsx
  55. 109 0
      web/app/components/base/chat/embedded-chatbot/header/index.tsx
  56. 5 14
      web/app/components/base/chat/embedded-chatbot/hooks.tsx
  57. 61 42
      web/app/components/base/chat/embedded-chatbot/index.tsx
  58. 118 0
      web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx
  59. 79 0
      web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx
  60. 52 0
      web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx
  61. 1 1
      web/app/components/base/chat/embedded-chatbot/theme/theme-context.ts
  62. 0 135
      web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn/index.tsx
  63. 23 0
      web/app/components/base/icons/assets/public/other/message-3-fill.svg
  64. 173 0
      web/app/components/base/icons/src/public/other/Message3Fill.json
  65. 16 0
      web/app/components/base/icons/src/public/other/Message3Fill.tsx
  66. 1 0
      web/app/components/base/icons/src/public/other/index.ts
  67. 3 3
      web/app/components/base/image-uploader/text-generation-image-uploader.tsx
  68. 28 26
      web/app/components/base/markdown.tsx
  69. 99 0
      web/app/components/base/new-audio-button/index.tsx
  70. 0 31
      web/app/components/base/regenerate-btn/index.tsx
  71. 5 6
      web/app/components/base/svg/index.tsx
  72. 9 5
      web/app/components/base/tab-header/index.tsx
  73. 0 9
      web/app/components/base/tab-header/style.module.css
  74. 164 177
      web/app/components/share/text-generation/index.tsx
  75. 49 0
      web/app/components/share/text-generation/info-modal.tsx
  76. 91 0
      web/app/components/share/text-generation/menu-dropdown.tsx
  77. 5 9
      web/app/components/share/text-generation/no-data/index.tsx
  78. 19 26
      web/app/components/share/text-generation/result/index.tsx
  79. 6 6
      web/app/components/share/text-generation/run-batch/csv-download/index.tsx
  80. 9 6
      web/app/components/share/text-generation/run-batch/csv-reader/index.tsx
  81. 0 11
      web/app/components/share/text-generation/run-batch/csv-reader/style.module.css
  82. 7 7
      web/app/components/share/text-generation/run-batch/index.tsx
  83. 15 6
      web/app/components/share/text-generation/run-batch/res-download/index.tsx
  84. 17 17
      web/app/components/share/text-generation/run-once/index.tsx
  85. 0 12
      web/app/components/share/text-generation/style.module.css
  86. 4 4
      web/app/components/workflow/panel/workflow-preview.tsx
  87. 1 1
      web/app/components/workflow/run/node.tsx
  88. 1 1
      web/app/components/workflow/run/tracing-panel.tsx
  89. 109 118
      web/app/styles/markdown.scss
  90. 47 0
      web/context/share-page-context.tsx
  91. 7 2
      web/i18n/en-US/share-app.ts
  92. 6 1
      web/i18n/zh-Hans/share-app.ts
  93. 21 11
      web/public/embed.js
  94. 2 0
      web/tailwind-common-config.ts
  95. 6 0
      web/themes/manual-dark.css
  96. 6 0
      web/themes/manual-light.css
  97. 44 0
      web/themes/markdown-dark.css
  98. 44 0
      web/themes/markdown-light.css

+ 4 - 1
web/app/(shareLayout)/layout.tsx

@@ -1,6 +1,7 @@
 import React from 'react'
 import type { FC } from 'react'
 import type { Metadata } from 'next'
+import { SharePageContextProvider } from '@/context/share-page-context'
 
 export const metadata: Metadata = {
   icons: 'data:,', // prevent browser from using default favicon
@@ -11,7 +12,9 @@ const Layout: FC<{
 }> = ({ children }) => {
   return (
     <div className="min-w-[300px] h-full pb-[env(safe-area-inset-bottom)]">
-      {children}
+      <SharePageContextProvider>
+        {children}
+      </SharePageContextProvider>
     </div>
   )
 }

+ 1 - 1
web/app/components/app/annotation/batch-add-annotation-modal/csv-downloader.tsx

@@ -42,7 +42,7 @@ const CSVDownload: FC = () => {
               <td className='h-9 pl-3 pr-2 border-b border-divider-regular'>{t('appAnnotation.batchModal.answer')}</td>
             </tr>
           </thead>
-          <tbody className='text-gray-700'>
+          <tbody className='text-text-secondary'>
             <tr>
               <td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.question')} 1</td>
               <td className='h-9 pl-3 pr-2 border-b border-divider-subtle text-[13px]'>{t('appAnnotation.batchModal.answer')} 1</td>

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

@@ -124,18 +124,9 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
       doSend(v.payload.message, v.payload.files)
   })
 
-  const varList = modelConfig.configs.prompt_variables.map((item: any) => {
-    return {
-      label: item.key,
-      value: inputs[item.key],
-    }
-  })
-
   return (
     <TextGeneration
       className='flex flex-col h-full overflow-y-auto border-none'
-      innerClassName='grow flex flex-col'
-      contentClassName='grow'
       content={completion}
       isLoading={!completion && isResponding}
       isResponding={isResponding}
@@ -144,8 +135,7 @@ const TextGenerationItem: FC<TextGenerationItemProps> = ({
       messageId={messageId}
       isError={false}
       onRetry={() => { }}
-      appId={appId}
-      varList={varList}
+      inSidePanel
     />
   )
 }

+ 0 - 3
web/app/components/app/configuration/debug/index.tsx

@@ -516,9 +516,6 @@ const Debug: FC<IDebug> = ({
                         messageId={messageId}
                         isError={false}
                         onRetry={() => { }}
-                        supportAnnotation
-                        appId={appId}
-                        varList={varList}
                         siteInfo={null}
                       />
                     </div>

+ 0 - 3
web/app/components/app/log/list.tsx

@@ -416,10 +416,7 @@ function DetailPanel({ detail, onFeedback }: IDetailPanel) {
               supportFeedback
               feedback={detail.message.feedbacks.find((item: any) => item.from_source === 'admin')}
               onFeedback={feedback => onFeedback(detail.message.id, feedback)}
-              supportAnnotation
               isShowTextToSpeech
-              appId={appDetail?.id}
-              varList={varList}
               siteInfo={null}
             />
           </div>

+ 0 - 26
web/app/components/app/text-generate/index.tsx

@@ -1,26 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React from 'react'
-import { format } from '@/service/base'
-
-export type ITextGenerationProps = {
-  value: string
-  className?: string
-}
-
-const TextGeneration: FC<ITextGenerationProps> = ({
-  value,
-  className,
-}) => {
-  return (
-    <div
-      className={className}
-      dangerouslySetInnerHTML={{
-        __html: format(value),
-      }}
-    >
-    </div>
-  )
-}
-
-export default React.memo(TextGeneration)

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

@@ -1,39 +1,40 @@
 'use client'
 import type { FC } from 'react'
-import React, { useEffect, useRef, useState } from 'react'
+import React, { useEffect, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import {
+  RiBookmark3Line,
   RiClipboardLine,
+  RiFileList3Line,
+  RiPlayList2Line,
+  RiReplay15Line,
+  RiSparklingFill,
+  RiSparklingLine,
+  RiThumbDownLine,
+  RiThumbUpLine,
 } from '@remixicon/react'
 import copy from 'copy-to-clipboard'
 import { useParams } from 'next/navigation'
-import { HandThumbDownIcon, HandThumbUpIcon } from '@heroicons/react/24/outline'
 import { useBoolean } from 'ahooks'
-import { HashtagIcon } from '@heroicons/react/24/solid'
 import ResultTab from './result-tab'
-import cn from '@/utils/classnames'
 import { Markdown } from '@/app/components/base/markdown'
 import Loading from '@/app/components/base/loading'
 import Toast from '@/app/components/base/toast'
-import AudioBtn from '@/app/components/base/audio-btn'
 import type { FeedbackType } from '@/app/components/base/chat/chat/type'
 import { fetchMoreLikeThis, updateFeedback } from '@/service/share'
-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 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'
 import type { WorkflowProcess } from '@/app/components/base/chat/types'
 import type { SiteInfo } from '@/models/share'
 import { useChatContext } from '@/app/components/base/chat/chat/context'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
+import NewAudioButton from '@/app/components/base/new-audio-button'
+import cn from '@/utils/classnames'
 
 const MAX_DEPTH = 3
 
-export interface IGenerationItemProps {
+export type IGenerationItemProps = {
   isWorkflow?: boolean
   workflowProcessData?: WorkflowProcess
   className?: string
@@ -56,31 +57,12 @@ export interface IGenerationItemProps {
   taskId?: string
   controlClearMoreLikeThis?: number
   supportFeedback?: boolean
-  supportAnnotation?: boolean
   isShowTextToSpeech?: boolean
-  appId?: string
-  varList?: { label: string; value: string | number | object }[]
-  innerClassName?: string
-  contentClassName?: string
-  footerClassName?: string
   hideProcessDetail?: boolean
   siteInfo: SiteInfo | null
+  inSidePanel?: boolean
 }
 
-export const SimpleBtn = ({ className, isDisabled, onClick, children }: {
-  className?: string
-  isDisabled?: boolean
-  onClick?: () => void
-  children: React.ReactNode
-}) => (
-  <div
-    className={cn(isDisabled ? 'border-gray-100 text-gray-300' : 'border-gray-200 text-gray-700 cursor-pointer hover:border-gray-300 hover:shadow-sm', 'flex items-center h-7 px-3 rounded-md border text-xs  font-medium', className)}
-    onClick={() => !isDisabled && onClick?.()}
-  >
-    {children}
-  </div>
-)
-
 export const copyIcon = (
   <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
     <path d="M9.3335 2.33341C9.87598 2.33341 10.1472 2.33341 10.3698 2.39304C10.9737 2.55486 11.4454 3.02657 11.6072 3.63048C11.6668 3.85302 11.6668 4.12426 11.6668 4.66675V10.0334C11.6668 11.0135 11.6668 11.5036 11.4761 11.8779C11.3083 12.2072 11.0406 12.4749 10.7113 12.6427C10.337 12.8334 9.84692 12.8334 8.86683 12.8334H5.1335C4.1534 12.8334 3.66336 12.8334 3.28901 12.6427C2.95973 12.4749 2.69201 12.2072 2.52423 11.8779C2.3335 11.5036 2.3335 11.0135 2.3335 10.0334V4.66675C2.3335 4.12426 2.3335 3.85302 2.39313 3.63048C2.55494 3.02657 3.02665 2.55486 3.63056 2.39304C3.8531 2.33341 4.12435 2.33341 4.66683 2.33341M5.60016 3.50008H8.40016C8.72686 3.50008 8.89021 3.50008 9.01499 3.4365C9.12475 3.38058 9.21399 3.29134 9.26992 3.18158C9.3335 3.05679 9.3335 2.89345 9.3335 2.56675V2.10008C9.3335 1.77338 9.3335 1.61004 9.26992 1.48525C9.21399 1.37549 9.12475 1.28625 9.01499 1.23033C8.89021 1.16675 8.72686 1.16675 8.40016 1.16675H5.60016C5.27347 1.16675 5.11012 1.16675 4.98534 1.23033C4.87557 1.28625 4.78634 1.37549 4.73041 1.48525C4.66683 1.61004 4.66683 1.77338 4.66683 2.10008V2.56675C4.66683 2.89345 4.66683 3.05679 4.73041 3.18158C4.78634 3.29134 4.87557 3.38058 4.98534 3.4365C5.11012 3.50008 5.27347 3.50008 5.60016 3.50008Z" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
@@ -109,22 +91,16 @@ const GenerationItem: FC<IGenerationItemProps> = ({
   taskId,
   controlClearMoreLikeThis,
   supportFeedback,
-  supportAnnotation,
   isShowTextToSpeech,
-  appId,
-  varList,
-  innerClassName,
-  contentClassName,
   hideProcessDetail,
   siteInfo,
+  inSidePanel,
 }) => {
   const { t } = useTranslation()
   const params = useParams()
   const isTop = depth === 1
-  const ref = useRef(null)
   const [completionRes, setCompletionRes] = useState('')
   const [childMessageId, setChildMessageId] = useState<string | null>(null)
-  const hasChild = !!childMessageId
   const [childFeedback, setChildFeedback] = useState<FeedbackType>({
     rating: null,
   })
@@ -140,8 +116,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
     setChildFeedback(childFeedback)
   }
 
-  const [isShowReplyModal, setIsShowReplyModal] = useState(false)
-  const question = (varList && varList?.length > 0) ? varList?.map(({ label, value }) => `${label}:${value}`).join('&') : ''
   const [isQuerying, { setTrue: startQuerying, setFalse: stopQuerying }] = useBoolean(false)
 
   const childProps = {
@@ -161,6 +135,7 @@ const GenerationItem: FC<IGenerationItemProps> = ({
     controlClearMoreLikeThis,
     isWorkflow,
     siteInfo,
+    taskId,
   }
 
   const handleMoreLikeThis = async () => {
@@ -178,19 +153,6 @@ const GenerationItem: FC<IGenerationItemProps> = ({
     stopQuerying()
   }
 
-  const mainStyle = (() => {
-    const res: React.CSSProperties = !isTop
-      ? {
-        background: depth % 2 === 0 ? 'linear-gradient(90.07deg, #F9FAFB 0.05%, rgba(249, 250, 251, 0) 99.93%)' : '#fff',
-      }
-      : {}
-
-    if (hasChild)
-      res.boxShadow = '0px 1px 2px rgba(16, 24, 40, 0.05)'
-
-    return res
-  })()
-
   useEffect(() => {
     if (controlClearMoreLikeThis) {
       setChildMessageId(null)
@@ -228,123 +190,125 @@ const GenerationItem: FC<IGenerationItemProps> = ({
     setShowPromptLogModal(true)
   }
 
-  const ratingContent = (
-    <>
-      {!isWorkflow && !isError && messageId && !feedback?.rating && (
-        <SimpleBtn className="!px-0">
-          <>
-            <div
-              onClick={() => {
-                onFeedback?.({
-                  rating: 'like',
-                })
-              }}
-              className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
-              <HandThumbUpIcon width={16} height={16} />
-            </div>
-            <div
-              onClick={() => {
-                onFeedback?.({
-                  rating: 'dislike',
-                })
-              }}
-              className='flex w-6 h-6 items-center justify-center rounded-md cursor-pointer hover:bg-gray-100'>
-              <HandThumbDownIcon width={16} height={16} />
-            </div>
-          </>
-        </SimpleBtn>
-      )}
-      {!isWorkflow && !isError && messageId && feedback?.rating === 'like' && (
-        <div
-          onClick={() => {
-            onFeedback?.({
-              rating: null,
-            })
-          }}
-          className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer  !text-primary-600 border border-primary-200 bg-primary-100 hover:border-primary-300 hover:bg-primary-200'>
-          <HandThumbUpIcon width={16} height={16} />
-        </div>
-      )}
-      {!isWorkflow && !isError && messageId && feedback?.rating === 'dislike' && (
-        <div
-          onClick={() => {
-            onFeedback?.({
-              rating: null,
-            })
-          }}
-          className='flex w-7 h-7 items-center justify-center rounded-md cursor-pointer  !text-red-600 border border-red-200 bg-red-100 hover:border-red-300 hover:bg-red-200'>
-          <HandThumbDownIcon width={16} height={16} />
-        </div>
-      )}
-    </>
-  )
-
   const [currentTab, setCurrentTab] = useState<string>('DETAIL')
+  const showResultTabs = !!workflowProcessData?.resultText || !!workflowProcessData?.files?.length
+  const switchTab = async (tab: string) => {
+    setCurrentTab(tab)
+  }
+  useEffect(() => {
+    if (workflowProcessData?.resultText || !!workflowProcessData?.files?.length)
+      switchTab('RESULT')
+    else
+      switchTab('DETAIL')
+  }, [workflowProcessData?.files?.length, workflowProcessData?.resultText])
 
   return (
-    <div ref={ref} className={cn(isTop ? `rounded-xl border ${!isError ? 'border-gray-200 bg-chat-bubble-bg' : 'border-[#FECDCA] bg-[#FEF3F2]'} ` : 'rounded-br-xl !mt-0', className)}
-      style={isTop
-        ? {
-          boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
-        }
-        : {}}
-    >
-      {isLoading
-        ? (
-          <div className='flex items-center h-10'><Loading type='area' /></div>
-        )
-        : (
-          <div
-            className={cn(!isTop && 'rounded-br-xl border-l-2 border-primary-400', 'p-4', innerClassName)}
-            style={mainStyle}
-          >
-            {(isTop && taskId) && (
-              <div className='mb-2 text-gray-500 border border-gray-200 box-border flex items-center rounded-md italic text-[11px] pl-1 pr-1.5 font-medium w-fit group-hover:opacity-100'>
-                <HashtagIcon className='w-3 h-3 text-gray-400 fill-current mr-1 stroke-current stroke-1' />
-                {taskId}
-              </div>)
-            }
-            <div className={`flex ${contentClassName}`}>
-              <div className='grow w-0'>
-                {siteInfo && workflowProcessData && (
-                  <WorkflowProcessItem
-                    data={workflowProcessData}
-                    expand={workflowProcessData.expand}
-                    hideProcessDetail={hideProcessDetail}
-                    hideInfo={hideProcessDetail}
-                    readonly={!siteInfo.show_workflow_steps}
-                  />
-                )}
-                {workflowProcessData && !isError && (
-                  <ResultTab data={workflowProcessData} content={content} currentTab={currentTab} onCurrentTabChange={setCurrentTab} />
-                )}
-                {isError && (
-                  <div className='text-gray-400 text-sm'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
-                )}
-                {!workflowProcessData && !isError && (typeof content === 'string') && (
+    <>
+      <div className={cn('relative', !isTop && 'mt-3', className)}>
+        {isLoading && (
+          <div className={cn('flex items-center h-10', !inSidePanel && 'bg-chat-bubble-bg rounded-2xl border-t border-divider-subtle')}><Loading type='area' /></div>
+        )}
+        {!isLoading && (
+          <>
+            {/* result content */}
+            <div className={cn(
+              'relative',
+              !inSidePanel && 'bg-chat-bubble-bg rounded-2xl border-t border-divider-subtle',
+            )}>
+              {workflowProcessData && (
+                <>
+                  <div className={cn(
+                    'p-3 pb-0',
+                    showResultTabs && 'border-b border-divider-subtle',
+                  )}>
+                    {taskId && (
+                      <div className={cn('mb-2 flex items-center system-2xs-medium-uppercase text-text-accent-secondary', isError && 'text-text-destructive')}>
+                        <RiPlayList2Line className='w-3 h-3 mr-1' />
+                        <span>{t('share.generation.execution')}</span>
+                        <span className='px-1'>·</span>
+                        <span>{taskId}</span>
+                      </div>
+                    )}
+                    {siteInfo && workflowProcessData && (
+                      <WorkflowProcessItem
+                        data={workflowProcessData}
+                        expand={workflowProcessData.expand}
+                        hideProcessDetail={hideProcessDetail}
+                        hideInfo={hideProcessDetail}
+                        readonly={!siteInfo.show_workflow_steps}
+                      />
+                    )}
+                    {showResultTabs && (
+                      <div className='flex items-center px-1 space-x-6'>
+                        <div
+                          className={cn(
+                            'py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
+                            currentTab === 'RESULT' && 'text-text-primary border-util-colors-blue-brand-blue-brand-600',
+                          )}
+                          onClick={() => switchTab('RESULT')}
+                        >{t('runLog.result')}</div>
+                        <div
+                          className={cn(
+                            'py-3 border-b-2 border-transparent system-sm-semibold-uppercase text-text-tertiary cursor-pointer',
+                            currentTab === 'DETAIL' && 'text-text-primary border-util-colors-blue-brand-blue-brand-600',
+                          )}
+                          onClick={() => switchTab('DETAIL')}
+                        >{t('runLog.detail')}</div>
+                      </div>
+                    )}
+                  </div>
+                  {!isError && (
+                    <ResultTab data={workflowProcessData} content={content} currentTab={currentTab} />
+                  )}
+                </>
+              )}
+              {!workflowProcessData && taskId && (
+                <div className={cn('sticky left-0 top-0 flex items-center w-full p-4 pb-3 bg-components-actionbar-bg rounded-t-2xl system-2xs-medium-uppercase text-text-accent-secondary', isError && 'text-text-destructive')}>
+                  <RiPlayList2Line className='w-3 h-3 mr-1' />
+                  <span>{t('share.generation.execution')}</span>
+                  <span className='px-1'>·</span>
+                  <span>{`${taskId}${depth > 1 ? `-${depth - 1}` : ''}`}</span>
+                </div>
+              )}
+              {isError && (
+                <div className='p-4 pt-0 text-text-quaternary body-lg-regular'>{t('share.generation.batchFailed.outputPlaceholder')}</div>
+              )}
+              {!workflowProcessData && !isError && (typeof content === 'string') && (
+                <div className={cn('p-4', taskId && 'pt-0')}>
                   <Markdown content={content} />
-                )}
-              </div>
+                </div>
+              )}
             </div>
-
-            <div className='flex items-center justify-between mt-3'>
-              <div className='flex items-center'>
-                {
-                  !isInWebApp && !isInstalledApp && !isResponding && (
-                    <SimpleBtn
-                      isDisabled={isError || !messageId}
-                      className={cn(isMobile && '!px-1.5', 'space-x-1 mr-1')}
-                      onClick={handleOpenLogModal}>
-                      <File02 className='w-3.5 h-3.5' />
-                      {!isMobile && <div>{t('common.operation.log')}</div>}
-                    </SimpleBtn>
-                  )
-                }
-                {((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
-                  <SimpleBtn
-                    isDisabled={isError || !messageId}
-                    className={cn(isMobile && '!px-1.5', 'space-x-1')}
-                    onClick={() => {
+            {/* meta data */}
+            <div className={cn(
+              'relative mt-1 h-4 px-4 text-text-quaternary system-xs-regular',
+              isMobile && ((childMessageId || isQuerying) && depth < 3) && 'pl-10',
+            )}>
+              {!isWorkflow && <span>{content?.length} {t('common.unit.char')}</span>}
+              {/* action buttons */}
+              <div className='absolute right-2 bottom-1 flex items-center'>
+                {!isInWebApp && !isInstalledApp && !isResponding && (
+                  <div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
+                    <ActionButton disabled={isError || !messageId} onClick={handleOpenLogModal}>
+                      <RiFileList3Line className='w-4 h-4' />
+                      {/* <div>{t('common.operation.log')}</div> */}
+                    </ActionButton>
+                  </div>
+                )}
+                <div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
+                  {moreLikeThis && (
+                    <ActionButton state={depth === MAX_DEPTH ? ActionButtonState.Disabled : ActionButtonState.Default} disabled={depth === MAX_DEPTH} onClick={handleMoreLikeThis}>
+                      <RiSparklingLine className='w-4 h-4' />
+                    </ActionButton>
+                  )}
+                  {isShowTextToSpeech && (
+                    <NewAudioButton
+                      id={messageId!}
+                      voice={config?.text_to_speech?.voice}
+                    />
+                  )}
+                  {((currentTab === 'RESULT' && workflowProcessData?.resultText) || !isWorkflow) && (
+                    <ActionButton disabled={isError || !messageId} onClick={() => {
                       const copyContent = isWorkflow ? workflowProcessData?.resultText : content
                       if (typeof copyContent === 'string')
                         copy(copyContent)
@@ -352,117 +316,68 @@ const GenerationItem: FC<IGenerationItemProps> = ({
                         copy(JSON.stringify(copyContent))
                       Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
                     }}>
-                    <RiClipboardLine className='w-3.5 h-3.5' />
-                    {!isMobile && <div>{t('common.operation.copy')}</div>}
-                  </SimpleBtn>
-                )}
-
-                {isInWebApp && (
-                  <>
-                    {!isWorkflow && (
-                      <SimpleBtn
-                        isDisabled={isError || !messageId}
-                        className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
-                        onClick={() => { onSave?.(messageId as string) }}
-                      >
-                        <Bookmark className='w-3.5 h-3.5' />
-                        {!isMobile && <div>{t('common.operation.save')}</div>}
-                      </SimpleBtn>
+                      <RiClipboardLine className='w-4 h-4' />
+                    </ActionButton>
+                  )}
+                  {isInWebApp && isError && (
+                    <ActionButton onClick={onRetry}>
+                      <RiReplay15Line className='w-4 h-4' />
+                    </ActionButton>
+                  )}
+                  {isInWebApp && !isWorkflow && (
+                    <ActionButton disabled={isError || !messageId} onClick={() => { onSave?.(messageId as string) }}>
+                      <RiBookmark3Line className='w-4 h-4' />
+                    </ActionButton>
+                  )}
+                </div>
+                {(supportFeedback || isInWebApp) && !isWorkflow && !isError && messageId && (
+                  <div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
+                    {!feedback?.rating && (
+                      <>
+                        <ActionButton onClick={() => onFeedback?.({ rating: 'like' })}>
+                          <RiThumbUpLine className='w-4 h-4' />
+                        </ActionButton>
+                        <ActionButton onClick={() => onFeedback?.({ rating: 'dislike' })}>
+                          <RiThumbDownLine className='w-4 h-4' />
+                        </ActionButton>
+                      </>
                     )}
-                    {(moreLikeThis && depth < MAX_DEPTH) && (
-                      <SimpleBtn
-                        isDisabled={isError || !messageId}
-                        className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
-                        onClick={handleMoreLikeThis}
-                      >
-                        <Stars02 className='w-3.5 h-3.5' />
-                        {!isMobile && <div>{t('appDebug.feature.moreLikeThis.title')}</div>}
-                      </SimpleBtn>
+                    {feedback?.rating === 'like' && (
+                      <ActionButton state={ActionButtonState.Active} onClick={() => onFeedback?.({ rating: null })}>
+                        <RiThumbUpLine className='w-4 h-4' />
+                      </ActionButton>
                     )}
-                    {isError && (
-                      <SimpleBtn
-                        onClick={onRetry}
-                        className={cn(isMobile && '!px-1.5', 'ml-2 space-x-1')}
-                      >
-                        <RefreshCcw01 className='w-3.5 h-3.5' />
-                        {!isMobile && <div>{t('share.generation.batchFailed.retry')}</div>}
-                      </SimpleBtn>
+                    {feedback?.rating === 'dislike' && (
+                      <ActionButton state={ActionButtonState.Destructive} onClick={() => onFeedback?.({ rating: null })}>
+                        <RiThumbDownLine className='w-4 h-4' />
+                      </ActionButton>
                     )}
-                    {!isError && messageId && !isWorkflow && (
-                      <div className="mx-3 w-[1px] h-[14px] bg-gray-200"></div>
-                    )}
-                    {ratingContent}
-                  </>
-                )}
-
-                {supportAnnotation && (
-                  <>
-                    <div className='ml-2 mr-1 h-[14px] w-[1px] bg-gray-200'></div>
-                    <AnnotationCtrlBtn
-                      appId={appId!}
-                      messageId={messageId!}
-                      className='ml-1'
-                      query={question}
-                      answer={content}
-                      // not support cache. So can not be cached
-                      cached={false}
-                      onAdded={() => {
-
-                      }}
-                      onEdit={() => setIsShowReplyModal(true)}
-                      onRemoved={() => { }}
-                    />
-                  </>
-                )}
-
-                <EditReplyModal
-                  appId={appId!}
-                  messageId={messageId!}
-                  isShow={isShowReplyModal}
-                  onHide={() => setIsShowReplyModal(false)}
-                  query={question}
-                  answer={content}
-                  onAdded={() => { }}
-                  onEdited={() => { }}
-                  createdAt={0}
-                  onRemove={() => { }}
-                  onlyEditResponse
-                />
-
-                {supportFeedback && (
-                  <div className='ml-1'>
-                    {ratingContent}
                   </div>
                 )}
-
-                {isShowTextToSpeech && (
-                  <>
-                    <div className='ml-2 mr-2 h-[14px] w-[1px] bg-gray-200'></div>
-                    <AudioBtn
-                      id={messageId!}
-                      className={'mr-1'}
-                      voice={config?.text_to_speech?.voice}
-                    />
-                  </>
-                )}
-              </div>
-              <div>
-                {!workflowProcessData && (
-                  <div className='text-xs text-gray-500'>{content?.length} {t('common.unit.char')}</div>
-                )}
               </div>
             </div>
-
-          </div>
+            {/* more like this elements */}
+            {!isTop && (
+              <div className={cn(
+                'absolute top-[-32px] w-4 h-[33px] flex justify-center',
+                isMobile ? 'left-[17px]' : 'left-[50%] translate-x-[-50%]',
+              )}>
+                <div className='h-full w-0.5 bg-divider-regular'></div>
+                <div className={cn(
+                  'absolute left-0 w-4 h-4 flex items-center justify-center bg-util-colors-blue-blue-500 rounded-2xl border-[0.5px] border-divider-subtle shadow-xs',
+                  isMobile ? 'top-[3.5px]' : 'top-2',
+                )}>
+                  <RiSparklingFill className='w-3 h-3 text-text-primary-on-surface' />
+                </div>
+              </div>
+            )}
+          </>
         )}
-
+      </div>
       {((childMessageId || isQuerying) && depth < 3) && (
-        <div className='pl-4'>
-          <GenerationItem {...childProps as any} />
-        </div>
+        <GenerationItem {...childProps as any} />
       )}
-
-    </div>
+    </>
   )
 }
 export default React.memo(GenerationItem)

+ 31 - 68
web/app/components/app/text-generate/item/result-tab.tsx

@@ -1,9 +1,6 @@
 import {
   memo,
-  useEffect,
 } from 'react'
-import { useTranslation } from 'react-i18next'
-import cn from '@/utils/classnames'
 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'
@@ -14,79 +11,45 @@ const ResultTab = ({
   data,
   content,
   currentTab,
-  onCurrentTabChange,
 }: {
   data?: WorkflowProcess
   content: any
   currentTab: string
-  onCurrentTabChange: (tab: string) => void
 }) => {
-  const { t } = useTranslation()
-
-  const switchTab = async (tab: string) => {
-    onCurrentTabChange(tab)
-  }
-  useEffect(() => {
-    if (data?.resultText || !!data?.files?.length)
-      switchTab('RESULT')
-    else
-      switchTab('DETAIL')
-  }, [data?.files?.length, data?.resultText])
-
   return (
-    <div className='grow relative flex flex-col'>
-      {(data?.resultText || !!data?.files?.length) && (
-        <div className='shrink-0 flex items-center mb-2 border-b-[0.5px] border-[rgba(0,0,0,0.05)]'>
-          <div
-            className={cn(
-              'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
-              currentTab === 'RESULT' && '!border-[rgb(21,94,239)] text-gray-700',
-            )}
-            onClick={() => switchTab('RESULT')}
-          >{t('runLog.result')}</div>
-          <div
-            className={cn(
-              'mr-6 py-3 border-b-2 border-transparent text-[13px] font-semibold leading-[18px] text-gray-400 cursor-pointer',
-              currentTab === 'DETAIL' && '!border-[rgb(21,94,239)] text-gray-700',
-            )}
-            onClick={() => switchTab('DETAIL')}
-          >{t('runLog.detail')}</div>
+    <>
+      {currentTab === 'RESULT' && (
+        <div className='p-4 space-y-3'>
+          {data?.resultText && <Markdown content={data?.resultText || ''} />}
+          {!!data?.files?.length && (
+            <div className='flex flex-col gap-2'>
+              {data?.files.map((item: any) => (
+                <div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
+                  <div className='py-1 text-text-tertiary '>{item.varName}</div>
+                  <FileList
+                    files={item.list}
+                    showDeleteAction={false}
+                    showDownloadAction
+                    canPreview
+                  />
+                </div>
+              ))}
+            </div>
+          )}
+        </div>
+      )}
+      {currentTab === 'DETAIL' && content && (
+        <div className='p-4'>
+          <CodeEditor
+            readOnly
+            title={<div>JSON OUTPUT</div>}
+            language={CodeLanguage.json}
+            value={content}
+            isJSONStringifyBeauty
+          />
         </div>
       )}
-      <div className={cn('grow bg-white')}>
-        {currentTab === 'RESULT' && (
-          <>
-            {data?.resultText && <Markdown content={data?.resultText || ''} />}
-            {!!data?.files?.length && (
-              <div className='flex flex-col gap-2'>
-                {data?.files.map((item: any) => (
-                  <div key={item.varName} className='flex flex-col gap-1 system-xs-regular'>
-                    <div className='py-1 text-text-tertiary '>{item.varName}</div>
-                    <FileList
-                      files={item.list}
-                      showDeleteAction={false}
-                      showDownloadAction
-                      canPreview
-                    />
-                  </div>
-                ))}
-              </div>
-            )}
-          </>
-        )}
-        {currentTab === 'DETAIL' && content && (
-          <div className='mt-1'>
-            <CodeEditor
-              readOnly
-              title={<div>JSON OUTPUT</div>}
-              language={CodeLanguage.json}
-              value={content}
-              isJSONStringifyBeauty
-            />
-          </div>
-        )}
-      </div>
-    </div>
+    </>
   )
 }
 

+ 31 - 52
web/app/components/app/text-generate/saved-items/index.tsx

@@ -1,15 +1,19 @@
 'use client'
 import type { FC } from 'react'
 import React from 'react'
+import {
+  RiClipboardLine,
+  RiDeleteBinLine,
+} from '@remixicon/react'
 import { useTranslation } from 'react-i18next'
 import copy from 'copy-to-clipboard'
 import NoData from './no-data'
 import cn from '@/utils/classnames'
 import type { SavedMessage } from '@/models/debug'
 import { Markdown } from '@/app/components/base/markdown'
-import { SimpleBtn, copyIcon } from '@/app/components/app/text-generate/item'
 import Toast from '@/app/components/base/toast'
-import AudioBtn from '@/app/components/base/audio-btn'
+import ActionButton from '@/app/components/base/action-button'
+import NewAudioButton from '@/app/components/base/new-audio-button'
 
 export type ISavedItemsProps = {
   className?: string
@@ -19,12 +23,6 @@ export type ISavedItemsProps = {
   onStartCreateContent: () => void
 }
 
-const removeIcon = (
-  <svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M5.25 1.75H8.75M1.75 3.5H12.25M11.0833 3.5L10.6742 9.63625C10.6129 10.5569 10.5822 11.0172 10.3833 11.3663C10.2083 11.6735 9.94422 11.9206 9.62597 12.0748C9.26448 12.25 8.80314 12.25 7.88045 12.25H6.11955C5.19686 12.25 4.73552 12.25 4.37403 12.0748C4.05577 11.9206 3.79172 11.6735 3.61666 11.3663C3.41781 11.0172 3.38713 10.5569 3.32575 9.63625L2.91667 3.5M5.83333 6.125V9.04167M8.16667 6.125V9.04167" stroke="#344054" strokeWidth="1.25" strokeLinecap="round" strokeLinejoin="round" />
-  </svg>
-)
-
 const SavedItems: FC<ISavedItemsProps> = ({
   className,
   isShowTextToSpeech,
@@ -35,56 +33,37 @@ const SavedItems: FC<ISavedItemsProps> = ({
   const { t } = useTranslation()
 
   return (
-    <div className={cn(className, 'space-y-3')}>
+    <div className={cn('space-y-4', className)}>
       {list.length === 0
         ? (
-          <div className='px-6'>
-            <NoData onStartCreateContent={onStartCreateContent} />
-          </div>
+          <NoData onStartCreateContent={onStartCreateContent} />
         )
         : (<>
           {list.map(({ id, answer }) => (
-            <div
-              key={id}
-              className='p-4 rounded-xl  bg-gray-50'
-              style={{
-                boxShadow: '0px 1px 2px rgba(16, 24, 40, 0.05)',
-              }}
-            >
-              <Markdown content={answer} />
-              <div className='flex items-center justify-between mt-3'>
-                <div className='flex items-center space-x-2'>
-                  <SimpleBtn
-                    className='space-x-1'
-                    onClick={() => {
-                      copy(answer)
-                      Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
-                    }}>
-                    {copyIcon}
-                    <div>{t('common.operation.copy')}</div>
-                  </SimpleBtn>
-
-                  <SimpleBtn
-                    className='space-x-1'
-                    onClick={() => {
-                      onRemove(id)
-                    }}>
-                    {removeIcon}
-                    <div>{t('common.operation.remove')}</div>
-                  </SimpleBtn>
-
-                  {isShowTextToSpeech && (
-                    <>
-                      <div className='ml-2 mr-2 h-[14px] w-[1px] bg-gray-200'></div>
-                      <AudioBtn
-                        value={answer}
-                        noCache={false}
-                        className={'mr-1'}
-                      />
-                    </>
-                  )}
+            <div key={id} className='relative'>
+              <div className={cn(
+                'p-4 bg-background-section-burn rounded-2xl',
+              )}>
+                <Markdown content={answer} />
+              </div>
+              <div className='mt-1 h-4 px-4 text-text-quaternary system-xs-regular'>
+                <span>{answer.length} {t('common.unit.char')}</span>
+              </div>
+              <div className='absolute right-2 bottom-1'>
+                <div className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
+                  {isShowTextToSpeech && <NewAudioButton value={answer}/>}
+                  <ActionButton onClick={() => {
+                    copy(answer)
+                    Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
+                  }}>
+                    <RiClipboardLine className='w-4 h-4' />
+                  </ActionButton>
+                  <ActionButton onClick={() => {
+                    onRemove(id)
+                  }}>
+                    <RiDeleteBinLine className='w-4 h-4' />
+                  </ActionButton>
                 </div>
-                <div className='text-xs text-gray-500'>{answer?.length} {t('common.unit.char')}</div>
               </div>
             </div>
           ))}

+ 14 - 23
web/app/components/app/text-generate/saved-items/no-data/index.tsx

@@ -2,47 +2,38 @@
 import type { FC } from 'react'
 import React from 'react'
 import { useTranslation } from 'react-i18next'
-import { PlusIcon } from '@heroicons/react/24/outline'
+import {
+  RiAddLine,
+  RiBookmark3Line,
+} from '@remixicon/react'
 import Button from '@/app/components/base/button'
 export type INoDataProps = {
   onStartCreateContent: () => void
 }
 
-const markIcon = (
-  <svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M4.16699 6.5C4.16699 5.09987 4.16699 4.3998 4.43948 3.86502C4.67916 3.39462 5.06161 3.01217 5.53202 2.77248C6.0668 2.5 6.76686 2.5 8.16699 2.5H11.8337C13.2338 2.5 13.9339 2.5 14.4686 2.77248C14.939 3.01217 15.3215 3.39462 15.5612 3.86502C15.8337 4.3998 15.8337 5.09987 15.8337 6.5V17.5L10.0003 14.1667L4.16699 17.5V6.5Z" stroke="#667085" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" />
-  </svg>
-)
-
-const lightIcon = (
-  <svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg" className="inline relative -top-3 -left-1.5"><path d="M5 6.5V5M8.93934 7.56066L10 6.5M10.0103 11.5H11.5103" stroke="#374151" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round"></path></svg>
-)
-
 const NoData: FC<INoDataProps> = ({
   onStartCreateContent,
 }) => {
   const { t } = useTranslation()
 
   return (
-    <div className='mt-[60px] px-5 py-4 rounded-2xl bg-gray-50 '>
-      <div className='flex items-center justify-center w-11 h-11 border border-gray-100 rounded-lg'>
-        {markIcon}
+    <div className='p-6 rounded-xl bg-background-section-burn '>
+      <div className='flex items-center justify-center w-10 h-10 border-[0.5px] border-components-card-border bg-components-card-bg-alt rounded-[10px] shadow-lg backdrop-blur-sm'>
+        <RiBookmark3Line className='w-4 h-4 text-text-accent'/>
       </div>
-      <div className='mt-2'>
-        <span className='text-gray-700 font-semibold'>{t('share.generation.savedNoData.title')}</span>
-        {lightIcon}
+      <div className='mt-3'>
+        <span className='text-text-secondary system-xl-semibold'>{t('share.generation.savedNoData.title')}</span>
       </div>
-      <div className='mt-2 text-gray-500 text-[13px] font-normal'>
+      <div className='mt-1 text-text-tertiary system-sm-regular'>
         {t('share.generation.savedNoData.description')}
       </div>
       <Button
-        className='mt-4'
+        variant='primary'
+        className='mt-3'
         onClick={onStartCreateContent}
       >
-        <div className='flex items-center space-x-2 text-primary-600 text-[13px] font-medium'>
-          <PlusIcon className='w-4 h-4' />
-          <span>{t('share.generation.savedNoData.startCreateContent')}</span>
-        </div>
+        <RiAddLine className='mr-1 w-4 h-4' />
+        <span>{t('share.generation.savedNoData.startCreateContent')}</span>
       </Button>
     </div>
   )

+ 4 - 0
web/app/components/base/action-button/index.css

@@ -5,6 +5,10 @@
         @apply inline-flex justify-center items-center cursor-pointer text-text-tertiary hover:text-text-secondary hover:bg-state-base-hover
     }
 
+    .action-btn-hover {
+        @apply bg-state-base-hover
+    }
+
     .action-btn-disabled {
         @apply cursor-not-allowed
     }

+ 3 - 0
web/app/components/base/action-button/index.tsx

@@ -8,6 +8,7 @@ enum ActionButtonState {
   Active = 'active',
   Disabled = 'disabled',
   Default = '',
+  Hover = 'hover',
 }
 
 const actionButtonVariants = cva(
@@ -41,6 +42,8 @@ function getActionButtonState(state: ActionButtonState) {
       return 'action-btn-active'
     case ActionButtonState.Disabled:
       return 'action-btn-disabled'
+    case ActionButtonState.Hover:
+      return 'action-btn-hover'
     default:
       return ''
   }

+ 0 - 117
web/app/components/base/audio-gallery/AudioPlayer.module.css

@@ -1,117 +0,0 @@
-.audioPlayer {
-  display: flex;
-  flex-direction: row;
-  align-items: center;
-  background-color: var(--color-components-chat-input-audio-bg-alt);
-  border-radius: 10px;
-  padding: 8px;
-  min-width: 240px;
-  max-width: 420px;
-  max-height: 40px;
-  backdrop-filter: blur(5px);
-  border: 1px solid var(--color-components-panel-border-subtle);
-  box-shadow: 0 1px 2px var(--color-shadow-shadow-3);
-  gap: 8px;
-}
-
-.playButton {
-  display: inline-flex;
-  width: 16px;
-  height: 16px;
-  border-radius: 50%;
-  background-color: var(--color-components-button-primary-bg);
-  color: var(--color-components-chat-input-audio-bg-alt);
-  border: none;
-  cursor: pointer;
-  align-items: center;
-  justify-content: center;
-  transition: background-color 0.1s;
-  flex-shrink: 0;
-}
-
-.playButton:hover {
-  background-color: var(--color-components-button-primary-bg-hover);
-}
-
-.playButton:disabled {
-  background-color: var(--color-components-button-primary-bg-disabled);
-}
-
-.audioControls {
-  flex-grow: 1;
-}
-
-.progressBarContainer {
-  height: 32px;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-}
-
-.waveform {
-  position: relative;
-  display: flex;
-  cursor: pointer;
-  height: 24px;
-  width: 100%;
-  flex-grow: 1;
-  align-items: center;
-  justify-content: center;
-}
-
-.progressBar {
-  position: absolute;
-  top: 0;
-  left: 0;
-  opacity: 0.5;
-  border-radius: 2px;
-  flex: none;
-  order: 55;
-  flex-grow: 0;
-  height: 100%;
-  background-color: rgba(66, 133, 244, 0.3);
-  pointer-events: none;
-}
-
-.timeDisplay {
-  /* position: absolute; */
-  color: var(--color-text-accent-secondary);
-  font-size: 12px;
-  order: 0;
-  height: 100%;
-  width: 50px;
-  display: inline-flex;
-  align-items: center;
-  justify-content: center;
-}
-
-/* .currentTime {
-    position: absolute;
-    bottom: calc(100% + 5px);
-    transform: translateX(-50%);
-    background-color: rgba(255,255,255,.8);
-    padding: 2px 4px;
-    border-radius:10px;
-    box-shadow: 0 1px 5px rgba(0, 0, 0, 0.08);
-} */
-
-.duration {
-  padding: 2px 4px;
-  border-radius: 10px;
-}
-
-.source_unavailable {
-  border: none;
-  display: flex;
-  align-items: center;
-  justify-content: center;
-  width: 100%;
-  height: 100%;
-  position: absolute;
-  color: #bdbdbf;
-}
-
-.playButton svg path,
-.playButton svg rect {
-  fill: currentColor;
-}

+ 22 - 23
web/app/components/base/audio-gallery/AudioPlayer.tsx

@@ -1,7 +1,13 @@
 import React, { useCallback, useEffect, useRef, useState } from 'react'
 import { t } from 'i18next'
-import styles from './AudioPlayer.module.css'
+import {
+  RiPauseCircleFill,
+  RiPlayLargeFill,
+} from '@remixicon/react'
 import Toast from '@/app/components/base/toast'
+import { useAppContext } from '@/context/app-context'
+import { Theme } from '@/types/app'
+import cn from '@/utils/classnames'
 
 type AudioPlayerProps = {
   src: string
@@ -18,6 +24,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
   const [hasStartedPlaying, setHasStartedPlaying] = useState(false)
   const [hoverTime, setHoverTime] = useState(0)
   const [isAudioAvailable, setIsAudioAvailable] = useState(true)
+  const { theme } = useAppContext()
 
   useEffect(() => {
     const audio = audioRef.current
@@ -230,11 +237,11 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
       let color
 
       if (index * barWidth <= playedWidth)
-        color = '#296DFF'
+        color = theme === Theme.light ? '#296DFF' : '#84ABFF'
       else if ((index * barWidth / width) * duration <= hoverTime)
-        color = 'rgba(21,90,239,.40)'
+        color = theme === Theme.light ? 'rgba(21,90,239,.40)' : 'rgba(200, 206, 218, 0.28)'
       else
-        color = 'rgba(21,90,239,.20)'
+        color = theme === Theme.light ? 'rgba(21,90,239,.20)' : 'rgba(200, 206, 218, 0.14)'
 
       const barHeight = value * height
       const rectX = index * barWidth
@@ -253,7 +260,7 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
         ctx.fillRect(rectX, rectY, rectWidth, rectHeight)
       }
     })
-  }, [currentTime, duration, hoverTime, waveformData])
+  }, [currentTime, duration, hoverTime, theme, waveformData])
 
   useEffect(() => {
     drawWaveform()
@@ -279,40 +286,32 @@ const AudioPlayer: React.FC<AudioPlayerProps> = ({ src }) => {
   }, [duration])
 
   return (
-    <div className={styles.audioPlayer}>
+    <div className='flex items-end gap-2 h-9 min-w-[240px] max-w-[420px] p-2 bg-components-chat-input-audio-bg-alt backdrop-blur-sm rounded-[10px] border border-components-panel-border-subtle shadow-xs'>
       <audio ref={audioRef} src={src} preload="auto"/>
-      <button className={styles.playButton} onClick={togglePlay} disabled={!isAudioAvailable}>
+      <button className='shrink-0 inline-flex items-center justify-center border-none text-text-accent hover:text-text-accent-secondary transition-all cursor-pointer disabled:text-components-button-primary-bg-disabled' onClick={togglePlay} disabled={!isAudioAvailable}>
         {isPlaying
           ? (
-            <svg viewBox="0 0 24 24" width="16" height="16">
-              <rect x="7" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
-              <rect x="15" y="6" width="3" height="12" rx="1.5" ry="1.5"/>
-            </svg>
+            <RiPauseCircleFill className='w-5 h-5' />
           )
           : (
-            <svg viewBox="0 0 24 24" width="16" height="16">
-              <path d="M8 5v14l11-7z" fill="currentColor"/>
-            </svg>
+            <RiPlayLargeFill className='w-5 h-5' />
           )}
       </button>
-      <div className={isAudioAvailable ? styles.audioControls : styles.audioControls_disabled} hidden={!isAudioAvailable}>
-        <div className={styles.progressBarContainer}>
+      <div className={cn(isAudioAvailable && 'grow')} hidden={!isAudioAvailable}>
+        <div className='h-8 flex items-center justify-center'>
           <canvas
             ref={canvasRef}
-            className={styles.waveform}
+            className='relative grow h-6 w-full flex items-center justify-center cursor-pointer'
             onClick={handleCanvasInteraction}
             onMouseMove={handleMouseMove}
             onMouseDown={handleCanvasInteraction}
           />
-          {/* <div className={styles.currentTime} style={{ left: `${(currentTime / duration) * 81}%`, bottom: '29px' }}>
-            {formatTime(currentTime)}
-          </div> */}
-          <div className={styles.timeDisplay}>
-            <span className={styles.duration}>{formatTime(duration)}</span>
+          <div className='inline-flex items-center justify-center min-w-[50px] text-text-accent-secondary system-xs-medium'>
+            <span className='px-0.5 py-1 rounded-[10px]'>{formatTime(duration)}</span>
           </div>
         </div>
       </div>
-      <div className={styles.source_unavailable} hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div>
+      <div className='absolute top-0 left-0 w-full h-full flex items-center justify-center text-text-quaternary' hidden={isAudioAvailable}>{t('common.operation.audioSourceUnavailable')}</div>
     </div>
   )
 }

+ 85 - 36
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx

@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo } from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
 import Chat from '../chat'
 import type {
   ChatConfig,
@@ -9,14 +9,17 @@ import type {
 import { useChat } from '../chat/hooks'
 import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
 import { useChatWithHistoryContext } from './context'
-import Header from './header'
-import ConfigPanel from './config-panel'
+import { InputVarType } from '@/app/components/workflow/types'
+import { TransferMethod } from '@/types/app'
+import InputsForm from '@/app/components/base/chat/chat-with-history/inputs-form'
 import {
   fetchSuggestedQuestions,
   getUrl,
   stopChatMessageResponding,
 } from '@/service/share'
+import AppIcon from '@/app/components/base/app-icon'
 import AnswerIcon from '@/app/components/base/answer-icon'
+import cn from '@/utils/classnames'
 
 const ChatWrapper = () => {
   const {
@@ -26,6 +29,7 @@ const ChatWrapper = () => {
     currentConversationItem,
     inputsForms,
     newConversationInputs,
+    newConversationInputsRef,
     handleNewConversationCompleted,
     isMobile,
     isInstalledApp,
@@ -65,6 +69,38 @@ const ChatWrapper = () => {
     appPrevChatTree,
     taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
   )
+  const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
+  const inputDisabled = useMemo(() => {
+    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 (!inputsFormValue?.[variable])
+          hasEmptyInput = label as string
+
+        if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
+          const files = inputsFormValue[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
+        }
+      })
+    }
+    if (hasEmptyInput)
+      return true
+
+    if (fileIsUploading)
+      return true
+    return false
+  }, [inputsFormValue, inputsForms])
 
   useEffect(() => {
     if (currentChatInstanceRef.current)
@@ -107,42 +143,48 @@ const ChatWrapper = () => {
     doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
   }, [chatList, doSend])
 
+  const messageList = useMemo(() => {
+    if (currentConversationId)
+      return chatList
+    return chatList.filter(item => !item.isOpeningStatement)
+  }, [chatList, currentConversationId])
+
+  const [collapsed, setCollapsed] = useState(!!currentConversationId)
+
   const chatNode = useMemo(() => {
-    if (inputsForms.length) {
-      return (
-        <>
-          <Header
-            isMobile={isMobile}
-            title={currentConversationItem?.name || ''}
-          />
-          {
-            !currentConversationId && (
-              <div className={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}>
-                <div className='mb-6' />
-                <ConfigPanel />
-                <div
-                  className='my-6 h-[1px]'
-                  style={{ background: 'linear-gradient(90deg, rgba(242, 244, 247, 0.00) 0%, #F2F4F7 49.17%, rgba(242, 244, 247, 0.00) 100%)' }}
-                />
-              </div>
-            )
-          }
-        </>
-      )
+    if (!inputsForms.length)
+      return null
+    if (isMobile) {
+      if (!currentConversationId)
+        return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
+      return null
     }
+    else {
+      return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
+    }
+  }, [inputsForms.length, isMobile, currentConversationId, collapsed])
 
+  const welcome = useMemo(() => {
+    const welcomeMessage = chatList.find(item => item.isOpeningStatement)
+    if (currentConversationId)
+      return null
+    if (!welcomeMessage)
+      return null
+    if (!collapsed && inputsForms.length > 0)
+      return null
     return (
-      <Header
-        isMobile={isMobile}
-        title={currentConversationItem?.name || ''}
-      />
+      <div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
+        <AppIcon
+          size='xl'
+          iconType={appData?.site.icon_type}
+          icon={appData?.site.icon}
+          background={appData?.site.icon_background}
+          imageUrl={appData?.site.icon_url}
+        />
+        <div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
+      </div>
     )
-  }, [
-    currentConversationId,
-    inputsForms,
-    currentConversationItem,
-    isMobile,
-  ])
+  }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
 
   const answerIcon = (appData?.site && appData.site.use_icon_as_answer_icon)
     ? <AnswerIcon
@@ -160,7 +202,7 @@ const ChatWrapper = () => {
       <Chat
         appData={appData}
         config={appConfig}
-        chatList={chatList}
+        chatList={messageList}
         isResponding={isResponding}
         chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
         chatFooterClassName='pb-4'
@@ -170,7 +212,12 @@ const ChatWrapper = () => {
         inputsForm={inputsForms}
         onRegenerate={doRegenerate}
         onStopResponding={handleStop}
-        chatNode={chatNode}
+        chatNode={
+          <>
+            {chatNode}
+            {welcome}
+          </>
+        }
         allToolIcons={appMeta?.tool_icons || {}}
         onFeedback={handleFeedback}
         suggestedQuestions={suggestedQuestions}
@@ -178,6 +225,8 @@ const ChatWrapper = () => {
         hideProcessDetail
         themeBuilder={themeBuilder}
         switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
+        inputDisabled={inputDisabled}
+        isMobile={isMobile}
       />
     </div>
   )

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

@@ -1,47 +0,0 @@
-import type { FC } from 'react'
-import { useTranslation } from 'react-i18next'
-import { memo } from 'react'
-import Textarea from '@/app/components/base/textarea'
-
-interface InputProps {
-  form: any
-  value: string
-  onChange: (variable: string, value: string) => void
-}
-const FormInput: FC<InputProps> = ({
-  form,
-  value,
-  onChange,
-}) => {
-  const { t } = useTranslation()
-  const {
-    type,
-    label,
-    required,
-    max_length,
-    variable,
-  } = form
-
-  if (type === 'paragraph') {
-    return (
-      <Textarea
-        value={value}
-        className='resize-none'
-        onChange={e => onChange(variable, e.target.value)}
-        placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-      />
-    )
-  }
-
-  return (
-    <input
-      className='grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none'
-      value={value || ''}
-      maxLength={max_length}
-      onChange={e => onChange(variable, e.target.value)}
-      placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-    />
-  )
-}
-
-export default memo(FormInput)

+ 0 - 117
web/app/components/base/chat/chat-with-history/config-panel/form.tsx

@@ -1,117 +0,0 @@
-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 {
-    appParams,
-    inputsForms,
-    newConversationInputs,
-    newConversationInputsRef,
-    handleNewConversationInputsChange,
-    isMobile,
-  } = useChatWithHistoryContext()
-
-  const handleFormChange = useCallback((variable: string, value: any) => {
-    handleNewConversationInputsChange({
-      ...newConversationInputsRef.current,
-      [variable]: value,
-    })
-  }, [newConversationInputsRef, handleNewConversationInputsChange])
-
-  const renderField = (form: any) => {
-    const {
-      label,
-      required,
-      variable,
-      options,
-    } = form
-
-    if (form.type === 'text-input' || form.type === 'paragraph') {
-      return (
-        <Input
-          form={form}
-          value={newConversationInputs[variable]}
-          onChange={handleFormChange}
-        />
-      )
-    }
-    if (form.type === 'number') {
-      return (
-        <input
-          className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
-          type="number"
-          value={newConversationInputs[variable] || ''}
-          onChange={e => handleFormChange(variable, e.target.value)}
-          placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-        />
-      )
-    }
-    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,
-            fileUploadConfig: (appParams as any).system_parameters,
-          }}
-        />
-      )
-    }
-    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,
-            fileUploadConfig: (appParams as any).system_parameters,
-          }}
-        />
-      )
-    }
-
-    return (
-      <PortalSelect
-        popupClassName='w-[200px]'
-        value={newConversationInputs[variable]}
-        items={options.map((option: string) => ({ value: option, name: option }))}
-        onSelect={item => handleFormChange(variable, item.value as string)}
-        placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-      />
-    )
-  }
-
-  if (!inputsForms.length)
-    return null
-
-  return (
-    <div className='mb-4 py-2'>
-      {
-        inputsForms.map(form => (
-          <div
-            key={form.variable}
-            className={`flex mb-3 last-of-type:mb-0 text-sm text-gray-900 ${isMobile && '!flex-wrap'}`}
-          >
-            <div className={`shrink-0 mr-2 py-2 w-[128px] ${isMobile && '!w-full'}`}>{form.label}</div>
-            {renderField(form)}
-          </div>
-        ))
-      }
-    </div>
-  )
-}
-
-export default Form

+ 0 - 172
web/app/components/base/chat/chat-with-history/config-panel/index.tsx

@@ -1,172 +0,0 @@
-import { useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { useChatWithHistoryContext } from '../context'
-import Form from './form'
-import Button from '@/app/components/base/button'
-import AppIcon from '@/app/components/base/app-icon'
-import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
-import { Edit02 } from '@/app/components/base/icons/src/vender/line/general'
-import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes'
-import LogoSite from '@/app/components/base/logo/logo-site'
-
-const ConfigPanel = () => {
-  const { t } = useTranslation()
-  const {
-    appData,
-    inputsForms,
-    handleStartChat,
-    showConfigPanelBeforeChat,
-    isMobile,
-  } = useChatWithHistoryContext()
-  const [collapsed, setCollapsed] = useState(true)
-  const customConfig = appData?.custom_config
-  const site = appData?.site
-
-  return (
-    <div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
-      <div
-        className={`
-          grow rounded-xl overflow-y-auto
-          ${showConfigPanelBeforeChat && 'border-[0.5px] border-gray-100 shadow-lg'}
-          ${!showConfigPanelBeforeChat && collapsed && 'border border-indigo-100'}
-          ${!showConfigPanelBeforeChat && !collapsed && 'border-[0.5px] border-gray-100 shadow-lg'}
-        `}
-      >
-        <div
-          className={`
-            flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
-            ${isMobile && '!px-4 !py-3'}
-          `}
-        >
-          {
-            showConfigPanelBeforeChat && (
-              <>
-                <div className='flex items-center h-8 text-2xl font-semibold text-gray-800'>
-                  <AppIcon
-                    iconType={appData?.site.icon_type}
-                    icon={appData?.site.icon}
-                    background='transparent'
-                    imageUrl={appData?.site.icon_url}
-                    size='small'
-                    className="mr-2"
-                  />
-                  {appData?.site.title}
-                </div>
-                {
-                  appData?.site.description && (
-                    <div className='mt-2 w-full text-sm text-gray-500'>
-                      {appData?.site.description}
-                    </div>
-                  )
-                }
-              </>
-            )
-          }
-          {
-            !showConfigPanelBeforeChat && collapsed && (
-              <>
-                <Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
-                <div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
-                  {t('share.chat.configStatusDes')}
-                </div>
-                <Button
-                  variant='secondary-accent'
-                  size='small'
-                  className='shrink-0'
-                  onClick={() => setCollapsed(false)}
-                >
-                  <Edit02 className='mr-1 w-3 h-3' />
-                  {t('common.operation.edit')}
-                </Button>
-              </>
-            )
-          }
-          {
-            !showConfigPanelBeforeChat && !collapsed && (
-              <>
-                <Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
-                <div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
-                  {t('share.chat.privatePromptConfigTitle')}
-                </div>
-              </>
-            )
-          }
-        </div>
-        {
-          !collapsed && !showConfigPanelBeforeChat && (
-            <div className='p-6 rounded-b-xl'>
-              <Form />
-              <div className={`pl-[136px] flex items-center ${isMobile && '!pl-0'}`}>
-                <Button
-                  variant='primary'
-                  className='mr-2'
-                  onClick={() => {
-                    setCollapsed(true)
-                    handleStartChat()
-                  }}
-                >
-                  {t('common.operation.save')}
-                </Button>
-                <Button
-                  onClick={() => setCollapsed(true)}
-                >
-                  {t('common.operation.cancel')}
-                </Button>
-              </div>
-            </div>
-          )
-        }
-        {
-          showConfigPanelBeforeChat && (
-            <div className='p-6 rounded-b-xl'>
-              <Form />
-              <Button
-                className={`${inputsForms.length && !isMobile && 'ml-[136px]'}`}
-                variant='primary'
-                size='large'
-                onClick={handleStartChat}
-              >
-                <MessageDotsCircle className='mr-2 w-4 h-4 text-white' />
-                {t('share.chat.startChat')}
-              </Button>
-            </div>
-          )
-        }
-      </div>
-      {
-        showConfigPanelBeforeChat && (site || customConfig) && (
-          <div className='mt-4 flex flex-wrap justify-between items-center py-2 text-xs text-gray-400'>
-            {site?.privacy_policy
-              ? <div className={`flex items-center ${isMobile && 'w-full justify-end'}`}>{t('share.chat.privacyPolicyLeft')}
-                <a
-                  className='text-gray-500 px-1'
-                  href={site?.privacy_policy}
-                  target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
-                {t('share.chat.privacyPolicyRight')}
-              </div>
-              : <div>
-              </div>}
-            {
-              customConfig?.remove_webapp_brand
-                ? null
-                : (
-                  <div className={`flex items-center justify-end ${isMobile && 'w-full'}`}>
-                    <div className='flex items-center pr-3 space-x-3'>
-                      <span className='uppercase'>{t('share.chat.poweredBy')}</span>
-                      {
-                        customConfig?.replace_webapp_logo
-                          ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
-                          : <LogoSite className='!h-5' />
-                      }
-                    </div>
-                  </div>
-                )
-            }
-          </div>
-        )
-      }
-    </div>
-  )
-}
-
-export default ConfigPanel

+ 5 - 3
web/app/components/base/chat/chat-with-history/context.tsx

@@ -28,13 +28,12 @@ export type ChatWithHistoryContextValue = {
   appPrevChatTree: ChatItemInTree[]
   pinnedConversationList: AppConversationData['data']
   conversationList: AppConversationData['data']
-  showConfigPanelBeforeChat: boolean
   newConversationInputs: Record<string, any>
   newConversationInputsRef: RefObject<Record<string, any>>
   handleNewConversationInputsChange: (v: Record<string, any>) => void
   inputsForms: any[]
   handleNewConversation: () => void
-  handleStartChat: () => void
+  handleStartChat: (callback?: any) => void
   handleChangeConversation: (conversationId: string) => void
   handlePinConversation: (conversationId: string) => void
   handleUnpinConversation: (conversationId: string) => void
@@ -49,6 +48,8 @@ export type ChatWithHistoryContextValue = {
   handleFeedback: (messageId: string, feedback: Feedback) => void
   currentChatInstanceRef: RefObject<{ handleStop: () => void }>
   themeBuilder?: ThemeBuilder
+  sidebarCollapseState?: boolean
+  handleSidebarCollapse: (state: boolean) => void
 }
 
 export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
@@ -56,7 +57,6 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
   appPrevChatTree: [],
   pinnedConversationList: [],
   conversationList: [],
-  showConfigPanelBeforeChat: false,
   newConversationInputs: {},
   newConversationInputsRef: { current: {} },
   handleNewConversationInputsChange: () => {},
@@ -75,5 +75,7 @@ export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>
   isInstalledApp: false,
   handleFeedback: () => {},
   currentChatInstanceRef: { current: { handleStop: () => {} } },
+  sidebarCollapseState: false,
+  handleSidebarCollapse: () => {},
 })
 export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

+ 126 - 38
web/app/components/base/chat/chat-with-history/header-in-mobile.tsx

@@ -1,60 +1,148 @@
-import { useState } from 'react'
+import { useCallback, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  RiMenuLine,
+} from '@remixicon/react'
 import { useChatWithHistoryContext } from './context'
+import Operation from './header/operation'
 import Sidebar from './sidebar'
+import MobileOperationDropdown from './header/mobile-operation-dropdown'
 import AppIcon from '@/app/components/base/app-icon'
-import {
-  Edit05,
-  Menu01,
-} from '@/app/components/base/icons/src/vender/line/general'
+import ActionButton from '@/app/components/base/action-button'
+import { Message3Fill } from '@/app/components/base/icons/src/public/other'
+import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
+import Confirm from '@/app/components/base/confirm'
+import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
+import type { ConversationItem } from '@/models/share'
 
 const HeaderInMobile = () => {
   const {
     appData,
+    currentConversationId,
+    currentConversationItem,
+    pinnedConversationList,
     handleNewConversation,
+    handlePinConversation,
+    handleUnpinConversation,
+    handleDeleteConversation,
+    handleRenameConversation,
+    conversationRenaming,
   } = useChatWithHistoryContext()
+  const { t } = useTranslation()
+  const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
+  const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
+  const [showRename, setShowRename] = useState<ConversationItem | null>(null)
+  const handleOperate = useCallback((type: string) => {
+    if (type === 'pin')
+      handlePinConversation(currentConversationId)
+
+    if (type === 'unpin')
+      handleUnpinConversation(currentConversationId)
+
+    if (type === 'delete')
+      setShowConfirm(currentConversationItem as any)
+
+    if (type === 'rename')
+      setShowRename(currentConversationItem as any)
+  }, [currentConversationId, currentConversationItem, handlePinConversation, handleUnpinConversation])
+  const handleCancelConfirm = useCallback(() => {
+    setShowConfirm(null)
+  }, [])
+  const handleDelete = useCallback(() => {
+    if (showConfirm)
+      handleDeleteConversation(showConfirm.id, { onSuccess: handleCancelConfirm })
+  }, [showConfirm, handleDeleteConversation, handleCancelConfirm])
+  const handleCancelRename = useCallback(() => {
+    setShowRename(null)
+  }, [])
+  const handleRename = useCallback((newName: string) => {
+    if (showRename)
+      handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
+  }, [showRename, handleRenameConversation, handleCancelRename])
   const [showSidebar, setShowSidebar] = useState(false)
+  const [showChatSettings, setShowChatSettings] = useState(false)
 
   return (
     <>
-      <div className='shrink-0 flex items-center px-3 h-[44px] border-b-[0.5px] border-b-gray-200'>
-        <div
-          className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg'
-          onClick={() => setShowSidebar(true)}
-        >
-          <Menu01 className='w-4 h-4 text-gray-700' />
+      <div className='shrink-0 flex items-center px-2 py-3 gap-1 bg-mask-top2bottom-gray-50-to-transparent'>
+        <ActionButton size='l' className='shrink-0' onClick={() => setShowSidebar(true)}>
+          <RiMenuLine className='w-[18px] h-[18px]' />
+        </ActionButton>
+        <div className='grow flex justify-center items-center'>
+          {!currentConversationId && (
+            <>
+              <AppIcon
+                className='mr-2'
+                size='tiny'
+                icon={appData?.site.icon}
+                iconType={appData?.site.icon_type}
+                imageUrl={appData?.site.icon_url}
+                background={appData?.site.icon_background}
+              />
+              <div className='text-text-secondary system-md-semibold truncate'>
+                {appData?.site.title}
+              </div>
+            </>
+          )}
+          {currentConversationId && (
+            <Operation
+              title={currentConversationItem?.name || ''}
+              isPinned={!!isPin}
+              togglePin={() => handleOperate(isPin ? 'unpin' : 'pin')}
+              isShowDelete
+              isShowRenameConversation
+              onRenameConversation={() => handleOperate('rename')}
+              onDelete={() => handleOperate('delete')}
+            />
+          )}
         </div>
-        <div className='grow flex justify-center items-center px-3'>
-          <AppIcon
-            className='mr-2'
-            size='tiny'
-            icon={appData?.site.icon}
-            iconType={appData?.site.icon_type}
-            imageUrl={appData?.site.icon_url}
-            background={appData?.site.icon_background}
-          />
-          <div className='py-1 text-base font-semibold text-gray-800 truncate'>
-            {appData?.site.title}
+        <MobileOperationDropdown
+          handleResetChat={handleNewConversation}
+          handleViewChatSettings={() => setShowChatSettings(true)}
+        />
+      </div>
+      {showSidebar && (
+        <div className='fixed inset-0 z-50 flex p-1 bg-background-overlay'
+          onClick={() => setShowSidebar(false)}
+        >
+          <div className='flex h-full w-[calc(100vw_-_40px)] bg-components-panel-bg backdrop-blur-sm rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
+            <Sidebar />
           </div>
         </div>
-        <div
-          className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg'
-          onClick={handleNewConversation}
+      )}
+      {showChatSettings && (
+        <div className='fixed inset-0 z-50 flex justify-end p-1 bg-background-overlay'
+          onClick={() => setShowChatSettings(false)}
         >
-          <Edit05 className='w-4 h-4 text-gray-700' />
-        </div>
-      </div>
-      {
-        showSidebar && (
-          <div className='fixed inset-0 z-50'
-            style={{ backgroundColor: 'rgba(35, 56, 118, 0.2)' }}
-            onClick={() => setShowSidebar(false)}
-          >
-            <div className='inline-block h-full bg-white' onClick={e => e.stopPropagation()}>
-              <Sidebar />
+          <div className='flex flex-col h-full w-[calc(100vw_-_40px)] bg-components-panel-bg backdrop-blur-sm rounded-xl shadow-lg' onClick={e => e.stopPropagation()}>
+            <div className='flex items-center gap-3 px-4 py-3 rounded-t-2xl border-b border-divider-subtle'>
+              <Message3Fill className='shrink-0 w-6 h-6' />
+              <div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
+            </div>
+            <div className='p-4'>
+              <InputsFormContent showTip />
             </div>
           </div>
-        )
-      }
+        </div>
+      )}
+      {!!showConfirm && (
+        <Confirm
+          title={t('share.chat.deleteConversation.title')}
+          content={t('share.chat.deleteConversation.content') || ''}
+          isShow
+          onCancel={handleCancelConfirm}
+          onConfirm={handleDelete}
+        />
+      )}
+      {showRename && (
+        <RenameModal
+          isShow
+          onClose={handleCancelRename}
+          saveLoading={conversationRenaming}
+          name={showRename?.name || ''}
+          onSave={handleRename}
+        />
+      )}
     </>
   )
 }

+ 0 - 25
web/app/components/base/chat/chat-with-history/header.tsx

@@ -1,25 +0,0 @@
-import type { FC } from 'react'
-import { memo } from 'react'
-
-type HeaderProps = {
-  title: string
-  isMobile: boolean
-}
-const Header: FC<HeaderProps> = ({
-  title,
-  isMobile,
-}) => {
-  return (
-    <div
-      className={`
-      sticky top-0 flex items-center px-8 h-16 bg-white/80 text-base font-medium 
-      text-gray-900 border-b-[0.5px] border-b-gray-100 backdrop-blur-md z-10
-      ${isMobile && '!h-12'}
-      `}
-    >
-      {title}
-    </div>
-  )
-}
-
-export default memo(Header)

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

@@ -0,0 +1,151 @@
+import { useCallback, useState } from 'react'
+import {
+  RiEditBoxLine,
+  RiLayoutRight2Line,
+  RiResetLeftLine,
+} from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import {
+  useChatWithHistoryContext,
+} from '../context'
+import Operation from './operation'
+import ActionButton from '@/app/components/base/action-button'
+import AppIcon from '@/app/components/base/app-icon'
+import Tooltip from '@/app/components/base/tooltip'
+import ViewFormDropdown from '@/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown'
+import Confirm from '@/app/components/base/confirm'
+import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
+import type { ConversationItem } from '@/models/share'
+import cn from '@/utils/classnames'
+
+const Header = () => {
+  const {
+    appData,
+    currentConversationId,
+    currentConversationItem,
+    inputsForms,
+    pinnedConversationList,
+    handlePinConversation,
+    handleUnpinConversation,
+    conversationRenaming,
+    handleRenameConversation,
+    handleDeleteConversation,
+    handleNewConversation,
+    sidebarCollapseState,
+    handleSidebarCollapse,
+  } = useChatWithHistoryContext()
+  const { t } = useTranslation()
+  const isSidebarCollapsed = sidebarCollapseState
+
+  const isPin = pinnedConversationList.some(item => item.id === currentConversationId)
+
+  const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
+  const [showRename, setShowRename] = useState<ConversationItem | null>(null)
+  const handleOperate = useCallback((type: string) => {
+    if (type === 'pin')
+      handlePinConversation(currentConversationId)
+
+    if (type === 'unpin')
+      handleUnpinConversation(currentConversationId)
+
+    if (type === 'delete')
+      setShowConfirm(currentConversationItem as any)
+
+    if (type === 'rename')
+      setShowRename(currentConversationItem as any)
+  }, [currentConversationId, currentConversationItem, handlePinConversation, handleUnpinConversation])
+  const handleCancelConfirm = useCallback(() => {
+    setShowConfirm(null)
+  }, [])
+  const handleDelete = useCallback(() => {
+    if (showConfirm)
+      handleDeleteConversation(showConfirm.id, { onSuccess: handleCancelConfirm })
+  }, [showConfirm, handleDeleteConversation, handleCancelConfirm])
+  const handleCancelRename = useCallback(() => {
+    setShowRename(null)
+  }, [])
+  const handleRename = useCallback((newName: string) => {
+    if (showRename)
+      handleRenameConversation(showRename.id, newName, { onSuccess: handleCancelRename })
+  }, [showRename, handleRenameConversation, handleCancelRename])
+
+  return (
+    <>
+      <div className='shrink-0 h-14 p-3 flex items-center justify-between'>
+        <div className={cn('flex items-center gap-1 transition-all duration-200 ease-in-out', !isSidebarCollapsed && 'opacity-0 user-select-none')}>
+          <ActionButton className={cn(!isSidebarCollapsed && 'cursor-default')} size='l' onClick={() => handleSidebarCollapse(false)}>
+            <RiLayoutRight2Line className='w-[18px] h-[18px]' />
+          </ActionButton>
+          <div className='shrink-0 mr-1'>
+            <AppIcon
+              size='large'
+              iconType={appData?.site.icon_type}
+              icon={appData?.site.icon}
+              background={appData?.site.icon_background}
+              imageUrl={appData?.site.icon_url}
+            />
+          </div>
+          {!currentConversationId && (
+            <div className={cn('grow text-text-secondary system-md-semibold truncate')}>{appData?.site.title}</div>
+          )}
+          {currentConversationId && currentConversationItem && isSidebarCollapsed && (
+            <>
+              <div className='p-1 text-divider-deep'>/</div>
+              <Operation
+                title={currentConversationItem?.name || ''}
+                isPinned={!!isPin}
+                togglePin={() => handleOperate(isPin ? 'unpin' : 'pin')}
+                isShowDelete
+                isShowRenameConversation
+                onRenameConversation={() => handleOperate('rename')}
+                onDelete={() => handleOperate('delete')}
+              />
+            </>
+          )}
+          <div className='px-1 flex items-center'>
+            <div className='h-[14px] w-px bg-divider-regular'></div>
+          </div>
+          {isSidebarCollapsed && (
+            <ActionButton size='l' onClick={handleNewConversation}>
+              <RiEditBoxLine className='w-[18px] h-[18px]' />
+            </ActionButton>
+          )}
+        </div>
+        <div className='flex items-center gap-1'>
+          {currentConversationId && (
+            <Tooltip
+              popupContent={t('share.chat.resetChat')}
+            >
+              <ActionButton size='l' onClick={handleNewConversation}>
+                <RiResetLeftLine className='w-[18px] h-[18px]' />
+              </ActionButton>
+            </Tooltip>
+          )}
+          {currentConversationId && inputsForms.length > 0 && (
+            <ViewFormDropdown />
+          )}
+        </div>
+      </div>
+      {!!showConfirm && (
+        <Confirm
+          title={t('share.chat.deleteConversation.title')}
+          content={t('share.chat.deleteConversation.content') || ''}
+          isShow
+          onCancel={handleCancelConfirm}
+          onConfirm={handleDelete}
+        />
+      )}
+      {showRename && (
+        <RenameModal
+          isShow
+          onClose={handleCancelRename}
+          saveLoading={conversationRenaming}
+          name={showRename?.name || ''}
+          onSave={handleRename}
+        />
+      )}
+    </>
+  )
+}
+
+export default Header

+ 55 - 0
web/app/components/base/chat/chat-with-history/header/mobile-operation-dropdown.tsx

@@ -0,0 +1,55 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  RiMoreFill,
+} from '@remixicon/react'
+import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
+
+type Props = {
+  handleResetChat: () => void
+  handleViewChatSettings: () => void
+}
+
+const MobileOperationDropdown = ({
+  handleResetChat,
+  handleViewChatSettings,
+}: Props) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={{
+        mainAxis: 4,
+        crossAxis: -4,
+      }}
+    >
+      <PortalToFollowElemTrigger
+        onClick={() => setOpen(v => !v)}
+      >
+        <ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
+          <RiMoreFill className='w-[18px] h-[18px]' />
+        </ActionButton>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className="z-40">
+        <div
+          className={'min-w-[160px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
+        >
+          <div className='flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' onClick={handleResetChat}>
+            <span className='grow'>{t('share.chat.resetChat')}</span>
+          </div>
+          <div className='flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover' onClick={handleViewChatSettings}>
+            <span className='grow'>{t('share.chat.viewChatSettings')}</span>
+          </div>
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+
+  )
+}
+
+export default MobileOperationDropdown

+ 73 - 0
web/app/components/base/chat/chat-with-history/header/operation.tsx

@@ -0,0 +1,73 @@
+'use client'
+import type { FC } from 'react'
+import React, { useState } from 'react'
+import type { Placement } from '@floating-ui/react'
+import {
+  RiArrowDownSLine,
+} from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
+import cn from '@/utils/classnames'
+
+type Props = {
+  title: string
+  isPinned: boolean
+  isShowRenameConversation?: boolean
+  onRenameConversation?: () => void
+  isShowDelete: boolean
+  togglePin: () => void
+  onDelete: () => void
+  placement?: Placement
+}
+
+const Operation: FC<Props> = ({
+  title,
+  isPinned,
+  togglePin,
+  isShowRenameConversation,
+  onRenameConversation,
+  isShowDelete,
+  onDelete,
+  placement = 'bottom-start',
+}) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement={placement}
+      offset={4}
+    >
+      <PortalToFollowElemTrigger
+        onClick={() => setOpen(v => !v)}
+      >
+        <div className={cn('flex items-center p-1.5 pl-2 rounded-lg text-text-secondary cursor-pointer hover:bg-state-base-hover', open && 'bg-state-base-hover')}>
+          <div className='system-md-semibold'>{title}</div>
+          <RiArrowDownSLine className='w-4 h-4 ' />
+        </div>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className="z-50">
+        <div
+          className={'min-w-[120px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
+        >
+          <div className={cn('flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={togglePin}>
+            <span className='grow'>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
+          </div>
+          {isShowRenameConversation && (
+            <div className={cn('flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={onRenameConversation}>
+              <span className='grow'>{t('explore.sidebar.action.rename')}</span>
+            </div>
+          )}
+          {isShowDelete && (
+            <div className={cn('group flex items-center space-x-1 px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive')} onClick={onDelete} >
+              <span className='grow'>{t('explore.sidebar.action.delete')}</span>
+            </div>
+          )}
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+export default React.memo(Operation)

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

@@ -110,6 +110,19 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
       changeLanguage(appData.site.default_language)
   }, [appData])
 
+  const [sidebarCollapseState, setSidebarCollapseState] = useState<boolean>(false)
+  const handleSidebarCollapse = useCallback((state: boolean) => {
+    if (appId) {
+      setSidebarCollapseState(state)
+      localStorage.setItem('webappSidebarCollapse', state ? 'collapsed' : 'expanded')
+    }
+  }, [appId, setSidebarCollapseState])
+  useEffect(() => {
+    if (appId) {
+      const localState = localStorage.getItem('webappSidebarCollapse')
+      setSidebarCollapseState(localState === 'collapsed')
+    }
+  }, [appId])
   const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, {
     defaultValue: {},
   })
@@ -122,7 +135,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
       })
     }
   }, [appId, conversationIdInfo, setConversationIdInfo])
-  const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true)
 
   const [newConversationId, setNewConversationId] = useState('')
   const chatShouldReloadKey = useMemo(() => {
@@ -287,23 +299,18 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
 
     return true
   }, [inputsForms, notify, t])
-  const handleStartChat = useCallback(() => {
+  const handleStartChat = useCallback((callback: any) => {
     if (checkInputsRequired()) {
-      setShowConfigPanelBeforeChat(false)
       setShowNewConversationItemInList(true)
+      callback?.()
     }
-  }, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired])
+  }, [setShowNewConversationItemInList, checkInputsRequired])
   const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } })
   const handleChangeConversation = useCallback((conversationId: string) => {
     currentChatInstanceRef.current.handleStop()
     setNewConversationId('')
     handleConversationIdInfoChange(conversationId)
-
-    if (conversationId === '' && !checkInputsRequired(true))
-      setShowConfigPanelBeforeChat(true)
-    else
-      setShowConfigPanelBeforeChat(false)
-  }, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired])
+  }, [handleConversationIdInfoChange])
   const handleNewConversation = useCallback(() => {
     currentChatInstanceRef.current.handleStop()
     setNewConversationId('')
@@ -313,11 +320,10 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
     }
     else if (currentConversationId) {
       handleConversationIdInfoChange('')
-      setShowConfigPanelBeforeChat(true)
       setShowNewConversationItemInList(true)
       handleNewConversationInputsChange({})
     }
-  }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
+  }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
   const handleUpdateConversationList = useCallback(() => {
     mutateAppConversationData()
     mutateAppPinnedConversationData()
@@ -435,8 +441,6 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
     appPrevChatTree,
     pinnedConversationList,
     conversationList,
-    showConfigPanelBeforeChat,
-    setShowConfigPanelBeforeChat,
     setShowNewConversationItemInList,
     newConversationInputs,
     newConversationInputsRef,
@@ -456,5 +460,7 @@ export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
     chatShouldReloadKey,
     handleFeedback,
     currentChatInstanceRef,
+    sidebarCollapseState,
+    handleSidebarCollapse,
   }
 }

+ 45 - 33
web/app/components/base/chat/chat-with-history/index.tsx

@@ -11,14 +11,15 @@ import {
 } from './context'
 import { useChatWithHistory } from './hooks'
 import Sidebar from './sidebar'
+import Header from './header'
 import HeaderInMobile from './header-in-mobile'
-import ConfigPanel from './config-panel'
 import ChatWrapper from './chat-wrapper'
 import type { InstalledApp } from '@/models/explore'
 import Loading from '@/app/components/base/loading'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
 import { checkOrSetAccessToken } from '@/app/components/share/utils'
 import AppUnavailable from '@/app/components/base/app-unavailable'
+import cn from '@/utils/classnames'
 
 type ChatWithHistoryProps = {
   className?: string
@@ -30,18 +31,18 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
     appInfoError,
     appData,
     appInfoLoading,
-    appPrevChatTree,
-    showConfigPanelBeforeChat,
     appChatListDataLoading,
     chatShouldReloadKey,
     isMobile,
     themeBuilder,
+    sidebarCollapseState,
   } = useChatWithHistoryContext()
-
-  const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatTree.length)
+  const isSidebarCollapsed = sidebarCollapseState
   const customConfig = appData?.custom_config
   const site = appData?.site
 
+  const [showSidePanel, setShowSidePanel] = useState(false)
+
   useEffect(() => {
     themeBuilder?.buildTheme(site?.chat_color_theme, site?.chat_color_theme_inverted)
     if (site) {
@@ -65,35 +66,44 @@ const ChatWithHistory: FC<ChatWithHistoryProps> = ({
   }
 
   return (
-    <div className={`h-full flex bg-white ${className} ${isMobile && 'flex-col'}`}>
-      {
-        !isMobile && (
+    <div className={cn(
+      'h-full flex bg-background-default-burn',
+      isMobile && 'flex-col',
+      className,
+    )}>
+      {!isMobile && (
+        <div className={cn(
+          'flex flex-col w-[236px] p-1 pr-0 transition-all duration-200 ease-in-out',
+          isSidebarCollapsed && 'w-0 !p-0 overflow-hidden',
+        )}>
           <Sidebar />
-        )
-      }
-      {
-        isMobile && (
-          <HeaderInMobile />
-        )
-      }
-      <div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatTree.length && 'flex items-center justify-center'}`}>
-        {
-          showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatTree.length && (
-            <div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}>
-              <ConfigPanel />
-            </div>
-          )
-        }
-        {
-          appChatListDataLoading && chatReady && (
+        </div>
+      )}
+      {isMobile && (
+        <HeaderInMobile />
+      )}
+      <div className={cn('relative grow p-2')}>
+        {isSidebarCollapsed && (
+          <div
+            className={cn(
+              'z-20 absolute top-0 w-[256px] h-full flex flex-col p-2 transition-all duration-500 ease-in-out',
+              showSidePanel ? 'left-0' : 'left-[-248px]',
+            )}
+            onMouseEnter={() => setShowSidePanel(true)}
+            onMouseLeave={() => setShowSidePanel(false)}
+          >
+            <Sidebar isPanel />
+          </div>
+        )}
+        <div className='h-full flex flex-col bg-chatbot-bg rounded-2xl border-[0,5px] border-components-panel-border-subtle overflow-hidden'>
+          {!isMobile && <Header />}
+          {appChatListDataLoading && (
             <Loading type='app' />
-          )
-        }
-        {
-          chatReady && !appChatListDataLoading && (
+          )}
+          {!appChatListDataLoading && (
             <ChatWrapper key={chatShouldReloadKey} />
-          )
-        }
+          )}
+        </div>
       </div>
     </div>
   )
@@ -123,7 +133,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
     appPrevChatTree,
     pinnedConversationList,
     conversationList,
-    showConfigPanelBeforeChat,
     newConversationInputs,
     newConversationInputsRef,
     handleNewConversationInputsChange,
@@ -142,6 +151,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
     appId,
     handleFeedback,
     currentChatInstanceRef,
+    sidebarCollapseState,
+    handleSidebarCollapse,
   } = useChatWithHistory(installedAppInfo)
 
   return (
@@ -157,7 +168,6 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
       appPrevChatTree,
       pinnedConversationList,
       conversationList,
-      showConfigPanelBeforeChat,
       newConversationInputs,
       newConversationInputsRef,
       handleNewConversationInputsChange,
@@ -178,6 +188,8 @@ const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
       handleFeedback,
       currentChatInstanceRef,
       themeBuilder,
+      sidebarCollapseState,
+      handleSidebarCollapse,
     }}>
       <ChatWithHistory className={className} />
     </ChatWithHistoryContext.Provider>

+ 118 - 0
web/app/components/base/chat/chat-with-history/inputs-form/content.tsx

@@ -0,0 +1,118 @@
+import React, { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useChatWithHistoryContext } from '../context'
+import Input from '@/app/components/base/input'
+import Textarea from '@/app/components/base/textarea'
+import { PortalSelect } from '@/app/components/base/select'
+import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
+import { InputVarType } from '@/app/components/workflow/types'
+
+type Props = {
+  showTip?: boolean
+}
+
+const InputsFormContent = ({ showTip }: Props) => {
+  const { t } = useTranslation()
+  const {
+    appParams,
+    inputsForms,
+    currentConversationId,
+    currentConversationItem,
+    newConversationInputs,
+    newConversationInputsRef,
+    handleNewConversationInputsChange,
+  } = useChatWithHistoryContext()
+  const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputs
+  const readonly = !!currentConversationId
+
+  const handleFormChange = useCallback((variable: string, value: any) => {
+    handleNewConversationInputsChange({
+      ...newConversationInputsRef.current,
+      [variable]: value,
+    })
+  }, [newConversationInputsRef, handleNewConversationInputsChange])
+
+  return (
+    <div className='space-y-4'>
+      {inputsForms.map(form => (
+        <div key={form.variable} className='space-y-1'>
+          <div className='h-6 flex items-center gap-1'>
+            <div className='text-text-secondary system-md-semibold'>{form.label}</div>
+            {!form.required && (
+              <div className='text-text-tertiary system-xs-regular'>{t('appDebug.variableTable.optional')}</div>
+            )}
+          </div>
+          {form.type === InputVarType.textInput && (
+            <Input
+              value={inputsFormValue?.[form.variable] || ''}
+              onChange={e => handleFormChange(form.variable, e.target.value)}
+              placeholder={form.label}
+              readOnly={readonly}
+              disabled={readonly}
+            />
+          )}
+          {form.type === InputVarType.number && (
+            <Input
+              type='number'
+              value={inputsFormValue?.[form.variable] || ''}
+              onChange={e => handleFormChange(form.variable, e.target.value)}
+              placeholder={form.label}
+              readOnly={readonly}
+              disabled={readonly}
+            />
+          )}
+          {form.type === InputVarType.paragraph && (
+            <Textarea
+              value={inputsFormValue?.[form.variable] || ''}
+              onChange={e => handleFormChange(form.variable, e.target.value)}
+              placeholder={form.label}
+              readOnly={readonly}
+              disabled={readonly}
+            />
+          )}
+          {form.type === InputVarType.select && (
+            <PortalSelect
+              popupClassName='w-[200px]'
+              value={inputsFormValue?.[form.variable]}
+              items={form.options.map((option: string) => ({ value: option, name: option }))}
+              onSelect={item => handleFormChange(form.variable, item.value as string)}
+              placeholder={form.label}
+              readonly={readonly}
+            />
+          )}
+          {form.type === InputVarType.singleFile && (
+            <FileUploaderInAttachmentWrapper
+              value={inputsFormValue?.[form.variable] ? [inputsFormValue?.[form.variable]] : []}
+              onChange={files => handleFormChange(form.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,
+                fileUploadConfig: (appParams as any).system_parameters,
+              }}
+            />
+          )}
+          {form.type === InputVarType.multiFiles && (
+            <FileUploaderInAttachmentWrapper
+              value={inputsFormValue?.[form.variable] || []}
+              onChange={files => handleFormChange(form.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,
+                fileUploadConfig: (appParams as any).system_parameters,
+              }}
+            />
+          )}
+        </div>
+      ))}
+      {showTip && (
+        <div className='text-text-tertiary system-xs-regular'>{t('share.chat.chatFormTip')}</div>
+      )}
+    </div>
+  )
+}
+
+export default InputsFormContent

+ 79 - 0
web/app/components/base/chat/chat-with-history/inputs-form/index.tsx

@@ -0,0 +1,79 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Message3Fill } from '@/app/components/base/icons/src/public/other'
+import Button from '@/app/components/base/button'
+import Divider from '@/app/components/base/divider'
+import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
+import { useChatWithHistoryContext } from '../context'
+import cn from '@/utils/classnames'
+
+type Props = {
+  collapsed: boolean
+  setCollapsed: (collapsed: boolean) => void
+}
+
+const InputsFormNode = ({
+  collapsed,
+  setCollapsed,
+}: Props) => {
+  const { t } = useTranslation()
+  const {
+    isMobile,
+    currentConversationId,
+    handleStartChat,
+    themeBuilder,
+  } = useChatWithHistoryContext()
+
+  return (
+    <div className={cn('pt-6 px-4 flex flex-col items-center', isMobile && 'pt-4')}>
+      <div className={cn(
+        'w-full max-w-[672px] bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-md',
+        collapsed && 'bg-components-card-bg border border-components-card-border shadow-none',
+      )}>
+        <div className={cn(
+          'flex items-center gap-3 px-6 py-4 rounded-t-2xl',
+          !collapsed && 'border-b border-divider-subtle',
+          isMobile && 'px-4 py-3',
+        )}>
+          <Message3Fill className='shrink-0 w-6 h-6' />
+          <div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
+          {collapsed && (
+            <Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(false)}>{currentConversationId ? t('common.operation.view') : t('common.operation.edit')}</Button>
+          )}
+          {!collapsed && currentConversationId && (
+            <Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(true)}>{t('common.operation.close')}</Button>
+          )}
+        </div>
+        {!collapsed && (
+          <div className={cn('p-6', isMobile && 'p-4')}>
+            <InputsFormContent showTip={!!currentConversationId} />
+          </div>
+        )}
+        {!collapsed && !currentConversationId && (
+          <div className={cn('p-6', isMobile && 'p-4')}>
+            <Button
+              variant='primary'
+              className='w-full'
+              onClick={() => handleStartChat(() => setCollapsed(true))}
+              style={
+                themeBuilder?.theme
+                  ? {
+                    backgroundColor: themeBuilder?.theme.primaryColor,
+                  }
+                  : {}
+              }
+            >{t('share.chat.startChat')}</Button>
+          </div>
+        )}
+      </div>
+      {collapsed && (
+        <div className='py-4 flex items-center w-full max-w-[720px]'>
+          <Divider bgStyle='gradient' className='basis-1/2 h-px rotate-180' />
+          <Divider bgStyle='gradient' className='basis-1/2 h-px' />
+        </div>
+      )}
+    </div>
+  )
+}
+
+export default InputsFormNode

+ 48 - 0
web/app/components/base/chat/chat-with-history/inputs-form/view-form-dropdown.tsx

@@ -0,0 +1,48 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  RiChatSettingsLine,
+} from '@remixicon/react'
+import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
+import { Message3Fill } from '@/app/components/base/icons/src/public/other'
+import InputsFormContent from '@/app/components/base/chat/chat-with-history/inputs-form/content'
+
+const ViewFormDropdown = () => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={{
+        mainAxis: 4,
+        crossAxis: 4,
+      }}
+    >
+      <PortalToFollowElemTrigger
+        onClick={() => setOpen(v => !v)}
+      >
+        <ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
+          <RiChatSettingsLine className='w-[18px] h-[18px]' />
+        </ActionButton>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className="z-50">
+        <div className='w-[400px] bg-components-panel-bg backdrop-blur-sm rounded-2xl border-[0.5px] border-components-panel-border shadow-lg'>
+          <div className='flex items-center gap-3 px-6 py-4 rounded-t-2xl border-b border-divider-subtle'>
+            <Message3Fill className='shrink-0 w-6 h-6' />
+            <div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
+          </div>
+          <div className='p-6'>
+            <InputsFormContent showTip />
+          </div>
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+
+  )
+}
+
+export default ViewFormDropdown

+ 88 - 55
web/app/components/base/chat/chat-with-history/sidebar/index.tsx

@@ -3,22 +3,34 @@ import {
   useState,
 } from 'react'
 import { useTranslation } from 'react-i18next'
+import {
+  RiEditBoxLine,
+  RiExpandRightLine,
+  RiLayoutLeft2Line,
+} from '@remixicon/react'
 import { useChatWithHistoryContext } from '../context'
-import List from './list'
 import AppIcon from '@/app/components/base/app-icon'
+import ActionButton from '@/app/components/base/action-button'
 import Button from '@/app/components/base/button'
-import { Edit05 } from '@/app/components/base/icons/src/vender/line/general'
-import type { ConversationItem } from '@/models/share'
+import List from '@/app/components/base/chat/chat-with-history/sidebar/list'
+import MenuDropdown from '@/app/components/share/text-generation/menu-dropdown'
 import Confirm from '@/app/components/base/confirm'
 import RenameModal from '@/app/components/base/chat/chat-with-history/sidebar/rename-modal'
+import LogoSite from '@/app/components/base/logo/logo-site'
+import type { ConversationItem } from '@/models/share'
+import cn from '@/utils/classnames'
+
+type Props = {
+  isPanel?: boolean
+}
 
-const Sidebar = () => {
+const Sidebar = ({ isPanel }: Props) => {
   const { t } = useTranslation()
   const {
     appData,
+    handleNewConversation,
     pinnedConversationList,
     conversationList,
-    handleNewConversation,
     currentConversationId,
     handleChangeConversation,
     handlePinConversation,
@@ -26,8 +38,12 @@ const Sidebar = () => {
     conversationRenaming,
     handleRenameConversation,
     handleDeleteConversation,
+    sidebarCollapseState,
+    handleSidebarCollapse,
     isMobile,
   } = useChatWithHistoryContext()
+  const isSidebarCollapsed = sidebarCollapseState
+
   const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
   const [showRename, setShowRename] = useState<ConversationItem | null>(null)
 
@@ -60,66 +76,83 @@ const Sidebar = () => {
   }, [showRename, handleRenameConversation, handleCancelRename])
 
   return (
-    <div className='shrink-0 h-full flex flex-col w-[240px] border-r border-r-gray-100'>
-      {
-        !isMobile && (
-          <div className='shrink-0 flex p-4'>
-            <AppIcon
-              className='mr-3'
-              size='small'
-              iconType={appData?.site.icon_type}
-              icon={appData?.site.icon}
-              background={appData?.site.icon_background}
-              imageUrl={appData?.site.icon_url}
-            />
-            <div className='py-1 text-base font-semibold text-gray-800'>
-              {appData?.site.title}
-            </div>
-          </div>
-        )
-      }
-      <div className='shrink-0 p-4'>
-        <Button
-          variant='secondary-accent'
-          className='justify-start w-full'
-          onClick={handleNewConversation}
-        >
-          <Edit05 className='mr-2 w-4 h-4' />
+    <div className={cn(
+      'grow flex flex-col',
+      isPanel && 'rounded-xl bg-components-panel-bg border-[0.5px] border-components-panel-border-subtle shadow-lg',
+    )}>
+      <div className={cn(
+        'shrink-0 flex items-center gap-3 p-3 pr-2',
+      )}>
+        <div className='shrink-0'>
+          <AppIcon
+            size='large'
+            iconType={appData?.site.icon_type}
+            icon={appData?.site.icon}
+            background={appData?.site.icon_background}
+            imageUrl={appData?.site.icon_url}
+          />
+        </div>
+        <div className={cn('grow text-text-secondary system-md-semibold truncate')}>{appData?.site.title}</div>
+        {!isMobile && isSidebarCollapsed && (
+          <ActionButton size='l' onClick={() => handleSidebarCollapse(false)}>
+            <RiExpandRightLine className='w-[18px] h-[18px]' />
+          </ActionButton>
+        )}
+        {!isMobile && !isSidebarCollapsed && (
+          <ActionButton size='l' onClick={() => handleSidebarCollapse(true)}>
+            <RiLayoutLeft2Line className='w-[18px] h-[18px]' />
+          </ActionButton>
+        )}
+      </div>
+      <div className='shrink-0 px-3 py-4'>
+        <Button variant='secondary-accent' className='w-full justify-center' onClick={handleNewConversation}>
+          <RiEditBoxLine className='w-4 h-4 mr-1' />
           {t('share.chat.newChat')}
         </Button>
       </div>
-      <div className='grow px-4 py-2 overflow-y-auto'>
-        {
-          !!pinnedConversationList.length && (
-            <div className='mb-4'>
-              <List
-                isPin
-                title={t('share.chat.pinnedTitle') || ''}
-                list={pinnedConversationList}
-                onChangeConversation={handleChangeConversation}
-                onOperate={handleOperate}
-                currentConversationId={currentConversationId}
-              />
-            </div>
-          )
-        }
-        {
-          !!conversationList.length && (
+      <div className='grow h-0 pt-4 px-3 space-y-2 overflow-y-auto'>
+        {/* pinned list */}
+        {!!pinnedConversationList.length && (
+          <div className='mb-4'>
             <List
-              title={(pinnedConversationList.length && t('share.chat.unpinnedTitle')) || ''}
-              list={conversationList}
+              isPin
+              title={t('share.chat.pinnedTitle') || ''}
+              list={pinnedConversationList}
               onChangeConversation={handleChangeConversation}
               onOperate={handleOperate}
               currentConversationId={currentConversationId}
             />
-          )
-        }
+          </div>
+        )}
+        {!!conversationList.length && (
+          <List
+            title={(pinnedConversationList.length && t('share.chat.unpinnedTitle')) || ''}
+            list={conversationList}
+            onChangeConversation={handleChangeConversation}
+            onOperate={handleOperate}
+            currentConversationId={currentConversationId}
+          />
+        )}
       </div>
-      {appData?.site.copyright && (
-        <div className='px-4 pb-4 text-xs text-gray-400'>
-          © {(new Date()).getFullYear()} {appData?.site.copyright}
+      <div className='shrink-0 p-3 flex items-center justify-between'>
+        <MenuDropdown placement='top-start' data={appData?.site} />
+        {/* powered by */}
+        <div className='shrink-0'>
+          {!appData?.custom_config?.remove_webapp_brand && (
+            <div className={cn(
+              'shrink-0 px-2 flex items-center gap-1.5',
+            )}>
+              <div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
+              {appData?.custom_config?.replace_webapp_logo && (
+                <img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
+              )}
+              {!appData?.custom_config?.replace_webapp_logo && (
+                <LogoSite className='!h-5' />
+              )}
+            </div>
+          )}
         </div>
-      )}
+      </div>
       {!!showConfirm && (
         <Confirm
           title={t('share.chat.deleteConversation.title')}

+ 11 - 11
web/app/components/base/chat/chat-with-history/sidebar/item.tsx

@@ -5,8 +5,8 @@ import {
 } from 'react'
 import { useHover } from 'ahooks'
 import type { ConversationItem } from '@/models/share'
-import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
-import ItemOperation from '@/app/components/explore/item-operation'
+import Operation from '@/app/components/base/chat/chat-with-history/sidebar/operation'
+import cn from '@/utils/classnames'
 
 type ItemProps = {
   isPin?: boolean
@@ -24,23 +24,23 @@ const Item: FC<ItemProps> = ({
 }) => {
   const ref = useRef(null)
   const isHovering = useHover(ref)
+  const isSelected = currentConversationId === item.id
 
   return (
     <div
       ref={ref}
       key={item.id}
-      className={`
-        flex mb-0.5 last-of-type:mb-0 py-1.5 pl-3 pr-1.5 text-sm font-medium text-gray-700 
-        rounded-lg cursor-pointer hover:bg-gray-50 group
-        ${currentConversationId === item.id && 'text-primary-600 bg-primary-50'}
-      `}
+      className={cn(
+        'group flex p-1 pl-3 rounded-lg cursor-pointer text-components-menu-item-text system-sm-medium hover:bg-state-base-hover',
+        isSelected && 'bg-state-accent-active hover:bg-state-accent-active text-text-accent',
+      )}
       onClick={() => onChangeConversation(item.id)}
     >
-      <MessageDotsCircle className={`shrink-0 mt-1 mr-2 w-4 h-4 text-gray-400 ${currentConversationId === item.id && 'text-primary-600'}`} />
-      <div className='grow py-0.5 break-all' title={item.name}>{item.name}</div>
+      <div className='grow p-1 pl-0 truncate' title={item.name}>{item.name}</div>
       {item.id !== '' && (
-        <div className='shrink-0 h-6' onClick={e => e.stopPropagation()}>
-          <ItemOperation
+        <div className='shrink-0' onClick={e => e.stopPropagation()}>
+          <Operation
+            isActive={isSelected}
             isPinned={!!isPin}
             isItemHovering={isHovering}
             togglePin={() => onOperate(isPin ? 'unpin' : 'pin', item)}

+ 14 - 20
web/app/components/base/chat/chat-with-history/sidebar/list.tsx

@@ -19,26 +19,20 @@ const List: FC<ListProps> = ({
   currentConversationId,
 }) => {
   return (
-    <div>
-      {
-        title && (
-          <div className='mb-0.5 px-3 h-[26px] text-xs font-medium text-gray-500'>
-            {title}
-          </div>
-        )
-      }
-      {
-        list.map(item => (
-          <Item
-            key={item.id}
-            isPin={isPin}
-            item={item}
-            onOperate={onOperate}
-            onChangeConversation={onChangeConversation}
-            currentConversationId={currentConversationId}
-          />
-        ))
-      }
+    <div className='space-y-0.5'>
+      {title && (
+        <div className='px-3 pt-2 pb-1 text-text-tertiary system-xs-medium-uppercase'>{title}</div>
+      )}
+      {list.map(item => (
+        <Item
+          key={item.id}
+          isPin={isPin}
+          item={item}
+          onOperate={onOperate}
+          onChangeConversation={onChangeConversation}
+          currentConversationId={currentConversationId}
+        />
+      ))}
     </div>
   )
 }

+ 101 - 0
web/app/components/base/chat/chat-with-history/sidebar/operation.tsx

@@ -0,0 +1,101 @@
+'use client'
+import type { FC } from 'react'
+import React, { useEffect, useRef, useState } from 'react'
+import {
+  RiDeleteBinLine,
+  RiEditLine,
+  RiMoreFill,
+  RiPushpinLine,
+  RiUnpinLine,
+} from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import { useBoolean } from 'ahooks'
+import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
+import cn from '@/utils/classnames'
+
+type Props = {
+  isActive?: boolean
+  isItemHovering?: boolean
+  isPinned: boolean
+  isShowRenameConversation?: boolean
+  onRenameConversation?: () => void
+  isShowDelete: boolean
+  togglePin: () => void
+  onDelete: () => void
+}
+
+const Operation: FC<Props> = ({
+  isActive,
+  isItemHovering,
+  isPinned,
+  togglePin,
+  isShowRenameConversation,
+  onRenameConversation,
+  isShowDelete,
+  onDelete,
+}) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+  const ref = useRef(null)
+  const [isHovering, { setTrue: setIsHovering, setFalse: setNotHovering }] = useBoolean(false)
+  useEffect(() => {
+    if (!isItemHovering && !isHovering)
+      setOpen(false)
+  }, [isItemHovering, isHovering])
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={4}
+    >
+      <PortalToFollowElemTrigger
+        onClick={() => setOpen(v => !v)}
+      >
+        <ActionButton
+          className={cn((isItemHovering || open) ? 'opacity-100' : 'opacity-0')}
+          state={
+            isActive
+              ? ActionButtonState.Active
+              : open
+                ? ActionButtonState.Hover
+                : ActionButtonState.Default
+          }
+        >
+          <RiMoreFill className='w-4 h-4' />
+        </ActionButton>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className="z-50">
+        <div
+          ref={ref}
+          className={'min-w-[120px] p-1 bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'}
+          onMouseEnter={setIsHovering}
+          onMouseLeave={setNotHovering}
+          onClick={(e) => {
+            e.stopPropagation()
+          }}
+        >
+          <div className={cn('flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={togglePin}>
+            {isPinned && <RiUnpinLine className='shrink-0 w-4 h-4 text-text-tertiary' />}
+            {!isPinned && <RiPushpinLine className='shrink-0 w-4 h-4 text-text-tertiary' />}
+            <span className='grow'>{isPinned ? t('explore.sidebar.action.unpin') : t('explore.sidebar.action.pin')}</span>
+          </div>
+          {isShowRenameConversation && (
+            <div className={cn('flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover')} onClick={onRenameConversation}>
+              <RiEditLine className='shrink-0 w-4 h-4 text-text-tertiary' />
+              <span className='grow'>{t('explore.sidebar.action.rename')}</span>
+            </div>
+          )}
+          {isShowDelete && (
+            <div className={cn('group flex items-center space-x-1 px-2 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-destructive-hover hover:text-text-destructive')} onClick={onDelete} >
+              <RiDeleteBinLine className={cn('shrink-0 w-4 h-4 text-text-tertiary group-hover:text-text-destructive')} />
+              <span className='grow'>{t('explore.sidebar.action.delete')}</span>
+            </div>
+          )}
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+  )
+}
+export default React.memo(Operation)

+ 5 - 4
web/app/components/base/chat/chat-with-history/sidebar/rename-modal.tsx

@@ -4,6 +4,7 @@ import React, { useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import Modal from '@/app/components/base/modal'
 import Button from '@/app/components/base/button'
+import Input from '@/app/components/base/input'
 
 export type IRenameModalProps = {
   isShow: boolean
@@ -29,16 +30,16 @@ const RenameModal: FC<IRenameModalProps> = ({
       isShow={isShow}
       onClose={onClose}
     >
-      <div className={'mt-6 font-medium text-sm leading-[21px] text-gray-900'}>{t('common.chat.conversationName')}</div>
-      <input className={'mt-2 w-full rounded-lg h-10 box-border px-3 text-sm leading-10 bg-gray-100'}
+      <div className={'mt-6 font-medium text-sm leading-[21px] text-text-primary'}>{t('common.chat.conversationName')}</div>
+      <Input className='mt-2 w-full h-10'
         value={tempName}
         onChange={e => setTempName(e.target.value)}
         placeholder={t('common.chat.conversationNamePlaceholder') || ''}
       />
 
       <div className='mt-10 flex justify-end'>
-        <Button className='mr-2 flex-shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
-        <Button variant='primary' className='flex-shrink-0' onClick={() => onSave(tempName)} loading={saveLoading}>{t('common.operation.save')}</Button>
+        <Button className='mr-2 shrink-0' onClick={onClose}>{t('common.operation.cancel')}</Button>
+        <Button variant='primary' className='shrink-0' onClick={() => onSave(tempName)} loading={saveLoading}>{t('common.operation.save')}</Button>
       </div>
     </Modal>
   )

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

@@ -105,7 +105,7 @@ const Answer: FC<AnswerProps> = ({
       <div className='shrink-0 relative w-10 h-10'>
         {answerIcon || <AnswerIcon />}
         {responding && (
-          <div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-white rounded-full shadow-xs border-[0.5px] border-gray-50'>
+          <div className='absolute -top-[3px] -left-[3px] pl-[6px] flex items-center w-4 h-4 bg-background-section-burn rounded-full shadow-xs border-[0.5px] border-divider-subtle'>
             <LoadingAnim type='avatar' />
           </div>
         )}

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

@@ -13,7 +13,7 @@ const More: FC<MoreProps> = ({
   const { t } = useTranslation()
 
   return (
-    <div className='flex items-center mt-1 h-[18px] text-xs text-gray-400 opacity-0 group-hover:opacity-100'>
+    <div className='mt-1 flex items-center system-xs-regular text-text-quaternary opacity-0 group-hover:opacity-100'>
       {
         more && (
           <>

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

@@ -5,23 +5,24 @@ import {
   useState,
 } from 'react'
 import { useTranslation } from 'react-i18next'
+import {
+  RiClipboardLine,
+  RiEditLine,
+  RiReplay15Line,
+  RiThumbDownLine,
+  RiThumbUpLine,
+} from '@remixicon/react'
 import type { ChatItem } from '../../types'
 import { useChatContext } from '../context'
-import RegenerateBtn from '@/app/components/base/regenerate-btn'
-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/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn'
+import copy from 'copy-to-clipboard'
+import Toast from '@/app/components/base/toast'
 import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
-import {
-  ThumbsDown,
-  ThumbsUp,
-} from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
-import Tooltip from '@/app/components/base/tooltip'
 import Log from '@/app/components/base/chat/chat/log'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
+import NewAudioButton from '@/app/components/base/new-audio-button'
+import cn from '@/utils/classnames'
 
-interface OperationProps {
+type OperationProps = {
   item: ChatItem
   question: string
   index: number
@@ -60,7 +61,6 @@ const Operation: FC<OperationProps> = ({
     adminFeedback,
     agent_thoughts,
   } = item
-  const hasAnnotation = !!annotation?.id
   const [localFeedback, setLocalFeedback] = useState(config?.supportAnnotation ? adminFeedback : feedback)
 
   const content = useMemo(() => {
@@ -102,121 +102,68 @@ const Operation: FC<OperationProps> = ({
       <div
         className={cn(
           'absolute flex justify-end gap-1',
-          hasWorkflowProcess && '-top-3.5 -right-3.5',
-          !positionRight && '-top-3.5 -right-3.5',
+          hasWorkflowProcess && '-bottom-4 right-2',
+          !positionRight && '-bottom-4 right-2',
           !hasWorkflowProcess && positionRight && '!top-[9px]',
         )}
         style={(!hasWorkflowProcess && positionRight) ? { left: contentWidth + 8 } : {}}
       >
+        {showPromptLog && (
+          <div className='hidden group-hover:block'>
+            <Log logItem={item} />
+          </div>
+        )}
         {!isOpeningStatement && (
-          <CopyBtn
-            value={content}
-            className='hidden group-hover:block'
-          />
+          <div className='hidden group-hover:flex ml-1 items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
+            {(config?.text_to_speech?.enabled) && (
+              <NewAudioButton
+                id={id}
+                value={content}
+                voice={config?.text_to_speech?.voice}
+              />
+            )}
+            <ActionButton onClick={() => {
+              copy(content)
+              Toast.notify({ type: 'success', message: t('common.actionMsg.copySuccessfully') })
+            }}>
+              <RiClipboardLine className='w-4 h-4' />
+            </ActionButton>
+            {!noChatInput && (
+              <ActionButton onClick={() => onRegenerate?.(item)}>
+                <RiReplay15Line className='w-4 h-4' />
+              </ActionButton>
+            )}
+            {(config?.supportAnnotation && config.annotation_reply?.enabled) && (
+              <ActionButton onClick={() => setIsShowReplyModal(true)}>
+                <RiEditLine className='w-4 h-4' />
+              </ActionButton>
+            )}
+          </div>
         )}
-
-        {!isOpeningStatement && (showPromptLog || config?.text_to_speech?.enabled) && (
-          <div className='hidden group-hover:flex items-center w-max h-[28px] p-0.5 rounded-lg bg-white border-[0.5px] border-gray-100 shadow-md shrink-0'>
-            {showPromptLog && (
+        {!isOpeningStatement && config?.supportFeedback && onFeedback && (
+          <div className='hidden group-hover:flex ml-1 items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'>
+            {!localFeedback?.rating && (
               <>
-                <Log logItem={item} />
-                <div className='mx-1 w-[1px] h-[14px] bg-gray-200' />
+                <ActionButton onClick={() => handleFeedback('like')}>
+                  <RiThumbUpLine className='w-4 h-4' />
+                </ActionButton>
+                <ActionButton onClick={() => handleFeedback('dislike')}>
+                  <RiThumbDownLine className='w-4 h-4' />
+                </ActionButton>
               </>
             )}
-
-            {(config?.text_to_speech?.enabled) && (
-              <>
-                <AudioBtn
-                  id={id}
-                  value={content}
-                  noCache={false}
-                  voice={config?.text_to_speech?.voice}
-                  className='hidden group-hover:block'
-                />
-              </>
+            {localFeedback?.rating === 'like' && (
+              <ActionButton state={ActionButtonState.Active} onClick={() => handleFeedback(null)}>
+                <RiThumbUpLine className='w-4 h-4' />
+              </ActionButton>
+            )}
+            {localFeedback?.rating === 'dislike' && (
+              <ActionButton state={ActionButtonState.Destructive} onClick={() => handleFeedback(null)}>
+                <RiThumbDownLine className='w-4 h-4' />
+              </ActionButton>
             )}
           </div>
         )}
-
-        {(!isOpeningStatement && config?.supportAnnotation && config.annotation_reply?.enabled) && (
-          <AnnotationCtrlBtn
-            appId={config?.appId || ''}
-            messageId={id}
-            annotationId={annotation?.id || ''}
-            className='hidden group-hover:block ml-1 shrink-0'
-            cached={hasAnnotation}
-            query={question}
-            answer={content}
-            onAdded={(id, authorName) => onAnnotationAdded?.(id, authorName, question, content, index)}
-            onEdit={() => setIsShowReplyModal(true)}
-            onRemoved={() => onAnnotationRemoved?.(index)}
-          />
-        )}
-        {
-          annotation?.id && (
-            <div
-              className='relative box-border flex items-center justify-center h-7 w-7 p-0.5 rounded-lg bg-white cursor-pointer text-[#444CE7] shadow-md group-hover:hidden'
-            >
-              <div className='p-1 rounded-lg bg-[#EEF4FF] '>
-                <MessageFast className='w-4 h-4' />
-              </div>
-            </div>
-          )
-        }
-        {
-          !isOpeningStatement && !noChatInput && <RegenerateBtn className='hidden group-hover:block mr-1' onClick={() => onRegenerate?.(item)} />
-        }
-        {
-          config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
-            <div className='hidden group-hover:flex shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
-              <Tooltip popupContent={t('appDebug.operation.agree')}>
-                <div
-                  className='flex items-center justify-center mr-0.5 w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
-                  onClick={() => handleFeedback('like')}
-                >
-                  <ThumbsUp className='w-4 h-4' />
-                </div>
-              </Tooltip>
-              <Tooltip
-                popupContent={t('appDebug.operation.disagree')}
-              >
-                <div
-                  className='flex items-center justify-center w-6 h-6 rounded-md hover:bg-black/5 hover:text-gray-800 cursor-pointer'
-                  onClick={() => handleFeedback('dislike')}
-                >
-                  <ThumbsDown className='w-4 h-4' />
-                </div>
-              </Tooltip>
-            </div>
-          )
-        }
-        {
-          config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
-            <Tooltip
-              popupContent={localFeedback.rating === 'like' ? t('appDebug.operation.cancelAgree') : t('appDebug.operation.cancelDisagree')}
-            >
-              <div
-                className={`
-                  flex items-center justify-center w-7 h-7 rounded-[10px] border-[2px] border-white cursor-pointer
-                  ${localFeedback.rating === 'like' && 'bg-blue-50 text-blue-600'}
-                  ${localFeedback.rating === 'dislike' && 'bg-red-100 text-red-600'}
-                `}
-                onClick={() => handleFeedback(null)}
-              >
-                {
-                  localFeedback.rating === 'like' && (
-                    <ThumbsUp className='w-4 h-4' />
-                  )
-                }
-                {
-                  localFeedback.rating === 'dislike' && (
-                    <ThumbsDown className='w-4 h-4' />
-                  )
-                }
-              </div>
-            </Tooltip>
-          )
-        }
       </div>
       <EditReplyModal
         isShow={isShowReplyModal}

+ 5 - 3
web/app/components/base/chat/chat/answer/suggested-questions.tsx

@@ -2,6 +2,7 @@ import type { FC } from 'react'
 import { memo } from 'react'
 import type { ChatItem } from '../../types'
 import { useChatContext } from '../context'
+import Button from '@/app/components/base/button'
 
 type SuggestedQuestionsProps = {
   item: ChatItem
@@ -21,13 +22,14 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
   return (
     <div className='flex flex-wrap'>
       {suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
-        <div
+        <Button
           key={index}
-          className='mt-1 mr-1 max-w-full last:mr-0 shrink-0 py-[5px] leading-[18px] items-center px-4 rounded-lg border border-gray-200 shadow-xs bg-white text-xs font-medium text-primary-600 cursor-pointer'
+          variant='secondary-accent'
+          className='mt-1 mr-1 max-w-full last:mr-0 shrink-0'
           onClick={() => onSend?.(question)}
         >
           {question}
-        </div>),
+        </Button>),
       )}
     </div>
   )

+ 7 - 20
web/app/components/base/chat/chat/answer/workflow-process.tsx

@@ -1,6 +1,5 @@
 import {
   useEffect,
-  useMemo,
   useState,
 } from 'react'
 import {
@@ -36,19 +35,6 @@ const WorkflowProcessItem = ({
   const succeeded = data.status === WorkflowRunningStatus.Succeeded
   const failed = data.status === WorkflowRunningStatus.Failed || data.status === WorkflowRunningStatus.Stopped
 
-  const background = useMemo(() => {
-    if (collapse)
-      return 'linear-gradient(90deg, rgba(200, 206, 218, 0.20) 0%, rgba(200, 206, 218, 0.04) 100%)'
-    if (running && !collapse)
-      return 'linear-gradient(180deg, #E1E4EA 0%, #EAECF0 100%)'
-
-    if (succeeded && !collapse)
-      return 'linear-gradient(180deg, #ECFDF3 0%, #F6FEF9 100%)'
-
-    if (failed && !collapse)
-      return 'linear-gradient(180deg, #FEE4E2 0%, #FEF3F2 100%)'
-  }, [running, succeeded, failed, collapse])
-
   useEffect(() => {
     setCollapse(!expand)
   }, [expand])
@@ -56,12 +42,13 @@ const WorkflowProcessItem = ({
   return (
     <div
       className={cn(
-        '-mx-1 px-2.5 rounded-xl border-[0.5px]',
-        collapse ? 'py-[7px] border-components-panel-border' : 'pt-[7px] px-1 pb-1 border-components-panel-border-subtle',
+        '-mx-1 px-2.5 rounded-xl',
+        collapse ? 'py-[7px] border-l-[0.25px] border-components-panel-border' : 'pt-[7px] px-1 pb-1 border-[0.5px] border-components-panel-border-subtle',
+        running && !collapse && 'bg-background-section-burn',
+        succeeded && !collapse && 'bg-state-success-hover',
+        failed && !collapse && 'bg-state-destructive-hover',
+        collapse && 'bg-workflow-process-bg',
       )}
-      style={{
-        background,
-      }}
     >
       <div
         className={cn('flex items-center cursor-pointer', !collapse && 'px-1.5', readonly && 'cursor-default')}
@@ -85,7 +72,7 @@ const WorkflowProcessItem = ({
         <div className={cn('system-xs-medium text-text-secondary', !collapse && 'grow')}>
           {t('workflow.common.workflowProcess')}
         </div>
-        {!readonly && <RiArrowRightSLine className={`'ml-1 w-4 h-4 text-text-tertiary' ${collapse ? '' : 'rotate-90'}`} />}
+        {!readonly && <RiArrowRightSLine className={cn('ml-1 w-4 h-4 text-text-tertiary', !collapse && 'rotate-90')} />}
       </div>
       {
         !collapse && !readonly && (

+ 3 - 0
web/app/components/base/chat/chat/chat-input-area/index.tsx

@@ -40,6 +40,7 @@ type ChatInputAreaProps = {
   inputsForm?: InputForm[]
   theme?: Theme | null
   isResponding?: boolean
+  disabled?: boolean
 }
 const ChatInputArea = ({
   showFeatureBar,
@@ -53,6 +54,7 @@ const ChatInputArea = ({
   inputsForm = [],
   theme,
   isResponding,
+  disabled,
 }: ChatInputAreaProps) => {
   const { t } = useTranslation()
   const { notify } = useToastContext()
@@ -155,6 +157,7 @@ const ChatInputArea = ({
         className={cn(
           'relative pb-[9px] bg-components-panel-bg-blur border border-components-chat-input-border rounded-xl shadow-md z-10',
           isDragActive && 'border border-dashed border-components-option-card-option-selected-border',
+          disabled && 'opacity-50 pointer-events-none border-components-panel-border shadow-none',
         )}
       >
         <div className='relative px-[9px] pt-[9px] max-h-[158px] overflow-x-hidden overflow-y-auto'>

+ 5 - 5
web/app/components/base/chat/chat/citation/index.tsx

@@ -77,9 +77,9 @@ const Citation: FC<CitationProps> = ({
 
   return (
     <div className='mt-3 -mb-1'>
-      <div className='flex items-center mb-2 text-xs font-medium text-gray-500'>
+      <div className='flex items-center mb-2 system-xs-medium text-text-tertiary'>
         {t('common.chat.citation.title')}
-        <div className='grow ml-2 h-[1px] bg-black/5' />
+        <div className='grow ml-2 h-[1px] bg-divider-regular' />
       </div>
       <div className='relative flex flex-wrap'>
         {
@@ -87,7 +87,7 @@ const Citation: FC<CitationProps> = ({
             <div
               key={index}
               className='absolute top-0 left-0 w-auto mr-1 mb-1 pl-7 pr-2 max-w-[240px] h-7 text-xs whitespace-nowrap opacity-0 -z-10'
-              ref={ele => (elesRef.current[index] = ele!)}
+              ref={(ele: any) => (elesRef.current[index] = ele!)}
             >
               {res.documentName}
             </div>
@@ -106,13 +106,13 @@ const Citation: FC<CitationProps> = ({
         {
           limitNumberInOneLine < resourcesLength && (
             <div
-              className='flex items-center px-2 h-7 bg-white rounded-lg text-xs font-medium text-gray-500 cursor-pointer'
+              className='flex items-center px-2 h-7 bg-components-panel-bg rounded-lg text-text-tertiary system-xs-medium cursor-pointer'
               onClick={() => setShowMore(v => !v)}
             >
               {
                 !showMore
                   ? `+ ${resourcesLength - limitNumberInOneLine}`
-                  : <RiArrowDownSLine className='w-4 h-4 text-gray-600 rotate-180' />
+                  : <RiArrowDownSLine className='w-4 h-4 text-text-tertiary rotate-180' />
               }
             </div>
           )

+ 12 - 12
web/app/components/base/chat/chat/citation/popup.tsx

@@ -47,29 +47,29 @@ const Popup: FC<PopupProps> = ({
       }}
     >
       <PortalToFollowElemTrigger onClick={() => setOpen(v => !v)}>
-        <div className='flex items-center px-2 max-w-[240px] h-7 bg-white rounded-lg'>
+        <div className='flex items-center px-2 max-w-[240px] h-7 bg-components-button-secondary-bg rounded-lg'>
           <FileIcon type={fileType} className='shrink-0 mr-1 w-4 h-4' />
-          <div className='text-xs text-gray-600 truncate'>{data.documentName}</div>
+          <div className='text-xs text-text-tertiary truncate'>{data.documentName}</div>
         </div>
       </PortalToFollowElemTrigger>
       <PortalToFollowElemContent style={{ zIndex: 1000 }}>
-        <div className='max-w-[360px] bg-gray-50 rounded-xl shadow-lg'>
+        <div className='max-w-[360px] bg-background-section-burn rounded-xl shadow-lg'>
           <div className='px-4 pt-3 pb-2'>
             <div className='flex items-center h-[18px]'>
               <FileIcon type={fileType} className='shrink-0 mr-1 w-4 h-4' />
-              <div className='text-xs font-medium text-gray-600 truncate'>{data.documentName}</div>
+              <div className='system-xs-medium text-text-tertiary truncate'>{data.documentName}</div>
             </div>
           </div>
-          <div className='px-4 py-0.5 max-h-[450px] bg-white rounded-lg overflow-y-auto'>
+          <div className='px-4 py-0.5 max-h-[450px] bg-components-panel-bg rounded-lg overflow-y-auto'>
             <div className='w-full'>
               {
                 data.sources.map((source, index) => (
                   <Fragment key={index}>
                     <div className='group py-3'>
                       <div className='flex items-center justify-between mb-2'>
-                        <div className='flex items-center px-1.5 h-5 border border-gray-200 rounded-md'>
-                          <Hash02 className='mr-0.5 w-3 h-3 text-gray-400' />
-                          <div className='text-[11px] font-medium text-gray-500'>
+                        <div className='flex items-center px-1.5 h-5 border border-divider-subtle rounded-md'>
+                          <Hash02 className='mr-0.5 w-3 h-3 text-text-quaternary' />
+                          <div className='text-[11px] font-medium text-text-tertiary'>
                             {source.segment_position || index + 1}
                           </div>
                         </div>
@@ -77,17 +77,17 @@ const Popup: FC<PopupProps> = ({
                           showHitInfo && (
                             <Link
                               href={`/datasets/${source.dataset_id}/documents/${source.document_id}`}
-                              className='hidden items-center h-[18px] text-xs text-primary-600 group-hover:flex'>
+                              className='hidden items-center h-[18px] text-xs text-text-accent group-hover:flex'>
                               {t('common.chat.citation.linkToDataset')}
                               <ArrowUpRight className='ml-1 w-3 h-3' />
                             </Link>
                           )
                         }
                       </div>
-                      <div className='text-[13px] text-gray-800 break-words'>{source.content}</div>
+                      <div className='text-[13px] text-text-secondary break-words'>{source.content}</div>
                       {
                         showHitInfo && (
-                          <div className='flex items-center mt-2 text-xs font-medium text-gray-500 flex-wrap'>
+                          <div className='flex items-center mt-2 system-xs-medium text-text-quaternary flex-wrap'>
                             <Tooltip
                               text={t('common.chat.citation.characters')}
                               data={source.word_count}
@@ -114,7 +114,7 @@ const Popup: FC<PopupProps> = ({
                     </div>
                     {
                       index !== data.sources.length - 1 && (
-                        <div className='my-1 h-[1px] bg-black/5' />
+                        <div className='my-1 h-[1px] bg-divider-regular' />
                       )
                     }
                   </Fragment>

+ 3 - 3
web/app/components/base/chat/chat/citation/progress-tooltip.tsx

@@ -28,14 +28,14 @@ const ProgressTooltip: FC<ProgressTooltipProps> = ({
         onMouseLeave={() => setOpen(false)}
       >
         <div className='grow flex items-center'>
-          <div className='mr-1 w-16 h-1.5 rounded-[3px] border border-gray-400 overflow-hidden'>
-            <div className='bg-gray-400 h-full' style={{ width: `${data * 100}%` }}></div>
+          <div className='mr-1 w-16 h-1.5 rounded-[3px] border border-components-progress-gray-border overflow-hidden'>
+            <div className='bg-components-progress-gray-progress h-full' style={{ width: `${data * 100}%` }}></div>
           </div>
           {data}
         </div>
       </PortalToFollowElemTrigger>
       <PortalToFollowElemContent style={{ zIndex: 1001 }}>
-        <div className='p-3 bg-white text-xs font-medium text-gray-500 rounded-lg shadow-lg'>
+        <div className='p-3 bg-components-tooltip-bg system-xs-medium text-text-quaternary rounded-lg shadow-lg'>
           {t('common.chat.citation.hitScore')} {data}
         </div>
       </PortalToFollowElemContent>

+ 1 - 1
web/app/components/base/chat/chat/citation/tooltip.tsx

@@ -35,7 +35,7 @@ const Tooltip: FC<TooltipProps> = ({
         </div>
       </PortalToFollowElemTrigger>
       <PortalToFollowElemContent style={{ zIndex: 1001 }}>
-        <div className='p-3 bg-white text-xs font-medium text-gray-500 rounded-lg shadow-lg'>
+        <div className='p-3 bg-components-tooltip-bg system-xs-medium text-text-quaternary rounded-lg shadow-lg'>
           {text} {data}
         </div>
       </PortalToFollowElemContent>

+ 2 - 1
web/app/components/base/chat/chat/hooks.ts

@@ -397,6 +397,7 @@ export const useChat = (
               )
               setSuggestQuestions(data)
             }
+            // eslint-disable-next-line unused-imports/no-unused-vars
             catch (e) {
               setSuggestQuestions([])
             }
@@ -555,7 +556,7 @@ export const useChat = (
             if (!item.execution_metadata?.parallel_id)
               return item.node_id === nodeFinishedData.node_id
 
-            return item.node_id === nodeFinishedData.node_id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata.parallel_id)
+            return item.node_id === nodeFinishedData.node_id && (item.execution_metadata?.parallel_id === nodeFinishedData.execution_metadata?.parallel_id)
           })
           responseItem.workflowProcess!.tracing[currentIndex] = nodeFinishedData as any
 

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

@@ -70,6 +70,8 @@ export type ChatProps = {
   showFileUpload?: boolean
   onFeatureBarClick?: (state: boolean) => void
   noSpacing?: boolean
+  inputDisabled?: boolean
+  isMobile?: boolean
 }
 
 const Chat: FC<ChatProps> = ({
@@ -106,6 +108,8 @@ const Chat: FC<ChatProps> = ({
   showFileUpload,
   onFeatureBarClick,
   noSpacing,
+  inputDisabled,
+  isMobile,
 }) => {
   const { t } = useTranslation()
   const { currentLogItem, setCurrentLogItem, showPromptLogModal, setShowPromptLogModal, showAgentLogModal, setShowAgentLogModal } = useAppStore(useShallow(state => ({
@@ -273,12 +277,14 @@ const Chat: FC<ChatProps> = ({
                 <TryToAsk
                   suggestedQuestions={suggestedQuestions}
                   onSend={onSend}
+                  isMobile={isMobile}
                 />
               )
             }
             {
               !noChatInput && (
                 <ChatInputArea
+                  disabled={inputDisabled}
                   showFeatureBar={showFeatureBar}
                   showFileUpload={showFileUpload}
                   featureBarDisabled={isResponding}

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

@@ -1,8 +1,8 @@
 import type { FC } from 'react'
-import { useTranslation } from 'react-i18next'
-import { File02 } from '@/app/components/base/icons/src/vender/line/files'
+import { RiFileList3Line } from '@remixicon/react'
 import type { IChatItem } from '@/app/components/base/chat/chat/type'
 import { useStore as useAppStore } from '@/app/components/app/store'
+import ActionButton from '@/app/components/base/action-button'
 
 type LogProps = {
   logItem: IChatItem
@@ -10,7 +10,6 @@ type LogProps = {
 const Log: FC<LogProps> = ({
   logItem,
 }) => {
-  const { t } = useTranslation()
   const setCurrentLogItem = useAppStore(s => s.setCurrentLogItem)
   const setShowPromptLogModal = useAppStore(s => s.setShowPromptLogModal)
   const setShowAgentLogModal = useAppStore(s => s.setShowAgentLogModal)
@@ -20,7 +19,7 @@ const Log: FC<LogProps> = ({
 
   return (
     <div
-      className='shrink-0 p-1 flex items-center justify-center rounded-[6px] font-medium text-gray-500 hover:bg-gray-50 cursor-pointer hover:text-gray-700'
+      className='ml-1 flex items-center gap-0.5 p-0.5 rounded-[10px] border-[0.5px] border-components-actionbar-border bg-components-actionbar-bg shadow-md backdrop-blur-sm'
       onClick={(e) => {
         e.stopPropagation()
         e.nativeEvent.stopImmediatePropagation()
@@ -33,8 +32,9 @@ const Log: FC<LogProps> = ({
           setShowPromptLogModal(true)
       }}
     >
-      <File02 className='mr-1 w-4 h-4' />
-      <div className='text-xs leading-4'>{runID ? t('appLog.viewLog') : isAgent ? t('appLog.agentLog') : t('appLog.promptLog')}</div>
+      <ActionButton>
+        <RiFileList3Line className='w-4 h-4' />
+      </ActionButton>
     </div>
   )
 }

+ 12 - 21
web/app/components/base/chat/chat/try-to-ask.tsx

@@ -2,46 +2,37 @@ import type { FC } from 'react'
 import { memo } from 'react'
 import { useTranslation } from 'react-i18next'
 import type { OnSend } from '../types'
-import { Star04 } from '@/app/components/base/icons/src/vender/solid/shapes'
 import Button from '@/app/components/base/button'
+import Divider from '@/app/components/base/divider'
+import cn from '@/utils/classnames'
 
 type TryToAskProps = {
   suggestedQuestions: string[]
   onSend: OnSend
+  isMobile?: boolean
 }
 const TryToAsk: FC<TryToAskProps> = ({
   suggestedQuestions,
   onSend,
+  isMobile,
 }) => {
   const { t } = useTranslation()
 
   return (
-    <div>
-      <div className='flex items-center mb-2.5 py-2'>
-        <div
-          className='grow h-[1px]'
-          style={{
-            background: 'linear-gradient(270deg, #F3F4F6 0%, rgba(243, 244, 246, 0) 100%)',
-          }}
-        />
-        <div className='shrink-0 flex items-center px-3 text-gray-500'>
-          <Star04 className='mr-1 w-2.5 h-2.5' />
-          <span className='text-xs text-gray-500 font-medium'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</span>
-        </div>
-        <div
-          className='grow h-[1px]'
-          style={{
-            background: 'linear-gradient(270deg, rgba(243, 244, 246, 0) 0%, #F3F4F6 100%)',
-          }}
-        />
+    <div className='mb-2 py-2'>
+      <div className={cn('flex items-center justify-between gap-2 mb-2.5', isMobile && 'justify-end')}>
+        <Divider bgStyle='gradient' className='grow h-px rotate-180' />
+        <div className='shrink-0 text-text-tertiary system-xs-medium-uppercase'>{t('appDebug.feature.suggestedQuestionsAfterAnswer.tryToAsk')}</div>
+        {!isMobile && <Divider bgStyle='gradient' className='grow h-px' />}
       </div>
-      <div className='flex flex-wrap justify-center'>
+      <div className={cn('flex flex-wrap justify-center', isMobile && 'justify-end')}>
         {
           suggestedQuestions.map((suggestQuestion, index) => (
             <Button
+              size='small'
               key={index}
               variant='secondary-accent'
-              className='mb-2 mr-2 last:mr-0'
+              className='mb-1 mr-1 last:mr-0'
               onClick={() => onSend(suggestQuestion)}
             >
               {suggestQuestion}

+ 89 - 24
web/app/components/base/chat/embedded-chatbot/chat-wrapper.tsx

@@ -1,4 +1,4 @@
-import { useCallback, useEffect, useMemo } from 'react'
+import { useCallback, useEffect, useMemo, useState } from 'react'
 import Chat from '../chat'
 import type {
   ChatConfig,
@@ -9,16 +9,19 @@ import type {
 import { useChat } from '../chat/hooks'
 import { getLastAnswer, isValidGeneratedAnswer } from '../utils'
 import { useEmbeddedChatbotContext } from './context'
-import ConfigPanel from './config-panel'
 import { isDify } from './utils'
-import cn from '@/utils/classnames'
+import { InputVarType } from '@/app/components/workflow/types'
+import { TransferMethod } from '@/types/app'
+import InputsForm from '@/app/components/base/chat/embedded-chatbot/inputs-form'
 import {
   fetchSuggestedQuestions,
   getUrl,
   stopChatMessageResponding,
 } from '@/service/share'
+import AppIcon from '@/app/components/base/app-icon'
 import LogoAvatar from '@/app/components/base/logo/logo-embedded-chat-avatar'
 import AnswerIcon from '@/app/components/base/answer-icon'
+import cn from '@/utils/classnames'
 
 const ChatWrapper = () => {
   const {
@@ -29,6 +32,7 @@ const ChatWrapper = () => {
     currentConversationItem,
     inputsForms,
     newConversationInputs,
+    newConversationInputsRef,
     handleNewConversationCompleted,
     isMobile,
     isInstalledApp,
@@ -67,6 +71,38 @@ const ChatWrapper = () => {
     appPrevChatList,
     taskId => stopChatMessageResponding('', taskId, isInstalledApp, appId),
   )
+  const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputsRef?.current
+  const inputDisabled = useMemo(() => {
+    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 (!inputsFormValue?.[variable])
+          hasEmptyInput = label as string
+
+        if ((type === InputVarType.singleFile || type === InputVarType.multiFiles) && inputsFormValue?.[variable]) {
+          const files = inputsFormValue[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
+        }
+      })
+    }
+    if (hasEmptyInput)
+      return true
+
+    if (fileIsUploading)
+      return true
+    return false
+  }, [inputsFormValue, inputsForms])
 
   useEffect(() => {
     if (currentChatInstanceRef.current)
@@ -108,26 +144,48 @@ const ChatWrapper = () => {
     doSend(question.content, question.message_files, true, isValidGeneratedAnswer(parentAnswer) ? parentAnswer : null)
   }, [chatList, doSend])
 
+  const messageList = useMemo(() => {
+    if (currentConversationId)
+      return chatList
+    return chatList.filter(item => !item.isOpeningStatement)
+  }, [chatList, currentConversationId])
+
+  const [collapsed, setCollapsed] = useState(!!currentConversationId)
+
   const chatNode = useMemo(() => {
-    if (inputsForms.length) {
-      return (
-        <>
-          {!currentConversationId && (
-            <div className={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}>
-              <div className='mb-6' />
-              <ConfigPanel />
-              <div
-                className='my-6 h-[1px]'
-                style={{ background: 'linear-gradient(90deg, rgba(242, 244, 247, 0.00) 0%, #F2F4F7 49.17%, rgba(242, 244, 247, 0.00) 100%)' }}
-              />
-            </div>
-          )}
-        </>
-      )
+    if (!inputsForms.length)
+      return null
+    if (isMobile) {
+      if (!currentConversationId)
+        return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
+      return <div className='mb-4'></div>
+    }
+    else {
+      return <InputsForm collapsed={collapsed} setCollapsed={setCollapsed} />
     }
+  }, [inputsForms.length, isMobile, currentConversationId, collapsed])
 
-    return null
-  }, [currentConversationId, inputsForms, isMobile])
+  const welcome = useMemo(() => {
+    const welcomeMessage = chatList.find(item => item.isOpeningStatement)
+    if (currentConversationId)
+      return null
+    if (!welcomeMessage)
+      return null
+    if (!collapsed && inputsForms.length > 0)
+      return null
+    return (
+      <div className={cn('h-[50vh] py-12 flex flex-col items-center justify-center gap-3')}>
+        <AppIcon
+          size='xl'
+          iconType={appData?.site.icon_type}
+          icon={appData?.site.icon}
+          background={appData?.site.icon_background}
+          imageUrl={appData?.site.icon_url}
+        />
+        <div className='text-text-tertiary body-2xl-regular'>{welcomeMessage.content}</div>
+      </div>
+    )
+  }, [appData?.site.icon, appData?.site.icon_background, appData?.site.icon_type, appData?.site.icon_url, chatList, collapsed, currentConversationId, inputsForms.length])
 
   const answerIcon = isDify()
     ? <LogoAvatar className='relative shrink-0' />
@@ -144,17 +202,22 @@ const ChatWrapper = () => {
     <Chat
       appData={appData}
       config={appConfig}
-      chatList={chatList}
+      chatList={messageList}
       isResponding={isResponding}
       chatContainerInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
-      chatFooterClassName='pb-4'
-      chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-4')}
+      chatFooterClassName={cn('pb-4', !isMobile && 'rounded-b-2xl')}
+      chatFooterInnerClassName={cn('mx-auto w-full max-w-full tablet:px-4', isMobile && 'px-2')}
       onSend={doSend}
       inputs={currentConversationId ? currentConversationItem?.inputs as any : newConversationInputs}
       inputsForm={inputsForms}
       onRegenerate={doRegenerate}
       onStopResponding={handleStop}
-      chatNode={chatNode}
+      chatNode={
+        <>
+          {chatNode}
+          {welcome}
+        </>
+      }
       allToolIcons={appMeta?.tool_icons || {}}
       onFeedback={handleFeedback}
       suggestedQuestions={suggestedQuestions}
@@ -162,6 +225,8 @@ const ChatWrapper = () => {
       hideProcessDetail
       themeBuilder={themeBuilder}
       switchSibling={siblingMessageId => setTargetMessageId(siblingMessageId)}
+      inputDisabled={inputDisabled}
+      isMobile={isMobile}
     />
   )
 }

+ 0 - 47
web/app/components/base/chat/embedded-chatbot/config-panel/form-input.tsx

@@ -1,47 +0,0 @@
-import type { FC } from 'react'
-import { useTranslation } from 'react-i18next'
-import { memo } from 'react'
-import Textarea from '@/app/components/base/textarea'
-
-interface InputProps {
-  form: any
-  value: string
-  onChange: (variable: string, value: string) => void
-}
-const FormInput: FC<InputProps> = ({
-  form,
-  value,
-  onChange,
-}) => {
-  const { t } = useTranslation()
-  const {
-    type,
-    label,
-    required,
-    max_length,
-    variable,
-  } = form
-
-  if (type === 'paragraph') {
-    return (
-      <Textarea
-        value={value}
-        className='resize-none'
-        onChange={e => onChange(variable, e.target.value)}
-        placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-      />
-    )
-  }
-
-  return (
-    <input
-      className='grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none'
-      value={value || ''}
-      maxLength={max_length}
-      onChange={e => onChange(variable, e.target.value)}
-      placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-    />
-  )
-}
-
-export default memo(FormInput)

+ 0 - 129
web/app/components/base/chat/embedded-chatbot/config-panel/form.tsx

@@ -1,129 +0,0 @@
-import { useCallback } from 'react'
-import { useTranslation } from 'react-i18next'
-import { useEmbeddedChatbotContext } 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 {
-    appParams,
-    inputsForms,
-    newConversationInputs,
-    newConversationInputsRef,
-    handleNewConversationInputsChange,
-    isMobile,
-  } = useEmbeddedChatbotContext()
-
-  const handleFormChange = useCallback((variable: string, value: any) => {
-    handleNewConversationInputsChange({
-      ...newConversationInputsRef.current,
-      [variable]: value,
-    })
-  }, [newConversationInputsRef, handleNewConversationInputsChange])
-
-  const renderField = (form: any) => {
-    const {
-      label,
-      required,
-      variable,
-      options,
-    } = form
-
-    if (form.type === 'text-input' || form.type === 'paragraph') {
-      return (
-        <Input
-          form={form}
-          value={newConversationInputs[variable]}
-          onChange={handleFormChange}
-        />
-      )
-    }
-    if (form.type === 'number') {
-      return (
-        <input
-          className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
-          type="number"
-          value={newConversationInputs[variable] || ''}
-          onChange={e => handleFormChange(variable, e.target.value)}
-          placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-        />
-      )
-    }
-
-    if (form.type === 'number') {
-      return (
-        <input
-          className="grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none"
-          type="number"
-          value={newConversationInputs[variable] || ''}
-          onChange={e => handleFormChange(variable, e.target.value)}
-          placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-        />
-      )
-    }
-    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,
-            fileUploadConfig: (appParams as any).system_parameters,
-          }}
-        />
-      )
-    }
-    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,
-            fileUploadConfig: (appParams as any).system_parameters,
-          }}
-        />
-      )
-    }
-
-    return (
-      <PortalSelect
-        popupClassName='w-[200px]'
-        value={newConversationInputs[variable]}
-        items={options.map((option: string) => ({ value: option, name: option }))}
-        onSelect={item => handleFormChange(variable, item.value as string)}
-        placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
-      />
-    )
-  }
-
-  if (!inputsForms.length)
-    return null
-
-  return (
-    <div className='mb-4 py-2'>
-      {
-        inputsForms.map(form => (
-          <div
-            key={form.variable}
-            className={`flex mb-3 last-of-type:mb-0 text-sm text-gray-900 ${isMobile && '!flex-wrap'}`}
-          >
-            <div className={`shrink-0 mr-2 py-2 w-[128px] ${isMobile && '!w-full'}`}>{form.label}</div>
-            {renderField(form)}
-          </div>
-        ))
-      }
-    </div>
-  )
-}
-
-export default Form

+ 0 - 180
web/app/components/base/chat/embedded-chatbot/config-panel/index.tsx

@@ -1,180 +0,0 @@
-import { useState } from 'react'
-import { useTranslation } from 'react-i18next'
-import { useEmbeddedChatbotContext } from '../context'
-import { useThemeContext } from '../theme/theme-context'
-import { CssTransform } from '../theme/utils'
-import Form from './form'
-import cn from '@/utils/classnames'
-import Button from '@/app/components/base/button'
-import AppIcon from '@/app/components/base/app-icon'
-import { MessageDotsCircle } from '@/app/components/base/icons/src/vender/solid/communication'
-import { Edit02 } from '@/app/components/base/icons/src/vender/line/general'
-import { Star06 } from '@/app/components/base/icons/src/vender/solid/shapes'
-import LogoSite from '@/app/components/base/logo/logo-site'
-
-const ConfigPanel = () => {
-  const { t } = useTranslation()
-  const {
-    appData,
-    inputsForms,
-    handleStartChat,
-    showConfigPanelBeforeChat,
-    isMobile,
-  } = useEmbeddedChatbotContext()
-  const [collapsed, setCollapsed] = useState(true)
-  const customConfig = appData?.custom_config
-  const site = appData?.site
-  const themeBuilder = useThemeContext()
-
-  return (
-    <div className='flex flex-col max-h-[80%] w-full max-w-[720px]'>
-      <div
-        className={cn(
-          'grow rounded-xl overflow-y-auto',
-          showConfigPanelBeforeChat && 'border-[0.5px] border-gray-100 shadow-lg',
-          !showConfigPanelBeforeChat && collapsed && 'border border-indigo-100',
-          !showConfigPanelBeforeChat && !collapsed && 'border-[0.5px] border-gray-100 shadow-lg',
-        )}
-      >
-        <div
-          style={CssTransform(themeBuilder.theme?.roundedBackgroundColorStyle ?? '')}
-          className={`
-            flex flex-wrap px-6 py-4 rounded-t-xl bg-indigo-25
-            ${isMobile && '!px-4 !py-3'}
-          `}
-        >
-          {
-            showConfigPanelBeforeChat && (
-              <>
-                <div className='flex items-center h-8 text-2xl font-semibold text-gray-800'>
-                  <AppIcon
-                    iconType={appData?.site.icon_type}
-                    icon={appData?.site.icon}
-                    imageUrl={appData?.site.icon_url}
-                    background='transparent'
-                    size='small'
-                    className="mr-2"
-                  />
-                  {appData?.site.title}
-                </div>
-                {
-                  appData?.site.description && (
-                    <div className='mt-2 w-full text-sm text-gray-500'>
-                      {appData?.site.description}
-                    </div>
-                  )
-                }
-              </>
-            )
-          }
-          {
-            !showConfigPanelBeforeChat && collapsed && (
-              <>
-                <Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
-                <div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
-                  {t('share.chat.configStatusDes')}
-                </div>
-                <Button
-                  styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
-                  variant='secondary-accent'
-                  size='small'
-                  className='shrink-0 text-white'
-                  onClick={() => setCollapsed(false)}
-                >
-                  <Edit02 className='mr-1 w-3 h-3' />
-                  {t('common.operation.edit')}
-                </Button>
-              </>
-            )
-          }
-          {
-            !showConfigPanelBeforeChat && !collapsed && (
-              <>
-                <Star06 className='mr-1 mt-1 w-4 h-4 text-indigo-600' />
-                <div className='grow py-[3px] text-[13px] text-indigo-600 leading-[18px] font-medium'>
-                  {t('share.chat.privatePromptConfigTitle')}
-                </div>
-              </>
-            )
-          }
-        </div>
-        {
-          !collapsed && !showConfigPanelBeforeChat && (
-            <div className='p-6 rounded-b-xl'>
-              <Form />
-              <div className={cn('pl-[136px] flex items-center', isMobile && '!pl-0')}>
-                <Button
-                  styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
-                  variant='primary'
-                  className='mr-2'
-                  onClick={() => {
-                    setCollapsed(true)
-                    handleStartChat()
-                  }}
-                >
-                  {t('common.operation.save')}
-                </Button>
-                <Button
-                  onClick={() => setCollapsed(true)}
-                >
-                  {t('common.operation.cancel')}
-                </Button>
-              </div>
-            </div>
-          )
-        }
-        {
-          showConfigPanelBeforeChat && (
-            <div className='p-6 rounded-b-xl'>
-              <Form />
-              <Button
-                styleCss={CssTransform(themeBuilder.theme?.backgroundButtonDefaultColorStyle ?? '')}
-                className={cn(inputsForms.length && !isMobile && 'ml-[136px]')}
-                variant='primary'
-                size='large'
-                onClick={handleStartChat}
-              >
-                <MessageDotsCircle className='mr-2 w-4 h-4 text-white' />
-                {t('share.chat.startChat')}
-              </Button>
-            </div>
-          )
-        }
-      </div>
-      {
-        showConfigPanelBeforeChat && (site || customConfig) && (
-          <div className='mt-4 flex flex-wrap justify-between items-center py-2 text-xs text-gray-400'>
-            {site?.privacy_policy
-              ? <div className={cn(isMobile && 'mb-2 w-full text-center')}>{t('share.chat.privacyPolicyLeft')}
-                <a
-                  className='text-gray-500 px-1'
-                  href={site?.privacy_policy}
-                  target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
-                {t('share.chat.privacyPolicyRight')}
-              </div>
-              : <div>
-              </div>}
-            {
-              customConfig?.remove_webapp_brand
-                ? null
-                : (
-                  <div className={cn('flex items-center justify-end', isMobile && 'w-full')}>
-                    <div className='flex items-center pr-3 space-x-3'>
-                      <span className='uppercase'>{t('share.chat.poweredBy')}</span>
-                      {
-                        customConfig?.replace_webapp_logo
-                          ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
-                          : <LogoSite className='!h-5' />
-                      }
-                    </div>
-                  </div>
-                )
-            }
-          </div>
-        )
-      }
-    </div>
-  )
-}
-
-export default ConfigPanel

+ 2 - 4
web/app/components/base/chat/embedded-chatbot/context.tsx

@@ -15,7 +15,7 @@ import type {
   ConversationItem,
 } from '@/models/share'
 
-export interface EmbeddedChatbotContextValue {
+export type EmbeddedChatbotContextValue = {
   appInfoError?: any
   appInfoLoading?: boolean
   appMeta?: AppMeta
@@ -27,13 +27,12 @@ export interface EmbeddedChatbotContextValue {
   appPrevChatList: ChatItem[]
   pinnedConversationList: AppConversationData['data']
   conversationList: AppConversationData['data']
-  showConfigPanelBeforeChat: boolean
   newConversationInputs: Record<string, any>
   newConversationInputsRef: RefObject<Record<string, any>>
   handleNewConversationInputsChange: (v: Record<string, any>) => void
   inputsForms: any[]
   handleNewConversation: () => void
-  handleStartChat: () => void
+  handleStartChat: (callback?: any) => void
   handleChangeConversation: (conversationId: string) => void
   handleNewConversationCompleted: (newConversationId: string) => void
   chatShouldReloadKey: string
@@ -50,7 +49,6 @@ export const EmbeddedChatbotContext = createContext<EmbeddedChatbotContextValue>
   appPrevChatList: [],
   pinnedConversationList: [],
   conversationList: [],
-  showConfigPanelBeforeChat: false,
   newConversationInputs: {},
   newConversationInputsRef: { current: {} },
   handleNewConversationInputsChange: () => {},

+ 0 - 56
web/app/components/base/chat/embedded-chatbot/header.tsx

@@ -1,56 +0,0 @@
-import type { FC } from 'react'
-import React from 'react'
-import { RiRefreshLine } from '@remixicon/react'
-import { useTranslation } from 'react-i18next'
-import type { Theme } from './theme/theme-context'
-import { CssTransform } from './theme/utils'
-import Tooltip from '@/app/components/base/tooltip'
-
-export type IHeaderProps = {
-  isMobile?: boolean
-  customerIcon?: React.ReactNode
-  title: string
-  theme?: Theme
-  onCreateNewChat?: () => void
-}
-const Header: FC<IHeaderProps> = ({
-  isMobile,
-  customerIcon,
-  title,
-  theme,
-  onCreateNewChat,
-}) => {
-  const { t } = useTranslation()
-  if (!isMobile)
-    return null
-
-  return (
-    <div
-      className={`
-        shrink-0 flex items-center justify-between h-14 px-4 
-      `}
-      style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
-    >
-      <div className="flex items-center space-x-2">
-        {customerIcon}
-        <div
-          className={'text-sm font-bold text-white'}
-          style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
-        >
-          {title}
-        </div>
-      </div>
-      <Tooltip
-        popupContent={t('share.chat.resetChat')}
-      >
-        <div className='flex cursor-pointer hover:rounded-lg hover:bg-black/5 w-8 h-8 items-center justify-center' onClick={() => {
-          onCreateNewChat?.()
-        }}>
-          <RiRefreshLine className="h-4 w-4 text-sm font-bold text-white" color={theme?.colorPathOnHeader}/>
-        </div>
-      </Tooltip>
-    </div>
-  )
-}
-
-export default React.memo(Header)

+ 109 - 0
web/app/components/base/chat/embedded-chatbot/header/index.tsx

@@ -0,0 +1,109 @@
+import type { FC } from 'react'
+import React from 'react'
+import { RiResetLeftLine } from '@remixicon/react'
+import { useTranslation } from 'react-i18next'
+import type { Theme } from '../theme/theme-context'
+import { CssTransform } from '../theme/utils'
+import {
+  useEmbeddedChatbotContext,
+} from '../context'
+import Tooltip from '@/app/components/base/tooltip'
+import ActionButton from '@/app/components/base/action-button'
+import Divider from '@/app/components/base/divider'
+import ViewFormDropdown from '@/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown'
+import LogoSite from '@/app/components/base/logo/logo-site'
+import cn from '@/utils/classnames'
+
+export type IHeaderProps = {
+  isMobile?: boolean
+  customerIcon?: React.ReactNode
+  title: string
+  theme?: Theme
+  onCreateNewChat?: () => void
+}
+const Header: FC<IHeaderProps> = ({
+  isMobile,
+  customerIcon,
+  title,
+  theme,
+  onCreateNewChat,
+}) => {
+  const { t } = useTranslation()
+  const {
+    appData,
+    currentConversationId,
+    inputsForms,
+  } = useEmbeddedChatbotContext()
+  if (!isMobile) {
+    return (
+      <div className='shrink-0 h-14 p-3 flex items-center justify-end'>
+        <div className='flex items-center gap-1'>
+          {/* powered by */}
+          <div className='shrink-0'>
+            {!appData?.custom_config?.remove_webapp_brand && (
+              <div className={cn(
+                'shrink-0 px-2 flex items-center gap-1.5',
+              )}>
+                <div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
+                {appData?.custom_config?.replace_webapp_logo && (
+                  <img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
+                )}
+                {!appData?.custom_config?.replace_webapp_logo && (
+                  <LogoSite className='!h-5' />
+                )}
+              </div>
+            )}
+          </div>
+          {currentConversationId && (
+            <Divider type='vertical' className='h-3.5' />
+          )}
+          {currentConversationId && (
+            <Tooltip
+              popupContent={t('share.chat.resetChat')}
+            >
+              <ActionButton size='l' onClick={onCreateNewChat}>
+                <RiResetLeftLine className='w-[18px] h-[18px]' />
+              </ActionButton>
+            </Tooltip>
+          )}
+          {currentConversationId && inputsForms.length > 0 && (
+            <ViewFormDropdown />
+          )}
+        </div>
+      </div>
+    )
+  }
+
+  return (
+    <div
+      className={cn('shrink-0 flex items-center justify-between h-14 px-3 rounded-t-2xl')}
+      style={Object.assign({}, CssTransform(theme?.backgroundHeaderColorStyle ?? ''), CssTransform(theme?.headerBorderBottomStyle ?? '')) }
+    >
+      <div className="grow flex items-center space-x-3">
+        {customerIcon}
+        <div
+          className='system-md-semibold truncate'
+          style={CssTransform(theme?.colorFontOnHeaderStyle ?? '')}
+        >
+          {title}
+        </div>
+      </div>
+      <div className='flex items-center gap-1'>
+        {currentConversationId && (
+          <Tooltip
+            popupContent={t('share.chat.resetChat')}
+          >
+            <ActionButton size='l' onClick={onCreateNewChat}>
+              <RiResetLeftLine className={cn('w-[18px] h-[18px]', theme?.colorPathOnHeader)} />
+            </ActionButton>
+          </Tooltip>
+        )}
+        {currentConversationId && inputsForms.length > 0 && (
+          <ViewFormDropdown iconColor={theme?.colorPathOnHeader} />
+        )}
+      </div>
+    </div>
+  )
+}
+
+export default React.memo(Header)

+ 5 - 14
web/app/components/base/chat/embedded-chatbot/hooks.tsx

@@ -88,7 +88,6 @@ export const useEmbeddedChatbot = () => {
       })
     }
   }, [appId, conversationIdInfo, setConversationIdInfo])
-  const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true)
 
   const [newConversationId, setNewConversationId] = useState('')
   const chatShouldReloadKey = useMemo(() => {
@@ -273,23 +272,18 @@ export const useEmbeddedChatbot = () => {
 
     return true
   }, [inputsForms, notify, t])
-  const handleStartChat = useCallback(() => {
+  const handleStartChat = useCallback((callback?: any) => {
     if (checkInputsRequired()) {
-      setShowConfigPanelBeforeChat(false)
       setShowNewConversationItemInList(true)
+      callback?.()
     }
-  }, [setShowConfigPanelBeforeChat, setShowNewConversationItemInList, checkInputsRequired])
+  }, [setShowNewConversationItemInList, checkInputsRequired])
   const currentChatInstanceRef = useRef<{ handleStop: () => void }>({ handleStop: () => { } })
   const handleChangeConversation = useCallback((conversationId: string) => {
     currentChatInstanceRef.current.handleStop()
     setNewConversationId('')
     handleConversationIdInfoChange(conversationId)
-
-    if (conversationId === '' && !checkInputsRequired(true))
-      setShowConfigPanelBeforeChat(true)
-    else
-      setShowConfigPanelBeforeChat(false)
-  }, [handleConversationIdInfoChange, setShowConfigPanelBeforeChat, checkInputsRequired])
+  }, [handleConversationIdInfoChange])
   const handleNewConversation = useCallback(() => {
     currentChatInstanceRef.current.handleStop()
     setNewConversationId('')
@@ -299,11 +293,10 @@ export const useEmbeddedChatbot = () => {
     }
     else if (currentConversationId) {
       handleConversationIdInfoChange('')
-      setShowConfigPanelBeforeChat(true)
       setShowNewConversationItemInList(true)
       handleNewConversationInputsChange({})
     }
-  }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
+  }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
 
   const handleNewConversationCompleted = useCallback((newConversationId: string) => {
     setNewConversationId(newConversationId)
@@ -336,8 +329,6 @@ export const useEmbeddedChatbot = () => {
     appPrevChatList,
     pinnedConversationList,
     conversationList,
-    showConfigPanelBeforeChat,
-    setShowConfigPanelBeforeChat,
     setShowNewConversationItemInList,
     newConversationInputs,
     newConversationInputsRef,

+ 61 - 42
web/app/components/base/chat/embedded-chatbot/index.tsx

@@ -4,7 +4,6 @@ import {
 } from 'react'
 import { useAsyncEffect } from 'ahooks'
 import { useTranslation } from 'react-i18next'
-import { RiLoopLeftLine } from '@remixicon/react'
 import {
   EmbeddedChatbotContext,
   useEmbeddedChatbotContext,
@@ -12,32 +11,30 @@ import {
 import { useEmbeddedChatbot } from './hooks'
 import { isDify } from './utils'
 import { useThemeContext } from './theme/theme-context'
-import cn from '@/utils/classnames'
+import { CssTransform } from './theme/utils'
 import { checkOrSetAccessToken } from '@/app/components/share/utils'
 import AppUnavailable from '@/app/components/base/app-unavailable'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
 import Loading from '@/app/components/base/loading'
 import LogoHeader from '@/app/components/base/logo/logo-embedded-chat-header'
 import Header from '@/app/components/base/chat/embedded-chatbot/header'
-import ConfigPanel from '@/app/components/base/chat/embedded-chatbot/config-panel'
 import ChatWrapper from '@/app/components/base/chat/embedded-chatbot/chat-wrapper'
-import Tooltip from '@/app/components/base/tooltip'
+import LogoSite from '@/app/components/base/logo/logo-site'
+import cn from '@/utils/classnames'
 
 const Chatbot = () => {
-  const { t } = useTranslation()
   const {
     isMobile,
     appInfoError,
     appInfoLoading,
     appData,
-    appPrevChatList,
-    showConfigPanelBeforeChat,
     appChatListDataLoading,
+    chatShouldReloadKey,
     handleNewConversation,
     themeBuilder,
   } = useEmbeddedChatbotContext()
+  const { t } = useTranslation()
 
-  const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
   const customConfig = appData?.custom_config
   const site = appData?.site
 
@@ -55,52 +52,76 @@ const Chatbot = () => {
 
   if (appInfoLoading) {
     return (
-      <Loading type='app' />
+      <>
+        {!isMobile && <Loading type='app' />}
+        {isMobile && (
+          <div className={cn('relative')}>
+            <div className={cn('flex flex-col h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border rounded-2xl shadow-xs')}>
+              <Loading type='app' />
+            </div>
+          </div>
+        )}
+      </>
     )
   }
 
   if (appInfoError) {
     return (
-      <AppUnavailable />
+      <>
+        {!isMobile && <AppUnavailable />}
+        {isMobile && (
+          <div className={cn('relative')}>
+            <div className={cn('flex flex-col h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border rounded-2xl shadow-xs')}>
+              <AppUnavailable />
+            </div>
+          </div>
+        )}
+      </>
     )
   }
   return (
-    <div>
-      <Header
-        isMobile={isMobile}
-        title={site?.title || ''}
-        customerIcon={isDify() ? difyIcon : ''}
-        theme={themeBuilder?.theme}
-        onCreateNewChat={handleNewConversation}
-      />
-      <div className='flex bg-white overflow-hidden'>
-        <div className={cn('h-[100vh] grow flex flex-col overflow-y-auto', isMobile && '!h-[calc(100vh_-_3rem)]')}>
-          {showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && (
-            <div className={cn('flex w-full items-center justify-center h-full tablet:px-4', isMobile && 'px-4')}>
-              <ConfigPanel />
-            </div>
-          )}
-          {appChatListDataLoading && chatReady && (
+    <div className='relative'>
+      <div
+        className={cn(
+          'flex flex-col border border-components-panel-border-subtle rounded-2xl',
+          isMobile ? 'h-[calc(100vh_-_60px)] border-[0.5px] border-components-panel-border shadow-xs' : 'h-[100vh] bg-chatbot-bg',
+        )}
+        style={isMobile ? Object.assign({}, CssTransform(themeBuilder?.theme?.backgroundHeaderColorStyle ?? '')) : {}}
+      >
+        <Header
+          isMobile={isMobile}
+          title={site?.title || ''}
+          customerIcon={isDify() ? difyIcon : ''}
+          theme={themeBuilder?.theme}
+          onCreateNewChat={handleNewConversation}
+        />
+        <div className={cn('grow flex flex-col overflow-y-auto', isMobile && '!h-[calc(100vh_-_3rem)] bg-chatbot-bg rounded-2xl')}>
+          {appChatListDataLoading && (
             <Loading type='app' />
           )}
-          {chatReady && !appChatListDataLoading && (
-            <div className='relative h-full pt-8 mx-auto w-full max-w-[720px]'>
-              {!isMobile && (
-                <div className='absolute top-2.5 right-3 z-20'>
-                  <Tooltip
-                    popupContent={t('share.chat.resetChat')}
-                  >
-                    <div className='p-1.5 bg-white border-[0.5px] border-gray-100 rounded-lg shadow-md cursor-pointer' onClick={handleNewConversation}>
-                      <RiLoopLeftLine className="h-4 w-4 text-gray-500"/>
-                    </div>
-                  </Tooltip>
-                </div>
+          {!appChatListDataLoading && (
+            <ChatWrapper key={chatShouldReloadKey} />
+          )}
+        </div>
+      </div>
+      {/* powered by */}
+      {isMobile && (
+        <div className='shrink-0 h-[60px] pl-2 flex items-center'>
+          {!appData?.custom_config?.remove_webapp_brand && (
+            <div className={cn(
+              'shrink-0 px-2 flex items-center gap-1.5',
+            )}>
+              <div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
+              {appData?.custom_config?.replace_webapp_logo && (
+                <img src={appData?.custom_config?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
+              )}
+              {!appData?.custom_config?.replace_webapp_logo && (
+                <LogoSite className='!h-5' />
               )}
-              <ChatWrapper />
             </div>
           )}
         </div>
-      </div>
+      )}
     </div>
   )
 }
@@ -122,7 +143,6 @@ const EmbeddedChatbotWrapper = () => {
     appPrevChatList,
     pinnedConversationList,
     conversationList,
-    showConfigPanelBeforeChat,
     newConversationInputs,
     newConversationInputsRef,
     handleNewConversationInputsChange,
@@ -150,7 +170,6 @@ const EmbeddedChatbotWrapper = () => {
     appPrevChatList,
     pinnedConversationList,
     conversationList,
-    showConfigPanelBeforeChat,
     newConversationInputs,
     newConversationInputsRef,
     handleNewConversationInputsChange,

+ 118 - 0
web/app/components/base/chat/embedded-chatbot/inputs-form/content.tsx

@@ -0,0 +1,118 @@
+import React, { useCallback } from 'react'
+import { useTranslation } from 'react-i18next'
+import { useEmbeddedChatbotContext } from '../context'
+import Input from '@/app/components/base/input'
+import Textarea from '@/app/components/base/textarea'
+import { PortalSelect } from '@/app/components/base/select'
+import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
+import { InputVarType } from '@/app/components/workflow/types'
+
+type Props = {
+  showTip?: boolean
+}
+
+const InputsFormContent = ({ showTip }: Props) => {
+  const { t } = useTranslation()
+  const {
+    appParams,
+    inputsForms,
+    currentConversationId,
+    currentConversationItem,
+    newConversationInputs,
+    newConversationInputsRef,
+    handleNewConversationInputsChange,
+  } = useEmbeddedChatbotContext()
+  const inputsFormValue = currentConversationId ? currentConversationItem?.inputs : newConversationInputs
+  const readonly = !!currentConversationId
+
+  const handleFormChange = useCallback((variable: string, value: any) => {
+    handleNewConversationInputsChange({
+      ...newConversationInputsRef.current,
+      [variable]: value,
+    })
+  }, [newConversationInputsRef, handleNewConversationInputsChange])
+
+  return (
+    <div className='space-y-4'>
+      {inputsForms.map(form => (
+        <div key={form.variable} className='space-y-1'>
+          <div className='h-6 flex items-center gap-1'>
+            <div className='text-text-secondary system-md-semibold'>{form.label}</div>
+            {!form.required && (
+              <div className='text-text-tertiary system-xs-regular'>{t('appDebug.variableTable.optional')}</div>
+            )}
+          </div>
+          {form.type === InputVarType.textInput && (
+            <Input
+              value={inputsFormValue?.[form.variable] || ''}
+              onChange={e => handleFormChange(form.variable, e.target.value)}
+              placeholder={form.label}
+              readOnly={readonly}
+              disabled={readonly}
+            />
+          )}
+          {form.type === InputVarType.number && (
+            <Input
+              type='number'
+              value={inputsFormValue?.[form.variable] || ''}
+              onChange={e => handleFormChange(form.variable, e.target.value)}
+              placeholder={form.label}
+              readOnly={readonly}
+              disabled={readonly}
+            />
+          )}
+          {form.type === InputVarType.paragraph && (
+            <Textarea
+              value={inputsFormValue?.[form.variable] || ''}
+              onChange={e => handleFormChange(form.variable, e.target.value)}
+              placeholder={form.label}
+              readOnly={readonly}
+              disabled={readonly}
+            />
+          )}
+          {form.type === InputVarType.select && (
+            <PortalSelect
+              popupClassName='w-[200px]'
+              value={inputsFormValue?.[form.variable]}
+              items={form.options.map((option: string) => ({ value: option, name: option }))}
+              onSelect={item => handleFormChange(form.variable, item.value as string)}
+              placeholder={form.label}
+              readonly={readonly}
+            />
+          )}
+          {form.type === InputVarType.singleFile && (
+            <FileUploaderInAttachmentWrapper
+              value={inputsFormValue?.[form.variable] ? [inputsFormValue?.[form.variable]] : []}
+              onChange={files => handleFormChange(form.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,
+                fileUploadConfig: (appParams as any).system_parameters,
+              }}
+            />
+          )}
+          {form.type === InputVarType.multiFiles && (
+            <FileUploaderInAttachmentWrapper
+              value={inputsFormValue?.[form.variable] || []}
+              onChange={files => handleFormChange(form.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,
+                fileUploadConfig: (appParams as any).system_parameters,
+              }}
+            />
+          )}
+        </div>
+      ))}
+      {showTip && (
+        <div className='text-text-tertiary system-xs-regular'>{t('share.chat.chatFormTip')}</div>
+      )}
+    </div>
+  )
+}
+
+export default InputsFormContent

+ 79 - 0
web/app/components/base/chat/embedded-chatbot/inputs-form/index.tsx

@@ -0,0 +1,79 @@
+import React from 'react'
+import { useTranslation } from 'react-i18next'
+import { Message3Fill } from '@/app/components/base/icons/src/public/other'
+import Button from '@/app/components/base/button'
+import Divider from '@/app/components/base/divider'
+import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
+import { useEmbeddedChatbotContext } from '../context'
+import cn from '@/utils/classnames'
+
+type Props = {
+  collapsed: boolean
+  setCollapsed: (collapsed: boolean) => void
+}
+
+const InputsFormNode = ({
+  collapsed,
+  setCollapsed,
+}: Props) => {
+  const { t } = useTranslation()
+  const {
+    isMobile,
+    currentConversationId,
+    themeBuilder,
+    handleStartChat,
+  } = useEmbeddedChatbotContext()
+
+  return (
+    <div className={cn('mb-6 pt-6 px-4 flex flex-col items-center', isMobile && 'mb-4 pt-4')}>
+      <div className={cn(
+        'w-full max-w-[672px] bg-components-panel-bg rounded-2xl border-[0.5px] border-components-panel-border shadow-md',
+        collapsed && 'bg-components-card-bg border border-components-card-border shadow-none',
+      )}>
+        <div className={cn(
+          'flex items-center gap-3 px-6 py-4 rounded-t-2xl',
+          !collapsed && 'border-b border-divider-subtle',
+          isMobile && 'px-4 py-3',
+        )}>
+          <Message3Fill className='shrink-0 w-6 h-6' />
+          <div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
+          {collapsed && (
+            <Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(false)}>{currentConversationId ? t('common.operation.view') : t('common.operation.edit')}</Button>
+          )}
+          {!collapsed && currentConversationId && (
+            <Button className='text-text-tertiary uppercase' size='small' variant='ghost' onClick={() => setCollapsed(true)}>{t('common.operation.close')}</Button>
+          )}
+        </div>
+        {!collapsed && (
+          <div className={cn('p-6', isMobile && 'p-4')}>
+            <InputsFormContent showTip={!!currentConversationId} />
+          </div>
+        )}
+        {!collapsed && !currentConversationId && (
+          <div className={cn('p-6', isMobile && 'p-4')}>
+            <Button
+              variant='primary'
+              className='w-full'
+              onClick={() => handleStartChat(() => setCollapsed(true))}
+              style={
+                themeBuilder?.theme
+                  ? {
+                    backgroundColor: themeBuilder?.theme.primaryColor,
+                  }
+                  : {}
+              }
+            >{t('share.chat.startChat')}</Button>
+          </div>
+        )}
+      </div>
+      {collapsed && (
+        <div className='py-4 flex items-center w-full max-w-[720px]'>
+          <Divider bgStyle='gradient' className='basis-1/2 h-px rotate-180' />
+          <Divider bgStyle='gradient' className='basis-1/2 h-px' />
+        </div>
+      )}
+    </div>
+  )
+}
+
+export default InputsFormNode

+ 52 - 0
web/app/components/base/chat/embedded-chatbot/inputs-form/view-form-dropdown.tsx

@@ -0,0 +1,52 @@
+import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import {
+  RiChatSettingsLine,
+} from '@remixicon/react'
+import { PortalToFollowElem, PortalToFollowElemContent, PortalToFollowElemTrigger } from '@/app/components/base/portal-to-follow-elem'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
+import { Message3Fill } from '@/app/components/base/icons/src/public/other'
+import InputsFormContent from '@/app/components/base/chat/embedded-chatbot/inputs-form/content'
+import cn from '@/utils/classnames'
+
+type Props = {
+  iconColor?: string
+}
+const ViewFormDropdown = ({ iconColor }: Props) => {
+  const { t } = useTranslation()
+  const [open, setOpen] = useState(false)
+
+  return (
+    <PortalToFollowElem
+      open={open}
+      onOpenChange={setOpen}
+      placement='bottom-end'
+      offset={{
+        mainAxis: 4,
+        crossAxis: 4,
+      }}
+    >
+      <PortalToFollowElemTrigger
+        onClick={() => setOpen(v => !v)}
+      >
+        <ActionButton size='l' state={open ? ActionButtonState.Hover : ActionButtonState.Default}>
+          <RiChatSettingsLine className={cn('w-[18px] h-[18px]', iconColor)} />
+        </ActionButton>
+      </PortalToFollowElemTrigger>
+      <PortalToFollowElemContent className="z-50">
+        <div className='w-[400px] bg-components-panel-bg backdrop-blur-sm rounded-2xl border-[0.5px] border-components-panel-border shadow-lg'>
+          <div className='flex items-center gap-3 px-6 py-4 rounded-t-2xl border-b border-divider-subtle'>
+            <Message3Fill className='shrink-0 w-6 h-6' />
+            <div className='grow text-text-secondary system-xl-semibold'>{t('share.chat.chatSettingsTitle')}</div>
+          </div>
+          <div className='p-6'>
+            <InputsFormContent showTip />
+          </div>
+        </div>
+      </PortalToFollowElemContent>
+    </PortalToFollowElem>
+
+  )
+}
+
+export default ViewFormDropdown

+ 1 - 1
web/app/components/base/chat/embedded-chatbot/theme/theme-context.ts

@@ -9,7 +9,7 @@ export class Theme {
   public backgroundHeaderColorStyle = 'backgroundImage: linear-gradient(to right, #2563eb, #0ea5e9)'
   public headerBorderBottomStyle = ''
   public colorFontOnHeaderStyle = 'color: white'
-  public colorPathOnHeader = 'white'
+  public colorPathOnHeader = 'text-text-primary-on-surface'
   public backgroundButtonDefaultColorStyle = 'backgroundColor: #1C64F2'
   public roundedBackgroundColorStyle = 'backgroundColor: rgb(245 248 255)'
   public chatBubbleColorStyle = 'backgroundColor: rgb(225 239 254)'

+ 0 - 135
web/app/components/base/features/new-feature-panel/annotation-reply/annotation-ctrl-btn/index.tsx

@@ -1,135 +0,0 @@
-'use client'
-import type { FC } from 'react'
-import React, { useRef, useState } from 'react'
-import { useHover } from 'ahooks'
-import { useTranslation } from 'react-i18next'
-import cn from '@/utils/classnames'
-import { MessageCheckRemove, MessageFastPlus } from '@/app/components/base/icons/src/vender/line/communication'
-import { MessageFast } from '@/app/components/base/icons/src/vender/solid/communication'
-import { Edit04 } from '@/app/components/base/icons/src/vender/line/general'
-import RemoveAnnotationConfirmModal from '@/app/components/app/annotation/remove-annotation-confirm-modal'
-import Tooltip from '@/app/components/base/tooltip'
-import { addAnnotation, delAnnotation } from '@/service/annotation'
-import Toast from '@/app/components/base/toast'
-import { useProviderContext } from '@/context/provider-context'
-import { useModalContext } from '@/context/modal-context'
-
-type Props = {
-  appId: string
-  messageId?: string
-  annotationId?: string
-  className?: string
-  cached: boolean
-  query: string
-  answer: string
-  onAdded: (annotationId: string, authorName: string) => void
-  onEdit: () => void
-  onRemoved: () => void
-}
-
-const CacheCtrlBtn: FC<Props> = ({
-  className,
-  cached,
-  query,
-  answer,
-  appId,
-  messageId,
-  annotationId,
-  onAdded,
-  onEdit,
-  onRemoved,
-}) => {
-  const { t } = useTranslation()
-  const { plan, enableBilling } = useProviderContext()
-  const isAnnotationFull = (enableBilling && plan.usage.annotatedResponse >= plan.total.annotatedResponse)
-  const { setShowAnnotationFullModal } = useModalContext()
-  const [showModal, setShowModal] = useState(false)
-  const cachedBtnRef = useRef<HTMLDivElement>(null)
-  const isCachedBtnHovering = useHover(cachedBtnRef)
-  const handleAdd = async () => {
-    if (isAnnotationFull) {
-      setShowAnnotationFullModal()
-      return
-    }
-    const res: any = await addAnnotation(appId, {
-      message_id: messageId,
-      question: query,
-      answer,
-    })
-    Toast.notify({
-      message: t('common.api.actionSuccess') as string,
-      type: 'success',
-    })
-    onAdded(res.id, res.account?.name)
-  }
-
-  const handleRemove = async () => {
-    await delAnnotation(appId, annotationId!)
-    Toast.notify({
-      message: t('common.api.actionSuccess') as string,
-      type: 'success',
-    })
-    onRemoved()
-    setShowModal(false)
-  }
-  return (
-    <div className={cn('inline-block', className)}>
-      <div className='inline-flex p-0.5 space-x-0.5 rounded-lg bg-white border border-gray-100 shadow-md text-gray-500 cursor-pointer'>
-        {cached
-          ? (
-            <div>
-              <div
-                ref={cachedBtnRef}
-                className={cn(isCachedBtnHovering ? 'bg-[#FEF3F2] text-[#D92D20]' : 'bg-[#EEF4FF] text-[#444CE7]', 'flex p-1 space-x-1 items-center rounded-md leading-4 text-xs font-medium')}
-                onClick={() => setShowModal(true)}
-              >
-                {!isCachedBtnHovering
-                  ? (
-                    <>
-                      <MessageFast className='w-4 h-4' />
-                      <div>{t('appDebug.feature.annotation.cached')}</div>
-                    </>
-                  )
-                  : <>
-                    <MessageCheckRemove className='w-4 h-4' />
-                    <div>{t('appDebug.feature.annotation.remove')}</div>
-                  </>}
-              </div>
-            </div>
-          )
-          : answer
-            ? (
-              <Tooltip
-                popupContent={t('appDebug.feature.annotation.add')}
-              >
-                <div
-                  className='p-1 rounded-md hover:bg-[#EEF4FF] hover:text-[#444CE7] cursor-pointer'
-                  onClick={handleAdd}
-                >
-                  <MessageFastPlus className='w-4 h-4' />
-                </div>
-              </Tooltip>
-            )
-            : null
-        }
-        <Tooltip
-          popupContent={t('appDebug.feature.annotation.edit')}
-        >
-          <div
-            className='p-1 cursor-pointer rounded-md hover:bg-black/5'
-            onClick={onEdit}
-          >
-            <Edit04 className='w-4 h-4' />
-          </div>
-        </Tooltip>
-
-      </div>
-      <RemoveAnnotationConfirmModal
-        isShow={showModal}
-        onHide={() => setShowModal(false)}
-        onRemove={handleRemove}
-      />
-    </div>
-  )
-}
-export default React.memo(CacheCtrlBtn)

+ 23 - 0
web/app/components/base/icons/assets/public/other/message-3-fill.svg

@@ -0,0 +1,23 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="message-3-fill">
+<g id="Vector" filter="url(#filter0_d_1071_49501)">
+<path d="M2 8.99374C2 5.68349 4.67654 3 8.00066 3H15.9993C19.3134 3 22 5.69478 22 8.99374V21H8.00066C4.68659 21 2 18.3052 2 15.0063V8.99374ZM14 11V13H16V11H14ZM8 11V13H10V11H8Z" fill="url(#paint0_linear_1071_49501)"/>
+</g>
+</g>
+<defs>
+<filter id="filter0_d_1071_49501" x="1.5" y="2.75" width="21" height="19" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
+<feFlood flood-opacity="0" result="BackgroundImageFix"/>
+<feColorMatrix in="SourceAlpha" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0" result="hardAlpha"/>
+<feOffset dy="0.25"/>
+<feGaussianBlur stdDeviation="0.25"/>
+<feComposite in2="hardAlpha" operator="out"/>
+<feColorMatrix type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"/>
+<feBlend mode="normal" in2="BackgroundImageFix" result="effect1_dropShadow_1071_49501"/>
+<feBlend mode="normal" in="SourceGraphic" in2="effect1_dropShadow_1071_49501" result="shape"/>
+</filter>
+<linearGradient id="paint0_linear_1071_49501" x1="12" y1="3" x2="12" y2="21" gradientUnits="userSpaceOnUse">
+<stop stop-color="#296DFF"/>
+<stop offset="1" stop-color="#0BA5EC"/>
+</linearGradient>
+</defs>
+</svg>

+ 173 - 0
web/app/components/base/icons/src/public/other/Message3Fill.json

@@ -0,0 +1,173 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "24",
+			"height": "24",
+			"viewBox": "0 0 24 24",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "message-3-fill"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Vector",
+							"filter": "url(#filter0_d_1071_49501)"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M2 8.99374C2 5.68349 4.67654 3 8.00066 3H15.9993C19.3134 3 22 5.69478 22 8.99374V21H8.00066C4.68659 21 2 18.3052 2 15.0063V8.99374ZM14 11V13H16V11H14ZM8 11V13H10V11H8Z",
+									"fill": "url(#paint0_linear_1071_49501)"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "filter",
+						"attributes": {
+							"id": "filter0_d_1071_49501",
+							"x": "1.5",
+							"y": "2.75",
+							"width": "21",
+							"height": "19",
+							"filterUnits": "userSpaceOnUse",
+							"color-interpolation-filters": "sRGB"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "feFlood",
+								"attributes": {
+									"flood-opacity": "0",
+									"result": "BackgroundImageFix"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feColorMatrix",
+								"attributes": {
+									"in": "SourceAlpha",
+									"type": "matrix",
+									"values": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0",
+									"result": "hardAlpha"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feOffset",
+								"attributes": {
+									"dy": "0.25"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feGaussianBlur",
+								"attributes": {
+									"stdDeviation": "0.25"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feComposite",
+								"attributes": {
+									"in2": "hardAlpha",
+									"operator": "out"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feColorMatrix",
+								"attributes": {
+									"type": "matrix",
+									"values": "0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.2 0"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feBlend",
+								"attributes": {
+									"mode": "normal",
+									"in2": "BackgroundImageFix",
+									"result": "effect1_dropShadow_1071_49501"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "feBlend",
+								"attributes": {
+									"mode": "normal",
+									"in": "SourceGraphic",
+									"in2": "effect1_dropShadow_1071_49501",
+									"result": "shape"
+								},
+								"children": []
+							}
+						]
+					},
+					{
+						"type": "element",
+						"name": "linearGradient",
+						"attributes": {
+							"id": "paint0_linear_1071_49501",
+							"x1": "12",
+							"y1": "3",
+							"x2": "12",
+							"y2": "21",
+							"gradientUnits": "userSpaceOnUse"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"stop-color": "#296DFF"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "stop",
+								"attributes": {
+									"offset": "1",
+									"stop-color": "#0BA5EC"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Message3Fill"
+}

+ 16 - 0
web/app/components/base/icons/src/public/other/Message3Fill.tsx

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

+ 1 - 0
web/app/components/base/icons/src/public/other/index.ts

@@ -1,3 +1,4 @@
 export { default as Icon3Dots } from './Icon3Dots'
 export { default as DefaultToolIcon } from './DefaultToolIcon'
+export { default as Message3Fill } from './Message3Fill'
 export { default as RowStruct } from './RowStruct'

+ 3 - 3
web/app/components/base/image-uploader/text-generation-image-uploader.tsx

@@ -50,7 +50,7 @@ const PasteImageLinkButton: FC<PasteImageLinkButtonProps> = ({
     >
       <PortalToFollowElemTrigger onClick={handleToggle}>
         <div className={`
-          relative flex items-center justify-center px-3 h-8 bg-components-option-card-option-bg hover:bg-components-option-card-option-bg-hover text-xs text-text-tertiary rounded-lg
+          relative flex items-center justify-center px-3 h-8 bg-components-button-tertiary-bg hover:bg-components-button-tertiary-bg-hover text-xs text-text-tertiary rounded-lg
           ${disabled ? 'cursor-not-allowed' : 'cursor-pointer'}
         `}>
           <Link03 className='mr-2 w-4 h-4' />
@@ -98,9 +98,9 @@ const TextGenerationImageUploader: FC<TextGenerationImageUploaderProps> = ({
       {
         hovering => (
           <div className={`
-            flex items-center justify-center px-3 h-8 bg-components-option-card-option-bg 
+            flex items-center justify-center px-3 h-8 bg-components-button-tertiary-bg  
             text-xs text-text-tertiary rounded-lg cursor-pointer
-            ${hovering && 'bg-components-option-card-option-bg-hover'}
+            ${hovering && 'hover:bg-components-button-tertiary-bg-hover'}
           `}>
             <ImagePlus className='mr-2 w-4 h-4' />
             {t('common.imageUploader.uploadFromComputer')}

+ 28 - 26
web/app/components/base/markdown.tsx

@@ -7,11 +7,14 @@ import RehypeKatex from 'rehype-katex'
 import RemarkGfm from 'remark-gfm'
 import RehypeRaw from 'rehype-raw'
 import SyntaxHighlighter from 'react-syntax-highlighter'
-import { atelierHeathLight } from 'react-syntax-highlighter/dist/esm/styles/hljs'
+import {
+  atelierHeathDark,
+  atelierHeathLight,
+} from 'react-syntax-highlighter/dist/esm/styles/hljs'
 import { Component, memo, useMemo, useRef, useState } from 'react'
 import { flow } from 'lodash-es'
-import cn from '@/utils/classnames'
-import CopyBtn from '@/app/components/base/copy-btn'
+import ActionButton from '@/app/components/base/action-button'
+import CopyIcon from '@/app/components/base/copy-icon'
 import SVGBtn from '@/app/components/base/svg'
 import Flowchart from '@/app/components/base/mermaid'
 import ImageGallery from '@/app/components/base/image-gallery'
@@ -22,6 +25,9 @@ import SVGRenderer from '@/app/components/base/svg-gallery'
 import MarkdownButton from '@/app/components/base/markdown-blocks/button'
 import MarkdownForm from '@/app/components/base/markdown-blocks/form'
 import ThinkBlock from '@/app/components/base/markdown-blocks/think-block'
+import { Theme } from '@/types/app'
+import { useAppContext } from '@/context/app-context'
+import cn from '@/utils/classnames'
 
 // Available language https://github.com/react-syntax-highlighter/react-syntax-highlighter/blob/master/AVAILABLE_LANGUAGES_HLJS.MD
 const capitalizationLanguageNameMap: Record<string, string> = {
@@ -100,7 +106,8 @@ export function PreCode(props: { children: any }) {
 // visit https://reactjs.org/docs/error-decoder.html?invariant=185 for the full message
 // or use the non-minified dev environment for full errors and additional helpful warnings.
 
-const CodeBlock: any = memo(({ inline, className, children, ...props }) => {
+const CodeBlock: any = memo(({ inline, className, children, ...props }: any) => {
+  const { theme } = useAppContext()
   const [isSVG, setIsSVG] = useState(true)
   const match = /language-(\w+)/.exec(className || '')
   const language = match?.[1]
@@ -140,10 +147,12 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }) => {
       return (
         <SyntaxHighlighter
           {...props}
-          style={atelierHeathLight}
+          style={theme === Theme.light ? atelierHeathLight : atelierHeathDark}
           customStyle={{
             paddingLeft: 12,
-            backgroundColor: '#fff',
+            borderBottomLeftRadius: '10px',
+            borderBottomRightRadius: '10px',
+            backgroundColor: 'var(--color-components-input-bg-normal)',
           }}
           language={match?.[1]}
           showLineNumbers
@@ -159,21 +168,14 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }) => {
     return <code {...props} className={className}>{children}</code>
 
   return (
-    <div>
-      <div
-        className='flex justify-between h-8 items-center p-1 pl-3 border-b'
-        style={{
-          borderColor: 'rgba(0, 0, 0, 0.05)',
-        }}
-      >
-        <div className='text-[13px] text-gray-500 font-normal'>{languageShowName}</div>
-        <div style={{ display: 'flex' }}>
+    <div className='relative'>
+      <div className='bg-components-input-bg-normal rounded-t-[10px] flex justify-between h-8 items-center p-1 pl-3 border-b border-divider-subtle'>
+        <div className='system-xs-semibold-uppercase text-text-secondary'>{languageShowName}</div>
+        <div className='flex items-center gap-1'>
           {(['mermaid', 'svg']).includes(language!) && <SVGBtn isSVG={isSVG} setIsSVG={setIsSVG} />}
-          <CopyBtn
-            className='mr-1'
-            value={String(children).replace(/\n$/, '')}
-            isPlain
-          />
+          <ActionButton>
+            <CopyIcon content={String(children).replace(/\n$/, '')}/>
+          </ActionButton>
         </div>
       </div>
       {renderCodeContent}
@@ -182,16 +184,16 @@ const CodeBlock: any = memo(({ inline, className, children, ...props }) => {
 })
 CodeBlock.displayName = 'CodeBlock'
 
-const VideoBlock: any = memo(({ node }) => {
-  const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src)
+const VideoBlock: any = memo(({ node }: any) => {
+  const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src)
   if (srcs.length === 0)
     return null
   return <VideoGallery key={srcs.join()} srcs={srcs} />
 })
 VideoBlock.displayName = 'VideoBlock'
 
-const AudioBlock: any = memo(({ node }) => {
-  const srcs = node.children.filter(child => 'properties' in child).map(child => (child as any).properties.src)
+const AudioBlock: any = memo(({ node }: any) => {
+  const srcs = node.children.filter((child: any) => 'properties' in child).map((child: any) => (child as any).properties.src)
   if (srcs.length === 0)
     return null
   return <AudioGallery key={srcs.join()} srcs={srcs} />
@@ -243,7 +245,7 @@ export function Markdown(props: { content: string; className?: string }) {
     preprocessLaTeX,
   ])(props.content)
   return (
-    <div className={cn(props.className, 'markdown-body')}>
+    <div className={cn('markdown-body', '!text-text-primary', props.className)}>
       <ReactMarkdown
         remarkPlugins={[
           RemarkGfm,
@@ -282,7 +284,7 @@ export function Markdown(props: { content: string; className?: string }) {
           p: Paragraph,
           button: MarkdownButton,
           form: MarkdownForm,
-          script: ScriptBlock,
+          script: ScriptBlock as any,
           details: ThinkBlock,
         }}
       >

+ 99 - 0
web/app/components/base/new-audio-button/index.tsx

@@ -0,0 +1,99 @@
+'use client'
+import { useState } from 'react'
+import { useParams, usePathname } from 'next/navigation'
+import {
+  RiVolumeUpLine,
+} from '@remixicon/react'
+import { t } from 'i18next'
+import Tooltip from '@/app/components/base/tooltip'
+import { AudioPlayerManager } from '@/app/components/base/audio-btn/audio.player.manager'
+import ActionButton, { ActionButtonState } from '@/app/components/base/action-button'
+
+type AudioBtnProps = {
+  id?: string
+  voice?: string
+  value?: string
+}
+
+type AudioState = 'initial' | 'loading' | 'playing' | 'paused' | 'ended'
+
+const AudioBtn = ({
+  id,
+  voice,
+  value,
+}: AudioBtnProps) => {
+  const [audioState, setAudioState] = useState<AudioState>('initial')
+
+  const params = useParams()
+  const pathname = usePathname()
+  const audio_finished_call = (event: string): any => {
+    switch (event) {
+      case 'ended':
+        setAudioState('ended')
+        break
+      case 'paused':
+        setAudioState('ended')
+        break
+      case 'loaded':
+        setAudioState('loading')
+        break
+      case 'play':
+        setAudioState('playing')
+        break
+      case 'error':
+        setAudioState('ended')
+        break
+    }
+  }
+  let url = ''
+  let isPublic = false
+
+  if (params.token) {
+    url = '/text-to-audio'
+    isPublic = true
+  }
+  else if (params.appId) {
+    if (pathname.search('explore/installed') > -1)
+      url = `/installed-apps/${params.appId}/text-to-audio`
+    else
+      url = `/apps/${params.appId}/text-to-audio`
+  }
+  const handleToggle = async () => {
+    if (audioState === 'playing' || audioState === 'loading') {
+      setTimeout(() => setAudioState('paused'), 1)
+      AudioPlayerManager.getInstance().getAudioPlayer(url, isPublic, id, value, voice, audio_finished_call).pauseAudio()
+    }
+    else {
+      setTimeout(() => setAudioState('loading'), 1)
+      AudioPlayerManager.getInstance().getAudioPlayer(url, isPublic, id, value, voice, audio_finished_call).playAudio()
+    }
+  }
+
+  const tooltipContent = {
+    initial: t('appApi.play'),
+    ended: t('appApi.play'),
+    paused: t('appApi.pause'),
+    playing: t('appApi.playing'),
+    loading: t('appApi.loading'),
+  }[audioState]
+
+  return (
+    <Tooltip
+      popupContent={tooltipContent}
+    >
+      <ActionButton
+        state={
+          audioState === 'loading' || audioState === 'playing'
+            ? ActionButtonState.Active
+            : ActionButtonState.Default
+        }
+        onClick={handleToggle}
+        disabled={audioState === 'loading'}
+      >
+        <RiVolumeUpLine className='w-4 h-4' />
+      </ActionButton>
+    </Tooltip>
+  )
+}
+
+export default AudioBtn

+ 0 - 31
web/app/components/base/regenerate-btn/index.tsx

@@ -1,31 +0,0 @@
-'use client'
-import { t } from 'i18next'
-import { Refresh } from '../icons/src/vender/line/general'
-import Tooltip from '@/app/components/base/tooltip'
-
-type Props = {
-  className?: string
-  onClick?: () => void
-}
-
-const RegenerateBtn = ({ className, onClick }: Props) => {
-  return (
-    <div className={`${className}`}>
-      <Tooltip
-        popupContent={t('appApi.regenerate') as string}
-      >
-        <div
-          className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
-          onClick={() => onClick?.()}
-          style={{
-            boxShadow: '0px 4px 8px -2px rgba(16, 24, 40, 0.1), 0px 2px 4px -2px rgba(16, 24, 40, 0.06)',
-          }}
-        >
-          <Refresh className="p-[3.5px] w-6 h-6 text-[#667085] hover:bg-gray-50" />
-        </div>
-      </Tooltip>
-    </div>
-  )
-}
-
-export default RegenerateBtn

+ 5 - 6
web/app/components/base/svg/index.tsx

@@ -1,5 +1,7 @@
 import React from 'react'
 import s from './style.module.css'
+import ActionButton from '../action-button'
+import cn from '@/utils/classnames'
 
 type ISVGBtnProps = {
   isSVG: boolean
@@ -11,12 +13,9 @@ const SVGBtn = ({
   setIsSVG,
 }: ISVGBtnProps) => {
   return (
-    <div
-      className={'box-border p-0.5 flex items-center justify-center rounded-md bg-white cursor-pointer'}
-      onClick={() => { setIsSVG(prevIsSVG => !prevIsSVG) }}
-    >
-      <div className={`w-6 h-6 rounded-md hover:bg-gray-50 ${s.svgIcon} ${isSVG ? s.svgIconed : ''}`}></div>
-    </div>
+    <ActionButton onClick={() => { setIsSVG(prevIsSVG => !prevIsSVG) }}>
+      <div className={cn('w-4 h-4', isSVG ? s.svgIconed : s.svgIcon)}></div>
+    </ActionButton>
   )
 }
 

+ 9 - 5
web/app/components/base/tab-header/index.tsx

@@ -1,13 +1,13 @@
 'use client'
 import type { FC } from 'react'
 import React from 'react'
-import s from './style.module.css'
 import cn from '@/utils/classnames'
 
 type Item = {
   id: string
   name: string
   isRight?: boolean
+  icon?: React.ReactNode
   extra?: React.ReactNode
 }
 
@@ -22,18 +22,22 @@ const TabHeader: FC<ITabHeaderProps> = ({
   value,
   onChange,
 }) => {
-  const renderItem = ({ id, name, extra }: Item) => (
+  const renderItem = ({ id, name, icon, extra }: Item) => (
     <div
       key={id}
-      className={cn(id === value ? `${s.itemActive} text-gray-900` : 'text-gray-500', 'relative flex items-center pb-1.5 leading-6 cursor-pointer')}
+      className={cn(
+        'relative flex items-center pt-2.5 pb-2 border-b-2 border-transparent system-md-semibold cursor-pointer',
+        id === value ? 'text-text-primary border-components-tab-active' : 'text-text-tertiary',
+      )}
       onClick={() => onChange(id)}
     >
-      <div className='text-base font-semibold'>{name}</div>
+      {icon || ''}
+      <div className='ml-2'>{name}</div>
       {extra || ''}
     </div>
   )
   return (
-    <div className='flex justify-between border-b border-gray-200 '>
+    <div className='flex justify-between'>
       <div className='flex space-x-4'>
         {items.filter(item => !item.isRight).map(renderItem)}
       </div>

+ 0 - 9
web/app/components/base/tab-header/style.module.css

@@ -1,9 +0,0 @@
-.itemActive::after {
-  content: '';
-  position: absolute;
-  bottom: -1px;
-  left: 0;
-  width: 100%;
-  height: 2px;
-  background-color: #155EEF;
-}

+ 164 - 177
web/app/components/share/text-generation/index.tsx

@@ -3,18 +3,16 @@ import type { FC } from 'react'
 import React, { useCallback, useEffect, useRef, useState } from 'react'
 import { useTranslation } from 'react-i18next'
 import {
+  RiBookmark3Line,
   RiErrorWarningFill,
 } from '@remixicon/react'
-import { useBoolean, useClickAway } from 'ahooks'
-import { XMarkIcon } from '@heroicons/react/24/outline'
+import { useBoolean } from 'ahooks'
 import { usePathname, useRouter, useSearchParams } from 'next/navigation'
 import TabHeader from '../../base/tab-header'
-import Button from '../../base/button'
 import { checkOrSetAccessToken } from '../utils'
-import s from './style.module.css'
+import MenuDropdown from './menu-dropdown'
 import RunBatch from './run-batch'
 import ResDownload from './run-batch/res-download'
-import cn from '@/utils/classnames'
 import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
 import RunOnce from '@/app/components/share/text-generation/run-once'
 import { fetchSavedMessage as doFetchSavedMessage, fetchAppInfo, fetchAppParams, removeMessage, saveMessage } from '@/service/share'
@@ -26,6 +24,7 @@ import type {
   TextToSpeechConfig,
 } from '@/models/debug'
 import AppIcon from '@/app/components/base/app-icon'
+import Badge from '@/app/components/base/badge'
 import { changeLanguage } from '@/i18n/i18next-config'
 import Loading from '@/app/components/base/loading'
 import { userInputsFormToPromptVariables } from '@/utils/model-config'
@@ -37,6 +36,8 @@ import Toast from '@/app/components/base/toast'
 import type { VisionFile, VisionSettings } from '@/types/app'
 import { Resolution, TransferMethod } from '@/types/app'
 import { useAppFavicon } from '@/hooks/use-app-favicon'
+import LogoSite from '@/app/components/base/logo/logo-site'
+import cn from '@/utils/classnames'
 
 const GROUP_SIZE = 5 // to avoid RPM(Request per minute) limit. The group task finished then the next group.
 enum TaskStatus {
@@ -72,8 +73,6 @@ const TextGeneration: FC<IMainProps> = ({
   const { t } = useTranslation()
   const media = useBreakpoints()
   const isPC = media === MediaType.pc
-  const isTablet = media === MediaType.tablet
-  const isMobile = media === MediaType.mobile
 
   const searchParams = useSearchParams()
   const mode = searchParams.get('mode') || 'create'
@@ -102,6 +101,7 @@ const TextGeneration: FC<IMainProps> = ({
   const [appId, setAppId] = useState<string>('')
   const [siteInfo, setSiteInfo] = useState<SiteInfo | null>(null)
   const [canReplaceLogo, setCanReplaceLogo] = useState<boolean>(false)
+  const [customConfig, setCustomConfig] = useState<Record<string, any> | null>(null)
   const [promptConfig, setPromptConfig] = useState<PromptConfig | null>(null)
   const [moreLikeThisConfig, setMoreLikeThisConfig] = useState<MoreLikeThisConfig | null>(null)
   const [textToSpeechConfig, setTextToSpeechConfig] = useState<TextToSpeechConfig | null>(null)
@@ -142,7 +142,7 @@ const TextGeneration: FC<IMainProps> = ({
     setAllTaskList([]) // clear batch task running status
 
     // eslint-disable-next-line ts/no-use-before-define
-    showResSidebar()
+    showResultPanel()
   }
 
   const [controlRetry, setControlRetry] = useState(0)
@@ -323,7 +323,7 @@ const TextGeneration: FC<IMainProps> = ({
     setControlStopResponding(Date.now())
 
     // eslint-disable-next-line ts/no-use-before-define
-    showResSidebar()
+    showResultPanel()
   }
   const handleCompleted = (completionRes: string, taskId?: number, isSuccess?: boolean) => {
     const allTaskListLatest = getLatestTaskList()
@@ -388,10 +388,11 @@ const TextGeneration: FC<IMainProps> = ({
   useEffect(() => {
     (async () => {
       const [appData, appParams]: any = await fetchInitData()
-      const { app_id: appId, site: siteInfo, can_replace_logo } = appData
+      const { app_id: appId, site: siteInfo, can_replace_logo, custom_config } = appData
       setAppId(appId)
       setSiteInfo(siteInfo as SiteInfo)
       setCanReplaceLogo(can_replace_logo)
+      setCustomConfig(custom_config)
       changeLanguage(siteInfo.default_language)
 
       const { user_input_form, more_like_this, file_upload, text_to_speech }: any = appParams
@@ -431,24 +432,21 @@ const TextGeneration: FC<IMainProps> = ({
     icon_url: siteInfo?.icon_url,
   })
 
-  const [isShowResSidebar, { setTrue: doShowResSidebar, setFalse: hideResSidebar }] = useBoolean(false)
-  const showResSidebar = () => {
+  const [isShowResultPanel, { setTrue: doShowResultPanel, setFalse: hideResultPanel }] = useBoolean(false)
+  const showResultPanel = () => {
     // fix: useClickAway hideResSidebar will close sidebar
     setTimeout(() => {
-      doShowResSidebar()
+      doShowResultPanel()
     }, 0)
   }
-  const resRef = useRef<HTMLDivElement>(null)
-  useClickAway(() => {
-    hideResSidebar()
-  }, resRef)
+  const [resultExisted, setResultExisted] = useState(false)
 
   const renderRes = (task?: Task) => (<Res
     key={task?.id}
     isWorkflow={isWorkflow}
     isCallBatchAPI={isCallBatchAPI}
     isPC={isPC}
-    isMobile={isMobile}
+    isMobile={!isPC}
     isInstalledApp={isInstalledApp}
     installedAppInfo={installedAppInfo}
     isError={task?.status === TaskStatus.failed}
@@ -458,7 +456,7 @@ const TextGeneration: FC<IMainProps> = ({
     controlSend={controlSend}
     controlRetry={task?.status === TaskStatus.failed ? controlRetry : 0}
     controlStopResponding={controlStopResponding}
-    onShowRes={showResSidebar}
+    onShowRes={showResultPanel}
     handleSaveMessage={handleSaveMessage}
     taskId={task?.id}
     onCompleted={handleCompleted}
@@ -466,77 +464,60 @@ const TextGeneration: FC<IMainProps> = ({
     completionFiles={completionFiles}
     isShowTextToSpeech={!!textToSpeechConfig?.enabled}
     siteInfo={siteInfo}
+    onRunStart={() => setResultExisted(true)}
   />)
 
   const renderBatchRes = () => {
     return (showTaskList.map(task => renderRes(task)))
   }
 
-  const resWrapClassNames = (() => {
-    if (isPC)
-      return 'grow h-full'
-
-    if (!isShowResSidebar)
-      return 'none'
-
-    return cn('fixed z-50 inset-0', isTablet ? 'pl-[128px]' : 'pl-6')
-  })()
-
   const renderResWrap = (
     <div
-      ref={resRef}
-      className={
-        cn(
-          'flex flex-col h-full shrink-0',
-          isPC ? 'px-10 py-8' : 'bg-gray-50',
-          isTablet && 'p-6', isMobile && 'p-4')
-      }
+      className={cn(
+        'relative flex flex-col h-full',
+        !isPC && 'h-[calc(100vh_-_36px)] rounded-t-2xl shadow-lg backdrop-blur-sm',
+        !isPC
+          ? isShowResultPanel
+            ? 'bg-background-default-burn'
+            : 'bg-components-panel-bg border-t-[0.5px] border-divider-regular'
+          : 'bg-chatbot-bg',
+      )}
     >
-      <>
-        <div className='flex items-center justify-between shrink-0'>
-          <div className='flex items-center space-x-3'>
-            <div className={s.starIcon}></div>
-            <div className='text-lg font-semibold text-gray-800'>{t('share.generation.title')}</div>
-          </div>
-          <div className='flex items-center space-x-2'>
-            {allFailedTaskList.length > 0 && (
-              <div className='flex items-center'>
-                <RiErrorWarningFill className='w-4 h-4 text-[#D92D20]' />
-                <div className='ml-1 text-[#D92D20]'>{t('share.generation.batchFailed.info', { num: allFailedTaskList.length })}</div>
-                <Button
-                  variant='primary'
-                  className='ml-2'
-                  onClick={handleRetryAllFailedTask}
-                >{t('share.generation.batchFailed.retry')}</Button>
-                <div className='mx-3 w-[1px] h-3.5 bg-gray-200'></div>
-              </div>
-            )}
-            {allSuccessTaskList.length > 0 && (
-              <ResDownload
-                isMobile={isMobile}
-                values={exportRes}
-              />
-            )}
-            {!isPC && (
-              <div
-                className='flex items-center justify-center cursor-pointer'
-                onClick={hideResSidebar}
-              >
-                <XMarkIcon className='w-4 h-4 text-gray-800' />
-              </div>
-            )}
-          </div>
-        </div>
-
-        <div className='overflow-y-auto grow'>
-          {!isCallBatchAPI ? renderRes() : renderBatchRes()}
-          {!noPendingTask && (
-            <div className='mt-4'>
-              <Loading type='area' />
-            </div>
+      {isCallBatchAPI && (
+        <div className={cn(
+          'shrink-0 px-14 pt-9 pb-2 flex items-center justify-between',
+          !isPC && 'px-4 pt-3 pb-1',
+        )}>
+          <div className='text-text-primary system-md-semibold-uppercase'>{t('share.generation.executions', { num: allTaskList.length })}</div>
+          {allSuccessTaskList.length > 0 && (
+            <ResDownload
+              isMobile={!isPC}
+              values={exportRes}
+            />
           )}
         </div>
-      </>
+      )}
+      <div className={cn(
+        'grow flex flex-col h-0 overflow-y-auto',
+        isPC && 'px-14 py-8',
+        isPC && isCallBatchAPI && 'pt-0',
+        !isPC && 'p-0 pb-2',
+      )}>
+        {!isCallBatchAPI ? renderRes() : renderBatchRes()}
+        {!noPendingTask && (
+          <div className='mt-4'>
+            <Loading type='area' />
+          </div>
+        )}
+      </div>
+      {isCallBatchAPI && allFailedTaskList.length > 0 && (
+        <div className='z-10 absolute bottom-6 left-1/2 -translate-x-1/2 flex items-center gap-2 p-3 rounded-xl bg-components-panel-bg-blur backdrop-blur-sm border border-components-panel-border shadow-lg'>
+          <RiErrorWarningFill className='w-4 h-4 text-text-destructive' />
+          <div className='text-text-secondary system-sm-medium'>{t('share.generation.batchFailed.info', { num: allFailedTaskList.length })}</div>
+          <div className='w-px h-3.5 bg-divider-regular'></div>
+          <div onClick={handleRetryAllFailedTask} className='text-text-accent system-sm-semibold-uppercase cursor-pointer'>{t('share.generation.batchFailed.retry')}</div>
+        </div>
+      )}
     </div>
   )
 
@@ -548,46 +529,34 @@ const TextGeneration: FC<IMainProps> = ({
   }
 
   return (
-    <>
+    <div className={cn(
+      'bg-background-default-burn',
+      isPC && 'flex',
+      !isPC && 'flex-col',
+      isInstalledApp ? 'h-full rounded-2xl shadow-md' : 'h-screen',
+    )}>
+      {/* Left */}
       <div className={cn(
-        isPC && 'flex',
-        isInstalledApp ? s.installedApp : 'h-screen',
-        'bg-gray-50',
+        'shrink-0 relative flex flex-col h-full',
+        isPC ? 'w-[600px] max-w-[50%]' : resultExisted ? 'h-[calc(100%_-_64px)]' : '',
+        isInstalledApp && 'rounded-l-2xl',
       )}>
-        {/* Left */}
-        <div className={cn(
-          isPC ? 'w-[600px] max-w-[50%] p-8' : 'p-4',
-          isInstalledApp && 'rounded-l-2xl',
-          'shrink-0 relative flex flex-col pb-10 h-full border-r border-gray-100 bg-white',
-        )}>
-          <div className='mb-6'>
-            <div className='flex items-center justify-between'>
-              <div className='flex items-center space-x-3'>
-                <AppIcon
-                  size="small"
-                  iconType={siteInfo.icon_type}
-                  icon={siteInfo.icon}
-                  background={siteInfo.icon_background || appDefaultIconBackground}
-                  imageUrl={siteInfo.icon_url}
-                />
-                <div className='text-lg font-semibold text-gray-800'>{siteInfo.title}</div>
-              </div>
-              {!isPC && (
-                <Button
-                  className='shrink-0 ml-2'
-                  onClick={showResSidebar}
-                >
-                  <div className='flex items-center space-x-2 text-primary-600 text-[13px] font-medium'>
-                    <div className={s.starIcon}></div>
-                    <span>{t('share.generation.title')}</span>
-                  </div>
-                </Button>
-              )}
-            </div>
-            {siteInfo.description && (
-              <div className='mt-2 text-xs text-gray-500'>{siteInfo.description}</div>
-            )}
+        {/* header */}
+        <div className={cn('shrink-0 space-y-4 border-b border-divider-subtle', isPC ? 'p-8 pb-0 bg-components-panel-bg' : 'p-4 pb-0')}>
+          <div className='flex items-center gap-3'>
+            <AppIcon
+              size={isPC ? 'large' : 'small'}
+              iconType={siteInfo.icon_type}
+              icon={siteInfo.icon}
+              background={siteInfo.icon_background || appDefaultIconBackground}
+              imageUrl={siteInfo.icon_url}
+            />
+            <div className='grow text-text-secondary system-md-semibold truncate'>{siteInfo.title}</div>
+            <MenuDropdown data={siteInfo} />
           </div>
+          {siteInfo.description && (
+            <div className='system-xs-regular text-text-tertiary'>{siteInfo.description}</div>
+          )}
           <TabHeader
             items={[
               { id: 'create', name: t('share.generation.tabs.create') },
@@ -597,11 +566,12 @@ const TextGeneration: FC<IMainProps> = ({
                   id: 'saved',
                   name: t('share.generation.tabs.saved'),
                   isRight: true,
+                  icon: <RiBookmark3Line className='w-4 h-4' />,
                   extra: savedMessages.length > 0
                     ? (
-                      <div className='ml-1 flex items-center h-5 px-1.5 rounded-md border border-gray-200 text-gray-500 text-xs font-medium'>
+                      <Badge className='ml-1'>
                         {savedMessages.length}
-                      </div>
+                      </Badge>
                     )
                     : null,
                 }]
@@ -610,72 +580,89 @@ const TextGeneration: FC<IMainProps> = ({
             value={currentTab}
             onChange={setCurrentTab}
           />
-          <div className='h-20 overflow-y-auto grow'>
-            <div className={cn(currentTab === 'create' ? 'block' : 'hidden')}>
-              <RunOnce
-                siteInfo={siteInfo}
-                inputs={inputs}
-                inputsRef={inputsRef}
-                onInputsChange={setInputs}
-                promptConfig={promptConfig}
-                onSend={handleSend}
-                visionConfig={visionConfig}
-                onVisionFilesChange={setCompletionFiles}
-              />
-            </div>
-            <div className={cn(isInBatchTab ? 'block' : 'hidden')}>
-              <RunBatch
-                vars={promptConfig.prompt_variables}
-                onSend={handleRunBatch}
-                isAllFinished={allTasksRun}
-              />
-            </div>
-
-            {currentTab === 'saved' && (
-              <SavedItems
-                className='mt-4'
-                isShowTextToSpeech={textToSpeechConfig?.enabled}
-                list={savedMessages}
-                onRemove={handleRemoveSavedMessage}
-                onStartCreateContent={() => setCurrentTab('create')}
-              />
-            )}
+        </div>
+        {/* form */}
+        <div className={cn(
+          'grow h-0 bg-components-panel-bg overflow-y-auto',
+          isPC ? 'px-8' : 'px-4',
+          !isPC && resultExisted && customConfig?.remove_webapp_brand && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
+        )}>
+          <div className={cn(currentTab === 'create' ? 'block' : 'hidden')}>
+            <RunOnce
+              siteInfo={siteInfo}
+              inputs={inputs}
+              inputsRef={inputsRef}
+              onInputsChange={setInputs}
+              promptConfig={promptConfig}
+              onSend={handleSend}
+              visionConfig={visionConfig}
+              onVisionFilesChange={setCompletionFiles}
+            />
           </div>
-
-          {/* copyright */}
+          <div className={cn(isInBatchTab ? 'block' : 'hidden')}>
+            <RunBatch
+              vars={promptConfig.prompt_variables}
+              onSend={handleRunBatch}
+              isAllFinished={allTasksRun}
+            />
+          </div>
+          {currentTab === 'saved' && (
+            <SavedItems
+              className={cn(isPC ? 'mt-6' : 'mt-4')}
+              isShowTextToSpeech={textToSpeechConfig?.enabled}
+              list={savedMessages}
+              onRemove={handleRemoveSavedMessage}
+              onStartCreateContent={() => setCurrentTab('create')}
+            />
+          )}
+        </div>
+        {/* powered by */}
+        {!customConfig?.remove_webapp_brand && (
           <div className={cn(
-            isInstalledApp ? 'left-[248px]' : 'left-8',
-            'fixed  bottom-4  flex space-x-2 text-gray-400 font-normal text-xs',
+            'shrink-0 py-3 flex items-center gap-1.5 bg-components-panel-bg',
+            isPC ? 'px-8' : 'px-4',
+            !isPC && resultExisted && 'rounded-b-2xl border-b-[0.5px] border-divider-regular',
           )}>
-            {siteInfo.copyright && (
-              <div className="">© {(new Date()).getFullYear()} {siteInfo.copyright}</div>
+            <div className='text-text-tertiary system-2xs-medium-uppercase'>{t('share.chat.poweredBy')}</div>
+            {customConfig?.replace_webapp_logo && (
+              <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
             )}
-            {siteInfo.privacy_policy && (
-              <>
-                {siteInfo.copyright && <div>·</div>}
-                <div>{t('share.chat.privacyPolicyLeft')}
-                  <a
-                    className='text-gray-500 px-1'
-                    href={siteInfo.privacy_policy}
-                    target='_blank' rel='noopener noreferrer'>{t('share.chat.privacyPolicyMiddle')}</a>
-                  {t('share.chat.privacyPolicyRight')}
-                </div>
-              </>
+            {!customConfig?.replace_webapp_logo && (
+              <LogoSite className='!h-5' />
             )}
           </div>
-        </div>
-
-        {/* Result */}
-        <div
-          className={resWrapClassNames}
-          style={{
-            background: (!isPC && isShowResSidebar) ? 'rgba(35, 56, 118, 0.2)' : 'none',
-          }}
-        >
-          {renderResWrap}
-        </div>
+        )}
+      </div>
+      {/* Result */}
+      <div className={cn(
+        isPC
+          ? 'grow h-full'
+          : isShowResultPanel
+            ? 'fixed z-50 inset-0 bg-background-overlay backdrop-blur-sm'
+            : resultExisted
+              ? 'relative shrink-0 h-16 pt-2.5 bg-background-default-burn overflow-hidden'
+              : '',
+      )}>
+        {!isPC && (
+          <div
+            className={cn(
+              isShowResultPanel
+                ? 'p-2 pt-6 flex items-center justify-center'
+                : 'z-10 absolute top-0 left-0 w-full px-2 pt-[3px] pb-[57px] flex items-center justify-center',
+            )}
+            onClick={() => {
+              if (isShowResultPanel)
+                hideResultPanel()
+              else
+                showResultPanel()
+            }}
+          >
+            <div className='w-8 h-1 rounded bg-divider-solid cursor-grab'/>
+          </div>
+        )}
+        {renderResWrap}
       </div>
-    </>
+    </div>
   )
 }
 

+ 49 - 0
web/app/components/share/text-generation/info-modal.tsx

@@ -0,0 +1,49 @@
+import React from 'react'
+import Modal from '@/app/components/base/modal'
+import AppIcon from '@/app/components/base/app-icon'
+import type { SiteInfo } from '@/models/share'
+import { appDefaultIconBackground } from '@/config'
+import cn from 'classnames'
+
+type Props = {
+  data?: SiteInfo
+  isShow: boolean
+  onClose: () => void
+}
+
+const InfoModal = ({
+  isShow,
+  onClose,
+  data,
+}: Props) => {
+  return (
+    <Modal
+      isShow={isShow}
+      onClose={onClose}
+      className='!p-0 min-w-[400px] max-w-[400px]'
+      closable
+    >
+      <div className={cn('pt-10 px-4 pb-8 flex flex-col items-center gap-4')}>
+        <AppIcon
+          size='xxl'
+          iconType={data?.icon_type}
+          icon={data?.icon}
+          background={data?.icon_background || appDefaultIconBackground}
+          imageUrl={data?.icon_url}
+        />
+        <div className='text-text-secondary system-xl-semibold'>{data?.title}</div>
+        <div className='text-text-tertiary system-xs-regular'>
+          {/* copyright */}
+          {data?.copyright && (
+            <div>© {(new Date()).getFullYear()} {data?.copyright}</div>
+          )}
+          {data?.custom_disclaimer && (
+            <div className='mt-2'>{data.custom_disclaimer}</div>
+          )}
+        </div>
+      </div>
+    </Modal>
+  )
+}
+
+export default InfoModal

+ 91 - 0
web/app/components/share/text-generation/menu-dropdown.tsx

@@ -0,0 +1,91 @@
+'use client'
+import type { FC } from 'react'
+import React, { useCallback, useRef, useState } from 'react'
+import { useTranslation } from 'react-i18next'
+import type { Placement } from '@floating-ui/react'
+import {
+  RiEqualizer2Line,
+} from '@remixicon/react'
+import ActionButton from '@/app/components/base/action-button'
+import {
+  PortalToFollowElem,
+  PortalToFollowElemContent,
+  PortalToFollowElemTrigger,
+} from '@/app/components/base/portal-to-follow-elem'
+import InfoModal from './info-modal'
+import type { SiteInfo } from '@/models/share'
+import cn from '@/utils/classnames'
+
+type Props = {
+  data?: SiteInfo
+  placement?: Placement
+}
+
+const MenuDropdown: FC<Props> = ({
+  data,
+  placement,
+}) => {
+  const { t } = useTranslation()
+  const [open, doSetOpen] = useState(false)
+  const openRef = useRef(open)
+  const setOpen = useCallback((v: boolean) => {
+    doSetOpen(v)
+    openRef.current = v
+  }, [doSetOpen])
+
+  const handleTrigger = useCallback(() => {
+    setOpen(!openRef.current)
+  }, [setOpen])
+
+  const [show, setShow] = useState(false)
+
+  return (
+    <>
+      <PortalToFollowElem
+        open={open}
+        onOpenChange={setOpen}
+        placement={placement || 'bottom-end'}
+        offset={{
+          mainAxis: 4,
+          crossAxis: -4,
+        }}
+      >
+        <PortalToFollowElemTrigger onClick={handleTrigger}>
+          <div>
+            <ActionButton size='l' className={cn(open && 'bg-state-base-hover')}>
+              <RiEqualizer2Line className='w-[18px] h-[18px]' />
+            </ActionButton>
+          </div>
+        </PortalToFollowElemTrigger>
+        <PortalToFollowElemContent className='z-50'>
+          <div className='w-[224px] bg-components-panel-bg-blur backdrop-blur-sm rounded-xl border-[0.5px] border-components-panel-border shadow-lg'>
+            <div className='p-1'>
+              {data?.privacy_policy && (
+                <a href={data.privacy_policy} target='_blank' className='flex items-center px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'>
+                  <span className='grow'>{t('share.chat.privacyPolicyMiddle')}</span>
+                </a>
+              )}
+              <div
+                onClick={() => {
+                  handleTrigger()
+                  setShow(true)
+                }}
+                className='px-3 py-1.5 rounded-lg text-text-secondary system-md-regular cursor-pointer hover:bg-state-base-hover'
+              >{t('common.userProfile.about')}</div>
+            </div>
+          </div>
+        </PortalToFollowElemContent>
+      </PortalToFollowElem>
+      {show && (
+        <InfoModal
+          isShow={show}
+          onClose={() => {
+            setShow(false)
+          }}
+          data={data}
+        />
+      )}
+    </>
+  )
+}
+export default React.memo(MenuDropdown)

+ 5 - 9
web/app/components/share/text-generation/no-data/index.tsx

@@ -1,22 +1,18 @@
 import type { FC } from 'react'
 import React from 'react'
+import {
+  RiSparklingFill,
+} from '@remixicon/react'
 import { useTranslation } from 'react-i18next'
 
-const StarIcon = (
-  <svg width="50" height="50" viewBox="0 0 50 50" fill="none" xmlns="http://www.w3.org/2000/svg">
-    <path d="M7.50033 48.3337V36.667M7.50033 13.3337V1.66699M1.66699 7.50033H13.3337M1.66699 42.5003H13.3337M27.3337 4.00032L23.2872 14.521C22.6292 16.2319 22.3002 17.0873 21.7886 17.8069C21.3351 18.4446 20.7779 19.0018 20.1402 19.4552C19.4206 19.9669 18.5652 20.2959 16.8543 20.9539L6.33366 25.0003L16.8543 29.0467C18.5652 29.7048 19.4206 30.0338 20.1402 30.5454C20.7779 30.9989 21.3351 31.5561 21.7886 32.1938C22.3002 32.9133 22.6292 33.7688 23.2872 35.4796L27.3337 46.0003L31.3801 35.4796C32.0381 33.7688 32.3671 32.9133 32.8788 32.1938C33.3322 31.5561 33.8894 30.9989 34.5271 30.5454C35.2467 30.0338 36.1021 29.7048 37.813 29.0467L48.3337 25.0003L37.813 20.9539C36.1021 20.2959 35.2467 19.9669 34.5271 19.4552C33.8894 19.0018 33.3322 18.4446 32.8788 17.8069C32.3671 17.0873 32.0381 16.2319 31.3801 14.521L27.3337 4.00032Z" stroke="#EAECF0" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
-  </svg>
-
-)
-
 export type INoDataProps = {}
 const NoData: FC<INoDataProps> = () => {
   const { t } = useTranslation()
   return (
     <div className='flex flex-col h-full w-full justify-center items-center'>
-      {StarIcon}
+      <RiSparklingFill className='w-12 h-12 text-text-empty-state-icon' />
       <div
-        className='mt-3 text-gray-300 text-xs leading-3'
+        className='mt-2 text-text-quaternary system-sm-regular'
       >
         {t('share.generation.noData')}
       </div>

+ 19 - 26
web/app/components/share/text-generation/result/index.tsx

@@ -4,7 +4,6 @@ import React, { useEffect, useRef, useState } from 'react'
 import { useBoolean } from 'ahooks'
 import { t } from 'i18next'
 import produce from 'immer'
-import cn from '@/utils/classnames'
 import TextGenerationRes from '@/app/components/app/text-generate/item'
 import NoData from '@/app/components/share/text-generation/no-data'
 import Toast from '@/app/components/base/toast'
@@ -13,7 +12,6 @@ import type { FeedbackType } from '@/app/components/base/chat/chat/type'
 import Loading from '@/app/components/base/loading'
 import type { PromptConfig } from '@/models/debug'
 import type { InstalledApp } from '@/models/explore'
-import type { ModerationService } from '@/models/common'
 import { TransferMethod, type VisionFile, type VisionSettings } from '@/types/app'
 import { NodeRunningStatus, WorkflowRunningStatus } from '@/app/components/workflow/types'
 import type { WorkflowProcess } from '@/app/components/base/chat/types'
@@ -24,7 +22,7 @@ import {
   getFilesInLogs,
 } from '@/app/components/base/file-uploader/utils'
 
-export interface IResultProps {
+export type IResultProps = {
   isWorkflow: boolean
   isCallBatchAPI: boolean
   isPC: boolean
@@ -43,11 +41,10 @@ export interface IResultProps {
   handleSaveMessage: (messageId: string) => void
   taskId?: number
   onCompleted: (completionRes: string, taskId?: number, success?: boolean) => void
-  enableModeration?: boolean
-  moderationService?: (text: string) => ReturnType<ModerationService>
   visionConfig: VisionSettings
   completionFiles: VisionFile[]
   siteInfo: SiteInfo | null
+  onRunStart: () => void
 }
 
 const Result: FC<IResultProps> = ({
@@ -72,6 +69,7 @@ const Result: FC<IResultProps> = ({
   visionConfig,
   completionFiles,
   siteInfo,
+  onRunStart,
 }) => {
   const [isResponding, { setTrue: setRespondingTrue, setFalse: setRespondingFalse }] = useBoolean(false)
   useEffect(() => {
@@ -183,8 +181,10 @@ const Result: FC<IResultProps> = ({
     let res: string[] = []
     let tempMessageId = ''
 
-    if (!isPC)
+    if (!isPC) {
       onShowRes()
+      onRunStart()
+    }
 
     setRespondingTrue()
     let isEnd = false
@@ -375,7 +375,6 @@ const Result: FC<IResultProps> = ({
     <TextGenerationRes
       isWorkflow={isWorkflow}
       workflowProcessData={workflowProcessData}
-      className='mt-3'
       isError={isError}
       onRetry={handleSend}
       content={completionRes}
@@ -398,7 +397,7 @@ const Result: FC<IResultProps> = ({
   )
 
   return (
-    <div className={cn(isNoData && !isCallBatchAPI && 'h-full')}>
+    <>
       {!isCallBatchAPI && !isWorkflow && (
         (isResponding && !completionRes)
           ? (
@@ -414,25 +413,19 @@ const Result: FC<IResultProps> = ({
             </>
           )
       )}
-      {
-        !isCallBatchAPI && isWorkflow && (
-          (isResponding && !workflowProcessData)
-            ? (
-              <div className='flex h-full w-full justify-center items-center'>
-                <Loading type='area' />
-              </div>
-            )
-            : !workflowProcessData
-              ? <NoData />
-              : renderTextGenerationRes()
-        )
-      }
-      {isCallBatchAPI && (
-        <div className='mt-2'>
-          {renderTextGenerationRes()}
-        </div>
+      {!isCallBatchAPI && isWorkflow && (
+        (isResponding && !workflowProcessData)
+          ? (
+            <div className='flex h-full w-full justify-center items-center'>
+              <Loading type='area' />
+            </div>
+          )
+          : !workflowProcessData
+            ? <NoData />
+            : renderTextGenerationRes()
       )}
-    </div>
+      {isCallBatchAPI && renderTextGenerationRes()}
+    </>
   )
 }
 export default React.memo(Result)

+ 6 - 6
web/app/components/share/text-generation/run-batch/csv-download/index.tsx

@@ -27,17 +27,17 @@ const CSVDownload: FC<ICSVDownloadProps> = ({
 
   return (
     <div className='mt-6'>
-      <div className='text-sm text-gray-900 font-medium'>{t('share.generation.csvStructureTitle')}</div>
+      <div className='system-sm-medium text-text-primary'>{t('share.generation.csvStructureTitle')}</div>
       <div className='mt-2 max-h-[500px] overflow-auto'>
-        <table className='w-full border-separate border-spacing-0 border border-gray-200 rounded-lg text-xs'>
-          <thead className='text-gray-500'>
+        <table className='table-fixed w-full border-separate border-spacing-0 border border-divider-regular rounded-lg text-xs'>
+          <thead className='text-text-tertiary'>
             <tr>
               {addQueryContentVars.map((item, i) => (
-                <td key={i} className='h-9 pl-4 border-b border-gray-200'>{item.name}</td>
+                <td key={i} className='h-9 pl-3 pr-2 border-b border-divider-regular'>{item.name}</td>
               ))}
             </tr>
           </thead>
-          <tbody className='text-gray-300'>
+          <tbody className='text-text-secondary'>
             <tr>
               {addQueryContentVars.map((item, i) => (
                 <td key={i} className='h-9 pl-4'>{item.name} {t('share.generation.field')}</td>
@@ -58,7 +58,7 @@ const CSVDownload: FC<ICSVDownloadProps> = ({
           template,
         ]}
       >
-        <div className='flex items-center h-[18px] space-x-1 text-[#155EEF] text-xs font-medium'>
+        <div className='flex items-center h-[18px] space-x-1 text-text-accent system-xs-medium'>
           <DownloadIcon className='w-3 h-3' />
           <span>{t('share.generation.downloadTemplate')}</span>
         </div>

+ 9 - 6
web/app/components/share/text-generation/run-batch/csv-reader/index.tsx

@@ -5,7 +5,6 @@ import {
   useCSVReader,
 } from 'react-papaparse'
 import { useTranslation } from 'react-i18next'
-import s from './style.module.css'
 import cn from '@/utils/classnames'
 import { Csv as CSVIcon } from '@/app/components/base/icons/src/public/files'
 
@@ -41,7 +40,11 @@ const CSVReader: FC<Props> = ({
         <>
           <div
             {...getRootProps()}
-            className={cn(s.zone, zoneHover && s.zoneHover, acceptedFile ? 'px-6' : 'justify-center border-dashed text-gray-500')}
+            className={cn(
+              'flex items-center h-20 rounded-xl bg-components-dropzone-bg border border-dashed border-components-dropzone-border system-sm-regular',
+              acceptedFile && 'px-6 bg-components-panel-on-panel-item-bg border-solid border-components-panel-border hover:bg-components-panel-on-panel-item-bg-hover hover:border-components-panel-bg-blur',
+              zoneHover && 'bg-components-dropzone-bg-accent border border-components-dropzone-border-accent',
+            )}
           >
             {
               acceptedFile
@@ -49,15 +52,15 @@ const CSVReader: FC<Props> = ({
                   <div className='w-full flex items-center space-x-2'>
                     <CSVIcon className="shrink-0" />
                     <div className='flex w-0 grow'>
-                      <span className='max-w-[calc(100%_-_30px)] text-ellipsis whitespace-nowrap overflow-hidden text-gray-800'>{acceptedFile.name.replace(/.csv$/, '')}</span>
-                      <span className='shrink-0 text-gray-500'>.csv</span>
+                      <span className='max-w-[calc(100%_-_30px)] truncate text-text-secondary'>{acceptedFile.name.replace(/.csv$/, '')}</span>
+                      <span className='shrink-0 text-text-tertiary'>.csv</span>
                     </div>
                   </div>
                 )
                 : (
-                  <div className='flex items-center justify-center space-x-2'>
+                  <div className='w-full flex items-center justify-center space-x-2'>
                     <CSVIcon className="shrink-0" />
-                    <div className='text-gray-500'>{t('share.generation.csvUploadTitle')}<span className='text-primary-400'>{t('share.generation.browse')}</span></div>
+                    <div className='text-text-tertiary'>{t('share.generation.csvUploadTitle')}<span className='text-text-accent cursor-pointer'>{t('share.generation.browse')}</span></div>
                   </div>
                 )}
           </div>

+ 0 - 11
web/app/components/share/text-generation/run-batch/csv-reader/style.module.css

@@ -1,11 +0,0 @@
-.zone {
-    @apply flex items-center h-20 rounded-xl bg-gray-50 border border-gray-200 cursor-pointer text-sm font-normal;
-}
-
-.zoneHover {
-    @apply border-solid bg-gray-100;
-}
-
-.info {
-    @apply text-gray-800 text-sm;
-}

+ 7 - 7
web/app/components/share/text-generation/run-batch/index.tsx

@@ -1,17 +1,16 @@
 'use client'
 import type { FC } from 'react'
 import React from 'react'
-import {
-  PlayIcon,
-} from '@heroicons/react/24/solid'
 import { useTranslation } from 'react-i18next'
 import {
   RiLoader2Line,
+  RiPlayLargeLine,
 } from '@remixicon/react'
 import CSVReader from './csv-reader'
 import CSVDownload from './csv-download'
-import cn from '@/utils/classnames'
 import Button from '@/app/components/base/button'
+import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import cn from '@/utils/classnames'
 export type IRunBatchProps = {
   vars: { name: string }[]
   onSend: (data: string[][]) => void
@@ -24,6 +23,8 @@ const RunBatch: FC<IRunBatchProps> = ({
   isAllFinished,
 }) => {
   const { t } = useTranslation()
+  const media = useBreakpoints()
+  const isPC = media === MediaType.pc
 
   const [csvData, setCsvData] = React.useState<string[][]>([])
   const [isParsed, setIsParsed] = React.useState(false)
@@ -36,16 +37,15 @@ const RunBatch: FC<IRunBatchProps> = ({
   const handleSend = () => {
     onSend(csvData)
   }
-  const Icon = isAllFinished ? PlayIcon : RiLoader2Line
+  const Icon = isAllFinished ? RiPlayLargeLine : RiLoader2Line
   return (
     <div className='pt-4'>
       <CSVReader onParsed={handleParsed} />
       <CSVDownload vars={vars} />
-      <div className='mt-4 h-[1px] bg-gray-100'></div>
       <div className='flex justify-end'>
         <Button
           variant="primary"
-          className='mt-4 pl-3 pr-4'
+          className={cn('mt-4 pl-3 pr-4', !isPC && 'grow')}
           onClick={handleSend}
           disabled={!isParsed || !isAllFinished}
         >

+ 15 - 6
web/app/components/share/text-generation/run-batch/res-download/index.tsx

@@ -1,13 +1,15 @@
 'use client'
 import type { FC } from 'react'
 import React from 'react'
+import { RiDownloadLine } from '@remixicon/react'
 import {
   useCSVDownloader,
 } from 'react-papaparse'
 import { useTranslation } from 'react-i18next'
-import cn from '@/utils/classnames'
-import { Download02 as DownloadIcon } from '@/app/components/base/icons/src/vender/solid/general'
+import ActionButton from '@/app/components/base/action-button'
 import Button from '@/app/components/base/button'
+import cn from '@/utils/classnames'
+
 export type IResDownloadProps = {
   isMobile: boolean
   values: Record<string, string>[]
@@ -31,10 +33,17 @@ const ResDownload: FC<IResDownloadProps> = ({
       }}
       data={values}
     >
-      <Button className={cn('space-x-2 bg-white', isMobile ? '!p-0 !w-8 justify-center' : '')}>
-        <DownloadIcon className='w-4 h-4 text-[#155EEF]' />
-        {!isMobile && <span className='text-[#155EEF]'>{t('common.operation.download')}</span>}
-      </Button>
+      {isMobile && (
+        <ActionButton>
+          <RiDownloadLine className='w-4 h-4' />
+        </ActionButton>
+      )}
+      {!isMobile && (
+        <Button className={cn('space-x-1')}>
+          <RiDownloadLine className='w-4 h-4' />
+          <span>{t('common.operation.download')}</span>
+        </Button>
+      )}
     </CSVDownloader>
   )
 }

+ 17 - 17
web/app/components/share/text-generation/run-once/index.tsx

@@ -2,18 +2,21 @@ import type { FC, FormEvent } from 'react'
 import React, { useCallback } from 'react'
 import { useTranslation } from 'react-i18next'
 import {
-  PlayIcon,
-} from '@heroicons/react/24/solid'
+  RiPlayLargeLine,
+} from '@remixicon/react'
 import Select from '@/app/components/base/select'
 import type { SiteInfo } from '@/models/share'
 import type { PromptConfig } from '@/models/debug'
 import Button from '@/app/components/base/button'
 import Textarea from '@/app/components/base/textarea'
+import Input from '@/app/components/base/input'
 import { DEFAULT_VALUE_MAX_LEN } from '@/config'
 import TextGenerationImageUploader from '@/app/components/base/image-uploader/text-generation-image-uploader'
 import type { VisionFile, VisionSettings } from '@/types/app'
 import { FileUploaderInAttachmentWrapper } from '@/app/components/base/file-uploader'
 import { getProcessedFiles } from '@/app/components/base/file-uploader/utils'
+import useBreakpoints, { MediaType } from '@/hooks/use-breakpoints'
+import cn from '@/utils/classnames'
 
 export type IRunOnceProps = {
   siteInfo: SiteInfo
@@ -35,6 +38,8 @@ const RunOnce: FC<IRunOnceProps> = ({
   onVisionFilesChange,
 }) => {
   const { t } = useTranslation()
+  const media = useBreakpoints()
+  const isPC = media === MediaType.pc
 
   const onClear = () => {
     const newInputs: Record<string, any> = {}
@@ -61,8 +66,8 @@ const RunOnce: FC<IRunOnceProps> = ({
         <form onSubmit={onSubmit}>
           {promptConfig.prompt_variables.map(item => (
             <div className='w-full mt-4' key={item.key}>
-              <label className='text-gray-900 text-sm font-medium'>{item.name}</label>
-              <div className='mt-2'>
+              <label className='h-6 flex items-center text-text-secondary system-md-semibold'>{item.name}</label>
+              <div className='mt-1'>
                 {item.type === 'select' && (
                   <Select
                     className='w-full'
@@ -70,13 +75,11 @@ const RunOnce: FC<IRunOnceProps> = ({
                     onSelect={(i) => { handleInputsChange({ ...inputsRef.current, [item.key]: i.value }) }}
                     items={(item.options || []).map(i => ({ name: i, value: i }))}
                     allowSearch={false}
-                    bgClassName='bg-gray-50'
                   />
                 )}
                 {item.type === 'string' && (
-                  <input
+                  <Input
                     type="text"
-                    className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
                     placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
                     value={inputs[item.key]}
                     onChange={(e) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
@@ -92,9 +95,8 @@ const RunOnce: FC<IRunOnceProps> = ({
                   />
                 )}
                 {item.type === 'number' && (
-                  <input
+                  <Input
                     type="number"
-                    className="block w-full p-2 text-gray-900 border border-gray-300 rounded-lg bg-gray-50 sm:text-xs focus:ring-blue-500 focus:border-blue-500 "
                     placeholder={`${item.name}${!item.required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
                     value={inputs[item.key]}
                     onChange={(e) => { handleInputsChange({ ...inputsRef.current, [item.key]: e.target.value }) }}
@@ -124,8 +126,8 @@ const RunOnce: FC<IRunOnceProps> = ({
           {
             visionConfig?.enabled && (
               <div className="w-full mt-4">
-                <div className="text-gray-900 text-sm font-medium">{t('common.imageUploader.imageUpload')}</div>
-                <div className='mt-2'>
+                <div className="h-6 flex items-center text-text-secondary system-md-semibold">{t('common.imageUploader.imageUpload')}</div>
+                <div className='mt-1'>
                   <TextGenerationImageUploader
                     settings={visionConfig}
                     onFilesChange={files => onVisionFilesChange(files.filter(file => file.progress !== -1).map(fileItem => ({
@@ -139,11 +141,8 @@ const RunOnce: FC<IRunOnceProps> = ({
               </div>
             )
           }
-          {promptConfig.prompt_variables.length > 0 && (
-            <div className='mt-4 h-[1px] bg-gray-100'></div>
-          )}
-          <div className='w-full mt-4'>
-            <div className="flex items-center justify-between">
+          <div className='w-full mt-6 mb-3'>
+            <div className="flex items-center justify-between gap-2">
               <Button
                 onClick={onClear}
                 disabled={false}
@@ -151,11 +150,12 @@ const RunOnce: FC<IRunOnceProps> = ({
                 <span className='text-[13px]'>{t('common.operation.clear')}</span>
               </Button>
               <Button
+                className={cn(!isPC && 'grow')}
                 type='submit'
                 variant="primary"
                 disabled={false}
               >
-                <PlayIcon className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
+                <RiPlayLargeLine className="shrink-0 w-4 h-4 mr-1" aria-hidden="true" />
                 <span className='text-[13px]'>{t('share.generation.run')}</span>
               </Button>
             </div>

+ 0 - 12
web/app/components/share/text-generation/style.module.css

@@ -1,12 +0,0 @@
-.installedApp {
-  height: 100%;
-  border-radius: 16px;
-  box-shadow: 0px 12px 16px -4px rgba(16, 24, 40, 0.08), 0px 4px 6px -2px rgba(16, 24, 40, 0.03);
-}
-
-.starIcon {
-  width: 16px;
-  height: 16px;
-  background: url(./icons/star.svg) center center no-repeat;
-  background-size: contain;
-}

+ 4 - 4
web/app/components/workflow/panel/workflow-preview.tsx

@@ -19,11 +19,11 @@ import { useStore } from '../store'
 import {
   WorkflowRunningStatus,
 } from '../types'
-import { SimpleBtn } from '../../app/text-generate/item'
 import Toast from '../../base/toast'
 import InputsPanel from './inputs-panel'
 import cn from '@/utils/classnames'
 import Loading from '@/app/components/base/loading'
+import Button from '@/app/components/base/button'
 
 const WorkflowPreview = () => {
   const { t } = useTranslation()
@@ -122,8 +122,8 @@ const WorkflowPreview = () => {
                 onClick={() => switchTab('DETAIL')}
               />
               {(workflowRunningData?.result.status === WorkflowRunningStatus.Succeeded && workflowRunningData?.resultText && typeof workflowRunningData?.resultText === 'string') && (
-                <SimpleBtn
-                  className={cn('ml-4 mb-4 inline-flex space-x-1')}
+                <Button
+                  className={cn('ml-4 mb-4 space-x-1')}
                   onClick={() => {
                     const content = workflowRunningData?.resultText
                     if (typeof content === 'string')
@@ -134,7 +134,7 @@ const WorkflowPreview = () => {
                   }}>
                   <RiClipboardLine className='w-3.5 h-3.5' />
                   <div>{t('common.operation.copy')}</div>
-                </SimpleBtn>
+                </Button>
               )}
             </>
           )}

+ 1 - 1
web/app/components/workflow/run/node.tsx

@@ -89,7 +89,7 @@ const NodePanel: FC<Props> = ({
         <div
           className={cn(
             'flex items-center pl-1 pr-3 cursor-pointer',
-            hideInfo ? 'py-2' : 'py-1.5',
+            hideInfo ? 'py-2 pl-2' : 'py-1.5',
             !collapseState && (hideInfo ? '!pb-1' : '!pb-1.5'),
           )}
           onClick={() => setCollapseState(!collapseState)}

+ 1 - 1
web/app/components/workflow/run/tracing-panel.tsx

@@ -170,7 +170,7 @@ const TracingPanel: FC<TracingPanelProps> = ({
 
   return (
     <div
-      className={cn(className || 'bg-components-panel-bg', 'py-2')}
+      className={cn('py-2', className)}
       onClick={(e) => {
         e.stopPropagation()
         e.nativeEvent.stopImmediatePropagation()

+ 109 - 118
web/app/styles/markdown.scss

@@ -1,77 +1,22 @@
-@mixin light {
-  color-scheme: light;
-  --color-prettylights-syntax-comment: #6e7781;
-  --color-prettylights-syntax-constant: #0550ae;
-  --color-prettylights-syntax-entity: #8250df;
-  --color-prettylights-syntax-storage-modifier-import: #24292f;
-  --color-prettylights-syntax-entity-tag: #116329;
-  --color-prettylights-syntax-keyword: #cf222e;
-  --color-prettylights-syntax-string: #0a3069;
-  --color-prettylights-syntax-variable: #953800;
-  --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
-  --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
-  --color-prettylights-syntax-invalid-illegal-bg: #82071e;
-  --color-prettylights-syntax-carriage-return-text: #f6f8fa;
-  --color-prettylights-syntax-carriage-return-bg: #cf222e;
-  --color-prettylights-syntax-string-regexp: #116329;
-  --color-prettylights-syntax-markup-list: #3b2300;
-  --color-prettylights-syntax-markup-heading: #0550ae;
-  --color-prettylights-syntax-markup-italic: #24292f;
-  --color-prettylights-syntax-markup-bold: #24292f;
-  --color-prettylights-syntax-markup-deleted-text: #82071e;
-  --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
-  --color-prettylights-syntax-markup-inserted-text: #116329;
-  --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
-  --color-prettylights-syntax-markup-changed-text: #953800;
-  --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
-  --color-prettylights-syntax-markup-ignored-text: #eaeef2;
-  --color-prettylights-syntax-markup-ignored-bg: #0550ae;
-  --color-prettylights-syntax-meta-diff-range: #8250df;
-  --color-prettylights-syntax-brackethighlighter-angle: #57606a;
-  --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
-  --color-prettylights-syntax-constant-other-reference-link: #0a3069;
-  --color-fg-default: #24292f;
-  --color-fg-muted: #57606a;
-  --color-fg-subtle: #6e7781;
-  --color-canvas-default: transparent;
-  --color-canvas-subtle: #f6f8fa;
-  --color-border-default: #d0d7de;
-  --color-border-muted: hsla(210, 18%, 87%, 1);
-  --color-neutral-muted: rgba(175, 184, 193, 0.2);
-  --color-accent-fg: #0969da;
-  --color-accent-emphasis: #0969da;
-  --color-attention-subtle: #fff8c5;
-  --color-danger-fg: #cf222e;
-}
+@import '../../themes/light';
+@import '../../themes/dark';
+@import '../../themes/markdown-light';
+@import '../../themes/markdown-dark';
 
 .markdown-body {
   -ms-text-size-adjust: 100%;
   -webkit-text-size-adjust: 100%;
-  margin: 4px 0 0 0;
-  color: #101828;
+  margin: 0;
+  color: var(--color-text-primary);
   background-color: var(--color-canvas-default);
-  font-size: 14px;
+  font-size: 15px;
   font-weight: 400;
-  line-height: 1.5;
+  line-height: 1.6;
   word-wrap: break-word;
   word-break: break-word;
   user-select: text;
 }
 
-.light {
-  @include light;
-}
-
-:root {
-  @include light;
-}
-
-@media (prefers-color-scheme: light) {
-  :root {
-    @include light;
-  }
-}
-
 .markdown-body .octicon {
   display: inline-block;
   fill: currentColor;
@@ -109,18 +54,44 @@
 
 .markdown-body a {
   background-color: transparent;
-  color: #155EEF;
+  color: var(--color-text-accent);
   text-decoration: none;
+  text-decoration-color: var(--color-text-accent);
+}
+
+.markdown-body a:hover {
+  position: relative;
+  color: var(--color-text-accent-secondary);
+  text-decoration-color: var(--color-text-accent-secondary);
+  text-decoration: underline;
 }
 
 .markdown-body abbr[title] {
+  position: relative;
   border-bottom: none;
   text-decoration: underline dotted;
+  text-decoration-color: var(--color-text-accent);
+}
+
+.markdown-body abbr[title]:hover::after {
+  @apply shadow-xl shadow-shadow-shadow-5 rounded-md;
+  position: absolute;
+  bottom: 100%;
+  left: 0;
+  display: block;
+  width: max-content;
+  content: attr(title);
+  padding: 6px;
+  font-size: 12px;
+  line-height: 1;
+  color: var(--color-text-secondary);
+  border: 0.5px solid var(--color-components-panel-border);
+  background-color: var(--color-components-tooltip-bg);
 }
 
 .markdown-body b,
 .markdown-body strong {
-  font-weight: var(--base-text-weight-semibold, 600);
+  font-weight: var(--base-text-weight-bold, 700);
 }
 
 .markdown-body dfn {
@@ -152,10 +123,15 @@
   top: -0.5em;
 }
 
+.markdown-body figure {
+  margin: 1em 40px;
+}
+
 .markdown-body img {
-  border-style: none;
   max-width: 100%;
   box-sizing: content-box;
+  border: 2px solid var(--color-effects-image-frame);
+  border-radius: 0;
   background-color: var(--color-canvas-default);
 }
 
@@ -167,20 +143,19 @@
   font-size: 1em;
 }
 
-.markdown-body figure {
-  margin: 1em 40px;
-}
-
 .markdown-body hr {
-  box-sizing: content-box;
-  overflow: hidden;
-  background: transparent;
-  border-bottom: 1px solid var(--color-border-muted);
-  height: 0.25em;
-  padding: 0;
   margin: 24px 0;
-  background-color: var(--color-border-default);
-  border: 0;
+}
+
+.markdown-body hr::before {
+  display: table;
+  content: "";
+}
+
+.markdown-body hr::after {
+  display: table;
+  clear: both;
+  content: "";
 }
 
 .markdown-body input {
@@ -197,13 +172,11 @@
 .markdown-body [type="submit"] {
   -webkit-appearance: button;
 }
-
 .markdown-body [type="checkbox"],
 .markdown-body [type="radio"] {
   box-sizing: border-box;
   padding: 0;
 }
-
 .markdown-body [type="number"]::-webkit-inner-spin-button,
 .markdown-body [type="number"]::-webkit-outer-spin-button {
   height: auto;
@@ -233,24 +206,16 @@
   opacity: 1;
 }
 
-.markdown-body hr::before {
-  display: table;
-  content: "";
-}
-
-.markdown-body hr::after {
-  display: table;
-  clear: both;
-  content: "";
-}
 
 .markdown-body table {
   border-spacing: 0;
-  border-collapse: collapse;
+  border-collapse: separate;
   display: block;
   width: max-content;
   max-width: 100%;
-  overflow: auto;
+  overflow: hidden;
+  border: 1px solid var(--color-divider-regular);
+  border-radius: 8px;
 }
 
 .markdown-body td,
@@ -302,17 +267,14 @@
 
 .markdown-body kbd {
   display: inline-block;
-  padding: 3px 5px;
+  padding: 2px 6px;
   font: 11px ui-monospace, SFMono-Regular, SF Mono, Menlo, Consolas,
     Liberation Mono, monospace;
-  line-height: 10px;
-  color: var(--color-fg-default);
+  line-height: 1;
+  color: var(--color-text-primary);
   vertical-align: middle;
-  background-color: var(--color-canvas-subtle);
-  border: solid 1px var(--color-neutral-muted);
-  border-bottom-color: var(--color-neutral-muted);
+  background-color: var(--color-components-input-bg-normal);
   border-radius: 6px;
-  box-shadow: inset 0 -1px 0 var(--color-neutral-muted);
 }
 
 .markdown-body h1,
@@ -327,17 +289,25 @@
   line-height: 1.25;
 }
 
-.markdown-body blockquote {
-  margin: 0;
-  padding: 0 8px;
-  border-left: 2px solid #2970FF;
+.markdown-body h1 {
+  font-size: 18px;
 }
 
-.markdown-body ul,
-.markdown-body ol {
-  margin-top: 0;
-  margin-bottom: 0;
-  padding-left: 2em;
+.markdown-body h2 {
+  font-size: 16px;
+}
+
+.markdown-body h3,
+.markdown-body h4,
+.markdown-body h5,
+.markdown-body h6 {
+  font-size: 14px;
+}
+
+.markdown-body blockquote {
+  margin: 0;
+  padding: 0 12px;
+  border-left: 3px solid var(--color-text-accent-secondary);
 }
 
 .markdown-body ol {
@@ -348,6 +318,11 @@
   list-style: disc;
 }
 
+.markdown-body>ol,
+.markdown-body>ul {
+  padding: 0;
+}
+
 .markdown-body ol ol,
 .markdown-body ul ol {
   list-style-type: lower-roman;
@@ -446,6 +421,11 @@
   margin-bottom: 12px;
 }
 
+.markdown-body ul,
+.markdown-body ol {
+  padding-left: 2em;
+}
+
 .markdown-body blockquote> :first-child {
   margin-top: 0;
 }
@@ -587,23 +567,35 @@
 }
 
 .markdown-body table th {
-  font-weight: var(--base-text-weight-semibold, 600);
+  color: var(--color-text-tertiary);
+  font-size: 12px;
+  font-weight: var(--base-text-weight-medium, 500);
+  white-space: nowrap;
+}
+
+.markdown-body table td {
+  color: var(--color-text-secondary);
+  font-size: 13px;
+  font-weight: var(--base-text-weight-normal, 400);
   white-space: nowrap;
 }
 
 .markdown-body table th,
 .markdown-body table td {
   padding: 6px 13px;
-  border: 1px solid var(--color-border-default);
 }
 
-.markdown-body table tr {
-  background-color: var(--color-canvas-default);
-  border-top: 1px solid var(--color-border-muted);
+.markdown-body table tr>th:not(:last-child),
+.markdown-body table tr>td:not(:last-child) {
+  border-right: 1px solid var(--color-divider-subtle);
+}
+
+.markdown-body table tbody tr:first-child td {
+  border-top: 1px solid var(--color-divider-regular);
 }
 
-.markdown-body table tr:nth-child(2n) {
-  background-color: var(--color-canvas-subtle);
+.markdown-body table tbody tr:not(:last-child) td {
+  border-bottom: 1px solid var(--color-divider-subtle);
 }
 
 .markdown-body table img {
@@ -761,11 +753,10 @@
 .markdown-body .highlight pre,
 .markdown-body pre {
   padding: 16px;
-  background: #fff;
+  background-color: transparent;
   overflow: auto;
   font-size: 85%;
   line-height: 1.45;
-  border-radius: 6px;
 }
 
 .markdown-body pre {
@@ -1043,5 +1034,5 @@
 }
 
 .markdown-body .react-syntax-highlighter-line-number {
-  color: #D0D5DD;
+  color: var(--color-text-quaternary);
 }

+ 47 - 0
web/context/share-page-context.tsx

@@ -0,0 +1,47 @@
+'use client'
+import { useCallback, useEffect, useState } from 'react'
+import { createContext, useContextSelector } from 'use-context-selector'
+import type { FC, ReactNode } from 'react'
+import { Theme } from '@/types/app'
+
+export type SharePageContextValue = {
+  theme: Theme
+  setTheme: (theme: Theme) => void
+}
+
+const SharePageContext = createContext<SharePageContextValue>({
+  theme: Theme.light,
+  setTheme: () => { },
+})
+
+export function useSelector<T>(selector: (value: SharePageContextValue) => T): T {
+  return useContextSelector(SharePageContext, selector)
+}
+
+export type SharePageContextProviderProps = {
+  children: ReactNode
+}
+
+export const SharePageContextProvider: FC<SharePageContextProviderProps> = ({ children }) => {
+  const [theme, setTheme] = useState<Theme>(Theme.light)
+  const handleSetTheme = useCallback((theme: Theme) => {
+    setTheme(theme)
+    globalThis.document.documentElement.setAttribute('data-theme', theme)
+  }, [])
+
+  useEffect(() => {
+    globalThis.document.documentElement.setAttribute('data-theme', theme)
+    // eslint-disable-next-line react-hooks/exhaustive-deps
+  }, [])
+
+  return (
+    <SharePageContext.Provider value={{
+      theme,
+      setTheme: handleSetTheme,
+    }}>
+      {children}
+    </SharePageContext.Provider>
+  )
+}
+
+export default SharePageContextProvider

+ 7 - 2
web/i18n/en-US/share-app.ts

@@ -5,11 +5,14 @@ const translation = {
     appUnknownError: 'App is unavailable',
   },
   chat: {
-    newChat: 'New chat',
+    newChat: 'Start New chat',
+    chatSettingsTitle: 'New chat setup',
+    chatFormTip: 'Chat settings cannot be modified after the chat has started.',
     pinnedTitle: 'Pinned',
-    unpinnedTitle: 'Chats',
+    unpinnedTitle: 'Recent',
     newChatDefaultName: 'New conversation',
     resetChat: 'Reset conversation',
+    viewChatSettings: 'View chat settings',
     poweredBy: 'Powered by',
     prompt: 'Prompt',
     privatePromptConfigTitle: 'Conversation settings',
@@ -47,6 +50,8 @@ const translation = {
     completionResult: 'Completion result',
     queryPlaceholder: 'Write your query content...',
     run: 'Execute',
+    execution: 'EXECUTION',
+    executions: '{{num}} EXECUTIONS',
     copy: 'Copy',
     resultTitle: 'AI Completion',
     noData: 'AI will give you what you want here.',

+ 6 - 1
web/i18n/zh-Hans/share-app.ts

@@ -5,11 +5,14 @@ const translation = {
     appUnknownError: '应用不可用',
   },
   chat: {
-    newChat: '新对话',
+    newChat: '开启新对话',
+    chatSettingsTitle: '新对话设置',
+    chatFormTip: '对话开始后,对话设置将无法修改。',
     pinnedTitle: '已置顶',
     unpinnedTitle: '对话列表',
     newChatDefaultName: '新的对话',
     resetChat: '重置对话',
+    viewChatSettings: '查看对话设置',
     poweredBy: 'Powered by',
     prompt: '提示词',
     privatePromptConfigTitle: '对话设置',
@@ -43,6 +46,8 @@ const translation = {
     completionResult: '生成结果',
     queryPlaceholder: '请输入文本内容',
     run: '运行',
+    execution: '运行',
+    executions: '{{num}} 次运行',
     copy: '拷贝',
     resultTitle: 'AI 书写',
     noData: 'AI 会在这里给你惊喜。',

+ 21 - 11
web/public/embed.js

@@ -69,11 +69,21 @@
       iframe.id = iframeId;
       iframe.src = iframeUrl;
       iframe.style.cssText = `
-        border: none; position: absolute; flex-direction: column; justify-content: space-between;
-        box-shadow: rgba(150, 150, 150, 0.2) 0px 10px 30px 0px, rgba(150, 150, 150, 0.2) 0px 0px 0px 1px;
-        bottom: 55px; right: 0; width: 24rem; max-width: calc(100vw - 2rem); height: 40rem;
-        max-height: calc(100vh - 6rem); border-radius: 0.75rem; display: flex; z-index: 2147483647;
-        overflow: hidden; left: unset; background-color: #F3F4F6;user-select: none;
+        position: absolute;
+        display: flex;
+        flex-direction: column;
+        justify-content: space-between;
+        left: unset;
+        right: 0;
+        bottom: 0;
+        width: 24rem;
+        max-width: calc(100vw - 2rem);
+        height: 43.75rem;
+        max-height: calc(100vh - 6rem);
+        border: none;
+        z-index: 2147483640;
+        overflow: hidden;
+        user-select: none;
       `;
 
       return iframe;
@@ -92,12 +102,12 @@
         const buttonInBottom = buttonRect.top - 5 > targetIframe.clientHeight
 
         if (buttonInBottom) {
-          targetIframe.style.bottom = `${buttonRect.height + 5}px`;
+          targetIframe.style.bottom = '0px';
           targetIframe.style.top = 'unset';
         }
         else {
           targetIframe.style.bottom = 'unset';
-          targetIframe.style.top = `${buttonRect.height + 5}px`;
+          targetIframe.style.top = '0px';
         }
 
         const buttonInRight = buttonRect.right > targetIframe.clientWidth;
@@ -148,8 +158,8 @@
           right: var(--${containerDiv.id}-right, 1rem);
           left: var(--${containerDiv.id}-left, unset);
           top: var(--${containerDiv.id}-top, unset);
-          width: var(--${containerDiv.id}-width, 50px);
-          height: var(--${containerDiv.id}-height, 50px);
+          width: var(--${containerDiv.id}-width, 48px);
+          height: var(--${containerDiv.id}-height, 48px);
           border-radius: var(--${containerDiv.id}-border-radius, 25px);
           background-color: var(--${containerDiv.id}-bg-color, #155EEF);
           box-shadow: var(--${containerDiv.id}-box-shadow, rgba(0, 0, 0, 0.2) 0px 4px 8px 0px);
@@ -161,7 +171,7 @@
       // Create display div for the button icon
       const displayDiv = document.createElement("div");
       displayDiv.style.cssText =
-        "display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;";
+        "position: relative; display: flex; align-items: center; justify-content: center; width: 100%; height: 100%; z-index: 2147483647;";
       displayDiv.innerHTML = svgIcons.open;
       containerDiv.appendChild(displayDiv);
       document.body.appendChild(containerDiv);
@@ -170,7 +180,7 @@
       containerDiv.addEventListener("click", function () {
         const targetIframe = document.getElementById(iframeId);
         if (!targetIframe) {
-          containerDiv.appendChild(createIframe());
+          containerDiv.prepend(createIframe());
           resetIframePosition();
           this.title = "Exit (ESC)";
           displayDiv.innerHTML = svgIcons.close;

+ 2 - 0
web/tailwind-common-config.ts

@@ -94,6 +94,8 @@ const config = {
         'chat-bubble-bg': 'var(--color-chat-bubble-bg)',
         'chat-input-mask': 'var(--color-chat-input-mask)',
         'workflow-process-bg': 'var(--color-workflow-process-bg)',
+        'workflow-run-failed-bg': 'var(--color-workflow-run-failed-bg)',
+        'workflow-batch-failed-bg': 'var(--color-workflow-batch-failed-bg)',
         'mask-top2bottom-gray-50-to-transparent': 'var(--mask-top2bottom-gray-50-to-transparent)',
         'marketplace-divider-bg': 'var(--color-marketplace-divider-bg)',
         'marketplace-plugin-empty': 'var(--color-marketplace-plugin-empty)',

+ 6 - 0
web/themes/manual-dark.css

@@ -11,6 +11,12 @@ html[data-theme="dark"] {
   --color-workflow-process-bg: linear-gradient(90deg,
       rgba(24, 24, 27, 0.25) 0%,
       rgba(24, 24, 27, 0.04) 100%);
+  --color-workflow-run-failed-bg: linear-gradient(98deg,
+      rgba(240, 68, 56, 0.12) 0%,
+      rgba(0, 0, 0, 0) 26.01%);
+  --color-workflow-batch-failed-bg: linear-gradient(92deg,
+      rgba(240, 68, 56, 0.3) 0%,
+      rgba(0, 0, 0, 0) 100%);
   --color-marketplace-divider-bg: linear-gradient(90deg,
       rgba(200, 206, 218, 0.14) 0%,
       rgba(0, 0, 0, 0) 100%);

+ 6 - 0
web/themes/manual-light.css

@@ -11,6 +11,12 @@ html[data-theme="light"] {
   --color-workflow-process-bg: linear-gradient(90deg,
       rgba(200, 206, 218, 0.2) 0%,
       rgba(200, 206, 218, 0.04) 100%);
+  --color-workflow-run-failed-bg: linear-gradient(98deg,
+      rgba(240, 68, 56, 0.10) 0%,
+      rgba(255, 255, 255, 0) 26.01%);
+  --color-workflow-batch-failed-bg: linear-gradient(92deg,
+      rgba(240, 68, 56, 0.25) 0%,
+      rgba(255, 255, 255, 0) 100%);
   --color-marketplace-divider-bg: linear-gradient(90deg,
       rgba(16, 24, 40, 0.08) 0%,
       rgba(255, 255, 255, 0) 100%);

+ 44 - 0
web/themes/markdown-dark.css

@@ -0,0 +1,44 @@
+html[data-theme="dark"] {
+    --color-prettylights-syntax-comment: #6e7781;
+    --color-prettylights-syntax-constant: #0550ae;
+    --color-prettylights-syntax-entity: #8250df;
+    --color-prettylights-syntax-storage-modifier-import: #24292f;
+    --color-prettylights-syntax-entity-tag: #116329;
+    --color-prettylights-syntax-keyword: #cf222e;
+    --color-prettylights-syntax-string: #0a3069;
+    --color-prettylights-syntax-variable: #953800;
+    --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
+    --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
+    --color-prettylights-syntax-invalid-illegal-bg: #82071e;
+    --color-prettylights-syntax-carriage-return-text: #f6f8fa;
+    --color-prettylights-syntax-carriage-return-bg: #cf222e;
+    --color-prettylights-syntax-string-regexp: #116329;
+    --color-prettylights-syntax-markup-list: #3b2300;
+    --color-prettylights-syntax-markup-heading: #0550ae;
+    --color-prettylights-syntax-markup-italic: #24292f;
+    --color-prettylights-syntax-markup-bold: #24292f;
+    --color-prettylights-syntax-markup-deleted-text: #82071e;
+    --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
+    --color-prettylights-syntax-markup-inserted-text: #116329;
+    --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
+    --color-prettylights-syntax-markup-changed-text: #953800;
+    --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
+    --color-prettylights-syntax-markup-ignored-text: #eaeef2;
+    --color-prettylights-syntax-markup-ignored-bg: #0550ae;
+    --color-prettylights-syntax-meta-diff-range: #8250df;
+    --color-prettylights-syntax-brackethighlighter-angle: #57606a;
+    --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
+    --color-prettylights-syntax-constant-other-reference-link: #0a3069;
+    --color-fg-default: #24292f;
+    --color-fg-muted: #57606a;
+    --color-fg-subtle: #6e7781;
+    --color-canvas-default: transparent;
+    --color-canvas-subtle: #f6f8fa;
+    --color-border-default: #d0d7de;
+    --color-border-muted: hsla(210, 18%, 87%, 1);
+    --color-neutral-muted: rgba(175, 184, 193, 0.2);
+    --color-accent-fg: #0969da;
+    --color-accent-emphasis: #0969da;
+    --color-attention-subtle: #fff8c5;
+    --color-danger-fg: #cf222e;
+  }

+ 44 - 0
web/themes/markdown-light.css

@@ -0,0 +1,44 @@
+html[data-theme="light"] {
+    --color-prettylights-syntax-comment: #6e7781;
+    --color-prettylights-syntax-constant: #0550ae;
+    --color-prettylights-syntax-entity: #8250df;
+    --color-prettylights-syntax-storage-modifier-import: #24292f;
+    --color-prettylights-syntax-entity-tag: #116329;
+    --color-prettylights-syntax-keyword: #cf222e;
+    --color-prettylights-syntax-string: #0a3069;
+    --color-prettylights-syntax-variable: #953800;
+    --color-prettylights-syntax-brackethighlighter-unmatched: #82071e;
+    --color-prettylights-syntax-invalid-illegal-text: #f6f8fa;
+    --color-prettylights-syntax-invalid-illegal-bg: #82071e;
+    --color-prettylights-syntax-carriage-return-text: #f6f8fa;
+    --color-prettylights-syntax-carriage-return-bg: #cf222e;
+    --color-prettylights-syntax-string-regexp: #116329;
+    --color-prettylights-syntax-markup-list: #3b2300;
+    --color-prettylights-syntax-markup-heading: #0550ae;
+    --color-prettylights-syntax-markup-italic: #24292f;
+    --color-prettylights-syntax-markup-bold: #24292f;
+    --color-prettylights-syntax-markup-deleted-text: #82071e;
+    --color-prettylights-syntax-markup-deleted-bg: #ffebe9;
+    --color-prettylights-syntax-markup-inserted-text: #116329;
+    --color-prettylights-syntax-markup-inserted-bg: #dafbe1;
+    --color-prettylights-syntax-markup-changed-text: #953800;
+    --color-prettylights-syntax-markup-changed-bg: #ffd8b5;
+    --color-prettylights-syntax-markup-ignored-text: #eaeef2;
+    --color-prettylights-syntax-markup-ignored-bg: #0550ae;
+    --color-prettylights-syntax-meta-diff-range: #8250df;
+    --color-prettylights-syntax-brackethighlighter-angle: #57606a;
+    --color-prettylights-syntax-sublimelinter-gutter-mark: #8c959f;
+    --color-prettylights-syntax-constant-other-reference-link: #0a3069;
+    --color-fg-default: #24292f;
+    --color-fg-muted: #57606a;
+    --color-fg-subtle: #6e7781;
+    --color-canvas-default: transparent;
+    --color-canvas-subtle: #f6f8fa;
+    --color-border-default: #d0d7de;
+    --color-border-muted: hsla(210, 18%, 87%, 1);
+    --color-neutral-muted: rgba(175, 184, 193, 0.2);
+    --color-accent-fg: #0969da;
+    --color-accent-emphasis: #0969da;
+    --color-attention-subtle: #fff8c5;
+    --color-danger-fg: #cf222e;
+  }