From 84375a01491159c859b7f8dc98317f1f71aff19f Mon Sep 17 00:00:00 2001 From: BetaHuhn Date: Fri, 1 Sep 2023 17:12:05 +0200 Subject: [PATCH] feat: incognito mode --- src/components/CreateChatButton.tsx | 6 +- src/components/DeleteChatsModal.tsx | 90 +++++++++++++++------ src/components/Layout.tsx | 70 +++++++++-------- src/db/index.ts | 1 + src/routes/ChatRoute.tsx | 2 +- src/routes/IndexRoute.tsx | 5 +- src/utils/floating.ts | 117 ++++++++++++++++++++++++++++ 7 files changed, 230 insertions(+), 61 deletions(-) create mode 100644 src/utils/floating.ts diff --git a/src/components/CreateChatButton.tsx b/src/components/CreateChatButton.tsx index 500c42f..6491061 100644 --- a/src/components/CreateChatButton.tsx +++ b/src/components/CreateChatButton.tsx @@ -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()) diff --git a/src/components/DeleteChatsModal.tsx b/src/components/DeleteChatsModal.tsx index b6ee133..3db2609 100644 --- a/src/components/DeleteChatsModal.tsx +++ b/src/components/DeleteChatsModal.tsx @@ -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 ( <> - Are you sure you want to delete your chats? + Are you sure you want to delete all your private chats? diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index b4f1c96..552d74f 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -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(null); @@ -93,19 +91,17 @@ export function Layout() { const [chats, setChats] = useState([]); 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([]); 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() { } /> - + {tab === "Chats" && } {tab === "Prompts" && ( setTab("Chats")} /> )} - + {tab === "Chats" && ( - - {incognitoMode ? "New Incognito Chat" : "New Chat"} - + <> + { incognitoMode && ( + + )} + + {incognitoMode ? "New Private Chat" : "New Chat"} + + )} {tab === "Prompts" && } - + } diff --git a/src/db/index.ts b/src/db/index.ts index 87b34af..39fcc91 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -13,6 +13,7 @@ export interface Chat { writingStyle?: string | null; writingFormat?: string | null; model?: string | null; + private?: boolean; createdAt: string; } diff --git a/src/routes/ChatRoute.tsx b/src/routes/ChatRoute.tsx index 7969588..4db949d 100644 --- a/src/routes/ChatRoute.tsx +++ b/src/routes/ChatRoute.tsx @@ -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 diff --git a/src/routes/IndexRoute.tsx b/src/routes/IndexRoute.tsx index 4d0a07c..1f143e2 100644 --- a/src/routes/IndexRoute.tsx +++ b/src/routes/IndexRoute.tsx @@ -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() { {settings?.openAiApiKey && ( - Create a New Chat + {incognitoMode ? "Create a New Private Chat" : "Create a New Chat"} )} diff --git a/src/utils/floating.ts b/src/utils/floating.ts new file mode 100644 index 0000000..4d3f815 --- /dev/null +++ b/src/utils/floating.ts @@ -0,0 +1,117 @@ +/** + * Float a number of things up on a page (hearts, flowers, 👌 ...) + *
+ * 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 + * animation-direction 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); + } + \ No newline at end of file