diff --git a/README.md b/README.md
index 6447bd3..6357e36 100644
--- a/README.md
+++ b/README.md
@@ -31,6 +31,39 @@ pnpm build
pnpm dev
```
+Then, you can use it in your code like this:
+
+```jsx
+import { Editor } from "inke";
+
+export default function App() {
+ return ;
+}
+```
+
+The `Editor` is a React component that takes in the following props:
+
+| Prop | Type | Description | Default |
+| --------------------- | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------- |
+| `completionApi` | `string` | The API route to use for the OpenAI completion API. | `/api/generate` |
+| `className` | `string` | Editor container classname. | `"relative min-h-[500px] w-full max-w-screen-lg border-stone-200 bg-white sm:mb-[calc(20vh)] sm:rounded-lg sm:border sm:shadow-lg"` |
+| `defaultValue` | `JSONContent` or `string` | The default value to use for the editor. | [`defaultEditorContent`](https://github.com/steven-tey/novel/blob/main/packages/core/src/ui/editor/default-content.tsx) |
+| `extensions` | `Extension[]` | A list of extensions to use for the editor, in addition to the [default Novel extensions](https://github.com/steven-tey/novel/blob/main/packages/core/src/ui/editor/extensions/index.tsx). | `[]` |
+| `editorProps` | `EditorProps` | Props to pass to the underlying Tiptap editor, in addition to the [default Novel editor props](https://github.com/steven-tey/novel/blob/main/packages/core/src/ui/editor/props.ts). | `{}` |
+| `onUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated. | `() => {}` |
+| `onDebouncedUpdate` | `(editor?: Editor) => void` | A callback function that is called whenever the editor is updated, but only after the defined debounce duration. | `() => {}` |
+| `debounceDuration` | `number` | The duration (in milliseconds) to debounce the `onDebouncedUpdate` callback. | `750` |
+| `storageKey` | `string` | The key to use for storing the editor's value in local storage. | `novel__content` |
+| `disableLocalStorage` | `boolean` | Enabling this option will prevent read/write content from/to local storage. | `false` |
+
+> **Note**: Make sure to define an API endpoint that matches the `completionApi` prop (default is `/api/generate`). This is needed for the AI autocompletions to work. Here's an example: https://github.com/yesmore/inke/blob/main/apps/web/app/api/generate/route.ts
+
+## Deploy Your Own
+
+You can deploy your own version of Novel to Vercel with one click:
+
+[![Deploy with Vercel](https://vercel.com/button)]()
+
## Tech Stack
Inke is built on the following stack:
diff --git a/apps/web/.env.example b/apps/web/.env.example
new file mode 100644
index 0000000..a0a4414
--- /dev/null
+++ b/apps/web/.env.example
@@ -0,0 +1,15 @@
+# This file will be committed to version control, so make sure not to have any
+# secrets in it. If you are cloning this repo, create a copy of this file named
+# ".env" and populate it with your secrets.
+
+# Get your OpenAI API key here: https://platform.openai.com/account/api-keys
+OPENAI_API_KEY=
+
+# OPTIONAL: Vercel Blob (for uploading images)
+# Get your Vercel Blob credentials here: https://vercel.com/docs/storage/vercel-blob/quickstart#quickstart
+BLOB_READ_WRITE_TOKEN=
+
+# OPTIONAL: Vercel KV (for ratelimiting)
+# Get your Vercel KV credentials here: https://vercel.com/docs/storage/vercel-kv/quickstart#quickstart
+KV_REST_API_URL=
+KV_REST_API_TOKEN=
diff --git a/apps/web/.gitignore b/apps/web/.gitignore
new file mode 100644
index 0000000..e985853
--- /dev/null
+++ b/apps/web/.gitignore
@@ -0,0 +1 @@
+.vercel
diff --git a/apps/web/.prettierignore b/apps/web/.prettierignore
new file mode 100644
index 0000000..d490ed0
--- /dev/null
+++ b/apps/web/.prettierignore
@@ -0,0 +1,4 @@
+pnpm-lock.yaml
+yarn.lock
+node_modules
+.next
\ No newline at end of file
diff --git a/apps/web/app/api/generate/route.ts b/apps/web/app/api/generate/route.ts
new file mode 100644
index 0000000..400ca3b
--- /dev/null
+++ b/apps/web/app/api/generate/route.ts
@@ -0,0 +1,81 @@
+import OpenAI from "openai";
+import { OpenAIStream, StreamingTextResponse } from "ai";
+import { kv } from "@vercel/kv";
+import { Ratelimit } from "@upstash/ratelimit";
+
+// Create an OpenAI API client (that's edge friendly!)
+const openai = new OpenAI({
+ apiKey: process.env.OPENAI_API_KEY || "",
+});
+
+// IMPORTANT! Set the runtime to edge: https://vercel.com/docs/functions/edge-functions/edge-runtime
+export const runtime = "edge";
+
+export async function POST(req: Request): Promise {
+ // Check if the OPENAI_API_KEY is set, if not return 400
+ if (!process.env.OPENAI_API_KEY || process.env.OPENAI_API_KEY === "") {
+ return new Response(
+ "Missing OPENAI_API_KEY – make sure to add it to your .env file.",
+ {
+ status: 400,
+ },
+ );
+ }
+ if (
+ process.env.NODE_ENV != "development" &&
+ process.env.KV_REST_API_URL &&
+ process.env.KV_REST_API_TOKEN
+ ) {
+ const ip = req.headers.get("x-forwarded-for");
+ const ratelimit = new Ratelimit({
+ redis: kv,
+ limiter: Ratelimit.slidingWindow(50, "1 d"),
+ });
+
+ const { success, limit, reset, remaining } = await ratelimit.limit(
+ `novel_ratelimit_${ip}`,
+ );
+
+ if (!success) {
+ return new Response("You have reached your request limit for the day.", {
+ status: 429,
+ headers: {
+ "X-RateLimit-Limit": limit.toString(),
+ "X-RateLimit-Remaining": remaining.toString(),
+ "X-RateLimit-Reset": reset.toString(),
+ },
+ });
+ }
+ }
+
+ let { prompt } = await req.json();
+
+ const response = await openai.chat.completions.create({
+ model: "gpt-3.5-turbo",
+ messages: [
+ {
+ role: "system",
+ content:
+ "You are an AI writing assistant that continues existing text based on context from prior text. " +
+ "Give more weight/priority to the later characters than the beginning ones. " +
+ "Limit your response to no more than 200 characters, but make sure to construct complete sentences.",
+ },
+ {
+ role: "user",
+ content: prompt,
+ },
+ ],
+ temperature: 0.7,
+ top_p: 1,
+ frequency_penalty: 0,
+ presence_penalty: 0,
+ stream: true,
+ n: 1,
+ });
+
+ // Convert the response into a friendly text-stream
+ const stream = OpenAIStream(response);
+
+ // Respond with the stream
+ return new StreamingTextResponse(stream);
+}
diff --git a/apps/web/app/api/upload/route.ts b/apps/web/app/api/upload/route.ts
new file mode 100644
index 0000000..4926b53
--- /dev/null
+++ b/apps/web/app/api/upload/route.ts
@@ -0,0 +1,31 @@
+import { put } from "@vercel/blob";
+import { NextResponse } from "next/server";
+
+export const runtime = "edge";
+
+export async function POST(req: Request) {
+ if (!process.env.BLOB_READ_WRITE_TOKEN) {
+ return new Response(
+ "Missing BLOB_READ_WRITE_TOKEN. Don't forget to add that to your .env file.",
+ {
+ status: 401,
+ },
+ );
+ }
+
+ const file = req.body || "";
+ const filename = req.headers.get("x-vercel-filename") || "file.txt";
+ const contentType = req.headers.get("content-type") || "text/plain";
+ const fileType = `.${contentType.split("/")[1]}`;
+
+ // construct final filename based on content-type if not provided
+ const finalName = filename.includes(fileType)
+ ? filename
+ : `${filename}${fileType}`;
+ const blob = await put(finalName, file, {
+ contentType,
+ access: "public",
+ });
+
+ return NextResponse.json(blob);
+}
diff --git a/apps/web/app/favicon.ico b/apps/web/app/favicon.ico
new file mode 100644
index 0000000..46c10cf
Binary files /dev/null and b/apps/web/app/favicon.ico differ
diff --git a/apps/web/app/layout.tsx b/apps/web/app/layout.tsx
new file mode 100644
index 0000000..f7aae09
--- /dev/null
+++ b/apps/web/app/layout.tsx
@@ -0,0 +1,37 @@
+import "@/styles/globals.css";
+
+import { Metadata } from "next";
+import { ReactNode } from "react";
+import Providers from "./providers";
+
+const title =
+ "Inke – Notion-style WYSIWYG editor with AI-powered autocompletions";
+const description =
+ "Inke is a Notion-style WYSIWYG editor with AI-powered autocompletions. Built with Tiptap, OpenAI, and Vercel AI SDK.";
+
+export const metadata: Metadata = {
+ title,
+ description,
+ openGraph: {
+ title,
+ description,
+ },
+ twitter: {
+ title,
+ description,
+ card: "summary_large_image",
+ creator: "@steventey",
+ },
+ metadataBase: new URL("https://inke.app"),
+ themeColor: "#ffffff",
+};
+
+export default function RootLayout({ children }: { children: ReactNode }) {
+ return (
+
+
+ {children}
+
+
+ );
+}
diff --git a/apps/web/app/page.tsx b/apps/web/app/page.tsx
new file mode 100644
index 0000000..54c2578
--- /dev/null
+++ b/apps/web/app/page.tsx
@@ -0,0 +1,19 @@
+import { Github } from "@/ui/icons";
+import Menu from "@/ui/menu";
+import Editor from "@/ui/editor";
+
+export default function Page() {
+ return (
+