596 lines
23 KiB
TypeScript
596 lines
23 KiB
TypeScript
/*
|
|
* @Author: ZhaoYing
|
|
* @Date: 2026-02-03 16:58:03
|
|
* @Last Modified by: ZhaoYing
|
|
* @Last Modified time: 2026-04-07 18:08:52
|
|
*/
|
|
/**
|
|
* Conversation Page
|
|
* Public conversation interface for shared applications
|
|
* Supports conversation history, streaming responses, and memory/web search features
|
|
*/
|
|
|
|
import { type FC, useState, useEffect, useRef } from 'react'
|
|
import { useParams, useLocation } from 'react-router-dom'
|
|
import { useTranslation } from 'react-i18next'
|
|
import InfiniteScroll from 'react-infinite-scroll-component';
|
|
import { Flex, Skeleton, App, Tooltip } from 'antd'
|
|
import clsx from 'clsx'
|
|
import dayjs from 'dayjs'
|
|
|
|
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken, getExperienceConfig } from '@/api/application'
|
|
import type { HistoryItem } from './types'
|
|
import Empty from '@/components/Empty'
|
|
import { formatDateTime } from '@/utils/format';
|
|
import { randomString } from '@/utils/common'
|
|
import ChatEmpty from '@/assets/images/empty/chatEmpty.png'
|
|
import Chat from '@/components/Chat'
|
|
import type { ChatItem } from '@/components/Chat/types'
|
|
import { type SSEMessage } from '@/utils/stream'
|
|
import { shareFileUploadUrlWithoutApiPrefix, getFileStatusById } from '@/api/fileStorage'
|
|
import ChatToolbar, { type ChatToolbarRef } from '@/components/Chat/ChatToolbar'
|
|
import type { Variable } from '@/views/Workflow/components/Properties/VariableList/types'
|
|
import type { Variable as AppVariable } from '@/views/ApplicationConfig/components/VariableList/types'
|
|
import type { FeaturesConfigForm } from '@/views/ApplicationConfig/types';
|
|
import { replaceVariables } from '@/views/ApplicationConfig/Agent'
|
|
|
|
const Conversation: FC = () => {
|
|
const { t } = useTranslation()
|
|
const { message: messageApi, modal } = App.useApp()
|
|
const { token } = useParams()
|
|
const location = useLocation()
|
|
const searchParams = new URLSearchParams(location.search)
|
|
const userId = searchParams.get('user_id')
|
|
const [loading, setLoading] = useState(false)
|
|
const [message, setMessage] = useState<string>('')
|
|
const [conversation_id, setConversationId] = useState<string | null>(null)
|
|
const [historyList, setHistoryList] = useState<HistoryItem[]>([])
|
|
const [groupHistoryList, setGroupHistoryList] = useState<Record<string, HistoryItem[]>>({})
|
|
const [chatList, setChatList] = useState<ChatItem[]>([])
|
|
const [pageLoading, setPageLoading] = useState(false);
|
|
const [page, setPage] = useState(1);
|
|
const [hasMore, setHasMore] = useState(true);
|
|
const scrollRef = useRef<HTMLDivElement>(null);
|
|
const toolbarRef = useRef<ChatToolbarRef>(null)
|
|
const audioPollingRef = useRef<Map<string, ReturnType<typeof setInterval>>>(new Map())
|
|
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
|
|
const [fileList, setFileList] = useState<any[]>([])
|
|
const [webSearch, setWebSearch] = useState(false)
|
|
const [isHasMemory, setIsHasMemory] = useState(false)
|
|
const [memory, setMemory] = useState(true)
|
|
const [features, setFeatures] = useState<FeaturesConfigForm>({} as FeaturesConfigForm)
|
|
const [config, setConfig] = useState<Record<string, any>>({})
|
|
const [audioStatusMap, setAudioStatusMap] = useState<Record<string, string>>({})
|
|
const streamLoadingRef = useRef(false)
|
|
const [isDeepThinking, setIsDeepThinking] = useState<Record<string, any>>({})
|
|
const [thinking, setThinking] = useState(false)
|
|
|
|
useEffect(() => {
|
|
return () => {
|
|
audioPollingRef.current.forEach((timer) => clearInterval(timer))
|
|
audioPollingRef.current.clear()
|
|
}
|
|
}, [])
|
|
|
|
useEffect(() => {
|
|
const shareToken = localStorage.getItem(`shareToken_${token}`)
|
|
setShareToken(shareToken)
|
|
if (shareToken && shareToken !== '') return
|
|
getShareToken(token as string, userId || randomString(12, false))
|
|
.then(res => {
|
|
const response = res as { access_token: string } || {}
|
|
localStorage.setItem(`shareToken_${token}`, response.access_token ?? '')
|
|
setShareToken(response.access_token ?? '')
|
|
})
|
|
}, [token])
|
|
|
|
useEffect(() => {
|
|
if (token && page === 1 && hasMore && historyList.length === 0 && shareToken) {
|
|
getHistory()
|
|
}
|
|
}, [token, shareToken, page, hasMore, historyList])
|
|
|
|
useEffect(() => {
|
|
if (shareToken && token) {
|
|
getExperienceConfig(token)
|
|
.then(res => {
|
|
const response = res as { variables: Variable[]; features: FeaturesConfigForm; model_parameters?: Record<string, any>; app_type: string; memory: boolean; }
|
|
toolbarRef.current?.setVariables(response.variables || [])
|
|
setConfig(response)
|
|
setFeatures(response.features)
|
|
setIsHasMemory((response.app_type === 'workflow' && response.memory) || response.memory)
|
|
setIsDeepThinking(response.model_parameters?.deep_thinking || false)
|
|
})
|
|
} else {
|
|
setChatList([])
|
|
}
|
|
}, [shareToken, token])
|
|
|
|
/** Group conversation history by date */
|
|
const groupHistoryByDate = (items: HistoryItem[]): Record<string, HistoryItem[]> => {
|
|
return items.reduce((groups: Record<string, HistoryItem[]>, item) => {
|
|
const date = formatDateTime(item.created_at, 'YYYY-MM-DD')
|
|
|
|
if (!groups[date]) {
|
|
groups[date] = [];
|
|
}
|
|
groups[date].push(item);
|
|
return groups;
|
|
}, {});
|
|
}
|
|
|
|
/** Fetch conversation history with pagination */
|
|
const getHistory = (flag: boolean = false) => {
|
|
if (!token || (pageLoading || !hasMore) && !flag) return
|
|
setPageLoading(true);
|
|
getConversationHistory(token, { page: flag ? 1 : page, pagesize: 20 })
|
|
.then(res => {
|
|
const response = res as { items: HistoryItem[], page: { hasnext: boolean; page: number; pagesize: number; total: number } }
|
|
const results = response?.items || []
|
|
let list = []
|
|
if (flag) {
|
|
setHistoryList(results);
|
|
list = [...results]
|
|
} else {
|
|
setHistoryList(historyList.concat(results));
|
|
list = [...historyList, ...results]
|
|
}
|
|
setHistoryList(list)
|
|
setGroupHistoryList(groupHistoryByDate(list))
|
|
if (page === 1 && !flag) {
|
|
setConversationId(list[0]?.id || '')
|
|
}
|
|
setPage(response.page.page + 1);
|
|
setHasMore(response.page.hasnext);
|
|
setLoading(false);
|
|
})
|
|
.finally(() => setPageLoading(false))
|
|
}
|
|
/** Switch to different conversation or start new one */
|
|
const handleChangeHistory = (id: string | null) => {
|
|
if (id !== conversation_id) setConversationId(id)
|
|
if (!id) setMessage('')
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (conversation_id) {
|
|
getConversationDetail(token as string, conversation_id)
|
|
.then(res => {
|
|
const response = res as { messages: ChatItem[] }
|
|
const messages = response?.messages || []
|
|
const historyAudioUrls = new Set(messages.map(m => m.meta_data?.audio_url).filter(Boolean))
|
|
audioPollingRef.current.forEach((timer, key) => {
|
|
if (!historyAudioUrls.has(key)) {
|
|
clearInterval(timer)
|
|
audioPollingRef.current.delete(key)
|
|
}
|
|
})
|
|
messages.forEach(msg => {
|
|
if (msg.role === 'assistant' && msg.meta_data?.audio_url && msg.meta_data?.audio_status === 'pending') {
|
|
startAudioPolling(msg.meta_data.audio_url, msg.meta_data.audio_url)
|
|
}
|
|
})
|
|
setChatList(messages.map(msg => {
|
|
if (msg.role === 'assistant' && msg.meta_data?.audio_url && audioPollingRef.current.has(msg.meta_data.audio_url)) {
|
|
return { ...msg, meta_data: { ...msg.meta_data, audio_status: 'pending' } }
|
|
}
|
|
return msg
|
|
}))
|
|
})
|
|
} else {
|
|
if (features?.opening_statement?.enabled && features?.opening_statement?.statement) {
|
|
setChatList([{
|
|
role: 'assistant',
|
|
content: features.opening_statement.statement,
|
|
created_at: Date.now(),
|
|
meta_data: {
|
|
suggested_questions: features.opening_statement?.suggested_questions
|
|
}
|
|
}])
|
|
} else {
|
|
setChatList([])
|
|
}
|
|
}
|
|
}, [conversation_id, features?.opening_statement?.statement])
|
|
|
|
const addUserMessage = (message: string = '', files?: any[]) => {
|
|
setChatList(prev => [...prev, {
|
|
conversation_id,
|
|
role: 'user',
|
|
content: message,
|
|
created_at: Date.now(),
|
|
meta_data: {
|
|
files
|
|
},
|
|
}])
|
|
}
|
|
|
|
const addAssistantMessage = () => {
|
|
setChatList(prev => [...prev, {
|
|
created_at: Date.now(),
|
|
role: 'assistant',
|
|
content: ''
|
|
}])
|
|
}
|
|
|
|
const updateAssistantMessage = (content: string = '', audio_url?: string, audio_status?: string, citations?: any[]) => {
|
|
if (!content && !audio_url && (!citations || citations?.length < 1)) return
|
|
if (streamLoadingRef.current) streamLoadingRef.current = false
|
|
setChatList(prev => {
|
|
const lastList = [...prev]
|
|
const lastIndex = lastList.length - 1
|
|
const lastMsg = lastList[lastIndex]
|
|
if (lastMsg?.role === 'assistant') {
|
|
return [
|
|
...lastList.slice(0, lastIndex),
|
|
{
|
|
...lastMsg,
|
|
content: lastMsg.content + content,
|
|
meta_data: {
|
|
...(lastMsg.meta_data || {}),
|
|
audio_url: audio_url || lastMsg.meta_data?.audio_url,
|
|
audio_status: audio_status || lastMsg.meta_data?.audio_status,
|
|
citations: citations || lastMsg.meta_data?.citations
|
|
}
|
|
}
|
|
]
|
|
}
|
|
return prev
|
|
})
|
|
}
|
|
const updateAssistantReasoningMessage = (content: string = '') => {
|
|
if (!content) return
|
|
if (streamLoadingRef.current) streamLoadingRef.current = false
|
|
setChatList(prev => {
|
|
const lastList = [...prev]
|
|
const lastIndex = lastList.length - 1
|
|
const lastMsg = lastList[lastIndex]
|
|
if (lastMsg?.role === 'assistant') {
|
|
return [
|
|
...lastList.slice(0, lastIndex),
|
|
{
|
|
...lastMsg,
|
|
meta_data: {
|
|
...(lastMsg.meta_data || {}),
|
|
reasoning_content: (lastMsg.meta_data?.reasoning_content || '') + content
|
|
}
|
|
}
|
|
]
|
|
}
|
|
return prev
|
|
})
|
|
}
|
|
useEffect(() => {
|
|
if (!Object.keys(audioStatusMap).length) return
|
|
setChatList(prev => prev.map(msg => {
|
|
if (msg.role === 'assistant' && msg.meta_data?.audio_url && audioStatusMap[msg.meta_data.audio_url]) {
|
|
return {
|
|
...msg,
|
|
meta_data: {
|
|
...msg.meta_data,
|
|
audio_status: audioStatusMap[msg.meta_data.audio_url]
|
|
}
|
|
}
|
|
}
|
|
return msg
|
|
}))
|
|
}, [audioStatusMap, chatList.length])
|
|
|
|
const startAudioPolling = (audioUrl: string, idToPoll: string) => {
|
|
if (audioPollingRef.current.has(idToPoll)) return
|
|
const fileId = audioUrl.split('/').pop()
|
|
if (!fileId) return
|
|
const timer = setInterval(() => {
|
|
getFileStatusById(fileId)
|
|
.then(res => {
|
|
const { status } = res as { status: string }
|
|
if (status && status !== 'pending') {
|
|
setAudioStatusMap(prev => ({ ...prev, [idToPoll]: status }))
|
|
clearInterval(audioPollingRef.current.get(idToPoll))
|
|
audioPollingRef.current.delete(idToPoll)
|
|
}
|
|
})
|
|
.catch(() => {
|
|
clearInterval(audioPollingRef.current.get(idToPoll))
|
|
audioPollingRef.current.delete(idToPoll)
|
|
})
|
|
}, 2000)
|
|
audioPollingRef.current.set(idToPoll, timer)
|
|
}
|
|
|
|
/** Send message and handle streaming response */
|
|
const handleSend = (msg?: string) => {
|
|
if (!token || !shareToken) return
|
|
const files = (toolbarRef.current?.getFiles() || []).filter(item => !['uploading', 'error'].includes(item.status))
|
|
const variables = toolbarRef.current?.getVariables() || []
|
|
let isCanSend = true
|
|
const params: Record<string, any> = {}
|
|
if (variables.length > 0) {
|
|
const needRequired: string[] = []
|
|
variables.forEach(vo => {
|
|
params[vo.name] = vo.value ?? vo.defaultValue
|
|
if (vo.required && (params[vo.name] === null || params[vo.name] === undefined || params[vo.name] === '')) {
|
|
isCanSend = false
|
|
needRequired.push(vo.name)
|
|
}
|
|
})
|
|
|
|
if (needRequired.length) {
|
|
messageApi.error(`${needRequired.join(',')} ${t('workflow.variableRequired')}`)
|
|
}
|
|
}
|
|
if (!isCanSend) return
|
|
|
|
setLoading(true)
|
|
streamLoadingRef.current = true
|
|
addUserMessage(msg || message, files)
|
|
addAssistantMessage()
|
|
toolbarRef.current?.setFiles([])
|
|
setFileList([])
|
|
|
|
let currentConversationId: string | null = null
|
|
const handleStreamMessage = (data: SSEMessage[]) => {
|
|
data.forEach((item) => {
|
|
const { content, conversation_id: curId, audio_url, citations } = item.data as {
|
|
content: string; conversation_id: string; audio_url?: string;
|
|
citations?: {
|
|
document_id: string;
|
|
file_name: string;
|
|
knowledge_id: string;
|
|
score: string;
|
|
}[]
|
|
}
|
|
switch (item.event) {
|
|
case 'start':
|
|
case 'node_start':
|
|
const { conversation_id: newId } = item.data as { conversation_id: string }
|
|
currentConversationId = newId
|
|
break
|
|
case 'reasoning':
|
|
updateAssistantReasoningMessage(content)
|
|
if (curId) currentConversationId = curId;
|
|
break
|
|
case 'message':
|
|
updateAssistantMessage(content, audio_url, audio_url ? 'pending' : undefined)
|
|
if (curId) currentConversationId = curId;
|
|
break
|
|
case 'end':
|
|
case 'workflow_end':
|
|
if (audio_url) {
|
|
updateAssistantMessage(content, audio_url, 'pending', citations)
|
|
const { file_id } = item.data as { file_id?: string }
|
|
const idToPoll = file_id || audio_url || ''
|
|
const fileId = audio_url.split('/').pop()
|
|
if (fileId && idToPoll) {
|
|
startAudioPolling(audio_url, idToPoll)
|
|
}
|
|
} else {
|
|
getHistory(true)
|
|
if (currentConversationId && currentConversationId !== conversation_id) {
|
|
setConversationId(currentConversationId)
|
|
}
|
|
}
|
|
if (citations && citations.length > 0) {
|
|
updateAssistantMessage(content, audio_url, undefined, citations)
|
|
}
|
|
setLoading(false)
|
|
getHistory(true)
|
|
if (currentConversationId && currentConversationId !== conversation_id) {
|
|
setConversationId(currentConversationId)
|
|
}
|
|
break
|
|
}
|
|
})
|
|
};
|
|
|
|
sendConversation({
|
|
web_search: webSearch,
|
|
memory,
|
|
message: msg || message || '',
|
|
stream: true,
|
|
conversation_id: conversation_id || null,
|
|
files: files.map(file => {
|
|
if (file.url) {
|
|
return file
|
|
} else {
|
|
return {
|
|
type: file.type,
|
|
transfer_method: 'local_file',
|
|
upload_file_id: file.response.data.file_id
|
|
}
|
|
}
|
|
}),
|
|
variables: params,
|
|
thinking,
|
|
}, handleStreamMessage, shareToken)
|
|
.catch(() => {
|
|
setLoading(false)
|
|
streamLoadingRef.current = false
|
|
})
|
|
.finally(() => {
|
|
setLoading(false)
|
|
streamLoadingRef.current = false
|
|
})
|
|
}
|
|
|
|
const handleChangeMemory = () => {
|
|
if (config.app_type === 'workflow') return;
|
|
let value = !memory
|
|
modal.confirm({
|
|
title: value ? t('memoryConversation.memoryTipTitle') : t('memoryConversation.memoryCancelTipTitle'),
|
|
okText: t('common.confirm'),
|
|
cancelText: t('common.cancel'),
|
|
onOk: () => {
|
|
setMemory(value)
|
|
},
|
|
onCancel: () => {
|
|
setMemory(!value)
|
|
}
|
|
})
|
|
}
|
|
const handleChangeDeepThinking = () => {
|
|
setThinking(prev => !prev)
|
|
}
|
|
|
|
const handleChangeVariables = (variables: Variable[]) => {
|
|
setChatList(prev => {
|
|
const firstMsg = prev[0]
|
|
console.log('firstMsg', firstMsg)
|
|
if (firstMsg && firstMsg.role === 'assistant' && firstMsg.content && features?.opening_statement?.enabled && features?.opening_statement.statement && variables.length > 0) {
|
|
firstMsg.content = replaceVariables(features?.opening_statement.statement, variables as unknown as AppVariable[])
|
|
}
|
|
return [firstMsg, ...prev.slice(1)]
|
|
})
|
|
}
|
|
|
|
console.log('chatList', chatList, streamLoadingRef.current)
|
|
|
|
return (
|
|
<Flex className="rb:w-full rb:p-[-16px]!">
|
|
<div className="rb:w-80 rb:h-screen rb:bg-[#F6F6F6] rb:overflow-hidden">
|
|
<Flex align="center" gap={8} className="rb:p-5!">
|
|
<div className="rb:size-6 rb:bg-cover rb:bg-[url('@/assets/images/conversation/redbear.png')]"></div>
|
|
<div className="rb:text-[16px] rb:leading-5 rb:font-[Gilroy-Extrabold] rb:font-extrabold">{t('memoryConversation.chatTitle')}</div>
|
|
</Flex>
|
|
|
|
<Flex align="center" gap={12}
|
|
className="rb:cursor-pointer rb:border rb:border-[#155EEF] rb:rounded-xl rb:p-3! rb:mx-4! rb:text-[16px] rb:font-medium rb:text-[#155EEF] rb:h-12! rb:mb-5!"
|
|
onClick={() => handleChangeHistory(null)}
|
|
>
|
|
<div
|
|
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:mr-2 rb:bg-cover rb:bg-[url('@/assets/images/conversation/conversation.svg')] rb:group-hover:bg-[url('@/assets/images/conversation/conversation_hover.svg')]"
|
|
></div>
|
|
{t('memoryConversation.startANewConversation')}
|
|
</Flex>
|
|
{historyList.length > 0 &&
|
|
<div
|
|
ref={scrollRef}
|
|
id="scrollableDiv"
|
|
className="rb:overflow-y-auto rb:h-[calc(100vh-144px)] rb:px-3!"
|
|
>
|
|
<InfiniteScroll
|
|
dataLength={historyList.length}
|
|
next={getHistory}
|
|
hasMore={hasMore}
|
|
loader={<Skeleton active />}
|
|
scrollableTarget="scrollableDiv"
|
|
>
|
|
{Object.entries(groupHistoryList).map(([date, items]) => (
|
|
<div key={date} className="rb:mt-6 rb:first:mt-0">
|
|
<div className="rb:leading-5 rb:text-[#5B6167] rb:mb-2 rb:pl-1 rb:font-regular">{date.replace(/\u200e|\u200f/g, '')}</div>
|
|
|
|
<Flex vertical gap={4}>
|
|
{items.map(item => (
|
|
<div key={item.updated_at} className="rb:mb-3">
|
|
<div className={clsx("rb:p-[8px_13px] rb:rounded-lg rb:leading-5 rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
|
'rb:bg-[#FFFFFF] rb:shadow-[0px_2px_4px_0px_rgba(0,0,0,0.15)] rb:font-medium rb:hover:bg-[#FFFFFF]!': item.id === conversation_id,
|
|
})}
|
|
onClick={() => handleChangeHistory(item.id)}
|
|
>
|
|
{item.title}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</Flex>
|
|
</div>
|
|
))}
|
|
</InfiniteScroll>
|
|
</div>
|
|
}
|
|
</div>
|
|
|
|
<div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]">
|
|
<div className='rb:w-190 rb:h-screen rb:mx-auto rb:pt-10 rb:pb-3'>
|
|
<Chat
|
|
empty={<Empty url={ChatEmpty} className="rb:h-full" size={[320,180]} title={t('memoryConversation.chatEmpty')} subTitle={t('memoryConversation.emptyDesc')} />}
|
|
contentClassName={!fileList.length ? "rb:h-[calc(100%-144px)] rb:w-full" : "rb:h-[calc(100%-208px)] rb:w-full"}
|
|
data={chatList}
|
|
streamLoading={streamLoadingRef.current}
|
|
loading={loading}
|
|
onChange={setMessage}
|
|
onSend={handleSend}
|
|
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
|
conversationId={conversation_id}
|
|
fileList={fileList}
|
|
fileChange={(list) => {
|
|
setFileList(list || [])
|
|
toolbarRef.current?.setFiles(list || [])
|
|
}}
|
|
>
|
|
<ChatToolbar
|
|
ref={toolbarRef}
|
|
features={features}
|
|
onFilesChange={setFileList}
|
|
uploadAction={shareFileUploadUrlWithoutApiPrefix}
|
|
uploadRequestConfig={{
|
|
headers: {
|
|
'Content-Type': 'multipart/form-data',
|
|
Authorization: `Bearer ${shareToken || ''}`,
|
|
}
|
|
}}
|
|
rightExtra={
|
|
(features?.web_search?.enabled || isHasMemory || isDeepThinking)
|
|
? <Flex align="center" justify="end" gap={8}>
|
|
{isDeepThinking &&
|
|
<Tooltip title={t('memoryConversation.deepThinking')}>
|
|
<Flex justify="center" align="center"
|
|
className={clsx("rb:size-7 rb:cursor-pointer rb:border rb:hover:bg-[#F6F6F6] rb:rounded-full rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]", {
|
|
'rb:bg-[rgba(21,94,239,0.06)] rb:border-[rgba(21,94,239,0.25)]': thinking,
|
|
'rb:border-[#EBEBEB]': !thinking,
|
|
})}
|
|
onClick={handleChangeDeepThinking}
|
|
>
|
|
<div className={clsx("rb:size-4 rb:bg-cover", {
|
|
"rb:bg-[url('@/assets/images/conversation/deepThinking.svg')]": !thinking,
|
|
"rb:bg-[url('@/assets/images/conversation/deepThinkingChecked.svg')]": thinking
|
|
})} />
|
|
</Flex>
|
|
</Tooltip>
|
|
}
|
|
{features?.web_search?.enabled &&
|
|
<Tooltip title={t('memoryConversation.web_search')}>
|
|
<Flex justify="center" align="center"
|
|
className={clsx("rb:size-7 rb:border rb:cursor-pointer rb:hover:bg-[#F6F6F6] rb:rounded-full rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]", {
|
|
'rb:bg-[rgba(21,94,239,0.06)] rb:border-[rgba(21,94,239,0.25)]': webSearch,
|
|
'rb:border-[#EBEBEB]': !webSearch,
|
|
})}
|
|
onClick={() => setWebSearch(prev => !prev)}
|
|
>
|
|
<div className={clsx("rb:size-4 rb:bg-cover", {
|
|
"rb:bg-[url('@/assets/images/conversation/online.svg')]": !webSearch,
|
|
"rb:bg-[url('@/assets/images/conversation/onlineChecked.svg')]": webSearch
|
|
})} />
|
|
</Flex>
|
|
</Tooltip>
|
|
}
|
|
{isHasMemory &&
|
|
<Tooltip title={t('memoryConversation.memory')}>
|
|
<Flex justify="center" align="center"
|
|
className={clsx("rb:size-7 rb:border rb:hover:bg-[#F6F6F6] rb:rounded-full rb:shadow-[0px_2px_12px_0px_rgba(23,23,25,0.12)]", {
|
|
'rb:bg-[rgba(21,94,239,0.06)] rb:border-[rgba(21,94,239,0.25)]': memory,
|
|
'rb:border-[#EBEBEB]': !memory,
|
|
'rb:cursor-pointer': config.app_type !== 'workflow',
|
|
'rb:cursor-not-allowed rb:opacity-65': config.app_type === 'workflow',
|
|
})}
|
|
onClick={handleChangeMemory}
|
|
>
|
|
<div className={clsx("rb:size-4 rb:bg-cover", {
|
|
"rb:bg-[url('@/assets/images/conversation/memoryFunction.svg')]": !memory,
|
|
"rb:bg-[url('@/assets/images/conversation/memoryFunctionChecked.svg')]": memory
|
|
})} />
|
|
</Flex>
|
|
</Tooltip>
|
|
}
|
|
</Flex>
|
|
: undefined
|
|
}
|
|
onVariablesChange={handleChangeVariables}
|
|
/>
|
|
</Chat>
|
|
</div>
|
|
</div>
|
|
</Flex>
|
|
)
|
|
}
|
|
export default Conversation
|