mirror of
https://github.com/deiucanta/chatpad.git
synced 2026-03-11 09:04:31 +00:00
fixes & discovery
This commit is contained in:
parent
d79dd616ce
commit
19db10b1b0
13 changed files with 84 additions and 271 deletions
8
Discovery.md
Normal file
8
Discovery.md
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
---
|
||||
app_name: "Chatpad AI"
|
||||
tagline: "Just another ChatGPT user-interface"
|
||||
git: "https://github.com/deta/chatpad"
|
||||
theme_color: "#08A678"
|
||||
---
|
||||
|
||||
|
||||
|
|
@ -1,5 +1,6 @@
|
|||
# Spacefile Docs: https://go.deta.dev/docs/spacefile/v0
|
||||
v: 0
|
||||
icon: public/icon.png
|
||||
micros:
|
||||
- name: app
|
||||
src: .
|
||||
|
|
|
|||
BIN
public/icon.png
Normal file
BIN
public/icon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 207 KiB |
|
|
@ -58,7 +58,6 @@ export function Chats({ search }: { search: string }) {
|
|||
<MainLink
|
||||
icon={<IconMessages size="1rem" />}
|
||||
color="teal"
|
||||
chat={chat}
|
||||
label={chat.description}
|
||||
/>
|
||||
</Link>
|
||||
|
|
|
|||
|
|
@ -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 })}
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title="Database"
|
||||
size="lg"
|
||||
withinPortal
|
||||
keepMounted
|
||||
>
|
||||
<Stack>
|
||||
<Flex>
|
||||
<Card withBorder sx={{ flex: 1 }}>
|
||||
<Text size="xl" align="center">
|
||||
{chatsCount}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
color="dimmed"
|
||||
align="center"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
>
|
||||
Chats
|
||||
</Text>
|
||||
</Card>
|
||||
<Card withBorder sx={{ flex: 1, marginLeft: -1 }}>
|
||||
<Text size="xl" align="center">
|
||||
{messagesCount}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
color="dimmed"
|
||||
align="center"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
>
|
||||
Messages
|
||||
</Text>
|
||||
</Card>
|
||||
<Card withBorder sx={{ flex: 1, marginLeft: -1 }}>
|
||||
<Text size="xl" align="center">
|
||||
{promptsCount}
|
||||
</Text>
|
||||
<Text
|
||||
size="xs"
|
||||
color="dimmed"
|
||||
align="center"
|
||||
tt="uppercase"
|
||||
fw={700}
|
||||
>
|
||||
Prompts
|
||||
</Text>
|
||||
</Card>
|
||||
</Flex>
|
||||
<Group>
|
||||
<Button
|
||||
variant="default"
|
||||
leftIcon={<IconDatabaseExport size={20} />}
|
||||
onClick={async () => {
|
||||
const blob = await db.export();
|
||||
download(
|
||||
blob,
|
||||
`chatpad-export-${new Date().toLocaleString()}.json`,
|
||||
"application/json"
|
||||
);
|
||||
notifications.show({
|
||||
title: "Exporting Data",
|
||||
message: "Your data is being exported.",
|
||||
});
|
||||
}}
|
||||
>
|
||||
Export Data
|
||||
</Button>
|
||||
<input
|
||||
id="file-upload-btn"
|
||||
type="file"
|
||||
onChange={async (e) => {
|
||||
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
|
||||
/>
|
||||
<Button
|
||||
component="label"
|
||||
htmlFor="file-upload-btn"
|
||||
variant="default"
|
||||
leftIcon={<IconDatabaseImport size={20} />}
|
||||
>
|
||||
Import Data
|
||||
</Button>
|
||||
</Group>
|
||||
<Group>
|
||||
<DeleteChatsModal onOpen={close} />
|
||||
<DeleteAllDataModal onOpen={close} />
|
||||
</Group>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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 (
|
||||
<>
|
||||
<Button
|
||||
onClick={open}
|
||||
variant="outline"
|
||||
color="red"
|
||||
leftIcon={<IconTrash size={20} />}
|
||||
>
|
||||
Delete All Data
|
||||
</Button>
|
||||
<Modal
|
||||
opened={opened}
|
||||
onClose={close}
|
||||
title="Delete All Data"
|
||||
size="md"
|
||||
withinPortal
|
||||
>
|
||||
<Stack>
|
||||
<Text size="sm">Are you sure you want to delete your data?</Text>
|
||||
<Button
|
||||
onClick={async () => {
|
||||
await db.delete();
|
||||
localStorage.clear();
|
||||
window.location.assign("/");
|
||||
}}
|
||||
color="red"
|
||||
>
|
||||
Delete
|
||||
</Button>
|
||||
</Stack>
|
||||
</Modal>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
|
@ -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() {
|
|||
</Tooltip>
|
||||
</SettingsModal>
|
||||
)}
|
||||
{config.allowDatabaseModal && (
|
||||
<DatabaseModal>
|
||||
<Tooltip label="Database">
|
||||
<ActionIcon sx={{ flex: 1 }} size="xl">
|
||||
<IconDatabase size={20} />
|
||||
</ActionIcon>
|
||||
</Tooltip>
|
||||
</DatabaseModal>
|
||||
)}
|
||||
{config.githubUrl && (
|
||||
<Tooltip label="Source Code">
|
||||
<ActionIcon
|
||||
|
|
|
|||
|
|
@ -1,21 +1,12 @@
|
|||
import { Group, Text, ThemeIcon, UnstyledButton } from "@mantine/core";
|
||||
import { useLiveQuery } from "dexie-react-hooks";
|
||||
import { Chat, db } from "../db";
|
||||
|
||||
interface MainLinkProps {
|
||||
icon: React.ReactNode;
|
||||
color: string;
|
||||
label: string;
|
||||
chat: Chat;
|
||||
}
|
||||
|
||||
export function MainLink({ icon, color, label, chat }: MainLinkProps) {
|
||||
const firstMessage = useLiveQuery(async () => {
|
||||
return (await db.messages.orderBy("createdAt").toArray()).filter(
|
||||
(m) => m.chatId === chat.id
|
||||
)[0];
|
||||
}, [chat]);
|
||||
|
||||
export function MainLink({ icon, color, label }: MainLinkProps) {
|
||||
return (
|
||||
<UnstyledButton
|
||||
sx={(theme) => ({
|
||||
|
|
@ -41,8 +32,7 @@ export function MainLink({ icon, color, label, chat }: MainLinkProps) {
|
|||
width: 0,
|
||||
}}
|
||||
>
|
||||
{label} <br />
|
||||
{firstMessage?.content}
|
||||
{label}
|
||||
</Text>
|
||||
</Group>
|
||||
</UnstyledButton>
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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.",
|
||||
|
|
|
|||
|
|
@ -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<Chat>;
|
||||
messages!: Table<Message>;
|
||||
prompts!: Table<Prompt>;
|
||||
settings!: Table<Settings>;
|
||||
// export class Database extends Dexie {
|
||||
// chats!: Table<Chat>;
|
||||
// messages!: Table<Message>;
|
||||
// prompts!: Table<Prompt>;
|
||||
// settings!: Table<Settings>;
|
||||
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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(),
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
Loading…
Reference in a new issue