109 lines
4.5 KiB
TypeScript
109 lines
4.5 KiB
TypeScript
import { useCallback, useContext, useMemo, useRef, useState } from "preact/hooks";
|
||
import { MessageTools, type IMessage } from "../../tools/messages";
|
||
import { StateContext } from "../../contexts/state";
|
||
import { DOMTools } from "../../tools/dom";
|
||
|
||
import styles from './message.module.css';
|
||
import { AutoTextarea } from "../autoTextarea";
|
||
import { useInputState } from "@common/hooks/useInputState";
|
||
|
||
interface IProps {
|
||
message: IMessage;
|
||
index: number;
|
||
isLastUser: boolean;
|
||
isLastAssistant: boolean;
|
||
}
|
||
|
||
export const Message = ({ message, index, isLastUser, isLastAssistant }: IProps) => {
|
||
const { messages, editMessage, editSummary, deleteMessage, setCurrentSwipe, setMessages, continueMessage } = useContext(StateContext);
|
||
const [editing, setEditing] = useState(false);
|
||
const [editedMessage, setEditedMessage] = useInputState('');
|
||
const textRef = useRef<HTMLDivElement>(null);
|
||
|
||
const swipe = useMemo(() => MessageTools.getSwipe(message), [message]);
|
||
|
||
const content = swipe?.content;
|
||
const summary = swipe?.summary;
|
||
const cost = swipe?.cost ?? 0;
|
||
const htmlContent = useMemo(() => MessageTools.format(content ?? ''), [content]);
|
||
|
||
const handleEnableEdit = useCallback(() => {
|
||
setEditing(true);
|
||
setEditedMessage(content ?? '');
|
||
}, [content]);
|
||
|
||
const handleSaveEdit = useCallback(() => {
|
||
editMessage(index, editedMessage.trim(), cost);
|
||
editSummary(index, '', 0);
|
||
setEditing(false);
|
||
}, [editMessage, editSummary, index, editedMessage, cost]);
|
||
|
||
const handleCancelEdit = useCallback(() => {
|
||
setEditing(false);
|
||
}, [editMessage, index]);
|
||
|
||
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 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]);
|
||
|
||
const handleContinueMessage = useCallback(() => {
|
||
continueMessage(true);
|
||
}, [continueMessage]);
|
||
|
||
return (
|
||
<div class={`${styles.message} ${styles[message.role]} ${isLastUser ? styles.lastUser : ''}`}>
|
||
<div class={styles.content}>
|
||
{editing
|
||
? <AutoTextarea onInput={setEditedMessage} value={editedMessage} />
|
||
: <>
|
||
<div class={styles.text} dangerouslySetInnerHTML={{ __html: htmlContent }} ref={textRef} />
|
||
{summary && <small class={styles.summary}>{summary}</small>}
|
||
{cost > 0 && <small class={styles.summary}>💲 {cost}</small>}
|
||
</>
|
||
}
|
||
<div class={styles.buttons}>
|
||
{editing
|
||
? <>
|
||
<button class='icon' onClick={handleSaveEdit} title='Save'>✔</button>
|
||
<button class='icon' onClick={handleDeleteMessage} title='Delete'>🗑️</button>
|
||
<button class='icon' onClick={handleStopHere} title='Stop here'>⛔</button>
|
||
<button class='icon' onClick={handleCancelEdit} title='Cancel'>❌</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={handleContinueMessage} title="Continue">▶</button>
|
||
</>}
|
||
<button class='icon' onClick={handleEnableEdit} title="Edit">🖊</button>
|
||
</>
|
||
}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
);
|
||
};
|