mirror of
http://112.124.100.131/huang.ze/ebiz-dify-ai.git
synced 2025-12-16 06:16:53 +08:00
chore: replace chat in web app (#2373)
This commit is contained in:
141
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
Normal file
141
web/app/components/base/chat/chat-with-history/chat-wrapper.tsx
Normal file
@@ -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
|
||||
@@ -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
|
||||
@@ -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
web/app/components/base/chat/chat-with-history/context.tsx
Normal file
74
web/app/components/base/chat/chat-with-history/context.tsx
Normal file
@@ -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)
|
||||
@@ -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
web/app/components/base/chat/chat-with-history/header.tsx
Normal file
25
web/app/components/base/chat/chat-with-history/header.tsx
Normal file
@@ -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
web/app/components/base/chat/chat-with-history/hooks.tsx
Normal file
385
web/app/components/base/chat/chat-with-history/hooks.tsx
Normal file
@@ -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
web/app/components/base/chat/chat-with-history/index.tsx
Normal file
195
web/app/components/base/chat/chat-with-history/index.tsx
Normal file
@@ -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
web/app/components/base/chat/chat-with-history/sidebar/index.tsx
Normal file
141
web/app/components/base/chat/chat-with-history/sidebar/index.tsx
Normal file
@@ -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
|
||||
@@ -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)
|
||||
@@ -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
|
||||
@@ -82,7 +82,7 @@ const Answer: FC<AnswerProps> = ({
|
||||
)
|
||||
}
|
||||
{
|
||||
hasAgentThoughts && !content && (
|
||||
hasAgentThoughts && (
|
||||
<AgentContent item={item} />
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -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'
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
web/app/components/base/chat/constants.ts
Normal file
1
web/app/components/base/chat/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const CONVERSATION_ID_INFO = 'conversationIdInfo'
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user