[
  {
    "path": ".dockerignore",
    "content": "*.md\r\nDockerfile\r\ndocker-compose.yml\r\nLICENSE\r\nnetlify.toml\r\nvercel.json\r\nnode_modules\r\n.vscode\r\n"
  },
  {
    "path": ".eslintignore",
    "content": "dist\npublic\nnode_modules\n.netlify\n.vercel\n.github\n.changeset\n"
  },
  {
    "path": ".eslintrc.js",
    "content": "module.exports = {\n  extends: ['@evan-yang', 'plugin:astro/recommended'],\n  rules: {\n    'no-console': ['error', { allow: ['error'] }],\n    'react/display-name': 'off',\n    'react-hooks/rules-of-hooks': 'off',\n    '@typescript-eslint/no-use-before-define': 'off',\n  },\n  overrides: [\n    {\n      files: ['*.astro'],\n      parser: 'astro-eslint-parser',\n      parserOptions: {\n        parser: '@typescript-eslint/parser',\n        extraFileExtensions: ['.astro'],\n      },\n      rules: {\n        'no-mixed-spaces-and-tabs': ['error', 'smart-tabs'],\n      },\n    },\n    {\n      // Define the configuration for `<script>` tag.\n      // Script in `<script>` is assigned a virtual file name with the `.js` extension.\n      files: ['**/*.astro/*.js', '*.astro/*.js'],\n      parser: '@typescript-eslint/parser',\n      rules: {\n        'prettier/prettier': 'off',\n      },\n    },\n  ],\n}\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bug_report_when_use.yml",
    "content": "name: 🐞 Bug report (When using)\ndescription: Report an issue or possible bug when using `chatgpt-demo`\nlabels: ['pending triage', 'use']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### Before submitting...\n        Thanks for taking the time to fill out this bug report! Please confirm the following points before submitting:\n\n        ✅ I have checked the bug was not already reported by searching on GitHub under issues.\n        ✅ Use English to ask questions. This allows more people to search and participate in the issue.\n  - type: input\n    id: os\n    attributes:\n      label: What operating system are you using?\n      placeholder: Mac, Windows, Linux\n    validations:\n      required: true\n  - type: input\n    id: browser\n    attributes:\n      label: What browser are you using?\n      placeholder: Chrome, Firefox, Safari\n    validations:\n      required: true\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of what the bug is.\n      placeholder: Bug description\n    validations:\n      required: true\n  - type: textarea\n    id: prompt\n    attributes:\n      label: What prompt did you enter?\n      description: If the issue is related to the prompt you entered, please fill in this field.\n  - type: textarea\n    id: console-logs\n    attributes:\n      label: Console Logs\n      description: Please check your browser and fill in the error message if it exists.\n  - type: checkboxes\n    id: will-pr\n    attributes:\n      label: Participation\n      options:\n        - label: I am willing to submit a pull request for this issue.\n          required: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/bus_report_when_deploying.yml",
    "content": "name: 🐞 Bug report (When self-deploying)\ndescription: Report an issue or possible bug when deploy to your own server or cloud.\nlabels: ['pending triage', 'deploy']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### Before submitting...\n        Thanks for taking the time to fill out this bug report! Please confirm the following points before submitting:\n\n        ✅ I am using **latest version of chatgpt-demo**. \n        ✅ I have checked the bug was not already reported by searching on GitHub under issues.\n        ✅ Use English to ask questions. This allows more people to search and participate in the issue.\n  - type: dropdown\n    id: server\n    attributes:\n      label: How is Anse deployed?\n      description: Select the used deployment method.\n      options:\n        - Node\n        - Docker\n        - Vercel\n        - Netlify\n        - Railway\n        - Others (Specify in description)\n    validations:\n      required: true\n  - type: textarea\n    id: bug-description\n    attributes:\n      label: Describe the bug\n      description: A clear and concise description of what the bug is.\n      placeholder: Bug description\n    validations:\n      required: true\n  - type: textarea\n    id: console-logs\n    attributes:\n      label: Console Logs\n      description: Please check your browser and node console, fill in the error message if it exists.\n  - type: checkboxes\n    id: will-pr\n    attributes:\n      label: Participation\n      options:\n        - label: I am willing to submit a pull request for this issue.\n          required: false"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/config.yml",
    "content": "blank_issues_enabled: false\ncontact_links:\n  - name: 💬 Discussions\n    url: https://github.com/anse-app/chatgpt-demo/discussions\n    about: Use discussions if you have an idea for improvement or for asking questions."
  },
  {
    "path": ".github/ISSUE_TEMPLATE/feature_request.yml",
    "content": "name: 🚀 Feature request\ndescription: Suggest a feature or an improvement\nlabels: ['enhancement']\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ### Before submitting...\n        Thank you for taking the time to fill out this feature request! Please confirm the following points before submitting:\n\n        ✅ I have checked the feature was not already submitted by searching on GitHub under issues or discussions.\n        ✅ Use English. This allows more people to search and participate in the issue.\n  - type: textarea\n    id: feature-description\n    attributes:\n      label: Describe the feature\n      description: A clear and concise description of what you think would be a helpful addition.\n    validations:\n      required: true\n  - type: textarea\n    id: additional-context\n    attributes:\n      label: Additional context\n      description: Any other context or screenshots about the feature request here.\n  - type: checkboxes\n    id: will-pr\n    attributes:\n      label: Participation\n      options:\n        - label: I am willing to submit a pull request for this feature.\n          required: false\n"
  },
  {
    "path": ".github/ISSUE_TEMPLATE/typo.yml",
    "content": "name: 👀 Typo / Grammar fix\ndescription: You can just go ahead and send a PR! Thank you!\nlabels: []\nbody:\n  - type: markdown\n    attributes:\n      value: |\n        ## PR Welcome!\n\n        If the typo / grammar issue is trivial and straightforward, you can help by **directly sending a quick pull request**!\n        If you spot multiple of them, we suggest combining them into a single PR. Thanks!\n  - type: textarea\n    id: context\n    attributes:\n      label: Additional context"
  },
  {
    "path": ".github/PULL_REQUEST_TEMPLATE.md",
    "content": "<!-- DO NOT IGNORE THE TEMPLATE!\nThank you for contributing!\nBefore submitting the PR, please make sure you do the following:\n- Discuss first. It's always better to open a feature request issue first to discuss with the maintainers whether the feature is desired and the design of those features.\n- Use [Conventional Commits](https://www.conventionalcommits.org/) for commit messages.\n- Check that there isn't already a PR that solves the problem the same way to avoid creating a duplicate.\n-->\n\n### Description\n\n<!-- Please insert your description here and provide especially info about the \"what\" this PR is solving -->\n\n### Linked Issues\n\n\n### Additional context\n\n<!-- e.g. is there anything you'd like reviewers to focus on? -->"
  },
  {
    "path": ".github/workflows/build-docker.yml",
    "content": "name: build_docker\n\non:\n  push:\n    branches: [main]\n\njobs:\n  build_docker:\n    name: Build docker\n    runs-on: ubuntu-latest\n    steps:\n      - name: Checkout\n        uses: actions/checkout@v3\n\n      - name: Set up QEMU\n        uses: docker/setup-qemu-action@v2\n      - name: Set up Docker Buildx\n        uses: docker/setup-buildx-action@v2\n      - name: Login to DockerHub\n        uses: docker/login-action@v2\n        with:\n        # https://hub.docker.com/settings/security?generateToken=true\n          username: ${{ secrets.DOCKERHUB_USERNAME }}\n          password: ${{ secrets.DOCKERHUB_TOKEN }}\n      - name: Build and push\n        id: docker_build\n        uses: docker/build-push-action@v4\n        with:\n          context: .\n          push: true\n          labels: ${{ steps.meta.outputs.labels }}\n          platforms: linux/amd64,linux/arm64\n          tags: |\n            ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-demo:${{ github.ref_name }}\n            ${{ secrets.DOCKERHUB_USERNAME }}/chatgpt-demo:latest\n"
  },
  {
    "path": ".github/workflows/lint.yml",
    "content": "name: Lint CI\n\non:\n  push:\n    branches:\n      - main\n\n  pull_request:\n    branches:\n      - main\n\njobs:\n  lint:\n    runs-on: ubuntu-latest\n    steps:\n      - uses: actions/checkout@v3\n\n      - name: Install pnpm\n        uses: pnpm/action-setup@v2\n        with:\n          version: latest\n\n      - name: Set node\n        uses: actions/setup-node@v3\n        with:\n          node-version: 18.x\n          cache: pnpm\n\n      - name: Install\n        run: pnpm install --no-frozen-lockfile\n\n      - name: Lint\n        run: pnpm run lint\n"
  },
  {
    "path": ".github/workflows/main.yml",
    "content": "name: Create and publish a Docker image\n\non:\n  push:\n    branches: ['main']\n\nenv:\n  REGISTRY: ghcr.io\n  IMAGE_NAME: ${{ github.repository }}\n\njobs:\n  build-and-push-image:\n    runs-on: ubuntu-latest\n    permissions:\n      contents: read\n      packages: write\n\n    steps:\n      - name: Checkout repository\n        uses: actions/checkout@v3\n\n      - name: Log in to the Container registry\n        uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9\n        with:\n          registry: ${{ env.REGISTRY }}\n          username: ${{ github.actor }}\n          password: ${{ secrets.GITHUB_TOKEN }}\n\n      - name: Extract metadata (tags, labels) for Docker\n        id: meta\n        uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38\n        with:\n          images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\n\n      - name: Build and push Docker image\n        uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc\n        with:\n          context: .\n          push: true\n          tags: ${{ steps.meta.outputs.tags }}\n          labels: ${{ steps.meta.outputs.labels }}\n"
  },
  {
    "path": ".github/workflows/sync.yml",
    "content": "name: Upstream Sync\n\npermissions:\n  contents: write\n\non:\n  schedule:\n    - cron: \"0 0 * * *\" # every day\n  workflow_dispatch:\n\njobs:\n  sync_latest_from_upstream:\n    name: Sync latest commits from upstream repo\n    runs-on: ubuntu-latest\n    if: ${{ github.event.repository.fork }}\n\n    steps:\n      # Step 1: run a standard checkout action\n      - name: Checkout target repo\n        uses: actions/checkout@v3\n\n      # Step 2: run the sync action\n      - name: Sync upstream changes\n        id: sync\n        uses: aormsby/Fork-Sync-With-Upstream-action@v3.4\n        with:\n          upstream_sync_repo: anse-app/chatgpt-demo\n          upstream_sync_branch: main\n          target_sync_branch: main\n          target_repo_token: ${{ secrets.GITHUB_TOKEN }} # automatically generated, no need to set\n\n          # Set test_mode true to run tests instead of the true action!!\n          test_mode: false\n\n      - name: Sync check\n        if: failure()\n        run: |\n          echo \"::error::由于权限不足，导致同步失败（这是预期的行为），请前往仓库首页手动执行[Sync fork]。\"\n          echo \"::error::Due to insufficient permissions, synchronization failed (as expected). Please go to the repository homepage and manually perform [Sync fork].\"\n          exit 1\n"
  },
  {
    "path": ".gitignore",
    "content": "# build output\ndist/\n.vercel/\n.netlify/\n\n# generated types\n.astro/\n\n# dependencies\nnode_modules/\n\n# logs\nnpm-debug.log*\nyarn-debug.log*\nyarn-error.log*\npnpm-debug.log*\n\n# environment variables\n.env\n.env.production\n\n# macOS-specific files\n.DS_Store\n\n# Local\n*.local\n\n**/.DS_Store\n\n# Editor directories and files\n.idea\n"
  },
  {
    "path": ".npmrc",
    "content": "registry=https://registry.npmjs.org/\nstrict-peer-dependencies=false\nauto-install-peers=true\nshamefully-hoist=true\n"
  },
  {
    "path": ".vscode/extensions.json",
    "content": "{\n  \"recommendations\": [\"astro-build.astro-vscode\",\"dbaeumer.vscode-eslint\",\"antfu.unocss\"],\n  \"unwantedRecommendations\": [],\n}\n"
  },
  {
    "path": ".vscode/launch.json",
    "content": "{\n  \"version\": \"0.2.0\",\n  \"configurations\": [\n    {\n      \"command\": \"./node_modules/.bin/astro dev\",\n      \"name\": \"Development server\",\n      \"request\": \"launch\",\n      \"type\": \"node-terminal\"\n    }\n  ]\n}\n"
  },
  {
    "path": ".vscode/settings.json",
    "content": "{\n  \"editor.codeActionsOnSave\": {\n    \"source.fixAll.eslint\": true\n  },\n  \"editor.formatOnSave\": false,\n  \"eslint.validate\": [\n      \"javascript\",\n      \"javascriptreact\",\n      \"astro\", // Enable .astro\n      \"typescript\", // Enable .ts\n      \"typescriptreact\" // Enable .tsx\n  ]\n}\n"
  },
  {
    "path": "Dockerfile",
    "content": "FROM node:alpine as builder\nWORKDIR /usr/src\nRUN npm install -g pnpm\nCOPY . .\nRUN pnpm install\nRUN pnpm run build\n\nFROM node:alpine\nWORKDIR /usr/src\nRUN npm install -g pnpm\nCOPY --from=builder /usr/src/dist ./dist\nCOPY --from=builder /usr/src/hack ./\nCOPY package.json pnpm-lock.yaml ./\nRUN pnpm install\nENV HOST=0.0.0.0 PORT=3000 NODE_ENV=production\nEXPOSE $PORT\nCMD [\"/bin/sh\", \"docker-entrypoint.sh\"]\n"
  },
  {
    "path": "LICENSE",
    "content": "MIT License\n\nCopyright (c) 2023 Diu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy\nof this software and associated documentation files (the \"Software\"), to deal\nin the Software without restriction, including without limitation the rights\nto use, copy, modify, merge, publish, distribute, sublicense, and/or sell\ncopies of the Software, and to permit persons to whom the Software is\nfurnished to do so, subject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,\nFITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE\nAUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER\nLIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,\nOUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE\nSOFTWARE.\n"
  },
  {
    "path": "README.md",
    "content": "# ChatGPT-API Demo\n\nEnglish | [简体中文](./README.zh-CN.md)\n\nA demo repo based on [OpenAI GPT-3.5 Turbo API.](https://platform.openai.com/docs/guides/chat)\n\n**🍿 Live preview**: https://chatgpt.ddiu.me\n\n> ⚠️ Notice: Our API Key limit has been exhausted. So the demo site is not available now.\n\n![chat-logo](https://cdn.jsdelivr.net/gh/yzh990918/static@master/chat-logo.webp)\n\n## Introducing `Anse`\n\nLooking for multi-chat, image-generation, and more powerful features? Take a look at our newly launched [Anse](https://github.com/anse-app/anse).\n\nMore info on https://github.com/ddiu8081/chatgpt-demo/discussions/247.\n\n[![image](https://user-images.githubusercontent.com/1998168/235048408-ca4015f5-4d3c-4c64-9a6c-9069a89cd23a.png)](https://github.com/anse-app/anse)\n\n## Running Locally\n\n### Pre environment\n1. **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.\n   ```bash\n    node -v\n   ```\n2. **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:\n   ```bash\n    npm i -g pnpm\n   ```\n3. **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).\n\n### Getting Started\n\n1. Install dependencies\n   ```bash\n    pnpm install\n   ```\n2. 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.\n   ```bash\n    OPENAI_API_KEY=sk-xxx...\n   ```\n3. Run the application, the local project runs on `http://localhost:3000/`\n   ```bash\n    pnpm run dev\n   ```\n\n## Deploy\n\n### Deploy With Vercel\n\n[![Deploy with Vercel](https://vercel.com/button)](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)\n\n\n\n> #### 🔒 Need website password?\n>\n> Deploy with the [`SITE_PASSWORD`](#environment-variables)\n>\n> <a href=\"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fddiu8081%2Fchatgpt-demo&env=OPENAI_API_KEY&env=SITE_PASSWORD&envDescription=OpenAI%20API%20Key&envLink=https%3A%2F%2Fplatform.openai.com%2Faccount%2Fapi-keys\" alt=\"Deploy with Vercel\" target=\"_blank\"><img src=\"https://vercel.com/button\" alt=\"Deploy with Vercel\" height=24 style=\"vertical-align: middle; margin-right: 4px;\"></a>\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230310/image.4wzfb79qt7k0.webp)\n\n\n### Deploy With Netlify\n\n[![Deploy with Netlify](https://www.netlify.com/img/deploy/button.svg)](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=)\n\n**Step-by-step deployment tutorial:**\n\n1. [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.\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230310/image.3nlt4hgzb16o.webp)\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230310/image.5fhfouap270g.webp)\n\n\n2. Select the branch you want to deploy, then configure environment variables in the project settings.\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230311/image.gfs9lx8c854.webp)\n\n3. Select the default build command and output directory, Click the `Deploy Site` button to start deploying the site.\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230311/image.4jky9e1wbojk.webp)\n\n\n### Deploy with Docker\n\nEnvironment variables refer to the documentation below. [Docker Hub address](https://hub.docker.com/r/ddiu8081/chatgpt-demo).\n\n**Direct run**\n```bash\ndocker run --name=chatgpt-demo -e OPENAI_API_KEY=YOUR_OPEN_API_KEY -p 3000:3000 -d ddiu8081/chatgpt-demo:latest\n```\n`-e` define environment variables in the container.\n\n\n**Docker compose**\n```yml\nversion: '3'\n\nservices:\n  chatgpt-demo:\n    image: ddiu8081/chatgpt-demo:latest\n    container_name: chatgpt-demo\n    restart: always\n    ports:\n      - '3000:3000'\n    environment:\n      - OPENAI_API_KEY=YOUR_OPEN_API_KEY\n      # - HTTPS_PROXY=YOUR_HTTPS_PROXY\n      # - OPENAI_API_BASE_URL=YOUR_OPENAI_API_BASE_URL\n      # - HEAD_SCRIPTS=YOUR_HEAD_SCRIPTS\n      # - PUBLIC_SECRET_KEY=YOUR_SECRET_KEY\n      # - SITE_PASSWORD=YOUR_SITE_PASSWORD\n      # - OPENAI_API_MODEL=YOUR_OPENAI_API_MODEL\n```\n\n```bash\n# start\ndocker compose up -d\n# down\ndocker-compose down\n```\n\n### Deploy with Sealos\n\n 1.Register a Sealos account for free [sealos cloud](https://cloud.sealos.io)\n\n2.Click  `App Launchpad` button\n\n![App Launchpad](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-1.34i8gi80j268.webp)\n\n3.Click `Create Application` button\n\n![Create Application](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-2.4t8q5px18eps.webp)\n\n4.Just fill in according to the following figure, and click on it after filling out `Deploy Application` button\n\n![Create Application](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-3.5x5exqk0o8lc.webp)\n\n```shell\nApp Name: chatgpt-demo\nImage Name: ddiu8081/chatgpt-demo:latest\nCPU: 0.5Core\nMemory: 1G\nContainer Ports: 3000\nAccessible to the Public: On\nEnvironment: OPENAI_API_KEY=YOUR_OPEN_API_KEY\n```\n\n5.Obtain the access link and click directly to access it. If you need to bind your own domain name, you can also fill in your own domain name in `Custom domain` and follow the prompts to configure the domain name CNAME\n\n![Create Application](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-4.4esqkqu70z9c.webp)\n\n6.Wait for one to two minutes and open this link\n\n![Open Link](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-5.5cgfpee3zeyo.webp)\n\n### Deploy on more servers\n\nPlease refer to the official deployment documentation: https://docs.astro.build/en/guides/deploy\n\n## Environment Variables\n\nYou can control the website through environment variables.\n\n| Name | Description | Default |\n| --- | --- | --- |\n| `OPENAI_API_KEY` | Your API Key for OpenAI. | `null` |\n| `HTTPS_PROXY` | Provide proxy for OpenAI API. e.g. `http://127.0.0.1:7890` | `null` |\n| `OPENAI_API_BASE_URL` | Custom base url for OpenAI API. | `https://api.openai.com` |\n| `HEAD_SCRIPTS` | Inject analytics or other scripts before `</head>` of the page | `null` |\n| `PUBLIC_SECRET_KEY` | Secret string for the project. Use for generating signatures for API calls | `null` |\n| `SITE_PASSWORD` | Set password for site, support multiple password separated by comma. If not set, site will be public | `null` |\n| `OPENAI_API_MODEL` | ID of the model to use. [List models](https://platform.openai.com/docs/api-reference/models/list) | `gpt-3.5-turbo` |\n\n## Enable Automatic Updates\n\nAfter 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:\n\n![](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230518/image.2hhnrsrd2t1c.webp)\n\n\n## Frequently Asked Questions\n\nQ: TypeError: fetch failed (can't connect to OpenAI Api)\n\nA: Configure environment variables `HTTPS_PROXY`，reference: https://github.com/ddiu8081/chatgpt-demo/issues/34\n\nQ: throw new TypeError(${context} is not a ReadableStream.)\n\nA: The Node version needs to be `v18` or later, reference: https://github.com/ddiu8081/chatgpt-demo/issues/65\n\nQ: Accelerate domestic access without the need for proxy deployment tutorial?\n\nA: You can refer to this tutorial: https://github.com/ddiu8081/chatgpt-demo/discussions/270\n\n## Contributing\n\nThis project exists thanks to all those who contributed.\n\nThank you to all our supporters!🙏\n\n[![img](https://contributors.nn.ci/api?repo=ddiu8081/chatgpt-demo)](https://github.com/ddiu8081/chatgpt-demo/graphs/contributors)\n\n## License\n\nMIT © [ddiu8081](https://github.com/ddiu8081/chatgpt-demo/blob/main/LICENSE)\n"
  },
  {
    "path": "README.zh-CN.md",
    "content": "# ChatGPT-API Demo\n\n[English](./README.md) | 简体中文\n\n一个基于 [OpenAI GPT-3.5 Turbo API](https://platform.openai.com/docs/guides/chat) 的 demo。\n\n**🍿 在线预览**: https://chatgpt.ddiu.me\n\n**🏖️ V2 版本 (Beta)**: https://v2.chatgpt.ddiu.me\n\n> ⚠️ 注意：我们的 API 密钥限制已用尽。所以演示站点现在不可用。\n\n![chat-logo](https://cdn.jsdelivr.net/gh/yzh990918/static@master/chat-logo.webp)\n\n## 本地运行\n\n### 前置环境\n\n1. **Node**: 检查您的开发环境和部署环境是否都使用 `Node v18` 或更高版本。你可以使用 [nvm](https://github.com/nvm-sh/nvm) 管理本地多个 `node` 版本。\n   ```bash\n    node -v\n   ```\n2. **PNPM**: 我们推荐使用 [pnpm](https://pnpm.io/) 来管理依赖，如果你从来没有安装过 pnpm，可以使用下面的命令安装：\n   ```bash\n    npm i -g pnpm\n   ```\n3. **OPENAI_API_KEY**: 在运行此应用程序之前，您需要从 OpenAI 获取 API 密钥。您可以在 [https://beta.openai.com/signup](https://beta.openai.com/signup) 注册 API 密钥。\n\n### 起步运行\n\n1. 安装依赖\n   ```bash\n    pnpm install\n   ```\n2. 复制 `.env.example` 文件，重命名为 `.env`，并添加你的 [OpenAI API key](https://platform.openai.com/account/api-keys) 到 `.env` 文件中\n   ```bash\n    OPENAI_API_KEY=sk-xxx...\n   ```\n3. 运行应用，本地项目运行在 `http://localhost:3000/`\n   ```bash\n    pnpm run dev\n   ```\n\n## 部署\n\n### 部署在 Vercel\n\n[![Deploy with Vercel](https://vercel.com/button)](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)\n\n\n\n> ###### 🔒 需要站点密码？\n>\n> 携带[`SITE_PASSWORD`](#environment-variables)进行部署\n>\n> <a href=\"https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fddiu8081%2Fchatgpt-demo&env=OPENAI_API_KEY&env=SITE_PASSWORD&envDescription=OpenAI%20API%20Key&envLink=https%3A%2F%2Fplatform.openai.com%2Faccount%2Fapi-keys\" alt=\"Deploy with Vercel\" target=\"_blank\"><img src=\"https://vercel.com/button\" alt=\"Deploy with Vercel\" height=24 style=\"vertical-align: middle; margin-right: 4px;\"></a>\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230310/image.4wzfb79qt7k0.webp)\n\n### 部署在 Netlify\n\n[![Deploy with Netlify](https://www.netlify.com/img/deploy/button.svg)](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=)\n\n**分步部署教程：**\n\n1. [Fork](https://github.com/ddiu8081/chatgpt-demo/fork) 此项目，前往 [https://app.netlify.com/start](https://app.netlify.com/start) 新建站点，选择你 `fork` 完成的项目，将其与 `GitHub` 帐户连接。\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230310/image.3nlt4hgzb16o.webp)\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230310/image.5fhfouap270g.webp)\n\n\n2. 选择要部署的分支，选择 `main` 分支，在项目设置中配置环境变量，环境变量配置参考下文。\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230310/image.6dvtfmoijb7k.webp)\n\n3. 选择默认的构建命令和输出目录，单击 `Deploy Site` 按钮开始部署站点。\n\n![image](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230310/image.e0n7c0zaen4.webp)\n\n### 部署在 Docker\n部署之前请确认 `.env` 文件正常配置，环境变量参考下方文档，[Docker Hub address](https://hub.docker.com/r/ddiu8081/chatgpt-demo).\n\n**一键运行**\n```bash\ndocker run --name=chatgpt-demo -e OPENAI_API_KEY=YOUR_OPEN_API_KEY -p 3000:3000 -d ddiu8081/chatgpt-demo:latest\n```\n`-e` 在容器中定义环境变量。\n\n**使用 Docker compose**\n```yml\nversion: '3'\n\nservices:\n  chatgpt-demo:\n    image: ddiu8081/chatgpt-demo:latest\n    container_name: chatgpt-demo\n    restart: always\n    ports:\n      - '3000:3000'\n    environment:\n      - OPENAI_API_KEY=YOUR_OPEN_API_KEY\n      # - HTTPS_PROXY=YOUR_HTTPS_PROXY\n      # - OPENAI_API_BASE_URL=YOUR_OPENAI_API_BASE_URL\n      # - HEAD_SCRIPTS=YOUR_HEAD_SCRIPTS\n      # - PUBLIC_SECRET_KEY=YOUR_SECRET_KEY\n      # - SITE_PASSWORD=YOUR_SITE_PASSWORD\n      # - OPENAI_API_MODEL=YOUR_OPENAI_API_MODEL\n```\n\n```bash\n# start\ndocker compose up -d\n# down\ndocker-compose down\n```\n\n### Sealos 部署\n\n 1.注册 Sealos 免费账号 [sealos cloud](https://cloud.sealos.io)\n\n2.点击  `App Launchpad` 按钮\n\n![App Launchpad](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-1.34i8gi80j268.webp)\n\n3.点击 `Create Application` 按钮\n\n![Create Application](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-2.4t8q5px18eps.webp)\n\n4.按照下图填写后，点击 `Deploy Application` 按钮\n\n![Create Application](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-3.5x5exqk0o8lc.webp)\n\n```shell\nApp Name: chatgpt-demo\nImage Name: ddiu8081/chatgpt-demo:latest\nCPU: 0.5Core\nMemory: 1G\nContainer Ports: 3000\nAccessible to the Public: On\nEnvironment: OPENAI_API_KEY=YOUR_OPEN_API_KEY\n```\n\n5.获取访问链接。如果你需要自定义域名，可以点击 `Custom domain` 按钮后按照提示解析域名 CNAME\n\n![Create Application](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-4.4esqkqu70z9c.webp)\n\n6.等待 1-2 分钟后点击链接，即可进去页面\n\n![Open Link](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230609/install-on-sealos-5.5cgfpee3zeyo.webp)\n\n### 部署在更多的服务器\n\n请参考官方部署文档：https://docs.astro.build/en/guides/deploy\n\n## 环境变量\n\n配置本地或者部署的环境变量\n\n| 名称 | 描述 | 默认 |\n| --- | --- | --- |\n| `OPENAI_API_KEY` | 你的 OpenAI API Key | `null` |\n| `HTTPS_PROXY` | 为 OpenAI API 提供代理。e.g. `http://127.0.0.1:7890` | `null` |\n| `OPENAI_API_BASE_URL` | 请求 OpenAI API 的自定义 Base URL. | `https://api.openai.com` |\n| `HEAD_SCRIPTS` | 在页面的 `</head>` 之前注入分析或其他脚本 | `null` |\n| `PUBLIC_SECRET_KEY` | 项目的秘密字符串。用于生成 API 调用的签名 | `null` |\n| `SITE_PASSWORD` | 为网站设置密码，支持使用英文逗号创建多个密码。如果未设置，则该网站将是公开的 | `null` |\n| `OPENAI_API_MODEL` | 使用的 OpenAI 模型。[模型列表](https://platform.openai.com/docs/api-reference/models/list) | `gpt-3.5-turbo` |\n\n## 开启同步更新\n\nFork 项目后，您需要在 Fork 项目的操作页面上手动启用工作流和上游同步操作。启用后，每天都会执行自动更新：\n\n![](https://cdn.jsdelivr.net/gh/yzh990918/static@master/20230518/image.2hhnrsrd2t1c.webp)\n\n## 常见问题\n\nQ: TypeError: fetch failed (can't connect to OpenAI Api)\n\nA: 配置环境变量 `HTTPS_PROXY`，参考：https://github.com/ddiu8081/chatgpt-demo/issues/34\n\nQ: throw new TypeError(${context} is not a ReadableStream.)\n\nA: Node 版本需要在 `v18` 或者更高，参考：https://github.com/ddiu8081/chatgpt-demo/issues/65\n\nQ: Accelerate domestic access without the need for proxy deployment tutorial?\n\nA: 你可以参考此教程：https://github.com/ddiu8081/chatgpt-demo/discussions/270\n\n## 参与贡献\n\n这个项目的存在要感谢所有做出贡献的人。\n\n感谢我们所有的支持者！🙏\n\n[![img](https://contributors.nn.ci/api?repo=ddiu8081/chatgpt-demo)](https://github.com/ddiu8081/chatgpt-demo/graphs/contributors)\n\n## License\n\nMIT © [ddiu8081](https://github.com/ddiu8081/chatgpt-demo/blob/main/LICENSE)\n"
  },
  {
    "path": "astro.config.mjs",
    "content": "import { defineConfig } from 'astro/config'\nimport unocss from 'unocss/astro'\nimport solidJs from '@astrojs/solid-js'\n\nimport node from '@astrojs/node'\nimport AstroPWA from '@vite-pwa/astro'\nimport vercel from '@astrojs/vercel/edge'\nimport netlify from '@astrojs/netlify/edge-functions'\nimport disableBlocks from './plugins/disableBlocks'\n\nconst envAdapter = () => {\n  switch (process.env.OUTPUT) {\n    case 'vercel': return vercel()\n    case 'netlify': return netlify()\n    default: return node({ mode: 'standalone' })\n  }\n}\n\n// https://astro.build/config\nexport default defineConfig({\n  integrations: [\n    unocss(),\n    solidJs(),\n    AstroPWA({\n      registerType: 'autoUpdate',\n      injectRegister: 'inline',\n      manifest: {\n        name: 'ChatGPT-API Demo',\n        short_name: 'ChatGPT Demo',\n        description: 'A demo repo based on OpenAI API',\n        theme_color: '#212129',\n        background_color: '#ffffff',\n        icons: [\n          {\n            src: 'pwa-192.png',\n            sizes: '192x192',\n            type: 'image/png',\n          },\n          {\n            src: 'pwa-512.png',\n            sizes: '512x512',\n            type: 'image/png',\n          },\n          {\n            src: 'icon.svg',\n            sizes: '32x32',\n            type: 'image/svg',\n            purpose: 'any maskable',\n          },\n        ],\n      },\n      client: {\n        installPrompt: true,\n        periodicSyncForUpdates: 20,\n      },\n      devOptions: {\n        enabled: true,\n      },\n    }),\n  ],\n  output: 'server',\n  adapter: envAdapter(),\n  vite: {\n    plugins: [\n      process.env.OUTPUT === 'vercel' && disableBlocks(),\n      process.env.OUTPUT === 'netlify' && disableBlocks(),\n    ],\n  },\n})\n"
  },
  {
    "path": "docker-compose.yml",
    "content": "version: '3'\n\nservices:\n  chatgpt-demo:\n    image: ddiu8081/chatgpt-demo:latest\n    container_name: chatgpt-demo\n    restart: always\n    ports:\n        - \"3000:3000\"\n    environment:\n      - OPENAI_API_KEY=YOUR_OPENAI_API_KEY\n      # - HTTPS_PROXY=YOUR_HTTPS_PROXY\n      # - OPENAI_API_BASE_URL=YOUR_OPENAI_API_BASE_URL\n      # - HEAD_SCRIPTS=YOUR_HEAD_SCRIPTS\n      # - PUBLIC_SECRET_KEY=YOUR_SECRET_KEY\n      # - SITE_PASSWORD=YOUR_SITE_PASSWORD\n      # - OPENAI_API_MODEL=YOUR_OPENAI_API_MODEL\n\n"
  },
  {
    "path": "hack/docker-entrypoint.sh",
    "content": "#!/bin/sh\n\nsub_service_pid=\"\"\n\nsub_service_command=\"node dist/server/entry.mjs\"\n\nfunction init() {\n    /bin/sh ./docker-env-replace.sh\n}\n\nfunction main {\n  init\n\n  echo \"Starting service...\"\n  eval \"$sub_service_command &\"\n  sub_service_pid=$!\n\n  trap cleanup SIGTERM SIGINT\n  echo \"Running script...\"\n  while [ true ]; do\n      sleep 5\n  done\n}\n\nfunction cleanup {\n  echo \"Cleaning up!\"\n  kill -TERM $sub_service_pid\n}\n\nmain\n"
  },
  {
    "path": "hack/docker-env-replace.sh",
    "content": "#!/bin/sh\n\n# Your API Key for OpenAI\nopenai_api_key=$OPENAI_API_KEY\n# Provide proxy for OpenAI API. e.g. http://127.0.0.1:7890\nhttps_proxy=$HTTPS_PROXY\n# Custom base url for OpenAI API. default: https://api.openai.com\nopenai_api_base_url=$OPENAI_API_BASE_URL\n# Inject analytics or other scripts before </head> of the page\nhead_scripts=$HEAD_SCRIPTS\n# Secret string for the project. Use for generating signatures for API calls\npublic_secret_key=$PUBLIC_SECRET_KEY\n# Set password for site, support multiple password separated by comma. If not set, site will be public\nsite_password=$SITE_PASSWORD\n# ID of the model to use. https://platform.openai.com/docs/api-reference/models/list\nopenai_api_model=$OPENAI_API_MODEL\n\nfor file in $(find ./dist -type f -name \"*.mjs\"); do\n  sed \"s|({}).OPENAI_API_KEY|\\\"$openai_api_key\\\"|g;\n  s|({}).HTTPS_PROXY|\\\"$https_proxy\\\"|g;\n  s|({}).OPENAI_API_BASE_URL|\\\"$openai_api_base_url\\\"|g;\n  s|({}).HEAD_SCRIPTS|\\\"$head_scripts\\\"|g;\n  s|({}).PUBLIC_SECRET_KEY|\\\"$public_secret_key\\\"|g;\n  s|({}).OPENAI_API_MODEL|\\\"$openai_api_model\\\"|g;\n  s|({}).SITE_PASSWORD|\\\"$site_password\\\"|g\" $file > tmp\n  mv tmp $file\ndone\n\nrm -rf tmp\n"
  },
  {
    "path": "netlify.toml",
    "content": "[build.environment]\n  NETLIFY_USE_PNPM = \"true\"\n  NODE_VERSION = \"18\"\n\n[build]\n  command = \"OUTPUT=netlify astro build\"\n  publish = \"dist\"\n\n[[headers]]\n  for = \"/manifest.webmanifest\"\n  [headers.values]\n    Content-Type = \"application/manifest+json\"\n"
  },
  {
    "path": "package.json",
    "content": "{\n  \"name\": \"chatgpt-api-demo\",\n  \"version\": \"0.0.1\",\n  \"packageManager\": \"pnpm@7.28.0\",\n  \"scripts\": {\n    \"dev\": \"astro dev\",\n    \"start\": \"astro dev\",\n    \"build\": \"astro build\",\n    \"build:vercel\": \"OUTPUT=vercel astro build\",\n    \"build:netlify\": \"OUTPUT=netlify astro build\",\n    \"preview\": \"astro preview\",\n    \"astro\": \"astro\",\n    \"lint\": \"eslint . --ext .js,.jsx,.ts,.tsx,.astro\",\n    \"lint:fix\": \"eslint . --ext .js,.jsx,.ts,.tsx,.astro --fix\"\n  },\n  \"dependencies\": {\n    \"@astrojs/netlify\": \"2.3.0\",\n    \"@astrojs/node\": \"^5.3.0\",\n    \"@astrojs/solid-js\": \"^2.2.0\",\n    \"@astrojs/vercel\": \"^3.5.0\",\n    \"@zag-js/slider\": \"^0.16.0\",\n    \"@zag-js/solid\": \"^0.16.0\",\n    \"astro\": \"^2.7.0\",\n    \"eslint\": \"^8.43.0\",\n    \"eventsource-parser\": \"^1.0.0\",\n    \"highlight.js\": \"^11.8.0\",\n    \"js-sha256\": \"^0.9.0\",\n    \"katex\": \"^0.16.7\",\n    \"markdown-it\": \"^13.0.1\",\n    \"markdown-it-highlightjs\": \"^4.0.1\",\n    \"markdown-it-katex\": \"^2.0.3\",\n    \"solid-js\": \"1.7.6\",\n    \"solidjs-use\": \"^2.1.0\",\n    \"undici\": \"^5.22.1\"\n  },\n  \"devDependencies\": {\n    \"@evan-yang/eslint-config\": \"^1.0.9\",\n    \"@iconify-json/carbon\": \"^1.1.18\",\n    \"@types/markdown-it\": \"^12.2.3\",\n    \"@typescript-eslint/parser\": \"^5.60.0\",\n    \"@vite-pwa/astro\": \"^0.1.1\",\n    \"eslint-plugin-astro\": \"^0.27.1\",\n    \"punycode\": \"^2.3.0\",\n    \"unocss\": \"^0.50.8\"\n  }\n}\n"
  },
  {
    "path": "plugins/disableBlocks.ts",
    "content": "export default function plugin() {\n  const transform = (code: string, id: string) => {\n    if (id.includes('pages/api/generate.ts')) {\n      return {\n        code: code.replace(/^.*?#vercel-disable-blocks([\\s\\S]+?)#vercel-end.*$/gm, ''),\n        map: null,\n      }\n    }\n  }\n\n  return {\n    name: 'vercel-disable-blocks',\n    enforce: 'pre',\n    transform,\n  }\n}\n"
  },
  {
    "path": "shims.d.ts",
    "content": "import type { AttributifyAttributes } from '@unocss/preset-attributify'\n\n// declare module 'solid-js' {\n//   namespace JSX {\n//     interface HTMLAttributes<T> extends AttributifyAttributes {}\n//   }\n// }\n\ndeclare global {\n  namespace astroHTML.JSX {\n    interface HTMLAttributes extends AttributifyAttributes { }\n  }\n  namespace JSX {\n    interface HTMLAttributes<> extends AttributifyAttributes {}\n  }\n}\n"
  },
  {
    "path": "src/components/ErrorMessageItem.tsx",
    "content": "import IconRefresh from './icons/Refresh'\nimport type { ErrorMessage } from '@/types'\n\ninterface Props {\n  data: ErrorMessage\n  onRetry?: () => void\n}\n\nexport default ({ data, onRetry }: Props) => {\n  return (\n    <div class=\"my-4 px-4 py-3 border border-red/50 bg-red/10\">\n      {data.code && <div class=\"text-red mb-1\">{data.code}</div>}\n      <div class=\"text-red op-70 text-sm\">{data.message}</div>\n      {onRetry && (\n        <div class=\"fie px-3 mb-2\">\n          <div onClick={onRetry} class=\"gpt-retry-btn border-red/50 text-red\">\n            <IconRefresh />\n            <span>Regenerate</span>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Footer.astro",
    "content": "<footer>\n  <p mt-8 text-xs op-30>\n    <span pr-1>Made by</span>\n    <a\n      b-slate-link\n      href=\"https://ddiu.io\" target=\"_blank\"\n    >\n      Diu\n    </a>\n    <span px-1>|</span>\n    <a\n      b-slate-link\n      href=\"https://github.com/ddiu8081/chatgpt-demo\" target=\"_blank\"\n    >\n      Source Code\n    </a>\n  </p>\n</footer>\n"
  },
  {
    "path": "src/components/Generator.tsx",
    "content": "import { Index, Show, createEffect, createSignal, onCleanup, onMount } from 'solid-js'\nimport { useThrottleFn } from 'solidjs-use'\nimport { generateSignature } from '@/utils/auth'\nimport IconClear from './icons/Clear'\nimport MessageItem from './MessageItem'\nimport SystemRoleSettings from './SystemRoleSettings'\nimport ErrorMessageItem from './ErrorMessageItem'\nimport type { ChatMessage, ErrorMessage } from '@/types'\n\nexport default () => {\n  let inputRef: HTMLTextAreaElement\n  const [currentSystemRoleSettings, setCurrentSystemRoleSettings] = createSignal('')\n  const [systemRoleEditing, setSystemRoleEditing] = createSignal(false)\n  const [messageList, setMessageList] = createSignal<ChatMessage[]>([])\n  const [currentError, setCurrentError] = createSignal<ErrorMessage>()\n  const [currentAssistantMessage, setCurrentAssistantMessage] = createSignal('')\n  const [loading, setLoading] = createSignal(false)\n  const [controller, setController] = createSignal<AbortController>(null)\n  const [isStick, setStick] = createSignal(false)\n  const [temperature, setTemperature] = createSignal(0.6)\n  const temperatureSetting = (value: number) => { setTemperature(value) }\n  const maxHistoryMessages = parseInt(import.meta.env.PUBLIC_MAX_HISTORY_MESSAGES || '9')\n\n  createEffect(() => (isStick() && smoothToBottom()))\n\n  onMount(() => {\n    let lastPostion = window.scrollY\n\n    window.addEventListener('scroll', () => {\n      const nowPostion = window.scrollY\n      nowPostion < lastPostion && setStick(false)\n      lastPostion = nowPostion\n    })\n\n    try {\n      if (sessionStorage.getItem('messageList'))\n        setMessageList(JSON.parse(sessionStorage.getItem('messageList')))\n\n      if (sessionStorage.getItem('systemRoleSettings'))\n        setCurrentSystemRoleSettings(sessionStorage.getItem('systemRoleSettings'))\n\n      if (localStorage.getItem('stickToBottom') === 'stick')\n        setStick(true)\n    } catch (err) {\n      console.error(err)\n    }\n\n    window.addEventListener('beforeunload', handleBeforeUnload)\n    onCleanup(() => {\n      window.removeEventListener('beforeunload', handleBeforeUnload)\n    })\n  })\n\n  const handleBeforeUnload = () => {\n    sessionStorage.setItem('messageList', JSON.stringify(messageList()))\n    sessionStorage.setItem('systemRoleSettings', currentSystemRoleSettings())\n    isStick() ? localStorage.setItem('stickToBottom', 'stick') : localStorage.removeItem('stickToBottom')\n  }\n\n  const handleButtonClick = async() => {\n    const inputValue = inputRef.value\n    if (!inputValue)\n      return\n\n    inputRef.value = ''\n    setMessageList([\n      ...messageList(),\n      {\n        role: 'user',\n        content: inputValue,\n      },\n    ])\n    requestWithLatestMessage()\n    instantToBottom()\n  }\n\n  const smoothToBottom = useThrottleFn(() => {\n    window.scrollTo({ top: document.body.scrollHeight, behavior: 'smooth' })\n  }, 300, false, true)\n\n  const instantToBottom = () => {\n    window.scrollTo({ top: document.body.scrollHeight, behavior: 'instant' })\n  }\n\n  const requestWithLatestMessage = async() => {\n    setLoading(true)\n    setCurrentAssistantMessage('')\n    setCurrentError(null)\n    const storagePassword = localStorage.getItem('pass')\n    try {\n      const controller = new AbortController()\n      setController(controller)\n      const requestMessageList = messageList().slice(-maxHistoryMessages)\n      if (currentSystemRoleSettings()) {\n        requestMessageList.unshift({\n          role: 'system',\n          content: currentSystemRoleSettings(),\n        })\n      }\n      const timestamp = Date.now()\n      const response = await fetch('/api/generate', {\n        method: 'POST',\n        body: JSON.stringify({\n          messages: requestMessageList,\n          time: timestamp,\n          pass: storagePassword,\n          sign: await generateSignature({\n            t: timestamp,\n            m: requestMessageList?.[requestMessageList.length - 1]?.content || '',\n          }),\n          temperature: temperature(),\n        }),\n        signal: controller.signal,\n      })\n      if (!response.ok) {\n        const error = await response.json()\n        console.error(error.error)\n        setCurrentError(error.error)\n        throw new Error('Request failed')\n      }\n      const data = response.body\n      if (!data)\n        throw new Error('No data')\n\n      const reader = data.getReader()\n      const decoder = new TextDecoder('utf-8')\n      let done = false\n\n      while (!done) {\n        const { value, done: readerDone } = await reader.read()\n        if (value) {\n          const char = decoder.decode(value)\n          if (char === '\\n' && currentAssistantMessage().endsWith('\\n'))\n            continue\n\n          if (char)\n            setCurrentAssistantMessage(currentAssistantMessage() + char)\n\n          isStick() && instantToBottom()\n        }\n        done = readerDone\n      }\n    } catch (e) {\n      console.error(e)\n      setLoading(false)\n      setController(null)\n      return\n    }\n    archiveCurrentMessage()\n    isStick() && instantToBottom()\n  }\n\n  const archiveCurrentMessage = () => {\n    if (currentAssistantMessage()) {\n      setMessageList([\n        ...messageList(),\n        {\n          role: 'assistant',\n          content: currentAssistantMessage(),\n        },\n      ])\n      setCurrentAssistantMessage('')\n      setLoading(false)\n      setController(null)\n      // Disable auto-focus on touch devices\n      if (!('ontouchstart' in document.documentElement || navigator.maxTouchPoints > 0))\n        inputRef.focus()\n    }\n  }\n\n  const clear = () => {\n    inputRef.value = ''\n    inputRef.style.height = 'auto'\n    setMessageList([])\n    setCurrentAssistantMessage('')\n    setCurrentError(null)\n  }\n\n  const stopStreamFetch = () => {\n    if (controller()) {\n      controller().abort()\n      archiveCurrentMessage()\n    }\n  }\n\n  const retryLastFetch = () => {\n    if (messageList().length > 0) {\n      const lastMessage = messageList()[messageList().length - 1]\n      if (lastMessage.role === 'assistant')\n        setMessageList(messageList().slice(0, -1))\n      requestWithLatestMessage()\n    }\n  }\n\n  const handleKeydown = (e: KeyboardEvent) => {\n    if (e.isComposing || e.shiftKey)\n      return\n\n    if (e.key === 'Enter') {\n      e.preventDefault()\n      handleButtonClick()\n    }\n  }\n\n  return (\n    <div my-6>\n      <SystemRoleSettings\n        canEdit={() => messageList().length === 0}\n        systemRoleEditing={systemRoleEditing}\n        setSystemRoleEditing={setSystemRoleEditing}\n        currentSystemRoleSettings={currentSystemRoleSettings}\n        setCurrentSystemRoleSettings={setCurrentSystemRoleSettings}\n        temperatureSetting={temperatureSetting}\n      />\n      <Index each={messageList()}>\n        {(message, index) => (\n          <MessageItem\n            role={message().role}\n            message={message().content}\n            showRetry={() => (message().role === 'assistant' && index === messageList().length - 1)}\n            onRetry={retryLastFetch}\n          />\n        )}\n      </Index>\n      {currentAssistantMessage() && (\n        <MessageItem\n          role=\"assistant\"\n          message={currentAssistantMessage}\n        />\n      )}\n      { currentError() && <ErrorMessageItem data={currentError()} onRetry={retryLastFetch} /> }\n      <Show\n        when={!loading()}\n        fallback={() => (\n          <div class=\"gen-cb-wrapper\">\n            <span>AI is thinking...</span>\n            <div class=\"gen-cb-stop\" onClick={stopStreamFetch}>Stop</div>\n          </div>\n        )}\n      >\n        <div class=\"gen-text-wrapper\" class:op-50={systemRoleEditing()}>\n          <textarea\n            ref={inputRef!}\n            disabled={systemRoleEditing()}\n            onKeyDown={handleKeydown}\n            placeholder=\"Enter something...\"\n            autocomplete=\"off\"\n            autofocus\n            onInput={() => {\n              inputRef.style.height = 'auto'\n              inputRef.style.height = `${inputRef.scrollHeight}px`\n            }}\n            rows=\"1\"\n            class=\"gen-textarea\"\n          />\n          <button onClick={handleButtonClick} disabled={systemRoleEditing()} gen-slate-btn>\n            Send\n          </button>\n          <button title=\"Clear\" onClick={clear} disabled={systemRoleEditing()} gen-slate-btn>\n            <IconClear />\n          </button>\n        </div>\n      </Show>\n      <div class=\"fixed bottom-5 left-5 rounded-md hover:bg-slate/10 w-fit h-fit transition-colors active:scale-90\" class:stick-btn-on={isStick()}>\n        <div>\n          <button class=\"p-2.5 text-base\" title=\"stick to bottom\" type=\"button\" onClick={() => setStick(!isStick())}>\n            <div i-ph-arrow-line-down-bold />\n          </button>\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Header.astro",
    "content": "---\nimport { model } from '../utils/openAI'\nimport Logo from './Logo.astro'\nimport Themetoggle from './Themetoggle.astro'\n---\n\n<header>\n  <div class=\"fb items-center\">\n    <Logo />\n    <Themetoggle />\n  </div>\n  <div class=\"fi mt-2\">\n    <span class=\"gpt-title\">ChatGPT</span>\n    <span class=\"gpt-subtitle\">Demo</span>\n  </div>\n  <p mt-1 op-60>Based on OpenAI API ({model}).</p>\n</header>\n"
  },
  {
    "path": "src/components/Logo.astro",
    "content": "<svg xmlns=\"http://www.w3.org/2000/svg\" width=\"48\" height=\"48\" viewBox=\"0 0 32 32\"><g fill=\"none\"><path fill=\"#F8312F\" d=\"M5 3.5a1.5 1.5 0 0 1-1 1.415V12l2.16 5.487L4 23c-1.1 0-2-.9-2-1.998v-7.004a2 2 0 0 1 1-1.728V4.915A1.5 1.5 0 1 1 5 3.5Zm25.05.05c0 .681-.44 1.26-1.05 1.468V12.2c.597.347 1 .994 1 1.73v7.01c0 1.1-.9 2-2 2l-2.94-5.68L28 11.93V5.018a1.55 1.55 0 1 1 2.05-1.468Z\"/><path fill=\"#FFB02E\" d=\"M11 4.5A1.5 1.5 0 0 1 12.5 3h7a1.5 1.5 0 0 1 .43 2.938c-.277.082-.57.104-.847.186l-3.053.904l-3.12-.908c-.272-.08-.56-.1-.832-.18A1.5 1.5 0 0 1 11 4.5Z\"/><path fill=\"#CDC4D6\" d=\"M22.05 30H9.95C6.66 30 4 27.34 4 24.05V12.03C4 8.7 6.7 6 10.03 6h11.95C25.3 6 28 8.7 28 12.03v12.03c0 3.28-2.66 5.94-5.95 5.94Z\"/><path fill=\"#212121\" d=\"M9.247 18.5h13.506c2.33 0 4.247-1.919 4.247-4.25A4.257 4.257 0 0 0 22.753 10H9.247A4.257 4.257 0 0 0 5 14.25a4.257 4.257 0 0 0 4.247 4.25Zm4.225 7.5h5.056C19.34 26 20 25.326 20 24.5s-.66-1.5-1.472-1.5h-5.056C12.66 23 12 23.674 12 24.5s.66 1.5 1.472 1.5Z\"/><path fill=\"#00A6ED\" d=\"M10.25 12C9.56 12 9 12.56 9 13.25v2.5a1.25 1.25 0 1 0 2.5 0v-2.5c0-.69-.56-1.25-1.25-1.25Zm11.5 0c-.69 0-1.25.56-1.25 1.25v2.5a1.25 1.25 0 1 0 2.5 0v-2.5c0-.69-.56-1.25-1.25-1.25Z\"/></g></svg>\n"
  },
  {
    "path": "src/components/MessageItem.tsx",
    "content": "import { createSignal } from 'solid-js'\nimport MarkdownIt from 'markdown-it'\nimport mdKatex from 'markdown-it-katex'\nimport mdHighlight from 'markdown-it-highlightjs'\nimport { useClipboard, useEventListener } from 'solidjs-use'\nimport IconRefresh from './icons/Refresh'\nimport type { Accessor } from 'solid-js'\nimport type { ChatMessage } from '@/types'\n\ninterface Props {\n  role: ChatMessage['role']\n  message: Accessor<string> | string\n  showRetry?: Accessor<boolean>\n  onRetry?: () => void\n}\n\nexport default ({ role, message, showRetry, onRetry }: Props) => {\n  const roleClass = {\n    system: 'bg-gradient-to-r from-gray-300 via-gray-200 to-gray-300',\n    user: 'bg-gradient-to-r from-purple-400 to-yellow-400',\n    assistant: 'bg-gradient-to-r from-yellow-200 via-green-200 to-green-300',\n  }\n  const [source] = createSignal('')\n  const { copy, copied } = useClipboard({ source, copiedDuring: 1000 })\n\n  useEventListener('click', (e) => {\n    const el = e.target as HTMLElement\n    let code = null\n\n    if (el.matches('div > div.copy-btn')) {\n      code = decodeURIComponent(el.dataset.code!)\n      copy(code)\n    }\n    if (el.matches('div > div.copy-btn > svg')) {\n      // eslint-disable-next-line @typescript-eslint/no-non-null-asserted-optional-chain\n      code = decodeURIComponent(el.parentElement?.dataset.code!)\n      copy(code)\n    }\n  })\n\n  const htmlString = () => {\n    const md = MarkdownIt({\n      linkify: true,\n      breaks: true,\n    }).use(mdKatex).use(mdHighlight)\n    const fence = md.renderer.rules.fence!\n    md.renderer.rules.fence = (...args) => {\n      const [tokens, idx] = args\n      const token = tokens[idx]\n      const rawCode = fence(...args)\n\n      return `<div relative>\n      <div data-code=${encodeURIComponent(token.content)} class=\"copy-btn gpt-copy-btn group\">\n          <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 32 32\"><path fill=\"currentColor\" d=\"M28 10v18H10V10h18m0-2H10a2 2 0 0 0-2 2v18a2 2 0 0 0 2 2h18a2 2 0 0 0 2-2V10a2 2 0 0 0-2-2Z\" /><path fill=\"currentColor\" d=\"M4 18H2V4a2 2 0 0 1 2-2h14v2H4Z\" /></svg>\n            <div class=\"group-hover:op-100 gpt-copy-tips\">\n              ${copied() ? 'Copied' : 'Copy'}\n            </div>\n      </div>\n      ${rawCode}\n      </div>`\n    }\n\n    if (typeof message === 'function')\n      return md.render(message())\n    else if (typeof message === 'string')\n      return md.render(message)\n\n    return ''\n  }\n\n  return (\n    <div class=\"py-2 -mx-4 px-4 transition-colors md:hover:bg-slate/3\">\n      <div class=\"flex gap-3 rounded-lg\" class:op-75={role === 'user'}>\n        <div class={`shrink-0 w-7 h-7 mt-4 rounded-full op-80 ${roleClass[role]}`} />\n        <div class=\"message prose break-words overflow-hidden\" innerHTML={htmlString()} />\n      </div>\n      {showRetry?.() && onRetry && (\n        <div class=\"fie px-3 mb-2\">\n          <div onClick={onRetry} class=\"gpt-retry-btn\">\n            <IconRefresh />\n            <span>Regenerate</span>\n          </div>\n        </div>\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/SettingsSlider.tsx",
    "content": "import { Slider } from './Slider'\nimport type { SettingsUI, SettingsUISlider } from '@/types/provider'\nimport type { Accessor } from 'solid-js'\n\ninterface Props {\n  settings: SettingsUI\n  editing: Accessor<boolean>\n  value: Accessor<number>\n  setValue: (v: number) => void\n}\n\nconst SettingsNotDefined = () => {\n  return (\n    <div class=\"op-25\">Not Defined</div>\n  )\n}\n\nexport default ({ settings, editing, value, setValue }: Props) => {\n  if (!settings.name || !settings.type) return null\n  const sliderSettings = settings as SettingsUISlider\n\n  return (\n    <div>\n      {editing() && (\n        <Slider\n          setValue={setValue}\n          max={sliderSettings.max}\n          value={value}\n          min={sliderSettings.min}\n          step={sliderSettings.step}\n        />\n      )}\n      {!editing() && value() && (\n        <div>{value()}</div>\n      )}\n      {!editing() && !value() && (\n        <SettingsNotDefined />\n      )}\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Slider.tsx",
    "content": "import * as slider from '@zag-js/slider'\nimport { normalizeProps, useMachine } from '@zag-js/solid'\nimport { createMemo, createUniqueId, mergeProps } from 'solid-js'\nimport type { Accessor } from 'solid-js'\nimport '../slider.css'\n\ninterface Props {\n  value: Accessor<number>\n  min: number\n  max: number\n  step: number\n  disabled?: boolean\n  setValue: (v: number) => void\n}\n\nexport const Slider = (selectProps: Props) => {\n  const props = mergeProps({\n    min: 0,\n    max: 2,\n    step: 0.01,\n    disabled: false,\n  }, selectProps)\n\n  const formatSliderValue = (value: number) => {\n    if (!value) return 0\n    return Number.isInteger(value) ? value : parseFloat(value.toFixed(2))\n  }\n\n  const [state, send] = useMachine(slider.machine({\n    id: createUniqueId(),\n    value: props.value(),\n    min: props.min,\n    max: props.max,\n    step: props.step,\n    disabled: props.disabled,\n    onChange: (details) => {\n      details && details.value && props.setValue(formatSliderValue(details.value))\n    },\n  }))\n  const api = createMemo(() => slider.connect(state, send, normalizeProps))\n  return (\n    <div {...api().rootProps}>\n      <div class=\"text-xs op-50 fb items-center\">\n        <span>Temperature</span>\n        <output {...api().outputProps}>{formatSliderValue(api().value)}</output>\n      </div>\n      <div class=\"mt-2\" {...api().controlProps}>\n        <div {...api().trackProps}>\n          <div {...api().rangeProps} />\n        </div>\n        <div {...api().thumbProps}>\n          <input {...api().hiddenInputProps} />\n        </div>\n      </div>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/SystemRoleSettings.tsx",
    "content": "import { Show, createEffect, createSignal } from 'solid-js'\nimport IconEnv from './icons/Env'\nimport IconX from './icons/X'\nimport SettingsSlider from './SettingsSlider'\nimport type { Accessor, Setter } from 'solid-js'\n\ninterface Props {\n  canEdit: Accessor<boolean>\n  systemRoleEditing: Accessor<boolean>\n  setSystemRoleEditing: Setter<boolean>\n  currentSystemRoleSettings: Accessor<string>\n  setCurrentSystemRoleSettings: Setter<string>\n  temperatureSetting: (value: number) => void\n}\n\nexport default (props: Props) => {\n  let systemInputRef: HTMLTextAreaElement\n  const [temperature, setTemperature] = createSignal(0.6)\n\n  const handleButtonClick = () => {\n    props.setCurrentSystemRoleSettings(systemInputRef.value)\n    props.setSystemRoleEditing(false)\n  }\n\n  createEffect(() => {\n    props.temperatureSetting(temperature())\n  })\n\n  return (\n    <div class=\"my-4\">\n      <Show when={!props.systemRoleEditing()}>\n        <Show when={props.currentSystemRoleSettings()}>\n          <div>\n            <div class=\"fi gap-1 op-50 dark:op-60\">\n              <Show when={props.canEdit()} fallback={<IconEnv />}>\n                <span onClick={() => props.setCurrentSystemRoleSettings('')} class=\"sys-edit-btn p-1 rd-50%\" > <IconX /> </span>\n              </Show>\n              <span>System Role ( Temp = {temperature()} ) : </span>\n            </div>\n            <div class=\"mt-1\">\n              {props.currentSystemRoleSettings()}\n            </div>\n          </div>\n        </Show>\n        <Show when={!props.currentSystemRoleSettings() && props.canEdit()}>\n          <span onClick={() => props.setSystemRoleEditing(!props.systemRoleEditing())} class=\"sys-edit-btn\">\n            <IconEnv />\n            <span>Add System Role</span>\n          </span>\n        </Show>\n      </Show>\n      <Show when={props.systemRoleEditing() && props.canEdit()}>\n        <div>\n          <div class=\"fi gap-1 op-50 dark:op-60\">\n            <IconEnv />\n            <span>System Role:</span>\n          </div>\n          <p class=\"my-2 leading-normal text-sm op-50 dark:op-60\">Gently instruct the assistant and set the behavior of the assistant.</p>\n          <div>\n            <textarea\n              ref={systemInputRef!}\n              placeholder=\"You are a helpful assistant, answer as concisely as possible....\"\n              autocomplete=\"off\"\n              autofocus\n              rows=\"3\"\n              gen-textarea\n            />\n          </div>\n          <div class=\"w-full fi fb\">\n            <button onClick={handleButtonClick} gen-slate-btn>\n              Set\n            </button>\n            <div class=\"w-full ml-2\">\n              <SettingsSlider\n                settings={{\n                  name: 'Temperature',\n                  type: 'slider',\n                  min: 0,\n                  max: 2,\n                  step: 0.01,\n                }}\n                editing={() => true}\n                value={temperature}\n                setValue={setTemperature}\n              />\n            </div>\n          </div>\n        </div>\n      </Show>\n    </div>\n  )\n}\n"
  },
  {
    "path": "src/components/Themetoggle.astro",
    "content": "<div id=\"themeToggle\" class=\"flex items-center justify-center w-10 h-10 rounded-md transition-colors hover:bg-slate/10\">\r\n  <svg class=\"theme_toggle_svg\" width=\"1.2em\" height=\"1.2em\" viewBox=\"0 0 24 24\" color=\"#858585\" fill=\"none\" stroke-width=\"2\" stroke-linecap=\"round\" stroke-linejoin=\"round\" stroke=\"currentColor\">\r\n    <mask id=\"myMask\">\r\n      <rect x=\"0\" y=\"0\" width=\"100%\" height=\"100%\" fill=\"white\"></rect>\r\n      <circle class=\"theme_toggle_circle1\" fill=\"black\" cx=\"100%\" cy=\"0%\"></circle>\r\n    </mask>\r\n    <circle class=\"theme_toggle_circle2\" cx=\"12\" cy=\"12\" fill=\"#858585\" mask=\"url(#myMask)\"></circle>\r\n    <g class=\"theme_toggle_g\" stroke=\"currentColor\" opacity=\"1\">\r\n      <line x1=\"12\" y1=\"1\" x2=\"12\" y2=\"3\"></line>\r\n      <line x1=\"12\" y1=\"21\" x2=\"12\" y2=\"23\"></line>\r\n      <line x1=\"4.22\" y1=\"4.22\" x2=\"5.64\" y2=\"5.64\"></line>\r\n      <line x1=\"18.36\" y1=\"18.36\" x2=\"19.78\" y2=\"19.78\"></line>\r\n      <line x1=\"1\" y1=\"12\" x2=\"3\" y2=\"12\"></line>\r\n      <line x1=\"21\" y1=\"12\" x2=\"23\" y2=\"12\"></line>\r\n      <line x1=\"4.22\" y1=\"19.78\" x2=\"5.64\" y2=\"18.36\"></line>\r\n      <line x1=\"18.36\" y1=\"5.64\" x2=\"19.78\" y2=\"4.22\"></line>\r\n    </g>\r\n  </svg>\r\n</div>\r\n\r\n<style>\r\n  #themeToggle {\r\n    border: 0;\r\n    cursor: pointer;\r\n  }\r\n  .theme_toggle_circle1 {\r\n    transition: cx .5s, cy .5s;\r\n    cx: 100%;\r\n    cy: 0%\r\n  }\r\n  .theme_toggle_circle2 {\r\n    transition: r .3s;\r\n  }\r\n  .theme_toggle_svg {\r\n    transition: transform .5s cubic-bezier(0.68, -0.55, 0.27, 1.55);\r\n    transform: rotate(90deg);\r\n  }\r\n .theme_toggle_g {\r\n    transition: opacity .5s;\r\n    opacity: 1;\r\n  }\r\n  :global(html.dark) #themeToggle .theme_toggle_circle1 {\r\n    cx: 50%;\r\n    cy: 23%;\r\n  }\r\n  :global(html.dark) #themeToggle .theme_toggle_svg {\r\n    transform: rotate(40deg);\r\n  }\r\n  :global(html.dark) #themeToggle .theme_toggle_g {\r\n    opacity: 0;\r\n  }\r\n</style>\r\n\r\n<script>\r\nconst themeToggle = document.getElementById('themeToggle')\r\nconst themeCircle1 = document.querySelector('.theme_toggle_circle1')\r\nconst themeCircle2 = document.querySelector('.theme_toggle_circle2')\r\nconst toogleThemeCircle = () => {\r\n  const darkMode = document.documentElement.classList.contains('dark') ?? localStorage.getItem('theme') === 'dark'\r\n  if (darkMode) {\r\n    themeCircle1.setAttribute('r', '9')\r\n    themeCircle2.setAttribute('r', '9')\r\n  } else {\r\n    themeCircle1.setAttribute('r', '5')\r\n    themeCircle2.setAttribute('r', '5')\r\n  }\r\n}\r\n\r\nconst listenColorSchema = () => {\r\n  const colorSchema = window.matchMedia('(prefers-color-scheme: dark)')\r\n  colorSchema.addEventListener('change', () => {\r\n    document.documentElement.classList.toggle('dark', colorSchema.matches)\r\n    toogleThemeCircle()\r\n  })\r\n}\r\n\r\nlistenColorSchema()\r\ntoogleThemeCircle()\r\n\r\nconst handleToggleClick = () => {\r\n  const element = document.documentElement\r\n  element.classList.toggle('dark')\r\n  const isDark = element.classList.contains('dark')\r\n  localStorage.setItem('theme', isDark ? 'dark' : 'light')\r\n  toogleThemeCircle()\r\n}\r\nthemeToggle.addEventListener('click', handleToggleClick)\r\n</script>\r\n"
  },
  {
    "path": "src/components/icons/Clear.tsx",
    "content": "export default () => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 24 24\"><path fill=\"currentColor\" d=\"M8 20v-5h2v5h9v-7H5v7h3zm-4-9h16V8h-6V4h-4v4H4v3zM3 21v-8H2V7a1 1 0 0 1 1-1h5V3a1 1 0 0 1 1-1h6a1 1 0 0 1 1 1v3h5a1 1 0 0 1 1 1v6h-1v8a1 1 0 0 1-1 1H4a1 1 0 0 1-1-1z\" /></svg>\n  )\n}\n"
  },
  {
    "path": "src/components/icons/Env.tsx",
    "content": "export default () => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1rem\" height=\"1rem\" viewBox=\"0 0 32 32\"><path fill=\"currentColor\" d=\"M30 15h-2.05A12.007 12.007 0 0 0 17 4.05V2h-2v2.05A12.007 12.007 0 0 0 4.05 15H2v2h2.05A12.007 12.007 0 0 0 15 27.95V30h2v-2.05A12.007 12.007 0 0 0 27.95 17H30ZM17 25.95V22h-2v3.95A10.017 10.017 0 0 1 6.05 17H10v-2H6.05A10.017 10.017 0 0 1 15 6.05V10h2V6.05A10.017 10.017 0 0 1 25.95 15H22v2h3.95A10.017 10.017 0 0 1 17 25.95Z\" /></svg>\n  )\n}\n"
  },
  {
    "path": "src/components/icons/Refresh.tsx",
    "content": "export default () => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 32 32\"><path d=\"M25.95 7.65l.005-.004c-.092-.11-.197-.206-.293-.312c-.184-.205-.367-.41-.563-.603c-.139-.136-.286-.262-.43-.391c-.183-.165-.366-.329-.558-.482c-.16-.128-.325-.247-.49-.367c-.192-.14-.385-.277-.585-.406a13.513 13.513 0 0 0-.533-.324q-.308-.179-.625-.341c-.184-.094-.37-.185-.56-.27c-.222-.1-.449-.191-.678-.28c-.19-.072-.378-.145-.571-.208c-.246-.082-.498-.15-.75-.217c-.186-.049-.368-.102-.556-.143c-.29-.063-.587-.107-.883-.15c-.16-.023-.315-.056-.476-.073A12.933 12.933 0 0 0 6 7.703V4H4v8h8v-2H6.811A10.961 10.961 0 0 1 16 5a11.111 11.111 0 0 1 1.189.067c.136.015.268.042.403.061c.25.037.501.075.746.128c.16.035.315.08.472.121c.213.057.425.114.633.183c.164.054.325.116.486.178c.193.074.384.15.57.235c.162.072.32.15.477.23q.268.136.526.286c.153.09.305.18.453.276c.168.11.33.224.492.342c.14.102.282.203.417.312c.162.13.316.268.47.406c.123.11.248.217.365.332c.167.164.323.338.479.512A10.993 10.993 0 1 1 5 16H3a13 13 0 1 0 22.95-8.35z\" fill=\"currentColor\" /></svg>\n  )\n}\n"
  },
  {
    "path": "src/components/icons/X.tsx",
    "content": "export default () => {\n  return (\n    <svg xmlns=\"http://www.w3.org/2000/svg\" width=\"1em\" height=\"1em\" viewBox=\"0 0 16 16\" ><path fill=\"currentColor\" d=\"M3.72 3.72a.75.75 0 0 1 1.06 0L8 6.94l3.22-3.22a.749.749 0 0 1 1.275.326.749.749 0 0 1-.215.734L9.06 8l3.22 3.22a.749.749 0 0 1-.326 1.275.749.749 0 0 1-.734-.215L8 9.06l-3.22 3.22a.751.751 0 0 1-1.042-.018.751.751 0 0 1-.018-1.042L6.94 8 3.72 4.78a.75.75 0 0 1 0-1.06Z\" /></svg>\n  )\n}\n"
  },
  {
    "path": "src/env.d.ts",
    "content": "/// <reference types=\"astro/client\" />\n\ninterface ImportMetaEnv {\n  readonly OPENAI_API_KEY: string\n  readonly HTTPS_PROXY: string\n  readonly OPENAI_API_BASE_URL: string\n  readonly HEAD_SCRIPTS: string\n  readonly PUBLIC_SECRET_KEY: string\n  readonly SITE_PASSWORD: string\n  readonly OPENAI_API_MODEL: string\n  readonly PUBLIC_MAX_HISTORY_MESSAGES: string\n}\n\ninterface ImportMeta {\n  readonly env: ImportMetaEnv\n}\n"
  },
  {
    "path": "src/layouts/Layout.astro",
    "content": "---\nimport { pwaInfo } from 'virtual:pwa-info'\n\nexport interface Props {\n  title: string;\n}\n\nconst { title } = Astro.props;\n---\n\n<!DOCTYPE html>\n<html>\n  <head>\n    <meta charset=\"UTF-8\" />\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1.0,viewport-fit=cover\">\n    <link rel=\"icon\" type=\"image/svg+xml\" href=\"/icon.svg\">\n    <link rel=\"apple-touch-icon\" href=\"/apple-touch-icon.png\" sizes=\"180x180\">\n    <link rel=\"mask-icon\" href=\"/icon.svg\" color=\"#FFFFFF\">\n    <meta name=\"theme-color\" content=\"#212129\">\n    <meta name=\"generator\" content={Astro.generator}>\n    <title>{title}</title>\n    <meta name=\"description\" content=\"A simple blog\">\n    { import.meta.env.HEAD_SCRIPTS && <Fragment set:html={import.meta.env.HEAD_SCRIPTS } /> }\n    { pwaInfo && <Fragment set:html={pwaInfo.webManifest.linkTag} /> }\n    { import.meta.env.PROD && pwaInfo && <Fragment set:html={pwaInfo.registerSW.scriptTag} /> }\n  </head>\n  <body>\n    <slot />\n  </body>\n</html>\n\n<style is:global>\n  :root {\n    --c-bg: #fbfbfb;\n    --c-fg: #444444;\n    --c-scroll: #d9d9d9;\n    --c-scroll-hover: #bbbbbb;\n    scrollbar-color: var(--c-scrollbar) var(--c-bg);\n  }\n\n  html {\n    font-family: system-ui, sans-serif;\n    background-color: var(--c-bg);\n    color: var(--c-fg);\n  }\n\n  html.dark {\n    --c-bg: #212129;\n    --c-fg: #ddddf0;\n    --c-scroll: #333333;\n    --c-scroll-hover: #555555;\n  }\n\n  main {\n    max-width: 70ch;\n    margin: 0 auto;\n    padding: 6rem 2rem 4rem;\n  }\n\n  ::-webkit-scrollbar {\n    width: 6px;\n    height: 6px;\n  }\n  ::-webkit-scrollbar-thumb {\n    background-color: var(--c-scroll);\n    border-radius: 4px;\n  }\n  ::-webkit-scrollbar-thumb:hover {\n    background-color: var(--c-scroll-hover);\n  }\n  ::-webkit-scrollbar-track {\n    background-color: var(--c-bg);\n  }\n</style>\n\n<script>\nconst initTheme = () => {\n  const darkSchema\n    = window.matchMedia\n    && window.matchMedia('(prefers-color-scheme: dark)').matches\n  const storageTheme = localStorage.getItem('theme')\n  if (storageTheme) {\n    document.documentElement.classList.toggle(\n      'dark',\n      storageTheme === 'dark',\n    )\n  } else {\n    document.documentElement.classList.toggle('dark', darkSchema)\n  }\n}\n\ninitTheme()\n</script>\n"
  },
  {
    "path": "src/message.css",
    "content": ".message pre {\n  background-color: #64748b10;\n  font-size: 0.8rem;\n  padding: 0.4rem 1rem;\n}\n\n.message .hljs {\n  background-color: transparent;\n}\n\n.message table {\n  font-size: 0.8em;\n}\n\n.message table thead tr {\n  background-color: #64748b40;\n  text-align: left;\n}\n\n.message table th, .message table td {\n  padding: 0.6rem 1rem;\n}\n\n.message table tbody tr:last-of-type {\n  border-bottom: 2px solid #64748b40;\n}"
  },
  {
    "path": "src/pages/api/auth.ts",
    "content": "import type { APIRoute } from 'astro'\n\nconst realPassword = import.meta.env.SITE_PASSWORD || ''\nconst passList = realPassword.split(',') || []\n\nexport const post: APIRoute = async(context) => {\n  const body = await context.request.json()\n\n  const { pass } = body\n  return new Response(JSON.stringify({\n    code: (!realPassword || pass === realPassword || passList.includes(pass)) ? 0 : -1,\n  }))\n}\n"
  },
  {
    "path": "src/pages/api/generate.ts",
    "content": "// #vercel-disable-blocks\r\nimport { ProxyAgent, fetch } from 'undici'\r\n// #vercel-end\r\nimport { generatePayload, parseOpenAIStream } from '@/utils/openAI'\r\nimport { verifySignature } from '@/utils/auth'\r\nimport type { APIRoute } from 'astro'\r\n\r\nconst apiKey = import.meta.env.OPENAI_API_KEY\r\nconst httpsProxy = import.meta.env.HTTPS_PROXY\r\nconst baseUrl = ((import.meta.env.OPENAI_API_BASE_URL) || 'https://api.openai.com').trim().replace(/\\/$/, '')\r\nconst sitePassword = import.meta.env.SITE_PASSWORD || ''\r\nconst passList = sitePassword.split(',') || []\r\n\r\nexport const post: APIRoute = async(context) => {\r\n  const body = await context.request.json()\r\n  const { sign, time, messages, pass, temperature } = body\r\n  if (!messages) {\r\n    return new Response(JSON.stringify({\r\n      error: {\r\n        message: 'No input text.',\r\n      },\r\n    }), { status: 400 })\r\n  }\r\n  if (sitePassword && !(sitePassword === pass || passList.includes(pass))) {\r\n    return new Response(JSON.stringify({\r\n      error: {\r\n        message: 'Invalid password.',\r\n      },\r\n    }), { status: 401 })\r\n  }\r\n  if (import.meta.env.PROD && !await verifySignature({ t: time, m: messages?.[messages.length - 1]?.content || '' }, sign)) {\r\n    return new Response(JSON.stringify({\r\n      error: {\r\n        message: 'Invalid signature.',\r\n      },\r\n    }), { status: 401 })\r\n  }\r\n  const initOptions = generatePayload(apiKey, messages, temperature)\r\n  // #vercel-disable-blocks\r\n  if (httpsProxy)\r\n    initOptions.dispatcher = new ProxyAgent(httpsProxy)\r\n  // #vercel-end\r\n\r\n  // eslint-disable-next-line @typescript-eslint/ban-ts-comment\r\n  // @ts-expect-error\r\n  const response = await fetch(`${baseUrl}/v1/chat/completions`, initOptions).catch((err: Error) => {\r\n    console.error(err)\r\n    return new Response(JSON.stringify({\r\n      error: {\r\n        code: err.name,\r\n        message: err.message,\r\n      },\r\n    }), { status: 500 })\r\n  }) as Response\r\n\r\n  return parseOpenAIStream(response) as Response\r\n}\r\n"
  },
  {
    "path": "src/pages/index.astro",
    "content": "---\r\nimport Layout from '../layouts/Layout.astro'\r\nimport Header from '../components/Header.astro'\r\nimport Footer from '../components/Footer.astro'\r\nimport Generator from '../components/Generator'\r\nimport '../message.css'\r\nimport 'katex/dist/katex.min.css'\r\nimport 'highlight.js/styles/atom-one-dark.css'\r\n---\r\n\r\n<Layout title=\"ChatGPT API Demo\">\r\n  <main >\r\n    <Header />\r\n    <Generator client:load />\r\n    <Footer />\r\n  </main>\r\n</Layout>\r\n\r\n<script>\r\nasync function checkCurrentAuth() {\r\n  const password = localStorage.getItem('pass')\r\n  const response = await fetch('/api/auth', {\r\n    method: 'POST',\r\n    headers: {\r\n      'Content-Type': 'application/json',\r\n    },\r\n    body: JSON.stringify({\r\n      pass: password,\r\n    }),\r\n  })\r\n  const responseJson = await response.json()\r\n  if (responseJson.code !== 0)\r\n    window.location.href = '/password'\r\n}\r\ncheckCurrentAuth()\r\n</script>\r\n"
  },
  {
    "path": "src/pages/password.astro",
    "content": "---\nimport Layout from '../layouts/Layout.astro'\n---\n\n<Layout title=\"Password Protection\">\n  <main class=\"h-screen col-fcc\">\n    <div class=\"op-30\">Please input password</div>\n    <div id=\"input_container\" class=\"flex mt-4\">\n      <input id=\"password_input\" type=\"password\" class=\"gpt-password-input\" />\n      <div id=\"submit\" class=\"gpt-password-submit\">\n        <div class=\"i-carbon-arrow-right\" />\n      </div>\n    </div>\n  </main>\n</Layout>\n\n<script>\nconst inputContainer = document.getElementById('input_container') as HTMLDivElement\nconst input = document.getElementById('password_input') as HTMLInputElement\nconst submitButton = document.getElementById('submit') as HTMLDivElement\n\ninput.onkeydown = async(event) => {\n  if (event.key === 'Enter')\n    handleSubmit()\n}\nsubmitButton.onclick = handleSubmit\n\nasync function handleSubmit() {\n  const password = input.value\n  const response = await fetch('/api/auth', {\n    method: 'POST',\n    headers: {\n      'Content-Type': 'application/json',\n    },\n    body: JSON.stringify({\n      pass: password,\n    }),\n  })\n  const responseJson = await response.json()\n  if (responseJson.code === 0) {\n    localStorage.setItem('pass', password)\n    window.location.href = '/'\n  } else {\n    inputContainer.classList.add('invalid')\n    setTimeout(() => {\n      inputContainer.classList.remove('invalid')\n    }, 300)\n  }\n}\n</script>\n\n<style>\n@keyframes shake {\n  0% {\n    transform: translateX(0);\n  }\n  25% {\n    transform: translateX(0.5rem);\n  }\n  75% {\n    transform: translateX(-0.5rem);\n  }\n  100% {\n    transform: translateX(0);\n  }\n}\n\n.invalid {\n  animation: shake 0.2s ease-in-out 0s 2;\n}\n</style>\n"
  },
  {
    "path": "src/slider.css",
    "content": "/* -----------------------------------------------------------------------------\n* Slider\n* -----------------------------------------------------------------------------*/\n\n[data-scope='slider'][data-part='root'] {\n  @apply w-full flex flex-col\n}\n[data-scope='slider'][data-part='root'][data-orientation='vertical'] {\n  @apply h-60\n}\n\n[data-scope='slider'][data-part='control'] {\n  --slider-thumb-size: 14px;\n  --slider-track-height: 4px;\n  @apply relative fcc cursor-pointer\n}\n[data-scope='slider'][data-part='control'][data-orientation='horizontal'] {\n  @apply h-[var(--slider-thumb-size)];\n}\n[data-scope='slider'][data-part='control'][data-orientation='vertical'] {\n  @apply w-[var(--slider-thumb-size)];\n}\n[data-scope='slider'][data-part='control']:hover [data-part='range'] {\n  @apply bg-gray-400 dark:bg-gray-600\n}\n[data-scope='slider'][data-part='control']:hover [data-part='thumb'] {\n  @apply bg-gray-300 dark:bg-gray-400\n}\n\n\n[data-scope='slider'][data-part='thumb'] {\n  all: unset;\n  @apply bg-gray-200 dark:bg-gray-500 w-[var(--slider-thumb-size)] h-[var(--slider-thumb-size)] rounded-full b-#c5c5d2 b-2\n}\n[data-scope='slider'][data-part='thumb'][data-disabled] {\n  @apply w-0\n}\n\n[data-scope='slider'] .control-area {\n  @apply flex mt-12px\n}\n\n.slider [data-orientation='horizontal'] .control-area {\n  flex-direction: column;\n  width: 100%;\n}\n\n.slider [data-orientation='vertical'] .control-area {\n  flex-direction: row;\n  height: 100%;\n}\n\n[data-scope='slider'][data-part='track'] {\n  @apply rounded-full bg-gray-200 dark:bg-neutral-700\n}\n[data-scope='slider'][data-part='track'][data-orientation='horizontal'] {\n  @apply h-[var(--slider-track-height)] w-full;\n}\n[data-scope='slider'][data-part='track'][data-orientation='vertical'] {\n  @apply h-full w-[var(--slider-track-height)];\n}\n\n[data-scope='slider'][data-part='range'] {\n  @apply bg-neutral-300 dark:bg-gray-700\n}\n[data-scope='slider'][data-part='range'][data-disabled] {\n   @apply bg-neutral-300 dark:bg-gray-600\n}\n[data-scope='slider'][data-part='range'][data-orientation='horizontal'] {\n  @apply h-full;\n}\n[data-scope='slider'][data-part='range'][data-orientation='vertical'] {\n   @apply w-full;\n}\n\n[data-scope='slider'][data-part='output'] {\n  margin-inline-start: 12px;\n}\n\n[data-scope='slider'][data-part='marker'] {\n  color: lightgray;\n}\n"
  },
  {
    "path": "src/types.ts",
    "content": "export interface ChatMessage {\n  role: 'system' | 'user' | 'assistant'\n  content: string\n}\n\nexport interface ErrorMessage {\n  code: string\n  message: string\n}\n"
  },
  {
    "path": "src/utils/auth.ts",
    "content": "import { sha256 } from 'js-sha256'\ninterface AuthPayload {\n  t: number\n  m: string\n}\n\nasync function digestMessage(message: string) {\n  if (typeof crypto !== 'undefined' && crypto?.subtle?.digest) {\n    const msgUint8 = new TextEncoder().encode(message)\n    const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8)\n    const hashArray = Array.from(new Uint8Array(hashBuffer))\n    return hashArray.map(b => b.toString(16).padStart(2, '0')).join('')\n  } else {\n    return sha256(message).toString()\n  }\n}\n\nexport const generateSignature = async(payload: AuthPayload) => {\n  const { t: timestamp, m: lastMessage } = payload\n  const secretKey = import.meta.env.PUBLIC_SECRET_KEY as string || ''\n  const signText = `${timestamp}:${lastMessage}:${secretKey}`\n  // eslint-disable-next-line no-return-await\n  return await digestMessage(signText)\n}\n\nexport const verifySignature = async(payload: AuthPayload, sign: string) => {\n  // if (Math.abs(payload.t - Date.now()) > 1000 * 60 * 5) {\n  //   return false\n  // }\n  const payloadSign = await generateSignature(payload)\n  return payloadSign === sign\n}\n"
  },
  {
    "path": "src/utils/openAI.ts",
    "content": "import { createParser } from 'eventsource-parser'\nimport type { ParsedEvent, ReconnectInterval } from 'eventsource-parser'\nimport type { ChatMessage } from '@/types'\n\nexport const model = import.meta.env.OPENAI_API_MODEL || 'gpt-3.5-turbo'\n\nexport const generatePayload = (\n  apiKey: string,\n  messages: ChatMessage[],\n  temperature: number,\n): RequestInit & { dispatcher?: any } => ({\n  headers: {\n    'Content-Type': 'application/json',\n    'Authorization': `Bearer ${apiKey}`,\n  },\n  method: 'POST',\n  body: JSON.stringify({\n    model,\n    messages,\n    temperature,\n    stream: true,\n  }),\n})\n\nexport const parseOpenAIStream = (rawResponse: Response) => {\n  const encoder = new TextEncoder()\n  const decoder = new TextDecoder()\n  if (!rawResponse.ok) {\n    return new Response(rawResponse.body, {\n      status: rawResponse.status,\n      statusText: rawResponse.statusText,\n    })\n  }\n\n  const stream = new ReadableStream({\n    async start(controller) {\n      const streamParser = (event: ParsedEvent | ReconnectInterval) => {\n        if (event.type === 'event') {\n          const data = event.data\n          if (data === '[DONE]') {\n            controller.close()\n            return\n          }\n          try {\n            // response = {\n            //   id: 'chatcmpl-6pULPSegWhFgi0XQ1DtgA3zTa1WR6',\n            //   object: 'chat.completion.chunk',\n            //   created: 1677729391,\n            //   model: 'gpt-3.5-turbo-0301',\n            //   choices: [\n            //     { delta: { content: '你' }, index: 0, finish_reason: null }\n            //   ],\n            // }\n            const json = JSON.parse(data)\n            const text = json.choices[0].delta?.content || ''\n            const queue = encoder.encode(text)\n            controller.enqueue(queue)\n          } catch (e) {\n            controller.error(e)\n          }\n        }\n      }\n\n      const parser = createParser(streamParser)\n      for await (const chunk of rawResponse.body as any)\n        parser.feed(decoder.decode(chunk))\n    },\n  })\n\n  return new Response(stream)\n}\n"
  },
  {
    "path": "tsconfig.json",
    "content": "{\n  \"extends\": \"astro/tsconfigs/base\",\n  \"compilerOptions\": {\n    \"baseUrl\": \".\",\n    \"jsx\": \"preserve\",\n    \"jsxImportSource\": \"solid-js\",\n    \"types\": [\"vite-plugin-pwa/info\"],\n    \"paths\": {\n      \"@/*\": [\"src/*\"],\n    },\n  }\n}\n"
  },
  {
    "path": "unocss.config.ts",
    "content": "import {\n  defineConfig,\n  presetAttributify,\n  presetIcons,\n  presetTypography,\n  presetUno,\n  transformerDirectives,\n  transformerVariantGroup,\n} from 'unocss'\n\nexport default defineConfig({\n  presets: [\n    presetUno(),\n    presetAttributify(),\n    presetIcons({\n      scale: 1.1,\n      cdn: 'https://esm.sh/',\n    }),\n    presetTypography({\n      cssExtend: {\n        'ul,ol': {\n          'padding-left': '2.25em',\n          'position': 'relative',\n        },\n      },\n    }),\n  ],\n  transformers: [transformerVariantGroup(), transformerDirectives()],\n  shortcuts: [{\n    'fc': 'flex justify-center',\n    'fi': 'flex items-center',\n    'fb': 'flex justify-between',\n    'fcc': 'fc items-center',\n    'fie': 'fi justify-end',\n    'col-fcc': 'flex-col fcc',\n    'inline-fcc': 'inline-flex items-center justify-center',\n    'base-focus': 'focus:(bg-op-20 ring-0 outline-none)',\n    'b-slate-link': 'border-b border-(slate none) hover:border-dashed',\n    'gpt-title': 'text-2xl font-extrabold mr-1',\n    'gpt-subtitle': 'text-(2xl transparent) font-extrabold bg-(clip-text gradient-to-r) from-sky-400 to-emerald-600',\n    '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',\n    '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',\n    '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',\n    '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',\n    'gpt-back-bottom-btn': 'gpt-back-top-btn bottom-20px transform-rotate-180deg',\n    'gpt-password-input': 'px-4 py-3 h-12 rounded-sm bg-(slate op-15) base-focus',\n    'gpt-password-submit': 'fcc h-12 w-12 bg-slate cursor-pointer bg-op-20 hover:bg-op-50',\n    'gen-slate-btn': 'h-12 px-4 py-2 bg-(slate op-15) hover:bg-op-20 rounded-sm',\n    'gen-cb-wrapper': 'h-12 my-4 fcc gap-4 bg-(slate op-15) rounded-sm',\n    'gen-cb-stop': 'px-2 py-0.5 border border-slate rounded-md text-sm op-70 cursor-pointer hover:bg-slate/10',\n    'gen-text-wrapper': 'my-4 fc gap-2 transition-opacity',\n    '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',\n    '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',\n    'stick-btn-on': '!bg-$c-fg text-$c-bg hover:op-80',\n  }],\n})\n"
  },
  {
    "path": "vercel.json",
    "content": "{\n  \"buildCommand\": \"OUTPUT=vercel astro build\"\n}"
  }
]