소스 검색

chore: replace chat in web app (#2373)

zxhlyh 1 년 전
부모
커밋
51d359268e
49개의 변경된 파일2100개의 추가작업 그리고 92개의 파일을 삭제
  1. 4 2
      web/app/(shareLayout)/chat/[token]/page.tsx
  2. 141 0
      web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
  3. 82 0
      web/app/components/base/chat/chat-with-history/config-panel/form.tsx
  4. 158 0
      web/app/components/base/chat/chat-with-history/config-panel/index.tsx
  5. 74 0
      web/app/components/base/chat/chat-with-history/context.tsx
  6. 60 0
      web/app/components/base/chat/chat-with-history/header-in-mobile.tsx
  7. 25 0
      web/app/components/base/chat/chat-with-history/header.tsx
  8. 385 0
      web/app/components/base/chat/chat-with-history/hooks.tsx
  9. 195 0
      web/app/components/base/chat/chat-with-history/index.tsx
  10. 141 0
      web/app/components/base/chat/chat-with-history/sidebar/index.tsx
  11. 58 0
      web/app/components/base/chat/chat-with-history/sidebar/item.tsx
  12. 46 0
      web/app/components/base/chat/chat-with-history/sidebar/list.tsx
  13. 1 1
      web/app/components/base/chat/chat/answer/index.tsx
  14. 65 0
      web/app/components/base/chat/chat/answer/operation.tsx
  15. 1 1
      web/app/components/base/chat/chat/answer/suggested-questions.tsx
  16. 3 0
      web/app/components/base/chat/chat/context.tsx
  17. 41 29
      web/app/components/base/chat/chat/hooks.ts
  18. 68 42
      web/app/components/base/chat/chat/index.tsx
  19. 1 1
      web/app/components/base/chat/chat/try-to-ask.tsx
  20. 1 0
      web/app/components/base/chat/constants.ts
  21. 9 0
      web/app/components/base/chat/types.ts
  22. 3 0
      web/app/components/base/confirm/common.tsx
  23. 10 0
      web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-down.svg
  24. 10 0
      web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-up.svg
  25. 10 0
      web/app/components/base/icons/assets/vender/line/general/edit-05.svg
  26. 5 0
      web/app/components/base/icons/assets/vender/line/general/menu-01.svg
  27. 5 0
      web/app/components/base/icons/assets/vender/line/general/pin-01.svg
  28. 9 0
      web/app/components/base/icons/assets/vender/solid/shapes/star-06.svg
  29. 66 0
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json
  30. 16 0
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.tsx
  31. 66 0
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json
  32. 16 0
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.tsx
  33. 2 0
      web/app/components/base/icons/src/vender/line/alertsAndFeedback/index.ts
  34. 66 0
      web/app/components/base/icons/src/vender/line/general/Edit05.json
  35. 16 0
      web/app/components/base/icons/src/vender/line/general/Edit05.tsx
  36. 39 0
      web/app/components/base/icons/src/vender/line/general/Menu01.json
  37. 16 0
      web/app/components/base/icons/src/vender/line/general/Menu01.tsx
  38. 39 0
      web/app/components/base/icons/src/vender/line/general/Pin01.json
  39. 16 0
      web/app/components/base/icons/src/vender/line/general/Pin01.tsx
  40. 3 0
      web/app/components/base/icons/src/vender/line/general/index.ts
  41. 62 0
      web/app/components/base/icons/src/vender/solid/shapes/Star06.json
  42. 16 0
      web/app/components/base/icons/src/vender/solid/shapes/Star06.tsx
  43. 1 0
      web/app/components/base/icons/src/vender/solid/shapes/index.ts
  44. 1 1
      web/app/components/explore/index.tsx
  45. 2 2
      web/app/components/explore/installed-app/index.tsx
  46. 1 1
      web/app/components/share/chat/index.tsx
  47. 20 5
      web/models/share.ts
  48. 14 7
      web/service/share.ts
  49. 11 0
      web/types/app.ts

+ 4 - 2
web/app/(shareLayout)/chat/[token]/page.tsx

@@ -1,12 +1,14 @@
+'use client'
+
 import type { FC } from 'react'
 import React from 'react'
 
 import type { IMainProps } from '@/app/components/share/chat'
-import Main from '@/app/components/share/chat'
+import ChatWithHistoryWrap from '@/app/components/base/chat/chat-with-history'
 
 const Chat: FC<IMainProps> = () => {
   return (
-    <Main />
+    <ChatWithHistoryWrap />
   )
 }
 

+ 141 - 0
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx

@@ -0,0 +1,141 @@
+import { useCallback, useEffect, useMemo } from 'react'
+import Chat from '../chat'
+import type {
+  ChatConfig,
+  OnSend,
+} from '../types'
+import { useChat } from '../chat/hooks'
+import { useChatWithHistoryContext } from './context'
+import Header from './header'
+import ConfigPanel from './config-panel'
+import {
+  fetchSuggestedQuestions,
+  getUrl,
+} from '@/service/share'
+
+const ChatWrapper = () => {
+  const {
+    appParams,
+    appPrevChatList,
+    currentConversationId,
+    currentConversationItem,
+    inputsForms,
+    newConversationInputs,
+    handleNewConversationCompleted,
+    isMobile,
+    isInstalledApp,
+    appId,
+    appMeta,
+    handleFeedback,
+    currentChatInstanceRef,
+  } = useChatWithHistoryContext()
+  const appConfig = useMemo(() => {
+    const config = appParams || {}
+
+    return {
+      ...config,
+      supportFeedback: true,
+    } as ChatConfig
+  }, [appParams])
+  const {
+    chatList,
+    handleSend,
+    handleStop,
+    isResponsing,
+    suggestedQuestions,
+  } = useChat(
+    appConfig,
+    undefined,
+    appPrevChatList,
+  )
+
+  useEffect(() => {
+    if (currentChatInstanceRef.current)
+      currentChatInstanceRef.current.handleStop = handleStop
+  }, [])
+
+  const doSend: OnSend = useCallback((message, files) => {
+    const data: any = {
+      query: message,
+      inputs: currentConversationId ? currentConversationItem?.inputs : newConversationInputs,
+      conversation_id: currentConversationId,
+    }
+
+    if (appConfig?.file_upload?.image.enabled && files?.length)
+      data.files = files
+
+    handleSend(
+      getUrl('chat-messages', isInstalledApp, appId || ''),
+      data,
+      {
+        onGetSuggestedQuestions: responseItemId => fetchSuggestedQuestions(responseItemId, isInstalledApp, appId),
+        onConversationComplete: currentConversationId ? undefined : handleNewConversationCompleted,
+        isPublicAPI: !isInstalledApp,
+      },
+    )
+  }, [
+    appConfig,
+    currentConversationId,
+    currentConversationItem,
+    handleSend,
+    newConversationInputs,
+    handleNewConversationCompleted,
+    isInstalledApp,
+    appId,
+  ])
+  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>
+            )
+          }
+        </>
+      )
+    }
+
+    return (
+      <Header
+        isMobile={isMobile}
+        title={currentConversationItem?.name || ''}
+      />
+    )
+  }, [
+    currentConversationId,
+    inputsForms,
+    currentConversationItem,
+    isMobile,
+  ])
+
+  return (
+    <Chat
+      config={appConfig}
+      chatList={chatList}
+      isResponsing={isResponsing}
+      chatContainerInnerClassName={`mx-auto pt-6 w-full max-w-[720px] ${isMobile && 'px-4'}`}
+      chatFooterClassName='pb-4'
+      chatFooterInnerClassName={`mx-auto w-full max-w-[720px] ${isMobile && 'px-4'}`}
+      onSend={doSend}
+      onStopResponding={handleStop}
+      chatNode={chatNode}
+      allToolIcons={appMeta?.tool_icons || {}}
+      onFeedback={handleFeedback}
+      suggestedQuestions={suggestedQuestions}
+    />
+  )
+}
+
+export default ChatWrapper

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

@@ -0,0 +1,82 @@
+import { useTranslation } from 'react-i18next'
+import { useChatWithHistoryContext } from '../context'
+import { PortalSelect } from '@/app/components/base/select'
+
+const Form = () => {
+  const { t } = useTranslation()
+  const {
+    inputsForms,
+    newConversationInputs,
+    handleNewConversationInputsChange,
+    isMobile,
+  } = useChatWithHistoryContext()
+
+  const handleFormChange = (variable: string, value: string) => {
+    handleNewConversationInputsChange({
+      ...newConversationInputs,
+      [variable]: value,
+    })
+  }
+
+  const renderField = (form: any) => {
+    const {
+      label,
+      required,
+      max_length,
+      variable,
+      options,
+    } = form
+
+    if (form.type === 'text-input') {
+      return (
+        <input
+          className='grow h-9 rounded-lg bg-gray-100 px-2.5 outline-none appearance-none'
+          value={newConversationInputs[variable] || ''}
+          maxLength={max_length}
+          onChange={e => handleFormChange(variable, e.target.value)}
+          placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+        />
+      )
+    }
+    if (form.type === 'paragraph') {
+      return (
+        <textarea
+          value={newConversationInputs[variable]}
+          className='grow h-[104px] rounded-lg bg-gray-100 px-2.5 py-2 outline-none appearance-none resize-none'
+          onChange={e => handleFormChange(variable, e.target.value)}
+          placeholder={`${label}${!required ? `(${t('appDebug.variableTable.optional')})` : ''}`}
+        />
+      )
+    }
+    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

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

@@ -0,0 +1,158 @@
+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 { 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 { FootLogo } from '@/app/components/share/chat/welcome/massive-component'
+
+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 text-2xl font-semibold text-gray-800'>
+                  {appData?.site.icon} {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
+                  className='shrink-0 px-2 py-0 h-6 bg-white text-xs font-medium text-primary-600 rounded-md'
+                  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
+                  type='primary'
+                  className='mr-2 text-sm font-medium'
+                  onClick={handleStartChat}
+                >
+                  {t('common.operation.save')}
+                </Button>
+                <Button
+                  className='text-sm font-medium'
+                  onClick={() => setCollapsed(true)}
+                >
+                  {t('common.operation.cancel')}
+                </Button>
+              </div>
+            </div>
+          )
+        }
+        {
+          showConfigPanelBeforeChat && (
+            <div className='p-6 rounded-b-xl'>
+              <Form />
+              <Button
+                className={`px-4 py-0 h-9 ${inputsForms.length && !isMobile && 'ml-[136px]'}`}
+                type='primary'
+                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'
+                  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'}`}>
+                    <a className='flex items-center pr-3 space-x-3' href="https://dify.ai/" target="_blank">
+                      <span className='uppercase'>{t('share.chat.powerBy')}</span>
+                      {
+                        customConfig?.replace_webapp_logo
+                          ? <img src={customConfig?.replace_webapp_logo} alt='logo' className='block w-auto h-5' />
+                          : <FootLogo />
+                      }
+                    </a>
+                  </div>
+                )
+            }
+          </div>
+        )
+      }
+    </div>
+  )
+}
+
+export default ConfigPanel

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

