├── 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 | 61 | 62 | 63 | Set 64 | 65 | 66 | 67 | 68 | ) 69 | } 70 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetTypography, 6 | presetUno, 7 | transformerDirectives, 8 | transformerVariantGroup, 9 | } from 'unocss' 10 | 11 | export default defineConfig({ 12 | presets: [ 13 | presetUno(), 14 | presetAttributify(), 15 | presetIcons({ 16 | scale: 1.1, 17 | cdn: 'https://esm.sh/', 18 | }), 19 | presetTypography({ 20 | cssExtend: { 21 | 'ul,ol': { 22 | 'padding-left': '2.25em', 23 | 'position': 'relative', 24 | }, 25 | }, 26 | }), 27 | ], 28 | transformers: [transformerVariantGroup(), transformerDirectives()], 29 | shortcuts: [{ 30 | 'fc': 'flex justify-center', 31 | 'fi': 'flex items-center', 32 | 'fb': 'flex justify-between', 33 | 'fcc': 'fc items-center', 34 | 'fie': 'fi justify-end', 35 | 'col-fcc': 'flex-col fcc', 36 | 'inline-fcc': 'inline-flex items-center justify-center', 37 | 'base-focus': 'focus:(bg-op-20 ring-0 outline-none)', 38 | 'b-slate-link': 'border-b border-(slate none) hover:border-dashed', 39 | 'gpt-title': 'text-2xl font-extrabold mr-1', 40 | 'gpt-subtitle': 'text-(2xl transparent) font-extrabold bg-(clip-text gradient-to-r) from-sky-400 to-emerald-600', 41 | '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', 42 | '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', 43 | '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', 44 | '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', 45 | 'gpt-back-bottom-btn': 'gpt-back-top-btn bottom-20px transform-rotate-180deg', 46 | 'gpt-password-input': 'px-4 py-3 h-12 rounded-sm bg-(slate op-15) base-focus', 47 | 'gpt-password-submit': 'fcc h-12 w-12 bg-slate cursor-pointer bg-op-20 hover:bg-op-50', 48 | 'gen-slate-btn': 'h-12 px-4 py-2 bg-(slate op-15) hover:bg-op-20 rounded-sm', 49 | 'gen-cb-wrapper': 'h-12 my-4 fcc gap-4 bg-(slate op-15) rounded-sm', 50 | 'gen-cb-stop': 'px-2 py-0.5 border border-slate rounded-md text-sm op-70 cursor-pointer hover:bg-slate/10', 51 | 'gen-text-wrapper': 'my-4 fc gap-2 transition-opacity', 52 | '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', 53 | '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', 54 | 'stick-btn-on': '!bg-$c-fg text-$c-bg hover:op-80', 55 | }], 56 | }) 57 | -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | export interface Props { 3 | title: string; 4 | } 5 | 6 | const { title } = Astro.props; 7 | --- 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | {title} 20 | 21 | { 22 | import.meta.env.HEAD_SCRIPTS 23 | ? ( 24 | 25 | ) 26 | : null 27 | } 28 | 29 | { 30 | import.meta.env.PROD && ( 31 | 32 | 33 | ) 34 | } 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 85 | 86 | 104 | -------------------------------------------------------------------------------- /src/components/MessageItem.tsx: -------------------------------------------------------------------------------- 1 | import { createSignal } from 'solid-js' 2 | import MarkdownIt from 'markdown-it' 3 | import mdKatex from 'markdown-it-katex' 4 | import mdHighlight from 'markdown-it-highlightjs' 5 | import { useClipboard, useEventListener } from 'solidjs-use' 6 | import IconRefresh from './icons/Refresh' 7 | import type { Accessor } from 'solid-js' 8 | import type { ChatMessage } from '@/types' 9 | 10 | interface Props { 11 | role: ChatMessage['role'] 12 | message: Accessor | string 13 | showRetry?: Accessor 14 | onRetry?: () => void 15 | } 16 | 17 | export default ({ role, message, showRetry, onRetry }: Props) => { 18 | const roleClass = { 19 | system: 'bg-gradient-to-r from-gray-300 via-gray-200 to-gray-300', 20 | user: 'bg-gradient-to-r from-purple-400 to-yellow-400', 21 | assistant: 'bg-gradient-to-r from-yellow-200 via-green-200 to-green-300', 22 | } 23 | const [source] = createSignal('') 24 | const { copy, copied } = useClipboard({ source, copiedDuring: 1000 }) 25 | 26 | useEventListener('click', (e) => { 27 | const el = e.target as HTMLElement 28 | let code = null 29 | 30 | if (el.matches('div > div.copy-btn')) { 31 | code = decodeURIComponent(el.dataset.code!) 32 | copy(code) 33 | } 34 | if (el.matches('div > div.copy-btn > svg')) { 35 | // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain 36 | code = decodeURIComponent(el.parentElement?.dataset.code!) 37 | copy(code) 38 | } 39 | }) 40 | 41 | const htmlString = () => { 42 | const md = MarkdownIt({ 43 | linkify: true, 44 | breaks: true, 45 | }).use(mdKatex).use(mdHighlight) 46 | const fence = md.renderer.rules.fence! 47 | md.renderer.rules.fence = (...args) => { 48 | const [tokens, idx] = args 49 | const token = tokens[idx] 50 | const rawCode = fence(...args) 51 | 52 | return ` 53 | 54 | 55 | 56 | ${copied() ? 'Copied' : 'Copy'} 57 | 58 | 59 | ${rawCode} 60 | ` 61 | } 62 | 63 | if (typeof message === 'function') 64 | return md.render(message()) 65 | else if (typeof message === 'string') 66 | return md.render(message) 67 | 68 | return '' 69 | } 70 | 71 | return ( 72 | 73 | 74 | 75 | 76 | 77 | {showRetry?.() && onRetry && ( 78 | 79 | 80 | 81 | Regenerate 82 | 83 | 84 | )} 85 | 86 | ) 87 | } 88 | -------------------------------------------------------------------------------- /src/components/Themetoggle.astro: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 54 | 55 | 90 | -------------------------------------------------------------------------------- /README.zh-CN.md: -------------------------------------------------------------------------------- 1 | # ChatGPT-API Demo 2 | 3 | [English](./README.md) | 简体中文 4 | 5 | 一个基于 [OpenAI GPT-3.5 Turbo API](https://platform.openai.com/docs/guides/chat) 的 demo。 6 | 7 | **🍿 在线预览**: https://chatgpt.ddiu.me 8 | 9 | **🏖️ V2 版本(Beta)**: https://v2.chatgpt.ddiu.me 10 | 11 | > ⚠️ 注意: 我们的API密钥限制已用尽。所以演示站点现在不可用。 12 | 13 |  14 | 15 | ## 本地运行 16 | 17 | ### 前置环境 18 | 19 | 1. **Node**: 检查您的开发环境和部署环境是否都使用 `Node v18` 或更高版本。你可以使用 [nvm](https://github.com/nvm-sh/nvm) 管理本地多个 `node` 版本。 20 | ```bash 21 | node -v 22 | ``` 23 | 2. **PNPM**: 我们推荐使用 [pnpm](https://pnpm.io/) 来管理依赖,如果你从来没有安装过 pnpm,可以使用下面的命令安装: 24 | ```bash 25 | npm i -g pnpm 26 | ``` 27 | 3. **OPENAI_API_KEY**: 在运行此应用程序之前,您需要从 OpenAI 获取 API 密钥。您可以在 [https://beta.openai.com/signup](https://beta.openai.com/signup) 注册 API 密钥。 28 | 29 | ### 起步运行 30 | 31 | 1. 安装依赖 32 | ```bash 33 | pnpm install 34 | ``` 35 | 2. 复制 `.env.example` 文件,重命名为 `.env`,并添加你的 [OpenAI API key](https://platform.openai.com/account/api-keys) 到 `.env` 文件中 36 | ```bash 37 | OPENAI_API_KEY=sk-xxx... 38 | ``` 39 | 3. 运行应用,本地项目运行在 `http://localhost:3000/` 40 | ```bash 41 | pnpm run dev 42 | ``` 43 | 44 | ## 部署 45 | 46 | ### 部署在 Vercel 47 | 48 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fddiu8081%2Fchatgpt-demo&env=OPENAI_API_KEY&envDescription=OpenAI%20API%20Key&envLink=https%3A%2F%2Fplatform.openai.com%2Faccount%2Fapi-keys) 49 | 50 | 51 | 52 | > ###### 🔒 需要站点密码? 53 | > 54 | > 携带[`SITE_PASSWORD`](#environment-variables)进行部署 55 | > 56 | > 57 | 58 |  59 | 60 | ### 部署在 Netlify 61 | 62 | [](https://app.netlify.com/start/deploy?repository=https://github.com/ddiu8081/chatgpt-demo#OPENAI_API_KEY=&HTTPS_PROXY=&OPENAI_API_BASE_URL=&HEAD_SCRIPTS=&PUBLIC_SECRET_KEY=&OPENAI_API_MODEL=&SITE_PASSWORD=) 63 | 64 | **分步部署教程:** 65 | 66 | 1. [Fork](https://github.com/ddiu8081/chatgpt-demo/fork) 此项目,前往 [https://app.netlify.com/start](https://app.netlify.com/start) 新建站点,选择你 `fork` 完成的项目,将其与 `GitHub` 帐户连接。 67 | 68 |  69 | 70 |  71 | 72 | 73 | 2. 选择要部署的分支,选择 `main` 分支, 在项目设置中配置环境变量,环境变量配置参考下文。 74 | 75 |  76 | 77 | 3. 选择默认的构建命令和输出目录,单击 `Deploy Site` 按钮开始部署站点。 78 | 79 |  80 | 81 | ### 部署在 Docker 82 | 部署之前请确认 `.env` 文件正常配置,环境变量参考下方文档, [Docker Hub address](https://hub.docker.com/r/ddiu8081/chatgpt-demo). 83 | 84 | **一键运行** 85 | ```bash 86 | docker run --name=chatgpt-demo -e OPENAI_API_KEY=YOUR_OPEN_API_KEY -p 3000:3000 -d ddiu8081/chatgpt-demo:latest 87 | ``` 88 | `-e` 在容器中定义环境变量。 89 | 90 | **使用 Docker compose** 91 | ```yml 92 | version: '3' 93 | 94 | services: 95 | chatgpt-demo: 96 | image: ddiu8081/chatgpt-demo:latest 97 | container_name: chatgpt-demo 98 | restart: always 99 | ports: 100 | - '3000:3000' 101 | environment: 102 | - OPENAI_API_KEY=YOUR_OPEN_API_KEY 103 | # - HTTPS_PROXY=YOUR_HTTPS_PROXY 104 | # - OPENAI_API_BASE_URL=YOUR_OPENAI_API_BASE_URL 105 | # - HEAD_SCRIPTS=YOUR_HEAD_SCRIPTS 106 | # - PUBLIC_SECRET_KEY=YOUR_SECRET_KEY 107 | # - SITE_PASSWORD=YOUR_SITE_PASSWORD 108 | # - OPENAI_API_MODEL=YOUR_OPENAI_API_MODEL 109 | ``` 110 | 111 | ```bash 112 | # start 113 | docker compose up -d 114 | # down 115 | docker-compose down 116 | ``` 117 | 118 | ### 部署在更多的服务器 119 | 120 | 请参考官方部署文档:https://docs.astro.build/en/guides/deploy 121 | 122 | ## 环境变量 123 | 124 | 配置本地或者部署的环境变量 125 | 126 | | 名称 | 描述 | 默认 | 127 | | --- | --- | --- | 128 | | `OPENAI_API_KEY` | 你的 OpenAI API Key | `null` | 129 | | `HTTPS_PROXY` | 为 OpenAI API 提供代理. e.g. `http://127.0.0.1:7890` | `null` | 130 | | `OPENAI_API_BASE_URL` | 请求 OpenAI API 的自定义 Base URL. | `https://api.openai.com` | 131 | | `HEAD_SCRIPTS` | 在页面的 `` 之前注入分析或其他脚本 | `null` | 132 | | `PUBLIC_SECRET_KEY` | 项目的秘密字符串。用于生成 API 调用的签名 | `null` | 133 | | `SITE_PASSWORD` | 为网站设置密码,支持使用英文逗号创建多个密码。如果未设置,则该网站将是公开的 | `null` | 134 | | `OPENAI_API_MODEL` | 使用的 OpenAI 模型. [模型列表](https://platform.openai.com/docs/api-reference/models/list) | `gpt-3.5-turbo` | 135 | 136 | ## 开启同步更新 137 | 138 | Fork 项目后,您需要在 Fork 项目的操作页面上手动启用工作流和上游同步操作。启用后,每天都会执行自动更新: 139 | 140 |  141 | 142 | ## 常见问题 143 | 144 | Q: TypeError: fetch failed (can't connect to OpenAI Api) 145 | 146 | A: 配置环境变量 `HTTPS_PROXY`,参考: https://github.com/ddiu8081/chatgpt-demo/issues/34 147 | 148 | Q: throw new TypeError(${context} is not a ReadableStream.) 149 | 150 | A: Node 版本需要在 `v18` 或者更高,参考: https://github.com/ddiu8081/chatgpt-demo/issues/65 151 | 152 | Q: Accelerate domestic access without the need for proxy deployment tutorial? 153 | 154 | A: 你可以参考此教程: https://github.com/ddiu8081/chatgpt-demo/discussions/270 155 | 156 | Q: `PWA` 不工作? 157 | 158 | A: 当前的 PWA 不支持 Netlify 部署,您可以选择 vercel 或 node 部署。 159 | 160 | ## 参与贡献 161 | 162 | 这个项目的存在要感谢所有做出贡献的人。 163 | 164 | 感谢我们所有的支持者!🙏 165 | 166 | [](https://github.com/ddiu8081/chatgpt-demo/graphs/contributors) 167 | 168 | ## License 169 | 170 | MIT © [ddiu8081](https://github.com/ddiu8081/chatgpt-demo/blob/main/LICENSE) 171 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # ChatGPT-API Demo 2 | 3 | English | [简体中文](./README.zh-CN.md) 4 | 5 | A demo repo based on [OpenAI GPT-3.5 Turbo API.](https://platform.openai.com/docs/guides/chat) 6 | 7 | **🍿 Live preview**: https://chatgpt.ddiu.me 8 | 9 | > ⚠️ Notice: Our API Key limit has been exhausted. So the demo site is not available now. 10 | 11 |  12 | 13 | ## Introducing `Anse` 14 | 15 | Looking for multi-chat, image-generation, and more powerful features? Take a look at our newly launched [Anse](https://github.com/anse-app/anse). 16 | 17 | More info on https://github.com/ddiu8081/chatgpt-demo/discussions/247. 18 | 19 | [](https://github.com/anse-app/anse) 20 | 21 | ## Running Locally 22 | 23 | ### Pre environment 24 | 1. **Node**: Check that both your development environment and deployment environment are using `Node v18` or later. You can use [nvm](https://github.com/nvm-sh/nvm) to manage multiple `node` versions locally。 25 | ```bash 26 | node -v 27 | ``` 28 | 2. **PNPM**: We recommend using [pnpm](https://pnpm.io/) to manage dependencies. If you have never installed pnpm, you can install it with the following command: 29 | ```bash 30 | npm i -g pnpm 31 | ``` 32 | 3. **OPENAI_API_KEY**: Before running this application, you need to obtain the API key from OpenAI. You can register the API key at [https://beta.openai.com/signup](https://beta.openai.com/signup). 33 | 34 | ### Getting Started 35 | 36 | 1. Install dependencies 37 | ```bash 38 | pnpm install 39 | ``` 40 | 2. Copy the `.env.example` file, then rename it to `.env`, and add your [OpenAI API key](https://platform.openai.com/account/api-keys) to the `.env` file. 41 | ```bash 42 | OPENAI_API_KEY=sk-xxx... 43 | ``` 44 | 3. Run the application, the local project runs on `http://localhost:3000/` 45 | ```bash 46 | pnpm run dev 47 | ``` 48 | 49 | ## Deploy 50 | 51 | ### Deploy With Vercel 52 | 53 | [](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fddiu8081%2Fchatgpt-demo&env=OPENAI_API_KEY&envDescription=OpenAI%20API%20Key&envLink=https%3A%2F%2Fplatform.openai.com%2Faccount%2Fapi-keys) 54 | 55 | 56 | 57 | > #### 🔒 Need website password? 58 | > 59 | > Deploy with the [`SITE_PASSWORD`](#environment-variables) 60 | > 61 | > 62 | 63 |  64 | 65 | 66 | ### Deploy With Netlify 67 | 68 | [](https://app.netlify.com/start/deploy?repository=https://github.com/ddiu8081/chatgpt-demo#OPENAI_API_KEY=&HTTPS_PROXY=&OPENAI_API_BASE_URL=&HEAD_SCRIPTS=&PUBLIC_SECRET_KEY=&OPENAI_API_MODEL=&SITE_PASSWORD=) 69 | 70 | **Step-by-step deployment tutorial:** 71 | 72 | 1. [Fork](https://github.com/ddiu8081/chatgpt-demo/fork) this project,Go to [https://app.netlify.com/start](https://app.netlify.com/start) new Site, select the project you `forked` done, and connect it with your `GitHub` account. 73 | 74 |  75 | 76 |  77 | 78 | 79 | 2. Select the branch you want to deploy, then configure environment variables in the project settings. 80 | 81 |  82 | 83 | 3. Select the default build command and output directory, Click the `Deploy Site` button to start deploying the site。 84 | 85 |  86 | 87 | 88 | ### Deploy with Docker 89 | 90 | Environment variables refer to the documentation below. [Docker Hub address](https://hub.docker.com/r/ddiu8081/chatgpt-demo). 91 | 92 | **Direct run** 93 | ```bash 94 | docker run --name=chatgpt-demo -e OPENAI_API_KEY=YOUR_OPEN_API_KEY -p 3000:3000 -d ddiu8081/chatgpt-demo:latest 95 | ``` 96 | `-e` define environment variables in the container. 97 | 98 | 99 | **Docker compose** 100 | ```yml 101 | version: '3' 102 | 103 | services: 104 | chatgpt-demo: 105 | image: ddiu8081/chatgpt-demo:latest 106 | container_name: chatgpt-demo 107 | restart: always 108 | ports: 109 | - '3000:3000' 110 | environment: 111 | - OPENAI_API_KEY=YOUR_OPEN_API_KEY 112 | # - HTTPS_PROXY=YOUR_HTTPS_PROXY 113 | # - OPENAI_API_BASE_URL=YOUR_OPENAI_API_BASE_URL 114 | # - HEAD_SCRIPTS=YOUR_HEAD_SCRIPTS 115 | # - PUBLIC_SECRET_KEY=YOUR_SECRET_KEY 116 | # - SITE_PASSWORD=YOUR_SITE_PASSWORD 117 | # - OPENAI_API_MODEL=YOUR_OPENAI_API_MODEL 118 | ``` 119 | 120 | ```bash 121 | # start 122 | docker compose up -d 123 | # down 124 | docker-compose down 125 | ``` 126 | 127 | ### Deploy on more servers 128 | 129 | Please refer to the official deployment documentation:https://docs.astro.build/en/guides/deploy 130 | 131 | ## Environment Variables 132 | 133 | You can control the website through environment variables. 134 | 135 | | Name | Description | Default | 136 | | --- | --- | --- | 137 | | `OPENAI_API_KEY` | Your API Key for OpenAI. | `null` | 138 | | `HTTPS_PROXY` | Provide proxy for OpenAI API. e.g. `http://127.0.0.1:7890` | `null` | 139 | | `OPENAI_API_BASE_URL` | Custom base url for OpenAI API. | `https://api.openai.com` | 140 | | `HEAD_SCRIPTS` | Inject analytics or other scripts before `` of the page | `null` | 141 | | `PUBLIC_SECRET_KEY` | Secret string for the project. Use for generating signatures for API calls | `null` | 142 | | `SITE_PASSWORD` | Set password for site, support multiple password separated by comma. If not set, site will be public | `null` | 143 | | `OPENAI_API_MODEL` | ID of the model to use. [List models](https://platform.openai.com/docs/api-reference/models/list) | `gpt-3.5-turbo` | 144 | 145 | ## Enable Automatic Updates 146 | 147 | After forking the project, you need to manually enable Workflows and Upstream Sync Action on the Actions page of the forked project. Once enabled, automatic updates will be scheduled every day: 148 | 149 |  150 | 151 | 152 | ## Frequently Asked Questions 153 | 154 | Q: TypeError: fetch failed (can't connect to OpenAI Api) 155 | 156 | A: Configure environment variables `HTTPS_PROXY`,reference: https://github.com/ddiu8081/chatgpt-demo/issues/34 157 | 158 | Q: throw new TypeError(${context} is not a ReadableStream.) 159 | 160 | A: The Node version needs to be `v18` or later,reference: https://github.com/ddiu8081/chatgpt-demo/issues/65 161 | 162 | Q: Accelerate domestic access without the need for proxy deployment tutorial? 163 | 164 | A: You can refer to this tutorial: https://github.com/ddiu8081/chatgpt-demo/discussions/270 165 | 166 | Q: `PWA` is not working? 167 | 168 | A: Current `PWA` does not support deployment on Netlify, you can choose vercel or node deployment. 169 | ## Contributing 170 | 171 | This project exists thanks to all those who contributed. 172 | 173 | Thank you to all our supporters!🙏 174 | 175 | [](https://github.com/ddiu8081/chatgpt-demo/graphs/contributors) 176 | 177 | ## License 178 | 179 | MIT © [ddiu8081](https://github.com/ddiu8081/chatgpt-demo/blob/main/LICENSE) 180 | -------------------------------------------------------------------------------- /src/components/Generator.tsx: -------------------------------------------------------------------------------- 1 | import { Index, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js' 2 | import { useThrottleFn } from 'solidjs-use' 3 | import { generateSignature } from '@/utils/auth' 4 | import IconClear from './icons/Clear' 5 | import MessageItem from './MessageItem' 6 | import SystemRoleSettings from './SystemRoleSettings' 7 | import ErrorMessageItem from './ErrorMessageItem' 8 | import type { ChatMessage, ErrorMessage } from '@/types' 9 | 10 | export default () => { 11 | let inputRef: HTMLTextAreaElement 12 | const [currentSystemRoleSettings, setCurrentSystemRoleSettings] = createSignal('') 13 | const [systemRoleEditing, setSystemRoleEditing] = createSignal(false) 14 | const [messageList, setMessageList] = createSignal([]) 15 | const [currentError, setCurrentError] = createSignal() 16 | const [currentAssistantMessage, setCurrentAssistantMessage] = createSignal('') 17 | const [loading, setLoading] = createSignal(false) 18 | const [controller, setController] = createSignal(null) 19 | const [isStick, setStick] = createSignal(false) 20 | 21 | createEffect(() => (isStick() && smoothToBottom())) 22 | 23 | onMount(() => { 24 | let lastPostion = window.scrollY 25 | 26 | window.addEventListener('scroll', () => { 27 | const nowPostion = window.scrollY 28 | nowPostion < lastPostion && setStick(false) 29 | lastPostion = nowPostion 30 | }) 31 | 32 | try { 33 | if (localStorage.getItem('messageList')) 34 | setMessageList(JSON.parse(localStorage.getItem('messageList'))) 35 | 36 | if (localStorage.getItem('systemRoleSettings')) 37 | setCurrentSystemRoleSettings(localStorage.getItem('systemRoleSettings')) 38 | 39 | if (localStorage.getItem('stickToBottom') === 'stick') 40 | setStick(true) 41 | } catch (err) { 42 | console.error(err) 43 | } 44 | 45 | window.addEventListener('beforeunload', handleBeforeUnload) 46 | onCleanup(() => { 47 | window.removeEventListener('beforeunload', handleBeforeUnload) 48 | }) 49 | }) 50 | 51 | const handleBeforeUnload = () => { 52 | localStorage.setItem('messageList', JSON.stringify(messageList())) 53 | localStorage.setItem('systemRoleSettings', currentSystemRoleSettings()) 54 | isStick() ? localStorage.setItem('stickToBottom', 'stick') : localStorage.removeItem('stickToBottom') 55 | } 56 | 57 | const handleButtonClick = async() => { 58 | const inputValue = inputRef.value 59 | if (!inputValue) 60 | return 61 | 62 | inputRef.value = '' 63 | setMessageList([ 64 | ...messageList(), 65 | { 66 | role: 'user', 67 | content: inputValue, 68 | }, 69 | ]) 70 | requestWithLatestMessage() 71 | instantToBottom() 72 | } 73 | 74 | const smoothToBottom = useThrottleFn(() => { 75 | window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' }) 76 | }, 300, false, true) 77 | 78 | const instantToBottom = () => { 79 | window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' }) 80 | } 81 | 82 | const requestWithLatestMessage = async() => { 83 | setLoading(true) 84 | setCurrentAssistantMessage('') 85 | setCurrentError(null) 86 | const storagePassword = localStorage.getItem('pass') 87 | try { 88 | const controller = new AbortController() 89 | setController(controller) 90 | const requestMessageList = [...messageList()] 91 | if (currentSystemRoleSettings()) { 92 | requestMessageList.unshift({ 93 | role: 'system', 94 | content: currentSystemRoleSettings(), 95 | }) 96 | } 97 | const timestamp = Date.now() 98 | const response = await fetch('/api/generate', { 99 | method: 'POST', 100 | body: JSON.stringify({ 101 | messages: requestMessageList, 102 | time: timestamp, 103 | pass: storagePassword, 104 | sign: await generateSignature({ 105 | t: timestamp, 106 | m: requestMessageList?.[requestMessageList.length - 1]?.content || '', 107 | }), 108 | }), 109 | signal: controller.signal, 110 | }) 111 | if (!response.ok) { 112 | const error = await response.json() 113 | console.error(error.error) 114 | setCurrentError(error.error) 115 | throw new Error('Request failed') 116 | } 117 | const data = response.body 118 | if (!data) 119 | throw new Error('No data') 120 | 121 | const reader = data.getReader() 122 | const decoder = new TextDecoder('utf-8') 123 | let done = false 124 | 125 | while (!done) { 126 | const { value, done: readerDone } = await reader.read() 127 | if (value) { 128 | const char = decoder.decode(value) 129 | if (char === '\n' && currentAssistantMessage().endsWith('\n')) 130 | continue 131 | 132 | if (char) 133 | setCurrentAssistantMessage(currentAssistantMessage() + char) 134 | 135 | isStick() && instantToBottom() 136 | } 137 | done = readerDone 138 | } 139 | } catch (e) { 140 | console.error(e) 141 | setLoading(false) 142 | setController(null) 143 | return 144 | } 145 | archiveCurrentMessage() 146 | isStick() && instantToBottom() 147 | } 148 | 149 | const archiveCurrentMessage = () => { 150 | if (currentAssistantMessage()) { 151 | setMessageList([ 152 | ...messageList(), 153 | { 154 | role: 'assistant', 155 | content: currentAssistantMessage(), 156 | }, 157 | ]) 158 | setCurrentAssistantMessage('') 159 | setLoading(false) 160 | setController(null) 161 | inputRef.focus() 162 | } 163 | } 164 | 165 | const clear = () => { 166 | inputRef.value = '' 167 | inputRef.style.height = 'auto' 168 | setMessageList([]) 169 | setCurrentAssistantMessage('') 170 | setCurrentError(null) 171 | } 172 | 173 | const stopStreamFetch = () => { 174 | if (controller()) { 175 | controller().abort() 176 | archiveCurrentMessage() 177 | } 178 | } 179 | 180 | const retryLastFetch = () => { 181 | if (messageList().length > 0) { 182 | const lastMessage = messageList()[messageList().length - 1] 183 | if (lastMessage.role === 'assistant') 184 | setMessageList(messageList().slice(0, -1)) 185 | 186 | requestWithLatestMessage() 187 | } 188 | } 189 | 190 | const handleKeydown = (e: KeyboardEvent) => { 191 | if (e.isComposing || e.shiftKey) 192 | return 193 | 194 | if (e.keyCode === 13) { 195 | e.preventDefault() 196 | handleButtonClick() 197 | } 198 | } 199 | 200 | return ( 201 | 202 | messageList().length === 0} 204 | systemRoleEditing={systemRoleEditing} 205 | setSystemRoleEditing={setSystemRoleEditing} 206 | currentSystemRoleSettings={currentSystemRoleSettings} 207 | setCurrentSystemRoleSettings={setCurrentSystemRoleSettings} 208 | /> 209 | 210 | {(message, index) => ( 211 | (message().role === 'assistant' && index === messageList().length - 1)} 215 | onRetry={retryLastFetch} 216 | /> 217 | )} 218 | 219 | {currentAssistantMessage() && ( 220 | 224 | )} 225 | { currentError() && } 226 | ( 229 | 230 | AI is thinking... 231 | Stop 232 | 233 | )} 234 | > 235 | 236 | { 244 | inputRef.style.height = 'auto' 245 | inputRef.style.height = `${inputRef.scrollHeight}px` 246 | }} 247 | rows="1" 248 | class="gen-textarea" 249 | /> 250 | 251 | Send 252 | 253 | 254 | 255 | 256 | 257 | 258 | 259 | 260 | setStick(!isStick())}> 261 | 262 | 263 | 264 | 265 | 266 | ) 267 | } 268 | --------------------------------------------------------------------------------
Based on OpenAI API ({model}).
Gently instruct the assistant and set the behavior of the assistant.