├── vercel.json ├── public ├── pwa-192.png ├── pwa-512.png ├── apple-touch-icon.png └── icon.svg ├── .eslintignore ├── .npmrc ├── .dockerignore ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── src ├── types.ts ├── components │ ├── icons │ │ ├── Clear.tsx │ │ ├── X.tsx │ │ ├── Env.tsx │ │ └── Refresh.tsx │ ├── Footer.astro │ ├── Header.astro │ ├── ErrorMessageItem.tsx │ ├── Logo.astro │ ├── SystemRoleSettings.tsx │ ├── MessageItem.tsx │ ├── Themetoggle.astro │ └── Generator.tsx ├── env.d.ts ├── pages │ ├── api │ │ ├── auth.ts │ │ └── generate.ts │ ├── index.astro │ └── password.astro ├── message.css ├── utils │ ├── auth.ts │ └── openAI.ts └── layouts │ └── Layout.astro ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── typo.yml │ ├── feature_request.yml │ ├── bus_report_when_deploying.yml │ └── bug_report_when_use.yml ├── workflows │ ├── lint.yml │ ├── build-docker.yml │ ├── main.yml │ └── sync.yml └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── netlify.toml ├── .gitignore ├── shims.d.ts ├── Dockerfile ├── hack ├── docker-entrypoint.sh └── docker-env-replace.sh ├── docker-compose.yml ├── .env.example ├── plugins └── disableBlocks.ts ├── .eslintrc.js ├── LICENSE ├── package.json ├── astro.config.mjs ├── unocss.config.ts ├── README.zh-CN.md └── README.md /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "OUTPUT=vercel astro build" 3 | } -------------------------------------------------------------------------------- /public/pwa-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijinglei/chatgpt-demo/main/public/pwa-192.png -------------------------------------------------------------------------------- /public/pwa-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijinglei/chatgpt-demo/main/public/pwa-512.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | public 3 | node_modules 4 | .netlify 5 | .vercel 6 | .github 7 | .changeset 8 | -------------------------------------------------------------------------------- /public/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sijinglei/chatgpt-demo/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | strict-peer-dependencies=false 3 | auto-install-peers=true 4 | shamefully-hoist=true 5 | -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md 2 | Dockerfile 3 | docker-compose.yml 4 | LICENSE 5 | netlify.toml 6 | vercel.json 7 | node_modules 8 | .vscode 9 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode","dbaeumer.vscode-eslint","antfu.unocss"], 3 | "unwantedRecommendations": [], 4 | } 5 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ChatMessage { 2 | role: 'system' | 'user' | 'assistant' 3 | content: string 4 | } 5 | 6 | export interface ErrorMessage { 7 | code: string 8 | message: string 9 | } 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Discussions 4 | url: https://github.com/anse-app/chatgpt-demo/discussions 5 | about: Use discussions if you have an idea for improvement or for asking questions. -------------------------------------------------------------------------------- /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "0.2.0", 3 | "configurations": [ 4 | { 5 | "command": "./node_modules/.bin/astro dev", 6 | "name": "Development server", 7 | "request": "launch", 8 | "type": "node-terminal" 9 | } 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "astro/tsconfigs/base", 3 | "compilerOptions": { 4 | "baseUrl": ".", 5 | "jsx": "preserve", 6 | "jsxImportSource": "solid-js", 7 | "types": ["vite-plugin-pwa/info"], 8 | "paths": { 9 | "@/*": ["src/*"], 10 | }, 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | NETLIFY_USE_PNPM = "true" 3 | NODE_VERSION = "18" 4 | 5 | [build] 6 | command = "OUTPUT=netlify astro build" 7 | publish = "dist" 8 | 9 | [[headers]] 10 | for = "/manifest.webmanifest" 11 | [headers.values] 12 | Content-Type = "application/manifest+json" 13 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.codeActionsOnSave": { 3 | "source.fixAll.eslint": true 4 | }, 5 | "editor.formatOnSave": false, 6 | "eslint.validate": [ 7 | "javascript", 8 | "javascriptreact", 9 | "astro", // Enable .astro 10 | "typescript", // Enable .ts 11 | "typescriptreact" // Enable .tsx 12 | ] 13 | } 14 | -------------------------------------------------------------------------------- /src/components/icons/Clear.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /src/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly OPENAI_API_KEY: string 5 | readonly HTTPS_PROXY: string 6 | readonly OPENAI_API_BASE_URL: string 7 | readonly HEAD_SCRIPTS: string 8 | readonly PUBLIC_SECRET_KEY: string 9 | readonly SITE_PASSWORD: string 10 | readonly OPENAI_API_MODEL: string 11 | } 12 | 13 | interface ImportMeta { 14 | readonly env: ImportMetaEnv 15 | } 16 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # build output 2 | dist/ 3 | .vercel/ 4 | .netlify/ 5 | 6 | # generated types 7 | .astro/ 8 | 9 | # dependencies 10 | node_modules/ 11 | 12 | # logs 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | pnpm-debug.log* 17 | 18 | # environment variables 19 | .env 20 | .env.production 21 | 22 | # macOS-specific files 23 | .DS_Store 24 | 25 | # Local 26 | *.local 27 | 28 | **/.DS_Store 29 | 30 | # Editor directories and files 31 | .idea 32 | -------------------------------------------------------------------------------- /src/components/icons/X.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/pages/api/auth.ts: -------------------------------------------------------------------------------- 1 | import type { APIRoute } from 'astro' 2 | 3 | const realPassword = import.meta.env.SITE_PASSWORD || '' 4 | const passList = realPassword.split(',') || [] 5 | 6 | export const post: APIRoute = async(context) => { 7 | const body = await context.request.json() 8 | 9 | const { pass } = body 10 | return new Response(JSON.stringify({ 11 | code: (!realPassword || pass === realPassword || passList.includes(pass)) ? 0 : -1, 12 | })) 13 | } 14 | -------------------------------------------------------------------------------- /shims.d.ts: -------------------------------------------------------------------------------- 1 | import type { AttributifyAttributes } from '@unocss/preset-attributify' 2 | 3 | // declare module 'solid-js' { 4 | // namespace JSX { 5 | // interface HTMLAttributes extends AttributifyAttributes {} 6 | // } 7 | // } 8 | 9 | declare global { 10 | namespace astroHTML.JSX { 11 | interface HTMLAttributes extends AttributifyAttributes { } 12 | } 13 | namespace JSX { 14 | interface HTMLAttributes<> extends AttributifyAttributes {} 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/components/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { model } from '../utils/openAI' 3 | import Logo from './Logo.astro' 4 | import Themetoggle from './Themetoggle.astro' 5 | --- 6 | 7 |
8 |
9 | 10 | 11 |
12 |
13 | ChatGPT 14 | Demo 15 |
16 |

