Files
gpustack-ui/src/pages/playground/components/multiple-chat/model-item.tsx
T

482 lines
13 KiB
TypeScript

import AutoTooltip from '@/components/auto-tooltip';
import IconFont from '@/components/icon-font';
import useOverlayScroller from '@/hooks/use-overlay-scroller';
import { fetchChunkedData, readStreamData } from '@/utils/fetch-chunk-data';
import {
ClearOutlined,
DeleteOutlined,
MoreOutlined,
SettingOutlined
} from '@ant-design/icons';
import { useIntl } from '@umijs/max';
import { Button, Checkbox, Dropdown, Popover, Select, Spin } from 'antd';
import _ from 'lodash';
import 'overlayscrollbars/overlayscrollbars.css';
import React, {
forwardRef,
useCallback,
useContext,
useEffect,
useImperativeHandle,
useMemo,
useRef,
useState
} from 'react';
import 'simplebar-react/dist/simplebar.min.css';
import { CHAT_API } from '../../apis';
import { OpenAIViewCode, Roles, generateMessages } from '../../config';
import CompareContext from '../../config/compare-context';
import { MessageItem, ModelSelectionItem } from '../../config/types';
import '../../style/model-item.less';
import ParamsSettings from '../params-settings';
import ReferenceParams from '../reference-params';
import ViewCodeModal from '../view-code-modal';
import MessageContent from './message-content';
import SystemMessage from './system-message';
interface ModelItemProps {
model: string;
modelList: ModelSelectionItem[];
instanceId: symbol;
ref: any;
}
const ModelItem: React.FC<ModelItemProps> = forwardRef(
({ model, modelList, instanceId }, ref) => {
const {
globalParams,
setGlobalParams,
setLoadingStatus,
handleDeleteModel,
handleApplySystemChangeToAll,
modelFullList,
loadingStatus
} = useContext(CompareContext);
const intl = useIntl();
const isApplyToAllModels = useRef(false);
const [systemMessage, setSystemMessage] = useState<string>('');
const [params, setParams] = useState<Record<string, any>>({
model: model
});
const [loading, setLoading] = useState(false);
const messageId = useRef<number>(0);
const [messageList, setMessageList] = useState<MessageItem[]>([]);
const [tokenResult, setTokenResult] = useState<any>(null);
const [show, setShow] = useState(false);
const contentRef = useRef<any>('');
const controllerRef = useRef<any>(null);
const currentMessageRef = useRef<MessageItem[]>([]);
const modelScrollRef = useRef<any>(null);
const messageListLengthCache = useRef<number>(0);
const { initialize, updateScrollerPosition } = useOverlayScroller();
const viewCodeMessage = useMemo(() => {
return generateMessages([
{ role: Roles.System, content: systemMessage },
...messageList
]);
}, [messageList, systemMessage]);
const setMessageId = () => {
messageId.current = messageId.current + 1;
};
const abortFetch = () => {
controllerRef.current?.abort?.();
setLoadingStatus(instanceId, false);
};
const joinMessage = (chunk: any) => {
setTokenResult({
...(chunk?.usage ?? {})
});
if (!chunk || !_.get(chunk, 'choices', [].length)) {
return;
}
contentRef.current =
contentRef.current + _.get(chunk, 'choices.0.delta.content', '');
setMessageList([
...messageList,
...currentMessageRef.current,
{
role: Roles.Assistant,
content: contentRef.current,
uid: messageId.current
}
]);
};
const submitMessage = async (currentMessage?: Omit<MessageItem, 'uid'>) => {
if (!params.model) return;
try {
setLoadingStatus(instanceId, true);
setMessageId();
controllerRef.current?.abort?.();
controllerRef.current = new AbortController();
const signal = controllerRef.current.signal;
currentMessageRef.current = currentMessage
? [
{
...currentMessage,
uid: messageId.current
}
]
: [];
setMessageList((preList) => {
return [...preList, ...currentMessageRef.current];
});
contentRef.current = '';
// ====== payload =================
const messageParams = [
{ role: Roles.System, content: systemMessage },
...messageList,
...currentMessageRef.current
];
const messages = generateMessages(messageParams);
const chatParams = {
messages: messages,
...params,
stream: true,
stream_options: {
include_usage: true
}
};
// ============== payload end ================
const result: any = await fetchChunkedData({
data: chatParams,
url: CHAT_API,
signal
});
if (result?.error) {
setTokenResult({
error: true,
errorMessage:
result?.data?.error?.message || result?.data?.message || ''
});
return;
}
setMessageId();
const { reader, decoder } = result;
await readStreamData(reader, decoder, (chunk: any) => {
if (chunk?.error) {
setTokenResult({
error: true,
errorMessage: chunk?.error?.message || chunk?.message || ''
});
return;
}
joinMessage(chunk);
});
} catch (error) {
// console.log('error:', error);
} finally {
setLoadingStatus(instanceId, false);
}
};
const handleDelete = () => {
handleDeleteModel(instanceId);
};
const handleSubmit = (currentMessage: Omit<MessageItem, 'uid'>) => {
const currentMsg =
currentMessage.content || currentMessage.imgs?.length
? currentMessage
: undefined;
submitMessage(currentMsg);
};
const handleApplyToAllModels = (e: any) => {
isApplyToAllModels.current = e.target.checked;
if (e.target.checked) {
setGlobalParams({
..._.omit(params, 'model')
});
}
};
const handleOnValuesChange = useCallback(
(changeValues: any, allValues: Record<string, any>) => {
if (isApplyToAllModels.current) {
setParams({
...params,
...allValues
});
setGlobalParams({
...allValues
});
} else {
setParams({
...params,
...changeValues
});
}
},
[params, isApplyToAllModels.current]
);
const handleClearMessage = () => {
setMessageList([]);
setTokenResult(null);
currentMessageRef.current = [];
};
const addNewMessage = (message: Omit<MessageItem, 'uid'>) => {
setMessageId();
setMessageList((preList) => {
return [
...preList,
{
...message,
uid: messageId.current
}
];
});
};
const handleCloseViewCode = () => {
setShow(false);
};
const handleModelChange = (value: string) => {
setParams({
...params,
model: value
});
handleClearMessage();
};
const handlePresetMessageList = (list: MessageItem[]) => {
currentMessageRef.current = [];
const messages = _.map(list, (item: Omit<MessageItem, 'uid'>) => {
setMessageId();
return {
role: item.role,
content: item.content,
uid: messageId.current
};
});
setTokenResult(null);
setMessageList(messages);
};
const modelOptions = useMemo(() => {
return modelFullList.filter((item) => {
return item.type !== 'empty';
});
}, [modelFullList]);
const actionItems = useMemo(() => {
const list = [
{
label: intl.formatMessage({ id: 'common.button.clear' }),
key: 'clear',
icon: <ClearOutlined />,
danger: false,
onClick: () => {
setMessageList([]);
setSystemMessage('');
}
},
{
label: intl.formatMessage({ id: 'playground.viewcode' }),
key: 'viewcode',
icon: <IconFont type="icon-code" />,
onClick: () => {
setShow(true);
}
}
];
if (modelList.length > 2) {
list.push({
label: intl.formatMessage({ id: 'common.button.delete' }),
key: 'delete',
icon: <DeleteOutlined />,
danger: true,
onClick: () => {
handleDelete();
}
});
}
return list;
}, [modelList, intl]);
useEffect(() => {
setParams({
...params,
...globalParams
});
}, [globalParams]);
useEffect(() => {
return () => {
abortFetch();
};
}, []);
useEffect(() => {
if (modelScrollRef.current) {
initialize(modelScrollRef.current);
}
}, [modelScrollRef.current, initialize]);
useEffect(() => {
if (loadingStatus[instanceId]) {
updateScrollerPosition();
}
}, [messageList]);
useEffect(() => {
if (messageList.length > messageListLengthCache.current) {
updateScrollerPosition();
}
messageListLengthCache.current = messageList.length;
}, [messageList.length]);
useImperativeHandle(ref, () => {
return {
submit: handleSubmit,
abortFetch,
addNewMessage,
clear: handleClearMessage,
presetPrompt: handlePresetMessageList,
setSystemMessage,
loading
};
});
return (
<div className="model-item">
<div className="header">
<span className="title">
<Select
style={{ width: '100%' }}
variant="borderless"
options={modelFullList}
onChange={handleModelChange}
value={params.model}
labelRender={(data) => {
return (
<AutoTooltip
ghost
tooltipProps={{
placement: 'right'
}}
minWidth={60}
maxWidth={180}
>
{data.label}
</AutoTooltip>
);
}}
optionRender={(data) => {
return (
<AutoTooltip
ghost
tooltipProps={{
placement: 'right'
}}
minWidth={60}
maxWidth={180}
>
{data.label}
</AutoTooltip>
);
}}
></Select>
</span>
<ReferenceParams usage={tokenResult} scaleable></ReferenceParams>
<span className="action">
<Dropdown
menu={{
items: actionItems
}}
placement="bottomRight"
>
<Button
type="text"
icon={<MoreOutlined style={{ fontSize: '14px' }} />}
size="small"
></Button>
</Dropdown>
<Popover
placement="bottomRight"
content={
<ParamsSettings
showModelSelector={false}
setParams={setParams}
modelList={modelList}
globalParams={globalParams}
selectedModel={params.model}
onValuesChange={handleOnValuesChange}
/>
}
trigger={['click']}
arrow={false}
fresh={true}
title={
<div>
<Checkbox onChange={handleApplyToAllModels}>
{intl.formatMessage({
id: 'playground.compare.applytoall'
})}
</Checkbox>
</div>
}
>
<Button
type="text"
icon={<SettingOutlined />}
size="small"
></Button>
</Popover>
</span>
</div>
<SystemMessage
showApplyToAll={true}
systemMessage={systemMessage}
applyToAll={handleApplySystemChangeToAll}
setSystemMessage={setSystemMessage}
></SystemMessage>
<div className="content" ref={modelScrollRef}>
<div>
<MessageContent
messageList={messageList}
setMessageList={setMessageList}
editable={true}
/>
<Spin
spinning={!!loadingStatus[instanceId]}
size="small"
style={{ width: '100%' }}
/>
</div>
</div>
<ViewCodeModal
{...OpenAIViewCode.chat}
open={show}
payLoad={{
messages: viewCodeMessage
}}
parameters={params}
onCancel={handleCloseViewCode}
title={intl.formatMessage({ id: 'playground.viewcode' })}
></ViewCodeModal>
</div>
);
}
);
export default React.memo(ModelItem);