Move to drizzle (#6)
This commit is contained in:
commit
45d3a8d0ee
33 changed files with 1145 additions and 750 deletions
|
|
@ -1,24 +1,31 @@
|
|||
"use server";
|
||||
|
||||
import { auth } from "@/auth";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { chats, db } from "@/lib/db/schema";
|
||||
import { and, eq } from "drizzle-orm";
|
||||
import { revalidatePath } from "next/cache";
|
||||
|
||||
export async function removeChat({ id, path }: { id: string; path: string }) {
|
||||
const session = await auth();
|
||||
export async function removeChat({
|
||||
id,
|
||||
path,
|
||||
userId,
|
||||
}: {
|
||||
id: string;
|
||||
userId: string;
|
||||
path: string;
|
||||
}) {
|
||||
// @todo next-auth doesn't work in server actions yet
|
||||
// const session = await auth();
|
||||
|
||||
const userId = session?.user?.email;
|
||||
if (!userId || !id) {
|
||||
throw new Error("Unauthorized");
|
||||
}
|
||||
// const userId = session?.user?.email;
|
||||
// if (!userId || !id) {
|
||||
// throw new Error("Unauthorized");
|
||||
// }
|
||||
|
||||
await prisma.chat.delete({
|
||||
where: {
|
||||
id,
|
||||
// TODO: Add scoping
|
||||
// userId,
|
||||
},
|
||||
});
|
||||
await db
|
||||
.delete(chats)
|
||||
.where(and(eq(chats.id, id), eq(chats.userId, userId)))
|
||||
.execute();
|
||||
|
||||
revalidatePath("/");
|
||||
revalidatePath("/chat/[id]");
|
||||
|
|
|
|||
|
|
@ -1,18 +1,31 @@
|
|||
import { auth } from "@/auth";
|
||||
import { Message, OpenAIStream, StreamingTextResponse } from "ai-connector";
|
||||
import { openai } from "@/lib/openai";
|
||||
import { chats, db } from "@/lib/db/schema";
|
||||
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
|
||||
console.log(req.auth); // todo fix types
|
||||
|
||||
const messages = json.messages.map((m: any) => ({
|
||||
content: m.content,
|
||||
role: m.role,
|
||||
}));
|
||||
const res = await openai.createChatCompletion({
|
||||
model: "gpt-3.5-turbo",
|
||||
messages: json.messages.map((m: Message) => ({
|
||||
content: m.content,
|
||||
role: m.role,
|
||||
})),
|
||||
messages,
|
||||
temperature: 0.7,
|
||||
top_p: 1,
|
||||
frequency_penalty: 1,
|
||||
|
|
@ -21,6 +34,36 @@ export const POST = auth(async function POST(req: Request) {
|
|||
stream: true,
|
||||
});
|
||||
|
||||
const stream = OpenAIStream(res);
|
||||
const stream = await OpenAIStream(res, {
|
||||
async onCompletion(completion) {
|
||||
// @ts-ignore
|
||||
if (req.auth?.user?.email == null) {
|
||||
return;
|
||||
}
|
||||
const title = json.messages[0].content.substring(0, 20);
|
||||
const payload = {
|
||||
id: crypto.randomUUID(),
|
||||
title,
|
||||
userId: (req as any).auth?.user?.email,
|
||||
messages: [
|
||||
...messages,
|
||||
{
|
||||
content: completion,
|
||||
role: "assistant",
|
||||
},
|
||||
],
|
||||
};
|
||||
const chat = await db
|
||||
.insert(chats)
|
||||
.values(payload)
|
||||
// .onConflictDoUpdate({
|
||||
// target: payload.id,
|
||||
// set: payload,
|
||||
// })
|
||||
.returning();
|
||||
console.log(chat);
|
||||
},
|
||||
});
|
||||
|
||||
return new StreamingTextResponse(stream);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
"use client";
|
||||
|
||||
import { type Message } from "@prisma/client";
|
||||
import { type Message } from "ai-connector";
|
||||
|
||||
import { ChatMessage } from "./chat-message";
|
||||
import { NextChatLogo } from "@/components/ui/nextchat-logo";
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
23
app/chat.tsx
23
app/chat.tsx
|
|
@ -1,9 +1,9 @@
|
|||
"use client";
|
||||
|
||||
import { type Message } from "@prisma/client";
|
||||
import { useChat, type Message as AIMessage } from "ai-connector";
|
||||
import { ChatList } from "./chat-list";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Prompt } from "./prompt";
|
||||
import { useChat, type Message } from "ai-connector";
|
||||
import { ChatList } from "./chat-list";
|
||||
|
||||
export interface ChatProps {
|
||||
// create?: (input: string) => Chat | undefined;
|
||||
|
|
@ -16,24 +16,29 @@ export function Chat({
|
|||
// create,
|
||||
initialMessages,
|
||||
}: ChatProps) {
|
||||
const router = useRouter();
|
||||
|
||||
const { isLoading, messages, reload, append } = useChat({
|
||||
initialMessages: initialMessages as any[],
|
||||
id,
|
||||
initialMessages: initialMessages as AIMessage[],
|
||||
// onCreate: (id: string) => {
|
||||
// router.push(`/chat/${id}`);
|
||||
// },
|
||||
});
|
||||
|
||||
return (
|
||||
<main className="transition-width relative min-h-full w-full flex-1 overflow-y-auto flex flex-col">
|
||||
<div className="flex-1">
|
||||
<ChatList messages={messages as Message[]} />
|
||||
<ChatList messages={messages} />
|
||||
</div>
|
||||
<div className="sticky light-gradient dark:bg-gradient-to-b dark:from-zinc-900 dark:to-zinc-950 bottom-0 left-0 w-full border-t bg-white dark:bg-black md:border-t-0 py-4 md:border-transparent md:!bg-transparent md:dark:border-transparent pr-0 lg:pr-[260px] flex dark:border-transparent items-center justify-center">
|
||||
<Prompt
|
||||
onSubmit={(v) =>
|
||||
onSubmit={(value) => {
|
||||
append({
|
||||
content: value,
|
||||
role: "user",
|
||||
content: v,
|
||||
})
|
||||
}
|
||||
});
|
||||
}}
|
||||
onRefresh={messages.length ? reload : undefined}
|
||||
isLoading={isLoading}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -1,10 +1,14 @@
|
|||
import { Sidebar } from "@/app/sidebar";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
||||
import { Chat } from "../../chat";
|
||||
import { type Metadata } from "next";
|
||||
import { auth } from "@/auth";
|
||||
import { db, chats } from "@/lib/db/schema";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Chat } from "@/app/chat";
|
||||
import { Message } from "ai-connector";
|
||||
|
||||
export const runtime = "edge";
|
||||
export const preferredRegion = "home";
|
||||
export interface ChatPageProps {
|
||||
params: {
|
||||
id: string;
|
||||
|
|
@ -14,30 +18,20 @@ export interface ChatPageProps {
|
|||
export async function generateMetadata({
|
||||
params,
|
||||
}: ChatPageProps): Promise<Metadata> {
|
||||
const chat = await prisma.chat.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
},
|
||||
const chat = await db.query.chats.findFirst({
|
||||
where: eq(chats.id, params.id),
|
||||
});
|
||||
return {
|
||||
title: chat?.title.slice(0, 50) ?? "Chat",
|
||||
};
|
||||
}
|
||||
|
||||
// Prisma does not support Edge without the Data Proxy currently
|
||||
export const runtime = "nodejs"; // default
|
||||
export const preferredRegion = "home";
|
||||
export const dynamic = "force-dynamic";
|
||||
export default async function ChatPage({ params }: ChatPageProps) {
|
||||
const session = await auth();
|
||||
const chat = await prisma.chat.findFirst({
|
||||
where: {
|
||||
id: params.id,
|
||||
},
|
||||
include: {
|
||||
messages: true,
|
||||
},
|
||||
const chat = await db.query.chats.findFirst({
|
||||
where: eq(chats.id, params.id),
|
||||
});
|
||||
|
||||
if (!chat) {
|
||||
throw new Error("Chat not found");
|
||||
}
|
||||
|
|
@ -46,7 +40,7 @@ export default async function ChatPage({ params }: ChatPageProps) {
|
|||
<div className="relative flex h-full w-full overflow-hidden">
|
||||
<Sidebar session={session} />
|
||||
<div className="flex h-full min-w-0 flex-1 flex-col">
|
||||
<Chat id={chat.id} initialMessages={chat.messages} />
|
||||
<Chat id={chat.id} initialMessages={chat.messages as Message[]} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -2,10 +2,8 @@ import { Chat } from "./chat";
|
|||
import { Sidebar } from "./sidebar";
|
||||
import { auth } from "@/auth";
|
||||
|
||||
// Prisma does not support Edge without the Data Proxy currently
|
||||
export const runtime = "nodejs"; // default
|
||||
export const runtime = "edge";
|
||||
export const preferredRegion = "home";
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export default async function IndexPage() {
|
||||
const session = await auth();
|
||||
|
|
|
|||
|
|
@ -11,10 +11,12 @@ export function SidebarItem({
|
|||
title,
|
||||
href,
|
||||
id,
|
||||
userId,
|
||||
}: {
|
||||
title: string;
|
||||
href: string;
|
||||
id: string;
|
||||
userId: string;
|
||||
}) {
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
|
|
@ -49,7 +51,7 @@ export function SidebarItem({
|
|||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
startTransition(() => {
|
||||
removeChat({ id, path: href }).then(() => {
|
||||
removeChat({ id, userId, path: href }).then(() => {
|
||||
router.push("/");
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -1,12 +1,13 @@
|
|||
import { NextChatLogo } from "@/components/ui/nextchat-logo";
|
||||
import { Login } from "@/components/ui/login";
|
||||
import { NextChatLogo } from "@/components/ui/nextchat-logo";
|
||||
import { UserMenu } from "@/components/ui/user-menu";
|
||||
import { prisma } from "@/lib/prisma";
|
||||
import { type Session } from "@auth/nextjs/types";
|
||||
import { db, chats } from "@/lib/db/schema";
|
||||
import { cn } from "@/lib/utils";
|
||||
import { type Session } from "@auth/nextjs/types";
|
||||
import { eq } from "drizzle-orm";
|
||||
import { Plus } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import { unstable_cache } from "next/cache";
|
||||
import Link from "next/link";
|
||||
import { SidebarItem } from "./sidebar-item";
|
||||
import { ExternalLink } from "./external-link";
|
||||
|
||||
|
|
@ -86,27 +87,31 @@ export function Sidebar({ session, newChat }: SidebarProps) {
|
|||
Sidebar.displayName = "Sidebar";
|
||||
|
||||
async function SidebarList({ session }: { session?: Session }) {
|
||||
const chats = await (
|
||||
const results: any[] = await (
|
||||
await unstable_cache(
|
||||
() =>
|
||||
prisma.chat.findMany({
|
||||
where: {
|
||||
// This is for debugging, need to add scope to the query later
|
||||
// userId: session?.user.id,
|
||||
},
|
||||
orderBy: {
|
||||
updatedAt: "desc",
|
||||
db.query.chats.findMany({
|
||||
columns: {
|
||||
id: true,
|
||||
title: true,
|
||||
},
|
||||
where: eq(chats.userId, session?.user?.email || ""),
|
||||
}),
|
||||
// @ts-ignore
|
||||
[session?.user?.id || ""],
|
||||
[session?.user?.email ?? ""],
|
||||
{
|
||||
revalidate: 3600,
|
||||
}
|
||||
)
|
||||
)();
|
||||
|
||||
return chats.map((c) => (
|
||||
<SidebarItem key={c.id} title={c.title} href={`/chat/${c.id}`} id={c.id} />
|
||||
return results.map((c) => (
|
||||
<SidebarItem
|
||||
key={c.id}
|
||||
title={c.title}
|
||||
userId={session?.user?.email ?? ""}
|
||||
href={`/chat/${c.id}`}
|
||||
id={c.id}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,130 +0,0 @@
|
|||
import { useState, useCallback, useRef, useEffect } from "react";
|
||||
import { type Message } from "@prisma/client";
|
||||
import { nanoid } from "@/lib/utils";
|
||||
|
||||
export function usePrompt({
|
||||
messages = [],
|
||||
id,
|
||||
}: {
|
||||
messages?: Message[];
|
||||
id: string | undefined;
|
||||
}) {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [messageList, setMessageList] = useState(messages);
|
||||
|
||||
const isLoadingRef = useRef(isLoading);
|
||||
const messageListRef = useRef(messageList);
|
||||
|
||||
useEffect(() => {
|
||||
isLoadingRef.current = isLoading;
|
||||
}, [isLoading]);
|
||||
|
||||
useEffect(() => {
|
||||
messageListRef.current = messageList;
|
||||
}, [messageList]);
|
||||
|
||||
const appendUserMessage = useCallback(
|
||||
async (content: string | Message) => {
|
||||
// Prevent multiple requests at once
|
||||
if (isLoadingRef.current) return;
|
||||
|
||||
const userMsg =
|
||||
typeof content === "string"
|
||||
? ({ id: nanoid(10), role: "user", content } as Message)
|
||||
: content;
|
||||
const assMsg = {
|
||||
id: nanoid(10),
|
||||
role: "assistant",
|
||||
content: "",
|
||||
} as Message;
|
||||
const messageListSnapshot = messageListRef.current;
|
||||
|
||||
// Reset output
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// Set user input immediately
|
||||
setMessageList([...messageListSnapshot, userMsg]);
|
||||
|
||||
// If streaming, we need to use fetchEventSource directly
|
||||
const response = await fetch(`/api/generate`, {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
id: id || nanoid(10),
|
||||
messages: [...messageListSnapshot, userMsg].map((m) => ({
|
||||
role: m.role,
|
||||
content: m.content,
|
||||
})),
|
||||
}),
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
// This data is a ReadableStream
|
||||
const data = response.body;
|
||||
if (!data) {
|
||||
return;
|
||||
}
|
||||
|
||||
const reader = data.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let done = false;
|
||||
let accumulatedValue = ""; // Variable to accumulate chunks
|
||||
|
||||
while (!done) {
|
||||
const { value, done: doneReading } = await reader.read();
|
||||
done = doneReading;
|
||||
const chunkValue = decoder.decode(value);
|
||||
accumulatedValue += chunkValue; // Accumulate the chunk value
|
||||
|
||||
// Check if the accumulated value contains the delimiter
|
||||
const delimiter = "\n";
|
||||
const chunks = accumulatedValue.split(delimiter);
|
||||
|
||||
// Process all chunks except the last one (which may be incomplete)
|
||||
while (chunks.length > 1) {
|
||||
const chunkToDispatch = chunks.shift(); // Get the first chunk
|
||||
if (chunkToDispatch && chunkToDispatch.length > 0) {
|
||||
const chunk = JSON.parse(chunkToDispatch);
|
||||
assMsg.content += chunk;
|
||||
setMessageList([...messageListSnapshot, userMsg, assMsg]);
|
||||
}
|
||||
}
|
||||
|
||||
// The last chunk may be incomplete, so keep it in the accumulated value
|
||||
accumulatedValue = chunks[0];
|
||||
}
|
||||
|
||||
// Process any remaining accumulated value after the loop is done
|
||||
if (accumulatedValue.length > 0) {
|
||||
assMsg.content += accumulatedValue;
|
||||
setMessageList([...messageListSnapshot, userMsg, assMsg]);
|
||||
}
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
},
|
||||
[id]
|
||||
);
|
||||
|
||||
const reloadLastMessage = useCallback(async () => {
|
||||
// Prevent multiple requests at once
|
||||
if (isLoadingRef.current) return;
|
||||
|
||||
const userMsg = messageListRef.current.at(-2);
|
||||
const assMsg = messageListRef.current.at(-1);
|
||||
|
||||
// Both should exist.
|
||||
if (!userMsg || !assMsg) return;
|
||||
|
||||
messageListRef.current = messageListRef.current.slice(0, -2);
|
||||
setMessageList(messageListRef.current);
|
||||
|
||||
await appendUserMessage(userMsg);
|
||||
}, [appendUserMessage]);
|
||||
|
||||
return {
|
||||
messageList,
|
||||
appendUserMessage,
|
||||
reloadLastMessage,
|
||||
isLoading,
|
||||
};
|
||||
}
|
||||
1
auth.ts
1
auth.ts
|
|
@ -5,6 +5,7 @@ import { NextResponse } from "next/server";
|
|||
export const {
|
||||
handlers: { GET, POST },
|
||||
auth,
|
||||
CSRF_experimental,
|
||||
} = NextAuth({
|
||||
// @ts-ignore
|
||||
providers: [GitHub],
|
||||
|
|
|
|||
|
|
@ -1,11 +1,13 @@
|
|||
"use client";
|
||||
import { signIn } from "@auth/nextjs/client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export function Login() {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<button
|
||||
className="inline-flex w-full items-center justify-center rounded border border-zinc-800 bg-white h-8 px-4 -my-1.5 text-sm leading-6 tracking-tight text-zinc-900 transition-colors ease-in-out hover:bg-zinc-100 disabled:cursor-not-allowed disabled:opacity-50"
|
||||
onClick={() => signIn("github")}
|
||||
onClick={() => router.push("/api/auth/signin")}
|
||||
>
|
||||
<span className="font-medium">Login</span>
|
||||
</button>
|
||||
|
|
|
|||
|
|
@ -2,15 +2,16 @@
|
|||
import { ThemeToggle } from "@/components/theme-toggle";
|
||||
import { type Session } from "@auth/nextjs/types";
|
||||
import * as DropdownMenu from "@radix-ui/react-dropdown-menu";
|
||||
import { signOut } from "@auth/nextjs/client";
|
||||
|
||||
import Image from "next/image";
|
||||
import { useRouter } from "next/navigation";
|
||||
|
||||
export interface UserMenuProps {
|
||||
session: Session;
|
||||
}
|
||||
|
||||
export function UserMenu({ session }: UserMenuProps) {
|
||||
const router = useRouter();
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<DropdownMenu.Root>
|
||||
|
|
@ -83,7 +84,7 @@ export function UserMenu({ session }: UserMenuProps) {
|
|||
</DropdownMenu.Item>
|
||||
<DropdownMenu.Item
|
||||
className="py-2 px-3 hover:bg-zinc-200 dark:hover:bg-zinc-800 transition-colors duration-200 cursor-pointer text-xs focus:outline-none"
|
||||
onClick={() => signOut()}
|
||||
onClick={() => router.push("/api/auth/signout")}
|
||||
>
|
||||
Log Out
|
||||
</DropdownMenu.Item>
|
||||
|
|
|
|||
6
drizzle.config.ts
Normal file
6
drizzle.config.ts
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
import type { Config } from "drizzle-kit";
|
||||
|
||||
export default {
|
||||
schema: "./lib/db/schema.ts",
|
||||
out: "./lib/db/migrations",
|
||||
} satisfies Config;
|
||||
19
lib/db/migrate.ts
Normal file
19
lib/db/migrate.ts
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
import { migrate } from "drizzle-orm/vercel-postgres/migrator";
|
||||
import { db } from "./schema";
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
dotenv.config();
|
||||
|
||||
// @ts-ignore
|
||||
migrate(db, {
|
||||
migrationsFolder: "lib/db/migrations",
|
||||
})
|
||||
.then(() => {
|
||||
console.log("Migration was succesfull!");
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("Migration failed:", err);
|
||||
})
|
||||
.finally(() => {
|
||||
console.info("Migration finished");
|
||||
});
|
||||
6
lib/db/migrations/0000_fair_groot.sql
Normal file
6
lib/db/migrations/0000_fair_groot.sql
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
CREATE TABLE IF NOT EXISTS "chats" (
|
||||
"id" text PRIMARY KEY NOT NULL,
|
||||
"role" text NOT NULL,
|
||||
"userId" text NOT NULL,
|
||||
"messages" json DEFAULT '{}'::json NOT NULL
|
||||
);
|
||||
1
lib/db/migrations/0001_naive_gunslinger.sql
Normal file
1
lib/db/migrations/0001_naive_gunslinger.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "chats" DROP COLUMN IF EXISTS "messages";
|
||||
1
lib/db/migrations/0002_striped_purifiers.sql
Normal file
1
lib/db/migrations/0002_striped_purifiers.sql
Normal file
|
|
@ -0,0 +1 @@
|
|||
ALTER TABLE "chats" ADD COLUMN "messages" json DEFAULT '{}'::json NOT NULL;
|
||||
49
lib/db/migrations/meta/0000_snapshot.json
Normal file
49
lib/db/migrations/meta/0000_snapshot.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
"id": "3bddf21a-46a3-4ee8-8b41-bb79cb80d904",
|
||||
"prevId": "00000000-0000-0000-0000-000000000000",
|
||||
"tables": {
|
||||
"chats": {
|
||||
"name": "chats",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"messages": {
|
||||
"name": "messages",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::json"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
}
|
||||
}
|
||||
42
lib/db/migrations/meta/0001_snapshot.json
Normal file
42
lib/db/migrations/meta/0001_snapshot.json
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
"id": "e8eef642-fc25-4737-8e75-5ad1e8c53b84",
|
||||
"prevId": "3bddf21a-46a3-4ee8-8b41-bb79cb80d904",
|
||||
"tables": {
|
||||
"chats": {
|
||||
"name": "chats",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
}
|
||||
}
|
||||
49
lib/db/migrations/meta/0002_snapshot.json
Normal file
49
lib/db/migrations/meta/0002_snapshot.json
Normal file
|
|
@ -0,0 +1,49 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
"id": "e3f4035a-a940-4529-b037-1737e178d88b",
|
||||
"prevId": "e8eef642-fc25-4737-8e75-5ad1e8c53b84",
|
||||
"tables": {
|
||||
"chats": {
|
||||
"name": "chats",
|
||||
"schema": "",
|
||||
"columns": {
|
||||
"id": {
|
||||
"name": "id",
|
||||
"type": "text",
|
||||
"primaryKey": true,
|
||||
"notNull": true
|
||||
},
|
||||
"role": {
|
||||
"name": "role",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"userId": {
|
||||
"name": "userId",
|
||||
"type": "text",
|
||||
"primaryKey": false,
|
||||
"notNull": true
|
||||
},
|
||||
"messages": {
|
||||
"name": "messages",
|
||||
"type": "json",
|
||||
"primaryKey": false,
|
||||
"notNull": true,
|
||||
"default": "'{}'::json"
|
||||
}
|
||||
},
|
||||
"indexes": {},
|
||||
"foreignKeys": {},
|
||||
"compositePrimaryKeys": {}
|
||||
}
|
||||
},
|
||||
"enums": {},
|
||||
"schemas": {},
|
||||
"_meta": {
|
||||
"schemas": {},
|
||||
"tables": {},
|
||||
"columns": {}
|
||||
}
|
||||
}
|
||||
27
lib/db/migrations/meta/_journal.json
Normal file
27
lib/db/migrations/meta/_journal.json
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{
|
||||
"version": "5",
|
||||
"dialect": "pg",
|
||||
"entries": [
|
||||
{
|
||||
"idx": 0,
|
||||
"version": "5",
|
||||
"when": 1685719783900,
|
||||
"tag": "0000_fair_groot",
|
||||
"breakpoints": false
|
||||
},
|
||||
{
|
||||
"idx": 1,
|
||||
"version": "5",
|
||||
"when": 1685719963202,
|
||||
"tag": "0001_naive_gunslinger",
|
||||
"breakpoints": false
|
||||
},
|
||||
{
|
||||
"idx": 2,
|
||||
"version": "5",
|
||||
"when": 1685720130302,
|
||||
"tag": "0002_striped_purifiers",
|
||||
"breakpoints": false
|
||||
}
|
||||
]
|
||||
}
|
||||
56
lib/db/schema.ts
Normal file
56
lib/db/schema.ts
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
import { sql } from "@vercel/postgres";
|
||||
import { json, pgTable, text } from "drizzle-orm/pg-core";
|
||||
import { drizzle } from "drizzle-orm/vercel-postgres";
|
||||
|
||||
// const connection = createPool({ connectionString: process.env.POSTGRES_URL });
|
||||
|
||||
export const chats = pgTable("chats", {
|
||||
id: text("id").notNull().primaryKey(),
|
||||
title: text("role").notNull(),
|
||||
userId: text("userId").notNull(),
|
||||
messages: json("messages").notNull().default({}),
|
||||
// .references(() => users.id),
|
||||
});
|
||||
|
||||
// export const messages = pgTable("messages", {
|
||||
// id: text("id").notNull().primaryKey(),
|
||||
// title: text("role").notNull(),
|
||||
// chatId: text("chatId")
|
||||
// .notNull()
|
||||
// .references(() => chats.id),
|
||||
// });
|
||||
|
||||
// export const chatsRelations = relations(chats, ({ many }) => ({
|
||||
// // user: one(users, {
|
||||
// // fields: [chats.userId],
|
||||
// // references: [users.id],
|
||||
// // }),
|
||||
// messages: many(messages),
|
||||
// }));
|
||||
|
||||
// export const messagesRelations = relations(messages, ({ one }) => ({
|
||||
// chat: one(chats, {
|
||||
// fields: [messages.chatId],
|
||||
// references: [chats.id],
|
||||
// }),
|
||||
// }));
|
||||
|
||||
export const db = drizzle(sql, {
|
||||
schema: {
|
||||
// users,
|
||||
// accounts,
|
||||
// sessions,
|
||||
// verificationTokens,
|
||||
chats,
|
||||
// chatsRelations,
|
||||
// messages,
|
||||
// messagesRelations,
|
||||
// usersRelations,
|
||||
},
|
||||
});
|
||||
|
||||
export type DbClient = typeof db;
|
||||
|
||||
export type Schema = {
|
||||
chats: typeof chats;
|
||||
};
|
||||
|
|
@ -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;
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import type { RefObject } from "react";
|
||||
import { useRef } from "react";
|
||||
import { isModifierPressed } from "./is-modifier-pressed";
|
||||
|
||||
export function useCmdEnterSubmit(): {
|
||||
formRef: RefObject<HTMLFormElement>;
|
||||
|
|
@ -11,8 +10,7 @@ export function useCmdEnterSubmit(): {
|
|||
const handleKeyDown = (
|
||||
event: React.KeyboardEvent<HTMLTextAreaElement>
|
||||
): void => {
|
||||
// Capture `⌘`|Ctrl + Enter
|
||||
if (isModifierPressed(event) && event.key === "Enter") {
|
||||
if (event.key === "Enter") {
|
||||
formRef.current?.requestSubmit();
|
||||
}
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,43 +0,0 @@
|
|||
import { RefObject, useEffect, useState } from "react";
|
||||
|
||||
interface Args extends IntersectionObserverInit {
|
||||
freezeOnceVisible?: boolean;
|
||||
}
|
||||
|
||||
function useIntersectionObserver(
|
||||
elementRef: RefObject<Element>,
|
||||
{
|
||||
threshold = 0,
|
||||
root = null,
|
||||
rootMargin = "0%",
|
||||
freezeOnceVisible = false,
|
||||
}: Args
|
||||
): IntersectionObserverEntry | undefined {
|
||||
const [entry, setEntry] = useState<IntersectionObserverEntry>();
|
||||
|
||||
const frozen = entry?.isIntersecting && freezeOnceVisible;
|
||||
|
||||
const updateEntry = ([entry]: IntersectionObserverEntry[]): void => {
|
||||
setEntry(entry);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const node = elementRef?.current; // DOM Ref
|
||||
const hasIOSupport = !!window.IntersectionObserver;
|
||||
|
||||
if (!hasIOSupport || frozen || !node) return;
|
||||
|
||||
const observerParams = { threshold, root, rootMargin };
|
||||
const observer = new IntersectionObserver(updateEntry, observerParams);
|
||||
|
||||
observer.observe(node);
|
||||
|
||||
return () => observer.disconnect();
|
||||
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [threshold, root, rootMargin, frozen]);
|
||||
|
||||
return entry;
|
||||
}
|
||||
|
||||
export default useIntersectionObserver;
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -1,11 +0,0 @@
|
|||
import { PrismaClient } from "@prisma/client";
|
||||
|
||||
declare global {
|
||||
var prisma: PrismaClient | undefined;
|
||||
}
|
||||
|
||||
const prisma = global.prisma || new PrismaClient();
|
||||
|
||||
if (process.env.NODE_ENV === "development") global.prisma = prisma;
|
||||
|
||||
export { prisma };
|
||||
209
lib/tinykeys.tsx
209
lib/tinykeys.tsx
|
|
@ -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 = `<sequence>`
|
||||
* <sequence> = `<press> <press> <press> ...`
|
||||
* <press> = `<key>` or `<mods>+<key>`
|
||||
* <mods> = `<mod>+<mod>+...`
|
||||
*/
|
||||
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<KeyBindingPress[], KeyBindingPress[]>();
|
||||
|
||||
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);
|
||||
};
|
||||
}
|
||||
22
package.json
22
package.json
|
|
@ -3,9 +3,12 @@
|
|||
"version": "0.0.2",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"dev": "prisma generate && next dev",
|
||||
"build": "prisma generate && next build",
|
||||
"studio": "prisma studio",
|
||||
"dev": "next dev",
|
||||
"build": "next build",
|
||||
"db:mg:create": "drizzle-kit generate:pg --config=drizzle.config.ts",
|
||||
"db:mg:apply": "ts-node --project tsconfig.migration.json ./lib/db/migrate --config=drizzle.config.ts",
|
||||
"db:check": "drizzle-kit check:pg --config=drizzle.config.ts",
|
||||
"db:up": "drizzle-kit up:pg --config=drizzle.config.ts",
|
||||
"start": "next start",
|
||||
"lint": "next lint",
|
||||
"lint:fix": "next lint --fix",
|
||||
|
|
@ -15,19 +18,19 @@
|
|||
"format:check": "prettier --check \"**/*.{ts,tsx,mdx}\" --cache"
|
||||
},
|
||||
"dependencies": {
|
||||
"@auth/core": "0.0.0-manual.527fff6c",
|
||||
"@auth/nextjs": "0.0.0-manual.030e8328",
|
||||
"@next-auth/prisma-adapter": "1.0.6",
|
||||
"@prisma/client": "^4.15.0",
|
||||
"@radix-ui/react-dropdown-menu": "^2.0.4",
|
||||
"@vercel/analytics": "^1.0.0",
|
||||
"@vercel/kv": "^0.2.1",
|
||||
"@vercel/postgres": "^0.3.0",
|
||||
"ai-connector": "^0.0.5",
|
||||
"body-scroll-lock": "4.0.0-beta.0",
|
||||
"class-variance-authority": "^0.4.0",
|
||||
"clsx": "^1.2.1",
|
||||
"common-tags": "^1.8.2",
|
||||
"cookie": "^0.5.0",
|
||||
"eventsource-parser": "^1.0.0",
|
||||
"drizzle-orm": "^0.26.0",
|
||||
"focus-trap-react": "^10.1.1",
|
||||
"framer-motion": "^10.12.4",
|
||||
"jose": "^4.14.4",
|
||||
|
|
@ -46,7 +49,8 @@
|
|||
"remark-gfm": "^3.0.1",
|
||||
"remark-math": "^5.1.1",
|
||||
"tailwind-merge": "^1.12.0",
|
||||
"tailwindcss-animate": "^1.0.5"
|
||||
"tailwindcss-animate": "^1.0.5",
|
||||
"uuid": "^9.0.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ianvs/prettier-plugin-sort-imports": "^3.7.1",
|
||||
|
|
@ -61,6 +65,8 @@
|
|||
"@types/react-syntax-highlighter": "^15.5.6",
|
||||
"@typescript-eslint/parser": "^5.58.0",
|
||||
"autoprefixer": "^10.4.13",
|
||||
"dotenv": "^16.0.3",
|
||||
"drizzle-kit": "^0.18.0",
|
||||
"eslint": "^8.31.0",
|
||||
"eslint-config-next": "13.4.5-canary.3",
|
||||
"eslint-config-prettier": "^8.3.0",
|
||||
|
|
@ -68,8 +74,8 @@
|
|||
"eslint-plugin-tailwindcss": "^3.8.0",
|
||||
"postcss": "^8.4.21",
|
||||
"prettier": "^2.7.1",
|
||||
"prisma": "^4.15.0",
|
||||
"tailwindcss": "^3.3.1",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.9.3"
|
||||
},
|
||||
"pnpm": {
|
||||
|
|
|
|||
853
pnpm-lock.yaml
generated
853
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
|
|
@ -1,78 +0,0 @@
|
|||
datasource db {
|
||||
provider = "postgresql"
|
||||
url = env("POSTGRES_PRISMA_URL") // uses connection pooling
|
||||
directUrl = env("POSTGRES_URL_NON_POOLING") // uses a direct connection
|
||||
shadowDatabaseUrl = env("POSTGRES_URL_NON_POOLING") // used for migrations
|
||||
}
|
||||
|
||||
generator client {
|
||||
provider = "prisma-client-js"
|
||||
previewFeatures = ["jsonProtocol"]
|
||||
}
|
||||
|
||||
model Chat {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
title String
|
||||
messages Message[]
|
||||
userId String?
|
||||
}
|
||||
|
||||
model Message {
|
||||
id String @id @default(cuid())
|
||||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
content String
|
||||
role String
|
||||
chat Chat @relation(fields: [chatId], references: [id], onDelete: Cascade)
|
||||
chatId String
|
||||
}
|
||||
|
||||
model Account {
|
||||
id String @id @default(cuid())
|
||||
userId String
|
||||
type String
|
||||
provider String
|
||||
providerAccountId String
|
||||
refresh_token String? @db.Text
|
||||
access_token String? @db.Text
|
||||
expires_at Int?
|
||||
token_type String?
|
||||
scope String?
|
||||
id_token String? @db.Text
|
||||
session_state String?
|
||||
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@unique([provider, providerAccountId])
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model Session {
|
||||
id String @id @default(cuid())
|
||||
sessionToken String @unique
|
||||
userId String
|
||||
expires DateTime
|
||||
user User @relation(fields: [userId], references: [id], onDelete: Cascade)
|
||||
|
||||
@@index([userId])
|
||||
}
|
||||
|
||||
model User {
|
||||
id String @id @default(cuid())
|
||||
name String?
|
||||
email String? @unique
|
||||
emailVerified DateTime?
|
||||
image String?
|
||||
accounts Account[]
|
||||
sessions Session[]
|
||||
}
|
||||
|
||||
model VerificationToken {
|
||||
identifier String
|
||||
token String @unique
|
||||
expires DateTime
|
||||
|
||||
@@unique([identifier, token])
|
||||
}
|
||||
11
tsconfig.migration.json
Normal file
11
tsconfig.migration.json
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
{
|
||||
"compilerOptions": {
|
||||
"module": "CommonJS",
|
||||
"moduleResolution": "Node",
|
||||
"target": "ES2020",
|
||||
"jsx": "react",
|
||||
"strictNullChecks": true,
|
||||
"strictFunctionTypes": true
|
||||
},
|
||||
"exclude": ["node_modules", "**/node_modules/*"]
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue