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
d4e6e53279
commit
207465955c
6 changed files with 283 additions and 189 deletions
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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 }}>
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
Loading…
Reference in a new issue