add web demo
This commit is contained in:
81
apps/web/app/api/generate/route.ts
Normal file
81
apps/web/app/api/generate/route.ts
Normal file
@@ -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<Response> {
|
||||
// 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);
|
||||
}
|
31
apps/web/app/api/upload/route.ts
Normal file
31
apps/web/app/api/upload/route.ts
Normal file
@@ -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);
|
||||
}
|
BIN
apps/web/app/favicon.ico
Normal file
BIN
apps/web/app/favicon.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 62 KiB |
37
apps/web/app/layout.tsx
Normal file
37
apps/web/app/layout.tsx
Normal file
@@ -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 (
|
||||
<html lang="en" suppressHydrationWarning>
|
||||
<body>
|
||||
<Providers>{children}</Providers>
|
||||
</body>
|
||||
</html>
|
||||
);
|
||||
}
|
19
apps/web/app/page.tsx
Normal file
19
apps/web/app/page.tsx
Normal file
@@ -0,0 +1,19 @@
|
||||
import { Github } from "@/ui/icons";
|
||||
import Menu from "@/ui/menu";
|
||||
import Editor from "@/ui/editor";
|
||||
|
||||
export default function Page() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col items-center sm:px-5 sm:pt-[calc(20vh)]">
|
||||
<a
|
||||
href="https://github.com/yesmore/inke"
|
||||
target="_blank"
|
||||
className="absolute bottom-5 left-5 z-10 max-h-fit rounded-lg p-2 transition-colors duration-200 hover:bg-stone-100 sm:bottom-auto sm:top-5"
|
||||
>
|
||||
<Github />
|
||||
</a>
|
||||
<Menu />
|
||||
<Editor />
|
||||
</div>
|
||||
);
|
||||
}
|
47
apps/web/app/providers.tsx
Normal file
47
apps/web/app/providers.tsx
Normal file
@@ -0,0 +1,47 @@
|
||||
"use client";
|
||||
|
||||
import { Dispatch, ReactNode, SetStateAction, createContext } from "react";
|
||||
import { ThemeProvider, useTheme } from "next-themes";
|
||||
import { Toaster } from "sonner";
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import useLocalStorage from "@/lib/hooks/use-local-storage";
|
||||
|
||||
export const AppContext = createContext<{
|
||||
font: string;
|
||||
setFont: Dispatch<SetStateAction<string>>;
|
||||
}>({
|
||||
font: "Default",
|
||||
setFont: () => {},
|
||||
});
|
||||
|
||||
const ToasterProvider = () => {
|
||||
const { theme } = useTheme() as {
|
||||
theme: "light" | "dark" | "system";
|
||||
};
|
||||
return <Toaster theme={theme} />;
|
||||
};
|
||||
|
||||
export default function Providers({ children }: { children: ReactNode }) {
|
||||
const [font, setFont] = useLocalStorage<string>("novel__font", "Default");
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
attribute="class"
|
||||
value={{
|
||||
light: "light-theme",
|
||||
dark: "dark-theme",
|
||||
}}
|
||||
>
|
||||
<AppContext.Provider
|
||||
value={{
|
||||
font,
|
||||
setFont,
|
||||
}}
|
||||
>
|
||||
<ToasterProvider />
|
||||
{children}
|
||||
<Analytics />
|
||||
</AppContext.Provider>
|
||||
</ThemeProvider>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user