(-1);
+ const messages: MessageDisplay[] = useMemo(() => {
+ if (!viewingChat) return [];
+ else return getListMessageDisplay(viewingChat.messages, currNodeId);
+ }, [currNodeId, viewingChat]);
+
+ const currConvId = viewingChat?.conv.id ?? null;
+ const pendingMsg: PendingMessage | undefined =
+ pendingMessages[currConvId ?? ''];
+
+ useEffect(() => {
+ // reset to latest node when conversation changes
+ setCurrNodeId(-1);
+ // scroll to bottom when conversation changes
+ scrollToBottom(false, 1);
+ }, [currConvId]);
+
+ const onChunk: CallbackGeneratedChunk = (currLeafNodeId?: Message['id']) => {
+ if (currLeafNodeId) {
+ setCurrNodeId(currLeafNodeId);
+ }
+ scrollToBottom(true);
};
- // scroll to bottom when conversation changes
- useEffect(() => {
- scrollToBottom(false);
- }, [viewingConversation?.id]);
-
const sendNewMessage = async () => {
- if (inputMsg.trim().length === 0 || isGenerating(currConvId)) return;
- const convId = viewingConversation?.id ?? StorageUtils.getNewConvId();
+ if (inputMsg.trim().length === 0 || isGenerating(currConvId ?? '')) return;
const lastInpMsg = inputMsg;
setInputMsg('');
- if (!viewingConversation) {
- // if user is creating a new conversation, redirect to the new conversation
- navigate(`/chat/${convId}`);
- }
scrollToBottom(false);
- // auto scroll as message is being generated
- const onChunk = () => scrollToBottom(true);
- if (!(await sendMessage(convId, inputMsg, onChunk))) {
+ setCurrNodeId(-1);
+ // get the last message node
+ const lastMsgNodeId = messages.at(-1)?.msg.id ?? null;
+ if (!(await sendMessage(currConvId, lastMsgNodeId, inputMsg, onChunk))) {
// restore the input message if failed
setInputMsg(lastInpMsg);
}
};
+ const handleEditMessage = async (msg: Message, content: string) => {
+ if (!viewingChat) return;
+ setCurrNodeId(msg.id);
+ scrollToBottom(false);
+ await replaceMessageAndGenerate(
+ viewingChat.conv.id,
+ msg.parent,
+ content,
+ onChunk
+ );
+ setCurrNodeId(-1);
+ scrollToBottom(false);
+ };
+
+ const handleRegenerateMessage = async (msg: Message) => {
+ if (!viewingChat) return;
+ setCurrNodeId(msg.parent);
+ scrollToBottom(false);
+ await replaceMessageAndGenerate(
+ viewingChat.conv.id,
+ msg.parent,
+ null,
+ onChunk
+ );
+ setCurrNodeId(-1);
+ scrollToBottom(false);
+ };
+
const hasCanvas = !!canvasData;
+ // due to some timing issues of StorageUtils.appendMsg(), we need to make sure the pendingMsg is not duplicated upon rendering (i.e. appears once in the saved conversation and once in the pendingMsg)
+ const pendingMsgDisplay: MessageDisplay[] =
+ pendingMsg && messages.at(-1)?.msg.id !== pendingMsg.id
+ ? [
+ {
+ msg: pendingMsg,
+ siblingLeafNodeIds: [],
+ siblingCurrIdx: 0,
+ isPending: true,
+ },
+ ]
+ : [];
+
return (
{/* placeholder to shift the message to the bottom */}
- {viewingConversation ? '' : 'Send a message to start'}
+ {viewingChat ? '' : 'Send a message to start'}
- {viewingConversation?.messages.map((msg) => (
+ {[...messages, ...pendingMsgDisplay].map((msg) => (
))}
-
- {pendingMsg && (
-
- )}
{/* chat input */}
@@ -118,10 +215,10 @@ export default function ChatScreen() {
id="msg-input"
dir="auto"
>
- {isGenerating(currConvId) ? (
+ {isGenerating(currConvId ?? '') ? (
diff --git a/examples/server/webui/src/components/Header.tsx b/examples/server/webui/src/components/Header.tsx
index 505350313..cbee394ba 100644
--- a/examples/server/webui/src/components/Header.tsx
+++ b/examples/server/webui/src/components/Header.tsx
@@ -25,12 +25,12 @@ export default function Header() {
);
}, [selectedTheme]);
- const { isGenerating, viewingConversation } = useAppContext();
- const isCurrConvGenerating = isGenerating(viewingConversation?.id ?? '');
+ const { isGenerating, viewingChat } = useAppContext();
+ const isCurrConvGenerating = isGenerating(viewingChat?.conv.id ?? '');
const removeConversation = () => {
- if (isCurrConvGenerating || !viewingConversation) return;
- const convId = viewingConversation.id;
+ if (isCurrConvGenerating || !viewingChat) return;
+ const convId = viewingChat?.conv.id;
if (window.confirm('Are you sure to delete this conversation?')) {
StorageUtils.remove(convId);
navigate('/');
@@ -38,9 +38,9 @@ export default function Header() {
};
const downloadConversation = () => {
- if (isCurrConvGenerating || !viewingConversation) return;
- const convId = viewingConversation.id;
- const conversationJson = JSON.stringify(viewingConversation, null, 2);
+ if (isCurrConvGenerating || !viewingChat) return;
+ const convId = viewingChat?.conv.id;
+ const conversationJson = JSON.stringify(viewingChat, null, 2);
const blob = new Blob([conversationJson], { type: 'application/json' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
@@ -75,38 +75,41 @@ export default function Header() {
{/* action buttons (top right) */}
-
- {/* "..." button */}
-
- {/* dropdown menu */}
-
-
+
+
+ {/* dropdown menu */}
+
+
+ )}
+
))}
- Conversations are saved to browser's localStorage
+ Conversations are saved to browser's IndexedDB