mirror of
https://github.com/danny-avila/LibreChat.git
synced 2025-12-18 09:20: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/specs/.test-results/
|
||||||
/e2e/playwright-report/
|
/e2e/playwright-report/
|
||||||
/playwright/.cache/
|
/playwright/.cache/
|
||||||
|
.DS_Store
|
||||||
|
|
@ -26,5 +26,6 @@ module.exports = {
|
||||||
"rules": {
|
"rules": {
|
||||||
'react/prop-types': ['off'],
|
'react/prop-types': ['off'],
|
||||||
'react/display-name': ['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-label": "^2.0.0",
|
||||||
"@radix-ui/react-slider": "^1.1.1",
|
"@radix-ui/react-slider": "^1.1.1",
|
||||||
"@radix-ui/react-tabs": "^1.0.3",
|
"@radix-ui/react-tabs": "^1.0.3",
|
||||||
|
"@tanstack/react-query": "^4.28.0",
|
||||||
"@types/jest": "^29.5.0",
|
"@types/jest": "^29.5.0",
|
||||||
"@types/node": "^18.15.10",
|
"@types/node": "^18.15.10",
|
||||||
"@types/react": "^18.0.30",
|
"@types/react": "^18.0.30",
|
||||||
|
|
@ -63,7 +64,6 @@
|
||||||
"remark-gfm": "^3.0.1",
|
"remark-gfm": "^3.0.1",
|
||||||
"remark-math": "^5.1.1",
|
"remark-math": "^5.1.1",
|
||||||
"remark-supersub": "^1.0.0",
|
"remark-supersub": "^1.0.0",
|
||||||
"swr": "^2.0.3",
|
|
||||||
"tailwind-merge": "^1.9.1",
|
"tailwind-merge": "^1.9.1",
|
||||||
"tailwindcss-animate": "^1.0.5",
|
"tailwindcss-animate": "^1.0.5",
|
||||||
"tailwindcss-radix": "^2.8.0",
|
"tailwindcss-radix": "^2.8.0",
|
||||||
|
|
@ -77,7 +77,13 @@
|
||||||
"@babel/plugin-transform-runtime": "^7.19.6",
|
"@babel/plugin-transform-runtime": "^7.19.6",
|
||||||
"@babel/preset-env": "^7.20.2",
|
"@babel/preset-env": "^7.20.2",
|
||||||
"@babel/preset-react": "^7.18.6",
|
"@babel/preset-react": "^7.18.6",
|
||||||
|
"@babel/preset-typescript": "^7.21.0",
|
||||||
"@babel/runtime": "^7.20.13",
|
"@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",
|
"@vitejs/plugin-react": "^3.1.0",
|
||||||
"autoprefixer": "^10.4.13",
|
"autoprefixer": "^10.4.13",
|
||||||
"babel-loader": "^9.1.2",
|
"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 { createBrowserRouter, RouterProvider, Navigate } from 'react-router-dom';
|
||||||
import Root from './routes/Root';
|
import Root from './routes/Root';
|
||||||
import Chat from './routes/Chat';
|
import Chat from './routes/Chat';
|
||||||
import Search from './routes/Search';
|
import Search from './routes/Search';
|
||||||
import store from './store';
|
import store from './store';
|
||||||
import userAuth from './utils/userAuth';
|
|
||||||
import { useRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
import { ScreenshotProvider } from './utils/screenshotContext.jsx';
|
import { ScreenshotProvider } from './utils/screenshotContext.jsx';
|
||||||
|
import { useGetSearchEnabledQuery, useGetUserQuery, useGetEndpointsQuery, useGetPresetsQuery} from '~/data-provider';
|
||||||
import axios from 'axios';
|
import {ReactQueryDevtools} from '@tanstack/react-query-devtools';
|
||||||
|
|
||||||
const router = createBrowserRouter([
|
const router = createBrowserRouter([
|
||||||
{
|
{
|
||||||
|
|
@ -43,58 +41,52 @@ const App = () => {
|
||||||
const setEndpointsConfig = useSetRecoilState(store.endpointsConfig);
|
const setEndpointsConfig = useSetRecoilState(store.endpointsConfig);
|
||||||
const setPresets = useSetRecoilState(store.presets);
|
const setPresets = useSetRecoilState(store.presets);
|
||||||
|
|
||||||
|
const searchEnabledQuery = useGetSearchEnabledQuery();
|
||||||
|
const userQuery = useGetUserQuery();
|
||||||
|
const endpointsQuery = useGetEndpointsQuery();
|
||||||
|
const presetsQuery = useGetPresetsQuery();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// fetch if seatch enabled
|
if(endpointsQuery.data) {
|
||||||
axios
|
setEndpointsConfig(endpointsQuery.data);
|
||||||
.get('/api/search/enable', {
|
} else if(endpointsQuery.isError) {
|
||||||
timeout: 1000,
|
console.error("Failed to get endpoints", endpointsQuery.error);
|
||||||
withCredentials: true
|
window.location.href = '/auth/login';
|
||||||
})
|
}
|
||||||
.then(res => {
|
}, [endpointsQuery.data, endpointsQuery.isError]);
|
||||||
setIsSearchEnabled(res.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
// fetch user
|
useEffect(() => {
|
||||||
userAuth()
|
if(presetsQuery.data) {
|
||||||
.then(user => setUser(user))
|
setPresets(presetsQuery.data);
|
||||||
.catch(err => console.log(err));
|
} else if(presetsQuery.isError) {
|
||||||
|
console.error("Failed to get presets", presetsQuery.error);
|
||||||
|
window.location.href = '/auth/login';
|
||||||
|
}
|
||||||
|
}, [presetsQuery.data, presetsQuery.isError]);
|
||||||
|
|
||||||
// fetch models
|
useEffect(() => {
|
||||||
axios
|
if (searchEnabledQuery.data) {
|
||||||
.get('/api/endpoints', {
|
setIsSearchEnabled(searchEnabledQuery.data);
|
||||||
timeout: 1000,
|
} else if(searchEnabledQuery.isError) {
|
||||||
withCredentials: true
|
console.error("Failed to get search enabled", searchEnabledQuery.error);
|
||||||
})
|
}
|
||||||
.then(({ data }) => {
|
}, [searchEnabledQuery.data, searchEnabledQuery.isError]);
|
||||||
setEndpointsConfig(data);
|
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
console.log('Not login!');
|
|
||||||
window.location.href = '/auth/login';
|
|
||||||
});
|
|
||||||
|
|
||||||
// fetch presets
|
useEffect(() => {
|
||||||
axios
|
if (userQuery.data) {
|
||||||
.get('/api/presets', {
|
setUser(userQuery.data);
|
||||||
timeout: 1000,
|
} else if(userQuery.isError) {
|
||||||
withCredentials: true
|
console.error("Failed to get user", userQuery.error);
|
||||||
})
|
window.location.href = '/auth/login';
|
||||||
.then(({ data }) => {
|
}
|
||||||
setPresets(data);
|
}, [userQuery.data, userQuery.isError]);
|
||||||
})
|
|
||||||
.catch(error => {
|
|
||||||
console.error(error);
|
|
||||||
console.log('Not login!');
|
|
||||||
window.location.href = '/auth/login';
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (user)
|
if (user)
|
||||||
return (
|
return (
|
||||||
<div>
|
<>
|
||||||
<RouterProvider router={router} />
|
<RouterProvider router={router} />
|
||||||
</div>
|
<ReactQueryDevtools initialIsOpen={false} />
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
else return <div className="flex h-screen"></div>;
|
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 { useRecoilState, useSetRecoilState } from 'recoil';
|
||||||
|
import { useUpdateConversationMutation } from '~/data-provider';
|
||||||
import RenameButton from './RenameButton';
|
import RenameButton from './RenameButton';
|
||||||
import DeleteButton from './DeleteButton';
|
import DeleteButton from './DeleteButton';
|
||||||
import ConvoIcon from '../svg/ConvoIcon';
|
import ConvoIcon from '../svg/ConvoIcon';
|
||||||
import manualSWR from '~/utils/fetchers';
|
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -15,6 +14,8 @@ export default function Conversation({ conversation, retainView }) {
|
||||||
const { refreshConversations } = store.useConversations();
|
const { refreshConversations } = store.useConversations();
|
||||||
const { switchToConversation } = store.useConversation();
|
const { switchToConversation } = store.useConversation();
|
||||||
|
|
||||||
|
const updateConvoMutation = useUpdateConversationMutation(currentConversation?.conversationId);
|
||||||
|
|
||||||
const [renaming, setRenaming] = useState(false);
|
const [renaming, setRenaming] = useState(false);
|
||||||
const inputRef = useRef(null);
|
const inputRef = useRef(null);
|
||||||
|
|
||||||
|
|
@ -22,8 +23,6 @@ export default function Conversation({ conversation, retainView }) {
|
||||||
|
|
||||||
const [titleInput, setTitleInput] = useState(title);
|
const [titleInput, setTitleInput] = useState(title);
|
||||||
|
|
||||||
const rename = manualSWR(`/api/convos/update`, 'post');
|
|
||||||
|
|
||||||
const clickHandler = async () => {
|
const clickHandler = async () => {
|
||||||
if (currentConversation?.conversationId === conversationId) {
|
if (currentConversation?.conversationId === conversationId) {
|
||||||
return;
|
return;
|
||||||
|
|
@ -59,15 +58,20 @@ export default function Conversation({ conversation, retainView }) {
|
||||||
if (titleInput === title) {
|
if (titleInput === title) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
rename.trigger({ conversationId, title: titleInput }).then(() => {
|
updateConvoMutation.mutate({ conversationId, title: titleInput });
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (updateConvoMutation.isSuccess) {
|
||||||
refreshConversations();
|
refreshConversations();
|
||||||
if (conversationId == currentConversation?.conversationId)
|
if (conversationId == currentConversation?.conversationId) {
|
||||||
setCurrentConversation(prevState => ({
|
setCurrentConversation(prevState => ({
|
||||||
...prevState,
|
...prevState,
|
||||||
title: titleInput
|
title: titleInput
|
||||||
}));
|
}));
|
||||||
});
|
}
|
||||||
};
|
}
|
||||||
|
}, [updateConvoMutation.isSuccess]);
|
||||||
|
|
||||||
const handleKeyDown = e => {
|
const handleKeyDown = e => {
|
||||||
if (e.key === 'Enter') {
|
if (e.key === 'Enter') {
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,8 @@
|
||||||
import React from 'react';
|
import { useEffect } from 'react';
|
||||||
import TrashIcon from '../svg/TrashIcon';
|
import TrashIcon from '../svg/TrashIcon';
|
||||||
import CrossIcon from '../svg/CrossIcon';
|
import CrossIcon from '../svg/CrossIcon';
|
||||||
import manualSWR from '~/utils/fetchers';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
|
import { useDeleteConversationMutation } from '~/data-provider';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
@ -10,13 +10,23 @@ export default function DeleteButton({ conversationId, renaming, cancelHandler,
|
||||||
const currentConversation = useRecoilValue(store.conversation) || {};
|
const currentConversation = useRecoilValue(store.conversation) || {};
|
||||||
const { newConversation } = store.useConversation();
|
const { newConversation } = store.useConversation();
|
||||||
const { refreshConversations } = store.useConversations();
|
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;
|
const handler = renaming ? cancelHandler : clickHandler;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,22 +1,15 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import { Input } from '~/components/ui/Input.tsx';
|
|
||||||
import { Label } from '~/components/ui/Label.tsx';
|
import { Label } from '~/components/ui/Label.tsx';
|
||||||
import { Checkbox } from '~/components/ui/Checkbox.tsx';
|
import { Checkbox } from '~/components/ui/Checkbox.tsx';
|
||||||
import SelectDropdown from '../../ui/SelectDropDown';
|
import SelectDropDown from '../../ui/SelectDropDown';
|
||||||
import { axiosPost } from '~/utils/fetchers.js';
|
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
import debounce from 'lodash/debounce';
|
import useDebounce from '~/hooks/useDebounce';
|
||||||
// import ModelDropDown from '../../ui/ModelDropDown';
|
import { useUpdateTokenCountMutation } from '~/data-provider';
|
||||||
// import { Slider } from '~/components/ui/Slider.tsx';
|
|
||||||
// import OptionHover from './OptionHover';
|
|
||||||
// import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
|
|
||||||
const defaultTextProps =
|
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';
|
'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) {
|
function Settings(props) {
|
||||||
const { readonly, context, systemMessage, jailbreak, toneStyle, setOption } = props;
|
const { readonly, context, systemMessage, jailbreak, toneStyle, setOption } = props;
|
||||||
const [tokenCount, setTokenCount] = useState(0);
|
const [tokenCount, setTokenCount] = useState(0);
|
||||||
|
|
@ -25,31 +18,26 @@ function Settings(props) {
|
||||||
const setSystemMessage = setOption('systemMessage');
|
const setSystemMessage = setOption('systemMessage');
|
||||||
const setJailbreak = setOption('jailbreak');
|
const setJailbreak = setOption('jailbreak');
|
||||||
const setToneStyle = value => setOption('toneStyle')(value.toLowerCase());
|
const setToneStyle = value => setOption('toneStyle')(value.toLowerCase());
|
||||||
|
const debouncedContext = useDebounce(context, 250);
|
||||||
// useEffect to update token count
|
const updateTokenCountMutation = useUpdateTokenCountMutation();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!context || context.trim() === '') {
|
if (!debouncedContext || debouncedContext.trim() === '') {
|
||||||
setTokenCount(0);
|
setTokenCount(0);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const debouncedPost = debounce(axiosPost, 250);
|
|
||||||
const handleTextChange = context => {
|
const handleTextChange = context => {
|
||||||
debouncedPost({
|
updateTokenCountMutation.mutate({ text: context }, {
|
||||||
url: '/api/tokenizer',
|
onSuccess: data => {
|
||||||
arg: { text: context },
|
|
||||||
callback: data => {
|
|
||||||
setTokenCount(data.count);
|
setTokenCount(data.count);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
handleTextChange(context);
|
handleTextChange(debouncedContext);
|
||||||
return () => debouncedPost.cancel();
|
}, [debouncedContext]);
|
||||||
}, [context]);
|
|
||||||
|
|
||||||
// console.log('data', data);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|
@ -62,7 +50,7 @@ function Settings(props) {
|
||||||
>
|
>
|
||||||
Tone Style <small className="opacity-40">(default: fast)</small>
|
Tone Style <small className="opacity-40">(default: fast)</small>
|
||||||
</Label>
|
</Label>
|
||||||
<SelectDropdown
|
<SelectDropDown
|
||||||
id="toneStyle-dropdown"
|
id="toneStyle-dropdown"
|
||||||
title={null}
|
title={null}
|
||||||
value={`${toneStyle.charAt(0).toUpperCase()}${toneStyle.slice(1)}`}
|
value={`${toneStyle.charAt(0).toUpperCase()}${toneStyle.slice(1)}`}
|
||||||
|
|
@ -151,14 +139,6 @@ function Settings(props) {
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{/* <HoverCard openDelay={300}>
|
|
||||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
|
||||||
</HoverCardTrigger>
|
|
||||||
<OptionHover
|
|
||||||
type="temp"
|
|
||||||
side="left"
|
|
||||||
/>
|
|
||||||
</HoverCard> */}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|
|
||||||
|
|
@ -1,12 +1,10 @@
|
||||||
import React from 'react';
|
|
||||||
import { useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import TextareaAutosize from 'react-textarea-autosize';
|
import TextareaAutosize from 'react-textarea-autosize';
|
||||||
import SelectDropdown from '../../ui/SelectDropDown';
|
import SelectDropDown from '../../ui/SelectDropDown';
|
||||||
import { Input } from '~/components/ui/Input.tsx';
|
import { Input } from '~/components/ui/Input.tsx';
|
||||||
import { Label } from '~/components/ui/Label.tsx';
|
import { Label } from '~/components/ui/Label.tsx';
|
||||||
import { Slider } from '~/components/ui/Slider.tsx';
|
import { Slider } from '~/components/ui/Slider.tsx';
|
||||||
import { InputNumber } from '~/components/ui/InputNumber.tsx';
|
import { InputNumber } from '~/components/ui/InputNumber.tsx';
|
||||||
// import { InputNumber } from '../../ui/InputNumber';
|
|
||||||
import OptionHover from './OptionHover';
|
import OptionHover from './OptionHover';
|
||||||
import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
|
import { HoverCard, HoverCardTrigger } from '~/components/ui/HoverCard.tsx';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
|
@ -38,7 +36,7 @@ function Settings(props) {
|
||||||
<div className="grid gap-6 sm:grid-cols-2">
|
<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="col-span-1 flex flex-col items-center justify-start gap-6">
|
||||||
<div className="grid w-full items-center gap-2">
|
<div className="grid w-full items-center gap-2">
|
||||||
<SelectDropdown
|
<SelectDropDown
|
||||||
value={model}
|
value={model}
|
||||||
setValue={setModel}
|
setValue={setModel}
|
||||||
availableValues={models}
|
availableValues={models}
|
||||||
|
|
@ -49,23 +47,6 @@ function Settings(props) {
|
||||||
)}
|
)}
|
||||||
containerClassName="flex w-full resize-none"
|
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>
|
||||||
<div className="grid w-full items-center gap-2">
|
<div className="grid w-full items-center gap-2">
|
||||||
<Label
|
<Label
|
||||||
|
|
@ -78,7 +59,6 @@ function Settings(props) {
|
||||||
id="chatGptLabel"
|
id="chatGptLabel"
|
||||||
disabled={readonly}
|
disabled={readonly}
|
||||||
value={chatGptLabel || ''}
|
value={chatGptLabel || ''}
|
||||||
// ref={inputRef}
|
|
||||||
onChange={e => setChatGptLabel(e.target.value || null)}
|
onChange={e => setChatGptLabel(e.target.value || null)}
|
||||||
placeholder="Set a custom name for ChatGPT"
|
placeholder="Set a custom name for ChatGPT"
|
||||||
className={cn(
|
className={cn(
|
||||||
|
|
@ -104,15 +84,6 @@ function Settings(props) {
|
||||||
defaultTextProps,
|
defaultTextProps,
|
||||||
'flex max-h-[300px] min-h-[100px] w-full resize-none px-3 py-2 '
|
'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>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -160,43 +131,6 @@ function Settings(props) {
|
||||||
side="left"
|
side="left"
|
||||||
/>
|
/>
|
||||||
</HoverCard>
|
</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}>
|
<HoverCard openDelay={300}>
|
||||||
<HoverCardTrigger className="grid w-full items-center gap-2">
|
<HoverCardTrigger className="grid w-full items-center gap-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex justify-between">
|
||||||
|
|
|
||||||
|
|
@ -1,19 +1,18 @@
|
||||||
import React, { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { useSetRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilValue } from 'recoil';
|
||||||
import axios from 'axios';
|
|
||||||
import DialogTemplate from '../ui/DialogTemplate';
|
import DialogTemplate from '../ui/DialogTemplate';
|
||||||
import { Dialog } from '../ui/Dialog.tsx';
|
import { Dialog } from '../ui/Dialog.tsx';
|
||||||
import { Input } from '../ui/Input.tsx';
|
import { Input } from '../ui/Input.tsx';
|
||||||
import { Label } from '../ui/Label.tsx';
|
import { Label } from '../ui/Label.tsx';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
import cleanupPreset from '~/utils/cleanupPreset';
|
import cleanupPreset from '~/utils/cleanupPreset';
|
||||||
|
import { useCreatePresetMutation } from '~/data-provider';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
|
const SaveAsPresetDialog = ({ open, onOpenChange, preset }) => {
|
||||||
const [title, setTitle] = useState(preset?.title || 'My Preset');
|
const [title, setTitle] = useState(preset?.title || 'My Preset');
|
||||||
const setPresets = useSetRecoilState(store.presets);
|
|
||||||
const endpointsFilter = useRecoilValue(store.endpointsFilter);
|
const endpointsFilter = useRecoilValue(store.endpointsFilter);
|
||||||
|
const createPresetMutation = useCreatePresetMutation();
|
||||||
|
|
||||||
const defaultTextProps =
|
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';
|
'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
|
endpointsFilter
|
||||||
});
|
});
|
||||||
|
createPresetMutation.mutate(_preset);
|
||||||
axios({
|
|
||||||
method: 'post',
|
|
||||||
url: '/api/presets',
|
|
||||||
data: _preset,
|
|
||||||
withCredentials: true
|
|
||||||
}).then(res => {
|
|
||||||
setPresets(res?.data);
|
|
||||||
});
|
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
import React, { useState, useEffect } from 'react';
|
import { useState } from 'react';
|
||||||
import { useRecoilValue, useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
import { cn } from '~/utils';
|
import { cn } from '~/utils';
|
||||||
import { Button } from '../../ui/Button.tsx';
|
import { Button } from '../../ui/Button.tsx';
|
||||||
import { Settings2 } from 'lucide-react';
|
import { Settings2 } from 'lucide-react';
|
||||||
import { Tabs, TabsList, TabsTrigger } from '../../ui/Tabs.tsx';
|
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 Settings from '../../Endpoints/BingAI/Settings.jsx';
|
||||||
import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
|
import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
|
||||||
import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
|
import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
|
||||||
|
|
@ -18,25 +18,12 @@ function BingAIOptions() {
|
||||||
const { endpoint, conversationId } = conversation;
|
const { endpoint, conversationId } = conversation;
|
||||||
const { toneStyle, context, systemMessage, jailbreak } = 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 (endpoint !== 'bingAI') return null;
|
||||||
if (conversationId !== 'new') return null;
|
if (conversationId !== 'new') return null;
|
||||||
|
|
||||||
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
|
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
|
||||||
|
|
||||||
const switchToSimpleMode = () => {
|
const switchToSimpleMode = () => {
|
||||||
// setConversation(prevState => ({
|
|
||||||
// ...prevState,
|
|
||||||
// context: null,
|
|
||||||
// systemMessage: null
|
|
||||||
// }));
|
|
||||||
setAdvancedMode(false);
|
setAdvancedMode(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -68,7 +55,7 @@ function BingAIOptions() {
|
||||||
(!advancedMode ? ' show' : '')
|
(!advancedMode ? ' show' : '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
<SelectDropdown
|
<SelectDropDown
|
||||||
title="Mode"
|
title="Mode"
|
||||||
value={jailbreak ? 'Sydney' : 'BingAI'}
|
value={jailbreak ? 'Sydney' : 'BingAI'}
|
||||||
setValue={value => setOption('jailbreak')(value === 'Sydney')}
|
setValue={value => setOption('jailbreak')(value === 'Sydney')}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
import React, { useEffect } from 'react';
|
import React, { useEffect } from 'react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import SelectDropdown from '../../ui/SelectDropDown.jsx';
|
import SelectDropDown from '../../ui/SelectDropDown';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
@ -12,21 +12,11 @@ function ChatGPTOptions() {
|
||||||
|
|
||||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// if (endpoint !== 'chatGPTBrowser') return;
|
|
||||||
// }, [conversation]);
|
|
||||||
|
|
||||||
if (endpoint !== 'chatGPTBrowser') return null;
|
if (endpoint !== 'chatGPTBrowser') return null;
|
||||||
if (conversationId !== 'new') return null;
|
if (conversationId !== 'new') return null;
|
||||||
|
|
||||||
const models = endpointsConfig?.['chatGPTBrowser']?.['availableModels'] || [];
|
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 => {
|
const setOption = param => newValue => {
|
||||||
let update = {};
|
let update = {};
|
||||||
update[param] = newValue;
|
update[param] = newValue;
|
||||||
|
|
@ -41,7 +31,7 @@ function ChatGPTOptions() {
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="openAIOptions-simple-container show flex w-full flex-wrap items-center justify-center gap-2">
|
<div className="openAIOptions-simple-container show flex w-full flex-wrap items-center justify-center gap-2">
|
||||||
<SelectDropdown
|
<SelectDropDown
|
||||||
value={model}
|
value={model}
|
||||||
setValue={setOption('model')}
|
setValue={setOption('model')}
|
||||||
availableValues={models}
|
availableValues={models}
|
||||||
|
|
|
||||||
|
|
@ -45,15 +45,6 @@ export default function PresetItem({ preset = {}, value, onSelect, onChangePrese
|
||||||
{icon}
|
{icon}
|
||||||
{preset?.title}
|
{preset?.title}
|
||||||
<small className="ml-2">({getPresetTitle()})</small>
|
<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" />
|
<div className="flex w-4 flex-1" />
|
||||||
<button
|
<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 "
|
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 { useRecoilValue, useRecoilState } from 'recoil';
|
||||||
import EditPresetDialog from '../../Endpoints/EditPresetDialog.jsx';
|
import EditPresetDialog from '../../Endpoints/EditPresetDialog';
|
||||||
import EndpointItems from './EndpointItems.jsx';
|
import EndpointItems from './EndpointItems';
|
||||||
import PresetItems from './PresetItems.jsx';
|
import PresetItems from './PresetItems';
|
||||||
import FileUpload from './FileUpload.jsx';
|
import FileUpload from './FileUpload';
|
||||||
import getIcon from '~/utils/getIcon';
|
import getIcon from '~/utils/getIcon';
|
||||||
import manualSWR, { handleFileSelected } from '~/utils/fetchers';
|
import { useDeletePresetMutation, useCreatePresetMutation } from '~/data-provider';
|
||||||
|
|
||||||
import { Button } from '../../ui/Button.tsx';
|
import { Button } from '../../ui/Button.tsx';
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
|
|
@ -17,33 +16,34 @@ import {
|
||||||
DropdownMenuTrigger
|
DropdownMenuTrigger
|
||||||
} from '../../ui/DropdownMenu.tsx';
|
} from '../../ui/DropdownMenu.tsx';
|
||||||
import { Dialog, DialogTrigger } from '../../ui/Dialog.tsx';
|
import { Dialog, DialogTrigger } from '../../ui/Dialog.tsx';
|
||||||
import DialogTemplate from '../../ui/DialogTemplate.jsx';
|
import DialogTemplate from '../../ui/DialogTemplate';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function NewConversationMenu() {
|
export default function NewConversationMenu() {
|
||||||
// const [modelSave, setModelSave] = useState(false);
|
|
||||||
const [menuOpen, setMenuOpen] = useState(false);
|
const [menuOpen, setMenuOpen] = useState(false);
|
||||||
const [presetModelVisible, setPresetModelVisible] = useState(false);
|
const [presetModelVisible, setPresetModelVisible] = useState(false);
|
||||||
const [preset, setPreset] = useState(false);
|
const [preset, setPreset] = useState(false);
|
||||||
|
|
||||||
// const models = useRecoilValue(store.models);
|
|
||||||
const availableEndpoints = useRecoilValue(store.availableEndpoints);
|
const availableEndpoints = useRecoilValue(store.availableEndpoints);
|
||||||
// const setCustomGPTModels = useSetRecoilState(store.customGPTModels);
|
|
||||||
const [presets, setPresets] = useRecoilState(store.presets);
|
const [presets, setPresets] = useRecoilState(store.presets);
|
||||||
|
|
||||||
const conversation = useRecoilValue(store.conversation) || {};
|
const conversation = useRecoilValue(store.conversation) || {};
|
||||||
const { endpoint, conversationId } = conversation;
|
const { endpoint, conversationId } = conversation;
|
||||||
// const { model, promptPrefix, chatGptLabel, conversationId } = conversation;
|
|
||||||
const { newConversation } = store.useConversation();
|
const { newConversation } = store.useConversation();
|
||||||
|
|
||||||
const { trigger: clearPresetsTrigger } = manualSWR(`/api/presets/delete`, 'post', res => {
|
const deletePresetsMutation = useDeletePresetMutation();
|
||||||
console.log(res);
|
const createPresetMutation = useCreatePresetMutation();
|
||||||
setPresets(res.data);
|
|
||||||
});
|
|
||||||
|
|
||||||
const importPreset = jsonData => {
|
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
|
// update the default model when availableModels changes
|
||||||
|
|
@ -65,7 +65,6 @@ export default function NewConversationMenu() {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
|
|
||||||
if (!newEndpoint) return;
|
if (!newEndpoint) return;
|
||||||
// else if (newEndpoint === endpoint) return;
|
|
||||||
else {
|
else {
|
||||||
newConversation({}, { endpoint: newEndpoint });
|
newConversation({}, { endpoint: newEndpoint });
|
||||||
}
|
}
|
||||||
|
|
@ -75,7 +74,6 @@ export default function NewConversationMenu() {
|
||||||
const onSelectPreset = newPreset => {
|
const onSelectPreset = newPreset => {
|
||||||
setMenuOpen(false);
|
setMenuOpen(false);
|
||||||
if (!newPreset) return;
|
if (!newPreset) return;
|
||||||
// else if (newEndpoint === endpoint) return;
|
|
||||||
else {
|
else {
|
||||||
newConversation({}, newPreset);
|
newConversation({}, newPreset);
|
||||||
}
|
}
|
||||||
|
|
@ -86,8 +84,12 @@ export default function NewConversationMenu() {
|
||||||
setPreset(preset);
|
setPreset(preset);
|
||||||
};
|
};
|
||||||
|
|
||||||
const clearPreset = () => {
|
const clearAllPresets = () => {
|
||||||
clearPresetsTrigger({});
|
deletePresetsMutation.mutate({arg: {}});
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDeletePreset = preset => {
|
||||||
|
deletePresetsMutation.mutate({arg: preset});
|
||||||
};
|
};
|
||||||
|
|
||||||
const icon = getIcon({
|
const icon = getIcon({
|
||||||
|
|
@ -99,9 +101,7 @@ export default function NewConversationMenu() {
|
||||||
});
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog
|
<Dialog>
|
||||||
// onOpenChange={onOpenChange}
|
|
||||||
>
|
|
||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
open={menuOpen}
|
open={menuOpen}
|
||||||
onOpenChange={setMenuOpen}
|
onOpenChange={setMenuOpen}
|
||||||
|
|
@ -109,7 +109,6 @@ export default function NewConversationMenu() {
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="outline"
|
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`}
|
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}
|
{icon}
|
||||||
|
|
@ -150,7 +149,6 @@ export default function NewConversationMenu() {
|
||||||
<Button
|
<Button
|
||||||
type="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"
|
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
|
Clear All
|
||||||
</Button>
|
</Button>
|
||||||
|
|
@ -159,7 +157,7 @@ export default function NewConversationMenu() {
|
||||||
title="Clear presets"
|
title="Clear presets"
|
||||||
description="Are you sure you want to clear all presets? This is irreversible."
|
description="Are you sure you want to clear all presets? This is irreversible."
|
||||||
selection={{
|
selection={{
|
||||||
selectHandler: clearPreset,
|
selectHandler: clearAllPresets,
|
||||||
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
selectClasses: 'bg-red-600 hover:bg-red-700 dark:hover:bg-red-800 text-white',
|
||||||
selectText: 'Clear'
|
selectText: 'Clear'
|
||||||
}}
|
}}
|
||||||
|
|
@ -176,7 +174,7 @@ export default function NewConversationMenu() {
|
||||||
presets={presets}
|
presets={presets}
|
||||||
onSelect={onSelectPreset}
|
onSelect={onSelectPreset}
|
||||||
onChangePreset={onChangePreset}
|
onChangePreset={onChangePreset}
|
||||||
onDeletePreset={clearPresetsTrigger}
|
onDeletePreset={onDeletePreset}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenuLabel className="dark:text-gray-300">No preset yet.</DropdownMenuLabel>
|
<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 { Settings2 } from 'lucide-react';
|
||||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||||
import SelectDropdown from '../../ui/SelectDropDown';
|
import SelectDropDown from '../../ui/SelectDropDown';
|
||||||
import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
|
import EndpointOptionsPopover from '../../Endpoints/EndpointOptionsPopover';
|
||||||
import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
|
import SaveAsPresetDialog from '../../Endpoints/SaveAsPresetDialog';
|
||||||
import { Button } from '../../ui/Button.tsx';
|
import { Button } from '../../ui/Button.tsx';
|
||||||
|
|
@ -21,20 +21,6 @@ function OpenAIOptions() {
|
||||||
|
|
||||||
const endpointsConfig = useRecoilValue(store.endpointsConfig);
|
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 (endpoint !== 'openAI') return null;
|
||||||
if (conversationId !== 'new') return null;
|
if (conversationId !== 'new') return null;
|
||||||
|
|
||||||
|
|
@ -43,15 +29,6 @@ function OpenAIOptions() {
|
||||||
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
|
const triggerAdvancedMode = () => setAdvancedMode(prev => !prev);
|
||||||
|
|
||||||
const switchToSimpleMode = () => {
|
const switchToSimpleMode = () => {
|
||||||
// setConversation(prevState => ({
|
|
||||||
// ...prevState,
|
|
||||||
// chatGptLabel: null,
|
|
||||||
// promptPrefix: null,
|
|
||||||
// temperature: 1,
|
|
||||||
// top_p: 1,
|
|
||||||
// presence_penalty: 0,
|
|
||||||
// frequency_penalty: 0
|
|
||||||
// }));
|
|
||||||
setAdvancedMode(false);
|
setAdvancedMode(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -79,17 +56,7 @@ function OpenAIOptions() {
|
||||||
(!advancedMode ? ' show' : '')
|
(!advancedMode ? ' show' : '')
|
||||||
}
|
}
|
||||||
>
|
>
|
||||||
{/* <ModelSelect
|
<SelectDropDown
|
||||||
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
|
|
||||||
value={model}
|
value={model}
|
||||||
setValue={setOption('model')}
|
setValue={setOption('model')}
|
||||||
availableValues={models}
|
availableValues={models}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
import { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
|
import { useRecoilValue, useResetRecoilState, useSetRecoilState } from 'recoil';
|
||||||
import { SSE } from '~/utils/sse.mjs';
|
import { SSE } from '~/data-provider/sse.mjs';
|
||||||
import createPayload from '~/utils/createPayload';
|
import createPayload from '~/data-provider/createPayload';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,15 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import { useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import copy from 'copy-to-clipboard';
|
import copy from 'copy-to-clipboard';
|
||||||
import SubRow from './Content/SubRow';
|
import SubRow from './Content/SubRow';
|
||||||
import Content from './Content/Content';
|
import Content from './Content/Content';
|
||||||
import MultiMessage from './MultiMessage';
|
import MultiMessage from './MultiMessage';
|
||||||
import HoverButtons from './HoverButtons';
|
import HoverButtons from './HoverButtons';
|
||||||
import SiblingSwitch from './SiblingSwitch';
|
import SiblingSwitch from './SiblingSwitch';
|
||||||
import { fetchById } from '~/utils/fetchers';
|
|
||||||
import getIcon from '~/utils/getIcon';
|
import getIcon from '~/utils/getIcon';
|
||||||
import { useMessageHandler } from '~/utils/handleSubmit';
|
import { useMessageHandler } from '~/utils/handleSubmit';
|
||||||
|
import { useGetConversationByIdQuery } from '~/data-provider';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function Message({
|
export default function Message({
|
||||||
|
|
@ -23,17 +22,17 @@ export default function Message({
|
||||||
siblingCount,
|
siblingCount,
|
||||||
setSiblingIdx
|
setSiblingIdx
|
||||||
}) {
|
}) {
|
||||||
|
const { text, searchResult, isCreatedByUser, error, submitting } = message;
|
||||||
const isSubmitting = useRecoilValue(store.isSubmitting);
|
const isSubmitting = useRecoilValue(store.isSubmitting);
|
||||||
const setLatestMessage = useSetRecoilState(store.latestMessage);
|
const setLatestMessage = useSetRecoilState(store.latestMessage);
|
||||||
// const { model, chatGptLabel, promptPrefix } = conversation;
|
|
||||||
const [abortScroll, setAbort] = useState(false);
|
const [abortScroll, setAbort] = useState(false);
|
||||||
const { text, searchResult, isCreatedByUser, error, submitting } = message;
|
|
||||||
const textEditor = useRef(null);
|
const textEditor = useRef(null);
|
||||||
const last = !message?.children?.length;
|
const last = !message?.children?.length;
|
||||||
const edit = message.messageId == currentEditId;
|
const edit = message.messageId == currentEditId;
|
||||||
const { ask, regenerate } = useMessageHandler();
|
const { ask, regenerate } = useMessageHandler();
|
||||||
const { switchToConversation } = store.useConversation();
|
const { switchToConversation } = store.useConversation();
|
||||||
const blinker = submitting && isSubmitting;
|
const blinker = submitting && isSubmitting;
|
||||||
|
const getConversationQuery = useGetConversationByIdQuery(message.conversationId, { enabled: false });
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (blinker && !abortScroll) {
|
if (blinker && !abortScroll) {
|
||||||
|
|
@ -99,10 +98,9 @@ export default function Message({
|
||||||
|
|
||||||
const clickSearchResult = async () => {
|
const clickSearchResult = async () => {
|
||||||
if (!searchResult) return;
|
if (!searchResult) return;
|
||||||
const convoResponse = await fetchById('convos', message.conversationId);
|
getConversationQuery.refetch(message.conversationId).then((response) => {
|
||||||
const convo = convoResponse.data;
|
switchToConversation(response.data);
|
||||||
|
});
|
||||||
switchToConversation(convo);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -1,27 +1,27 @@
|
||||||
import React from 'react';
|
import { useEffect } from 'react';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
import TrashIcon from '../svg/TrashIcon';
|
import TrashIcon from '../svg/TrashIcon';
|
||||||
import { useSWRConfig } from 'swr';
|
|
||||||
import manualSWR from '~/utils/fetchers';
|
|
||||||
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
|
import { Dialog, DialogTrigger } from '../ui/Dialog.tsx';
|
||||||
import DialogTemplate from '../ui/DialogTemplate';
|
import DialogTemplate from '../ui/DialogTemplate';
|
||||||
|
import { useClearConversationsMutation } from '~/data-provider';
|
||||||
|
|
||||||
export default function ClearConvos() {
|
export default function ClearConvos() {
|
||||||
const { newConversation } = store.useConversation();
|
const { newConversation } = store.useConversation();
|
||||||
const { refreshConversations } = store.useConversations();
|
const { refreshConversations } = store.useConversations();
|
||||||
const { mutate } = useSWRConfig();
|
const clearConvosMutation = useClearConversationsMutation();
|
||||||
|
|
||||||
const { trigger } = manualSWR(`/api/convos/clear`, 'post', () => {
|
|
||||||
newConversation();
|
|
||||||
refreshConversations();
|
|
||||||
mutate(`/api/convos`);
|
|
||||||
});
|
|
||||||
|
|
||||||
const clickHandler = () => {
|
const clickHandler = () => {
|
||||||
console.log('Clearing conversations...');
|
console.log('Clearing conversations...');
|
||||||
trigger({});
|
clearConvosMutation.mutate();
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (clearConvosMutation.isSuccess) {
|
||||||
|
newConversation();
|
||||||
|
refreshConversations();
|
||||||
|
}
|
||||||
|
}, [clearConvosMutation.isSuccess]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
|
|
|
||||||
|
|
@ -1,17 +1,14 @@
|
||||||
import React from 'react';
|
|
||||||
import SearchBar from './SearchBar';
|
import SearchBar from './SearchBar';
|
||||||
import ClearConvos from './ClearConvos';
|
import ClearConvos from './ClearConvos';
|
||||||
import DarkMode from './DarkMode';
|
import DarkMode from './DarkMode';
|
||||||
import Logout from './Logout';
|
import Logout from './Logout';
|
||||||
import ExportConversation from './ExportConversation';
|
import ExportConversation from './ExportConversation';
|
||||||
|
|
||||||
export default function NavLinks({ fetch, onSearchSuccess, clearSearch, isSearchEnabled }) {
|
export default function NavLinks({ clearSearch, isSearchEnabled }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!!isSearchEnabled && (
|
{!!isSearchEnabled && (
|
||||||
<SearchBar
|
<SearchBar
|
||||||
fetch={fetch}
|
|
||||||
onSuccess={onSearchSuccess}
|
|
||||||
clearSearch={clearSearch}
|
clearSearch={clearSearch}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
|
|
||||||
|
|
@ -1,66 +1,29 @@
|
||||||
import React, { useCallback, useEffect, useState } from 'react';
|
|
||||||
import { debounce } from 'lodash';
|
|
||||||
import { Search } from 'lucide-react';
|
import { Search } from 'lucide-react';
|
||||||
import { useRecoilState } from 'recoil';
|
import { useRecoilState } from 'recoil';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function SearchBar({ fetch, clearSearch }) {
|
export default function SearchBar({ clearSearch }) {
|
||||||
// const dispatch = useDispatch();
|
|
||||||
const [inputValue, setInputValue] = useState('');
|
|
||||||
const [searchQuery, setSearchQuery] = useRecoilState(store.searchQuery);
|
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 handleKeyUp = e => {
|
||||||
const { value } = e.target;
|
const { value } = e.target;
|
||||||
if (e.keyCode === 8 && value === '') {
|
if (e.keyCode === 8 && value === '') {
|
||||||
// Value after clearing input: ""
|
|
||||||
console.log(`Value after clearing input: "${value}"`);
|
|
||||||
setSearchQuery('');
|
setSearchQuery('');
|
||||||
clearSearch();
|
clearSearch();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const changeHandler = e => {
|
|
||||||
let q = e.target.value;
|
|
||||||
setInputValue(q);
|
|
||||||
q = q.trim();
|
|
||||||
|
|
||||||
if (q === '') {
|
|
||||||
setSearchQuery('');
|
|
||||||
clearSearch();
|
|
||||||
} else {
|
|
||||||
debouncedChangeHandler(q);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
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">
|
<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" />}
|
{<Search className="h-4 w-4" />}
|
||||||
<input
|
<input
|
||||||
// ref={inputRef}
|
|
||||||
type="text"
|
type="text"
|
||||||
className="m-0 mr-0 w-full border-none bg-transparent p-0 text-sm leading-tight outline-none"
|
className="m-0 mr-0 w-full border-none bg-transparent p-0 text-sm leading-tight outline-none"
|
||||||
value={inputValue}
|
value={searchQuery}
|
||||||
onChange={changeHandler}
|
onChange={e => setSearchQuery(e.target.value)}
|
||||||
placeholder="Search messages"
|
placeholder="Search messages"
|
||||||
onKeyUp={handleKeyUp}
|
onKeyUp={handleKeyUp}
|
||||||
// onBlur={onRename}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,13 +1,12 @@
|
||||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
import { useState, useEffect, useRef } from 'react';
|
||||||
import _ from 'lodash';
|
|
||||||
import NewChat from './NewChat';
|
import NewChat from './NewChat';
|
||||||
import Spinner from '../svg/Spinner';
|
import Spinner from '../svg/Spinner';
|
||||||
import Pages from '../Conversations/Pages';
|
import Pages from '../Conversations/Pages';
|
||||||
import Conversations from '../Conversations';
|
import Conversations from '../Conversations';
|
||||||
import NavLinks from './NavLinks';
|
import NavLinks from './NavLinks';
|
||||||
import { searchFetcher, swr } from '~/utils/fetchers';
|
import { useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useGetConversationsQuery, useSearchQuery } from '~/data-provider';
|
||||||
|
import useDebounce from '~/hooks/useDebounce';
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
|
|
||||||
export default function Nav({ navVisible, setNavVisible }) {
|
export default function Nav({ navVisible, setNavVisible }) {
|
||||||
|
|
@ -16,13 +15,15 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
const containerRef = useRef(null);
|
const containerRef = useRef(null);
|
||||||
const scrollPositionRef = useRef(null);
|
const scrollPositionRef = useRef(null);
|
||||||
|
|
||||||
// const dispatch = useDispatch();
|
|
||||||
const [conversations, setConversations] = useState([]);
|
const [conversations, setConversations] = useState([]);
|
||||||
// current page
|
// current page
|
||||||
const [pageNumber, setPageNumber] = useState(1);
|
const [pageNumber, setPageNumber] = useState(1);
|
||||||
// total pages
|
// total pages
|
||||||
const [pages, setPages] = useState(1);
|
const [pages, setPages] = useState(1);
|
||||||
|
|
||||||
|
// data provider
|
||||||
|
const getConversationsQuery = useGetConversationsQuery(pageNumber);
|
||||||
|
|
||||||
// search
|
// search
|
||||||
const searchQuery = useRecoilValue(store.searchQuery);
|
const searchQuery = useRecoilValue(store.searchQuery);
|
||||||
const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
|
const isSearchEnabled = useRecoilValue(store.isSearchEnabled);
|
||||||
|
|
@ -33,29 +34,18 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
const conversation = useRecoilValue(store.conversation);
|
const conversation = useRecoilValue(store.conversation);
|
||||||
const { conversationId } = conversation || {};
|
const { conversationId } = conversation || {};
|
||||||
const setSearchResultMessages = useSetRecoilState(store.searchResultMessages);
|
const setSearchResultMessages = useSetRecoilState(store.searchResultMessages);
|
||||||
|
|
||||||
// refreshConversationsHint is used for other components to ask refresh of Nav
|
|
||||||
const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint);
|
const refreshConversationsHint = useRecoilValue(store.refreshConversationsHint);
|
||||||
|
|
||||||
const { refreshConversations } = store.useConversations();
|
const { refreshConversations } = store.useConversations();
|
||||||
|
|
||||||
const [isFetching, setIsFetching] = useState(false);
|
const [isFetching, setIsFetching] = useState(false);
|
||||||
|
|
||||||
const onSuccess = (data, searchFetch = false) => {
|
const debouncedSearchTerm = useDebounce(searchQuery, 750);
|
||||||
if (isSearching) {
|
const searchQueryFn = useSearchQuery(debouncedSearchTerm, pageNumber, {
|
||||||
return;
|
enabled: !!debouncedSearchTerm &&
|
||||||
}
|
debouncedSearchTerm.length > 0 &&
|
||||||
|
isSearchEnabled &&
|
||||||
let { conversations, pages } = data;
|
isSearching,
|
||||||
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 onSearchSuccess = (data, expectedPage) => {
|
const onSearchSuccess = (data, expectedPage) => {
|
||||||
const res = data;
|
const res = data;
|
||||||
|
|
@ -63,21 +53,21 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
if (expectedPage) {
|
if (expectedPage) {
|
||||||
setPageNumber(expectedPage);
|
setPageNumber(expectedPage);
|
||||||
}
|
}
|
||||||
setPageNumber(res.pageNumber);
|
|
||||||
setPages(res.pages);
|
setPages(res.pages);
|
||||||
setIsFetching(false);
|
setIsFetching(false);
|
||||||
searchPlaceholderConversation();
|
searchPlaceholderConversation();
|
||||||
setSearchResultMessages(res.messages);
|
setSearchResultMessages(res.messages);
|
||||||
};
|
};
|
||||||
|
|
||||||
// TODO: dont need this
|
useEffect(() => {
|
||||||
const fetch = useCallback(
|
//we use isInitialLoading here instead of isLoading because query is disabled by default
|
||||||
_.partialRight(
|
if (searchQueryFn.isInitialLoading) {
|
||||||
searchFetcher.bind(null, () => setIsFetching(true)),
|
setIsFetching(true);
|
||||||
onSearchSuccess
|
}
|
||||||
),
|
else if (searchQueryFn.data) {
|
||||||
[setIsFetching]
|
onSearchSuccess(searchQueryFn.data);
|
||||||
);
|
}
|
||||||
|
}, [searchQueryFn.data, searchQueryFn.isInitialLoading])
|
||||||
|
|
||||||
const clearSearch = () => {
|
const clearSearch = () => {
|
||||||
setPageNumber(1);
|
setPageNumber(1);
|
||||||
|
|
@ -85,38 +75,39 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
if (conversationId == 'search') {
|
if (conversationId == 'search') {
|
||||||
newConversation();
|
newConversation();
|
||||||
}
|
}
|
||||||
// dispatch(setDisabled(false));
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, isLoading, mutate } = swr(`/api/convos?pageNumber=${pageNumber}`, onSuccess, {
|
|
||||||
revalidateOnMount: false
|
|
||||||
});
|
|
||||||
|
|
||||||
const nextPage = async () => {
|
const nextPage = async () => {
|
||||||
moveToTop();
|
moveToTop();
|
||||||
|
setPageNumber(pageNumber + 1);
|
||||||
if (!isSearching) {
|
|
||||||
setPageNumber(prev => prev + 1);
|
|
||||||
await mutate();
|
|
||||||
} else {
|
|
||||||
await fetch(searchQuery, +pageNumber + 1);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const previousPage = async () => {
|
const previousPage = async () => {
|
||||||
moveToTop();
|
moveToTop();
|
||||||
|
setPageNumber(pageNumber - 1);
|
||||||
if (!isSearching) {
|
|
||||||
setPageNumber(prev => prev - 1);
|
|
||||||
await mutate();
|
|
||||||
} else {
|
|
||||||
await fetch(searchQuery, +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(() => {
|
useEffect(() => {
|
||||||
if (!isSearching) {
|
if (!isSearching) {
|
||||||
mutate();
|
getConversationsQuery.refetch();
|
||||||
}
|
}
|
||||||
}, [pageNumber, conversationId, refreshConversationsHint]);
|
}, [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 = () => {
|
const toggleNavVisible = () => {
|
||||||
setNavVisible(prev => !prev);
|
setNavVisible(prev => !prev);
|
||||||
};
|
};
|
||||||
|
|
||||||
// useEffect(() => {
|
|
||||||
// moveTo();
|
|
||||||
// }, [data]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setNavVisible(false);
|
setNavVisible(false);
|
||||||
}, [conversationId]);
|
}, [conversationId, setNavVisible]);
|
||||||
|
|
||||||
const containerClasses =
|
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 h-full justify-center items-center'
|
||||||
: 'flex flex-col gap-2 text-gray-100 text-sm';
|
: 'flex flex-col gap-2 text-gray-100 text-sm';
|
||||||
|
|
||||||
|
|
@ -176,8 +153,7 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
ref={containerRef}
|
ref={containerRef}
|
||||||
>
|
>
|
||||||
<div className={containerClasses}>
|
<div className={containerClasses}>
|
||||||
{/* {(isLoading && pageNumber === 1) ? ( */}
|
{(getConversationsQuery.isLoading && pageNumber === 1) || isFetching ? (
|
||||||
{(isLoading && pageNumber === 1) || isFetching ? (
|
|
||||||
<Spinner />
|
<Spinner />
|
||||||
) : (
|
) : (
|
||||||
<Conversations
|
<Conversations
|
||||||
|
|
@ -195,8 +171,6 @@ export default function Nav({ navVisible, setNavVisible }) {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<NavLinks
|
<NavLinks
|
||||||
fetch={fetch}
|
|
||||||
onSearchSuccess={onSearchSuccess}
|
|
||||||
clearSearch={clearSearch}
|
clearSearch={clearSearch}
|
||||||
isSearchEnabled={isSearchEnabled}
|
isSearchEnabled={isSearchEnabled}
|
||||||
/>
|
/>
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ import CheckMark from '../svg/CheckMark.jsx';
|
||||||
import { Listbox, Transition } from '@headlessui/react';
|
import { Listbox, Transition } from '@headlessui/react';
|
||||||
import { cn } from '~/utils/';
|
import { cn } from '~/utils/';
|
||||||
|
|
||||||
function SelectDropdown({
|
function SelectDropDown({
|
||||||
title = 'Model',
|
title = 'Model',
|
||||||
value,
|
value,
|
||||||
disabled,
|
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 { conversation, message, endpointOption } = submission;
|
||||||
const { conversationId } = conversation;
|
const { conversationId } = conversation;
|
||||||
const { endpoint } = endpointOption;
|
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 { createRoot } from 'react-dom/client';
|
||||||
// import { Provider } from 'react-redux';
|
|
||||||
// import { store } from './src/store';
|
|
||||||
import { RecoilRoot } from 'recoil';
|
import { RecoilRoot } from 'recoil';
|
||||||
|
import { QueryClient, QueryClientProvider } from "@tanstack/react-query";
|
||||||
import { ThemeProvider } from './hooks/ThemeContext';
|
import { ThemeProvider } from './hooks/ThemeContext';
|
||||||
import App from './App';
|
import App from './App';
|
||||||
import './style.css';
|
import './style.css';
|
||||||
|
|
@ -12,10 +9,14 @@ import './mobile.css';
|
||||||
const container = document.getElementById('root');
|
const container = document.getElementById('root');
|
||||||
const root = createRoot(container);
|
const root = createRoot(container);
|
||||||
|
|
||||||
|
const queryClient = new QueryClient();
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
<RecoilRoot>
|
<QueryClientProvider client={queryClient}>
|
||||||
<ThemeProvider>
|
<RecoilRoot>
|
||||||
<App />
|
<ThemeProvider>
|
||||||
</ThemeProvider>
|
<App />
|
||||||
</RecoilRoot>
|
</ThemeProvider>
|
||||||
|
</RecoilRoot>
|
||||||
|
</QueryClientProvider>
|
||||||
);
|
);
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,4 @@
|
||||||
import React, { useEffect } from 'react';
|
import { useEffect } from 'react';
|
||||||
import { useNavigate, useParams } from 'react-router-dom';
|
import { useNavigate, useParams } from 'react-router-dom';
|
||||||
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
|
||||||
|
|
||||||
|
|
@ -7,7 +7,7 @@ import Messages from '../components/Messages';
|
||||||
import TextChat from '../components/Input';
|
import TextChat from '../components/Input';
|
||||||
|
|
||||||
import store from '~/store';
|
import store from '~/store';
|
||||||
import manualSWR from '~/utils/fetchers';
|
import { useGetMessagesByConvoId, useGetConversationByIdMutation } from '~/data-provider';
|
||||||
|
|
||||||
export default function Chat() {
|
export default function Chat() {
|
||||||
const searchQuery = useRecoilValue(store.searchQuery);
|
const searchQuery = useRecoilValue(store.searchQuery);
|
||||||
|
|
@ -18,9 +18,9 @@ export default function Chat() {
|
||||||
const { conversationId } = useParams();
|
const { conversationId } = useParams();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
|
|
||||||
const { trigger: messagesTrigger } = manualSWR(`/api/messages/${conversation?.conversationId}`, 'get');
|
//disabled by default, we only enable it when messagesTree is null
|
||||||
|
const messagesQuery = useGetMessagesByConvoId(conversationId, { enabled: false });
|
||||||
const { trigger: conversationTrigger } = manualSWR(`/api/convos/${conversationId}`, 'get');
|
const getConversationMutation = useGetConversationByIdMutation(conversationId, setConversation);
|
||||||
|
|
||||||
// when conversation changed or conversationId (in url) changed
|
// when conversation changed or conversationId (in url) changed
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|
@ -31,13 +31,7 @@ export default function Chat() {
|
||||||
newConversation();
|
newConversation();
|
||||||
} else if (conversationId) {
|
} else if (conversationId) {
|
||||||
// fetch it from server
|
// fetch it from server
|
||||||
conversationTrigger()
|
getConversationMutation.mutate();
|
||||||
.then(setConversation)
|
|
||||||
.catch(error => {
|
|
||||||
console.error('failed to fetch the conversation');
|
|
||||||
console.error(error);
|
|
||||||
newConversation();
|
|
||||||
});
|
|
||||||
setMessages(null);
|
setMessages(null);
|
||||||
} else {
|
} else {
|
||||||
navigate(`/chat/new`);
|
navigate(`/chat/new`);
|
||||||
|
|
@ -51,13 +45,30 @@ export default function Chat() {
|
||||||
}
|
}
|
||||||
}, [conversation, conversationId]);
|
}, [conversation, conversationId]);
|
||||||
|
|
||||||
// when messagesTree is null (<=> messages is null)
|
|
||||||
// we need to fetch message list from server
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (messagesTree === null) {
|
if(getConversationMutation.isError) {
|
||||||
messagesTrigger().then(setMessages);
|
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 not a conversation
|
||||||
if (conversation?.conversationId === 'search') return null;
|
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