@@ -0,0 +1,74 @@
+'use client'
+
+import type { RefObject } from 'react'
+import { createContext, useContext } from 'use-context-selector'
+import type {
+  Callback,
+  ChatConfig,
+  ChatItem,
+  Feedback,
+} from '../types'
+import type {
+  AppConversationData,
+  AppData,
+  AppMeta,
+  ConversationItem,
+} from '@/models/share'
+
+export type ChatWithHistoryContextValue = {
+  appInfoLoading?: boolean
+  appMeta?: AppMeta
+  appData?: AppData
+  appParams?: ChatConfig
+  appChatListDataLoading?: boolean
+  currentConversationId: string
+  currentConversationItem?: ConversationItem
+  appPrevChatList: ChatItem[]
+  pinnedConversationList: AppConversationData['data']
+  conversationList: AppConversationData['data']
+  showConfigPanelBeforeChat: boolean
+  newConversationInputs: Record<string, any>
+  handleNewConversationInputsChange: (v: Record<string, any>) => void
+  inputsForms: any[]
+  handleNewConversation: () => void
+  handleStartChat: () => void
+  handleChangeConversation: (conversationId: string) => void
+  handlePinConversation: (conversationId: string) => void
+  handleUnpinConversation: (conversationId: string) => void
+  handleDeleteConversation: (conversationId: string, callback: Callback) => void
+  conversationRenaming: boolean
+  handleRenameConversation: (conversationId: string, newName: string, callback: Callback) => void
+  handleNewConversationCompleted: (newConversationId: string) => void
+  chatShouldReloadKey: string
+  isMobile: boolean
+  isInstalledApp: boolean
+  appId?: string
+  handleFeedback: (messageId: string, feedback: Feedback) => void
+  currentChatInstanceRef: RefObject<{ handleStop: () => void }>
+}
+
+export const ChatWithHistoryContext = createContext<ChatWithHistoryContextValue>({
+  currentConversationId: '',
+  appPrevChatList: [],
+  pinnedConversationList: [],
+  conversationList: [],
+  showConfigPanelBeforeChat: false,
+  newConversationInputs: {},
+  handleNewConversationInputsChange: () => {},
+  inputsForms: [],
+  handleNewConversation: () => {},
+  handleStartChat: () => {},
+  handleChangeConversation: () => {},
+  handlePinConversation: () => {},
+  handleUnpinConversation: () => {},
+  handleDeleteConversation: () => {},
+  conversationRenaming: false,
+  handleRenameConversation: () => {},
+  handleNewConversationCompleted: () => {},
+  chatShouldReloadKey: '',
+  isMobile: false,
+  isInstalledApp: false,
+  handleFeedback: () => {},
+  currentChatInstanceRef: { current: { handleStop: () => {} } },
+})
+export const useChatWithHistoryContext = () => useContext(ChatWithHistoryContext)

+ 60 - 0
web/app/components/base/chat/chat-with-history/header-in-mobile.tsx

@@ -0,0 +1,60 @@
+import { useState } from 'react'
+import { useChatWithHistoryContext } from './context'
+import Sidebar from './sidebar'
+import AppIcon from '@/app/components/base/app-icon'
+import {
+  Edit05,
+  Menu01,
+} from '@/app/components/base/icons/src/vender/line/general'
+
+const HeaderInMobile = () => {
+  const {
+    appData,
+    handleNewConversation,
+  } = useChatWithHistoryContext()
+  const [showSidebar, setShowSidebar] = 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>
+        <div className='grow flex justify-center items-center px-3'>
+          <AppIcon
+            className='mr-2'
+            size='tiny'
+            icon={appData?.site.icon}
+            background={appData?.site.icon_background}
+          />
+          <div className='py-1 text-base font-semibold text-gray-800 truncate'>
+            {appData?.site.title}
+          </div>
+        </div>
+        <div
+          className='shrink-0 flex items-center justify-center w-8 h-8 rounded-lg'
+          onClick={handleNewConversation}
+        >
+          <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>
+          </div>
+        )
+      }
+    </>
+  )
+}
+
+export default HeaderInMobile

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

@@ -0,0 +1,25 @@
+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)

+ 385 - 0
web/app/components/base/chat/chat-with-history/hooks.tsx

