170 lines
4.5 KiB
TypeScript
170 lines
4.5 KiB
TypeScript
import { Alert, ScrollShadow, Spinner } from '@heroui/react';
|
|
import { useChat, useMujian } from '@mujian/js-sdk/react';
|
|
import { useEffect, useMemo, useState } from 'react';
|
|
import { useGlobalStore } from '@/store/global';
|
|
import { MessageList } from './MessageList';
|
|
import { MsgSend } from './MsgSend';
|
|
|
|
// 扩展Window接口以包含chat对象
|
|
declare global {
|
|
interface Window {
|
|
$mj_engine: {
|
|
chat: {
|
|
complete?: (message: string) => Promise<void>;
|
|
};
|
|
};
|
|
}
|
|
}
|
|
|
|
window.$mj_engine = {
|
|
chat: {},
|
|
};
|
|
|
|
export const Chat = () => {
|
|
const { init, projectInfo } = useGlobalStore();
|
|
const mujian = useMujian();
|
|
const [inputValue, setInputValue] = useState('');
|
|
const {
|
|
messages,
|
|
status,
|
|
error,
|
|
append,
|
|
continueGenerate,
|
|
regenerate,
|
|
stop,
|
|
deleteMessage,
|
|
editMessage,
|
|
setSwipe,
|
|
} = useChat({
|
|
onError: (e) => {
|
|
console.error(e);
|
|
},
|
|
});
|
|
|
|
// 自定义删除消息函数
|
|
const handleDeleteMessage = async (messageId: string) => {
|
|
// 找到要删除的消息
|
|
const messageIndex = messages.findIndex((msg) => msg.id === messageId);
|
|
if (messageIndex === -1) return;
|
|
|
|
let deletedUserMessageContent = '';
|
|
|
|
// 检查前一条消息是否是用户消息且被删除的消息是AI消息
|
|
if (messageIndex > 0 && messages[messageIndex].role === 'assistant') {
|
|
const prevMessage = messages[messageIndex - 1];
|
|
if (prevMessage.role === 'user') {
|
|
// 保存用户消息内容
|
|
deletedUserMessageContent = prevMessage.content;
|
|
// 同时删除前一条用户消息
|
|
deleteMessage(prevMessage.id);
|
|
}
|
|
}
|
|
|
|
// 删除当前消息
|
|
await deleteMessage(messageId);
|
|
|
|
// 如果有删除的用户消息,将其内容填入发送框
|
|
if (deletedUserMessageContent) {
|
|
setInputValue(deletedUserMessageContent);
|
|
}
|
|
};
|
|
|
|
const paddingTop = useMemo(() => {
|
|
const params = new URLSearchParams(window.location.search);
|
|
return parseInt(params.get('insetTop') || '0', 10);
|
|
}, []);
|
|
|
|
// biome-ignore lint/correctness/useExhaustiveDependencies: onMount ONLY
|
|
useEffect(() => {
|
|
init(mujian);
|
|
}, []);
|
|
|
|
const onSend = append;
|
|
|
|
// 将chat对象挂载到window上
|
|
useEffect(() => {
|
|
window.$mj_engine.chat.complete = async (message: string) => {
|
|
await onSend(message);
|
|
};
|
|
|
|
return () => {
|
|
delete window.$mj_engine.chat.complete;
|
|
};
|
|
}, [onSend]);
|
|
|
|
const handleInputChange = (value: string) => {
|
|
setInputValue(value);
|
|
};
|
|
|
|
if (status === 'uninitialized') {
|
|
return (
|
|
<div className="w-full h-full flex justify-center items-center">
|
|
<Spinner />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const errorMessage =
|
|
Boolean(error) &&
|
|
(typeof error === 'object' &&
|
|
error &&
|
|
'message' in error &&
|
|
typeof error.message === 'string'
|
|
? error.message
|
|
: '哎呀,发生了未知错误,刷新页面再试试吧~');
|
|
|
|
return (
|
|
<div className="size-full relative">
|
|
<div
|
|
className="h-full opacity-30 blur-xs object-cover absolute top-0 left-1/2 w-[min(640px,100%)] -translate-x-1/2 bg-cover bg-top bg-no-repeat [mask-image:linear-gradient(to_right,transparent,black_100px,black_calc(100%-100px),transparent)]"
|
|
style={{
|
|
backgroundImage: projectInfo
|
|
? `url(${projectInfo.coverImageUrl})`
|
|
: undefined,
|
|
}}
|
|
/>
|
|
<div
|
|
className="size-full z-10 p-3 flex flex-col"
|
|
style={{
|
|
paddingTop,
|
|
}}
|
|
>
|
|
<ScrollShadow hideScrollBar className="h-full" size={20}>
|
|
<MessageList
|
|
data={messages}
|
|
sendMessage={async (q) => {
|
|
await onSend(q);
|
|
}}
|
|
onContinue={continueGenerate}
|
|
onDelete={handleDeleteMessage}
|
|
onEdit={editMessage}
|
|
onSwipe={setSwipe}
|
|
onRegenerate={regenerate}
|
|
/>
|
|
{errorMessage && (
|
|
<Alert
|
|
hideIcon
|
|
className="mt-2 mb-6"
|
|
classNames={{
|
|
mainWrapper: 'min-h-4 ml-0',
|
|
}}
|
|
color="danger"
|
|
title={errorMessage}
|
|
variant="bordered"
|
|
/>
|
|
)}
|
|
</ScrollShadow>
|
|
<div className="flex flex-col gap-2 mt-2">
|
|
<MsgSend
|
|
running={status === 'streaming'}
|
|
onSend={onSend}
|
|
onStop={stop}
|
|
value={inputValue}
|
|
onChange={handleInputChange}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
);
|
|
};
|