Skip to content

Commit

Permalink
Update to AI SDK + App Router.
Browse files Browse the repository at this point in the history
  • Loading branch information
leerob committed Jul 15, 2023
1 parent 6ce60e4 commit 597101d
Show file tree
Hide file tree
Showing 19 changed files with 1,202 additions and 2,769 deletions.
39 changes: 39 additions & 0 deletions app/api/chat/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Configuration, OpenAIApi } from 'openai-edge';
import { OpenAIStream, StreamingTextResponse } from 'ai';

// Create an OpenAI API client (that's edge friendly!)
const config = new Configuration({
apiKey: process.env.OPENAI_API_KEY,
});
const openai = new OpenAIApi(config);

// Set the runtime to edge for best performance
export const runtime = 'edge';

export async function POST(req: Request) {
const { vibe, bio } = await req.json();

// Ask OpenAI for a streaming completion given the prompt
const response = await openai.createChatCompletion({
model: 'gpt-3.5-turbo',
stream: true,
messages: [
{
role: 'user',
content: `Generate 2 ${vibe} twitter biographies with no hashtags and clearly labeled "1." and "2.". ${
vibe === 'Funny'
? "Make sure there is a joke in there and it's a little ridiculous."
: null
}
Make sure each generated biography is less than 160 characters, has short sentences that are found in Twitter bios, and base them on this context: ${bio}${
bio.slice(-1) === '.' ? '' : '.'
}`,
},
],
});

// Convert the response into a friendly text-stream
const stream = OpenAIStream(response);
// Respond with the stream
return new StreamingTextResponse(stream);
}
File renamed without changes.
38 changes: 38 additions & 0 deletions app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
import { Analytics } from '@vercel/analytics/react';
import { Metadata } from 'next';
import '../styles/globals.css';

const title = 'Twitter Bio Generator';
const description = 'Generate your next Twitter bio in seconds.';

export const metadata: Metadata = {
metadataBase: new URL('https://twitterbio.io'),
title,
description,
openGraph: {
title,
description,
locale: 'en_US',
type: 'website',
},
twitter: {
card: 'summary_large_image',
title,
description,
},
};

export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return (
<html lang="en">
<body>
{children}
<Analytics />
</body>
</html>
);
}
File renamed without changes
100 changes: 36 additions & 64 deletions pages/index.tsx → app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,53 +1,39 @@
import type { NextPage } from "next";
import Head from "next/head";
import Image from "next/image";
import { useRef, useState } from "react";
import { Toaster, toast } from "react-hot-toast";
import DropDown, { VibeType } from "../components/DropDown";
import Footer from "../components/Footer";
import Github from "../components/GitHub";
import Header from "../components/Header";
import LoadingDots from "../components/LoadingDots";
import {
createParser,
ParsedEvent,
ReconnectInterval,
} from "eventsource-parser";
'use client';

