wip: use base to store chats and messages

This commit is contained in:
BetaHuhn 2023-08-28 17:43:14 +02:00
parent 290b0acc93
commit f163198b82
10 changed files with 310 additions and 70 deletions

View file

@ -8,3 +8,4 @@ micros:
primary: true
commands:
- NODE_OPTIONS="--max-old-space-size=1024" npm run build
dev: npm run dev

92
package-lock.json generated
View file

@ -43,6 +43,7 @@
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^4.0.4",
"buffer": "^5.7.1",
"deta": "^2.0.0-rc.1",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"vite": "^4.4.9"
@ -2643,6 +2644,15 @@
"node": ">=6"
}
},
"node_modules/deta": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/deta/-/deta-2.0.0-rc.1.tgz",
"integrity": "sha512-NsNVMB54m2WDZQUIWmH51C8kvtUi/U3M2iuj/WdmL2qCmPhhVxsRP2skloAcjk4pgJRklIIEYPGo808vGBDY8w==",
"dev": true,
"dependencies": {
"node-fetch": "^2.6.7"
}
},
"node_modules/detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@ -5723,6 +5733,26 @@
}
}
},
"node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
@ -6927,6 +6957,12 @@
"node": ">=8.0"
}
},
"node_modules/tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"node_modules/trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@ -7409,6 +7445,22 @@
"node": "^10 || ^12 || >=14"
}
},
"node_modules/webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"node_modules/whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"dependencies": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"node_modules/which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@ -9262,6 +9314,15 @@
"resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
"integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA=="
},
"deta": {
"version": "2.0.0-rc.1",
"resolved": "https://registry.npmjs.org/deta/-/deta-2.0.0-rc.1.tgz",
"integrity": "sha512-NsNVMB54m2WDZQUIWmH51C8kvtUi/U3M2iuj/WdmL2qCmPhhVxsRP2skloAcjk4pgJRklIIEYPGo808vGBDY8w==",
"dev": true,
"requires": {
"node-fetch": "^2.6.7"
}
},
"detect-libc": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
@ -11316,6 +11377,15 @@
"styled-jsx": "5.1.1"
}
},
"node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"dev": true,
"requires": {
"whatwg-url": "^5.0.0"
}
},
"node-releases": {
"version": "2.0.13",
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
@ -12151,6 +12221,12 @@
"is-number": "^7.0.0"
}
},
"tr46": {
"version": "0.0.3",
"resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz",
"integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==",
"dev": true
},
"trim-lines": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@ -12425,6 +12501,22 @@
}
}
},
"webidl-conversions": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz",
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==",
"dev": true
},
"whatwg-url": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz",
"integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==",
"dev": true,
"requires": {
"tr46": "~0.0.3",
"webidl-conversions": "^3.0.0"
}
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View file

@ -15,6 +15,7 @@
"@types/react-dom": "^18.0.11",
"@vitejs/plugin-react": "^4.0.4",
"buffer": "^5.7.1",
"deta": "^2.0.0-rc.1",
"path-browserify": "^1.0.1",
"process": "^0.11.10",
"vite": "^4.4.9"

View file

