diff --git a/Discovery.md b/Discovery.md index 510e849..cc91940 100644 --- a/Discovery.md +++ b/Discovery.md @@ -1,5 +1,5 @@ --- -app_name: "Dialogue AI" +app_name: "Dialogue" tagline: "An open-source ChatGPT UI alternative" git: "https://github.com/deta/dialogue" theme_color: "#F76705" @@ -19,4 +19,4 @@ ported_from: "https://github.com/deiucanta/chatpad" ## Credits -Dialogue AI is a fork of [Chatpad AI](https://github.com/deiucanta/chatpad) with modfications to make it run on Deta Space and various changes and new features like "Incognito Mode", shared chats and a reworked prompts system. It is built on top of the amazing work done by [@deiucanta](https://github.com/deiucanta) and contributors. \ No newline at end of file +Dialogue is a fork of [Chatpad AI](https://github.com/deiucanta/chatpad) with modfications to make it run on Deta Space and various changes and new features like "Incognito Mode", shared chats and a reworked prompts system. It is built on top of the amazing work done by [@deiucanta](https://github.com/deiucanta) and contributors. diff --git a/README.md b/README.md index 4644f6f..3661e9f 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ icon -# Dialogue AI +# Dialogue An open-source ChatGPT UI alternative. Fork of [Chatpad AI](https://github.com/deiucanta/chatpad) @@ -33,4 +33,4 @@ An open-source ChatGPT UI alternative. Fork of [Chatpad AI](https://github.com/d ## Credits -Dialogue AI is a fork of [Chatpad AI](https://github.com/deiucanta/chatpad) with modfications to make it run on Deta Space and various changes and new features like "Incognito Mode", shared chats and a reworked prompts system. It is built on top of the amazing work done by [@deiucanta](https://github.com/deiucanta) and contributors. +Dialogue is a fork of [Chatpad AI](https://github.com/deiucanta/chatpad) with modfications to make it run on Deta Space and various changes and new features like "Incognito Mode", shared chats and a reworked prompts system. It is built on top of the amazing work done by [@deiucanta](https://github.com/deiucanta) and contributors. diff --git a/index.html b/index.html index 2ff269e..010d68a 100644 --- a/index.html +++ b/index.html @@ -2,7 +2,7 @@ - Dialogue AI + Dialogue ([]); + const [spotlightActions, setSpotlightActions] = useState([]); - useEffect(() => { - const dataFetch = async () => { - const res = await fetch(`/api/space/actions`) - const items = await res.json() - const actions = items as unknown as AppAction[] + useEffect(() => { + const dataFetch = async () => { + const res = await fetch(`/api/space/actions`); + const items = await res.json(); + const actions = items as unknown as AppAction[]; - setSpaceAppActions(actions) + setSpaceAppActions(actions); - const spotlightActionsItems = actions.map(action => ({ - title: action.title, - description: action.instance_alias, - icon: action.icon_url ? {action.title} : , - onTrigger: async () => { - try { - if (action) { - const titleKeys = ['title', 'name'] - const contentKeys = ['content', 'description', 'text'] - - let payload: { [key: string]: string } = {} - for (const input of action.input!) { - if (titleKeys.includes(input.name)) { - payload[input.name] = chat?.description || 'Dialogue AI Chat' - } else if (contentKeys.includes(input.name)) { - payload[input.name] = action.app_name === 'Minima' ? contentHtml : contentText - } else if (input.name === 'html') { - payload[input.name] = contentHtml - } - } + const spotlightActionsItems = actions.map((action) => ({ + title: action.title, + description: action.instance_alias, + icon: action.icon_url ? ( + {action.title} + ) : ( + + ), + onTrigger: async () => { + try { + if (action) { + const titleKeys = ["title", "name"]; + const contentKeys = ["content", "description", "text"]; - const res = await fetch(`/api/space/actions/invoke`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json' - }, - body: JSON.stringify({ - instanceId: action.instance_id, - actionName: action.name, - payload: payload - }) - }) + let payload: { [key: string]: string } = {}; + for (const input of action.input!) { + if (titleKeys.includes(input.name)) { + payload[input.name] = chat?.description || "Dialogue Chat"; + } else if (contentKeys.includes(input.name)) { + payload[input.name] = + action.app_name === "Minima" ? contentHtml : contentText; + } else if (input.name === "html") { + payload[input.name] = contentHtml; + } + } - const invocation = await res.json() - console.log(invocation) - if (invocation.type === '@deta/raw') { - navigator.clipboard.writeText(invocation.data) - } else if (invocation.type === '@deta/detail') { - navigator.clipboard.writeText(invocation.data.ref || invocation.data.url || invocation.data.title || invocation.data.text) - } else { - navigator.clipboard.writeText(JSON.stringify(invocation.data)) - } - - notifications.show({ - title: "Success", - color: "green", - message: `Action "${action.title}" invoked successfully.`, - }) - } else { - notifications.show({ - title: "Error", - color: "red", - message: "Integration not found.", - }); - } - } catch (error: any) { - if (error.toJSON().message === "Network Error") { - notifications.show({ - title: "Error", - color: "red", - message: "No internet connection.", - }); - } - const message = error.response?.data?.error?.message; - if (message) { - notifications.show({ - title: "Error", - color: "red", - message, - }); - } - } + const res = await fetch(`/api/space/actions/invoke`, { + method: "POST", + headers: { + "Content-Type": "application/json", }, - })) + body: JSON.stringify({ + instanceId: action.instance_id, + actionName: action.name, + payload: payload, + }), + }); - setSpotlightActions(spotlightActionsItems) - spotlight.open() - } - if (open) { - dataFetch() - } - }, [open]) + const invocation = await res.json(); + console.log(invocation); + if (invocation.type === "@deta/raw") { + navigator.clipboard.writeText(invocation.data); + } else if (invocation.type === "@deta/detail") { + navigator.clipboard.writeText( + invocation.data.ref || + invocation.data.url || + invocation.data.title || + invocation.data.text + ); + } else { + navigator.clipboard.writeText(JSON.stringify(invocation.data)); + } - const handleOpen = () => { - setOpen(true) + notifications.show({ + title: "Success", + color: "green", + message: `Action "${action.title}" invoked successfully.`, + }); + } else { + notifications.show({ + title: "Error", + color: "red", + message: "Integration not found.", + }); + } + } catch (error: any) { + if (error.toJSON().message === "Network Error") { + notifications.show({ + title: "Error", + color: "red", + message: "No internet connection.", + }); + } + const message = error.response?.data?.error?.message; + if (message) { + notifications.show({ + title: "Error", + color: "red", + message, + }); + } + } + }, + })); + + setSpotlightActions(spotlightActionsItems); + spotlight.open(); + }; + if (open) { + dataFetch(); } + }, [open]); - return ( - <> - {cloneElement(children, { onClick: handleOpen })} - {open && ( - } - searchPlaceholder="Search Space App Actions..." - shortcut={null} - nothingFoundMessage="Nothing found..." - onSpotlightOpen={() => setOpen(true)} - onSpotlightClose={() => setOpen(false)} - limit={Infinity} - styles={{ - 'content': { - border: '4px solid #dbdbdb88' - }, - 'searchInput': { - 'border-bottom': '3px solid #dbdbdb54 !important', - } - }} - > - - )} - {/* + const handleOpen = () => { + setOpen(true); + }; + + return ( + <> + {cloneElement(children, { onClick: handleOpen })} + {open && ( + } + searchPlaceholder="Search Space App Actions..." + shortcut={null} + nothingFoundMessage="Nothing found..." + onSpotlightOpen={() => setOpen(true)} + onSpotlightClose={() => setOpen(false)} + limit={Infinity} + styles={{ + content: { + border: "4px solid #dbdbdb88", + }, + searchInput: { + "border-bottom": "3px solid #dbdbdb54 !important", + }, + }} + > + )} + {/*
{ try { @@ -162,7 +176,7 @@ export function SelectIntegrationModal({ let payload: { [key: string]: string } = {} for (const input of action.input!) { if (titleKeys.includes(input.name)) { - payload[input.name] = chat?.description || 'Dialogue AI Chat' + payload[input.name] = chat?.description || 'Dialogue Chat' } else if (contentKeys.includes(input.name)) { payload[input.name] = contentHtml } @@ -234,6 +248,6 @@ export function SelectIntegrationModal({
*/} - - ); + + ); } diff --git a/src/layouts/Base.tsx b/src/layouts/Base.tsx index 07c896a..7b3891e 100644 --- a/src/layouts/Base.tsx +++ b/src/layouts/Base.tsx @@ -1,28 +1,28 @@ import { - ActionIcon, - AppShell, - Box, - Burger, - Flex, - MediaQuery, - Navbar, - rem, - ScrollArea, - SegmentedControl, - TextInput, - Tooltip, - Text, - useMantineColorScheme, - useMantineTheme, - Skeleton, - Stack, + ActionIcon, + AppShell, + Box, + Burger, + Flex, + MediaQuery, + Navbar, + rem, + ScrollArea, + SegmentedControl, + TextInput, + Tooltip, + Text, + useMantineColorScheme, + useMantineTheme, + Skeleton, + Stack, } from "@mantine/core"; import { - IconSearch, - IconSettings, - IconSpy, - IconSpyOff, - IconX, + IconSearch, + IconSettings, + IconSpy, + IconSpyOff, + IconX, } from "@tabler/icons-react"; import { Link, useNavigate, useRouter } from "@tanstack/react-location"; import { Suspense, useEffect, useState } from "react"; @@ -33,326 +33,358 @@ import { CreatePromptModal } from "../components/CreatePromptModal"; import { LogoIcon } from "../components/Logo"; import { SettingsModal } from "../components/SettingsModal"; import { config } from "../utils/config"; -import { ChatContext, ChatsContext, IncognitoModeContext, PromptsContext, SettingsContext, SpaceAppActionsContext } from "../hooks/contexts"; +import { + ChatContext, + ChatsContext, + IncognitoModeContext, + PromptsContext, + SettingsContext, + SpaceAppActionsContext, +} from "../hooks/contexts"; import { ChatHeader } from "../components/ChatHeader"; import { useLocalStorage } from "@mantine/hooks"; import { CreateChatButton } from "../components/CreateChatButton"; import { DeleteChatsModal } from "../components/DeleteChatsModal"; import React from "react"; -const Prompts = React.lazy(() => import('../components/Prompts')); +const Prompts = React.lazy(() => import("../components/Prompts")); export function BaseLayout({ children }: { children: React.ReactNode }) { - const theme = useMantineTheme(); - const navigate = useNavigate(); - const router = useRouter(); + const theme = useMantineTheme(); + const navigate = useNavigate(); + const router = useRouter(); - const { colorScheme, toggleColorScheme } = useMantineColorScheme(); - const chatId = useChatId(); + const { colorScheme, toggleColorScheme } = useMantineColorScheme(); + const chatId = useChatId(); - const [opened, setOpened] = useState(false); - const [tab, setTab] = useState<"Chats" | "Prompts">("Chats"); - const [search, setSearch] = useState(""); + 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, getInitialValueInEffect: false - }); + const [incognitoMode, setIncognitoMode] = useLocalStorage({ + key: "incognito-mode", + defaultValue: false, + getInitialValueInEffect: false, + }); - const [settings, setSettings] = useState(null); - useEffect(() => { - const dataFetch = async () => { - let item = await detaDB.settings.get('general'); + const [settings, setSettings] = useState(null); + useEffect(() => { + const dataFetch = async () => { + let item = await detaDB.settings.get("general"); - if (!item) { - item = await detaDB.settings.put({ - key: "general", - openAiModel: config.defaultModel, - openAiApiType: config.defaultType, - openAiApiAuth: config.defaultAuth, - ...(config.defaultKey != '' && { openAiApiKey: config.defaultKey }), - ...(config.defaultBase != '' && { openAiApiBase: config.defaultBase }), - ...(config.defaultVersion != '' && { openAiApiVersion: config.defaultVersion }), - }, 'general'); - } + if (!item) { + item = await detaDB.settings.put( + { + key: "general", + openAiModel: config.defaultModel, + openAiApiType: config.defaultType, + openAiApiAuth: config.defaultAuth, + ...(config.defaultKey != "" && { openAiApiKey: config.defaultKey }), + ...(config.defaultBase != "" && { openAiApiBase: config.defaultBase }), + ...(config.defaultVersion != "" && { + openAiApiVersion: config.defaultVersion, + }), + }, + "general" + ); + } - setSettings(item as unknown as Settings); - }; + setSettings(item as unknown as Settings); + }; - dataFetch(); - }, []); + dataFetch(); + }, []); - const [spaceAppActions, setSpaceAppActions] = useState(null); - useEffect(() => { - const dataFetch = async () => { - const res = await fetch(`/api/space/actions/config`) - const config = await res.json() + const [spaceAppActions, setSpaceAppActions] = useState(null); + useEffect(() => { + const dataFetch = async () => { + const res = await fetch(`/api/space/actions/config`); + const config = await res.json(); - if (config.isSetup) { - setSpaceAppActions([]) - } - }; + if (config.isSetup) { + setSpaceAppActions([]); + } + }; - dataFetch(); - }, []); + dataFetch(); + }, []); - const [chat, setChat] = useState(null); - useEffect(() => { - const dataFetch = async () => { - const item = await detaDB.chats.get(chatId!); - const fetchedChat = item as unknown as Chat + const [chat, setChat] = useState(null); + useEffect(() => { + const dataFetch = async () => { + const item = await detaDB.chats.get(chatId!); + const fetchedChat = item as unknown as Chat; - setChat(fetchedChat); + setChat(fetchedChat); - if (fetchedChat.private && !incognitoMode) { - setIncognitoMode(true) - if (colorScheme === 'light') { - toggleColorScheme() - } - } else if (!(fetchedChat.private ?? false) && incognitoMode) { - setIncognitoMode(false) - if (colorScheme === 'dark') { - toggleColorScheme() - } - } - }; - - if (chatId) { - dataFetch(); - } else { - setChat(null); + if (fetchedChat.private && !incognitoMode) { + setIncognitoMode(true); + if (colorScheme === "light") { + toggleColorScheme(); } - }, [chatId]); - - const [fetchingChats, setFetchingChats] = useState(false); - const [chats, setChats] = useState([]); - useEffect(() => { - const dataFetch = async () => { - setFetchingChats(true); - const { items } = await detaDB.chats.fetch(incognitoMode ? { private: true } : { 'private?ne': true }); - - setChats(items as unknown as Chat[]); - setFetchingChats(false); - }; - - dataFetch(); - }, [incognitoMode]); - - const [prompts, setPrompts] = useState([]); - useEffect(() => { - const dataFetch = async () => { - const { items } = await detaDB.prompts.fetch(); - - setPrompts(items as unknown as Prompt[]); - }; - - dataFetch(); - }, []); - - const border = `${rem(1)} solid ${ - theme.colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[2] - }`; - - useEffect(() => { - setOpened(false); - }, [router.state.location]); - - const handleIncognito = () => { - 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 }); + } else if (!(fetchedChat.private ?? false) && incognitoMode) { + setIncognitoMode(false); + if (colorScheme === "dark") { + toggleColorScheme(); } + } + }; - setIncognitoMode(newValue) + if (chatId) { + dataFetch(); + } else { + setChat(null); + } + }, [chatId]); - const isDark = colorScheme === 'dark' - if (newValue && !isDark) { - toggleColorScheme() - } else if (!newValue && isDark) { - toggleColorScheme() - } + const [fetchingChats, setFetchingChats] = useState(false); + const [chats, setChats] = useState([]); + useEffect(() => { + const dataFetch = async () => { + setFetchingChats(true); + const { items } = await detaDB.chats.fetch( + incognitoMode ? { private: true } : { "private?ne": true } + ); + + setChats(items as unknown as Chat[]); + setFetchingChats(false); + }; + + dataFetch(); + }, [incognitoMode]); + + const [prompts, setPrompts] = useState([]); + useEffect(() => { + const dataFetch = async () => { + const { items } = await detaDB.prompts.fetch(); + + setPrompts(items as unknown as Prompt[]); + }; + + dataFetch(); + }, []); + + const border = `${rem(1)} solid ${ + theme.colorScheme === "dark" ? theme.colors.dark[4] : theme.colors.gray[2] + }`; + + useEffect(() => { + setOpened(false); + }, [router.state.location]); + + const handleIncognito = () => { + 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 }); } - return ( - - - - - - - + + + + + + + ); } diff --git a/src/layouts/Public.tsx b/src/layouts/Public.tsx index 3dd47e5..37720c9 100644 --- a/src/layouts/Public.tsx +++ b/src/layouts/Public.tsx @@ -1,7 +1,4 @@ -import { - AppShell, - useMantineColorScheme, -} from "@mantine/core"; +import { AppShell, useMantineColorScheme } from "@mantine/core"; import { useEffect, useState } from "react"; import { Chat } from "../db"; import { usePublicChatId } from "../hooks/useChatId"; @@ -10,52 +7,54 @@ import { ChatHeader } from "../components/ChatHeader"; import { useLocalStorage } from "@mantine/hooks"; export function PublicLayout({ children }: { children: React.ReactNode }) { - const { colorScheme } = useMantineColorScheme(); - const chatId = usePublicChatId(); + const { colorScheme } = useMantineColorScheme(); + const chatId = usePublicChatId(); - const [incognitoMode, setIncognitoMode] = useLocalStorage({ - key: 'incognito-mode', defaultValue: false, getInitialValueInEffect: false - }); + const [incognitoMode, setIncognitoMode] = useLocalStorage({ + key: "incognito-mode", + defaultValue: false, + getInitialValueInEffect: false, + }); - const [chat, setChat] = useState(null); - useEffect(() => { - const dataFetch = async () => { - try { - const res = await fetch(`/api/public/chats/${chatId}`) - const item = await res.json() + const [chat, setChat] = useState(null); + useEffect(() => { + const dataFetch = async () => { + try { + const res = await fetch(`/api/public/chats/${chatId}`); + const item = await res.json(); - const fetchedChat = item as unknown as Chat - setChat(fetchedChat); + const fetchedChat = item as unknown as Chat; + setChat(fetchedChat); - document.title = fetchedChat.description ? `${fetchedChat.description} | Dialogue AI` : 'Dialogue AI' - } catch (e) { - console.error(e) - } - }; + document.title = fetchedChat.description + ? `${fetchedChat.description} | Dialogue` + : "Dialogue"; + } catch (e) { + console.error(e); + } + }; - if (chatId) { - dataFetch(); - } else { - setChat(null); - } - }, [chatId]); + if (chatId) { + dataFetch(); + } else { + setChat(null); + } + }, [chatId]); - return ( - - - - ) : undefined - } - layout="alt" - padding={0} - > - {children} - - - - ); + return ( + + + : undefined} + layout="alt" + padding={0} + > + {children} + + + + ); } diff --git a/src/routes/IndexRoute.tsx b/src/routes/IndexRoute.tsx index f928ce0..0223239 100644 --- a/src/routes/IndexRoute.tsx +++ b/src/routes/IndexRoute.tsx @@ -1,5 +1,22 @@ -import { Container, SimpleGrid, Flex, Button, Text, useMantineTheme, Center, Box, Tooltip, ActionIcon, Card } from "@mantine/core"; -import { useChats, useIncognitoMode, usePrompts, useSettings } from "../hooks/contexts"; +import { + Container, + SimpleGrid, + Flex, + Button, + Text, + useMantineTheme, + Center, + Box, + Tooltip, + ActionIcon, + Card, +} from "@mantine/core"; +import { + useChats, + useIncognitoMode, + usePrompts, + useSettings, +} from "../hooks/contexts"; import { Welcome } from "../components/Welcome"; import { LogoIcon } from "../components/Logo"; import { CreateChatButton } from "../components/CreateChatButton"; @@ -9,120 +26,148 @@ import { notifications } from "@mantine/notifications"; import { Chat, detaDB, generateKey } from "../db"; import { useNavigate } from "@tanstack/react-location"; - export function IndexRoute() { - const { settings } = useSettings() - const theme = useMantineTheme() - const { incognitoMode } = useIncognitoMode() - const navigate = useNavigate(); + const { settings } = useSettings(); + const theme = useMantineTheme(); + const { incognitoMode } = useIncognitoMode(); + const navigate = useNavigate(); - const { setChats } = useChats() - const { prompts } = usePrompts() + const { setChats } = useChats(); + const { prompts } = usePrompts(); - return ( - <> -
- {prompts && prompts.length > 0 ? ( - - - - - Dialogue AI - - - - An open-source ChatGPT UI alternative - - - {prompts.slice(-9).reverse().map((prompt) => ( - - - - {prompt.title} - - - {prompt.content || prompt.writingCharacter || prompt.writingTone || prompt.writingFormat || prompt.writingStyle || "No Instructions"} - - + return ( + <> +
+ {prompts && prompts.length > 0 ? ( + + + + + Dialogue + + + + An open-source ChatGPT UI alternative + + + {prompts + .slice(-9) + .reverse() + .map((prompt) => ( + + + + {prompt.title} + + + {prompt.content || + prompt.writingCharacter || + prompt.writingTone || + prompt.writingFormat || + prompt.writingStyle || + "No Instructions"} + + - - { - if (!settings?.openAiApiKey) { - notifications.show({ - title: "Error", - color: "red", - message: "OpenAI API Key is not defined. Please set your API Key", - }); - return; - }; + + { + 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: prompt.title ? `New ${prompt.title} Chat` : "New Chat", - prompt: prompt.key, - writingInstructions: prompt.content, - writingCharacter: prompt.writingCharacter, - writingTone: prompt.writingTone, - writingStyle: prompt.writingStyle, - writingFormat: prompt.writingFormat, - totalTokens: 0, - createdAt: new Date().toISOString(), - }, generateKey()) + const item = await detaDB.chats.put( + { + description: prompt.title + ? `New ${prompt.title} Chat` + : "New Chat", + prompt: prompt.key, + writingInstructions: prompt.content, + writingCharacter: prompt.writingCharacter, + writingTone: prompt.writingTone, + writingStyle: prompt.writingStyle, + writingFormat: prompt.writingFormat, + totalTokens: 0, + createdAt: new Date().toISOString(), + }, + generateKey() + ); - const chat = item as unknown as Chat - setChats(current => ([...(current || []), chat])) + const chat = item as unknown as Chat; + setChats((current) => [...(current || []), chat]); - navigate({ to: `/chats/${chat.key}` }); - }} - > - - - - - ))} - - - - {settings?.openAiApiKey && ( - - {incognitoMode ? "Create a New Private Chat" : "Create a New Chat"} - - )} - - - - - - ) : ( - - )} -
- - ); + navigate({ to: `/chats/${chat.key}` }); + }} + > + + + +
+ ))} +
+ + + {settings?.openAiApiKey && ( + + {incognitoMode ? "Create a New Private Chat" : "Create a New Chat"} + + )} + + + + +
+ ) : ( + + )} +
+ + ); }