Spaces:
Configuration error
Configuration error
Upload 22 files
Browse files- .env.example +1 -0
- .gitignore +25 -0
- .npmrc +1 -0
- README.md +27 -13
- astro.config.mjs +23 -0
- package.json +32 -0
- pnpm-lock.yaml +0 -0
- public/favicon.svg +13 -0
- shims.d.ts +16 -0
- src/components/Footer.astro +18 -0
- src/components/Generator.tsx +116 -0
- src/components/Header.astro +12 -0
- src/components/Logo.astro +1 -0
- src/components/MessageItem.tsx +35 -0
- src/components/icons/Clear.tsx +5 -0
- src/env.d.ts +1 -0
- src/layouts/Layout.astro +34 -0
- src/message.css +26 -0
- src/pages/api/generate.ts +67 -0
- src/pages/index.astro +17 -0
- src/types.ts +4 -0
- tsconfig.json +7 -0
.env.example
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
OPENAI_API_KEY=
|
.gitignore
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# build output
|
2 |
+
dist/
|
3 |
+
.vercel/
|
4 |
+
|
5 |
+
# generated types
|
6 |
+
.astro/
|
7 |
+
|
8 |
+
# dependencies
|
9 |
+
node_modules/
|
10 |
+
|
11 |
+
# logs
|
12 |
+
npm-debug.log*
|
13 |
+
yarn-debug.log*
|
14 |
+
yarn-error.log*
|
15 |
+
pnpm-debug.log*
|
16 |
+
|
17 |
+
# environment variables
|
18 |
+
.env
|
19 |
+
.env.production
|
20 |
+
|
21 |
+
# macOS-specific files
|
22 |
+
.DS_Store
|
23 |
+
|
24 |
+
# Local
|
25 |
+
*.local
|
.npmrc
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
registry=https://registry.npmjs.org/
|
README.md
CHANGED
@@ -1,13 +1,27 @@
|
|
1 |
-
|
2 |
-
|
3 |
-
|
4 |
-
|
5 |
-
|
6 |
-
|
7 |
-
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# ChatGPT-API Demo
|
2 |
+
|
3 |
+
A demo repo based on [OpenAI GPT-3.5 Turbo API](https://platform.openai.com/docs/guides/chat).
|
4 |
+
|
5 |
+
## How to build
|
6 |
+
|
7 |
+
1. Setup & Install dependencies
|
8 |
+
|
9 |
+
> First, you need [Node.js](https://nodejs.org/) (v18+) installed.
|
10 |
+
|
11 |
+
```shell
|
12 |
+
npm i
|
13 |
+
```
|
14 |
+
|
15 |
+
2. Make a copy of `.env.example`, then rename it to `.env`
|
16 |
+
3. Add your [OpenAI API key](https://platform.openai.com/account/api-keys) to `.env`
|
17 |
+
```
|
18 |
+
OPENAI_API_KEY=sk-xxx...
|
19 |
+
```
|
20 |
+
4. Run the app
|
21 |
+
```shell
|
22 |
+
npm run dev
|
23 |
+
```
|
24 |
+
|
25 |
+
## License
|
26 |
+
|
27 |
+
MIT
|
astro.config.mjs
ADDED
@@ -0,0 +1,23 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { defineConfig } from 'astro/config'
|
2 |
+
import vercel from '@astrojs/vercel/edge'
|
3 |
+
import unocss from 'unocss/astro'
|
4 |
+
import { presetUno } from 'unocss'
|
5 |
+
import presetAttributify from '@unocss/preset-attributify'
|
6 |
+
import presetTypography from '@unocss/preset-typography'
|
7 |
+
import solidJs from '@astrojs/solid-js'
|
8 |
+
|
9 |
+
// https://astro.build/config
|
10 |
+
export default defineConfig({
|
11 |
+
integrations: [
|
12 |
+
unocss({
|
13 |
+
presets: [
|
14 |
+
presetAttributify(),
|
15 |
+
presetUno(),
|
16 |
+
presetTypography(),
|
17 |
+
]
|
18 |
+
}),
|
19 |
+
solidJs()
|
20 |
+
],
|
21 |
+
output: 'server',
|
22 |
+
adapter: vercel()
|
23 |
+
});
|
package.json
ADDED
@@ -0,0 +1,32 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"name": "novel-gpt",
|
3 |
+
"type": "module",
|
4 |
+
"version": "0.0.1",
|
5 |
+
"scripts": {
|
6 |
+
"dev": "astro dev",
|
7 |
+
"start": "astro dev",
|
8 |
+
"build": "astro build",
|
9 |
+
"preview": "astro preview",
|
10 |
+
"astro": "astro"
|
11 |
+
},
|
12 |
+
"dependencies": {
|
13 |
+
"@astrojs/solid-js": "^2.0.2",
|
14 |
+
"@astrojs/vercel": "^3.1.3",
|
15 |
+
"@unocss/reset": "^0.50.1",
|
16 |
+
"astro": "^2.0.15",
|
17 |
+
"eventsource-parser": "^0.1.0",
|
18 |
+
"highlight.js": "^11.7.0",
|
19 |
+
"katex": "^0.6.0",
|
20 |
+
"markdown-it": "^13.0.1",
|
21 |
+
"markdown-it-highlightjs": "^4.0.1",
|
22 |
+
"markdown-it-katex": "^2.0.3",
|
23 |
+
"solid-js": "^1.6.11"
|
24 |
+
},
|
25 |
+
"devDependencies": {
|
26 |
+
"@types/markdown-it": "^12.2.3",
|
27 |
+
"@unocss/preset-attributify": "^0.50.1",
|
28 |
+
"@unocss/preset-typography": "^0.50.3",
|
29 |
+
"punycode": "^2.3.0",
|
30 |
+
"unocss": "^0.50.1"
|
31 |
+
}
|
32 |
+
}
|
pnpm-lock.yaml
ADDED
The diff for this file is too large to render.
See raw diff
|
|
public/favicon.svg
ADDED
shims.d.ts
ADDED
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { AttributifyAttributes } from '@unocss/preset-attributify'
|
2 |
+
|
3 |
+
// declare module 'solid-js' {
|
4 |
+
// namespace JSX {
|
5 |
+
// interface HTMLAttributes<T> extends AttributifyAttributes {}
|
6 |
+
// }
|
7 |
+
// }
|
8 |
+
|
9 |
+
declare global {
|
10 |
+
namespace astroHTML.JSX {
|
11 |
+
interface HTMLAttributes extends AttributifyAttributes { }
|
12 |
+
}
|
13 |
+
namespace JSX {
|
14 |
+
interface HTMLAttributes<T> extends AttributifyAttributes {}
|
15 |
+
}
|
16 |
+
}
|
src/components/Footer.astro
ADDED
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
<footer op-60>
|
2 |
+
<p mt-6 text-sm text-slate>
|
3 |
+
<span pr-1>Made by</span>
|
4 |
+
<a
|
5 |
+
border-b border-slate border-none hover:border-dashed
|
6 |
+
href="https://ddiu.io" target="_blank"
|
7 |
+
>
|
8 |
+
Diu
|
9 |
+
</a>
|
10 |
+
<span px-1>|</span>
|
11 |
+
<a
|
12 |
+
border-b border-slate border-none hover:border-dashed
|
13 |
+
href="https://github.com/ddiu8081/chatgpt-demo" target="_blank"
|
14 |
+
>
|
15 |
+
Source Code
|
16 |
+
</a>
|
17 |
+
</p>
|
18 |
+
</footer>
|
src/components/Generator.tsx
ADDED
@@ -0,0 +1,116 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import { createSignal, For, Show } from 'solid-js'
|
2 |
+
import MessageItem from './MessageItem'
|
3 |
+
import IconClear from './icons/Clear'
|
4 |
+
import type { ChatMessage } from '../types'
|
5 |
+
|
6 |
+
export default () => {
|
7 |
+
let inputRef: HTMLInputElement
|
8 |
+
const [messageList, setMessageList] = createSignal<ChatMessage[]>([])
|
9 |
+
const [currentAssistantMessage, setCurrentAssistantMessage] = createSignal('')
|
10 |
+
const [loading, setLoading] = createSignal(false)
|
11 |
+
|
12 |
+
const handleButtonClick = async () => {
|
13 |
+
const inputValue = inputRef.value
|
14 |
+
if (!inputValue) {
|
15 |
+
return
|
16 |
+
}
|
17 |
+
setLoading(true)
|
18 |
+
// @ts-ignore
|
19 |
+
if (window?.umami) umami.trackEvent('chat_generate')
|
20 |
+
inputRef.value = ''
|
21 |
+
setMessageList([
|
22 |
+
...messageList(),
|
23 |
+
{
|
24 |
+
role: 'user',
|
25 |
+
content: inputValue,
|
26 |
+
},
|
27 |
+
])
|
28 |
+
|
29 |
+
const response = await fetch('/api/generate', {
|
30 |
+
method: 'POST',
|
31 |
+
body: JSON.stringify({
|
32 |
+
messages: messageList(),
|
33 |
+
}),
|
34 |
+
})
|
35 |
+
if (!response.ok) {
|
36 |
+
throw new Error(response.statusText)
|
37 |
+
}
|
38 |
+
const data = response.body
|
39 |
+
if (!data) {
|
40 |
+
throw new Error('No data')
|
41 |
+
}
|
42 |
+
const reader = data.getReader()
|
43 |
+
const decoder = new TextDecoder('utf-8')
|
44 |
+
let done = false
|
45 |
+
|
46 |
+
while (!done) {
|
47 |
+
const { value, done: readerDone } = await reader.read()
|
48 |
+
if (value) {
|
49 |
+
let char = decoder.decode(value)
|
50 |
+
if (char === '\n' && currentAssistantMessage().endsWith('\n')) {
|
51 |
+
continue
|
52 |
+
}
|
53 |
+
if (char) {
|
54 |
+
setCurrentAssistantMessage(currentAssistantMessage() + char)
|
55 |
+
}
|
56 |
+
}
|
57 |
+
done = readerDone
|
58 |
+
}
|
59 |
+
setMessageList([
|
60 |
+
...messageList(),
|
61 |
+
{
|
62 |
+
role: 'assistant',
|
63 |
+
content: currentAssistantMessage(),
|
64 |
+
},
|
65 |
+
])
|
66 |
+
setCurrentAssistantMessage('')
|
67 |
+
setLoading(false)
|
68 |
+
}
|
69 |
+
|
70 |
+
const clear = () => {
|
71 |
+
inputRef.value = ''
|
72 |
+
setMessageList([])
|
73 |
+
setCurrentAssistantMessage('')
|
74 |
+
}
|
75 |
+
|
76 |
+
return (
|
77 |
+
<div my-6>
|
78 |
+
<For each={messageList()}>{(message) => <MessageItem role={message.role} message={message.content} />}</For>
|
79 |
+
{ currentAssistantMessage() && <MessageItem role="assistant" message={currentAssistantMessage} /> }
|
80 |
+
<Show when={!loading()} fallback={() => <div class="h-12 my-4 flex items-center justify-center bg-slate bg-op-15 text-slate rounded-sm">AI is thinking...</div>}>
|
81 |
+
<div class="my-4 flex items-center gap-2">
|
82 |
+
<input
|
83 |
+
ref={inputRef!}
|
84 |
+
type="text"
|
85 |
+
id="input"
|
86 |
+
placeholder="Enter something..."
|
87 |
+
autocomplete='off'
|
88 |
+
autofocus
|
89 |
+
disabled={loading()}
|
90 |
+
onKeyDown={(e) => {
|
91 |
+
e.key === 'Enter' && !e.isComposing && handleButtonClick()
|
92 |
+
}}
|
93 |
+
w-full
|
94 |
+
px-4
|
95 |
+
h-12
|
96 |
+
text-slate
|
97 |
+
rounded-sm
|
98 |
+
bg-slate
|
99 |
+
bg-op-15
|
100 |
+
focus:bg-op-20
|
101 |
+
focus:ring-0
|
102 |
+
focus:outline-none
|
103 |
+
placeholder:text-slate-400
|
104 |
+
placeholder:op-30
|
105 |
+
/>
|
106 |
+
<button onClick={handleButtonClick} disabled={loading()} h-12 px-4 py-2 bg-slate bg-op-15 hover:bg-op-20 text-slate rounded-sm>
|
107 |
+
Send
|
108 |
+
</button>
|
109 |
+
<button title='Clear' onClick={clear} disabled={loading()} h-12 px-4 py-2 bg-slate bg-op-15 hover:bg-op-20 text-slate rounded-sm>
|
110 |
+
<IconClear />
|
111 |
+
</button>
|
112 |
+
</div>
|
113 |
+
</Show>
|
114 |
+
</div>
|
115 |
+
)
|
116 |
+
}
|
src/components/Header.astro
ADDED
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
import Logo from './Logo.astro'
|
3 |
+
---
|
4 |
+
|
5 |
+
<header>
|
6 |
+
<Logo />
|
7 |
+
<div class="flex items-center mt-2">
|
8 |
+
<span class="text-2xl text-slate font-extrabold mr-1">ChatGPT</span>
|
9 |
+
<span class="text-2xl text-transparent font-extrabold bg-clip-text bg-gradient-to-r from-sky-400 to-emerald-600">Demo</span>
|
10 |
+
</div>
|
11 |
+
<p mt-1 text-slate op-60>Based on OpenAI API (gpt-3.5-turbo).</p>
|
12 |
+
</header>
|
src/components/Logo.astro
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 32 32"><g fill="none"><path fill="#F8312F" d="M5 3.5a1.5 1.5 0 0 1-1 1.415V12l2.16 5.487L4 23c-1.1 0-2-.9-2-1.998v-7.004a2 2 0 0 1 1-1.728V4.915A1.5 1.5 0 1 1 5 3.5Zm25.05.05c0 .681-.44 1.26-1.05 1.468V12.2c.597.347 1 .994 1 1.73v7.01c0 1.1-.9 2-2 2l-2.94-5.68L28 11.93V5.018a1.55 1.55 0 1 1 2.05-1.468Z"/><path fill="#FFB02E" d="M11 4.5A1.5 1.5 0 0 1 12.5 3h7a1.5 1.5 0 0 1 .43 2.938c-.277.082-.57.104-.847.186l-3.053.904l-3.12-.908c-.272-.08-.56-.1-.832-.18A1.5 1.5 0 0 1 11 4.5Z"/><path fill="#CDC4D6" d="M22.05 30H9.95C6.66 30 4 27.34 4 24.05V12.03C4 8.7 6.7 6 10.03 6h11.95C25.3 6 28 8.7 28 12.03v12.03c0 3.28-2.66 5.94-5.95 5.94Z"/><path fill="#212121" d="M9.247 18.5h13.506c2.33 0 4.247-1.919 4.247-4.25A4.257 4.257 0 0 0 22.753 10H9.247A4.257 4.257 0 0 0 5 14.25a4.257 4.257 0 0 0 4.247 4.25Zm4.225 7.5h5.056C19.34 26 20 25.326 20 24.5s-.66-1.5-1.472-1.5h-5.056C12.66 23 12 23.674 12 24.5s.66 1.5 1.472 1.5Z"/><path fill="#00A6ED" d="M10.25 12C9.56 12 9 12.56 9 13.25v2.5a1.25 1.25 0 1 0 2.5 0v-2.5c0-.69-.56-1.25-1.25-1.25Zm11.5 0c-.69 0-1.25.56-1.25 1.25v2.5a1.25 1.25 0 1 0 2.5 0v-2.5c0-.69-.56-1.25-1.25-1.25Z"/></g></svg>
|
src/components/MessageItem.tsx
ADDED
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { Accessor } from 'solid-js'
|
2 |
+
import type { ChatMessage } from '../types'
|
3 |
+
import MarkdownIt from 'markdown-it'
|
4 |
+
// @ts-ignore
|
5 |
+
import mdKatex from 'markdown-it-katex'
|
6 |
+
import mdHighlight from 'markdown-it-highlightjs'
|
7 |
+
|
8 |
+
interface Props {
|
9 |
+
role: ChatMessage['role']
|
10 |
+
message: Accessor<string> | string
|
11 |
+
}
|
12 |
+
|
13 |
+
export default ({ role, message }: Props) => {
|
14 |
+
const roleClass = {
|
15 |
+
system: 'bg-gradient-to-r from-gray-300 via-gray-200 to-gray-300',
|
16 |
+
user: 'bg-gradient-to-r from-purple-400 to-yellow-400',
|
17 |
+
assistant: 'bg-gradient-to-r from-yellow-200 via-green-200 to-green-300',
|
18 |
+
}
|
19 |
+
const htmlString = () => {
|
20 |
+
const md = MarkdownIt().use(mdKatex).use(mdHighlight)
|
21 |
+
|
22 |
+
if (typeof message === 'function') {
|
23 |
+
return md.render(message())
|
24 |
+
} else if (typeof message === 'string') {
|
25 |
+
return md.render(message)
|
26 |
+
}
|
27 |
+
return ''
|
28 |
+
}
|
29 |
+
return (
|
30 |
+
<div class="flex py-2 gap-3 -mx-4 px-4 rounded-lg transition-colors md:hover:bg-slate/3" class:op-75={ role === 'user' }>
|
31 |
+
<div class={ `shrink-0 w-7 h-7 mt-4 rounded-full op-80 ${ roleClass[role] }` }></div>
|
32 |
+
<div class="message prose text-slate break-words overflow-hidden" innerHTML={htmlString()} />
|
33 |
+
</div>
|
34 |
+
)
|
35 |
+
}
|
src/components/icons/Clear.tsx
ADDED
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export default () => {
|
2 |
+
return (
|
3 |
+
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 24 24"><path fill="currentColor" d="M8 20v-5h2v5h9v-7H5v7h3zm-4-9h16V8h-6V4h-4v4H4v3zM3 21v-8H2V7a1 1 0 0 1 1-1h5V3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v3h5a1 1 0 0 1 1 1v6h-1v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z"></path></svg>
|
4 |
+
)
|
5 |
+
}
|
src/env.d.ts
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
/// <reference types="astro/client" />
|
src/layouts/Layout.astro
ADDED
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
export interface Props {
|
3 |
+
title: string;
|
4 |
+
}
|
5 |
+
|
6 |
+
const { title } = Astro.props;
|
7 |
+
---
|
8 |
+
|
9 |
+
<!DOCTYPE html>
|
10 |
+
<html lang="zh-CN">
|
11 |
+
<head>
|
12 |
+
<meta charset="UTF-8" />
|
13 |
+
<meta name="viewport" content="width=device-width" />
|
14 |
+
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
15 |
+
<meta name="generator" content={Astro.generator} />
|
16 |
+
<title>{title}</title>
|
17 |
+
<script async defer data-website-id="918699fd-0704-408b-bda3-3f28c1bd9d1b" src="https://stats.ddiu.io/app.js"></script>
|
18 |
+
</head>
|
19 |
+
<body>
|
20 |
+
<slot />
|
21 |
+
</body>
|
22 |
+
</html>
|
23 |
+
<style is:global>
|
24 |
+
html {
|
25 |
+
font-family: system-ui, sans-serif;
|
26 |
+
background-color: #171921;
|
27 |
+
color: #ffffff;
|
28 |
+
}
|
29 |
+
main {
|
30 |
+
max-width: 70ch;
|
31 |
+
margin: 0 auto;
|
32 |
+
padding: 6rem 2rem 4rem;
|
33 |
+
}
|
34 |
+
</style>
|
src/message.css
ADDED
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
.message pre {
|
2 |
+
background-color: #64748b10;
|
3 |
+
font-size: 0.8rem;
|
4 |
+
padding: 0.4rem 1rem;
|
5 |
+
}
|
6 |
+
|
7 |
+
.message .hljs {
|
8 |
+
background-color: transparent;
|
9 |
+
}
|
10 |
+
|
11 |
+
.message table {
|
12 |
+
font-size: 0.8em;
|
13 |
+
}
|
14 |
+
|
15 |
+
.message table thead tr {
|
16 |
+
background-color: #64748b40;
|
17 |
+
text-align: left;
|
18 |
+
}
|
19 |
+
|
20 |
+
.message table th, .message table td {
|
21 |
+
padding: 0.6rem 1rem;
|
22 |
+
}
|
23 |
+
|
24 |
+
.message table tbody tr:last-of-type {
|
25 |
+
border-bottom: 2px solid #64748b40;
|
26 |
+
}
|
src/pages/api/generate.ts
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import type { APIRoute } from 'astro'
|
2 |
+
import { createParser, ParsedEvent, ReconnectInterval } from 'eventsource-parser'
|
3 |
+
|
4 |
+
const apiKey = import.meta.env.OPENAI_API_KEY
|
5 |
+
|
6 |
+
export const post: APIRoute = async (context) => {
|
7 |
+
const body = await context.request.json()
|
8 |
+
const messages = body.messages
|
9 |
+
const encoder = new TextEncoder()
|
10 |
+
const decoder = new TextDecoder()
|
11 |
+
|
12 |
+
if (!messages) {
|
13 |
+
return new Response('No input text')
|
14 |
+
}
|
15 |
+
|
16 |
+
const completion = await fetch('https://api.openai.com/v1/chat/completions', {
|
17 |
+
headers: {
|
18 |
+
'Content-Type': 'application/json',
|
19 |
+
Authorization: `Bearer ${apiKey}`,
|
20 |
+
},
|
21 |
+
method: 'POST',
|
22 |
+
body: JSON.stringify({
|
23 |
+
model: 'gpt-3.5-turbo',
|
24 |
+
messages,
|
25 |
+
temperature: 0.6,
|
26 |
+
stream: true,
|
27 |
+
}),
|
28 |
+
})
|
29 |
+
|
30 |
+
const stream = new ReadableStream({
|
31 |
+
async start(controller) {
|
32 |
+
const streamParser = (event: ParsedEvent | ReconnectInterval) => {
|
33 |
+
if (event.type === 'event') {
|
34 |
+
const data = event.data
|
35 |
+
if (data === '[DONE]') {
|
36 |
+
controller.close()
|
37 |
+
return
|
38 |
+
}
|
39 |
+
try {
|
40 |
+
// response = {
|
41 |
+
// id: 'chatcmpl-6pULPSegWhFgi0XQ1DtgA3zTa1WR6',
|
42 |
+
// object: 'chat.completion.chunk',
|
43 |
+
// created: 1677729391,
|
44 |
+
// model: 'gpt-3.5-turbo-0301',
|
45 |
+
// choices: [
|
46 |
+
// { delta: { content: '你' }, index: 0, finish_reason: null }
|
47 |
+
// ],
|
48 |
+
// }
|
49 |
+
const json = JSON.parse(data)
|
50 |
+
const text = json.choices[0].delta?.content
|
51 |
+
const queue = encoder.encode(text)
|
52 |
+
controller.enqueue(queue)
|
53 |
+
} catch (e) {
|
54 |
+
controller.error(e)
|
55 |
+
}
|
56 |
+
}
|
57 |
+
}
|
58 |
+
|
59 |
+
const parser = createParser(streamParser)
|
60 |
+
for await (const chunk of completion.body as any) {
|
61 |
+
parser.feed(decoder.decode(chunk))
|
62 |
+
}
|
63 |
+
},
|
64 |
+
})
|
65 |
+
|
66 |
+
return new Response(stream)
|
67 |
+
}
|
src/pages/index.astro
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
---
|
2 |
+
import Layout from '../layouts/Layout.astro'
|
3 |
+
import Header from '../components/Header.astro'
|
4 |
+
import Footer from '../components/Footer.astro'
|
5 |
+
import Generator from '../components/Generator'
|
6 |
+
import '../message.css'
|
7 |
+
import 'katex/dist/katex.min.css'
|
8 |
+
import 'highlight.js/styles/atom-one-dark.css'
|
9 |
+
---
|
10 |
+
|
11 |
+
<Layout title="ChatGPT API Demo">
|
12 |
+
<main>
|
13 |
+
<Header />
|
14 |
+
<Generator client:load />
|
15 |
+
<Footer />
|
16 |
+
</main>
|
17 |
+
</Layout>
|
src/types.ts
ADDED
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
1 |
+
export interface ChatMessage {
|
2 |
+
role: 'system' | 'user' | 'assistant'
|
3 |
+
content: string
|
4 |
+
}
|
tsconfig.json
ADDED
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{
|
2 |
+
"extends": "astro/tsconfigs/strict",
|
3 |
+
"compilerOptions": {
|
4 |
+
"jsx": "preserve",
|
5 |
+
"jsxImportSource": "solid-js"
|
6 |
+
}
|
7 |
+
}
|