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-02-05 03:26:42 +08:00
|
|
|
|
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>
|
|
|
|
);
|
|
|
|
});
|