Merge pull request #165 from danny-avila/dano/react-query-typescript

Refactor: Create data-provider for api services with React Query and TypeScript
This commit is contained in:
Danny Avila 2023-04-07 22:14:44 -04:00 committed by GitHub
commit 0a80f836f0
35 changed files with 1562 additions and 686 deletions

1
.gitignore vendored
View file

@ -57,3 +57,4 @@ src/style - official.css
/e2e/specs/.test-results/
/e2e/playwright-report/
/playwright/.cache/
.DS_Store

View file

@ -26,5 +26,6 @@ module.exports = {
"rules": {
'react/prop-types': ['off'],
'react/display-name': ['off'],
"no-debugger":"off",
}
}

844
client/package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -30,6 +30,7 @@
"@radix-ui/react-label": "^2.0.0",
"@radix-ui/react-slider": "^1.1.1",
"@radix-ui/react-tabs": "^1.0.3",
"@tanstack/react-query": "^4.28.0",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.10",
"@types/react": "^18.0.30",
@ -63,7 +64,6 @@
"remark-gfm": "^3.0.1",
"remark-math": "^5.1.1",
"remark-supersub": "^1.0.0",
"swr": "^2.0.3",
"tailwind-merge": "^1.9.1",
"tailwindcss-animate": "^1.0.5",
"tailwindcss-radix": "^2.8.0",
@ -77,7 +77,13 @@
"@babel/plugin-transform-runtime": "^7.19.6",
"@babel/preset-env": "^7.20.2",
"@babel/preset-react": "^7.18.6",
"@babel/preset-typescript": "^7.21.0",
"@babel/runtime": "^7.20.13",
"@tanstack/react-query-devtools": "^4.29.0",
"@types/jest": "^29.5.0",
"@types/node": "^18.15.10",
"@types/react": "^18.0.30",
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^3.1.0",
"autoprefixer": "^10.4.13",
"babel-loader": "^9.1.2",

View file

