diff --git a/app/api/chat/route.ts b/app/api/chat/route.ts index fef1de2..07a21f4 100644 --- a/app/api/chat/route.ts +++ b/app/api/chat/route.ts @@ -1,10 +1,20 @@ import { auth } from "@/auth"; import { chats, db } from "@/lib/db/schema"; -import { openai } from "@/lib/openai"; import { OpenAIStream, StreamingTextResponse } from "ai-connector"; +import { Configuration, OpenAIApi } from "openai-edge"; export const runtime = "edge"; +const configuration = new Configuration({ + apiKey: process.env.OPENAI_API_KEY, +}); + +const openai = new OpenAIApi(configuration); + +if (!process.env.OPENAI_API_KEY) { + throw new Error("Missing env var from OpenAI"); +} + export const POST = auth(async function POST(req: Request) { const json = await req.json(); // @ts-ignore diff --git a/app/chat-message.tsx b/app/chat-message.tsx index 7713293..8b84e4a 100644 --- a/app/chat-message.tsx +++ b/app/chat-message.tsx @@ -1,10 +1,10 @@ -import { type Message } from "@prisma/client"; import CodeBlock from "@/components/ui/codeblock"; import { MemoizedReactMarkdown } from "@/components/markdown"; import remarkGfm from "remark-gfm"; import remarkMath from "remark-math"; import { cn } from "@/lib/utils"; import { fontMessage } from "@/lib/fonts"; +import { Message } from "ai-connector"; export interface ChatMessageProps { message: Message; } diff --git a/lib/db/index.ts b/lib/db/index.ts deleted file mode 100644 index 60dd07d..0000000 --- a/lib/db/index.ts +++ /dev/null @@ -1,268 +0,0 @@ -/** - *
- *

Official Drizzle ORM adapter for Auth.js / NextAuth.js.

- * - * - * - *
- * - * ## Installation - * - * ```bash npm2yarn2pnpm - * npm install next-auth drizzle-orm @auth/drizzle-adapter - * npm install drizzle-kit --save-dev - * ``` - * - * @module @auth/drizzle-adapter - */ -import type { Adapter } from "@auth/core/adapters"; -import { and, eq } from "drizzle-orm"; -import type { DbClient, Schema } from "./schema"; - -/** - * ## Setup - * - * Add this adapter to your `pages/api/[...nextauth].js` next-auth configuration object: - * - * ```js title="pages/api/auth/[...nextauth].js" - * import NextAuth from "next-auth" - * import GoogleProvider from "next-auth/providers/google" - * import { DrizzleAdapter } from "@auth/drizzle-adapter" - * import { db } from "./db-schema" - * - * export default NextAuth({ - * adapter: DrizzleAdapter(db), - * providers: [ - * GoogleProvider({ - * clientId: process.env.GOOGLE_CLIENT_ID, - * clientSecret: process.env.GOOGLE_CLIENT_SECRET, - * }), - * ], - * }) - * ``` - * - * ## Advanced usage - * - * ### Create the Drizzle schema from scratch - * - * You'll need to create a database schema that includes the minimal schema for a `next-auth` adapter. - * Be sure to use the Drizzle driver version that you're using for your project. - * - * > This schema is adapted for use in Drizzle and based upon our main [schema](https://authjs.dev/reference/adapters#models) - * - * - * ```json title="db-schema.ts" - * - * import { integer, pgTable, text, primaryKey } from 'drizzle-orm/pg-core'; - * import { drizzle } from 'drizzle-orm/node-postgres'; - * import { migrate } from 'drizzle-orm/node-postgres/migrator'; - * import { Pool } from 'pg' - * import { ProviderType } from 'next-auth/providers'; - * - * export const users = pgTable('users', { - * id: text('id').notNull().primaryKey(), - * name: text('name'), - * email: text("email").notNull(), - * emailVerified: integer("emailVerified"), - * image: text("image"), - * }); - * - * export const accounts = pgTable("accounts", { - * userId: text("userId").notNull().references(() => users.id, { onDelete: "cascade" }), - * type: text("type").$type().notNull(), - * provider: text("provider").notNull(), - * providerAccountId: text("providerAccountId").notNull(), - * refresh_token: text("refresh_token"), - * access_token: text("access_token"), - * expires_at: integer("expires_at"), - * token_type: text("token_type"), - * scope: text("scope"), - * id_token: text("id_token"), - * session_state: text("session_state"), - * }, (account) => ({ - * _: primaryKey(account.provider, account.providerAccountId) - * })) - * - * export const sessions = pgTable("sessions", { - * userId: text("userId").notNull().references(() => users.id, { onDelete: "cascade" }), - * sessionToken: text("sessionToken").notNull().primaryKey(), - * expires: integer("expires").notNull(), - * }) - * - * export const verificationTokens = pgTable("verificationToken", { - * identifier: text("identifier").notNull(), - * token: text("token").notNull(), - * expires: integer("expires").notNull() - * }, (vt) => ({ - * _: primaryKey(vt.identifier, vt.token) - * })) - * - * const pool = new Pool({ - * connectionString: "YOUR_CONNECTION_STRING" - * }); - * - * export const db = drizzle(pool); - * - * migrate(db, { migrationsFolder: "./drizzle" }) - * - * ``` - * - **/ -export function PgAdapter( - client: DbClient, - { users, sessions, verificationTokens, accounts }: Schema -): Adapter { - return { - createUser: async (data) => { - return client - .insert(users) - .values({ ...data, id: crypto.randomUUID() as string }) - .returning() - .then((res) => res[0]); - }, - getUser: async (data) => { - return ( - client - .select() - .from(users) - .where(eq(users.id, data)) - .then((res) => res[0]) ?? null - ); - }, - getUserByEmail: async (data) => { - return ( - client - .select() - .from(users) - .where(eq(users.email, data)) - .then((res) => res[0]) ?? null - ); - }, - createSession: async (data) => { - return client - .insert(sessions) - .values(data) - .returning() - .then((res) => res[0]); - }, - getSessionAndUser: async (data) => { - return ( - client - .select({ - session: sessions, - user: users, - }) - .from(sessions) - .where(eq(sessions.sessionToken, data)) - .innerJoin(users, eq(users.id, sessions.userId)) - .then((res) => res[0]) ?? null - ); - }, - updateUser: async (data) => { - if (!data.id) { - throw new Error("No user id."); - } - - return client - .update(users) - .set(data) - .where(eq(users.id, data.id)) - .returning() - .then((res) => res[0]); - }, - updateSession: async (data) => { - return client - .update(sessions) - .set(data) - .where(eq(sessions.sessionToken, data.sessionToken)) - .returning() - .then((res) => res[0]); - }, - linkAccount: async (rawAccount) => { - const updatedAccount = await client - .insert(accounts) - .values(rawAccount) - .returning() - .then((res) => res[0]); - - // Drizzle will return `null` for fields that are not defined. - // However, the return type is expecting `undefined`. - const account = { - ...updatedAccount, - access_token: updatedAccount.access_token ?? undefined, - token_type: updatedAccount.token_type ?? undefined, - id_token: updatedAccount.id_token ?? undefined, - refresh_token: updatedAccount.refresh_token ?? undefined, - scope: updatedAccount.scope ?? undefined, - expires_at: updatedAccount.expires_at ?? undefined, - session_state: updatedAccount.session_state ?? undefined, - }; - - return account; - }, - getUserByAccount: async (account) => { - const dbAccount = await client - .select() - .from(accounts) - .where( - and( - eq(accounts.providerAccountId, account.providerAccountId), - eq(accounts.provider, account.provider) - ) - ) - .leftJoin(users, eq(accounts.userId, users.id)) - .then((res) => res[0]); - - return dbAccount.users; - }, - deleteSession: async (sessionToken) => { - await client - .delete(sessions) - .where(eq(sessions.sessionToken, sessionToken)); - }, - createVerificationToken: async (token) => { - return client - .insert(verificationTokens) - .values(token) - .returning() - .then((res) => res[0]); - }, - useVerificationToken: async (token) => { - try { - return ( - client - .delete(verificationTokens) - .where( - and( - eq(verificationTokens.identifier, token.identifier), - eq(verificationTokens.token, token.token) - ) - ) - .returning() - .then((res) => res[0]) ?? null - ); - } catch (err) { - throw new Error("No verification token found."); - } - }, - deleteUser: async (id) => { - await client - .delete(users) - .where(eq(users.id, id)) - .returning() - .then((res) => res[0]); - }, - unlinkAccount: async (account) => { - await client - .delete(accounts) - .where( - and( - eq(accounts.providerAccountId, account.providerAccountId), - eq(accounts.provider, account.provider) - ) - ); - - return undefined; - }, - }; -} diff --git a/lib/hooks/is-modifier-pressed.tsx b/lib/hooks/is-modifier-pressed.tsx deleted file mode 100644 index 10e3180..0000000 --- a/lib/hooks/is-modifier-pressed.tsx +++ /dev/null @@ -1,10 +0,0 @@ -import { isAppleDevice } from "@/lib/tinykeys"; - -/** - * Uses the Meta key on macOS, and the Ctrl key on other platforms. - */ -export const isModifierPressed = ( - event: KeyboardEvent | React.KeyboardEvent -): boolean => { - return isAppleDevice() ? event.metaKey : event.ctrlKey; -}; diff --git a/lib/hooks/use-command-enter-submit.tsx b/lib/hooks/use-command-enter-submit.tsx index 5c5d75c..bf178c7 100644 --- a/lib/hooks/use-command-enter-submit.tsx +++ b/lib/hooks/use-command-enter-submit.tsx @@ -1,6 +1,5 @@ import type { RefObject } from "react"; import { useRef } from "react"; -import { isModifierPressed } from "./is-modifier-pressed"; export function useCmdEnterSubmit(): { formRef: RefObject; @@ -11,8 +10,7 @@ export function useCmdEnterSubmit(): { const handleKeyDown = ( event: React.KeyboardEvent ): void => { - // Capture `⌘`|Ctrl + Enter - if (isModifierPressed(event) && event.key === "Enter") { + if (event.key === "Enter") { formRef.current?.requestSubmit(); } }; diff --git a/lib/openai.ts b/lib/openai.ts deleted file mode 100644 index a25ea2f..0000000 --- a/lib/openai.ts +++ /dev/null @@ -1,62 +0,0 @@ -import { - createParser, - type ParsedEvent, - type ReconnectInterval, -} from "eventsource-parser"; -import { Configuration, OpenAIApi } from "openai-edge"; - -const configuration = new Configuration({ - apiKey: process.env.OPENAI_API_KEY, -}); - -export const openai = new OpenAIApi(configuration); - -if (!process.env.OPENAI_API_KEY) { - throw new Error("Missing env var from OpenAI"); -} - -export async function OpenAIStream(res: Response) { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - let counter = 0; - - const stream = new ReadableStream({ - async start(controller) { - function onParse(event: ParsedEvent | ReconnectInterval) { - if (event.type === "event") { - const data = event.data; - if (data === "[DONE]") { - controller.close(); - return; - } - try { - const json = JSON.parse(data) as { - choices: any; - }; - const text = - json.choices[0]?.delta?.content ?? json.choices[0]?.text ?? ""; - - if (counter < 2 && (text.match(/\n/) || []).length) { - return; - } - - const queue = encoder.encode(JSON.stringify(text) + "\n"); - controller.enqueue(queue); - counter++; - } catch (e) { - controller.error(e); - } - } - } - - const parser = createParser(onParse); - - for await (const chunk of res.body as any) { - parser.feed(decoder.decode(chunk)); - } - }, - }); - - return stream; -} diff --git a/lib/tinykeys.tsx b/lib/tinykeys.tsx deleted file mode 100644 index 820d8cb..0000000 --- a/lib/tinykeys.tsx +++ /dev/null @@ -1,209 +0,0 @@ -// forked from https://github.com/jamiebuilds/tinykeys -// to fix navigator not being defined in SSR context -// import { type ModifierKey } from "react"; - -type ModifierKey = any; -/* - -MIT License - -Copyright (c) 2020 Jamie Kyle - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. - -*/ - -type KeyBindingPress = [string[], string]; - -/** - * A map of keybinding strings to event handlers. - */ -export interface KeyBindingMap { - [keybinding: string]: (event: React.KeyboardEvent) => void; -} - -export interface Options { - ignoreFocus?: boolean; -} - -/** - * These are the modifier keys that change the meaning of keybindings. - * - * Note: Ignoring "AltGraph" because it is covered by the others. - */ -let KEYBINDING_MODIFIER_KEYS = ["Shift", "Meta", "Alt", "Control"]; - -/** - * Keybinding sequences should timeout if individual key presses are more than - * 1s apart. - */ -let TIMEOUT = 1000; - -/** - * When focus is on these elements, ignore the keydown event. - */ -let inputs = ["select", "textarea", "input"]; - -export const isAppleDevice = (): boolean => { - return /Mac|iPod|iPhone|iPad/.test(navigator.platform); -}; - -/** - * Parses a "Key Binding String" into its parts - * - * grammar = `` - * = ` ...` - * = `` or `+` - * = `++...` - */ -function parse(str: string): KeyBindingPress[] { - const modifierKey = (isAppleDevice() ? "Meta" : "Control") as ModifierKey; - return str - .trim() - .split(" ") - .map((press) => { - let mods = press.split("+"); - - let key = mods.pop()!; - mods = mods.map((mod) => (mod === "$mod" ? modifierKey : mod)); - return [mods, key]; - }); -} - -/** - * This tells us if a series of events matches a key binding sequence either - * partially or exactly. - */ -function match(event: React.KeyboardEvent, press: KeyBindingPress): boolean { - // prettier-ignore - return !( - // Allow either the `event.key` or the `event.code` - // MDN event.key: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/key - // MDN event.code: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/code - ( - press[1].toUpperCase() !== event.key.toUpperCase() && - press[1] !== event.code - ) || - - // Ensure all the modifiers in the keybinding are pressed. - press[0].find((mod) => { - return !event.getModifierState(mod as ModifierKey) - }) || - - // KEYBINDING_MODIFIER_KEYS (Shift/Control/etc) change the meaning of a - // keybinding. So if they are pressed but aren't part of this keybinding, - // then we don't have a match. - KEYBINDING_MODIFIER_KEYS.find(mod => { - return !press[0].includes(mod) && event.getModifierState(mod as ModifierKey) - }) - ) -} - -/** - * Subscribes to keybindings. - * - * Returns an unsubscribe method. - * - * @example - * ```js - * import keybindings from "../src/keybindings" - * - * keybindings(window, { - * "Shift+d": () => { - * alert("The 'Shift' and 'd' keys were pressed at the same time") - * }, - * "y e e t": () => { - * alert("The keys 'y', 'e', 'e', and 't' were pressed in order") - * }, - * "$mod+d": () => { - * alert("Either 'Control+d' or 'Meta+d' were pressed") - * }, - * }) - * ``` - */ -export default function keybindings( - target: Window | HTMLElement, - keyBindingMap: KeyBindingMap, - options: Options = {} -): () => void { - const keyBindings = Object.keys(keyBindingMap).map((key) => { - return [parse(key), keyBindingMap[key]] as const; - }); - - const possibleMatches = new Map(); - - let timer: any = null; - - const onKeyDown = (event: React.KeyboardEvent): void => { - // Ignore modifier keydown events - // Note: This works because: - // - non-modifiers will always return false - // - if the current keypress is a modifier then it will return true when we check its state - // MDN: https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/getModifierState - if ( - event.getModifierState && - event.getModifierState(event.key as ModifierKey) - ) { - return; - } - - // Ignore event when a focusable item is focused - if (options.ignoreFocus) { - if (document.activeElement) { - if ( - inputs.indexOf(document.activeElement.tagName.toLowerCase()) !== -1 || - (document.activeElement as HTMLElement).contentEditable === "true" - ) { - return; - } - } - } - - keyBindings.forEach((keyBinding) => { - let sequence = keyBinding[0]; - - let callback = keyBinding[1]; - - let prev = possibleMatches.get(sequence); - - let remainingExpectedPresses = prev ? prev : sequence; - - let currentExpectedPress = remainingExpectedPresses[0]; - - let matches = match(event, currentExpectedPress); - - if (!matches) { - possibleMatches.delete(sequence); - } else if (remainingExpectedPresses.length > 1) { - possibleMatches.set(sequence, remainingExpectedPresses.slice(1)); - } else { - possibleMatches.delete(sequence); - callback(event); - } - }); - - clearTimeout(timer); - timer = setTimeout(possibleMatches.clear.bind(possibleMatches), TIMEOUT); - }; - - target.addEventListener("keydown", onKeyDown as any); - return (): void => { - target.removeEventListener("keydown", onKeyDown as any); - }; -} diff --git a/package.json b/package.json index 52b1db2..f571296 100644 --- a/package.json +++ b/package.json @@ -31,7 +31,6 @@ "common-tags": "^1.8.2", "cookie": "^0.5.0", "drizzle-orm": "^0.26.0", - "eventsource-parser": "^1.0.0", "focus-trap-react": "^10.1.1", "framer-motion": "^10.12.4", "jose": "^4.14.4", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 02191d7..463b48e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -43,9 +43,6 @@ dependencies: drizzle-orm: specifier: ^0.26.0 version: 0.26.0(@vercel/postgres@0.3.0) - eventsource-parser: - specifier: ^1.0.0 - version: 1.0.0 focus-trap-react: specifier: ^10.1.1 version: 10.1.1(prop-types@15.8.1)(react-dom@18.2.0)(react@18.2.0)