This commit is contained in:
Cogent Apps
2023-03-14 11:00:40 +00:00
parent 4a5e8c9e16
commit 645b66b988
104 changed files with 11064 additions and 1565 deletions

View File

@@ -0,0 +1,111 @@
import styled from '@emotion/styled';
import { Button, Drawer, Tabs } from "@mantine/core";
import { useMediaQuery } from '@mantine/hooks';
import { useCallback } from 'react';
import UserOptionsTab from './user';
import GenerationOptionsTab from './options';
import { useAppDispatch, useAppSelector } from '../../store';
import { closeSettingsUI, selectSettingsTab, setTab } from '../../store/settings-ui';
import SpeechOptionsTab from './speech';
const Container = styled.div`
padding: .4rem 1rem 1rem 1rem;
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
max-width: 100vw;
max-height: 100vh;
@media (max-width: 40em) {
padding: 0;
}
.mantine-Tabs-root {
display: flex;
flex-direction: column;
height: calc(100% - 3rem);
@media (max-width: 40em) {
height: calc(100% - 5rem);
}
}
.mantine-Tabs-tab {
padding: 1.2rem 1.618rem 0.8rem 1.618rem;
@media (max-width: 40em) {
padding: 1rem;
span {
display: none;
}
}
}
.mantine-Tabs-panel {
flex-grow: 1;
overflow-y: scroll;
overflow-x: hidden;
min-height: 0;
margin-left: 0;
padding: 1.2rem 0 3rem 0;
@media (max-width: 40em) {
padding: 1.2rem 1rem 3rem 1rem;
}
}
#save {
position: absolute;
bottom: 0;
left: 0;
right: 0;
padding: 0 1rem 1rem 1rem;
opacity: 1;
.mantine-Button-root {
height: 3rem;
}
}
`;
export interface SettingsDrawerProps {
}
export default function SettingsDrawer(props: SettingsDrawerProps) {
const tab = useAppSelector(selectSettingsTab);
const small = useMediaQuery('(max-width: 40em)');
const dispatch = useAppDispatch();
const close = useCallback(() => dispatch(closeSettingsUI()), [dispatch]);
const onTabChange = useCallback((tab: string) => dispatch(setTab(tab)), [dispatch]);
return (
<Drawer size="50rem"
position='right'
opened={!!tab}
onClose={close}
transition="slide-left"
transitionDuration={200}
withCloseButton={false}>
<Container>
<Tabs value={tab} onTabChange={onTabChange} style={{ margin: '0rem' }}>
<Tabs.List grow={small}>
<Tabs.Tab value="options">Options</Tabs.Tab>
<Tabs.Tab value="user">User</Tabs.Tab>
<Tabs.Tab value="speech">Speech</Tabs.Tab>
</Tabs.List>
<UserOptionsTab />
<GenerationOptionsTab />
<SpeechOptionsTab />
</Tabs>
<div id="save">
<Button variant="light" fullWidth size="md" onClick={close}>
Save and Close
</Button>
</div>
</Container>
</Drawer>
)
}

View File

@@ -0,0 +1,13 @@
export default function SettingsOption(props: {
focused?: boolean;
heading?: string;
children?: any;
span?: number;
}) {
return (
<section className={props.focused ? 'focused' : ''}>
{props.heading && <h3>{props.heading}</h3>}
{props.children}
</section>
);
}

View File

