├── .dockerignore ├── .env ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.yml │ └── feature_request.yml └── workflows │ └── build-docker-image.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── CHANGELOG.md ├── Dockerfile ├── LICENSE ├── README.md ├── assets ├── demo-en.png ├── demo-zh.png ├── screenshots │ ├── screenshot-1-en.png │ ├── screenshot-1-zh.png │ ├── screenshot-2-en.png │ ├── screenshot-2-zh.png │ ├── screenshot-3-en.png │ └── screenshot-3-zh.png └── speechgpt-icon-text.svg ├── docs ├── README.zh.md ├── developer-guide.md └── developer-guide.zh.md ├── index.html ├── nginx.conf ├── package.json ├── postcss.config.cjs ├── public └── speechgpt.svg ├── src ├── App.tsx ├── apis │ ├── amazonPolly.ts │ ├── azureTTS.ts │ ├── azureToken.ts │ └── openai.ts ├── assets │ └── arrow-down.svg ├── components │ ├── AboutDialog.tsx │ ├── AppearanceSelector.tsx │ ├── AzureSpeechToText.tsx │ ├── BrowserSpeechToText.tsx │ ├── ButtonGroup.tsx │ ├── Content.tsx │ ├── ConversationPanel.tsx │ ├── Conversations │ │ ├── ConversationIcons.tsx │ │ ├── ConversationItem.tsx │ │ └── Sidebar.tsx │ ├── EllipsisMenu.tsx │ ├── Header.tsx │ ├── Icons │ │ ├── AboutIcon.tsx │ │ ├── BlubIcon.tsx │ │ ├── ChatIcon.tsx │ │ ├── HelpIcon.tsx │ │ ├── MicrophoneIcon.tsx │ │ ├── OKCircleIcon.tsx │ │ ├── RightTriangleIcon.tsx │ │ ├── SpeakerIcon.tsx │ │ ├── SpeechGPTIcon.tsx │ │ ├── SpinnerIcon.tsx │ │ ├── StartCircleIcon.tsx │ │ ├── StopCircleIcon.tsx │ │ ├── WarningIcon.tsx │ │ └── XIcon.tsx │ ├── InputPanel.tsx │ ├── LocaleSelector.tsx │ ├── MobileSettingsSelector.tsx │ ├── Notification.tsx │ ├── Record.tsx │ ├── Settings │ │ ├── ChatSection.tsx │ │ ├── PollyVoice.tsx │ │ ├── RecognitionSection.tsx │ │ ├── SettingContent.tsx │ │ ├── SettingDialog.tsx │ │ ├── SettingSelector.tsx │ │ ├── SynthesisSection.tsx │ │ ├── azureTtsVoice.tsx │ │ └── base │ │ │ ├── SettingCheckText.tsx │ │ │ ├── SettingDivider.tsx │ │ │ ├── SettingGroup.tsx │ │ │ ├── SettingInput.tsx │ │ │ ├── SettingSelect.tsx │ │ │ ├── SettingSlider.tsx │ │ │ ├── SettingSubtitle.tsx │ │ │ ├── SettingSwitch.tsx │ │ │ ├── SettingTextArea.tsx │ │ │ ├── SettingTitle.tsx │ │ │ └── SettingWarningText.tsx │ ├── Tips.tsx │ └── base │ │ ├── Button.tsx │ │ ├── Dialog.tsx │ │ ├── DropdownMenu.tsx │ │ ├── Input.tsx │ │ ├── RangeSlider.tsx │ │ ├── Select.tsx │ │ ├── Textarea.tsx │ │ ├── TippyButton.tsx │ │ └── Toggle.tsx ├── constants │ └── data.ts ├── css │ └── index.css ├── db │ ├── chat.ts │ └── index.ts ├── helpers │ ├── markdown │ │ ├── index.tsx │ │ ├── matcher.ts │ │ └── parser │ │ │ ├── Blockquote.tsx │ │ │ ├── Bold.tsx │ │ │ ├── BoldEmphasis.tsx │ │ │ ├── Br.tsx │ │ │ ├── CodeBlock.tsx │ │ │ ├── DoneList.tsx │ │ │ ├── Emphasis.tsx │ │ │ ├── Heading.tsx │ │ │ ├── HorizontalRules.tsx │ │ │ ├── Image.tsx │ │ │ ├── InlineCode.tsx │ │ │ ├── Link.tsx │ │ │ ├── OrderedList.tsx │ │ │ ├── Paragraph.tsx │ │ │ ├── PlainLink.tsx │ │ │ ├── PlainText.tsx │ │ │ ├── Strikethrough.tsx │ │ │ ├── Table.tsx │ │ │ ├── Tag.tsx │ │ │ ├── TodoList.tsx │ │ │ ├── UnorderedList.tsx │ │ │ └── index.ts │ └── utils.ts ├── i18n.ts ├── locales │ ├── en.json │ ├── es.json │ └── zh-CN.json ├── main.tsx ├── pages │ ├── Home.tsx │ └── NotFound.tsx ├── store │ ├── index.ts │ ├── module │ │ ├── global.ts │ │ ├── index.ts │ │ └── session.ts │ └── reducer │ │ ├── global.ts │ │ └── session.ts ├── typings │ ├── chat.d.ts │ ├── env.d.ts │ ├── global.d.ts │ └── session.d.ts ├── utils │ ├── speechSynthesis.ts │ └── version.ts └── vite-env.d.ts ├── tailwind.config.cjs ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | **/.dockerignore 2 | **/.git 3 | **/.gitignore 4 | **/.settings 5 | **/.vscode 6 | **/.idea 7 | **/dist 8 | **/Dockerfile* 9 | **/node_modules 10 | **/npm-debug.log 11 | **/yarn-error.log 12 | LICENSE 13 | README.md 14 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_ACCESS_CODE=REPLACE_WITH_YOUR_OWN 2 | VITE_OPENAI_API_KEY=REPLACE_WITH_YOUR_OWN 3 | VITE_OPENAI_HOST=REPLACE_WITH_YOUR_OWN 4 | VITE_OPENAI_MODEL=REPLACE_WITH_YOUR_OWN 5 | VITE_AWS_REGION=REPLACE_WITH_YOUR_OWN 6 | VITE_AWS_ACCESS_KEY_ID=REPLACE_WITH_YOUR_OWN 7 | VITE_AWS_ACCESS_KEY=REPLACE_WITH_YOUR_OWN 8 | VITE_AZURE_REGION=REPLACE_WITH_YOUR_OWN 9 | VITE_AZURE_KEY=REPLACE_WITH_YOUR_OWN 10 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.yml: -------------------------------------------------------------------------------- 1 | name: Bug Report 2 | description: Create a report to help us improve 3 | labels: [bug] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | If you are reporting a new issue, make sure that we do not have any duplicates already open. You can ensure this by searching the issue list for this repository. If there is a duplicate, please close your issue and add a comment to the existing issue instead. 9 | - type: textarea 10 | attributes: 11 | label: Describe the bug 12 | description: | 13 | Briefly describe the problem you are having in a few paragraphs. 14 | validations: 15 | required: true 16 | - type: textarea 17 | attributes: 18 | label: Steps to reproduce 19 | description: | 20 | Provide the steps to reproduce the issue. 21 | placeholder: | 22 | 1. Go to '...' 23 | 2. Click on '....' 24 | 3. See error 25 | validations: 26 | required: true 27 | - type: textarea 28 | attributes: 29 | label: Screenshots or additional context 30 | description: | 31 | Add screenshots or any other context about the problem. 32 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.yml: -------------------------------------------------------------------------------- 1 | name: Feature Request 2 | description: Suggest an idea for this project 3 | labels: [enhancement] 4 | body: 5 | - type: markdown 6 | attributes: 7 | value: | 8 | Thanks for taking the time to suggest an idea for Memos! 9 | - type: textarea 10 | attributes: 11 | label: Is your feature request related to a problem? 12 | description: | 13 | A clear and concise description of what the problem is. 14 | placeholder: | 15 | I'm always frustrated when [...] 16 | validations: 17 | required: true 18 | - type: textarea 19 | attributes: 20 | label: Describe the solution you'd like 21 | description: | 22 | A clear and concise description of what you want to happen. 23 | validations: 24 | required: true 25 | - type: textarea 26 | attributes: 27 | label: Additional context 28 | description: Add any other context or screenshots about the feature request. 29 | -------------------------------------------------------------------------------- /.github/workflows/build-docker-image.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | 3 | on: 4 | push: 5 | branches: [ "main" ] 6 | pull_request: 7 | branches: [ "main" ] 8 | 9 | env: 10 | IMAGE_NAME: speechgpt 11 | DOCKER_USERNAME: ${{ secrets.DOCKER_USERNAME }} 12 | DOCKER_PASSWORD: ${{ secrets.DOCKER_PASSWORD }} 13 | 14 | jobs: 15 | build-and-push-image: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout code 19 | uses: actions/checkout@v2 20 | 21 | - name: Login to Docker Hub 22 | uses: docker/login-action@v1 23 | with: 24 | username: ${{ env.DOCKER_USERNAME }} 25 | password: ${{ env.DOCKER_PASSWORD }} 26 | 27 | - name: Build and push Docker image 28 | uses: docker/build-push-action@v2 29 | with: 30 | context: . 31 | push: true 32 | tags: ${{ env.DOCKER_USERNAME }}/${{ env.IMAGE_NAME }}:latest 33 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | node_modules 11 | dist 12 | dist-ssr 13 | *.local 14 | 15 | # Editor directories and files 16 | .vscode/* 17 | !.vscode/extensions.json 18 | .idea 19 | .DS_Store 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /dist 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 100, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "es5", 8 | "bracketSpacing": true, 9 | "jsxBracketSameLine": false, 10 | "arrowParens": "avoid", 11 | "proseWrap": "preserve" 12 | } 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 📝 Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/). 6 | 7 | ## [0.5.1] - 2023-05-15 8 | ### 🛠️ Changed 9 | - Retain disable speaker and disable microphone states after refresh 10 | - Disable send button when the input is empty 11 | - Hide record button when the microphone is disabled 12 | - Hide replay button when the speaker is disabled 13 | - Reset conversation confirmation 14 | 15 | ### 🐞 Fixed 16 | - Reset confirmation disappear problem 17 | 18 | ## [0.5.0] - 2023-05-12 19 | ### 🛠️ Changed 20 | - Brand-new UI design 21 | - New about page design 22 | - New color theme 23 | - New icon library 24 | 25 | ### ✨ Added 26 | - Allow to change conversation color 27 | - Allow to clear all conversations 28 | - Delete requires confirmations 29 | 30 | ### 🐞 Fixed 31 | - Delete multiple conversations at once error 32 | 33 | ## [0.4.3] - 2023-04-26 34 | ### 🛠️ Changed 35 | - Improve website SEO 36 | - Update `README.md` 37 | 38 | ### ✨ Added 39 | - Add developer guide 40 | 41 | ### 🐞 Fixed 42 | - Typo in the settings page 43 | 44 | ## [0.4.2] - 2023-04-22 45 | ### 🛠️ Changed 46 | - Update settings about page link icons 47 | - Improve the stability of the Azure speech synthesis service 48 | - i18n of the input panel 49 | 50 | ## [0.4.1] - 2023-04-19 51 | 52 | ### ✨ Added 53 | - Allow user to set access code to protect the app (#61) 54 | - Add change log 55 | 56 | ## [0.4.0] - 2023-04-17 57 | ### ✨ Added 58 | - Support multiple conversions 59 | - Allow to change OpenAI model 60 | 61 | ### 🐞 Fixed 62 | - Hide button when text is selected 63 | 64 | ## [0.3.0] - 2023-04-10 65 | ### ✨ Added 66 | - Add replay button 67 | - Supports saving chat records locally 68 | 69 | ### 🐞 Fixed 70 | - Hide keyboard on mobile when user send a message 71 | - Fix bug when using environment variables to set OpenAI Key 72 | - Cannot delete message error 73 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Use the ARM64 Node.js image as the builder stage 2 | FROM node:18-bullseye-slim AS builder 3 | 4 | ARG VITE_OPENAI_API_KEY=REPLACE_WITH_YOUR_OWN 5 | ARG VITE_OPENAI_HOST=REPLACE_WITH_YOUR_OWN 6 | ARG VITE_AWS_REGION=REPLACE_WITH_YOUR_OWN 7 | ARG VITE_AWS_ACCESS_KEY_ID=REPLACE_WITH_YOUR_OWN 8 | ARG VITE_AWS_ACCESS_KEY=REPLACE_WITH_YOUR_OWN 9 | ARG VITE_AZURE_REGION=REPLACE_WITH_YOUR_OWN 10 | ARG VITE_AZURE_KEY=REPLACE_WITH_YOUR_OWN 11 | 12 | ENV VITE_OPENAI_API_KEY=$VITE_OPENAI_API_KEY \ 13 | VITE_OPENAI_HOST=$VITE_OPENAI_HOST \ 14 | VITE_AWS_REGION=$VITE_AWS_REGION \ 15 | VITE_AWS_ACCESS_KEY_ID=$VITE_AWS_ACCESS_KEY_ID \ 16 | VITE_AWS_ACCESS_KEY=$VITE_AWS_ACCESS_KEY \ 17 | VITE_AZURE_REGION=$VITE_AZURE_REGION \ 18 | VITE_AZURE_KEY=$VITE_AZURE_KEY 19 | 20 | WORKDIR /app 21 | COPY package.json yarn.lock ./ 22 | RUN yarn install 23 | COPY . . 24 | RUN yarn build 25 | 26 | # Use a smaller ARM64-compatible base image for the final stage 27 | FROM arm64v8/nginx:alpine 28 | 29 | COPY nginx.conf /etc/nginx/nginx.conf 30 | WORKDIR /usr/share/nginx/html 31 | COPY --from=builder /app/dist . 32 | 33 | ENTRYPOINT ["nginx", "-g", "daemon off;"] 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 SpeechGPT 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | SpeechGPT 3 |

