├── vercel.json ├── .gitattributes ├── public ├── pwa-192.png ├── pwa-512.png ├── apple-touch-icon.png └── icon.svg ├── .eslintignore ├── .dockerignore ├── .npmrc ├── .vscode ├── extensions.json ├── launch.json └── settings.json ├── src ├── components │ ├── Footer.astro │ ├── icons │ │ ├── Clear.tsx │ │ ├── X.tsx │ │ ├── Env.tsx │ │ └── Refresh.tsx │ ├── Header.astro │ ├── Logo.astro │ ├── ErrorMessageItem.tsx │ ├── SettingsSlider.tsx │ ├── Slider.tsx │ ├── Themetoggle.astro │ ├── MessageItem.tsx │ ├── SystemRoleSettings.tsx │ └── Generator.tsx ├── types.ts ├── pages │ ├── api │ │ ├── auth.ts │ │ └── generate.ts │ ├── index.astro │ └── password.astro ├── env.d.ts ├── message.css ├── utils │ ├── openAI.ts │ └── auth.ts ├── layouts │ └── Layout.astro └── slider.css ├── .github ├── ISSUE_TEMPLATE │ ├── config.yml │ ├── typo.yml │ ├── feature_request.yml │ ├── bus_report_when_deploying.yml │ └── bug_report_when_use.yml └── PULL_REQUEST_TEMPLATE.md ├── tsconfig.json ├── netlify.toml ├── plugins └── disableBlocks.ts ├── .gitignore ├── shims.d.ts ├── docker-compose.yml ├── hack ├── docker-entrypoint.sh └── docker-env-replace.sh ├── .env.example ├── Dockerfile ├── .eslintrc.js ├── LICENSE ├── package.json ├── astro.config.mjs ├── README.md ├── unocss.config.ts └── README.zh-CN.md /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "buildCommand": "OUTPUT=vercel astro build" 3 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /public/pwa-192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rockyzsu/GeminiChat/main/public/pwa-192.png -------------------------------------------------------------------------------- /public/pwa-512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Rockyzsu/GeminiChat/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/Rockyzsu/GeminiChat/main/public/apple-touch-icon.png -------------------------------------------------------------------------------- /.dockerignore: -------------------------------------------------------------------------------- 1 | *.md 2 | Dockerfile 3 | docker-compose.yml 4 | LICENSE 5 | netlify.toml 6 | vercel.json 7 | node_modules 8 | .vscode 9 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmjs.org/ 2 | strict-peer-dependencies=false 3 | auto-install-peers=true 4 | shamefully-hoist=true 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": ["astro-build.astro-vscode","dbaeumer.vscode-eslint","antfu.unocss"], 3 | "unwantedRecommendations": [], 4 | } 5 | -------------------------------------------------------------------------------- /src/components/Footer.astro: -------------------------------------------------------------------------------- 1 | 11 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | blank_issues_enabled: false 2 | contact_links: 3 | - name: 💬 Discussions 4 | url: https://github.com/babaohuang/GeminiProChat/discussions 5 | about: Use discussions if you have an idea for improvement or for asking questions. -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface ChatPart { 2 | text: string; 3 | } 4 | 5 | export interface ChatMessage { 6 | role: 'model' | 'user'; 7 | parts: ChatPart[]; 8 | } 9 | 10 | export interface ErrorMessage { 11 | code: string; 12 | message: string; 13 | } -------------------------------------------------------------------------------- /.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": "explicit" 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/Header.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import Logo from './Logo.astro' 3 | import Themetoggle from './Themetoggle.astro' 4 | --- 5 | 6 |
7 |
8 | 9 | 10 |
11 |
12 | Gemini Pro 13 | Chat 14 |
15 |

Based on Gemini Pro API.

