104 lines
4.2 KiB
TypeScript
104 lines
4.2 KiB
TypeScript
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||
import { formatMessage, type IMessage } from "../messages";
|
||
import { GlobalContext } from "../context";
|
||
|
||
interface IProps {
|
||
message: IMessage;
|
||
index: number;
|
||
isLastUser: boolean;
|
||
isLastAssistant: boolean;
|
||
}
|
||
|
||
export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps) => {
|
||
const { editMessage, deleteMessage, setCurrentSwipe } = useContext(GlobalContext);
|
||
const [editing, setEditing] = useState(false);
|
||
const [savedMessage, setSavedMessage] = useState('');
|
||
const textareaRef = useRef<HTMLTextAreaElement>(null);
|
||
const textRef = useRef<HTMLDivElement>(null);
|
||
|
||
const swipe = useMemo(() => message.swipes[message.currentSwipe], [message.swipes, message.currentSwipe]);
|
||
const content = useMemo(() => swipe?.displayContent ?? swipe?.content, [swipe]);
|
||
const htmlContent = useMemo(() => formatMessage(content ?? ''), [content]);
|
||
|
||
const handleToggleEdit = useCallback(() => {
|
||
setEditing(!editing);
|
||
if (!editing) {
|
||
setSavedMessage(content);
|
||
}
|
||
}, [editing, content]);
|
||
|
||
const handleCancelEdit = useCallback(() => {
|
||
setEditing(false);
|
||
editMessage(index, savedMessage);
|
||
}, [editMessage, index, savedMessage]);
|
||
|
||
const handleDeleteMessage = useCallback(() => {
|
||
if (confirm('Delete message?')) {
|
||
setEditing(false);
|
||
deleteMessage(index);
|
||
}
|
||
}, [deleteMessage, index]);
|
||
|
||
const handleEdit = useCallback((e: InputEvent) => {
|
||
if (e.target instanceof HTMLTextAreaElement) {
|
||
const newContent = e.target.value;
|
||
editMessage(index, newContent);
|
||
}
|
||
}, [editMessage, index]);
|
||
|
||
const handleSwipeLeft = useCallback(() => {
|
||
setCurrentSwipe(index, message.currentSwipe - 1);
|
||
if (textRef.current) {
|
||
textRef.current.style.animationName = '';
|
||
textRef.current.style.animationName = 'swipe-from-left';
|
||
}
|
||
}, [setCurrentSwipe, index, message]);
|
||
|
||
const handleSwipeRight = useCallback(() => {
|
||
setCurrentSwipe(index, message.currentSwipe + 1);
|
||
if (textRef.current) {
|
||
textRef.current.style.animationName = '';
|
||
textRef.current.style.animationName = 'swipe-from-right';
|
||
}
|
||
}, [setCurrentSwipe, index, message]);
|
||
|
||
useEffect(() => {
|
||
if (textareaRef.current) {
|
||
const area = textareaRef.current;
|
||
area.style.height = '0'; // reset
|
||
area.style.height = `${area.scrollHeight + 10}px`;
|
||
}
|
||
}, [content, editing]);
|
||
|
||
return (
|
||
<div class={`message role-${message.role} ${isLastUser ? 'last-user' : ''}`}>
|
||
<div class="content">
|
||
{editing
|
||
? <textarea onInput={handleEdit} value={content} class="edit-input" ref={textareaRef} />
|
||
: <div class="text" dangerouslySetInnerHTML={{ __html: htmlContent }} ref={textRef}/>
|
||
}
|
||
{(isLastUser || message.role === 'assistant') &&
|
||
<div class="buttons">
|
||
{editing
|
||
? <>
|
||
<button class="icon" onClick={handleToggleEdit}>✔</button>
|
||
<button class="icon" onClick={handleDeleteMessage}>🗑️</button>
|
||
<button class="icon" onClick={handleCancelEdit}>❌</button>
|
||
</>
|
||
: <>
|
||
{isLastAssistant &&
|
||
<div class="swipes">
|
||
<div onClick={handleSwipeLeft}>◀</div>
|
||
<div>{message.currentSwipe + 1}/{message.swipes.length}</div>
|
||
<div onClick={handleSwipeRight}>▶</div>
|
||
</div>
|
||
}
|
||
<button class="icon" onClick={handleToggleEdit}>🖊</button>
|
||
</>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
);
|
||
}; |