1
0
Code Issues Pull Requests Packages Projects Releases Wiki Activity GitHub Gitee

add web demo

This commit is contained in:
songjunxi
2023-10-23 09:02:56 +08:00
parent dbb5b1bdfb
commit bab0cf49a1
29 changed files with 6440 additions and 535 deletions

View 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);
}

View 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

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

37
apps/web/app/layout.tsx Normal file
View 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
View 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>
);
}

View 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>
);
}