optimize: 1. stream request optimize; 2. replace Chat component
This commit is contained in:
@@ -1,7 +1,8 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { Application } from '@/views/ApplicationManagement/types'
|
||||
import type { Config } from '@/views/ApplicationConfig/types'
|
||||
import { handleSSE } from '@/utils/stream'
|
||||
import { handleSSE, type SSEMessage } from '@/utils/stream'
|
||||
import type { QueryParams } from '@/views/Conversation/types'
|
||||
|
||||
// 应用列表
|
||||
export const getApplicationListUrl = '/apps'
|
||||
@@ -37,10 +38,10 @@ export const saveMultiAgentConfig = (app_id: string, values: Config) => {
|
||||
return request.put(`/apps/${app_id}/multi-agent`, values)
|
||||
}
|
||||
// 模型比对试运行
|
||||
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: string) => void) => {
|
||||
export const runCompare = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE(`/apps/${app_id}/draft/run/compare`, values, onMessage)
|
||||
}
|
||||
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: string) => void) => {
|
||||
export const draftRun = (app_id: string, values: Record<string, unknown>, onMessage?: (data: SSEMessage[]) => void) => {
|
||||
return handleSSE(`/apps/${app_id}/draft/run`, values, onMessage)
|
||||
}
|
||||
// 删除应用
|
||||
@@ -76,18 +77,7 @@ export const getConversationHistory = (share_token: string, data: { page: number
|
||||
})
|
||||
}
|
||||
// 发送体验对话
|
||||
export const sendConversation = (share_token: string, values: {
|
||||
message: string;
|
||||
web_search: boolean;
|
||||
memory: boolean;
|
||||
stream: boolean;
|
||||
conversation_id: string | null;
|
||||
}, onMessage, shareToken: string) => {
|
||||
// return request.post(`/public/share/chat`, values, {
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${localStorage.getItem(`shareToken_${share_token}`)}`
|
||||
// }
|
||||
// })
|
||||
export const sendConversation = (values: QueryParams, onMessage: (data: SSEMessage[]) => void, shareToken: string) => {
|
||||
return handleSSE(`/public/share/chat`, values, onMessage, {
|
||||
headers: {
|
||||
'Authorization': `Bearer ${shareToken}`
|
||||
|
||||
@@ -3,7 +3,47 @@ import i18n from '@/i18n'
|
||||
import { cookieUtils } from './request'
|
||||
const API_PREFIX = '/api'
|
||||
|
||||
export const handleSSE = async (url: string, data: any, onMessage?: (data: string) => void, config = {}) => {
|
||||
export interface SSEMessage {
|
||||
event?: string
|
||||
data?: string | object
|
||||
}
|
||||
export function parseSSEToJSON(sseString: string) {
|
||||
const events: SSEMessage[] = []
|
||||
const lines = sseString.trim().split('\n')
|
||||
|
||||
let currentEvent: SSEMessage = {}
|
||||
|
||||
try {
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('event:')) {
|
||||
if (Object.keys(currentEvent).length > 0) {
|
||||
events.push(currentEvent)
|
||||
currentEvent = {}
|
||||
}
|
||||
currentEvent.event = line.substring(6).trim()
|
||||
} else if (line.startsWith('data:')) {
|
||||
const dataStr = line.substring(5).trim()
|
||||
try {
|
||||
currentEvent.data = JSON.parse(dataStr.replace(/"/g, '"'))
|
||||
} catch {
|
||||
currentEvent.data = dataStr
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (Object.keys(currentEvent).length > 0) {
|
||||
events.push(currentEvent)
|
||||
}
|
||||
|
||||
return events
|
||||
} catch (error) {
|
||||
console.error('Parse stream error:', error)
|
||||
return []
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export const handleSSE = async (url: string, data: any, onMessage?: (data: SSEMessage[]) => void, config = { headers: {} }) => {
|
||||
try {
|
||||
const token = cookieUtils.get('authToken');
|
||||
const response = await fetch(`${API_PREFIX}${url}`, {
|
||||
@@ -37,7 +77,7 @@ export const handleSSE = async (url: string, data: any, onMessage?: (data: strin
|
||||
|
||||
const chunk = decoder.decode(value, { stream: true });
|
||||
if (onMessage) {
|
||||
onMessage(chunk);
|
||||
onMessage(parseSSEToJSON(chunk) ?? {});
|
||||
}
|
||||
}
|
||||
break;
|
||||
|
||||
@@ -239,6 +239,7 @@ const Agent = forwardRef<AgentRef>((_props, ref) => {
|
||||
return [
|
||||
...(prev || []).map(item => ({
|
||||
...item,
|
||||
conversation_id: undefined,
|
||||
list: []
|
||||
})),
|
||||
newChatItem
|
||||
|
||||
@@ -1,46 +1,125 @@
|
||||
import { type FC, useRef, useEffect, useState } from 'react';
|
||||
import { type FC, useEffect, useState } from 'react';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import clsx from 'clsx'
|
||||
import { Input, Form } from 'antd'
|
||||
import ChatIcon from '@/assets/images/application/chat.png'
|
||||
import ChatSendIcon from '@/assets/images/application/chatSend.svg'
|
||||
import DebuggingEmpty from '@/assets/images/application/debuggingEmpty.png'
|
||||
import type { ChatItem, ChatData, Config } from '../types'
|
||||
import type { ChatData, Config } from '../types'
|
||||
import { runCompare, draftRun } from '@/api/application'
|
||||
import Empty from '@/components/Empty'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import ChatContent from '@/components/Chat/ChatContent'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
|
||||
interface ChatProps {
|
||||
chatList: ChatData[];
|
||||
data: Config;
|
||||
updateChatList: (list: ChatData[]) => void;
|
||||
updateChatList: React.Dispatch<React.SetStateAction<ChatData[]>>;
|
||||
handleSave: (flag?: boolean) => Promise<any>;
|
||||
source?: 'cluster' | 'agent';
|
||||
source?: 'multi_agent' | 'agent';
|
||||
}
|
||||
const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, source = 'agent' }) => {
|
||||
const { t } = useTranslation();
|
||||
const [form] = Form.useForm<{ message: string }>()
|
||||
const scrollContainerRefs = useRef<(HTMLDivElement | null)[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [isCluster, setIsCluster] = useState(source === 'cluster')
|
||||
const [isCluster, setIsCluster] = useState(source === 'multi_agent')
|
||||
const [conversationId, setConversationId] = useState<string | null>(null)
|
||||
const [compareLoading, setCompareLoading] = useState(false)
|
||||
|
||||
// 当聊天列表更新时,自动滚动到底部
|
||||
|
||||
useEffect(() => {
|
||||
// 延迟一下,确保DOM已经更新
|
||||
setTimeout(() => {
|
||||
scrollContainerRefs.current.forEach(container => {
|
||||
if (container) {
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
});
|
||||
}, 0);
|
||||
}, [chatList]);
|
||||
useEffect(() => {
|
||||
setIsCluster(source === 'cluster')
|
||||
setIsCluster(source === 'multi_agent')
|
||||
}, [source])
|
||||
|
||||
const addUserMessage = (message: string) => {
|
||||
const newUserMessage: ChatItem = {
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now(),
|
||||
};
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), newUserMessage]
|
||||
})))
|
||||
}
|
||||
const addAssistantMessage = () => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
};
|
||||
|
||||
if (isCluster) {
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
} else {
|
||||
const assistantMessages: Record<string, ChatItem> = {}
|
||||
chatList.forEach(item => {
|
||||
assistantMessages[item.model_config_id as string] = assistantMessage
|
||||
})
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessages[item.model_config_id as string]]
|
||||
})))
|
||||
}
|
||||
}
|
||||
const updateAssistantMessage = (content?: string, model_config_id?: string, conversation_id?: string) => {
|
||||
if (!content || !model_config_id) return
|
||||
updateChatList(prev => {
|
||||
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[targetIndex]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[targetIndex] = {
|
||||
...modelChatList[targetIndex],
|
||||
conversation_id: conversation_id,
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
}
|
||||
return prev;
|
||||
})
|
||||
}
|
||||
const updateErrorAssistantMessage = (message_length: number, model_config_id?: string) => {
|
||||
if (message_length > 0 || !model_config_id) return
|
||||
|
||||
updateChatList(prev => {
|
||||
const targetIndex = prev.findIndex(item => item.model_config_id === model_config_id);
|
||||
if (targetIndex > -1) {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[targetIndex]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[targetIndex] = {
|
||||
...modelChatList[targetIndex],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
}
|
||||
|
||||
return prev
|
||||
})
|
||||
}
|
||||
const handleSend = () => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
@@ -48,182 +127,47 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = form.getFieldValue('message')
|
||||
if (!message || message.trim() === '') return
|
||||
const newUserMessage: ChatItem = {
|
||||
role: 'question',
|
||||
content: message,
|
||||
time: Date.now(),
|
||||
};
|
||||
updateChatList((prev: ChatData[]) => {
|
||||
return prev.map(item => ({
|
||||
...item,
|
||||
list: [
|
||||
...(item.list || []),
|
||||
newUserMessage
|
||||
]
|
||||
}))
|
||||
})
|
||||
if (!message?.trim()) return
|
||||
|
||||
addUserMessage(message)
|
||||
form.setFieldsValue({ message: undefined })
|
||||
// 添加空的助手消息用于流式更新
|
||||
const assistantMessages: Record<string, ChatItem> = {};
|
||||
if (isCluster) {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'answer',
|
||||
content: '',
|
||||
time: Date.now(),
|
||||
};
|
||||
assistantMessages['cluster'] = assistantMessage;
|
||||
updateChatList((prev: ChatData[]) => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
} else {
|
||||
chatList.forEach(item => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'answer',
|
||||
content: '',
|
||||
time: Date.now(),
|
||||
};
|
||||
assistantMessages[item.model_config_id] = assistantMessage;
|
||||
});
|
||||
updateChatList((prev: ChatData[]) => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessages[item.model_config_id]]
|
||||
})))
|
||||
}
|
||||
addAssistantMessage()
|
||||
|
||||
const handleStreamMessage = (data: string) => {
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
setCompareLoading(false)
|
||||
try {
|
||||
const lines = data.split('\n');
|
||||
let currentEvent = '';
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.substring(6).trim();
|
||||
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_message')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.content && parsed.model_config_id) {
|
||||
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
conversation_id: parsed.conversation_id,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: msg.content + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'message')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
if (parsed.content) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: (msg.content || '') + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (line.startsWith('data:') && (!isCluster && currentEvent === 'model_end')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.message_length === 0 && parsed.model_config_id) {
|
||||
const targetIndex = chatList.findIndex(item => item.model_config_id === parsed.model_config_id);
|
||||
if (targetIndex !== -1) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === targetIndex) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: null };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
}
|
||||
} else if (line.startsWith('data:') && (isCluster && currentEvent === 'model_end')) {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
if (parsed.message_length === 0) {
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'answer') {
|
||||
return { ...msg, content: null };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
|
||||
if (parsed.conversation_id) {
|
||||
setConversationId(parsed.conversation_id);
|
||||
}
|
||||
} else if (currentEvent === 'compare_end') {
|
||||
data.map(item => {
|
||||
const { model_config_id, conversation_id, content, message_length } = item.data as { model_config_id: string; conversation_id: string; content: string; message_length: number };
|
||||
|
||||
switch(item.event) {
|
||||
case 'model_message':
|
||||
updateAssistantMessage(content, model_config_id, conversation_id)
|
||||
break;
|
||||
case 'model_end':
|
||||
updateErrorAssistantMessage(message_length, model_config_id)
|
||||
break;
|
||||
case 'compare_end':
|
||||
setLoading(false);
|
||||
}
|
||||
break;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse stream data error:', e);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
if (isCluster) {
|
||||
draftRun(data.app_id, { message, conversation_id: conversationId, stream: true }, handleStreamMessage)
|
||||
.finally(() => setLoading(false))
|
||||
} else {
|
||||
runCompare(data.app_id, {
|
||||
message,
|
||||
models: chatList.map(item => ({
|
||||
model_config_id: item.model_config_id,
|
||||
label: item.label,
|
||||
model_parameters: item.model_parameters,
|
||||
conversation_id: item.conversation_id
|
||||
})),
|
||||
variables: {},
|
||||
"parallel": true,
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
}, handleStreamMessage)
|
||||
.finally(() => setLoading(false));
|
||||
}
|
||||
runCompare(data.app_id, {
|
||||
message,
|
||||
models: chatList.map(item => ({
|
||||
model_config_id: item.model_config_id,
|
||||
label: item.label,
|
||||
model_parameters: item.model_parameters,
|
||||
conversation_id: item.conversation_id
|
||||
})),
|
||||
variables: {},
|
||||
"parallel": true,
|
||||
"stream": true,
|
||||
"timeout": 60,
|
||||
}, handleStreamMessage)
|
||||
.finally(() => setLoading(false));
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
@@ -231,6 +175,131 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const addClusterAssistantMessage = () => {
|
||||
const assistantMessage: ChatItem = {
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
created_at: Date.now(),
|
||||
};
|
||||
updateChatList(prev => prev.map(item => ({
|
||||
...item,
|
||||
list: [...(item.list || []), assistantMessage]
|
||||
})))
|
||||
}
|
||||
const updateClusterAssistantMessage = (content?: string) => {
|
||||
if (!content) return
|
||||
updateChatList(prev => {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[0]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[0] = {
|
||||
...modelChatList[0],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: lastMsg.content + content
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
})
|
||||
updateChatList((prev: ChatData[]) => prev.map((item, index) => {
|
||||
if (index === 0) {
|
||||
return {
|
||||
...item,
|
||||
list: item.list?.map((msg, msgIndex) => {
|
||||
if (msgIndex === item.list!.length - 1 && msg.role === 'assistant') {
|
||||
return { ...msg, content: (msg.content || '') + content };
|
||||
}
|
||||
return msg;
|
||||
}) || []
|
||||
};
|
||||
}
|
||||
return item;
|
||||
}))
|
||||
}
|
||||
const updateClusterErrorAssistantMessage = (message_length: number) => {
|
||||
if (message_length > 0) return
|
||||
|
||||
updateChatList(prev => {
|
||||
const modelChatList = [...prev]
|
||||
const curModelChat = modelChatList[0]
|
||||
const curChatMsgList = curModelChat.list || []
|
||||
const lastMsg = curChatMsgList[curChatMsgList.length - 1]
|
||||
if (lastMsg.role === 'assistant') {
|
||||
modelChatList[0] = {
|
||||
...modelChatList[0],
|
||||
list: [
|
||||
...curChatMsgList.slice(0, curChatMsgList.length - 1),
|
||||
{
|
||||
...lastMsg,
|
||||
content: null
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
return [...modelChatList]
|
||||
})
|
||||
}
|
||||
const handleClusterSend = () => {
|
||||
if (loading) return
|
||||
setLoading(true)
|
||||
setCompareLoading(true)
|
||||
handleSave(false)
|
||||
.then(() => {
|
||||
const message = form.getFieldValue('message')
|
||||
if (!message || message.trim() === '') return
|
||||
addUserMessage(message)
|
||||
form.setFieldsValue({ message: undefined })
|
||||
addClusterAssistantMessage()
|
||||
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
setCompareLoading(false)
|
||||
|
||||
data.map(item => {
|
||||
const { conversation_id, content, message_length } = item.data as { conversation_id: string, content: string, message_length: number };
|
||||
|
||||
switch(item.event) {
|
||||
case 'message':
|
||||
updateClusterAssistantMessage(content)
|
||||
if (conversation_id && conversationId !== conversation_id) {
|
||||
setConversationId(conversation_id);
|
||||
}
|
||||
break;
|
||||
case 'model_end':
|
||||
updateClusterErrorAssistantMessage(message_length)
|
||||
break;
|
||||
case 'compare_end':
|
||||
setLoading(false);
|
||||
break;
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
setTimeout(() => {
|
||||
draftRun(
|
||||
data.app_id,
|
||||
{
|
||||
message,
|
||||
conversation_id: conversationId,
|
||||
stream: true
|
||||
},
|
||||
handleStreamMessage
|
||||
)
|
||||
.finally(() => setLoading(false))
|
||||
}, 0)
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false)
|
||||
setCompareLoading(false)
|
||||
})
|
||||
}
|
||||
|
||||
const handleDelete = (index: number) => {
|
||||
updateChatList(chatList.filter((_, voIndex) => voIndex !== index))
|
||||
}
|
||||
@@ -258,69 +327,55 @@ const Chat: FC<ChatProps> = ({ chatList, data, updateChatList, handleSave, sourc
|
||||
<div className={clsx(
|
||||
"rb:grid rb:bg-[#F0F3F8] rb:text-center rb:flex-[0_0_auto]",
|
||||
{
|
||||
'rb:rounded-tr-[12px]': index === chatList.length - 1,
|
||||
'rb:rounded-tl-[12px]': index === 0,
|
||||
'rb:rounded-tr-xl': index === chatList.length - 1,
|
||||
'rb:rounded-tl-xl': index === 0,
|
||||
}
|
||||
)}>
|
||||
<div className='rb:relative rb:p-[10px_12px] rb:overflow-hidden'>
|
||||
<div className="rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:w-[calc(100%-24px)]">{chat.label}</div>
|
||||
<div
|
||||
className="rb:w-[16px] rb:h-[16px] rb:cursor-pointer rb:absolute rb:top-[12px] rb:right-[12px] rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
className="rb:w-4 rb:h-4 rb:cursor-pointer rb:absolute rb:top-3 rb:right-3 rb:bg-cover rb:bg-[url('@/assets/images/close.svg')] rb:hover:bg-[url('@/assets/images/close_hover.svg')]"
|
||||
onClick={() => handleDelete(index)}
|
||||
></div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{!chat.list || chat.list.length === 0
|
||||
? <Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} className="rb:h-full" size={[240, 200]} />
|
||||
: (
|
||||
<div ref={el => scrollContainerRefs.current[index] = el} className={clsx(`rb:relative rb:overflow-y-auto rb:overflow-x-hidden`, {
|
||||
'rb:h-[calc(100vh-186px)]': isCluster,
|
||||
'rb:h-[calc(100vh-286px)]': !isCluster,
|
||||
})}>
|
||||
{chat.list?.map((vo, voIndex) => {
|
||||
if (compareLoading && voIndex === chat.list?.length - 1) {
|
||||
return null
|
||||
}
|
||||
return (
|
||||
<div key={voIndex} className={clsx("rb:relative rb:mt-[24px]", {
|
||||
'rb:right-[16px] rb:text-right': vo.role === 'question',
|
||||
'rb:left-[16px] rb:text-left': vo.role !== 'question',
|
||||
})}>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[16px] rb:font-regular">{vo.role === 'question' ? 'You' : chat.label}</div>
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-[8px] rb:mt-[6px] rb:leading-[18px] rb:p-[10px_12px_2px_12px] rb:inline-block', {
|
||||
'rb:border-[rgba(255,93,52,0.30)] rb:bg-[rgba(255,93,52,0.08)] rb:text-[#FF5D34]': vo.role !== 'question' && vo.content === null,
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': vo.role === 'question' && vo.content,
|
||||
'rb:bg-[#ffffff] rb:border-[rgba(235,235,235,1)]': vo.role !== 'question' && (vo.content || vo.content === ''),
|
||||
'rb:max-w-[400px]': chatList.length === 1,
|
||||
'rb:max-w-[260px]': chatList.length === 2,
|
||||
'rb:max-w-[150px]': chatList.length === 3,
|
||||
'rb:max-w-[108px]': chatList.length === 4,
|
||||
})}>
|
||||
<Markdown content={vo.content === null ? t('application.ReplyException') : vo.content} />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
<ChatContent
|
||||
classNames={{
|
||||
'rb:mx-[16px] rb:pt-[24px]': true,
|
||||
'rb:h-[calc(100vh-186px)]': isCluster,
|
||||
'rb:h-[calc(100vh-286px)]': !isCluster,
|
||||
}}
|
||||
contentClassNames={{
|
||||
'rb:max-w-[400px]!': chatList.length === 1,
|
||||
'rb:max-w-[260px]!': chatList.length === 2,
|
||||
'rb:max-w-[150px]!': chatList.length === 3,
|
||||
'rb:max-w-[108px]!': chatList.length === 4,
|
||||
}}
|
||||
empty={<Empty url={ChatIcon} title={t('application.chatEmpty')} isNeedSubTitle={false} size={[240, 200]} className="rb:h-full" />}
|
||||
data={chat.list || []}
|
||||
streamLoading={compareLoading}
|
||||
labelPosition="top"
|
||||
labelFormat={(item) => item.role === 'user' ? 'You' : chat.label}
|
||||
errorDesc={t('application.ReplyException')}
|
||||
/>
|
||||
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="rb:flex rb:items-center rb:gap-[10px] rb:p-[16px]">
|
||||
<div className="rb:flex rb:items-center rb:gap-2.5 rb:p-4">
|
||||
<Form form={form} style={{width: 'calc(100% - 54px)'}}>
|
||||
<Form.Item name="message" className="rb:mb-[0]!">
|
||||
<Form.Item name="message" className="rb:mb-0!">
|
||||
<Input
|
||||
className="rb:h-[44px] rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
className="rb:h-11 rb:shadow-[0px_2px_8px_0px_rgba(33,35,50,0.1)]"
|
||||
placeholder={t('application.chatPlaceholder')}
|
||||
onPressEnter={handleSend}
|
||||
onPressEnter={isCluster ? handleClusterSend : handleSend}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-[44px] rb:h-[44px] rb:cursor-pointer", {
|
||||
<img src={ChatSendIcon} className={clsx("rb:w-11 rb:h-11 rb:cursor-pointer", {
|
||||
'rb:opacity-50': loading,
|
||||
})} onClick={handleSend} />
|
||||
})} onClick={isCluster ? handleClusterSend : handleSend} />
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { KnowledgeBaseListItem } from '@/views/KnowledgeBase/types'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
|
||||
export interface ModelConfig {
|
||||
label?: string;
|
||||
@@ -139,11 +140,6 @@ export interface ApiExtensionModalData {
|
||||
export interface ApiExtensionModalRef {
|
||||
handleOpen: () => void;
|
||||
}
|
||||
export interface ChatItem {
|
||||
role: 'answer' | 'question';
|
||||
content?: string;
|
||||
time: number;
|
||||
}
|
||||
export interface ChatData {
|
||||
label?: string;
|
||||
model_config_id?: string;
|
||||
|
||||
@@ -2,16 +2,24 @@ 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 } from 'antd'
|
||||
import { Flex, Skeleton, Form } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import Chat, { type ChatItem } from '@/views/MemoryConversation/components/Chat'
|
||||
import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.svg'
|
||||
import { getConversationHistory, sendConversation, getConversationDetail, getShareToken } from '@/api/application'
|
||||
import type { HistoryItem } from './types'
|
||||
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 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 dayjs from 'dayjs'
|
||||
import { type SSEMessage } from '@/utils/stream'
|
||||
|
||||
const Conversation: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
@@ -20,13 +28,8 @@ const Conversation: FC = () => {
|
||||
const searchParams = new URLSearchParams(location.search)
|
||||
const userId = searchParams.get('user_id')
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [chatLoading, setChatLoading] = useState(false)
|
||||
const [query, setQuery] = useState<{
|
||||
message?: string;
|
||||
web_search?: boolean;
|
||||
memory?: boolean;
|
||||
conversation_id?: string;
|
||||
}>({})
|
||||
const [streamLoading, setStreamLoading] = 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[]>>({})
|
||||
@@ -36,14 +39,18 @@ const Conversation: FC = () => {
|
||||
const [hasMore, setHasMore] = useState(true);
|
||||
const scrollRef = useRef<HTMLDivElement>(null);
|
||||
const [shareToken, setShareToken] = useState<string | null>(localStorage.getItem(`shareToken_${token}`))
|
||||
|
||||
const [form] = Form.useForm<QueryParams>()
|
||||
const queryValues = Form.useWatch<QueryParams>([], form)
|
||||
useEffect(() => {
|
||||
const shareToken = localStorage.getItem(`shareToken_${token}`)
|
||||
setShareToken(shareToken)
|
||||
if (shareToken && shareToken !== '') return
|
||||
getShareToken(token as string, userId || randomString(12, false))
|
||||
.then(res => {
|
||||
localStorage.setItem(`shareToken_${token}`, res?.access_token || '')
|
||||
setShareToken(res?.access_token || '')
|
||||
const response = res as { access_token: string } || {}
|
||||
localStorage.setItem(`shareToken_${token}`, response.access_token ?? '')
|
||||
setShareToken(response.access_token ?? '')
|
||||
})
|
||||
}, [token])
|
||||
|
||||
@@ -73,7 +80,7 @@ const Conversation: FC = () => {
|
||||
setPageLoading(true);
|
||||
getConversationHistory(token, { page: flag ? 1 : page, pagesize: 20 })
|
||||
.then(res => {
|
||||
const response = res as { items: HistoryItem[], page: { hasnext: boolean } }
|
||||
const response = res as { items: HistoryItem[], page: { hasnext: boolean; page: number; pagesize: number; total: number } }
|
||||
const results = response?.items || []
|
||||
let list = []
|
||||
if (flag) {
|
||||
@@ -101,7 +108,7 @@ const Conversation: FC = () => {
|
||||
setConversationId(id)
|
||||
}
|
||||
if (!id) {
|
||||
setQuery({})
|
||||
setMessage('')
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
@@ -116,72 +123,81 @@ const Conversation: FC = () => {
|
||||
}
|
||||
}, [conversation_id])
|
||||
|
||||
const addUserMessage = (message: string = '') => {
|
||||
const newUserMessage: ChatItem = {
|
||||
conversation_id,
|
||||
role: 'user',
|
||||
content: message,
|
||||
created_at: Date.now()
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
}
|
||||
const addAssistantMessage = () => {
|
||||
const newAssistantMessage: ChatItem = {
|
||||
created_at: Date.now(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}
|
||||
setChatList(prev => [...prev, newAssistantMessage])
|
||||
}
|
||||
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
|
||||
})
|
||||
}
|
||||
|
||||
const handleSend = () => {
|
||||
if (!token || !shareToken) {
|
||||
return
|
||||
}
|
||||
// 添加必需的id和conversation_id属性
|
||||
const newUserMessage: ChatItem = {
|
||||
conversation_id,
|
||||
role: 'user',
|
||||
content: query?.message || '',
|
||||
created_at: Date.now()
|
||||
};
|
||||
setChatList(prev => [...prev, newUserMessage])
|
||||
|
||||
setLoading(true)
|
||||
setChatLoading(true)
|
||||
setChatList(prev => [...prev, {
|
||||
created_at: Date.now(),
|
||||
role: 'assistant',
|
||||
content: '',
|
||||
}])
|
||||
let currentConversationId: string | null = null
|
||||
const handleStreamMessage = (data: string) => {
|
||||
setChatLoading(false)
|
||||
try {
|
||||
const lines = data.split('\n');
|
||||
let currentEvent = '';
|
||||
setStreamLoading(true)
|
||||
addUserMessage(message)
|
||||
addAssistantMessage()
|
||||
|
||||
|
||||
for (let i = 0; i < lines.length; i++) {
|
||||
const line = lines[i].trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
currentEvent = line.substring(6).trim();
|
||||
} else if (line.startsWith('data:') && currentEvent === 'message') {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
|
||||
if (parsed.content) {
|
||||
setChatList(prev => prev.map((msg, msgIndex) => {
|
||||
if (msgIndex === prev!.length - 1 && msg.role === 'assistant') {
|
||||
return { ...msg, content: msg.content + parsed.content };
|
||||
}
|
||||
return msg;
|
||||
}))
|
||||
}
|
||||
} else if (line.startsWith('data:') && currentEvent === 'start') {
|
||||
const jsonData = line.substring(5).trim();
|
||||
const parsed = JSON.parse(jsonData);
|
||||
currentConversationId = parsed.conversation_id
|
||||
} else if (currentEvent === 'end') {
|
||||
setLoading(false);
|
||||
let currentConversationId: string | null = null
|
||||
const handleStreamMessage = (data: SSEMessage[]) => {
|
||||
data.forEach((item) => {
|
||||
switch(item.event) {
|
||||
case 'start':
|
||||
const { conversation_id: newId } = item.data as { conversation_id: string }
|
||||
currentConversationId = newId
|
||||
break
|
||||
case 'message':
|
||||
const { content } = item.data as { content: string }
|
||||
updateAssistantMessage(content)
|
||||
break
|
||||
case 'end':
|
||||
setLoading(false)
|
||||
if (currentConversationId && currentConversationId !== conversation_id) {
|
||||
setConversationId(currentConversationId)
|
||||
getHistory(true)
|
||||
}
|
||||
}
|
||||
getHistory(true)
|
||||
break
|
||||
}
|
||||
} catch (e) {
|
||||
console.error('Parse stream data error:', e);
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
sendConversation(token as string, {
|
||||
message: query?.message || '',
|
||||
web_search: query?.web_search || false,
|
||||
memory: query?.memory || false,
|
||||
sendConversation({
|
||||
...queryValues,
|
||||
message: message || '',
|
||||
stream: true,
|
||||
conversation_id: conversation_id || null,
|
||||
}, handleStreamMessage, shareToken)
|
||||
@@ -192,12 +208,12 @@ const Conversation: FC = () => {
|
||||
|
||||
return (
|
||||
<Flex className="rb:w-full rb:p-[-16px]!">
|
||||
<div className="rb:w-[345px] rb:h-[100vh] rb:overflow-hidden rb:border-r rb:border-[#EAECEE] rb:p-[12px]">
|
||||
<div className="rb:group rb:flex rb:items-center rb:justify-center rb:font-regular rb:cursor-pointer rb:mb-[20px] rb:border rb:border-[#DFE4ED] rb:hover:border-[#155EEF] rb:hover:text-[#155EEF] rb:rounded-[8px] rb:py-[10px]"
|
||||
<div className="rb:w-[345px] rb:h-screen rb:overflow-hidden rb:border-r rb:border-[#EAECEE] rb:p-3">
|
||||
<div className="rb:group rb:flex rb:items-center rb:justify-center rb:font-regular rb:cursor-pointer rb:mb-5 rb:border rb:border-[#DFE4ED] rb:hover:border-[#155EEF] rb:hover:text-[#155EEF] rb:rounded-lg rb:py-2.5"
|
||||
onClick={() => handleChangeHistory(null)}
|
||||
>
|
||||
<div
|
||||
className="rb:w-[20px] rb:h-[20px] rb:cursor-pointer rb:mr-[8px] rb:bg-cover rb:bg-[url('@/assets/images/conversation/conversation.svg')] rb:group-hover:bg-[url('@/assets/images/conversation/conversation_hover.svg')]"
|
||||
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')}
|
||||
</div>
|
||||
@@ -216,11 +232,11 @@ const Conversation: FC = () => {
|
||||
scrollableTarget="scrollableDiv"
|
||||
>
|
||||
{Object.entries(groupHistoryList).map(([date, items]) => (
|
||||
<div key={date} className="rb:mt-[24px] rb:first:mt-0">
|
||||
<div className="rb:leading-[20px] rb:text-[#5B6167] rb:mb-[8px] rb:pl-[4px] rb:font-regular">{date.replace(/\u200e|\u200f/g, '')}</div>
|
||||
<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>
|
||||
{items.map(item => (
|
||||
<div key={item.updated_at} className="rb:mb-[12px]">
|
||||
<div className={clsx("rb:p-[8px_13px] rb:rounded-[8px] rb:leading-[20px] rb:cursor-pointer rb:hover:bg-[#F0F3F8]", {
|
||||
<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)}
|
||||
@@ -237,18 +253,38 @@ const Conversation: FC = () => {
|
||||
<img src={BgImg} className="rb:absolute rb:bottom-0 rb:left-0 rb:w-[345px]" />
|
||||
</div>
|
||||
|
||||
<div className="rb:relative rb:h-[100vh] rb:px-[16px] rb:flex-[1_1_auto]">
|
||||
<div className="rb:relative rb:h-screen rb:px-4 rb:flex-[1_1_auto]">
|
||||
<Chat
|
||||
source="conversation"
|
||||
empty={
|
||||
<Empty url={AnalysisEmptyIcon} subTitle={t('memoryConversation.emptyDesc')} />
|
||||
}
|
||||
query={query}
|
||||
empty={<Empty url={AnalysisEmptyIcon} className="rb:h-full" subTitle={t('memoryConversation.emptyDesc')} />}
|
||||
contentClassName="rb:h-[calc(100%-152px)]"
|
||||
data={chatList}
|
||||
streamLoading={streamLoading}
|
||||
loading={loading}
|
||||
onChange={setQuery}
|
||||
onChange={setMessage}
|
||||
onSend={handleSend}
|
||||
/>
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
>
|
||||
<Form form={form}>
|
||||
<Flex gap={8}>
|
||||
<Form.Item name="web_search" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.web_search`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="memory" valuePropName="checked" className="rb:mb-0!">
|
||||
<ButtonCheckbox
|
||||
icon={MemoryFunctionIcon}
|
||||
checkedIcon={MemoryFunctionCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.memory`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
</Form>
|
||||
</Chat>
|
||||
</div>
|
||||
</Flex>
|
||||
)
|
||||
|
||||
@@ -10,4 +10,12 @@ export interface HistoryItem {
|
||||
is_active: boolean;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
}
|
||||
|
||||
export interface QueryParams {
|
||||
message?: string;
|
||||
web_search?: boolean;
|
||||
memory?: boolean;
|
||||
stream: boolean;
|
||||
conversation_id?: string | null;
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
import { type FC, type ReactNode, useEffect, useRef, useState } from 'react'
|
||||
import { Flex } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
import ChatInput from './ChatInput'
|
||||
import type { TestParams } from '../index'
|
||||
import dayjs from 'dayjs'
|
||||
import Markdown from '@/components/Markdown'
|
||||
|
||||
interface ChatProps {
|
||||
empty?: ReactNode;
|
||||
data: ChatItem[];
|
||||
query?: TestParams;
|
||||
onChange: (query: TestParams) => void;
|
||||
onSend: () => void;
|
||||
loading: boolean;
|
||||
source?: 'conversation' | 'memory';
|
||||
}
|
||||
export interface ChatItem {
|
||||
id?: string;
|
||||
conversation_id?: string | null;
|
||||
role?: 'user' | 'assistant';
|
||||
content?: string;
|
||||
message?: string;
|
||||
created_at?: number | string;
|
||||
meta_data?: Record<string, string | number>[];
|
||||
}
|
||||
|
||||
const Chat: FC<ChatProps> = ({ empty, data, query, onChange, onSend, loading, source = 'memory' }) => {
|
||||
const scrollContainerRefs = useRef<(HTMLDivElement | null)>(null)
|
||||
const [isMemory, setIsMemory] = useState<boolean>(source === 'memory')
|
||||
|
||||
useEffect(() => {
|
||||
setIsMemory(source === 'memory')
|
||||
}, [source])
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
if (scrollContainerRefs.current) {
|
||||
scrollContainerRefs.current.scrollTop = scrollContainerRefs.current.scrollHeight;
|
||||
}
|
||||
}, 0);
|
||||
}, [data])
|
||||
|
||||
return (
|
||||
<div className="rb:h-full rb:relative rb:pt-[8px]">
|
||||
{data.length === 0 ? (
|
||||
<Flex vertical justify="space-between" className="rb:h-full rb:w-full rb:relative">
|
||||
{/* Empty */}
|
||||
<div className="rb:h-[calc(100%-144px)] rb:overflow-y-auto rb:overflow-x-hidden rb:flex rb:items-center rb:justify-center">
|
||||
{empty}
|
||||
</div>
|
||||
|
||||
<ChatInput source={source} query={query} onChange={onChange} onSend={onSend} loading={loading} />
|
||||
</Flex>
|
||||
)
|
||||
: (
|
||||
<div ref={scrollContainerRefs} className={clsx("rb:relative rb:overflow-y-auto", {
|
||||
'rb:h-[calc(100%-152px)]': !isMemory,
|
||||
'rb:h-[calc(100vh-362px)]': isMemory
|
||||
})}>
|
||||
{data.map((item, index) => (
|
||||
<div key={index} className={clsx("rb:relative", {
|
||||
'rb:mt-[24px]': index !== 0,
|
||||
'rb:right-[0] rb:text-right': item.role === 'user',
|
||||
'rb:left-[0] rb:text-left': item.role === 'assistant',
|
||||
})}>
|
||||
<div className={clsx('rb:border rb:text-left rb:rounded-[8px] rb:mt-[6px] rb:leading-[18px] rb:p-[10px_12px_2px_12px] rb:inline-block rb:max-w-[400px]', {
|
||||
'rb:bg-[rgba(21,94,239,0.08)] rb:border-[rgba(21,94,239,0.30)]': item.role === 'user',
|
||||
'rb:bg-[#FFFFFF] rb:border-[#EBEBEB]': item.role === 'assistant',
|
||||
})}>
|
||||
<Markdown content={item.content || ''} />
|
||||
</div>
|
||||
<div className="rb:text-[#5B6167] rb:text-[12px] rb:leading-[16px] rb:font-regular">{dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
|
||||
<ChatInput source={source} query={query} onChange={onChange} onSend={onSend} loading={loading} />
|
||||
</div>
|
||||
)
|
||||
}
|
||||
export default Chat
|
||||
@@ -1,143 +0,0 @@
|
||||
import { type FC, useEffect, useState } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Flex, Input, Form } from 'antd'
|
||||
import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
import DeepThinkingIcon from '@/assets/images/conversation/deepThinking.svg'
|
||||
import SendIcon from '@/assets/images/conversation/send.svg'
|
||||
import SendDisabledIcon from '@/assets/images/conversation/sendDisabled.svg'
|
||||
import ButtonCheckbox from '@/components/ButtonCheckbox'
|
||||
import DeepThinkingCheckedIcon from '@/assets/images/conversation/deepThinkingChecked.svg'
|
||||
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
import LoadingIcon from '@/assets/images/conversation/loading.svg'
|
||||
import type { TestParams } from '../index'
|
||||
|
||||
interface ChatInputProps {
|
||||
query?: TestParams;
|
||||
onChange: (query: TestParams) => void;
|
||||
onSend: () => void;
|
||||
loading: boolean;
|
||||
source: 'conversation' | 'memory';
|
||||
}
|
||||
const searchSwitchList = [
|
||||
{
|
||||
icon: DeepThinkingIcon,
|
||||
checkedIcon: DeepThinkingCheckedIcon,
|
||||
value: '0',
|
||||
label: 'deepThinking' // 深度思考
|
||||
},
|
||||
{
|
||||
icon: MemoryFunctionIcon,
|
||||
checkedIcon: MemoryFunctionCheckedIcon,
|
||||
value: '1',
|
||||
label: 'normalReply' // 普通回复
|
||||
},
|
||||
{
|
||||
icon: OnlineIcon,
|
||||
checkedIcon: OnlineCheckedIcon,
|
||||
value: '2',
|
||||
label: 'quickReply' // 快速回复
|
||||
},
|
||||
]
|
||||
|
||||
const ChatInput: FC<ChatInputProps> = ({ source,query, onChange, onSend, loading }) => {
|
||||
const [form] = Form.useForm()
|
||||
const { t } = useTranslation();
|
||||
const values = Form.useWatch([], form);
|
||||
const [search_switch, setSearchSwitch] = useState('0')
|
||||
|
||||
useEffect(() => {
|
||||
if (onChange) {
|
||||
onChange({...values, search_switch})
|
||||
}
|
||||
}, [values, search_switch, onChange])
|
||||
useEffect(() => {
|
||||
if (!query?.message) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [form, query?.message])
|
||||
useEffect(() => {
|
||||
if (loading) {
|
||||
form.setFieldsValue({
|
||||
message: undefined,
|
||||
})
|
||||
}
|
||||
}, [loading])
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
form.setFieldsValue({
|
||||
search_switch: value,
|
||||
})
|
||||
setSearchSwitch(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<Form form={form} layout="vertical" className="rb:absolute rb:bottom-[12px] rb:left-0 rb:right-0">
|
||||
<Flex vertical justify="space-between" className="rb:border rb:border-[#DFE4ED] rb:rounded-[12px] rb:min-h-[120px]">
|
||||
<Form.Item name="message" className="rb:mb-[0]!">
|
||||
<Input.TextArea
|
||||
className="rb:m-[10px_12px_10px_12px]! rb:p-[0]! rb:w-[calc(100%-24px)]! rb:flex-[1_1_auto]"
|
||||
// rows={4}
|
||||
variant="borderless"
|
||||
autoSize={{ minRows: 2, maxRows: 2 }}
|
||||
onChange={(e) => onChange({ ...query, message: e.target.value })}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter' && !e.shiftKey && e.target.value?.trim() !== '' && !loading) {
|
||||
e.preventDefault();
|
||||
onSend();
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Flex align="center" justify="space-between" className="rb:m-[0_10px_10px_10px]!">
|
||||
{source === 'memory' &&
|
||||
<Flex gap={8}>
|
||||
{searchSwitchList.map(item => (
|
||||
<ButtonCheckbox
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
checkedIcon={item.checkedIcon}
|
||||
checked={search_switch === item.value}
|
||||
onChange={() => handleChange(item.value)}
|
||||
>
|
||||
{t(`memoryConversation.${item.label}`)}
|
||||
</ButtonCheckbox>
|
||||
))}
|
||||
</Flex>
|
||||
}
|
||||
{source === 'conversation' &&
|
||||
<Flex gap={8}>
|
||||
<Form.Item name="web_search" valuePropName="checked" className="rb:mb-[0]!">
|
||||
<ButtonCheckbox
|
||||
icon={OnlineIcon}
|
||||
checkedIcon={OnlineCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.web_search`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
<Form.Item name="memory" valuePropName="checked" className="rb:mb-[0]!">
|
||||
<ButtonCheckbox
|
||||
icon={MemoryFunctionIcon}
|
||||
checkedIcon={MemoryFunctionCheckedIcon}
|
||||
>
|
||||
{t(`memoryConversation.memory`)}
|
||||
</ButtonCheckbox>
|
||||
</Form.Item>
|
||||
</Flex>
|
||||
}
|
||||
{loading ? <img src={LoadingIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" />:
|
||||
!values || !values?.message || values?.message?.trim() === '' ?
|
||||
<img src={SendDisabledIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" />
|
||||
: <img src={SendIcon} className="rb:w-[22px] rb:h-[22px] rb:cursor-pointer" onClick={onSend} />
|
||||
}
|
||||
</Flex>
|
||||
</Flex>
|
||||
</Form>
|
||||
)
|
||||
}
|
||||
|
||||
export default ChatInput
|
||||
@@ -1,16 +1,48 @@
|
||||
import { type FC, type ReactNode, useState, useEffect } from 'react'
|
||||
import { useTranslation } from 'react-i18next'
|
||||
import { Col, Row, App, Skeleton, Space, Select } from 'antd'
|
||||
import { Col, Row, App, Skeleton, Space, Select, Flex } from 'antd'
|
||||
import clsx from 'clsx'
|
||||
|
||||
import ConversationEmptyIcon from '@/assets/images/conversation/conversationEmpty.svg'
|
||||
import AnalysisEmptyIcon from '@/assets/images/conversation/analysisEmpty.png'
|
||||
import Card from './components/Card'
|
||||
import Chat from './components/Chat'
|
||||
import { readService, getUserMemoryList } from '@/api/memory'
|
||||
import Empty from '@/components/Empty'
|
||||
import Markdown from '@/components/Markdown'
|
||||
import type { Data } from '@/views/UserMemory/types'
|
||||
import Chat from '@/components/Chat'
|
||||
import MemoryFunctionIcon from '@/assets/images/conversation/memoryFunction.svg'
|
||||
import OnlineIcon from '@/assets/images/conversation/online.svg'
|
||||
import DeepThinkingIcon from '@/assets/images/conversation/deepThinking.svg'
|
||||
import ButtonCheckbox from '@/components/ButtonCheckbox'
|
||||
import DeepThinkingCheckedIcon from '@/assets/images/conversation/deepThinkingChecked.svg'
|
||||
import OnlineCheckedIcon from '@/assets/images/conversation/onlineChecked.svg'
|
||||
import MemoryFunctionCheckedIcon from '@/assets/images/conversation/memoryFunctionChecked.svg'
|
||||
import type { ChatItem } from '@/components/Chat/types'
|
||||
import dayjs from 'dayjs'
|
||||
import type { AnyObject } from 'antd/es/_util/type';
|
||||
|
||||
|
||||
const searchSwitchList = [
|
||||
{
|
||||
icon: DeepThinkingIcon,
|
||||
checkedIcon: DeepThinkingCheckedIcon,
|
||||
value: '0',
|
||||
label: 'deepThinking' // 深度思考
|
||||
},
|
||||
{
|
||||
icon: MemoryFunctionIcon,
|
||||
checkedIcon: MemoryFunctionCheckedIcon,
|
||||
value: '1',
|
||||
label: 'normalReply' // 普通回复
|
||||
},
|
||||
{
|
||||
icon: OnlineIcon,
|
||||
checkedIcon: OnlineCheckedIcon,
|
||||
value: '2',
|
||||
label: 'quickReply' // 快速回复
|
||||
},
|
||||
]
|
||||
|
||||
export interface TestParams {
|
||||
group_id: string;
|
||||
@@ -30,8 +62,8 @@ interface DataItem {
|
||||
export interface LogItem {
|
||||
type: string;
|
||||
title: string;
|
||||
data?: DataItem[] | Record<string, string>;
|
||||
raw_results?: string;
|
||||
data?: DataItem[] | AnyObject;
|
||||
raw_results?: string | AnyObject;
|
||||
summary?: string;
|
||||
query?: string;
|
||||
reason?: string;
|
||||
@@ -41,7 +73,7 @@ export interface LogItem {
|
||||
}
|
||||
|
||||
const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
<div className="rb:p-[12px] rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-[8px]">
|
||||
<div className="rb:p-3 rb:bg-[#F6F8FC] rb:border rb:border-[#DFE4ED] rb:rounded-lg">
|
||||
{children}
|
||||
</div>
|
||||
)
|
||||
@@ -49,17 +81,13 @@ const ContentWrapper: FC<{ children: ReactNode }> = ({ children }) => (
|
||||
const MemoryConversation: FC = () => {
|
||||
const { t } = useTranslation()
|
||||
const { message } = App.useApp();
|
||||
const [query, setQuery] = useState<TestParams>({
|
||||
group_id: '',
|
||||
message: '',
|
||||
search_switch: '0',
|
||||
history: [],
|
||||
})
|
||||
const [userId, setUserId] = useState<string>()
|
||||
const [loading, setLoading] = useState<boolean>(false)
|
||||
const [chatData, setChatData] = useState<{ content: string; created_at: string | number; role: string }[]>([])
|
||||
const [chatData, setChatData] = useState<ChatItem[]>([])
|
||||
const [logs, setLogs] = useState<LogItem[]>([])
|
||||
const [userList, setUserList] = useState<Data[]>([])
|
||||
const [search_switch, setSearchSwitch] = useState('0')
|
||||
const [msg, setMsg] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
getUserMemoryList().then(res => {
|
||||
@@ -75,11 +103,12 @@ const MemoryConversation: FC = () => {
|
||||
message.warning(t('common.inputPlaceholder', { title: t('memoryConversation.userID') }))
|
||||
return
|
||||
}
|
||||
setChatData(prev => [...prev, { content: query.message || '', created_at: new Date().getTime(), role: 'user' }])
|
||||
setChatData(prev => [...prev, { content: msg, created_at: new Date().getTime(), role: 'user' }])
|
||||
setLoading(true)
|
||||
readService({
|
||||
...query,
|
||||
message: msg,
|
||||
group_id: userId,
|
||||
search_switch: search_switch,
|
||||
history: [],
|
||||
})
|
||||
.then(res => {
|
||||
@@ -92,6 +121,10 @@ const MemoryConversation: FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setSearchSwitch(value)
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Row gutter={16}>
|
||||
@@ -101,7 +134,7 @@ const MemoryConversation: FC = () => {
|
||||
value: item.end_user?.id,
|
||||
label: item?.name,
|
||||
}))}
|
||||
filterOption={(inputValue, option) => option.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
filterOption={(inputValue, option) => option?.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
showSearch={true}
|
||||
// filterOption={(inputValue, option) => option.label?.toLowerCase().indexOf(inputValue.toLowerCase()) !== -1}
|
||||
placeholder={t('memoryConversation.searchPlaceholder')}
|
||||
@@ -118,14 +151,29 @@ const MemoryConversation: FC = () => {
|
||||
>
|
||||
<Chat
|
||||
empty={
|
||||
<Empty url={ConversationEmptyIcon} size={[140, 100]} title={t('memoryConversation.conversationContentEmpty')} />
|
||||
<Empty url={ConversationEmptyIcon} className="rb:h-full" size={[140, 100]} title={t('memoryConversation.conversationContentEmpty')} />
|
||||
}
|
||||
contentClassName='rb:h-[calc(100vh-362px)]'
|
||||
data={chatData}
|
||||
query={query}
|
||||
onChange={setQuery}
|
||||
onChange={setMsg}
|
||||
onSend={handleSend}
|
||||
loading={loading}
|
||||
/>
|
||||
labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')}
|
||||
>
|
||||
<Flex gap={8}>
|
||||
{searchSwitchList.map(item => (
|
||||
<ButtonCheckbox
|
||||
key={item.value}
|
||||
icon={item.icon}
|
||||
checkedIcon={item.checkedIcon}
|
||||
checked={search_switch === item.value}
|
||||
onChange={() => handleChange(item.value)}
|
||||
>
|
||||
{t(`memoryConversation.${item.label}`)}
|
||||
</ButtonCheckbox>
|
||||
))}
|
||||
</Flex>
|
||||
</Chat>
|
||||
</Card>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
@@ -147,8 +195,8 @@ const MemoryConversation: FC = () => {
|
||||
{logs.map((log, logIndex) => (
|
||||
<div key={logIndex}
|
||||
className={clsx(
|
||||
`rb:p-[16px_24px] rb:rounded-[8px]`,
|
||||
'rb:border-[1px] rb:border-[#DFE4ED]',
|
||||
`rb:p-[16px_24px] rb:rounded-lg`,
|
||||
'rb:border rb:border-[#DFE4ED]',
|
||||
{
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#155EEF]': logIndex % 3 === 0,
|
||||
'rb:shadow-[inset_4px_0px_0px_0px_#369F21]': logIndex % 3 === 1,
|
||||
@@ -156,14 +204,14 @@ const MemoryConversation: FC = () => {
|
||||
}
|
||||
)}
|
||||
>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px] rb:mb-[24px]">{log.title}</div>
|
||||
<div className="rb:text-[16px] rb:font-medium rb:leading-[22px] rb:mb-6">{log.title}</div>
|
||||
{log.type === 'problem_split' && Array.isArray(log.data) && log.data.length > 0
|
||||
? <Space size={12} direction="vertical" style={{width: '100%'}}>
|
||||
{log.data.map(vo => (
|
||||
<ContentWrapper key={vo.id}>
|
||||
<>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{vo.id}. {vo.question}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{vo.reason}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{vo.reason}</div>
|
||||
</>
|
||||
</ContentWrapper>
|
||||
))}
|
||||
@@ -175,7 +223,7 @@ const MemoryConversation: FC = () => {
|
||||
<>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{key}</div>
|
||||
{(log.data as Record<string, string[]>)[key].map((item, index) => (
|
||||
<div key={index} className="rb:mt-[8px] rb:text-[#5B6167] rb:text-[12px]">{item}</div>
|
||||
<div key={index} className="rb:mt-2 rb:text-[#5B6167] rb:text-[12px]">{item}</div>
|
||||
))}
|
||||
</>
|
||||
</ContentWrapper>
|
||||
@@ -183,15 +231,15 @@ const MemoryConversation: FC = () => {
|
||||
</Space>
|
||||
: log.type === 'search_result' && log.raw_results
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className='rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]'>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className='rb:mt-2 rb:text-[12px] rb:text-[#5B6167]'>
|
||||
{typeof log.raw_results === 'string'
|
||||
? <Markdown content={log.raw_results} />
|
||||
: <>
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string }, index: number) => (
|
||||
<div key={index}>{item.statement}</div>
|
||||
))}
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string }, index: number) => (
|
||||
<div key={index}>{item.content}</div>
|
||||
))}
|
||||
</>
|
||||
@@ -203,26 +251,26 @@ const MemoryConversation: FC = () => {
|
||||
: log.type === 'verification'
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332]">{log.query}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{log.reason}</div>
|
||||
<div className="rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]">{log.result}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{log.reason}</div>
|
||||
<div className="rb:mt-2 rb:text-[12px] rb:text-[#5B6167]">{log.result}</div>
|
||||
</ContentWrapper>
|
||||
: log.type === 'output_type'
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167]">{log.summary}</div>
|
||||
</ContentWrapper>
|
||||
: log.type === 'input_summary' && log.raw_results
|
||||
? <ContentWrapper>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-[8px]">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[12px] rb:text-[#5B6167] rb:mb-[8px]">{log.summary}</div>
|
||||
<div className='rb:mt-[8px] rb:text-[12px] rb:text-[#5B6167]'>
|
||||
<div className="rb:font-medium rb:text-[#212332] rb:mb-2">{log.query}</div>
|
||||
<div className="rb:font-medium rb:text-[12px] rb:text-[#5B6167] rb:mb-2">{log.summary}</div>
|
||||
<div className='rb:mt-2 rb:text-[12px] rb:text-[#5B6167]'>
|
||||
{typeof log.raw_results === 'string'
|
||||
? <Markdown content={log.raw_results} />
|
||||
: <>
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.statements.length > 0 && log.raw_results.reranked_results?.statements.map((item: { statement: string; } , index: number) => (
|
||||
<div key={index}>{item.statement}</div>
|
||||
))}
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item, index) => (
|
||||
{log.raw_results.reranked_results?.summaries.length > 0 && log.raw_results.reranked_results?.summaries.map((item: { content: string; }, index: number) => (
|
||||
<div key={index}>{item.content}</div>
|
||||
))}
|
||||
</>
|
||||
|
||||
Reference in New Issue
Block a user