4 | 5 |

6 | Website • 7 | [中文] 8 |

9 | 10 |

11 | SpeechGPT Website Demo 12 |

13 | 14 | ## 🌟 Introduction 15 | SpeechGPT is a web application that enables you to converse with ChatGPT.
16 | You can utilize this app to improve your language speaking skills or simply have fun chatting with ChatGPT. 17 | 18 | ## 🚀 Features 19 | - 📖 **Open source and free**: Anyone can use, modify it without cost. 20 | - 🔒 **Privacy First**: All data is stored locally. 21 | - 📱 **Mobile friendly**: Designed to be accessible and usable on mobile devices. 22 | - 📚 **Support for multiple languages**: Supports over 100 languages. 23 | - 🎙 **Speech Recognition**: Includes both built-in speech recognition and integration with Azure Speech Services. 24 | - 🔊 **Speech Synthesis**: Includes built-in speech synthesis, as well as integration with Amazon Polly and Azure Speech Services. 25 | 26 | ## 📸 Screenshots 27 | 28 | 29 | 30 | 31 | 32 | 33 |
Screenshot 1Screenshot 2Screenshot 3
34 | 35 | ## 📖 Tutorial 36 | 1. Set the OpenAI API Key
37 | - Go to Settings and navigate to the Chat section. 38 | - Set the OpenAI API Key. 39 | - If you don't have an OpenAI API Key, follow this tutorial on [how to get an OpenAI API Key](https://www.windowscentral.com/software-apps/how-to-get-an-openai-api-key). 40 | 2. Set up Azure Speech Services (optional) 41 | - Go to Settings and navigate to the Synthesis section. 42 | - Change the Speech Synthesis Service to Azure TTS. 43 | - Set the Azure Region and Azure Access Key. 44 | 3. Set up Amazon Polly (optional) 45 | - Go to Settings and navigate to the Synthesis section. 46 | - Change the Speech Synthesis Service to Amazon Polly. 47 | - Set the AWS Region, AWS Access Key ID, and Secret Access Key (the Access Key should have the AmazonPollyFullAccess policy). 48 | - If you don't have an AWS Access Key, follow this tutorial on [how to create an IAM user in AWS](https://www.techtarget.com/searchcloudcomputing/tutorial/Step-by-step-guide-on-how-to-create-an-IAM-user-in-AWS). 49 | 50 | ## 💻 Development Guide and Changelog 51 | - For more information on setting up your development environment, please see our [Development Guide](./docs/developer-guide.md). 52 | - To view the project's history of notable changes, please check the [Changelog](./CHANGELOG.md). 53 | 54 | ## 🚢 Deployment 55 | 56 | ### Deploying with Vercel 57 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fhahahumble%2Fspeechgpt&env=VITE_OPENAI_API_KEY,VITE_OPENAI_HOST,VITE_AWS_REGION,VITE_AWS_ACCESS_KEY_ID,VITE_AWS_ACCESS_KEY,VITE_AZURE_REGION,VITE_AZURE_KEY&envDescription=If%20you%20do%20not%20want%20to%20provide%20a%20value%2C%20use%20REPLACE_WITH_YOUR_OWN.&project-name=speechgpt&repository-name=speechgpt) 58 | 59 | ### Deploying with Docker 60 | 1. Pull the Docker image:arm64. 61 | ```bash 62 | docker pull hahahumble/speechgpt 63 | ``` 64 | 65 | 2. Run the Docker container. 66 | ```bash 67 | docker run -d -p 8080:8080 --name speechgpt hahahumble/speechgpt 68 | ``` 69 | 70 | 3. Visit `http://localhost:8080/` to access the application. 71 | 72 | ### Building and running the Docker image 73 | 1. Build the Docker image. 74 | ```bash 75 | docker build -t speechgpt:arm64 -f Dockerfile . 76 | ``` 77 | 78 | 2. Run the Docker container. 79 | ```bash 80 | docker run -d -p 8080:8080 --name=speechgpt speechgpt 81 | ``` 82 | 83 | 3. Visit `http://localhost:8080/` to access the application. 84 | 85 | ## 📄 License 86 | This project is licensed under the terms of the [MIT license](/LICENSE). 87 | -------------------------------------------------------------------------------- /assets/demo-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahahumble/speechgpt/215e3a07a65eafede2ba3257939a4e59cfd01ae6/assets/demo-en.png -------------------------------------------------------------------------------- /assets/demo-zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahahumble/speechgpt/215e3a07a65eafede2ba3257939a4e59cfd01ae6/assets/demo-zh.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot-1-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahahumble/speechgpt/215e3a07a65eafede2ba3257939a4e59cfd01ae6/assets/screenshots/screenshot-1-en.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot-1-zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahahumble/speechgpt/215e3a07a65eafede2ba3257939a4e59cfd01ae6/assets/screenshots/screenshot-1-zh.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot-2-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahahumble/speechgpt/215e3a07a65eafede2ba3257939a4e59cfd01ae6/assets/screenshots/screenshot-2-en.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot-2-zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahahumble/speechgpt/215e3a07a65eafede2ba3257939a4e59cfd01ae6/assets/screenshots/screenshot-2-zh.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot-3-en.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahahumble/speechgpt/215e3a07a65eafede2ba3257939a4e59cfd01ae6/assets/screenshots/screenshot-3-en.png -------------------------------------------------------------------------------- /assets/screenshots/screenshot-3-zh.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hahahumble/speechgpt/215e3a07a65eafede2ba3257939a4e59cfd01ae6/assets/screenshots/screenshot-3-zh.png -------------------------------------------------------------------------------- /docs/README.zh.md: -------------------------------------------------------------------------------- 1 |

2 | SpeechGPT 3 |

4 | 5 |

6 | 网站 7 |

8 | 9 |

10 | SpeechGPT Website Demo 11 |

12 | 13 | ## 🌟 简介 14 | SpeechGPT 是一个让你与 ChatGPT 聊天的网站。
15 | 你可以使用 SpeechGPT 来练习你的口语,或者只是和 ChatGPT 闲聊。 16 | 17 | ## 🚀 特点 18 | - 📖 **开源免费**: 任何人都可以免费使用、修改。 19 | - 🔒 **隐私至上**: 所有数据都存储在本地,保护用户隐私。 20 | - 📱 **移动端友好**: 具有响应式设计。 21 | - 📚 **支持多种语言**: 支持超过 100 种语言。 22 | - 🎙 **语音识别**: 包括浏览器内置的语音识别功能和与 Azure 语音服务的集成。 23 | - 🔊 **语音合成**: 包括浏览器内置的语音合成功能,以及与 Amazon Polly 和 Azure 语音服务的集成。 24 | 25 | ## 📸 屏幕截图 26 | 27 | 28 | 29 | 30 | 31 | 32 |
Screenshot 1Screenshot 2Screenshot 3
33 | 34 | ## 📖 教程 35 | 1. 设置 OpenAI API Key
36 | - 进入设置,进入对话部分。 37 | - 设置 OpenAI API Key。 38 | - 如果您没有 OpenAI API Key,请按照[如何获取 OpenAI API Key 的教程](https://www.windowscentral.com/software-apps/how-to-get-an-openai-api-key)进行操作。 39 | 2. 设置 Azure 语音服务(可选) 40 | - 进入设置,进入语音合成部分。 41 | - 将语音合成服务更改为 Azure TTS。 42 | - 设置 Azure 区域和 Azure 访问密钥。 43 | 3. 设置 Amazon Polly(可选) 44 | - 进入设置,进入语音合成部分。 45 | - 将语音合成服务更改为 Amazon Polly。 46 | - 设置 AWS 区域、AWS 访问密钥 ID 和密钥访问密钥(访问密钥应具有 AmazonPollyFullAccess 策略)。 47 | - 如果您没有 AWS Access Key,请按照[如何在 AWS 中创建 IAM 用户的教程](https://www.techtarget.com/searchcloudcomputing/tutorial/Step-by-step-guide-on-how-to-create-an-IAM-user-in-AWS)进行操作。 48 | 49 | ## 💻 开发指南和更新日志 50 | - 如需了解有关设置开发环境的更多信息,请查看我们的[开发指南](./developer-guide.zh.md)。 51 | - 若要查看项目的重要更改历史,请查阅[更新日志](../CHANGELOG.md)。 52 | 53 | ## 🚢 部署 54 | ### 使用 Vercel 部署 55 | [![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fhahahumble%2Fspeechgpt&env=VITE_OPENAI_API_KEY,VITE_OPENAI_HOST,VITE_AWS_REGION,VITE_AWS_ACCESS_KEY_ID,VITE_AWS_ACCESS_KEY,VITE_AZURE_REGION,VITE_AZURE_KEY&envDescription=If%20you%20do%20not%20want%20to%20provide%20a%20value%2C%20use%20REPLACE_WITH_YOUR_OWN.&project-name=speechgpt&repository-name=speechgpt) 56 | 57 | ### 使用 Docker 部署 58 | 1. 拉取 Docker 镜像 59 | ```bash 60 | docker pull hahahumble/speechgpt 61 | ``` 62 | 63 | 2. 运行 Docker 容器 64 | ```bash 65 | docker run -d -p 8080:8080 --name speechgpt hahahumble/speechgpt 66 | ``` 67 | 68 | 3. 访问 `http://localhost:8080/` 69 | 70 | ### 构建和运行 Docker 镜像 71 | 1. 构建 Docker 镜像 72 | ```bash 73 | docker build -t speechgpt . 74 | ``` 75 | 76 | 2. 运行 Docker 容器 77 | ```bash 78 | docker run -d -p 8080:8080 --name=speechgpt speechgpt 79 | ``` 80 | 81 | 3. 访问 `http://localhost:8080/` 82 | 83 | ## 📄 许可 84 | 本项目根据 [MIT 许可证](/LICENSE) 的条款进行许可。 85 | -------------------------------------------------------------------------------- /docs/developer-guide.md: -------------------------------------------------------------------------------- 1 | # Development Guide for SpeechGPT 2 | This development guide provides an overview of the SpeechGPT project, its technology stack, and the file structure. Follow the instructions below to get started with development. 3 | 4 | ## Technology Stack 5 | SpeechGPT uses the following technology stack: 6 | 7 | - [React 18](https://reactjs.org/) 8 | - [Vite](https://vitejs.dev/) 9 | - [TypeScript](https://www.typescriptlang.org/) 10 | - [Redux Toolkit](https://redux-toolkit.js.org/) 11 | - [TailwindCSS](https://tailwindcss.com/) 12 | 13 | ## File Structure 14 | The project is organized as follows: 15 | 16 | ```text 17 | . 18 | ├── .github 19 | ├── assets 20 | ├── docs 21 | ├── public 22 | └── src 23 | ├── apis 24 | ├── assets 25 | ├── components 26 | ├── constants 27 | ├── css 28 | ├── db 29 | ├── helpers 30 | ├── locales 31 | ├── pages 32 | ├── store 33 | ├── typings 34 | └── utils 35 | ``` 36 | 37 | ## Directory Descriptions 38 | - `.github`: Contains GitHub Actions workflows and configurations. 39 | - `assets`: Contains project-specific assets, such as images or other media files. 40 | - `docs`: Holds documentation files for the project. 41 | - `public`: Includes static files and assets used in the project. 42 | - `src`: The main source code directory for the application. 43 | - `apis`: Contains API-related functions and configurations. 44 | - `components`: Holds React components used throughout the application. 45 | - `constants`: Includes constant values and configurations. 46 | - `css`: Holds CSS files and styles for the project. 47 | - `db`: Contains database configurations and related files. 48 | - `helpers`: Stores helper functions for specific tasks and logic. 49 | - `locales`: Stores localization and internationalization files. 50 | - `pages`: Contains the main page components for the application. 51 | - `store`: Contains Redux store configurations and related files. 52 | - `typings`: Contains TypeScript type definitions and interfaces. 53 | - `utils`: Stores utility functions and helper methods. 54 | 55 | ## Git Commit Guidelines 56 | When committing changes to the repository, follow these commit message conventions to maintain a consistent and readable Git history: 57 | 58 | - `feat`: New features or modifications to existing features. 59 | - `fix`: Bug fixes. 60 | - `docs`: Changes to documentation files. 61 | - `style`: Formatting changes that do not affect code execution (e.g., whitespace, formatting, missing semicolons, etc.). 62 | - `refactor`: Code changes that are neither new features nor bug fixes but improve code quality. 63 | - `perf`: Performance-enhancing code changes. 64 | - `test`: Adding missing tests or updating existing tests. 65 | - `chore`: Changes to build processes or auxiliary tools. 66 | - `revert`: Reverting a previous commit (e.g., revert: type(scope): subject (revert to version: xxxx)). 67 | 68 | ## Getting Started 69 | 1. Clone the repository. 70 | ```shell 71 | git clone https://github.com/hahahumble/speechgpt.git 72 | ``` 73 | 74 | 2. Install the required dependencies. 75 | ```shell 76 | yarn install 77 | ``` 78 | 79 | 3. Start the development server. The application should be accessible at http://localhost:5173. 80 | ```shell 81 | yarn dev 82 | ``` 83 | 84 | 4. Build the application for production. The output files will be in the dist directory. 85 | ```shell 86 | yarn build 87 | ``` 88 | 89 | 5. Run the application in production mode. The application should be accessible at http://localhost:4173. 90 | ```shell 91 | yarn serve 92 | ``` 93 | 94 | Code formatting(Using Prettier). 95 | ```shell 96 | yarn format 97 | ``` 98 | 99 | 🚀 Start your SpeechGPT development journey! If you encounter any problems during the development process, please feel free to check out the documentation or submit a question on GitHub. Have fun developing! 🎉 100 | -------------------------------------------------------------------------------- /docs/developer-guide.zh.md: -------------------------------------------------------------------------------- 1 | # SpeechGPT 开发指南 2 | 本开发指南概述了 SpeechGPT 项目、技术栈和文件结构。按照以下说明开始开发。 3 | 4 | ## 技术栈 5 | SpeechGPT 使用以下技术栈: 6 | 7 | - [React 18](https://reactjs.org/) 8 | - [Vite](https://vitejs.dev/) 9 | - [TypeScript](https://www.typescriptlang.org/) 10 | - [Redux Toolkit](https://redux-toolkit.js.org/) 11 | - [TailwindCSS](https://tailwindcss.com/) 12 | 13 | ## 文件结构 14 | 项目组织如下: 15 | 16 | ```text 17 | . 18 | ├── .github 19 | ├── assets 20 | ├── docs 21 | ├── public 22 | └── src 23 | ├── apis 24 | ├── assets 25 | ├── components 26 | ├── constants 27 | ├── css 28 | ├── db 29 | ├── helpers 30 | ├── locales 31 | ├── pages 32 | ├── store 33 | ├── typings 34 | └── utils 35 | ``` 36 | 37 | ## 目录描述 38 | - `.github`: 包含 GitHub Actions 工作流和配置。 39 | - `assets`: 包含项目特定的资源,如图像或其他媒体文件。 40 | - `docs`: 存放项目文档文件。 41 | - `public`: 包含项目中使用的静态文件和资源。 42 | - `src`: 应用程序的主要源代码目录。 43 | - `apis`: 包含 API 相关的函数和配置。 44 | - `components`: 存放应用程序中使用的 React 组件。 45 | - `constants`: 包含常量值和配置。 46 | - `css`: 存放项目的 CSS 文件和样式。 47 | - `db`: 包含数据库配置和相关文件。 48 | - `helpers`: 存储特定任务和逻辑的辅助函数。 49 | - `locales`: 存放本地化和国际化文件。 50 | - `pages`: 包含应用程序的主要页面组件。 51 | - `store`: 包含 Redux 存储配置和相关文件。 52 | - `typings`: 包含 TypeScript 类型定义和接口。 53 | - `utils`: 存储实用函数和辅助方法。 54 | 55 | ## Git 提交指南 56 | 在将更改提交到仓库时,请遵循这些提交消息约定,以保持一致且可读的 Git 历史记录: 57 | 58 | - `feat`:新增功能或对现有功能的修改。 59 | - `fix`:修复 bug。 60 | - `docs`:文档文件的更改。 61 | - `style`:不影响代码执行的格式更改(例如,空格、格式化、缺失的分号等)。 62 | - `refactor`:既非新功能,也非 bug 修复,但能提高代码质量的更改。 63 | - `perf`:提高性能的代码更改。 64 | - `test`:添加缺失的测试或更新现有测试。 65 | - `revert`:撤销以前的提交(例如,revert: type(scope): subject(回滚到版本:xxxx))。 66 | 67 | ## 开始 68 | 1. 克隆仓库。 69 | ```shell 70 | git clone https://github.com/hahahumble/speechgpt.git 71 | ``` 72 | 73 | 2. 安装所需的依赖项。 74 | ```shell 75 | yarn install 76 | ``` 77 | 78 | 3. 启动开发服务器。应用程序应可在 http://localhost:5173 上访问。 79 | ```shell 80 | yarn dev 81 | ``` 82 | 83 | 4. 为生产环境构建应用程序。输出文件将位于 dist 目录中。 84 | ```shell 85 | yarn build 86 | ``` 87 | 88 | 5. 以生产模式运行应用程序。应用程序应可在 http://localhost:4173 上访问。 89 | ```shell 90 | yarn serve 91 | ``` 92 | 93 | 代码格式化(使用 Prettier)。 94 | ```shell 95 | yarn format 96 | ``` 97 | 98 | 🚀 开始您的 SpeechGPT 开发之旅吧!如果您在开发过程中遇到任何问题,请随时查阅相关文档或在 GitHub 上提交问题。祝您开发愉快!🎉 99 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | SpeechGPT 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /nginx.conf: -------------------------------------------------------------------------------- 1 | worker_processes 1; 2 | events { 3 | worker_connections 1024; 4 | } 5 | http { 6 | include mime.types; 7 | default_type application/octet-stream; 8 | sendfile on; 9 | keepalive_timeout 65; 10 | gzip on; 11 | server { 12 | listen 8080; 13 | server_name localhost; 14 | location / { 15 | root /usr/share/nginx/html; 16 | index index.html index.htm; 17 | try_files $uri $uri/ /index.html; 18 | } 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "speechgpt", 3 | "private": true, 4 | "version": "0.5.1", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "preview": "vite preview", 10 | "format": "prettier --write \"src/**/*.{ts,tsx,js,jsx,json,css,scss,md}\"" 11 | }, 12 | "dependencies": { 13 | "@aws-sdk/client-polly": "^3.303.0", 14 | "@aws-sdk/client-transcribe-streaming": "^3.303.0", 15 | "@aws-sdk/polly-request-presigner": "^3.303.0", 16 | "@headlessui/react": "^1.7.13", 17 | "@heroicons/react": "^2.0.18", 18 | "@reduxjs/toolkit": "^1.9.3", 19 | "@tabler/icons-react": "^2.18.0", 20 | "@tippyjs/react": "^4.2.6", 21 | "@types/i18next": "^13.0.0", 22 | "@types/react-speech-recognition": "^3.9.1", 23 | "@types/react-transition-group": "^4.4.6", 24 | "@types/uuid": "^9.0.1", 25 | "aws-sdk": "^2.1348.0", 26 | "axios": "^1.3.4", 27 | "classnames": "^2.3.2", 28 | "dexie": "^3.2.3", 29 | "dexie-react-hooks": "^1.1.3", 30 | "highlight.js": "^11.7.0", 31 | "i18next": "^22.4.13", 32 | "microsoft-cognitiveservices-speech-sdk": "^1.26.0", 33 | "openai": "^3.2.1", 34 | "react": "^18.2.0", 35 | "react-cool-onclickoutside": "^1.7.0", 36 | "react-device-detect": "^2.2.3", 37 | "react-dom": "^18.2.0", 38 | "react-hook-speech-to-text": "^0.8.0", 39 | "react-hot-toast": "^2.4.0", 40 | "react-i18next": "^12.2.0", 41 | "react-markdown": "^8.0.6", 42 | "react-redux": "^8.0.5", 43 | "react-router-dom": "^6.10.0", 44 | "react-scroll": "^1.8.9", 45 | "react-textarea-autosize": "^8.4.1", 46 | "react-transition-group": "^4.4.5", 47 | "uuid": "^9.0.0" 48 | }, 49 | "devDependencies": { 50 | "@types/react": "^18.0.31", 51 | "@types/react-dom": "^18.0.11", 52 | "@types/react-scroll": "^1.8.6", 53 | "@vitejs/plugin-react": "^3.1.0", 54 | "autoprefixer": "^10.4.14", 55 | "postcss": "^8.4.23", 56 | "prettier": "2.8.7", 57 | "tailwindcss": "^3.3.1", 58 | "typescript": "^5.0.3", 59 | "vite": "^4.2.1" 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /postcss.config.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | tailwindcss: {}, 4 | autoprefixer: {}, 5 | }, 6 | } 7 | -------------------------------------------------------------------------------- /public/speechgpt.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | import { BrowserRouter as Router, Route, Routes } from 'react-router-dom'; 2 | import React from 'react'; 3 | import Home from './pages/Home'; 4 | import { initialGlobalState, initialSessionState } from './store/module'; 5 | import NotFound from './pages/NotFound'; 6 | import 'highlight.js/styles/github.css'; 7 | 8 | function App() { 9 | initialGlobalState(); 10 | initialSessionState(); 11 | return ( 12 | 13 | 14 | } /> 15 | } /> 16 | 17 | 18 | ); 19 | } 20 | 21 | export default App; 22 | -------------------------------------------------------------------------------- /src/apis/amazonPolly.ts: -------------------------------------------------------------------------------- 1 | import { Polly } from '@aws-sdk/client-polly'; 2 | import { getSynthesizeSpeechUrl } from '@aws-sdk/polly-request-presigner'; 3 | 4 | const speechParams = { 5 | OutputFormat: 'mp3', 6 | SampleRate: '16000', 7 | VoiceId: 'Matthew', 8 | TextType: 'text', 9 | Text: '', 10 | Engine: 'neural', 11 | }; 12 | 13 | export default async function speechSynthesizeWithPolly( 14 | text: string, 15 | voiceId: string = 'Matthew', 16 | engine: string = 'neural', 17 | aws_region: string, 18 | aws_id: string, 19 | aws_key: string 20 | ) { 21 | speechParams.Text = text; 22 | speechParams.VoiceId = voiceId; 23 | speechParams.Engine = engine; 24 | 25 | try { 26 | const polly = new Polly({ 27 | region: aws_region, 28 | credentials: { 29 | accessKeyId: aws_id, 30 | secretAccessKey: aws_key, 31 | }, 32 | }); 33 | // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/Polly.html#synthesizeSpeech-property 34 | return getSynthesizeSpeechUrl({ client: polly, params: speechParams }); 35 | } catch (err) { 36 | console.log('Error', err); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/apis/azureTTS.ts: -------------------------------------------------------------------------------- 1 | import * as sdk from 'microsoft-cognitiveservices-speech-sdk'; 2 | import { azureSynthesisErrorNotify } from '../components/Notification'; 3 | 4 | const speechSynthesizeWithAzure = async ( 5 | subscriptionKey: string, 6 | region: string, 7 | text: string, 8 | voiceName: string, 9 | language: string 10 | ) => { 11 | console.time('Azure speech synthesis'); 12 | const speechConfig = sdk.SpeechConfig.fromSubscription(subscriptionKey, region); 13 | speechConfig.speechRecognitionLanguage = language; 14 | speechConfig.speechSynthesisVoiceName = voiceName; 15 | const player = new sdk.SpeakerAudioDestination(); 16 | const audioConfig = sdk.AudioConfig.fromSpeakerOutput(player); 17 | const speechSynthesizer = new sdk.SpeechSynthesizer(speechConfig, audioConfig); 18 | speechSynthesizer.speakTextAsync( 19 | text, 20 | result => { 21 | console.timeEnd('Azure speech synthesis'); 22 | speechSynthesizer.close(); 23 | }, 24 | error => { 25 | console.log(error); 26 | azureSynthesisErrorNotify(); 27 | speechSynthesizer.close(); 28 | } 29 | ); 30 | return player; 31 | }; 32 | export default speechSynthesizeWithAzure; 33 | -------------------------------------------------------------------------------- /src/apis/azureToken.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | export async function getAzureToken(subscriptionKey: string, region: string): Promise { 4 | const url = `https://${region}.api.cognitive.microsoft.com/sts/v1.0/issueToken`; 5 | 6 | try { 7 | const response = await axios.post(url, null, { 8 | headers: { 9 | 'Ocp-Apim-Subscription-Key': subscriptionKey, 10 | 'Content-Type': 'application/x-www-form-urlencoded', 11 | }, 12 | }); 13 | 14 | return response.data; 15 | } catch (error) { 16 | throw new Error(`Error getting token: ${error}`); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/apis/openai.ts: -------------------------------------------------------------------------------- 1 | export default async function sendRequest( 2 | messages: string[], 3 | openaiApiKey: string, 4 | openaiHost: string, 5 | openaiModel: string, 6 | callback: (data: any) => void 7 | ) { 8 | const requestOptions = { 9 | method: 'POST', 10 | headers: { 11 | 'Content-Type': 'application/json', 12 | Authorization: 'Bearer ' + openaiApiKey, 13 | }, 14 | body: JSON.stringify({ 15 | model: openaiModel || 'gpt-3.5-turbo', 16 | messages: messages, 17 | }), 18 | }; 19 | 20 | const openaiHostAddress = openaiHost || 'api.openai.com'; 21 | 22 | fetch('https://' + openaiHostAddress + '/v1/chat/completions', requestOptions) 23 | .then(response => response.json()) 24 | .then(data => { 25 | callback(data); 26 | }) 27 | .catch(err => { 28 | return err; 29 | }); 30 | } 31 | -------------------------------------------------------------------------------- /src/assets/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/components/AboutDialog.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import BaseDialog from './base/Dialog'; 4 | import TippyButton from './base/TippyButton'; 5 | 6 | import { IconX, IconInfoCircle, IconWorldWww, IconBrandGithub } from '@tabler/icons-react'; 7 | import SettingGroup from './Settings/base/SettingGroup'; 8 | import { getVersion } from '../utils/version'; 9 | 10 | interface AboutDialogProps { 11 | open: boolean; 12 | onClose: () => void; 13 | notify: any; 14 | } 15 | 16 | function AboutDialog({ open, onClose, notify }: AboutDialogProps) { 17 | const { i18n } = useTranslation(); 18 | 19 | return ( 20 | 21 |
22 | } 26 | style="hover:bg-gray-200 active:bg-gray-300" 27 | /> 28 |
29 |
30 |
31 |
32 | 33 |
34 | {i18n.t('common.about')} 35 |
36 |
37 | 38 |
39 | {i18n.t('setting.about.intro')} 40 |
41 | 42 |
43 |
44 |
49 |
50 |
51 |
52 | 53 |
54 | {i18n.t('setting.about.link') as string} 55 |
56 | 57 | 58 |
59 | 60 | 61 | Official Website 62 | 63 |
64 |
65 | 66 | 70 | GitHub 71 | 72 |
73 |
74 |
75 | 76 |
77 | {i18n.t('setting.about.version') as string} 78 |
79 | 80 | 81 |
82 | Version: {getVersion()} 83 |
84 |
85 |
86 |
87 |
88 | ); 89 | } 90 | 91 | export default AboutDialog; 92 | -------------------------------------------------------------------------------- /src/components/AppearanceSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState, useRef } from 'react'; 2 | import { MoonIcon, SunIcon, ComputerDesktopIcon, CheckIcon } from '@heroicons/react/20/solid'; 3 | import { Menu } from '@headlessui/react'; 4 | import DropdownMenu from './base/DropdownMenu'; 5 | import Tippy from '@tippyjs/react'; 6 | import 'tippy.js/dist/tippy.css'; 7 | import { useTranslation } from 'react-i18next'; 8 | import { useGlobalStore } from '../store/module'; 9 | 10 | interface AppearanceOption { 11 | key: 'light' | 'dark' | 'system'; 12 | label: any; 13 | icon: JSX.Element; 14 | } 15 | 16 | function AppearanceSelector() { 17 | const { i18n } = useTranslation(); 18 | 19 | const { appearance, setAppearance } = useGlobalStore(); 20 | 21 | const isMount = useRef(true); 22 | 23 | useEffect(() => { 24 | if (isMount.current) { 25 | const storedAppearance = localStorage.getItem('appearance'); 26 | if (storedAppearance) { 27 | setAppearance(storedAppearance); 28 | } 29 | isMount.current = false; 30 | } else { 31 | switch (appearance) { 32 | case 'light': 33 | document.documentElement.classList.remove('dark'); 34 | break; 35 | case 'dark': 36 | document.documentElement.classList.add('dark'); 37 | break; 38 | case 'system': 39 | const darkModeMediaQuery = window.matchMedia('(prefers-color-scheme: dark)'); 40 | if (darkModeMediaQuery.matches) { 41 | document.documentElement.classList.add('dark'); 42 | } else { 43 | document.documentElement.classList.remove('dark'); 44 | } 45 | break; 46 | } 47 | localStorage.setItem('appearance', appearance); 48 | } 49 | }, [appearance, setAppearance]); 50 | 51 | const appearanceOptions: AppearanceOption[] = [ 52 | { key: 'light', label: 'Light', icon: }, 53 | { key: 'dark', label: 'Dark', icon: }, 54 | { 55 | key: 'system', 56 | label: 'System', 57 | icon: , 58 | }, 59 | ]; 60 | 61 | const button = ( 62 | 70 | 71 | {appearanceOptions.find(option => option.key === appearance)?.icon} 72 | 73 | 74 | ); 75 | 76 | return ( 77 | 78 | {appearanceOptions.map(option => ( 79 | 80 | {({ active }) => ( 81 | 96 | )} 97 | 98 | ))} 99 | 100 | ); 101 | } 102 | 103 | export default AppearanceSelector; 104 | -------------------------------------------------------------------------------- /src/components/AzureSpeechToText.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState } from 'react'; 2 | import * as sdk from 'microsoft-cognitiveservices-speech-sdk'; 3 | import { existEnvironmentVariable, getEnvironmentVariable } from '../helpers/utils'; 4 | 5 | interface AzureSpeechToTextProps { 6 | subscriptionKey: string; 7 | region: string; 8 | isListening: boolean; 9 | language?: string; 10 | setTranscript: (update: ((prevTranscript: string) => string) | string) => void; 11 | setIsListening: (update: ((prevIsListening: boolean) => boolean) | boolean) => void; 12 | setWaiting: (update: ((prevWaiting: boolean) => boolean) | boolean) => void; 13 | notify: any; 14 | accessCode: string; 15 | } 16 | 17 | const AzureSpeechToText: React.FC = ({ 18 | subscriptionKey, 19 | region, 20 | isListening, 21 | language = 'en-US', 22 | setIsListening, 23 | setTranscript, 24 | setWaiting, 25 | notify, 26 | accessCode, 27 | }) => { 28 | const [recognizer, setRecognizer] = useState(null); 29 | 30 | React.useEffect(() => { 31 | if (isListening) { 32 | startSpeechRecognition(); 33 | } else { 34 | if (recognizer) { 35 | recognizer.stopContinuousRecognitionAsync(); 36 | } else { 37 | // console.log('Recognizer is null'); 38 | } 39 | } 40 | }, [isListening]); 41 | 42 | const startSpeechRecognition = () => { 43 | if (accessCode !== getEnvironmentVariable('ACCESS_CODE')) { 44 | notify.invalidAccessCodeNotify(); 45 | setIsListening(false); 46 | setWaiting(false); 47 | return; 48 | } 49 | 50 | setWaiting(true); 51 | 52 | if (subscriptionKey === '' || region === '') { 53 | notify.emptyAzureKeyNotify(); 54 | setIsListening(false); 55 | setWaiting(false); 56 | return; 57 | } 58 | 59 | const speechConfig = sdk.SpeechConfig.fromSubscription(subscriptionKey, region); 60 | speechConfig.speechRecognitionLanguage = language; 61 | 62 | const audioConfig = sdk.AudioConfig.fromDefaultMicrophoneInput(); 63 | const newRecognizer = new sdk.SpeechRecognizer(speechConfig, audioConfig); 64 | 65 | newRecognizer.recognizing = (s, e) => { 66 | console.log(`Recognizing: ${e.result.text}`); 67 | }; 68 | 69 | newRecognizer.recognized = (s, e) => { 70 | console.log(`Recognized: ${e.result.text}`); 71 | if (e.result.text !== undefined) { 72 | setTranscript(e.result.text); 73 | } 74 | }; 75 | 76 | newRecognizer.canceled = (s, e) => { 77 | // @ts-ignore 78 | if (e.errorCode === sdk.CancellationErrorCode.ErrorAPIKey) { 79 | console.error('Invalid or incorrect subscription key'); 80 | } else { 81 | console.log(`Canceled: ${e.errorDetails}`); 82 | notify.azureRecognitionErrorNotify(); 83 | } 84 | setIsListening(false); 85 | setWaiting(false); 86 | }; 87 | 88 | newRecognizer.sessionStopped = (s, e) => { 89 | console.log('Session stopped'); 90 | newRecognizer.stopContinuousRecognitionAsync(); 91 | setIsListening(false); 92 | setWaiting(false); 93 | }; 94 | 95 | newRecognizer.startContinuousRecognitionAsync( 96 | () => { 97 | setWaiting(false); 98 | console.log('Listening...'); 99 | }, 100 | error => { 101 | console.log(`Error: ${error}`); 102 | notify.azureRecognitionErrorNotify(); 103 | newRecognizer.stopContinuousRecognitionAsync(); 104 | setIsListening(false); 105 | } 106 | ); 107 | 108 | setRecognizer(newRecognizer); 109 | }; 110 | 111 | return null; 112 | }; 113 | 114 | export default AzureSpeechToText; 115 | -------------------------------------------------------------------------------- /src/components/BrowserSpeechToText.tsx: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect } from 'react'; 2 | import {} from './Notification'; 3 | 4 | interface BrowserSpeechToTextProps { 5 | isListening: boolean; 6 | language: string; 7 | setIsListening: (update: ((prevIsListening: boolean) => boolean) | boolean) => void; 8 | setTranscript: (update: ((prevTranscript: string) => string) | string) => void; 9 | notify: any; 10 | } 11 | 12 | const SpeechRecognition = 13 | ('SpeechRecognition' in window || 'webkitSpeechRecognition' in window) && 14 | ((window as any).SpeechRecognition || (window as any).webkitSpeechRecognition); 15 | 16 | const globalRecognition = SpeechRecognition ? new SpeechRecognition() : null; 17 | 18 | const BrowserSpeechToText: React.FC = ({ 19 | isListening, 20 | language, 21 | setIsListening, 22 | setTranscript, 23 | notify, 24 | }) => { 25 | const [recognition, setRecognition] = useState(globalRecognition); 26 | 27 | useEffect(() => { 28 | if (recognition) { 29 | recognition.interimResults = true; 30 | recognition.continuous = true; 31 | recognition.lang = language; 32 | 33 | recognition.onresult = (event: SpeechRecognitionEvent) => { 34 | let currentTranscript = ''; 35 | 36 | for (let i = event.resultIndex; i < event.results.length; i++) { 37 | const result = event.results[i]; 38 | const text = result[0].transcript; 39 | 40 | if (result.isFinal) { 41 | setTranscript(text); 42 | } else { 43 | currentTranscript += text; 44 | } 45 | } 46 | }; 47 | 48 | // @ts-ignore 49 | recognition.onerror = (event: SpeechRecognitionError) => { 50 | console.log('Error:', event.error); 51 | notify.errorBuiltinSpeechRecognitionNotify(); 52 | setIsListening(false); 53 | }; 54 | } else { 55 | console.log('SpeechRecognition API is not supported in this browser'); 56 | notify.errorBuiltinSpeechRecognitionNotify(); 57 | } 58 | 59 | return () => { 60 | if (recognition) { 61 | recognition.stop(); 62 | } 63 | }; 64 | }, [isListening, language, recognition]); 65 | 66 | useEffect(() => { 67 | if (isListening) { 68 | if (recognition) { 69 | recognition.start(); 70 | } 71 | } else { 72 | if (recognition) { 73 | recognition.stop(); 74 | } 75 | } 76 | }, [isListening, recognition]); 77 | 78 | return null; 79 | }; 80 | 81 | export default BrowserSpeechToText; 82 | -------------------------------------------------------------------------------- /src/components/ButtonGroup.tsx: -------------------------------------------------------------------------------- 1 | import TippyButton from './base/TippyButton'; 2 | import React, { useRef, useState } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | 5 | import { 6 | IconVolume, 7 | IconVolume3, 8 | IconMicrophone, 9 | IconMicrophoneOff, 10 | IconMessages, 11 | IconRotateClockwise, 12 | IconPlayerPause, 13 | IconPlayerPlay, 14 | IconBackspace, 15 | IconCheck, 16 | } from '@tabler/icons-react'; 17 | import { useGlobalStore } from '../store/module'; 18 | import { isMobile } from 'react-device-detect'; 19 | 20 | interface ButtonGroupProps { 21 | disableMicrophone: boolean; 22 | onClickDisableMicrophone: () => void; 23 | disableSpeaker: boolean; 24 | onClickDisableSpeaker: () => void; 25 | stopSpeaking: () => void; 26 | clearConversation: () => void; 27 | clearUserInput: () => void; 28 | notify: any; 29 | status: string; 30 | finished: boolean; 31 | } 32 | 33 | function ButtonGroup({ 34 | disableMicrophone, 35 | onClickDisableMicrophone, 36 | disableSpeaker, 37 | onClickDisableSpeaker, 38 | stopSpeaking, 39 | clearConversation, 40 | clearUserInput, 41 | notify, 42 | status, 43 | finished, 44 | }: ButtonGroupProps) { 45 | const { i18n } = useTranslation(); 46 | const [isConfirmingReset, setIsConfirmingReset] = useState(false); 47 | 48 | const resetTimeoutRef = useRef(null); 49 | 50 | function handleResetClick() { 51 | if (!isConfirmingReset) { 52 | setIsConfirmingReset(true); 53 | 54 | if (resetTimeoutRef.current) { 55 | clearTimeout(resetTimeoutRef.current); 56 | } 57 | 58 | resetTimeoutRef.current = setTimeout(() => { 59 | setIsConfirmingReset(false); 60 | }, 6000); 61 | } else { 62 | clearConversation(); 63 | setIsConfirmingReset(false); 64 | 65 | if (resetTimeoutRef.current) { 66 | clearTimeout(resetTimeoutRef.current); 67 | } 68 | } 69 | } 70 | 71 | function MicrophoneButton() { 72 | if (!disableMicrophone) { 73 | return ( 74 | } 78 | style="hover:bg-slate-200 active:bg-slate-300" 79 | /> 80 | ); 81 | } else { 82 | return ( 83 | } 87 | style="hover:bg-slate-200 active:bg-slate-300" 88 | /> 89 | ); 90 | } 91 | } 92 | 93 | function SpeakerButton() { 94 | if (!disableSpeaker) { 95 | return ( 96 | } 100 | style="hover:bg-slate-200 active:bg-slate-300" 101 | /> 102 | ); 103 | } else { 104 | return ( 105 | } 109 | style="hover:bg-slate-200 active:bg-slate-300" 110 | /> 111 | ); 112 | } 113 | } 114 | 115 | return ( 116 |
117 |
118 | 119 | 120 |
121 |
122 | {!disableSpeaker && ( 123 | <> 124 | {status === 'speaking' && !finished && ( 125 | } 129 | style="hover:bg-slate-200 active:bg-slate-300" 130 | /> 131 | )} 132 | {status !== 'speaking' && !finished && ( 133 | } 137 | style="hover:bg-slate-200 active:bg-slate-300" 138 | /> 139 | )} 140 | 141 | )} 142 | {isConfirmingReset ? ( 143 | } 147 | style="hover:bg-slate-200 active:bg-slate-300" 148 | /> 149 | ) : ( 150 | } 154 | style="hover:bg-slate-200 active:bg-slate-300" 155 | /> 156 | )} 157 | { 159 | clearUserInput(); 160 | notify.clearedNotify(); 161 | }} 162 | tooltip={i18n.t('common.clear-input') as string} 163 | icon={} 164 | style="hover:bg-slate-200 active:bg-slate-300" 165 | /> 166 |
167 |
168 | ); 169 | } 170 | 171 | export default ButtonGroup; 172 | -------------------------------------------------------------------------------- /src/components/ConversationPanel.tsx: -------------------------------------------------------------------------------- 1 | import Tips from './Tips'; 2 | import TippyButton from './base/TippyButton'; 3 | import { Element } from 'react-scroll'; 4 | import React, { useState } from 'react'; 5 | import { marked } from '../helpers/markdown'; 6 | import { Chat } from '../db/chat'; 7 | import { useTranslation } from 'react-i18next'; 8 | import { useGlobalStore, useSessionStore } from '../store/module'; 9 | import { IconCheck, IconCopy, IconTrash, IconVolume } from '@tabler/icons-react'; 10 | import { isMobile } from 'react-device-detect'; 11 | 12 | interface ConversationPanelProps { 13 | conversations: Chat[]; 14 | deleteContent: (index: any) => void; 15 | copyContentToClipboard: (content: string) => void; 16 | generateSpeech: (content: string) => void; 17 | } 18 | 19 | function ConversationPanel({ 20 | conversations, 21 | deleteContent, 22 | copyContentToClipboard, 23 | generateSpeech, 24 | }: ConversationPanelProps) { 25 | const { i18n } = useTranslation(); 26 | const { disableSpeaker } = useGlobalStore(); 27 | 28 | const [isHidden, setIsHidden] = useState(false); 29 | const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); 30 | 31 | const handleMouseUp = () => { 32 | setIsHidden(window.getSelection()?.toString().length !== 0); 33 | }; 34 | 35 | const handleMouseDown = () => { 36 | setIsHidden(true); 37 | }; 38 | 39 | const { currentSessionId } = useSessionStore(); 40 | 41 | function handleDeleteClick(id: number | undefined) { 42 | if (!isConfirmingDelete) { 43 | setIsConfirmingDelete(true); 44 | 45 | setTimeout(() => { 46 | // Automatically reset isConfirmingDelete state after 10 seconds 47 | setIsConfirmingDelete(false); 48 | }, 6000); 49 | } else { 50 | deleteContent(id); 51 | setIsConfirmingDelete(false); 52 | } 53 | } 54 | 55 | function ChatIcon({ role }: { role: 'user' | 'assistant' | 'system' }) { 56 | if (role === 'user') { 57 | return ( 58 |
59 | ); 60 | } else { 61 | return ( 62 |
63 | ); 64 | } 65 | } 66 | 67 | function isConversationEmpty() { 68 | return ( 69 | conversations.length === 0 || 70 | conversations.filter(conversation => conversation.sessionId === currentSessionId).length === 0 71 | ); 72 | } 73 | 74 | return ( 75 | 76 | {isConversationEmpty() && } 77 | {conversations 78 | .filter(conversation => conversation.sessionId === currentSessionId) 79 | .map((conversation, index) => ( 80 |
84 | 85 |
92 | {marked(conversation.content ?? '')} 93 |
94 |
99 | {!disableSpeaker && ( 100 | { 102 | generateSpeech(conversation.content); 103 | }} 104 | tooltip={i18n.t('common.replay') as string} 105 | icon={} 106 | style="bg-slate-100 active:bg-slate-300 rounded-sm" 107 | /> 108 | )} 109 | {isMobile ? ( 110 | { 112 | deleteContent(conversation.id); 113 | }} 114 | tooltip={i18n.t('common.delete') as string} 115 | icon={} 116 | style="bg-slate-100 active:bg-slate-300 rounded-sm" 117 | /> 118 | ) : ( 119 | { 121 | handleDeleteClick(conversation.id); 122 | }} 123 | tooltip={ 124 | isConfirmingDelete 125 | ? (i18n.t('common.confirm') as string) 126 | : (i18n.t('common.delete') as string) 127 | } 128 | icon={ 129 | isConfirmingDelete ? ( 130 | 131 | ) : ( 132 | 133 | ) 134 | } 135 | style="bg-slate-100 active:bg-slate-300 rounded-sm" 136 | /> 137 | )} 138 | { 140 | copyContentToClipboard(conversation.content); 141 | }} 142 | tooltip={i18n.t('common.copy') as string} 143 | icon={} 144 | style="bg-slate-100 active:bg-slate-300 rounded-sm" 145 | /> 146 |
147 |
148 | ))} 149 |
150 | ); 151 | } 152 | 153 | export default ConversationPanel; 154 | -------------------------------------------------------------------------------- /src/components/Conversations/ConversationIcons.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export const BlueCircle: React.FC = () => { 4 | return ( 5 | 6 | 7 | 8 | ); 9 | }; 10 | 11 | export const RedCircle: React.FC = () => { 12 | return ( 13 | 14 | 15 | 16 | ); 17 | }; 18 | 19 | export const GreenCircle: React.FC = () => { 20 | return ( 21 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export const OrangeCircle: React.FC = () => { 28 | return ( 29 | 30 | 31 | 32 | ); 33 | }; 34 | 35 | export const PurpleCircle: React.FC = () => { 36 | return ( 37 | 38 | 39 | 40 | ); 41 | }; 42 | 43 | export const VioletCircle: React.FC = () => { 44 | return ( 45 | 46 | 47 | 48 | ); 49 | }; 50 | 51 | export const CyanCircle: React.FC = () => { 52 | return ( 53 | 54 | 55 | 56 | ); 57 | }; 58 | -------------------------------------------------------------------------------- /src/components/EllipsisMenu.tsx: -------------------------------------------------------------------------------- 1 | import React, { Fragment } from 'react'; 2 | import { Popover, Transition } from '@headlessui/react'; 3 | import { EllipsisVerticalIcon } from '@heroicons/react/24/outline'; 4 | import { IconInfoCircle, IconMessage2, IconSettings } from '@tabler/icons-react'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | interface EllipsisMenuProps { 8 | setOpenSetting: (open: boolean) => void; 9 | setOpenAbout: (open: boolean) => void; 10 | } 11 | 12 | function EllipsisMenu({ setOpenSetting, setOpenAbout }: EllipsisMenuProps) { 13 | const { i18n } = useTranslation(); 14 | 15 | function handleFeedback() { 16 | window.open('https://github.com/hahahumble/speechgpt/issues'); 17 | } 18 | 19 | function handleAbout() { 20 | setOpenAbout(true); 21 | } 22 | 23 | const buttons = [ 24 | { name: i18n.t('common.setting'), icon: IconSettings, onClick: () => setOpenSetting(true) }, 25 | { name: i18n.t('common.feedback'), icon: IconMessage2, onClick: handleFeedback }, 26 | { name: i18n.t('common.about'), icon: IconInfoCircle, onClick: handleAbout }, 27 | ]; 28 | 29 | return ( 30 | 31 | 32 | 33 | 34 | 35 | 44 | 45 |
46 |
47 | {buttons.map(item => ( 48 |
53 |
54 |
59 |
60 |
{item.name}
61 |
62 |
63 | ))} 64 |
65 |
66 |
67 | © {new Date().getFullYear()} SpeechGPT 68 |
69 |
70 |
71 |
72 |
73 |
74 | ); 75 | } 76 | 77 | export default EllipsisMenu; 78 | -------------------------------------------------------------------------------- /src/components/Header.tsx: -------------------------------------------------------------------------------- 1 | import LanguageSelector from './LocaleSelector'; 2 | import AppearanceSelector from './AppearanceSelector'; 3 | import React from 'react'; 4 | import SpeechGPTIcon from './Icons/SpeechGPTIcon'; 5 | 6 | function Header() { 7 | return ( 8 |
9 | 10 |
11 |
12 | 13 | SpeechGPT 14 | 15 |
16 |
17 | 18 | {/**/} 19 |
20 |
21 |
22 | ); 23 | } 24 | 25 | export default Header; 26 | -------------------------------------------------------------------------------- /src/components/Icons/AboutIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface AboutIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const AboutIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default AboutIcon; 28 | -------------------------------------------------------------------------------- /src/components/Icons/BlubIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface BlubIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const BlubIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default BlubIcon; 28 | -------------------------------------------------------------------------------- /src/components/Icons/ChatIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface ChatIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const ChatIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default ChatIcon; 28 | -------------------------------------------------------------------------------- /src/components/Icons/HelpIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface HelpIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const HelpIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default HelpIcon; 31 | -------------------------------------------------------------------------------- /src/components/Icons/MicrophoneIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface MicrophoneIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const MicrophoneIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | 25 | 26 | 27 | ); 28 | }; 29 | 30 | export default MicrophoneIcon; 31 | -------------------------------------------------------------------------------- /src/components/Icons/OKCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface OKCircleIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const OKCircleIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 28 | 29 | ); 30 | }; 31 | 32 | export default OKCircleIcon; 33 | -------------------------------------------------------------------------------- /src/components/Icons/RightTriangleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface RightTriangleIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const RightTriangleIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default RightTriangleIcon; 28 | -------------------------------------------------------------------------------- /src/components/Icons/SpeakerIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface SpeakerIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const SpeakerIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default SpeakerIcon; 28 | -------------------------------------------------------------------------------- /src/components/Icons/SpeechGPTIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface SpeechGPTIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const SpeechGPTIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 19 | 23 | 27 | 31 | 35 | 39 | 40 | 48 | 49 | 50 | 51 | 52 | 60 | 61 | 62 | 63 | 64 | 72 | 73 | 74 | 75 | 76 | 84 | 85 | 86 | 87 | 88 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | ); 103 | }; 104 | 105 | export default SpeechGPTIcon; 106 | -------------------------------------------------------------------------------- /src/components/Icons/SpinnerIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface SpinnerIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const SpinnerIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 28 | 29 | 30 | ); 31 | }; 32 | 33 | export default SpinnerIcon; 34 | -------------------------------------------------------------------------------- /src/components/Icons/StartCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface StartCircleIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const StartCircleIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default StartCircleIcon; 29 | -------------------------------------------------------------------------------- /src/components/Icons/StopCircleIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface StopCircleIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const StopCircleIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default StopCircleIcon; 29 | -------------------------------------------------------------------------------- /src/components/Icons/WarningIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface WarningIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const WarningIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | {' '} 23 | 24 | 25 | ); 26 | }; 27 | 28 | export default WarningIcon; 29 | -------------------------------------------------------------------------------- /src/components/Icons/XIcon.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import classNames from 'classnames'; 3 | 4 | interface XIconProps { 5 | className?: string; 6 | strokeWidth?: string; 7 | } 8 | 9 | const XIcon: React.FC = ({ className, strokeWidth }) => { 10 | return ( 11 | 22 | 23 | 24 | ); 25 | }; 26 | 27 | export default XIcon; 28 | -------------------------------------------------------------------------------- /src/components/InputPanel.tsx: -------------------------------------------------------------------------------- 1 | import TextareaAutosize from 'react-textarea-autosize'; 2 | import React from 'react'; 3 | import StartCircleIcon from './Icons/StartCircleIcon'; 4 | import StopCircleIcon from './Icons/StopCircleIcon'; 5 | import SpinnerIcon from './Icons/SpinnerIcon'; 6 | 7 | import { useTranslation } from 'react-i18next'; 8 | 9 | import { IconSend } from '@tabler/icons-react'; 10 | 11 | interface InputPanelProps { 12 | status: string; 13 | disableMicrophone: boolean; 14 | startRecording: () => void; 15 | stopRecording: () => void; 16 | handleSend: () => void; 17 | inputRef: React.RefObject; 18 | userInput: string; 19 | setUserInput: (value: string) => void; 20 | handleInputKeyDown: (event: React.KeyboardEvent) => void; 21 | waiting: boolean; 22 | notify: any; 23 | } 24 | 25 | function InputPanel({ 26 | status, 27 | disableMicrophone, 28 | startRecording, 29 | stopRecording, 30 | handleSend, 31 | inputRef, 32 | userInput, 33 | setUserInput, 34 | handleInputKeyDown, 35 | waiting, 36 | notify, 37 | }: InputPanelProps) { 38 | const { i18n } = useTranslation(); 39 | 40 | function RecordButton() { 41 | if (status === 'idle' && !disableMicrophone) { 42 | return ( 43 | 51 | ); 52 | } else if (status === 'speaking' || status === 'waiting' || disableMicrophone) { 53 | return ( 54 | 61 | ); 62 | } else if (status === 'recording' && !disableMicrophone && waiting) { 63 | return ( 64 | 71 | ); 72 | } else if (status === 'recording' && !disableMicrophone && !waiting) { 73 | return ( 74 | 82 | ); 83 | } else { 84 | return ( 85 | 92 | ); 93 | } 94 | } 95 | 96 | function SendButton() { 97 | if ( 98 | (status === 'idle' || status === 'recording' || status === 'connecting') && 99 | userInput.length > 0 100 | ) { 101 | return ( 102 | 110 | ); 111 | } else if ( 112 | (status === 'idle' || status === 'recording' || status === 'connecting') && 113 | userInput.length == 0 114 | ) { 115 | return ( 116 | 124 | ); 125 | } else { 126 | return ( 127 | 135 | ); 136 | } 137 | } 138 | 139 | return ( 140 |
141 | setUserInput(event.target.value)} 148 | onKeyDown={handleInputKeyDown} 149 | maxRows={5} 150 | /> 151 |
152 | {/*
{i18n.t('common.status')}: {status}
*/} 153 | {!disableMicrophone && } 154 | 155 |
156 |
157 | ); 158 | } 159 | 160 | export default InputPanel; 161 | -------------------------------------------------------------------------------- /src/components/LocaleSelector.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import 'tippy.js/dist/tippy.css'; 4 | import { useGlobalStore } from '../store/module'; 5 | 6 | interface LanguageOption { 7 | code: string; 8 | name: string; 9 | } 10 | 11 | function LanguageSelector() { 12 | const { i18n } = useTranslation(); 13 | const { locale, setLocale } = useGlobalStore(); 14 | const selectRef = useRef(null); 15 | const hiddenSpanRef = useRef(null); 16 | 17 | useEffect(() => { 18 | if (locale === '') { 19 | const browserLanguage = navigator.language.split(/[-_]/)[0]; 20 | setLocale(browserLanguage); 21 | } 22 | setTimeout(() => { 23 | i18n.changeLanguage(locale); 24 | }, 100); 25 | }, [locale]); 26 | 27 | const languages: LanguageOption[] = [ 28 | { code: 'en', name: 'English' }, 29 | { code: 'es', name: 'Español' }, 30 | { code: 'zh', name: '中文' }, 31 | ]; 32 | 33 | useEffect(() => { 34 | if (selectRef.current && hiddenSpanRef.current) { 35 | const width = hiddenSpanRef.current.offsetWidth + 25; 36 | selectRef.current.style.width = width + 'px'; 37 | } 38 | }, [locale]); 39 | 40 | const handleLanguageChange = (e: React.ChangeEvent) => { 41 | setLocale(e.target.value); 42 | }; 43 | 44 | const selectedLanguage = languages.find(lang => lang.code === locale); 45 | 46 | return ( 47 |
48 | 49 | {selectedLanguage?.name} 50 | 51 | 63 |
64 | ); 65 | } 66 | 67 | export default LanguageSelector; 68 | -------------------------------------------------------------------------------- /src/components/MobileSettingsSelector.tsx: -------------------------------------------------------------------------------- 1 | import { Fragment } from 'react'; 2 | import { Listbox, Transition } from '@headlessui/react'; 3 | import { CheckIcon, ChevronUpDownIcon } from '@heroicons/react/20/solid'; 4 | 5 | interface MobileSettingsSelectorProps { 6 | catalogItems: string[]; 7 | selected: string | null; 8 | setSelected: (item: string) => void; 9 | } 10 | 11 | export default function MobileSettingsSelector({ 12 | catalogItems, 13 | selected, 14 | setSelected, 15 | }: MobileSettingsSelectorProps) { 16 | return ( 17 |
18 | 19 |
20 | 21 | {selected} 22 | 23 | 25 | 26 | 32 | 33 | {catalogItems.map((item, itemIdx) => ( 34 | 37 | `hover:cursor-pointer text-left relative cursor-default select-none pl-3 py-1.5 ${ 38 | active ? 'bg-gray-100 text-gray-900' : 'text-gray-900' 39 | }` 40 | } 41 | value={item} 42 | > 43 | {({ selected }) => ( 44 | <> 45 | 48 | {item} 49 | 50 | {selected ? ( 51 | 52 | 54 | ) : null} 55 | 56 | )} 57 | 58 | ))} 59 | 60 | 61 |
62 |
63 |
64 | ); 65 | } 66 | -------------------------------------------------------------------------------- /src/components/Notification.tsx: -------------------------------------------------------------------------------- 1 | import toast from 'react-hot-toast'; 2 | import i18next from 'i18next'; 3 | 4 | const notificationStyle = { 5 | borderRadius: '24px', 6 | padding: '8px 16px', // x, y 7 | }; 8 | 9 | export const clearedNotify = () => { 10 | toast.success(i18next.t('notification.cleared') as string, { 11 | style: notificationStyle, 12 | }); 13 | }; 14 | 15 | export const copiedNotify = () => { 16 | toast.success(i18next.t('notification.copied') as string, { 17 | style: notificationStyle, 18 | }); 19 | }; 20 | 21 | export const resetNotify = () => { 22 | toast.success(i18next.t('notification.reset') as string, { 23 | style: notificationStyle, 24 | }); 25 | }; 26 | 27 | export const invalidAccessCodeNotify = () => { 28 | toast.error(i18next.t('notification.invalid-access-code') as string, { 29 | style: notificationStyle, 30 | }); 31 | }; 32 | 33 | // OpenAI 34 | export const invalidOpenAiKeyNotify = () => { 35 | toast.error(i18next.t('notification.invalid-openai-key') as string, { 36 | style: notificationStyle, 37 | }); 38 | }; 39 | 40 | export const invalidOpenAiRequestNotify = () => { 41 | toast.error(i18next.t('notification.invalid-openai-request') as string, { 42 | style: notificationStyle, 43 | }); 44 | }; 45 | 46 | export const invalidOpenAiModelNotify = () => { 47 | toast.error(i18next.t('notification.invalid-openai-model') as string, { 48 | style: notificationStyle, 49 | }); 50 | }; 51 | 52 | export const openAiErrorNotify = () => { 53 | toast.error(i18next.t('notification.openai-error') as string, { 54 | style: notificationStyle, 55 | }); 56 | }; 57 | 58 | export const emptyOpenAiKeyNotify = () => { 59 | toast.error(i18next.t('notification.empty-openai-key') as string, { 60 | style: notificationStyle, 61 | }); 62 | }; 63 | 64 | export const networkErrorNotify = () => { 65 | toast.error(i18next.t('notification.network-error') as string, { 66 | style: notificationStyle, 67 | }); 68 | }; 69 | 70 | export const deletedNotify = () => { 71 | toast.success(i18next.t('notification.deleted') as string, { 72 | style: notificationStyle, 73 | }); 74 | }; 75 | 76 | export const cannotBeEmptyNotify = () => { 77 | toast.error(i18next.t('notification.cannot-be-empty') as string, { 78 | style: notificationStyle, 79 | }); 80 | }; 81 | 82 | // Builtin services 83 | export const errorBuiltinSpeechRecognitionNotify = () => { 84 | toast.error(i18next.t('notification.builtin-recognition-error') as string, { 85 | style: notificationStyle, 86 | }); 87 | }; 88 | 89 | export const errorBuiltinSpeechSynthesisNotify = () => { 90 | toast.error(i18next.t('notification.builtin-synthesis-error') as string, { 91 | style: notificationStyle, 92 | }); 93 | }; 94 | 95 | // Azure 96 | export const emptyAzureKeyNotify = () => { 97 | toast.error(i18next.t('notification.empty-azure-key') as string, { 98 | style: notificationStyle, 99 | }); 100 | }; 101 | 102 | export const azureRecognitionErrorNotify = () => { 103 | toast.error(i18next.t('notification.azure-recognition-error') as string, { 104 | style: notificationStyle, 105 | }); 106 | }; 107 | 108 | export const azureSynthesisErrorNotify = () => { 109 | toast.error(i18next.t('notification.azure-synthesis-error') as string, { 110 | style: notificationStyle, 111 | }); 112 | }; 113 | 114 | export const invalidAzureKeyNotify = () => { 115 | toast.error(i18next.t('notification.invalid-azure-key') as string, { 116 | style: notificationStyle, 117 | }); 118 | }; 119 | 120 | // AWS 121 | export const awsErrorNotify = () => { 122 | toast.error(i18next.t('notification.polly-synthesis-error') as string, { 123 | style: notificationStyle, 124 | }); 125 | }; 126 | 127 | export const allConversationClearNotify = () => { 128 | toast.success(i18next.t('notification.all-conversations-clear') as string, { 129 | style: notificationStyle, 130 | }); 131 | }; 132 | -------------------------------------------------------------------------------- /src/components/Record.tsx: -------------------------------------------------------------------------------- 1 | import useSpeechToText, { ResultType } from 'react-hook-speech-to-text'; 2 | import { useEffect } from 'react'; 3 | 4 | type baseStatus = 'idle' | 'waiting' | 'speaking' | 'recording'; 5 | 6 | interface RecordProps { 7 | setResults: (results: string) => void; 8 | setInterimResult: (interimResult: string) => void; 9 | recording: boolean; 10 | setRecording: (recording: boolean) => void; 11 | status: string; 12 | setStatus: (status: baseStatus) => void; 13 | language: string; 14 | } 15 | 16 | function Record({ 17 | setResults, 18 | setInterimResult, 19 | recording, 20 | setRecording, 21 | status, 22 | setStatus, 23 | language, 24 | }: RecordProps) { 25 | // https://github.com/Riley-Brown/react-speech-to-text 26 | const { error, interimResult, isRecording, results, startSpeechToText, stopSpeechToText } = 27 | useSpeechToText({ 28 | continuous: true, 29 | useLegacyResults: false, 30 | speechRecognitionProperties: { 31 | lang: language, 32 | }, 33 | }) as { 34 | error: string; 35 | interimResult: string | undefined; 36 | isRecording: boolean; 37 | results: ResultType[]; 38 | setResults: import('react').Dispatch>; 39 | startSpeechToText: () => Promise; 40 | stopSpeechToText: () => void; 41 | }; 42 | 43 | useEffect(() => { 44 | console.log('error', error); 45 | }, [error]); 46 | 47 | useEffect(() => { 48 | if (results.length > 0) { 49 | // console.log(results); 50 | setResults(results[results.length - 1].transcript); 51 | // setResults(results.map((result) => result.transcript).join(' ')); 52 | } 53 | }, [results]); 54 | 55 | // useEffect(() => { 56 | // if (interimResult) { 57 | // console.log('interimResult', interimResult); 58 | // // setInterimResult(interimResult); 59 | // } 60 | // }, [interimResult]); 61 | 62 | useEffect(() => { 63 | if (interimResult) { 64 | console.log('interimResult', interimResult); 65 | } 66 | }, [interimResult]); 67 | 68 | // start recognition when the user clicks the record button 69 | useEffect(() => { 70 | if (recording) { 71 | console.log('start: startSpeechToText()'); 72 | startSpeechToText(); 73 | } else { 74 | console.log('stop'); 75 | stopSpeechToText(); 76 | } 77 | }, [recording]); 78 | 79 | useEffect(() => { 80 | if (!isRecording) { 81 | console.log('isrecording false'); 82 | setRecording(false); 83 | } 84 | }, [isRecording]); 85 | 86 | useEffect(() => { 87 | if (status == 'speaking' || status == 'waiting') { 88 | console.log('stop'); 89 | stopSpeechToText(); 90 | } 91 | }, [status]); 92 | 93 | return null; 94 | } 95 | 96 | export default Record; 97 | -------------------------------------------------------------------------------- /src/components/Settings/ChatSection.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import SettingTextArea from './base/SettingTextArea'; 3 | import SettingDivider from './base/SettingDivider'; 4 | import SettingTitle from './base/SettingTitle'; 5 | import SettingInput from './base/SettingInput'; 6 | import SettingSwitch from './base/SettingSwitch'; 7 | import SettingSlider from './base/SettingSlider'; 8 | import SettingGroup from './base/SettingGroup'; 9 | import SettingCheckText from './base/SettingCheckText'; 10 | 11 | import { useGlobalStore } from '../../store/module'; 12 | import { useTranslation } from 'react-i18next'; 13 | import { existEnvironmentVariable } from '../../helpers/utils'; 14 | import { azureRegions } from '../../constants/data'; 15 | import SettingSelect from './base/SettingSelect'; 16 | 17 | interface ChatSectionProps {} 18 | 19 | const openaiModels = [ 20 | 'gpt-3.5-turbo', 21 | 'gpt-3.5-turbo-0301', // deprecated on June 1st, 2023 22 | 'gpt-4', 23 | 'gpt-4-0314', // deprecated on June 14th, 2023 24 | 'gpt-4-32k', 25 | 'gpt-4-32k-0314', // deprecated on June 14th, 2023 26 | ]; 27 | 28 | const ChatSection: React.FC = ({}) => { 29 | const { key, setKey, chat, setChat } = useGlobalStore(); 30 | 31 | const { i18n } = useTranslation(); 32 | 33 | return ( 34 |
35 | 36 | 37 | {existEnvironmentVariable('OPENAI_API_KEY') ? ( 38 | 41 | ) : ( 42 | <> 43 | setKey({ ...key, openaiApiKey: e })} 49 | placeholder={i18n.t('setting.chat.api-key') as string} 50 | className={''} 51 | /> 52 | setKey({ ...key, openaiHost: e })} 59 | placeholder={i18n.t('setting.chat.default-host-address') as string} 60 | className={''} 61 | /> 62 | setKey({ ...key, openaiModel: e })} 69 | /> 70 | 71 | )} 72 | {existEnvironmentVariable('ACCESS_CODE') ? ( 73 | setKey({ ...key, accessCode: e })} 80 | placeholder={i18n.t('setting.chat.code') as string} 81 | className={''} 82 | /> 83 | ) : ( 84 | <> 85 | )} 86 | 87 | 88 | 89 | 90 | 91 | setChat({ ...chat, systemRole: e })} 97 | placeholder={i18n.t('setting.chat.type-something') as string} 98 | maxRows={4} 99 | /> 100 | setChat({ ...chat, defaultPrompt: e })} 105 | placeholder={i18n.t('setting.chat.type-something') as string} 106 | maxRows={4} 107 | /> 108 | 109 | 110 | 111 | 112 | 113 | setChat({ ...chat, useAssistant: e })} 118 | /> 119 | setChat({ ...chat, temperature: e })} 125 | min={'0'} 126 | max={'2'} 127 | step={'0.1'} 128 | /> 129 | setChat({ ...chat, maxMessages: e })} 135 | min={'1'} 136 | max={'100'} 137 | step={'1'} 138 | /> 139 | 140 |
141 | ); 142 | }; 143 | 144 | export default ChatSection; 145 | -------------------------------------------------------------------------------- /src/components/Settings/PollyVoice.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import SettingSelect from './base/SettingSelect'; 3 | 4 | import { 5 | pollyStandardSupportedLanguages, 6 | pollyStandardVoices, 7 | pollyNeuralVoices, 8 | pollyNeuralSupportedLanguages, 9 | } from '../../constants/data'; 10 | import { useGlobalStore } from '../../store/module'; 11 | import { useTranslation } from 'react-i18next'; 12 | 13 | const PollyVoice = () => { 14 | const { i18n } = useTranslation(); 15 | 16 | const { speech, setSpeech } = useGlobalStore(); 17 | 18 | const languageCode = speech.pollyLanguage; 19 | 20 | useEffect(() => { 21 | if ( 22 | pollyStandardSupportedLanguages.includes(languageCode) && 23 | speech.pollyEngine === 'Standard' 24 | ) { 25 | if (!pollyStandardVoices[languageCode].includes(speech.pollyVoice)) { 26 | setSpeech({ 27 | ...speech, 28 | pollyVoice: pollyStandardVoices[languageCode][0], 29 | }); 30 | } 31 | } else if ( 32 | pollyNeuralSupportedLanguages.includes(languageCode) && 33 | speech.pollyEngine === 'Neural' 34 | ) { 35 | if (!pollyNeuralVoices[languageCode].includes(speech.pollyVoice)) { 36 | setSpeech({ ...speech, pollyVoice: pollyNeuralVoices[languageCode][0] }); 37 | } 38 | } 39 | }, [languageCode]); 40 | 41 | switch (speech.pollyEngine) { 42 | case 'Standard': 43 | if (pollyStandardSupportedLanguages.includes(languageCode)) { 44 | return ( 45 | setSpeech({ ...speech, pollyVoice: e })} 52 | /> 53 | ); 54 | } else { 55 | return ( 56 |
57 | {i18n.t('setting.synthesis.polly-standard-not-supported') as string} 58 |
59 | ); 60 | } 61 | case 'Neural': 62 | if (pollyNeuralSupportedLanguages.includes(languageCode)) { 63 | return ( 64 | setSpeech({ ...speech, pollyVoice: e })} 71 | /> 72 | ); 73 | } else { 74 | return ( 75 |
76 | {i18n.t('setting.synthesis.polly-neural-not-supported') as string} 77 |
78 | ); 79 | } 80 | default: 81 | return null; 82 | } 83 | }; 84 | 85 | export default PollyVoice; 86 | -------------------------------------------------------------------------------- /src/components/Settings/RecognitionSection.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useState } from 'react'; 2 | import SettingTitle from './base/SettingTitle'; 3 | import SettingDivider from './base/SettingDivider'; 4 | import SettingSelect from './base/SettingSelect'; 5 | import SettingGroup from './base/SettingGroup'; 6 | import SettingInput from './base/SettingInput'; 7 | import SettingSlider from './base/SettingSlider'; 8 | import SettingSwitch from './base/SettingSwitch'; 9 | import SettingCheckText from './base/SettingCheckText'; 10 | import SettingWarningText from './base/SettingWarningText'; 11 | 12 | import { 13 | awsRegions, 14 | azureRegions, 15 | azureSpeechRecognitionLanguagesLocale, 16 | speechRecognitionSystemLanguagesLocale, 17 | } from '../../constants/data'; 18 | 19 | import { browserName, isMobile } from 'react-device-detect'; 20 | import { useGlobalStore } from '../../store/module'; 21 | import { useTranslation } from 'react-i18next'; 22 | import { existEnvironmentVariable } from '../../helpers/utils'; 23 | 24 | interface RecognitionSectionProps {} 25 | 26 | const RecognitionSection: React.FC = ({}) => { 27 | const { i18n } = useTranslation(); 28 | const { key, setKey, voice, setVoice } = useGlobalStore(); 29 | const voiceServices = 30 | browserName !== 'Chrome' || isMobile 31 | ? ['Azure Speech to Text'] 32 | : ['System', 'Azure Speech to Text']; 33 | 34 | useEffect(() => { 35 | if (!key.azureRegion) { 36 | setKey({ ...key, azureRegion: azureRegions[0] }); 37 | } 38 | }, [key.azureRegion]); 39 | 40 | useEffect(() => { 41 | if (!key.awsRegion) { 42 | setKey({ ...key, awsRegion: awsRegions[0] }); 43 | } 44 | }, [key.awsRegion]); 45 | 46 | function getSystemLanguageCode(language: string) { 47 | return Object.keys(speechRecognitionSystemLanguagesLocale).find( 48 | key => speechRecognitionSystemLanguagesLocale[key] === language 49 | ); 50 | } 51 | 52 | function getAzureLanguageCode(language: string) { 53 | return Object.keys(azureSpeechRecognitionLanguagesLocale).find( 54 | key => azureSpeechRecognitionLanguagesLocale[key] === language 55 | ); 56 | } 57 | 58 | return ( 59 |
60 | 61 | 62 | {browserName !== 'Chrome' && !isMobile && ( 63 | 66 | )} 67 | {isMobile && ( 68 | 69 | )} 70 | setVoice({ ...voice, service: e })} 78 | /> 79 | {voice.service === 'Azure Speech to Text' && ( 80 | <> 81 | {existEnvironmentVariable('AZURE_REGION') && existEnvironmentVariable('AZURE_KEY') ? ( 82 | 87 | ) : ( 88 | <> 89 | setKey({ ...key, azureRegion: e })} 98 | /> 99 | setKey({ ...key, azureKey: e })} 106 | /> 107 | 108 | )} 109 | 110 | )} 111 | 112 | 113 | 114 | 115 | 116 | {voice.service === 'System' && ( 117 | setVoice({ ...voice, systemLanguage: getSystemLanguageCode(e) })} 124 | /> 125 | )} 126 | {voice.service === 'Azure Speech to Text' && ( 127 | setVoice({ ...voice, azureLanguage: getAzureLanguageCode(e) })} 134 | /> 135 | )} 136 | setVoice({ ...voice, autoStart: e })} 141 | /> 142 | {voice.autoStart && ( 143 | setVoice({ ...voice, startTime: e })} 149 | min={'0'} 150 | max={'5'} 151 | step={'0.2'} 152 | /> 153 | )} 154 | 155 |
156 | ); 157 | }; 158 | 159 | export default RecognitionSection; 160 | -------------------------------------------------------------------------------- /src/components/Settings/SettingContent.tsx: -------------------------------------------------------------------------------- 1 | import ChatSection from './ChatSection'; 2 | import SynthesisSection from './SynthesisSection'; 3 | import RecognitionSection from './RecognitionSection'; 4 | import React from 'react'; 5 | import { useTranslation } from 'react-i18next'; 6 | 7 | interface SettingContentProps { 8 | selected: string | null; 9 | } 10 | 11 | function SettingContent({ selected }: SettingContentProps) { 12 | const { i18n } = useTranslation(); 13 | 14 | switch (selected) { 15 | case i18n.t('setting.chat.title') as string: 16 | return ; 17 | case i18n.t('setting.synthesis.title') as string: 18 | return ; 19 | case i18n.t('setting.recognition.title') as string: 20 | return ; 21 | // case i18n.t('setting.about.title') as string: 22 | // return ; 23 | default: 24 | return ; 25 | } 26 | } 27 | 28 | export default SettingContent; 29 | -------------------------------------------------------------------------------- /src/components/Settings/SettingDialog.tsx: -------------------------------------------------------------------------------- 1 | import BaseDialog from '../base/Dialog'; 2 | import React, { useEffect, useState } from 'react'; 3 | import XIcon from '../Icons/XIcon'; 4 | import SettingSelector from './SettingSelector'; 5 | import TippyButton from '../base/TippyButton'; 6 | import SettingContent from './SettingContent'; 7 | import MobileSettingsSelector from '../MobileSettingsSelector'; 8 | import ChatIcon from '../Icons/ChatIcon'; 9 | import SpeakerIcon from '../Icons/SpeakerIcon'; 10 | import MicrophoneIcon from '../Icons/MicrophoneIcon'; 11 | import AboutIcon from '../Icons/AboutIcon'; 12 | import { useTranslation } from 'react-i18next'; 13 | 14 | interface SettingDialogProps { 15 | open: boolean; 16 | onClose: () => void; 17 | } 18 | 19 | function SettingDialog({ open, onClose }: SettingDialogProps) { 20 | const { i18n } = useTranslation(); 21 | 22 | const [selected, setSelected] = useState(i18n.t('setting.chat.title') as string); 23 | 24 | useEffect(() => { 25 | setSelected(i18n.t('setting.chat.title') as string); 26 | }, [i18n.t('setting.chat.title')]); 27 | 28 | const catalogItems = [ 29 | i18n.t('setting.chat.title') as string, 30 | i18n.t('setting.synthesis.title') as string, 31 | i18n.t('setting.recognition.title') as string, 32 | // i18n.t('setting.about.title') as string, 33 | ]; 34 | 35 | const catalogIcons = [ 36 | , 37 | , 38 | , 39 | // , 40 | ]; 41 | 42 | return ( 43 | 44 |
45 | } 49 | style="hover:bg-slate-200 active:bg-slate-300" 50 | /> 51 |
52 |
53 |
54 | 59 |
60 |
61 | 67 |
68 |
69 | 70 |
71 |
72 |
73 | ); 74 | } 75 | 76 | export default SettingDialog; 77 | -------------------------------------------------------------------------------- /src/components/Settings/SettingSelector.tsx: -------------------------------------------------------------------------------- 1 | interface SettingSelectorProps { 2 | selected: string | null; 3 | onSelect: (item: string) => void; 4 | catalogItems: string[]; 5 | catalogIcons: JSX.Element[]; 6 | } 7 | 8 | function SettingSelector({ selected, onSelect, catalogItems, catalogIcons }: SettingSelectorProps) { 9 | return ( 10 |
11 | {catalogItems.map((item, key) => ( 12 |
onSelect(item)} 18 | > 19 |
{catalogIcons[key]}
20 |
{item}
21 |
22 | ))} 23 |
24 | ); 25 | } 26 | 27 | export default SettingSelector; 28 | -------------------------------------------------------------------------------- /src/components/Settings/azureTtsVoice.tsx: -------------------------------------------------------------------------------- 1 | import React, { useEffect } from 'react'; 2 | import SettingSelect from './base/SettingSelect'; 3 | 4 | import { azureSpeechSynthesisVoices } from '../../constants/data'; 5 | import { useGlobalStore } from '../../store/module'; 6 | import { useTranslation } from 'react-i18next'; 7 | 8 | const azureTtsVoice = () => { 9 | const { i18n } = useTranslation(); 10 | 11 | const { speech, setSpeech } = useGlobalStore(); 12 | 13 | const languageCode = speech.azureLanguage; 14 | 15 | useEffect(() => { 16 | if (!azureSpeechSynthesisVoices[languageCode].includes(speech.azureVoice)) { 17 | setSpeech({ 18 | ...speech, 19 | azureVoice: azureSpeechSynthesisVoices[languageCode][0], 20 | }); 21 | } 22 | }, [languageCode]); 23 | 24 | return ( 25 | setSpeech({ ...speech, azureVoice: e })} 33 | /> 34 | ); 35 | }; 36 | 37 | export default azureTtsVoice; 38 | -------------------------------------------------------------------------------- /src/components/Settings/base/SettingCheckText.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import OKCircleIcon from '../../Icons/OKCircleIcon'; 3 | 4 | interface SettingCheckTextProps { 5 | text: string; 6 | } 7 | 8 | function SettingCheckText({ text }: SettingCheckTextProps) { 9 | return ( 10 |
11 | 12 |
{text}
13 |
14 | ); 15 | } 16 | 17 | export default SettingCheckText; 18 | -------------------------------------------------------------------------------- /src/components/Settings/base/SettingDivider.tsx: -------------------------------------------------------------------------------- 1 | function SettingDivider() { 2 | return
; 3 | } 4 | 5 | export default SettingDivider; 6 | -------------------------------------------------------------------------------- /src/components/Settings/base/SettingGroup.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | interface SettingGroupProps { 4 | children: React.ReactNode; 5 | } 6 | function SettingGroup({ children }: SettingGroupProps) { 7 | return
{children}
; 8 | } 9 | 10 | export default SettingGroup; 11 | -------------------------------------------------------------------------------- /src/components/Settings/base/SettingInput.tsx: -------------------------------------------------------------------------------- 1 | import SettingSubtitle from './SettingSubtitle'; 2 | import Input from '../../base/Input'; 3 | 4 | interface SettingInputProps { 5 | text: string; 6 | helpText?: string; 7 | id: string; 8 | type: string; 9 | className?: string; 10 | value: string; 11 | placeholder: string; 12 | onChange: (value: string) => void; 13 | } 14 | 15 | function SettingInput({ 16 | text, 17 | helpText, 18 | id, 19 | type, 20 | className, 21 | value, 22 | placeholder, 23 | onChange, 24 | }: SettingInputProps) { 25 | return ( 26 |
27 | 28 | onChange(e.target.value)} 33 | className={`h-9 mx-0.5 ${className}`} 34 | placeholder={placeholder} 35 | /> 36 |
37 | ); 38 | } 39 | 40 | export default SettingInput; 41 | -------------------------------------------------------------------------------- /src/components/Settings/base/SettingSelect.tsx: -------------------------------------------------------------------------------- 1 | import SettingSubtitle from './SettingSubtitle'; 2 | import Select from '../../base/Select'; 3 | 4 | interface SettingSelectProps { 5 | text: string; 6 | helpText?: string; 7 | options: string[]; 8 | value: string; 9 | onChange: (value: string) => void; 10 | className?: string; 11 | selectClassName?: string; 12 | } 13 | 14 | function SettingSelect({ 15 | text, 16 | helpText, 17 | options, 18 | value, 19 | onChange, 20 | className, 21 | selectClassName, 22 | }: SettingSelectProps) { 23 | return ( 24 |
25 | 26 | onChange(parseFloat(e.target.value))} 50 | step={step} 51 | /> 52 | 61 |
62 |
63 | ); 64 | } 65 | 66 | export default SettingSlider; 67 | -------------------------------------------------------------------------------- /src/components/Settings/base/SettingSubtitle.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import Tippy from '@tippyjs/react'; 3 | import HelpIcon from '../../Icons/HelpIcon'; 4 | 5 | interface SettingSubtitleProps { 6 | text: string; 7 | helpText?: string; 8 | } 9 | 10 | function SettingSubtitle({ text, helpText }: SettingSubtitleProps) { 11 | return ( 12 |
13 | {text} 14 | {helpText && ( 15 |
16 | 24 |
25 | 26 |
27 |
28 |
29 | )} 30 |
31 | ); 32 | } 33 | 34 | export default SettingSubtitle; 35 | -------------------------------------------------------------------------------- /src/components/Settings/base/SettingSwitch.tsx: -------------------------------------------------------------------------------- 1 | import SettingSubtitle from './SettingSubtitle'; 2 | import Toggle from '../../base/Toggle'; 3 | 4 | interface SettingSwitchProps { 5 | text: string; 6 | helpText?: string; 7 | checked: boolean; 8 | onChange: (checked: boolean) => void; 9 | className?: string; 10 | } 11 | 12 | function SettingSwitch({ text, helpText, checked, onChange, className }: SettingSwitchProps) { 13 | return ( 14 |
15 | 16 | 17 |
18 | ); 19 | } 20 | 21 | export default SettingSwitch; 22 | -------------------------------------------------------------------------------- /src/components/Settings/base/SettingTextArea.tsx: -------------------------------------------------------------------------------- 1 | import Textarea from '../../base/Textarea'; 2 | import SettingSubtitle from './SettingSubtitle'; 3 | 4 | interface SettingTextAreaProps { 5 | text: string; 6 | helpText?: string; 7 | value: string; 8 | onChange: (value: string) => void; 9 | className?: string; 10 | placeholder?: string; 11 | maxRows?: number; 12 | } 13 | 14 | function SettingTextArea({ 15 | text, 16 | helpText, 17 | value, 18 | onChange, 19 | className, 20 | placeholder, 21 | maxRows, 22 | }: SettingTextAreaProps) { 23 | return ( 24 |
25 | 26 |