├── .env.development ├── .eslintrc.cjs ├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ └── feature_request.md ├── dependabot.yml └── workflows │ ├── build.yml │ └── release.yml ├── .gitignore ├── LICENSE ├── Makefile ├── README.md ├── index.html ├── package.json ├── postcss.config.js ├── src ├── App.css ├── App.tsx ├── api │ ├── restful │ │ ├── api.ts │ │ ├── model.ts │ │ └── util.ts │ └── sse │ │ ├── event.ts │ │ ├── server-ability.ts │ │ └── sse.tsx ├── assets │ ├── bg │ │ ├── clement │ │ │ └── December 21 Night.jpg │ │ ├── no-copyright │ │ │ ├── 84974784818869.5d6bfdf4e8260.png │ │ │ └── 959e3384818869.5d6bfdf2b5e1b.png │ │ ├── noise-lg.svg │ │ ├── noise.svg │ │ └── wikiart-public-domain │ │ │ ├── Composition VIII 1923.jpg │ │ │ └── simultaneous-counter-composition-1930.jpg │ ├── font │ │ └── Borel │ │ │ ├── Borel-Regular.ttf │ │ │ └── OFL.txt │ └── svg │ │ ├── Google_2015_logo.svg │ │ ├── gemini.ico │ │ ├── ios-spinner.svg │ │ └── openai-icon.svg ├── auth │ └── auth.tsx ├── config.ts ├── data-structure │ ├── client-option.tsx │ ├── message.tsx │ └── provider-api-refrence │ │ ├── chat-gpt.ts │ │ ├── elevenlabs-tts.ts │ │ ├── gemini.ts │ │ ├── google-stt.ts │ │ ├── google-tts.ts │ │ ├── llm.ts │ │ ├── types.ts │ │ └── whisper.ts ├── data │ ├── chat.ts │ ├── google-sst-language.ts │ ├── google-tts-language.ts │ └── prompt.ts ├── error.tsx ├── experiment │ ├── experiment.tsx │ ├── subscribe-test.tsx │ └── valtio.tsx ├── favicon.png ├── home │ ├── chat-window │ │ ├── attached │ │ │ ├── attached-item.tsx │ │ │ └── attached-preview.tsx │ │ ├── chat-window.tsx │ │ ├── compnent │ │ │ ├── audio.tsx │ │ │ ├── drop-down-menu.tsx │ │ │ ├── error-boundary.tsx │ │ │ ├── my-error.tsx │ │ │ ├── my-text.tsx │ │ │ ├── prompt-attached-button.tsx │ │ │ ├── theme.ts │ │ │ └── widget │ │ │ │ ├── highlightjs-plugins │ │ │ │ ├── copy-button-plugin.css │ │ │ │ ├── copy-button-plugin.tsx │ │ │ │ └── language-label-plugin.tsx │ │ │ │ └── icon.tsx │ │ ├── message-list │ │ │ ├── menu.tsx │ │ │ ├── message-list.tsx │ │ │ └── row.tsx │ │ ├── prompt-attached.tsx │ │ ├── prompt │ │ │ ├── prompt-editor-item.tsx │ │ │ ├── prompt-editor.tsx │ │ │ ├── prompt-item.tsx │ │ │ └── prompt-list.tsx │ │ ├── recorder.tsx │ │ └── text-area.tsx │ ├── home.tsx │ └── panel │ │ ├── chat-list │ │ ├── avatar.tsx │ │ ├── chat-component.tsx │ │ ├── chat-list.tsx │ │ ├── draggable-chat.tsx │ │ └── preview.tsx │ │ ├── current │ │ ├── current.tsx │ │ └── other-setting.tsx │ │ ├── global │ │ ├── global.tsx │ │ ├── other-setting.tsx │ │ └── shortcuts-setting.tsx │ │ ├── panel.tsx │ │ └── shared │ │ ├── llm │ │ ├── chat-gpt.tsx │ │ ├── gemini.tsx │ │ └── llm.tsx │ │ ├── select-box-or-not-available.tsx │ │ ├── stt │ │ ├── google-stt.tsx │ │ ├── stt.tsx │ │ └── whisper.tsx │ │ ├── tts │ │ ├── elevenlabs.tsx │ │ ├── google-tts.tsx │ │ └── tts.tsx │ │ └── widget │ │ ├── button.tsx │ │ ├── discrete-range.tsx │ │ ├── logo.tsx │ │ ├── select-box.tsx │ │ ├── separator.tsx │ │ ├── slider-range.tsx │ │ └── switch.tsx ├── index.css ├── main.tsx ├── other.ts ├── prose.css ├── shared-types.ts ├── state │ ├── app-state.ts │ ├── control-state.ts │ ├── dangerous.ts │ ├── db.ts │ ├── layout-state.ts │ ├── message-state.ts │ ├── migration.ts │ ├── network-state.ts │ ├── promt-state.ts │ └── shortcuts.ts ├── util │ ├── enhanced-recorder.ts │ └── util.tsx ├── vite-env.d.ts ├── wallpaper │ ├── art.tsx │ ├── granim-wallpaper.tsx │ └── wallpaper.tsx ├── window-listeners.tsx └── worker │ ├── subscribe-audio-duration-update.tsx │ ├── subscribe-sending-message.tsx │ ├── timeout-content-detection.tsx │ └── workers.tsx ├── tailwind.config.js ├── tsconfig.json ├── tsconfig.node.json ├── vite.config.ts └── yarn.lock /.env.development: -------------------------------------------------------------------------------- 1 | VITE_REACT_APP_ENDPOINT=http://localhost:8000/api -------------------------------------------------------------------------------- /.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: {browser: true, es2020: true}, 4 | extends: [ 5 | 'eslint:recommended', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react-hooks/recommended', 8 | "plugin:valtio/recommended" 9 | ], 10 | ignorePatterns: ['dist', '.eslintrc.cjs'], 11 | parser: '@typescript-eslint/parser', 12 | plugins: ['react-refresh'], 13 | rules: { 14 | 'react-refresh/only-export-components': [ 15 | 'warn', 16 | {allowConstantExport: true}, 17 | ], 18 | }, 19 | } 20 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | 40 | **Error message and stack trace** 41 | ``` 42 | Version 43 | 44 | Not Available 45 | 46 | Status 47 | 48 | None 49 | 50 | Error Message 51 | 52 | Cannot read properties of undefined (reading 'llm') 53 | 54 | Stack trace 55 | 56 | TypeError: Cannot read properties of undefined (reading 'llm') 57 | at http://localhost:5173/src/home/chat-window/message-list.tsx:43:31 58 | at commitHookEffectListMount (http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:16904:34) 59 | at invokePassiveEffectMountInDEV (http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:18320:19) 60 | at invokeEffectsInDev (http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:19697:19) 61 | at commitDoubleInvokeEffectsInDEV (http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:19682:15) 62 | at flushPassiveEffectsImpl (http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:19499:13) 63 | at flushPassiveEffects (http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:19443:22) 64 | at http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:19324:17 65 | at workLoop (http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:197:42) 66 | at flushWork (http://localhost:5173/node_modules/.vite/deps/chunk-GYWC62UC.js?v=78cad7ae:176:22) 67 | ``` 68 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | # To get started with Dependabot version updates, you'll need to specify which 2 | # package ecosystems to update and where the package manifests are located. 3 | # Please see the documentation for all configuration options: 4 | # https://docs.github.com/github/administering-a-repository/configuration-options-for-dependency-updates 5 | 6 | version: 2 7 | updates: 8 | - package-ecosystem: "yarn" # See documentation for possible values 9 | directory: "/" # Location of package manifests 10 | schedule: 11 | interval: "weekly" 12 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: build 5 | 6 | on: 7 | push: 8 | branches: [ "main" ] 9 | pull_request: 10 | branches: [ "main" ] 11 | workflow_dispatch: 12 | 13 | jobs: 14 | build: 15 | name: Build 16 | runs-on: ubuntu-latest 17 | steps: 18 | - uses: actions/checkout@v3 19 | 20 | - name: Set Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 20.x 24 | 25 | - name: Run install 26 | uses: Borales/actions-yarn@v4.2.0 27 | with: 28 | cmd: install # will run `yarn install` command 29 | - name: Build production bundle 30 | uses: Borales/actions-yarn@v4.2.0 31 | with: 32 | cmd: build 33 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | # This workflow will build a golang project 2 | # For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-go 3 | 4 | name: release 5 | 6 | on: 7 | release: 8 | types: [ published ] 9 | workflow_dispatch: 10 | 11 | permissions: 12 | contents: write 13 | 14 | jobs: 15 | build: 16 | name: binaries 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v3 20 | 21 | - name: Set Node.js 22 | uses: actions/setup-node@v3 23 | with: 24 | node-version: 20.x 25 | 26 | - name: release 27 | run: make release 28 | 29 | - name: Upload the artifacts 30 | uses: skx/github-action-publish-binaries@release-2.0 31 | env: 32 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 33 | with: 34 | args: 'build/*.tar.gz build/*.zip' 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | dist-ssr 4 | *.local 5 | 6 | # Editor directories and files 7 | .vscode/* 8 | !.vscode/extensions.json 9 | .idea 10 | .DS_Store 11 | *.suo 12 | *.ntvs* 13 | *.njsproj 14 | *.sln 15 | *.sw? 16 | 17 | node_modules/ 18 | npm-debug.log 19 | yarn-error.log 20 | 21 | build/ 22 | dist/ 23 | chunks-report.html 24 | 25 | .env.local 26 | .env.development.local 27 | .env.test.local 28 | .env.production.local 29 | 30 | .vscode/ 31 | .idea/ 32 | *.code-workspace 33 | 34 | *.swp 35 | *.tmp 36 | 37 | # Logs 38 | logs 39 | *.log 40 | npm-debug.log* 41 | yarn-debug.log* 42 | yarn-error.log* 43 | pnpm-debug.log* 44 | lerna-debug.log* 45 | 46 | 47 | coverage/ 48 | 49 | .npmrc 50 | .yarnrc 51 | 52 | *.generated.* 53 | 54 | .cache/ 55 | 56 | *.bak 57 | 58 | Thumbs.db 59 | 60 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 proxoar 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 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: build 2 | build: 3 | yarn install && yarn build 4 | 5 | # build and copy dist to proxoar/talk 6 | .PHONY: copy 7 | copy: 8 | make build 9 | rm -rf ../talk/web/html && cp -r ./build/dist ../talk/web/html 10 | 11 | .PHONY: run 12 | run: 13 | yarn dev 14 | 15 | .PHONY: release 16 | release: 17 | make build 18 | cd build; \ 19 | tar -czf dist.tar.gz dist; \ 20 | zip -r dist.zip dist; 21 | 22 | .PHONY: clean 23 | clean: 24 | echo "Cleaning up..." 25 | rm -rf build -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # talk-web 2 | 3 | Talk-web, the web-based interface for [proxoar/talk](https://github.com/proxoar/talk), is a single-page application designed to 4 | emulate the user experience of a native app. 5 | 6 | For more info, refer to [proxoar/talk](https://github.com/proxoar/talk) 7 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Talk 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "talk-vite", 3 | "private": true, 4 | "version": "2.0.3", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "tsc && vite build", 9 | "lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0", 10 | "preview": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@headlessui/react": "^1.7.17", 14 | "@microsoft/fetch-event-source": "^2.0.1", 15 | "@tanstack/react-virtual": "^3.0.0-beta.60", 16 | "@types/highlight.js": "^10.1.0", 17 | "@types/markdown-it": "^13.0.2", 18 | "@types/markdown-it-emoji": "^2.0.2", 19 | "axios": "1.6.0", 20 | "crypto-js": "4.2.0", 21 | "date-fns": "^2.30.0", 22 | "downshift": "^8.1.0", 23 | "framer-motion": "^10.16.1", 24 | "granim": "^2.0.0", 25 | "highlight.js": "^11.8.0", 26 | "highlightjs-copy": "^1.0.5", 27 | "localforage": "^1.10.0", 28 | "lodash": "^4.17.21", 29 | "markdown-it": "^13.0.2", 30 | "markdown-it-emoji": "^2.0.2", 31 | "markdown-it-footnote": "^3.0.3", 32 | "markdown-it-link-attributes": "^4.0.1", 33 | "markdown-it-multimd-table": "^4.2.3", 34 | "markdown-it-sub": "^1.0.0", 35 | "markdown-it-sup": "^1.0.0", 36 | "markdown-it-task-lists": "^2.1.1", 37 | "react": "^18.2.0", 38 | "react-copy-to-clipboard": "^5.1.0", 39 | "react-countdown": "^2.3.5", 40 | "react-dnd": "^16.0.1", 41 | "react-dnd-html5-backend": "^16.0.1", 42 | "react-dom": "^18.2.0", 43 | "react-helmet-async": "^1.3.0", 44 | "react-icons": "^4.10.1", 45 | "react-nice-avatar": "^1.4.1", 46 | "react-router": "^6.15.0", 47 | "react-router-dom": "^6.15.0", 48 | "semver": "^7.5.4", 49 | "valtio": "^1.11.2", 50 | "wavesurfer.js": "^7.3.0" 51 | }, 52 | "devDependencies": { 53 | "@tailwindcss/typography": "^0.5.9", 54 | "@types/crypto-js": "^4.1.1", 55 | "@types/gradient-string": "^1.1.2", 56 | "@types/granim": "^2.0.1", 57 | "@types/lodash": "^4.14.198", 58 | "@types/markdown-it-footnote": "^3.0.1", 59 | "@types/markdown-it-link-attributes": "^3.0.2", 60 | "@types/node": "^20.5.0", 61 | "@types/react": "^18.2.15", 62 | "@types/react-copy-to-clipboard": "^5.0.4", 63 | "@types/react-dom": "^18.2.7", 64 | "@types/react-helmet": "^6.1.6", 65 | "@types/react-virtualized": "^9.21.22", 66 | "@types/react-window": "^1.8.5", 67 | "@types/uuid": "^9.0.2", 68 | "@types/wavesurfer.js": "^6.0.6", 69 | "@typescript-eslint/eslint-plugin": "^6.0.0", 70 | "@typescript-eslint/parser": "^6.0.0", 71 | "@vitejs/plugin-react": "^4.0.4", 72 | "autoprefixer": "^10.4.15", 73 | "eslint": "^8.45.0", 74 | "eslint-plugin-react-hooks": "^4.6.0", 75 | "eslint-plugin-react-refresh": "^0.4.3", 76 | "eslint-plugin-valtio": "^0.6.2", 77 | "postcss": "8.4.32", 78 | "postcss-import": "^15.1.0", 79 | "rollup-plugin-visualizer": "^5.12.0", 80 | "tailwindcss": "^3.3.3", 81 | "typescript": "^5.2.2", 82 | "vite": "5.0.12", 83 | "vite-plugin-svgr": "^4.0.0" 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /postcss.config.js: -------------------------------------------------------------------------------- 1 | export default { 2 | plugins: { 3 | 'postcss-import': {}, 4 | tailwindcss: {}, 5 | autoprefixer: {}, 6 | }, 7 | } 8 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/App.css -------------------------------------------------------------------------------- /src/App.tsx: -------------------------------------------------------------------------------- 1 | export default function App() { 2 | return ( 3 |
4 |
5 | ) 6 | } 7 | -------------------------------------------------------------------------------- /src/api/restful/api.ts: -------------------------------------------------------------------------------- 1 | import axios, {AxiosError} from "axios" 2 | import {ChatReq} from "./model.ts" 3 | import {generateHash} from "../../util/util.tsx" 4 | import {networkState} from "../../state/network-state.ts" 5 | import {appState, setLoggedIn} from "../../state/app-state.ts" 6 | import {APIEndpoint} from "../../config.ts" 7 | 8 | const axiosInstance = axios.create({ 9 | baseURL: APIEndpoint(), 10 | timeout: 5000, 11 | }) 12 | 13 | axiosInstance.interceptors.request.use((config) => { 14 | config.headers['Stream-ID'] = networkState.streamId 15 | if (!config.headers['Authorization']) { 16 | // do not override Authorization headerZ 17 | config.headers['Authorization'] = 'Bearer ' + appState.auth.passwordHash 18 | } 19 | return config 20 | }) 21 | 22 | axiosInstance.interceptors.response.use(response => { 23 | return response 24 | }, 25 | (error: AxiosError) => { 26 | if (error.response && error.response.status === 401) { 27 | setLoggedIn(false) 28 | console.info('Unauthorized', error.response) 29 | } 30 | return Promise.reject(error) 31 | } 32 | ) 33 | 34 | export const postChat = (chat: ChatReq) => { 35 | return axiosInstance.post("chat", chat) 36 | } 37 | 38 | export const postAudioChat = (audio: Blob, fileName: string, chat: ChatReq) => { 39 | const formData = new FormData() 40 | formData.append('audio', audio, fileName) 41 | formData.append('chat', JSON.stringify(chat)) 42 | return axiosInstance.postForm("audio-chat", formData) 43 | } 44 | 45 | export const login = (password?: string) => { 46 | return axiosInstance.get("health", { 47 | headers: password ? { 48 | 'Authorization': 'Bearer ' + generateHash(password) 49 | } : {} 50 | }) 51 | } 52 | -------------------------------------------------------------------------------- /src/api/restful/model.ts: -------------------------------------------------------------------------------- 1 | import {LLMMessage} from "../../shared-types.ts" 2 | 3 | export type ChatReq = { 4 | chatId: string // unique ID for every chat 5 | ticketId: string // A distinctive ID for each request, utilised by the client to associate messages. 6 | ms: LLMMessage[] 7 | talkOption: TalkOption 8 | } 9 | 10 | /** 11 | * When make a request to the Talk server, append a TalkOption to the request. 12 | * Each LLMOption, TTSOption, and SSTOption can only engage one provider at a time. 13 | * For instance, either `talkOption.llm.chatGPT` or `talkOption.llm.claude` should be set to `undefined`. 14 | */ 15 | export type TalkOption = { 16 | toText: boolean, // transcribe user's speech to text, requires STTOption option 17 | toSpeech: boolean, // synthesize user's text to speech, requires TTSOption 18 | completion: boolean, // completion, requires messages or result of transcription, require LLMOption 19 | completionToSpeech: boolean, // synthesize result of completion to speech, requires TTSOption 20 | sttOption?: STTOption, 21 | ttsOption?: TTSOption, 22 | llmOption?: LLMOption 23 | } 24 | 25 | export type LLMOption = { 26 | chatGPT?: ChatGPTOption 27 | gemini?: GeminiOption 28 | } 29 | 30 | export type ChatGPTOption = { 31 | model: string 32 | maxTokens: number 33 | temperature: number 34 | topP: number 35 | presencePenalty: number 36 | frequencyPenalty: number 37 | } 38 | 39 | export type GeminiOption = { 40 | model: string 41 | // stopSequences: string[] 42 | maxOutputTokens: number 43 | temperature: number 44 | topP: number 45 | topK: number 46 | } 47 | 48 | export type STTOption = { 49 | whisper?: WhisperOption 50 | google?: GoogleSTTOption 51 | } 52 | 53 | export type WhisperOption = { 54 | model: string 55 | } 56 | 57 | export type GoogleSTTOption = { 58 | recognizer: string 59 | model?: string 60 | language?: string 61 | } 62 | 63 | export type TTSOption = { 64 | google?: GoogleTTSOption 65 | elevenlabs?: ElevenlabsTTSOption 66 | } 67 | 68 | export type GoogleTTSOption = { 69 | // if VoiceId is provided, LanguageCode and Gender will not be used 70 | voiceId?: string 71 | // if languageCode is undefined, server should return an error, it's users' responsibility to choose a language 72 | languageCode?: string 73 | /** 74 | * An unspecified gender. 75 | * In VoiceSelectionParams, this means that the client doesn't care which 76 | * gender the selected voice will have. In the Voice field of 77 | * ListVoicesResponse, this may mean that the voice doesn't fit any of the 78 | * other categories in this enum, or that the gender of the voice isn't known. 79 | * SsmlVoiceGender_SSML_VOICE_GENDER_UNSPECIFIED SsmlVoiceGender = 0 80 | * A male voice. 81 | * SsmlVoiceGender_MALE SsmlVoiceGender = 1 82 | * A female voice. 83 | * SsmlVoiceGender_FEMALE SsmlVoiceGender = 2 84 | * A gender-neutral voice. This voice is not yet supported. 85 | * SsmlVoiceGender_NEUTRAL SsmlVoiceGender = 3 86 | */ 87 | gender?: GoogleTTSGender 88 | speakingRate: number 89 | pitch: number 90 | volumeGainDb: number 91 | } 92 | 93 | export type ElevenlabsTTSOption = { 94 | voiceId: string 95 | stability: number 96 | clarity: number 97 | } 98 | 99 | export enum GoogleTTSGender { 100 | unspecified = 0, 101 | male = 1, 102 | female = 2, 103 | neutral = 3 104 | } 105 | -------------------------------------------------------------------------------- /src/api/restful/util.ts: -------------------------------------------------------------------------------- 1 | import {isAttached, Message} from "../../data-structure/message.tsx" 2 | import {LLMMessage} from "../../shared-types.ts" 3 | 4 | export const attachedMessages = (messages:Message[], maxAttached: number): LLMMessage[] => { 5 | if (maxAttached <= 0) { 6 | return [] 7 | } 8 | const hist: LLMMessage[] = [] 9 | for (let i = messages.length - 1; i >= 0; i--) { 10 | if (hist.length === maxAttached) { 11 | break 12 | } 13 | const m = messages[i] 14 | if (isAttached(m)) { 15 | hist.push({role: m.role, content: m.text}) 16 | } 17 | } 18 | return hist.reverse() 19 | } -------------------------------------------------------------------------------- /src/api/sse/event.ts: -------------------------------------------------------------------------------- 1 | import {Role} from "../../shared-types.ts" 2 | 3 | export const EventMessageThinking = "message/thinking" 4 | export const EventMessageTextTyping = "message/text/typing" 5 | export const EventMessageTextEOF = "message/text/EOF" 6 | export const EventMessageAudio = "message/audio" 7 | export const EventMessageError = "message/error" 8 | export const EventSystemAbility = "system/ability" 9 | export const EventSystemNotification = "system/notification" 10 | export const EventKeepAlive = "" 11 | 12 | export type SSEMsgMeta = { 13 | // unique ID for the whole chat(contains maybe hundreds of messages) 14 | chatId: string 15 | // unique ID for each request 16 | ticketId: string 17 | // unique ID for each message 18 | messageID: string 19 | role: Role 20 | } 21 | 22 | export type SSEMsgText = SSEMsgMeta & { 23 | text: string 24 | } 25 | 26 | export type SSEMsgAudio = SSEMsgMeta & { 27 | audio: string; // base64 of byte array 28 | durationMs?: number 29 | } 30 | 31 | export type SSEMsgError = SSEMsgMeta & { 32 | errMsg: string 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/api/sse/server-ability.ts: -------------------------------------------------------------------------------- 1 | // ServerAbility guide clients in adjusting all parameters. 2 | export type ServerAbility = { 3 | demo: boolean 4 | llm: ServerLLM 5 | tts: ServerTTS 6 | stt: ServerSTT 7 | } 8 | 9 | // TTS 10 | export type ServerTTS = { 11 | available: boolean 12 | google: ServerGoogleTTS 13 | elevenlabs: ServerElevenlabs 14 | } 15 | 16 | export type ServerGoogleTTS = { 17 | available: boolean 18 | voices?: TaggedItem[] 19 | } 20 | 21 | export type ServerElevenlabs = { 22 | available: boolean 23 | voices?: TaggedItem[] 24 | } 25 | 26 | // STT 27 | export type ServerSTT = { 28 | available: boolean 29 | whisper: ServerWhisper 30 | google: ServerGoogleSTT 31 | } 32 | 33 | export type ServerWhisper = { 34 | available: boolean 35 | models?: string[] 36 | } 37 | 38 | export type ServerGoogleSTT = { 39 | available: boolean 40 | recognizers?: TaggedItem[] 41 | } 42 | 43 | // LLM 44 | export type ServerLLM = { 45 | available: boolean 46 | chatGPT: ServerChatGPT 47 | gemini: ServerGemini 48 | } 49 | 50 | export type ServerChatGPT = { 51 | available: boolean 52 | models?: Model[] 53 | } 54 | 55 | export type ServerGemini = { 56 | available: boolean 57 | models?: Model[] 58 | } 59 | 60 | export type Model = { 61 | name: string 62 | displayName: string 63 | } 64 | 65 | // other 66 | export type TaggedItem = { 67 | id: string 68 | name: string 69 | tags?: string [] // gender, accent, age, etc 70 | } 71 | 72 | export const defaultServerAbility = (): ServerAbility => { 73 | return { 74 | demo: false, 75 | llm: { 76 | available: false, 77 | chatGPT: { 78 | available: false, 79 | }, 80 | gemini: { 81 | available: false, 82 | } 83 | }, 84 | tts: { 85 | available: false, 86 | google: { 87 | available: false 88 | }, 89 | elevenlabs: { 90 | available: false 91 | } 92 | }, 93 | stt: { 94 | available: false, 95 | whisper: { 96 | available: false 97 | }, 98 | google: { 99 | available: false 100 | } 101 | }, 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/api/sse/sse.tsx: -------------------------------------------------------------------------------- 1 | import {useEffect} from 'react' 2 | import {useSnapshot} from "valtio/react" 3 | import {networkState} from "../../state/network-state.ts" 4 | import {appState, findChatProxy, findMessage, findMessage2} from "../../state/app-state.ts" 5 | import {ServerAbility} from "./server-ability.ts" 6 | import {newError, newThinking, onAudio, onEOF, onError, onThinking, onTyping} from "../../data-structure/message.tsx" 7 | import { 8 | EventKeepAlive, 9 | EventMessageAudio, 10 | EventMessageError, 11 | EventMessageTextEOF, 12 | EventMessageTextTyping, 13 | EventMessageThinking, 14 | EventSystemAbility, 15 | SSEMsgAudio, 16 | SSEMsgError, 17 | SSEMsgMeta, 18 | SSEMsgText 19 | } from "./event.ts" 20 | import {base64ToBlob, generateAudioId, randomHash32Char} from "../../util/util.tsx" 21 | import {audioDb} from "../../state/db.ts" 22 | import {audioPlayerMimeType, SSEEndpoint} from "../../config.ts" 23 | import {adjustOption} from "../../data-structure/client-option.tsx" 24 | import {createDemoChatIfNecessary} from "../../data/chat.ts"; 25 | 26 | 27 | export const SSE = () => { 28 | const {passwordHash} = useSnapshot(appState.auth) 29 | // console.info("SSE rendered", new Date().toLocaleString()) 30 | 31 | 32 | useEffect(() => { 33 | const ep = SSEEndpoint() 34 | const streamId = randomHash32Char() 35 | networkState.streamId = streamId 36 | const url = `${ep}?stream=${streamId}&passwordHash=${passwordHash}` 37 | const eventSource = new EventSource(url); 38 | console.info("connecting to [SSE]: ", url) 39 | 40 | eventSource.onopen = (event) => { 41 | console.info("[SSE] connected to server, response: ", event, new Date().toISOString()) 42 | eventSource.withCredentials 43 | } 44 | 45 | eventSource.onerror = (event) => { 46 | console.error("[SSE] error: ", event, new Date().toISOString()); 47 | } 48 | 49 | eventSource.addEventListener(EventSystemAbility, (event: MessageEvent) => { 50 | console.debug("received ability from SSE server", event.type, event.data) 51 | const sa: ServerAbility = JSON.parse(event.data) 52 | adjustOption(appState.option, sa) 53 | for (const chat of appState.chats) { 54 | adjustOption(chat.option, sa) 55 | } 56 | // eslint-disable-next-line valtio/state-snapshot-rule 57 | appState.ability = sa 58 | createDemoChatIfNecessary() 59 | }) 60 | 61 | eventSource.addEventListener(EventMessageThinking, (event: MessageEvent) => { 62 | console.debug("received EventMessageThinking") 63 | const meta: SSEMsgMeta = JSON.parse(event.data) 64 | const found = findChatProxy(meta.chatId, true) 65 | if (!found) { 66 | return 67 | } 68 | const chat = found[0] 69 | const msg = findMessage(chat, meta.messageID) 70 | if (msg) { 71 | onThinking(msg) 72 | } else { 73 | const message = newThinking(meta.messageID, meta.ticketId, meta.role) 74 | chat.messages.push(message) 75 | } 76 | }) 77 | 78 | eventSource.addEventListener(EventMessageTextTyping, (event: MessageEvent) => { 79 | const text: SSEMsgText = JSON.parse(event.data) 80 | const msg = findMessage2(text.chatId, text.messageID, true) 81 | if (msg) { 82 | onTyping(msg, text.text) 83 | } 84 | }) 85 | 86 | eventSource.addEventListener(EventMessageTextEOF, (event: MessageEvent) => { 87 | const text: SSEMsgText = JSON.parse(event.data) 88 | const msg = findMessage2(text.chatId, text.messageID, true) 89 | if (msg) { 90 | onEOF(msg, text.text ?? "") 91 | } 92 | }) 93 | 94 | eventSource.addEventListener(EventMessageAudio, (event: MessageEvent) => { 95 | const audio: SSEMsgAudio = JSON.parse(event.data) 96 | const msg = findMessage2(audio.chatId, audio.messageID, true) 97 | if (msg) { 98 | const blob = base64ToBlob(audio.audio, audioPlayerMimeType) 99 | const audioId = generateAudioId("synthesis") 100 | audioDb.setItem(audioId, blob, () => { 101 | onAudio(msg, {id: audioId, durationMs: audio.durationMs}) 102 | }).then(() => true) 103 | } 104 | }) 105 | 106 | eventSource.addEventListener(EventMessageError, (event: MessageEvent) => { 107 | const error: SSEMsgError = JSON.parse(event.data) 108 | const found = findChatProxy(error.chatId, true) 109 | if (!found) { 110 | return 111 | } 112 | const chat = found[0] 113 | const msg = findMessage(chat, error.messageID) 114 | if (msg) { 115 | onError(msg, error.errMsg) 116 | } else { 117 | const m = newError(error.messageID, error.ticketId, error.role, error.errMsg) 118 | chat.messages.push(m) 119 | } 120 | }) 121 | 122 | eventSource.addEventListener(EventKeepAlive, () => { 123 | // nothing to do 124 | }) 125 | 126 | return () => { 127 | console.info("[SSE] trying to abort") 128 | eventSource.close() 129 | } 130 | }, 131 | [passwordHash] 132 | ) 133 | return null 134 | } 135 | -------------------------------------------------------------------------------- /src/assets/bg/clement/December 21 Night.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/assets/bg/clement/December 21 Night.jpg -------------------------------------------------------------------------------- /src/assets/bg/no-copyright/84974784818869.5d6bfdf4e8260.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/assets/bg/no-copyright/84974784818869.5d6bfdf4e8260.png -------------------------------------------------------------------------------- /src/assets/bg/no-copyright/959e3384818869.5d6bfdf2b5e1b.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/assets/bg/no-copyright/959e3384818869.5d6bfdf2b5e1b.png -------------------------------------------------------------------------------- /src/assets/bg/noise-lg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/bg/noise.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/assets/bg/wikiart-public-domain/Composition VIII 1923.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/assets/bg/wikiart-public-domain/Composition VIII 1923.jpg -------------------------------------------------------------------------------- /src/assets/bg/wikiart-public-domain/simultaneous-counter-composition-1930.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/assets/bg/wikiart-public-domain/simultaneous-counter-composition-1930.jpg -------------------------------------------------------------------------------- /src/assets/font/Borel/Borel-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/assets/font/Borel/Borel-Regular.ttf -------------------------------------------------------------------------------- /src/assets/font/Borel/OFL.txt: -------------------------------------------------------------------------------- 1 | Copyright 2023 The Borel Project Authors (https://github.com/RosaWagner/Borel) 2 | 3 | This Font Software is licensed under the SIL Open Font License, Version 1.1. 4 | This license is copied below, and is also available with a FAQ at: 5 | http://scripts.sil.org/OFL 6 | 7 | 8 | ----------------------------------------------------------- 9 | SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 10 | ----------------------------------------------------------- 11 | 12 | PREAMBLE 13 | The goals of the Open Font License (OFL) are to stimulate worldwide 14 | development of collaborative font projects, to support the font creation 15 | efforts of academic and linguistic communities, and to provide a free and 16 | open framework in which fonts may be shared and improved in partnership 17 | with others. 18 | 19 | The OFL allows the licensed fonts to be used, studied, modified and 20 | redistributed freely as long as they are not sold by themselves. The 21 | fonts, including any derivative works, can be bundled, embedded, 22 | redistributed and/or sold with any software provided that any reserved 23 | names are not used by derivative works. The fonts and derivatives, 24 | however, cannot be released under any other type of license. The 25 | requirement for fonts to remain under this license does not apply 26 | to any document created using the fonts or their derivatives. 27 | 28 | DEFINITIONS 29 | "Font Software" refers to the set of files released by the Copyright 30 | Holder(s) under this license and clearly marked as such. This may 31 | include source files, build scripts and documentation. 32 | 33 | "Reserved Font Name" refers to any names specified as such after the 34 | copyright statement(s). 35 | 36 | "Original Version" refers to the collection of Font Software components as 37 | distributed by the Copyright Holder(s). 38 | 39 | "Modified Version" refers to any derivative made by adding to, deleting, 40 | or substituting -- in part or in whole -- any of the components of the 41 | Original Version, by changing formats or by porting the Font Software to a 42 | new environment. 43 | 44 | "Author" refers to any designer, engineer, programmer, technical 45 | writer or other person who contributed to the Font Software. 46 | 47 | PERMISSION & CONDITIONS 48 | Permission is hereby granted, free of charge, to any person obtaining 49 | a copy of the Font Software, to use, study, copy, merge, embed, modify, 50 | redistribute, and sell modified and unmodified copies of the Font 51 | Software, subject to the following conditions: 52 | 53 | 1) Neither the Font Software nor any of its individual components, 54 | in Original or Modified Versions, may be sold by itself. 55 | 56 | 2) Original or Modified Versions of the Font Software may be bundled, 57 | redistributed and/or sold with any software, provided that each copy 58 | contains the above copyright notice and this license. These can be 59 | included either as stand-alone text files, human-readable headers or 60 | in the appropriate machine-readable metadata fields within text or 61 | binary files as long as those fields can be easily viewed by the user. 62 | 63 | 3) No Modified Version of the Font Software may use the Reserved Font 64 | Name(s) unless explicit written permission is granted by the corresponding 65 | Copyright Holder. This restriction only applies to the primary font name as 66 | presented to the users. 67 | 68 | 4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font 69 | Software shall not be used to promote, endorse or advertise any 70 | Modified Version, except to acknowledge the contribution(s) of the 71 | Copyright Holder(s) and the Author(s) or with their explicit written 72 | permission. 73 | 74 | 5) The Font Software, modified or unmodified, in part or in whole, 75 | must be distributed entirely under this license, and must not be 76 | distributed under any other license. The requirement for fonts to 77 | remain under this license does not apply to any document created 78 | using the Font Software. 79 | 80 | TERMINATION 81 | This license becomes null and void if any of the above conditions are 82 | not met. 83 | 84 | DISCLAIMER 85 | THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 86 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF 87 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT 88 | OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE 89 | COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 90 | INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL 91 | DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 92 | FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM 93 | OTHER DEALINGS IN THE FONT SOFTWARE. 94 | -------------------------------------------------------------------------------- /src/assets/svg/Google_2015_logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 7 | 9 | 10 | 12 | 14 | -------------------------------------------------------------------------------- /src/assets/svg/gemini.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/assets/svg/gemini.ico -------------------------------------------------------------------------------- /src/assets/svg/ios-spinner.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/assets/svg/openai-icon.svg: -------------------------------------------------------------------------------- 1 | 3 | 5 | 6 | -------------------------------------------------------------------------------- /src/auth/auth.tsx: -------------------------------------------------------------------------------- 1 | import {motion} from 'framer-motion' 2 | import React, {useCallback, useEffect, useState} from 'react' 3 | import {AxiosError, AxiosResponse} from "axios" 4 | import {useNavigate} from "react-router-dom" 5 | import {login} from "../api/restful/api.ts" 6 | import {savePassAsHash, setLoggedIn} from "../state/app-state.ts" 7 | import {cx} from "../util/util.tsx" 8 | import {Helmet} from 'react-helmet-async' 9 | import {GranimWallpaper} from "../wallpaper/granim-wallpaper.tsx" 10 | 11 | const detectDelay = 1000 12 | const fadeOutDuration = 1500 13 | 14 | // (0,0) -> (-500,0) -> (500,0) -> ... 15 | const shakeAnimation = { 16 | x: [0, -30, 30, -30, 30, 0], 17 | y: [0, 0, 0, 0, 0, 0], 18 | } 19 | 20 | export default function Auth() { 21 | const navigate = useNavigate() 22 | 23 | const [textLight, setTextLight] = useState(false) 24 | 25 | const [inputValue, setInputValue] = useState('') 26 | const [shake, setShake] = useState(false) 27 | const [startFadeOut, setStartFadeOut] = useState(false) 28 | const [startFadeIn, setStartFadeIn] = useState(false) 29 | 30 | // use this function to detect where password is required by Talk server 31 | const handleLogin = useCallback((detect: boolean, password?: string) => { 32 | setShake(false) 33 | login(password).then((r: AxiosResponse) => { 34 | console.info("login is successful", r.status, r.data) 35 | if (password) { 36 | savePassAsHash(password) 37 | } 38 | setLoggedIn(true) 39 | setStartFadeOut(true) 40 | setTimeout(() => navigate("/chat"), fadeOutDuration) 41 | }).catch((e: AxiosError) => { 42 | console.info("failed to login", e) 43 | if (!detect) { 44 | setInputValue('') 45 | setShake(true) 46 | } 47 | }) 48 | }, [navigate]) 49 | 50 | // detect if login is required 51 | useEffect(() => { 52 | const t = setTimeout(() => handleLogin(true) 53 | , detectDelay) 54 | return () => clearTimeout(t) 55 | }, [handleLogin]) 56 | 57 | const handleSubmit = useCallback((event: React.FormEvent) => { 58 | event.preventDefault() 59 | handleLogin(false, inputValue) 60 | }, [handleLogin, inputValue]) 61 | 62 | 63 | const onDark = useCallback((isDark: boolean) => { 64 | setTextLight(isDark) 65 | }, []) 66 | 67 | useEffect(() => { 68 | setStartFadeIn(true) 69 | }, []) 70 | 71 | return ( 72 | // fadeOutDuration is shorter than duration-2000 to avoid staying in a white page 73 |
74 | 75 | Talk - login 76 | 77 | {/* Add more meta tags as needed */} 78 | 79 |
80 | {} 81 |
83 |

86 | Let's talk 87 |

88 | 93 | {/**/} 94 | { 101 | setInputValue(e.target.value) 102 | setShake(false) 103 | }} 104 | className={cx("appearance-none w-full h-16 rounded-lg outline-none caret-transparent", 105 | "text-6xl text-center tracking-widest bg-white backdrop-blur bg-opacity-10 transition duration-5000", 106 | textLight ? "text-neutral-200" : "text-neutral-800")} 107 | /> 108 | 109 |
110 |
111 |
112 | ) 113 | } 114 | -------------------------------------------------------------------------------- /src/config.ts: -------------------------------------------------------------------------------- 1 | import {currentProtocolHostPortPath, joinUrl} from "./util/util.tsx" 2 | 3 | export const audioPlayerMimeType = 'audio/mpeg' 4 | export const maxLoadedVoice = 6 5 | export const messageTimeoutSeconds = 60 6 | // a recoding which is less than minSpeakTimeMillis should be discarded 7 | export const minSpeakTimeMillis = 500 8 | 9 | export function SSEEndpoint(): string { 10 | const ep = joinUrl(APIEndpoint(), "events") 11 | console.debug("SSEEndpoint:", ep) 12 | return ep 13 | } 14 | 15 | export function APIEndpoint(): string { 16 | let ep = import.meta.env.VITE_REACT_APP_ENDPOINT 17 | if (ep) { 18 | return ep 19 | } 20 | ep = joinUrl(currentProtocolHostPortPath(), "api") 21 | console.debug("RestfulEndpoint:", ep) 22 | return ep 23 | } 24 | 25 | export type RecordingMimeType = { 26 | mimeType: string 27 | fileName: string 28 | } 29 | 30 | export const popularMimeTypes: RecordingMimeType[] = [ 31 | {mimeType: 'audio/webm; codecs=vp9', fileName: "audio.webm"}, 32 | {mimeType: 'audio/webm; codecs=opus', fileName: "audio.webm"}, 33 | {mimeType: 'audio/webm', fileName: "audio.webm"}, 34 | {mimeType: 'audio/mp4', fileName: "audio.mp4"}, 35 | ] 36 | 37 | -------------------------------------------------------------------------------- /src/data-structure/provider-api-refrence/chat-gpt.ts: -------------------------------------------------------------------------------- 1 | import {Choice, FloatRange, IntRange, k} from "./types.ts" 2 | 3 | export type ChatGPTAPIReference = { 4 | readonly maxTokens: IntRange 5 | readonly temperature: FloatRange 6 | readonly topP: FloatRange 7 | readonly presencePenalty: FloatRange 8 | readonly frequencyPenalty: FloatRange 9 | } 10 | 11 | // see https://platform.openai.com/docs/api-reference/chat/create 12 | export const chatGPTAPIReference: ChatGPTAPIReference = { 13 | maxTokens: { 14 | rangeStart: 0, 15 | rangeEnd: Number.MAX_SAFE_INTEGER, 16 | default: k, 17 | }, 18 | temperature: { 19 | rangeStart: 0, 20 | rangeEnd: 2, 21 | default: 1, 22 | }, 23 | topP: { 24 | rangeStart: 0, 25 | rangeEnd: 1, 26 | default: 1, 27 | }, 28 | presencePenalty: { 29 | rangeStart: -2, 30 | rangeEnd: 2, 31 | default: 0, 32 | }, 33 | frequencyPenalty: { 34 | rangeStart: -2, 35 | rangeEnd: 2, 36 | default: 0, 37 | }, 38 | } 39 | 40 | export const attachNumberChoices: Choice[] = [ 41 | {value: 1, name: "1", tags: []}, 42 | {value: 2, name: "2", tags: []}, 43 | {value: 3, name: "3", tags: []}, 44 | {value: 4, name: "4", tags: []}, 45 | {value: 5, name: "5", tags: []}, 46 | {value: 6, name: "6", tags: []}, 47 | {value: 7, name: "7", tags: []}, 48 | {value: 8, name: "8", tags: []}, 49 | {value: 9, name: "9", tags: []}, 50 | {value: 10, name: "10", tags: []}, 51 | {value: 20, name: "20", tags: []}, 52 | {value: 30, name: "30", tags: []}, 53 | {value: 40, name: "40", tags: []}, 54 | {value: 50, name: "50", tags: []}, 55 | {value: 100, name: "100", tags: []}, 56 | {value: Number.MAX_SAFE_INTEGER, name: "∞", tags: []} 57 | ] 58 | 59 | export const tokenChoices: Choice[] = [ 60 | {value: 50, name: "50", tags: []}, 61 | {value: 100, name: "100", tags: []}, 62 | {value: 200, name: "200", tags: []}, 63 | {value: 500, name: "500", tags: []}, 64 | {value: k, name: "1k", tags: []}, 65 | {value: 2 * k, name: "2k", tags: []}, 66 | {value: 4 * k, name: "4k", tags: []}, 67 | {value: 8 * k, name: "8k", tags: []}, 68 | {value: 16 * k, name: "16k", tags: []}, 69 | {value: 32 * k, name: "32k", tags: []}, 70 | {value: 64 * k, name: "64k", tags: []}, 71 | {value: Number.MAX_SAFE_INTEGER, name: "∞", tags: []}, 72 | ] -------------------------------------------------------------------------------- /src/data-structure/provider-api-refrence/elevenlabs-tts.ts: -------------------------------------------------------------------------------- 1 | import {FloatRange} from "./types.ts" 2 | 3 | export type ElevenlabsAPIReference = { 4 | stability: FloatRange 5 | clarity: FloatRange 6 | } 7 | 8 | // see https://platform.openai.com/docs/models/whisper 9 | export const elevenlabsAPIReference: ElevenlabsAPIReference = { 10 | stability: { 11 | rangeStart: 0, 12 | rangeEnd: 1, 13 | default: 0.5, 14 | }, 15 | clarity: { 16 | rangeStart: 0, 17 | rangeEnd: 1, 18 | default: 0.75, 19 | }, 20 | } 21 | -------------------------------------------------------------------------------- /src/data-structure/provider-api-refrence/gemini.ts: -------------------------------------------------------------------------------- 1 | import {Choice, FloatRange, IntRange, k} from "./types.ts" 2 | 3 | export type GeminiAPIReference = { 4 | readonly maxOutputTokens: IntRange 5 | readonly temperature: FloatRange 6 | readonly topP: FloatRange 7 | readonly topK: IntRange 8 | } 9 | 10 | // https://cloud.google.com/vertex-ai/docs/generative-ai/model-reference/gemini 11 | export const geminiAPIReference: GeminiAPIReference = { 12 | maxOutputTokens: { 13 | rangeStart: 1, 14 | rangeEnd: 8 * k, 15 | default: 8 * k, 16 | }, 17 | temperature: { 18 | rangeStart: 0, 19 | rangeEnd: 1, 20 | default: 0.9, 21 | }, 22 | topP: { 23 | rangeStart: 0, 24 | rangeEnd: 1, 25 | default: 1, 26 | }, 27 | topK: { 28 | rangeStart: 1, 29 | rangeEnd: 40, 30 | default: 32, 31 | }, 32 | } 33 | 34 | export const maxOutputTokens: Choice[] = [ 35 | {value: 50, name: "50", tags: []}, 36 | {value: 100, name: "100", tags: []}, 37 | {value: 200, name: "200", tags: []}, 38 | {value: 500, name: "500", tags: []}, 39 | {value: k, name: "1k", tags: []}, 40 | {value: 2 * k, name: "2k", tags: []}, 41 | {value: 4 * k, name: "4k", tags: []}, 42 | {value: 8 * k, name: "8k", tags: []}, 43 | ] -------------------------------------------------------------------------------- /src/data-structure/provider-api-refrence/google-stt.ts: -------------------------------------------------------------------------------- 1 | import {ChooseOne} from "./types.ts" 2 | import {googleTTSLanguageStrings} from "../../data/google-tts-language.ts" 3 | 4 | export type GoogleSTTAPIReference = { 5 | language: ChooseOne 6 | models: ChooseOne 7 | } 8 | 9 | // see https://cloud.google.com/text-to-speech/docs/reference/rest/v1/AudioConfig 10 | export const googleSTTAPIReference: GoogleSTTAPIReference = { 11 | language: { 12 | choices: googleTTSLanguageStrings, 13 | default: { 14 | value: "en-GB", 15 | name: "English (Great Britain)", 16 | tags: [] 17 | }, 18 | }, 19 | models: { 20 | choices: [ 21 | { 22 | name: "short", 23 | value: "short", 24 | tags: [] 25 | }, 26 | { 27 | name: "long", 28 | value: "long", 29 | tags: [] 30 | }, 31 | { 32 | name: "chirp", 33 | value: "chirp", 34 | tags: [] 35 | }, 36 | { 37 | name: "telephony", 38 | value: "telephony", 39 | tags: [] 40 | }, 41 | { 42 | name: "medical_dictation", 43 | value: "medical_dictation", 44 | tags: [] 45 | }, 46 | { 47 | name: "medical_conversation", 48 | value: "medical_conversation", 49 | tags: [] 50 | }, 51 | ], 52 | default: { 53 | name: "short", 54 | value: "short", 55 | tags: [] 56 | }, 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/data-structure/provider-api-refrence/google-tts.ts: -------------------------------------------------------------------------------- 1 | import {ChooseOne, FloatRange} from "./types.ts" 2 | import {GoogleTTSGender} from "../../api/restful/model.ts" 3 | import {googleTTSLanguageStrings} from "../../data/google-tts-language.ts" 4 | 5 | export type GoogleTTSAPIReference = { 6 | language: ChooseOne 7 | gender: ChooseOne 8 | speakingRate: FloatRange 9 | pitch: FloatRange 10 | volumeGainDb: FloatRange 11 | } 12 | 13 | // see https://cloud.google.com/text-to-speech/docs/reference/rest/v1/AudioConfig 14 | export const googleTTSAPIReference: GoogleTTSAPIReference = { 15 | language: { 16 | choices: googleTTSLanguageStrings, 17 | default: { 18 | value: "en-GB", 19 | name: "English (Great Britain)", 20 | tags: [] 21 | }, 22 | }, 23 | gender: { 24 | choices: [ 25 | { 26 | name: "unspecified", 27 | value: GoogleTTSGender.unspecified, 28 | tags: [] 29 | }, 30 | { 31 | name: "male", 32 | value: GoogleTTSGender.male, 33 | tags: [] 34 | }, 35 | { 36 | name: "female", 37 | value: GoogleTTSGender.female, 38 | tags: [] 39 | }, 40 | { 41 | name: "neutral", 42 | value: GoogleTTSGender.neutral, 43 | tags: [] 44 | }, 45 | ], 46 | default: { 47 | name: "unspecified", 48 | value: GoogleTTSGender.unspecified, 49 | tags: [] 50 | }, 51 | }, 52 | speakingRate: { 53 | rangeStart: 0.25, 54 | rangeEnd: 4, 55 | default: 1, 56 | }, 57 | pitch: { 58 | rangeStart: -20, 59 | rangeEnd: 20, 60 | default: 0, 61 | }, 62 | volumeGainDb: { 63 | rangeStart: -96.0, 64 | rangeEnd: 16.0, 65 | default: 0.0, 66 | }, 67 | } -------------------------------------------------------------------------------- /src/data-structure/provider-api-refrence/llm.ts: -------------------------------------------------------------------------------- 1 | import {IntRange} from "./types.ts" 2 | 3 | export type LLMAPIReference = { 4 | readonly maxAttached: IntRange 5 | } 6 | 7 | export const llmAPIReference: LLMAPIReference = { 8 | maxAttached: { 9 | rangeStart: 0, 10 | rangeEnd: Number.MAX_SAFE_INTEGER, 11 | default: 4, 12 | }, 13 | } -------------------------------------------------------------------------------- /src/data-structure/provider-api-refrence/types.ts: -------------------------------------------------------------------------------- 1 | export const k = 1024 2 | 3 | export interface IntRange { 4 | readonly rangeStart: number 5 | readonly rangeEnd: number 6 | readonly default: number 7 | } 8 | 9 | export interface FloatRange { 10 | readonly rangeStart: number 11 | readonly rangeEnd: number 12 | readonly default: number 13 | } 14 | 15 | export interface ChooseOne { 16 | readonly choices: Choice[] 17 | readonly default: Choice 18 | } 19 | 20 | export interface Choice { 21 | readonly name: string 22 | readonly value: T 23 | readonly tags: string[] 24 | } 25 | 26 | export const emptyStringChoice: Choice = { 27 | name: "", 28 | value: "", 29 | tags: [] 30 | } -------------------------------------------------------------------------------- /src/data-structure/provider-api-refrence/whisper.ts: -------------------------------------------------------------------------------- 1 | import {ChooseOne} from "./types.ts" 2 | 3 | export type WhisperAPIReference = { 4 | models: ChooseOne 5 | } 6 | 7 | // see https://platform.openai.com/docs/api-reference/audio/createTranscription 8 | export const whisperAPIReference = (): WhisperAPIReference => { 9 | return { 10 | models: { 11 | choices: [{name: "whisper-1", value: "whisper-1", tags: []}], 12 | default: {name: "whisper-1", value: "whisper-1", tags: []} 13 | } 14 | } 15 | } -------------------------------------------------------------------------------- /src/data/chat.ts: -------------------------------------------------------------------------------- 1 | import {newReceived, newSent} from "../data-structure/message.tsx"; 2 | import {appState, createChat} from "../state/app-state.ts"; 3 | 4 | export const createDemoChatIfNecessary = () => { 5 | if (appState.ability.demo && appState.chats.length === 0 && !appState.pref.dismissDemo) { 6 | createChat("Demo", [ 7 | newSent("Hello!"), 8 | newReceived(`Hello! How may I assist you today? 9 | 10 | Feel free to ask me anything. Please note, I’ll reply with **pseudo** text and voice. 11 | 12 | For genuine AI responses, you may want to set up your own instance following the instructions at 13 | [proxoar/talk](https://github.com/proxoar/talk).`), 14 | ]) 15 | } 16 | } -------------------------------------------------------------------------------- /src/data/google-tts-language.ts: -------------------------------------------------------------------------------- 1 | import {googleSTTLanguageSettings} from "./google-sst-language.ts" 2 | 3 | // see https://www.gstatic.com/cloud-site-ux/text_to_speech/text_to_speech.min.js 4 | // after comparing line by line, we found that googleSTTLanguageSettings and googleTTSLanguageStrings are identical 5 | // at the moment(2023-09-11 10:33:52 Mon) 6 | export const googleTTSLanguageStrings = googleSTTLanguageSettings -------------------------------------------------------------------------------- /src/error.tsx: -------------------------------------------------------------------------------- 1 | import {useCallback, useState} from 'react' 2 | import {useNavigate, useRouteError} from "react-router-dom" 3 | import {CountDownButton, ResetButton} from "./home/panel/shared/widget/button.tsx" 4 | import {IoRefreshSharp} from "react-icons/io5" 5 | import {BsTrash3} from "react-icons/bs" 6 | import {cx} from "./util/util.tsx" 7 | import {Helmet} from 'react-helmet-async' 8 | import * as packageJson from '../package.json' 9 | import {GranimWallpaper} from "./wallpaper/granim-wallpaper.tsx" 10 | import {clearMessages, currentChatProxy} from "./state/app-state.ts"; 11 | import {clearChats, clearSettings} from "./state/dangerous.ts"; 12 | 13 | export default function Error() { 14 | 15 | const [textLight, setTextLight] = useState(false) 16 | const navigate = useNavigate() 17 | const error = useRouteError() as Record 18 | 19 | const onDark = useCallback((isDark: boolean) => { 20 | setTextLight(isDark) 21 | }, []) 22 | 23 | return ( 24 |
25 | 26 | Talk - Error 27 | 28 | 29 | 30 |
33 | 34 |
36 |

We're sorry that something went wrong.

37 |

Here is something you can try to fix it.

38 |

Try them one by one until back to normal.

39 |
40 | navigate("/")} 44 | icon={} 45 | /> 46 | } 51 | action={() => { 52 | const chat = currentChatProxy() 53 | if (chat) { 54 | clearMessages(chat) 55 | } 56 | navigate("/") 57 | }} 58 | /> 59 | } 64 | action={() => { 65 | clearChats() 66 | navigate("/") 67 | }} 68 | /> 69 | } 74 | action={() => { 75 | clearSettings() 76 | navigate("/") 77 | }} 78 | /> 79 | 80 |
81 |
84 |
85 |

Version

86 |

{packageJson.version}

87 |
88 |
89 |

Status

90 |

{error?.['statusCode'] ?? "None"} {error?.['statusText'] ?? ""}

91 |
92 |
93 |

Error Message

94 |

{error?.['message'] ?? "None"}

95 |
96 |
97 |

Stack trace

98 |

{error?.['stack'] ?? "None"}

99 |
100 | {error?.['stack'] && 101 | Report this on GitHub 106 | } 107 |
108 |
109 | ) 110 | } -------------------------------------------------------------------------------- /src/experiment/experiment.tsx: -------------------------------------------------------------------------------- 1 | import {cx, getRandomElement} from "../util/util.tsx" 2 | import React from "react" 3 | import {Art, wikiarts} from "../wallpaper/art.tsx" 4 | 5 | 6 | export const Experiment = () => { 7 | const art = getRandomElement(...wikiarts) 8 | return ( 9 | 10 | ) 11 | } 12 | 13 | const Wallpaper: React.FC = ({ 14 | author, 15 | name, 16 | date, 17 | imageUrl, 18 | pageUrl, 19 | imageClassName = "bg-cover bg-center blur brightness-75", 20 | noiseClassname = "opacity-80 brightness-100" 21 | }) => { 22 | return ( 23 |
24 |
29 |
30 |
32 |
33 | 34 |

{name}

36 |
37 |
39 |

{author},

40 |

{date}

41 |
42 |
43 |
44 | ) 45 | } -------------------------------------------------------------------------------- /src/experiment/subscribe-test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import {proxy} from "valtio"; 3 | import {subscribeKey} from "valtio/utils"; 4 | 5 | interface MState { 6 | loadAudio: boolean 7 | isAttached: boolean 8 | } 9 | 10 | interface MessageState { 11 | record: Record 12 | } 13 | 14 | const messageState = proxy({ 15 | record: {} 16 | }) 17 | 18 | const clear = () => { 19 | const keys = Object.keys(messageState.record); 20 | for (const key of keys) { 21 | delete messageState.record[key] 22 | } 23 | } 24 | subscribeKey(messageState.record, "a", () => { 25 | console.log("messageState.state.a changed:", messageState.record["a"]) 26 | }) 27 | 28 | export const Greeting: React.FC = () => { 29 | return ( 30 |
31 |

{ 32 | messageState.record["a"].loadAudio = true 33 | }}> 34 | set a 35 |

36 |

{ 37 | delete messageState.record["a"] 38 | }}> 39 | delete a 40 |

41 |

{ 42 | clear() 43 | }}> 44 | clear 45 |

46 |
47 | ) 48 | } 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/favicon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codegenius2/Talk-web/17eaef99bc62a8259c47b81627a8da2629f9dbb8/src/favicon.png -------------------------------------------------------------------------------- /src/home/chat-window/attached/attached-item.tsx: -------------------------------------------------------------------------------- 1 | import React, {useEffect, useState} from "react" 2 | import {LLMMessage} from "../../../shared-types.ts"; 3 | import {cx} from "../../../util/util.tsx"; 4 | import {userColor, assistantColor} from "../compnent/theme.ts"; 5 | import {LiaEllipsisHSolid} from "react-icons/lia"; 6 | import {useSnapshot} from "valtio/react"; 7 | import {layoutState} from "../../../state/layout-state.ts"; 8 | 9 | type Props = { 10 | message: LLMMessage 11 | } 12 | 13 | export const AttachedItem: React.FC = ({message}) => { 14 | 15 | const [fullText, setFullText] = useState(false) 16 | const {isPAPinning, isPAFloating} = useSnapshot(layoutState) 17 | useEffect(() => { 18 | setFullText(false) 19 | }, [isPAPinning, isPAFloating]); 20 | let theme 21 | switch (message.role) { 22 | case "user": 23 | theme = userTheme 24 | break; 25 | case "assistant": 26 | theme = assistantTheme 27 | break; 28 | case "system": 29 | theme = systemTheme 30 | break; 31 | } 32 | 33 | return ( 34 |
35 |
setFullText(true)} 37 | className={cx("flex rounded-xl whitespace-pre-wrap px-3 pt-0.5 pb-0.5", 38 | theme.bg, theme.text, theme.other 39 | )}> 40 | 41 | {fullText && 42 |
{message.content}
43 | } 44 | {!fullText && 45 |
46 | {message.content.slice(0, 100)} 47 | {message.content.length > 100 && } 48 |
} 49 |
50 |
51 | 52 | ) 53 | } 54 | 55 | const userTheme = { 56 | bg: userColor.bg, 57 | text: userColor.text, 58 | other: "max-w-[80%] self-end", 59 | } 60 | 61 | const assistantTheme = { 62 | bg: assistantColor.bg, 63 | text: assistantColor.text, 64 | other: "max-w-[80%] self-start", 65 | } 66 | 67 | const systemTheme = { 68 | bg: "bg-fuchsia-600 bg-opacity-80 backdrop-blur", 69 | text: "text-violet-100", 70 | other: "max-w-[80%] self-center", 71 | } 72 | -------------------------------------------------------------------------------- /src/home/chat-window/chat-window.tsx: -------------------------------------------------------------------------------- 1 | import {appState, Chat, currentChatProxy} from "../../state/app-state.ts" 2 | import TextArea from "./text-area.tsx" 3 | import Recorder from "./recorder.tsx" 4 | import React, {useEffect, useRef, useState} from "react" 5 | import {subscribeKey} from "valtio/utils" 6 | import {MessageList} from "./message-list/message-list.tsx" 7 | import {PromptAttached} from "./prompt-attached.tsx" 8 | import {cx} from "../../util/util.tsx" 9 | import {useSnapshot} from "valtio/react" 10 | import {layoutState} from "../../state/layout-state.ts" 11 | import {throttle} from "lodash" 12 | import {PromptAttachedButton} from "./compnent/prompt-attached-button.tsx" 13 | 14 | 15 | export const ChatWindow: React.FC = () => { 16 | 17 | const [chatProxy, setChatProxy] = useState(undefined) 18 | // console.info("ChatWindow rendered", new Date().toLocaleString()) 19 | const buttonRef = useRef(null) 20 | 21 | const {isPAFloating, isPAPinning} = useSnapshot(layoutState) 22 | const {showRecorder} = useSnapshot(appState.pref) 23 | 24 | useEffect(() => { 25 | const callback = () => { 26 | const cp = currentChatProxy() 27 | setChatProxy(cp) 28 | } 29 | const unsubscribe = subscribeKey(appState, "currentChatId", callback) 30 | callback() 31 | return unsubscribe 32 | }, []) 33 | 34 | 35 | const handleMouseMove = throttle((event: React.MouseEvent) => { 36 | if (buttonRef.current) { 37 | const rect = buttonRef.current.getBoundingClientRect() 38 | const [x, y] = [(rect.left + 18), (rect.top + 18)] 39 | layoutState.PAButtonDistance = Math.hypot(x - event.clientX, y - event.clientY) 40 | } 41 | }, 50) 42 | 43 | return ( 44 |
50 | {chatProxy !== undefined && 51 | <> 52 |
53 | 54 |
55 |
62 | 63 |
64 | 65 |
67 | <> 68 | 69 |
72 |