refactor: nav and search.

feat: use recoil to replace redux
feat: use react-native

THIS IS NOT FINISHED. DONT USE THIS
This commit is contained in:
Wentao Lyu 2023-03-28 20:36:21 +08:00
parent d8ccc5b870
commit af3d74b104
33 changed files with 1142 additions and 473 deletions

View file

@ -0,0 +1,106 @@
import models from './models';
import { atom, selector, useRecoilValue, useSetRecoilState, useResetRecoilState } from 'recoil';
import buildTree from '~/utils/buildTree';
// current conversation, can be null (need to be fetched from server)
// sample structure
// {
// conversationId: "new",
// title: "New Chat",
// jailbreakConversationId: null,
// conversationSignature: null,
// clientId: null,
// invocationId: null,
// model: "chatgpt",
// chatGptLabel: null,
// promptPrefix: null,
// user: null,
// suggestions: [],
// toneStyle: null,
// }
const conversation = atom({
key: 'conversation',
default: null
});
// current messages of the conversation, must be an array
// sample structure
// [{text, sender, messageId, parentMessageId, isCreatedByUser}]
const messages = atom({
key: 'messages',
default: []
});
const messagesTree = selector({
key: 'messagesTree',
get: ({ get }) => {
return buildTree(get(messages));
}
});
const latestMessage = atom({
key: 'latestMessage',
default: null
});
const useConversation = () => {
const modelsFilter = useRecoilValue(models.modelsFilter);
const setConversation = useSetRecoilState(conversation);
const setMessages = useSetRecoilState(messages);
const resetLatestMessage = useResetRecoilState(latestMessage);
const newConversation = ({ model = null, chatGptLabel = null, promptPrefix = null } = {}) => {
const getDefaultModel = () => {
try {
// try to read latest selected model from local storage
const lastSelected = JSON.parse(localStorage.getItem('model'));
const { model: _model, chatGptLabel: _chatGptLabel, promptPrefix: _promptPrefix } = lastSelected;
if (modelsFilter[_model]) {
model = _model;
chatGptLabel = _chatGptLabel;
promptPrefix = _promptPrefix;
return;
}
} catch (error) {}
// if anything happens, reset to default model
if (modelsFilter?.chatgpt) model = 'chatgpt';
else if (modelsFilter?.bingai) model = 'bingai';
else if (modelsFilter?.chatgptBrowser) model = 'chatgptBrowser';
chatGptLabel = null;
promptPrefix = null;
};
if (model === null)
// get the default model
getDefaultModel();
setConversation({
conversationId: 'new',
title: 'New Chat',
jailbreakConversationId: null,
conversationSignature: null,
clientId: null,
invocationId: null,
model: model,
chatGptLabel: chatGptLabel,
promptPrefix: promptPrefix,
user: null,
suggestions: [],
toneStyle: null
});
setMessages([]);
resetLatestMessage();
};
return { newConversation };
};
export default {
conversation,
messages,
messagesTree,
latestMessage,
useConversation
};

View file

@ -0,0 +1,27 @@
import React from "react";
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from "recoil";
const refreshConversationsHint = atom({
key: "refreshConversationsHint",
default: 1,
});
const useConversations = () => {
const setRefreshConversationsHint = useSetRecoilState(
refreshConversationsHint
);
const refreshConversations = () =>
setRefreshConversationsHint((prevState) => prevState + 1);
return { refreshConversations };
};
export default { refreshConversationsHint, useConversations };

View file

