Skip to content

Commit 3c916ab

Browse files
authored
Merge pull request Nutlope#75 from Nutlope/use-edge-runtime
Use Edge runtime
2 parents 5cb686c + b77049b commit 3c916ab

18 files changed

Lines changed: 771 additions & 443 deletions

File tree

app/(main)/actions.ts

Lines changed: 5 additions & 57 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,21 @@
11
"use server";
22

3-
import prisma from "@/lib/prisma";
4-
import { notFound } from "next/navigation";
5-
import Together from "together-ai";
6-
import { z } from "zod";
3+
import { getPrisma } from "@/lib/prisma";
74
import {
85
getMainCodingPrompt,
96
screenshotToCodePrompt,
107
softwareArchitectPrompt,
118
} from "@/lib/prompts";
9+
import { notFound } from "next/navigation";
10+
import Together from "together-ai";
1211

1312
export async function createChat(
1413
prompt: string,
1514
model: string,
1615
quality: "high" | "low",
1716
screenshotUrl: string | undefined,
1817
) {
18+
const prisma = getPrisma();
1919
const chat = await prisma.chat.create({
2020
data: {
2121
model,
@@ -98,7 +98,6 @@ export async function createChat(
9898
messages: [
9999
{
100100
role: "user",
101-
// @ts-expect-error Need to fix the TypeScript library type
102101
content: [
103102
{ type: "text", text: screenshotToCodePrompt },
104103
{
@@ -185,6 +184,7 @@ export async function createMessage(
185184
text: string,
186185
role: "assistant" | "user",
187186
) {
187+
const prisma = getPrisma();
188188
const chat = await prisma.chat.findUnique({
189189
where: { id: chatId },
190190
include: { messages: true },
@@ -204,55 +204,3 @@ export async function createMessage(
204204

205205
return newMessage;
206206
}
207-
208-
export async function getNextCompletionStreamPromise(
209-
messageId: string,
210-
model: string,
211-
) {
212-
const message = await prisma.message.findUnique({ where: { id: messageId } });
213-
if (!message) notFound();
214-
215-
const messagesRes = await prisma.message.findMany({
216-
where: { chatId: message.chatId, position: { lte: message.position } },
217-
orderBy: { position: "asc" },
218-
});
219-
220-
let messages = z
221-
.array(
222-
z.object({
223-
role: z.enum(["system", "user", "assistant"]),
224-
content: z.string(),
225-
}),
226-
)
227-
.parse(messagesRes);
228-
229-
if (messages.length > 10) {
230-
messages = [messages[0], messages[1], messages[2], ...messages.slice(-7)];
231-
}
232-
233-
let options: ConstructorParameters<typeof Together>[0] = {};
234-
if (process.env.HELICONE_API_KEY) {
235-
options.baseURL = "https://together.helicone.ai/v1";
236-
options.defaultHeaders = {
237-
"Helicone-Auth": `Bearer ${process.env.HELICONE_API_KEY}`,
238-
"Helicone-Property-appname": "LlamaCoder",
239-
"Helicone-Session-Id": message.chatId,
240-
"Helicone-Session-Name": "LlamaCoder Chat",
241-
};
242-
}
243-
244-
const together = new Together(options);
245-
return {
246-
streamPromise: new Promise<ReadableStream>(async (resolve) => {
247-
const res = await together.chat.completions.create({
248-
model,
249-
messages: messages.map((m) => ({ role: m.role, content: m.content })),
250-
stream: true,
251-
temperature: 0.2,
252-
max_tokens: 9000,
253-
});
254-
255-
resolve(res.toReadableStream());
256-
}),
257-
};
258-
}

app/(main)/chats/[id]/chat-box.tsx

Lines changed: 53 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,8 @@ import ArrowRightIcon from "@/components/icons/arrow-right";
44
import Spinner from "@/components/spinner";
55
import assert from "assert";
66
import { useRouter } from "next/navigation";
7-
import { useEffect, useRef, useTransition } from "react";
8-
import TextareaAutosize from "react-textarea-autosize";
9-
import { createMessage, getNextCompletionStreamPromise } from "../../actions";
7+
import { useEffect, useRef, useState, useTransition } from "react";
8+
import { createMessage } from "../../actions";
109
import { type Chat } from "./page";
1110

1211
export default function ChatBox({
@@ -23,6 +22,11 @@ export default function ChatBox({
2322
const disabled = isPending || isStreaming;
2423
const didFocusOnce = useRef(false);
2524
const textareaRef = useRef<HTMLTextAreaElement>(null);
25+
const [prompt, setPrompt] = useState("");
26+
const textareaResizePrompt = prompt
27+
.split("\n")
28+
.map((text) => (text === "" ? "a" : text))
29+
.join("\n");
2630

2731
useEffect(() => {
2832
if (!textareaRef.current) return;
@@ -39,42 +43,60 @@ export default function ChatBox({
3943
<div className="mx-auto mb-5 flex w-full max-w-prose shrink-0 px-8">
4044
<form
4145
className="relative flex w-full"
42-
action={async (formData) => {
46+
action={async () => {
4347
startTransition(async () => {
44-
const prompt = formData.get("prompt");
45-
assert.ok(typeof prompt === "string");
46-
4748
const message = await createMessage(chat.id, prompt, "user");
48-
const { streamPromise } = await getNextCompletionStreamPromise(
49-
message.id,
50-
chat.model,
51-
);
52-
onNewStreamPromise(streamPromise);
49+
const streamPromise = fetch(
50+
"/api/get-next-completion-stream-promise",
51+
{
52+
method: "POST",
53+
body: JSON.stringify({
54+
messageId: message.id,
55+
model: chat.model,
56+
}),
57+
},
58+
).then((res) => {
59+
if (!res.body) {
60+
throw new Error("No body on response");
61+
}
62+
return res.body;
63+
});
5364

54-
router.refresh();
65+
onNewStreamPromise(streamPromise);
66+
startTransition(() => {
67+
router.refresh();
68+
setPrompt("");
69+
});
5570
});
5671
}}
5772
>
5873
<fieldset className="w-full" disabled={disabled}>
5974
<div className="relative flex rounded-lg border-4 border-gray-300 bg-white">
60-
<TextareaAutosize
61-
ref={textareaRef}
62-
placeholder="Follow up"
63-
autoFocus={!disabled}
64-
required
65-
name="prompt"
66-
rows={2}
67-
minRows={2}
68-
className="peer relative w-full resize-none bg-transparent p-2 placeholder-gray-500 focus:outline-none disabled:opacity-50"
69-
onKeyDown={(event) => {
70-
if (event.key === "Enter" && !event.shiftKey) {
71-
event.preventDefault();
72-
const target = event.target;
73-
if (!(target instanceof HTMLTextAreaElement)) return;
74-
target.closest("form")?.requestSubmit();
75-
}
76-
}}
77-
/>
75+
<div className="relative w-full">
76+
<div className="w-full p-2">
77+
<p className="invisible min-h-[48px] w-full whitespace-pre-wrap">
78+
{textareaResizePrompt}
79+
</p>
80+
</div>
81+
<textarea
82+
ref={textareaRef}
83+
placeholder="Follow up"
84+
autoFocus={!disabled}
85+
value={prompt}
86+
onChange={(e) => setPrompt(e.target.value)}
87+
required
88+
name="prompt"
89+
className="peer absolute inset-0 w-full resize-none bg-transparent p-2 placeholder-gray-500 focus:outline-none disabled:opacity-50"
90+
onKeyDown={(event) => {
91+
if (event.key === "Enter" && !event.shiftKey) {
92+
event.preventDefault();
93+
const target = event.target;
94+
if (!(target instanceof HTMLTextAreaElement)) return;
95+
target.closest("form")?.requestSubmit();
96+
}
97+
}}
98+
/>
99+
</div>
78100
<div className="pointer-events-none absolute inset-0 rounded peer-focus:outline peer-focus:outline-offset-0 peer-focus:outline-blue-500" />
79101

80102
<div className="absolute bottom-1.5 right-1.5 flex has-[:disabled]:opacity-50">

app/(main)/chats/[id]/chat-log.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ export default function ChatLog({
6161

6262
function UserMessage({ content }: { content: string }) {
6363
return (
64-
<div className="relative inline-flex w-4/5 items-end gap-3 self-end">
64+
<div className="relative inline-flex max-w-[80%] items-end gap-3 self-end">
6565
<div className="whitespace-pre-wrap rounded bg-white px-4 py-2 text-gray-600 shadow">
6666
{content}
6767
</div>

app/(main)/chats/[id]/code-viewer.tsx

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,25 @@
11
"use client";
22

3-
import CodeRunner from "@/components/code-runner";
43
import ChevronLeftIcon from "@/components/icons/chevron-left";
54
import ChevronRightIcon from "@/components/icons/chevron-right";
65
import CloseIcon from "@/components/icons/close-icon";
76
import RefreshIcon from "@/components/icons/refresh";
8-
import SyntaxHighlighter from "@/components/syntax-highlighter";
97
import { extractFirstCodeBlock, splitByFirstCodeFence } from "@/lib/utils";
108
import { useState } from "react";
119
import type { Chat, Message } from "./page";
1210
import { Share } from "./share";
1311
import { StickToBottom } from "use-stick-to-bottom";
12+
import dynamic from "next/dynamic";
13+
14+
const CodeRunner = dynamic(() => import("@/components/code-runner"), {
15+
ssr: false,
16+
});
17+
const SyntaxHighlighter = dynamic(
18+
() => import("@/components/syntax-highlighter"),
19+
{
20+
ssr: false,
21+
},
22+
);
1423

1524
export default function CodeViewer({
1625
chat,

app/(main)/chats/[id]/page.client.tsx

Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
"use client";
22

3-
import {
4-
createMessage,
5-
getNextCompletionStreamPromise,
6-
} from "@/app/(main)/actions";
3+
import { createMessage } from "@/app/(main)/actions";
74
import LogoSmall from "@/components/icons/logo-small";
85
import { splitByFirstCodeFence } from "@/lib/utils";
96
import Link from "next/link";
@@ -153,11 +150,22 @@ export default function PageClient({ chat }: { chat: Chat }) {
153150
newMessageText,
154151
"user",
155152
);
156-
const { streamPromise } =
157-
await getNextCompletionStreamPromise(
158-
message.id,
159-
chat.model,
160-
);
153+
154+
const streamPromise = fetch(
155+
"/api/get-next-completion-stream-promise",
156+
{
157+
method: "POST",
158+
body: JSON.stringify({
159+
messageId: message.id,
160+
model: chat.model,
161+
}),
162+
},
163+
).then((res) => {
164+
if (!res.body) {
165+
throw new Error("No body on response");
166+
}
167+
return res.body;
168+
});
161169
setStreamPromise(streamPromise);
162170
router.refresh();
163171
});

app/(main)/chats/[id]/page.tsx

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
import client from "@/lib/prisma";
2-
import PageClient from "./page.client";
1+
import { getPrisma } from "@/lib/prisma";
32
import { notFound } from "next/navigation";
43
import { cache } from "react";
4+
import PageClient from "./page.client";
55

66
export default async function Page({
77
params,
@@ -17,7 +17,8 @@ export default async function Page({
1717
}
1818

1919
const getChatById = cache(async (id: string) => {
20-
return await client.chat.findFirst({
20+
const prisma = getPrisma();
21+
return await prisma.chat.findFirst({
2122
where: { id },
2223
include: { messages: { orderBy: { position: "asc" } } },
2324
});
@@ -26,4 +27,5 @@ const getChatById = cache(async (id: string) => {
2627
export type Chat = NonNullable<Awaited<ReturnType<typeof getChatById>>>;
2728
export type Message = Chat["messages"][number];
2829

30+
export const runtime = "edge";
2931
export const maxDuration = 45;

0 commit comments

Comments
 (0)