From 7a9f0c956cdd52b113a768ebd316ccc87f434bed Mon Sep 17 00:00:00 2001 From: Avraham Sakal Date: Sun, 13 Jul 2025 20:14:53 -0400 Subject: [PATCH] setup postgres and kyseley for most data instead of milvus --- .kanelrc.cjs | 11 + components/Link.tsx | 10 - database/generated/Database.ts | 8 + database/generated/public/Conversations.ts | 25 + database/generated/public/FactTriggers.ts | 32 ++ database/generated/public/Facts.ts | 28 + database/generated/public/Messages.ts | 32 ++ database/generated/public/PublicSchema.ts | 23 + database/generated/public/Role.ts | 10 + database/generated/public/Tools.ts | 36 ++ database/generated/public/Users.ts | 28 + database/milvus.ts | 22 +- database/postgres.ts | 21 + layouts/LayoutDefault.tsx | 128 ++++- layouts/hover.css | 20 + package.json | 11 +- pages/+config.ts | 4 +- pages/chat/+Page.tsx | 112 ---- pages/chat/@id/+Page.tsx | 142 +++++ pages/chat/@id/+data.ts | 13 + pages/chat/README.md | 5 +- pages/chat/trpc.ts | 66 ++- pages/chat/types.ts | 20 - pnpm-lock.yaml | 624 +++++++++++++++++++++ state.ts | 38 ++ trpc/server.ts | 2 + types.ts | 36 ++ 27 files changed, 1338 insertions(+), 169 deletions(-) create mode 100644 .kanelrc.cjs delete mode 100644 components/Link.tsx create mode 100644 database/generated/Database.ts create mode 100644 database/generated/public/Conversations.ts create mode 100644 database/generated/public/FactTriggers.ts create mode 100644 database/generated/public/Facts.ts create mode 100644 database/generated/public/Messages.ts create mode 100644 database/generated/public/PublicSchema.ts create mode 100644 database/generated/public/Role.ts create mode 100644 database/generated/public/Tools.ts create mode 100644 database/generated/public/Users.ts create mode 100644 database/postgres.ts create mode 100644 layouts/hover.css delete mode 100644 pages/chat/+Page.tsx create mode 100644 pages/chat/@id/+Page.tsx create mode 100644 pages/chat/@id/+data.ts delete mode 100644 pages/chat/types.ts create mode 100644 state.ts create mode 100644 types.ts diff --git a/.kanelrc.cjs b/.kanelrc.cjs new file mode 100644 index 0000000..2117b91 --- /dev/null +++ b/.kanelrc.cjs @@ -0,0 +1,11 @@ +const { makeKyselyHook } = require("kanel-kysely"); + +module.exports = { + connection: "postgres://neondb_owner:npg_sOVmj8vWq2zG@ep-withered-king-adiz9gpi-pooler.c-2.us-east-1.aws.neon.tech:5432/neondb?sslmode=require&channel_binding=true", + enumStyle: "type", + outputPath: "./database/generated", + preRenderHooks: [makeKyselyHook()], + customTypeMap: { + "pg_catalog.timestamp": "string", + } +}; \ No newline at end of file diff --git a/components/Link.tsx b/components/Link.tsx deleted file mode 100644 index ab0f598..0000000 --- a/components/Link.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { usePageContext } from "vike-react/usePageContext"; -import { NavLink } from "@mantine/core"; - -export function Link({ href, label }: { href: string; label: string }) { - const pageContext = usePageContext(); - const { urlPathname } = pageContext; - const isActive = - href === "/" ? urlPathname === href : urlPathname.startsWith(href); - return ; -} diff --git a/database/generated/Database.ts b/database/generated/Database.ts new file mode 100644 index 0000000..2fa9940 --- /dev/null +++ b/database/generated/Database.ts @@ -0,0 +1,8 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { default as PublicSchema } from './public/PublicSchema'; + +type Database = PublicSchema; + +export default Database; diff --git a/database/generated/public/Conversations.ts b/database/generated/public/Conversations.ts new file mode 100644 index 0000000..660622f --- /dev/null +++ b/database/generated/public/Conversations.ts @@ -0,0 +1,25 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { UsersId } from './Users'; +import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; + +/** Identifier type for public.conversations */ +export type ConversationsId = number & { __brand: 'public.conversations' }; + +/** Represents the table public.conversations */ +export default interface ConversationsTable { + id: ColumnType; + + title: ColumnType; + + created_at: ColumnType; + + user_id: ColumnType; +} + +export type Conversations = Selectable; + +export type NewConversations = Insertable; + +export type ConversationsUpdate = Updateable; diff --git a/database/generated/public/FactTriggers.ts b/database/generated/public/FactTriggers.ts new file mode 100644 index 0000000..27bb280 --- /dev/null +++ b/database/generated/public/FactTriggers.ts @@ -0,0 +1,32 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { FactsId } from './Facts'; +import type { ConversationsId } from './Conversations'; +import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; + +/** Identifier type for public.fact_triggers */ +export type FactTriggersId = number & { __brand: 'public.fact_triggers' }; + +/** Represents the table public.fact_triggers */ +export default interface FactTriggersTable { + id: ColumnType; + + fact_id: ColumnType; + + trigger_phrase: ColumnType; + + priority_multiplier: ColumnType; + + priority_multiplier_reason: ColumnType; + + scope_conversation_id: ColumnType; + + created_at: ColumnType; +} + +export type FactTriggers = Selectable; + +export type NewFactTriggers = Insertable; + +export type FactTriggersUpdate = Updateable; diff --git a/database/generated/public/Facts.ts b/database/generated/public/Facts.ts new file mode 100644 index 0000000..869a654 --- /dev/null +++ b/database/generated/public/Facts.ts @@ -0,0 +1,28 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { UsersId } from './Users'; +import type { MessagesId } from './Messages'; +import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; + +/** Identifier type for public.facts */ +export type FactsId = number & { __brand: 'public.facts' }; + +/** Represents the table public.facts */ +export default interface FactsTable { + id: ColumnType; + + user_id: ColumnType; + + source_message_id: ColumnType; + + content: ColumnType; + + created_at: ColumnType; +} + +export type Facts = Selectable; + +export type NewFacts = Insertable; + +export type FactsUpdate = Updateable; diff --git a/database/generated/public/Messages.ts b/database/generated/public/Messages.ts new file mode 100644 index 0000000..20caba6 --- /dev/null +++ b/database/generated/public/Messages.ts @@ -0,0 +1,32 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { ConversationsId } from './Conversations'; +import type { default as Role } from './Role'; +import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; + +/** Identifier type for public.messages */ +export type MessagesId = number & { __brand: 'public.messages' }; + +/** Represents the table public.messages */ +export default interface MessagesTable { + id: ColumnType; + + conversation_id: ColumnType; + + index: ColumnType; + + content: ColumnType; + + running_summary: ColumnType; + + created_at: ColumnType; + + role: ColumnType; +} + +export type Messages = Selectable; + +export type NewMessages = Insertable; + +export type MessagesUpdate = Updateable; diff --git a/database/generated/public/PublicSchema.ts b/database/generated/public/PublicSchema.ts new file mode 100644 index 0000000..5612c18 --- /dev/null +++ b/database/generated/public/PublicSchema.ts @@ -0,0 +1,23 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { default as UsersTable } from './Users'; +import type { default as MessagesTable } from './Messages'; +import type { default as ToolsTable } from './Tools'; +import type { default as FactTriggersTable } from './FactTriggers'; +import type { default as FactsTable } from './Facts'; +import type { default as ConversationsTable } from './Conversations'; + +export default interface PublicSchema { + users: UsersTable; + + messages: MessagesTable; + + tools: ToolsTable; + + fact_triggers: FactTriggersTable; + + facts: FactsTable; + + conversations: ConversationsTable; +} diff --git a/database/generated/public/Role.ts b/database/generated/public/Role.ts new file mode 100644 index 0000000..309d9ed --- /dev/null +++ b/database/generated/public/Role.ts @@ -0,0 +1,10 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +/** Represents the enum public.role */ +type Role = + | 'user' + | 'assistant' + | 'system'; + +export default Role; diff --git a/database/generated/public/Tools.ts b/database/generated/public/Tools.ts new file mode 100644 index 0000000..b18899f --- /dev/null +++ b/database/generated/public/Tools.ts @@ -0,0 +1,36 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { UsersId } from './Users'; +import type { MessagesId } from './Messages'; +import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; + +/** Identifier type for public.tools */ +export type ToolsId = number & { __brand: 'public.tools' }; + +/** Represents the table public.tools */ +export default interface ToolsTable { + id: ColumnType; + + user_id: ColumnType; + + source_message_id: ColumnType; + + name: ColumnType; + + description: ColumnType; + + parameter_schema: ColumnType; + + implementation_language: ColumnType; + + implementation_code: ColumnType; + + created_at: ColumnType; +} + +export type Tools = Selectable; + +export type NewTools = Insertable; + +export type ToolsUpdate = Updateable; diff --git a/database/generated/public/Users.ts b/database/generated/public/Users.ts new file mode 100644 index 0000000..0dcf5bb --- /dev/null +++ b/database/generated/public/Users.ts @@ -0,0 +1,28 @@ +// @generated +// This file is automatically generated by Kanel. Do not modify manually. + +import type { ColumnType, Selectable, Insertable, Updateable } from 'kysely'; + +/** Identifier type for public.users */ +export type UsersId = number & { __brand: 'public.users' }; + +/** Represents the table public.users */ +export default interface UsersTable { + id: ColumnType; + + username: ColumnType; + + password: ColumnType; + + email: ColumnType; + + last_login: ColumnType; + + created_at: ColumnType; +} + +export type Users = Selectable; + +export type NewUsers = Insertable; + +export type UsersUpdate = Updateable; diff --git a/database/milvus.ts b/database/milvus.ts index 901fed8..97c547b 100644 --- a/database/milvus.ts +++ b/database/milvus.ts @@ -24,16 +24,16 @@ async function initialize() { console.log("Creating collection: facts"); await client.createCollection({ collection_name: "facts", - auto_id: true, fields: [ { name: "id", data_type: DataType.Int64, is_primary_key: true, + autoID: true, }, { name: "user_id", - data_type: DataType.Int64, + data_type: DataType.Int32, description: "Foreign key linking to the Users Collection. Crucial if you have multiple users.", }, @@ -78,12 +78,12 @@ async function initialize() { console.log("Creating collection: fact_triggers"); await client.createCollection({ collection_name: "fact_triggers", - auto_id: true, fields: [ { name: "id", data_type: DataType.Int64, is_primary_key: true, + autoID: true, }, { name: "fact_id", @@ -140,16 +140,16 @@ async function initialize() { console.log("Creating collection: conversations"); await client.createCollection({ collection_name: "conversations", - auto_id: true, fields: [ { name: "id", data_type: DataType.Int64, is_primary_key: true, + autoID: true, }, { name: "user_id", - data_type: DataType.Int64, + data_type: DataType.Int32, description: "Foreign key linking to the Users Collection. This is the user who sent this message.", }, @@ -186,16 +186,16 @@ async function initialize() { console.log("Creating collection: conversation_messages"); await client.createCollection({ collection_name: "conversation_messages", - auto_id: true, fields: [ { name: "id", data_type: DataType.Int64, is_primary_key: true, + autoID: true, }, { name: "user_id", - data_type: DataType.Int64, + data_type: DataType.Int32, description: "Foreign key linking to the Users Collection. This is the user who sent this message.", }, @@ -268,19 +268,19 @@ async function initialize() { console.log("Creating collection: tools"); await client.createCollection({ collection_name: "tools", - auto_id: true, fields: [ /** Primary key, unique identifier for each fact. */ { name: "id", data_type: DataType.Int64, is_primary_key: true, + autoID: true, }, /** Foreign key linking to the Users Collection. * Crucial if you have multiple users. */ { name: "user_id", - data_type: DataType.Int64, + data_type: DataType.Int32, description: "Foreign key linking to the Users Collection. Crucial if you have multiple users.", }, @@ -362,12 +362,12 @@ async function initialize() { // console.log("Creating collection: users"); // await client.createCollection({ // collection_name: "users", - // auto_id: true, // fields: [ // { // name: "id", - // data_type: DataType.Int64, + // data_type: DataType.Int32, // is_primary_key: true, + // autoID: true, // }, // { // name: "username", diff --git a/database/postgres.ts b/database/postgres.ts new file mode 100644 index 0000000..ef75c4c --- /dev/null +++ b/database/postgres.ts @@ -0,0 +1,21 @@ +import { Pool } from "pg"; +import { Kysely, PostgresDialect } from "kysely"; +import type Database from "./generated/Database"; + +export const pool = new Pool({ + connectionString: + "postgres://neondb_owner:npg_sOVmj8vWq2zG@ep-withered-king-adiz9gpi-pooler.c-2.us-east-1.aws.neon.tech:5432/neondb?sslmode=require&channel_binding=true", + // channelBinding: require ? +}); + +const dialect = new PostgresDialect({ + pool, +}); + +// Database interface is passed to Kysely's constructor, and from now on, Kysely +// knows your database structure. +// Dialect is passed to Kysely's constructor, and from now on, Kysely knows how +// to communicate with your database. +export const db = new Kysely({ + dialect, +}); diff --git a/layouts/LayoutDefault.tsx b/layouts/LayoutDefault.tsx index 4a790d9..2f9f0e3 100644 --- a/layouts/LayoutDefault.tsx +++ b/layouts/LayoutDefault.tsx @@ -1,15 +1,62 @@ import "@mantine/core/styles.css"; -import { AppShell, Burger, Group, Image, MantineProvider } from "@mantine/core"; +import { + AppShell, + Burger, + Group, + Image, + MantineProvider, + NavLink, +} from "@mantine/core"; +import { + IconHome2, + IconChevronRight, + IconActivity, + IconTrash, + IconCircle, + IconCircleFilled, + IconTrashFilled, +} from "@tabler/icons-react"; import { useDisclosure } from "@mantine/hooks"; import theme from "./theme.js"; - import logoUrl from "../assets/logo.svg"; -import { Link } from "../components/Link"; +import { useStore } from "../state.js"; +import { useEffect } from "react"; +import { trpc } from "../trpc/client.js"; +import { usePageContext } from "vike-react/usePageContext"; +import "./hover.css"; +import type { ConversationsId } from "../database/generated/public/Conversations.js"; export default function LayoutDefault({ children, }: { children: React.ReactNode }) { + const pageContext = usePageContext(); + const { urlPathname } = pageContext; const [opened, { toggle }] = useDisclosure(); + const conversations = useStore((state) => state.conversations); + const setConversations = useStore((state) => state.setConversations); + const addConversation = useStore((state) => state.addConversation); + const removeConversation = useStore((state) => state.removeConversation); + const conversationId = useStore((state) => state.conversationId); + + useEffect(() => { + trpc.chat.listConversations.query().then((res) => { + setConversations(res); + }); + }, [setConversations]); + + // useEffect(() => { + // if (isConversationListExpanded) { + // trpc.chat.listConversations.query().then((res) => { + // setConversations(res); + // }); + // } + // }, [isConversationListExpanded]); + + function handleDeleteConversation(conversationId: ConversationsId) { + removeConversation(conversationId); + trpc.chat.deleteConversation.mutate({ id: conversationId }); + } + return ( - - - - + + + + } + rightSection={ + + } + variant="subtle" + active={urlPathname.startsWith("/chat")} + > + {conversations.map((conversation) => ( + + + + + } + rightSection={ + <> + { + e.stopPropagation(); + e.preventDefault(); + handleDeleteConversation(conversation.id); + }} + className="show-by-default" + /> + { + e.stopPropagation(); + e.preventDefault(); + handleDeleteConversation(conversation.id); + }} + className="show-on-hover border-on-hover" + /> + + } + variant="subtle" + active={conversation.id === conversationId} + /> + ))} + {children} diff --git a/layouts/hover.css b/layouts/hover.css new file mode 100644 index 0000000..9efa255 --- /dev/null +++ b/layouts/hover.css @@ -0,0 +1,20 @@ +.hover-container .show-on-hover { + display: none; +} + +.hover-container:hover .show-on-hover { + display: block; +} + +.hover-container .show-by-default { + display: block; +} + +.hover-container:hover .show-by-default { + display: none; +} + +.border-on-hover:hover { + border: 1px solid var(--mantine-color-gray-9); + border-radius: var(--mantine-radius-sm); +} diff --git a/package.json b/package.json index 347ecd4..ef03e0d 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,8 @@ "format": "biome format --write .", "preview:wrangler": "wrangler pages dev", "deploy:wrangler": "wrangler pages deploy", - "deploy": "run-s build deploy:wrangler" + "deploy": "run-s build deploy:wrangler", + "generate-types": "kanel" }, "dependencies": { "@ai-sdk/react": "^1.2.12", @@ -18,6 +19,7 @@ "@mantine/hooks": "^8.1.1", "@openrouter/ai-sdk-provider": "^0.7.2", "@sinclair/typebox": "^0.34.37", + "@tabler/icons-react": "^3.34.0", "@trpc/client": "^11.4.2", "@trpc/server": "^11.4.2", "@universal-middleware/core": "^0.4.8", @@ -27,6 +29,8 @@ "ai": "^4.3.16", "dotenv": "^17.0.0", "hono": "^4.8.2", + "kysely": "^0.28.2", + "pg": "^8.16.3", "react": "^19.1.0", "react-dom": "^19.1.0", "vike": "^0.4.235", @@ -40,8 +44,11 @@ "@cloudflare/workers-types": "^4.20250620.0", "@hono/vite-dev-server": "^0.19.1", "@types/node": "^20.19.0", + "@types/pg": "^8.15.4", "@types/react": "^19.1.8", "@types/react-dom": "^19.1.6", + "kanel": "^3.14.1", + "kanel-kysely": "^0.7.1", "npm-run-all2": "^8.0.4", "postcss": "^8.5.6", "postcss-preset-mantine": "^1.17.0", @@ -52,4 +59,4 @@ "wrangler": "^4.20.5" }, "type": "module" -} +} \ No newline at end of file diff --git a/pages/+config.ts b/pages/+config.ts index 38a47a6..64c6059 100644 --- a/pages/+config.ts +++ b/pages/+config.ts @@ -10,8 +10,8 @@ export default { Layout, // https://vike.dev/head-tags - title: "My Vike App", - description: "Demo showcasing Vike", + title: "Trainable AI", + description: "The Chatbot that Remembers", passToClient: ["user"], extends: vikeReact, diff --git a/pages/chat/+Page.tsx b/pages/chat/+Page.tsx deleted file mode 100644 index 7b59176..0000000 --- a/pages/chat/+Page.tsx +++ /dev/null @@ -1,112 +0,0 @@ -import { JsonInput, Tabs, Textarea } from "@mantine/core"; -import { trpc } from "../../trpc/client"; -import { create } from "zustand"; -import type { Message as UIMessage } from "ai"; -import type { OtherParameters, Store } from "./types.js"; - -const defaultSystemPrompt = `You are a helpful assistant that answers questions based on the provided context. If you don't know the answer, just say that you don't know, don't try to make up an answer.`; -const defaultParameters = { - temperature: 0.5, - max_tokens: 100, -} as OtherParameters; - -const useStore = create()((set) => ({ - messages: [], - message: "", - systemPrompt: defaultSystemPrompt, - parameters: defaultParameters, - loading: false, - setMessages: (messages) => set({ messages }), - setMessage: (message) => set({ message }), - setSystemPrompt: (systemPrompt) => set({ systemPrompt }), - setParameters: (parameters) => set({ parameters }), - setLoading: (loading) => set({ loading }), -})); - -export default function ChatPage() { - const messages = useStore((state) => state.messages); - const message = useStore((state) => state.message); - const systemPrompt = useStore((state) => state.systemPrompt); - const parameters = useStore((state) => state.parameters); - const loading = useStore((state) => state.loading); - const setMessages = useStore((state) => state.setMessages); - const setMessage = useStore((state) => state.setMessage); - const setSystemPrompt = useStore((state) => state.setSystemPrompt); - const setParameters = useStore((state) => state.setParameters); - const setLoading = useStore((state) => state.setLoading); - - return ( - - - Message - System Prompt - Parameters - - - -