MAJOR REFACTOR (#494)
* move index to home folder, create state and context files and barrell folder * Sanity Check Commit: reducer added to home.tsx manual QA all working * WIP: promptBar * fix missing json parse on folders and prompts * split context and add promptbar context * add context to nested prompt componets and componetize Folder componet * remove log * Create buttons folder and componetize sidebar action button * tidy up prompt handlers * componetized sidebar * added back chatbar componet to left side sidebar * monster commit: Componetized the common code between chatbar and promptbar into new componet Sidebar and added context to both bars * add useFetch service * added prettier import sort to keep imports ordered and easier to indentify * added react query and useFetch to work with RQ * added apiService, errorService and reactQuery * add callback and tidy up error service * refactor chat and child componets to useContext * fix extra calls and bad calls to mel endpoint * minor import cleanup --------- Co-authored-by: jc.durbin <jc.durbin@ardanis.com>
This commit is contained in:
+6
-3
@@ -1,11 +1,14 @@
|
||||
import { ChatBody, Message } from '@/types/chat';
|
||||
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
||||
import { OpenAIError, OpenAIStream } from '@/utils/server';
|
||||
import tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json';
|
||||
import { Tiktoken, init } from '@dqbd/tiktoken/lite/init';
|
||||
|
||||
import { ChatBody, Message } from '@/types/chat';
|
||||
|
||||
// @ts-expect-error
|
||||
import wasm from '../../node_modules/@dqbd/tiktoken/lite/tiktoken_bg.wasm?module';
|
||||
|
||||
import tiktokenModel from '@dqbd/tiktoken/encoders/cl100k_base.json';
|
||||
import { Tiktoken, init } from '@dqbd/tiktoken/lite/init';
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
};
|
||||
|
||||
+6
-3
@@ -1,11 +1,14 @@
|
||||
import { Message } from '@/types/chat';
|
||||
import { GoogleBody, GoogleSource } from '@/types/google';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
import { OPENAI_API_HOST } from '@/utils/app/const';
|
||||
import { cleanSourceText } from '@/utils/server/google';
|
||||
|
||||
import { Message } from '@/types/chat';
|
||||
import { GoogleBody, GoogleSource } from '@/types/google';
|
||||
|
||||
import { Readability } from '@mozilla/readability';
|
||||
import endent from 'endent';
|
||||
import jsdom, { JSDOM } from 'jsdom';
|
||||
import { NextApiRequest, NextApiResponse } from 'next';
|
||||
|
||||
const handler = async (req: NextApiRequest, res: NextApiResponse<any>) => {
|
||||
try {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { Dispatch, createContext } from 'react';
|
||||
|
||||
import { ActionType } from '@/hooks/useCreateReducer';
|
||||
|
||||
import { Conversation } from '@/types/chat';
|
||||
import { KeyValuePair } from '@/types/data';
|
||||
import { FolderType } from '@/types/folder';
|
||||
|
||||
import { HomeInitialState } from './home.state';
|
||||
|
||||
export interface HomeContextProps {
|
||||
state: HomeInitialState;
|
||||
dispatch: Dispatch<ActionType<HomeInitialState>>;
|
||||
handleNewConversation: () => void;
|
||||
handleCreateFolder: (name: string, type: FolderType) => void;
|
||||
handleDeleteFolder: (folderId: string) => void;
|
||||
handleUpdateFolder: (folderId: string, name: string) => void;
|
||||
handleSelectConversation: (conversation: Conversation) => void;
|
||||
handleUpdateConversation: (
|
||||
conversation: Conversation,
|
||||
data: KeyValuePair,
|
||||
) => void;
|
||||
}
|
||||
|
||||
const HomeContext = createContext<HomeContextProps>(undefined!);
|
||||
|
||||
export default HomeContext;
|
||||
@@ -0,0 +1,52 @@
|
||||
import { Conversation, Message } from '@/types/chat';
|
||||
import { ErrorMessage } from '@/types/error';
|
||||
import { FolderInterface } from '@/types/folder';
|
||||
import { OpenAIModel, OpenAIModelID } from '@/types/openai';
|
||||
import { PluginKey } from '@/types/plugin';
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
export interface HomeInitialState {
|
||||
apiKey: string;
|
||||
pluginKeys: PluginKey[];
|
||||
loading: boolean;
|
||||
lightMode: 'light' | 'dark';
|
||||
messageIsStreaming: boolean;
|
||||
modelError: ErrorMessage | null;
|
||||
models: OpenAIModel[];
|
||||
folders: FolderInterface[];
|
||||
conversations: Conversation[];
|
||||
selectedConversation: Conversation | undefined;
|
||||
currentMessage: Message | undefined;
|
||||
prompts: Prompt[];
|
||||
showChatbar: boolean;
|
||||
showPromptbar: boolean;
|
||||
currentFolder: FolderInterface | undefined;
|
||||
messageError: boolean;
|
||||
searchTerm: string;
|
||||
defaultModelId: OpenAIModelID | undefined;
|
||||
serverSideApiKeyIsSet: boolean;
|
||||
serverSidePluginKeysSet: boolean;
|
||||
}
|
||||
|
||||
export const initialState: HomeInitialState = {
|
||||
apiKey: '',
|
||||
loading: false,
|
||||
pluginKeys: [],
|
||||
lightMode: 'dark',
|
||||
messageIsStreaming: false,
|
||||
modelError: null,
|
||||
models: [],
|
||||
folders: [],
|
||||
conversations: [],
|
||||
selectedConversation: undefined,
|
||||
currentMessage: undefined,
|
||||
prompts: [],
|
||||
showPromptbar: true,
|
||||
showChatbar: true,
|
||||
currentFolder: undefined,
|
||||
messageError: false,
|
||||
searchTerm: '',
|
||||
defaultModelId: undefined,
|
||||
serverSideApiKeyIsSet: false,
|
||||
serverSidePluginKeysSet: false,
|
||||
};
|
||||
@@ -0,0 +1,424 @@
|
||||
import { useEffect, useRef, useState } from 'react';
|
||||
import { useQuery } from 'react-query';
|
||||
|
||||
import { GetServerSideProps } from 'next';
|
||||
import { useTranslation } from 'next-i18next';
|
||||
import { serverSideTranslations } from 'next-i18next/serverSideTranslations';
|
||||
import Head from 'next/head';
|
||||
|
||||
import { useCreateReducer } from '@/hooks/useCreateReducer';
|
||||
|
||||
import useErrorService from '@/services/errorService';
|
||||
import useApiService from '@/services/useApiService';
|
||||
|
||||
import {
|
||||
cleanConversationHistory,
|
||||
cleanSelectedConversation,
|
||||
} from '@/utils/app/clean';
|
||||
import { DEFAULT_SYSTEM_PROMPT } from '@/utils/app/const';
|
||||
import {
|
||||
saveConversation,
|
||||
saveConversations,
|
||||
updateConversation,
|
||||
} from '@/utils/app/conversation';
|
||||
import { saveFolders } from '@/utils/app/folders';
|
||||
import { savePrompts } from '@/utils/app/prompts';
|
||||
|
||||
import { Conversation } from '@/types/chat';
|
||||
import { KeyValuePair } from '@/types/data';
|
||||
import { FolderInterface, FolderType } from '@/types/folder';
|
||||
import { OpenAIModelID, OpenAIModels, fallbackModelID } from '@/types/openai';
|
||||
import { Prompt } from '@/types/prompt';
|
||||
|
||||
import { Chat } from '@/components/Chat/Chat';
|
||||
import { Chatbar } from '@/components/Chatbar/Chatbar';
|
||||
import { Navbar } from '@/components/Mobile/Navbar';
|
||||
import Promptbar from '@/components/Promptbar';
|
||||
|
||||
import HomeContext from './home.context';
|
||||
import { HomeInitialState, initialState } from './home.state';
|
||||
|
||||
import { v4 as uuidv4 } from 'uuid';
|
||||
|
||||
interface Props {
|
||||
serverSideApiKeyIsSet: boolean;
|
||||
serverSidePluginKeysSet: boolean;
|
||||
defaultModelId: OpenAIModelID;
|
||||
}
|
||||
|
||||
const Home = ({
|
||||
serverSideApiKeyIsSet,
|
||||
serverSidePluginKeysSet,
|
||||
defaultModelId,
|
||||
}: Props) => {
|
||||
const { t } = useTranslation('chat');
|
||||
const { getModels } = useApiService();
|
||||
const { getModelsError } = useErrorService();
|
||||
const [initialRender, setInitialRender] = useState<boolean>(true);
|
||||
|
||||
const contextValue = useCreateReducer<HomeInitialState>({
|
||||
initialState,
|
||||
});
|
||||
|
||||
const {
|
||||
state: {
|
||||
apiKey,
|
||||
lightMode,
|
||||
folders,
|
||||
conversations,
|
||||
selectedConversation,
|
||||
|
||||
prompts,
|
||||
},
|
||||
dispatch,
|
||||
} = contextValue;
|
||||
|
||||
const stopConversationRef = useRef<boolean>(false);
|
||||
|
||||
const { data, error, refetch } = useQuery(
|
||||
['GetModels', apiKey, serverSideApiKeyIsSet],
|
||||
({ signal }) => {
|
||||
if (!apiKey && !serverSideApiKeyIsSet) return null;
|
||||
|
||||
return getModels(
|
||||
{
|
||||
key: apiKey,
|
||||
},
|
||||
signal,
|
||||
);
|
||||
},
|
||||
{ enabled: true, refetchOnMount: false },
|
||||
);
|
||||
|
||||
useEffect(() => {
|
||||
if (data) dispatch({ field: 'models', value: data });
|
||||
}, [data, dispatch]);
|
||||
|
||||
useEffect(() => {
|
||||
dispatch({ field: 'modelError', value: getModelsError(error) });
|
||||
}, [dispatch, error, getModelsError]);
|
||||
|
||||
// FETCH MODELS ----------------------------------------------
|
||||
|
||||
const handleSelectConversation = (conversation: Conversation) => {
|
||||
dispatch({
|
||||
field: 'selectedConversation',
|
||||
value: conversation,
|
||||
});
|
||||
|
||||
saveConversation(conversation);
|
||||
};
|
||||
|
||||
// FOLDER OPERATIONS --------------------------------------------
|
||||
|
||||
const handleCreateFolder = (name: string, type: FolderType) => {
|
||||
const newFolder: FolderInterface = {
|
||||
id: uuidv4(),
|
||||
name,
|
||||
type,
|
||||
};
|
||||
|
||||
const updatedFolders = [...folders, newFolder];
|
||||
|
||||
dispatch({ field: 'folders', value: updatedFolders });
|
||||
saveFolders(updatedFolders);
|
||||
};
|
||||
|
||||
const handleDeleteFolder = (folderId: string) => {
|
||||
const updatedFolders = folders.filter((f) => f.id !== folderId);
|
||||
dispatch({ field: 'folders', value: updatedFolders });
|
||||
saveFolders(updatedFolders);
|
||||
|
||||
const updatedConversations: Conversation[] = conversations.map((c) => {
|
||||
if (c.folderId === folderId) {
|
||||
return {
|
||||
...c,
|
||||
folderId: null,
|
||||
};
|
||||
}
|
||||
|
||||
return c;
|
||||
});
|
||||
|
||||
dispatch({ field: 'conversations', value: updatedConversations });
|
||||
saveConversations(updatedConversations);
|
||||
|
||||
const updatedPrompts: Prompt[] = prompts.map((p) => {
|
||||
if (p.folderId === folderId) {
|
||||
return {
|
||||
...p,
|
||||
folderId: null,
|
||||
};
|
||||
}
|
||||
|
||||
return p;
|
||||
});
|
||||
|
||||
dispatch({ field: 'prompts', value: updatedPrompts });
|
||||
savePrompts(updatedPrompts);
|
||||
};
|
||||
|
||||
const handleUpdateFolder = (folderId: string, name: string) => {
|
||||
const updatedFolders = folders.map((f) => {
|
||||
if (f.id === folderId) {
|
||||
return {
|
||||
...f,
|
||||
name,
|
||||
};
|
||||
}
|
||||
|
||||
return f;
|
||||
});
|
||||
|
||||
dispatch({ field: 'folders', value: updatedFolders });
|
||||
|
||||
saveFolders(updatedFolders);
|
||||
};
|
||||
|
||||
// CONVERSATION OPERATIONS --------------------------------------------
|
||||
|
||||
const handleNewConversation = () => {
|
||||
const lastConversation = conversations[conversations.length - 1];
|
||||
|
||||
const newConversation: Conversation = {
|
||||
id: uuidv4(),
|
||||
name: `${t('New Conversation')}`,
|
||||
messages: [],
|
||||
model: lastConversation?.model || {
|
||||
id: OpenAIModels[defaultModelId].id,
|
||||
name: OpenAIModels[defaultModelId].name,
|
||||
maxLength: OpenAIModels[defaultModelId].maxLength,
|
||||
tokenLimit: OpenAIModels[defaultModelId].tokenLimit,
|
||||
},
|
||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||
folderId: null,
|
||||
};
|
||||
|
||||
const updatedConversations = [...conversations, newConversation];
|
||||
|
||||
dispatch({ field: 'selectedConversation', value: newConversation });
|
||||
dispatch({ field: 'conversations', value: updatedConversations });
|
||||
|
||||
saveConversation(newConversation);
|
||||
saveConversations(updatedConversations);
|
||||
|
||||
dispatch({ field: 'loading', value: false });
|
||||
};
|
||||
|
||||
const handleUpdateConversation = (
|
||||
conversation: Conversation,
|
||||
data: KeyValuePair,
|
||||
) => {
|
||||
const updatedConversation = {
|
||||
...conversation,
|
||||
[data.key]: data.value,
|
||||
};
|
||||
|
||||
const { single, all } = updateConversation(
|
||||
updatedConversation,
|
||||
conversations,
|
||||
);
|
||||
|
||||
dispatch({ field: 'selectedConversation', value: single });
|
||||
dispatch({ field: 'conversations', value: all });
|
||||
};
|
||||
|
||||
// EFFECTS --------------------------------------------
|
||||
|
||||
useEffect(() => {
|
||||
if (window.innerWidth < 640) {
|
||||
dispatch({ field: 'showChatbar', value: false });
|
||||
}
|
||||
}, [selectedConversation]);
|
||||
|
||||
useEffect(() => {
|
||||
defaultModelId &&
|
||||
dispatch({ field: 'defaultModelId', value: defaultModelId });
|
||||
serverSideApiKeyIsSet &&
|
||||
dispatch({
|
||||
field: 'serverSideApiKeyIsSet',
|
||||
value: serverSideApiKeyIsSet,
|
||||
});
|
||||
serverSidePluginKeysSet &&
|
||||
dispatch({
|
||||
field: 'serverSidePluginKeysSet',
|
||||
value: serverSidePluginKeysSet,
|
||||
});
|
||||
}, [defaultModelId, serverSideApiKeyIsSet, serverSidePluginKeysSet]);
|
||||
|
||||
// ON LOAD --------------------------------------------
|
||||
|
||||
useEffect(() => {
|
||||
console.log('initialize', serverSideApiKeyIsSet);
|
||||
const theme = localStorage.getItem('theme');
|
||||
if (theme) {
|
||||
dispatch({ field: 'lightMode', value: theme as 'dark' | 'light' });
|
||||
}
|
||||
|
||||
const apiKey = localStorage.getItem('apiKey');
|
||||
|
||||
if (serverSideApiKeyIsSet) {
|
||||
console.log('trigger key', apiKey);
|
||||
dispatch({ field: 'apiKey', value: '' });
|
||||
|
||||
localStorage.removeItem('apiKey');
|
||||
} else if (apiKey) {
|
||||
dispatch({ field: 'apiKey', value: apiKey });
|
||||
}
|
||||
|
||||
const pluginKeys = localStorage.getItem('pluginKeys');
|
||||
if (serverSidePluginKeysSet) {
|
||||
dispatch({ field: 'pluginKeys', value: [] });
|
||||
localStorage.removeItem('pluginKeys');
|
||||
} else if (pluginKeys) {
|
||||
dispatch({ field: 'pluginKeys', value: pluginKeys });
|
||||
}
|
||||
|
||||
if (window.innerWidth < 640) {
|
||||
dispatch({ field: 'showChatbar', value: false });
|
||||
}
|
||||
|
||||
const showChatbar = localStorage.getItem('showChatbar');
|
||||
if (showChatbar) {
|
||||
dispatch({ field: 'showChatbar', value: showChatbar === 'true' });
|
||||
}
|
||||
|
||||
const showPromptbar = localStorage.getItem('showPromptbar');
|
||||
if (showPromptbar) {
|
||||
dispatch({ field: 'showPromptbar', value: showPromptbar === 'true' });
|
||||
}
|
||||
|
||||
const folders = localStorage.getItem('folders');
|
||||
if (folders) {
|
||||
dispatch({ field: 'folders', value: JSON.parse(folders) });
|
||||
}
|
||||
|
||||
const prompts = localStorage.getItem('prompts');
|
||||
if (prompts) {
|
||||
dispatch({ field: 'prompts', value: JSON.parse(prompts) });
|
||||
}
|
||||
|
||||
const conversationHistory = localStorage.getItem('conversationHistory');
|
||||
if (conversationHistory) {
|
||||
const parsedConversationHistory: Conversation[] =
|
||||
JSON.parse(conversationHistory);
|
||||
const cleanedConversationHistory = cleanConversationHistory(
|
||||
parsedConversationHistory,
|
||||
);
|
||||
|
||||
dispatch({ field: 'conversations', value: cleanedConversationHistory });
|
||||
}
|
||||
|
||||
const selectedConversation = localStorage.getItem('selectedConversation');
|
||||
if (selectedConversation) {
|
||||
const parsedSelectedConversation: Conversation =
|
||||
JSON.parse(selectedConversation);
|
||||
const cleanedSelectedConversation = cleanSelectedConversation(
|
||||
parsedSelectedConversation,
|
||||
);
|
||||
|
||||
dispatch({
|
||||
field: 'selectedConversation',
|
||||
value: cleanedSelectedConversation,
|
||||
});
|
||||
} else {
|
||||
dispatch({
|
||||
field: 'selectedConversation',
|
||||
value: {
|
||||
id: uuidv4(),
|
||||
name: 'New conversation',
|
||||
messages: [],
|
||||
model: OpenAIModels[defaultModelId],
|
||||
prompt: DEFAULT_SYSTEM_PROMPT,
|
||||
folderId: null,
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [
|
||||
defaultModelId,
|
||||
dispatch,
|
||||
serverSideApiKeyIsSet,
|
||||
serverSidePluginKeysSet,
|
||||
]);
|
||||
|
||||
return (
|
||||
<HomeContext.Provider
|
||||
value={{
|
||||
...contextValue,
|
||||
handleNewConversation,
|
||||
handleCreateFolder,
|
||||
handleDeleteFolder,
|
||||
handleUpdateFolder,
|
||||
handleSelectConversation,
|
||||
handleUpdateConversation,
|
||||
}}
|
||||
>
|
||||
<Head>
|
||||
<title>Chatbot UI</title>
|
||||
<meta name="description" content="ChatGPT but better." />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="height=device-height ,width=device-width, initial-scale=1, user-scalable=no"
|
||||
/>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
{selectedConversation && (
|
||||
<main
|
||||
className={`flex h-screen w-screen flex-col text-sm text-white dark:text-white ${lightMode}`}
|
||||
>
|
||||
<div className="fixed top-0 w-full sm:hidden">
|
||||
<Navbar
|
||||
selectedConversation={selectedConversation}
|
||||
onNewConversation={handleNewConversation}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex h-full w-full pt-[48px] sm:pt-0">
|
||||
<Chatbar />
|
||||
|
||||
<div className="flex flex-1">
|
||||
<Chat stopConversationRef={stopConversationRef} />
|
||||
</div>
|
||||
|
||||
<Promptbar />
|
||||
</div>
|
||||
</main>
|
||||
)}
|
||||
</HomeContext.Provider>
|
||||
);
|
||||
};
|
||||
export default Home;
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async ({ locale }) => {
|
||||
const defaultModelId =
|
||||
(process.env.DEFAULT_MODEL &&
|
||||
Object.values(OpenAIModelID).includes(
|
||||
process.env.DEFAULT_MODEL as OpenAIModelID,
|
||||
) &&
|
||||
process.env.DEFAULT_MODEL) ||
|
||||
fallbackModelID;
|
||||
|
||||
let serverSidePluginKeysSet = false;
|
||||
|
||||
const googleApiKey = process.env.GOOGLE_API_KEY;
|
||||
const googleCSEId = process.env.GOOGLE_CSE_ID;
|
||||
|
||||
if (googleApiKey && googleCSEId) {
|
||||
serverSidePluginKeysSet = true;
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
serverSideApiKeyIsSet: !!process.env.OPENAI_API_KEY,
|
||||
defaultModelId,
|
||||
serverSidePluginKeysSet,
|
||||
...(await serverSideTranslations(locale ?? 'en', [
|
||||
'common',
|
||||
'chat',
|
||||
'sidebar',
|
||||
'markdown',
|
||||
'promptbar',
|
||||
])),
|
||||
},
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1 @@
|
||||
export { default, getServerSideProps } from './home';
|
||||
+3
-2
@@ -1,6 +1,7 @@
|
||||
import { OpenAIModel, OpenAIModelID, OpenAIModels } from '@/types/openai';
|
||||
import { OPENAI_API_HOST } from '@/utils/app/const';
|
||||
|
||||
import { OpenAIModel, OpenAIModelID, OpenAIModels } from '@/types/openai';
|
||||
|
||||
export const config = {
|
||||
runtime: 'edge',
|
||||
};
|
||||
@@ -17,7 +18,7 @@ const handler = async (req: Request): Promise<Response> => {
|
||||
Authorization: `Bearer ${key ? key : process.env.OPENAI_API_KEY}`,
|
||||
...(process.env.OPENAI_ORGANIZATION && {
|
||||
'OpenAI-Organization': process.env.OPENAI_ORGANIZATION,
|
||||
})
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user