@@ -0,0 +1,385 @@
+import {
+  useCallback,
+  useEffect,
+  useMemo,
+  useRef,
+  useState,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import useSWR from 'swr'
+import { useLocalStorageState } from 'ahooks'
+import produce from 'immer'
+import type {
+  Callback,
+  ChatConfig,
+  ChatItem,
+  Feedback,
+} from '../types'
+import { CONVERSATION_ID_INFO } from '../constants'
+import {
+  delConversation,
+  fetchAppInfo,
+  fetchAppMeta,
+  fetchAppParams,
+  fetchChatList,
+  fetchConversations,
+  generationConversationName,
+  pinConversation,
+  renameConversation,
+  unpinConversation,
+  updateFeedback,
+} from '@/service/share'
+import type { InstalledApp } from '@/models/explore'
+import type {
+  AppData,
+  ConversationItem,
+} from '@/models/share'
+import { addFileInfos, sortAgentSorts } from '@/app/components/tools/utils'
+import { useToastContext } from '@/app/components/base/toast'
+
+export const useChatWithHistory = (installedAppInfo?: InstalledApp) => {
+  const isInstalledApp = useMemo(() => !!installedAppInfo, [installedAppInfo])
+  const { data: appInfo, isLoading: appInfoLoading } = useSWR(installedAppInfo ? null : 'appInfo', fetchAppInfo)
+
+  const appData = useMemo(() => {
+    if (isInstalledApp) {
+      const { id, app } = installedAppInfo!
+      return {
+        app_id: id,
+        site: { title: app.name, icon: app.icon, icon_background: app.icon_background, prompt_public: false, copyright: '' },
+        plan: 'basic',
+      } as AppData
+    }
+
+    return appInfo
+  }, [isInstalledApp, installedAppInfo, appInfo])
+  const appId = useMemo(() => appData?.app_id, [appData])
+
+  const [conversationIdInfo, setConversationIdInfo] = useLocalStorageState<Record<string, string>>(CONVERSATION_ID_INFO, {
+    defaultValue: {},
+  })
+  const currentConversationId = useMemo(() => conversationIdInfo?.[appId || ''] || '', [appId, conversationIdInfo])
+  const handleConversationIdInfoChange = useCallback((changeConversationId: string) => {
+    if (appId) {
+      setConversationIdInfo({
+        ...conversationIdInfo,
+        [appId || '']: changeConversationId,
+      })
+    }
+  }, [appId, conversationIdInfo, setConversationIdInfo])
+  const [showConfigPanelBeforeChat, setShowConfigPanelBeforeChat] = useState(true)
+
+  const [newConversationId, setNewConversationId] = useState('')
+  const chatShouldReloadKey = useMemo(() => {
+    if (currentConversationId === newConversationId)
+      return ''
+
+    return currentConversationId
+  }, [currentConversationId, newConversationId])
+
+  const { data: appParams } = useSWR(['appParams', isInstalledApp, appId], () => fetchAppParams(isInstalledApp, appId))
+  const { data: appMeta } = useSWR(['appMeta', isInstalledApp, appId], () => fetchAppMeta(isInstalledApp, appId))
+  const { data: appPinnedConversationData, mutate: mutateAppPinnedConversationData } = useSWR(['appConversationData', isInstalledApp, appId, true], () => fetchConversations(isInstalledApp, appId, undefined, true, 100))
+  const { data: appConversationData, isLoading: appConversationDataLoading, mutate: mutateAppConversationData } = useSWR(['appConversationData', isInstalledApp, appId, false], () => fetchConversations(isInstalledApp, appId, undefined, false, 100))
+  const { data: appChatListData, isLoading: appChatListDataLoading } = useSWR(chatShouldReloadKey ? ['appChatList', chatShouldReloadKey, isInstalledApp, appId] : null, () => fetchChatList(chatShouldReloadKey, isInstalledApp, appId))
+
+  const appPrevChatList = useMemo(() => {
+    const data = appChatListData?.data || []
+    const chatList: ChatItem[] = []
+
+    if (currentConversationId && data.length) {
+      data.forEach((item: any) => {
+        chatList.push({
+          id: `question-${item.id}`,
+          content: item.query,
+          isAnswer: false,
+          message_files: item.message_files?.filter((file: any) => file.belongs_to === 'user') || [],
+        })
+        chatList.push({
+          id: item.id,
+          content: item.answer,
+          agent_thoughts: addFileInfos(item.agent_thoughts ? sortAgentSorts(item.agent_thoughts) : item.agent_thoughts, item.message_files),
+          feedback: item.feedback,
+          isAnswer: true,
+          citation: item.retriever_resources,
+          message_files: item.message_files?.filter((file: any) => file.belongs_to === 'assistant') || [],
+        })
+      })
+    }
+
+    return chatList
+  }, [appChatListData, currentConversationId])
+
+  const [showNewConversationItemInList, setShowNewConversationItemInList] = useState(false)
+
+  const pinnedConversationList = useMemo(() => {
+    return appPinnedConversationData?.data || []
+  }, [appPinnedConversationData])
+  const { t } = useTranslation()
+  const newConversationInputsRef = useRef<Record<string, any>>({})
+  const [newConversationInputs, setNewConversationInputs] = useState<Record<string, any>>({})
+  const handleNewConversationInputsChange = useCallback((newInputs: Record<string, any>) => {
+    newConversationInputsRef.current = newInputs
+    setNewConversationInputs(newInputs)
+  }, [])
+  const inputsForms = useMemo(() => {
+    return (appParams?.user_input_form || []).filter((item: any) => item.paragraph || item.select || item['text-input']).map((item: any) => {
+      if (item.paragraph) {
+        return {
+          ...item.paragraph,
+          type: 'paragraph',
+        }
+      }
+      if (item.select) {
+        return {
+          ...item.select,
+          type: 'select',
+        }
+      }
+      return {
+        ...item['text-input'],
+        type: 'text-input',
+      }
+    })
+  }, [appParams])
+  useEffect(() => {
+    const conversationInputs: Record<string, any> = {}
+
+    inputsForms.forEach((item: any) => {
+      conversationInputs[item.variable] = item.default || ''
+    })
+    handleNewConversationInputsChange(conversationInputs)
+  }, [handleNewConversationInputsChange, inputsForms])
+
+  const { data: newConversation } = useSWR(newConversationId ? [isInstalledApp, appId, newConversationId] : null, () => generationConversationName(isInstalledApp, appId, newConversationId))
+  const [originConversationList, setOriginConversationList] = useState<ConversationItem[]>([])
+  useEffect(() => {
+    if (appConversationData?.data && !appConversationDataLoading)
+      setOriginConversationList(appConversationData?.data)
+  }, [appConversationData, appConversationDataLoading])
+  const conversationList = useMemo(() => {
+    const data = originConversationList.slice()
+
+    if (showNewConversationItemInList && data[0]?.id !== '') {
+      data.unshift({
+        id: '',
+        name: t('share.chat.newChatDefaultName'),
+        inputs: {},
+        introduction: '',
+      })
+    }
+    return data
+  }, [originConversationList, showNewConversationItemInList, t])
+
+  useEffect(() => {
+    if (newConversation) {
+      setOriginConversationList(produce((draft) => {
+        const index = draft.findIndex(item => item.id === newConversation.id)
+
+        if (index > -1)
+          draft[index] = newConversation
+        else
+          draft.unshift(newConversation)
+      }))
+    }
+  }, [newConversation])
+
+  const currentConversationItem = useMemo(() => {
+    let coversationItem = conversationList.find(item => item.id === currentConversationId)
+
+    if (!coversationItem && pinnedConversationList.length)
+      coversationItem = pinnedConversationList.find(item => item.id === currentConversationId)
+
+    return coversationItem
+  }, [conversationList, currentConversationId, pinnedConversationList])
+
+  const { notify } = useToastContext()
+  const checkInputsRequired = useCallback((silent?: boolean) => {
+    if (inputsForms.length) {
+      for (let i = 0; i < inputsForms.length; i += 1) {
+        const item = inputsForms[i]
+
+        if (item.required && !newConversationInputsRef.current[item.variable]) {
+          if (!silent) {
+            notify({
+              type: 'error',
+              message: t('appDebug.errorMessage.valueOfVarRequired', { key: item.variable }),
+            })
+          }
+          return
+        }
+      }
+      return true
+    }
+
+    return true
+  }, [inputsForms, notify, t])
+  const handleStartChat = useCallback(() => {
+    if (checkInputsRequired()) {
+      setShowConfigPanelBeforeChat(false)
+      setShowNewConversationItemInList(true)
+    }
+  }, [setShowConfigPanelBeforeChat, 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])
+  const handleNewConversation = useCallback(() => {
+    currentChatInstanceRef.current.handleStop()
+    setNewConversationId('')
+
+    if (showNewConversationItemInList) {
+      handleChangeConversation('')
+    }
+    else if (currentConversationId) {
+      handleConversationIdInfoChange('')
+      setShowConfigPanelBeforeChat(true)
+      setShowNewConversationItemInList(true)
+      handleNewConversationInputsChange({})
+    }
+  }, [handleChangeConversation, currentConversationId, handleConversationIdInfoChange, setShowConfigPanelBeforeChat, setShowNewConversationItemInList, showNewConversationItemInList, handleNewConversationInputsChange])
+  const handleUpdateConversationList = useCallback(() => {
+    mutateAppConversationData()
+    mutateAppPinnedConversationData()
+  }, [mutateAppConversationData, mutateAppPinnedConversationData])
+
+  const handlePinConversation = useCallback(async (conversationId: string) => {
+    await pinConversation(isInstalledApp, appId, conversationId)
+    notify({ type: 'success', message: t('common.api.success') })
+    handleUpdateConversationList()
+  }, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
+
+  const handleUnpinConversation = useCallback(async (conversationId: string) => {
+    await unpinConversation(isInstalledApp, appId, conversationId)
+    notify({ type: 'success', message: t('common.api.success') })
+    handleUpdateConversationList()
+  }, [isInstalledApp, appId, notify, t, handleUpdateConversationList])
+
+  const [conversationDeleting, setConversationDeleting] = useState(false)
+  const handleDeleteConversation = useCallback(async (
+    conversationId: string,
+    {
+      onSuccess,
+    }: Callback,
+  ) => {
+    if (conversationDeleting)
+      return
+
+    try {
+      setConversationDeleting(true)
+      await delConversation(isInstalledApp, appId, conversationId)
+      notify({ type: 'success', message: t('common.api.success') })
+      onSuccess()
+    }
+    finally {
+      setConversationDeleting(false)
+    }
+
+    if (conversationId === currentConversationId)
+      handleNewConversation()
+
+    handleUpdateConversationList()
+  }, [isInstalledApp, appId, notify, t, handleUpdateConversationList, handleNewConversation, currentConversationId, conversationDeleting])
+
+  const [conversationRenaming, setConversationRenaming] = useState(false)
+  const handleRenameConversation = useCallback(async (
+    conversationId: string,
+    newName: string,
+    {
+      onSuccess,
+    }: Callback,
+  ) => {
+    if (conversationRenaming)
+      return
+
+    if (!newName.trim()) {
+      notify({
+        type: 'error',
+        message: t('common.chat.conversationNameCanNotEmpty'),
+      })
+      return
+    }
+
+    setConversationRenaming(true)
+    try {
+      await renameConversation(isInstalledApp, appId, conversationId, newName)
+
+      notify({
+        type: 'success',
+        message: t('common.actionMsg.modifiedSuccessfully'),
+      })
+      setOriginConversationList(produce((draft) => {
+        const index = originConversationList.findIndex(item => item.id === conversationId)
+        const item = draft[index]
+
+        draft[index] = {
+          ...item,
+          name: newName,
+        }
+      }))
+      onSuccess()
+    }
+    finally {
+      setConversationRenaming(false)
+    }
+  }, [isInstalledApp, appId, notify, t, conversationRenaming, originConversationList])
+
+  const handleNewConversationCompleted = useCallback((newConversationId: string) => {
+    setNewConversationId(newConversationId)
+    handleConversationIdInfoChange(newConversationId)
+    setShowNewConversationItemInList(false)
+    mutateAppConversationData()
+  }, [mutateAppConversationData, handleConversationIdInfoChange])
+
+  const handleFeedback = useCallback(async (messageId: string, feedback: Feedback) => {
+    await updateFeedback({ url: `/messages/${messageId}/feedbacks`, body: { rating: feedback.rating } }, isInstalledApp, appId)
+    notify({ type: 'success', message: t('common.api.success') })
+  }, [isInstalledApp, appId, t, notify])
+
+  return {
+    appInfoLoading,
+    isInstalledApp,
+    appId,
+    currentConversationId,
+    currentConversationItem,
+    handleConversationIdInfoChange,
+    appData,
+    appParams: appParams || {} as ChatConfig,
+    appMeta,
+    appPinnedConversationData,
+    appConversationData,
+    appConversationDataLoading,
+    appChatListData,
+    appChatListDataLoading,
+    appPrevChatList,
+    pinnedConversationList,
+    conversationList,
+    showConfigPanelBeforeChat,
+    setShowConfigPanelBeforeChat,
+    setShowNewConversationItemInList,
+    newConversationInputs,
+    handleNewConversationInputsChange,
+    inputsForms,
+    handleNewConversation,
+    handleStartChat,
+    handleChangeConversation,
+    handlePinConversation,
+    handleUnpinConversation,
+    conversationDeleting,
+    handleDeleteConversation,
+    conversationRenaming,
+    handleRenameConversation,
+    handleNewConversationCompleted,
+    newConversationId,
+    chatShouldReloadKey,
+    handleFeedback,
+    currentChatInstanceRef,
+  }
+}

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

@@ -0,0 +1,195 @@
+import type { FC } from 'react'
+import {
+  useEffect,
+  useState,
+} from 'react'
+import { useAsyncEffect } from 'ahooks'
+import {
+  ChatWithHistoryContext,
+  useChatWithHistoryContext,
+} from './context'
+import { useChatWithHistory } from './hooks'
+import Sidebar from './sidebar'
+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'
+
+type ChatWithHistoryProps = {
+  className?: string
+}
+const ChatWithHistory: FC<ChatWithHistoryProps> = ({
+  className,
+}) => {
+  const {
+    appData,
+    appInfoLoading,
+    appPrevChatList,
+    showConfigPanelBeforeChat,
+    appChatListDataLoading,
+    chatShouldReloadKey,
+    isMobile,
+  } = useChatWithHistoryContext()
+
+  const chatReady = (!showConfigPanelBeforeChat || !!appPrevChatList.length)
+  const customConfig = appData?.custom_config
+  const site = appData?.site
+
+  useEffect(() => {
+    if (site) {
+      if (customConfig)
+        document.title = `${site.title}`
+      else
+        document.title = `${site.title} - Powered by Dify`
+    }
+  }, [site, customConfig])
+
+  if (appInfoLoading) {
+    return (
+      <Loading type='app' />
+    )
+  }
+
+  return (
+    <div className={`h-full flex bg-white ${className} ${isMobile && 'flex-col'}`}>
+      {
+        !isMobile && (
+          <Sidebar />
+        )
+      }
+      {
+        isMobile && (
+          <HeaderInMobile />
+        )
+      }
+      <div className={`grow overflow-hidden ${showConfigPanelBeforeChat && !appPrevChatList.length && 'flex items-center justify-center'}`}>
+        {
+          showConfigPanelBeforeChat && !appChatListDataLoading && !appPrevChatList.length && (
+            <div className={`flex w-full items-center justify-center h-full ${isMobile && 'px-4'}`}>
+              <ConfigPanel />
+            </div>
+          )
+        }
+        {
+          appChatListDataLoading && chatReady && (
+            <Loading type='app' />
+          )
+        }
+        {
+          chatReady && !appChatListDataLoading && (
+            <ChatWrapper key={chatShouldReloadKey} />
+          )
+        }
+      </div>
+    </div>
+  )
+}
+
+export type ChatWithHistoryWrapProps = {
+  installedAppInfo?: InstalledApp
+  className?: string
+}
+const ChatWithHistoryWrap: FC<ChatWithHistoryWrapProps> = ({
+  installedAppInfo,
+  className,
+}) => {
+  const media = useBreakpoints()
+  const isMobile = media === MediaType.mobile
+
+  const {
+    appInfoLoading,
+    appData,
+    appParams,
+    appMeta,
+    appChatListDataLoading,
+    currentConversationId,
+    currentConversationItem,
+    appPrevChatList,
+    pinnedConversationList,
+    conversationList,
+    showConfigPanelBeforeChat,
+    newConversationInputs,
+    handleNewConversationInputsChange,
+    inputsForms,
+    handleNewConversation,
+    handleStartChat,
+    handleChangeConversation,
+    handlePinConversation,
+    handleUnpinConversation,
+    handleDeleteConversation,
+    conversationRenaming,
+    handleRenameConversation,
+    handleNewConversationCompleted,
+    chatShouldReloadKey,
+    isInstalledApp,
+    appId,
+    handleFeedback,
+    currentChatInstanceRef,
+  } = useChatWithHistory(installedAppInfo)
+
+  return (
+    <ChatWithHistoryContext.Provider value={{
+      appInfoLoading,
+      appData,
+      appParams,
+      appMeta,
+      appChatListDataLoading,
+      currentConversationId,
+      currentConversationItem,
+      appPrevChatList,
+      pinnedConversationList,
+      conversationList,
+      showConfigPanelBeforeChat,
+      newConversationInputs,
+      handleNewConversationInputsChange,
+      inputsForms,
+      handleNewConversation,
+      handleStartChat,
+      handleChangeConversation,
+      handlePinConversation,
+      handleUnpinConversation,
+      handleDeleteConversation,
+      conversationRenaming,
+      handleRenameConversation,
+      handleNewConversationCompleted,
+      chatShouldReloadKey,
+      isMobile,
+      isInstalledApp,
+      appId,
+      handleFeedback,
+      currentChatInstanceRef,
+    }}>
+      <ChatWithHistory className={className} />
+    </ChatWithHistoryContext.Provider>
+  )
+}
+
+const ChatWithHistoryWrapWithCheckToken: FC<ChatWithHistoryWrapProps> = ({
+  installedAppInfo,
+  className,
+}) => {
+  const [inited, setInited] = useState(false)
+
+  useAsyncEffect(async () => {
+    if (!inited) {
+      if (!installedAppInfo)
+        await checkOrSetAccessToken()
+      setInited(true)
+    }
+  }, [])
+
+  if (!inited)
+    return null
+
+  return (
+    <ChatWithHistoryWrap
+      installedAppInfo={installedAppInfo}
+      className={className}
+    />
+  )
+}
+
+export default ChatWithHistoryWrapWithCheckToken

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

@@ -0,0 +1,141 @@
+import {
+  useCallback,
+  useState,
+} from 'react'
+import { useTranslation } from 'react-i18next'
+import { useChatWithHistoryContext } from '../context'
+import List from './list'
+import AppIcon from '@/app/components/base/app-icon'
+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 Confirm from '@/app/components/base/confirm'
+import RenameModal from '@/app/components/share/chat/sidebar/rename-modal'
+
+const Sidebar = () => {
+  const { t } = useTranslation()
+  const {
+    appData,
+    pinnedConversationList,
+    conversationList,
+    handleNewConversation,
+    currentConversationId,
+    handleChangeConversation,
+    handlePinConversation,
+    handleUnpinConversation,
+    conversationRenaming,
+    handleRenameConversation,
+    handleDeleteConversation,
+    isMobile,
+  } = useChatWithHistoryContext()
+  const [showConfirm, setShowConfirm] = useState<ConversationItem | null>(null)
+  const [showRename, setShowRename] = useState<ConversationItem | null>(null)
+
+  const handleOperate = useCallback((type: string, item: ConversationItem) => {
+    if (type === 'pin')
+      handlePinConversation(item.id)
+
+    if (type === 'unpin')
+      handleUnpinConversation(item.id)
+
+    if (type === 'delete')
+      setShowConfirm(item)
+
+    if (type === 'rename')
+      setShowRename(item)
+  }, [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-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'
+              icon={appData?.site.icon}
+              background={appData?.site.icon_background}
+            />
+            <div className='py-1 text-base font-semibold text-gray-800'>
+              {appData?.site.title}
+            </div>
+          </div>
+        )
+      }
+      <div className='shrink-0 p-4'>
+        <Button
+          className='justify-start px-3 py-0 w-full h-9 text-sm font-medium text-primary-600'
+          onClick={handleNewConversation}
+        >
+          <Edit05 className='mr-2 w-4 h-4' />
+          {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 && (
+            <List
+              title={(pinnedConversationList.length && t('share.chat.unpinnedTitle')) || ''}
+              list={conversationList}
+              onChangeConversation={handleChangeConversation}
+              onOperate={handleOperate}
+              currentConversationId={currentConversationId}
+            />
+          )
+        }
+      </div>
+      <div className='px-4 pb-4 text-xs text-gray-400'>
+        © {appData?.site.copyright || appData?.site.title} {(new Date()).getFullYear()}
+      </div>
+      {!!showConfirm && (
+        <Confirm
+          title={t('share.chat.deleteConversation.title')}
+          content={t('share.chat.deleteConversation.content') || ''}
+          isShow
+          onClose={handleCancelConfirm}
+          onCancel={handleCancelConfirm}
+          onConfirm={handleDelete}
+        />
+      )}
+      {showRename && (
+        <RenameModal
+          isShow
+          onClose={handleCancelRename}
+          saveLoading={conversationRenaming}
+          name={showRename?.name || ''}
+          onSave={handleRename}
+        />
+      )}
+    </div>
+  )
+}
+
+export default Sidebar

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

@@ -0,0 +1,58 @@
+import type { FC } from 'react'
+import {
+  memo,
+  useRef,
+} 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'
+
+type ItemProps = {
+  isPin?: boolean
+  item: ConversationItem
+  onOperate: (type: string, item: ConversationItem) => void
+  onChangeConversation: (conversationId: string) => void
+  currentConversationId: string
+}
+const Item: FC<ItemProps> = ({
+  isPin,
+  item,
+  onOperate,
+  onChangeConversation,
+  currentConversationId,
+}) => {
+  const ref = useRef(null)
+  const isHovering = useHover(ref)
+
+  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'}
+      `}
+      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>
+      {item.id !== '' && (
+        <div className='shrink-0 h-6' onClick={e => e.stopPropagation()}>
+          <ItemOperation
+            isPinned={!!isPin}
+            isItemHovering={isHovering}
+            togglePin={() => onOperate(isPin ? 'unpin' : 'pin', item)}
+            isShowDelete
+            isShowRenameConversation
+            onRenameConversation={() => onOperate('rename', item)}
+            onDelete={() => onOperate('delete', item)}
+          />
+        </div>
+      )}
+    </div>
+  )
+}
+
+export default memo(Item)

+ 46 - 0
web/app/components/base/chat/chat-with-history/sidebar/list.tsx

@@ -0,0 +1,46 @@
+import type { FC } from 'react'
+import Item from './item'
+import type { ConversationItem } from '@/models/share'
+
+type ListProps = {
+  isPin?: boolean
+  title?: string
+  list: ConversationItem[]
+  onOperate: (type: string, item: ConversationItem) => void
+  onChangeConversation: (conversationId: string) => void
+  currentConversationId: string
+}
+const List: FC<ListProps> = ({
+  isPin,
+  title,
+  list,
+  onOperate,
+  onChangeConversation,
+  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>
+  )
+}
+
+export default List

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

@@ -82,7 +82,7 @@ const Answer: FC<AnswerProps> = ({
               )
             }
             {
-              hasAgentThoughts && !content && (
+              hasAgentThoughts && (
                 <AgentContent item={item} />
               )
             }

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

@@ -1,5 +1,6 @@
 import type { FC } from 'react'
 import { useState } from 'react'
+import { useTranslation } from 'react-i18next'
 import type { ChatItem } from '../../types'
 import { useCurrentAnswerIsResponsing } from '../hooks'
 import { useChatContext } from '../context'
@@ -8,6 +9,11 @@ import { MessageFast } from '@/app/components/base/icons/src/vender/solid/commun
 import AudioBtn from '@/app/components/base/audio-btn'
 import AnnotationCtrlBtn from '@/app/components/app/configuration/toolbox/annotation/annotation-ctrl-btn'
 import EditReplyModal from '@/app/components/app/annotation/edit-annotation-modal'
+import {
+  ThumbsDown,
+  ThumbsUp,
+} from '@/app/components/base/icons/src/vender/line/alertsAndFeedback'
+import TooltipPlus from '@/app/components/base/tooltip-plus'
 
 type OperationProps = {
   item: ChatItem
@@ -19,11 +25,13 @@ const Operation: FC<OperationProps> = ({
   question,
   index,
 }) => {
+  const { t } = useTranslation()
   const {
     config,
     onAnnotationAdded,
     onAnnotationEdited,
     onAnnotationRemoved,
+    onFeedback,
   } = useChatContext()
   const [isShowReplyModal, setIsShowReplyModal] = useState(false)
   const responsing = useCurrentAnswerIsResponsing(item.id)
@@ -32,8 +40,18 @@ const Operation: FC<OperationProps> = ({
     isOpeningStatement,
     content,
     annotation,
+    feedback,
   } = item
   const hasAnnotation = !!annotation?.id
+  const [localFeedback, setLocalFeedback] = useState(feedback)
+
+  const handleFeedback = async (rating: 'like' | 'dislike' | null) => {
+    if (!config?.supportFeedback || !onFeedback)
+      return
+
+    await onFeedback?.(id, { rating })
+    setLocalFeedback({ rating })
+  }
 
   return (
     <div className='absolute top-[-14px] right-[-14px] flex justify-end gap-1'>
@@ -90,6 +108,53 @@ const Operation: FC<OperationProps> = ({
           </div>
         )
       }
+      {
+        config?.supportFeedback && !localFeedback?.rating && onFeedback && !isOpeningStatement && (
+          <div className='hidden group-hover:flex ml-1 shrink-0 items-center px-0.5 bg-white border-[0.5px] border-gray-100 shadow-md text-gray-500 rounded-lg'>
+            <TooltipPlus 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>
+            </TooltipPlus>
+            <TooltipPlus 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>
+            </TooltipPlus>
+          </div>
+        )
+      }
+      {
+        config?.supportFeedback && localFeedback?.rating && onFeedback && !isOpeningStatement && (
+          <TooltipPlus 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>
+          </TooltipPlus>
+        )
+      }
     </div>
   )
 }

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

@@ -19,7 +19,7 @@ const SuggestedQuestions: FC<SuggestedQuestionsProps> = ({
 
   return (
     <div className='flex flex-wrap'>
-      {suggestedQuestions.map((question, index) => (
+      {suggestedQuestions.filter(q => !!q && q.trim()).map((question, index) => (
         <div
           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'

+ 3 - 0
web/app/components/base/chat/chat/context.tsx

@@ -15,6 +15,7 @@ export type ChatContextValue = Pick<ChatProps, 'config'
   | 'onAnnotationEdited'
   | 'onAnnotationAdded'
   | 'onAnnotationRemoved'
+  | 'onFeedback'
 >
 
 const ChatContext = createContext<ChatContextValue>({
@@ -38,6 +39,7 @@ export const ChatContextProvider = ({
   onAnnotationEdited,
   onAnnotationAdded,
   onAnnotationRemoved,
+  onFeedback,
 }: ChatContextProviderProps) => {
   return (
     <ChatContext.Provider value={{
@@ -52,6 +54,7 @@ export const ChatContextProvider = ({
       onAnnotationEdited,
       onAnnotationAdded,
       onAnnotationRemoved,
+      onFeedback,
     }}>
       {children}
     </ChatContext.Provider>

+ 41 - 29
web/app/components/base/chat/chat/hooks.ts

@@ -5,7 +5,7 @@ import {
   useState,
 } from 'react'
 import { useTranslation } from 'react-i18next'
-import { produce } from 'immer'
+import { produce, setAutoFreeze } from 'immer'
 import dayjs from 'dayjs'
 import type {
   ChatConfig,
@@ -23,8 +23,10 @@ import type { Annotation } from '@/models/log'
 
 type GetAbortController = (abortController: AbortController) => void
 type SendCallback = {
-  onGetConvesationMessages: (conversationId: string, getAbortController: GetAbortController) => Promise<any>
+  onGetConvesationMessages?: (conversationId: string, getAbortController: GetAbortController) => Promise<any>
   onGetSuggestedQuestions?: (responseItemId: string, getAbortController: GetAbortController) => Promise<any>
+  onConversationComplete?: (conversationId: string) => void
+  isPublicAPI?: boolean
 }
 
 export const useCheckPromptVariables = () => {
@@ -67,7 +69,7 @@ export const useCheckPromptVariables = () => {
 }
 
 export const useChat = (
-  config: ChatConfig,
+  config?: ChatConfig,
   promptVariablesConfig?: {
     inputs: Inputs
     promptVariables: PromptVariable[]
@@ -90,10 +92,17 @@ export const useChat = (
   const suggestedQuestionsAbortControllerRef = useRef<AbortController | null>(null)
   const checkPromptVariables = useCheckPromptVariables()
 
+  useEffect(() => {
+    setAutoFreeze(false)
+    return () => {
+      setAutoFreeze(true)
+    }
+  }, [])
+
   const handleUpdateChatList = useCallback((newChatList: ChatItem[]) => {
     setChatList(newChatList)
     chatListRef.current = newChatList
-  }, [])
+  }, [setChatList])
   const handleResponsing = useCallback((isResponsing: boolean) => {
     setIsResponsing(isResponsing)
     isResponsingRef.current = isResponsing
@@ -103,22 +112,19 @@ export const useChat = (
     return replaceStringWithValues(str, promptVariablesConfig?.promptVariables || [], promptVariablesConfig?.inputs || {})
   }, [promptVariablesConfig?.inputs, promptVariablesConfig?.promptVariables])
   useEffect(() => {
-    if (config.opening_statement && !chatList.length) {
-      handleUpdateChatList([{
-        id: `${Date.now()}`,
-        content: getIntroduction(config.opening_statement),
-        isAnswer: true,
-        isOpeningStatement: true,
-        suggestedQuestions: config.suggested_questions,
-      }])
+    if (config?.opening_statement && chatListRef.current.filter(item => item.isOpeningStatement).length === 0) {
+      handleUpdateChatList([
+        {
+          id: `${Date.now()}`,
+          content: getIntroduction(config.opening_statement),
+          isAnswer: true,
+          isOpeningStatement: true,
+          suggestedQuestions: config.suggested_questions,
+        },
+        ...chatListRef.current,
+      ])
     }
-  }, [
-    config.opening_statement,
-    config.suggested_questions,
-    getIntroduction,
-    chatList,
-    handleUpdateChatList,
-  ])
+  }, [])
 
   const handleStop = useCallback(() => {
     hasStopResponded.current = true
@@ -136,7 +142,7 @@ export const useChat = (
   const handleRestart = useCallback(() => {
     handleStop()
     connversationId.current = ''
-    const newChatList = config.opening_statement
+    const newChatList = config?.opening_statement
       ? [{
         id: `${Date.now()}`,
         content: config.opening_statement,
@@ -181,6 +187,8 @@ export const useChat = (
     {
       onGetConvesationMessages,
       onGetSuggestedQuestions,
+      onConversationComplete,
+      isPublicAPI,
     }: SendCallback,
   ) => {
     setSuggestQuestions([])
@@ -248,6 +256,7 @@ export const useChat = (
         body: bodyParams,
       },
       {
+        isPublicAPI,
         getAbortController: (abortController) => {
           abortControllerRef.current = abortController
         },
@@ -286,7 +295,10 @@ export const useChat = (
           if (hasError)
             return
 
-          if (connversationId.current && !hasStopResponded.current) {
+          if (onConversationComplete)
+            onConversationComplete(connversationId.current)
+
+          if (connversationId.current && !hasStopResponded.current && onGetConvesationMessages) {
             const { data }: any = await onGetConvesationMessages(
               connversationId.current,
               newAbortController => conversationMessagesAbortControllerRef.current = newAbortController,
@@ -315,7 +327,7 @@ export const useChat = (
             })
             handleUpdateChatList(newChatList)
           }
-          if (config.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
+          if (config?.suggested_questions_after_answer?.enabled && !hasStopResponded.current && onGetSuggestedQuestions) {
             const { data }: any = await onGetSuggestedQuestions(
               responseItem.id,
               newAbortController => suggestedQuestionsAbortControllerRef.current = newAbortController,
@@ -409,7 +421,7 @@ export const useChat = (
     return true
   }, [
     checkPromptVariables,
-    config.suggested_questions_after_answer,
+    config?.suggested_questions_after_answer,
     updateCurrentQA,
     t,
     notify,
@@ -419,7 +431,7 @@ export const useChat = (
   ])
 
   const handleAnnotationEdited = useCallback((query: string, answer: string, index: number) => {
-    setChatList(chatListRef.current.map((item, i) => {
+    handleUpdateChatList(chatListRef.current.map((item, i) => {
       if (i === index - 1) {
         return {
           ...item,
@@ -438,9 +450,9 @@ export const useChat = (
       }
       return item
     }))
-  }, [])
+  }, [handleUpdateChatList])
   const handleAnnotationAdded = useCallback((annotationId: string, authorName: string, query: string, answer: string, index: number) => {
-    setChatList(chatListRef.current.map((item, i) => {
+    handleUpdateChatList(chatListRef.current.map((item, i) => {
       if (i === index - 1) {
         return {
           ...item,
@@ -468,9 +480,9 @@ export const useChat = (
       }
       return item
     }))
-  }, [])
+  }, [handleUpdateChatList])
   const handleAnnotationRemoved = useCallback((index: number) => {
-    setChatList(chatListRef.current.map((item, i) => {
+    handleUpdateChatList(chatListRef.current.map((item, i) => {
       if (i === index) {
         return {
           ...item,
@@ -483,7 +495,7 @@ export const useChat = (
       }
       return item
     }))
-  }, [])
+  }, [handleUpdateChatList])
 
   return {
     chatList,

+ 68 - 42
web/app/components/base/chat/chat/index.tsx

@@ -12,6 +12,7 @@ import { useThrottleEffect } from 'ahooks'
 import type {
   ChatConfig,
   ChatItem,
+  Feedback,
   OnSend,
 } from '../types'
 import Question from './question'
@@ -32,7 +33,9 @@ export type ChatProps = {
   noChatInput?: boolean
   onSend?: OnSend
   chatContainerclassName?: string
+  chatContainerInnerClassName?: string
   chatFooterClassName?: string
+  chatFooterInnerClassName?: string
   suggestedQuestions?: string[]
   showPromptLog?: boolean
   questionIcon?: ReactNode
@@ -41,6 +44,8 @@ export type ChatProps = {
   onAnnotationEdited?: (question: string, answer: string, index: number) => void
   onAnnotationAdded?: (annotationId: string, authorName: string, question: string, answer: string, index: number) => void
   onAnnotationRemoved?: (index: number) => void
+  chatNode?: ReactNode
+  onFeedback?: (messageId: string, feedback: Feedback) => void
 }
 const Chat: FC<ChatProps> = ({
   config,
@@ -51,7 +56,9 @@ const Chat: FC<ChatProps> = ({
   onStopResponding,
   noChatInput,
   chatContainerclassName,
+  chatContainerInnerClassName,
   chatFooterClassName,
+  chatFooterInnerClassName,
   suggestedQuestions,
   showPromptLog,
   questionIcon,
@@ -60,10 +67,14 @@ const Chat: FC<ChatProps> = ({
   onAnnotationAdded,
   onAnnotationEdited,
   onAnnotationRemoved,
+  chatNode,
+  onFeedback,
 }) => {
   const { t } = useTranslation()
   const chatContainerRef = useRef<HTMLDivElement>(null)
+  const chatContainerInnerRef = useRef<HTMLDivElement>(null)
   const chatFooterRef = useRef<HTMLDivElement>(null)
+  const chatFooterInnerRef = useRef<HTMLDivElement>(null)
 
   const handleScrolltoBottom = () => {
     if (chatContainerRef.current)
@@ -75,6 +86,9 @@ const Chat: FC<ChatProps> = ({
 
     if (chatContainerRef.current && chatFooterRef.current)
       chatFooterRef.current.style.width = `${chatContainerRef.current.clientWidth}px`
+
+    if (chatContainerInnerRef.current && chatFooterInnerRef.current)
+      chatFooterInnerRef.current.style.width = `${chatContainerInnerRef.current.clientWidth}px`
   }, [chatList], { wait: 500 })
 
   useEffect(() => {
@@ -111,32 +125,39 @@ const Chat: FC<ChatProps> = ({
       onAnnotationAdded={onAnnotationAdded}
       onAnnotationEdited={onAnnotationEdited}
       onAnnotationRemoved={onAnnotationRemoved}
+      onFeedback={onFeedback}
     >
       <div className='relative h-full'>
         <div
           ref={chatContainerRef}
           className={`relative h-full overflow-y-auto ${chatContainerclassName}`}
         >
-          {
-            chatList.map((item, index) => {
-              if (item.isAnswer) {
+          {chatNode}
+          <div
+            ref={chatContainerInnerRef}
+            className={`${chatContainerInnerClassName}`}
+          >
+            {
+              chatList.map((item, index) => {
+                if (item.isAnswer) {
+                  return (
+                    <Answer
+                      key={item.id}
+                      item={item}
+                      question={chatList[index - 1]?.content}
+                      index={index}
+                    />
+                  )
+                }
                 return (
-                  <Answer
+                  <Question
                     key={item.id}
                     item={item}
-                    question={chatList[index - 1]?.content}
-                    index={index}
                   />
                 )
-              }
-              return (
-                <Question
-                  key={item.id}
-                  item={item}
-                />
-              )
-            })
-          }
+              })
+            }
+          </div>
         </div>
         <div
           className={`absolute bottom-0 ${(hasTryToAsk || !noChatInput || !noStopResponding) && chatFooterClassName}`}
@@ -145,33 +166,38 @@ const Chat: FC<ChatProps> = ({
             background: 'linear-gradient(0deg, #F9FAFB 40%, rgba(255, 255, 255, 0.00) 100%)',
           }}
         >
-          {
-            !noStopResponding && isResponsing && (
-              <div className='flex justify-center mb-2'>
-                <Button className='py-0 px-3 h-7 bg-white shadow-xs' onClick={onStopResponding}>
-                  <StopCircle className='mr-[5px] w-3.5 h-3.5 text-gray-500' />
-                  <span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
-                </Button>
-              </div>
-            )
-          }
-          {
-            hasTryToAsk && (
-              <TryToAsk
-                suggestedQuestions={suggestedQuestions}
-                onSend={onSend}
-              />
-            )
-          }
-          {
-            !noChatInput && (
-              <ChatInput
-                visionConfig={config?.file_upload?.image}
-                speechToTextConfig={config?.speech_to_text}
-                onSend={onSend}
-              />
-            )
-          }
+          <div
+            ref={chatFooterInnerRef}
+            className={`${chatFooterInnerClassName}`}
+          >
+            {
+              !noStopResponding && isResponsing && (
+                <div className='flex justify-center mb-2'>
+                  <Button className='py-0 px-3 h-7 bg-white shadow-xs' onClick={onStopResponding}>
+                    <StopCircle className='mr-[5px] w-3.5 h-3.5 text-gray-500' />
+                    <span className='text-xs text-gray-500 font-normal'>{t('appDebug.operation.stopResponding')}</span>
+                  </Button>
+                </div>
+              )
+            }
+            {
+              hasTryToAsk && (
+                <TryToAsk
+                  suggestedQuestions={suggestedQuestions}
+                  onSend={onSend}
+                />
+              )
+            }
+            {
+              !noChatInput && (
+                <ChatInput
+                  visionConfig={config?.file_upload?.image}
+                  speechToTextConfig={config?.speech_to_text}
+                  onSend={onSend}
+                />
+              )
+            }
+          </div>
         </div>
       </div>
     </ChatContextProvider>

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

@@ -34,7 +34,7 @@ const TryToAsk: FC<TryToAskProps> = ({
           }}
         />
       </div>
-      <div className='flex flex-wrap'>
+      <div className='flex flex-wrap justify-center'>
         {
           suggestedQuestions.map((suggestQuestion, index) => (
             <Button

+ 1 - 0
web/app/components/base/chat/constants.ts

@@ -0,0 +1 @@
+export const CONVERSATION_ID_INFO = 'conversationIdInfo'

+ 9 - 0
web/app/components/base/chat/types.ts

@@ -44,8 +44,17 @@ export type EnableType = {
 export type ChatConfig = Omit<ModelConfig, 'model'> & {
   supportAnnotation?: boolean
   appId?: string
+  supportFeedback?: boolean
 }
 
 export type ChatItem = IChatItem
 
 export type OnSend = (message: string, files?: VisionFile[]) => void
+
+export type Callback = {
+  onSuccess: () => void
+}
+
+export type Feedback = {
+  rating: 'like' | 'dislike' | null
+}

+ 3 - 0
web/app/components/base/confirm/common.tsx

@@ -20,6 +20,7 @@ export type ConfirmCommonProps = {
   confirmBtnClassName?: string
   confirmText?: string
   confirmWrapperClassName?: string
+  confirmDisabled?: boolean
 }
 
 const ConfirmCommon: FC<ConfirmCommonProps> = ({
@@ -34,6 +35,7 @@ const ConfirmCommon: FC<ConfirmCommonProps> = ({
   confirmBtnClassName,
   confirmText,
   confirmWrapperClassName,
+  confirmDisabled,
 }) => {
   const { t } = useTranslation()
 
@@ -78,6 +80,7 @@ const ConfirmCommon: FC<ConfirmCommonProps> = ({
                 type='primary'
                 className={confirmBtnClassName || ''}
                 onClick={onConfirm}
+                disabled={confirmDisabled}
               >
                 {confirmText || CONFIRM_MAP[type].confirmText}
               </Button>

+ 10 - 0
web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-down.svg

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon" clip-path="url(#clip0_17340_934)">
+<path id="Icon_2" d="M11.3333 1.33398V8.66732M14.6666 6.53398V3.46732C14.6666 2.72058 14.6666 2.34721 14.5213 2.062C14.3935 1.81111 14.1895 1.60714 13.9386 1.47931C13.6534 1.33398 13.28 1.33398 12.5333 1.33398H5.41196C4.43764 1.33398 3.95048 1.33398 3.55701 1.51227C3.21022 1.66941 2.91549 1.92227 2.70745 2.24113C2.4714 2.60291 2.39732 3.08441 2.24917 4.0474L1.90045 6.31407C1.70505 7.58419 1.60735 8.21926 1.79582 8.7134C1.96125 9.14711 2.27239 9.50978 2.6759 9.73923C3.13564 10.0007 3.77818 10.0007 5.06324 10.0007H5.59995C5.97332 10.0007 6.16001 10.0007 6.30261 10.0733C6.42806 10.1372 6.53004 10.2392 6.59396 10.3647C6.66662 10.5073 6.66662 10.6939 6.66662 11.0673V13.0234C6.66662 13.9313 7.40262 14.6673 8.31051 14.6673C8.52706 14.6673 8.7233 14.5398 8.81125 14.3419L11.0518 9.30077C11.1537 9.07148 11.2046 8.95684 11.2852 8.87278C11.3563 8.79847 11.4438 8.74165 11.5406 8.70678C11.6501 8.66732 11.7756 8.66732 12.0265 8.66732H12.5333C13.28 8.66732 13.6534 8.66732 13.9386 8.52199C14.1895 8.39416 14.3935 8.19019 14.5213 7.93931C14.6666 7.65409 14.6666 7.28072 14.6666 6.53398Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_17340_934">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 10 - 0
web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-up.svg

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="Icon" clip-path="url(#clip0_17340_931)">
+<path id="Icon_2" d="M4.66671 14.6673V7.33398M1.33337 8.66732V13.334C1.33337 14.0704 1.93033 14.6673 2.66671 14.6673H11.6175C12.6047 14.6673 13.4442 13.9471 13.5943 12.9714L14.3122 8.30477C14.4986 7.09325 13.5613 6.00065 12.3355 6.00065H10C9.63185 6.00065 9.33337 5.70217 9.33337 5.33398V2.97788C9.33337 2.06998 8.59738 1.33398 7.68948 1.33398C7.47293 1.33398 7.27669 1.46151 7.18875 1.6594L4.84267 6.93808C4.73567 7.17883 4.49692 7.33398 4.23346 7.33398H2.66671C1.93033 7.33398 1.33337 7.93094 1.33337 8.66732Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_17340_931">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

+ 10 - 0
web/app/components/base/icons/assets/vender/line/general/edit-05.svg

@@ -0,0 +1,10 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="edit-05" clip-path="url(#clip0_17249_52683)">
+<path id="Icon" d="M7.33325 2.66617H4.53325C3.41315 2.66617 2.85309 2.66617 2.42527 2.88415C2.04895 3.0759 1.74299 3.38186 1.55124 3.75819C1.33325 4.18601 1.33325 4.74606 1.33325 5.86617V11.4662C1.33325 12.5863 1.33325 13.1463 1.55124 13.5741C1.74299 13.9505 2.04895 14.2564 2.42527 14.4482C2.85309 14.6662 3.41315 14.6662 4.53325 14.6662H10.1333C11.2534 14.6662 11.8134 14.6662 12.2412 14.4482C12.6176 14.2564 12.9235 13.9505 13.1153 13.5741C13.3333 13.1463 13.3333 12.5863 13.3333 11.4662V8.66617M5.33323 10.6662H6.4496C6.77572 10.6662 6.93878 10.6662 7.09223 10.6293C7.22828 10.5967 7.35834 10.5428 7.47763 10.4697C7.61219 10.3872 7.72749 10.2719 7.95809 10.0413L14.3333 3.66617C14.8855 3.11388 14.8855 2.21845 14.3333 1.66617C13.781 1.11388 12.8855 1.11388 12.3333 1.66617L5.95808 8.04133C5.72747 8.27193 5.61217 8.38723 5.52971 8.52179C5.45661 8.64108 5.40274 8.77114 5.37007 8.90719C5.33323 9.06064 5.33323 9.2237 5.33323 9.54982V10.6662Z" stroke="#155EEF" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+<defs>
+<clipPath id="clip0_17249_52683">
+<rect width="16" height="16" fill="white"/>
+</clipPath>
+</defs>
+</svg>

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

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="menu-01">
+<path id="Icon" d="M2 8H14M2 4H14M2 12H14" stroke="#344054" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

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

@@ -0,0 +1,5 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="pin-01">
+<path id="Icon" d="M8.00037 10.0007L8.00037 14.6673M5.3337 4.87274V6.29315C5.3337 6.43183 5.3337 6.50117 5.32009 6.56749C5.30801 6.62633 5.28804 6.68327 5.26071 6.73677C5.22991 6.79706 5.18659 6.8512 5.09996 6.95949L4.05344 8.26764C3.60962 8.82242 3.3877 9.09982 3.38745 9.33326C3.38723 9.53629 3.47954 9.72835 3.63822 9.85501C3.82067 10.0007 4.1759 10.0007 4.88637 10.0007H11.1144C11.8248 10.0007 12.1801 10.0007 12.3625 9.85501C12.5212 9.72835 12.6135 9.53629 12.6133 9.33326C12.613 9.09982 12.3911 8.82242 11.9473 8.26764L10.9008 6.95949C10.8141 6.8512 10.7708 6.79706 10.74 6.73677C10.7127 6.68327 10.6927 6.62633 10.6806 6.56749C10.667 6.50117 10.667 6.43183 10.667 6.29315V4.87274C10.667 4.79599 10.667 4.75761 10.6714 4.71977C10.6752 4.68615 10.6816 4.65287 10.6905 4.62023C10.7006 4.58348 10.7148 4.54785 10.7433 4.47659L11.4152 2.7968C11.6113 2.30674 11.7093 2.06171 11.6684 1.86502C11.6327 1.693 11.5305 1.54206 11.384 1.44499C11.2166 1.33398 10.9527 1.33398 10.4249 1.33398H5.57587C5.04806 1.33398 4.78416 1.33398 4.61671 1.44499C4.47027 1.54206 4.36808 1.693 4.33233 1.86502C4.29146 2.06171 4.38947 2.30674 4.58549 2.7968L5.25741 4.47659C5.28591 4.54785 5.30017 4.58348 5.31019 4.62023C5.3191 4.65287 5.32551 4.68615 5.32936 4.71977C5.3337 4.75761 5.3337 4.79599 5.3337 4.87274Z" stroke="#667085" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/>
+</g>
+</svg>

+ 9 - 0
web/app/components/base/icons/assets/vender/solid/shapes/star-06.svg

@@ -0,0 +1,9 @@
+<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
+<g id="star-06">
+<g id="Solid">
+<path d="M3.66675 1.33268C3.66675 0.964492 3.36827 0.666016 3.00008 0.666016C2.63189 0.666016 2.33341 0.964492 2.33341 1.33268V2.33268H1.33341C0.965225 2.33268 0.666748 2.63116 0.666748 2.99935C0.666748 3.36754 0.965225 3.66602 1.33341 3.66602H2.33341V4.66602C2.33341 5.0342 2.63189 5.33268 3.00008 5.33268C3.36827 5.33268 3.66675 5.0342 3.66675 4.66602V3.66602H4.66675C5.03494 3.66602 5.33341 3.36754 5.33341 2.99935C5.33341 2.63116 5.03494 2.33268 4.66675 2.33268H3.66675V1.33268Z" fill="#444CE7"/>
+<path d="M3.66675 11.3327C3.66675 10.9645 3.36827 10.666 3.00008 10.666C2.63189 10.666 2.33341 10.9645 2.33341 11.3327V12.3327H1.33341C0.965225 12.3327 0.666748 12.6312 0.666748 12.9993C0.666748 13.3675 0.965225 13.666 1.33341 13.666H2.33341V14.666C2.33341 15.0342 2.63189 15.3327 3.00008 15.3327C3.36827 15.3327 3.66675 15.0342 3.66675 14.666V13.666H4.66675C5.03494 13.666 5.33341 13.3675 5.33341 12.9993C5.33341 12.6312 5.03494 12.3327 4.66675 12.3327H3.66675V11.3327Z" fill="#444CE7"/>
+<path d="M9.28898 1.76003C9.18995 1.50257 8.94259 1.33268 8.66675 1.33268C8.3909 1.33268 8.14354 1.50257 8.04452 1.76003L6.8884 4.76594C6.68813 5.28663 6.6252 5.43668 6.53912 5.55774C6.45274 5.67921 6.34661 5.78534 6.22514 5.87172C6.10408 5.9578 5.95403 6.02073 5.43334 6.221L2.42743 7.37712C2.16997 7.47614 2.00008 7.7235 2.00008 7.99935C2.00008 8.2752 2.16997 8.52256 2.42743 8.62158L5.43334 9.7777C5.95403 9.97797 6.10408 10.0409 6.22514 10.127C6.34661 10.2134 6.45274 10.3195 6.53912 10.441C6.6252 10.562 6.68813 10.7121 6.8884 11.2328L8.04452 14.2387C8.14354 14.4961 8.3909 14.666 8.66675 14.666C8.9426 14.666 9.18995 14.4961 9.28898 14.2387L10.4451 11.2328C10.6454 10.7121 10.7083 10.562 10.7944 10.441C10.8808 10.3195 10.9869 10.2134 11.1084 10.127C11.2294 10.0409 11.3795 9.97797 11.9002 9.7777L14.9061 8.62158C15.1635 8.52256 15.3334 8.2752 15.3334 7.99935C15.3334 7.7235 15.1635 7.47614 14.9061 7.37712L11.9002 6.221C11.3795 6.02073 11.2294 5.9578 11.1084 5.87172C10.9869 5.78534 10.8808 5.67921 10.7944 5.55774C10.7083 5.43668 10.6454 5.28663 10.4451 4.76594L9.28898 1.76003Z" fill="#444CE7"/>
+</g>
+</g>
+</svg>

+ 66 - 0
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json

@@ -0,0 +1,66 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon",
+					"clip-path": "url(#clip0_17340_934)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon_2",
+							"d": "M11.3333 1.33398V8.66732M14.6666 6.53398V3.46732C14.6666 2.72058 14.6666 2.34721 14.5213 2.062C14.3935 1.81111 14.1895 1.60714 13.9386 1.47931C13.6534 1.33398 13.28 1.33398 12.5333 1.33398H5.41196C4.43764 1.33398 3.95048 1.33398 3.55701 1.51227C3.21022 1.66941 2.91549 1.92227 2.70745 2.24113C2.4714 2.60291 2.39732 3.08441 2.24917 4.0474L1.90045 6.31407C1.70505 7.58419 1.60735 8.21926 1.79582 8.7134C1.96125 9.14711 2.27239 9.50978 2.6759 9.73923C3.13564 10.0007 3.77818 10.0007 5.06324 10.0007H5.59995C5.97332 10.0007 6.16001 10.0007 6.30261 10.0733C6.42806 10.1372 6.53004 10.2392 6.59396 10.3647C6.66662 10.5073 6.66662 10.6939 6.66662 11.0673V13.0234C6.66662 13.9313 7.40262 14.6673 8.31051 14.6673C8.52706 14.6673 8.7233 14.5398 8.81125 14.3419L11.0518 9.30077C11.1537 9.07148 11.2046 8.95684 11.2852 8.87278C11.3563 8.79847 11.4438 8.74165 11.5406 8.70678C11.6501 8.66732 11.7756 8.66732 12.0265 8.66732H12.5333C13.28 8.66732 13.6534 8.66732 13.9386 8.52199C14.1895 8.39416 14.3935 8.19019 14.5213 7.93931C14.6666 7.65409 14.6666 7.28072 14.6666 6.53398Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_17340_934"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "16",
+									"height": "16",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "ThumbsDown"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ThumbsDown.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 = 'ThumbsDown'
+
+export default Icon

+ 66 - 0
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json

@@ -0,0 +1,66 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "Icon",
+					"clip-path": "url(#clip0_17340_931)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon_2",
+							"d": "M4.66671 14.6673V7.33398M1.33337 8.66732V13.334C1.33337 14.0704 1.93033 14.6673 2.66671 14.6673H11.6175C12.6047 14.6673 13.4442 13.9471 13.5943 12.9714L14.3122 8.30477C14.4986 7.09325 13.5613 6.00065 12.3355 6.00065H10C9.63185 6.00065 9.33337 5.70217 9.33337 5.33398V2.97788C9.33337 2.06998 8.59738 1.33398 7.68948 1.33398C7.47293 1.33398 7.27669 1.46151 7.18875 1.6594L4.84267 6.93808C4.73567 7.17883 4.49692 7.33398 4.23346 7.33398H2.66671C1.93033 7.33398 1.33337 7.93094 1.33337 8.66732Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_17340_931"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "16",
+									"height": "16",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "ThumbsUp"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './ThumbsUp.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 = 'ThumbsUp'
+
+export default Icon

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

@@ -1,2 +1,4 @@
 export { default as AlertCircle } from './AlertCircle'
 export { default as AlertTriangle } from './AlertTriangle'
+export { default as ThumbsDown } from './ThumbsDown'
+export { default as ThumbsUp } from './ThumbsUp'

+ 66 - 0
web/app/components/base/icons/src/vender/line/general/Edit05.json

@@ -0,0 +1,66 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "edit-05",
+					"clip-path": "url(#clip0_17249_52683)"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M7.33325 2.66617H4.53325C3.41315 2.66617 2.85309 2.66617 2.42527 2.88415C2.04895 3.0759 1.74299 3.38186 1.55124 3.75819C1.33325 4.18601 1.33325 4.74606 1.33325 5.86617V11.4662C1.33325 12.5863 1.33325 13.1463 1.55124 13.5741C1.74299 13.9505 2.04895 14.2564 2.42527 14.4482C2.85309 14.6662 3.41315 14.6662 4.53325 14.6662H10.1333C11.2534 14.6662 11.8134 14.6662 12.2412 14.4482C12.6176 14.2564 12.9235 13.9505 13.1153 13.5741C13.3333 13.1463 13.3333 12.5863 13.3333 11.4662V8.66617M5.33323 10.6662H6.4496C6.77572 10.6662 6.93878 10.6662 7.09223 10.6293C7.22828 10.5967 7.35834 10.5428 7.47763 10.4697C7.61219 10.3872 7.72749 10.2719 7.95809 10.0413L14.3333 3.66617C14.8855 3.11388 14.8855 2.21845 14.3333 1.66617C13.781 1.11388 12.8855 1.11388 12.3333 1.66617L5.95808 8.04133C5.72747 8.27193 5.61217 8.38723 5.52971 8.52179C5.45661 8.64108 5.40274 8.77114 5.37007 8.90719C5.33323 9.06064 5.33323 9.2237 5.33323 9.54982V10.6662Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			},
+			{
+				"type": "element",
+				"name": "defs",
+				"attributes": {},
+				"children": [
+					{
+						"type": "element",
+						"name": "clipPath",
+						"attributes": {
+							"id": "clip0_17249_52683"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "rect",
+								"attributes": {
+									"width": "16",
+									"height": "16",
+									"fill": "white"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Edit05"
+}

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

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Edit05.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 = 'Edit05'
+
+export default Icon

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

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "menu-01"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M2 8H14M2 4H14M2 12H14",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Menu01"
+}

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

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Menu01.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 = 'Menu01'
+
+export default Icon

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

@@ -0,0 +1,39 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "pin-01"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "path",
+						"attributes": {
+							"id": "Icon",
+							"d": "M8.00037 10.0007L8.00037 14.6673M5.3337 4.87274V6.29315C5.3337 6.43183 5.3337 6.50117 5.32009 6.56749C5.30801 6.62633 5.28804 6.68327 5.26071 6.73677C5.22991 6.79706 5.18659 6.8512 5.09996 6.95949L4.05344 8.26764C3.60962 8.82242 3.3877 9.09982 3.38745 9.33326C3.38723 9.53629 3.47954 9.72835 3.63822 9.85501C3.82067 10.0007 4.1759 10.0007 4.88637 10.0007H11.1144C11.8248 10.0007 12.1801 10.0007 12.3625 9.85501C12.5212 9.72835 12.6135 9.53629 12.6133 9.33326C12.613 9.09982 12.3911 8.82242 11.9473 8.26764L10.9008 6.95949C10.8141 6.8512 10.7708 6.79706 10.74 6.73677C10.7127 6.68327 10.6927 6.62633 10.6806 6.56749C10.667 6.50117 10.667 6.43183 10.667 6.29315V4.87274C10.667 4.79599 10.667 4.75761 10.6714 4.71977C10.6752 4.68615 10.6816 4.65287 10.6905 4.62023C10.7006 4.58348 10.7148 4.54785 10.7433 4.47659L11.4152 2.7968C11.6113 2.30674 11.7093 2.06171 11.6684 1.86502C11.6327 1.693 11.5305 1.54206 11.384 1.44499C11.2166 1.33398 10.9527 1.33398 10.4249 1.33398H5.57587C5.04806 1.33398 4.78416 1.33398 4.61671 1.44499C4.47027 1.54206 4.36808 1.693 4.33233 1.86502C4.29146 2.06171 4.38947 2.30674 4.58549 2.7968L5.25741 4.47659C5.28591 4.54785 5.30017 4.58348 5.31019 4.62023C5.3191 4.65287 5.32551 4.68615 5.32936 4.71977C5.3337 4.75761 5.3337 4.79599 5.3337 4.87274Z",
+							"stroke": "currentColor",
+							"stroke-width": "1.5",
+							"stroke-linecap": "round",
+							"stroke-linejoin": "round"
+						},
+						"children": []
+					}
+				]
+			}
+		]
+	},
+	"name": "Pin01"
+}

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

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Pin01.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 = 'Pin01'
+
+export default Icon

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

@@ -5,6 +5,7 @@ export { default as DotsHorizontal } from './DotsHorizontal'
 export { default as Edit02 } from './Edit02'
 export { default as Edit03 } from './Edit03'
 export { default as Edit04 } from './Edit04'
+export { default as Edit05 } from './Edit05'
 export { default as Hash02 } from './Hash02'
 export { default as HelpCircle } from './HelpCircle'
 export { default as InfoCircle } from './InfoCircle'
@@ -13,6 +14,8 @@ export { default as LinkExternal01 } from './LinkExternal01'
 export { default as LinkExternal02 } from './LinkExternal02'
 export { default as Loading02 } from './Loading02'
 export { default as LogOut01 } from './LogOut01'
+export { default as Menu01 } from './Menu01'
+export { default as Pin01 } from './Pin01'
 export { default as Pin02 } from './Pin02'
 export { default as Plus } from './Plus'
 export { default as SearchLg } from './SearchLg'

+ 62 - 0
web/app/components/base/icons/src/vender/solid/shapes/Star06.json

@@ -0,0 +1,62 @@
+{
+	"icon": {
+		"type": "element",
+		"isRootNode": true,
+		"name": "svg",
+		"attributes": {
+			"width": "16",
+			"height": "16",
+			"viewBox": "0 0 16 16",
+			"fill": "none",
+			"xmlns": "http://www.w3.org/2000/svg"
+		},
+		"children": [
+			{
+				"type": "element",
+				"name": "g",
+				"attributes": {
+					"id": "star-06"
+				},
+				"children": [
+					{
+						"type": "element",
+						"name": "g",
+						"attributes": {
+							"id": "Solid"
+						},
+						"children": [
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M3.66675 1.33268C3.66675 0.964492 3.36827 0.666016 3.00008 0.666016C2.63189 0.666016 2.33341 0.964492 2.33341 1.33268V2.33268H1.33341C0.965225 2.33268 0.666748 2.63116 0.666748 2.99935C0.666748 3.36754 0.965225 3.66602 1.33341 3.66602H2.33341V4.66602C2.33341 5.0342 2.63189 5.33268 3.00008 5.33268C3.36827 5.33268 3.66675 5.0342 3.66675 4.66602V3.66602H4.66675C5.03494 3.66602 5.33341 3.36754 5.33341 2.99935C5.33341 2.63116 5.03494 2.33268 4.66675 2.33268H3.66675V1.33268Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M3.66675 11.3327C3.66675 10.9645 3.36827 10.666 3.00008 10.666C2.63189 10.666 2.33341 10.9645 2.33341 11.3327V12.3327H1.33341C0.965225 12.3327 0.666748 12.6312 0.666748 12.9993C0.666748 13.3675 0.965225 13.666 1.33341 13.666H2.33341V14.666C2.33341 15.0342 2.63189 15.3327 3.00008 15.3327C3.36827 15.3327 3.66675 15.0342 3.66675 14.666V13.666H4.66675C5.03494 13.666 5.33341 13.3675 5.33341 12.9993C5.33341 12.6312 5.03494 12.3327 4.66675 12.3327H3.66675V11.3327Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							},
+							{
+								"type": "element",
+								"name": "path",
+								"attributes": {
+									"d": "M9.28898 1.76003C9.18995 1.50257 8.94259 1.33268 8.66675 1.33268C8.3909 1.33268 8.14354 1.50257 8.04452 1.76003L6.8884 4.76594C6.68813 5.28663 6.6252 5.43668 6.53912 5.55774C6.45274 5.67921 6.34661 5.78534 6.22514 5.87172C6.10408 5.9578 5.95403 6.02073 5.43334 6.221L2.42743 7.37712C2.16997 7.47614 2.00008 7.7235 2.00008 7.99935C2.00008 8.2752 2.16997 8.52256 2.42743 8.62158L5.43334 9.7777C5.95403 9.97797 6.10408 10.0409 6.22514 10.127C6.34661 10.2134 6.45274 10.3195 6.53912 10.441C6.6252 10.562 6.68813 10.7121 6.8884 11.2328L8.04452 14.2387C8.14354 14.4961 8.3909 14.666 8.66675 14.666C8.9426 14.666 9.18995 14.4961 9.28898 14.2387L10.4451 11.2328C10.6454 10.7121 10.7083 10.562 10.7944 10.441C10.8808 10.3195 10.9869 10.2134 11.1084 10.127C11.2294 10.0409 11.3795 9.97797 11.9002 9.7777L14.9061 8.62158C15.1635 8.52256 15.3334 8.2752 15.3334 7.99935C15.3334 7.7235 15.1635 7.47614 14.9061 7.37712L11.9002 6.221C11.3795 6.02073 11.2294 5.9578 11.1084 5.87172C10.9869 5.78534 10.8808 5.67921 10.7944 5.55774C10.7083 5.43668 10.6454 5.28663 10.4451 4.76594L9.28898 1.76003Z",
+									"fill": "currentColor"
+								},
+								"children": []
+							}
+						]
+					}
+				]
+			}
+		]
+	},
+	"name": "Star06"
+}

+ 16 - 0
web/app/components/base/icons/src/vender/solid/shapes/Star06.tsx

@@ -0,0 +1,16 @@
+// GENERATE BY script
+// DON NOT EDIT IT MANUALLY
+
+import * as React from 'react'
+import data from './Star06.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 = 'Star06'
+
+export default Icon

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

@@ -1 +1,2 @@
 export { default as Star04 } from './Star04'
+export { default as Star06 } from './Star06'

+ 1 - 1
web/app/components/explore/index.tsx

@@ -46,7 +46,7 @@ const Explore: FC<IExploreProps> = ({
         }
       >
         <Sidebar controlUpdateInstalledApps={controlUpdateInstalledApps} />
-        <div className='grow'>
+        <div className='grow w-0'>
           {children}
         </div>
       </ExploreContext.Provider>

+ 2 - 2
web/app/components/explore/installed-app/index.tsx

@@ -3,9 +3,9 @@ import type { FC } from 'react'
 import React from 'react'
 import { useContext } from 'use-context-selector'
 import ExploreContext from '@/context/explore-context'
-import ChatApp from '@/app/components/share/chat'
 import TextGenerationApp from '@/app/components/share/text-generation'
 import Loading from '@/app/components/base/loading'
+import ChatWithHistory from '@/app/components/base/chat/chat-with-history'
 
 export type IInstalledAppProps = {
   id: string
@@ -29,7 +29,7 @@ const InstalledApp: FC<IInstalledAppProps> = ({
     <div className='h-full py-2 pl-0 pr-2 sm:p-2'>
       {installedApp?.app.mode === 'chat'
         ? (
-          <ChatApp isInstalledApp installedAppInfo={installedApp} />
+          <ChatWithHistory installedAppInfo={installedApp} className='rounded-2xl shadow-md overflow-hidden' />
         )
         : (
           <TextGenerationApp isInstalledApp installedAppInfo={installedApp}/>

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

@@ -853,7 +853,7 @@ const Main: FC<IMainProps> = ({
         <Header
           title={siteInfo.title}
           icon={siteInfo.icon || ''}
-          icon_background={siteInfo.icon_background}
+          icon_background={siteInfo.icon_background || ''}
           isMobile={isMobile}
           onShowSideBar={showSidebar}
           onCreateNewChat={handleStartChatOnSidebar}

+ 20 - 5
web/models/share.ts

@@ -11,11 +11,11 @@ export type ConversationItem = {
 
 export type SiteInfo = {
   title: string
-  icon: string
-  icon_background: string
-  description: string
-  default_language: Locale
-  prompt_public: boolean
+  icon?: string
+  icon_background?: string
+  description?: string
+  default_language?: Locale
+  prompt_public?: boolean
   copyright?: string
   privacy_policy?: string
 }
@@ -23,3 +23,18 @@ export type SiteInfo = {
 export type AppMeta = {
   tool_icons: Record<string, string>
 }
+
+export type AppData = {
+  app_id: string
+  can_replace_logo?: boolean
+  custom_config?: Record<string, any>
+  enable_site?: boolean
+  end_user_id?: string
+  site: SiteInfo
+}
+
+export type AppConversationData = {
+  data: ConversationItem[]
+  has_more: boolean
+  limit: number
+}

+ 14 - 7
web/service/share.ts

@@ -4,6 +4,13 @@ import {
   delPublic as del, getPublic as get, patchPublic as patch, postPublic as post, ssePost,
 } from './base'
 import type { Feedbacktype } from '@/app/components/app/chat/type'
+import type {
+  AppConversationData,
+  AppData,
+  AppMeta,
+  ConversationItem,
+} from '@/models/share'
+import type { ChatConfig } from '@/app/components/base/chat/types'
 
 function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boolean) {
   switch (action) {
@@ -18,7 +25,7 @@ function getAction(action: 'get' | 'post' | 'del' | 'patch', isInstalledApp: boo
   }
 }
 
-function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) {
+export function getUrl(url: string, isInstalledApp: boolean, installedAppId: string) {
   return isInstalledApp ? `installed-apps/${installedAppId}/${url.startsWith('/') ? url.slice(1) : url}` : url
 }
 
@@ -59,11 +66,11 @@ export const sendCompletionMessage = async (body: Record<string, any>, { onData,
 }
 
 export const fetchAppInfo = async () => {
-  return get('/site')
+  return get('/site') as Promise<AppData>
 }
 
 export const fetchConversations = async (isInstalledApp: boolean, installedAppId = '', last_id?: string, pinned?: boolean, limit?: number) => {
-  return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { ...{ limit: limit || 20 }, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } })
+  return getAction('get', isInstalledApp)(getUrl('conversations', isInstalledApp, installedAppId), { params: { ...{ limit: limit || 20 }, ...(last_id ? { last_id } : {}), ...(pinned !== undefined ? { pinned } : {}) } }) as Promise<AppConversationData>
 }
 
 export const pinConversation = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
@@ -83,11 +90,11 @@ export const renameConversation = async (isInstalledApp: boolean, installedAppId
 }
 
 export const generationConversationName = async (isInstalledApp: boolean, installedAppId = '', id: string) => {
-  return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { auto_generate: true } })
+  return getAction('post', isInstalledApp)(getUrl(`conversations/${id}/name`, isInstalledApp, installedAppId), { body: { auto_generate: true } }) as Promise<ConversationItem>
 }
 
 export const fetchChatList = async (conversationId: string, isInstalledApp: boolean, installedAppId = '') => {
-  return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } })
+  return getAction('get', isInstalledApp)(getUrl('messages', isInstalledApp, installedAppId), { params: { conversation_id: conversationId, limit: 20, last_id: '' } }) as any
 }
 
 // Abandoned API interface
@@ -97,11 +104,11 @@ export const fetchChatList = async (conversationId: string, isInstalledApp: bool
 
 // init value. wait for server update
 export const fetchAppParams = async (isInstalledApp: boolean, installedAppId = '') => {
-  return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId))
+  return (getAction('get', isInstalledApp))(getUrl('parameters', isInstalledApp, installedAppId)) as Promise<ChatConfig>
 }
 
 export const fetchAppMeta = async (isInstalledApp: boolean, installedAppId = '') => {
-  return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId))
+  return (getAction('get', isInstalledApp))(getUrl('meta', isInstalledApp, installedAppId)) as Promise<AppMeta>
 }
 
 export const updateFeedback = async ({ url, body }: { url: string; body: Feedbacktype }, isInstalledApp: boolean, installedAppId = '') => {

+ 11 - 0
web/types/app.ts

@@ -70,6 +70,7 @@ export type PromptVariable = {
 }
 
 export type TextTypeFormItem = {
+  default: string
   label: string
   variable: string
   required: boolean
@@ -77,11 +78,19 @@ export type TextTypeFormItem = {
 }
 
 export type SelectTypeFormItem = {
+  default: string
   label: string
   variable: string
   required: boolean
   options: string[]
 }
+
+export type ParagraphTypeFormItem = {
+  default: string
+  label: string
+  variable: string
+  required: boolean
+}
 /**
  * User Input Form Item
  */
@@ -89,6 +98,8 @@ export type UserInputFormItem = {
   'text-input': TextTypeFormItem
 } | {
   'select': SelectTypeFormItem
+} | {
+  'paragraph': TextTypeFormItem
 }
 
 export type AgentTool = {