Skip to content

Commit 9ad12f0

Browse files
feat: update openai template (#193)
* feat: update openai template * chore: satisfy ci * docs: update README
1 parent 762e240 commit 9ad12f0

39 files changed

+7666
-49
lines changed

axum/openai/.gitignore

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
/target
2+
.shuttle*
3+
Secrets*.toml

axum/openai/Cargo.toml

+17-4
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,22 @@ edition = "2021"
55

66
[dependencies]
77
async-openai = "0.23.0"
8-
axum = "0.7.3"
9-
serde_json = "1"
8+
argon2 = "0.5.3"
9+
axum = "0.7.4"
10+
axum-extra = { version = "0.9.4", features = ["cookie", "cookie-private"] }
11+
derive_more = { version = "1.0.0", features = ["full"] }
12+
jsonwebtoken = "9.3.0"
13+
serde = { version = "1.0.215", features = ["derive"] }
14+
serde_json = "1.0.133"
15+
16+
sqlx = { version = "0.8.2", features = [
17+
"runtime-tokio-rustls",
18+
"postgres",
19+
"macros",
20+
] }
21+
tokio = { version = "1.28.2", features = ["macros", "rt-multi-thread"] }
22+
tower-http = { version = "0.6.2", features = ["cors", "fs"] }
23+
shuttle-runtime = "0.49.0"
1024
shuttle-axum = "0.49.0"
25+
shuttle-shared-db = { version = "0.49.0", features = ["postgres"] }
1126
shuttle-openai = "0.49.0"
12-
shuttle-runtime = "0.49.0"
13-
tokio = "1.26.0"

axum/openai/README.md

+17-5
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,19 @@
1-
A simple endpoint that sends a chat message to ChatGPT and returns the response.
1+
## Shuttle AI Playground
2+
This template enables you to spin up an AI playground in the style of OpenAI.
23

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

5-
```sh
6-
curl http://localhost:8000 -H 'content-type: application/json' --data '{"message":"What is shuttle.rs?"}'
7-
```
10+
## How to use
11+
Before using this, you will need an OpenAI API key.
12+
13+
1) Ensure the OpenAI API key is in your `Secrets.toml` file (see file for syntax if not sure how to use).
14+
2) Run `npm --prefix frontend install && npm --prefix frontend run build` to build the Next.js frontend.
15+
3) Use `shuttle run` to run the template locally - or use `shuttle deploy` to deploy!
16+
17+
## Troubleshooting
18+
- The default port is at 8000. If you are already running something here, you can use `--port` to select a different port.
19+
- 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).

axum/openai/Shuttle.toml

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
[build]
2+
assets = ["frontend/dist/*"]
3+
4+
[deploy]
5+
include = ["frontend/dist/*"]

axum/openai/frontend/.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"extends": ["next/core-web-vitals", "next/typescript"]
3+
}

axum/openai/frontend/.gitignore

