Repository: anse-app/chatgpt-demo
Branch: main
Commit: 03b47a79d901
Files: 58
Total size: 74.1 KB
Directory structure:
gitextract_m59yg5z4/
├── .dockerignore
├── .eslintignore
├── .eslintrc.js
├── .github/
│ ├── ISSUE_TEMPLATE/
│ │ ├── bug_report_when_use.yml
│ │ ├── bus_report_when_deploying.yml
│ │ ├── config.yml
│ │ ├── feature_request.yml
│ │ └── typo.yml
│ ├── PULL_REQUEST_TEMPLATE.md
│ └── workflows/
│ ├── build-docker.yml
│ ├── lint.yml
│ ├── main.yml
│ └── sync.yml
├── .gitignore
├── .npmrc
├── .vscode/
│ ├── extensions.json
│ ├── launch.json
│ └── settings.json
├── Dockerfile
├── LICENSE
├── README.md
├── README.zh-CN.md
├── astro.config.mjs
├── docker-compose.yml
├── hack/
│ ├── docker-entrypoint.sh
│ └── docker-env-replace.sh
├── netlify.toml
├── package.json
├── plugins/
│ └── disableBlocks.ts
├── shims.d.ts
├── src/
│ ├── components/
│ │ ├── ErrorMessageItem.tsx
│ │ ├── Footer.astro
│ │ ├── Generator.tsx
│ │ ├── Header.astro
│ │ ├── Logo.astro
│ │ ├── MessageItem.tsx
│ │ ├── SettingsSlider.tsx
│ │ ├── Slider.tsx
│ │ ├── SystemRoleSettings.tsx
│ │ ├── Themetoggle.astro
│ │ └── icons/
│ │ ├── Clear.tsx
│ │ ├── Env.tsx
│ │ ├── Refresh.tsx
│ │ └── X.tsx
│ ├── env.d.ts
│ ├── layouts/
│ │ └── Layout.astro
│ ├── message.css
│ ├── pages/
│ │ ├── api/
│ │ │ ├── auth.ts
│ │ │ └── generate.ts
│ │ ├── index.astro
│ │ └── password.astro
│ ├── slider.css
│ ├── types.ts
│ └── utils/
│ ├── auth.ts
│ └── openAI.ts
├── tsconfig.json
├── unocss.config.ts
└── vercel.json
================================================
FILE CONTENTS
================================================
================================================
FILE: .dockerignore
================================================
*.md
Dockerfile
docker-compose.yml
LICENSE
netlify.toml
vercel.json
node_modules
.vscode
================================================
FILE: .eslintignore
================================================
dist
public
node_modules
.netlify
.vercel
.github
.changeset
================================================
FILE: .eslintrc.js
================================================
module.exports = {
extends: ['@evan-yang', 'plugin:astro/recommended'],
rules: {
'no-console': ['error', { allow: ['error'] }],
'react/display-name': 'off',
'react-hooks/rules-of-hooks': 'off',
'@typescript-eslint/no-use-before-define': 'off',
},
overrides: [
{
files: ['*.astro'],
parser: 'astro-eslint-parser',
parserOptions: {
parser: '@typescript-eslint/parser',
extraFileExtensions: ['.astro'],
},
rules: {
'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'],
},
},
{
// Define the configuration for `
================================================
FILE: src/components/icons/Clear.tsx
================================================
export default () => {
return (
)
}
================================================
FILE: src/components/icons/Env.tsx
================================================
export default () => {
return (
)
}
================================================
FILE: src/components/icons/Refresh.tsx
================================================
export default () => {
return (
)
}
================================================
FILE: src/components/icons/X.tsx
================================================
export default () => {
return (
)
}
================================================
FILE: src/env.d.ts
================================================
///
interface ImportMetaEnv {
readonly OPENAI_API_KEY: string
readonly HTTPS_PROXY: string
readonly OPENAI_API_BASE_URL: string
readonly HEAD_SCRIPTS: string
readonly PUBLIC_SECRET_KEY: string
readonly SITE_PASSWORD: string
readonly OPENAI_API_MODEL: string
readonly PUBLIC_MAX_HISTORY_MESSAGES: string
}
interface ImportMeta {
readonly env: ImportMetaEnv
}
================================================
FILE: src/layouts/Layout.astro
================================================
---
import { pwaInfo } from 'virtual:pwa-info'
export interface Props {
title: string;
}
const { title } = Astro.props;
---
{title}
{ import.meta.env.HEAD_SCRIPTS && }
{ pwaInfo && }
{ import.meta.env.PROD && pwaInfo && }
================================================
FILE: src/message.css
================================================
.message pre {
background-color: #64748b10;
font-size: 0.8rem;
padding: 0.4rem 1rem;
}
.message .hljs {
background-color: transparent;
}
.message table {
font-size: 0.8em;
}
.message table thead tr {
background-color: #64748b40;
text-align: left;
}
.message table th, .message table td {
padding: 0.6rem 1rem;
}
.message table tbody tr:last-of-type {
border-bottom: 2px solid #64748b40;
}
================================================
FILE: src/pages/api/auth.ts
================================================
import type { APIRoute } from 'astro'
const realPassword = import.meta.env.SITE_PASSWORD || ''
const passList = realPassword.split(',') || []
export const post: APIRoute = async(context) => {
const body = await context.request.json()
const { pass } = body
return new Response(JSON.stringify({
code: (!realPassword || pass === realPassword || passList.includes(pass)) ? 0 : -1,
}))
}
================================================
FILE: src/pages/api/generate.ts
================================================
// #vercel-disable-blocks
import { ProxyAgent, fetch } from 'undici'
// #vercel-end
import { generatePayload, parseOpenAIStream } from '@/utils/openAI'
import { verifySignature } from '@/utils/auth'
import type { APIRoute } from 'astro'
const apiKey = import.meta.env.OPENAI_API_KEY
const httpsProxy = import.meta.env.HTTPS_PROXY
const baseUrl = ((import.meta.env.OPENAI_API_BASE_URL) || 'https://api.openai.com').trim().replace(/\/$/, '')
const sitePassword = import.meta.env.SITE_PASSWORD || ''
const passList = sitePassword.split(',') || []
export const post: APIRoute = async(context) => {
const body = await context.request.json()
const { sign, time, messages, pass, temperature } = body
if (!messages) {
return new Response(JSON.stringify({
error: {
message: 'No input text.',
},
}), { status: 400 })
}
if (sitePassword && !(sitePassword === pass || passList.includes(pass))) {
return new Response(JSON.stringify({
error: {
message: 'Invalid password.',
},
}), { status: 401 })
}
if (import.meta.env.PROD && !await verifySignature({ t: time, m: messages?.[messages.length - 1]?.content || '' }, sign)) {
return new Response(JSON.stringify({
error: {
message: 'Invalid signature.',
},
}), { status: 401 })
}
const initOptions = generatePayload(apiKey, messages, temperature)
// #vercel-disable-blocks
if (httpsProxy)
initOptions.dispatcher = new ProxyAgent(httpsProxy)
// #vercel-end
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-expect-error
const response = await fetch(`${baseUrl}/v1/chat/completions`, initOptions).catch((err: Error) => {
console.error(err)
return new Response(JSON.stringify({
error: {
code: err.name,
message: err.message,
},
}), { status: 500 })
}) as Response
return parseOpenAIStream(response) as Response
}
================================================
FILE: src/pages/index.astro
================================================
---
import Layout from '../layouts/Layout.astro'
import Header from '../components/Header.astro'
import Footer from '../components/Footer.astro'
import Generator from '../components/Generator'
import '../message.css'
import 'katex/dist/katex.min.css'
import 'highlight.js/styles/atom-one-dark.css'
---
================================================
FILE: src/pages/password.astro
================================================
---
import Layout from '../layouts/Layout.astro'
---
Please input password
================================================
FILE: src/slider.css
================================================
/* -----------------------------------------------------------------------------
* Slider
* -----------------------------------------------------------------------------*/
[data-scope='slider'][data-part='root'] {
@apply w-full flex flex-col
}
[data-scope='slider'][data-part='root'][data-orientation='vertical'] {
@apply h-60
}
[data-scope='slider'][data-part='control'] {
--slider-thumb-size: 14px;
--slider-track-height: 4px;
@apply relative fcc cursor-pointer
}
[data-scope='slider'][data-part='control'][data-orientation='horizontal'] {
@apply h-[var(--slider-thumb-size)];
}
[data-scope='slider'][data-part='control'][data-orientation='vertical'] {
@apply w-[var(--slider-thumb-size)];
}
[data-scope='slider'][data-part='control']:hover [data-part='range'] {
@apply bg-gray-400 dark:bg-gray-600
}
[data-scope='slider'][data-part='control']:hover [data-part='thumb'] {
@apply bg-gray-300 dark:bg-gray-400
}
[data-scope='slider'][data-part='thumb'] {
all: unset;
@apply bg-gray-200 dark:bg-gray-500 w-[var(--slider-thumb-size)] h-[var(--slider-thumb-size)] rounded-full b-#c5c5d2 b-2
}
[data-scope='slider'][data-part='thumb'][data-disabled] {
@apply w-0
}
[data-scope='slider'] .control-area {
@apply flex mt-12px
}
.slider [data-orientation='horizontal'] .control-area {
flex-direction: column;
width: 100%;
}
.slider [data-orientation='vertical'] .control-area {
flex-direction: row;
height: 100%;
}
[data-scope='slider'][data-part='track'] {
@apply rounded-full bg-gray-200 dark:bg-neutral-700
}
[data-scope='slider'][data-part='track'][data-orientation='horizontal'] {
@apply h-[var(--slider-track-height)] w-full;
}
[data-scope='slider'][data-part='track'][data-orientation='vertical'] {
@apply h-full w-[var(--slider-track-height)];
}
[data-scope='slider'][data-part='range'] {
@apply bg-neutral-300 dark:bg-gray-700
}
[data-scope='slider'][data-part='range'][data-disabled] {
@apply bg-neutral-300 dark:bg-gray-600
}
[data-scope='slider'][data-part='range'][data-orientation='horizontal'] {
@apply h-full;
}
[data-scope='slider'][data-part='range'][data-orientation='vertical'] {
@apply w-full;
}
[data-scope='slider'][data-part='output'] {
margin-inline-start: 12px;
}
[data-scope='slider'][data-part='marker'] {
color: lightgray;
}
================================================
FILE: src/types.ts
================================================
export interface ChatMessage {
role: 'system' | 'user' | 'assistant'
content: string
}
export interface ErrorMessage {
code: string
message: string
}
================================================
FILE: src/utils/auth.ts
================================================
import { sha256 } from 'js-sha256'
interface AuthPayload {
t: number
m: string
}
async function digestMessage(message: string) {
if (typeof crypto !== 'undefined' && crypto?.subtle?.digest) {
const msgUint8 = new TextEncoder().encode(message)
const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)
const hashArray = Array.from(new Uint8Array(hashBuffer))
return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')
} else {
return sha256(message).toString()
}
}
export const generateSignature = async(payload: AuthPayload) => {
const { t: timestamp, m: lastMessage } = payload
const secretKey = import.meta.env.PUBLIC_SECRET_KEY as string || ''
const signText = `${timestamp}:${lastMessage}:${secretKey}`
// eslint-disable-next-line no-return-await
return await digestMessage(signText)
}
export const verifySignature = async(payload: AuthPayload, sign: string) => {
// if (Math.abs(payload.t - Date.now()) > 1000 * 60 * 5) {
// return false
// }
const payloadSign = await generateSignature(payload)
return payloadSign === sign
}
================================================
FILE: src/utils/openAI.ts
================================================
import { createParser } from 'eventsource-parser'
import type { ParsedEvent, ReconnectInterval } from 'eventsource-parser'
import type { ChatMessage } from '@/types'
export const model = import.meta.env.OPENAI_API_MODEL || 'gpt-3.5-turbo'
export const generatePayload = (
apiKey: string,
messages: ChatMessage[],
temperature: number,
): RequestInit & { dispatcher?: any } => ({
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${apiKey}`,
},
method: 'POST',
body: JSON.stringify({
model,
messages,
temperature,
stream: true,
}),
})
export const parseOpenAIStream = (rawResponse: Response) => {
const encoder = new TextEncoder()
const decoder = new TextDecoder()
if (!rawResponse.ok) {
return new Response(rawResponse.body, {
status: rawResponse.status,
statusText: rawResponse.statusText,
})
}
const stream = new ReadableStream({
async start(controller) {
const streamParser = (event: ParsedEvent | ReconnectInterval) => {
if (event.type === 'event') {
const data = event.data
if (data === '[DONE]') {
controller.close()
return
}
try {
// response = {
// id: 'chatcmpl-6pULPSegWhFgi0XQ1DtgA3zTa1WR6',
// object: 'chat.completion.chunk',
// created: 1677729391,
// model: 'gpt-3.5-turbo-0301',
// choices: [
// { delta: { content: '你' }, index: 0, finish_reason: null }
// ],
// }
const json = JSON.parse(data)
const text = json.choices[0].delta?.content || ''
const queue = encoder.encode(text)
controller.enqueue(queue)
} catch (e) {
controller.error(e)
}
}
}
const parser = createParser(streamParser)
for await (const chunk of rawResponse.body as any)
parser.feed(decoder.decode(chunk))
},
})
return new Response(stream)
}
================================================
FILE: tsconfig.json
================================================
{
"extends": "astro/tsconfigs/base",
"compilerOptions": {
"baseUrl": ".",
"jsx": "preserve",
"jsxImportSource": "solid-js",
"types": ["vite-plugin-pwa/info"],
"paths": {
"@/*": ["src/*"],
},
}
}
================================================
FILE: unocss.config.ts
================================================
import {
defineConfig,
presetAttributify,
presetIcons,
presetTypography,
presetUno,
transformerDirectives,
transformerVariantGroup,
} from 'unocss'
export default defineConfig({
presets: [
presetUno(),
presetAttributify(),
presetIcons({
scale: 1.1,
cdn: 'https://esm.sh/',
}),
presetTypography({
cssExtend: {
'ul,ol': {
'padding-left': '2.25em',
'position': 'relative',
},
},
}),
],
transformers: [transformerVariantGroup(), transformerDirectives()],
shortcuts: [{
'fc': 'flex justify-center',
'fi': 'flex items-center',
'fb': 'flex justify-between',
'fcc': 'fc items-center',
'fie': 'fi justify-end',
'col-fcc': 'flex-col fcc',
'inline-fcc': 'inline-flex items-center justify-center',
'base-focus': 'focus:(bg-op-20 ring-0 outline-none)',
'b-slate-link': 'border-b border-(slate none) hover:border-dashed',
'gpt-title': 'text-2xl font-extrabold mr-1',
'gpt-subtitle': 'text-(2xl transparent) font-extrabold bg-(clip-text gradient-to-r) from-sky-400 to-emerald-600',
'gpt-copy-btn': 'absolute top-12px right-12px z-3 fcc border b-transparent w-8 h-8 p-2 bg-light-300 dark:bg-dark-300 op-90 cursor-pointer',
'gpt-copy-tips': 'op-0 h-7 bg-black px-2.5 py-1 box-border text-xs c-white fcc rounded absolute z-1 transition duration-600 whitespace-nowrap -top-8',
'gpt-retry-btn': 'fi gap-1 px-2 py-0.5 op-70 border border-slate rounded-md text-sm cursor-pointer hover:bg-slate/10',
'gpt-back-top-btn': 'fcc p-2.5 text-base rounded-md hover:bg-slate/10 fixed bottom-60px right-20px z-10 cursor-pointer transition-colors',
'gpt-back-bottom-btn': 'gpt-back-top-btn bottom-20px transform-rotate-180deg',
'gpt-password-input': 'px-4 py-3 h-12 rounded-sm bg-(slate op-15) base-focus',
'gpt-password-submit': 'fcc h-12 w-12 bg-slate cursor-pointer bg-op-20 hover:bg-op-50',
'gen-slate-btn': 'h-12 px-4 py-2 bg-(slate op-15) hover:bg-op-20 rounded-sm',
'gen-cb-wrapper': 'h-12 my-4 fcc gap-4 bg-(slate op-15) rounded-sm',
'gen-cb-stop': 'px-2 py-0.5 border border-slate rounded-md text-sm op-70 cursor-pointer hover:bg-slate/10',
'gen-text-wrapper': 'my-4 fc gap-2 transition-opacity',
'gen-textarea': 'w-full px-3 py-3 min-h-12 max-h-36 rounded-sm bg-(slate op-15) resize-none base-focus placeholder:op-50 dark:(placeholder:op-30) scroll-pa-8px',
'sys-edit-btn': 'inline-fcc gap-1 text-sm bg-slate/20 px-2 py-1 rounded-md transition-colors cursor-pointer hover:bg-slate/50',
'stick-btn-on': '!bg-$c-fg text-$c-bg hover:op-80',
}],
})
================================================
FILE: vercel.json
================================================
{
"buildCommand": "OUTPUT=vercel astro build"
}