@ -1,113 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
error: false,
title: 'ChatGPT Clone',
conversationId: null,
parentMessageId: null,
jailbreakConversationId: null,
conversationSignature: null,
clientId: null,
invocationId: null,
toneStyle: null,
chatGptLabel: null,
promptPrefix: null,
convosLoading: false,
pageNumber: 1,
pages: 1,
refreshConvoHint: 0,
search: false,
latestMessage: null,
convos: [],
convoMap: {},
};
const currentSlice = createSlice({
name: 'convo',
initialState,
reducers: {
refreshConversation: (state) => {
state.refreshConvoHint = state.refreshConvoHint + 1;
},
setConversation: (state, action) => {
// return { ...state, ...action.payload };
for (const key in action.payload) {
if (Object.hasOwnProperty.call(action.payload, key)) {
state[key] = action.payload[key];
}
}
},
setError: (state, action) => {
state.error = action.payload;
},
increasePage: (state) => {
state.pageNumber = state.pageNumber + 1;
},
decreasePage: (state) => {
state.pageNumber = state.pageNumber - 1;
},
setPage: (state, action) => {
state.pageNumber = action.payload;
},
setNewConvo: (state) => {
state.error = false;
state.title = 'ChatGPT Clone';
state.conversationId = null;
state.parentMessageId = null;
state.jailbreakConversationId = null;
state.conversationSignature = null;
state.clientId = null;
state.invocationId = null;
state.toneStyle = null;
state.chatGptLabel = null;
state.promptPrefix = null;
state.convosLoading = false;
state.latestMessage = null;
},
setConvos: (state, action) => {
const { convos, searchFetch } = action.payload;
if (searchFetch) {
state.convos = convos;
} else {
state.convos = convos.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
}
// state.convoMap = convos.reduce((acc, curr) => {
// acc[curr.conversationId] = { ...curr };
// delete acc[curr.conversationId].conversationId;
// return acc;
// }, {});
},
setPages: (state, action) => {
state.pages = action.payload;
},
removeConvo: (state, action) => {
state.convos = state.convos.filter((convo) => convo.conversationId !== action.payload);
},
removeAll: (state) => {
state.convos = [];
},
setLatestMessage: (state, action) => {
state.latestMessage = action.payload;
}
}
});
export const {
refreshConversation,
setConversation,
setPages,
setConvos,
setNewConvo,
setError,
increasePage,
decreasePage,
setPage,
removeConvo,
removeAll,
setLatestMessage
} = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -1,22 +1,15 @@
import { configureStore } from '@reduxjs/toolkit';
import conversation from './conversation';
import conversations from './conversations';
import models from './models';
import user from './user';
import submission from './submission';
import search from './search';
import convoReducer from './convoSlice.js';
import messageReducer from './messageSlice.js';
import modelReducer from './modelSlice.js';
import submitReducer from './submitSlice.js';
import textReducer from './textSlice.js';
import userReducer from './userReducer.js';
import searchReducer from './searchSlice.js';
export const store = configureStore({
reducer: {
convo: convoReducer,
messages: messageReducer,
models: modelReducer,
text: textReducer,
submit: submitReducer,
user: userReducer,
search: searchReducer
},
devTools: true
});
export default {
...conversation,
...conversations,
...models,
...user,
...submission,
...search
};

View file

@ -1,35 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
import buildTree from '~/utils/buildTree';
const initialState = {
messages: [],
messageTree: []
};
const currentSlice = createSlice({
name: 'messages',
initialState,
reducers: {
setMessages: (state, action) => {
state.messages = action.payload;
const groupAll = action.payload[0]?.searchResult;
if (groupAll) console.log('grouping all messages');
state.messageTree = buildTree(action.payload, groupAll);
},
setEmptyMessage: (state) => {
state.messages = [
{
messageId: '1',
conversationId: '1',
parentMessageId: '1',
sender: '',
text: ''
}
]
},
}
});
export const { setMessages, setEmptyMessage } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -1,68 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
models: [
{
_id: '0',
name: 'ChatGPT',
value: 'chatgpt',
model: 'chatgpt'
},
{
_id: '1',
name: 'CustomGPT',
value: 'chatgptCustom',
model: 'chatgptCustom'
},
{
_id: '2',
name: 'BingAI',
value: 'bingai',
model: 'bingai'
},
{
_id: '3',
name: 'Sydney',
value: 'sydney',
model: 'sydney'
},
{
_id: '4',
name: 'ChatGPT',
value: 'chatgptBrowser',
model: 'chatgptBrowser'
},
],
modelMap: {},
initial: { chatgpt: false, chatgptCustom: false, bingai: false, sydney: false, chatgptBrowser: false }
// initial: { chatgpt: true, chatgptCustom: true, bingai: true, }
};
const currentSlice = createSlice({
name: 'models',
initialState,
reducers: {
setModels: (state, action) => {
const models = [...initialState.models, ...action.payload];
state.models = models;
const modelMap = {};
models.slice(initialState.models.length).forEach((modelItem) => {
modelMap[modelItem.value] = {
chatGptLabel: modelItem.chatGptLabel,
promptPrefix: modelItem.promptPrefix,
model: 'chatgptCustom'
};
});
state.modelMap = modelMap;
},
setInitial: (state, action) => {
state.initial = action.payload;
}
}
});
export const { setModels, setInitial } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -0,0 +1,80 @@
import React from "react";
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from "recoil";
const customGPTModels = atom({
key: "customGPTModels",
default: [],
});
const models = selector({
key: "models",
get: ({ get }) => {
return [
{
_id: "0",
name: "ChatGPT",
value: "chatgpt",
model: "chatgpt",
},
{
_id: "1",
name: "CustomGPT",
value: "chatgptCustom",
model: "chatgptCustom",
},
{
_id: "2",
name: "BingAI",
value: "bingai",
model: "bingai",
},
{
_id: "3",
name: "Sydney",
value: "sydney",
model: "sydney",
},
{
_id: "4",
name: "ChatGPT",
value: "chatgptBrowser",
model: "chatgptBrowser",
},
...get(customGPTModels),
];
},
});
const modelsFilter = atom({
key: "modelsFilter",
default: {
chatgpt: false,
chatgptCustom: false,
bingai: false,
sydney: false,
chatgptBrowser: false,
},
});
const availableModels = selector({
key: "availableModels",
get: ({ get }) => {
const m = get(models);
const f = get(modelsFilter);
return m.filter(({ model }) => f[model]);
},
});
// const modelAvailable
export default {
customGPTModels,
models,
modelsFilter,
availableModels,
};

