buster/web/src/layouts/ChatLayout/ChatContainer/ChatContent/ChatUserMessage.tsx

173 lines
5.2 KiB
TypeScript
Raw Normal View History

2025-03-30 11:13:40 +08:00
'use client';
2025-02-01 02:04:49 +08:00
import type { BusterChatMessageRequest } from '@/api/asset_interfaces';
2025-04-21 22:35:19 +08:00
import React, { useState, useRef, useEffect } from 'react';
2025-03-11 02:49:45 +08:00
import { Paragraph } from '@/components/ui/typography';
2025-01-28 08:08:52 +08:00
import { MessageContainer } from './MessageContainer';
2025-03-30 11:13:40 +08:00
import { Tooltip } from '@/components/ui/tooltip';
import { cn } from '@/lib/classMerge';
2025-04-18 13:21:39 +08:00
import { PenWriting, Copy } from '@/components/ui/icons';
2025-03-30 11:13:40 +08:00
import { Button } from '@/components/ui/buttons';
import { useBusterNotifications } from '@/context/BusterNotifications';
2025-04-21 22:35:19 +08:00
import { useMemoizedFn, useMount } from '@/hooks';
2025-03-30 11:13:40 +08:00
import { InputTextArea } from '@/components/ui/inputs/InputTextArea';
import { useBusterNewChatContextSelector } from '@/context/Chats';
export const ChatUserMessage: React.FC<{
messageId: string;
chatId: string;
isCompletedStream: boolean;
requestMessage: NonNullable<BusterChatMessageRequest>;
}> = React.memo(({ messageId, chatId, isCompletedStream, requestMessage }) => {
2025-04-18 13:21:39 +08:00
const { openSuccessMessage } = useBusterNotifications();
2025-03-30 11:13:40 +08:00
const [isTooltipOpen, setIsTooltipOpen] = useState(false);
const [isEditing, setIsEditing] = useState(false);
2025-01-28 08:08:52 +08:00
2025-03-30 11:13:40 +08:00
const { sender_avatar, sender_id, sender_name, request } = requestMessage;
2025-03-30 11:13:40 +08:00
const onSetIsEditing = useMemoizedFn((isEditing: boolean) => {
setIsEditing(isEditing);
setIsTooltipOpen(false);
});
2025-01-28 08:08:52 +08:00
2025-04-18 13:21:39 +08:00
const handleCopy = useMemoizedFn((e?: React.ClipboardEvent) => {
// Prevent default copy behavior
//I do not know why this is needed, but it is...
2025-04-22 02:43:34 +08:00
if (e && e.clipboardData) {
2025-04-18 13:21:39 +08:00
e.preventDefault();
e.clipboardData.setData('text/plain', request);
} else {
navigator.clipboard.writeText(request);
}
openSuccessMessage('Copied to clipboard');
});
2025-03-30 11:13:40 +08:00
return (
<MessageContainer
senderName={sender_name}
senderId={sender_id}
senderAvatar={sender_avatar}
onMouseEnter={() => setIsTooltipOpen(true)}
onMouseLeave={() => setIsTooltipOpen(false)}>
{isEditing ? (
<EditMessage
messageId={messageId}
chatId={chatId}
requestMessage={requestMessage}
onSetIsEditing={onSetIsEditing}
/>
) : (
<>
2025-04-18 13:21:39 +08:00
<div>
2025-04-19 07:00:31 +08:00
<Paragraph className="break-words whitespace-pre-wrap" onCopy={handleCopy}>
2025-04-18 13:21:39 +08:00
{request}
</Paragraph>
</div>
2025-03-30 11:13:40 +08:00
{isCompletedStream && (
<RequestMessageTooltip
isTooltipOpen={isTooltipOpen}
requestMessage={requestMessage}
setIsEditing={setIsEditing}
2025-04-18 13:21:39 +08:00
onCopy={handleCopy}
2025-03-30 11:13:40 +08:00
/>
)}
</>
)}
</MessageContainer>
);
});
2025-01-28 08:08:52 +08:00
ChatUserMessage.displayName = 'ChatUserMessage';
2025-03-30 11:13:40 +08:00
const RequestMessageTooltip: React.FC<{
isTooltipOpen: boolean;
requestMessage: NonNullable<BusterChatMessageRequest>;
setIsEditing: (isEditing: boolean) => void;
2025-04-18 13:21:39 +08:00
onCopy: () => void;
}> = React.memo(({ isTooltipOpen, requestMessage, setIsEditing, onCopy }) => {
2025-03-30 11:13:40 +08:00
const { openSuccessMessage } = useBusterNotifications();
const onEdit = useMemoizedFn(() => {
setIsEditing(true);
});
return (
<div
className={cn(
2025-04-18 13:21:39 +08:00
'absolute top-0 right-1 -translate-y-1 transform',
2025-03-30 11:13:40 +08:00
'bg-background z-50 rounded border shadow',
'transition-all duration-200',
isTooltipOpen ? 'scale-100 opacity-100' : 'scale-95 opacity-0'
)}>
<Tooltip title={'Edit'} side={'bottom'}>
<Button
prefix={<PenWriting />}
className="hover:bg-item-select!"
variant={'ghost'}
onClick={onEdit}
/>
</Tooltip>
<Tooltip title={'Copy'} side={'bottom'}>
<Button
prefix={<Copy />}
className="hover:bg-item-select!"
variant={'ghost'}
onClick={onCopy}
/>
</Tooltip>
</div>
);
});
RequestMessageTooltip.displayName = 'RequestMessageTooltip';
const EditMessage: React.FC<{
requestMessage: NonNullable<BusterChatMessageRequest>;
onSetIsEditing: (isEditing: boolean) => void;
messageId: string;
chatId: string;
}> = React.memo(({ requestMessage, onSetIsEditing, messageId, chatId }) => {
const [prompt, setPrompt] = useState(requestMessage.request);
2025-04-21 22:35:19 +08:00
const textAreaRef = useRef<HTMLTextAreaElement>(null);
2025-03-30 11:13:40 +08:00
const onReplaceMessageInChat = useBusterNewChatContextSelector((x) => x.onReplaceMessageInChat);
2025-04-21 22:35:19 +08:00
const onSave = useMemoizedFn(() => {
2025-03-30 11:13:40 +08:00
onReplaceMessageInChat({
chatId,
messageId,
prompt
});
onSetIsEditing(false);
});
2025-04-21 22:35:19 +08:00
useMount(() => {
// Using requestAnimationFrame to ensure the DOM is ready
requestAnimationFrame(() => {
if (textAreaRef.current) {
textAreaRef.current.focus();
textAreaRef.current.select();
}
});
});
2025-03-30 11:13:40 +08:00
return (
<div className="-mt-1 flex flex-col space-y-2">
<InputTextArea
2025-04-21 22:35:19 +08:00
ref={textAreaRef}
2025-03-30 11:13:40 +08:00
autoResize={{ minRows: 3, maxRows: 10 }}
value={prompt}
2025-04-21 22:35:19 +08:00
onPressEnter={onSave}
2025-03-30 11:13:40 +08:00
onChange={(e) => setPrompt(e.target.value)}
/>
<div className="flex justify-end space-x-2">
<Button variant={'ghost'} onClick={() => onSetIsEditing(false)}>
Cancel
</Button>
2025-04-21 22:35:19 +08:00
<Button variant={'black'} onClick={onSave}>
2025-03-30 11:13:40 +08:00
Submit
</Button>
</div>
</div>
);
});