mirror of
https://github.com/csunny/DB-GPT.git
synced 2025-07-23 20:26:15 +00:00
Co-authored-by: Fangyin Cheng <staneyffer@gmail.com> Co-authored-by: 谨欣 <echo.cmy@antgroup.com> Co-authored-by: 严志勇 <yanzhiyong@tiansuixiansheng.com> Co-authored-by: yanzhiyong <932374019@qq.com>
348 lines
11 KiB
TypeScript
348 lines
11 KiB
TypeScript
import { ChatContext } from '@/app/chat-context';
|
|
import { apiInterceptors, getAppInfo, getChatHistory, getDialogueList, postChatModeParamsList } from '@/client/api';
|
|
import useUser from '@/hooks/use-user';
|
|
import { IApp } from '@/types/app';
|
|
import { ChatHistoryResponse } from '@/types/chat';
|
|
import { getUserId } from '@/utils';
|
|
import { HEADER_USER_ID_KEY } from '@/utils/constants/index';
|
|
import { EventStreamContentType, fetchEventSource } from '@microsoft/fetch-event-source';
|
|
import { useRequest } from 'ahooks';
|
|
import { Spin } from 'antd';
|
|
import dynamic from 'next/dynamic';
|
|
import { useSearchParams } from 'next/navigation';
|
|
import React, { createContext, useContext, useEffect, useMemo, useRef, useState } from 'react';
|
|
import Header from './components/Header';
|
|
import InputContainer from './components/InputContainer';
|
|
|
|
const Content = dynamic(() => import('@/pages/mobile/chat/components/Content'), { ssr: false });
|
|
|
|
interface MobileChatProps {
|
|
model: string;
|
|
temperature: number;
|
|
resource: any;
|
|
setResource: React.Dispatch<React.SetStateAction<any>>;
|
|
setTemperature: React.Dispatch<React.SetStateAction<number>>;
|
|
setModel: React.Dispatch<React.SetStateAction<string>>;
|
|
scene: string;
|
|
history: ChatHistoryResponse; // 会话内容
|
|
setHistory: React.Dispatch<React.SetStateAction<ChatHistoryResponse>>;
|
|
scrollViewRef: React.RefObject<HTMLDivElement>; // 会话可滚动区域
|
|
appInfo: IApp;
|
|
conv_uid: string;
|
|
resourceList?: Record<string, any>[];
|
|
order: React.MutableRefObject<number>;
|
|
handleChat: (_content?: string) => Promise<void>;
|
|
canAbort: boolean;
|
|
setCarAbort: React.Dispatch<React.SetStateAction<boolean>>;
|
|
canNewChat: boolean;
|
|
setCanNewChat: React.Dispatch<React.SetStateAction<boolean>>;
|
|
ctrl: React.MutableRefObject<AbortController | undefined>;
|
|
userInput: string;
|
|
setUserInput: React.Dispatch<React.SetStateAction<string>>;
|
|
getChatHistoryRun: () => void;
|
|
}
|
|
|
|
export const MobileChatContext = createContext<MobileChatProps>({
|
|
model: '',
|
|
temperature: 0.5,
|
|
resource: null,
|
|
setModel: () => {},
|
|
setTemperature: () => {},
|
|
setResource: () => {},
|
|
scene: '',
|
|
history: [],
|
|
setHistory: () => {},
|
|
scrollViewRef: { current: null },
|
|
appInfo: {} as IApp,
|
|
conv_uid: '',
|
|
resourceList: [],
|
|
order: { current: 1 },
|
|
handleChat: () => Promise.resolve(),
|
|
canAbort: false,
|
|
setCarAbort: () => {},
|
|
canNewChat: false,
|
|
setCanNewChat: () => {},
|
|
ctrl: { current: undefined },
|
|
userInput: '',
|
|
setUserInput: () => {},
|
|
getChatHistoryRun: () => {},
|
|
});
|
|
|
|
const MobileChat: React.FC = () => {
|
|
// 从url上获取基本参数
|
|
const searchParams = useSearchParams();
|
|
const chatScene = searchParams?.get('chat_scene') ?? '';
|
|
const appCode = searchParams?.get('app_code') ?? '';
|
|
// 模型列表
|
|
const { modelList } = useContext(ChatContext);
|
|
|
|
const [history, setHistory] = useState<ChatHistoryResponse>([]);
|
|
const [model, setModel] = useState<string>('');
|
|
const [temperature, setTemperature] = useState<number>(0.5);
|
|
const [resource, setResource] = useState<any>(null);
|
|
const scrollViewRef = useRef<HTMLDivElement>(null);
|
|
// 用户输入
|
|
const [userInput, setUserInput] = useState<string>('');
|
|
// 回复可以终止
|
|
const [canAbort, setCarAbort] = useState<boolean>(false);
|
|
// 是否可以开始新的提问,上一次回答结束或者暂停才可以开始新的提问
|
|
const [canNewChat, setCanNewChat] = useState<boolean>(true);
|
|
|
|
const ctrl = useRef<AbortController>();
|
|
const order = useRef<number>(1);
|
|
|
|
// 用户信息
|
|
const userInfo = useUser();
|
|
|
|
// 会话id
|
|
const conv_uid = useMemo(() => `${userInfo?.user_no}_${appCode}`, [appCode, userInfo]);
|
|
|
|
// 获取历史会话记录
|
|
const { run: getChatHistoryRun, loading: historyLoading } = useRequest(
|
|
async () => await apiInterceptors(getChatHistory(`${userInfo?.user_no}_${appCode}`)),
|
|
{
|
|
manual: true,
|
|
onSuccess: data => {
|
|
const [, res] = data;
|
|
const viewList = res?.filter(item => item.role === 'view');
|
|
if (viewList && viewList.length > 0) {
|
|
order.current = viewList[viewList.length - 1].order + 1;
|
|
}
|
|
setHistory(res || []);
|
|
},
|
|
},
|
|
);
|
|
|
|
// 获取应用信息
|
|
const {
|
|
data: appInfo,
|
|
run: getAppInfoRun,
|
|
loading: appInfoLoading,
|
|
} = useRequest(
|
|
async (params: { chat_scene: string; app_code: string }) => {
|
|
const [, res] = await apiInterceptors(getAppInfo(params));
|
|
return res ?? ({} as IApp);
|
|
},
|
|
{
|
|
manual: true,
|
|
},
|
|
);
|
|
|
|
// 获取可选择的资源类型列表
|
|
const {
|
|
run,
|
|
data,
|
|
loading: resourceLoading,
|
|
} = useRequest(
|
|
async () => {
|
|
const [, res] = await apiInterceptors(postChatModeParamsList(chatScene));
|
|
setResource(res?.[0]?.space_id || res?.[0]?.param);
|
|
return res ?? [];
|
|
},
|
|
{
|
|
manual: true,
|
|
},
|
|
);
|
|
|
|
// 获取会话列表
|
|
const { run: getDialogueListRun, loading: dialogueListLoading } = useRequest(
|
|
async () => {
|
|
const [, res] = await apiInterceptors(getDialogueList());
|
|
return res ?? [];
|
|
},
|
|
{
|
|
manual: true,
|
|
onSuccess: data => {
|
|
const filterDialogue = data?.filter(item => item.conv_uid === conv_uid)?.[0];
|
|
filterDialogue?.select_param && setResource(JSON.parse(filterDialogue?.select_param));
|
|
},
|
|
},
|
|
);
|
|
|
|
// 获取应用信息
|
|
useEffect(() => {
|
|
if (chatScene && appCode && modelList.length) {
|
|
getAppInfoRun({ chat_scene: chatScene, app_code: appCode });
|
|
}
|
|
}, [appCode, chatScene, getAppInfoRun, modelList]);
|
|
|
|
// 设置历史会话记录
|
|
useEffect(() => {
|
|
appCode && getChatHistoryRun();
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [appCode]);
|
|
|
|
// 设置默认模型
|
|
useEffect(() => {
|
|
if (modelList.length > 0) {
|
|
// 获取应用信息中的model值
|
|
const infoModel = appInfo?.param_need?.filter(item => item.type === 'model')?.[0]?.value;
|
|
setModel(infoModel || modelList[0]);
|
|
}
|
|
}, [modelList, appInfo]);
|
|
|
|
// 设置默认温度;
|
|
useEffect(() => {
|
|
// 获取应用信息中的model值
|
|
const infoTemperature = appInfo?.param_need?.filter(item => item.type === 'temperature')?.[0]?.value;
|
|
setTemperature(infoTemperature || 0.5);
|
|
}, [appInfo]);
|
|
|
|
// 获取可选择资源列表
|
|
useEffect(() => {
|
|
if (chatScene && appInfo?.app_code) {
|
|
const resourceVal = appInfo?.param_need?.filter(item => item.type === 'resource')?.[0]?.value;
|
|
const bindResourceVal = appInfo?.param_need?.filter(item => item.type === 'resource')?.[0]?.bind_value;
|
|
bindResourceVal && setResource(bindResourceVal);
|
|
['database', 'knowledge', 'plugin', 'awel_flow'].includes(resourceVal) && !bindResourceVal && run();
|
|
}
|
|
}, [appInfo, chatScene, run]);
|
|
|
|
// 处理会话
|
|
const handleChat = async (content?: string) => {
|
|
setUserInput('');
|
|
ctrl.current = new AbortController();
|
|
const params = {
|
|
chat_mode: chatScene,
|
|
model_name: model,
|
|
user_input: content || userInput,
|
|
conv_uid,
|
|
temperature,
|
|
app_code: appInfo?.app_code,
|
|
...(resource && { select_param: resource }),
|
|
};
|
|
if (history && history.length > 0) {
|
|
const viewList = history?.filter(item => item.role === 'view');
|
|
order.current = viewList[viewList.length - 1].order + 1;
|
|
}
|
|
const tempHistory: ChatHistoryResponse = [
|
|
{
|
|
role: 'human',
|
|
context: content || userInput,
|
|
model_name: model,
|
|
order: order.current,
|
|
time_stamp: 0,
|
|
},
|
|
{
|
|
role: 'view',
|
|
context: '',
|
|
model_name: model,
|
|
order: order.current,
|
|
time_stamp: 0,
|
|
thinking: true,
|
|
},
|
|
];
|
|
const index = tempHistory.length - 1;
|
|
setHistory([...history, ...tempHistory]);
|
|
setCanNewChat(false);
|
|
try {
|
|
await fetchEventSource(`${process.env.API_BASE_URL ?? ''}/api/v1/chat/completions`, {
|
|
method: 'POST',
|
|
headers: {
|
|
'Content-Type': 'application/json',
|
|
[HEADER_USER_ID_KEY]: getUserId() ?? '',
|
|
},
|
|
signal: ctrl.current.signal,
|
|
body: JSON.stringify(params),
|
|
openWhenHidden: true,
|
|
async onopen(response) {
|
|
if (response.ok && response.headers.get('content-type') === EventStreamContentType) {
|
|
return;
|
|
}
|
|
},
|
|
onclose() {
|
|
ctrl.current?.abort();
|
|
setCanNewChat(true);
|
|
setCarAbort(false);
|
|
},
|
|
onerror(err) {
|
|
throw new Error(err);
|
|
},
|
|
onmessage: event => {
|
|
let message = event.data;
|
|
try {
|
|
message = JSON.parse(message).vis;
|
|
} catch {
|
|
message.replaceAll('\\n', '\n');
|
|
}
|
|
if (message === '[DONE]') {
|
|
setCanNewChat(true);
|
|
setCarAbort(false);
|
|
} else if (message?.startsWith('[ERROR]')) {
|
|
tempHistory[index].context = message?.replace('[ERROR]', '');
|
|
tempHistory[index].thinking = false;
|
|
setHistory([...history, ...tempHistory]);
|
|
setCanNewChat(true);
|
|
setCarAbort(false);
|
|
} else {
|
|
setCarAbort(true);
|
|
tempHistory[index].context = message;
|
|
tempHistory[index].thinking = false;
|
|
setHistory([...history, ...tempHistory]);
|
|
}
|
|
},
|
|
});
|
|
} catch {
|
|
ctrl.current?.abort();
|
|
tempHistory[index].context = 'Sorry, we meet some error, please try again later.';
|
|
tempHistory[index].thinking = false;
|
|
setHistory([...tempHistory]);
|
|
setCanNewChat(true);
|
|
setCarAbort(false);
|
|
}
|
|
};
|
|
|
|
// 如果是原生应用,拉取会话列表获取资源参数
|
|
useEffect(() => {
|
|
if (chatScene && chatScene !== 'chat_agent') {
|
|
getDialogueListRun();
|
|
}
|
|
}, [chatScene, getDialogueListRun]);
|
|
|
|
return (
|
|
<MobileChatContext.Provider
|
|
value={{
|
|
model,
|
|
resource,
|
|
setModel,
|
|
setTemperature,
|
|
setResource,
|
|
temperature,
|
|
appInfo: appInfo as IApp,
|
|
conv_uid,
|
|
scene: chatScene,
|
|
history,
|
|
scrollViewRef,
|
|
setHistory,
|
|
resourceList: data,
|
|
order,
|
|
handleChat,
|
|
setCanNewChat,
|
|
ctrl,
|
|
canAbort,
|
|
setCarAbort,
|
|
canNewChat,
|
|
userInput,
|
|
setUserInput,
|
|
getChatHistoryRun,
|
|
}}
|
|
>
|
|
<Spin
|
|
size='large'
|
|
className='flex h-screen w-screen justify-center items-center max-h-screen'
|
|
spinning={historyLoading || appInfoLoading || resourceLoading || dialogueListLoading}
|
|
>
|
|
<div className='flex flex-col h-screen bg-gradient-light dark:bg-gradient-dark p-4 pt-0'>
|
|
<div ref={scrollViewRef} className='flex flex-col flex-1 overflow-y-auto mb-3'>
|
|
<Header />
|
|
<Content />
|
|
</div>
|
|
{appInfo?.app_code && <InputContainer />}
|
|
</div>
|
|
</Spin>
|
|
</MobileChatContext.Provider>
|
|
);
|
|
};
|
|
|
|
export default MobileChat;
|