/* * @Author: ZhaoYing * @Date: 2026-02-03 16:58:03 * @Last Modified by: ZhaoYing * @Last Modified time: 2026-02-03 16:58:35 */ /** * 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, Form } from 'antd' import clsx from 'clsx' import dayjs from 'dayjs' import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application' import type { HistoryItem, QueryParams } from './types' import Empty from '@/components/Empty' import { formatDateTime } from '@/utils/format'; import { randomString } from '@/utils/common' import BgImg from '@/assets/images/conversation/bg.png' import ChatEmpty from '@/assets/images/empty/chatEmpty.png' import Chat from '@/components/Chat' import type { ChatItem } from '@/components/Chat/types' import ButtonCheckbox from '@/components/ButtonCheckbox' import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg' import OnlineIcon from '@/assets/images/conversation/online.svg' import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg' import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg' import { type SSEMessage } from '@/utils/stream' /** * Conversation component for shared applications */ const Conversation: FC = () => { const { t } = useTranslation() const { token } = useParams() const location = useLocation() const searchParams = new URLSearchParams(location.search) const userId = searchParams.get('user_id') const [loading, setLoading] = useState(false) const [streamLoading, setStreamLoading] = useState(false) const [message, setMessage] = useState('') const [conversation_id, setConversationId] = useState(null) const [historyList, setHistoryList] = useState([]) const [groupHistoryList, setGroupHistoryList] = useState>({}) const [chatList, setChatList] = useState([]) const [pageLoading, setPageLoading] = useState(false); const [page, setPage] = useState(1); const [hasMore, setHasMore] = useState(true); const scrollRef = useRef(null); const [shareToken, setShareToken] = useState(localStorage.getItem(`shareToken_${token}`)) const [form] = Form.useForm() const queryValues = Form.useWatch([], form) 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]) /** Group conversation history by date */ const groupHistoryByDate = (items: HistoryItem[]): Record => { return items.reduce((groups: Record, 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[] } setChatList(response?.messages || []) }) } else { setChatList([]) } }, [conversation_id]) /** Add user message to chat */ const addUserMessage = (message: string = '') => { const newUserMessage: ChatItem = { conversation_id, role: 'user', content: message, created_at: Date.now() }; setChatList(prev => [...prev, newUserMessage]) } /** Add empty assistant message placeholder */ const addAssistantMessage = () => { const newAssistantMessage: ChatItem = { created_at: Date.now(), role: 'assistant', content: '', } setChatList(prev => [...prev, newAssistantMessage]) } /** Update assistant message with streaming content */ const updateAssistantMessage = (content: string = '') => { if (!content) return if (streamLoading) { setStreamLoading(false) } setChatList(prev => { const lastList = [...prev] const lastIndex = lastList.length - 1 const lastMsg = lastList[lastIndex] if (lastMsg?.role === 'assistant') { return [ ...lastList.slice(0, lastList.length - 1), { ...lastMsg, content: lastMsg.content + content } ] } return prev }) } /** Send message and handle streaming response */ const handleSend = () => { if (!token || !shareToken) { return } setLoading(true) setStreamLoading(true) addUserMessage(message) addAssistantMessage() let currentConversationId: string | null = null const handleStreamMessage = (data: SSEMessage[]) => { data.forEach((item) => { switch(item.event) { case 'start': case 'node_start': const { conversation_id: newId } = item.data as { conversation_id: string } currentConversationId = newId break case 'message': const { content, chunk, conversation_id: curId } = item.data as { content: string; chunk: string; conversation_id: string; } updateAssistantMessage(content ?? chunk) if (curId) { currentConversationId = curId; } break case 'end': case 'workflow_end': setLoading(false) if (currentConversationId && currentConversationId !== conversation_id) { setConversationId(currentConversationId) } getHistory(true) break } }) }; sendConversation({ ...queryValues, message: message || '', stream: true, conversation_id: conversation_id || null, }, handleStreamMessage, shareToken) .finally(() => { setLoading(false) }) } return (
handleChangeHistory(null)} >
{t('memoryConversation.startANewConversation')}
{historyList.length > 0 &&
} // endMessage={It is all, nothing more 🤐} scrollableTarget="scrollableDiv" > {Object.entries(groupHistoryList).map(([date, items]) => (
{date.replace(/\u200e|\u200f/g, '')}
{items.map(item => (
handleChangeHistory(item.id)} > {item.title}
))}
))}
}
} contentClassName="rb:h-[calc(100%-152px)] " data={chatList} streamLoading={streamLoading} loading={loading} onChange={setMessage} onSend={handleSend} labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')} >
{t(`memoryConversation.web_search`)} {t(`memoryConversation.memory`)}
) } export default Conversation