mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-16 16:30:15 +01:00
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:
commit
0a80f836f0
35 changed files with 1562 additions and 686 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -57,3 +57,4 @@ src/style - official.css
|
|||
/e2e/specs/.test-results/
|
||||
/e2e/playwright-report/
|
||||
/playwright/.cache/
|
||||
.DS_Store
|
||||
|
|
@ -26,5 +26,6 @@ module.exports = {
|
|||
"rules": {
|
||||
'react/prop-types': ['off'],
|
||||
'react/display-name': ['off'],
|
||||
"no-debugger":"off",
|
||||
}
|
||||
}
|
||||
|
|
|
|||
844
client/package-lock.json
generated
844
client/package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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>;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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') {
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
</>
|
||||
|
|
|
|||
|
|
@ -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">
|
||||
|
|
|
|||
|
|
@ -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(() => {
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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 "
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
)}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
47
client/src/data-provider/api-endpoints.ts
Normal file
47
client/src/data-provider/api-endpoints.ts
Normal 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`;
|
||||
}
|
||||
|
|
@ -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;
|
||||
66
client/src/data-provider/data-service.ts
Normal file
66
client/src/data-provider/data-service.ts
Normal 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}});
|
||||
}
|
||||
9
client/src/data-provider/headers-helpers.ts
Normal file
9
client/src/data-provider/headers-helpers.ts
Normal 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;
|
||||
}
|
||||
7
client/src/data-provider/index.ts
Normal file
7
client/src/data-provider/index.ts
Normal 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';
|
||||
224
client/src/data-provider/react-query-service.ts
Normal file
224
client/src/data-provider/react-query-service.ts
Normal 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]);
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
62
client/src/data-provider/request.ts
Normal file
62
client/src/data-provider/request.ts
Normal 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,
|
||||
};
|
||||
163
client/src/data-provider/types.ts
Normal file
163
client/src/data-provider/types.ts
Normal 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 = {}
|
||||
22
client/src/hooks/useDebounce.js
Normal file
22
client/src/hooks/useDebounce.js
Normal 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;
|
||||
|
|
@ -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>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue