├── .nvmrc ├── .eslintrc.json ├── src ├── state │ ├── index.ts │ ├── common.ts │ └── settings.ts ├── constants │ ├── index.ts │ ├── common.ts │ └── prompts.ts ├── lib │ └── utils.ts ├── components │ ├── NotificationBar.tsx │ ├── UserPanel.tsx │ ├── AuthModal.tsx │ ├── VoiceSetting.tsx │ ├── Footer.tsx │ ├── Navbar.tsx │ ├── ui │ │ ├── switch.tsx │ │ ├── button.tsx │ │ └── dialog.tsx │ ├── User │ │ ├── UserInfo.tsx │ │ ├── LoginForm.tsx │ │ └── RegisterForm.tsx │ ├── Settings.tsx │ ├── PlayerBtn.tsx │ ├── RecordBtn.tsx │ ├── Onboarding.tsx │ ├── AIPanel.tsx │ ├── MessageItem.tsx │ ├── HistoryPanel.tsx │ ├── Content.tsx │ └── TalkBoost.tsx ├── utils.ts ├── apis │ ├── chatAI.ts │ └── azureTTS.ts ├── i18n.ts ├── locales │ ├── zh.json │ └── en.json └── globals.css ├── pages ├── index.tsx ├── _document.tsx └── _app.tsx ├── public ├── favicon.ico └── images │ └── logo.png ├── prisma ├── migrations │ ├── migration_lock.toml │ └── 20230820080439_init │ │ └── migration.sql └── schema.prisma ├── .prettierrc.json ├── .env.example ├── app └── api │ └── user │ ├── logout │ └── route.ts │ ├── login │ └── route.ts │ └── register │ └── route.ts ├── next.config.js ├── components.json ├── postcss.config.js ├── .vscode └── settings.json ├── .gitignore ├── README_CN.md ├── .github └── workflows │ └── comment-help-wanted.yml ├── scripts └── addDatabaseUser.mjs ├── tsconfig.json ├── README.md ├── package.json └── tailwind.config.js /.nvmrc: -------------------------------------------------------------------------------- 1 | 18 -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": ["next", "prettier"] 3 | } 4 | -------------------------------------------------------------------------------- /src/state/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | export * from './settings' 3 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from './prompts' 2 | export * from './common' 3 | -------------------------------------------------------------------------------- /pages/index.tsx: -------------------------------------------------------------------------------- 1 | import TalkBoost from '@/components/TalkBoost' 2 | 3 | export default TalkBoost 4 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circle-hotaru/talk-boost-ai/HEAD/public/favicon.ico -------------------------------------------------------------------------------- /public/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/circle-hotaru/talk-boost-ai/HEAD/public/images/logo.png -------------------------------------------------------------------------------- /src/state/common.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai' 2 | 3 | export const authModalOpenAtom = atom(false) 4 | export const userInfoModalOpenAtom = atom(false) 5 | -------------------------------------------------------------------------------- /prisma/migrations/migration_lock.toml: -------------------------------------------------------------------------------- 1 | # Please do not edit this file manually 2 | # It should be added in your version-control system (i.e. Git) 3 | provider = "postgresql" -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "semi": false, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "jsxSingleQuote": true, 7 | "plugins": ["prettier-plugin-tailwindcss"] 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/utils.ts: -------------------------------------------------------------------------------- 1 | import { type ClassValue, clsx } from "clsx" 2 | import { twMerge } from "tailwind-merge" 3 | 4 | export function cn(...inputs: ClassValue[]) { 5 | return twMerge(clsx(inputs)) 6 | } 7 | -------------------------------------------------------------------------------- /.env.example: -------------------------------------------------------------------------------- 1 | NEXT_PUBLIC_CHAT_API_KEY= 2 | NEXT_PUBLIC_CHAT_API_PROXY= 3 | NEXT_PUBLIC_AZURE_SPEECH_KEY= 4 | NEXT_PUBLIC_AZURE_SPEECH_REGION= 5 | GOOGLE_ANALYTICS_ID= 6 | DATABASE_URL= 7 | DATABASE_DIRECT_URL= 8 | JWT_SECRET= -------------------------------------------------------------------------------- /app/api/user/logout/route.ts: -------------------------------------------------------------------------------- 1 | import { NextResponse } from 'next/server' 2 | 3 | export async function GET() { 4 | const response = NextResponse.json({}) 5 | response.cookies.set('token', '', { maxAge: 0 }) 6 | return response 7 | } 8 | -------------------------------------------------------------------------------- /next.config.js: -------------------------------------------------------------------------------- 1 | /** @type {import('next').NextConfig} */ 2 | const nextConfig = { 3 | reactStrictMode: false, 4 | swcMinify: true, 5 | experimental: { 6 | appDir: true, 7 | }, 8 | } 9 | 10 | module.exports = nextConfig 11 | -------------------------------------------------------------------------------- /src/constants/common.ts: -------------------------------------------------------------------------------- 1 | export const voices = [ 2 | { id: 'en-US-AvaMultilingualNeural', name: 'Ava' }, 3 | { id: 'en-US-AndrewMultilingualNeural', name: 'Andrew' }, 4 | { id: 'en-US-EmmaMultilingualNeural', name: 'Emma' }, 5 | { id: 'en-US-BrianMultilingualNeural', name: 'Brian' }, 6 | ] 7 | -------------------------------------------------------------------------------- /src/state/settings.ts: -------------------------------------------------------------------------------- 1 | import { atom } from 'jotai' 2 | import { voices } from '@/constants' 3 | 4 | export const openVoiceAtom = atom(true) 5 | export const openAiCount = atom(0) 6 | 7 | export const recordNowHistory = atom(0) 8 | 9 | export const recordNowHistoryName = atom('') 10 | export const voiceIdAtom = atom(voices[0].id) 11 | -------------------------------------------------------------------------------- /src/components/NotificationBar.tsx: -------------------------------------------------------------------------------- 1 | import { Alert } from 'antd' 2 | import { useTranslation } from 'react-i18next' 3 | 4 | const NotificationBar = () => { 5 | const { t } = useTranslation() 6 | return ( 7 |
8 | 9 |
10 | ) 11 | } 12 | 13 | export default NotificationBar 14 | -------------------------------------------------------------------------------- /components.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://ui.shadcn.com/schema.json", 3 | "style": "default", 4 | "rsc": false, 5 | "tsx": true, 6 | "tailwind": { 7 | "config": "tailwind.config.js", 8 | "css": "src/globals.css", 9 | "baseColor": "slate", 10 | "cssVariables": true, 11 | "prefix": "" 12 | }, 13 | "aliases": { 14 | "components": "@/components", 15 | "utils": "@/lib/utils" 16 | } 17 | } -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | 'postcss-flexbugs-fixes', 4 | 'postcss-preset-env', 5 | [ 6 | 'tailwindcss', 7 | { 8 | // 在生产环境中移除未使用的 CSS 代码 9 | purge: process.env.NODE_ENV === 'production', 10 | // 配置文件路径 11 | config: './tailwind.config.js', 12 | }, 13 | ], 14 | [ 15 | 'autoprefixer', 16 | { 17 | // 针对 flexbox 布局的一些 bug 进行修复 18 | flexbox: 'no-2009', 19 | }, 20 | ], 21 | ], 22 | } 23 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "editor.tabSize": 2, 4 | "editor.formatOnSave": true, 5 | "prettier.singleQuote": true, 6 | "prettier.semi": false, 7 | "prettier.trailingComma": "es5", 8 | "typescript.tsdk": "node_modules/.pnpm/typescript@5.0.3/node_modules/typescript/lib", 9 | "typescript.enablePromptUseWorkspaceTsdk": true, 10 | "cSpell.words": [ 11 | "incircle", 12 | "antd", 13 | "IELTS", 14 | "OPENAI", 15 | "Avvvatars", 16 | "lucide-react" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # next.js 12 | /.next/ 13 | /out/ 14 | 15 | # production 16 | /build 17 | 18 | # misc 19 | .DS_Store 20 | *.pem 21 | 22 | # debug 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | .pnpm-debug.log* 27 | 28 | # local env files 29 | .env 30 | .env*.local 31 | 32 | # vercel 33 | .vercel 34 | 35 | # typescript 36 | *.tsbuildinfo 37 | next-env.d.ts 38 | -------------------------------------------------------------------------------- /prisma/migrations/20230820080439_init/migration.sql: -------------------------------------------------------------------------------- 1 | -- CreateTable 2 | CREATE TABLE "users" ( 3 | "userId" SERIAL NOT NULL, 4 | "name" TEXT NOT NULL, 5 | "email" TEXT NOT NULL, 6 | "password" TEXT NOT NULL, 7 | "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 | "updatedAt" TIMESTAMP(3) NOT NULL, 9 | 10 | CONSTRAINT "users_pkey" PRIMARY KEY ("userId") 11 | ); 12 | 13 | -- CreateIndex 14 | CREATE UNIQUE INDEX "users_name_key" ON "users"("name"); 15 | 16 | -- CreateIndex 17 | CREATE UNIQUE INDEX "users_email_key" ON "users"("email"); 18 | -------------------------------------------------------------------------------- /prisma/schema.prisma: -------------------------------------------------------------------------------- 1 | // This is your Prisma schema file, 2 | // learn more about it in the docs: https://pris.ly/d/prisma-schema 3 | 4 | generator client { 5 | provider = "prisma-client-js" 6 | } 7 | 8 | datasource db { 9 | provider = "postgresql" 10 | url = env("DATABASE_URL") 11 | directUrl = env("DATABASE_DIRECT_URL") 12 | } 13 | 14 | 15 | model User { 16 | userId Int @id @default(autoincrement()) 17 | name String @unique 18 | email String @unique 19 | password String 20 | 21 | createdAt DateTime @default(now()) 22 | updatedAt DateTime @updatedAt 23 | 24 | @@map("users") 25 | } -------------------------------------------------------------------------------- /src/utils.ts: -------------------------------------------------------------------------------- 1 | export const isIOS = () => { 2 | const userAgent = navigator.userAgent.toLowerCase() 3 | return /iphone|ipad|ipod/.test(userAgent) 4 | } 5 | 6 | export const setLocal = (name, data) => { 7 | if (!name) return 8 | if (typeof data !== 'string') { 9 | data = JSON.stringify(data) 10 | } 11 | window.localStorage.setItem(name, data) 12 | } 13 | 14 | export const getLocal = (name) => { 15 | if (!name) return 16 | let item = window.localStorage.getItem(name) 17 | return JSON.parse(item) 18 | } 19 | 20 | export const removeLocal = (name) => { 21 | if (!name) return 22 | window.localStorage.removeItem(name) 23 | } 24 | -------------------------------------------------------------------------------- /README_CN.md: -------------------------------------------------------------------------------- 1 |

TalkBoostAI

2 | 3 | [English](./README.md) | 简体中文 4 | 5 | ## 🌟 Introduction 6 | 7 | TalkBoostAI 是一个利用 AI 来帮助你提高英语口语交流能力的网页应用,免费体验 chatGPT 的 AI 模型。 8 | 9 | ## ✨ 演示 10 | 11 | 欢迎直接体验[我们的网站](https://talk.incircles.xyz/) :) 12 | 13 | ## 🚀 产品功能 14 | 15 | - 💬 与 AI 自然对话,交流无压力 16 | - 🎙️ 支持语音交流,沉浸式练习 17 | - ⚡️ 完全免费体验 chatGPT 的 AI 模型 18 | 19 | ## 👨‍🚀 开发 20 | 21 | ### 设置 .env 22 | 23 | 请在项目目录中,创建一个名为 .env 的文件。 24 | 25 | ### 设置数据库 26 | 27 | https://supabase.com/partners/integrations/prisma 28 | 29 | ### 运行应用 30 | 31 | ``` 32 | pnpm install 33 | pnpm dev 34 | ``` 35 | 36 | 在浏览器打开 http://localhost:3000 查看应用。 37 | -------------------------------------------------------------------------------- /.github/workflows/comment-help-wanted.yml: -------------------------------------------------------------------------------- 1 | name: Comment help wanted 2 | on: 3 | issues: 4 | types: 5 | - labeled 6 | jobs: 7 | comment-help-wanted: 8 | if: github.event.label.name == 'help wanted' 9 | runs-on: ubuntu-latest 10 | permissions: 11 | issues: write 12 | steps: 13 | - name: Add comment 14 | run: gh issue comment "$NUMBER" --body "$BODY" 15 | env: 16 | GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | GH_REPO: ${{ github.repository }} 18 | NUMBER: ${{ github.event.issue.number }} 19 | BODY: > 20 | This issue is available for anyone to work on. 21 | **Make sure to reference this issue in your pull request.** 22 | :sparkles: Thank you for your contribution! :sparkles: 23 | -------------------------------------------------------------------------------- /scripts/addDatabaseUser.mjs: -------------------------------------------------------------------------------- 1 | import { PrismaClient } from '@prisma/client' 2 | import bcrypt from 'bcrypt' 3 | 4 | const prisma = new PrismaClient() 5 | 6 | async function main() { 7 | // ... you will write your Prisma Client queries here 8 | const salt = bcrypt.genSaltSync(10) 9 | const hashedPassword = await bcrypt.hash('password123', salt) 10 | 11 | const user = await prisma.user.create({ 12 | data: { 13 | name: 'Alice', 14 | email: 'Alice@example.com', 15 | password: hashedPassword, 16 | }, 17 | }) 18 | console.log('User created successfully!\n', user) 19 | } 20 | 21 | main() 22 | .then(async () => { 23 | await prisma.$disconnect() 24 | }) 25 | .catch(async (e) => { 26 | console.error(e) 27 | await prisma.$disconnect() 28 | process.exit(1) 29 | }) 30 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "lib": ["dom", "dom.iterable", "esnext"], 5 | "allowJs": true, 6 | "skipLibCheck": true, 7 | "strict": false, 8 | "forceConsistentCasingInFileNames": true, 9 | "noEmit": true, 10 | "esModuleInterop": true, 11 | "module": "esnext", 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "isolatedModules": true, 15 | "jsx": "preserve", 16 | "incremental": true, 17 | "baseUrl": ".", 18 | "paths": { 19 | "@/*": ["src/*"] 20 | }, 21 | "plugins": [ 22 | { 23 | "name": "next" 24 | } 25 | ], 26 | "strictNullChecks": false 27 | }, 28 | "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"], 29 | "exclude": ["node_modules"] 30 | } 31 | -------------------------------------------------------------------------------- /src/components/UserPanel.tsx: -------------------------------------------------------------------------------- 1 | import ReactMarkdown from 'react-markdown' 2 | import Avvvatars from 'avvvatars-react' 3 | import { isMobile } from 'react-device-detect' 4 | 5 | const UserPanel: React.FC<{ content: string }> = ({ content }) => { 6 | const user = JSON.parse(window.localStorage.getItem('user') ?? '{}') 7 | 8 | return ( 9 |
10 | 15 | {content} 16 | 17 |
18 | 23 |
24 |
25 | ) 26 | } 27 | 28 | export default UserPanel 29 | -------------------------------------------------------------------------------- /src/apis/chatAI.ts: -------------------------------------------------------------------------------- 1 | import { Messages } from '@/components/TalkBoost' 2 | 3 | const defaultAPI = 'https://api.openai.com' 4 | const proxyAPI = process.env.NEXT_PUBLIC_CHAT_API_PROXY 5 | const apiUrl = proxyAPI ?? defaultAPI 6 | const apiKey = process.env.NEXT_PUBLIC_CHAT_API_KEY 7 | 8 | export const requestChatAI = async (messages: Messages): Promise => { 9 | const requestOptions = { 10 | method: 'POST', 11 | headers: { 12 | 'Content-Type': 'application/json', 13 | Authorization: !!apiKey ? `Bearer ${apiKey}` : null, 14 | }, 15 | body: JSON.stringify({ 16 | model: 'gpt-4.1-mini', 17 | messages: messages, 18 | }), 19 | } 20 | 21 | const response = await fetch(`${apiUrl}/v1/chat/completions`, requestOptions) 22 | if (!response.ok) { 23 | throw new Error('Failed to get AI response') 24 | } 25 | const data = await response.json() 26 | return data.choices[0].message.content 27 | } 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

TalkBoostAI

2 | 3 | English | [简体中文](./README_CN.md) 4 | 5 | ## 🌟 Introduction 6 | 7 | TalkBoostAI is a web application that utilizes AI to help you improve your English speaking and conversation skills. It allows you to freely experience chatGPT's AI model. 8 | 9 | ## ✨ Demo 10 | 11 | For the best demo experience, try [our site](https://talk.incircles.xyz/) directly :) 12 | 13 | ## 🚀 Features 14 | 15 | - 💬 Converse with AI naturally, communicate without stress 16 | - 🎙️ Voice input and output for immersive practice 17 | - ⚡️ Freely experience chatGPT's AI model 18 | 19 | ## 👨‍🚀 Development 20 | 21 | ### Setup .env 22 | 23 | In the project directory, create a file called .env. 24 | 25 | ### Setup database 26 | 27 | https://supabase.com/partners/integrations/prisma 28 | 29 | ### Running the Application 30 | 31 | ``` 32 | pnpm install 33 | pnpm dev 34 | ``` 35 | 36 | Open http://localhost:3000 to see the app. 37 | -------------------------------------------------------------------------------- /src/i18n.ts: -------------------------------------------------------------------------------- 1 | import i18n from 'i18next' 2 | import { initReactI18next } from 'react-i18next' 3 | import LanguageDetector from 'i18next-browser-languagedetector' 4 | import enTranslation from './locales/en.json' 5 | import zhTranslation from './locales/zh.json' 6 | 7 | // the translations 8 | // (tip move them in a JSON file and import them, 9 | // or even better, manage them separated from your code: https://react.i18next.com/guides/multiple-translation-files) 10 | const resources = { 11 | en: { 12 | translation: enTranslation, 13 | }, 14 | zh: { 15 | translation: zhTranslation, 16 | }, 17 | } 18 | 19 | i18n 20 | .use(LanguageDetector) 21 | .use(initReactI18next) // passes i18n down to react-i18next 22 | .init({ 23 | resources, 24 | fallbackLng: 'en', 25 | detection: { 26 | order: ['navigator', 'htmlTag'], 27 | }, 28 | interpolation: { 29 | escapeValue: false, 30 | }, 31 | }) 32 | 33 | export default i18n 34 | -------------------------------------------------------------------------------- /src/locales/zh.json: -------------------------------------------------------------------------------- 1 | { 2 | "mode": { 3 | "english_communication": "😊 英语交流", 4 | "french_communication": "🥐 法语交流", 5 | "japanese_communication": "🌸 日语交流", 6 | "spanish_communication": "🌮 西班牙语交流", 7 | "IELTS_speaking_test": "🎓 雅思口语测试" 8 | }, 9 | "error": { 10 | "input_username": "请输入用户名!", 11 | "input_email": "请输入邮箱!", 12 | "input_password": "请输入密码!" 13 | }, 14 | "chat": "聊天", 15 | "in_chat": "进行中", 16 | "send": "发送", 17 | "translate": "翻译", 18 | "use_voice_answer": "使用语音回答", 19 | "new_chat": "新的对话", 20 | "register": "注册", 21 | "login": "登录", 22 | "username": "用户名", 23 | "email": "邮箱", 24 | "password": "密码", 25 | "go_to_login": "去登录", 26 | "go_to_register": "去注册", 27 | "logout": "登出", 28 | "user_exists": "用户已存在", 29 | "history": "历史记录", 30 | "delete": "删除", 31 | "switch_language": "切换语言", 32 | "voice_model": "语音模型", 33 | "notification": "语音服务暂不可用", 34 | "tarot_master_link": "塔罗大师" 35 | } 36 | -------------------------------------------------------------------------------- /src/components/AuthModal.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react' 2 | import { useTranslation } from 'react-i18next' 3 | import { Modal } from 'antd' 4 | import { useAtom } from 'jotai' 5 | import { authModalOpenAtom } from '@/state' 6 | import RegisterForm from './User/RegisterForm' 7 | import LoginForm from './User/LoginForm' 8 | 9 | const AuthModal: React.FC = () => { 10 | const { t } = useTranslation() 11 | const [open, setOpen] = useAtom(authModalOpenAtom) 12 | const [isRegister, setIsRegister] = useState(true) 13 | 14 | return ( 15 | setOpen(false)} 19 | footer={null} 20 | closeIcon={false} 21 | > 22 | {isRegister ? ( 23 | setIsRegister(false)} /> 24 | ) : ( 25 | setIsRegister(true)} /> 26 | )} 27 | 28 | ) 29 | } 30 | 31 | export default AuthModal 32 | -------------------------------------------------------------------------------- /src/components/VoiceSetting.tsx: -------------------------------------------------------------------------------- 1 | import { useTranslation } from 'react-i18next' 2 | import { useAtom } from 'jotai' 3 | import { voiceIdAtom } from '@/state' 4 | import { voices } from '@/constants' 5 | import { Select } from 'antd' 6 | 7 | const VoiceSettingModal: React.FC = () => { 8 | const { t } = useTranslation() 9 | const [voiceId, setVoiceId] = useAtom(voiceIdAtom) 10 | 11 | const handleOptionChange = (value: string) => { 12 | setVoiceId(value) 13 | } 14 | 15 | return ( 16 |
17 | 20 | 27 |
28 | ) 29 | } 30 | 31 | export default VoiceSettingModal 32 | -------------------------------------------------------------------------------- /pages/_document.tsx: -------------------------------------------------------------------------------- 1 | import { Html, Head, Main, NextScript } from 'next/document' 2 | import Script from 'next/script' 3 | 4 | export default function Document() { 5 | const GOOGLE_ANALYTICS_ID = process.env.GOOGLE_ANALYTICS_ID 6 | return ( 7 | 8 | 9 | 13 |