@ -1,9 +1,8 @@
import { ActionIcon, Flex, Menu } from "@mantine/core";
import { IconDotsVertical, IconMessages } from "@tabler/icons-react";
import { Link } from "@tanstack/react-location";
import { useLiveQuery } from "dexie-react-hooks";
import { useMemo } from "react";
import { db } from "../db";
import { useEffect, useMemo, useState } from "react";
import { Chat, detaDB } from "../db";
import { useChatId } from "../hooks/useChatId";
import { DeleteChatModal } from "./DeleteChatModal";
import { EditChatModal } from "./EditChatModal";
@ -11,9 +10,23 @@ import { MainLink } from "./MainLink";
export function Chats({ search }: { search: string }) {
const chatId = useChatId();
const chats = useLiveQuery(() =>
db.chats.orderBy("createdAt").reverse().toArray()
);
const [chats, setChats] = useState<Chat[]>();
useEffect(() => {
// fetch data
const dataFetch = async () => {
const { items } = await detaDB.chats.fetch();
setChats(items as unknown as Chat[]);
};
dataFetch();
}, []);
// const chats = useLiveQuery(() =>
// db.chats.orderBy("createdAt").reverse().toArray()
// );
const filteredChats = useMemo(
() =>
(chats ?? []).filter((chat) => {
@ -27,8 +40,8 @@ export function Chats({ search }: { search: string }) {
<>
{filteredChats.map((chat) => (
<Flex
key={chat.id}
className={chatId === chat.id ? "active" : undefined}
key={chat.key}
className={chatId === chat.key ? "active" : undefined}
sx={(theme) => ({
marginTop: 1,
"&:hover, &.active": {
@ -39,7 +52,7 @@ export function Chats({ search }: { search: string }) {
},
})}
>
<Link to={`/chats/${chat.id}`} style={{ flex: 1 }}>
<Link to={`/chats/${chat.key}`} style={{ flex: 1 }}>
<MainLink
icon={<IconMessages size="1rem" />}
color="teal"

View file

@ -29,10 +29,8 @@ import {
IconX,
} from "@tabler/icons-react";
import { Link, Outlet, useNavigate, useRouter } from "@tanstack/react-location";
import { useLiveQuery } from "dexie-react-hooks";
import { nanoid } from "nanoid";
import { useEffect, useState } from "react";
import { db } from "../db";
import { Chat, db, detaDB, generateKey } from "../db";
import { useChatId } from "../hooks/useChatId";
import { Chats } from "./Chats";
import { CreatePromptModal } from "./CreatePromptModal";
@ -58,9 +56,17 @@ export function Layout() {
const [search, setSearch] = useState("");
const chatId = useChatId();
const chat = useLiveQuery(async () => {
if (!chatId) return null;
return db.chats.get(chatId);
const [chat, setChat] = useState<Chat>();
useEffect(() => {
const dataFetch = async () => {
const item = await detaDB.chats.get(chatId!);
setChat(item as unknown as Chat);
};
dataFetch();
}, [chatId]);
const border = `${rem(1)} solid ${
@ -133,14 +139,21 @@ export function Layout() {
<Button
fullWidth
leftIcon={<IconPlus size={20} />}
onClick={() => {
const id = nanoid();
db.chats.add({
id,
onClick={async () => {
const item = await detaDB.chats.put({
description: "New Chat",
totalTokens: 0,
createdAt: new Date(),
});
createdAt: new Date().toISOString(),
}, generateKey())
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}` });
}}
>
@ -282,10 +295,7 @@ export function Layout() {
height: "100%",
}}
>
{`${chat.description} - ${chat.totalTokens ?? 0} tokens ~ $${(
((chat.totalTokens ?? 0) * 0.002) /
1000
).toFixed(5)}`}
{chat.description}
</div>
</Header>
) : undefined

View file

@ -1,16 +1,18 @@
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 {
id: string;
key: string;
description: string;
totalTokens: number;
createdAt: Date;
}
export interface Message {
id: string;
key: string;
chatId: string;
role: "system" | "assistant" | "user";
content: string;
@ -18,7 +20,7 @@ export interface Message {
}
export interface Prompt {
id: string;
key: string;
title: string;
content: string;
createdAt: Date;
@ -34,6 +36,24 @@ export interface Settings {
openAiApiVersion?: string;
}
export const deta = Deta()
export const detaDB = {
chats: deta.Base("chats"),
messages: deta.Base("messages"),
prompts: deta.Base("prompts"),
settings: deta.Base("settings"),
}
// Used as large number to make sure keys are generated in descending order
const maxDateNowValue = 8.64e15
export const generateKey = (ascending: boolean = true): string => {
const timestamp = ascending ? Date.now() : maxDateNowValue - Date.now()
return `${ timestamp.toString(16) }${ nanoid(5) }`
}
export class Database extends Dexie {
chats!: Table<Chat>;
messages!: Table<Message>;

View file

@ -14,10 +14,10 @@ import {
import { notifications } from "@mantine/notifications";
import { useLiveQuery } from "dexie-react-hooks";
import { nanoid } from "nanoid";
import { KeyboardEvent, useState, type ChangeEvent } from "react";
import { KeyboardEvent, useState, type ChangeEvent, useEffect } from "react";
import { AiOutlineSend } from "react-icons/ai";
import { MessageItem } from "../components/MessageItem";
import { db } from "../db";
import { Chat, Message, db, detaDB, generateKey } from "../db";
import { useChatId } from "../hooks/useChatId";
import { config } from "../utils/config";
import {
@ -30,10 +30,24 @@ export function ChatRoute() {
const apiKey = useLiveQuery(async () => {
return (await db.settings.where({ id: "general" }).first())?.openAiApiKey;
});
const messages = useLiveQuery(() => {
if (!chatId) return [];
return db.messages.where("chatId").equals(chatId).sortBy("createdAt");
const [messages, setMessages] = useState<Message[]>([]);
useEffect(() => {
// fetch data
const dataFetch = async () => {
const { items } = await detaDB.messages.fetch({ chatId }, { desc: false });
setMessages(items as unknown as Message[]);
};
dataFetch();
}, [chatId]);
// const messages = useLiveQuery(() => {
// if (!chatId) return [];
// return db.messages.where("chatId").equals(chatId).sortBy("createdAt");
// }, [chatId]);
const userMessages =
messages
?.filter((message) => message.role === "user")
@ -43,11 +57,23 @@ export function ChatRoute() {
const [contentDraft, setContentDraft] = useState("");
const [submitting, setSubmitting] = useState(false);
const chat = useLiveQuery(async () => {
if (!chatId) return null;
return db.chats.get(chatId);
const [chat, setChat] = useState<Chat>();
useEffect(() => {
const dataFetch = async () => {
const item = await detaDB.chats.get(chatId!);
setChat(item as unknown as Chat);
};
dataFetch();
}, [chatId]);
// const chat = useLiveQuery(async () => {
// if (!chatId) return null;
// return db.chats.get(chatId);
// }, [chatId]);
const [writingCharacter, setWritingCharacter] = useState<string | null>(null);
const [writingTone, setWritingTone] = useState<string | null>(null);
const [writingStyle, setWritingStyle] = useState<string | null>(null);
@ -90,23 +116,42 @@ export function ChatRoute() {
try {
setSubmitting(true);
await db.messages.add({
id: nanoid(),
const userMessage = await detaDB.messages.put({
chatId,
content,
role: "user",
createdAt: new Date(),
});
createdAt: new Date().toISOString(),
}, generateKey())
setMessages(current => [...current, userMessage as unknown as Message])
// await db.messages.add({
// id: nanoid(),
// chatId,
// content,
// role: "user",
// createdAt: new Date(),
// });
setContent("");
const messageId = nanoid();
await db.messages.add({
id: messageId,
const systemMessage = await detaDB.messages.put({
chatId,
content: "█",
role: "assistant",
createdAt: new Date(),
});
createdAt: new Date().toISOString(),
}, generateKey())
setMessages(current => [...current, systemMessage as unknown as Message])
const messageId = systemMessage!.key as string
// await db.messages.add({
// id: messageId,
// chatId,
// content: "█",
// role: "assistant",
// createdAt: new Date(),
// });
await createStreamChatCompletion(
apiKey,
@ -122,15 +167,26 @@ export function ChatRoute() {
{ role: "user", content },
],
chatId,
messageId
messageId,
(content) => {
setMessages(current => current.map(message => {
if (message.key === messageId) {
return { ...message, content };
}
return message;
}));
}
);
setSubmitting(false);
if (chat?.description === "New Chat") {
const messages = await db.messages
.where({ chatId })
.sortBy("createdAt");
const res = await detaDB.messages.fetch({ chatId })
const messages = res.items as unknown as Message[]
// const messages = await db.messages
// .where({ chatId })
// .sortBy("createdAt");
const createChatDescription = await createChatCompletion(apiKey, [
{
role: "system",
@ -150,15 +206,24 @@ export function ChatRoute() {
createChatDescription.data.choices[0].message?.content;
if (createChatDescription.data.usage) {
await db.chats.where({ id: chatId }).modify((chat) => {
chat.description = chatDescription ?? "New Chat";
if (chat.totalTokens) {
chat.totalTokens +=
createChatDescription.data.usage!.total_tokens;
} else {
chat.totalTokens = createChatDescription.data.usage!.total_tokens;
}
});
const chatUpdates = {
description: chatDescription ?? "New Chat",
// todo: add to existing count instead of replacing
totalTokens: createChatDescription.data.usage!.total_tokens,
}
await detaDB.chats.update(chatUpdates, chatId)
setChat(chat => ({ ...chat!, ...chatUpdates }))
// await db.chats.where({ id: chatId }).modify((chat) => {
// chat.description = chatDescription ?? "New Chat";
// if (chat.totalTokens) {
// chat.totalTokens +=
// createChatDescription.data.usage!.total_tokens;
// } else {
// chat.totalTokens = createChatDescription.data.usage!.total_tokens;
// }
// });
}
}
} catch (error: any) {
@ -221,7 +286,7 @@ export function ChatRoute() {
<Container pt="xl" pb={100}>
<Stack spacing="xs">
{messages?.map((message) => (
<MessageItem key={message.id} message={message} />
<MessageItem key={message.key} message={message} />
))}
</Stack>
{submitting && (

View file

@ -1,7 +1,7 @@
import { encode } from "gpt-token-utils";
import { ChatCompletionRequestMessage, Configuration, OpenAIApi } from "openai";
import { OpenAIExt } from "openai-ext";
import { db } from "../db";
import { db, detaDB } from "../db";
import { config } from "./config";
function getClient(
@ -24,7 +24,8 @@ export async function createStreamChatCompletion(
apiKey: string,
messages: ChatCompletionRequestMessage[],
chatId: string,
messageId: string
messageId: string,
onContent: (content: string, isFinal: boolean) => void
) {
const settings = await db.settings.get("general");
const model = settings?.openAiModel ?? config.defaultModel;
@ -42,6 +43,7 @@ export async function createStreamChatCompletion(
if (isFinal) {
setTotalTokens(chatId, content);
}
onContent(content, isFinal);
},
onDone(stream) {},
onError(error, stream) {
@ -52,24 +54,25 @@ export async function createStreamChatCompletion(
);
}
function setStreamContent(
async function setStreamContent(
messageId: string,
content: string,
isFinal: boolean
) {
content = isFinal ? content : content + "█";
db.messages.update(messageId, { content: content });
await detaDB.messages.update({ content: content }, messageId);
}
function setTotalTokens(chatId: string, content: string) {
let total_tokens = encode(content).length;
db.chats.where({ id: chatId }).modify((chat) => {
if (chat.totalTokens) {
chat.totalTokens += total_tokens;
} else {
chat.totalTokens = total_tokens;
}
});
// db.chats.where({ id: chatId }).modify((chat) => {
// if (chat.totalTokens) {
// chat.totalTokens += total_tokens;
// } else {
// chat.totalTokens = total_tokens;
// }
// });
}
export async function createChatCompletion(

View file

@ -4,4 +4,7 @@ import react from '@vitejs/plugin-react'
// https://vitejs.dev/config/
export default defineConfig({
plugins: [react()],
server: {
port: process.env.PORT ? parseInt(process.env.PORT) : 3000,
}
})

View file

@ -1260,6 +1260,13 @@
"resolved" "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz"
"version" "2.0.3"
"deta@^2.0.0-rc.1":
"integrity" "sha512-NsNVMB54m2WDZQUIWmH51C8kvtUi/U3M2iuj/WdmL2qCmPhhVxsRP2skloAcjk4pgJRklIIEYPGo808vGBDY8w=="
"resolved" "https://registry.npmjs.org/deta/-/deta-2.0.0-rc.1.tgz"
"version" "2.0.0-rc.1"
dependencies:
"node-fetch" "^2.6.7"
"detect-libc@^1.0.3":
"integrity" "sha512-pGjwhsmsp4kL2RTz08wcOlGN83otlqHeD/Z5T8GXZB+/YcpQ/dgo+lbU8ZsGxV0HIvqqxo9l7mqYwyYMD9bKDg=="
"resolved" "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz"
@ -3042,6 +3049,13 @@
"@next/swc-win32-ia32-msvc" "13.2.4"
"@next/swc-win32-x64-msvc" "13.2.4"
"node-fetch@^2.6.7":
"integrity" "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A=="
"resolved" "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz"
"version" "2.7.0"
dependencies:
"whatwg-url" "^5.0.0"
"node-releases@^2.0.13":
"integrity" "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ=="
"resolved" "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz"
@ -3832,6 +3846,11 @@
dependencies:
"is-number" "^7.0.0"
"tr46@~0.0.3":
"integrity" "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw=="
"resolved" "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz"
"version" "0.0.3"
"trim-lines@^3.0.0":
"integrity" "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg=="
"resolved" "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz"
@ -4052,6 +4071,19 @@
optionalDependencies:
"fsevents" "~2.3.2"
"webidl-conversions@^3.0.0":
"integrity" "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
"resolved" "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz"
"version" "3.0.1"
"whatwg-url@^5.0.0":
"integrity" "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw=="
"resolved" "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz"
"version" "5.0.0"
dependencies:
"tr46" "~0.0.3"
"webidl-conversions" "^3.0.0"
"which-boxed-primitive@^1.0.2":
"integrity" "sha512-bwZdv0AKLpplFY2KZRX6TvyuN7ojjr7lwkg6ml0roIy9YeuSr7JS372qlNW18UQYzgYK9ziGcerWqZOmEn9VNg=="
"resolved" "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz"