diff --git a/Discovery.md b/Discovery.md new file mode 100644 index 0000000..4259c1c --- /dev/null +++ b/Discovery.md @@ -0,0 +1,8 @@ +--- +app_name: "Chatpad AI" +tagline: "Just another ChatGPT user-interface" +git: "https://github.com/deta/chatpad" +theme_color: "#08A678" +--- + + diff --git a/Spacefile b/Spacefile index 40091d6..0923cf5 100644 --- a/Spacefile +++ b/Spacefile @@ -1,5 +1,6 @@ # Spacefile Docs: https://go.deta.dev/docs/spacefile/v0 v: 0 +icon: public/icon.png micros: - name: app src: . diff --git a/public/icon.png b/public/icon.png new file mode 100644 index 0000000..70348ba Binary files /dev/null and b/public/icon.png differ diff --git a/src/components/Chats.tsx b/src/components/Chats.tsx index 2852896..a6af30a 100644 --- a/src/components/Chats.tsx +++ b/src/components/Chats.tsx @@ -58,7 +58,6 @@ export function Chats({ search }: { search: string }) { } color="teal" - chat={chat} label={chat.description} /> diff --git a/src/components/DatabaseModal.tsx b/src/components/DatabaseModal.tsx deleted file mode 100644 index 513e641..0000000 --- a/src/components/DatabaseModal.tsx +++ /dev/null @@ -1,145 +0,0 @@ -import { Button, Card, Flex, Group, Modal, Stack, Text } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; -import { notifications } from "@mantine/notifications"; -import { IconDatabaseExport, IconDatabaseImport } from "@tabler/icons-react"; -import { useLiveQuery } from "dexie-react-hooks"; -import download from "downloadjs"; -import { cloneElement, ReactElement } from "react"; -import { db } from "../db"; -import { DeleteAllDataModal } from "./DeleteAllDataModal"; -import { DeleteChatsModal } from "./DeleteChatsModal"; - -export function DatabaseModal({ children }: { children: ReactElement }) { - const [opened, { open, close }] = useDisclosure(false); - const chatsCount = useLiveQuery(async () => { - return (await db.chats.toArray()).length; - }, []); - const messagesCount = useLiveQuery(async () => { - return (await db.messages.toArray()).length; - }, []); - const promptsCount = useLiveQuery(async () => { - return (await db.prompts.toArray()).length; - }); - - return ( - <> - {cloneElement(children, { onClick: open })} - - - - - - {chatsCount} - - - Chats - - - - - {messagesCount} - - - Messages - - - - - {promptsCount} - - - Prompts - - - - - - { - const file = e.currentTarget.files?.[0]; - if (!file) return; - db.import(file, { - acceptNameDiff: true, - overwriteValues: true, - acceptMissingTables: true, - clearTablesBeforeImport: true, - }) - .then(() => { - notifications.show({ - title: "Importing data", - message: "Your data is being imported.", - }); - }) - .catch((error) => { - notifications.show({ - title: "Error", - color: "red", - message: "The file you selected is invalid", - }); - }); - }} - accept="application/json" - hidden - /> - - - - - - - - - - ); -} diff --git a/src/components/DeleteAllDataModal.tsx b/src/components/DeleteAllDataModal.tsx deleted file mode 100644 index 9b4cde6..0000000 --- a/src/components/DeleteAllDataModal.tsx +++ /dev/null @@ -1,42 +0,0 @@ -import { Button, Modal, Stack, Text } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; -import { IconTrash } from "@tabler/icons-react"; -import { db } from "../db"; - -export function DeleteAllDataModal({ onOpen }: { onOpen: () => void }) { - const [opened, { open, close }] = useDisclosure(false, { onOpen }); - - return ( - <> - - - - Are you sure you want to delete your data? - - - - - ); -} diff --git a/src/components/Layout.tsx b/src/components/Layout.tsx index a69ff9e..8b9531a 100644 --- a/src/components/Layout.tsx +++ b/src/components/Layout.tsx @@ -19,7 +19,6 @@ import { import { IconBrandGithub, IconBrandTwitter, - IconDatabase, IconMessage, IconMoonStars, IconPlus, @@ -34,7 +33,6 @@ import { Chat, detaDB, generateKey, Prompt, Settings } from "../db"; import { useChatId } from "../hooks/useChatId"; import { Chats } from "./Chats"; import { CreatePromptModal } from "./CreatePromptModal"; -import { DatabaseModal } from "./DatabaseModal"; import { LogoText } from "./Logo"; import { Prompts } from "./Prompts"; import { SettingsModal } from "./SettingsModal"; @@ -281,15 +279,6 @@ export function Layout() { )} - {config.allowDatabaseModal && ( - - - - - - - - )} {config.githubUrl && ( { - return (await db.messages.orderBy("createdAt").toArray()).filter( - (m) => m.chatId === chat.id - )[0]; - }, [chat]); - +export function MainLink({ icon, color, label }: MainLinkProps) { return ( ({ @@ -41,8 +32,7 @@ export function MainLink({ icon, color, label, chat }: MainLinkProps) { width: 0, }} > - {label}
- {firstMessage?.content} + {label}
diff --git a/src/components/Prompts.tsx b/src/components/Prompts.tsx index fd5993d..fc5a369 100644 --- a/src/components/Prompts.tsx +++ b/src/components/Prompts.tsx @@ -104,7 +104,7 @@ export function Prompts({ navigate({ to: `/chats/${chat.key}` }); onPlay(); - const result = await createChatCompletion(settings?.openAiApiKey, [ + const result = await createChatCompletion(settings, [ { role: "system", content: diff --git a/src/components/SettingsModal.tsx b/src/components/SettingsModal.tsx index 2168aaf..23507f6 100644 --- a/src/components/SettingsModal.tsx +++ b/src/components/SettingsModal.tsx @@ -14,7 +14,7 @@ import { import { useDisclosure } from "@mantine/hooks"; import { notifications } from "@mantine/notifications"; import { cloneElement, ReactElement, useEffect, useState } from "react"; -import { db, detaDB } from "../db"; +import { Settings, detaDB } from "../db"; import { config } from "../utils/config"; import { checkOpenAIKey } from "../utils/openai"; import { useSettings } from "../hooks/contexts"; @@ -63,7 +63,7 @@ export function SettingsModal({ children }: { children: ReactElement }) { try { setSubmitting(true); event.preventDefault(); - await checkOpenAIKey(value); + await checkOpenAIKey({ ...(settings as Settings), openAiApiKey: value }); if (settings?.openAiApiKey) { await detaDB.settings.update({ @@ -92,14 +92,14 @@ export function SettingsModal({ children }: { children: ReactElement }) { message: "Your OpenAI Key has been saved.", }); } catch (error: any) { - if (error.toJSON().message === "Network Error") { + if (error.message === "Network Error") { notifications.show({ title: "Error", color: "red", message: "No internet connection.", }); } - const message = error.response?.data?.error?.message; + const message = error.message if (message) { notifications.show({ title: "Error", @@ -150,9 +150,13 @@ export function SettingsModal({ children }: { children: ReactElement }) { onChange={async (value) => { setSubmitting(true); try { - await db.settings.update("general", { - openAiApiType: value ?? 'openai', - }); + const updates = { + openAiApiType: value as any ?? 'openai', + } + await detaDB.settings.update(updates, "general"); + + setSettings(current => ({ ...(current!), ...updates })) + notifications.show({ title: "Saved", message: "Your OpenAI Type has been saved.", @@ -186,9 +190,12 @@ export function SettingsModal({ children }: { children: ReactElement }) { onChange={async (value) => { setSubmitting(true); try { - await db.settings.update("general", { + const updates = { openAiModel: value ?? undefined, - }); + } + await detaDB.settings.update(updates, "general"); + setSettings(current => ({ ...(current!), ...updates })) + notifications.show({ title: "Saved", message: "Your OpenAI Model has been saved.", @@ -226,9 +233,12 @@ export function SettingsModal({ children }: { children: ReactElement }) { onChange={async (value) => { setSubmitting(true); try { - await db.settings.update("general", { - openAiApiAuth: value ?? 'none', - }); + const updates = { + openAiApiAuth: value as any ?? 'none', + } + await detaDB.settings.update(updates, "general"); + setSettings(current => ({ ...(current!), ...updates })) + notifications.show({ title: "Saved", message: "Your OpenAI Auth has been saved.", @@ -261,10 +271,13 @@ export function SettingsModal({ children }: { children: ReactElement }) { try { setSubmitting(true); event.preventDefault(); - await db.settings.where({ id: "general" }).modify((row) => { - row.openAiApiBase = base; - console.log(row); - }); + + const updates = { + openAiApiBase: base, + } + await detaDB.settings.update(updates, "general"); + setSettings(current => ({ ...(current!), ...updates })) + notifications.show({ title: "Saved", message: "Your OpenAI Base has been saved.", @@ -309,10 +322,13 @@ export function SettingsModal({ children }: { children: ReactElement }) { try { setSubmitting(true); event.preventDefault(); - await db.settings.where({ id: "general" }).modify((row) => { - row.openAiApiVersion = version; - console.log(row); - }); + + const updates = { + openAiApiVersion: version, + } + await detaDB.settings.update(updates, "general"); + setSettings(current => ({ ...(current!), ...updates })) + notifications.show({ title: "Saved", message: "Your OpenAI Version has been saved.", diff --git a/src/db/index.ts b/src/db/index.ts index e7edc46..9a7daf6 100644 --- a/src/db/index.ts +++ b/src/db/index.ts @@ -1,7 +1,5 @@ import { Deta } from "deta"; -import Dexie, { Table } from "dexie"; import "dexie-export-import"; -import { config } from "../utils/config"; import { nanoid } from "nanoid"; export interface Chat { @@ -54,33 +52,33 @@ export const generateKey = (ascending: boolean = true): string => { return `${ timestamp.toString(16) }${ nanoid(5) }` } -export class Database extends Dexie { - chats!: Table; - messages!: Table; - prompts!: Table; - settings!: Table; +// export class Database extends Dexie { +// chats!: Table; +// messages!: Table; +// prompts!: Table; +// settings!: Table; - constructor() { - super("chatpad"); - this.version(2).stores({ - chats: "id, createdAt", - messages: "id, chatId, createdAt", - prompts: "id, createdAt", - settings: "id", - }); +// constructor() { +// super("chatpad"); +// this.version(2).stores({ +// chats: "id, createdAt", +// messages: "id, chatId, createdAt", +// prompts: "id, createdAt", +// settings: "id", +// }); - this.on("populate", async () => { - db.settings.add({ - 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 }), - }); - }); - } -} +// this.on("populate", async () => { +// db.settings.add({ +// 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 }), +// }); +// }); +// } +// } -export const db = new Database(); +// export const db = new Database(); diff --git a/src/routes/ChatRoute.tsx b/src/routes/ChatRoute.tsx index 6ab984d..d2685af 100644 --- a/src/routes/ChatRoute.tsx +++ b/src/routes/ChatRoute.tsx @@ -155,7 +155,7 @@ export function ChatRoute() { // }); await createStreamChatCompletion( - settings.openAiApiKey, + settings, [ { role: "system", @@ -188,7 +188,7 @@ export function ChatRoute() { // const messages = await db.messages // .where({ chatId }) // .sortBy("createdAt"); - const createChatDescription = await createChatCompletion(settings.openAiApiKey, [ + const createChatDescription = await createChatCompletion(settings, [ { role: "system", content: getSystemMessage(), diff --git a/src/utils/openai.ts b/src/utils/openai.ts index 39b42a1..fe42e57 100644 --- a/src/utils/openai.ts +++ b/src/utils/openai.ts @@ -1,7 +1,7 @@ import { encode } from "gpt-token-utils"; import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai"; import { OpenAIExt } from "openai-ext"; -import { db, detaDB } from "../db"; +import { Settings, detaDB } from "../db"; import { config } from "./config"; import { useDebounce } from "./debounce"; @@ -22,13 +22,12 @@ function getClient( } export async function createStreamChatCompletion( - apiKey: string, + settings: Settings, messages: ChatCompletionRequestMessage[], chatId: string, messageId: string, onContent: (content: string, isFinal: boolean) => void ) { - const settings = await db.settings.get("general"); const model = settings?.openAiModel ?? config.defaultModel; const debouncedSetStreamContent = useDebounce(setStreamContent, 200); @@ -39,7 +38,7 @@ export async function createStreamChatCompletion( messages, }, { - apiKey: apiKey, + apiKey: settings.openAiApiKey!, handler: { onContent(content, isFinal) { debouncedSetStreamContent(messageId, content, isFinal); @@ -83,15 +82,15 @@ async function setTotalTokens(chatId: string, content: string) { } export async function createChatCompletion( - apiKey: string, + settings: Settings, messages: ChatCompletionRequestMessage[] ) { - const settings = await db.settings.get("general"); const model = settings?.openAiModel ?? config.defaultModel; const type = settings?.openAiApiType ?? config.defaultType; const auth = settings?.openAiApiAuth ?? config.defaultAuth; const base = settings?.openAiApiBase ?? config.defaultBase; const version = settings?.openAiApiVersion ?? config.defaultVersion; + const apiKey = settings.openAiApiKey!; const client = getClient(apiKey, type, auth, base); return client.createChatCompletion( @@ -112,8 +111,8 @@ export async function createChatCompletion( ); } -export async function checkOpenAIKey(apiKey: string) { - return createChatCompletion(apiKey, [ +export async function checkOpenAIKey(settings: Settings) { + return createChatCompletion(settings, [ { role: "user", content: "hello",