0.4
This commit is contained in:
@@ -25,6 +25,11 @@
|
|||||||
"quoteStyle": "single"
|
"quoteStyle": "single"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"css": {
|
||||||
|
"parser": {
|
||||||
|
"tailwindDirectives": true
|
||||||
|
}
|
||||||
|
},
|
||||||
"overrides": [
|
"overrides": [
|
||||||
{
|
{
|
||||||
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
"includes": ["**/*.{ts,tsx,js,jsx}"],
|
||||||
@@ -319,4 +324,3 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -18,11 +18,10 @@
|
|||||||
"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.41",
|
"@mujian/js-sdk": "0.0.6-beta.56",
|
||||||
"@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",
|
||||||
"biome": "^0.3.3",
|
|
||||||
"dayjs": "1.11.18",
|
"dayjs": "1.11.18",
|
||||||
"lodash-es": "4.17.21",
|
"lodash-es": "4.17.21",
|
||||||
"lucide-react": "^0.546.0",
|
"lucide-react": "^0.546.0",
|
||||||
@@ -37,6 +36,7 @@
|
|||||||
"zustand": "5.0.8"
|
"zustand": "5.0.8"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"@biomejs/biome": "^2.3.10",
|
||||||
"@mujian/cli": "0.0.0",
|
"@mujian/cli": "0.0.0",
|
||||||
"@types/lodash-es": "4.17.12",
|
"@types/lodash-es": "4.17.12",
|
||||||
"@types/node": "22.13.9",
|
"@types/node": "22.13.9",
|
||||||
|
|||||||
854
pnpm-lock.yaml
generated
854
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
|||||||
import { Button, ButtonGroup, cn } from '@heroui/react';
|
import { Button, ButtonGroup, cn } from '@heroui/react';
|
||||||
|
import { mjChatCls } from '@/utils/cls';
|
||||||
|
|
||||||
export type EditActionsProps = {
|
export type EditActionsProps = {
|
||||||
isUser: boolean;
|
isUser: boolean;
|
||||||
@@ -17,26 +18,38 @@ export const EditActions = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={mjChatCls(
|
||||||
`flex w-full row-start-2 col-start-1 col-end-4`,
|
['msg-edit-actions-wrapper'],
|
||||||
isUser ? 'justify-end' : 'justify-start',
|
cn(
|
||||||
|
`flex w-full row-start-2 col-start-1 col-end-4`,
|
||||||
|
isUser ? 'justify-end' : 'justify-start',
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
className="bg-white/15 text-default rounded-full"
|
className={mjChatCls(
|
||||||
|
['msg-edit-actions-button-group'],
|
||||||
|
'bg-white/15 text-default rounded-full',
|
||||||
|
)}
|
||||||
radius="full"
|
radius="full"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="text-default"
|
className={mjChatCls(
|
||||||
|
['msg-edit-actions-button', 'msg-edit-actions-button-save'],
|
||||||
|
'text-default',
|
||||||
|
)}
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={onSaveEdit}
|
onPress={onSaveEdit}
|
||||||
>
|
>
|
||||||
保存
|
保存
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
className="text-default"
|
className={mjChatCls(
|
||||||
|
['msg-edit-actions-button', 'msg-edit-actions-button-cancel'],
|
||||||
|
'text-default',
|
||||||
|
)}
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={onCancelEdit}
|
onPress={onCancelEdit}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import {
|
|||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { ChevronLeftIcon, ChevronRightIcon, Ellipsis } from 'lucide-react';
|
import { ChevronLeftIcon, ChevronRightIcon, Ellipsis } from 'lucide-react';
|
||||||
import React from 'react';
|
import React from 'react';
|
||||||
|
import { mjChatCls } from '@/utils/cls';
|
||||||
import { useMessageActions } from './useMessageActions';
|
import { useMessageActions } from './useMessageActions';
|
||||||
|
|
||||||
export type LastMessageActionsProps = {
|
export type LastMessageActionsProps = {
|
||||||
@@ -22,6 +23,7 @@ export type LastMessageActionsProps = {
|
|||||||
onRegenerate: (() => Promise<void>) | undefined;
|
onRegenerate: (() => Promise<void>) | undefined;
|
||||||
onContinue: (() => Promise<void>) | undefined;
|
onContinue: (() => Promise<void>) | undefined;
|
||||||
onEditButtonClick: () => void;
|
onEditButtonClick: () => void;
|
||||||
|
onCopyClick: () => void;
|
||||||
onSwipe: (direction: 'prev' | 'next') => void;
|
onSwipe: (direction: 'prev' | 'next') => void;
|
||||||
onMessageTextClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
onMessageTextClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
@@ -38,6 +40,7 @@ export const MessageActions = ({
|
|||||||
onRegenerate,
|
onRegenerate,
|
||||||
onContinue,
|
onContinue,
|
||||||
onEditButtonClick,
|
onEditButtonClick,
|
||||||
|
onCopyClick,
|
||||||
onSwipe,
|
onSwipe,
|
||||||
onMessageTextClick,
|
onMessageTextClick,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -45,6 +48,7 @@ export const MessageActions = ({
|
|||||||
const dropdownItems = useMessageActions({
|
const dropdownItems = useMessageActions({
|
||||||
isLastMsg,
|
isLastMsg,
|
||||||
onEditButtonClick,
|
onEditButtonClick,
|
||||||
|
onCopyClick,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
onContinue,
|
onContinue,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -54,31 +58,82 @@ export const MessageActions = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={mjChatCls(
|
||||||
'message-actions flex justify-between row-start-2 col-start-1 col-end-4',
|
['msg-actions-wrapper'],
|
||||||
|
cn('flex justify-between row-start-2 col-start-1 col-end-4'),
|
||||||
)}
|
)}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
onClick={onMessageTextClick} /* Stop propagation */
|
onClick={onMessageTextClick} /* Stop propagation */
|
||||||
>
|
>
|
||||||
<div className="flex items-start">
|
<div
|
||||||
|
className={mjChatCls(
|
||||||
|
['msg-actions-dropdown-container'],
|
||||||
|
'flex items-start',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{!isFirstMsg && (
|
{!isFirstMsg && (
|
||||||
<Dropdown placement="top-start" className="dark" size="sm">
|
<Dropdown
|
||||||
|
placement="top-start"
|
||||||
|
className={mjChatCls(['msg-actions-dropdown'], 'dark')}
|
||||||
|
size="sm"
|
||||||
|
>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<Button
|
<Button
|
||||||
variant="light"
|
variant="light"
|
||||||
size="sm"
|
size="sm"
|
||||||
className="bg-white/15 text-default min-w-10"
|
className={mjChatCls(
|
||||||
|
['msg-actions-dropdown-button'],
|
||||||
|
'bg-white/15 text-default min-w-10',
|
||||||
|
)}
|
||||||
>
|
>
|
||||||
<Ellipsis size={14}/>
|
<Ellipsis
|
||||||
|
size={14}
|
||||||
|
className={mjChatCls(['msg-actions-dropdown-button-icon'])}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu items={dropdownItems}>
|
<DropdownMenu
|
||||||
|
items={dropdownItems}
|
||||||
|
className={mjChatCls(
|
||||||
|
['msg-actions-dropdown-menu'],
|
||||||
|
'text-[initial]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={item.key}
|
key={item.key}
|
||||||
color={item.color}
|
color={item.color}
|
||||||
className={item.className}
|
className={mjChatCls(
|
||||||
|
[
|
||||||
|
'msg-actions-dropdown-menu-item',
|
||||||
|
`msg-actions-dropdown-menu-item-${item.key}`,
|
||||||
|
],
|
||||||
|
item.className || '',
|
||||||
|
)}
|
||||||
onPress={item.onPress}
|
onPress={item.onPress}
|
||||||
|
classNames={{
|
||||||
|
wrapper: mjChatCls(
|
||||||
|
[
|
||||||
|
'msg-actions-dropdown-menu-item-wrapper',
|
||||||
|
`msg-actions-dropdown-menu-item-${item.key}-wrapper`,
|
||||||
|
],
|
||||||
|
item.className || '',
|
||||||
|
),
|
||||||
|
base: mjChatCls(
|
||||||
|
[
|
||||||
|
'msg-actions-dropdown-menu-item-base',
|
||||||
|
`msg-actions-dropdown-menu-item-${item.key}-base`,
|
||||||
|
],
|
||||||
|
item.className || '',
|
||||||
|
),
|
||||||
|
title: mjChatCls(
|
||||||
|
[
|
||||||
|
'msg-actions-dropdown-menu-item-title',
|
||||||
|
`msg-actions-dropdown-menu-item-${item.key}-title`,
|
||||||
|
],
|
||||||
|
item.className || '',
|
||||||
|
),
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -87,34 +142,52 @@ export const MessageActions = ({
|
|||||||
</Dropdown>
|
</Dropdown>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="flex items-end">
|
<div className={mjChatCls(['swipe-action-container'], 'flex items-end')}>
|
||||||
{isLastMsg && swipes.length > 1 && (
|
{isLastMsg && swipes.length > 1 && (
|
||||||
<ButtonGroup
|
<ButtonGroup
|
||||||
className="bg-white/15 rounded-full backdrop-blur-sm text-default"
|
className={mjChatCls(
|
||||||
|
['swipe-action-button-group'],
|
||||||
|
'bg-white/15 rounded-full backdrop-blur-sm text-default',
|
||||||
|
)}
|
||||||
radius="full"
|
radius="full"
|
||||||
size="sm"
|
size="sm"
|
||||||
variant="light"
|
variant="light"
|
||||||
>
|
>
|
||||||
<Button
|
<Button
|
||||||
className="min-w-8 px-2 text-default"
|
className={mjChatCls(
|
||||||
|
['swipe-action-prev'],
|
||||||
|
'min-w-8 px-2 text-default',
|
||||||
|
)}
|
||||||
isDisabled={swipes.length <= 1}
|
isDisabled={swipes.length <= 1}
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => onSwipe('prev')}
|
onPress={() => onSwipe('prev')}
|
||||||
>
|
>
|
||||||
<ChevronLeftIcon className="w-4 h-4" />
|
<ChevronLeftIcon
|
||||||
|
className={mjChatCls(['swipe-action-prev-icon'], 'w-4 h-4')}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
<div className="flex items-center px-1 text-xs">
|
<div
|
||||||
|
className={mjChatCls(
|
||||||
|
['swipe-action-index'],
|
||||||
|
'flex items-center px-1 text-xs',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{swipes.length > 0
|
{swipes.length > 0
|
||||||
? `${activeSwipeId + 1}/${swipes.length}`
|
? `${activeSwipeId + 1}/${swipes.length}`
|
||||||
: '1/1'}
|
: '1/1'}
|
||||||
</div>
|
</div>
|
||||||
<Button
|
<Button
|
||||||
className="min-w-8 px-2 text-default"
|
className={mjChatCls(
|
||||||
|
['swipe-action-next'],
|
||||||
|
'min-w-8 px-2 text-default',
|
||||||
|
)}
|
||||||
isDisabled={swipes.length <= 1}
|
isDisabled={swipes.length <= 1}
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => onSwipe('next')}
|
onPress={() => onSwipe('next')}
|
||||||
>
|
>
|
||||||
<ChevronRightIcon className="w-4 h-4" />
|
<ChevronRightIcon
|
||||||
|
className={mjChatCls(['swipe-action-next-icon'], 'w-4 h-4')}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import {
|
|||||||
Spinner,
|
Spinner,
|
||||||
} from '@heroui/react';
|
} from '@heroui/react';
|
||||||
import { type Message as BaseMessage, MdRenderer } from '@mujian/js-sdk/react';
|
import { type Message as BaseMessage, MdRenderer } from '@mujian/js-sdk/react';
|
||||||
|
import { mjChatCls } from '@/utils/cls';
|
||||||
import { MessageEditArea } from './MessageEditArea';
|
import { MessageEditArea } from './MessageEditArea';
|
||||||
import { useMessageActions } from './useMessageActions';
|
import { useMessageActions } from './useMessageActions';
|
||||||
|
|
||||||
@@ -19,6 +20,7 @@ export type MessageBubbleProps = {
|
|||||||
isStreaming: boolean;
|
isStreaming: boolean;
|
||||||
currentMessage: string;
|
currentMessage: string;
|
||||||
onEditButtonClick: () => void;
|
onEditButtonClick: () => void;
|
||||||
|
onCopyClick: () => void;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
onEditAreaShow: () => void;
|
onEditAreaShow: () => void;
|
||||||
};
|
};
|
||||||
@@ -31,44 +33,72 @@ export const MessageBubble = ({
|
|||||||
isStreaming,
|
isStreaming,
|
||||||
currentMessage,
|
currentMessage,
|
||||||
onEditButtonClick,
|
onEditButtonClick,
|
||||||
|
onCopyClick,
|
||||||
onDelete,
|
onDelete,
|
||||||
onEditAreaShow,
|
onEditAreaShow,
|
||||||
}: MessageBubbleProps) => {
|
}: MessageBubbleProps) => {
|
||||||
const dropdownItems = useMessageActions({
|
const dropdownItems = useMessageActions({
|
||||||
isLastMsg: false,
|
isLastMsg: false,
|
||||||
onEditButtonClick,
|
onEditButtonClick,
|
||||||
|
onCopyClick,
|
||||||
onDelete,
|
onDelete,
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={mjChatCls(
|
||||||
'row-start-1 col-start-1 col-end-4 flex flex-col size-full min-w-0 message-wrapper',
|
[
|
||||||
isUser ? 'items-end' : 'items-stretch',
|
'msg-container-wrapper',
|
||||||
|
isUser
|
||||||
|
? 'msg-container-wrapper-user'
|
||||||
|
: 'msg-container-wrapper-assistant',
|
||||||
|
],
|
||||||
|
cn(
|
||||||
|
'row-start-1 col-start-1 col-end-4 flex flex-col size-full min-w-0 py-3',
|
||||||
|
isUser ? 'items-end' : 'items-stretch',
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
>
|
>
|
||||||
{!isEditing ? (
|
{!isEditing ? (
|
||||||
isUser ? (
|
isUser ? (
|
||||||
<Dropdown placement="top-end" className="dark">
|
<Dropdown
|
||||||
|
placement="top-end"
|
||||||
|
className={mjChatCls(['msg-edit-dropdown'], 'dark')}
|
||||||
|
>
|
||||||
<DropdownTrigger>
|
<DropdownTrigger>
|
||||||
<div
|
<div
|
||||||
className={cn(
|
className={mjChatCls(
|
||||||
'h-full rounded-xl dark:prose-invert text-blue-100 p-4 bg-black/50',
|
['msg-edit-trigger'],
|
||||||
{
|
cn(
|
||||||
'rounded-br-sm': isUser,
|
'h-full rounded-xl dark:prose-invert text-blue-100 p-4 bg-black/50',
|
||||||
},
|
{
|
||||||
|
'rounded-br-sm': isUser,
|
||||||
|
},
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
role="presentation"
|
role="presentation"
|
||||||
>
|
>
|
||||||
<MdRenderer content={currentMessage} />
|
<MdRenderer content={currentMessage} />
|
||||||
</div>
|
</div>
|
||||||
</DropdownTrigger>
|
</DropdownTrigger>
|
||||||
<DropdownMenu items={dropdownItems}>
|
<DropdownMenu
|
||||||
|
items={dropdownItems}
|
||||||
|
className={mjChatCls(
|
||||||
|
['msg-edit-dropdown-menu'],
|
||||||
|
'text-[initial]',
|
||||||
|
)}
|
||||||
|
>
|
||||||
{(item) => (
|
{(item) => (
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
key={item.key}
|
key={item.key}
|
||||||
color={item.color}
|
color={item.color}
|
||||||
className={item.className}
|
className={mjChatCls(
|
||||||
|
[
|
||||||
|
'msg-edit-dropdown-menu-item',
|
||||||
|
`msg-edit-dropdown-menu-item-${item.key}`,
|
||||||
|
],
|
||||||
|
item.className || '',
|
||||||
|
)}
|
||||||
onPress={item.onPress}
|
onPress={item.onPress}
|
||||||
>
|
>
|
||||||
{item.label}
|
{item.label}
|
||||||
@@ -77,9 +107,9 @@ export const MessageBubble = ({
|
|||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
) : (
|
) : (
|
||||||
<div className="py-4">
|
// <div className={mjChatCls(['msg-content-wrapper'], "py-4")}>
|
||||||
<MdRenderer content={currentMessage} />
|
<MdRenderer content={currentMessage} />
|
||||||
</div>
|
// </div>
|
||||||
)
|
)
|
||||||
) : (
|
) : (
|
||||||
<MessageEditArea
|
<MessageEditArea
|
||||||
@@ -90,7 +120,12 @@ export const MessageBubble = ({
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{isStreaming && !currentMessage?.length && (
|
{isStreaming && !currentMessage?.length && (
|
||||||
<div className="flex w-full items-center">
|
<div
|
||||||
|
className={mjChatCls(
|
||||||
|
['msg-container-message-spinner'],
|
||||||
|
'flex w-full items-center',
|
||||||
|
)}
|
||||||
|
>
|
||||||
<Spinner variant="dots" />
|
<Spinner variant="dots" />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { cn } from '@heroui/react';
|
import { cn } from '@heroui/react';
|
||||||
import { useEffect, useRef, useState } from 'react';
|
import { useEffect, useRef, useState } from 'react';
|
||||||
|
import { mjChatCls } from '@/utils/cls';
|
||||||
|
|
||||||
export type MessageEditAreaProps = {
|
export type MessageEditAreaProps = {
|
||||||
isUser: boolean;
|
isUser: boolean;
|
||||||
@@ -39,16 +40,22 @@ export const MessageEditArea = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="w-full"
|
className={mjChatCls(['msg-edit-textarea-wrapper'], 'w-full')}
|
||||||
style={{
|
style={{
|
||||||
height: wrapHeight,
|
height: wrapHeight,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<textarea
|
<textarea
|
||||||
ref={textareaRef}
|
ref={textareaRef}
|
||||||
className={cn(
|
className={mjChatCls(
|
||||||
'w-full outline-none resize-none h-fit overflow-hidden p-4 bg-background rounded-xl text-[16px]',
|
[
|
||||||
isUser ? 'rounded-br-sm' : 'rounded-bl-sm',
|
'msg-edit-textarea',
|
||||||
|
isUser ? 'msg-edit-textarea-user' : 'msg-edit-textarea-assistant',
|
||||||
|
],
|
||||||
|
cn(
|
||||||
|
'w-full outline-none resize-none h-fit overflow-hidden p-4 bg-background rounded-xl text-[16px]',
|
||||||
|
isUser ? 'rounded-br-sm' : 'rounded-bl-sm',
|
||||||
|
),
|
||||||
)}
|
)}
|
||||||
value={editedMessage}
|
value={editedMessage}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ export type DropdownItem = {
|
|||||||
export type UseDropdownItemsProps = {
|
export type UseDropdownItemsProps = {
|
||||||
isLastMsg: boolean;
|
isLastMsg: boolean;
|
||||||
onEditButtonClick: () => void;
|
onEditButtonClick: () => void;
|
||||||
|
onCopyClick: () => void;
|
||||||
onRegenerate?: (() => Promise<void>) | undefined;
|
onRegenerate?: (() => Promise<void>) | undefined;
|
||||||
onContinue?: (() => Promise<void>) | undefined;
|
onContinue?: (() => Promise<void>) | undefined;
|
||||||
onDelete: () => void;
|
onDelete: () => void;
|
||||||
@@ -23,6 +24,7 @@ export type UseDropdownItemsProps = {
|
|||||||
export const useMessageActions = ({
|
export const useMessageActions = ({
|
||||||
isLastMsg,
|
isLastMsg,
|
||||||
onEditButtonClick,
|
onEditButtonClick,
|
||||||
|
onCopyClick,
|
||||||
onRegenerate,
|
onRegenerate,
|
||||||
onContinue,
|
onContinue,
|
||||||
onDelete,
|
onDelete,
|
||||||
@@ -33,6 +35,11 @@ export const useMessageActions = ({
|
|||||||
label: '编辑',
|
label: '编辑',
|
||||||
onPress: onEditButtonClick,
|
onPress: onEditButtonClick,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
key: 'copy',
|
||||||
|
label: '复制',
|
||||||
|
onPress: onCopyClick,
|
||||||
|
},
|
||||||
...(isLastMsg
|
...(isLastMsg
|
||||||
? [
|
? [
|
||||||
{
|
{
|
||||||
@@ -58,4 +65,3 @@ export const useMessageActions = ({
|
|||||||
|
|
||||||
return dropdownItems;
|
return dropdownItems;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { cn } from '@heroui/react';
|
import { addToast, cn } from '@heroui/react';
|
||||||
import { type Message as BaseMessage } from '@mujian/js-sdk/react';
|
import { type Message as BaseMessage, useMujian } from '@mujian/js-sdk/react';
|
||||||
import React, { type MouseEventHandler } from 'react';
|
import React, { type MouseEventHandler } from 'react';
|
||||||
|
import { mjChatCls } from '@/utils/cls';
|
||||||
import { EditActions } from './components/EditActions';
|
import { EditActions } from './components/EditActions';
|
||||||
import { MessageActions } from './components/MessageActions';
|
import { MessageActions } from './components/MessageActions';
|
||||||
import { MessageBubble } from './components/MessageBubble';
|
import { MessageBubble } from './components/MessageBubble';
|
||||||
@@ -89,6 +90,7 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
|
|||||||
const messageRef = React.useRef<HTMLDivElement>(null); // 使用 useRef 获取当前组件的引用
|
const messageRef = React.useRef<HTMLDivElement>(null); // 使用 useRef 获取当前组件的引用
|
||||||
const [isEditing, setIsEditing] = React.useState(false); // State to toggle edit mode
|
const [isEditing, setIsEditing] = React.useState(false); // State to toggle edit mode
|
||||||
const [editedMessage, setEditedMessage] = React.useState(content);
|
const [editedMessage, setEditedMessage] = React.useState(content);
|
||||||
|
const mujian = useMujian();
|
||||||
|
|
||||||
const renderedMessage = isUser ? content : swipes[activeSwipeId];
|
const renderedMessage = isUser ? content : swipes[activeSwipeId];
|
||||||
|
|
||||||
@@ -114,6 +116,22 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const handleCopy = async () => {
|
||||||
|
try {
|
||||||
|
await mujian.utils.clipboard.writeText(originalMessage.content);
|
||||||
|
addToast({
|
||||||
|
color: 'success',
|
||||||
|
description: '复制成功',
|
||||||
|
timeout: 1500,
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
|
addToast({
|
||||||
|
description: '复制失败,请重试。',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Separate function to handle message deletion
|
// Separate function to handle message deletion
|
||||||
const handleDeleteMessage = async () => {
|
const handleDeleteMessage = async () => {
|
||||||
if (id) {
|
if (id) {
|
||||||
@@ -157,14 +175,15 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
|
|||||||
<div
|
<div
|
||||||
key={`message-${activeSwipeId}`}
|
key={`message-${activeSwipeId}`}
|
||||||
ref={messageRef} // 将 ref 绑定到根容器
|
ref={messageRef} // 将 ref 绑定到根容器
|
||||||
className={cn(
|
className={mjChatCls(
|
||||||
'grid items-end gap-2 mb-2 w-full grid-cols-[32px_1fr_32px]',
|
['msg-item-wrapper'],
|
||||||
|
cn('grid items-end gap-2 mb-2 w-full grid-cols-[32px_1fr_32px]'),
|
||||||
)}
|
)}
|
||||||
onContextMenu={(event) => {
|
onContextMenu={(event) => {
|
||||||
// 按ctrl点击时打印ID
|
// 按ctrl点击时打印ID
|
||||||
if (event.ctrlKey) {
|
if (event.ctrlKey) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
console.log('Message ID:', id);
|
console.log('Official Chat Message ID:', id);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -174,6 +193,7 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
|
|||||||
isEditing={isEditing}
|
isEditing={isEditing}
|
||||||
editedMessage={editedMessage}
|
editedMessage={editedMessage}
|
||||||
onEditChange={setEditedMessage}
|
onEditChange={setEditedMessage}
|
||||||
|
onCopyClick={handleCopy}
|
||||||
isStreaming={isStreaming}
|
isStreaming={isStreaming}
|
||||||
currentMessage={renderedMessage}
|
currentMessage={renderedMessage}
|
||||||
onEditButtonClick={handleEditButtonClick}
|
onEditButtonClick={handleEditButtonClick}
|
||||||
@@ -206,6 +226,7 @@ export const MessageItem = React.memo((props: MessageItemProps) => {
|
|||||||
onRegenerate={onRegenerate}
|
onRegenerate={onRegenerate}
|
||||||
onContinue={onContinue}
|
onContinue={onContinue}
|
||||||
onEditButtonClick={handleEditButtonClick}
|
onEditButtonClick={handleEditButtonClick}
|
||||||
|
onCopyClick={handleCopy}
|
||||||
onSwipe={handleSwipe}
|
onSwipe={handleSwipe}
|
||||||
onMessageTextClick={handleMessageTextClick}
|
onMessageTextClick={handleMessageTextClick}
|
||||||
onDelete={handleDeleteMessage}
|
onDelete={handleDeleteMessage}
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import type { Message as MessageType } from '@mujian/js-sdk/react';
|
import type { Message as MessageType } from '@mujian/js-sdk/react';
|
||||||
import { Thread, type VListHandle } from '@mujian/js-sdk/react';
|
import { Thread, type VListHandle } from '@mujian/js-sdk/react';
|
||||||
|
import { throttle } from 'lodash-es';
|
||||||
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
import React, { useEffect, useMemo, useRef, useState } from 'react';
|
||||||
import { useGlobalStore } from '@/store/global';
|
import { useGlobalStore } from '@/store/global';
|
||||||
import { MessageItem } from './MessageItem/';
|
import { MessageItem } from './MessageItem/';
|
||||||
@@ -12,6 +13,7 @@ export type MsgProps = {
|
|||||||
onDelete: (messageId: string) => Promise<void>;
|
onDelete: (messageId: string) => Promise<void>;
|
||||||
onEdit: (messageId: string, content: string) => Promise<void>;
|
onEdit: (messageId: string, content: string) => Promise<void>;
|
||||||
onSwipe: (messageId: string, swipeId: number) => Promise<void>;
|
onSwipe: (messageId: string, swipeId: number) => Promise<void>;
|
||||||
|
onNeedMore: () => Promise<void>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OVERSCAN = 10;
|
const OVERSCAN = 10;
|
||||||
@@ -25,11 +27,13 @@ export const MessageList = React.memo(
|
|||||||
onDelete,
|
onDelete,
|
||||||
onEdit,
|
onEdit,
|
||||||
onSwipe,
|
onSwipe,
|
||||||
|
onNeedMore,
|
||||||
}: MsgProps) => {
|
}: MsgProps) => {
|
||||||
const prevLengthRef = useRef(0);
|
const prevLastMesRef = useRef<string | undefined>(undefined);
|
||||||
const { activePersona, projectInfo } = useGlobalStore();
|
const { activePersona, projectInfo } = useGlobalStore();
|
||||||
const vListRef = useRef<VListHandle>(null);
|
const vListRef = useRef<VListHandle>(null);
|
||||||
const [visibleRange, setVisibleRange] = useState<[number, number]>([0, 0]);
|
const [visibleRange, setVisibleRange] = useState<[number, number]>([0, 0]);
|
||||||
|
const [shifting, setShifting] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const vl = vListRef.current;
|
const vl = vListRef.current;
|
||||||
@@ -38,31 +42,48 @@ export const MessageList = React.memo(
|
|||||||
}
|
}
|
||||||
const isScrolledBottom = () =>
|
const isScrolledBottom = () =>
|
||||||
vl.viewportSize + vl.scrollOffset + 16 >= vl.scrollSize;
|
vl.viewportSize + vl.scrollOffset + 16 >= vl.scrollSize;
|
||||||
const isFirstLoading = prevLengthRef.current === 0;
|
|
||||||
|
|
||||||
|
const lastMessage = data[data.length - 1];
|
||||||
const shouldScroll = [
|
const shouldScroll = [
|
||||||
isFirstLoading, // 首次加载
|
lastMessage?.id !== prevLastMesRef.current, // 新消息
|
||||||
data.length > prevLengthRef.current, // 新消息
|
lastMessage?.isStreaming && isScrolledBottom(), // 正在生成
|
||||||
data[data.length - 1]?.isStreaming && isScrolledBottom(), // 正在生成
|
|
||||||
].some(Boolean);
|
].some(Boolean);
|
||||||
|
|
||||||
if (shouldScroll) {
|
if (shouldScroll) {
|
||||||
vListRef.current?.scrollTo(isFirstLoading ? 99999999 : vl.scrollSize);
|
vListRef.current?.scrollTo(
|
||||||
|
prevLastMesRef.current ? vl.scrollSize : 9999999,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
prevLengthRef.current = data.length;
|
prevLastMesRef.current = lastMessage?.id;
|
||||||
}, [data]);
|
}, [data]);
|
||||||
|
|
||||||
const onScroll = () => {
|
const onScroll = throttle(
|
||||||
const vl = vListRef.current;
|
() => {
|
||||||
if (!vl) {
|
const vl = vListRef.current;
|
||||||
return;
|
if (!vl) {
|
||||||
}
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const startIndex = vl.findStartIndex();
|
const startIndex = vl.findStartIndex();
|
||||||
const endIndex = vl.findEndIndex();
|
const endIndex = vl.findEndIndex();
|
||||||
setVisibleRange([startIndex, endIndex]);
|
setVisibleRange([startIndex, endIndex]);
|
||||||
};
|
|
||||||
|
if (startIndex === 0) {
|
||||||
|
// 让VList计算元素相对底部的距离,加载完毕后再切回顶部距离
|
||||||
|
setShifting(true);
|
||||||
|
onNeedMore().finally(() => {
|
||||||
|
setTimeout(() => {
|
||||||
|
setShifting(false);
|
||||||
|
}, 200);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
},
|
||||||
|
300,
|
||||||
|
{
|
||||||
|
leading: false,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
const keepMountedList = useMemo(() => {
|
const keepMountedList = useMemo(() => {
|
||||||
if (visibleRange[0] === 0 && visibleRange[1] === 0) {
|
if (visibleRange[0] === 0 && visibleRange[1] === 0) {
|
||||||
@@ -88,6 +109,7 @@ export const MessageList = React.memo(
|
|||||||
overscan: OVERSCAN,
|
overscan: OVERSCAN,
|
||||||
keepMounted: keepMountedList,
|
keepMounted: keepMountedList,
|
||||||
onScroll,
|
onScroll,
|
||||||
|
shift: shifting,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{(props) => (
|
{(props) => (
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
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';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
onSend: (query: string) => void;
|
onSend: (query: string) => void;
|
||||||
@@ -39,46 +40,74 @@ export const MsgSend = ({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Textarea
|
<Textarea
|
||||||
className="dark"
|
className={mjChatCls(['input-textarea'], 'dark')}
|
||||||
classNames={{
|
classNames={{
|
||||||
label: 'text-black/50 dark:text-white/90',
|
label: mjChatCls(
|
||||||
input: [
|
['input-textarea-label'],
|
||||||
'bg-transparent',
|
'text-black/50 dark:text-white/90',
|
||||||
'text-black/90 dark:text-white/90',
|
),
|
||||||
'placeholder:text-default-700/50 dark:placeholder:text-white/60',
|
input: mjChatCls(
|
||||||
],
|
['input-textarea-input'],
|
||||||
innerWrapper: 'bg-transparent items-center',
|
[
|
||||||
inputWrapper: ['shadow-xl', 'bg-dark/40', 'dark:bg-black/40'],
|
'bg-transparent',
|
||||||
mainWrapper: 'px-2',
|
'text-black/90 dark:text-white/90',
|
||||||
|
'placeholder:text-default-700/50 dark:placeholder:text-white/60',
|
||||||
|
].join(' '),
|
||||||
|
),
|
||||||
|
innerWrapper: mjChatCls(
|
||||||
|
['input-textarea-innerWrapper'],
|
||||||
|
'bg-transparent items-center',
|
||||||
|
),
|
||||||
|
inputWrapper: mjChatCls(
|
||||||
|
['input-textarea-inputWrapper'],
|
||||||
|
'shadow-xl bg-dark/40 dark:bg-black/40',
|
||||||
|
),
|
||||||
|
mainWrapper: mjChatCls(['input-textarea-mainWrapper'], 'px-2'),
|
||||||
}}
|
}}
|
||||||
disabled={running}
|
disabled={running}
|
||||||
endContent={
|
endContent={
|
||||||
running ? (
|
running ? (
|
||||||
<Button
|
<Button
|
||||||
|
className={mjChatCls(
|
||||||
|
['input-textarea-stop-button'],
|
||||||
|
'bg-primary',
|
||||||
|
)}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="primary"
|
|
||||||
radius="full"
|
radius="full"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={() => {
|
onPress={() => {
|
||||||
onStop();
|
onStop();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<CircleStopIcon className="w-5 h-5" />
|
<CircleStopIcon
|
||||||
|
className={mjChatCls(
|
||||||
|
['input-textarea-stop-button-icon'],
|
||||||
|
'w-5 h-5',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
|
className={mjChatCls(
|
||||||
|
['input-textarea-send-button'],
|
||||||
|
'bg-primary',
|
||||||
|
)}
|
||||||
isIconOnly
|
isIconOnly
|
||||||
color="primary"
|
|
||||||
radius="full"
|
radius="full"
|
||||||
size="sm"
|
size="sm"
|
||||||
onPress={handleSend}
|
onPress={handleSend}
|
||||||
isDisabled={isEmptyInput}
|
isDisabled={isEmptyInput}
|
||||||
>
|
>
|
||||||
<SendIcon className="w-5 h-5" />
|
<SendIcon
|
||||||
|
className={mjChatCls(
|
||||||
|
['input-textarea-send-button-icon'],
|
||||||
|
'w-5 h-5',
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</Button>
|
</Button>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
maxLength={1000}
|
maxLength={50000}
|
||||||
maxRows={4}
|
maxRows={4}
|
||||||
minRows={1}
|
minRows={1}
|
||||||
placeholder={running ? '正在生成...' : '按 Enter 发送'}
|
placeholder={running ? '正在生成...' : '按 Enter 发送'}
|
||||||
|
|||||||
115
src/pages/chat/QuickReply.tsx
Normal file
115
src/pages/chat/QuickReply.tsx
Normal file
@@ -0,0 +1,115 @@
|
|||||||
|
import { Chip, ScrollShadow } from '@heroui/react';
|
||||||
|
import type { QuickReply as QuickReplyType } from '@mujian/js-sdk/types';
|
||||||
|
import { useEffect, useRef } from 'react';
|
||||||
|
import { mjChatCls } from '@/utils/cls';
|
||||||
|
|
||||||
|
export interface QuickReplyProps {
|
||||||
|
quickReplies: Array<QuickReplyType>;
|
||||||
|
onSend: (query: string) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuickReply = (props: QuickReplyProps) => {
|
||||||
|
const { quickReplies, onSend } = props;
|
||||||
|
|
||||||
|
const scrollShadowRef = useRef<HTMLDivElement>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const handleWheel = (e: WheelEvent) => {
|
||||||
|
if (Math.abs(e.deltaY) > 0 && scrollShadowRef.current) {
|
||||||
|
e.preventDefault();
|
||||||
|
scrollShadowRef.current.scrollLeft += e.deltaY;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
scrollShadowRef?.current?.addEventListener('wheel', handleWheel, {
|
||||||
|
passive: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
return () =>
|
||||||
|
scrollShadowRef?.current?.removeEventListener('wheel', handleWheel);
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const lastDragTime = useRef(0);
|
||||||
|
|
||||||
|
const onmousedown = (e: React.MouseEvent<HTMLDivElement>) => {
|
||||||
|
// 防止文字选择
|
||||||
|
e.preventDefault();
|
||||||
|
|
||||||
|
// 禁用页面文字选择
|
||||||
|
document.body.style.userSelect = 'none';
|
||||||
|
document.body.style.webkitUserSelect = 'none';
|
||||||
|
|
||||||
|
lastDragTime.current = 0;
|
||||||
|
let dragged = false;
|
||||||
|
// biome-ignore lint/suspicious/noExplicitAny: 抄来的代码
|
||||||
|
const mouseMove: EventListener = (e: any) => {
|
||||||
|
dragged = true;
|
||||||
|
if (scrollShadowRef.current) {
|
||||||
|
scrollShadowRef.current.scrollLeft += e.movementX * -2;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const mouseUp: EventListener = () => {
|
||||||
|
// 恢复文字选择
|
||||||
|
document.body.style.userSelect = '';
|
||||||
|
document.body.style.webkitUserSelect = '';
|
||||||
|
|
||||||
|
document.removeEventListener('mousemove', mouseMove);
|
||||||
|
document.removeEventListener('mouseup', mouseUp);
|
||||||
|
if (dragged) {
|
||||||
|
lastDragTime.current = Date.now();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
document.addEventListener('mousemove', mouseMove);
|
||||||
|
document.addEventListener('mouseup', mouseUp);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<ScrollShadow
|
||||||
|
ref={scrollShadowRef}
|
||||||
|
hideScrollBar
|
||||||
|
className={mjChatCls(
|
||||||
|
['input-quickreply-container'],
|
||||||
|
'flex w-full overflow-auto gap-2',
|
||||||
|
)}
|
||||||
|
orientation="horizontal"
|
||||||
|
style={{
|
||||||
|
userSelect: 'none',
|
||||||
|
WebkitUserSelect: 'none',
|
||||||
|
MozUserSelect: 'none',
|
||||||
|
msUserSelect: 'none',
|
||||||
|
}}
|
||||||
|
onMouseDown={onmousedown}
|
||||||
|
>
|
||||||
|
{quickReplies.map((qr, index) => (
|
||||||
|
<Chip
|
||||||
|
key={index}
|
||||||
|
className={mjChatCls(
|
||||||
|
['input-quickreply-chip', `input-quickreply-chip-${index + 1}`],
|
||||||
|
'bg-white/15 text-default shrink-0 cursor-pointer',
|
||||||
|
)}
|
||||||
|
classNames={{
|
||||||
|
base: mjChatCls([
|
||||||
|
'input-quickreply-chip-base',
|
||||||
|
`input-quickreply-chip-${index + 1}-base`,
|
||||||
|
]),
|
||||||
|
content: mjChatCls([
|
||||||
|
'input-quickreply-chip-content',
|
||||||
|
`input-quickreply-chip-${index + 1}-content`,
|
||||||
|
]),
|
||||||
|
}}
|
||||||
|
radius="full"
|
||||||
|
variant="light"
|
||||||
|
onClick={() => {
|
||||||
|
const isClick = Date.now() - lastDragTime.current > 50;
|
||||||
|
if (isClick && qr.message) {
|
||||||
|
onSend(qr.message);
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{qr.label}
|
||||||
|
</Chip>
|
||||||
|
))}
|
||||||
|
</ScrollShadow>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,10 +1,12 @@
|
|||||||
import { Alert, ScrollShadow, Spinner } from '@heroui/react';
|
import { Alert, ScrollShadow, Spinner, ToastProvider } from '@heroui/react';
|
||||||
import { useChat, useMujian } from '@mujian/js-sdk/react';
|
import { useChat, useMujian } from '@mujian/js-sdk/react';
|
||||||
import type { ProjectInfo } from '@mujian/js-sdk/types';
|
import type { PersonaInfo, ProjectInfo } from '@mujian/js-sdk/types';
|
||||||
import { 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 { MessageList } from './MessageList';
|
import { MessageList } from './MessageList';
|
||||||
import { MsgSend } from './MsgSend';
|
import { MsgSend } from './MsgSend';
|
||||||
|
import { QuickReply } from './QuickReply';
|
||||||
|
|
||||||
// 扩展Window接口以包含chat对象
|
// 扩展Window接口以包含chat对象
|
||||||
declare global {
|
declare global {
|
||||||
@@ -14,6 +16,13 @@ declare global {
|
|||||||
chat: {
|
chat: {
|
||||||
complete?: (message: string) => Promise<void>;
|
complete?: (message: string) => Promise<void>;
|
||||||
project?: ProjectInfo;
|
project?: ProjectInfo;
|
||||||
|
settings: {
|
||||||
|
persona?: PersonaInfo;
|
||||||
|
};
|
||||||
|
/**
|
||||||
|
* @param index 下标从1开始
|
||||||
|
*/
|
||||||
|
setFirstMesIndex?: (index: number) => Promise<void>;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
@@ -25,12 +34,15 @@ window.$mj_engine = {
|
|||||||
chat: {
|
chat: {
|
||||||
complete: undefined,
|
complete: undefined,
|
||||||
project: undefined,
|
project: undefined,
|
||||||
|
settings: {
|
||||||
|
persona: undefined,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Chat = () => {
|
export const Chat = () => {
|
||||||
const { init, projectInfo } = useGlobalStore();
|
const { init, projectInfo, activePersona } = useGlobalStore();
|
||||||
const mujian = useMujian();
|
const mujian = useMujian();
|
||||||
const [inputValue, setInputValue] = useState('');
|
const [inputValue, setInputValue] = useState('');
|
||||||
const {
|
const {
|
||||||
@@ -44,15 +56,18 @@ export const Chat = () => {
|
|||||||
deleteMessage,
|
deleteMessage,
|
||||||
editMessage,
|
editMessage,
|
||||||
setSwipe,
|
setSwipe,
|
||||||
|
loadMoreMessage,
|
||||||
} = useChat({
|
} = useChat({
|
||||||
onError: (e) => {
|
onError: (e) => {
|
||||||
console.error(e);
|
console.error(e);
|
||||||
},
|
},
|
||||||
|
pageSize: 20,
|
||||||
});
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.$mj_engine.ai.chat.project = projectInfo ?? undefined;
|
window.$mj_engine.ai.chat.project = projectInfo ?? undefined;
|
||||||
}, [projectInfo]);
|
window.$mj_engine.ai.chat.settings.persona = activePersona ?? undefined;
|
||||||
|
}, [projectInfo, activePersona]);
|
||||||
|
|
||||||
// 自定义删除消息函数
|
// 自定义删除消息函数
|
||||||
const handleDeleteMessage = async (messageId: string) => {
|
const handleDeleteMessage = async (messageId: string) => {
|
||||||
@@ -92,18 +107,47 @@ export const Chat = () => {
|
|||||||
init(mujian);
|
init(mujian);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const onSend = append;
|
const onSend = useCallback(
|
||||||
|
(query: string) => {
|
||||||
|
let _q = query;
|
||||||
|
|
||||||
|
const marcos = [
|
||||||
|
{
|
||||||
|
variable: 'user',
|
||||||
|
value: activePersona?.name || '',
|
||||||
|
},
|
||||||
|
{ variable: 'char', value: projectInfo?.title || '' },
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const macro of marcos) {
|
||||||
|
_q = _q.replace(new RegExp(`{{${macro.variable}}}`, 'g'), macro.value);
|
||||||
|
}
|
||||||
|
|
||||||
|
return append(_q);
|
||||||
|
},
|
||||||
|
[append, projectInfo, activePersona],
|
||||||
|
);
|
||||||
|
|
||||||
// 将chat对象挂载到window上
|
// 将chat对象挂载到window上
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.$mj_engine.ai.chat.complete = async (message: string) => {
|
window.$mj_engine.ai.chat.complete = async (message) => {
|
||||||
await onSend(message);
|
await onSend(message);
|
||||||
};
|
};
|
||||||
|
window.$mj_engine.ai.chat.setFirstMesIndex = async (oneBasedIndex) => {
|
||||||
return () => {
|
if (messages.length > 1) {
|
||||||
delete window.$mj_engine.ai.chat.complete;
|
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]);
|
}, [onSend, messages, setSwipe]);
|
||||||
|
|
||||||
const handleInputChange = (value: string) => {
|
const handleInputChange = (value: string) => {
|
||||||
setInputValue(value);
|
setInputValue(value);
|
||||||
@@ -127,9 +171,13 @@ export const Chat = () => {
|
|||||||
: '哎呀,发生了未知错误,刷新页面再试试吧~');
|
: '哎呀,发生了未知错误,刷新页面再试试吧~');
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="size-full relative">
|
<div className={mjChatCls(['container'], 'size-full relative')}>
|
||||||
|
<ToastProvider placement="top-center" toastProps={{ color: 'danger' }} />
|
||||||
<div
|
<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)]"
|
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)]',
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
backgroundImage: projectInfo
|
backgroundImage: projectInfo
|
||||||
? `url(${projectInfo.coverImageUrl})`
|
? `url(${projectInfo.coverImageUrl})`
|
||||||
@@ -137,12 +185,19 @@ export const Chat = () => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
className="size-full z-10 p-3 flex flex-col"
|
className={mjChatCls(
|
||||||
|
['container-content'],
|
||||||
|
'size-full z-10 p-3 flex flex-col',
|
||||||
|
)}
|
||||||
style={{
|
style={{
|
||||||
paddingTop,
|
paddingTop,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<ScrollShadow hideScrollBar className="h-full" size={20}>
|
<ScrollShadow
|
||||||
|
hideScrollBar
|
||||||
|
className={mjChatCls(['container-content-scroll-shadow'], 'h-full pb-2')}
|
||||||
|
size={20}
|
||||||
|
>
|
||||||
<MessageList
|
<MessageList
|
||||||
data={messages}
|
data={messages}
|
||||||
sendMessage={async (q) => {
|
sendMessage={async (q) => {
|
||||||
@@ -153,13 +208,17 @@ export const Chat = () => {
|
|||||||
onEdit={editMessage}
|
onEdit={editMessage}
|
||||||
onSwipe={setSwipe}
|
onSwipe={setSwipe}
|
||||||
onRegenerate={regenerate}
|
onRegenerate={regenerate}
|
||||||
|
onNeedMore={loadMoreMessage}
|
||||||
/>
|
/>
|
||||||
{errorMessage && (
|
{errorMessage && (
|
||||||
<Alert
|
<Alert
|
||||||
hideIcon
|
hideIcon
|
||||||
className="mt-2 mb-6"
|
className={mjChatCls(['error-message'], 'mt-2 mb-6')}
|
||||||
classNames={{
|
classNames={{
|
||||||
mainWrapper: 'min-h-4 ml-0',
|
mainWrapper: mjChatCls(
|
||||||
|
['error-message-mainWrapper'],
|
||||||
|
'min-h-4 ml-0',
|
||||||
|
),
|
||||||
}}
|
}}
|
||||||
color="danger"
|
color="danger"
|
||||||
title={errorMessage}
|
title={errorMessage}
|
||||||
@@ -167,7 +226,16 @@ export const Chat = () => {
|
|||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</ScrollShadow>
|
</ScrollShadow>
|
||||||
<div className="flex flex-col gap-2 mt-2">
|
<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
|
<MsgSend
|
||||||
running={status === 'streaming'}
|
running={status === 'streaming'}
|
||||||
onSend={onSend}
|
onSend={onSend}
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ body,
|
|||||||
height: 100vh;
|
height: 100vh;
|
||||||
}
|
}
|
||||||
|
|
||||||
.mes_text> :last-child {
|
.mes_text > :last-child {
|
||||||
margin-bottom: 0;
|
margin-bottom: 0;
|
||||||
}
|
}
|
||||||
|
|||||||
9
src/utils/cls.ts
Normal file
9
src/utils/cls.ts
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
const PREFIX = 'mj-chat';
|
||||||
|
|
||||||
|
export const mjChatCls = (classNames: string[], defaultClassNames?: string) => {
|
||||||
|
return (
|
||||||
|
classNames.map((cls) => `${PREFIX}-${cls}`).join(' ') +
|
||||||
|
' ' +
|
||||||
|
(defaultClassNames || '')
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -10,5 +10,6 @@ export default defineConfig({
|
|||||||
alias: {
|
alias: {
|
||||||
'@': path.resolve(__dirname, './src'),
|
'@': path.resolve(__dirname, './src'),
|
||||||
},
|
},
|
||||||
|
// dedupe: ['react', 'react-dom'],
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user