@@ -0,0 +1,63 @@
import SettingsTab from "./tab";
import SettingsOption from "./option";
import { Button, Slider, Textarea } from "@mantine/core";
import { useCallback, useMemo } from "react";
import { defaultSystemPrompt } from "../../openai";
import { useAppDispatch, useAppSelector } from "../../store";
import { resetSystemPrompt, selectSystemPrompt, selectTemperature, setSystemPrompt, setTemperature } from "../../store/parameters";
import { selectSettingsOption } from "../../store/settings-ui";
import { FormattedMessage, useIntl } from "react-intl";
export default function GenerationOptionsTab(props: any) {
const intl = useIntl();
const option = useAppSelector(selectSettingsOption);
const initialSystemPrompt = useAppSelector(selectSystemPrompt);
const temperature = useAppSelector(selectTemperature);
const dispatch = useAppDispatch();
const onSystemPromptChange = useCallback((event: React.ChangeEvent<HTMLTextAreaElement>) => dispatch(setSystemPrompt(event.target.value)), [dispatch]);
const onResetSystemPrompt = useCallback(() => dispatch(resetSystemPrompt()), [dispatch]);
const onTemperatureChange = useCallback((value: number) => dispatch(setTemperature(value)), [dispatch]);
const resettable = initialSystemPrompt
&& (initialSystemPrompt?.trim() !== defaultSystemPrompt.trim());
const systemPromptOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: "System Prompt" })}
focused={option === 'system-prompt'}>
<Textarea
value={initialSystemPrompt || defaultSystemPrompt}
onChange={onSystemPromptChange}
minRows={5}
maxRows={10}
autosize />
<p style={{ marginBottom: '0.7rem' }}>
<FormattedMessage 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."
values={{ code: chunk => <code style={{ whiteSpace: 'nowrap' }}>{chunk}</code> }} />
</p>
{resettable && <Button size="xs" compact variant="light" onClick={onResetSystemPrompt}>
<FormattedMessage defaultMessage="Reset to default" />
</Button>}
</SettingsOption>
), [option, initialSystemPrompt, resettable, onSystemPromptChange, onResetSystemPrompt]);
const temperatureOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: "Temperature: {temperature, number, ::.0}", }, { temperature })}
focused={option === 'temperature'}>
<Slider value={temperature} onChange={onTemperatureChange} step={0.1} min={0} max={1} precision={3} />
<p>
<FormattedMessage 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." />
</p>
</SettingsOption>
), [temperature, option, onTemperatureChange]);
const elem = useMemo(() => (
<SettingsTab name="options">
{systemPromptOption}
{temperatureOption}
</SettingsTab>
), [systemPromptOption, temperatureOption]);
return elem;
}

View File

@@ -0,0 +1,81 @@
import SettingsTab from "./tab";
import SettingsOption from "./option";
import { Button, Select, TextInput } from "@mantine/core";
import { useAppDispatch, useAppSelector } from "../../store";
import { selectElevenLabsApiKey, setElevenLabsApiKey } from "../../store/api-keys";
import { useCallback, useEffect, useMemo, useState } from "react";
import { selectVoice, setVoice } from "../../store/voices";
import { getVoices } from "../../tts/elevenlabs";
import { selectSettingsOption } from "../../store/settings-ui";
import { defaultVoiceList } from "../../tts/defaults";
import { FormattedMessage, useIntl } from "react-intl";
export default function SpeechOptionsTab() {
const intl = useIntl();
const option = useAppSelector(selectSettingsOption);
const elevenLabsApiKey = useAppSelector(selectElevenLabsApiKey);
const voice = useAppSelector(selectVoice);
const dispatch = useAppDispatch();
const onElevenLabsApiKeyChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => dispatch(setElevenLabsApiKey(event.target.value)), [dispatch]);
const onVoiceChange = useCallback((value: string) => dispatch(setVoice(value)), [dispatch]);
const [voices, setVoices] = useState<any[]>(defaultVoiceList);
useEffect(() => {
if (elevenLabsApiKey) {
getVoices().then(data => {
if (data?.voices?.length) {
setVoices(data.voices);
}
});
}
}, [elevenLabsApiKey]);
const apiKeyOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: 'Your ElevenLabs Text-to-Speech API Key (optional)' })}
focused={option === 'elevenlabs-api-key'}>
<TextInput placeholder={intl.formatMessage({ defaultMessage: "Paste your API key here" })}
value={elevenLabsApiKey || ''} onChange={onElevenLabsApiKeyChange} />
<p>
<FormattedMessage defaultMessage="Give ChatGPT a realisic human voice by connecting your ElevenLabs account (preview the available voices below). <a>Click here to sign up.</a>"
values={{
a: (chunks: any) => <a href="https://beta.elevenlabs.io" target="_blank" rel="noreferrer">{chunks}</a>
}} />
</p>
<p>
<FormattedMessage 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." />
</p>
</SettingsOption>
), [option, elevenLabsApiKey, onElevenLabsApiKeyChange]);
const voiceOption = useMemo(() => (
<SettingsOption heading={intl.formatMessage({ defaultMessage: 'Voice' })}
focused={option === 'elevenlabs-voice'}>
<Select
value={voice}
onChange={onVoiceChange}
data={[
...voices.map(v => ({ label: v.name, value: v.voice_id })),
]} />
<audio controls style={{ display: 'none' }} id="voice-preview" key={voice}>
<source src={voices.find(v => v.voice_id === voice)?.preview_url} type="audio/mpeg" />
</audio>
<Button onClick={() => (document.getElementById('voice-preview') as HTMLMediaElement)?.play()} variant='light' compact style={{ marginTop: '1rem' }}>
<i className='fa fa-headphones' />
<span>
<FormattedMessage defaultMessage="Preview voice" />
</span>
</Button>
</SettingsOption>
), [option, voice, voices, onVoiceChange]);
const elem = useMemo(() => (
<SettingsTab name="speech">
{apiKeyOption}
{voices.length > 0 && voiceOption}
</SettingsTab>
), [apiKeyOption, voiceOption, voices.length]);
return elem;
}

