Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: update openai template #193

Merged
merged 3 commits into from
Nov 25, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions axum/openai/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/target
.shuttle*
Secrets*.toml
21 changes: 17 additions & 4 deletions axum/openai/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,22 @@ edition = "2021"

[dependencies]
async-openai = "0.23.0"
axum = "0.7.3"
serde_json = "1"
argon2 = "0.5.3"
axum = "0.7.4"
axum-extra = { version = "0.9.4", features = ["cookie", "cookie-private"] }
derive_more = { version = "1.0.0", features = ["full"] }
jsonwebtoken = "9.3.0"
serde = { version = "1.0.215", features = ["derive"] }
serde_json = "1.0.133"

sqlx = { version = "0.8.2", features = [
"runtime-tokio-rustls",
"postgres",
"macros",
] }
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }
tower-http = { version = "0.6.2", features = ["cors", "fs"] }
shuttle-runtime = "0.49.0"
shuttle-axum = "0.49.0"
shuttle-shared-db = { version = "0.49.0", features = ["postgres"] }
shuttle-openai = "0.49.0"
shuttle-runtime = "0.49.0"
tokio = "1.26.0"
22 changes: 17 additions & 5 deletions axum/openai/README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,19 @@
A simple endpoint that sends a chat message to ChatGPT and returns the response.
## Shuttle AI Playground
joshua-mo-143 marked this conversation as resolved.
Show resolved Hide resolved
This template enables you to spin up an AI playground in the style of OpenAI.

Set your OpenAI API key in `Secrets.toml`, then try it on a local run with:
## Features
- Frontend (via Next.js)
- Authentication and sign-ups with cookie-wrapped JWTs
- Database
- OpenAI

```sh
curl http://localhost:8000 -H 'content-type: application/json' --data '{"message":"What is shuttle.rs?"}'
```
## How to use
Before using this, you will need an OpenAI API key.

1) Ensure the OpenAI API key is in your `Secrets.toml` file (see file for syntax if not sure how to use).
2) Run `npm --prefix frontend install && npm --prefix frontend run build` to build the Next.js frontend.
3) Use `shuttle run` to run the template locally - or use `shuttle deploy` to deploy!

## Troubleshooting
- The default port is at 8000. If you are already running something here, you can use `--port` to select a different port.
- Your OpenAI client may error out if you don't have your OpenAI API key set correctly (should be `OPENAI_API_KEY` in Secrets.toml).
5 changes: 5 additions & 0 deletions axum/openai/Shuttle.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
[build]
assets = ["frontend/dist/*"]

[deploy]
include = ["frontend/dist/*"]
3 changes: 3 additions & 0 deletions axum/openai/frontend/.eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
{
"extends": ["next/core-web-vitals", "next/typescript"]
}
43 changes: 43 additions & 0 deletions axum/openai/frontend/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.

