diff --git a/.gitignore b/.gitignore index 3c7674b..f44c2c8 100644 --- a/.gitignore +++ b/.gitignore @@ -140,3 +140,5 @@ dist # aws-cdk .cdk.staging cdk.out + +db.json \ No newline at end of file diff --git a/database/lowdb.ts b/database/lowdb.ts new file mode 100644 index 0000000..4e2e228 --- /dev/null +++ b/database/lowdb.ts @@ -0,0 +1,30 @@ +import { Low } from "lowdb"; +import { JSONFile } from "lowdb/node"; + +export type Conversation = { + id: string; + title: string; + userId: string; +}; + +type DB = { + conversations: Array; + messages: Array<{ + id: string; + conversationId: string; + content: string; + role: "user" | "assistant" | "system" | "data"; + index: number; + createdAt: string; + runningSummary?: string; + }>; +}; + +export const db = new Low(new JSONFile("db.json"), { + conversations: [], + messages: [], +}); +/** Initialize the database. Sets `db.data` to the default state if the file doesn't exist. */ +await db.read(); +/** Write the database to the file, in case it didn't exist before. */ +await db.write(); diff --git a/package.json b/package.json index 03134f8..461f87a 100644 --- a/package.json +++ b/package.json @@ -31,6 +31,8 @@ "hono": "^4.8.2", "immer": "^10.1.1", "kysely": "^0.28.2", + "lowdb": "^7.0.1", + "nanoid": "^5.1.5", "pg": "^8.16.3", "react": "^19.1.0", "react-dom": "^19.1.0", diff --git a/pages/chat/@id/+Page.tsx b/pages/chat/@id/+Page.tsx index 9cdb29a..29b9d26 100644 --- a/pages/chat/@id/+Page.tsx +++ b/pages/chat/@id/+Page.tsx @@ -17,13 +17,12 @@ import { import { usePageContext } from "vike-react/usePageContext"; import { useData } from "vike-react/useData"; import type { Data } from "./+data"; -import type { ConversationsId } from "../../../database/generated/public/Conversations"; import type { CommittedMessage, DraftMessage } from "../../../types"; import Markdown from "react-markdown"; export default function ChatPage() { const pageContext = usePageContext(); - const conversationId = Number(pageContext.routeParams.id) as ConversationsId; + const conversationId = pageContext.routeParams.id; const conversationTitle = useStore( (state) => state.conversations.find((c) => c.id === conversationId)?.title, ); @@ -126,7 +125,7 @@ export default function ChatPage() { content: response.insertedAssistantMessage?.content, index: response.insertedAssistantMessage?.index, runningSummary: - response.insertedAssistantMessage?.running_summary || + response.insertedAssistantMessage?.runningSummary || undefined, } as CommittedMessage, ]; @@ -185,17 +184,18 @@ function Messages({ bdrs="md" >
- {"index" in message ? message.index : ""} - {message.role} + {"index" in message ? message.index : ""} {message.role}
{message.content} - {"runningSummary" in message && ( + + {"runningSummary" in message && ( +
Running Summary: {message.runningSummary}
- )} -
+ + )} ))} diff --git a/pages/chat/@id/+data.ts b/pages/chat/@id/+data.ts index 76930e1..c9f7c67 100644 --- a/pages/chat/@id/+data.ts +++ b/pages/chat/@id/+data.ts @@ -7,10 +7,10 @@ export const data = async (pageContext: PageContextServer) => { const { id } = pageContext.routeParams; const caller = createCaller({}); const conversation = await caller.fetchConversation({ - id: Number(id), + id, }); const messages = await caller.fetchMessages({ - conversationId: Number(id), + conversationId: id, }); return { conversation, messages }; }; diff --git a/pages/chat/trpc.ts b/pages/chat/trpc.ts index 31e1ec4..9702428 100644 --- a/pages/chat/trpc.ts +++ b/pages/chat/trpc.ts @@ -17,9 +17,8 @@ import { env } from "../../server/env.js"; // ConsistencyLevelEnum, // type NumberArrayId, // } from "@zilliz/milvus2-sdk-node"; -import { db } from "../../database/postgres"; -import type { ConversationsId } from "../../database/generated/public/Conversations"; -import type { UsersId } from "../../database/generated/public/Users"; +import { db } from "../../database/lowdb"; +import { nanoid } from "nanoid"; const mainSystemPrompt = ({ systemPrompt, @@ -47,75 +46,64 @@ const openrouter = createOpenRouter({ export const chat = router({ listConversations: publicProcedure.query(async () => { - const rows = await db.selectFrom("conversations").selectAll().execute(); + const rows = await db.data.conversations; return rows; }), fetchConversation: publicProcedure - .input((x) => x as { id: number }) + .input((x) => x as { id: string }) .query(async ({ input: { id } }) => { - const row = await db - .selectFrom("conversations") - .selectAll() - .where("id", "=", id as ConversationsId) - .executeTakeFirst(); + const row = await db.data.conversations.find((c) => c.id === id); return row; }), createConversation: publicProcedure.mutation(async () => { const title = "New Conversation"; - const row = await db - .insertInto("conversations") - .values({ - title, - user_id: 1 as UsersId, - }) - .returningAll() - .executeTakeFirst(); + const row = { + id: nanoid(), + title, + userId: "1", + }; + await db.data.conversations.push(row); + db.write(); return row; }), deleteConversation: publicProcedure - .input((x) => x as { id: number }) + .input((x) => x as { id: string }) .mutation(async ({ input: { id } }) => { - const result = await db - .deleteFrom("conversations") - .where("id", "=", Number(id) as ConversationsId) - .execute(); - return result; + await db.data.conversations.splice( + db.data.conversations.findIndex((c) => c.id === id), + 1, + ); + db.write(); + return { ok: true }; }), updateConversationTitle: publicProcedure .input( (x) => x as { - id: number; + id: string; title: string; }, ) .mutation(async ({ input: { id, title } }) => { - const result = await db - .updateTable("conversations") - .set({ title }) - .where("id", "=", Number(id) as ConversationsId) - .execute(); - return result[0]; + const conversation = await db.data.conversations.find((c) => c.id === id); + if (!conversation) throw new Error("Conversation not found"); + conversation.title = title; + db.write(); + return { ok: true }; }), fetchMessages: publicProcedure - .input((x) => x as { conversationId: number }) + .input((x) => x as { conversationId: string }) .query(async ({ input: { conversationId } }) => { - const rows = await db - .selectFrom("messages") - .selectAll() - .where("conversation_id", "=", conversationId as ConversationsId) - .execute(); - return rows.map((row) => ({ - ...row, - conversationId: conversationId as ConversationsId, - runningSummary: row.running_summary, - })) as Array; + const rows = await db.data.messages.filter( + (m) => m.conversationId === conversationId, + ); + return rows as Array; }), sendMessage: publicProcedure .input( (x) => x as { - conversationId: number; + conversationId: string; messages: Array; systemPrompt: string; parameters: OtherParameters; @@ -140,17 +128,17 @@ export const chat = router({ .runningSummary as string) : ""; /** Save the incoming message to the database. */ - const insertedUserMessage = await db - .insertInto("messages") - .values({ - conversation_id: conversationId as ConversationsId, - content: messages[messages.length - 1].content, - role: "user" as const, - index: messages.length - 1, - created_at: new Date().toISOString(), - }) - .returning(["id", "index"]) - .executeTakeFirst(); + const insertedUserMessage: CommittedMessage = { + id: nanoid(), + conversationId, + content: messages[messages.length - 1].content, + role: "user" as const, + index: messages.length - 1, + createdAt: new Date().toISOString(), + }; + await db.data.messages.push(insertedUserMessage); + // do not db.write() until the end + /** Generate a new message from the model, but hold-off on adding it to * the database until we produce the associated running-summary, below. * The model should be given the conversation summary thus far, and of @@ -249,18 +237,17 @@ export const chat = router({ tools: undefined, ...parameters, }); - const insertedAssistantMessage = await db - .insertInto("messages") - .values({ - conversation_id: conversationId as ConversationsId, - content: mainResponse.text, - running_summary: runningSummaryResponse.text, - role: "assistant" as const, - index: messages.length, - created_at: new Date().toISOString(), - }) - .returningAll() - .executeTakeFirst(); + const insertedAssistantMessage: CommittedMessage = { + id: nanoid(), + conversationId, + content: mainResponse.text, + runningSummary: runningSummaryResponse.text, + role: "assistant" as const, + index: messages.length, + createdAt: new Date().toISOString(), + }; + await db.data.messages.push(insertedAssistantMessage); + await db.write(); /** TODO: notify the caller, somehow, that some messages were saved to * the database and/or were outfitted with runningSummaries, so the * caller can update its UI state. */ diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 4e24524..1442f91 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -68,6 +68,12 @@ importers: kysely: specifier: ^0.28.2 version: 0.28.2 + lowdb: + specifier: ^7.0.1 + version: 7.0.1 + nanoid: + specifier: ^5.1.5 + version: 5.1.5 pg: specifier: ^8.16.3 version: 8.16.3 @@ -1903,6 +1909,10 @@ packages: longest-streak@3.1.0: resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==} + lowdb@7.0.1: + resolution: {integrity: sha512-neJAj8GwF0e8EpycYIDFqEPcx9Qz4GUho20jWFR7YiFeXzF1YMLdxB36PypcTSPMA+4+LvgyMacYhlr18Zlymw==} + engines: {node: '>=18'} + lru-cache@10.4.3: resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} @@ -2071,6 +2081,11 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + nanoid@5.1.5: + resolution: {integrity: sha512-Ir/+ZpE9fDsNH0hQ3C68uyThDXzYcim2EqcZ8zn8Chtt1iylPT9xXJB0kPCnqzgcEGikO9RxSrh63MsmVCU7Fw==} + engines: {node: ^18 || >=20} + hasBin: true + node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} @@ -2682,6 +2697,10 @@ packages: stacktracey@2.1.8: resolution: {integrity: sha512-Kpij9riA+UNg7TnphqjH7/CzctQ/owJGNbFkfEeve4Z4uxT5+JapVLFXcsurIfN34gnTWZNJ/f7NMG0E8JDzTw==} + steno@4.0.2: + resolution: {integrity: sha512-yhPIQXjrlt1xv7dyPQg2P17URmXbuM5pdGkpiMB3RenprfiBlvK415Lctfe0eshk90oA7/tNq7WEiMK8RSP39A==} + engines: {node: '>=18'} + stoppable@1.1.0: resolution: {integrity: sha512-KXDYZ9dszj6bzvnEMRYvxgeTHU74QBFL54XKtP3nyMuJ81CFYtABZ3bAzL2EdFUaEwJOBOgENyFj3R7oTzDyyw==} engines: {node: '>=4', npm: '>=6'} @@ -4688,6 +4707,10 @@ snapshots: longest-streak@3.1.0: {} + lowdb@7.0.1: + dependencies: + steno: 4.0.2 + lru-cache@10.4.3: {} lru-cache@11.1.0: {} @@ -4981,6 +5004,8 @@ snapshots: nanoid@3.3.11: {} + nanoid@5.1.5: {} + node-releases@2.0.19: {} normalize-range@0.1.2: {} @@ -5606,6 +5631,8 @@ snapshots: as-table: 1.0.55 get-source: 2.0.12 + steno@4.0.2: {} + stoppable@1.1.0: {} string-width@4.2.3: diff --git a/state.ts b/state.ts index 9ccdf0c..4cfacec 100644 --- a/state.ts +++ b/state.ts @@ -11,7 +11,7 @@ export const defaultParameters = { export const useStore = create()( immer((set, get) => ({ - selectedConversationId: 0 as ConversationsId, + selectedConversationId: "", conversations: [], messages: [], message: "", diff --git a/types.ts b/types.ts index cb67d97..3ce375e 100644 --- a/types.ts +++ b/types.ts @@ -1,32 +1,29 @@ import type { Message as UIMessage } from "ai"; import type { generateText } from "ai"; -import type { - Conversations, - ConversationsId, -} from "./database/generated/public/Conversations"; +import type { Conversation } from "./database/lowdb.js"; export type OtherParameters = Omit< Parameters[0], "model" | "messages" | "abortSignal" >; -export type ConversationUI = Conversations & {}; +export type ConversationUI = Conversation & {}; export type Store = { /** This is a string because Milvus sends it as a string, and the value * overflows the JS integer anyway. */ - selectedConversationId: ConversationsId; + selectedConversationId: string; conversations: Array; messages: Array; message: string; systemPrompt: string; parameters: OtherParameters; loading: boolean; - setConversationId: (conversationId: ConversationsId) => void; + setConversationId: (conversationId: string) => void; setConversationTitle: (conversationTitle: string) => void; setConversations: (conversations: Array) => void; addConversation: (conversation: ConversationUI) => void; - removeConversation: (conversationId: ConversationsId) => void; + removeConversation: (conversationId: string) => void; setMessages: (messages: Array) => void; setMessage: (message: string) => void; setSystemPrompt: (systemPrompt: string) => void; @@ -35,11 +32,12 @@ export type Store = { }; /** The message while it's being typed in the input box. */ -export type DraftMessage = Omit; +export type DraftMessage = Omit; /** The message after it's been saved to the database. */ export type CommittedMessage = DraftMessage & { - id: number; - conversationId: ConversationsId; + id: string; + conversationId: string; index: number; runningSummary?: string; + createdAt: string; };