Cogent Apps 2023-03-17 03:03:49 +00:00
commit bf9ab7d97e
18 changed files with 317 additions and 189 deletions

View File

@ -49,7 +49,7 @@
"build": "GENERATE_SOURCEMAP=false craco build", "build": "GENERATE_SOURCEMAP=false craco build",
"test": "craco test", "test": "craco test",
"eject": "craco eject", "eject": "craco eject",
"extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file public/lang/en-us.json --format simple --id-interpolation-pattern '[sha512:contenthash:base64:6]'" "extract": "formatjs extract 'src/**/*.ts*' --ignore='**/*.d.ts' --out-file public/lang/en-us.json --id-interpolation-pattern '[sha512:contenthash:base64:6]'"
}, },
"eslintConfig": { "eslintConfig": {
"extends": [ "extends": [

View File

@ -1,44 +1,222 @@
{ {
"+G35mR": "Open sidebar", "+G35mR": {
"/OKZrc": "Find your API key here.", "defaultMessage": "Open sidebar"
"3T9nRn": "Your API key is stored only on this device and never transmitted to anyone except OpenAI.", },
"47FYwb": "Cancel", "+LMWDJ": {
"4l6vz1": "Copy", "defaultMessage": "Chat History",
"6PgVSe": "Regenerate", "description": "Heading for the chat history screen"
"A4iXFN": "Temperature: {temperature, number, ::.0}", },
"BdPrnc": "Chat with GPT - Unofficial ChatGPT app", "/xTTti": {
"BwIZY+": "System Prompt", "defaultMessage": "User settings",
"ECx3EW": "Chat with GPT", "description": "Menu item that opens the user settings screen"
"ExZfjk": "Sign in <h>to sync</h>", },
"HIqSlE": "Preview voice", "0vL5u1": {
"HyS0qp": "Close sidebar", "defaultMessage": "Create an account"
"J3ca41": "Play", },
"KKa5Br": "Give ChatGPT a realisic human voice by connecting your ElevenLabs account (preview the available voices below). <a>Click here to sign up.</a>", "1j61Mn": {
"KbaJTs": "Loading audio...", "defaultMessage": "Or create an account",
"L5s+z7": "OpenAI API key usage is billed at a pay-as-you-go rate, separate from your ChatGPT subscription.", "description": "Label for a button on the Sign In page that lets the user create an account instead"
"NRJ4IQ": "Note: GPT-4 will only work if your OpenAI account has been granted access to the new model. <a>Request access here.</a>", },
"O83lC6": "Enter a message here...", "1r/ryM": {
"OKhRC6": "Share", "defaultMessage": "Cancel",
"Q97T+z": "Paste your API key here", "description": "Label for the button that can be clicked while the AI is generating a response to cancel generation"
"UT7Nkj": "New Chat", },
"Ua8luY": "Hello, how can I help you today?", "2GFjIN": {
"VL24Xt": "Search your chats", "defaultMessage": "Enter your password"
"X0ha1a": "Save changes", },
"Xzm66E": "Connect your OpenAI account to get started", "3T9nRn": {
"c60o5M": "Your OpenAI API Key", "defaultMessage": "Your API key is stored only on this device and never transmitted to anyone except OpenAI."
"jkpK/t": "Your ElevenLabs Text-to-Speech API Key (optional)", },
"jtu3jt": "You can find your API key by clicking your avatar or initials in the top right of the ElevenLabs website, then clicking Profile. Your API key is stored only on this device and never transmitted to anyone except ElevenLabs.", "4I+enA": {
"mhtiX2": "Customize system prompt", "defaultMessage": "GPT 4 (requires invite)"
"mnJYBQ": "Voice", },
"p556q3": "Copied", "5sg7KC": {
"q/uwLT": "Stop", "defaultMessage": "Password"
"rhSI1/": "Model", },
"role-chatgpt": "ChatGPT", "6LJByb": {
"role-system": "System", "defaultMessage": "Loading audio...",
"role-user": "You", "description": "Message indicating that text-to-speech audio is buffering"
"role-user-formal": "User", },
"sPtnbA": "The System Prompt is shown to ChatGPT by the &quot;System&quot; before your first message. The <code>'{{ datetime }}'</code> tag is automatically replaced by the current date and time.", "74eGxP": {
"ss6kle": "Reset to default", "defaultMessage": "New Chat",
"tZdXp/": "The temperature parameter controls the randomness of the AI's responses. Lower values will make the AI more predictable, while higher values will make it more creative.", "description": "Label for the button used to start a new chat session"
"wEQDC6": "Edit" },
} "8CYNFt": {
"defaultMessage": "No chats yet.",
"description": "Message shown on the Chat History screen for new users who haven't started their first chat session"
},
"8HJxXG": {
"defaultMessage": "Sign up"
},
"Bi0lS9": {
"defaultMessage": "Share",
"description": "Label for the button used to create a public share URL for a chat log"
},
"Bm3EKs": {
"defaultMessage": "Save changes",
"description": "Label for a button that appears when the user is editing the text of one of their messages, to save the changes"
},
"CJwO9s": {
"defaultMessage": "GPT 3.5 Turbo (default)"
},
"FEzBCd": {
"defaultMessage": "Untitled",
"description": "default title for untitled chat sessions"
},
"Fko0yT": {
"defaultMessage": "Stop",
"description": "Label for the button that stops text-to-speech playback"
},
"HyS0qp": {
"defaultMessage": "Close sidebar"
},
"JpZMMj": {
"defaultMessage": "Voice",
"description": "Heading for the setting that lets users choose an ElevenLabs text-to-speech voice, on the settings screen"
},
"KKa5Br": {
"defaultMessage": "Give ChatGPT a realisic human voice by connecting your ElevenLabs account (preview the available voices below). <a>Click here to sign up.</a>"
},
"L5s+z7": {
"defaultMessage": "OpenAI API key usage is billed at a pay-as-you-go rate, separate from your ChatGPT subscription."
},
"MI5gZ+": {
"defaultMessage": "Download SVG"
},
"N2NGTf": {
"defaultMessage": "Chat with GPT",
"description": "app name"
},
"NRJ4IQ": {
"defaultMessage": "Note: GPT-4 will only work if your OpenAI account has been granted access to the new model. <a>Request access here.</a>"
},
"NgCT/u": {
"defaultMessage": "Enter your email address"
},
"O83lC6": {
"defaultMessage": "Enter a message here..."
},
"Q97T+z": {
"defaultMessage": "Paste your API key here"
},
"S9BFby": {
"defaultMessage": "Play",
"description": "Label for the button that starts text-to-speech playback"
},
"SQJto2": {
"defaultMessage": "Sign in"
},
"SQh9En": {
"defaultMessage": "Sign in <h>to sync</h>",
"description": "Label for sign in button, indicating the purpose of signing in is to sync your data between devices"
},
"Tgo3vj": {
"defaultMessage": "Edit",
"description": "Label for the button the user can click to edit the text of one of their messages"
},
"VL24Xt": {
"defaultMessage": "Search your chats"
},
"WOuVxP": {
"defaultMessage": "Model",
"description": "Heading for the setting that lets users choose a model to interact with, on the settings screen"
},
"Xzm66E": {
"defaultMessage": "Connect your OpenAI account to get started"
},
"Y0tGn6": {
"defaultMessage": "Or sign in to an existing account",
"description": "Label for a button on the Create Account page that lets the user sign into their existing account instead"
},
"bIacvz": {
"defaultMessage": "Chat with GPT - Unofficial ChatGPT app",
"description": "HTML title tag"
},
"cAtzqn": {
"defaultMessage": "System Prompt",
"description": "Heading for the setting that lets users customize the System Prompt, on the settings screen"
},
"cmcjSh": {
"defaultMessage": "Preview voice",
"description": "Label for the button that plays a preview of the selected ElevenLabs text-to-speech voice"
},
"f/hGIY": {
"defaultMessage": "Hello, how can I help you today?",
"description": "A friendly message that appears at the start of new chat sessions"
},
"gzJlXS": {
"defaultMessage": "Share",
"description": "Label for a button which shares the text of a chat message using the user device's share functionality"
},
"hJZwTS": {
"defaultMessage": "Email address"
},
"iqbKb2": {
"defaultMessage": "Save and Close",
"description": "Label for the button that closes the Settings screen, saving any changes"
},
"jtu3jt": {
"defaultMessage": "You can find your API key by clicking your avatar or initials in the top right of the ElevenLabs website, then clicking Profile. Your API key is stored only on this device and never transmitted to anyone except ElevenLabs."
},
"ljHOzQ": {
"defaultMessage": "Copied",
"description": "Label for copy-to-clipboard button after a successful copy"
},
"pv41j8": {
"defaultMessage": "Temperature: {temperature, number, ::.0}",
"description": "Label for the button that opens a modal for setting the 'temperature' (randomness) of AI responses"
},
"raQMIg": {
"defaultMessage": "Cancel",
"description": "Label for a button that appears when the user is editing the text of one of their messages, to cancel without saving changes"
},
"role-chatgpt": {
"defaultMessage": "ChatGPT",
"description": "Label that is shown above messages written by the AI (as opposed to the user)"
},
"role-system": {
"defaultMessage": "System",
"description": "Label that is shown above messages inserted into the conversation automatically by the system (as opposed to either the user or AI)"
},
"role-user": {
"defaultMessage": "You",
"description": "Label that is shown above messages written by the user (as opposed to the AI) in the user's own chat sessions (first person)."
},
"role-user-formal": {
"defaultMessage": "User",
"description": "Label that is shown above messages written by the user (as opposed to the AI) for publicly shared conversation (third person, formal)."
},
"sPtnbA": {
"defaultMessage": "The System Prompt is shown to ChatGPT by the &quot;System&quot; before your first message. The <code>'{{ datetime }}'</code> tag is automatically replaced by the current date and time."
},
"ss6kle": {
"defaultMessage": "Reset to default"
},
"sskUPZ": {
"defaultMessage": "Your ElevenLabs Text-to-Speech API Key (optional)",
"description": "Heading for the ElevenLabs API key setting on the settings screen"
},
"tZdXp/": {
"defaultMessage": "The temperature parameter controls the randomness of the AI's responses. Lower values will make the AI more predictable, while higher values will make it more creative."
},
"upBSoW": {
"defaultMessage": "Copy",
"description": "Label for copy-to-clipboard button"
},
"w5Dmuu": {
"defaultMessage": "Customize system prompt",
"description": "Label for the button that opens a modal for customizing the 'system prompt', a message used to customize and influence how the AI responds."
},
"y1F8Hs": {
"defaultMessage": "Your OpenAI API Key",
"description": "Heading for the OpenAI API key setting on the settings screen"
},
"zBmup+": {
"defaultMessage": "Regenerate",
"description": "Label for the button used to ask the AI to regenerate one of its messages. Since message generations are stochastic, the resulting message will be different."
},
"zFt1cV": {
"defaultMessage": "Find your API key here.",
"description": "Label for the link that takes the user to the page on the OpenAI website where they can find their API key."
}
}

View File

@ -1,6 +1,7 @@
import styled from "@emotion/styled"; import styled from "@emotion/styled";
import { Button, Modal, PasswordInput, TextInput } from "@mantine/core"; import { Button, Modal, PasswordInput, TextInput } from "@mantine/core";
import { useCallback, useState } from "react"; import { useCallback, useState } from "react";
import { FormattedMessage, useIntl } from "react-intl";
import { useAppDispatch, useAppSelector } from "../../store"; import { useAppDispatch, useAppSelector } from "../../store";
import { closeModals, openLoginModal, openSignupModal, selectModal } from "../../store/ui"; import { closeModals, openLoginModal, openSignupModal, selectModal } from "../../store/ui";
@ -35,6 +36,7 @@ const Container = styled.form`
export function LoginModal(props: any) { export function LoginModal(props: any) {
const modal = useAppSelector(selectModal); const modal = useAppSelector(selectModal);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl();
const onClose = useCallback(() => dispatch(closeModals()), [dispatch]); const onClose = useCallback(() => dispatch(closeModals()), [dispatch]);
const onCreateAccountClick = useCallback(() => dispatch(openSignupModal()), [dispatch]); const onCreateAccountClick = useCallback(() => dispatch(openSignupModal()), [dispatch]);
@ -42,24 +44,26 @@ export function LoginModal(props: any) {
return <Modal opened={modal === 'login'} onClose={onClose} withCloseButton={false}> return <Modal opened={modal === 'login'} onClose={onClose} withCloseButton={false}>
<Container action="/chatapi/login" method="post"> <Container action="/chatapi/login" method="post">
<h2> <h2>
Sign in <FormattedMessage defaultMessage={"Sign in"} />
</h2> </h2>
<input type="hidden" name="redirect_url" value={window.location.href} /> <input type="hidden" name="redirect_url" value={window.location.href} />
<TextInput label="Email address" <TextInput
label={intl.formatMessage({ defaultMessage: "Email address" })}
name="username" name="username"
placeholder="Enter your email address" placeholder={intl.formatMessage({ defaultMessage: "Enter your email address" })}
type="email" type="email"
required /> required />
<PasswordInput label="Password" <PasswordInput
label={intl.formatMessage({ defaultMessage: "Password" })}
name="password" name="password"
placeholder="Enter your password" placeholder={intl.formatMessage({ defaultMessage: "Enter your password" })}
maxLength={500} maxLength={500}
required /> required />
<Button fullWidth type="submit"> <Button fullWidth type="submit">
Sign in <FormattedMessage defaultMessage={"Sign in"} />
</Button> </Button>
<Button fullWidth variant="subtle" onClick={onCreateAccountClick}> <Button fullWidth variant="subtle" onClick={onCreateAccountClick}>
Or create an account <FormattedMessage defaultMessage={"Or create an account"} description="Label for a button on the Sign In page that lets the user create an account instead" />
</Button> </Button>
</Container> </Container>
</Modal> </Modal>
@ -68,6 +72,7 @@ export function LoginModal(props: any) {
export function CreateAccountModal(props: any) { export function CreateAccountModal(props: any) {
const modal = useAppSelector(selectModal); const modal = useAppSelector(selectModal);
const dispatch = useAppDispatch(); const dispatch = useAppDispatch();
const intl = useIntl();
const onClose = useCallback(() => dispatch(closeModals()), [dispatch]); const onClose = useCallback(() => dispatch(closeModals()), [dispatch]);
const onSignInClick = useCallback(() => dispatch(openLoginModal()), [dispatch]); const onSignInClick = useCallback(() => dispatch(openLoginModal()), [dispatch]);
@ -75,25 +80,27 @@ export function CreateAccountModal(props: any) {
return <Modal opened={modal === 'signup'} onClose={onClose} withCloseButton={false}> return <Modal opened={modal === 'signup'} onClose={onClose} withCloseButton={false}>
<Container action="/chatapi/register" method="post"> <Container action="/chatapi/register" method="post">
<h2> <h2>
Create an account <FormattedMessage defaultMessage={"Create an account"} />
</h2> </h2>
<input type="hidden" name="redirect_url" value={window.location.href} /> <input type="hidden" name="redirect_url" value={window.location.href} />
<TextInput label="Email address" <TextInput
label={intl.formatMessage({ defaultMessage: "Email address" })}
name="username" name="username"
placeholder="Enter your email address" placeholder={intl.formatMessage({ defaultMessage: "Enter your email address" })}
type="email" type="email"
required /> required />
<PasswordInput label="Password" <PasswordInput
label={intl.formatMessage({ defaultMessage: "Password" })}
name="password" name="password"
placeholder="Enter your password" placeholder={intl.formatMessage({ defaultMessage: "Enter your password" })}
minLength={6} minLength={6}
maxLength={500} maxLength={500}
required /> required />
<Button fullWidth type="submit"> <Button fullWidth type="submit">
Sign up <FormattedMessage defaultMessage={"Sign up"} />
</Button> </Button>
<Button fullWidth variant="subtle" onClick={onSignInClick}> <Button fullWidth variant="subtle" onClick={onSignInClick}>
Or sign in to an existing account <FormattedMessage defaultMessage={"Or sign in to an existing account"} description="Label for a button on the Create Account page that lets the user sign into their existing account instead" />
</Button> </Button>
</Container> </Container>
</Modal> </Modal>

View File

@ -160,16 +160,16 @@ export default function Header(props: HeaderProps) {
<Helmet> <Helmet>
<title> <title>
{props.title ? `${props.title} - ` : ''} {props.title ? `${props.title} - ` : ''}
{intl.formatMessage({ defaultMessage: "Chat with GPT - Unofficial ChatGPT app" })} {intl.formatMessage({ defaultMessage: "Chat with GPT - Unofficial ChatGPT app", description: "HTML title tag" })}
</title> </title>
</Helmet> </Helmet>
{!sidebarOpen && <Burger opened={sidebarOpen} onClick={onBurgerClick} aria-label={burgerLabel} transitionDuration={0} />} {!sidebarOpen && <Burger opened={sidebarOpen} onClick={onBurgerClick} aria-label={burgerLabel} transitionDuration={0} />}
{context.isHome && <h2>{intl.formatMessage({ defaultMessage: "Chat with GPT" })}</h2>} {context.isHome && <h2>{intl.formatMessage({ defaultMessage: "Chat with GPT", description: "app name" })}</h2>}
<div className="spacer" /> <div className="spacer" />
<HeaderButton icon="search" onClick={spotlight.openSpotlight} /> <HeaderButton icon="search" onClick={spotlight.openSpotlight} />
<HeaderButton icon="gear" onClick={openSettings} /> <HeaderButton icon="gear" onClick={openSettings} />
{backend.current && !props.share && props.canShare && typeof navigator.share !== 'undefined' && <HeaderButton icon="share" onClick={props.onShare}> {backend.current && !props.share && props.canShare && typeof navigator.share !== 'undefined' && <HeaderButton icon="share" onClick={props.onShare}>
<FormattedMessage defaultMessage="Share" /> <FormattedMessage defaultMessage="Share" description="Label for the button used to create a public share URL for a chat log" />
</HeaderButton>} </HeaderButton>}
{backend.current && !context.authenticated && ( {backend.current && !context.authenticated && (
<HeaderButton onClick={() => { <HeaderButton onClick={() => {
@ -180,13 +180,14 @@ export default function Header(props: HeaderProps) {
} }
}}> }}>
<FormattedMessage defaultMessage="Sign in <h>to sync</h>" <FormattedMessage defaultMessage="Sign in <h>to sync</h>"
description="Label for sign in button, indicating the purpose of signing in is to sync your data between devices"
values={{ values={{
h: (chunks: any) => <span className="hide-on-mobile">{chunks}</span> h: (chunks: any) => <span className="hide-on-mobile">{chunks}</span>
}} /> }} />
</HeaderButton> </HeaderButton>
)} )}
<HeaderButton icon="plus" onClick={onNewChat} loading={loading} variant="light"> <HeaderButton icon="plus" onClick={onNewChat} loading={loading} variant="light">
<FormattedMessage defaultMessage="New Chat" /> <FormattedMessage defaultMessage="New Chat" description="Label for the button used to start a new chat session" />
</HeaderButton> </HeaderButton>
</HeaderContainer> </HeaderContainer>
), [sidebarOpen, onBurgerClick, props.title, props.share, props.canShare, props.onShare, openSettings, onNewChat, loading, context.authenticated, context.isHome, context.isShare, spotlight.openSpotlight]); ), [sidebarOpen, onBurgerClick, props.title, props.share, props.canShare, props.onShare, openSettings, onNewChat, loading, context.authenticated, context.isHome, context.isShare, spotlight.openSpotlight]);

View File

@ -79,7 +79,7 @@ export default function MessageInput(props: MessageInputProps) {
<Button variant="subtle" size="xs" compact onClick={() => { <Button variant="subtle" size="xs" compact onClick={() => {
context.chat.cancelReply(context.currentChat.leaf!.id); context.chat.cancelReply(context.currentChat.leaf!.id);
}}> }}>
<FormattedMessage defaultMessage={"Cancel"} /> <FormattedMessage defaultMessage={"Cancel"} description="Label for the button that can be clicked while the AI is generating a response to cancel generation" />
</Button> </Button>
<Loader size="xs" style={{ padding: '0 0.8rem 0 0.5rem' }} /> <Loader size="xs" style={{ padding: '0 0.8rem 0 0.5rem' }} />
</>)} </>)}
@ -119,7 +119,7 @@ export default function MessageInput(props: MessageInputProps) {
compact compact
onClick={onCustomizeSystemPromptClick}> onClick={onCustomizeSystemPromptClick}>
<span> <span>
<FormattedMessage defaultMessage={"Customize system prompt"} /> <FormattedMessage defaultMessage={"Customize system prompt"} description="Label for the button that opens a modal for customizing the 'system prompt', a message used to customize and influence how the AI responds." />
</span> </span>
</Button> </Button>
<Button variant="subtle" <Button variant="subtle"
@ -129,6 +129,7 @@ export default function MessageInput(props: MessageInputProps) {
onClick={onTemperatureClick}> onClick={onTemperatureClick}>
<span> <span>
<FormattedMessage defaultMessage="Temperature: {temperature, number, ::.0}" <FormattedMessage defaultMessage="Temperature: {temperature, number, ::.0}"
description="Label for the button that opens a modal for setting the 'temperature' (randomness) of AI responses"
values={{ temperature }} /> values={{ temperature }} />
</span> </span>
</Button> </Button>

View File

@ -97,7 +97,10 @@ export function Markdown(props: MarkdownProps) {
{({ copy, copied }) => ( {({ copy, copied }) => (
<Button variant="subtle" size="sm" compact onClick={copy}> <Button variant="subtle" size="sm" compact onClick={copy}>
<i className="fa fa-clipboard" /> <i className="fa fa-clipboard" />
<span>{copied ? <FormattedMessage defaultMessage="Copied" /> : <FormattedMessage defaultMessage="Copy" />}</span> <span>
{copied ? <FormattedMessage defaultMessage="Copied" description="Label for copy-to-clipboard button after a successful copy" />
: <FormattedMessage defaultMessage="Copy" description="Label for copy-to-clipboard button" />}
</span>
</Button> </Button>
)} )}
</CopyButton> </CopyButton>

View File

@ -208,15 +208,15 @@ export default function MessageComponent(props: { message: Message, last: boolea
switch (role) { switch (role) {
case 'user': case 'user':
if (share) { if (share) {
return intl.formatMessage({ id: 'role-user-formal', defaultMessage: 'User' }); return intl.formatMessage({ id: 'role-user-formal', defaultMessage: 'User', description: "Label that is shown above messages written by the user (as opposed to the AI) for publicly shared conversation (third person, formal)." });
} else { } else {
return intl.formatMessage({ id: 'role-user', defaultMessage: 'You' }); return intl.formatMessage({ id: 'role-user', defaultMessage: 'You', description: "Label that is shown above messages written by the user (as opposed to the AI) in the user's own chat sessions (first person)." });
} }
break; break;
case 'assistant': case 'assistant':
return intl.formatMessage({ id: 'role-chatgpt', defaultMessage: 'ChatGPT' }); return intl.formatMessage({ id: 'role-chatgpt', defaultMessage: 'ChatGPT', description: "Label that is shown above messages written by the AI (as opposed to the user)" });
case 'system': case 'system':
return intl.formatMessage({ id: 'role-system', defaultMessage: 'System' }); return intl.formatMessage({ id: 'role-system', defaultMessage: 'System', description: "Label that is shown above messages inserted into the conversation automatically by the system (as opposed to either the user or AI)" });
default: default:
return role; return role;
} }
@ -243,7 +243,8 @@ export default function MessageComponent(props: { message: Message, last: boolea
{({ copy, copied }) => ( {({ copy, copied }) => (
<Button variant="subtle" size="sm" compact onClick={copy} style={{ marginLeft: '1rem' }}> <Button variant="subtle" size="sm" compact onClick={copy} style={{ marginLeft: '1rem' }}>
<i className="fa fa-clipboard" /> <i className="fa fa-clipboard" />
<span>{copied ? <FormattedMessage defaultMessage="Copied" /> : <FormattedMessage defaultMessage="Copy" />}</span> {copied ? <FormattedMessage defaultMessage="Copied" description="Label for copy-to-clipboard button after a successful copy" />
: <FormattedMessage defaultMessage="Copy" description="Label for copy-to-clipboard button" />}
</Button> </Button>
)} )}
</CopyButton> </CopyButton>
@ -251,7 +252,7 @@ export default function MessageComponent(props: { message: Message, last: boolea
<Button variant="subtle" size="sm" compact onClick={() => share(props.message.content)}> <Button variant="subtle" size="sm" compact onClick={() => share(props.message.content)}>
<i className="fa fa-share" /> <i className="fa fa-share" />
<span> <span>
<FormattedMessage defaultMessage="Share" /> <FormattedMessage defaultMessage="Share" description="Label for a button which shares the text of a chat message using the user device's share functionality" />
</span> </span>
</Button> </Button>
)} )}
@ -261,14 +262,17 @@ export default function MessageComponent(props: { message: Message, last: boolea
setEditing(v => !v); setEditing(v => !v);
}}> }}>
<i className="fa fa-edit" /> <i className="fa fa-edit" />
<span>{editing ? <FormattedMessage defaultMessage="Cancel" /> : <FormattedMessage defaultMessage="Edit" />}</span> <span>
{editing ? <FormattedMessage defaultMessage="Cancel" description="Label for a button that appears when the user is editing the text of one of their messages, to cancel without saving changes" />
: <FormattedMessage defaultMessage="Edit" description="Label for the button the user can click to edit the text of one of their messages" />}
</span>
</Button> </Button>
)} )}
{!context.isShare && props.message.role === 'assistant' && ( {!context.isShare && props.message.role === 'assistant' && (
<Button variant="subtle" size="sm" compact onClick={() => context.regenerateMessage(props.message)}> <Button variant="subtle" size="sm" compact onClick={() => context.regenerateMessage(props.message)}>
<i className="fa fa-refresh" /> <i className="fa fa-refresh" />
<span> <span>
<FormattedMessage defaultMessage="Regenerate" /> <FormattedMessage defaultMessage="Regenerate" description="Label for the button used to ask the AI to regenerate one of its messages. Since message generations are stochastic, the resulting message will be different." />
</span> </span>
</Button> </Button>
)} )}
@ -279,10 +283,10 @@ export default function MessageComponent(props: { message: Message, last: boolea
onChange={e => setContent(e.currentTarget.value)} onChange={e => setContent(e.currentTarget.value)}
autosize={true} /> autosize={true} />
<Button variant="light" onClick={() => context.editMessage(props.message, content)}> <Button variant="light" onClick={() => context.editMessage(props.message, content)}>
<FormattedMessage defaultMessage="Save changes" /> <FormattedMessage defaultMessage="Save changes" description="Label for a button that appears when the user is editing the text of one of their messages, to save the changes" />
</Button> </Button>
<Button variant="subtle" onClick={() => setEditing(false)}> <Button variant="subtle" onClick={() => setEditing(false)}>
<FormattedMessage defaultMessage="Cancel" /> <FormattedMessage defaultMessage="Cancel" description="Label for a button that appears when the user is editing the text of one of their messages, to cancel without saving changes" />
</Button> </Button>
</Editor>)} </Editor>)}
</div> </div>

View File

@ -1,79 +0,0 @@
import styled from "@emotion/styled";
import { Markdown } from "../markdown";
import { Page } from "../page";
const title = "Learn about Chat with GPT";
const content = `
# About Chat with GPT
Chat with GPT is an open-source, unofficial ChatGPT app with extra features and more ways to customize your experience.
ChatGPT is an AI assistant developed by OpenAI. It's designed to understand natural language and generate human-like responses to a wide range of questions and prompts. ChatGPT has been trained on a massive dataset of text from the internet, which allows it to draw on a vast amount of knowledge and information to answer questions and engage in conversation. ChatGPT is constantly being improved. Feel free to ask it anything!
[Join the Discord.](https://discord.gg/mS5QvKykvv)
## Features
- 🚀 **Fast** response times.
- 🔎 **Search** through your past chat conversations.
- 📄 View and customize the System Prompt - the **secret prompt** the system shows the AI before your messages.
- 🌡 Adjust the **creativity and randomness** of responses by setting the Temperature setting. Higher temperature means more creativity.
- 💬 Give ChatGPT AI a **realistic human voice** by connecting your ElevenLabs text-to-speech account.
- **Share** your favorite chat sessions online using public share URLs.
- 📋 Easily **copy-and-paste** ChatGPT messages.
- 🖼 **Full markdown support** including code, tables, and math.
- 🫰 Pay for only what you use with the ChatGPT API.
## Bring your own API keys
### OpenAI
To get started with Chat with GPT, you will need to add your OpenAI API key on the settings screen. Click "Connect your OpenAI account to get started" on the home page to begin. Once you have added your API key, you can start chatting with ChatGPT.
Your API key is stored only on your device and is never transmitted to anyone except OpenAI. Please note that OpenAI API key usage is billed at a pay-as-you-go rate, separate from your ChatGPT subscription.
### ElevenLabs
To use the realistic AI text-to-speech feature, you will need to add your ElevenLabs API key by clicking "Play" next to any message.
Your API key is stored only on your device and never transmitted to anyone except ElevenLabs.
## Roadmap
- Edit messages (coming soon)
- Regenerate messages (coming soon)
- [Suggest feature ideas on the Discord](https://discord.gg/mS5QvKykvv)
`;
const Container = styled.div`
flex-grow: 1;
overflow-y: auto;
padding-top: 2rem;
padding-bottom: 3rem;
.inner {
max-width: 50rem;
margin-left: auto;
margin-right: auto;
font-weight: "Work Sans", sans-serif;
* {
color: white !important;
}
h1, h2 {
border-bottom: thin solid rgba(255, 255, 255, 0.2);
padding-bottom: 1rem;
margin-bottom: 1rem;
}
}
`;
export default function AboutPage(props: any) {
return <Page id={'about'} headerProps={{ title }}>
<Container>
<Markdown content={content} className='inner' />
</Container>
</Page>;
}

View File

@ -27,7 +27,8 @@ export default function LandingPage(props: any) {
return <Page id={'landing'} showSubHeader={true}> return <Page id={'landing'} showSubHeader={true}>
<Container> <Container>
<p> <p>
<FormattedMessage defaultMessage={'Hello, how can I help you today?'} /> <FormattedMessage defaultMessage={'Hello, how can I help you today?'}
description="A friendly message that appears at the start of new chat sessions" />
</p> </p>
{!openAIApiKey && ( {!openAIApiKey && (
<Button size="xs" variant="light" compact onClick={onConnectButtonClick}> <Button size="xs" variant="light" compact onClick={onConnectButtonClick}>

View File

@ -7,6 +7,7 @@ import GenerationOptionsTab from './options';
import { useAppDispatch, useAppSelector } from '../../store'; import { useAppDispatch, useAppSelector } from '../../store';
import { closeSettingsUI, selectSettingsTab, setTab } from '../../store/settings-ui'; import { closeSettingsUI, selectSettingsTab, setTab } from '../../store/settings-ui';
import SpeechOptionsTab from './speech'; import SpeechOptionsTab from './speech';
import { FormattedMessage } from 'react-intl';
const Container = styled.div` const Container = styled.div`
padding: .4rem 1rem 1rem 1rem; padding: .4rem 1rem 1rem 1rem;
@ -102,7 +103,9 @@ export default function SettingsDrawer(props: SettingsDrawerProps) {
</Tabs> </Tabs>
<div id="save"> <div id="save">
<Button variant="light" fullWidth size="md" onClick={close}> <Button variant="light" fullWidth size="md" onClick={close}>
Save and Close <FormattedMessage defaultMessage={"Save and Close"}
description="Label for the button that closes the Settings screen, saving any changes"
/>
</Button> </Button>
</div> </div>
</Container> </Container>

View File

@ -30,7 +30,7 @@ export default function GenerationOptionsTab(props: any) {
&& (model?.trim() !== defaultModel.trim()); && (model?.trim() !== defaultModel.trim());
const systemPromptOption = useMemo(() => ( const systemPromptOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: "System Prompt" })} <SettingsOption heading={intl.formatMessage({ defaultMessage: "System Prompt", description: "Heading for the setting that lets users customize the System Prompt, on the settings screen" })}
focused={option === 'system-prompt'}> focused={option === 'system-prompt'}>
<Textarea <Textarea
value={initialSystemPrompt || defaultSystemPrompt} value={initialSystemPrompt || defaultSystemPrompt}
@ -49,13 +49,19 @@ export default function GenerationOptionsTab(props: any) {
), [option, initialSystemPrompt, resettableSystemPromopt, onSystemPromptChange, onResetSystemPrompt]); ), [option, initialSystemPrompt, resettableSystemPromopt, onSystemPromptChange, onResetSystemPrompt]);
const modelOption = useMemo(() => ( const modelOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: "Model" })} <SettingsOption heading={intl.formatMessage({ defaultMessage: "Model", description: "Heading for the setting that lets users choose a model to interact with, on the settings screen" })}
focused={option === 'model'}> focused={option === 'model'}>
<Select <Select
value={model || defaultModel} value={model || defaultModel}
data={[ data={[
{ label: "GPT 3.5 Turbo (default)", value: "gpt-3.5-turbo" }, {
{ label: "GPT 4 (requires invite)", value: "gpt-4" }, label: intl.formatMessage({ defaultMessage: "GPT 3.5 Turbo (default)" }),
value: "gpt-3.5-turbo",
},
{
label: intl.formatMessage({ defaultMessage: "GPT 4 (requires invite)" }),
value: "gpt-4",
},
]} ]}
onChange={onModelChange} /> onChange={onModelChange} />
{model === 'gpt-4' && ( {model === 'gpt-4' && (
@ -71,7 +77,10 @@ export default function GenerationOptionsTab(props: any) {
), [option, model, resettableModel, onModelChange, onResetModel]); ), [option, model, resettableModel, onModelChange, onResetModel]);
const temperatureOption = useMemo(() => ( const temperatureOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: "Temperature: {temperature, number, ::.0}", }, { temperature })} <SettingsOption heading={intl.formatMessage({
defaultMessage: "Temperature: {temperature, number, ::.0}",
description: "Label for the button that opens a modal for setting the 'temperature' (randomness) of AI responses",
}, { temperature })}
focused={option === 'temperature'}> focused={option === 'temperature'}>
<Slider value={temperature} onChange={onTemperatureChange} step={0.1} min={0} max={1} precision={3} /> <Slider value={temperature} onChange={onTemperatureChange} step={0.1} min={0} max={1} precision={3} />
<p> <p>

View File

@ -33,7 +33,7 @@ export default function SpeechOptionsTab() {
}, [elevenLabsApiKey]); }, [elevenLabsApiKey]);
const apiKeyOption = useMemo(() => ( const apiKeyOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: 'Your ElevenLabs Text-to-Speech API Key (optional)' })} <SettingsOption heading={intl.formatMessage({ defaultMessage: 'Your ElevenLabs Text-to-Speech API Key (optional)', description: "Heading for the ElevenLabs API key setting on the settings screen" })}
focused={option === 'elevenlabs-api-key'}> focused={option === 'elevenlabs-api-key'}>
<TextInput placeholder={intl.formatMessage({ defaultMessage: "Paste your API key here" })} <TextInput placeholder={intl.formatMessage({ defaultMessage: "Paste your API key here" })}
value={elevenLabsApiKey || ''} onChange={onElevenLabsApiKeyChange} /> value={elevenLabsApiKey || ''} onChange={onElevenLabsApiKeyChange} />
@ -50,7 +50,7 @@ export default function SpeechOptionsTab() {
), [option, elevenLabsApiKey, onElevenLabsApiKeyChange]); ), [option, elevenLabsApiKey, onElevenLabsApiKeyChange]);
const voiceOption = useMemo(() => ( const voiceOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: 'Voice' })} <SettingsOption heading={intl.formatMessage({ defaultMessage: 'Voice', description: 'Heading for the setting that lets users choose an ElevenLabs text-to-speech voice, on the settings screen' })}
focused={option === 'elevenlabs-voice'}> focused={option === 'elevenlabs-voice'}>
<Select <Select
value={voice} value={voice}
@ -64,7 +64,7 @@ export default function SpeechOptionsTab() {
<Button onClick={() => (document.getElementById('voice-preview') as HTMLMediaElement)?.play()} variant='light' compact style={{ marginTop: '1rem' }}> <Button onClick={() => (document.getElementById('voice-preview') as HTMLMediaElement)?.play()} variant='light' compact style={{ marginTop: '1rem' }}>
<i className='fa fa-headphones' /> <i className='fa fa-headphones' />
<span> <span>
<FormattedMessage defaultMessage="Preview voice" /> <FormattedMessage defaultMessage="Preview voice" description="Label for the button that plays a preview of the selected ElevenLabs text-to-speech voice" />
</span> </span>
</Button> </Button>
</SettingsOption> </SettingsOption>

View File

@ -17,7 +17,7 @@ export default function UserOptionsTab(props: any) {
const elem = useMemo(() => ( const elem = useMemo(() => (
<SettingsTab name="user"> <SettingsTab name="user">
<SettingsOption heading={intl.formatMessage({ defaultMessage: "Your OpenAI API Key" })} <SettingsOption heading={intl.formatMessage({ defaultMessage: "Your OpenAI API Key", description: "Heading for the OpenAI API key setting on the settings screen" })}
focused={option === 'openai-api-key'}> focused={option === 'openai-api-key'}>
<TextInput <TextInput
placeholder={intl.formatMessage({ defaultMessage: "Paste your API key here" })} placeholder={intl.formatMessage({ defaultMessage: "Paste your API key here" })}
@ -25,7 +25,7 @@ export default function UserOptionsTab(props: any) {
onChange={onOpenAIApiKeyChange} /> onChange={onOpenAIApiKeyChange} />
<p> <p>
<a href="https://platform.openai.com/account/api-keys" target="_blank" rel="noreferrer"> <a href="https://platform.openai.com/account/api-keys" target="_blank" rel="noreferrer">
<FormattedMessage defaultMessage="Find your API key here." /> <FormattedMessage defaultMessage="Find your API key here." description="Label for the link that takes the user to the page on the OpenAI website where they can find their API key." />
</a> </a>
</p> </p>
<p> <p>

View File

@ -2,7 +2,7 @@ import styled from '@emotion/styled';
import { ActionIcon, Avatar, Burger, Button, Menu } from '@mantine/core'; import { ActionIcon, Avatar, Burger, Button, Menu } from '@mantine/core';
import { useElementSize } from '@mantine/hooks'; import { useElementSize } from '@mantine/hooks';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useIntl } from 'react-intl'; import { FormattedMessage, useIntl } from 'react-intl';
import { backend } from '../../backend'; import { backend } from '../../backend';
import { useAppContext } from '../../context'; import { useAppContext } from '../../context';
import { useAppDispatch, useAppSelector } from '../../store'; import { useAppDispatch, useAppSelector } from '../../store';
@ -115,7 +115,7 @@ export default function Sidebar(props: {
const elem = useMemo(() => ( const elem = useMemo(() => (
<Container className={"sidebar " + (sidebarOpen ? 'opened' : 'closed')} ref={ref}> <Container className={"sidebar " + (sidebarOpen ? 'opened' : 'closed')} ref={ref}>
<div className="sidebar-header"> <div className="sidebar-header">
<h2>Chat History</h2> <h2><FormattedMessage defaultMessage={"Chat History"} description="Heading for the chat history screen" /></h2>
<Burger opened={sidebarOpen} onClick={onBurgerClick} aria-label={burgerLabel} transitionDuration={0} /> <Burger opened={sidebarOpen} onClick={onBurgerClick} aria-label={burgerLabel} transitionDuration={0} />
</div> </div>
<div className="sidebar-content"> <div className="sidebar-content">
@ -141,12 +141,12 @@ export default function Sidebar(props: {
<Menu.Item onClick={() => { <Menu.Item onClick={() => {
dispatch(setTab('user')); dispatch(setTab('user'));
}} icon={<i className="fas fa-gear" />}> }} icon={<i className="fas fa-gear" />}>
User settings <FormattedMessage defaultMessage={"User settings"} description="Menu item that opens the user settings screen" />
</Menu.Item> </Menu.Item>
{/* {/*
<Menu.Divider /> <Menu.Divider />
<Menu.Item color="red" onClick={() => backend.current?.logout()} icon={<i className="fas fa-sign-out-alt" />}> <Menu.Item color="red" onClick={() => backend.current?.logout()} icon={<i className="fas fa-sign-out-alt" />}>
Sign out <FormattedMessage defaultMessage={"Sign out"} />
</Menu.Item> </Menu.Item>
*/} */}
</Menu.Dropdown> </Menu.Dropdown>

View File

@ -1,5 +1,6 @@
import styled from '@emotion/styled'; import styled from '@emotion/styled';
import { useCallback, useEffect } from 'react'; import { useCallback, useEffect } from 'react';
import { FormattedMessage } from 'react-intl';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { useAppContext } from '../../context'; import { useAppContext } from '../../context';
import { useAppDispatch } from '../../store'; import { useAppDispatch } from '../../store';
@ -83,12 +84,12 @@ export default function RecentChats(props: any) {
onClick={onClick} onClick={onClick}
data-chat-id={c.chatID} data-chat-id={c.chatID}
className={c.chatID === currentChatID ? 'selected' : ''}> className={c.chatID === currentChatID ? 'selected' : ''}>
<strong>{c.title || 'Untitled'}</strong> <strong>{c.title || <FormattedMessage defaultMessage={"Untitled"} description="default title for untitled chat sessions" />}</strong>
</ChatListItem> </ChatListItem>
))} ))}
</ChatList>} </ChatList>}
{recentChats.length === 0 && <Empty> {recentChats.length === 0 && <Empty>
No chats yet. <FormattedMessage defaultMessage={"No chats yet."} description="Message shown on the Chat History screen for new users who haven't started their first chat session" />
</Empty>} </Empty>}
</Container> </Container>
); );

View File

@ -6,13 +6,12 @@ import { IntlProvider } from 'react-intl';
import { Provider } from 'react-redux'; import { Provider } from 'react-redux';
import { createBrowserRouter, RouterProvider } from "react-router-dom"; import { createBrowserRouter, RouterProvider } from "react-router-dom";
import { PersistGate } from 'redux-persist/integration/react'; import { PersistGate } from 'redux-persist/integration/react';
import AboutPage from './components/pages/about';
import ChatPage from './components/pages/chat';
import LandingPage from './components/pages/landing';
import { AppContextProvider } from './context'; import { AppContextProvider } from './context';
import store, { persistor } from './store'; import store, { persistor } from './store';
import ChatPage from './components/pages/chat';
import LandingPage from './components/pages/landing';
import './backend'; import './backend';
import './index.scss'; import './index.scss';
@ -41,12 +40,6 @@ const router = createBrowserRouter([
<ChatPage share={true} /> <ChatPage share={true} />
</AppContextProvider>, </AppContextProvider>,
}, },
{
path: "/about",
element: <AppContextProvider>
<AboutPage />
</AppContextProvider>,
},
]); ]);
const root = ReactDOM.createRoot( const root = ReactDOM.createRoot(
@ -54,11 +47,17 @@ const root = ReactDOM.createRoot(
); );
async function loadLocaleData(locale: string) { async function loadLocaleData(locale: string) {
const messages = await fetch(`/lang/${locale}.json`); const response = await fetch(`/lang/${locale}.json`);
if (!messages.ok) { if (!response.ok) {
throw new Error("Failed to load locale data"); throw new Error("Failed to load locale data");
} }
return messages.json() const messages: any = await response.json();
for (const key of Object.keys(messages)) {
if (typeof messages[key] !== 'string') {
messages[key] = messages[key].defaultMessage;
}
}
return messages;
} }
async function bootstrapApplication() { async function bootstrapApplication() {

View File

@ -2,7 +2,7 @@ import { createChatCompletion, defaultModel } from "./openai";
import { OpenAIMessage, Chat } from "./types"; import { OpenAIMessage, Chat } from "./types";
const systemPrompt = ` const systemPrompt = `
Please read the following exchange and write a short, concise title describing the topic. Please read the following exchange and write a short, concise title describing the topic (in the user's language).
`.trim(); `.trim();
const userPrompt = (user: string, assistant: string) => ` const userPrompt = (user: string, assistant: string) => `

View File

@ -272,13 +272,13 @@ export function ElevenLabsReaderButton(props: { selector: string }) {
<Button variant="subtle" size="sm" compact onClickCapture={onClick} loading={status === 'init'}> <Button variant="subtle" size="sm" compact onClickCapture={onClick} loading={status === 'init'}>
{status !== 'init' && <i className="fa fa-headphones" />} {status !== 'init' && <i className="fa fa-headphones" />}
{status === 'idle' && <span> {status === 'idle' && <span>
<FormattedMessage defaultMessage="Play" /> <FormattedMessage defaultMessage="Play" description="Label for the button that starts text-to-speech playback" />
</span>} </span>}
{status === 'buffering' && <span> {status === 'buffering' && <span>
<FormattedMessage defaultMessage="Loading audio..." /> <FormattedMessage defaultMessage="Loading audio..." description="Message indicating that text-to-speech audio is buffering" />
</span>} </span>}
{status !== 'idle' && status !== 'buffering' && <span> {status !== 'idle' && status !== 'buffering' && <span>
<FormattedMessage defaultMessage="Stop" /> <FormattedMessage defaultMessage="Stop" description="Label for the button that stops text-to-speech playback" />
</span>} </span>}
</Button> </Button>
); );