101 lines
4.1 KiB
TypeScript
101 lines
4.1 KiB
TypeScript
import { useCallback, useContext, useEffect, useMemo, useRef, useState } from "preact/hooks";
|
||
import { MessageTools, type IMessage } from "../../messages";
|
||
import { StateContext } from "../../contexts/state";
|
||
import { DOMTools } from "../../dom";
|
||
|
||
import styles from './message.module.css';
|
||
import { AutoTextarea } from "../autoTextarea";
|
||
|
||
interface IProps {
|
||
message: IMessage;
|
||
index: number;
|
||
isLastUser: boolean;
|
||
isLastAssistant: boolean;
|
||
}
|
||
|
||
export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps) => {
|
||
const { messages, editMessage, deleteMessage, setCurrentSwipe, setMessages } = useContext(StateContext);
|
||
const [editing, setEditing] = useState(false);
|
||
const [savedMessage, setSavedMessage] = useState('');
|
||
const textRef = useRef<HTMLDivElement>(null);
|
||
|
||
const content = useMemo(() => MessageTools.getSwipe(message)?.content, [message]);
|
||
const htmlContent = useMemo(() => MessageTools.format(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 handleStopHere = useCallback(() => {
|
||
if (confirm('Delete all messages after that?')) {
|
||
setMessages(messages.filter((_, i) => i <= index));
|
||
setEditing(false);
|
||
}
|
||
}, [messages, setMessages, 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);
|
||
DOMTools.animate(textRef.current, 'swipe-from-left');
|
||
}, [setCurrentSwipe, index, message]);
|
||
|
||
const handleSwipeRight = useCallback(() => {
|
||
setCurrentSwipe(index, message.currentSwipe + 1);
|
||
DOMTools.animate(textRef.current, 'swipe-from-right');
|
||
}, [setCurrentSwipe, index, message]);
|
||
|
||
return (
|
||
<div class={`${styles.message} ${styles[message.role]} ${isLastUser ? styles.lastUser : ''}`}>
|
||
<div class={styles.content}>
|
||
{editing
|
||
? <AutoTextarea onInput={handleEdit} value={content} />
|
||
: <div class={styles.text} dangerouslySetInnerHTML={{ __html: htmlContent }} ref={textRef} />
|
||
}
|
||
{(isLastUser || message.role === 'assistant') &&
|
||
<div class={styles.buttons}>
|
||
{editing
|
||
? <>
|
||
<button class='icon' onClick={handleToggleEdit}>✔</button>
|
||
<button class='icon' onClick={handleDeleteMessage}>🗑️</button>
|
||
<button class='icon' onClick={handleStopHere} title='Stop here'>⛔</button>
|
||
<button class='icon' onClick={handleCancelEdit}>❌</button>
|
||
</>
|
||
: <>
|
||
{isLastAssistant &&
|
||
<div class={styles.swipes}>
|
||
<div onClick={handleSwipeLeft}>◀</div>
|
||
<div>{message.currentSwipe + 1}/{message.swipes.length}</div>
|
||
<div onClick={handleSwipeRight}>▶</div>
|
||
</div>
|
||
}
|
||
<button class='icon' onClick={handleToggleEdit} title="Edit">🖊</button>
|
||
</>
|
||
}
|
||
</div>
|
||
}
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|