+43
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2+
3+
# dependencies
4+
/node_modules
5+
/.pnp
6+
.pnp.*
7+
.yarn/*
8+
!.yarn/patches
9+
!.yarn/plugins
10+
!.yarn/releases
11+
!.yarn/versions
12+
13+
# testing
14+
/coverage
15+
16+
# next.js
17+
/.next/
18+
/out/
19+
20+
# production
21+
/build
22+
23+
# misc
24+
.DS_Store
25+
*.pem
26+
27+
# debug
28+
npm-debug.log*
29+
yarn-debug.log*
30+
yarn-error.log*
31+
32+
# env files (can opt-in for committing if needed)
33+
.env*
34+
35+
# vercel
36+
.vercel
37+
38+
# typescript
39+
*.tsbuildinfo
40+
next-env.d.ts
41+
42+
# build
43+
dist

axum/openai/frontend/app/app/page.tsx

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
"use client";
2+
import { ChatMessage } from "@/components/ChatMessage";
3+
import { ConversationButton } from "@/components/ConversationButton";
4+
import { useRouter } from "next/navigation";
5+
import { useEffect, useState } from "react";
6+
7+
export interface GPTMessage {
8+
role: string;
9+
message: string;
10+
}
11+
12+
export default function AppPage() {
13+
const router = useRouter();
14+
const [model, setModel] = useState<string>("gpt-4o");
15+
const [message, setMessage] = useState<string>("");
16+
const [conversationId, setConversationId] = useState<number>(0);
17+
const [conversationList, setConversationList] = useState<number[]>([]);
18+
const [messages, setMessages] = useState<GPTMessage[]>([]);
19+
const [loading, setLoading] = useState<boolean>(false);
20+
21+
function newChat() {
22+
setConversationId(0);
23+
setMessages([]);
24+
}
25+
26+
async function handleSubmit(e: React.FormEvent<HTMLFormElement>) {
27+
e.preventDefault();
28+
29+
if (conversationId === 0) {
30+
const res = await fetch(`/api/chat/create`, {
31+
method: "POST",
32+
headers: {
33+
"content-type": "application/json",
34+
},
35+
body: JSON.stringify({
36+
prompt: message,
37+
model: model,
38+
}),
39+
});
40+
41+
const data = await res.json();
42+
43+
const new_conversation_list = conversationList;
44+
new_conversation_list.unshift(data.conversation_id);
45+
console.log(data.conversation_id);
46+
setConversationId(data.conversation_id);
47+
setConversationList(new_conversation_list);
48+
}
49+
50+
setMessages((prev) => {
51+
return [...prev, { message: message, role: "user" }];
52+
});
53+
setMessage("");
54+
setLoading(true);
55+
56+
const conversation_res = await fetch(
57+
`/api/chat/conversations/${conversationId}`,
58+
{
59+
method: "POST",
60+
headers: {
61+
"content-type": "application/json",
62+
},
63+
body: JSON.stringify({
64+
prompt: message,
65+
model: model,
66+
}),
67+
},
68+
);
69+
70+
const promptReqData = await conversation_res.json();
71+
72+
setMessages((prev) => {
73+
return [...prev, { message: promptReqData.response, role: "system" }];
74+
});
75+
setLoading(false);
76+
}
77+
78+
useEffect(() => {
79+
const fetchData = async () => {
80+
const res = await fetch("/api/chat/conversations");
81+
if (res.ok) {
82+
const data = await res.json();
83+
setConversationList(data);
84+
} else {
85+
router.replace("/login");
86+
}
87+
};
88+
89+
fetchData();
90+
}, []);
91+
92+
return (
93+
<main className="grid grid-cols-6 grid-rows-1 w-full h-full min-h-screen">
94+
<div
95+
id="sidebar"
96+
className="col-span-1 row-span-1 border-r border-[#333]"
97+
>
98+
<h1>Shuttle</h1>
99+
<div className="p-4">
100+
<button
101+
className="px-4 py-1 w-full text-left rounded-md bg-gradient-to-r from-orange-700 to-yellow-400"
102+
onClick={() => newChat()}
103+
>
104+
New chat
105+
</button>
106+
<h2 className="text-center font-bold mt-4">Conversations</h2>
107+
<div className="py-4 flex flex-col items-start gap-2">
108+
{conversationList.map((x) => (
109+
<ConversationButton
110+
id={x}
111+
key={x}
112+
setMessages={setMessages}
113+
setConversationId={setConversationId}
114+
active={conversationId === x}
115+
/>
116+
))}
117+
</div>
118+
</div>
119+
</div>
120+
<div className="col-span-5 row-span-1 flex flex-col p-4 w-full gap-4 max-h-screen overflow-auto">
121+
<select
122+
className="font-bold text-slate-300 w-[15%] bg-gray-800 px-4 py-2"
123+
onChange={(e) => setModel((e.target as HTMLSelectElement).value)}
124+
>
125+
<option className="text-black" value="gpt-4o">
126+
gpt-4o
127+
</option>
128+
<option className="text-black" value="gpt-4o-mini">
129+
gpt-4o-mini
130+
</option>
131+
<option className="text-black" value="gpt-4o-preview">
132+
gpt-4o-preview
133+
</option>
134+
</select>
135+
<div
136+
id="chatbox"
137+
className="flex flex-col gap-4 w-full h-full min-h-[90%-2px] max-h-[90%-2px] overflow-y-scroll"
138+
>
139+
{messages.map((x, idx) => (
140+
<ChatMessage
141+
message={x.message}
142+
role={x.role}
143+
key={`message=${idx}`}
144+
/>
145+
))}
146+
{loading ? <p> Waiting for response... </p> : null}
147+
</div>
148+
<form
149+
className="w-full flex flex-row gap-2 self-end "
150+
onSubmit={(e) => handleSubmit(e)}
151+
>
152+
<input
153+
className="w-full px-4 py-2 text-black"
154+
name="message"
155+
type="text"
156+
value={message}
157+
onInput={(e) => setMessage((e.target as HTMLInputElement).value)}
158+
required
159+
></input>
160+
<button type="submit" className="bg-slate-300 text-black px-4 py-2">
161+
Send
162+
</button>
163+
</form>
164+
</div>
165+
</main>
166+
);
167+
}

axum/openai/frontend/app/favicon.ico

25.3 KB
Binary file not shown.
66.3 KB
Binary file not shown.
64.7 KB
Binary file not shown.

axum/openai/frontend/app/globals.css

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
@tailwind base;
2+
@tailwind components;
3+
@tailwind utilities;
4+
5+
:root {
6+
--background: #ffffff;
7+
--foreground: #171717;
8+
}
9+
10+
@media (prefers-color-scheme: dark) {
11+
:root {
12+
--background: #0a0a0a;
13+
--foreground: #ededed;
14+
}
15+
}
16+
17+
body {
18+
color: var(--foreground);
19+
background: var(--background);
20+
font-family: Arial, Helvetica, sans-serif;
21+
}

axum/openai/frontend/app/layout.tsx

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
import type { Metadata } from "next";
2+
import localFont from "next/font/local";
3+
import "./globals.css";
4+
5+
const geistSans = localFont({
6+
src: "./fonts/GeistVF.woff",
7+
variable: "--font-geist-sans",
8+
weight: "100 900",
9+
});
10+
const geistMono = localFont({
11+
src: "./fonts/GeistMonoVF.woff",
12+
variable: "--font-geist-mono",
13+
weight: "100 900",
14+
});
15+
16+
export const metadata: Metadata = {
17+
title: "Create Next App",
18+
description: "Generated by create next app",
19+
};
20+
21+
export default function RootLayout({
22+
children,
23+
}: Readonly<{
24+
children: React.ReactNode;
25+
}>) {
26+
return (
27+
<html lang="en">
28+
<body
29+
className={`${geistSans.variable} ${geistMono.variable} antialiased`}
30+
>
31+
{children}
32+
</body>
33+
</html>
34+
);
35+
}

0 commit comments

Comments
 (0)