Bläddra i källkod

chore: replace chat in web app (#2373)

zxhlyh 1 år sedan
förälder
incheckning
51d359268e
49 ändrade filer med 2100 tillägg och 92 borttagningar
  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>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 10 - 0
web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-down.svg


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 10 - 0
web/app/components/base/icons/assets/vender/line/alertsAndFeedback/thumbs-up.svg


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 10 - 0
web/app/components/base/icons/assets/vender/line/general/edit-05.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>

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 5 - 0
web/app/components/base/icons/assets/vender/line/general/pin-01.svg


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 9 - 0
web/app/components/base/icons/assets/vender/solid/shapes/star-06.svg


Filskillnaden har hållts tillbaka eftersom den är för stor
+ 66 - 0
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsDown.json


+ 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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 66 - 0
web/app/components/base/icons/src/vender/line/alertsAndFeedback/ThumbsUp.json


+ 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'

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 66 - 0
web/app/components/base/icons/src/vender/line/general/Edit05.json


+ 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

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 39 - 0
web/app/components/base/icons/src/vender/line/general/Pin01.json


+ 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'

Filskillnaden har hållts tillbaka eftersom den är för stor
+ 62 - 0
web/app/components/base/icons/src/vender/solid/shapes/Star06.json


+ 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 = {