Files
official-chat/src/pages/chat/index.tsx
Cledwyn Lew 54f9dc947d init
2025-11-25 13:32:36 +08:00

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>
);
};