@ -1,15 +1,13 @@
import React, { useEffect, useState } from 'react';
import { useEffect } from 'react';
import { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
import Root from './routes/Root';
import Chat from './routes/Chat';
import Search from './routes/Search';
import store from './store';
import userAuth from './utils/userAuth';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { ScreenshotProvider } from './utils/screenshotContext.jsx';
import axios from 'axios';
import { useGetSearchEnabledQuery, useGetUserQuery, useGetEndpointsQuery, useGetPresetsQuery} from '~/data-provider';
import {ReactQueryDevtools} from '@tanstack/react-query-devtools';
const router = createBrowserRouter([
{
@ -43,58 +41,52 @@ const App = () => {
const setEndpointsConfig = useSetRecoilState(store.endpointsConfig);
const setPresets = useSetRecoilState(store.presets);
const searchEnabledQuery = useGetSearchEnabledQuery();
const userQuery = useGetUserQuery();
const endpointsQuery = useGetEndpointsQuery();
const presetsQuery = useGetPresetsQuery();
useEffect(() => {
// fetch if seatch enabled
axios
.get('/api/search/enable', {
timeout: 1000,
withCredentials: true
})
.then(res => {
setIsSearchEnabled(res.data);
});
if(endpointsQuery.data) {
setEndpointsConfig(endpointsQuery.data);
} else if(endpointsQuery.isError) {
console.error("Failed to get endpoints", endpointsQuery.error);
window.location.href = '/auth/login';
}
}, [endpointsQuery.data, endpointsQuery.isError]);
// fetch user
userAuth()
.then(user => setUser(user))
.catch(err => console.log(err));
useEffect(() => {
if(presetsQuery.data) {
setPresets(presetsQuery.data);
} else if(presetsQuery.isError) {
console.error("Failed to get presets", presetsQuery.error);
window.location.href = '/auth/login';
}
}, [presetsQuery.data, presetsQuery.isError]);
// fetch models
axios
.get('/api/endpoints', {
timeout: 1000,
withCredentials: true
})
.then(({ data }) => {
setEndpointsConfig(data);
})
.catch(error => {
console.error(error);
console.log('Not login!');
window.location.href = '/auth/login';
});
useEffect(() => {
if (searchEnabledQuery.data) {
setIsSearchEnabled(searchEnabledQuery.data);
} else if(searchEnabledQuery.isError) {
console.error("Failed to get search enabled", searchEnabledQuery.error);
}
}, [searchEnabledQuery.data, searchEnabledQuery.isError]);
// fetch presets
axios
.get('/api/presets', {
timeout: 1000,
withCredentials: true
})
.then(({ data }) => {
setPresets(data);
})
.catch(error => {
console.error(error);
console.log('Not login!');
window.location.href = '/auth/login';
});
}, []);
useEffect(() => {
if (userQuery.data) {
setUser(userQuery.data);
} else if(userQuery.isError) {
console.error("Failed to get user", userQuery.error);
window.location.href = '/auth/login';
}
}, [userQuery.data, userQuery.isError]);
if (user)
return (
<div>
<>
<RouterProvider router={router} />
</div>
<ReactQueryDevtools initialIsOpen={false} />
</>
);
else return <div className="flex h-screen"></div>;
};

View file

@ -1,10 +1,9 @@
import React, { useState, useRef } from 'react';
import { useState, useRef, useEffect} from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useUpdateConversationMutation } from '~/data-provider';
import RenameButton from './RenameButton';
import DeleteButton from './DeleteButton';
import ConvoIcon from '../svg/ConvoIcon';
import manualSWR from '~/utils/fetchers';
import store from '~/store';
@ -15,6 +14,8 @@ export default function Conversation({ conversation, retainView }) {
const { refreshConversations } = store.useConversations();
const { switchToConversation } = store.useConversation();
const updateConvoMutation = useUpdateConversationMutation(currentConversation?.conversationId);
const [renaming, setRenaming] = useState(false);
const inputRef = useRef(null);
@ -22,8 +23,6 @@ export default function Conversation({ conversation, retainView }) {
const [titleInput, setTitleInput] = useState(title);
const rename = manualSWR(`/api/convos/update`, 'post');
const clickHandler = async () => {
if (currentConversation?.conversationId === conversationId) {
return;
@ -59,15 +58,20 @@ export default function Conversation({ conversation, retainView }) {
if (titleInput === title) {
return;
}
rename.trigger({ conversationId, title: titleInput }).then(() => {
updateConvoMutation.mutate({ conversationId, title: titleInput });
};
useEffect(() => {
if (updateConvoMutation.isSuccess) {
refreshConversations();
if (conversationId == currentConversation?.conversationId)
if (conversationId == currentConversation?.conversationId) {
setCurrentConversation(prevState => ({
...prevState,
title: titleInput
}));
});
};
}
}
}, [updateConvoMutation.isSuccess]);
const handleKeyDown = e => {
if (e.key === 'Enter') {

View file

@ -1,8 +1,8 @@
import React from 'react';
import { useEffect } from 'react';
import TrashIcon from '../svg/TrashIcon';
import CrossIcon from '../svg/CrossIcon';
import manualSWR from '~/utils/fetchers';
import { useRecoilValue } from 'recoil';
import { useDeleteConversationMutation } from '~/data-provider';
import store from '~/store';
@ -10,13 +10,23 @@ export default function DeleteButton({ conversationId, renaming, cancelHandler,
const currentConversation = useRecoilValue(store.conversation) || {};
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
if (currentConversation?.conversationId == conversationId) newConversation();
refreshConversations();
retainView();
});
const clickHandler = () => trigger({ conversationId, source: 'button' });
const deleteConvoMutation = useDeleteConversationMutation(conversationId);
useEffect(() => {
if(deleteConvoMutation.isSuccess) {
if (currentConversation?.conversationId == conversationId) newConversation();
refreshConversations();
retainView();
}
}, [deleteConvoMutation.isSuccess]);
const clickHandler = () => {
deleteConvoMutation.mutate({conversationId, source: 'button' });
};
const handler = renaming ? cancelHandler : clickHandler;
return (

View file

@ -1,22 +1,15 @@
import React, { useEffect, useState } from 'react';
import { useEffect, useState } from 'react';
import TextareaAutosize from 'react-textarea-autosize';
import { Input } from '~/components/ui/Input.tsx';
import { Label } from '~/components/ui/Label.tsx';
import { Checkbox } from '~/components/ui/Checkbox.tsx';
import SelectDropdown from '../../ui/SelectDropDown';
import { axiosPost } from '~/utils/fetchers.js';
import SelectDropDown from '../../ui/SelectDropDown';
import { cn } from '~/utils/';
import debounce from 'lodash/debounce';
// import ModelDropDown from '../../ui/ModelDropDown';
// import { Slider } from '~/components/ui/Slider.tsx';
// import OptionHover from './OptionHover';
// import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
import useDebounce from '~/hooks/useDebounce';
import { useUpdateTokenCountMutation } from '~/data-provider';
const defaultTextProps =
'rounded-md border border-gray-200 focus:border-slate-400 focus:bg-gray-50 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.05)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-500 dark:bg-gray-700 focus:dark:bg-gray-600 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
const optionText =
'p-0 shadow-none text-right pr-1 h-8 border-transparent focus:ring-[#10a37f] focus:ring-offset-0 focus:ring-opacity-100 hover:bg-gray-800/10 dark:hover:bg-white/10 focus:bg-gray-800/10 dark:focus:bg-white/10 transition-colors';
function Settings(props) {
const { readonly, context, systemMessage, jailbreak, toneStyle, setOption } = props;
const [tokenCount, setTokenCount] = useState(0);
@ -25,31 +18,26 @@ function Settings(props) {
const setSystemMessage = setOption('systemMessage');
const setJailbreak = setOption('jailbreak');
const setToneStyle = value => setOption('toneStyle')(value.toLowerCase());
const debouncedContext = useDebounce(context, 250);
const updateTokenCountMutation = useUpdateTokenCountMutation();
// useEffect to update token count
useEffect(() => {
if (!context || context.trim() === '') {
useEffect(() => {
if (!debouncedContext || debouncedContext.trim() === '') {
setTokenCount(0);
return;
}
const debouncedPost = debounce(axiosPost, 250);
const handleTextChange = context => {
debouncedPost({
url: '/api/tokenizer',
arg: { text: context },
callback: data => {
updateTokenCountMutation.mutate({ text: context }, {
onSuccess: data => {
setTokenCount(data.count);
}
});
};
handleTextChange(context);
return () => debouncedPost.cancel();
}, [context]);
handleTextChange(debouncedContext);
}, [debouncedContext]);
// console.log('data', data);
return (
<>
@ -62,7 +50,7 @@ function Settings(props) {
>
Tone Style <small className="opacity-40">(default: fast)</small>
</Label>
<SelectDropdown
<SelectDropDown
id="toneStyle-dropdown"
title={null}
value={`${toneStyle.charAt(0).toUpperCase()}${toneStyle.slice(1)}`}
@ -151,14 +139,6 @@ function Settings(props) {
/>
</div>
)}
{/* <HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
</HoverCardTrigger>
<OptionHover
type="temp"
side="left"
/>
</HoverCard> */}
</div>
</div>
</>

View file

@ -1,12 +1,10 @@
import React from 'react';
import { useRecoilValue } from 'recoil';
import TextareaAutosize from 'react-textarea-autosize';
import SelectDropdown from '../../ui/SelectDropDown';
import SelectDropDown from '../../ui/SelectDropDown';
import { Input } from '~/components/ui/Input.tsx';
import { Label } from '~/components/ui/Label.tsx';
import { Slider } from '~/components/ui/Slider.tsx';
import { InputNumber } from '~/components/ui/InputNumber.tsx';
// import { InputNumber } from '../../ui/InputNumber';
import OptionHover from './OptionHover';
import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
import { cn } from '~/utils/';
@ -38,7 +36,7 @@ function Settings(props) {
<div className="grid gap-6 sm:grid-cols-2">
<div className="col-span-1 flex flex-col items-center justify-start gap-6">
<div className="grid w-full items-center gap-2">
<SelectDropdown
<SelectDropDown
value={model}
setValue={setModel}
availableValues={models}
@ -49,23 +47,6 @@ function Settings(props) {
)}
containerClassName="flex w-full resize-none"
/>
{/* <Label
htmlFor="model"
className="text-left text-sm font-medium"
>
Model
</Label>
<Input
id="model"
value={model}
// ref={inputRef}
onChange={e => setModel(e.target.value)}
placeholder="Set a custom name for ChatGPT"
className={cn(
defaultTextProps,
'flex h-10 max-h-10 w-full resize-none px-3 py-2 focus:outline-none focus:ring-0 focus:ring-opacity-0 focus:ring-offset-0'
)}
/> */}
</div>
<div className="grid w-full items-center gap-2">
<Label
@ -78,7 +59,6 @@ function Settings(props) {
id="chatGptLabel"
disabled={readonly}
value={chatGptLabel || ''}
// ref={inputRef}
onChange={e => setChatGptLabel(e.target.value || null)}
placeholder="Set a custom name for ChatGPT"
className={cn(
@ -104,15 +84,6 @@ function Settings(props) {
defaultTextProps,
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 '
)}
// onFocus={() => {
// textareaRef.current.classList.remove('max-h-10');
// textareaRef.current.classList.add('max-h-52');
// }}
// onBlur={() => {
// textareaRef.current.classList.remove('max-h-52');
// textareaRef.current.classList.add('max-h-10');
// }}
// ref={textareaRef}
/>
</div>
</div>
@ -160,43 +131,6 @@ function Settings(props) {
side="left"
/>
</HoverCard>
{/* <HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">
<Label
htmlFor="chatGptLabel"
className="text-left text-sm font-medium"
>
Max tokens
</Label>
<Input
id="max-tokens-int"
disabled
value={maxTokens}
onChange={e => setMaxTokens(e.target.value)}
className={cn(
defaultTextProps,
cn(optionText, 'h-auto w-12 border-0 group-hover/temp:border-gray-200')
)}
/>
</div>
<Slider
disabled={readonly}
value={[maxTokens]}
onValueChange={value => setMaxTokens(value[0])}
max={2048} // should be dynamic to the currently selected model
min={1}
step={1}
className="flex h-4 w-full"
/>
</HoverCardTrigger>
<OptionHover
type="max"
side="left"
/>
</HoverCard> */}
<HoverCard openDelay={300}>
<HoverCardTrigger className="grid w-full items-center gap-2">
<div className="flex justify-between">

View file

@ -1,19 +1,18 @@
import React, { useEffect, useState } from 'react';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import axios from 'axios';
import { useEffect, useState } from 'react';
import { useRecoilValue } from 'recoil';
import DialogTemplate from '../ui/DialogTemplate';
import { Dialog } from '../ui/Dialog.tsx';
import { Input } from '../ui/Input.tsx';
import { Label } from '../ui/Label.tsx';
import { cn } from '~/utils/';
import cleanupPreset from '~/utils/cleanupPreset';
import { useCreatePresetMutation } from '~/data-provider';
import store from '~/store';
const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
const [title, setTitle] = useState(preset?.title || 'My Preset');
const setPresets = useSetRecoilState(store.presets);
const endpointsFilter = useRecoilValue(store.endpointsFilter);
const createPresetMutation = useCreatePresetMutation();
const defaultTextProps =
'rounded-md border border-gray-300 bg-transparent text-sm shadow-[0_0_10px_rgba(0,0,0,0.10)] outline-none placeholder:text-gray-400 focus:outline-none focus:ring-gray-400 focus:ring-opacity-20 focus:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50 dark:border-gray-400 dark:bg-gray-700 dark:text-gray-50 dark:shadow-[0_0_15px_rgba(0,0,0,0.10)] dark:focus:border-gray-400 dark:focus:outline-none dark:focus:ring-0 dark:focus:ring-gray-400 dark:focus:ring-offset-0';
@ -26,15 +25,7 @@ const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
},
endpointsFilter
});
axios({
method: 'post',
url: '/api/presets',
data: _preset,
withCredentials: true
}).then(res => {
setPresets(res?.data);
});
createPresetMutation.mutate(_preset);
};
useEffect(() => {

View file

@ -1,10 +1,10 @@
import React, { useState, useEffect } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
import { useState } from 'react';
import { useRecoilState } from 'recoil';
import { cn } from '~/utils';
import { Button } from '../../ui/Button.tsx';
import { Settings2 } from 'lucide-react';
import { Tabs, TabsList, TabsTrigger } from '../../ui/Tabs.tsx';
import SelectDropdown from '../../ui/SelectDropDown';
import SelectDropDown from '../../ui/SelectDropDown';
import Settings from '../../Endpoints/BingAI/Settings.jsx';
import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
@ -18,25 +18,12 @@ function BingAIOptions() {
const { endpoint, conversationId } = conversation;
const { toneStyle, context, systemMessage, jailbreak } = conversation;
// useEffect(() => {
// if (endpoint !== 'bingAI') return;
// const mustInAdvancedMode = context !== null || systemMessage !== null;
// if (mustInAdvancedMode && !advancedMode) setAdvancedMode(true);
// }, [conversation, advancedMode]);
if (endpoint !== 'bingAI') return null;
if (conversationId !== 'new') return null;
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
const switchToSimpleMode = () => {
// setConversation(prevState => ({
// ...prevState,
// context: null,
// systemMessage: null
// }));
setAdvancedMode(false);
};
@ -68,7 +55,7 @@ function BingAIOptions() {
(!advancedMode ? ' show' : '')
}
>
<SelectDropdown
<SelectDropDown
title="Mode"
value={jailbreak ? 'Sydney' : 'BingAI'}
setValue={value => setOption('jailbreak')(value === 'Sydney')}

View file

@ -1,6 +1,6 @@
import React, { useEffect } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import SelectDropdown from '../../ui/SelectDropDown.jsx';
import SelectDropDown from '../../ui/SelectDropDown';
import { cn } from '~/utils/';
import store from '~/store';
@ -12,21 +12,11 @@ function ChatGPTOptions() {
const endpointsConfig = useRecoilValue(store.endpointsConfig);
// useEffect(() => {
// if (endpoint !== 'chatGPTBrowser') return;
// }, [conversation]);
if (endpoint !== 'chatGPTBrowser') return null;
if (conversationId !== 'new') return null;
const models = endpointsConfig?.['chatGPTBrowser']?.['availableModels'] || [];
// const modelMap = new Map([
// ['Default (GPT-3.5)', 'text-davinci-002-render-sha'],
// ['Legacy (GPT-3.5)', 'text-davinci-002-render-paid'],
// ['GPT-4', 'gpt-4']
// ]);
const setOption = param => newValue => {
let update = {};
update[param] = newValue;
@ -41,7 +31,7 @@ function ChatGPTOptions() {
return (
<div className="openAIOptions-simple-container show flex w-full flex-wrap items-center justify-center gap-2">
<SelectDropdown
<SelectDropDown
value={model}
setValue={setOption('model')}
availableValues={models}

View file

@ -45,15 +45,6 @@ export default function PresetItem({ preset = {}, value, onSelect, onChangePrese
{icon}
{preset?.title}
<small className="ml-2">({getPresetTitle()})</small>
{/* <RenameButton
twcss={`ml-auto mr-2 ${buttonClass.className}`}
onRename={onRename}
renaming={renaming}
onMouseOver={handleMouseOver}
onMouseOut={handleMouseOut}
renameHandler={renameHandler}
/> */}
<div className="flex w-4 flex-1" />
<button
className="invisible m-0 mr-1 rounded-md p-2 text-gray-400 hover:text-gray-700 group-hover:visible dark:text-gray-400 dark:hover:text-gray-200 "

View file

@ -1,12 +1,11 @@
import React, { useState, useEffect } from 'react';
import { useState, useEffect } from 'react';
import { useRecoilValue, useRecoilState } from 'recoil';
import EditPresetDialog from '../../Endpoints/EditPresetDialog.jsx';
import EndpointItems from './EndpointItems.jsx';
import PresetItems from './PresetItems.jsx';
import FileUpload from './FileUpload.jsx';
import EditPresetDialog from '../../Endpoints/EditPresetDialog';
import EndpointItems from './EndpointItems';
import PresetItems from './PresetItems';
import FileUpload from './FileUpload';
import getIcon from '~/utils/getIcon';
import manualSWR, { handleFileSelected } from '~/utils/fetchers';
import { useDeletePresetMutation, useCreatePresetMutation } from '~/data-provider';
import { Button } from '../../ui/Button.tsx';
import {
DropdownMenu,
@ -17,33 +16,34 @@ import {
DropdownMenuTrigger
} from '../../ui/DropdownMenu.tsx';
import { Dialog, DialogTrigger } from '../../ui/Dialog.tsx';
import DialogTemplate from '../../ui/DialogTemplate.jsx';
import DialogTemplate from '../../ui/DialogTemplate';
import store from '~/store';
export default function NewConversationMenu() {
// const [modelSave, setModelSave] = useState(false);
const [menuOpen, setMenuOpen] = useState(false);
const [presetModelVisible, setPresetModelVisible] = useState(false);
const [preset, setPreset] = useState(false);
// const models = useRecoilValue(store.models);
const availableEndpoints = useRecoilValue(store.availableEndpoints);
// const setCustomGPTModels = useSetRecoilState(store.customGPTModels);
const [presets, setPresets] = useRecoilState(store.presets);
const conversation = useRecoilValue(store.conversation) || {};
const { endpoint, conversationId } = conversation;
// const { model, promptPrefix, chatGptLabel, conversationId } = conversation;
const { newConversation } = store.useConversation();
const { trigger: clearPresetsTrigger } = manualSWR(`/api/presets/delete`, 'post', res => {
console.log(res);
setPresets(res.data);
});
const deletePresetsMutation = useDeletePresetMutation();
const createPresetMutation = useCreatePresetMutation();
const importPreset = jsonData => {
handleFileSelected(jsonData).then(setPresets);
createPresetMutation.mutate({...jsonData}, {
onSuccess: (data) => {
setPresets(data);
},
onError: (error) => {
console.error('Error uploading the preset:', error);
}
})
};
// update the default model when availableModels changes
@ -65,7 +65,6 @@ export default function NewConversationMenu() {
setMenuOpen(false);
if (!newEndpoint) return;
// else if (newEndpoint === endpoint) return;
else {
newConversation({}, { endpoint: newEndpoint });
}
@ -75,7 +74,6 @@ export default function NewConversationMenu() {
const onSelectPreset = newPreset => {
setMenuOpen(false);
if (!newPreset) return;
// else if (newEndpoint === endpoint) return;
else {
newConversation({}, newPreset);
}
@ -86,8 +84,12 @@ export default function NewConversationMenu() {
setPreset(preset);
};
const clearPreset = () => {
clearPresetsTrigger({});
const clearAllPresets = () => {
deletePresetsMutation.mutate({arg: {}});
};
const onDeletePreset = preset => {
deletePresetsMutation.mutate({arg: preset});
};
const icon = getIcon({
@ -99,9 +101,7 @@ export default function NewConversationMenu() {
});
return (
<Dialog
// onOpenChange={onOpenChange}
>
<Dialog>
<DropdownMenu
open={menuOpen}
onOpenChange={setMenuOpen}
@ -109,7 +109,6 @@ export default function NewConversationMenu() {
<DropdownMenuTrigger asChild>
<Button
variant="outline"
// style={{backgroundColor: 'rgb(16, 163, 127)'}}
className={`group relative mt-[-8px] mb-[-12px] ml-0 items-center rounded-md border-0 p-1 outline-none focus:ring-0 focus:ring-offset-0 dark:data-[state=open]:bg-opacity-50 md:left-1 md:ml-[-12px] md:pl-1`}
>
{icon}
@ -150,7 +149,6 @@ export default function NewConversationMenu() {
<Button
type="button"
className="h-auto bg-transparent px-2 py-1 text-xs font-medium font-normal text-red-700 hover:bg-slate-200 hover:text-red-700 dark:bg-transparent dark:text-red-400 dark:hover:bg-gray-800 dark:hover:text-red-400"
// onClick={clearPreset}
>
Clear All
</Button>
@ -159,7 +157,7 @@ export default function NewConversationMenu() {
title="Clear presets"
description="Are you sure you want to clear all presets? This is irreversible."
selection={{
selectHandler: clearPreset,
selectHandler: clearAllPresets,
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
selectText: 'Clear'
}}
@ -176,7 +174,7 @@ export default function NewConversationMenu() {
presets={presets}
onSelect={onSelectPreset}
onChangePreset={onChangePreset}
onDeletePreset={clearPresetsTrigger}
onDeletePreset={onDeletePreset}
/>
) : (
<DropdownMenuLabel className="dark:text-gray-300">No preset yet.</DropdownMenuLabel>

View file

@ -1,7 +1,7 @@
import React, { useEffect, useState } from 'react';
import { useState } from 'react';
import { Settings2 } from 'lucide-react';
import { useRecoilState, useRecoilValue } from 'recoil';
import SelectDropdown from '../../ui/SelectDropDown';
import SelectDropDown from '../../ui/SelectDropDown';
import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
import { Button } from '../../ui/Button.tsx';
@ -21,20 +21,6 @@ function OpenAIOptions() {
const endpointsConfig = useRecoilValue(store.endpointsConfig);
// useEffect(() => {
// if (endpoint !== 'openAI') return;
// const mustInAdvancedMode =
// chatGptLabel !== null ||
// promptPrefix !== null ||
// temperature !== 1 ||
// top_p !== 1 ||
// presence_penalty !== 0 ||
// frequency_penalty !== 0;
// if (mustInAdvancedMode && !advancedMode) setAdvancedMode(true);
// }, [conversation, advancedMode]);
if (endpoint !== 'openAI') return null;
if (conversationId !== 'new') return null;
@ -43,15 +29,6 @@ function OpenAIOptions() {
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
const switchToSimpleMode = () => {
// setConversation(prevState => ({
// ...prevState,
// chatGptLabel: null,
// promptPrefix: null,
// temperature: 1,
// top_p: 1,
// presence_penalty: 0,
// frequency_penalty: 0
// }));
setAdvancedMode(false);
};
@ -79,17 +56,7 @@ function OpenAIOptions() {
(!advancedMode ? ' show' : '')
}
>
{/* <ModelSelect
model={model}
availableModels={availableModels}
onChange={setOption('model')}
type="button"
className={cn(
cardStyle,
' z-50 flex h-[40px] items-center justify-center px-4 hover:bg-slate-50 data-[state=open]:bg-slate-50 dark:hover:bg-gray-600 dark:data-[state=open]:bg-gray-600'
)}
/> */}
<SelectDropdown
<SelectDropDown
value={model}
setValue={setOption('model')}
availableValues={models}

View file

@ -1,7 +1,7 @@
import { useEffect } from 'react';
import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
import { SSE } from '~/utils/sse.mjs';
import createPayload from '~/utils/createPayload';
import { SSE } from '~/data-provider/sse.mjs';
import createPayload from '~/data-provider/createPayload';
import store from '~/store';

View file

@ -1,16 +1,15 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import { useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
import { useState, useEffect, useRef } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import copy from 'copy-to-clipboard';
import SubRow from './Content/SubRow';
import Content from './Content/Content';
import MultiMessage from './MultiMessage';
import HoverButtons from './HoverButtons';
import SiblingSwitch from './SiblingSwitch';
import { fetchById } from '~/utils/fetchers';
import getIcon from '~/utils/getIcon';
import { useMessageHandler } from '~/utils/handleSubmit';
import { useGetConversationByIdQuery } from '~/data-provider';
import { cn } from '~/utils/';
import store from '~/store';
export default function Message({
@ -23,17 +22,17 @@ export default function Message({
siblingCount,
setSiblingIdx
}) {
const { text, searchResult, isCreatedByUser, error, submitting } = message;
const isSubmitting = useRecoilValue(store.isSubmitting);
const setLatestMessage = useSetRecoilState(store.latestMessage);
// const { model, chatGptLabel, promptPrefix } = conversation;
const [abortScroll, setAbort] = useState(false);
const { text, searchResult, isCreatedByUser, error, submitting } = message;
const textEditor = useRef(null);
const last = !message?.children?.length;
const edit = message.messageId == currentEditId;
const { ask, regenerate } = useMessageHandler();
const { switchToConversation } = store.useConversation();
const blinker = submitting && isSubmitting;
const getConversationQuery = useGetConversationByIdQuery(message.conversationId, { enabled: false });
useEffect(() => {
if (blinker && !abortScroll) {
@ -99,10 +98,9 @@ export default function Message({
const clickSearchResult = async () => {
if (!searchResult) return;
const convoResponse = await fetchById('convos', message.conversationId);
const convo = convoResponse.data;
switchToConversation(convo);
getConversationQuery.refetch(message.conversationId).then((response) => {
switchToConversation(response.data);
});
};
return (

View file

@ -1,27 +1,27 @@
import React from 'react';
import { useEffect } from 'react';
import store from '~/store';
import TrashIcon from '../svg/TrashIcon';
import { useSWRConfig } from 'swr';
import manualSWR from '~/utils/fetchers';
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
import DialogTemplate from '../ui/DialogTemplate';
import { useClearConversationsMutation } from '~/data-provider';
export default function ClearConvos() {
const { newConversation } = store.useConversation();
const { refreshConversations } = store.useConversations();
const { mutate } = useSWRConfig();
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
newConversation();
refreshConversations();
mutate(`/api/convos`);
});
const clearConvosMutation = useClearConversationsMutation();
const clickHandler = () => {
console.log('Clearing conversations...');
trigger({});
clearConvosMutation.mutate();
};
useEffect(() => {
if (clearConvosMutation.isSuccess) {
newConversation();
refreshConversations();
}
}, [clearConvosMutation.isSuccess]);
return (
<Dialog>
<DialogTrigger asChild>

View file

@ -1,17 +1,14 @@
import React from 'react';
import SearchBar from './SearchBar';
import ClearConvos from './ClearConvos';
import DarkMode from './DarkMode';
import Logout from './Logout';
import ExportConversation from './ExportConversation';
export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) {
export default function NavLinks({ clearSearch, isSearchEnabled }) {
return (
<>
{!!isSearchEnabled && (
<SearchBar
fetch={fetch}
onSuccess={onSearchSuccess}
clearSearch={clearSearch}
/>
)}

View file

@ -1,66 +1,29 @@
import React, { useCallback, useEffect, useState } from 'react';
import { debounce } from 'lodash';
import { Search } from 'lucide-react';
import { useRecoilState } from 'recoil';
import store from '~/store';
export default function SearchBar({ fetch, clearSearch }) {
// const dispatch = useDispatch();
const [inputValue, setInputValue] = useState('');
export default function SearchBar({ clearSearch }) {
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
// const [inputValue, setInputValue] = useState('');
const debouncedChangeHandler = useCallback(
debounce(q => {
setSearchQuery(q);
}, 750),
[setSearchQuery]
);
useEffect(() => {
if (searchQuery.length > 0) {
fetch(searchQuery, 1);
setInputValue(searchQuery);
}
}, [searchQuery]);
const handleKeyUp = e => {
const { value } = e.target;
if (e.keyCode === 8 && value === '') {
// Value after clearing input: ""
console.log(`Value after clearing input: "${value}"`);
setSearchQuery('');
clearSearch();
}
};
const changeHandler = e => {
let q = e.target.value;
setInputValue(q);
q = q.trim();
if (q === '') {
setSearchQuery('');
clearSearch();
} else {
debouncedChangeHandler(q);
}
};
return (
<div className="flex cursor-pointer items-center gap-3 rounded-md py-3 px-3 text-sm text-white transition-colors duration-200 hover:bg-gray-500/10">
{<Search className="h-4 w-4" />}
<input
// ref={inputRef}
type="text"
className="m-0 mr-0 w-full border-none bg-transparent p-0 text-sm leading-tight outline-none"
value={inputValue}
onChange={changeHandler}
value={searchQuery}
onChange={e => setSearchQuery(e.target.value)}
placeholder="Search messages"
onKeyUp={handleKeyUp}
// onBlur={onRename}
/>
</div>
);

View file

@ -1,13 +1,12 @@
import React, { useState, useEffect, useRef, useCallback } from 'react';
import _ from 'lodash';
import { useState, useEffect, useRef } from 'react';
import NewChat from './NewChat';
import Spinner from '../svg/Spinner';
import Pages from '../Conversations/Pages';
import Conversations from '../Conversations';
import NavLinks from './NavLinks';
import { searchFetcher, swr } from '~/utils/fetchers';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useGetConversationsQuery, useSearchQuery } from '~/data-provider';
import useDebounce from '~/hooks/useDebounce';
import store from '~/store';
export default function Nav({ navVisible, setNavVisible }) {
@ -16,12 +15,14 @@ export default function Nav({ navVisible, setNavVisible }) {
const containerRef = useRef(null);
const scrollPositionRef = useRef(null);
// const dispatch = useDispatch();
const [conversations, setConversations] = useState([]);
// current page
const [pageNumber, setPageNumber] = useState(1);
// total pages
const [pages, setPages] = useState(1);
// data provider
const getConversationsQuery = useGetConversationsQuery(pageNumber);
// search
const searchQuery = useRecoilValue(store.searchQuery);
@ -33,29 +34,18 @@ export default function Nav({ navVisible, setNavVisible }) {
const conversation = useRecoilValue(store.conversation);
const { conversationId } = conversation || {};
const setSearchResultMessages = useSetRecoilState(store.searchResultMessages);
// refreshConversationsHint is used for other components to ask refresh of Nav
const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint);
const { refreshConversations } = store.useConversations();
const [isFetching, setIsFetching] = useState(false);
const onSuccess = (data, searchFetch = false) => {
if (isSearching) {
return;
}
let { conversations, pages } = data;
if (pageNumber > pages) {
setPageNumber(pages);
} else {
if (!searchFetch)
conversations = conversations.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
setConversations(conversations);
setPages(pages);
}
};
const debouncedSearchTerm = useDebounce(searchQuery, 750);
const searchQueryFn = useSearchQuery(debouncedSearchTerm, pageNumber, {
enabled: !!debouncedSearchTerm &&
debouncedSearchTerm.length > 0 &&
isSearchEnabled &&
isSearching,
});
const onSearchSuccess = (data, expectedPage) => {
const res = data;
@ -63,21 +53,21 @@ export default function Nav({ navVisible, setNavVisible }) {
if (expectedPage) {
setPageNumber(expectedPage);
}
setPageNumber(res.pageNumber);
setPages(res.pages);
setIsFetching(false);
searchPlaceholderConversation();
setSearchResultMessages(res.messages);
};
// TODO: dont need this
const fetch = useCallback(
_.partialRight(
searchFetcher.bind(null, () => setIsFetching(true)),
onSearchSuccess
),
[setIsFetching]
);
useEffect(() => {
//we use isInitialLoading here instead of isLoading because query is disabled by default
if (searchQueryFn.isInitialLoading) {
setIsFetching(true);
}
else if (searchQueryFn.data) {
onSearchSuccess(searchQueryFn.data);
}
}, [searchQueryFn.data, searchQueryFn.isInitialLoading])
const clearSearch = () => {
setPageNumber(1);
@ -85,38 +75,39 @@ export default function Nav({ navVisible, setNavVisible }) {
if (conversationId == 'search') {
newConversation();
}
// dispatch(setDisabled(false));
};
const { data, isLoading, mutate } = swr(`/api/convos?pageNumber=${pageNumber}`, onSuccess, {
revalidateOnMount: false
});
const nextPage = async () => {
moveToTop();
if (!isSearching) {
setPageNumber(prev => prev + 1);
await mutate();
} else {
await fetch(searchQuery, +pageNumber + 1);
}
setPageNumber(pageNumber + 1);
};
const previousPage = async () => {
moveToTop();
if (!isSearching) {
setPageNumber(prev => prev - 1);
await mutate();
} else {
await fetch(searchQuery, +pageNumber - 1);
}
setPageNumber(pageNumber - 1);
};
useEffect(() => {
if (getConversationsQuery.data) {
if (isSearching) {
return;
}
let { conversations, pages } = getConversationsQuery.data;
if (pageNumber > pages) {
setPageNumber(pages);
} else {
if (!isSearching) {
conversations = conversations.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
setConversations(conversations);
setPages(pages);
}
}
}, [getConversationsQuery.isSuccess, getConversationsQuery.data, isSearching, pageNumber]);
useEffect(() => {
if (!isSearching) {
mutate();
getConversationsQuery.refetch();
}
}, [pageNumber, conversationId, refreshConversationsHint]);
@ -127,31 +118,17 @@ export default function Nav({ navVisible, setNavVisible }) {
}
};
const moveTo = () => {
const container = containerRef.current;
if (container && scrollPositionRef.current !== null) {
const { scrollHeight, clientHeight } = container;
const maxScrollTop = scrollHeight - clientHeight;
container.scrollTop = Math.min(maxScrollTop, scrollPositionRef.current);
}
};
const toggleNavVisible = () => {
setNavVisible(prev => !prev);
};
// useEffect(() => {
// moveTo();
// }, [data]);
useEffect(() => {
setNavVisible(false);
}, [conversationId]);
}, [conversationId, setNavVisible]);
const containerClasses =
isLoading && pageNumber === 1
getConversationsQuery.isLoading && pageNumber === 1
? 'flex flex-col gap-2 text-gray-100 text-sm h-full justify-center items-center'
: 'flex flex-col gap-2 text-gray-100 text-sm';
@ -176,8 +153,7 @@ export default function Nav({ navVisible, setNavVisible }) {
ref={containerRef}
>
<div className={containerClasses}>
{/* {(isLoading && pageNumber === 1) ? ( */}
{(isLoading && pageNumber === 1) || isFetching ? (
{(getConversationsQuery.isLoading && pageNumber === 1) || isFetching ? (
<Spinner />
) : (
<Conversations
@ -195,8 +171,6 @@ export default function Nav({ navVisible, setNavVisible }) {
</div>
</div>
<NavLinks
fetch={fetch}
onSearchSuccess={onSearchSuccess}
clearSearch={clearSearch}
isSearchEnabled={isSearchEnabled}
/>

View file

@ -3,7 +3,7 @@ import CheckMark from '../svg/CheckMark.jsx';
import { Listbox, Transition } from '@headlessui/react';
import { cn } from '~/utils/';
function SelectDropdown({
function SelectDropDown({
title = 'Model',
value,
disabled,
@ -111,4 +111,4 @@ function SelectDropdown({
);
}
export default SelectDropdown;
export default SelectDropDown;

View file

@ -0,0 +1,47 @@
export const user = () => {
return `/api/me`;
};
export const messages = (id: string) => {
return `/api/messages/${id}`;
};
export const conversations = (pageNumber: string) => {
return `/api/convos?pageNumber=${pageNumber}`;
};
export const conversationById = (id: string) => {
return `/api/convos/${id}`;
};
export const updateConversation = () => {
return `/api/convos/update`;
};
export const deleteConversation = () => {
return `/api/convos/clear`;
};
export const search = (q: string, pageNumber: string) => {
return `/api/search?q=${q}&pageNumber=${pageNumber}`;
}
export const searchEnabled = () => {
return `/api/search/enable`;
}
export const presets = () => {
return `/api/presets`;
}
export const deletePreset = () => {
return `/api/presets/delete`;
}
export const aiEndpoints = () => {
return `/api/endpoints`;
}
export const tokenizer = () => {
return `/api/tokenizer`;
}

View file

@ -1,4 +1,6 @@
export default function createPayload(submission) {
import type { TSubmission } from './types';
export default function createPayload(submission: TSubmission) {
const { conversation, message, endpointOption } = submission;
const { conversationId } = conversation;
const { endpoint } = endpointOption;

View file

@ -0,0 +1,66 @@
import * as t from './types';
import request from './request';
import * as endpoints from './api-endpoints';
export function getConversations(pageNumber: string): Promise<t.TGetConversationsResponse> {
return request.get(endpoints.conversations(pageNumber));
}
export function deleteConversation(payload: t.TDeleteConversationRequest) {
//todo: this should be a DELETE request
return request.post(endpoints.deleteConversation(), {arg: payload});
}
export function clearAllConversations(): Promise<unknown> {
return request.post(endpoints.deleteConversation(), {arg: {}});
}
export function getMessagesByConvoId(id: string): Promise<t.TMessage[]> {
return request.get(endpoints.messages(id));
}
export function getConversationById(id: string): Promise<t.TConversation> {
return request.get(endpoints.conversationById(id));
}
export function updateConversation(
payload: t.TUpdateConversationRequest
): Promise<t.TUpdateConversationResponse> {
return request.post(endpoints.updateConversation(), {arg: payload});
}
export function getPresets(): Promise<t.TPreset[]> {
return request.get(endpoints.presets());
}
export function createPreset(payload: t.TPreset): Promise<t.TPreset[]> {
return request.post(endpoints.presets(), payload);
}
export function updatePreset(payload: t.TPreset): Promise<t.TPreset[]> {
return request.post(endpoints.presets(), payload);
}
export function deletePreset(arg: t.TPreset | {}): Promise<t.TPreset[]> {
return request.post(endpoints.deletePreset(), arg);
}
export function getSearchEnabled(): Promise<boolean> {
return request.get(endpoints.searchEnabled());
}
export function getUser(): Promise<t.TUser> {
return request.get(endpoints.user());
}
export const searchConversations = async(q: string, pageNumber: string): Promise<t.TSearchResults> => {
return request.get(endpoints.search(q, pageNumber));
}
export const getAIEndpoints = () => {
return request.get(endpoints.aiEndpoints());
}
export const updateTokenCount = (text: string) => {
return request.post(endpoints.tokenizer(), {arg: {text}});
}

View file

@ -0,0 +1,9 @@
import axios from 'axios';
export function setAcceptLanguageHeader(value: string): void {
axios.defaults.headers.common['Accept-Language'] = value;
}
export function setTokenHeader(token: string) {
axios.defaults.headers.common['Authorization'] = 'Bearer ' + token;
}

View file

@ -0,0 +1,7 @@
export * from './data-service';
// export * from './endpoints';
export * from './request';
export * from './types';
export * from './react-query-service';
export * from './headers-helpers';
// export * from './events';

View file

@ -0,0 +1,224 @@
import {
UseQueryOptions,
useQuery,
useMutation,
useQueryClient,
UseMutationResult,
QueryObserverResult,
} from "@tanstack/react-query";
import * as t from "./types";
import * as dataService from "./data-service";
export enum QueryKeys {
messages = "messsages",
allConversations = "allConversations",
conversation = "conversation",
searchEnabled = "searchEnabled",
user = "user",
endpoints = "endpoints",
presets = "presets",
searchResults = "searchResults",
tokenCount = "tokenCount",
}
export const useGetUserQuery = (): QueryObserverResult<t.TUser> => {
return useQuery<t.TUser>([QueryKeys.user], () => dataService.getUser(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
});
};
export const useGetMessagesByConvoId = (
id: string,
config?: UseQueryOptions<t.TMessage[]>
): QueryObserverResult<t.TMessage[]> => {
return useQuery<t.TMessage[]>([QueryKeys.messages, id], () =>
dataService.getMessagesByConvoId(id),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
}
);
};
export const useGetConversationByIdQuery = (
id: string,
config?: UseQueryOptions<t.TConversation>
): QueryObserverResult<t.TConversation> => {
return useQuery<t.TConversation>([QueryKeys.conversation, id], () =>
dataService.getConversationById(id),
{
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config
}
);
}
//This isn't ideal because its just a query and we're using mutation, but it was the only way
//to make it work with how the Chat component is structured
export const useGetConversationByIdMutation = (
id: string,
callback: (data: t.TConversation) => void
): UseMutationResult<t.TConversation> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.getConversationById(id),
{
onSuccess: (res: t.TConversation) => {
callback(res);
queryClient.invalidateQueries([QueryKeys.conversation, id]);
},
}
);
};
export const useUpdateConversationMutation = (
id: string
): UseMutationResult<t.TUpdateConversationResponse, unknown, t.TUpdateConversationRequest, unknown> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TUpdateConversationRequest) =>
dataService.updateConversation(payload),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.conversation, id]);
queryClient.invalidateQueries([QueryKeys.allConversations]);
},
}
);
};
export const useDeleteConversationMutation = (
id?: string
): UseMutationResult<t.TDeleteConversationResponse, unknown, t.TDeleteConversationRequest, unknown> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TDeleteConversationRequest) =>
dataService.deleteConversation(payload),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.conversation, id]);
queryClient.invalidateQueries([QueryKeys.allConversations]);
},
}
);
};
export const useClearConversationsMutation = (): UseMutationResult<unknown> => {
const queryClient = useQueryClient();
return useMutation(() => dataService.clearAllConversations(), {
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.allConversations]);
},
});
};
export const useGetConversationsQuery = (pageNumber: string): QueryObserverResult<t.TConversation[]> => {
return useQuery([QueryKeys.allConversations, pageNumber], () =>
dataService.getConversations(pageNumber), {
refetchOnReconnect: false,
refetchOnMount: false,
}
);
}
export const useGetSearchEnabledQuery = (config?: UseQueryOptions<boolean>): QueryObserverResult<boolean> => {
return useQuery<boolean>([QueryKeys.searchEnabled], () =>
dataService.getSearchEnabled(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config,
}
);
}
export const useGetEndpointsQuery = (): QueryObserverResult<t.TEndpoints> => {
return useQuery([QueryKeys.endpoints], () =>
dataService.getAIEndpoints(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
}
);
}
export const useCreatePresetMutation = (): UseMutationResult<t.TPreset[], unknown, t.TPreset, unknown> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TPreset) =>
dataService.createPreset(payload),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
}
);
};
export const useUpdatePresetMutation = (): UseMutationResult<t.TPreset[], unknown, t.TPreset, unknown> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TPreset) =>
dataService.updatePreset(payload),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
}
);
};
export const useGetPresetsQuery = (): QueryObserverResult<t.TPreset[], unknown> => {
return useQuery([QueryKeys.presets], () => dataService.getPresets(), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
});
};
export const useDeletePresetMutation = (): UseMutationResult<t.TPreset[], unknown, t.TPreset | {}, unknown> => {
const queryClient = useQueryClient();
return useMutation(
(payload: t.TPreset | {}) =>
dataService.deletePreset(payload),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.presets]);
},
}
);
}
export const useSearchQuery = (
searchQuery: string,
pageNumber: string,
config?: UseQueryOptions<t.TSearchResults>
): QueryObserverResult<t.TSearchResults> => {
return useQuery<t.TSearchResults>([QueryKeys.searchResults, pageNumber, searchQuery], () =>
dataService.searchConversations(searchQuery, pageNumber), {
refetchOnWindowFocus: false,
refetchOnReconnect: false,
refetchOnMount: false,
...config
}
);
}
export const useUpdateTokenCountMutation = (): UseMutationResult<t.TUpdateTokenCountResponse, unknown, string, unknown> => {
const queryClient = useQueryClient();
return useMutation(
(text: string) =>
dataService.updateTokenCount(text),
{
onSuccess: () => {
queryClient.invalidateQueries([QueryKeys.tokenCount]);
},
}
);
}

View file

@ -0,0 +1,62 @@
import axios, { AxiosRequestConfig } from "axios";
async function _get<T>(url: string, options?: AxiosRequestConfig): Promise<T> {
const response = await axios.get(url, { withCredentials: true, ...options});
return response.data;
}
async function _post(url: string, data?: any) {
const response = await axios.post(url, JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
return response.data;
}
async function _postMultiPart(
url: string,
formData: FormData,
options?: AxiosRequestConfig
) {
const response = await axios.post(url, formData, {
...options,
headers: { "Content-Type": "multipart/form-data" },
});
return response.data;
}
async function _put(url: string, data?: any) {
const response = await axios.put(url, JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
return response.data;
}
async function _delete<T>(url: string): Promise<T> {
const response = await axios.delete(url);
return response.data;
}
async function _deleteWithOptions<T>(
url: string,
options?: AxiosRequestConfig
): Promise<T> {
const response = await axios.delete(url, {...options});
return response.data;
}
async function _patch(url: string, data?: any) {
const response = await axios.patch(url, JSON.stringify(data), {
headers: { "Content-Type": "application/json" },
});
return response.data;
}
export default {
get: _get,
post: _post,
postMultiPart: _postMultiPart,
put: _put,
delete: _delete,
deleteWithOptions: _deleteWithOptions,
patch: _patch,
};

View file

@ -0,0 +1,163 @@
export type TMessage = {
messageId: string,
conversationId: string,
clientId: string,
parentMessageId: string,
sender: string,
text: string,
isCreatedByUser: boolean,
error: boolean,
createdAt: string,
updatedAt: string,
};
export type TSubmission = {
clientId?: string;
context?: string;
conversationId?: string;
conversationSignature?: string;
current: boolean;
endpoint: EModelEndpoint;
invocationId: number;
isCreatedByUser: boolean;
jailbreak: boolean;
jailbreakConversationId?: string;
messageId: string;
overrideParentMessageId?: string | boolean;
parentMessageId?: string;
sender: string;
systemMessage?: string;
text: string;
toneStyle?: string;
model?: string;
promptPrefix?: string;
temperature?: number;
top_p?: number;
presence_penalty?: number;
frequence_penalty?: number;
}
export enum EModelEndpoint {
azureOpenAI = 'azureOpenAI',
openAI = 'openAI',
bingAI = 'bingAI',
chatGPT = 'chatGPT',
chatGPTBrowser = 'chatGPTBrowser'
}
export type TConversation = {
conversationId: string;
title: string;
user?: string;
endpoint: EModelEndpoint;
suggestions?: string[];
messages?: TMessage[];
createdAt: string;
updatedAt: string;
// for azureOpenAI, openAI only
chatGptLabel?: string;
model?: string;
promptPrefix?: string;
temperature?: number;
top_p?: number;
presence_penalty?: number;
// for bingAI only
jailbreak?: boolean;
jailbreakConversationId?: string;
conversationSignature?: string;
parentMessageId?: string;
clientId?: string;
invocationId?: string;
toneStyle?: string;
}
export type TPreset = {
title: string,
endpoint: EModelEndpoint,
conversationSignature?: string,
createdAt?: string,
updatedAt?: string,
presetId?: string,
user?: string,
// for azureOpenAI, openAI only
chatGptLabel?: string,
frequence_penalty?: number,
model?: string,
presence_penalty?: number,
promptPrefix?: string,
temperature?: number,
top_p?: number,
//for BingAI
clientId?: string,
invocationId?: number,
jailbreak?: boolean,
jailbreakPresetId?: string,
presetSignature?: string,
toneStyle?: string,
}
export type TUser = {
username: string,
display: string
};
export type TGetConversationsResponse = {
conversations: TConversation[],
pageNumber: string,
pageSize: string | number,
pages: string | number
};
export type TUpdateConversationRequest = {
conversationId: string,
title: string,
};
export type TUpdateConversationResponse = {
data: TConversation
};
export type TDeleteConversationRequest = {
conversationId?: string,
source?: string
}
export type TDeleteConversationResponse = {
acknowledged: boolean,
deletedCount: number,
messages: {
acknowledged: boolean,
deletedCount: number
}
};
export type TSearchResults = {
conversations: TConversation[],
messages: TMessage[],
pageNumber: string,
pageSize: string | number,
pages: string | number
filter: {}
};
export type TEndpoints = {
azureOpenAI: boolean,
bingAI: boolean,
ChatGptBrowser: {
availableModels: []
}
OpenAI: {
availableModels: []
}
};
export type TUpdateTokenCountResponse = {
count: number,
};
export type TMessageTreeNode = {}
export type TSearchMessage = {}
export type TSearchMessageTreeNode = {}

View file

@ -0,0 +1,22 @@
import { useState, useEffect } from 'react';
function useDebounce(value, delay) {
const [debouncedValue, setDebouncedValue] = useState(value);
useEffect(
() => {
const handler = setTimeout(() => {
setDebouncedValue(value);
}, delay);
return () => {
clearTimeout(handler);
};
},
[value, delay]
);
return debouncedValue;
}
export default useDebounce;

View file

@ -1,9 +1,6 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
// import { Provider } from 'react-redux';
// import { store } from './src/store';
import { RecoilRoot } from 'recoil';
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
import { ThemeProvider } from './hooks/ThemeContext';
import App from './App';
import './style.css';
@ -12,10 +9,14 @@ import './mobile.css';
const container = document.getElementById('root');
const root = createRoot(container);
const queryClient = new QueryClient();
root.render(
<RecoilRoot>
<ThemeProvider>
<App />
</ThemeProvider>
</RecoilRoot>
<QueryClientProvider client={queryClient}>
<RecoilRoot>
<ThemeProvider>
<App />
</ThemeProvider>
</RecoilRoot>
</QueryClientProvider>
);

View file

@ -1,4 +1,4 @@
import React, { useEffect } from 'react';
import { useEffect } from 'react';
import { useNavigate, useParams } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
@ -7,7 +7,7 @@ import Messages from '../components/Messages';
import TextChat from '../components/Input';
import store from '~/store';
import manualSWR from '~/utils/fetchers';
import { useGetMessagesByConvoId, useGetConversationByIdMutation } from '~/data-provider';
export default function Chat() {
const searchQuery = useRecoilValue(store.searchQuery);
@ -18,9 +18,9 @@ export default function Chat() {
const { conversationId } = useParams();
const navigate = useNavigate();
const { trigger: messagesTrigger } = manualSWR(`/api/messages/${conversation?.conversationId}`, 'get');
const { trigger: conversationTrigger } = manualSWR(`/api/convos/${conversationId}`, 'get');
//disabled by default, we only enable it when messagesTree is null
const messagesQuery = useGetMessagesByConvoId(conversationId, { enabled: false });
const getConversationMutation = useGetConversationByIdMutation(conversationId, setConversation);
// when conversation changed or conversationId (in url) changed
useEffect(() => {
@ -31,13 +31,7 @@ export default function Chat() {
newConversation();
} else if (conversationId) {
// fetch it from server
conversationTrigger()
.then(setConversation)
.catch(error => {
console.error('failed to fetch the conversation');
console.error(error);
newConversation();
});
getConversationMutation.mutate();
setMessages(null);
} else {
navigate(`/chat/new`);
@ -51,13 +45,30 @@ export default function Chat() {
}
}, [conversation, conversationId]);
// when messagesTree is null (<=> messages is null)
// we need to fetch message list from server
useEffect(() => {
if (messagesTree === null) {
messagesTrigger().then(setMessages);
if(getConversationMutation.isError) {
console.error('failed to fetch the conversation');
console.error(getConversationMutation.error);
newConversation();
}
}, [conversation?.conversationId]);
}, [getConversationMutation.isError, newConversation]);
useEffect(() => {
if (messagesTree === null && conversation?.conversationId) {
messagesQuery.refetch(conversation?.conversationId);
}
}, [conversation?.conversationId, messagesQuery, messagesTree]);
useEffect(() => {
if (messagesQuery.data) {
setMessages(messagesQuery.data);
} else if(messagesQuery.isError) {
console.error('failed to fetch the messages');
console.error(messagesQuery.error);
setMessages(null);
}
}, [messagesQuery.data, messagesQuery.isError, setMessages]);
// if not a conversation
if (conversation?.conversationId === 'search') return null;

View file

@ -1,96 +0,0 @@
/* eslint-disable react-hooks/rules-of-hooks */
import axios from 'axios';
import useSWR from 'swr';
import useSWRMutation from 'swr/mutation';
const fetcher = url => fetch(url, { credentials: 'include' }).then(res => res.json());
const axiosFetcher = async (url, params) => {
console.log(params, 'params');
return axios.get(url, params);
};
export const postRequest = async (url, { arg }) => {
return await axios({
method: 'post',
url: url,
withCredentials: true,
data: { arg }
});
};
export const axiosPost = async ({ url, arg, callback }) => {
try {
const response = await axios.post(url, { arg }, { withCredentials: true });
callback(response.data);
} catch (error) {
console.error('An error occurred while making the axios post request:', error);
}
};
export const searchFetcher = async (pre, q, pageNumber, callback) => {
pre();
const { data } = await axios.get(`/api/search?q=${q}&pageNumber=${pageNumber}`);
console.log('search data', data);
callback(data);
};
export const fetchById = async (path, conversationId) => {
return await axios.get(`/api/${path}/${conversationId}`);
// console.log(`fetch ${path} data`, data);
// callback(data);
};
export const swr = (path, successCallback, options) => {
const _options = { ...options };
if (successCallback) {
_options.onSuccess = successCallback;
}
return useSWR(path, fetcher, _options);
};
export default function manualSWR(path, type, successCallback) {
const options = {};
if (successCallback) {
options.onSuccess = successCallback;
}
const fetchFunction = type === 'get' ? fetcher : postRequest;
return useSWRMutation(path, fetchFunction, options);
}
export function useManualSWR({ path, params, type, onSuccess }) {
const options = {};
if (onSuccess) {
options.onSuccess = onSuccess;
}
console.log(params, 'params');
const fetchFunction = type === 'get' ? _.partialRight(axiosFetcher, params) : postRequest;
return useSWRMutation(path, fetchFunction, options);
}
export const handleFileSelected = async jsonData => {
try {
const response = await axios({
url: '/api/presets',
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
withCredentials: true,
data: JSON.stringify(jsonData)
});
// if (!response.ok) {
// throw new Error(`Error: ${response.statusText}`);
// }
console.log(response);
return response.data;
} catch (error) {
console.error('Error uploading the preset:', error);
}
};