16 |
17 | -------------------------------------------------------------------------------- /plugins/disableBlocks.ts: -------------------------------------------------------------------------------- 1 | export default function plugin() { 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 | } 10 | 11 | return { 12 | name: 'vercel-disable-blocks', 13 | enforce: 'pre', 14 | transform, 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /.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/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | interface ImportMetaEnv { 4 | readonly GEMINI_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 | readonly PUBLIC_MAX_HISTORY_MESSAGES: string 12 | } 13 | 14 | interface ImportMeta { 15 | readonly env: ImportMetaEnv 16 | } 17 | -------------------------------------------------------------------------------- /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 -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | 3 | services: 4 | geminiprochat: 5 | build: . 6 | container_name: geminiprochat 7 | restart: always 8 | ports: 9 | - "3000:3000" 10 | environment: 11 | - GEMINI_API_KEY=YOUR_GEMINI_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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /public/icon.svg: -------------------------------------------------------------------------------- 1 | 70 Basic icons by Xicons.co -------------------------------------------------------------------------------- /src/components/Logo.astro: -------------------------------------------------------------------------------- 1 | 70 Basic icons by Xicons.co 2 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | # Your API Key for GEMINI_API 2 | GEMINI_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 | 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 | # Set the maximum number of historical messages used for contextual contact 16 | PUBLIC_MAX_HISTORY_MESSAGES= 17 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use node:alpine as the base image 2 | FROM node:18.15-alpine 3 | 4 | # Set the working directory 5 | WORKDIR /usr/src 6 | 7 | # Copy all necessary files to the working directory 8 | COPY . . 9 | 10 | # Install global dependencies 11 | RUN npm install -g pnpm 12 | 13 | # Install project dependencies 14 | RUN pnpm install 15 | 16 | # Build the project 17 | RUN pnpm run build 18 | 19 | # Copy the hack directory to the container 20 | COPY hack ./ 21 | 22 | # Set environment variables 23 | ENV HOST=0.0.0.0 PORT=3000 NODE_ENV=production LANG=C.UTF-8 24 | 25 | # Expose the port the app runs on 26 | EXPOSE $PORT 27 | 28 | # Ensure the entrypoint script is executable 29 | RUN chmod +x ./docker-entrypoint.sh 30 | 31 | # Set the command to run when the container starts 32 | CMD ["/bin/sh", "docker-entrypoint.sh"] -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 8 | 9 | ### Description 10 | 11 | 12 | 13 | ### Linked Issues 14 | 15 | 16 | ### Additional context 17 | 18 | -------------------------------------------------------------------------------- /src/utils/openAI.ts: -------------------------------------------------------------------------------- 1 | import { GoogleGenerativeAI } from '@fuyun/generative-ai' 2 | 3 | const apiKey = process.env.GEMINI_API_KEY 4 | const apiBaseUrl = process.env.API_BASE_URL 5 | 6 | const genAI = apiBaseUrl 7 | ? new GoogleGenerativeAI(apiKey, apiBaseUrl) 8 | : new GoogleGenerativeAI(apiKey) 9 | 10 | export const startChatAndSendMessageStream = async(history: ChatMessage[], newMessage: string) => { 11 | const model = genAI.getGenerativeModel({ model: 'gemini-pro' }) 12 | 13 | const chat = model.startChat({ 14 | history: history.map(msg => ({ 15 | role: msg.role, 16 | parts: msg.parts.map(part => part.text).join(''), // Join parts into a single string 17 | })), 18 | generationConfig: { 19 | maxOutputTokens: 8000, 20 | }, 21 | }) 22 | 23 | // Use sendMessageStream for streaming responses 24 | const result = await chat.sendMessageStream(newMessage) 25 | return result.stream 26 | } -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /src/components/SettingsSlider.tsx: -------------------------------------------------------------------------------- 1 | import { Slider } from './Slider' 2 | import type { SettingsUI, SettingsUISlider } from '@/types/provider' 3 | import type { Accessor } from 'solid-js' 4 | 5 | interface Props { 6 | settings: SettingsUI 7 | editing: Accessor 8 | value: Accessor 9 | setValue: (v: number) => void 10 | } 11 | 12 | const SettingsNotDefined = () => { 13 | return ( 14 |
Not Defined
15 | ) 16 | } 17 | 18 | export default ({ settings, editing, value, setValue }: Props) => { 19 | if (!settings.name || !settings.type) return null 20 | const sliderSettings = settings as SettingsUISlider 21 | 22 | return ( 23 |
24 | {editing() && ( 25 | 32 | )} 33 | {!editing() && value() && ( 34 |
{value()}
35 | )} 36 | {!editing() && !value() && ( 37 | 38 | )} 39 |
40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /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 | gemini_api_key=$GEMINI_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|({}).GEMINI_API_KEY|\"$gemini_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|({}).SITE_PASSWORD|\"$site_password\"|g" $file > tmp 26 | mv tmp $file 27 | done 28 | 29 | rm -rf tmp 30 | -------------------------------------------------------------------------------- /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.3.0", 18 | "@astrojs/node": "^5.3.0", 19 | "@astrojs/solid-js": "^2.2.0", 20 | "@astrojs/vercel": "^3.5.0", 21 | "@fuyun/generative-ai": "0.1.1", 22 | "@zag-js/slider": "^0.16.0", 23 | "@zag-js/solid": "^0.16.0", 24 | "astro": "^2.7.0", 25 | "eslint": "^8.43.0", 26 | "eventsource-parser": "^1.0.0", 27 | "highlight.js": "^11.8.0", 28 | "js-sha256": "^0.9.0", 29 | "katex": "^0.16.7", 30 | "markdown-it": "^13.0.1", 31 | "markdown-it-highlightjs": "^4.0.1", 32 | "markdown-it-katex": "^2.0.3", 33 | "solid-js": "1.7.6", 34 | "solidjs-use": "^2.1.0", 35 | "undici": "^5.22.1" 36 | }, 37 | "devDependencies": { 38 | "@evan-yang/eslint-config": "^1.0.9", 39 | "@iconify-json/carbon": "^1.1.18", 40 | "@types/markdown-it": "^12.2.3", 41 | "@typescript-eslint/parser": "^5.60.0", 42 | "@vite-pwa/astro": "^0.1.1", 43 | "eslint-plugin-astro": "^0.27.1", 44 | "punycode": "^2.3.0", 45 | "unocss": "^0.50.8" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /.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 Gemini Pro Chat 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 -------------------------------------------------------------------------------- /src/components/Slider.tsx: -------------------------------------------------------------------------------- 1 | import * as slider from '@zag-js/slider' 2 | import { normalizeProps, useMachine } from '@zag-js/solid' 3 | import { createMemo, createUniqueId, mergeProps } from 'solid-js' 4 | import type { Accessor } from 'solid-js' 5 | import '../slider.css' 6 | 7 | interface Props { 8 | value: Accessor 9 | min: number 10 | max: number 11 | step: number 12 | disabled?: boolean 13 | setValue: (v: number) => void 14 | } 15 | 16 | export const Slider = (selectProps: Props) => { 17 | const props = mergeProps({ 18 | min: 0, 19 | max: 2, 20 | step: 0.01, 21 | disabled: false, 22 | }, selectProps) 23 | 24 | const formatSliderValue = (value: number) => { 25 | if (!value) return 0 26 | return Number.isInteger(value) ? value : parseFloat(value.toFixed(2)) 27 | } 28 | 29 | const [state, send] = useMachine(slider.machine({ 30 | id: createUniqueId(), 31 | value: props.value(), 32 | min: props.min, 33 | max: props.max, 34 | step: props.step, 35 | disabled: props.disabled, 36 | onChange: (details) => { 37 | details && details.value && props.setValue(formatSliderValue(details.value)) 38 | }, 39 | })) 40 | const api = createMemo(() => slider.connect(state, send, normalizeProps)) 41 | return ( 42 |
43 |
44 | Temperature 45 | {formatSliderValue(api().value)} 46 |
47 |
48 |
49 |
50 |
51 |
52 | 53 |
54 |
55 |
56 | ) 57 | } 58 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report_when_use.yml: -------------------------------------------------------------------------------- 1 | name: 🐞 Bug report (When using) 2 | description: Report an issue or possible bug when using `Gemini Pro Chat` 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 AstroPWA from '@vite-pwa/astro' 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 | switch (process.env.OUTPUT) { 13 | case 'vercel': return vercel() 14 | case 'netlify': return netlify() 15 | default: return node({ mode: 'standalone' }) 16 | } 17 | } 18 | 19 | // https://astro.build/config 20 | export default defineConfig({ 21 | integrations: [ 22 | unocss(), 23 | solidJs(), 24 | AstroPWA({ 25 | registerType: 'autoUpdate', 26 | injectRegister: 'inline', 27 | manifest: { 28 | name: 'ChatGPT-API Demo', 29 | short_name: 'ChatGPT Demo', 30 | description: 'A demo repo based on OpenAI API', 31 | theme_color: '#212129', 32 | background_color: '#ffffff', 33 | icons: [ 34 | { 35 | src: 'pwa-192.png', 36 | sizes: '192x192', 37 | type: 'image/png', 38 | }, 39 | { 40 | src: 'pwa-512.png', 41 | sizes: '512x512', 42 | type: 'image/png', 43 | }, 44 | { 45 | src: 'icon.svg', 46 | sizes: '32x32', 47 | type: 'image/svg', 48 | purpose: 'any maskable', 49 | }, 50 | ], 51 | }, 52 | client: { 53 | installPrompt: true, 54 | periodicSyncForUpdates: 20, 55 | }, 56 | devOptions: { 57 | enabled: true, 58 | }, 59 | }), 60 | ], 61 | output: 'server', 62 | adapter: envAdapter(), 63 | vite: { 64 | plugins: [ 65 | process.env.OUTPUT === 'vercel' && disableBlocks(), 66 | process.env.OUTPUT === 'netlify' && disableBlocks(), 67 | ], 68 | }, 69 | }) 70 | -------------------------------------------------------------------------------- /src/layouts/Layout.astro: -------------------------------------------------------------------------------- 1 | --- 2 | import { pwaInfo } from 'virtual:pwa-info' 3 | 4 | export interface Props { 5 | title: string; 6 | } 7 | 8 | const { title } = Astro.props; 9 | --- 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | {title} 22 | 23 | { import.meta.env.HEAD_SCRIPTS && } 24 | { pwaInfo && } 25 | { import.meta.env.PROD && pwaInfo && } 26 | 27 | 28 | 29 | 30 | 31 | 32 | 75 | 76 | 94 | -------------------------------------------------------------------------------- /src/slider.css: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------------------------------------- 2 | * Slider 3 | * -----------------------------------------------------------------------------*/ 4 | 5 | [data-scope='slider'][data-part='root'] { 6 | @apply w-full flex flex-col 7 | } 8 | [data-scope='slider'][data-part='root'][data-orientation='vertical'] { 9 | @apply h-60 10 | } 11 | 12 | [data-scope='slider'][data-part='control'] { 13 | --slider-thumb-size: 14px; 14 | --slider-track-height: 4px; 15 | @apply relative fcc cursor-pointer 16 | } 17 | [data-scope='slider'][data-part='control'][data-orientation='horizontal'] { 18 | @apply h-[var(--slider-thumb-size)]; 19 | } 20 | [data-scope='slider'][data-part='control'][data-orientation='vertical'] { 21 | @apply w-[var(--slider-thumb-size)]; 22 | } 23 | [data-scope='slider'][data-part='control']:hover [data-part='range'] { 24 | @apply bg-gray-400 dark:bg-gray-600 25 | } 26 | [data-scope='slider'][data-part='control']:hover [data-part='thumb'] { 27 | @apply bg-gray-300 dark:bg-gray-400 28 | } 29 | 30 | 31 | [data-scope='slider'][data-part='thumb'] { 32 | all: unset; 33 | @apply bg-gray-200 dark:bg-gray-500 w-[var(--slider-thumb-size)] h-[var(--slider-thumb-size)] rounded-full b-#c5c5d2 b-2 34 | } 35 | [data-scope='slider'][data-part='thumb'][data-disabled] { 36 | @apply w-0 37 | } 38 | 39 | [data-scope='slider'] .control-area { 40 | @apply flex mt-12px 41 | } 42 | 43 | .slider [data-orientation='horizontal'] .control-area { 44 | flex-direction: column; 45 | width: 100%; 46 | } 47 | 48 | .slider [data-orientation='vertical'] .control-area { 49 | flex-direction: row; 50 | height: 100%; 51 | } 52 | 53 | [data-scope='slider'][data-part='track'] { 54 | @apply rounded-full bg-gray-200 dark:bg-neutral-700 55 | } 56 | [data-scope='slider'][data-part='track'][data-orientation='horizontal'] { 57 | @apply h-[var(--slider-track-height)] w-full; 58 | } 59 | [data-scope='slider'][data-part='track'][data-orientation='vertical'] { 60 | @apply h-full w-[var(--slider-track-height)]; 61 | } 62 | 63 | [data-scope='slider'][data-part='range'] { 64 | @apply bg-neutral-300 dark:bg-gray-700 65 | } 66 | [data-scope='slider'][data-part='range'][data-disabled] { 67 | @apply bg-neutral-300 dark:bg-gray-600 68 | } 69 | [data-scope='slider'][data-part='range'][data-orientation='horizontal'] { 70 | @apply h-full; 71 | } 72 | [data-scope='slider'][data-part='range'][data-orientation='vertical'] { 73 | @apply w-full; 74 | } 75 | 76 | [data-scope='slider'][data-part='output'] { 77 | margin-inline-start: 12px; 78 | } 79 | 80 | [data-scope='slider'][data-part='marker'] { 81 | color: lightgray; 82 | } 83 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GeminiProChat 2 | 3 | Minimal web UI for GeminiPro Chat. 4 | 5 | Live demo: [Gemini Pro Chat](https://www.geminiprochat.com) 6 | 7 | [![image](https://github.com/babaohuang/GeminiProChat/assets/559171/d02fd440-401a-410d-a112-4b10935624c6)](https://www.geminiprochat.com) 8 | 9 | ## Acknowledgements 10 | 11 | This project is inspired by and based on the following open-source project: 12 | 13 | - [ChatGPT-Demo](https://github.com/anse-app/chatgpt-demo) - For the foundational codebase and features. 14 | 15 | 16 | ## Running Locally 17 | 18 | ### Pre environment 19 | 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. 20 | 21 | ```bash 22 | node -v 23 | ``` 24 | 25 | 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: 26 | 27 | ```bash 28 | npm i -g pnpm 29 | ``` 30 | 31 | 3. **GEMINI_API_KEY**: Before running this application, you need to obtain the API key from Google. You can register the API key at [https://makersuite.google.com/app/apikey](https://makersuite.google.com/app/apikey). 32 | 33 | ### Getting Started 34 | 35 | 1. Install dependencies 36 | 37 | ```bash 38 | pnpm install 39 | ``` 40 | 41 | 2. Copy the `.env.example` file, then rename it to `.env`, and add your [GEMINI API key](https://makersuite.google.com/app/apikey) to the `.env` file. 42 | 43 | ```bash 44 | GEMINI_API_KEY=AIzaSy... 45 | ``` 46 | 47 | 3. Run the application, the local project runs on `http://localhost:3000/` 48 | 49 | ```bash 50 | pnpm run dev 51 | ``` 52 | 53 | ## Deploy 54 | 55 | ### Deploy With Vercel 56 | 57 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https://github.com/babaohuang/GeminiProChat&env=GEMINI_API_KEY&envDescription=Google%20API%20Key%20for%20GeminiProChat&envLink=https://makersuite.google.com/app/apikey) 58 | 59 | Just click the button above and follow the instructions to deploy your own copy of the app. 60 | 61 | > :warning: **Important Notice Regarding API Access**: Due to Google's strict IP restrictions on Gemini API requests, some regions' IPs may not be able to access their API. Currently, there is an issue with deployments on Vercel, as some of Vercel's edge function nodes are located in IP regions not supported by Google. As a result, users in certain locations might encounter the message **"User location is not supported for the API use."** Please be aware of this limitation when using the service. 62 | 63 | -------------------------------------------------------------------------------- /src/pages/api/generate.ts: -------------------------------------------------------------------------------- 1 | import { startChatAndSendMessageStream } from '@/utils/openAI' 2 | import { verifySignature } from '@/utils/auth' 3 | import type { APIRoute } from 'astro' 4 | 5 | const sitePassword = import.meta.env.SITE_PASSWORD || '' 6 | const passList = sitePassword.split(',') || [] 7 | 8 | export const post: APIRoute = async(context) => { 9 | const body = await context.request.json() 10 | const { sign, time, messages, pass } = body 11 | 12 | if (!messages || messages.length === 0 || messages[messages.length - 1].role !== 'user') { 13 | return new Response(JSON.stringify({ 14 | error: { 15 | message: 'Invalid message history: The last message must be from user role.', 16 | }, 17 | }), { status: 400 }) 18 | } 19 | 20 | if (sitePassword && !(sitePassword === pass || passList.includes(pass))) { 21 | return new Response(JSON.stringify({ 22 | error: { 23 | message: 'Invalid password.', 24 | }, 25 | }), { status: 401 }) 26 | } 27 | 28 | if (import.meta.env.PROD && !await verifySignature({ t: time, m: messages[messages.length - 1].parts.map(part => part.text).join('') }, sign)) { 29 | return new Response(JSON.stringify({ 30 | error: { 31 | message: 'Invalid signature.', 32 | }, 33 | }), { status: 401 }) 34 | } 35 | 36 | try { 37 | const history = messages.slice(0, -1) // All messages except the last one 38 | const newMessage = messages[messages.length - 1].parts.map(part => part.text).join('') 39 | 40 | // Start chat and send message with streaming 41 | const stream = await startChatAndSendMessageStream(history, newMessage) 42 | 43 | const responseStream = new ReadableStream({ 44 | async start(controller) { 45 | for await (const chunk of stream) { 46 | const text = await chunk.text() 47 | const queue = new TextEncoder().encode(text) 48 | controller.enqueue(queue) 49 | } 50 | controller.close() 51 | }, 52 | }) 53 | 54 | return new Response(responseStream, { status: 200, headers: { 'Content-Type': 'text/plain; charset=utf-8' } }) 55 | } catch (error) { 56 | console.error(error) 57 | const errorMessage = error.message 58 | const regex = /https?:\/\/[^\s]+/g 59 | const filteredMessage = errorMessage.replace(regex, '').trim() 60 | const messageParts = filteredMessage.split('[400 Bad Request]') 61 | const cleanMessage = messageParts.length > 1 ? messageParts[1].trim() : filteredMessage 62 | 63 | return new Response(JSON.stringify({ 64 | error: { 65 | code: error.name, 66 | message: cleanMessage, 67 | }, 68 | }), { status: 500 }) 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /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/components/Themetoggle.astro: -------------------------------------------------------------------------------- 1 |
2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 53 | 54 | 89 | -------------------------------------------------------------------------------- /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/SystemRoleSettings.tsx: -------------------------------------------------------------------------------- 1 | import { Show, createEffect, createSignal } from 'solid-js' 2 | import IconEnv from './icons/Env' 3 | import IconX from './icons/X' 4 | import SettingsSlider from './SettingsSlider' 5 | import type { Accessor, Setter } from 'solid-js' 6 | 7 | interface Props { 8 | canEdit: Accessor 9 | systemRoleEditing: Accessor 10 | setSystemRoleEditing: Setter 11 | currentSystemRoleSettings: Accessor 12 | setCurrentSystemRoleSettings: Setter 13 | temperatureSetting: (value: number) => void 14 | } 15 | 16 | export default (props: Props) => { 17 | let systemInputRef: HTMLTextAreaElement 18 | const [temperature, setTemperature] = createSignal(0.6) 19 | 20 | const handleButtonClick = () => { 21 | props.setCurrentSystemRoleSettings(systemInputRef.value) 22 | props.setSystemRoleEditing(false) 23 | } 24 | 25 | createEffect(() => { 26 | props.temperatureSetting(temperature()) 27 | }) 28 | 29 | return ( 30 |
31 | 32 | 33 |
34 |
35 | }> 36 | props.setCurrentSystemRoleSettings('')} class="sys-edit-btn p-1 rd-50%" > 37 | 38 | System Role ( Temp = {temperature()} ) : 39 |
40 |
41 | {props.currentSystemRoleSettings()} 42 |
43 |
44 |
45 | 46 | props.setSystemRoleEditing(!props.systemRoleEditing())} class="sys-edit-btn"> 47 | 48 | Add System Role 49 | 50 | 51 |
52 | 53 |
54 |
55 | 56 | System Role: 57 |
58 |

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

59 |
60 |