# dependencies
/node_modules
/.pnp
.pnp.*
.yarn/*
!.yarn/patches
!.yarn/plugins
!.yarn/releases
!.yarn/versions

# testing
/coverage

# next.js
/.next/
/out/

# production
/build

# misc
.DS_Store
*.pem

# debug
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# env files (can opt-in for committing if needed)
.env*

# vercel
.vercel

# typescript
*.tsbuildinfo
next-env.d.ts

# build
dist
167 changes: 167 additions & 0 deletions axum/openai/frontend/app/app/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
"use client";
import { ChatMessage } from "@/components/ChatMessage";
import { ConversationButton } from "@/components/ConversationButton";
import { useRouter } from "next/navigation";
import { useEffect, useState } from "react";

export interface GPTMessage {
role: string;
message: string;
}

export default function AppPage() {
const router = useRouter();
const [model, setModel] = useState<string>("gpt-4o");
const [message, setMessage] = useState<string>("");
const [conversationId, setConversationId] = useState<number>(0);
const [conversationList, setConversationList] = useState<number[]>([]);
const [messages, setMessages] = useState<GPTMessage[]>([]);
const [loading, setLoading] = useState<boolean>(false);

function newChat() {
setConversationId(0);
setMessages([]);
}

async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
e.preventDefault();

if (conversationId === 0) {
const res = await fetch(`/api/chat/create`, {
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
prompt: message,
model: model,
}),
});

const data = await res.json();

const new_conversation_list = conversationList;
new_conversation_list.unshift(data.conversation_id);
console.log(data.conversation_id);
setConversationId(data.conversation_id);
setConversationList(new_conversation_list);
}

setMessages((prev) => {
return [...prev, { message: message, role: "user" }];
});
setMessage("");
setLoading(true);

const conversation_res = await fetch(
`/api/chat/conversations/${conversationId}`,
{
method: "POST",
headers: {
"content-type": "application/json",
},
body: JSON.stringify({
prompt: message,
model: model,
}),
},
);

const promptReqData = await conversation_res.json();

setMessages((prev) => {
return [...prev, { message: promptReqData.response, role: "system" }];
});
setLoading(false);
}

useEffect(() => {
const fetchData = async () => {
const res = await fetch("/api/chat/conversations");
if (res.ok) {
const data = await res.json();
setConversationList(data);
} else {
router.replace("/login");
}
};

fetchData();
}, []);

return (
<main className="grid grid-cols-6 grid-rows-1 w-full h-full min-h-screen">
<div
id="sidebar"
className="col-span-1 row-span-1 border-r border-[#333]"
>
<h1>Shuttle</h1>
<div className="p-4">
<button
className="px-4 py-1 w-full text-left rounded-md bg-gradient-to-r from-orange-700 to-yellow-400"
onClick={() => newChat()}
>
New chat
</button>
<h2 className="text-center font-bold mt-4">Conversations</h2>
<div className="py-4 flex flex-col items-start gap-2">
{conversationList.map((x) => (
<ConversationButton
id={x}
key={x}
setMessages={setMessages}
setConversationId={setConversationId}
active={conversationId === x}
/>
))}
</div>
</div>
</div>
<div className="col-span-5 row-span-1 flex flex-col p-4 w-full gap-4 max-h-screen overflow-auto">
<select
className="font-bold text-slate-300 w-[15%] bg-gray-800 px-4 py-2"
onChange={(e) => setModel((e.target as HTMLSelectElement).value)}
>
<option className="text-black" value="gpt-4o">
gpt-4o
</option>
<option className="text-black" value="gpt-4o-mini">
gpt-4o-mini
</option>
<option className="text-black" value="gpt-4o-preview">
gpt-4o-preview
</option>
</select>
<div
id="chatbox"
className="flex flex-col gap-4 w-full h-full min-h-[90%-2px] max-h-[90%-2px] overflow-y-scroll"
>
{messages.map((x, idx) => (
<ChatMessage
message={x.message}
role={x.role}
key={`message=${idx}`}
/>
))}
{loading ? <p> Waiting for response... </p> : null}
</div>
<form
className="w-full flex flex-row gap-2 self-end "
onSubmit={(e) => handleSubmit(e)}
>
<input
className="w-full px-4 py-2 text-black"
name="message"
type="text"
value={message}
onInput={(e) => setMessage((e.target as HTMLInputElement).value)}
required
></input>
<button type="submit" className="bg-slate-300 text-black px-4 py-2">
Send
</button>
</form>
</div>
</main>
);
}
Binary file added axum/openai/frontend/app/favicon.ico
Binary file not shown.
Binary file added axum/openai/frontend/app/fonts/GeistMonoVF.woff
Binary file not shown.
Binary file added axum/openai/frontend/app/fonts/GeistVF.woff
Binary file not shown.
21 changes: 21 additions & 0 deletions axum/openai/frontend/app/globals.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
@tailwind base;
@tailwind components;
@tailwind utilities;

:root {
--background: #ffffff;
--foreground: #171717;
}

@media (prefers-color-scheme: dark) {
:root {
--background: #0a0a0a;
--foreground: #ededed;
}
}

body {
color: var(--foreground);
background: var(--background);
font-family: Arial, Helvetica, sans-serif;
}
35 changes: 35 additions & 0 deletions axum/openai/frontend/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
import type { Metadata } from "next";
import localFont from "next/font/local";
import "./globals.css";

const geistSans = localFont({
src: "./fonts/GeistVF.woff",
variable: "--font-geist-sans",
weight: "100 900",
});
const geistMono = localFont({
src: "./fonts/GeistMonoVF.woff",
variable: "--font-geist-mono",
weight: "100 900",
});

export const metadata: Metadata = {
title: "Create Next App",
description: "Generated by create next app",
};

export default function RootLayout({
children,
}: Readonly<{
children: React.ReactNode;
}>) {
return (
<html lang="en">
<body
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
>
{children}
</body>
</html>
);
}
Loading