mirror of
https://github.com/deiucanta/chatpad.git
synced 2026-03-11 09:04:31 +00:00
feat: incognito mode
This commit is contained in:
parent
85fbb117c4
commit
84375a0149
7 changed files with 230 additions and 61 deletions
|
|
@ -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())
|
||||
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,7 @@ export interface Chat {
|
|||
writingStyle?: string | null;
|
||||
writingFormat?: string | null;
|
||||
model?: string | null;
|
||||
private?: boolean;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
117
src/utils/floating.ts
Normal 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);
|
||||
}
|
||||
|
||||
Loading…
Reference in a new issue