View file

@ -0,0 +1,25 @@
import { atom, selector } from 'recoil';
const isSearchEnabled = atom({
key: 'isSearchEnabled',
default: null
});
const searchQuery = atom({
key: 'searchQuery',
default: ''
});
const isSearching = selector({
key: 'isSearching',
get: ({ get }) => {
const data = get(searchQuery);
return !!data;
}
});
export default {
isSearchEnabled,
isSearching,
searchQuery
};

View file

@ -1,35 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
searchEnabled: false,
search: false,
query: '',
inputValue: '',
};
const currentSlice = createSlice({
name: 'search',
initialState,
reducers: {
setInputValue: (state, action) => {
state.inputValue = action.payload;
},
setSearchState: (state, action) => {
state.searchEnabled = action.payload;
},
setQuery: (state, action) => {
const q = action.payload;
state.query = q;
if (q === '') {
state.search = false;
} else if (q?.length > 0 && !state.search) {
state.search = true;
}
},
}
});
export const { setInputValue, setSearchState, setQuery } = currentSlice.actions;
export default currentSlice.reducer;

View file

@ -0,0 +1,37 @@
import React from "react";
import { useNavigate } from "react-router-dom";
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
useSetRecoilState,
} from "recoil";
import buildTree from "~/utils/buildTree";
// current submission
// submit any new value to this state will cause new message to be send.
// set to null to give up any submission
// {
// conversation, // target submission, must have: model, chatGptLabel, promptPrefix
// messages, // old messages
// message, // request message
// initialResponse, // response message
// isRegenerate=false, // isRegenerate?
// }
const submission = atom({
key: "submission",
default: null,
});
const isSubmitting = atom({
key: "isSubmitting",
default: false,
});
export default {
submission,
isSubmitting,
};

View file

@ -1,57 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
isSubmitting: false,
submission: {},
stopStream: false,
disabled: true,
model: 'chatgpt',
promptPrefix: null,
chatGptLabel: null,
customModel: null,
cursor: true,
};
const currentSlice = createSlice({
name: 'submit',
initialState,
reducers: {
setSubmitState: (state, action) => {
state.isSubmitting = action.payload;
},
setSubmission: (state, action) => {
state.submission = action.payload;
if (Object.keys(action.payload).length === 0) {
state.isSubmitting = false;
}
},
setStopStream: (state, action) => {
state.stopStream = action.payload;
},
setDisabled: (state, action) => {
state.disabled = action.payload;
},
setModel: (state, action) => {
state.model = action.payload;
},
toggleCursor: (state, action) => {
if (action.payload) {
state.cursor = action.payload;
} else {
state.cursor = !state.cursor;
}
},
setCustomGpt: (state, action) => {
state.promptPrefix = action.payload.promptPrefix;
state.chatGptLabel = action.payload.chatGptLabel;
},
setCustomModel: (state, action) => {
state.customModel = action.payload;
}
}
});
export const { toggleCursor, setSubmitState, setSubmission, setStopStream, setDisabled, setModel, setCustomGpt, setCustomModel } =
currentSlice.actions;
export default currentSlice.reducer;

View file

@ -1,19 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
text: '',
};
const currentSlice = createSlice({
name: 'text',
initialState,
reducers: {
setText: (state, action) => {
state.text = action.payload;
},
}
});
export const { setText } = currentSlice.actions;
export default currentSlice.reducer;

17
client/src/store/user.js Normal file
View file

@ -0,0 +1,17 @@
import React from "react";
import {
RecoilRoot,
atom,
selector,
useRecoilState,
useRecoilValue,
} from "recoil";
const user = atom({
key: "user",
default: null,
});
export default {
user,
};

View file

@ -1,19 +0,0 @@
import { createSlice } from '@reduxjs/toolkit';
const initialState = {
user: null,
};
const currentSlice = createSlice({
name: 'user',
initialState,
reducers: {
setUser: (state, action) => {
state.user = action.payload;
},
}
});
export const { setUser } = currentSlice.actions;
export default currentSlice.reducer;