Compare commits

...

2 Commits

Author SHA1 Message Date
Cledwyn Lew
362f82fc3e 0.6.1-patch-1 2026-02-09 14:05:47 +08:00
Cledwyn Lew
6f90010c9e 0.6.1 2026-02-09 14:05:45 +08:00
10 changed files with 151 additions and 155 deletions

2
.npmrc
View File

@@ -1 +1 @@
registry=https://registry.npmmirror.com # registry=https://registry.npmmirror.com

View File

@@ -18,7 +18,7 @@
"dependencies": { "dependencies": {
"@heroui/react": "2.8.3", "@heroui/react": "2.8.3",
"@heroui/theme": "2.4.21", "@heroui/theme": "2.4.21",
"@mujian/js-sdk": "0.0.6-beta.65", "@mujian/js-sdk": "0.0.6-beta.69",
"@tailwindcss/vite": "4.1.12", "@tailwindcss/vite": "4.1.12",
"ahooks": "3.9.5", "ahooks": "3.9.5",
"axios": "1.11.0", "axios": "1.11.0",

11
pnpm-lock.yaml generated
View File

@@ -15,8 +15,8 @@ importers:
specifier: 2.4.21 specifier: 2.4.21
version: 2.4.21(tailwindcss@4.1.12) version: 2.4.21(tailwindcss@4.1.12)
'@mujian/js-sdk': '@mujian/js-sdk':
specifier: 0.0.6-beta.65 specifier: 0.0.6-beta.69
version: 0.0.6-beta.65(react-dom@19.1.1(react@19.1.1))(react@19.1.1) version: 0.0.6-beta.69(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
'@tailwindcss/vite': '@tailwindcss/vite':
specifier: 4.1.12 specifier: 4.1.12
version: 4.1.12(vite@7.1.2(@types/node@22.13.9)(jiti@2.5.1)(lightningcss@1.30.1)) version: 4.1.12(vite@7.1.2(@types/node@22.13.9)(jiti@2.5.1)(lightningcss@1.30.1))
@@ -996,8 +996,8 @@ packages:
'@jridgewell/trace-mapping@0.3.30': '@jridgewell/trace-mapping@0.3.30':
resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==}
'@mujian/js-sdk@0.0.6-beta.65': '@mujian/js-sdk@0.0.6-beta.69':
resolution: {integrity: sha512-QMAYn6wx4uLlGSBjkAH8O9gR4x0vgHUpIKupIJSlgIKfjIqgYOZEvvx3/4anH2l6ESXGTR521x2ri9uQj5cuyw==} resolution: {integrity: sha512-hdiKHrbq+AngnV9e7woBTqnPhROJaejDgkFhh4IYpgAloIrarWuNbH1ZUvZU2G6Gqn1o4MwkymrXSmDXe9rMQA==}
peerDependencies: peerDependencies:
react: ~19.1.1 react: ~19.1.1
react-dom: ~19.1.1 react-dom: ~19.1.1
@@ -3942,11 +3942,12 @@ snapshots:
'@jridgewell/resolve-uri': 3.1.2 '@jridgewell/resolve-uri': 3.1.2
'@jridgewell/sourcemap-codec': 1.5.5 '@jridgewell/sourcemap-codec': 1.5.5
'@mujian/js-sdk@0.0.6-beta.65(react-dom@19.1.1(react@19.1.1))(react@19.1.1)': '@mujian/js-sdk@0.0.6-beta.69(react-dom@19.1.1(react@19.1.1))(react@19.1.1)':
dependencies: dependencies:
'@adobe/css-tools': 4.4.4 '@adobe/css-tools': 4.4.4
ahooks: 3.9.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1) ahooks: 3.9.5(react-dom@19.1.1(react@19.1.1))(react@19.1.1)
dompurify: 3.3.1 dompurify: 3.3.1
lodash-es: 4.17.21
postmate: 1.5.2 postmate: 1.5.2
react: 19.1.1 react: 19.1.1
react-dom: 19.1.1(react@19.1.1) react-dom: 19.1.1(react@19.1.1)

View File

@@ -22,6 +22,7 @@ export type LastMessageActionsProps = {
isEditing: boolean; isEditing: boolean;
swipes: string[]; swipes: string[];
activeSwipeId: number; activeSwipeId: number;
panicked?: boolean;
onRegenerate: (() => Promise<void>) | undefined; onRegenerate: (() => Promise<void>) | undefined;
onContinue: (() => Promise<void>) | undefined; onContinue: (() => Promise<void>) | undefined;
onEditButtonClick: () => void; onEditButtonClick: () => void;
@@ -116,6 +117,7 @@ export const MessageActions = ({
item.className || '', item.className || '',
)} )}
onPress={item.onPress} onPress={item.onPress}
isDisabled={item.disabled}
classNames={{ classNames={{
wrapper: mjChatCls( wrapper: mjChatCls(
[ [

View File

@@ -26,7 +26,7 @@ export type MessageBubbleProps = {
}; };
export const MessageBubble = ({ export const MessageBubble = ({
// message, message,
isUser, isUser,
isEditing, isEditing,
editedMessage, editedMessage,
@@ -79,7 +79,13 @@ export const MessageBubble = ({
)} )}
role="presentation" role="presentation"
> >
<MdRenderer content={currentMessage} /> <MdRenderer
content={currentMessage}
extra={{
messageId: message.id,
swipeId: message.activeSwipeId,
}}
/>
</div> </div>
</DropdownTrigger> </DropdownTrigger>
<DropdownMenu <DropdownMenu
@@ -103,6 +109,7 @@ export const MessageBubble = ({
item.className || '', item.className || '',
)} )}
onPress={item.onPress} onPress={item.onPress}
isDisabled={item.disabled}
> >
{item.label} {item.label}
</DropdownItem> </DropdownItem>
@@ -111,7 +118,13 @@ export const MessageBubble = ({
</Dropdown> </Dropdown>
) : ( ) : (
<div className={mjChatCls(['msg-content'], 'py-4')}> <div className={mjChatCls(['msg-content'], 'py-4')}>
<MdRenderer content={currentMessage} /> <MdRenderer
content={currentMessage}
extra={{
messageId: message.id,
swipeId: message.activeSwipeId,
}}
/>
</div> </div>
) )
) : ( ) : (

View File

@@ -1,3 +1,6 @@
import { useContext } from 'react';
import { PanicContext } from '../../PanicContext';
export type DropdownItem = { export type DropdownItem = {
key: string; key: string;
label: string; label: string;
@@ -10,6 +13,7 @@ export type DropdownItem = {
| 'warning' | 'warning'
| 'danger'; | 'danger';
className?: string; className?: string;
disabled?: boolean;
}; };
export type UseDropdownItemsProps = { export type UseDropdownItemsProps = {
@@ -29,11 +33,14 @@ export const useMessageActions = ({
onContinue, onContinue,
onDelete, onDelete,
}: UseDropdownItemsProps): DropdownItem[] => { }: UseDropdownItemsProps): DropdownItem[] => {
const { panicked } = useContext(PanicContext);
const dropdownItems: DropdownItem[] = [ const dropdownItems: DropdownItem[] = [
{ {
key: 'edit', key: 'edit',
label: '编辑', label: '编辑',
onPress: onEditButtonClick, onPress: onEditButtonClick,
disabled: panicked,
}, },
{ {
key: 'copy', key: 'copy',
@@ -46,11 +53,13 @@ export const useMessageActions = ({
key: 'regenerate', key: 'regenerate',
label: '重说', label: '重说',
onPress: async () => await onRegenerate?.(), onPress: async () => await onRegenerate?.(),
disabled: panicked,
}, },
{ {
key: 'continue', key: 'continue',
label: '继续', label: '继续',
onPress: async () => await onContinue?.(), onPress: async () => await onContinue?.(),
disabled: panicked,
}, },
] ]
: []), : []),
@@ -60,6 +69,7 @@ export const useMessageActions = ({
onPress: onDelete, onPress: onDelete,
color: 'danger', color: 'danger',
className: 'text-danger', className: 'text-danger',
disabled: panicked,
}, },
]; ];

View File

@@ -83,6 +83,8 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
swipes = [], swipes = [],
activeSwipeId, activeSwipeId,
mjv,
// sendAt, // sendAt,
} = message; } = message;
const isUser = role === 'user'; const isUser = role === 'user';
@@ -183,7 +185,7 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
// 按ctrl点击时打印ID // 按ctrl点击时打印ID
if (event.ctrlKey) { if (event.ctrlKey) {
event.preventDefault(); event.preventDefault();
console.log('Official Chat Message ID:', id); console.log('Official Chat Message ID:', id, mjv);
} }
}} }}
> >

View File

@@ -1,6 +1,8 @@
import { Button, Textarea } from '@heroui/react'; import { Button, Textarea } from '@heroui/react';
import { CircleStopIcon, SendIcon } from 'lucide-react'; import { CircleStopIcon, SendIcon } from 'lucide-react';
import { mjChatCls } from '@/utils/cls'; import { mjChatCls } from '@/utils/cls';
import { useContext } from 'react';
import { PanicContext } from './PanicContext';
interface Props { interface Props {
onSend: (query: string) => void; onSend: (query: string) => void;
@@ -28,6 +30,8 @@ export const MsgSend = ({
value, value,
onChange, onChange,
}: Props) => { }: Props) => {
const { panicked } = useContext(PanicContext);
const isEmptyInput = value.trim().length === 0; const isEmptyInput = value.trim().length === 0;
const handleSend = () => { const handleSend = () => {
@@ -64,7 +68,7 @@ export const MsgSend = ({
), ),
mainWrapper: mjChatCls(['input-textarea-mainWrapper'], 'px-2'), mainWrapper: mjChatCls(['input-textarea-mainWrapper'], 'px-2'),
}} }}
disabled={running} disabled={running || panicked}
endContent={ endContent={
running ? ( running ? (
<Button <Button

View File

@@ -0,0 +1,3 @@
import { createContext } from 'react';
export const PanicContext = createContext({ panicked: false });

View File

@@ -1,46 +1,13 @@
import { Alert, ScrollShadow, Spinner, ToastProvider } from '@heroui/react'; import { Alert, ScrollShadow, Spinner, ToastProvider } from '@heroui/react';
import { useChat, useMujian } from '@mujian/js-sdk/react'; import { useChat, useMjEngine, useMujian } from '@mujian/js-sdk/react';
import type { PersonaInfo, ProjectInfo } from '@mujian/js-sdk/types';
import { useCallback, useEffect, useMemo, useState } from 'react'; import { useCallback, useEffect, useMemo, useState } from 'react';
import { useGlobalStore } from '@/store/global'; import { useGlobalStore } from '@/store/global';
import { mjChatCls } from '@/utils/cls'; import { mjChatCls } from '@/utils/cls';
import { MessageList } from './MessageList'; import { MessageList } from './MessageList';
import { MsgSend } from './MsgSend'; import { MsgSend } from './MsgSend';
import { PanicContext } from './PanicContext';
import { QuickReply } from './QuickReply'; import { QuickReply } from './QuickReply';
// 扩展Window接口以包含chat对象
declare global {
interface Window {
$mj_engine: {
ai: {
chat: {
complete?: (message: string) => Promise<void>;
project?: ProjectInfo;
settings: {
persona?: PersonaInfo;
};
/**
* @param index 下标从1开始
*/
setFirstMesIndex?: (index: number) => Promise<void>;
};
};
};
}
}
window.$mj_engine = {
ai: {
chat: {
complete: undefined,
project: undefined,
settings: {
persona: undefined,
},
},
},
};
export const Chat = () => { export const Chat = () => {
const { init, projectInfo, activePersona } = useGlobalStore(); const { init, projectInfo, activePersona } = useGlobalStore();
const mujian = useMujian(); const mujian = useMujian();
@@ -64,11 +31,6 @@ export const Chat = () => {
pageSize: 20, pageSize: 20,
}); });
useEffect(() => {
window.$mj_engine.ai.chat.project = projectInfo ?? undefined;
window.$mj_engine.ai.chat.settings.persona = activePersona ?? undefined;
}, [projectInfo, activePersona]);
// 自定义删除消息函数 // 自定义删除消息函数
const handleDeleteMessage = async (messageId: string) => { const handleDeleteMessage = async (messageId: string) => {
// 找到要删除的消息 // 找到要删除的消息
@@ -128,26 +90,13 @@ export const Chat = () => {
[append, projectInfo, activePersona], [append, projectInfo, activePersona],
); );
// 将chat对象挂载到window上 useMjEngine({
useEffect(() => { projectInfo: projectInfo ?? undefined,
window.$mj_engine.ai.chat.complete = async (message) => { activePersona: activePersona ?? undefined,
await onSend(message); onSend,
}; messages,
window.$mj_engine.ai.chat.setFirstMesIndex = async (oneBasedIndex) => { setSwipe,
if (messages.length > 1) { });
throw new Error('已有新消息,不允许切换开场白');
}
if (messages.length === 0) {
throw new Error('断言失败:没有开场消息,无法切换开场白');
}
const zeroBasedIndex = Math.floor(oneBasedIndex) - 1;
const swipeLength = messages[0].swipes.length;
if (zeroBasedIndex < 0 || zeroBasedIndex >= swipeLength) {
return;
}
setSwipe(messages[0].id, zeroBasedIndex);
};
}, [onSend, messages, setSwipe]);
const handleInputChange = (value: string) => { const handleInputChange = (value: string) => {
setInputValue(value); setInputValue(value);
@@ -175,61 +124,67 @@ export const Chat = () => {
margin-top: ${paddingTop}px; margin-top: ${paddingTop}px;
}`; }`;
const saveFailed =
status !== 'streaming' &&
messages.some((m) => m.id.startsWith('not_saved'));
return ( return (
<div className={mjChatCls(['container'], 'size-full relative')}> <PanicContext.Provider
<style> {toastStyle} </style> value={{ panicked: saveFailed || Boolean(errorMessage) }}
<ToastProvider >
placement="top-center" <div className={mjChatCls(['container'], 'size-full relative')}>
toastProps={{ color: 'danger' }} <style> {toastStyle} </style>
disableAnimation={true} <ToastProvider
regionProps={{ placement="top-center"
classNames: { toastProps={{ color: 'danger' }}
base: 'toast-region', disableAnimation={true}
}, regionProps={{
}} classNames: {
/> base: 'toast-region',
<div },
className={mjChatCls( }}
['container-background'], />
'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)]', <div
)}
style={{
backgroundImage: projectInfo
? `url(${projectInfo.coverImageUrl})`
: undefined,
}}
/>
<div
className={mjChatCls(
['container-content'],
'size-full z-10 p-3 flex flex-col',
)}
style={{
paddingTop,
}}
>
<ScrollShadow
hideScrollBar
className={mjChatCls( className={mjChatCls(
['container-content-scroll-shadow'], ['container-background'],
'h-full pb-2', '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)]',
)} )}
size={20} style={{
backgroundImage: projectInfo
? `url(${projectInfo.coverImageUrl})`
: undefined,
}}
/>
<div
className={mjChatCls(
['container-content'],
'size-full z-10 p-3 flex flex-col',
)}
style={{
paddingTop,
}}
> >
<MessageList <ScrollShadow
data={messages} hideScrollBar
sendMessage={async (q) => { className={mjChatCls(
await onSend(q); ['container-content-scroll-shadow'],
}} 'h-full pb-2',
onContinue={continueGenerate} )}
onDelete={handleDeleteMessage} size={20}
onEdit={editMessage} >
onSwipe={setSwipe} <MessageList
onRegenerate={regenerate} data={messages}
onNeedMore={loadMoreMessage} sendMessage={async (q) => {
/> await onSend(q);
{status !== 'streaming' && }}
messages.some((m) => m.id.startsWith('not_saved')) && ( onContinue={continueGenerate}
onDelete={handleDeleteMessage}
onEdit={editMessage}
onSwipe={setSwipe}
onRegenerate={regenerate}
onNeedMore={loadMoreMessage}
/>
{saveFailed && (
<Alert <Alert
hideIcon hideIcon
title="最近一次保存消息失败,请刷新页面后重试" title="最近一次保存消息失败,请刷新页面后重试"
@@ -241,41 +196,47 @@ export const Chat = () => {
variant="bordered" variant="bordered"
/> />
)} )}
{errorMessage && ( {errorMessage && (
<Alert <Alert
hideIcon hideIcon
className={mjChatCls(['error-message'], 'mt-2 mb-6')} className={mjChatCls(['error-message'], 'mt-2 mb-6')}
classNames={{ classNames={{
mainWrapper: mjChatCls( mainWrapper: mjChatCls(
['error-message-mainWrapper'], ['error-message-mainWrapper'],
'min-h-4 ml-0', 'min-h-4 ml-0',
), ),
}} description: 'opacity-80 text-xs',
color="danger" }}
title={errorMessage} color="danger"
variant="bordered" title="出错啦,请刷新页面后重试"
/> description={errorMessage}
)} variant="bordered"
</ScrollShadow>
<div
className={mjChatCls(['input-container'], 'flex flex-col gap-4 mt-2')}
>
{projectInfo?.config?.qrConfig?.qrList &&
projectInfo?.config?.qrConfig?.qrList.length > 0 && (
<QuickReply
quickReplies={projectInfo?.config?.qrConfig.qrList}
onSend={onSend}
/> />
)} )}
<MsgSend </ScrollShadow>
running={status === 'streaming'} <div
onSend={onSend} className={mjChatCls(
onStop={stop} ['input-container'],
value={inputValue} 'flex flex-col gap-4 mt-2',
onChange={handleInputChange} )}
/> >
{projectInfo?.config?.qrConfig?.qrList &&
projectInfo?.config?.qrConfig?.qrList.length > 0 && (
<QuickReply
quickReplies={projectInfo?.config?.qrConfig.qrList}
onSend={onSend}
/>
)}
<MsgSend
running={status === 'streaming'}
onSend={onSend}
onStop={stop}
value={inputValue}
onChange={handleInputChange}
/>
</div>
</div> </div>
</div> </div>
</div> </PanicContext.Provider>
); );
}; };