feat(chat): 优化聊天组件功能和交互

- 重构了聊天容器组件,提高了代码可读性和维护性
- 添加了加载指示器,提升了用户体验
- 优化了会话历史记录的处理逻辑
- 增加了对 vis-thinking 代码块的解析和格式化功能
- 改进了聊天回复的加载状态管理
main
Tuzki 5 months ago
parent 4044358b09
commit 60749dc2e0
  1. 48
      web/components/chat/chat-container.tsx
  2. 10
      web/components/chat/chat-content/vis-thinking.tsx
  3. 6
      web/hooks/use-chat.ts
  4. 25
      web/pages/chat/index.tsx

@ -13,29 +13,29 @@ import MuiLoading from '../common/loading';
import Completion from './completion';
import Header from './header';
// Function to extract JSON from vis-thinking code blocks
// 从可视化代码块中提取JSON的函数
const parseVisThinking = (content: any) => {
// Check if content is a string
// 检查内容是否为字符串
if (typeof content !== 'string') {
return content;
}
// Check if this is a vis-thinking code block
// 检查这是否是vis-thinking代码块
if (content.startsWith('```vis-thinking') || content.includes('```vis-thinking')) {
// Find where the JSON part begins
// We're looking for the first occurrence of '{"' after the vis-thinking header
// 找到JSON部分的开始位置
// 我们正在寻找vis-thinking标题之后第一个出现的{"
const jsonStartIndex = content.indexOf('{"');
if (jsonStartIndex !== -1) {
// Extract everything from the JSON start to the end
// 提取从JSON开始到结束的所有内容
const jsonContent = content.substring(jsonStartIndex);
// Attempt to parse the JSON
// 尝试解析JSON
try {
return JSON.parse(jsonContent);
} catch {
// If there's a parsing error, try to clean up the JSON string
// This might happen if there are backticks at the end
// 如果解析出错,尝试清理JSON字符串
// 这可能是因为有尾随的反引号
const cleanedContent = jsonContent.replace(/```$/g, '').trim();
try {
return JSON.parse(cleanedContent);
@ -47,39 +47,39 @@ const parseVisThinking = (content: any) => {
}
}
// If it's not a vis-thinking block, try to parse it directly as JSON
// 如果它不是一个vis-thinking块,请尝试直接将其解析为JSON
try {
return typeof content === 'string' ? JSON.parse(content) : content;
} catch {
// If it's not valid JSON, return the original content
// 如果不是有效的JSON,则返回原始内容
console.log('Not JSON format or vis-thinking format, returning original content');
return content;
}
};
// Function to extract the thinking part from vis-thinking code blocks while preserving tags
// 函数从vis-thinking代码块中提取思考部分,同时保留标记
const formatToVisThinking = (content: any) => {
// Only process strings
// 仅处理字符串
if (typeof content !== 'string') {
return content;
}
// Check if this is a vis-thinking code block
// 检查这是否是一个vis-thinking代码块
if (content.startsWith('```vis-thinking') || content.includes('```vis-thinking')) {
// Find the start of the vis-thinking block
// 找到vis-thinking块的开始位置
const blockStartIndex = content.indexOf('```vis-thinking');
const thinkingStartIndex = blockStartIndex + '```vis-thinking'.length;
// Find the end of the vis-thinking block
// 找到vis-thinking块的结束位置
const thinkingEndIndex = content.indexOf('```', thinkingStartIndex);
if (thinkingEndIndex !== -1) {
// Extract the thinking content with the tags
// 提取带有标签的思考内容
return content.substring(blockStartIndex, thinkingEndIndex + 3);
}
}
// If it's not a vis-thinking block or can't extract thinking part, return the original content
// 如果不是vis-thinking块或无法提取思维部分,则返回原始内容
return content;
};
@ -103,10 +103,10 @@ const ChatContainer = () => {
const contextTemp = list[list.length - 1]?.context;
if (contextTemp) {
try {
// First, parse the context to handle vis-thinking code blocks
// 首先,解析上下文以处理可视化代码块
const parsedContext = parseVisThinking(contextTemp);
// Then, handle the normal JSON processing
// 然后,处理正常的JSON解析
const contextObj =
typeof parsedContext === 'object'
? parsedContext
@ -130,7 +130,7 @@ const ChatContainer = () => {
useEffect(() => {
if (!history.length) return;
/** use last view model_name as default model name */
/** 使用上一个视图model_name作为默认模型名称 */
const lastView = history.filter(i => i.role === 'view')?.slice(-1)?.[0];
lastView?.model_name && setModel(lastView.model_name);
@ -196,9 +196,9 @@ const ChatContainer = () => {
/>
</div>
{/* Use flex-auto to ensure the remaining height is filled */}
{/* 使用flex-auto确保剩余高度被填充 */}
<div className='flex-auto flex overflow-hidden'>
{/* Left chart area */}
{/* 左侧图表区域 */}
{!!chartsData?.length && (
<div
className={classNames('overflow-auto', {
@ -225,7 +225,7 @@ const ChatContainer = () => {
'w-full h-full px-4 lg:px-8': scene !== 'chat_dashboard',
})}
>
{/* Wrap the Completion component in a container with a specific height */}
{/* 将Completion组件包装在具有特定高度的容器中 */}
<div className='h-full overflow-hidden'>
<Completion messages={history} onSubmit={handleChat} onFormatContent={formatToVisThinking} />
</div>

@ -1,12 +1,17 @@
import { CaretDownOutlined, CaretRightOutlined } from '@ant-design/icons';
import React from 'react';
import { Spin } from 'antd';
import { LoadingOutlined } from '@ant-design/icons';
import React, { useContext } from 'react';
import { useTranslation } from 'react-i18next';
import { ChatContentContext } from '@/pages/chat';
interface Props {
content: string;
}
export function VisThinking({ content }: Props) {
const {replyLoading} = useContext(ChatContentContext);
const { t } = useTranslation();
const [expanded, setExpanded] = React.useState(true); // Control the expansion of the thinking process
// console.log("VisThinking", content)
@ -20,7 +25,8 @@ export function VisThinking({ content }: Props) {
<span className='mr-2 font-thin text-[16px] text-gray-500 dark:text-gray-300'>
{expanded ? <CaretDownOutlined /> : <CaretRightOutlined />}
</span>
<span className='text-gray-700 dark:text-gray-300'>{t('cot_title')}</span>
<span className='text-gray-700 dark:text-gray-300 mr-1'>{t('cot_title')}</span>
<Spin spinning={replyLoading} indicator={<LoadingOutlined spin />} size="small" />
</div>
</div>

@ -27,8 +27,11 @@ if (typeof window !== 'undefined') {
authToken = JSON.parse(tokenStr)?.token || '';
}
const useChat = ({ queryAgentURL = '/api/v1/chat/completions', app_code }: Props) => {
// 定义一个状态变量来存储 AbortController 实例
const [ctrl, setCtrl] = useState<AbortController>({} as AbortController);
// 从 ChatContext 中获取 scene 值
const { scene } = useContext(ChatContext);
// 定义一个回调函数来发起聊天请求
const chat = useCallback(
async ({ data, chatId, onMessage, onClose, onDone, onError, ctrl }: ChatParams) => {
ctrl && setCtrl(ctrl);
@ -111,7 +114,8 @@ const useChat = ({ queryAgentURL = '/api/v1/chat/completions', app_code }: Props
[queryAgentURL, app_code, scene],
);
// 返回一个包含 chat 函数和 ctrl 状态的对象
return { chat, ctrl };
};
export default useChat;
export default useChat;

@ -14,12 +14,15 @@ import dynamic from 'next/dynamic';
import { useSearchParams } from 'next/navigation';
import React, { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react';
// 动态导入DbEditor组件,SSR设置为false
const DbEditor = dynamic(() => import('@/components/chat/db-editor'), {
ssr: false,
});
// 动态导入ChatContainer组件,SSR设置为false
const ChatContainer = dynamic(() => import('@/components/chat/chat-container'), { ssr: false });
const { Content } = Layout;
// 定义ChatContentProps接口,用于ChatContentContext的类型
interface ChatContentProps {
history: ChatHistoryResponse; // 会话记录列表
replyLoading: boolean; // 对话回复loading
@ -47,6 +50,8 @@ interface ChatContentProps {
refreshAppInfo: () => void;
setHistory: React.Dispatch<React.SetStateAction<ChatHistoryResponse>>;
}
// 创建ChatContentContext上下文
export const ChatContentContext = createContext<ChatContentProps>({
history: [],
replyLoading: false,
@ -75,6 +80,7 @@ export const ChatContentContext = createContext<ChatContentProps>({
handleChat: () => Promise.resolve(),
});
// 定义Chat组件,为React函数组件
const Chat: React.FC = () => {
const { model, currentDialogInfo } = useContext(ChatContext);
const { isContract, setIsContract, setIsMenuExpand } = useContext(ChatContext);
@ -103,6 +109,7 @@ const Chat: React.FC = () => {
const [modelValue, setModelValue] = useState<string>('');
const { mode } = useContext(ChatContext);
// 更新温度值、最大新令牌值、模型值和资源值
useEffect(() => {
setTemperatureValue(appInfo?.param_need?.filter(item => item.type === 'temperature')[0]?.value || 0.6);
setMaxNewTokensValue(appInfo?.param_need?.filter(item => item.type === 'max_new_tokens')[0]?.value || 4000);
@ -112,16 +119,15 @@ const Chat: React.FC = () => {
);
}, [appInfo, dbName, knowledgeId, model]);
// 根据不同的场景设置菜单展开状态和合约状态
useEffect(() => {
// 仅初始化执行,防止dashboard页面无法切换状态
setIsMenuExpand(scene !== 'chat_dashboard');
// 路由变了要取消Editor模式,再进来是默认的Preview模式
if (chatId && scene) {
setIsContract(false);
}
}, [chatId, scene]);
// 是否是默认小助手
// 判断是否为默认小助手
const isChatDefault = useMemo(() => {
return !chatId && !scene;
}, [chatId, scene]);
@ -152,12 +158,13 @@ const Chat: React.FC = () => {
},
);
// 列表当前活跃对话
// 获取当前活跃对话
const currentDialogue = useMemo(() => {
const [, list] = dialogueList;
return list?.find(item => item.conv_uid === chatId) || ({} as IChatDialogueSchema);
}, [chatId, dialogueList]);
// 判断是否需要查询应用信息
useEffect(() => {
const initMessage = getInitMessage();
if (currentDialogInfo.chat_scene === scene && !isChatDefault && !(initMessage && initMessage.message)) {
@ -182,7 +189,7 @@ const Chat: React.FC = () => {
},
});
// 会话提问
// 处理会话逻辑
const handleChat = useCallback(
(content: string, data?: Record<string, any>) => {
return new Promise<void>(resolve => {
@ -258,8 +265,8 @@ const Chat: React.FC = () => {
[chatId, history, modelValue, chat, scene],
);
// 初始化获取会话历史记录
useAsyncEffect(async () => {
// 如果是默认小助手,不获取历史记录
if (isChatDefault) {
return;
}
@ -270,6 +277,7 @@ const Chat: React.FC = () => {
await getHistory();
}, [chatId, scene, getHistory]);
// 重置会话历史记录和顺序
useEffect(() => {
if (isChatDefault) {
order.current = 1;
@ -277,7 +285,9 @@ const Chat: React.FC = () => {
}
}, [isChatDefault]);
// 根据不同的场景渲染内容
const contentRender = () => {
console.log('render content', scene, isChatDefault, isContract);
if (scene === 'chat_dashboard') {
return isContract ? <DbEditor /> : <ChatContainer />;
} else {
@ -296,6 +306,7 @@ const Chat: React.FC = () => {
}
};
// 提供ChatContentContext上下文,包含会话相关的状态和方法
return (
<ChatContentContext.Provider
value={{
@ -345,4 +356,4 @@ const Chat: React.FC = () => {
);
};
export default Chat;
export default Chat;
Loading…
Cancel
Save