|
@@ -5,6 +5,7 @@ import {
|
|
|
Conversation,
|
|
Conversation,
|
|
|
Message,
|
|
Message,
|
|
|
PendingMessage,
|
|
PendingMessage,
|
|
|
|
|
+ ViewingChat,
|
|
|
} from './types';
|
|
} from './types';
|
|
|
import StorageUtils from './storage';
|
|
import StorageUtils from './storage';
|
|
|
import {
|
|
import {
|
|
@@ -13,24 +14,25 @@ import {
|
|
|
getSSEStreamAsync,
|
|
getSSEStreamAsync,
|
|
|
} from './misc';
|
|
} from './misc';
|
|
|
import { BASE_URL, CONFIG_DEFAULT, isDev } from '../Config';
|
|
import { BASE_URL, CONFIG_DEFAULT, isDev } from '../Config';
|
|
|
-import { matchPath, useLocation } from 'react-router';
|
|
|
|
|
|
|
+import { matchPath, useLocation, useNavigate } from 'react-router';
|
|
|
|
|
|
|
|
interface AppContextValue {
|
|
interface AppContextValue {
|
|
|
// conversations and messages
|
|
// conversations and messages
|
|
|
- viewingConversation: Conversation | null;
|
|
|
|
|
|
|
+ viewingChat: ViewingChat | null;
|
|
|
pendingMessages: Record<Conversation['id'], PendingMessage>;
|
|
pendingMessages: Record<Conversation['id'], PendingMessage>;
|
|
|
isGenerating: (convId: string) => boolean;
|
|
isGenerating: (convId: string) => boolean;
|
|
|
sendMessage: (
|
|
sendMessage: (
|
|
|
- convId: string,
|
|
|
|
|
|
|
+ convId: string | null,
|
|
|
|
|
+ leafNodeId: Message['id'] | null,
|
|
|
content: string,
|
|
content: string,
|
|
|
- onChunk?: CallbackGeneratedChunk
|
|
|
|
|
|
|
+ onChunk: CallbackGeneratedChunk
|
|
|
) => Promise<boolean>;
|
|
) => Promise<boolean>;
|
|
|
stopGenerating: (convId: string) => void;
|
|
stopGenerating: (convId: string) => void;
|
|
|
replaceMessageAndGenerate: (
|
|
replaceMessageAndGenerate: (
|
|
|
convId: string,
|
|
convId: string,
|
|
|
- origMsgId: Message['id'],
|
|
|
|
|
- content?: string,
|
|
|
|
|
- onChunk?: CallbackGeneratedChunk
|
|
|
|
|
|
|
+ parentNodeId: Message['id'], // the parent node of the message to be replaced
|
|
|
|
|
+ content: string | null,
|
|
|
|
|
+ onChunk: CallbackGeneratedChunk
|
|
|
) => Promise<void>;
|
|
) => Promise<void>;
|
|
|
|
|
|
|
|
// canvas
|
|
// canvas
|
|
@@ -44,23 +46,33 @@ interface AppContextValue {
|
|
|
setShowSettings: (show: boolean) => void;
|
|
setShowSettings: (show: boolean) => void;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
-// for now, this callback is only used for scrolling to the bottom of the chat
|
|
|
|
|
-type CallbackGeneratedChunk = () => void;
|
|
|
|
|
|
|
+// this callback is used for scrolling to the bottom of the chat and switching to the last node
|
|
|
|
|
+export type CallbackGeneratedChunk = (currLeafNodeId?: Message['id']) => void;
|
|
|
|
|
|
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
|
const AppContext = createContext<AppContextValue>({} as any);
|
|
const AppContext = createContext<AppContextValue>({} as any);
|
|
|
|
|
|
|
|
|
|
+const getViewingChat = async (convId: string): Promise<ViewingChat | null> => {
|
|
|
|
|
+ const conv = await StorageUtils.getOneConversation(convId);
|
|
|
|
|
+ if (!conv) return null;
|
|
|
|
|
+ return {
|
|
|
|
|
+ conv: conv,
|
|
|
|
|
+ // all messages from all branches, not filtered by last node
|
|
|
|
|
+ messages: await StorageUtils.getMessages(convId),
|
|
|
|
|
+ };
|
|
|
|
|
+};
|
|
|
|
|
+
|
|
|
export const AppContextProvider = ({
|
|
export const AppContextProvider = ({
|
|
|
children,
|
|
children,
|
|
|
}: {
|
|
}: {
|
|
|
children: React.ReactElement;
|
|
children: React.ReactElement;
|
|
|
}) => {
|
|
}) => {
|
|
|
const { pathname } = useLocation();
|
|
const { pathname } = useLocation();
|
|
|
|
|
+ const navigate = useNavigate();
|
|
|
const params = matchPath('/chat/:convId', pathname);
|
|
const params = matchPath('/chat/:convId', pathname);
|
|
|
const convId = params?.params?.convId;
|
|
const convId = params?.params?.convId;
|
|
|
|
|
|
|
|
- const [viewingConversation, setViewingConversation] =
|
|
|
|
|
- useState<Conversation | null>(null);
|
|
|
|
|
|
|
+ const [viewingChat, setViewingChat] = useState<ViewingChat | null>(null);
|
|
|
const [pendingMessages, setPendingMessages] = useState<
|
|
const [pendingMessages, setPendingMessages] = useState<
|
|
|
Record<Conversation['id'], PendingMessage>
|
|
Record<Conversation['id'], PendingMessage>
|
|
|
>({});
|
|
>({});
|
|
@@ -75,12 +87,12 @@ export const AppContextProvider = ({
|
|
|
useEffect(() => {
|
|
useEffect(() => {
|
|
|
// also reset the canvas data
|
|
// also reset the canvas data
|
|
|
setCanvasData(null);
|
|
setCanvasData(null);
|
|
|
- const handleConversationChange = (changedConvId: string) => {
|
|
|
|
|
|
|
+ const handleConversationChange = async (changedConvId: string) => {
|
|
|
if (changedConvId !== convId) return;
|
|
if (changedConvId !== convId) return;
|
|
|
- setViewingConversation(StorageUtils.getOneConversation(convId));
|
|
|
|
|
|
|
+ setViewingChat(await getViewingChat(changedConvId));
|
|
|
};
|
|
};
|
|
|
StorageUtils.onConversationChanged(handleConversationChange);
|
|
StorageUtils.onConversationChanged(handleConversationChange);
|
|
|
- setViewingConversation(StorageUtils.getOneConversation(convId ?? ''));
|
|
|
|
|
|
|
+ getViewingChat(convId ?? '').then(setViewingChat);
|
|
|
return () => {
|
|
return () => {
|
|
|
StorageUtils.offConversationChanged(handleConversationChange);
|
|
StorageUtils.offConversationChanged(handleConversationChange);
|
|
|
};
|
|
};
|
|
@@ -118,23 +130,39 @@ export const AppContextProvider = ({
|
|
|
|
|
|
|
|
const generateMessage = async (
|
|
const generateMessage = async (
|
|
|
convId: string,
|
|
convId: string,
|
|
|
- onChunk?: CallbackGeneratedChunk
|
|
|
|
|
|
|
+ leafNodeId: Message['id'],
|
|
|
|
|
+ onChunk: CallbackGeneratedChunk
|
|
|
) => {
|
|
) => {
|
|
|
if (isGenerating(convId)) return;
|
|
if (isGenerating(convId)) return;
|
|
|
|
|
|
|
|
const config = StorageUtils.getConfig();
|
|
const config = StorageUtils.getConfig();
|
|
|
- const currConversation = StorageUtils.getOneConversation(convId);
|
|
|
|
|
|
|
+ const currConversation = await StorageUtils.getOneConversation(convId);
|
|
|
if (!currConversation) {
|
|
if (!currConversation) {
|
|
|
throw new Error('Current conversation is not found');
|
|
throw new Error('Current conversation is not found');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ const currMessages = StorageUtils.filterByLeafNodeId(
|
|
|
|
|
+ await StorageUtils.getMessages(convId),
|
|
|
|
|
+ leafNodeId,
|
|
|
|
|
+ false
|
|
|
|
|
+ );
|
|
|
const abortController = new AbortController();
|
|
const abortController = new AbortController();
|
|
|
setAbort(convId, abortController);
|
|
setAbort(convId, abortController);
|
|
|
|
|
|
|
|
|
|
+ if (!currMessages) {
|
|
|
|
|
+ throw new Error('Current messages are not found');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const pendingId = Date.now() + 1;
|
|
|
let pendingMsg: PendingMessage = {
|
|
let pendingMsg: PendingMessage = {
|
|
|
- id: Date.now() + 1,
|
|
|
|
|
|
|
+ id: pendingId,
|
|
|
|
|
+ convId,
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ timestamp: pendingId,
|
|
|
role: 'assistant',
|
|
role: 'assistant',
|
|
|
content: null,
|
|
content: null,
|
|
|
|
|
+ parent: leafNodeId,
|
|
|
|
|
+ children: [],
|
|
|
};
|
|
};
|
|
|
setPending(convId, pendingMsg);
|
|
setPending(convId, pendingMsg);
|
|
|
|
|
|
|
@@ -144,7 +172,7 @@ export const AppContextProvider = ({
|
|
|
...(config.systemMessage.length === 0
|
|
...(config.systemMessage.length === 0
|
|
|
? []
|
|
? []
|
|
|
: [{ role: 'system', content: config.systemMessage } as APIMessage]),
|
|
: [{ role: 'system', content: config.systemMessage } as APIMessage]),
|
|
|
- ...normalizeMsgsForAPI(currConversation?.messages ?? []),
|
|
|
|
|
|
|
+ ...normalizeMsgsForAPI(currMessages),
|
|
|
];
|
|
];
|
|
|
if (config.excludeThoughtOnReq) {
|
|
if (config.excludeThoughtOnReq) {
|
|
|
messages = filterThoughtFromMsgs(messages);
|
|
messages = filterThoughtFromMsgs(messages);
|
|
@@ -205,8 +233,7 @@ export const AppContextProvider = ({
|
|
|
const lastContent = pendingMsg.content || '';
|
|
const lastContent = pendingMsg.content || '';
|
|
|
if (addedContent) {
|
|
if (addedContent) {
|
|
|
pendingMsg = {
|
|
pendingMsg = {
|
|
|
- id: pendingMsg.id,
|
|
|
|
|
- role: 'assistant',
|
|
|
|
|
|
|
+ ...pendingMsg,
|
|
|
content: lastContent + addedContent,
|
|
content: lastContent + addedContent,
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
@@ -221,7 +248,7 @@ export const AppContextProvider = ({
|
|
|
};
|
|
};
|
|
|
}
|
|
}
|
|
|
setPending(convId, pendingMsg);
|
|
setPending(convId, pendingMsg);
|
|
|
- onChunk?.();
|
|
|
|
|
|
|
+ onChunk(); // don't need to switch node for pending message
|
|
|
}
|
|
}
|
|
|
} catch (err) {
|
|
} catch (err) {
|
|
|
setPending(convId, null);
|
|
setPending(convId, null);
|
|
@@ -236,37 +263,53 @@ export const AppContextProvider = ({
|
|
|
}
|
|
}
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- if (pendingMsg.content) {
|
|
|
|
|
- StorageUtils.appendMsg(currConversation.id, {
|
|
|
|
|
- id: pendingMsg.id,
|
|
|
|
|
- content: pendingMsg.content,
|
|
|
|
|
- role: pendingMsg.role,
|
|
|
|
|
- timings: pendingMsg.timings,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (pendingMsg.content !== null) {
|
|
|
|
|
+ await StorageUtils.appendMsg(pendingMsg as Message, leafNodeId);
|
|
|
}
|
|
}
|
|
|
setPending(convId, null);
|
|
setPending(convId, null);
|
|
|
- onChunk?.(); // trigger scroll to bottom
|
|
|
|
|
|
|
+ onChunk(pendingId); // trigger scroll to bottom and switch to the last node
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const sendMessage = async (
|
|
const sendMessage = async (
|
|
|
- convId: string,
|
|
|
|
|
|
|
+ convId: string | null,
|
|
|
|
|
+ leafNodeId: Message['id'] | null,
|
|
|
content: string,
|
|
content: string,
|
|
|
- onChunk?: CallbackGeneratedChunk
|
|
|
|
|
|
|
+ onChunk: CallbackGeneratedChunk
|
|
|
): Promise<boolean> => {
|
|
): Promise<boolean> => {
|
|
|
- if (isGenerating(convId) || content.trim().length === 0) return false;
|
|
|
|
|
|
|
+ if (isGenerating(convId ?? '') || content.trim().length === 0) return false;
|
|
|
|
|
|
|
|
- StorageUtils.appendMsg(convId, {
|
|
|
|
|
- id: Date.now(),
|
|
|
|
|
- role: 'user',
|
|
|
|
|
- content,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (convId === null || convId.length === 0 || leafNodeId === null) {
|
|
|
|
|
+ const conv = await StorageUtils.createConversation(
|
|
|
|
|
+ content.substring(0, 256)
|
|
|
|
|
+ );
|
|
|
|
|
+ convId = conv.id;
|
|
|
|
|
+ leafNodeId = conv.currNode;
|
|
|
|
|
+ // if user is creating a new conversation, redirect to the new conversation
|
|
|
|
|
+ navigate(`/chat/${convId}`);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ const currMsgId = now;
|
|
|
|
|
+ StorageUtils.appendMsg(
|
|
|
|
|
+ {
|
|
|
|
|
+ id: currMsgId,
|
|
|
|
|
+ timestamp: now,
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ convId,
|
|
|
|
|
+ role: 'user',
|
|
|
|
|
+ content,
|
|
|
|
|
+ parent: leafNodeId,
|
|
|
|
|
+ children: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ leafNodeId
|
|
|
|
|
+ );
|
|
|
|
|
+ onChunk(currMsgId);
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
- await generateMessage(convId, onChunk);
|
|
|
|
|
|
|
+ await generateMessage(convId, currMsgId, onChunk);
|
|
|
return true;
|
|
return true;
|
|
|
} catch (_) {
|
|
} catch (_) {
|
|
|
- // rollback
|
|
|
|
|
- StorageUtils.popMsg(convId);
|
|
|
|
|
|
|
+ // TODO: rollback
|
|
|
}
|
|
}
|
|
|
return false;
|
|
return false;
|
|
|
};
|
|
};
|
|
@@ -279,22 +322,33 @@ export const AppContextProvider = ({
|
|
|
// if content is undefined, we remove last assistant message
|
|
// if content is undefined, we remove last assistant message
|
|
|
const replaceMessageAndGenerate = async (
|
|
const replaceMessageAndGenerate = async (
|
|
|
convId: string,
|
|
convId: string,
|
|
|
- origMsgId: Message['id'],
|
|
|
|
|
- content?: string,
|
|
|
|
|
- onChunk?: CallbackGeneratedChunk
|
|
|
|
|
|
|
+ parentNodeId: Message['id'], // the parent node of the message to be replaced
|
|
|
|
|
+ content: string | null,
|
|
|
|
|
+ onChunk: CallbackGeneratedChunk
|
|
|
) => {
|
|
) => {
|
|
|
if (isGenerating(convId)) return;
|
|
if (isGenerating(convId)) return;
|
|
|
|
|
|
|
|
- StorageUtils.filterAndKeepMsgs(convId, (msg) => msg.id < origMsgId);
|
|
|
|
|
- if (content) {
|
|
|
|
|
- StorageUtils.appendMsg(convId, {
|
|
|
|
|
- id: Date.now(),
|
|
|
|
|
- role: 'user',
|
|
|
|
|
- content,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ if (content !== null) {
|
|
|
|
|
+ const now = Date.now();
|
|
|
|
|
+ const currMsgId = now;
|
|
|
|
|
+ StorageUtils.appendMsg(
|
|
|
|
|
+ {
|
|
|
|
|
+ id: currMsgId,
|
|
|
|
|
+ timestamp: now,
|
|
|
|
|
+ type: 'text',
|
|
|
|
|
+ convId,
|
|
|
|
|
+ role: 'user',
|
|
|
|
|
+ content,
|
|
|
|
|
+ parent: parentNodeId,
|
|
|
|
|
+ children: [],
|
|
|
|
|
+ },
|
|
|
|
|
+ parentNodeId
|
|
|
|
|
+ );
|
|
|
|
|
+ parentNodeId = currMsgId;
|
|
|
}
|
|
}
|
|
|
|
|
+ onChunk(parentNodeId);
|
|
|
|
|
|
|
|
- await generateMessage(convId, onChunk);
|
|
|
|
|
|
|
+ await generateMessage(convId, parentNodeId, onChunk);
|
|
|
};
|
|
};
|
|
|
|
|
|
|
|
const saveConfig = (config: typeof CONFIG_DEFAULT) => {
|
|
const saveConfig = (config: typeof CONFIG_DEFAULT) => {
|
|
@@ -306,7 +360,7 @@ export const AppContextProvider = ({
|
|
|
<AppContext.Provider
|
|
<AppContext.Provider
|
|
|
value={{
|
|
value={{
|
|
|
isGenerating,
|
|
isGenerating,
|
|
|
- viewingConversation,
|
|
|
|
|
|
|
+ viewingChat,
|
|
|
pendingMessages,
|
|
pendingMessages,
|
|
|
sendMessage,
|
|
sendMessage,
|
|
|
stopGenerating,
|
|
stopGenerating,
|