const Home: NextPage = () => {
const [loading, setLoading] = useState(false);
const [bio, setBio] = useState("");
const [vibe, setVibe] = useState<VibeType>("Professional");
const [generatedBios, setGeneratedBios] = useState<String>("");
import Image from 'next/image';
import { useRef, useState } from 'react';
import { Toaster, toast } from 'react-hot-toast';
import DropDown, { VibeType } from '../components/DropDown';
import Footer from '../components/Footer';
import Github from '../components/GitHub';
import Header from '../components/Header';

export default function Page() {
const [loading, setLoading] = useState(false);
const [bio, setBio] = useState('');
const [vibe, setVibe] = useState<VibeType>('Professional');
const [generatedBios, setGeneratedBios] = useState<String>('');
const bioRef = useRef<null | HTMLDivElement>(null);

const scrollToBios = () => {
if (bioRef.current !== null) {
bioRef.current.scrollIntoView({ behavior: "smooth" });
bioRef.current.scrollIntoView({ behavior: 'smooth' });
}
};

const prompt = `Generate 2 ${vibe} twitter biographies with no hashtags and clearly labeled "1." and "2.". ${
vibe === "Funny"
? "Make sure there is a joke in there and it's a little ridiculous."
: null
}
Make sure each generated biography is less than 160 characters, has short sentences that are found in Twitter bios, and base them on this context: ${bio}${
bio.slice(-1) === "." ? "" : "."
}`;

const generateBio = async (e: any) => {
e.preventDefault();
setGeneratedBios("");
setGeneratedBios('');
setLoading(true);
const response = await fetch("/api/generate", {
method: "POST",

const response = await fetch('/api/chat', {
method: 'POST',
headers: {
"Content-Type": "application/json",
'Content-Type': 'application/json',
},
body: JSON.stringify({
prompt,
vibe,
bio,
}),
});

Expand All @@ -61,40 +47,24 @@ const Home: NextPage = () => {
return;
}

const onParse = (event: ParsedEvent | ReconnectInterval) => {
if (event.type === "event") {
const data = event.data;
try {
const text = JSON.parse(data).text ?? ""
setGeneratedBios((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);
setGeneratedBios((prev) => prev + chunkValue);
}

scrollToBios();
setLoading(false);
};

return (
<div className="flex max-w-5xl mx-auto flex-col items-center justify-center py-2 min-h-screen">
<Head>
<title>Twitter Bio Generator</title>
<link rel="icon" href="/favicon.ico" />
</Head>

<Header />
<main className="flex flex-1 w-full flex-col items-center justify-center text-center px-4 mt-12 sm:mt-20">
<a
Expand All @@ -120,7 +90,7 @@ const Home: NextPage = () => {
className="mb-5 sm:mb-0"
/>
<p className="text-left font-medium">
Copy your current bio{" "}
Copy your current bio{' '}
<span className="text-slate-500">
(or write a few sentences about yourself)
</span>
Expand All @@ -133,7 +103,7 @@ const Home: NextPage = () => {
rows={4}
className="w-full rounded-md border-gray-300 shadow-sm focus:border-black focus:ring-black my-5"
placeholder={
"e.g. Senior Developer Advocate @vercel. Tweeting about web development, AI, and React / Next.js. Writing nutlope.substack.com."
'e.g. Senior Developer Advocate @vercel. Tweeting about web development, AI, and React / Next.js. Writing nutlope.substack.com.'
}
/>
<div className="flex mb-5 items-center space-x-3">
Expand All @@ -157,7 +127,11 @@ const Home: NextPage = () => {
className="bg-black rounded-xl text-white font-medium px-4 py-2 sm:mt-10 mt-8 hover:bg-black/80 w-full"
disabled
>
<LoadingDots color="white" style="large" />
<span className="loading">
<span style={{ backgroundColor: 'white' }} />
<span style={{ backgroundColor: 'white' }} />
<span style={{ backgroundColor: 'white' }} />
</span>
</button>
)}
</div>
Expand All @@ -180,16 +154,16 @@ const Home: NextPage = () => {
</div>
<div className="space-y-8 flex flex-col items-center justify-center max-w-xl mx-auto">
{generatedBios
.substring(generatedBios.indexOf("1") + 3)
.split("2.")
.substring(generatedBios.indexOf('1') + 3)
.split('2.')
.map((generatedBio) => {
return (
<div
className="bg-white rounded-xl shadow-md p-4 hover:bg-gray-100 transition cursor-copy border"
onClick={() => {
navigator.clipboard.writeText(generatedBio);
toast("Bio copied to clipboard", {
icon: "✂️",
toast('Bio copied to clipboard', {
icon: '✂️',
});
}}
key={generatedBio}
Expand All @@ -206,6 +180,4 @@ const Home: NextPage = () => {
<Footer />
</div>
);
};

export default Home;
}
Binary file added app/twitter-image.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 8 additions & 10 deletions components/Footer.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,31 @@
import Link from "next/link";
import Link from 'next/link';

export default function Footer() {
return (
<footer className="text-center h-16 sm:h-20 w-full sm:pt-2 pt-4 border-t mt-5 flex sm:flex-row flex-col justify-between items-center px-3 space-y-3 sm:mb-0 mb-3">
<div>
Powered by{" "}
Powered by{' '}
<a
href="https://openai.com/blog/chatgpt"
target="_blank"
rel="noreferrer"
className="font-bold hover:underline transition underline-offset-2"
>
ChatGPT{" "}
ChatGPT{' '}
</a>
and{" "}
and{' '}
<a
href="https://vercel.com/"
href="https://sdk.vercel.ai/docs"
target="_blank"
rel="noreferrer"
className="font-bold hover:underline transition underline-offset-2"
>
Vercel Edge Functions.
Vercel AI SDK
</a>
</div>
<div className="flex space-x-4 pb-4 sm:pb-0">
<Link
href="https://twitter.com/nutlope"
className="group"
aria-label="TaxPal on Twitter"
aria-label="Nutlope Twitter"
>
<svg
aria-hidden="true"
Expand All @@ -39,7 +37,7 @@ export default function Footer() {
<Link
href="https://github.com/Nutlope/twitterbio"
className="group"
aria-label="TaxPal on GitHub"
aria-label="GitHub"
>
<svg
aria-hidden="true"
Expand Down
23 changes: 0 additions & 23 deletions components/LoadingDots.tsx

This file was deleted.

9 changes: 4 additions & 5 deletions next.config.js
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
/** @type {import('next').NextConfig} */
module.exports = {
reactStrictMode: true,
async redirects() {
return [
{
source: "/github",
destination: "https://github.com/Nutlope/twitterbio",
source: '/github',
destination: 'https://github.com/Nutlope/twitterbio',
permanent: false,
},
{
source: "/deploy",
destination: "https://vercel.com/templates/next.js/twitter-bio",
source: '/deploy',
destination: 'https://vercel.com/templates/next.js/twitter-bio',
permanent: false,
},
];
Expand Down
Loading

0 comments on commit 597101d

Please sign in to comment.