diff --git a/.vscode/settings.json b/.vscode/settings.json new file mode 100644 index 000000000..0967ef424 --- /dev/null +++ b/.vscode/settings.json @@ -0,0 +1 @@ +{} diff --git a/app/(main)/page.tsx b/app/(main)/page.tsx index cc304c3f1..ac75dac77 100644 --- a/app/(main)/page.tsx +++ b/app/(main)/page.tsx @@ -11,11 +11,6 @@ import { ArrowUpOnSquareIcon } from "@heroicons/react/24/outline"; import * as Select from "@radix-ui/react-select"; import * as Switch from "@radix-ui/react-switch"; import * as Tooltip from "@radix-ui/react-tooltip"; -import { - createParser, - ParsedEvent, - ReconnectInterval, -} from "eventsource-parser"; import { AnimatePresence, motion } from "framer-motion"; import { FormEvent, useEffect, useState } from "react"; import { toast, Toaster } from "sonner"; @@ -26,6 +21,24 @@ export default function Home() { let [status, setStatus] = useState< "initial" | "creating" | "created" | "updating" | "updated" >("initial"); + let [prompt, setPrompt] = useState(""); + let models = [ + { + label: "Llama 3.1 405B", + value: "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", + }, + { + label: "Llama 3.1 70B", + value: "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", + }, + { + label: "Gemma 2 27B", + value: "google/gemma-2-27b-it", + }, + ]; + let [model, setModel] = useState(models[0].value); + let [shadcn, setShadcn] = useState(false); + let [modification, setModification] = useState(""); let [generatedCode, setGeneratedCode] = useState(""); let [initialAppConfig, setInitialAppConfig] = useState({ model: "", @@ -39,7 +52,7 @@ export default function Home() { let loading = status === "creating" || status === "updating"; - async function generateCode(e: FormEvent) { + async function createApp(e: FormEvent) { e.preventDefault(); if (status !== "initial") { @@ -49,134 +62,70 @@ export default function Home() { setStatus("creating"); setGeneratedCode(""); - let formData = new FormData(e.currentTarget); - let model = formData.get("model"); - let prompt = formData.get("prompt"); - let shadcn = !!formData.get("shadcn"); - if (typeof prompt !== "string" || typeof model !== "string") { - return; - } - let newMessages = [{ role: "user", content: prompt }]; - - const chatRes = await fetch("/api/generateCode", { + let res = await fetch("/api/generateCode", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - messages: newMessages, model, shadcn, + messages: [{ role: "user", content: prompt }], }), }); - if (!chatRes.ok) { - throw new Error(chatRes.statusText); - } - // This data is a ReadableStream - const data = chatRes.body; - if (!data) { - return; + if (!res.ok) { + throw new Error(res.statusText); } - const onParse = (event: ParsedEvent | ReconnectInterval) => { - if (event.type === "event") { - const data = event.data; - try { - const text = JSON.parse(data).text ?? ""; - setGeneratedCode((prev) => prev + text); - } catch (e) { - console.error(e); - } - } - }; - - // https://web.dev/streams/#the-getreader-and-read-methods - const reader = data.getReader(); - const decoder = new TextDecoder(); - const parser = createParser(onParse); - let done = false; - - while (!done) { - const { value, done: doneReading } = await reader.read(); - done = doneReading; - const chunkValue = decoder.decode(value); - parser.feed(chunkValue); + + if (!res.body) { + throw new Error("No response body"); } - newMessages = [ - ...newMessages, - { role: "assistant", content: generatedCode }, - ]; + for await (let chunk of readStream(res.body)) { + setGeneratedCode((prev) => prev + chunk); + } + setMessages([{ role: "user", content: prompt }]); setInitialAppConfig({ model, shadcn }); - setMessages(newMessages); setStatus("created"); } - async function modifyCode(e: FormEvent) { + async function updateApp(e: FormEvent) { e.preventDefault(); setStatus("updating"); - let formData = new FormData(e.currentTarget); - let prompt = formData.get("prompt"); - if (typeof prompt !== "string") { - return; - } - let newMessages = [...messages, { role: "user", content: prompt }]; + let codeMessage = { role: "assistant", content: generatedCode }; + let modificationMessage = { role: "user", content: modification }; setGeneratedCode(""); - const chatRes = await fetch("/api/generateCode", { + + const res = await fetch("/api/generateCode", { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ - messages: newMessages, + messages: [...messages, codeMessage, modificationMessage], model: initialAppConfig.model, shadcn: initialAppConfig.shadcn, }), }); - if (!chatRes.ok) { - throw new Error(chatRes.statusText); - } - // This data is a ReadableStream - const data = chatRes.body; - if (!data) { - return; + if (!res.ok) { + throw new Error(res.statusText); } - const onParse = (event: ParsedEvent | ReconnectInterval) => { - if (event.type === "event") { - const data = event.data; - try { - const text = JSON.parse(data).text ?? ""; - setGeneratedCode((prev) => prev + text); - } catch (e) { - console.error(e); - } - } - }; - - // https://web.dev/streams/#the-getreader-and-read-methods - const reader = data.getReader(); - const decoder = new TextDecoder(); - const parser = createParser(onParse); - let done = false; - - while (!done) { - const { value, done: doneReading } = await reader.read(); - done = doneReading; - const chunkValue = decoder.decode(value); - parser.feed(chunkValue); + + if (!res.body) { + throw new Error("No response body"); } - newMessages = [ - ...newMessages, - { role: "assistant", content: generatedCode }, - ]; + for await (let chunk of readStream(res.body)) { + setGeneratedCode((prev) => prev + chunk); + } - setMessages(newMessages); + setMessages((m) => [...m, codeMessage, modificationMessage]); setStatus("updated"); } @@ -208,7 +157,7 @@ export default function Home() {
into an app -
+
@@ -216,6 +165,8 @@ export default function Home() {
setPrompt(e.target.value)} name="prompt" className="w-full rounded-l-3xl bg-transparent px-6 py-5 text-lg focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500" placeholder="Build me a calculator app..." @@ -239,8 +190,9 @@ export default function Home() {

Model:

setModel(value)} > @@ -251,22 +203,7 @@ export default function Home() { - {[ - { - label: "Llama 3.1 405B", - value: - "meta-llama/Meta-Llama-3.1-405B-Instruct-Turbo", - }, - { - label: "Llama 3.1 70B", - value: - "meta-llama/Meta-Llama-3.1-70B-Instruct-Turbo", - }, - { - label: "Gemma 2 27B", - value: "google/gemma-2-27b-it", - }, - ].map((model) => ( + {models.map((model) => ( setShadcn(value)} > @@ -323,14 +262,16 @@ export default function Home() { ref={ref} >
- +
setModification(e.target.value)} className="w-full rounded-l-3xl bg-transparent px-6 py-5 text-lg focus-visible:outline focus-visible:outline-2 focus-visible:outline-blue-500 disabled:cursor-not-allowed" placeholder="Make changes to your app here" /> @@ -455,3 +396,17 @@ async function minDelay(promise: Promise, ms: number) { return p; } + +async function* readStream(response: ReadableStream) { + let reader = response.pipeThrough(new TextDecoderStream()).getReader(); + let done = false; + + while (!done) { + let { value, done: streamDone } = await reader.read(); + done = streamDone; + + if (value) yield value; + } + + reader.releaseLock(); +} diff --git a/app/api/generateCode/route.ts b/app/api/generateCode/route.ts index ac6921d9a..cdc1522ea 100644 --- a/app/api/generateCode/route.ts +++ b/app/api/generateCode/route.ts @@ -1,37 +1,73 @@ import shadcnDocs from "@/utils/shadcn-docs"; -import { - TogetherAIStream, - TogetherAIStreamPayload, -} from "@/utils/TogetherAIStream"; import dedent from "dedent"; +import Together from "together-ai"; +import { z } from "zod"; -export const runtime = "edge"; +let options: ConstructorParameters[0] = {}; +if (process.env.HELICONE_API_KEY) { + options.baseURL = "https://together.helicone.ai/v1"; + options.defaultHeaders = { + "Helicone-Auth": `Bearer ${process.env.HELICONE_API_KEY}`, + }; +} + +let together = new Together(options); export async function POST(req: Request) { - let { messages, model, shadcn } = await req.json(); + let json = await req.json(); + let result = z + .object({ + model: z.string(), + shadcn: z.boolean().default(false), + messages: z.array( + z.object({ + role: z.enum(["user", "assistant"]), + content: z.string(), + }), + ), + }) + .safeParse(json); + + if (result.error) { + return new Response(result.error.message, { status: 422 }); + } + + let { model, messages, shadcn } = result.data; let systemPrompt = getSystemPrompt(shadcn); - const payload: TogetherAIStreamPayload = { + let res = await together.chat.completions.create({ model, messages: [ { role: "system", content: systemPrompt, }, - ...messages.map((message: any) => { - if (message.role === "user") { - message.content += - "\nPlease ONLY return code, NO backticks or language names."; - } - return message; - }), + ...messages.map((message) => ({ + ...message, + content: + message.role === "user" + ? message.content + + "\nPlease ONLY return code, NO backticks or language names." + : message.content, + })), ], stream: true, temperature: 0.2, - }; - const stream = await TogetherAIStream(payload); + }); - return new Response(stream, { + let textStream = res + .toReadableStream() + .pipeThrough(new TextDecoderStream()) + .pipeThrough( + new TransformStream({ + transform(chunk, controller) { + let text = JSON.parse(chunk).choices[0].text; + controller.enqueue(text); + }, + }), + ); + + return new Response(textStream, { headers: new Headers({ "Cache-Control": "no-cache", }), @@ -63,8 +99,7 @@ function getSystemPrompt(shadcn: boolean) { ${shadcnDocs .map( - (component) => - ` + (component) => ` ${component.name} @@ -88,3 +123,5 @@ function getSystemPrompt(shadcn: boolean) { return dedent(systemPrompt); } + +export const runtime = "edge"; diff --git a/app/share/[id]/layout.tsx b/app/share/[id]/layout.tsx index dda140fdb..951ae7a40 100644 --- a/app/share/[id]/layout.tsx +++ b/app/share/[id]/layout.tsx @@ -1,7 +1,3 @@ -export default function Layout({ - children, -}: Readonly<{ - children: React.ReactNode; -}>) { +export default function Layout({ children }: { children: React.ReactNode }) { return {children}; } diff --git a/package-lock.json b/package-lock.json index a0813ced6..6de8cae04 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "dependencies": { "@codesandbox/sandpack-react": "^2.18.2", "@codesandbox/sandpack-themes": "^2.0.21", + "@conform-to/zod": "^1.1.5", "@heroicons/react": "^2.1.5", "@prisma/client": "^5.18.0", "@radix-ui/react-radio-group": "^1.2.0", @@ -24,7 +25,9 @@ "next-plausible": "^3.12.0", "react": "^18", "react-dom": "^18", - "sonner": "^1.5.0" + "sonner": "^1.5.0", + "together-ai": "^0.6.0-alpha.4", + "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^20", @@ -217,6 +220,22 @@ "resolved": "https://registry.npmjs.org/@codesandbox/sandpack-themes/-/sandpack-themes-2.0.21.tgz", "integrity": "sha512-CMH/MO/dh6foPYb/3eSn2Cu/J3+1+/81Fsaj7VggICkCrmRk0qG5dmgjGAearPTnRkOGORIPHuRqwNXgw0E6YQ==" }, + "node_modules/@conform-to/dom": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@conform-to/dom/-/dom-1.1.5.tgz", + "integrity": "sha512-tWI3YxWvDaZFSl4QN8Y0WbgBxsWEfcTrSMtKic3YTt/hGVJXjQ8qURBdsJXudzJ38zwAr5GhpLWmyI6BbG9+gA==" + }, + "node_modules/@conform-to/zod": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@conform-to/zod/-/zod-1.1.5.tgz", + "integrity": "sha512-ho/742wgfTWf2u5tKo4GT88xcKcvTOCDPOi4o1WEmePPmJNP5ezeMQSKSdKdS0rFinZl9ZTH8gV5FQUNPHAA7A==", + "dependencies": { + "@conform-to/dom": "1.1.5" + }, + "peerDependencies": { + "zod": "^3.21.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -1486,11 +1505,19 @@ "version": "20.14.13", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.14.13.tgz", "integrity": "sha512-+bHoGiZb8UiQ0+WEtmph2IWQCjIqg8MDZMAV+ppRRhUZnquF5mQkP/9vpSwJClEiSM/C7fZZExPzfU0vJTyp8w==", - "dev": true, "dependencies": { "undici-types": "~5.26.4" } }, + "node_modules/@types/node-fetch": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.11.tgz", + "integrity": "sha512-24xFj9R5+rfQJLRyM56qh+wnVSYhyXC2tkoBndtY0U+vubqNsYXGjufB2nn8Q6gt0LrARwL6UBtMCSVCwl4B1g==", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.0" + } + }, "node_modules/@types/prop-types": { "version": "15.7.12", "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.12.tgz", @@ -1653,6 +1680,17 @@ "node": ">=16" } }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, "node_modules/acorn": { "version": "8.12.1", "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.12.1.tgz", @@ -1674,6 +1712,17 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/agentkeepalive": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.5.0.tgz", + "integrity": "sha512-5GG/5IbQQpC9FpkRGsSvZI5QYeSCzlJHdpBQntCsuTOxhKD8lqKhrleg2Yi7yvMIf82Ycmmqln9U8V9qwEiJew==", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/ajv": { "version": "6.12.6", "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", @@ -1935,6 +1984,11 @@ "integrity": "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ==", "dev": true }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==" + }, "node_modules/available-typed-arrays": { "version": "1.0.7", "resolved": "https://registry.npmjs.org/available-typed-arrays/-/available-typed-arrays-1.0.7.tgz", @@ -2218,6 +2272,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -2469,6 +2534,14 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "engines": { + "node": ">=0.4.0" + } + }, "node_modules/dequal": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", @@ -3335,6 +3408,14 @@ "es5-ext": "~0.10.14" } }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "engines": { + "node": ">=6" + } + }, "node_modules/eventsource-parser": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-1.1.2.tgz", @@ -3496,6 +3577,44 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/form-data": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz", + "integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==" + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "engines": { + "node": ">= 14" + } + }, "node_modules/framer-motion": { "version": "11.3.21", "resolved": "https://registry.npmjs.org/framer-motion/-/framer-motion-11.3.21.tgz", @@ -3830,6 +3949,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "dependencies": { + "ms": "^2.0.0" + } + }, "node_modules/ieee754": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", @@ -4572,6 +4699,25 @@ "node": ">= 0.6" } }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", @@ -4608,8 +4754,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mz": { "version": "2.7.0", @@ -4739,6 +4884,43 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -6288,6 +6470,34 @@ "node": ">=8.0" } }, + "node_modules/together-ai": { + "version": "0.6.0-alpha.4", + "resolved": "https://registry.npmjs.org/together-ai/-/together-ai-0.6.0-alpha.4.tgz", + "integrity": "sha512-aeMAMkDJn6Pzdr93cDc9dQksshGMBr+6v1096NI82iAnozuT/11ngrW4h9iiQWChh/CoiD0vWDASendG/v84CQ==", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7", + "web-streams-polyfill": "^3.2.1" + } + }, + "node_modules/together-ai/node_modules/@types/node": { + "version": "18.19.45", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.45.tgz", + "integrity": "sha512-VZxPKNNhjKmaC1SUYowuXSRSMGyQGmQjvvA1xE4QZ0xce2kLtEhPDS+kqpCPBZYgqblCLQ2DAjSzmgCM5auvhA==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==" + }, "node_modules/ts-api-utils": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.3.0.tgz", @@ -6456,8 +6666,7 @@ "node_modules/undici-types": { "version": "5.26.5", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", - "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", - "dev": true + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" }, "node_modules/unicode-trie": { "version": "2.0.0", @@ -6529,6 +6738,28 @@ "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/which": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", @@ -6757,6 +6988,14 @@ "version": "0.3.3", "resolved": "https://registry.npmjs.org/yoga-wasm-web/-/yoga-wasm-web-0.3.3.tgz", "integrity": "sha512-N+d4UJSJbt/R3wqY7Coqs5pcV0aUj2j9IaQ3rNj9bVCLld8tTGKRa2USARjnvZJWVx1NDmQev8EknoczaOQDOA==" + }, + "node_modules/zod": { + "version": "3.23.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.23.8.tgz", + "integrity": "sha512-XBx9AXhXktjUqnepgTiE5flcKIYWi/rme0Eaj+5Y0lftuGBq+jyRu/md4WnuxqgP1ubdpNCsYEYPxrzVHD8d6g==", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 93ceca253..3e4871df8 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "dependencies": { "@codesandbox/sandpack-react": "^2.18.2", "@codesandbox/sandpack-themes": "^2.0.21", + "@conform-to/zod": "^1.1.5", "@heroicons/react": "^2.1.5", "@prisma/client": "^5.18.0", "@radix-ui/react-radio-group": "^1.2.0", @@ -25,7 +26,9 @@ "next-plausible": "^3.12.0", "react": "^18", "react-dom": "^18", - "sonner": "^1.5.0" + "sonner": "^1.5.0", + "together-ai": "^0.6.0-alpha.4", + "zod": "^3.23.8" }, "devDependencies": { "@types/node": "^20", diff --git a/utils/TogetherAIStream.ts b/utils/TogetherAIStream.ts deleted file mode 100644 index e881f3707..000000000 --- a/utils/TogetherAIStream.ts +++ /dev/null @@ -1,113 +0,0 @@ -import { - createParser, - ParsedEvent, - ReconnectInterval, -} from "eventsource-parser"; - -export type ChatGPTAgent = "user" | "system"; - -export interface ChatGPTMessage { - role: ChatGPTAgent; - content: string; -} - -export interface TogetherAIStreamPayload { - model: string; - messages: ChatGPTMessage[]; - stream: boolean; - temperature: number; -} - -export async function TogetherAIStream(payload: TogetherAIStreamPayload) { - const encoder = new TextEncoder(); - const decoder = new TextDecoder(); - - let res; - - if (process.env.HELICONE_API_KEY) { - res = await fetch("https://together.helicone.ai/v1/chat/completions", { - headers: { - "Content-Type": "application/json", - "Helicone-Auth": `Bearer ${process.env.HELICONE_API_KEY}`, - Authorization: `Bearer ${process.env.TOGETHER_API_KEY ?? ""}`, - }, - method: "POST", - body: JSON.stringify(payload), - }); - } else { - res = await fetch("https://api.together.xyz/v1/chat/completions", { - headers: { - "Content-Type": "application/json", - Authorization: `Bearer ${process.env.TOGETHER_API_KEY ?? ""}`, - }, - method: "POST", - body: JSON.stringify(payload), - }); - } - - const readableStream = new ReadableStream({ - async start(controller) { - // callback - const onParse = (event: ParsedEvent | ReconnectInterval) => { - if (event.type === "event") { - const data = event.data; - controller.enqueue(encoder.encode(data)); - } - }; - - // optimistic error handling - if (res.status !== 200) { - const data = { - status: res.status, - statusText: res.statusText, - body: await res.text(), - }; - console.log( - `Error: recieved non-200 status code, ${JSON.stringify(data)}`, - ); - controller.close(); - return; - } - - // stream response (SSE) from OpenAI may be fragmented into multiple chunks - // this ensures we properly read chunks and invoke an event for each SSE event stream - const parser = createParser(onParse); - // https://web.dev/streams/#asynchronous-iteration - for await (const chunk of res.body as any) { - parser.feed(decoder.decode(chunk)); - } - }, - }); - - let counter = 0; - const transformStream = new TransformStream({ - async transform(chunk, controller) { - const data = decoder.decode(chunk); - // https://beta.openai.com/docs/api-reference/completions/create#completions/create-stream - if (data === "[DONE]") { - controller.terminate(); - return; - } - try { - const json = JSON.parse(data); - const text = json.choices[0].delta?.content || ""; - if (counter < 2 && (text.match(/\n/) || []).length) { - // this is a prefix character (i.e., "\n\n"), do nothing - return; - } - // stream transformed JSON resposne as SSE - const payload = { text: text }; - // https://developer.mozilla.org/en-US/docs/Web/API/Server-sent_events/Using_server-sent_events#event_stream_format - controller.enqueue( - encoder.encode(`data: ${JSON.stringify(payload)}\n\n`), - ); - counter++; - } catch (e) { - // maybe parse error - controller.error(e); - } - }, - }); - - return readableStream.pipeThrough(transformStream); -}