zss2341 commited on
Commit
b06ed0c
1 Parent(s): b918a2b

Upload 22 files

Browse files
.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
- title: ChatGPT Turbo Engine Superfast
3
- emoji: 🐢
4
- colorFrom: purple
5
- colorTo: pink
6
- sdk: streamlit
7
- sdk_version: 1.17.0
8
- app_file: app.py
9
- pinned: false
10
- license: artistic-2.0
11
- ---
12
-
13
- Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
+ }