Based on OpenAI API ({model}).

17 |
18 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:alpine as builder 2 | WORKDIR /usr/src 3 | RUN npm install -g pnpm 4 | COPY package.json pnpm-lock.yaml ./ 5 | RUN pnpm install 6 | COPY . . 7 | RUN pnpm run build 8 | 9 | FROM node:alpine 10 | WORKDIR /usr/src 11 | RUN npm install -g pnpm 12 | COPY --from=builder /usr/src/dist ./dist 13 | COPY --from=builder /usr/src/hack ./ 14 | COPY package.json pnpm-lock.yaml ./ 15 | RUN pnpm install 16 | ENV HOST=0.0.0.0 PORT=3000 NODE_ENV=production 17 | EXPOSE $PORT 18 | CMD ["/bin/sh", "docker-entrypoint.sh"] 19 | -------------------------------------------------------------------------------- /src/components/icons/Env.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /src/message.css: -------------------------------------------------------------------------------- 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 | } -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/typo.yml: -------------------------------------------------------------------------------- 1 | name: 👀 Typo / Grammar fix 2 | description: You can just go ahead and send a PR! Thank you! 3 | labels: [] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ## PR Welcome! 9 | 10 | If the typo / grammar issue is trivial and straightforward, you can help by **directly sending a quick pull request**! 11 | If you spot multiple of them, we suggest combining them into a single PR. Thanks! 12 | - type: textarea 13 | id: context 14 | attributes: 15 | label: Additional context -------------------------------------------------------------------------------- /hack/docker-entrypoint.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | sub_service_pid="" 4 | 5 | sub_service_command="node dist/server/entry.mjs" 6 | 7 | function init() { 8 | /bin/sh ./docker-env-replace.sh 9 | } 10 | 11 | function main { 12 | init 13 | 14 | echo "Starting service..." 15 | eval "$sub_service_command &" 16 | sub_service_pid=$! 17 | 18 | trap cleanup SIGTERM SIGINT 19 | echo "Running script..." 20 | while [ true ]; do 21 | sleep 5 22 | done 23 | } 24 | 25 | function cleanup { 26 | echo "Cleaning up!" 27 | kill -TERM $sub_service_pid 28 | } 29 | 30 | main 31 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | chatgpt-demo: 5 | image: ddiu8081/chatgpt-demo:latest 6 | container_name: chatgpt-demo 7 | restart: always 8 | ports: 9 | - "3000:3000" 10 | environment: 11 | - OPENAI_API_KEY=YOUR_OPENAI_API_KEY 12 | # - HTTPS_PROXY=YOUR_HTTPS_PROXY 13 | # - OPENAI_API_BASE_URL=YOUR_OPENAI_API_BASE_URL 14 | # - HEAD_SCRIPTS=YOUR_HEAD_SCRIPTS 15 | # - PUBLIC_SECRET_KEY=YOUR_SECRET_KEY 16 | # - SITE_PASSWORD=YOUR_SITE_PASSWORD 17 | # - OPENAI_API_MODEL=YOUR_OPENAI_API_MODEL 18 | 19 | -------------------------------------------------------------------------------- /.github/workflows/lint.yml: -------------------------------------------------------------------------------- 1 | name: Lint CI 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | 8 | pull_request: 9 | branches: 10 | - main 11 | 12 | jobs: 13 | lint: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v3 17 | 18 | - name: Install pnpm 19 | uses: pnpm/action-setup@v2 20 | 21 | - name: Set node 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 18.x 25 | cache: pnpm 26 | 27 | - name: Install 28 | run: pnpm install --no-frozen-lockfile 29 | 30 | - name: Lint 31 | run: pnpm run lint 32 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Your API Key for OpenAI 2 | OPENAI_API_KEY= 3 | # Provide proxy for OpenAI API. e.g. http://127.0.0.1:7890 4 | HTTPS_PROXY= 5 | # Custom base url for OpenAI API. default: https://api.openai.com 6 | OPENAI_API_BASE_URL= 7 | # Inject analytics or other scripts before of the page 8 | HEAD_SCRIPTS= 9 | # Secret string for the project. Use for generating signatures for API calls 10 | PUBLIC_SECRET_KEY= 11 | # Set password for site, support multiple password separated by comma. If not set, site will be public 12 | SITE_PASSWORD= 13 | # ID of the model to use. https://platform.openai.com/docs/api-reference/models/list 14 | OPENAI_API_MODEL= 15 | -------------------------------------------------------------------------------- /plugins/disableBlocks.ts: -------------------------------------------------------------------------------- 1 | export default function plugin(platform?: string) { 2 | const transform = (code: string, id: string) => { 3 | if (id.includes('pages/api/generate.ts')) { 4 | return { 5 | code: code.replace(/^.*?#vercel-disable-blocks([\s\S]+?)#vercel-end.*?$/gm, ''), 6 | map: null, 7 | } 8 | } 9 | if (platform === 'netlify' && id.includes('layouts/Layout.astro')) { 10 | return { 11 | code: code.replace(/^.*?([\s\S]+?).*?$/gm, ''), 12 | map: null, 13 | } 14 | } 15 | } 16 | 17 | return { 18 | name: 'vercel-disable-blocks', 19 | enforce: 'pre', 20 | transform, 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/ErrorMessageItem.tsx: -------------------------------------------------------------------------------- 1 | import IconRefresh from './icons/Refresh' 2 | import type { ErrorMessage } from '@/types' 3 | 4 | interface Props { 5 | data: ErrorMessage 6 | onRetry?: () => void 7 | } 8 | 9 | export default ({ data, onRetry }: Props) => { 10 | return ( 11 |
12 | {data.code &&
{data.code}
} 13 |
{data.message}
14 | {onRetry && ( 15 |
16 |
17 | 18 | Regenerate 19 |
20 |
21 | )} 22 |
23 | ) 24 | } 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Description 10 | 11 | 12 | 13 | ### Linked Issues 14 | 15 | 16 | ### Additional context 17 | 18 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | extends: ['@evan-yang', 'plugin:astro/recommended'], 3 | rules: { 4 | 'no-console': ['error', { allow: ['error'] }], 5 | 'react/display-name': 'off', 6 | 'react-hooks/rules-of-hooks': 'off', 7 | '@typescript-eslint/no-use-before-define': 'off', 8 | }, 9 | overrides: [ 10 | { 11 | files: ['*.astro'], 12 | parser: 'astro-eslint-parser', 13 | parserOptions: { 14 | parser: '@typescript-eslint/parser', 15 | extraFileExtensions: ['.astro'], 16 | }, 17 | rules: { 18 | 'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'], 19 | }, 20 | }, 21 | { 22 | // Define the configuration for ` 37 | -------------------------------------------------------------------------------- /src/components/icons/Refresh.tsx: -------------------------------------------------------------------------------- 1 | export default () => { 2 | return ( 3 | 4 | ) 5 | } 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Diu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/build-docker.yml: -------------------------------------------------------------------------------- 1 | name: build_docker 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | 7 | jobs: 8 | build_docker: 9 | name: Build docker 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v3 14 | 15 | - name: Set up QEMU 16 | uses: docker/setup-qemu-action@v2 17 | - name: Set up Docker Buildx 18 | uses: docker/setup-buildx-action@v2 19 | - name: Login to DockerHub 20 | uses: docker/login-action@v2 21 | with: 22 | # https://hub.docker.com/settings/security?generateToken=true 23 | username: ${{ secrets.DOCKERHUB_USERNAME }} 24 | password: ${{ secrets.DOCKERHUB_TOKEN }} 25 | - name: Build and push 26 | id: docker_build 27 | uses: docker/build-push-action@v4 28 | with: 29 | context: . 30 | push: true 31 | labels: ${{ steps.meta.outputs.labels }} 32 | platforms: linux/amd64,linux/arm64 33 | tags: | 34 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-demo:${{ github.ref_name }} 35 | ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-demo:latest 36 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/Logo.astro: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/utils/auth.ts: -------------------------------------------------------------------------------- 1 | import { sha256 } from 'js-sha256' 2 | interface AuthPayload { 3 | t: number 4 | m: string 5 | } 6 | 7 | async function digestMessage(message: string) { 8 | if (typeof crypto !== 'undefined' && crypto?.subtle?.digest) { 9 | const msgUint8 = new TextEncoder().encode(message) 10 | const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8) 11 | const hashArray = Array.from(new Uint8Array(hashBuffer)) 12 | return hashArray.map(b => b.toString(16).padStart(2, '0')).join('') 13 | } else { 14 | return sha256(message).toString() 15 | } 16 | } 17 | 18 | export const generateSignature = async(payload: AuthPayload) => { 19 | const { t: timestamp, m: lastMessage } = payload 20 | const secretKey = import.meta.env.PUBLIC_SECRET_KEY as string || '' 21 | const signText = `${timestamp}:${lastMessage}:${secretKey}` 22 | // eslint-disable-next-line no-return-await 23 | return await digestMessage(signText) 24 | } 25 | 26 | export const verifySignature = async(payload: AuthPayload, sign: string) => { 27 | // if (Math.abs(payload.t - Date.now()) > 1000 * 60 * 5) { 28 | // return false 29 | // } 30 | const payloadSign = await generateSignature(payload) 31 | return payloadSign === sign 32 | } 33 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: 🚀 Feature request 2 | description: Suggest a feature or an improvement 3 | labels: ['enhancement'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ### Before submitting... 9 | Thank you for taking the time to fill out this feature request! Please confirm the following points before submitting: 10 | 11 | ✅ I have checked the feature was not already submitted by searching on GitHub under issues or discussions. 12 | ✅ Use English. This allows more people to search and participate in the issue. 13 | - type: textarea 14 | id: feature-description 15 | attributes: 16 | label: Describe the feature 17 | description: A clear and concise description of what you think would be a helpful addition. 18 | validations: 19 | required: true 20 | - type: textarea 21 | id: additional-context 22 | attributes: 23 | label: Additional context 24 | description: Any other context or screenshots about the feature request here. 25 | - type: checkboxes 26 | id: will-pr 27 | attributes: 28 | label: Participation 29 | options: 30 | - label: I am willing to submit a pull request for this feature. 31 | required: false 32 | -------------------------------------------------------------------------------- /hack/docker-env-replace.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | # Your API Key for OpenAI 4 | openai_api_key=$OPENAI_API_KEY 5 | # Provide proxy for OpenAI API. e.g. http://127.0.0.1:7890 6 | https_proxy=$HTTPS_PROXY 7 | # Custom base url for OpenAI API. default: https://api.openai.com 8 | openai_api_base_url=$OPENAI_API_BASE_URL 9 | # Inject analytics or other scripts before of the page 10 | head_scripts=$HEAD_SCRIPTS 11 | # Secret string for the project. Use for generating signatures for API calls 12 | public_secret_key=$PUBLIC_SECRET_KEY 13 | # Set password for site, support multiple password separated by comma. If not set, site will be public 14 | site_password=$SITE_PASSWORD 15 | # ID of the model to use. https://platform.openai.com/docs/api-reference/models/list 16 | openai_api_model=$OPENAI_API_MODEL 17 | 18 | for file in $(find ./dist -type f -name "*.mjs"); do 19 | sed "s/({}).OPENAI_API_KEY/\"$openai_api_key\"/g; 20 | s/({}).HTTPS_PROXY/\"$https_proxy\"/g; 21 | s/({}).OPENAI_API_BASE_URL/\"$openai_api_base_url\"/g; 22 | s/({}).HEAD_SCRIPTS/\"$head_scripts\"/g; 23 | s/({}).PUBLIC_SECRET_KEY/\"$public_secret_key\"/g; 24 | s/({}).OPENAI_API_MODEL/\"$openai_api_model\"/g; 25 | s/process.env.SITE_PASSWORD/\"$site_password\"/g" $file > tmp 26 | mv tmp $file 27 | done 28 | 29 | rm -rf tmp 30 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: Create and publish a Docker image 2 | 3 | on: 4 | push: 5 | branches: ['main'] 6 | 7 | env: 8 | REGISTRY: ghcr.io 9 | IMAGE_NAME: ${{ github.repository }} 10 | 11 | jobs: 12 | build-and-push-image: 13 | runs-on: ubuntu-latest 14 | permissions: 15 | contents: read 16 | packages: write 17 | 18 | steps: 19 | - name: Checkout repository 20 | uses: actions/checkout@v3 21 | 22 | - name: Log in to the Container registry 23 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 24 | with: 25 | registry: ${{ env.REGISTRY }} 26 | username: ${{ github.actor }} 27 | password: ${{ secrets.GITHUB_TOKEN }} 28 | 29 | - name: Extract metadata (tags, labels) for Docker 30 | id: meta 31 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 32 | with: 33 | images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} 34 | 35 | - name: Build and push Docker image 36 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 37 | with: 38 | context: . 39 | push: true 40 | tags: ${{ steps.meta.outputs.tags }} 41 | labels: ${{ steps.meta.outputs.labels }} 42 | -------------------------------------------------------------------------------- /.github/workflows/sync.yml: -------------------------------------------------------------------------------- 1 | name: Upstream Sync 2 | 3 | permissions: 4 | contents: write 5 | 6 | on: 7 | schedule: 8 | - cron: "0 0 * * *" # every day 9 | workflow_dispatch: 10 | 11 | jobs: 12 | sync_latest_from_upstream: 13 | name: Sync latest commits from upstream repo 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event.repository.fork }} 16 | 17 | steps: 18 | # Step 1: run a standard checkout action 19 | - name: Checkout target repo 20 | uses: actions/checkout@v3 21 | 22 | # Step 2: run the sync action 23 | - name: Sync upstream changes 24 | id: sync 25 | uses: aormsby/Fork-Sync-With-Upstream-action@v3.4 26 | with: 27 | upstream_sync_repo: anse-app/chatgpt-demo 28 | upstream_sync_branch: main 29 | target_sync_branch: main 30 | target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set 31 | 32 | # Set test_mode true to run tests instead of the true action!! 33 | test_mode: false 34 | 35 | - name: Sync check 36 | if: failure() 37 | run: | 38 | echo "::error::由于权限不足,导致同步失败(这是预期的行为),请前往仓库首页手动执行[Sync fork]。" 39 | echo "::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork]." 40 | exit 1 41 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatgpt-api-demo", 3 | "version": "0.0.1", 4 | "packageManager": "pnpm@7.28.0", 5 | "scripts": { 6 | "dev": "astro dev", 7 | "start": "astro dev", 8 | "build": "astro build", 9 | "build:vercel": "OUTPUT=vercel astro build", 10 | "build:netlify": "OUTPUT=netlify astro build", 11 | "preview": "astro preview", 12 | "astro": "astro", 13 | "lint": "eslint . --ext .js,.jsx,.ts,.tsx,.astro", 14 | "lint:fix": "eslint . --ext .js,.jsx,.ts,.tsx,.astro --fix" 15 | }, 16 | "dependencies": { 17 | "@astrojs/netlify": "2.0.0", 18 | "@astrojs/node": "^5.0.4", 19 | "@astrojs/solid-js": "^2.0.2", 20 | "@astrojs/vercel": "^3.1.3", 21 | "@unocss/reset": "^0.50.1", 22 | "astro": "^2.0.15", 23 | "eslint": "^8.36.0", 24 | "eventsource-parser": "^0.1.0", 25 | "highlight.js": "^11.7.0", 26 | "js-sha256": "^0.9.0", 27 | "katex": "^0.6.0", 28 | "markdown-it": "^13.0.1", 29 | "markdown-it-highlightjs": "^4.0.1", 30 | "markdown-it-katex": "^2.0.3", 31 | "solid-js": "1.6.12", 32 | "solidjs-use": "^1.2.0", 33 | "undici": "^5.20.0" 34 | }, 35 | "devDependencies": { 36 | "@iconify-json/carbon": "^1.1.16", 37 | "@types/markdown-it": "^12.2.3", 38 | "@typescript-eslint/parser": "^5.54.1", 39 | "@unocss/preset-attributify": "^0.50.1", 40 | "@unocss/preset-icons": "^0.50.4", 41 | "@unocss/preset-typography": "^0.50.3", 42 | "eslint-plugin-astro": "^0.24.0", 43 | "punycode": "^2.3.0", 44 | "unocss": "^0.50.1", 45 | "@evan-yang/eslint-config": "^1.0.1", 46 | "vite-plugin-pwa": "^0.14.1" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bus_report_when_deploying.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report (When self-deploying) 2 | description: Report an issue or possible bug when deploy to your own server or cloud. 3 | labels: ['pending triage', 'deploy'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ### Before submitting... 9 | Thanks for taking the time to fill out this bug report! Please confirm the following points before submitting: 10 | 11 | ✅ I am using **latest version of chatgpt-demo**. 12 | ✅ I have checked the bug was not already reported by searching on GitHub under issues. 13 | ✅ Use English to ask questions. This allows more people to search and participate in the issue. 14 | - type: dropdown 15 | id: server 16 | attributes: 17 | label: How is Anse deployed? 18 | description: Select the used deployment method. 19 | options: 20 | - Node 21 | - Docker 22 | - Vercel 23 | - Netlify 24 | - Railway 25 | - Others (Specify in description) 26 | validations: 27 | required: true 28 | - type: textarea 29 | id: bug-description 30 | attributes: 31 | label: Describe the bug 32 | description: A clear and concise description of what the bug is. 33 | placeholder: Bug description 34 | validations: 35 | required: true 36 | - type: textarea 37 | id: console-logs 38 | attributes: 39 | label: Console Logs 40 | description: Please check your browser and node console, fill in the error message if it exists. 41 | - type: checkboxes 42 | id: will-pr 43 | attributes: 44 | label: Participation 45 | options: 46 | - label: I am willing to submit a pull request for this issue. 47 | required: false -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_when_use.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report (When using) 2 | description: Report an issue or possible bug when using `chatgpt-demo` 3 | labels: ['pending triage', 'use'] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | ### Before submitting... 9 | Thanks for taking the time to fill out this bug report! Please confirm the following points before submitting: 10 | 11 | ✅ I have checked the bug was not already reported by searching on GitHub under issues. 12 | ✅ Use English to ask questions. This allows more people to search and participate in the issue. 13 | - type: input 14 | id: os 15 | attributes: 16 | label: What operating system are you using? 17 | placeholder: Mac, Windows, Linux 18 | validations: 19 | required: true 20 | - type: input 21 | id: browser 22 | attributes: 23 | label: What browser are you using? 24 | placeholder: Chrome, Firefox, Safari 25 | validations: 26 | required: true 27 | - type: textarea 28 | id: bug-description 29 | attributes: 30 | label: Describe the bug 31 | description: A clear and concise description of what the bug is. 32 | placeholder: Bug description 33 | validations: 34 | required: true 35 | - type: textarea 36 | id: prompt 37 | attributes: 38 | label: What prompt did you enter? 39 | description: If the issue is related to the prompt you entered, please fill in this field. 40 | - type: textarea 41 | id: console-logs 42 | attributes: 43 | label: Console Logs 44 | description: Please check your browser and fill in the error message if it exists. 45 | - type: checkboxes 46 | id: will-pr 47 | attributes: 48 | label: Participation 49 | options: 50 | - label: I am willing to submit a pull request for this issue. 51 | required: false -------------------------------------------------------------------------------- /src/pages/password.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Layout from '../layouts/Layout.astro' 3 | --- 4 | 5 | 6 |
7 |
Please input password
8 |
9 | 10 |
11 |
12 |
13 |
14 |
15 |
16 | 17 | 51 | 52 | 72 | -------------------------------------------------------------------------------- /astro.config.mjs: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'astro/config' 2 | import unocss from 'unocss/astro' 3 | import solidJs from '@astrojs/solid-js' 4 | 5 | import node from '@astrojs/node' 6 | import { VitePWA } from 'vite-plugin-pwa' 7 | import vercel from '@astrojs/vercel/edge' 8 | import netlify from '@astrojs/netlify/edge-functions' 9 | import disableBlocks from './plugins/disableBlocks' 10 | 11 | const envAdapter = () => { 12 | if (process.env.OUTPUT === 'vercel') { 13 | return vercel() 14 | } else if (process.env.OUTPUT === 'netlify') { 15 | return netlify() 16 | } else { 17 | return node({ 18 | mode: 'standalone', 19 | }) 20 | } 21 | } 22 | 23 | // https://astro.build/config 24 | export default defineConfig({ 25 | integrations: [ 26 | unocss(), 27 | solidJs(), 28 | ], 29 | output: 'server', 30 | adapter: envAdapter(), 31 | vite: { 32 | plugins: [ 33 | process.env.OUTPUT === 'vercel' && disableBlocks(), 34 | process.env.OUTPUT === 'netlify' && disableBlocks('netlify'), 35 | process.env.OUTPUT !== 'netlify' && VitePWA({ 36 | registerType: 'autoUpdate', 37 | manifest: { 38 | name: 'ChatGPT-API Demo', 39 | short_name: 'ChatGPT Demo', 40 | description: 'A demo repo based on OpenAI API', 41 | theme_color: '#212129', 42 | background_color: '#ffffff', 43 | icons: [ 44 | { 45 | src: 'pwa-192.png', 46 | sizes: '192x192', 47 | type: 'image/png', 48 | }, 49 | { 50 | src: 'pwa-512.png', 51 | sizes: '512x512', 52 | type: 'image/png', 53 | }, 54 | { 55 | src: 'icon.svg', 56 | sizes: '32x32', 57 | type: 'image/svg', 58 | purpose: 'any maskable', 59 | }, 60 | ], 61 | }, 62 | client: { 63 | installPrompt: true, 64 | periodicSyncForUpdates: 20, 65 | }, 66 | devOptions: { 67 | enabled: true, 68 | }, 69 | }), 70 | ], 71 | }, 72 | }) 73 | -------------------------------------------------------------------------------- /src/pages/api/generate.ts: -------------------------------------------------------------------------------- 1 | // #vercel-disable-blocks 2 | import { ProxyAgent, fetch } from 'undici' 3 | // #vercel-end 4 | import { generatePayload, parseOpenAIStream } from '@/utils/openAI' 5 | import { verifySignature } from '@/utils/auth' 6 | import type { APIRoute } from 'astro' 7 | 8 | const apiKey = import.meta.env.OPENAI_API_KEY 9 | const httpsProxy = import.meta.env.HTTPS_PROXY 10 | const baseUrl = ((import.meta.env.OPENAI_API_BASE_URL) || 'https://api.openai.com').trim().replace(/\/$/, '') 11 | const sitePassword = import.meta.env.SITE_PASSWORD || '' 12 | const passList = sitePassword.split(',') || [] 13 | 14 | export const post: APIRoute = async(context) => { 15 | const body = await context.request.json() 16 | const { sign, time, messages, pass } = body 17 | if (!messages) { 18 | return new Response(JSON.stringify({ 19 | error: { 20 | message: 'No input text.', 21 | }, 22 | }), { status: 400 }) 23 | } 24 | if (sitePassword && !(sitePassword === pass || passList.includes(pass))) { 25 | return new Response(JSON.stringify({ 26 | error: { 27 | message: 'Invalid password.', 28 | }, 29 | }), { status: 401 }) 30 | } 31 | if (import.meta.env.PROD && !await verifySignature({ t: time, m: messages?.[messages.length - 1]?.content || '' }, sign)) { 32 | return new Response(JSON.stringify({ 33 | error: { 34 | message: 'Invalid signature.', 35 | }, 36 | }), { status: 401 }) 37 | } 38 | const initOptions = generatePayload(apiKey, messages) 39 | // #vercel-disable-blocks 40 | if (httpsProxy) 41 | initOptions.dispatcher = new ProxyAgent(httpsProxy) 42 | // #vercel-end 43 | 44 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 45 | // @ts-expect-error 46 | const response = await fetch(`${baseUrl}/v1/chat/completions`, initOptions).catch((err: Error) => { 47 | console.error(err) 48 | return new Response(JSON.stringify({ 49 | error: { 50 | code: err.name, 51 | message: err.message, 52 | }, 53 | }), { status: 500 }) 54 | }) as Response 55 | 56 | return parseOpenAIStream(response) as Response 57 | } 58 | -------------------------------------------------------------------------------- /src/utils/openAI.ts: -------------------------------------------------------------------------------- 1 | import { createParser } from 'eventsource-parser' 2 | import type { ParsedEvent, ReconnectInterval } from 'eventsource-parser' 3 | import type { ChatMessage } from '@/types' 4 | 5 | export const model = import.meta.env.OPENAI_API_MODEL || 'gpt-3.5-turbo' 6 | 7 | export const generatePayload = (apiKey: string, messages: ChatMessage[]): RequestInit & { dispatcher?: any } => ({ 8 | headers: { 9 | 'Content-Type': 'application/json', 10 | 'Authorization': `Bearer ${apiKey}`, 11 | }, 12 | method: 'POST', 13 | body: JSON.stringify({ 14 | model, 15 | messages, 16 | temperature: 0.6, 17 | stream: true, 18 | }), 19 | }) 20 | 21 | export const parseOpenAIStream = (rawResponse: Response) => { 22 | const encoder = new TextEncoder() 23 | const decoder = new TextDecoder() 24 | if (!rawResponse.ok) { 25 | return new Response(rawResponse.body, { 26 | status: rawResponse.status, 27 | statusText: rawResponse.statusText, 28 | }) 29 | } 30 | 31 | const stream = new ReadableStream({ 32 | async start(controller) { 33 | const streamParser = (event: ParsedEvent | ReconnectInterval) => { 34 | if (event.type === 'event') { 35 | const data = event.data 36 | if (data === '[DONE]') { 37 | controller.close() 38 | return 39 | } 40 | try { 41 | // response = { 42 | // id: 'chatcmpl-6pULPSegWhFgi0XQ1DtgA3zTa1WR6', 43 | // object: 'chat.completion.chunk', 44 | // created: 1677729391, 45 | // model: 'gpt-3.5-turbo-0301', 46 | // choices: [ 47 | // { delta: { content: '你' }, index: 0, finish_reason: null } 48 | // ], 49 | // } 50 | const json = JSON.parse(data) 51 | const text = json.choices[0].delta?.content || '' 52 | const queue = encoder.encode(text) 53 | controller.enqueue(queue) 54 | } catch (e) { 55 | controller.error(e) 56 | } 57 | } 58 | } 59 | 60 | const parser = createParser(streamParser) 61 | for await (const chunk of rawResponse.body as any) 62 | parser.feed(decoder.decode(chunk)) 63 | }, 64 | }) 65 | 66 | return new Response(stream) 67 | } 68 | -------------------------------------------------------------------------------- /src/components/SystemRoleSettings.tsx: -------------------------------------------------------------------------------- 1 | import { Show } from 'solid-js' 2 | import IconEnv from './icons/Env' 3 | import IconX from './icons/X' 4 | import type { Accessor, Setter } from 'solid-js' 5 | 6 | interface Props { 7 | canEdit: Accessor 8 | systemRoleEditing: Accessor 9 | setSystemRoleEditing: Setter 10 | currentSystemRoleSettings: Accessor 11 | setCurrentSystemRoleSettings: Setter 12 | } 13 | 14 | export default (props: Props) => { 15 | let systemInputRef: HTMLTextAreaElement 16 | 17 | const handleButtonClick = () => { 18 | props.setCurrentSystemRoleSettings(systemInputRef.value) 19 | props.setSystemRoleEditing(false) 20 | } 21 | 22 | return ( 23 |
24 | 25 | 26 |
27 |
28 | }> 29 | props.setCurrentSystemRoleSettings('')} class="sys-edit-btn p-1 rd-50%" > 30 | 31 | System Role: 32 |
33 |
34 | {props.currentSystemRoleSettings()} 35 |
36 |
37 |
38 | 39 | props.setSystemRoleEditing(!props.systemRoleEditing())} class="sys-edit-btn"> 40 | 41 | Add System Role 42 | 43 | 44 |
45 | 46 |
47 |
48 | 49 | System Role: 50 |
51 |

Gently instruct the assistant and set the behavior of the assistant.

52 |
53 |