View File

@@ -0,0 +1,64 @@
import styled from "@emotion/styled";
import { Tabs } from "@mantine/core";
const Settings = styled.div`
font-family: "Work Sans", sans-serif;
color: white;
section {
margin-bottom: .618rem;
padding: 0.618rem;
h3 {
font-size: 1rem;
font-weight: bold;
margin-bottom: 1rem;
}
p {
line-height: 1.7;
margin-top: 0.8rem;
font-size: 1rem;
}
a {
color: white;
text-decoration : underline;
}
code {
font-family: "Fira Code", monospace;
}
}
.focused {
border: thin solid rgba(255, 255, 255, 0.1);
border-radius: 0.25rem;
animation: flash 3s;
}
@keyframes flash {
0% {
border-color: rgba(255, 0, 0, 0);
}
50% {
border-color: rgba(255, 0, 0, 1);
}
100% {
border-color: rgba(255, 255, 255, .1);
}
}
`;
export default function SettingsTab(props: {
name: string;
children?: any;
}) {
return (
<Tabs.Panel value={props.name}>
<Settings>
{props.children}
</Settings>
</Tabs.Panel>
);
}

View File

@@ -0,0 +1,42 @@
import SettingsTab from "./tab";
import SettingsOption from "./option";
import { TextInput } from "@mantine/core";
import { useCallback, useMemo } from "react";
import { useAppDispatch, useAppSelector } from "../../store";
import { selectOpenAIApiKey, setOpenAIApiKeyFromEvent } from "../../store/api-keys";
import { selectSettingsOption } from "../../store/settings-ui";
import { FormattedMessage, useIntl } from "react-intl";
export default function UserOptionsTab(props: any) {
const option = useAppSelector(selectSettingsOption);
const openaiApiKey = useAppSelector(selectOpenAIApiKey);
const intl = useIntl()
const dispatch = useAppDispatch();
const onOpenAIApiKeyChange = useCallback((event: React.ChangeEvent<HTMLInputElement>) => dispatch(setOpenAIApiKeyFromEvent(event)), [dispatch]);
const elem = useMemo(() => (
<SettingsTab name="user">
<SettingsOption heading={intl.formatMessage({ defaultMessage: "Your OpenAI API Key" })}
focused={option === 'openai-api-key'}>
<TextInput
placeholder={intl.formatMessage({ defaultMessage: "Paste your API key here" })}
value={openaiApiKey || ''}
onChange={onOpenAIApiKeyChange} />
<p>
<a href="https://platform.openai.com/account/api-keys" target="_blank" rel="noreferrer">
<FormattedMessage defaultMessage="Find your API key here." />
</a>
</p>
<p>
<FormattedMessage defaultMessage="Your API key is stored only on this device and never transmitted to anyone except OpenAI." />
</p>
<p>
<FormattedMessage defaultMessage="OpenAI API key usage is billed at a pay-as-you-go rate, separate from your ChatGPT subscription." />
</p>
</SettingsOption>
</SettingsTab>
), [option, openaiApiKey, onOpenAIApiKeyChange]);
return elem;
}