feat: incognito mode

This commit is contained in:
BetaHuhn 2023-09-01 17:12:05 +02:00
parent 85fbb117c4
commit 84375a0149
7 changed files with 230 additions and 61 deletions

View file

@ -2,7 +2,7 @@ import { notifications } from "@mantine/notifications";
import { Chat, detaDB, generateKey } from "../db";
import { Button } from "@mantine/core";
import { IconPlus } from "@tabler/icons-react";
import { useChats, useSettings } from "../hooks/contexts";
import { useChats, useIncognitoMode, useSettings } from "../hooks/contexts";
import { useNavigate } from "@tanstack/react-location";
import { ReactNode } from "react";
@ -11,6 +11,7 @@ export function CreateChatButton(props: { children: ReactNode, [x:string]: any }
const navigate = useNavigate();
const { settings } = useSettings()
const { setChats } = useChats()
const { incognitoMode } = useIncognitoMode()
const handleCreate = async () => {
if (!settings?.openAiApiKey) {
@ -23,9 +24,10 @@ export function CreateChatButton(props: { children: ReactNode, [x:string]: any }
};
const item = await detaDB.chats.put({
description: "New Chat",
description: incognitoMode ? "New Private Chat" : "New Chat",
prompt: null,
totalTokens: 0,
private: incognitoMode,
createdAt: new Date().toISOString(),
}, generateKey())

View file

@ -1,49 +1,91 @@
import { Button, Modal, Stack, Text } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import { IconTrash } from "@tabler/icons-react";
import { IconFlame } from "@tabler/icons-react";
import { Chat, Message, detaDB } from "../db";
import { notifications } from "@mantine/notifications";
import floating from "../utils/floating";
import { useChat, useChats } from "../hooks/contexts";
import { useNavigate } from "@tanstack/react-location";
export function DeleteChatsModal({ onOpen }: { onOpen: () => void }) {
export function DeleteChatsModal({ onOpen }: { onOpen?: () => void }) {
const [opened, { open, close }] = useDisclosure(false, { onOpen });
const navigate = useNavigate()
const { setChats } = useChats()
const { setChat } = useChat()
const deleteChats = async () => {
let res = await detaDB.chats.fetch({ private: true })
let chats = res.items;
while (res.last) {
res = await detaDB.chats.fetch({ private: true }, { last: res.last });
chats = chats.concat(res.items);
}
await Promise.all(chats.map(async (chat) => {
await detaDB.chats.delete((chat as unknown as Chat).key)
res = await detaDB.messages.fetch({ chatId: chat.key })
let messages = res.items;
while (res.last) {
res = await detaDB.messages.fetch({ chatId: chat.key }, { last: res.last })
messages = messages.concat(res.items);
}
await Promise.all(messages.map(async (message) => {
await detaDB.messages.delete((message as unknown as Message).key)
}))
}))
for (let i = 0; i < 100; i++) {
setTimeout(() => {
floating({
content: "🔥",
number: 2,
duration: 0.5,
repeat: 1,
elem: undefined
});
}, (i * 5) + (Math.random() * 10));
}
setChat(null)
setChats([])
navigate({ to: '/', replace: true })
notifications.show({
title: "Burned",
color: "green",
message: "Private chats deleted.",
});
close()
}
return (
<>
<Button
fullWidth
variant="light"
leftIcon={<IconFlame size={20} />}
onClick={open}
variant="outline"
color="red"
leftIcon={<IconTrash size={20} />}
>
Delete Chats
Burn Private Chats
</Button>
<Modal
opened={opened}
onClose={close}
title="Delete Chats"
title="Burn Private Chats"
size="md"
withinPortal
>
<Stack>
<Text size="sm">Are you sure you want to delete your chats?</Text>
<Text size="sm">Are you sure you want to delete all your private chats?</Text>
<Button
onClick={async () => {
// todo: handle pagination
const { items: chats } = await detaDB.chats.fetch()
await Promise.all(chats.map(async (chat) => {
await detaDB.chats.delete((chat as unknown as Chat).key)
}))
const { items: messages } = await detaDB.messages.fetch()
await Promise.all(messages.map(async (message) => {
await detaDB.messages.delete((message as unknown as Message).key)
}))
localStorage.clear();
window.location.assign("/");
}}
onClick={deleteChats}
color="red"
>
Delete
Burn Them
</Button>
</Stack>
</Modal>

View file

@ -3,6 +3,7 @@ import {
AppShell,
Box,
Burger,
Flex,
MediaQuery,
Navbar,
rem,
@ -20,7 +21,7 @@ import {
IconSpyOff,
IconX,
} from "@tabler/icons-react";
import { Link, Outlet, useRouter } from "@tanstack/react-location";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-location";
import { useEffect, useState } from "react";
import { Chat, detaDB, Prompt, Settings } from "../db";
import { useChatId } from "../hooks/useChatId";
@ -34,25 +35,22 @@ import { ChatContext, ChatsContext, IncognitoModeContext, PromptsContext, Settin
import { ChatHeader } from "./ChatHeader";
import { useLocalStorage } from "@mantine/hooks";
import { CreateChatButton } from "./CreateChatButton";
declare global {
interface Window {
todesktop?: any;
}
}
import { DeleteChatsModal } from "./DeleteChatsModal";
export function Layout() {
const theme = useMantineTheme();
const [opened, setOpened] = useState(false);
const [tab, setTab] = useState<"Chats" | "Prompts">("Chats");
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const navigate = useNavigate();
const router = useRouter();
const [search, setSearch] = useState("");
const { colorScheme, toggleColorScheme } = useMantineColorScheme();
const chatId = useChatId();
const [opened, setOpened] = useState(false);
const [tab, setTab] = useState<"Chats" | "Prompts">("Chats");
const [search, setSearch] = useState("");
const [incognitoMode, setIncognitoMode] = useLocalStorage({
key: 'incognito-mode', defaultValue: false
key: 'incognito-mode', defaultValue: false, getInitialValueInEffect: false
});
const [settings, setSettings] = useState<Settings | null>(null);
@ -93,19 +91,17 @@ export function Layout() {
const [chats, setChats] = useState<Chat[]>([]);
useEffect(() => {
// fetch data
const dataFetch = async () => {
const { items } = await detaDB.chats.fetch();
const { items } = await detaDB.chats.fetch(incognitoMode ? { private: true } : { 'private?ne': true });
setChats(items as unknown as Chat[]);
};
dataFetch();
}, []);
}, [incognitoMode]);
const [prompts, setPrompts] = useState<Prompt[]>([]);
useEffect(() => {
// fetch data
const dataFetch = async () => {
const { items } = await detaDB.prompts.fetch();
@ -124,16 +120,21 @@ export function Layout() {
}, [router.state.location]);
const handleIncognito = () => {
if (incognitoMode) {
setIncognitoMode(false)
if (colorScheme === 'dark') {
toggleColorScheme()
}
} else {
setIncognitoMode(true)
if (colorScheme !== 'dark') {
toggleColorScheme()
}
const newValue = !incognitoMode
// if we are in a chat that doesn't match the mode, navigate to home view
if (chat && (chat?.private ?? false) !== newValue) {
setChat(null)
navigate({ to: `/`, replace: true });
}
setIncognitoMode(newValue)
const isDark = colorScheme === 'dark'
if (newValue && !isDark) {
toggleColorScheme()
} else if (!newValue && isDark) {
toggleColorScheme()
}
}
@ -255,21 +256,26 @@ export function Layout() {
}
/>
</Navbar.Section>
<Navbar.Section grow component={ScrollArea}>
<Navbar.Section grow component={ScrollArea} id="chats">
{tab === "Chats" && <Chats search={search} />}
{tab === "Prompts" && (
<Prompts search={search} onPlay={() => setTab("Chats")} />
)}
</Navbar.Section>
<Navbar.Section>
<Box sx={{ padding: 10 }}>
<Flex direction="column" p={10} gap="xs">
{tab === "Chats" && (
<CreateChatButton fullWidth>
{incognitoMode ? "New Incognito Chat" : "New Chat"}
</CreateChatButton>
<>
{ incognitoMode && (
<DeleteChatsModal />
)}
<CreateChatButton fullWidth>
{incognitoMode ? "New Private Chat" : "New Chat"}
</CreateChatButton>
</>
)}
{tab === "Prompts" && <CreatePromptModal />}
</Box>
</Flex>
</Navbar.Section>
</Navbar>
}

View file

@ -13,6 +13,7 @@ export interface Chat {
writingStyle?: string | null;
writingFormat?: string | null;
model?: string | null;
private?: boolean;
createdAt: string;
}

View file

@ -198,7 +198,7 @@ export function ChatRoute() {
setChatStream(chatCompletionStream)
setSubmitting(false);
if (chat?.description === "New Chat") {
if (chat?.description === "New Chat" || chat?.description === "New Private Chat") {
const res = await detaDB.messages.fetch({ chatId })
const messages = res.items as unknown as Message[]
// const messages = await db.messages

View file

@ -17,12 +17,13 @@ import {
} from "@tabler/icons-react";
import { Logo } from "../components/Logo";
import { SettingsModal } from "../components/SettingsModal";
import { useSettings } from "../hooks/contexts";
import { useIncognitoMode, useSettings } from "../hooks/contexts";
import { CreateChatButton } from "../components/CreateChatButton";
export function IndexRoute() {
const { settings } = useSettings()
const theme = useMantineTheme()
const { incognitoMode } = useIncognitoMode()
return (
<>
@ -58,7 +59,7 @@ export function IndexRoute() {
<Flex mt={50} align='center' gap='md'>
{settings?.openAiApiKey && (
<CreateChatButton size="md">
Create a New Chat
{incognitoMode ? "Create a New Private Chat" : "Create a New Chat"}
</CreateChatButton>
)}
<SettingsModal>

117
src/utils/floating.ts Normal file
View file

@ -0,0 +1,117 @@
/**
* Float a number of things up on a page (hearts, flowers, 👌 ...)
* <br>
* You give the options in an object.
*
* @module floating
* @param {string} [options.content='👌']
* the character or string to float
* @param {number} [options.number=1]
* the number of items
* @param {number} [options.duration=10]
* the amount of seconds it takes to float up
* @param {number|string} [options.repeat='infinite']
* the number of times you want the animation to repeat
* @param {string} [options.direction='normal']
* The <a href="https://developer.mozilla.org/en-US/docs/Web/CSS/animation-direction">
* animation-direction</a> of the main animation
* @param {number|array} [options.sizes=2]
* The size (in em) of each element. Giving two values in an array will
* give a random size between those values.
*/
export default function floating(
{
content = '👌',
number = 1,
duration = 10,
repeat = 1,
direction = 'normal',
size = 2,
elem = document.body
} = {}
) {
const style = document.createElement('style');
style.id = 'floating-style';
if (!document.getElementById('floating-style')) {
document.head.appendChild(style);
}
const MAX = 201;
const styles = `
.float-container {
width: 100vw;
height: 100vh;
overflow: hidden;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
z-index: 999999;
}
.float-container div * {
width: 1em;
height: 1em
}
@keyframes float{
${Array.apply(null, { length: MAX + 1 })
.map((v, x) => ({
percent: x * 100 / MAX,
width: Math.sin(x),
height: 110 + x * (-120 / MAX),
}))
.map(
({ percent, width, height }) =>
`${percent}% {
transform: translate(
${width}vw,
${height}vh
)
}`
)
.join('')}
}`;
document.getElementById('floating-style').innerHTML = styles;
const container = document.createElement('div');
container.className = 'float-container';
const _size = Array.isArray(size)
? Math.floor(Math.random() * (size[1] - size[0] + 1)) + size[0]
: size;
for (let i = 0; i < number; i++) {
const floater = document.createElement('div');
floater.innerHTML = content;
floater.style.cssText = `
position: absolute;
left: 0;
font-size: ${_size}em;
transform: translateY(110vh);
animation:
float
${duration}s
linear
${i * Math.random()}s
${repeat}
${direction};
margin-left: ${Math.random() * 100}vw;`;
floater.addEventListener('animationend', e => {
if (e.animationName === 'float') {
container.removeChild(floater);
}
});
container.appendChild(floater);
}
elem.appendChild(container);
}