feat: incognito mode

This commit is contained in:
BetaHuhn 2023-08-31 18:12:26 +02:00
parent d4e6e53279
commit 207465955c
6 changed files with 283 additions and 189 deletions

View file

@ -6,7 +6,7 @@ import {
import { useHotkeys, useLocalStorage } from "@mantine/hooks";
import { Notifications } from "@mantine/notifications";
import {
createHashHistory,
createBrowserHistory,
ReactLocation,
Router,
} from "@tanstack/react-location";
@ -14,7 +14,7 @@ import { ChatRoute } from "../routes/ChatRoute";
import { IndexRoute } from "../routes/IndexRoute";
import { Layout } from "./Layout";
const history = createHashHistory();
const history = createBrowserHistory();
const location = new ReactLocation({ history });
export function App() {

View file

@ -6,10 +6,12 @@ import { EditChatModal } from "./EditChatModal";
import { MainLink } from "./MainLink";
import { useHover } from "@mantine/hooks";
import { Chat } from "../db";
import { useIncognitoMode } from "../hooks/contexts";
export function ChatItem({ chat, active = false }: { chat: Chat, active?: boolean }) {
const { hovered, ref } = useHover();
const theme = useMantineTheme();
const { incognitoMode } = useIncognitoMode()
return (
<Flex
@ -24,6 +26,13 @@ export function ChatItem({ chat, active = false }: { chat: Chat, active?: boolea
? theme.colors.dark[6]
: theme.colors.gray[1],
},
...(incognitoMode ? {
filter: 'blur(2.5px)',
transition: 'filter 300ms ease-out',
"&:hover, &.active": {
filter: 'none'
}
} : {})
})}
>
<Link to={`/chats/${chat.key}`} style={{ flex: 1 }}>

View file

@ -27,25 +27,24 @@ export function EditChatModal({
const [writingTone, setWritingTone] = useState<string | null>(null);
const [writingStyle, setWritingStyle] = useState<string | null>(null);
const [writingFormat, setWritingFormat] = useState<string | null>(null);
const [model, setModel] = useState<string>('default');
const [value, setValue] = useState("");
useEffect(() => {
setValue(chat?.description ?? "");
setModel(chat.model ?? 'default')
setWritingInstructions(chat.writingInstructions ?? null)
setWritingCharacter(chat.writingCharacter ?? null)
setWritingTone(chat.writingTone ?? null)
setWritingStyle(chat.writingStyle ?? null)
setWritingFormat(chat.writingFormat ?? null)
if (chat.model) setModel(chat.model)
if (chat.prompt) {
setPromptKey(chat.prompt)
} else {
setPromptKey(null)
if (chat.writingInstructions) setWritingInstructions(chat.writingInstructions)
if (chat.writingCharacter) setWritingCharacter(chat.writingCharacter)
if (chat.writingTone) setWritingTone(chat.writingTone)
if (chat.writingStyle) setWritingStyle(chat.writingStyle)
if (chat.writingFormat) setWritingFormat(chat.writingFormat)
}
}, [chat]);
}, [chat, opened]);
useEffect(() => {
const prompt = prompts.find(prompt => prompt.key === promptKey)
@ -73,6 +72,17 @@ export function EditChatModal({
}
}, [writingInstructions, writingCharacter, writingFormat, writingStyle, writingTone]);
// const handleClose = () => {
// close()
// setPromptKey(null)
// setWritingInstructions(null)
// setWritingCharacter(null)
// setWritingTone(null)
// setWritingStyle(null)
// setWritingFormat(null)
// setModel('default')
// }
return (
<>
{cloneElement(children, { onClick: open })}

View file

@ -18,6 +18,8 @@ import {
IconPlus,
IconSearch,
IconSettings,
IconSpy,
IconSpyOff,
IconX,
} from "@tabler/icons-react";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-location";
@ -30,9 +32,10 @@ import { LogoText } from "./Logo";
import { Prompts } from "./Prompts";
import { SettingsModal } from "./SettingsModal";
import { config } from "../utils/config";
import { ChatContext, ChatsContext, PromptsContext, SettingsContext } from "../hooks/contexts";
import { ChatContext, ChatsContext, IncognitoModeContext, PromptsContext, SettingsContext } from "../hooks/contexts";
import { ChatHeader } from "./ChatHeader";
import { notifications } from "@mantine/notifications";
import { useLocalStorage } from "@mantine/hooks";
declare global {
interface Window {
@ -44,13 +47,17 @@ export function Layout() {
const theme = useMantineTheme();
const [opened, setOpened] = useState(false);
const [tab, setTab] = useState<"Chats" | "Prompts">("Chats");
const { colorScheme } = useMantineColorScheme();
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const navigate = useNavigate();
const router = useRouter();
const [search, setSearch] = useState("");
const chatId = useChatId();
const [incognitoMode, setIncognitoMode] = useLocalStorage({
key: 'incognito-mode', defaultValue: false
});
const [settings, setSettings] = useState<Settings | null>(null);
useEffect(() => {
const dataFetch = async () => {
@ -119,172 +126,209 @@ export function Layout() {
setOpened(false);
}, [router.state.location]);
const handleIncognito = () => {
if (incognitoMode) {
setIncognitoMode(false)
if (colorScheme === 'dark') {
toggleColorScheme()
}
} else {
setIncognitoMode(true)
if (colorScheme !== 'dark') {
toggleColorScheme()
}
}
}
return (
<SettingsContext.Provider value={{ settings: settings, setSettings: setSettings }}>
<ChatContext.Provider value={{ chat: chat, setChat: setChat }}>
<ChatsContext.Provider value={{ chats: chats, setChats: setChats }}>
<PromptsContext.Provider value={{ prompts: prompts, setPrompts: setPrompts }}>
<AppShell
className={`${colorScheme}-theme`}
navbarOffsetBreakpoint="sm"
navbar={
<Navbar width={{ md: 300 }} hiddenBreakpoint="md" hidden={!opened}>
<Navbar.Section className="app-region-drag">
<Box
style={{
height: 60,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: 10,
borderBottom: border,
}}
>
<Link
to="/"
className="app-region-no-drag"
style={{ marginTop: 10, padding: 4 }}
<IncognitoModeContext.Provider value={{ incognitoMode: incognitoMode, setIncognitoMode: setIncognitoMode }}>
<AppShell
className={`${colorScheme}-theme`}
navbarOffsetBreakpoint="sm"
navbar={
<Navbar width={{ md: 300 }} hiddenBreakpoint="md" hidden={!opened}>
<Navbar.Section className="app-region-drag">
<Box
style={{
height: 60,
display: "flex",
alignItems: "center",
justifyContent: "space-between",
padding: 10,
borderBottom: border,
}}
>
<LogoText
style={{
height: 22,
color: theme.colors[theme.primaryColor][6],
display: "block",
}}
/>
</Link>
<SettingsModal>
<Tooltip label="Settings">
<ActionIcon
size="xl"
sx={(theme) => ({
[theme.fn.smallerThan('md')]: {
marginRight: '2.5rem',
},
})}
>
<IconSettings size={20} />
</ActionIcon>
</Tooltip>
</SettingsModal>
</Box>
</Navbar.Section>
<Navbar.Section
sx={(theme) => ({
padding: rem(4),
background:
theme.colorScheme === "dark"
? theme.colors.dark[8]
: theme.colors.gray[1],
borderBottom: border,
})}
>
<SegmentedControl
fullWidth
value={tab}
onChange={(value) => setTab(value as typeof tab)}
data={["Chats", "Prompts"]}
/>
</Navbar.Section>
<Navbar.Section
sx={(theme) => ({
padding: rem(4),
background:
theme.colorScheme === "dark"
? theme.colors.dark[7]
: theme.white,
borderBottom: border,
})}
>
<TextInput
variant="unstyled"
radius={0}
placeholder="Search"
value={search}
onChange={(event) =>
setSearch(event.currentTarget.value.toLowerCase())
}
sx={{ paddingInline: 4 }}
icon={<IconSearch opacity={0.8} size={20} />}
rightSection={
!!search && (
<ActionIcon onClick={() => setSearch("")}>
<IconX opacity={0.5} size={20} />{" "}
</ActionIcon>
)
}
/>
</Navbar.Section>
<Navbar.Section grow component={ScrollArea}>
{tab === "Chats" && <Chats search={search} />}
{tab === "Prompts" && (
<Prompts search={search} onPlay={() => setTab("Chats")} />
)}
</Navbar.Section>
<Navbar.Section>
<Box sx={{ padding: 10 }}>
{tab === "Chats" && (
<Button
fullWidth
leftIcon={<IconPlus size={20} />}
onClick={async () => {
if (!settings?.openAiApiKey) {
notifications.show({
title: "Error",
color: "red",
message: "OpenAI API Key is not defined. Please set your API Key",
});
return;
};
const item = await detaDB.chats.put({
description: "New Chat",
prompt: null,
totalTokens: 0,
createdAt: new Date().toISOString(),
}, generateKey())
setChats(chats => ([...(chats || []), item as unknown as Chat]))
const id = item!.key as string
// const id = nanoid();
// db.chats.add({
// id,
// description: "New Chat",
// totalTokens: 0,
// createdAt: new Date(),
// });
navigate({ to: `/chats/${id}`, replace: true });
}}
<Link
to="/"
className="app-region-no-drag"
style={{ marginTop: 10, padding: 4 }}
>
New Chat
</Button>
<LogoText
style={{
height: 22,
color: theme.colors[theme.primaryColor][6],
display: "block",
}}
/>
</Link>
<Box style={{ display: 'flex', alignItems: 'center' }}>
{incognitoMode ? (
<Tooltip label="Disable Incognito Mode">
<ActionIcon
size="xl"
onClick={handleIncognito}
>
<IconSpyOff size={20} />
</ActionIcon>
</Tooltip>
) : (
<Tooltip label="Incognito Mode">
<ActionIcon
size="xl"
onClick={handleIncognito}
>
<IconSpy size={20} />
</ActionIcon>
</Tooltip>
)}
<SettingsModal>
<Tooltip label="Settings">
<ActionIcon
size="xl"
sx={(theme) => ({
[theme.fn.smallerThan('md')]: {
marginRight: '2.5rem',
},
})}
>
<IconSettings size={20} />
</ActionIcon>
</Tooltip>
</SettingsModal>
</Box>
</Box>
</Navbar.Section>
<Navbar.Section
sx={(theme) => ({
padding: rem(4),
background:
theme.colorScheme === "dark"
? theme.colors.dark[8]
: theme.colors.gray[1],
borderBottom: border,
})}
>
<SegmentedControl
fullWidth
value={tab}
onChange={(value) => setTab(value as typeof tab)}
data={["Chats", "Prompts"]}
/>
</Navbar.Section>
<Navbar.Section
sx={(theme) => ({
padding: rem(4),
background:
theme.colorScheme === "dark"
? theme.colors.dark[7]
: theme.white,
borderBottom: border,
})}
>
<TextInput
variant="unstyled"
radius={0}
placeholder="Search"
value={search}
onChange={(event) =>
setSearch(event.currentTarget.value.toLowerCase())
}
sx={{ paddingInline: 4 }}
icon={<IconSearch opacity={0.8} size={20} />}
rightSection={
!!search && (
<ActionIcon onClick={() => setSearch("")}>
<IconX opacity={0.5} size={20} />{" "}
</ActionIcon>
)
}
/>
</Navbar.Section>
<Navbar.Section grow component={ScrollArea}>
{tab === "Chats" && <Chats search={search} />}
{tab === "Prompts" && (
<Prompts search={search} onPlay={() => setTab("Chats")} />
)}
{tab === "Prompts" && <CreatePromptModal />}
</Box>
</Navbar.Section>
</Navbar>
}
header={
chat ? (
<ChatHeader />
) : undefined
}
layout="alt"
padding={0}
>
<MediaQuery largerThan="md" styles={{ display: "none" }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size="sm"
color={theme.colors.gray[6]}
className="app-region-no-drag"
sx={{ position: "fixed", top: 16, right: 16, zIndex: 100 }}
/>
</MediaQuery>
<Outlet />
</AppShell>
</Navbar.Section>
<Navbar.Section>
<Box sx={{ padding: 10 }}>
{tab === "Chats" && (
<Button
fullWidth
leftIcon={<IconPlus size={20} />}
onClick={async () => {
if (!settings?.openAiApiKey) {
notifications.show({
title: "Error",
color: "red",
message: "OpenAI API Key is not defined. Please set your API Key",
});
return;
};
const item = await detaDB.chats.put({
description: "New Chat",
prompt: null,
totalTokens: 0,
createdAt: new Date().toISOString(),
}, generateKey())
setChats(chats => ([...(chats || []), item as unknown as Chat]))
const id = item!.key as string
// const id = nanoid();
// db.chats.add({
// id,
// description: "New Chat",
// totalTokens: 0,
// createdAt: new Date(),
// });
navigate({ to: `/chats/${id}`, replace: true });
}}
>
{incognitoMode ? "New Incognito Chat" : "New Chat"}
</Button>
)}
{tab === "Prompts" && <CreatePromptModal />}
</Box>
</Navbar.Section>
</Navbar>
}
header={
chat ? (
<ChatHeader />
) : undefined
}
layout="alt"
padding={0}
>
<MediaQuery largerThan="md" styles={{ display: "none" }}>
<Burger
opened={opened}
onClick={() => setOpened((o) => !o)}
size="sm"
color={theme.colors.gray[6]}
className="app-region-no-drag"
sx={{ position: "fixed", top: 16, right: 16, zIndex: 100 }}
/>
</MediaQuery>
<Outlet />
</AppShell>
</IncognitoModeContext.Provider>
</PromptsContext.Provider>
</ChatsContext.Provider>
</ChatContext.Provider>

View file

@ -3,13 +3,16 @@ import {
Anchor,
Button,
Flex,
List,
Modal,
PasswordInput,
TextInput,
Select,
Stack,
Text,
useMantineColorScheme,
Box,
SegmentedControl,
Center,
} from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { notifications } from "@mantine/notifications";
@ -18,6 +21,7 @@ import { Settings, detaDB } from "../db";
import { config } from "../utils/config";
import { checkOpenAIKey } from "../utils/openai";
import { useSettings } from "../hooks/contexts";
import { IconMoonStars, IconSunHigh } from "@tabler/icons-react";
export function SettingsModal({ children }: { children: ReactElement }) {
const [opened, { open, close }] = useDisclosure(false);
@ -31,6 +35,7 @@ export function SettingsModal({ children }: { children: ReactElement }) {
const [version, setVersion] = useState("");
const { settings, setSettings } = useSettings()
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
useEffect(() => {
if (settings?.openAiApiKey) {
@ -58,6 +63,33 @@ export function SettingsModal({ children }: { children: ReactElement }) {
{cloneElement(children, { onClick: open })}
<Modal opened={opened} onClose={close} title="Settings" size="lg">
<Stack>
<Box>
<Text size="sm" weight={500} mb={4}>Theme</Text>
<SegmentedControl
value={colorScheme}
onChange={() => toggleColorScheme()}
data={[
{
value: 'light',
label: (
<Center>
<IconSunHigh size="1rem" />
<Box ml={10}>Light</Box>
</Center>
),
},
{
value: 'dark',
label: (
<Center>
<IconMoonStars size="1rem" />
<Box ml={10}>Dark</Box>
</Center>
),
},
]}
/>
</Box>
<form
onSubmit={async (event) => {
try {
@ -127,24 +159,14 @@ export function SettingsModal({ children }: { children: ReactElement }) {
</Button>
</Flex>
</form>
<List withPadding>
<List.Item>
<Text size="sm">
<Anchor
href="https://platform.openai.com/account/api-keys"
target="_blank"
>
Get your OpenAI API key
</Anchor>
</Text>
</List.Item>
<List.Item>
<Text size="sm" color="dimmed">
The API Key is stored locally on your browser and never sent
anywhere else.
</Text>
</List.Item>
</List>
<Text size="sm">
<Anchor
href="https://platform.openai.com/account/api-keys"
target="_blank"
>
Get your OpenAI API key
</Anchor>
</Text>
<Select
label="OpenAI Type"
value={type}

View file

@ -36,3 +36,12 @@ export const SettingsContext = React.createContext<{
export function useSettings() {
return useContext(SettingsContext);
}
export const IncognitoModeContext = React.createContext<{
incognitoMode: boolean,
setIncognitoMode: React.Dispatch<React.SetStateAction<boolean>>
}>({ incognitoMode: false, setIncognitoMode: () => {} });
export function useIncognitoMode() {
return useContext(IncognitoModeContext);
}