diff --git a/package.json b/package.json index 3d36757..dad3e6c 100644 --- a/package.json +++ b/package.json @@ -9,6 +9,7 @@ "@mantine/modals": "^5.10.5", "@mantine/notifications": "^5.10.5", "@mantine/spotlight": "^5.10.5", + "@reduxjs/toolkit": "^1.9.3", "@testing-library/jest-dom": "^5.16.5", "@testing-library/react": "^13.4.0", "@testing-library/user-event": "^13.5.0", @@ -35,9 +36,11 @@ "react-dom": "^18.2.0", "react-helmet": "^6.1.0", "react-markdown": "^8.0.5", + "react-redux": "^8.0.5", "react-router-dom": "^6.8.2", "react-scripts": "5.0.1", "react-syntax-highlighter": "^15.5.0", + "redux-persist": "^6.0.0", "rehype-katex": "^6.0.2", "remark-gfm": "^3.0.1", "remark-math": "^5.1.1", diff --git a/src/components/header.tsx b/src/components/header.tsx index c8dd200..8d777d1 100644 --- a/src/components/header.tsx +++ b/src/components/header.tsx @@ -2,12 +2,15 @@ import styled from '@emotion/styled'; import Helmet from 'react-helmet'; import { useSpotlight } from '@mantine/spotlight'; import { Button, ButtonProps } from '@mantine/core'; -import { useCallback, useState } from 'react'; +import { useCallback, useMemo, useState } from 'react'; import { Link, useNavigate } from 'react-router-dom'; import { APP_NAME } from '../values'; import { useAppContext } from '../context'; import { backend } from '../backend'; import { MenuItem, primaryMenu, secondaryMenu } from '../menus'; +import { useAppDispatch, useAppSelector } from '../store'; +import { selectOpenAIApiKey } from '../store/api-keys'; +import { setTab } from '../store/settings-ui'; const HeaderContainer = styled.div` display: flex; @@ -129,6 +132,8 @@ export default function Header(props: HeaderProps) { const navigate = useNavigate(); const spotlight = useSpotlight(); const [loading, setLoading] = useState(false); + const openAIApiKey = useAppSelector(selectOpenAIApiKey); + const dispatch = useAppDispatch(); const onNewChat = useCallback(async () => { setLoading(true); @@ -137,33 +142,37 @@ export default function Header(props: HeaderProps) { }, [navigate]); const openSettings = useCallback(() => { - context.settings.open(context.apiKeys.openai ? 'options' : 'user'); - }, [context]); + dispatch(setTab(openAIApiKey ? 'options' : 'user')); + }, [dispatch, openAIApiKey]); - return - - {props.title ? `${props.title} - ` : ''}{APP_NAME} - Unofficial ChatGPT app - - {props.title &&

{props.title}

} - {!props.title && (

-
- {APP_NAME}
- An unofficial ChatGPT app -
-

)} -
- - - {backend && !props.share && props.canShare && typeof navigator.share !== 'undefined' && - Share - } - {backend && !context.authenticated && ( - backend.current?.signIn()}>Sign in to sync - )} - - New Chat - - ; + const header = useMemo(() => ( + + + {props.title ? `${props.title} - ` : ''}{APP_NAME} - Unofficial ChatGPT app + + {props.title &&

{props.title}

} + {!props.title && (

+
+ {APP_NAME}
+ An unofficial ChatGPT app +
+

)} +
+ + + {backend && !props.share && props.canShare && typeof navigator.share !== 'undefined' && + Share + } + {backend && !context.authenticated && ( + backend.current?.signIn()}>Sign in to sync + )} + + New Chat + + + ), [props.title, props.share, props.canShare, props.onShare, openSettings, onNewChat, loading, context.authenticated, spotlight.openSpotlight]); + + return header; } function SubHeaderMenuItem(props: { item: MenuItem }) { @@ -176,9 +185,13 @@ function SubHeaderMenuItem(props: { item: MenuItem }) { } export function SubHeader(props: any) { - return - {primaryMenu.map(item => )} -
- {secondaryMenu.map(item => )} - ; + const elem = useMemo(() => ( + + {primaryMenu.map(item => )} +
+ {secondaryMenu.map(item => )} + + ), []); + + return elem; } \ No newline at end of file diff --git a/src/components/input.tsx b/src/components/input.tsx index 32b1eb9..b6a6ecd 100644 --- a/src/components/input.tsx +++ b/src/components/input.tsx @@ -3,6 +3,10 @@ import { Button, ActionIcon, Textarea } from '@mantine/core'; import { useCallback, useMemo } from 'react'; import { useLocation } from 'react-router-dom'; import { useAppContext } from '../context'; +import { useAppDispatch, useAppSelector } from '../store'; +import { selectMessage, setMessage } from '../store/message'; +import { selectTemperature } from '../store/parameters'; +import { openSystemPromptPanel, openTemperaturePanel } from '../store/settings-ui'; const Container = styled.div` background: #292933; @@ -40,18 +44,25 @@ export interface MessageInputProps { } export default function MessageInput(props: MessageInputProps) { + const temperature = useAppSelector(selectTemperature); + const message = useAppSelector(selectMessage); + const context = useAppContext(); + const dispatch = useAppDispatch(); + + const onCustomizeSystemPromptClick = useCallback(() => dispatch(openSystemPromptPanel()), [dispatch]); + const onTemperatureClick = useCallback(() => dispatch(openTemperaturePanel()), [dispatch]); + const onChange = useCallback((e: React.ChangeEvent) => { + dispatch(setMessage(e.target.value)); + }, [dispatch]); + const pathname = useLocation().pathname; - const onChange = useCallback((e: React.ChangeEvent) => { - context.setMessage(e.target.value); - }, [context]); - const onSubmit = useCallback(async () => { - if (await context.onNewMessage(context.message)) { - context.setMessage(''); + if (await context.onNewMessage(message)) { + dispatch(setMessage('')); } - }, [context]); + }, [context, message, dispatch]); const onKeyDown = useCallback((e: React.KeyboardEvent) => { if (e.key === 'Enter' && e.shiftKey === false && !props.disabled) { @@ -71,9 +82,6 @@ export default function MessageInput(props: MessageInputProps) { ); }, [onSubmit, props.disabled]); - const openSystemPromptPanel = useCallback(() => context.settings.open('options', 'system-prompt'), [context.settings]); - const openTemperaturePanel = useCallback(() => context.settings.open('options', 'temperature'), [context.settings]); - const messagesToDisplay = context.currentChat.messagesToDisplay; const disabled = context.generating || messagesToDisplay[messagesToDisplay.length - 1]?.role === 'user' @@ -83,7 +91,7 @@ export default function MessageInput(props: MessageInputProps) { if (context.isShare || (!isLandingPage && !context.id)) { return null; } - + return