├── src ├── renderer │ ├── src │ │ ├── components │ │ │ ├── canvas │ │ │ │ ├── canvas.tsx │ │ │ │ ├── subtitle.tsx │ │ │ │ ├── ws-status.tsx │ │ │ │ ├── background.tsx │ │ │ │ └── canvas-styles.tsx │ │ │ ├── sidebar │ │ │ │ ├── invite-dialog.tsx │ │ │ │ ├── setting │ │ │ │ │ ├── tts.tsx │ │ │ │ │ ├── agent.tsx │ │ │ │ │ ├── live2d.tsx │ │ │ │ │ ├── about.tsx │ │ │ │ │ └── asr.tsx │ │ │ │ ├── bottom-tab.tsx │ │ │ │ ├── chat-bubble.tsx │ │ │ │ ├── browser-panel.tsx │ │ │ │ ├── screen-panel.tsx │ │ │ │ └── camera-panel.tsx │ │ │ ├── ui │ │ │ │ ├── provider.tsx │ │ │ │ ├── close-button.tsx │ │ │ │ ├── number-input.tsx │ │ │ │ ├── radio.tsx │ │ │ │ ├── checkbox.tsx │ │ │ │ ├── field.tsx │ │ │ │ ├── tag.tsx │ │ │ │ ├── button.tsx │ │ │ │ ├── toaster.tsx │ │ │ │ ├── tooltip.tsx │ │ │ │ ├── switch.tsx │ │ │ │ ├── alert.tsx │ │ │ │ ├── input-group.tsx │ │ │ │ ├── popover.tsx │ │ │ │ ├── drawer.tsx │ │ │ │ ├── dialog.tsx │ │ │ │ ├── color-mode.tsx │ │ │ │ ├── avatar.tsx │ │ │ │ ├── slider.tsx │ │ │ │ └── clipboard.tsx │ │ │ ├── footer │ │ │ │ ├── ai-state-indicator.tsx │ │ │ │ └── footer-styles.tsx │ │ │ ├── electron │ │ │ │ ├── electron-style.tsx │ │ │ │ └── title-bar.tsx │ │ │ └── phone-call │ │ │ │ └── phone-call-styles.ts │ │ ├── hooks │ │ │ ├── sidebar │ │ │ │ ├── use-invite-dialog.ts │ │ │ │ ├── use-chat-history-panel.ts │ │ │ │ ├── use-capture-screen.ts │ │ │ │ ├── use-camera-panel.ts │ │ │ │ ├── use-sidebar.ts │ │ │ │ ├── setting │ │ │ │ │ ├── use-live2d-settings.ts │ │ │ │ │ ├── use-agent-settings.ts │ │ │ │ │ └── use-asr-settings.ts │ │ │ │ ├── use-group-drawer.tsx │ │ │ │ └── use-history-drawer.ts │ │ │ ├── canvas │ │ │ │ ├── use-background.ts │ │ │ │ ├── use-subtitle-display.ts │ │ │ │ ├── use-ws-status.ts │ │ │ │ └── use-live2d-expression.ts │ │ │ ├── utils │ │ │ │ ├── use-mic-toggle.ts │ │ │ │ ├── use-trigger-speak.ts │ │ │ │ ├── use-force-ignore-mouse.ts │ │ │ │ ├── use-local-storage.ts │ │ │ │ ├── use-send-audio.tsx │ │ │ │ ├── use-interrupt.ts │ │ │ │ └── use-switch-character.tsx │ │ │ ├── footer │ │ │ │ ├── use-footer.ts │ │ │ │ └── use-text-input.tsx │ │ │ └── electron │ │ │ │ ├── use-input-subtitle.ts │ │ │ │ └── use-draggable.ts │ │ ├── vite-env.d.ts │ │ ├── env.d.ts │ │ ├── electron.d.ts │ │ ├── index.css │ │ ├── context │ │ │ ├── group-context.tsx │ │ │ ├── browser-context.tsx │ │ │ ├── subtitle-context.tsx │ │ │ ├── character-config-context.tsx │ │ │ ├── websocket-context.tsx │ │ │ ├── proactive-speak-context.tsx │ │ │ ├── screen-capture-context.tsx │ │ │ └── mode-context.tsx │ │ ├── i18n.ts │ │ ├── utils │ │ │ ├── task-queue.ts │ │ │ └── audio-manager.ts │ │ └── main.tsx │ ├── WebSDK │ │ ├── Framework │ │ │ ├── .gitattributes │ │ │ ├── .vscode │ │ │ │ ├── settings.json │ │ │ │ ├── extensions.json │ │ │ │ └── tasks.json │ │ │ ├── .editorconfig │ │ │ ├── .gitignore │ │ │ ├── package.json │ │ │ ├── tsconfig.json │ │ │ ├── src │ │ │ │ ├── cubismframeworkconfig.ts │ │ │ │ ├── icubismallcator.ts │ │ │ │ ├── type │ │ │ │ │ ├── csmrectf.ts │ │ │ │ │ └── csmstring.ts │ │ │ │ ├── id │ │ │ │ │ ├── cubismid.ts │ │ │ │ │ └── cubismidmanager.ts │ │ │ │ ├── model │ │ │ │ │ └── cubismmodeluserdatajson.ts │ │ │ │ ├── utils │ │ │ │ │ ├── cubismjsonextension.ts │ │ │ │ │ └── cubismstring.ts │ │ │ │ └── motion │ │ │ │ │ └── cubismmotionmanager.ts │ │ │ ├── LICENSE.md │ │ │ └── .eslintrc.yml │ │ ├── Core │ │ │ ├── RedistributableFiles.txt │ │ │ ├── LICENSE.md │ │ │ ├── README.ja.md │ │ │ └── README.md │ │ └── src │ │ │ ├── lappglmanager.ts │ │ │ ├── lapppal.ts │ │ │ └── lappdefine.ts │ ├── public │ │ └── favicon.ico │ └── index.html └── preload │ └── index.d.ts ├── .gitattributes ├── resources ├── icon.icns ├── icon.ico └── icon.png ├── .eslintignore ├── tsconfig.json ├── README.md ├── tsconfig.web.json ├── tsconfig.app.json ├── tsconfig.node.json ├── .gitignore ├── .eslintrc.js ├── i18next-scanner.config.js ├── docs └── i18n-usage.md ├── electron-builder.yml ├── LICENSE ├── electron.vite.config.ts ├── vite.config.ts ├── package.json └── CLAUDE.md /src/renderer/src/components/canvas/canvas.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/src/components/sidebar/invite-dialog.tsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/sidebar/use-invite-dialog.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=lf 2 | -------------------------------------------------------------------------------- /src/renderer/src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /resources/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/Open-LLM-VTuber-Web/main/resources/icon.icns -------------------------------------------------------------------------------- /resources/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/Open-LLM-VTuber-Web/main/resources/icon.ico -------------------------------------------------------------------------------- /resources/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/Open-LLM-VTuber-Web/main/resources/icon.png -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | src/renderer/WebSDK/**/*.ts 2 | src/renderer/MotionSync/**/*.js 3 | src/renderer/MotionSync/**/*.ts -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "typescript.tsdk": "./node_modules/typescript/lib" 3 | } 4 | -------------------------------------------------------------------------------- /src/renderer/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/K-Jadeja/Open-LLM-VTuber-Web/main/src/renderer/public/favicon.ico -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "editorconfig.editorconfig" 4 | ] 5 | } 6 | -------------------------------------------------------------------------------- /src/renderer/src/components/sidebar/setting/tts.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/react'; 2 | 3 | function TTS(): JSX.Element { 4 | return ; 5 | } 6 | 7 | export default TTS; 8 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | end_of_line = lf 6 | indent_size = 2 7 | indent_style = space 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /src/renderer/src/env.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | api?: { 3 | setIgnoreMouseEvents: (ignore: boolean) => void 4 | showContextMenu?: () => void 5 | onModeChanged: (callback: (mode: string) => void) => void 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Core/RedistributableFiles.txt: -------------------------------------------------------------------------------- 1 | The following is a list of files available for redistribution 2 | under the terms of the Live2D Proprietary Software License Agreement: 3 | 4 | - live2dcubismcore.d.ts 5 | - live2dcubismcore.js 6 | - live2dcubismcore.min.js 7 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Open-LLM-Vtuber 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/.gitignore: -------------------------------------------------------------------------------- 1 | # Package files 2 | node_modules/ 3 | # Build files 4 | dist/ 5 | # Other files 6 | .vs/ 7 | .idea/ 8 | *.iml 9 | .DS_Store 10 | # Exclude some VSCode setting files. 11 | .vscode/* 12 | !/.vscode/extensions.json 13 | !/.vscode/settings.json 14 | !/.vscode/tasks.json 15 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [{ "path": "./tsconfig.node.json" }, { "path": "./tsconfig.web.json" }], 4 | "compilerOptions": { 5 | "noImplicitAny": false, 6 | "strictPropertyInitialization": false, 7 | "strict": false 8 | }, 9 | "exclude": [ 10 | 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/provider.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { ChakraProvider, defaultSystem } from '@chakra-ui/react'; 4 | import { 5 | ColorModeProvider, 6 | type ColorModeProviderProps, 7 | } from './color-mode'; 8 | 9 | export function Provider(props: ColorModeProviderProps) { 10 | return ( 11 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/canvas/use-background.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useBgUrl } from '@/context/bgurl-context'; 3 | 4 | export const useBackground = () => { 5 | const context = useBgUrl(); 6 | 7 | const backgroundUrl = useMemo(() => { 8 | if (!context) return null; 9 | return context.backgroundUrl; 10 | }, [context?.backgroundUrl]); 11 | 12 | return { 13 | backgroundUrl, 14 | isLoaded: !!context, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/canvas/use-subtitle-display.ts: -------------------------------------------------------------------------------- 1 | import { useMemo } from 'react'; 2 | import { useSubtitle } from '@/context/subtitle-context'; 3 | 4 | export const useSubtitleDisplay = () => { 5 | const context = useSubtitle(); 6 | 7 | const subtitleText = useMemo(() => { 8 | if (!context) return null; 9 | return context.subtitleText; 10 | }, [context?.subtitleText]); 11 | 12 | return { 13 | subtitleText, 14 | isLoaded: !!context, 15 | }; 16 | }; 17 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Core/LICENSE.md: -------------------------------------------------------------------------------- 1 | ## Live2D Proprietary Software License 2 | 3 | Live2D Cubism Core is available under Live2D Proprietary Software License. 4 | 5 | * [Live2D Proprietary Software License Agreement](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html) 6 | * [Live2D Proprietary Software 使用許諾契約書](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_jp.html) 7 | * [Live2D Proprietary Software 使用授权协议](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_cn.html) 8 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/close-button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps } from '@chakra-ui/react'; 2 | import { IconButton as ChakraIconButton } from '@chakra-ui/react'; 3 | import * as React from 'react'; 4 | import { LuX } from 'react-icons/lu'; 5 | 6 | export type CloseButtonProps = ButtonProps 7 | 8 | export const CloseButton = React.forwardRef< 9 | HTMLButtonElement, 10 | CloseButtonProps 11 | >((props, ref) => ( 12 | 13 | {props.children ?? } 14 | 15 | )); 16 | -------------------------------------------------------------------------------- /src/renderer/src/electron.d.ts: -------------------------------------------------------------------------------- 1 | import { IpcRenderer } from 'electron'; 2 | 3 | declare global { 4 | interface Window { 5 | // Define the structure of the API exposed by your preload script 6 | electron?: { 7 | ipcRenderer: IpcRenderer; 8 | process: { 9 | platform: string; 10 | }; 11 | // Add other methods or properties exposed by preload script if any 12 | }; 13 | // Add other custom window properties if needed 14 | api?: unknown; // Keep existing check if needed 15 | } 16 | } 17 | 18 | // Export {} is needed to make this a module 19 | export {}; 20 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "tsc", 5 | "test": "tsc --noEmit", 6 | "lint": "eslint src --ext .ts", 7 | "lint:fix": "eslint src --ext .ts --fix", 8 | "clean": "rimraf dist" 9 | }, 10 | "devDependencies": { 11 | "@typescript-eslint/eslint-plugin": "^7.2.0", 12 | "@typescript-eslint/parser": "^7.2.0", 13 | "eslint": "^8.57.0", 14 | "eslint-config-prettier": "^9.1.0", 15 | "eslint-plugin-prettier": "^5.1.3", 16 | "prettier": "^3.2.5", 17 | "rimraf": "^5.0.5", 18 | "typescript": "^5.4.2" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/sidebar/use-chat-history-panel.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useCallback } from 'react'; 2 | import { useChatHistory } from '@/context/chat-history-context'; 3 | 4 | export function useChatHistoryPanel() { 5 | const { messages } = useChatHistory(); 6 | const messageListRef = useRef(null); 7 | 8 | const handleMessageUpdate = useCallback(() => { 9 | if (messageListRef.current) { 10 | messageListRef.current.scrollTop = messageListRef.current.scrollHeight; 11 | } 12 | }, []); 13 | 14 | return { 15 | messages, 16 | messageListRef, 17 | handleMessageUpdate, 18 | }; 19 | } 20 | -------------------------------------------------------------------------------- /src/renderer/src/components/footer/ai-state-indicator.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from '@chakra-ui/react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useAiState } from '@/context/ai-state-context'; 4 | import { footerStyles } from './footer-styles'; 5 | 6 | function AIStateIndicator(): JSX.Element { 7 | const { t } = useTranslation(); 8 | const { aiState } = useAiState(); 9 | const styles = footerStyles.aiIndicator; 10 | 11 | return ( 12 | 13 | {t(`aiState.${aiState}`)} 14 | 15 | ); 16 | } 17 | 18 | export default AIStateIndicator; 19 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/utils/use-mic-toggle.ts: -------------------------------------------------------------------------------- 1 | import { useVAD } from '@/context/vad-context'; 2 | import { useAiState } from '@/context/ai-state-context'; 3 | 4 | export function useMicToggle() { 5 | const { startMic, stopMic, micOn } = useVAD(); 6 | const { aiState, setAiState } = useAiState(); 7 | 8 | const handleMicToggle = async (): Promise => { 9 | if (micOn) { 10 | stopMic(); 11 | if (aiState === 'listening') { 12 | setAiState('idle'); 13 | } 14 | } else { 15 | await startMic(); 16 | } 17 | }; 18 | 19 | return { 20 | handleMicToggle, 21 | micOn, 22 | }; 23 | } 24 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "moduleResolution": "node", 5 | "esModuleInterop": true, 6 | "experimentalDecorators": true, 7 | "forceConsistentCasingInFileNames": true, 8 | "outDir": "./dist", 9 | "declaration": true, 10 | "declarationMap": true, 11 | "sourceMap": true, 12 | "emitDecoratorMetadata": true, 13 | "noImplicitAny": true, 14 | "useUnknownInCatchVariables": true 15 | }, 16 | "include": [ 17 | "src/**/*.ts", 18 | "../Core/*.ts" 19 | ], 20 | "exclude": [ 21 | "node_modules", 22 | "dist" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Open LLM Vtuber 2 | 3 | An Electron application with React and TypeScript 4 | 5 | ## Recommended IDE Setup 6 | 7 | - [VSCode](https://code.visualstudio.com/) + [ESLint](https://marketplace.visualstudio.com/items?itemName=dbaeumer.vscode-eslint) + [Prettier](https://marketplace.visualstudio.com/items?itemName=esbenp.prettier-vscode) 8 | 9 | ## Project Setup 10 | 11 | ### Install 12 | 13 | ```bash 14 | $ npm install 15 | ``` 16 | 17 | ### Development 18 | 19 | ```bash 20 | $ npm run dev 21 | ``` 22 | 23 | ### Build 24 | 25 | ```bash 26 | # For windows 27 | $ npm run build:win 28 | 29 | # For macOS 30 | $ npm run build:mac 31 | 32 | # For Linux 33 | $ npm run build:linux 34 | ``` 35 | -------------------------------------------------------------------------------- /tsconfig.web.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | "composite": true, 5 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.web.tsbuildinfo", 6 | "baseUrl": ".", 7 | "paths": { 8 | "@/*": ["src/renderer/src/*"], 9 | "@framework/*": ["src/renderer/WebSDK/Framework/src/*"], 10 | "@cubismsdksamples/*": ["src/renderer/WebSDK/src/*"], 11 | "@motionsyncframework/*": ["src/renderer/WebSDK/src/Framework/src/*"], 12 | "@motionsync/*": ["src/renderer/MotionSync/motionsync/*"] 13 | } 14 | }, 15 | "include": [ 16 | "src/renderer/**/*.ts", 17 | "src/renderer/**/*.tsx", 18 | "src/renderer/**/*.d.ts", 19 | "src/renderer/WebSDK/src/**/*" 20 | ] 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/utils/use-trigger-speak.ts: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useWebSocket } from '@/context/websocket-context'; 3 | import { useMediaCapture } from './use-media-capture'; 4 | 5 | export function useTriggerSpeak() { 6 | const { sendMessage } = useWebSocket(); 7 | const { captureAllMedia } = useMediaCapture(); 8 | 9 | const sendTriggerSignal = useCallback( 10 | async (actualIdleTime: number) => { 11 | const images = await captureAllMedia(); 12 | sendMessage({ 13 | type: "ai-speak-signal", 14 | idle_time: actualIdleTime, 15 | images, 16 | }); 17 | }, 18 | [sendMessage, captureAllMedia], 19 | ); 20 | 21 | return { 22 | sendTriggerSignal, 23 | }; 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Core/README.ja.md: -------------------------------------------------------------------------------- 1 | [English](README.md) / [日本語](README.ja.md) 2 | 3 | --- 4 | 5 | # Live2D Cubism Core 6 | 7 | このフォルダーには、JavaScriptまたはTypeScriptアプリケーションを開発するためのコアライブラリファイルが含まれています。 8 | 9 | 10 | ## ファイルリスト 11 | 12 | ### live2dcubismcore.d.ts 13 | 14 | このファイルには、`live2dcubismcore.js`に関するTypeScriptの型情報が含まれています。 15 | TypeScriptで開発する場合は、このファイルを`live2dcubismcore.js`とともに使用してください。 16 | 17 | ### live2dcubismcore.js 18 | 19 | このファイルには、CubismCoreの機能といくつかのラッパーが含まれています。 20 | JavaScriptで開発する場合は、このファイルを使用してください。 21 | 22 | ### live2dcubismcore.js.map 23 | 24 | このファイルは、`live2dcubismcore.d.ts`と`live2dcubismcore.js`の間のソースマップです。 25 | デバッグ時にこのファイルを使用します。 26 | 27 | ### live2dcubismcore.min.js 28 | 29 | このファイルは、`live2dcubismcore.js`のminify版です。 30 | このファイルを本番環境で使用します。 31 | -------------------------------------------------------------------------------- /src/renderer/src/index.css: -------------------------------------------------------------------------------- 1 | body, html { 2 | margin: 0; 3 | padding: 0; 4 | overflow: visible; 5 | background-color: transparent; 6 | } 7 | 8 | /* Phone Call UI Animations */ 9 | @keyframes pulse { 10 | 0%, 100% { 11 | opacity: 1; 12 | } 13 | 50% { 14 | opacity: 0.5; 15 | } 16 | } 17 | 18 | /* Mobile viewport adjustments */ 19 | @supports(env(safe-area-inset-bottom)) { 20 | .phone-call-controls { 21 | padding-bottom: calc(2rem + env(safe-area-inset-bottom)); 22 | } 23 | } 24 | 25 | /* Mobile-first responsive design */ 26 | @media (max-width: 768px) { 27 | body { 28 | user-select: none; 29 | -webkit-user-select: none; 30 | -webkit-touch-callout: none; 31 | -webkit-tap-highlight-color: transparent; 32 | } 33 | } 34 | 35 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/.vscode/tasks.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": "2.0.0", 3 | "tasks": [ 4 | { 5 | "type": "npm", 6 | "script": "install", 7 | "problemMatcher": [] 8 | }, 9 | { 10 | "type": "npm", 11 | "script": "build", 12 | "group": "build", 13 | "problemMatcher": [] 14 | }, 15 | { 16 | "type": "npm", 17 | "script": "test", 18 | "group": "test", 19 | "problemMatcher": [] 20 | }, 21 | { 22 | "type": "npm", 23 | "script": "lint", 24 | "group": "test", 25 | "problemMatcher": [] 26 | }, 27 | { 28 | "type": "npm", 29 | "script": "lint:fix", 30 | "group": "test", 31 | "problemMatcher": [] 32 | }, 33 | { 34 | "type": "npm", 35 | "script": "clear", 36 | "problemMatcher": [] 37 | } 38 | ] 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Core/README.md: -------------------------------------------------------------------------------- 1 | [English](README.md) / [日本語](README.ja.md) 2 | 3 | --- 4 | 5 | # Live2D Cubism Core 6 | 7 | This folder contains core library files for developing JavaScript or TypeScript applications. 8 | 9 | 10 | ## File List 11 | 12 | ### live2dcubismcore.d.ts 13 | 14 | This file contains typescript type information about `live2dcubismcore.js`. 15 | Use this file with `live2dcubismcore.js` when developing with TypeScript. 16 | 17 | ### live2dcubismcore.js 18 | 19 | This file contains Cubism Core features and some wrapper features. 20 | Use this file when developing with JavaScript. 21 | 22 | ### live2dcubismcore.js.map 23 | 24 | This file is the source map between `live2dcubismcore.d.ts` and `live2dcubismcore.js`. 25 | Use this file when debugging. 26 | 27 | ### live2dcubismcore.min.js 28 | 29 | This file is the minified version of `live2dcubismcore.js`. 30 | Use this file in production. 31 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", 5 | "target": "ES2020", 6 | "useDefineForClassFields": true, 7 | "lib": [ 8 | "ES2020", 9 | "DOM", 10 | "DOM.Iterable" 11 | ], 12 | "module": "ESNext", 13 | "skipLibCheck": true, 14 | /* Bundler mode */ 15 | "moduleResolution": "bundler", 16 | "allowImportingTsExtensions": true, 17 | "isolatedModules": true, 18 | "moduleDetection": "force", 19 | "noEmit": true, 20 | "jsx": "react-jsx", 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | "baseUrl": ".", 27 | "paths": { 28 | "@/*": [ 29 | "./src/*" 30 | ] 31 | } 32 | }, 33 | "include": [ 34 | "src" 35 | ] 36 | } 37 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/number-input.tsx: -------------------------------------------------------------------------------- 1 | import { NumberInput as ChakraNumberInput } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | 4 | export interface NumberInputProps extends ChakraNumberInput.RootProps {} 5 | 6 | export const NumberInputRoot = React.forwardRef< 7 | HTMLDivElement, 8 | NumberInputProps 9 | >((props, ref) => { 10 | const { children, ...rest } = props; 11 | return ( 12 | 13 | {children} 14 | 15 | 16 | 17 | 18 | 19 | ); 20 | }); 21 | 22 | export const NumberInputField = ChakraNumberInput.Input; 23 | export const NumberInputScrubber = ChakraNumberInput.Scrubber; 24 | export const NumberInputLabel = ChakraNumberInput.Label; 25 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/radio.tsx: -------------------------------------------------------------------------------- 1 | import { RadioGroup as ChakraRadioGroup } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | 4 | export interface RadioProps extends ChakraRadioGroup.ItemProps { 5 | rootRef?: React.Ref 6 | inputProps?: React.InputHTMLAttributes 7 | } 8 | 9 | export const Radio = React.forwardRef( 10 | (props, ref) => { 11 | const { 12 | children, inputProps, rootRef, ...rest 13 | } = props; 14 | return ( 15 | 16 | 17 | 18 | {children && ( 19 | {children} 20 | )} 21 | 22 | ); 23 | }, 24 | ); 25 | 26 | export const RadioGroup = ChakraRadioGroup.Root; 27 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/checkbox.tsx: -------------------------------------------------------------------------------- 1 | import { Checkbox as ChakraCheckbox } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | 4 | export interface CheckboxProps extends ChakraCheckbox.RootProps { 5 | icon?: React.ReactNode 6 | inputProps?: React.InputHTMLAttributes 7 | rootRef?: React.Ref 8 | } 9 | 10 | export const Checkbox = React.forwardRef( 11 | (props, ref) => { 12 | const { 13 | icon, children, inputProps, rootRef, ...rest 14 | } = props; 15 | return ( 16 | 17 | 18 | 19 | {icon || } 20 | 21 | {children != null && ( 22 | {children} 23 | )} 24 | 25 | ); 26 | }, 27 | ); 28 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/utils/use-force-ignore-mouse.ts: -------------------------------------------------------------------------------- 1 | import { create } from "zustand"; 2 | 3 | // Define the state store interface 4 | interface ForceIgnoreMouseState { 5 | // Whether mouse events are forcibly ignored 6 | forceIgnoreMouse: boolean; 7 | // Set the force ignore mouse state 8 | setForceIgnoreMouse: (forceIgnore: boolean) => void; 9 | } 10 | 11 | // Create a global store for force ignore mouse state 12 | const useForceIgnoreMouseStore = create((set) => ({ 13 | forceIgnoreMouse: false, 14 | setForceIgnoreMouse: (forceIgnore) => set({ forceIgnoreMouse: forceIgnore }), 15 | })); 16 | 17 | /** 18 | * Hook to access and manage force ignore mouse state 19 | * This is used to enable/disable mouse interaction with the model in pet mode 20 | */ 21 | export function useForceIgnoreMouse() { 22 | const { forceIgnoreMouse, setForceIgnoreMouse } = useForceIgnoreMouseStore(); 23 | 24 | return { 25 | forceIgnoreMouse, 26 | setForceIgnoreMouse, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/src/cubismframeworkconfig.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | //======================================================== 9 | // ログ出力関数の設定 10 | //======================================================== 11 | 12 | //---------- ログ出力レベル 選択項目 定義 ---------- 13 | // 詳細ログ出力設定 14 | export const CSM_LOG_LEVEL_VERBOSE = 0; 15 | // デバッグログ出力設定 16 | export const CSM_LOG_LEVEL_DEBUG = 1; 17 | // Infoログ出力設定 18 | export const CSM_LOG_LEVEL_INFO = 2; 19 | // 警告ログ出力設定 20 | export const CSM_LOG_LEVEL_WARNING = 3; 21 | // エラーログ出力設定 22 | export const CSM_LOG_LEVEL_ERROR = 4; 23 | // ログ出力オフ設定 24 | export const CSM_LOG_LEVEL_OFF = 5; 25 | 26 | /** 27 | * ログ出力レベル設定。 28 | * 29 | * 強制的にログ出力レベルを変える時に定義を有効にする。 30 | * CSM_LOG_LEVEL_VERBOSE ~ CSM_LOG_LEVEL_OFF を選択する。 31 | */ 32 | export const CSM_LOG_LEVEL: number = CSM_LOG_LEVEL_VERBOSE; 33 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/sidebar/use-capture-screen.ts: -------------------------------------------------------------------------------- 1 | import { useRef, useState, useEffect } from 'react'; 2 | import { useScreenCaptureContext } from '@/context/screen-capture-context'; 3 | 4 | export function useCaptureScreen() { 5 | const videoRef = useRef(null); 6 | const [isHovering, setIsHovering] = useState(false); 7 | const { stream, isStreaming, error, startCapture, stopCapture } = useScreenCaptureContext(); 8 | 9 | const toggleCapture = () => { 10 | if (isStreaming) { 11 | stopCapture(); 12 | } else { 13 | startCapture(); 14 | } 15 | }; 16 | 17 | const handleMouseEnter = () => setIsHovering(true); 18 | const handleMouseLeave = () => setIsHovering(false); 19 | 20 | useEffect(() => { 21 | if (videoRef.current) { 22 | videoRef.current.srcObject = stream; 23 | } 24 | }, [stream]); 25 | 26 | return { 27 | videoRef, 28 | error, 29 | isHovering, 30 | isStreaming, 31 | stream, 32 | toggleCapture, 33 | handleMouseEnter, 34 | handleMouseLeave, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "incremental": true, 4 | "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", 5 | "target": "ES2022", 6 | "lib": [ 7 | "ES2023" 8 | ], 9 | "module": "ESNext", 10 | "skipLibCheck": true, 11 | /* Bundler mode */ 12 | "moduleResolution": "bundler", 13 | "allowImportingTsExtensions": true, 14 | "isolatedModules": true, 15 | "moduleDetection": "force", 16 | "noEmit": true, 17 | /* Linting */ 18 | "strict": false, 19 | // "noUnusedLocals": false, 20 | // "noUnusedParameters": false, 21 | // "noFallthroughCasesInSwitch": false, 22 | // "allowUnusedLabels": true, 23 | // "allowUnreachableCode": true, 24 | // "noImplicitAny": false, 25 | // "noImplicitThis": false, 26 | // "noImplicitReturns": false, 27 | // "noPropertyAccessFromIndexSignature": false, 28 | // "noUncheckedIndexedAccess": false, 29 | // "exactOptionalPropertyTypes": false, 30 | // "noImplicitOverride": false 31 | }, 32 | "include": [ 33 | "vite.config.ts" 34 | ] 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/src/components/canvas/subtitle.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text } from '@chakra-ui/react'; 2 | import { memo } from 'react'; 3 | import { canvasStyles } from './canvas-styles'; 4 | import { useSubtitleDisplay } from '@/hooks/canvas/use-subtitle-display'; 5 | import { useSubtitle } from '@/context/subtitle-context'; 6 | 7 | // Type definitions 8 | interface SubtitleTextProps { 9 | text: string 10 | } 11 | 12 | // Reusable components 13 | const SubtitleText = memo(({ text }: SubtitleTextProps) => ( 14 | 15 | {text} 16 | 17 | )); 18 | 19 | SubtitleText.displayName = 'SubtitleText'; 20 | 21 | // Main component 22 | const Subtitle = memo((): JSX.Element | null => { 23 | const { subtitleText, isLoaded } = useSubtitleDisplay(); 24 | const { showSubtitle } = useSubtitle(); 25 | 26 | if (!isLoaded || !subtitleText || !showSubtitle) return null; 27 | 28 | return ( 29 | 30 | 31 | 32 | ); 33 | }); 34 | 35 | Subtitle.displayName = 'Subtitle'; 36 | 37 | export default Subtitle; 38 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/field.tsx: -------------------------------------------------------------------------------- 1 | import { Field as ChakraField } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | 4 | export interface FieldProps extends Omit { 5 | label?: React.ReactNode 6 | helperText?: React.ReactNode 7 | errorText?: React.ReactNode 8 | optionalText?: React.ReactNode 9 | } 10 | 11 | export const Field = React.forwardRef( 12 | (props, ref) => { 13 | const { 14 | label, children, helperText, errorText, optionalText, ...rest 15 | } = props; 16 | return ( 17 | 18 | {label && ( 19 | 20 | {label} 21 | 22 | 23 | )} 24 | {children} 25 | {helperText && ( 26 | {helperText} 27 | )} 28 | {errorText && ( 29 | {errorText} 30 | )} 31 | 32 | ); 33 | }, 34 | ); 35 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/utils/use-local-storage.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | 3 | export function useLocalStorage( 4 | key: string, 5 | initialValue: T, 6 | options?: { 7 | filter?: (value: T) => T 8 | }, 9 | ) { 10 | const [storedValue, setStoredValue] = useState(() => { 11 | try { 12 | const item = window.localStorage.getItem(key); 13 | const parsedValue = item ? JSON.parse(item) : initialValue; 14 | return parsedValue; 15 | } catch (error) { 16 | console.error(`Error reading localStorage key "${key}":`, error); 17 | return initialValue; 18 | } 19 | }); 20 | 21 | const setValue = (value: T | ((val: T) => T)) => { 22 | try { 23 | const valueToStore = value instanceof Function ? value(storedValue) : value; 24 | const filteredValue = options?.filter ? options.filter(valueToStore) : valueToStore; 25 | setStoredValue(valueToStore); 26 | window.localStorage.setItem(key, JSON.stringify(filteredValue)); 27 | } catch (error) { 28 | console.error(`Error setting localStorage key "${key}":`, error); 29 | } 30 | }; 31 | 32 | return [storedValue, setValue] as const; 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/tag.tsx: -------------------------------------------------------------------------------- 1 | import { Tag as ChakraTag } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | 4 | export interface TagProps extends ChakraTag.RootProps { 5 | startElement?: React.ReactNode 6 | endElement?: React.ReactNode 7 | onClose?: VoidFunction 8 | closable?: boolean 9 | } 10 | 11 | export const Tag = React.forwardRef( 12 | (props, ref) => { 13 | const { 14 | startElement, 15 | endElement, 16 | onClose, 17 | closable = !!onClose, 18 | children, 19 | ...rest 20 | } = props; 21 | 22 | return ( 23 | 24 | {startElement && ( 25 | {startElement} 26 | )} 27 | {children} 28 | {endElement && ( 29 | {endElement} 30 | )} 31 | {closable && ( 32 | 33 | 34 | 35 | )} 36 | 37 | ); 38 | }, 39 | ); 40 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/utils/use-send-audio.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from "react"; 2 | import { useWebSocket } from "@/context/websocket-context"; 3 | import { useMediaCapture } from "@/hooks/utils/use-media-capture"; 4 | 5 | export function useSendAudio() { 6 | const { sendMessage } = useWebSocket(); 7 | const { captureAllMedia } = useMediaCapture(); 8 | 9 | const sendAudioPartition = useCallback( 10 | async (audio: Float32Array) => { 11 | const chunkSize = 4096; 12 | 13 | // Send the audio data in chunks 14 | for (let index = 0; index < audio.length; index += chunkSize) { 15 | const endIndex = Math.min(index + chunkSize, audio.length); 16 | const chunk = audio.slice(index, endIndex); 17 | sendMessage({ 18 | type: "mic-audio-data", 19 | audio: Array.from(chunk), 20 | // Only send images with first chunk 21 | }); 22 | } 23 | 24 | // Send end signal after all chunks 25 | const images = await captureAllMedia(); 26 | sendMessage({ type: "mic-audio-end", images }); 27 | }, 28 | [sendMessage, captureAllMedia], 29 | ); 30 | 31 | return { 32 | sendAudioPartition, 33 | }; 34 | } 35 | -------------------------------------------------------------------------------- /src/preload/index.d.ts: -------------------------------------------------------------------------------- 1 | import { ElectronAPI } from '@electron-toolkit/preload'; 2 | 3 | declare global { 4 | interface Window { 5 | electron: ElectronAPI 6 | api: { 7 | setIgnoreMouseEvents: (ignore: boolean) => void 8 | toggleForceIgnoreMouse: () => void 9 | onForceIgnoreMouseChanged: (callback: (isForced: boolean) => void) => void 10 | onModeChanged: (callback: (mode: 'pet' | 'window') => void) => void 11 | showContextMenu: (x: number, y: number) => void 12 | onMicToggle: (callback: () => void) => void 13 | onInterrupt: (callback: () => void) => void 14 | updateComponentHover: (componentId: string, isHovering: boolean) => void 15 | onToggleInputSubtitle: (callback: () => void) => void 16 | onToggleScrollToResize: (callback: () => void) => void 17 | onSwitchCharacter: (callback: (filename: string) => void) => void 18 | setMode: (mode: 'window' | 'pet') => void 19 | getConfigFiles: () => Promise 20 | updateConfigFiles: (files: any[]) => void 21 | } 22 | } 23 | } 24 | 25 | interface IpcRenderer { 26 | on(channel: 'mode-changed', func: (_event: any, mode: 'pet' | 'window') => void): void; 27 | send(channel: string, ...args: any[]): void; 28 | } 29 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/sidebar/use-camera-panel.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-shadow */ 2 | import { useRef, useState } from 'react'; 3 | import { useCamera } from '@/context/camera-context'; 4 | 5 | export const useCameraPanel = () => { 6 | const videoRef = useRef(null); 7 | const [error, setError] = useState(''); 8 | const [isHovering, setIsHovering] = useState(false); 9 | const { 10 | isStreaming, stream, startCamera, stopCamera, 11 | } = useCamera(); 12 | 13 | const toggleCamera = async (): Promise => { 14 | try { 15 | if (isStreaming) { 16 | stopCamera(); 17 | } else { 18 | await startCamera(); 19 | } 20 | setError(''); 21 | } catch (error) { 22 | let errorMessage = 'Unable to access camera'; 23 | if (error instanceof Error) { 24 | errorMessage = error.message; 25 | } 26 | setError(errorMessage); 27 | } 28 | }; 29 | 30 | const handleMouseEnter = () => setIsHovering(true); 31 | const handleMouseLeave = () => setIsHovering(false); 32 | 33 | return { 34 | videoRef, 35 | error, 36 | isHovering, 37 | isStreaming, 38 | stream, 39 | toggleCamera, 40 | handleMouseEnter, 41 | handleMouseLeave, 42 | }; 43 | }; 44 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/sidebar/use-sidebar.ts: -------------------------------------------------------------------------------- 1 | import { useDisclosure } from '@chakra-ui/react'; 2 | import { useWebSocket } from '@/context/websocket-context'; 3 | import { useInterrupt } from '@/components/canvas/live2d'; 4 | import { useChatHistory } from '@/context/chat-history-context'; 5 | import { useMode, ModeType } from '@/context/mode-context'; 6 | 7 | export const useSidebar = () => { 8 | const disclosure = useDisclosure(); 9 | const { sendMessage } = useWebSocket(); 10 | const { interrupt } = useInterrupt(); 11 | const { currentHistoryUid, messages, updateHistoryList } = useChatHistory(); 12 | const { setMode, mode, isElectron } = useMode(); 13 | 14 | const createNewHistory = (): void => { 15 | if (currentHistoryUid && messages.length > 0) { 16 | const latestMessage = messages[messages.length - 1]; 17 | updateHistoryList(currentHistoryUid, latestMessage); 18 | } 19 | 20 | interrupt(); 21 | sendMessage({ 22 | type: 'create-new-history', 23 | }); 24 | }; 25 | 26 | return { 27 | settingsOpen: disclosure.open, 28 | onSettingsOpen: disclosure.onOpen, 29 | onSettingsClose: disclosure.onClose, 30 | createNewHistory, 31 | setMode, 32 | currentMode: mode, 33 | isElectron, 34 | }; 35 | }; 36 | -------------------------------------------------------------------------------- /src/renderer/src/components/canvas/ws-status.tsx: -------------------------------------------------------------------------------- 1 | import { Box } from '@chakra-ui/react'; 2 | import React, { memo } from 'react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { canvasStyles } from './canvas-styles'; 5 | import { useWSStatus } from '@/hooks/canvas/use-ws-status'; 6 | 7 | // Type definitions 8 | interface StatusContentProps { 9 | textKey: string 10 | } 11 | 12 | // Reusable components 13 | const StatusContent: React.FC = ({ textKey }) => { 14 | const { t } = useTranslation(); 15 | return t(textKey); 16 | }; 17 | const MemoizedStatusContent = memo(StatusContent); 18 | 19 | // Main component 20 | const WebSocketStatus = memo((): JSX.Element => { 21 | const { 22 | color, textKey, handleClick, isDisconnected, 23 | } = useWSStatus(); 24 | 25 | return ( 26 | 35 | 36 | 37 | ); 38 | }); 39 | 40 | WebSocketStatus.displayName = 'WebSocketStatus'; 41 | 42 | export default WebSocketStatus; 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | pnpm-lock.yaml 9 | lerna-debug.log* 10 | 11 | # Dependencies 12 | node_modules 13 | .yarn 14 | # package-lock.json 15 | 16 | # Build 17 | release 18 | dist 19 | dist-ssr 20 | build 21 | *.local 22 | .docusaurus 23 | .cache-loader 24 | coverage 25 | 26 | # Editor directories and files 27 | .vscode/* 28 | !.vscode/extensions.json 29 | .idea 30 | .DS_Store 31 | *.suo 32 | *.ntvs* 33 | *.njsproj 34 | *.sln 35 | *.sw? 36 | *.swp 37 | *.swo 38 | *.sublime-project 39 | *.sublime-workspace 40 | *.iml 41 | *.code-workspace 42 | 43 | # React DevTools 44 | packages/react-devtools-*/dist 45 | packages/react-devtools-extensions/chrome/build 46 | packages/react-devtools-extensions/chrome/*.crx 47 | packages/react-devtools-extensions/chrome/*.pem 48 | packages/react-devtools-extensions/firefox/build 49 | packages/react-devtools-extensions/firefox/*.xpi 50 | packages/react-devtools-extensions/firefox/*.pem 51 | packages/react-devtools-extensions/shared/build 52 | packages/react-devtools-extensions/.tempUserDataDir 53 | 54 | # Temp files 55 | *~ 56 | *.pyc 57 | .grunt 58 | out 59 | 60 | # Cursor files 61 | .cursorrules 62 | 63 | # Backend directory (contains cloned repository) 64 | backend/ -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | extends: [ 4 | 'airbnb', 5 | 'airbnb/hooks', 6 | 'plugin:@typescript-eslint/recommended', 7 | 'plugin:react/recommended', 8 | ], 9 | plugins: ['@typescript-eslint', 'react'], 10 | settings: { 11 | 'import/resolver': { 12 | node: { 13 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 14 | }, 15 | }, 16 | }, 17 | rules: { 18 | 'no-unused-vars': 'off', 19 | 'max-len': 'off', 20 | '@typescript-eslint/no-explicit-any': 'off', 21 | '@typescript-eslint/no-unused-vars': 'off', 22 | 'no-console': 'off', 23 | 'react/jsx-filename-extension': [1, { extensions: ['.tsx', '.jsx'] }], 24 | 'import/extensions': 'off', 25 | 'import/no-extraneous-dependencies': ['error', { devDependencies: true }], 26 | 'react/react-in-jsx-scope': 'off', 27 | 'react/jsx-props-no-spreading': 'off', 28 | 'import/no-unresolved': 'off', 29 | 'import/prefer-default-export': 'off', 30 | quotes: 'off', 31 | 'operator-linebreak': 'off', 32 | 'react/display-name': 'off', 33 | 'react-hooks/exhaustive-deps': 'off', 34 | 'consistent-return': 'off', 35 | 'object-curly-newline': 'off', 36 | 'react/require-default-props': 'off', 37 | }, 38 | }; 39 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/button.tsx: -------------------------------------------------------------------------------- 1 | import type { ButtonProps as ChakraButtonProps } from '@chakra-ui/react'; 2 | import { 3 | AbsoluteCenter, 4 | Button as ChakraButton, 5 | Span, 6 | Spinner, 7 | } from '@chakra-ui/react'; 8 | import * as React from 'react'; 9 | 10 | interface ButtonLoadingProps { 11 | loading?: boolean 12 | loadingText?: React.ReactNode 13 | } 14 | 15 | export interface ButtonProps extends ChakraButtonProps, ButtonLoadingProps {} 16 | 17 | export const Button = React.forwardRef( 18 | (props, ref) => { 19 | const { 20 | loading, disabled, loadingText, children, ...rest 21 | } = props; 22 | return ( 23 | 24 | {loading && !loadingText ? ( 25 | <> 26 | 27 | 28 | 29 | {children} 30 | 31 | ) : loading && loadingText ? ( 32 | <> 33 | 34 | {loadingText} 35 | 36 | ) : ( 37 | children 38 | )} 39 | 40 | ); 41 | }, 42 | ); 43 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/toaster.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import { 4 | Toaster as ChakraToaster, 5 | Portal, 6 | Spinner, 7 | Stack, 8 | Toast, 9 | createToaster, 10 | } from '@chakra-ui/react'; 11 | 12 | export const toaster = createToaster({ 13 | placement: 'top-end', 14 | pauseOnPageIdle: true, 15 | max: 5, 16 | }); 17 | 18 | export function Toaster() { 19 | return ( 20 | 21 | 22 | {(toast) => ( 23 | 24 | {toast.type === 'loading' ? ( 25 | 26 | ) : ( 27 | 28 | )} 29 | 30 | {toast.title && {toast.title}} 31 | {toast.description && ( 32 | {toast.description} 33 | )} 34 | 35 | {toast.action && ( 36 | {toast.action.label} 37 | )} 38 | {toast.meta?.closable && } 39 | 40 | )} 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/canvas/use-ws-status.ts: -------------------------------------------------------------------------------- 1 | import { useMemo, useCallback } from 'react'; 2 | import { useWebSocket } from '@/context/websocket-context'; 3 | 4 | interface WSStatusInfo { 5 | color: string 6 | textKey: string 7 | isDisconnected: boolean 8 | handleClick: () => void 9 | } 10 | 11 | export const useWSStatus = () => { 12 | const { wsState, reconnect } = useWebSocket(); 13 | 14 | const handleClick = useCallback(() => { 15 | if (wsState !== 'OPEN' && wsState !== 'CONNECTING') { 16 | reconnect(); 17 | } 18 | }, [wsState, reconnect]); 19 | 20 | const statusInfo = useMemo((): WSStatusInfo => { 21 | switch (wsState) { 22 | case 'OPEN': 23 | return { 24 | color: 'green.500', 25 | textKey: 'wsStatus.connected', 26 | isDisconnected: false, 27 | handleClick, 28 | }; 29 | case 'CONNECTING': 30 | return { 31 | color: 'yellow.500', 32 | textKey: 'wsStatus.connecting', 33 | isDisconnected: false, 34 | handleClick, 35 | }; 36 | default: 37 | return { 38 | color: 'red.500', 39 | textKey: 'wsStatus.clickToReconnect', 40 | isDisconnected: true, 41 | handleClick, 42 | }; 43 | } 44 | }, [wsState, handleClick]); 45 | 46 | return statusInfo; 47 | }; 48 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/utils/use-interrupt.ts: -------------------------------------------------------------------------------- 1 | import { useAiState } from '@/context/ai-state-context'; 2 | import { useWebSocket } from '@/context/websocket-context'; 3 | import { useChatHistory } from '@/context/chat-history-context'; 4 | import { audioTaskQueue } from '@/utils/task-queue'; 5 | import { useSubtitle } from '@/context/subtitle-context'; 6 | import { useAudioTask } from './use-audio-task'; 7 | 8 | export const useInterrupt = () => { 9 | const { aiState, setAiState } = useAiState(); 10 | const { sendMessage } = useWebSocket(); 11 | const { fullResponse, clearResponse } = useChatHistory(); 12 | // const { currentModel } = useLive2DModel(); 13 | const { subtitleText, setSubtitleText } = useSubtitle(); 14 | const { stopCurrentAudioAndLipSync } = useAudioTask(); 15 | 16 | const interrupt = (sendSignal = true) => { 17 | if (aiState !== 'thinking-speaking') return; 18 | console.log('Interrupting conversation chain'); 19 | 20 | stopCurrentAudioAndLipSync(); 21 | 22 | audioTaskQueue.clearQueue(); 23 | 24 | setAiState('interrupted'); 25 | 26 | if (sendSignal) { 27 | sendMessage({ 28 | type: 'interrupt-signal', 29 | text: fullResponse, 30 | }); 31 | } 32 | 33 | clearResponse(); 34 | 35 | if (subtitleText === 'Thinking...') { 36 | setSubtitleText(''); 37 | } 38 | console.log('Interrupted!'); 39 | }; 40 | 41 | return { interrupt }; 42 | }; 43 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/tooltip.tsx: -------------------------------------------------------------------------------- 1 | import { Tooltip as ChakraTooltip, Portal } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | 4 | export interface TooltipProps extends ChakraTooltip.RootProps { 5 | showArrow?: boolean 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | content: React.ReactNode 9 | contentProps?: ChakraTooltip.ContentProps 10 | disabled?: boolean 11 | } 12 | 13 | export const Tooltip = React.forwardRef( 14 | (props, ref) => { 15 | const { 16 | showArrow, 17 | children, 18 | disabled, 19 | portalled, 20 | content, 21 | contentProps, 22 | portalRef, 23 | ...rest 24 | } = props; 25 | 26 | if (disabled) return children; 27 | 28 | return ( 29 | 30 | {children} 31 | 32 | 33 | 34 | {showArrow && ( 35 | 36 | 37 | 38 | )} 39 | {content} 40 | 41 | 42 | 43 | 44 | ); 45 | }, 46 | ); 47 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/switch.tsx: -------------------------------------------------------------------------------- 1 | import { Switch as ChakraSwitch } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | 4 | export interface SwitchProps extends ChakraSwitch.RootProps { 5 | inputProps?: React.InputHTMLAttributes 6 | rootRef?: React.Ref 7 | trackLabel?: { on: React.ReactNode; off: React.ReactNode } 8 | thumbLabel?: { on: React.ReactNode; off: React.ReactNode } 9 | } 10 | 11 | export const Switch = React.forwardRef( 12 | (props, ref) => { 13 | const { 14 | inputProps, children, rootRef, trackLabel, thumbLabel, ...rest 15 | } = props; 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | {thumbLabel && ( 23 | 24 | {thumbLabel?.on} 25 | 26 | )} 27 | 28 | {trackLabel && ( 29 | 30 | {trackLabel.on} 31 | 32 | )} 33 | 34 | {children != null && ( 35 | {children} 36 | )} 37 | 38 | ); 39 | }, 40 | ); 41 | -------------------------------------------------------------------------------- /i18next-scanner.config.js: -------------------------------------------------------------------------------- 1 | // i18next-scanner configuration 2 | module.exports = { 3 | input: [ 4 | 'src/renderer/src/**/*.{js,jsx,ts,tsx}', 5 | // Use ! to filter out files or directories 6 | '!src/renderer/src/**/*.spec.{js,jsx,ts,tsx}', 7 | '!src/renderer/src/i18n/**', 8 | '!**/node_modules/**', 9 | ], 10 | output: './src/renderer/src', 11 | options: { 12 | debug: true, 13 | func: { 14 | list: ['t', 'i18next.t', 'i18n.t'], 15 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 16 | }, 17 | trans: { 18 | component: 'Trans', 19 | extensions: ['.js', '.jsx', '.ts', '.tsx'], 20 | fallbackKey(ns, value) { 21 | return value; 22 | }, 23 | }, 24 | lngs: ['en', 'zh'], 25 | ns: ['translation'], 26 | defaultLng: 'en', 27 | defaultNs: 'translation', 28 | defaultValue(lng, ns, key) { 29 | if (lng === 'en') { 30 | // Return key as the default value for English 31 | return key; 32 | } 33 | // Return empty string for other languages 34 | return ''; 35 | }, 36 | resource: { 37 | loadPath: 'src/renderer/src/locales/{{lng}}/{{ns}}.json', 38 | savePath: 'locales/{{lng}}/{{ns}}.json', 39 | jsonIndent: 2, 40 | lineEnding: '\n', 41 | }, 42 | sort: true, 43 | removeUnusedKeys: true, 44 | nsSeparator: ':', 45 | keySeparator: '.', 46 | pluralSeparator: '_', 47 | contextSeparator: '_', 48 | contextDefaultValues: [], 49 | interpolation: { 50 | prefix: '{{', 51 | suffix: '}}', 52 | }, 53 | }, 54 | }; 55 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/alert.tsx: -------------------------------------------------------------------------------- 1 | import { Alert as ChakraAlert } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | import { CloseButton } from './close-button'; 4 | 5 | export interface AlertProps extends Omit { 6 | startElement?: React.ReactNode 7 | endElement?: React.ReactNode 8 | title?: React.ReactNode 9 | icon?: React.ReactElement 10 | closable?: boolean 11 | onClose?: () => void 12 | } 13 | 14 | export const Alert = React.forwardRef( 15 | (props, ref) => { 16 | const { 17 | title, 18 | children, 19 | icon, 20 | closable, 21 | onClose, 22 | startElement, 23 | endElement, 24 | ...rest 25 | } = props; 26 | return ( 27 | 28 | {startElement || {icon}} 29 | {children ? ( 30 | 31 | {title} 32 | {children} 33 | 34 | ) : ( 35 | {title} 36 | )} 37 | {endElement} 38 | {closable && ( 39 | 47 | )} 48 | 49 | ); 50 | }, 51 | ); 52 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/src/icubismallcator.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | /** 9 | * メモリアロケーションを抽象化したクラス 10 | * 11 | * メモリ確保・解放処理をプラットフォーム側で実装して 12 | * フレームワークから呼び出すためのインターフェース 13 | */ 14 | export abstract class ICubismAllocator { 15 | /** 16 | * アラインメント制約なしのヒープ・メモリーを確保します 17 | * 18 | * @param size 確保するバイト数 19 | * @return 成功すると割り当てられたメモリのアドレス。そうでなければ'0'を返す 20 | */ 21 | public abstract allocate(size: number): any; 22 | 23 | /** 24 | * アラインメント制約なしのヒープ・メモリーを解放します。 25 | * 26 | * @param memory 解放するメモリのアドレス 27 | */ 28 | public abstract deallocate(memory: any): void; 29 | 30 | /** 31 | * アラインメント制約有のヒープ・メモリーを確保します。 32 | * @param size 確保するバイト数 33 | * @param alignment メモリーブロックのアラインメント幅 34 | * @return 成功すると割り当てられたメモリのアドレス。そうでなければ'0'を返す 35 | */ 36 | public abstract allocateAligned(size: number, alignment: number): any; 37 | 38 | /** 39 | * アラインメント制約ありのヒープ・メモリーを解放します。 40 | * @param alignedMemory 解放するメモリのアドレス 41 | */ 42 | public abstract deallocateAligned(alignedMemory: any): void; 43 | } 44 | 45 | // Namespace definition for compatibility. 46 | import * as $ from './icubismallcator'; 47 | // eslint-disable-next-line @typescript-eslint/no-namespace 48 | export namespace Live2DCubismFramework { 49 | export const ICubismAllocator = $.ICubismAllocator; 50 | export type ICubismAllocator = $.ICubismAllocator; 51 | } 52 | -------------------------------------------------------------------------------- /src/renderer/src/components/sidebar/bottom-tab.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | import { Tabs } from '@chakra-ui/react' 3 | import { FiCamera, FiMonitor, FiGlobe } from 'react-icons/fi' 4 | import { useTranslation } from 'react-i18next' 5 | import { sidebarStyles } from './sidebar-styles' 6 | import CameraPanel from './camera-panel' 7 | import ScreenPanel from './screen-panel' 8 | import BrowserPanel from './browser-panel' 9 | 10 | function BottomTab(): JSX.Element { 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 | 19 | 20 | 21 | 22 | {t('sidebar.camera')} 23 | 24 | 25 | 26 | {t('sidebar.screen')} 27 | 28 | 29 | 30 | {t('sidebar.browser')} 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | } 48 | 49 | export default BottomTab 50 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/utils/use-switch-character.tsx: -------------------------------------------------------------------------------- 1 | import { useCallback } from 'react'; 2 | import { useWebSocket } from '@/context/websocket-context'; 3 | import { useConfig } from '@/context/character-config-context'; 4 | import { useInterrupt } from '@/components/canvas/live2d'; 5 | import { useVAD } from '@/context/vad-context'; 6 | import { useSubtitle } from '@/context/subtitle-context'; 7 | import { useAiState } from '@/context/ai-state-context'; 8 | import { useLive2DConfig } from '@/context/live2d-config-context'; 9 | 10 | export function useSwitchCharacter() { 11 | const { sendMessage } = useWebSocket(); 12 | const { confName, getFilenameByName } = useConfig(); 13 | const { interrupt } = useInterrupt(); 14 | const { stopMic } = useVAD(); 15 | const { setSubtitleText } = useSubtitle(); 16 | const { setAiState } = useAiState(); 17 | const { setModelInfo } = useLive2DConfig(); 18 | const switchCharacter = useCallback((fileName: string) => { 19 | const currentFilename = getFilenameByName(confName); 20 | 21 | if (currentFilename === fileName) { 22 | console.log('Skipping character switch - same configuration file'); 23 | return; 24 | } 25 | 26 | setSubtitleText('New Character Loading...'); 27 | interrupt(); 28 | stopMic(); 29 | setAiState('loading'); 30 | setModelInfo(undefined); 31 | sendMessage({ 32 | type: 'switch-config', 33 | file: fileName, 34 | }); 35 | console.log('Switch Character fileName: ', fileName); 36 | }, [confName, getFilenameByName, sendMessage, interrupt, stopMic, setSubtitleText, setAiState]); 37 | 38 | return { switchCharacter }; 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/src/components/sidebar/chat-bubble.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Text, Flex } from '@chakra-ui/react'; 2 | import { Avatar, AvatarGroup } from '@/components/ui/avatar'; 3 | import { Message } from '@/services/websocket-service'; 4 | 5 | // Type definitions 6 | interface ChatBubbleProps { 7 | message: Message; 8 | isSelected?: boolean; 9 | onClick?: () => void; 10 | } 11 | 12 | // Main component 13 | export function ChatBubble({ message, isSelected, onClick }: ChatBubbleProps): JSX.Element { 14 | const isAI = message.role === 'ai'; 15 | 16 | return ( 17 | 26 | 27 | 28 | 34 | 35 | 36 | 37 | {message.name || (isAI ? 'AI' : 'Me')} 38 | 39 | 44 | {message.content} 45 | 46 | 47 | {new Date(message.timestamp).toLocaleTimeString()} 48 | 49 | 50 | 51 | 52 | ); 53 | } 54 | -------------------------------------------------------------------------------- /docs/i18n-usage.md: -------------------------------------------------------------------------------- 1 | # i18n (国际化) 使用指南 2 | 3 | 本项目使用 i18next 实现多语言支持,目前支持英语和中文。 4 | 5 | ## 目录结构 6 | 7 | ``` 8 | src/renderer/src/ 9 | ├── i18n.ts # i18next 配置文件 10 | ├── locales/ # 翻译文件目录 11 | │ ├── en/ # 英语翻译 12 | │ │ └── translation.json # 英语翻译文件 13 | │ └── zh/ # 中文翻译 14 | │ └── translation.json # 中文翻译文件 15 | ``` 16 | 17 | ## 使用方法 18 | 19 | ### 在组件中使用 20 | 21 | ```tsx 22 | import { useTranslation } from 'react-i18next'; 23 | 24 | function MyComponent() { 25 | // 获取翻译函数和其他国际化工具 26 | const { t, i18n } = useTranslation(); 27 | 28 | // 使用 t 函数翻译文本 29 | return ( 30 |
31 |

{t('common.settings')}

32 |

{t('settings.general.language')}

33 | 34 | {/* 改变语言 */} 35 | 36 | 37 |
38 | ); 39 | } 40 | ``` 41 | 42 | ### 添加新的翻译 43 | 44 | 1. 在代码中使用 `t('your.translation.key')` 添加新的翻译键 45 | 2. 运行扫描命令提取翻译键: 46 | 47 | ```bash 48 | npm run extract-translations 49 | ``` 50 | 51 | 3. 添加的翻译键会更新到 `src/renderer/src/locales/en/translation.json` 和 `src/renderer/src/locales/zh/translation.json` 文件中 52 | 4. 编辑中文翻译文件,为每个键添加对应的中文翻译 53 | 54 | ## 翻译键命名规则 55 | 56 | 我们使用点号分隔的命名空间来组织翻译键,例如: 57 | 58 | - `common.save` - 通用的"保存"按钮文本 59 | - `settings.general.language` - 设置页面中的语言设置项 60 | 61 | ## 嵌套翻译 62 | 63 | 对于包含变量的文本,可以使用插值: 64 | 65 | ```tsx 66 | // 在翻译文件中 67 | { 68 | "welcome": "欢迎, {{name}}!" 69 | } 70 | 71 | // 在组件中 72 | t('welcome', { name: 'John' }) // "欢迎, John!" 73 | ``` 74 | 75 | ## 自动检测语言 76 | 77 | 系统会自动检测用户的浏览器语言设置,并使用最接近的可用语言。用户也可以手动切换语言,选择会被保存在 localStorage 中。 -------------------------------------------------------------------------------- /src/renderer/src/components/sidebar/setting/agent.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import { Stack } from '@chakra-ui/react'; 3 | import { useTranslation } from 'react-i18next'; 4 | import { settingStyles } from './setting-styles'; 5 | import { useAgentSettings } from '@/hooks/sidebar/setting/use-agent-settings'; 6 | import { SwitchField, NumberField } from './common'; 7 | 8 | interface AgentProps { 9 | onSave?: (callback: () => void) => () => void 10 | onCancel?: (callback: () => void) => () => void 11 | } 12 | 13 | function Agent({ onSave, onCancel }: AgentProps): JSX.Element { 14 | const { t } = useTranslation(); 15 | const { 16 | settings, 17 | handleAllowProactiveSpeakChange, 18 | handleIdleSecondsChange, 19 | handleAllowButtonTriggerChange, 20 | } = useAgentSettings({ onSave, onCancel }); 21 | 22 | return ( 23 | 24 | 29 | 30 | {settings.allowProactiveSpeak && ( 31 | handleIdleSecondsChange(Number(value))} 35 | min={0} 36 | step={0.1} 37 | allowMouseWheel 38 | /> 39 | )} 40 | 41 | 46 | 47 | ); 48 | } 49 | 50 | export default Agent; 51 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/input-group.tsx: -------------------------------------------------------------------------------- 1 | import type { BoxProps, InputElementProps } from '@chakra-ui/react'; 2 | import { Group, InputElement } from '@chakra-ui/react'; 3 | import * as React from 'react'; 4 | 5 | export interface InputGroupProps extends BoxProps { 6 | startElementProps?: InputElementProps 7 | endElementProps?: InputElementProps 8 | startElement?: React.ReactNode 9 | endElement?: React.ReactNode 10 | children: React.ReactElement 11 | startOffset?: InputElementProps['paddingStart'] 12 | endOffset?: InputElementProps['paddingEnd'] 13 | } 14 | 15 | export const InputGroup = React.forwardRef( 16 | (props, ref) => { 17 | const { 18 | startElement, 19 | startElementProps, 20 | endElement, 21 | endElementProps, 22 | children, 23 | startOffset = '6px', 24 | endOffset = '6px', 25 | ...rest 26 | } = props; 27 | 28 | const child = React.Children.only>(children); 29 | 30 | return ( 31 | 32 | {startElement && ( 33 | 34 | {startElement} 35 | 36 | )} 37 | {React.cloneElement(child, { 38 | ...(startElement && { 39 | ps: `calc(var(--input-height) - ${startOffset})`, 40 | }), 41 | ...(endElement && { pe: `calc(var(--input-height) - ${endOffset})` }), 42 | ...children.props, 43 | })} 44 | {endElement && ( 45 | 46 | {endElement} 47 | 48 | )} 49 | 50 | ); 51 | }, 52 | ); 53 | -------------------------------------------------------------------------------- /src/renderer/src/components/sidebar/setting/live2d.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | /* eslint-disable react-hooks/rules-of-hooks */ 3 | import { Stack } from '@chakra-ui/react'; 4 | import { useEffect } from 'react'; 5 | import { useTranslation } from 'react-i18next'; 6 | import { settingStyles } from './setting-styles'; 7 | import { useLive2dSettings } from '@/hooks/sidebar/setting/use-live2d-settings'; 8 | import { SwitchField } from './common'; 9 | 10 | interface live2DProps { 11 | onSave?: (callback: () => void) => () => void 12 | onCancel?: (callback: () => void) => () => void 13 | } 14 | 15 | function live2D({ onSave, onCancel }: live2DProps): JSX.Element { 16 | const { t } = useTranslation(); 17 | const { 18 | modelInfo, 19 | handleInputChange, 20 | handleSave, 21 | handleCancel, 22 | } = useLive2dSettings(); 23 | 24 | useEffect(() => { 25 | if (!onSave || !onCancel) return; 26 | 27 | const cleanupSave = onSave(handleSave); 28 | const cleanupCancel = onCancel(handleCancel); 29 | 30 | return (): void => { 31 | cleanupSave?.(); 32 | cleanupCancel?.(); 33 | }; 34 | }, [onSave, onCancel]); 35 | 36 | return ( 37 | 38 | handleInputChange('pointerInteractive', checked)} 42 | /> 43 | 44 | handleInputChange('scrollToResize', checked)} 48 | /> 49 | 50 | ); 51 | } 52 | 53 | export default live2D; 54 | -------------------------------------------------------------------------------- /src/renderer/src/context/group-context.tsx: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useState, useMemo } from 'react'; 2 | 3 | interface GroupContextState { 4 | selfUid: string; 5 | groupMembers: string[]; 6 | isOwner: boolean; 7 | setSelfUid: (uid: string) => void; 8 | setGroupMembers: (members: string[]) => void; 9 | setIsOwner: (isOwner: boolean) => void; 10 | sortedGroupMembers: string[]; 11 | resetGroupState: () => void; 12 | } 13 | 14 | const GroupContext = createContext(null); 15 | 16 | export function GroupProvider({ children }: { children: React.ReactNode }) { 17 | const [selfUid, setSelfUid] = useState(''); 18 | const [groupMembers, setGroupMembers] = useState([]); 19 | const [isOwner, setIsOwner] = useState(false); 20 | 21 | const resetGroupState = () => { 22 | setGroupMembers([]); 23 | setIsOwner(false); 24 | }; 25 | 26 | const sortedGroupMembers = useMemo(() => { 27 | if (!groupMembers.includes(selfUid)) return groupMembers; 28 | 29 | return [ 30 | selfUid, 31 | ...groupMembers.filter((memberId) => memberId !== selfUid), 32 | ]; 33 | }, [groupMembers, selfUid]); 34 | 35 | return ( 36 | // eslint-disable-next-line react/jsx-no-constructed-context-values 37 | 48 | {children} 49 | 50 | ); 51 | } 52 | 53 | export function useGroup() { 54 | const context = useContext(GroupContext); 55 | if (!context) { 56 | throw new Error('useGroup must be used within a GroupProvider'); 57 | } 58 | return context; 59 | } 60 | -------------------------------------------------------------------------------- /src/renderer/src/components/canvas/background.tsx: -------------------------------------------------------------------------------- 1 | import { Box, Image } from '@chakra-ui/react'; 2 | import { memo, useEffect, useRef } from 'react'; 3 | import { canvasStyles } from './canvas-styles'; 4 | import { useCamera } from '@/context/camera-context'; 5 | import { useBgUrl } from '@/context/bgurl-context'; 6 | 7 | const Background = memo(({ children }: { children?: React.ReactNode }) => { 8 | const videoRef = useRef(null); 9 | const { 10 | backgroundStream, isBackgroundStreaming, startBackgroundCamera, stopBackgroundCamera, 11 | } = useCamera(); 12 | const { useCameraBackground, backgroundUrl } = useBgUrl(); 13 | 14 | useEffect(() => { 15 | if (useCameraBackground) { 16 | startBackgroundCamera(); 17 | } else { 18 | stopBackgroundCamera(); 19 | } 20 | }, [useCameraBackground, startBackgroundCamera, stopBackgroundCamera]); 21 | 22 | useEffect(() => { 23 | if (videoRef.current && backgroundStream) { 24 | videoRef.current.srcObject = backgroundStream; 25 | } 26 | }, [backgroundStream]); 27 | 28 | return ( 29 | 30 | {useCameraBackground ? ( 31 | 51 | ); 52 | }); 53 | 54 | Background.displayName = 'Background'; 55 | 56 | export default Background; 57 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/popover.tsx: -------------------------------------------------------------------------------- 1 | import { Popover as ChakraPopover, Portal } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | import { CloseButton } from './close-button'; 4 | 5 | interface PopoverContentProps extends ChakraPopover.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | } 9 | 10 | export const PopoverContent = React.forwardRef< 11 | HTMLDivElement, 12 | PopoverContentProps 13 | >((props, ref) => { 14 | const { portalled = true, portalRef, ...rest } = props; 15 | return ( 16 | 17 | 18 | 19 | 20 | 21 | ); 22 | }); 23 | 24 | export const PopoverArrow = React.forwardRef< 25 | HTMLDivElement, 26 | ChakraPopover.ArrowProps 27 | >((props, ref) => ( 28 | 29 | 30 | 31 | )); 32 | 33 | export const PopoverCloseTrigger = React.forwardRef< 34 | HTMLButtonElement, 35 | ChakraPopover.CloseTriggerProps 36 | >((props, ref) => ( 37 | 45 | 46 | 47 | )); 48 | 49 | export const PopoverTitle = ChakraPopover.Title; 50 | export const PopoverDescription = ChakraPopover.Description; 51 | export const PopoverFooter = ChakraPopover.Footer; 52 | export const PopoverHeader = ChakraPopover.Header; 53 | export const PopoverRoot = ChakraPopover.Root; 54 | export const PopoverBody = ChakraPopover.Body; 55 | export const PopoverTrigger = ChakraPopover.Trigger; 56 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/drawer.tsx: -------------------------------------------------------------------------------- 1 | import { Drawer as ChakraDrawer, Portal } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | import { CloseButton } from './close-button'; 4 | 5 | interface DrawerContentProps extends ChakraDrawer.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | offset?: ChakraDrawer.ContentProps['padding'] 9 | } 10 | 11 | export const DrawerContent = React.forwardRef< 12 | HTMLDivElement, 13 | DrawerContentProps 14 | >((props, ref) => { 15 | const { 16 | children, portalled = true, portalRef, offset, ...rest 17 | } = props; 18 | return ( 19 | 20 | 21 | 22 | {children} 23 | 24 | 25 | 26 | ); 27 | }); 28 | 29 | export const DrawerCloseTrigger = React.forwardRef< 30 | HTMLButtonElement, 31 | ChakraDrawer.CloseTriggerProps 32 | >((props, ref) => ( 33 | 40 | 41 | 42 | )); 43 | 44 | export const DrawerTrigger = ChakraDrawer.Trigger; 45 | export const DrawerRoot = ChakraDrawer.Root; 46 | export const DrawerFooter = ChakraDrawer.Footer; 47 | export const DrawerHeader = ChakraDrawer.Header; 48 | export const DrawerBody = ChakraDrawer.Body; 49 | export const DrawerBackdrop = ChakraDrawer.Backdrop; 50 | export const DrawerDescription = ChakraDrawer.Description; 51 | export const DrawerTitle = ChakraDrawer.Title; 52 | export const DrawerActionTrigger = ChakraDrawer.ActionTrigger; 53 | -------------------------------------------------------------------------------- /src/renderer/src/components/canvas/canvas-styles.tsx: -------------------------------------------------------------------------------- 1 | export const canvasStyles = { 2 | background: { 3 | container: { 4 | position: 'relative', 5 | width: '100%', 6 | height: '100%', 7 | overflow: 'hidden', 8 | pointerEvents: 'auto', 9 | }, 10 | image: { 11 | position: 'absolute', 12 | top: '0', 13 | left: '0', 14 | width: '100%', 15 | height: '100%', 16 | objectFit: 'cover', 17 | zIndex: 1, 18 | }, 19 | video: { 20 | position: 'absolute' as const, 21 | top: '0', 22 | left: '0', 23 | width: '100%', 24 | height: '100%', 25 | objectFit: 'cover' as const, 26 | zIndex: 1, 27 | transform: 'scaleX(-1)' as const, 28 | }, 29 | }, 30 | canvas: { 31 | container: { 32 | position: 'relative', 33 | width: '100%', 34 | height: '100%', 35 | zIndex: '1', 36 | pointerEvents: 'auto', 37 | }, 38 | }, 39 | subtitle: { 40 | container: { 41 | backgroundColor: 'rgba(0, 0, 0, 0.7)', 42 | padding: '15px 30px', 43 | borderRadius: '12px', 44 | minWidth: '60%', 45 | maxWidth: '95%', 46 | }, 47 | text: { 48 | color: 'white', 49 | fontSize: '1.5rem', 50 | textAlign: 'center', 51 | lineHeight: '1.4', 52 | whiteSpace: 'pre-wrap', 53 | }, 54 | }, 55 | wsStatus: { 56 | container: { 57 | position: 'relative', 58 | // top: '20px', 59 | // left: '20px', 60 | zIndex: 2, 61 | padding: '8px 16px', 62 | borderRadius: '20px', 63 | fontSize: '14px', 64 | fontWeight: 'medium', 65 | color: 'white', 66 | transition: 'all 0.2s', 67 | cursor: 'pointer', 68 | userSelect: 'none', 69 | _hover: { 70 | opacity: 0.8, 71 | }, 72 | }, 73 | }, 74 | }; 75 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/src/lappglmanager.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | export let canvas: HTMLCanvasElement | null = null; 9 | export let gl: WebGLRenderingContext | null = null; 10 | export let s_instance: LAppGlManager | null = null; 11 | /** 12 | * Cubism SDKのサンプルで使用するWebGLを管理するクラス 13 | */ 14 | export class LAppGlManager { 15 | /** 16 | * クラスのインスタンス(シングルトン)を返す。 17 | * インスタンスが生成されていない場合は内部でインスタンスを生成する。 18 | * 19 | * @return クラスのインスタンス 20 | */ 21 | public static getInstance(): LAppGlManager { 22 | if (s_instance == null) { 23 | s_instance = new LAppGlManager(); 24 | } 25 | 26 | return s_instance; 27 | } 28 | 29 | /** 30 | * クラスのインスタンス(シングルトン)を解放する。 31 | */ 32 | public static releaseInstance(): void { 33 | if (s_instance != null) { 34 | s_instance.release(); 35 | } 36 | 37 | s_instance = null; 38 | } 39 | 40 | constructor() { 41 | // Use existing canvas instead of creating a new one 42 | canvas = document.getElementById('canvas') as HTMLCanvasElement; 43 | // canvas = document.createElement("canvas"); 44 | 45 | if (!canvas) { 46 | console.warn("Canvas element not found during LAppGlManager initialization"); 47 | return; 48 | } 49 | 50 | gl = canvas.getContext("webgl2"); 51 | 52 | if (!gl) { 53 | // gl初期化失敗 54 | alert("Cannot initialize WebGL. This browser does not support."); 55 | gl = null; 56 | 57 | document.body.innerHTML = 58 | "This browser does not support the <canvas> element."; 59 | } 60 | } 61 | 62 | /** 63 | * 解放する。 64 | */ 65 | public release(): void {} 66 | } 67 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/dialog.tsx: -------------------------------------------------------------------------------- 1 | import { Dialog as ChakraDialog, Portal } from '@chakra-ui/react'; 2 | import * as React from 'react'; 3 | import { CloseButton } from './close-button'; 4 | 5 | interface DialogContentProps extends ChakraDialog.ContentProps { 6 | portalled?: boolean 7 | portalRef?: React.RefObject 8 | backdrop?: boolean 9 | } 10 | 11 | export const DialogContent = React.forwardRef< 12 | HTMLDivElement, 13 | DialogContentProps 14 | >((props, ref) => { 15 | const { 16 | children, 17 | portalled = true, 18 | portalRef, 19 | backdrop = true, 20 | ...rest 21 | } = props; 22 | 23 | return ( 24 | 25 | {backdrop && } 26 | 27 | 28 | {children} 29 | 30 | 31 | 32 | ); 33 | }); 34 | 35 | export const DialogCloseTrigger = React.forwardRef< 36 | HTMLButtonElement, 37 | ChakraDialog.CloseTriggerProps 38 | >((props, ref) => ( 39 | 46 | 47 | {props.children} 48 | 49 | 50 | )); 51 | 52 | export const DialogRoot = ChakraDialog.Root; 53 | export const DialogFooter = ChakraDialog.Footer; 54 | export const DialogHeader = ChakraDialog.Header; 55 | export const DialogBody = ChakraDialog.Body; 56 | export const DialogBackdrop = ChakraDialog.Backdrop; 57 | export const DialogTitle = ChakraDialog.Title; 58 | export const DialogDescription = ChakraDialog.Description; 59 | export const DialogTrigger = ChakraDialog.Trigger; 60 | export const DialogActionTrigger = ChakraDialog.ActionTrigger; 61 | -------------------------------------------------------------------------------- /electron-builder.yml: -------------------------------------------------------------------------------- 1 | appId: com.electron.app 2 | productName: open-llm-vtuber-electron 3 | directories: 4 | buildResources: resources 5 | output: release/${version} 6 | files: 7 | - 'dist/**/*' 8 | - 'out/**/*' 9 | - 'resources/**/*' 10 | - '!**/.vscode/*' 11 | - '!src/*' 12 | - '!electron.vite.config.{js,ts,mjs,cjs}' 13 | - '!{.eslintignore,.eslintrc.cjs,.prettierignore,.prettierrc.yaml,dev-app-update.yml,CHANGELOG.md,README.md}' 14 | - '!{.env,.env.*,.npmrc,pnpm-lock.yaml}' 15 | - '!{tsconfig.json,tsconfig.node.json,tsconfig.web.json}' 16 | asarUnpack: 17 | - resources/** 18 | win: 19 | executableName: open-llm-vtuber-electron 20 | icon: resources/icon.ico 21 | nsis: 22 | artifactName: ${name}-${version}-setup.${ext} 23 | shortcutName: ${productName} 24 | uninstallDisplayName: ${productName} 25 | createDesktopShortcut: always 26 | differentialPackage: false 27 | mac: 28 | icon: resources/icon.icns 29 | target: 30 | - target: dmg 31 | arch: [x64, arm64] 32 | entitlementsInherit: build/entitlements.mac.plist 33 | extendInfo: 34 | - NSCameraUsageDescription: Application requests access to the device's camera. 35 | - NSMicrophoneUsageDescription: Application requests access to the device's microphone. 36 | - NSDocumentsFolderUsageDescription: Application requests access to the user's Documents folder. 37 | - NSDownloadsFolderUsageDescription: Application requests access to the user's Downloads folder. 38 | notarize: false 39 | dmg: 40 | artifactName: ${name}-${version}.${ext} 41 | writeUpdateInfo: false 42 | linux: 43 | target: 44 | - AppImage 45 | - snap 46 | - deb 47 | maintainer: electronjs.org 48 | category: Utility 49 | appImage: 50 | artifactName: ${name}-${version}.${ext} 51 | npmRebuild: false 52 | publish: 53 | provider: github 54 | releaseType: release 55 | publishAutoUpdate: false 56 | electronDownload: 57 | mirror: https://npmmirror.com/mirrors/electron/ -------------------------------------------------------------------------------- /src/renderer/src/i18n.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable import/no-extraneous-dependencies */ 2 | import i18n from "i18next"; 3 | import { initReactI18next } from "react-i18next"; 4 | import LanguageDetector from "i18next-browser-languagedetector"; 5 | 6 | // Import translation resources 7 | import enTranslation from "./locales/en/translation.json"; 8 | import zhTranslation from "./locales/zh/translation.json"; 9 | 10 | // Configure i18next instance 11 | i18n 12 | // Detect user language 13 | .use(LanguageDetector) 14 | // Pass the i18n instance to react-i18next 15 | .use(initReactI18next) 16 | // Initialize i18next 17 | .init({ 18 | // Default language when detection fails 19 | fallbackLng: "en", 20 | // Debug mode for development 21 | debug: process.env.NODE_ENV === "development", 22 | // Namespaces configuration 23 | defaultNS: "translation", 24 | ns: ["translation"], 25 | // Resources containing translations 26 | resources: { 27 | en: { 28 | translation: enTranslation, 29 | }, 30 | zh: { 31 | translation: zhTranslation, 32 | }, 33 | }, 34 | // Language detection options 35 | detection: { 36 | // Order and from where user language should be detected 37 | order: ["localStorage", "navigator"], 38 | // Cache user language detection 39 | caches: ["localStorage"], 40 | // HTML attribute with which to set language 41 | htmlTag: document.documentElement, 42 | }, 43 | // Escaping special characters 44 | interpolation: { 45 | escapeValue: false, // React already safes from XSS 46 | }, 47 | // React config 48 | react: { 49 | useSuspense: true, 50 | }, 51 | }); 52 | 53 | // Save language change to localStorage 54 | i18n.on("languageChanged", (lng) => { 55 | localStorage.setItem("i18nextLng", lng); 56 | // Update HTML document lang attribute 57 | document.documentElement.lang = lng; 58 | }); 59 | 60 | export default i18n; 61 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/footer/use-footer.ts: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, KeyboardEvent } from 'react'; 2 | import { useVAD } from '@/context/vad-context'; 3 | import { useTextInput } from '@/hooks/footer/use-text-input'; 4 | import { useInterrupt } from '@/hooks/utils/use-interrupt'; 5 | import { useMicToggle } from '@/hooks/utils/use-mic-toggle'; 6 | import { useAiState, AiStateEnum } from '@/context/ai-state-context'; 7 | import { useTriggerSpeak } from '@/hooks/utils/use-trigger-speak'; 8 | import { useProactiveSpeak } from '@/context/proactive-speak-context'; 9 | 10 | export const useFooter = () => { 11 | const { 12 | inputText: inputValue, 13 | setInputText: handleChange, 14 | handleKeyPress: handleKey, 15 | handleCompositionStart, 16 | handleCompositionEnd, 17 | } = useTextInput(); 18 | 19 | const { interrupt } = useInterrupt(); 20 | const { startMic, autoStartMicOn } = useVAD(); 21 | const { handleMicToggle, micOn } = useMicToggle(); 22 | const { setAiState, aiState } = useAiState(); 23 | const { sendTriggerSignal } = useTriggerSpeak(); 24 | const { settings } = useProactiveSpeak(); 25 | 26 | const handleInputChange = (e: ChangeEvent) => { 27 | handleChange({ target: { value: e.target.value } } as ChangeEvent); 28 | setAiState(AiStateEnum.WAITING); 29 | }; 30 | 31 | const handleKeyPress = (e: KeyboardEvent) => { 32 | handleKey(e as any); 33 | }; 34 | 35 | const handleInterrupt = () => { 36 | if (aiState === AiStateEnum.THINKING_SPEAKING) { 37 | interrupt(); 38 | if (autoStartMicOn) { 39 | startMic(); 40 | } 41 | } else if (settings.allowButtonTrigger) { 42 | sendTriggerSignal(-1); 43 | } 44 | }; 45 | 46 | return { 47 | inputValue, 48 | handleInputChange, 49 | handleKeyPress, 50 | handleCompositionStart, 51 | handleCompositionEnd, 52 | handleInterrupt, 53 | handleMicToggle, 54 | micOn, 55 | }; 56 | }; 57 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/src/lapppal.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | /** 9 | * プラットフォーム依存機能を抽象化する Cubism Platform Abstraction Layer. 10 | * 抽象化Cubism平台依赖功能。 11 | * 12 | * ファイル読み込みや時刻取得等のプラットフォームに依存する関数をまとめる。 13 | * 将依赖于平台的功能(如文件读取和时间获取)集中在一起。 14 | */ 15 | export class LAppPal { 16 | /** 17 | * ファイルをバイトデータとして読みこむ 读取文件为字节数据 18 | * 19 | * @param filePath 読み込み対象ファイルのパス [Path to the file to be read] 20 | * @return 21 | * { 22 | * buffer, 読み込んだバイトデータ [Read byte data] 23 | * size ファイルサイズ [File size] 24 | * } 25 | */ 26 | public static loadFileAsBytes( 27 | filePath: string, 28 | callback: (arrayBuffer: ArrayBuffer, size: number) => void 29 | ): void { 30 | fetch(filePath) 31 | .then(response => response.arrayBuffer()) 32 | .then(arrayBuffer => callback(arrayBuffer, arrayBuffer.byteLength)); 33 | } 34 | 35 | /** 36 | * デルタ時間(前回フレームとの差分)を取得する 37 | * @return デルタ時間[ms] 38 | * 39 | * 获取增量时间(与上一帧的差异) 40 | */ 41 | public static getDeltaTime(): number { 42 | return this.s_deltaTime; 43 | } 44 | 45 | public static updateTime(modifyLastFrameTime: boolean = true): void { 46 | this.s_currentFrame = Date.now(); 47 | this.s_deltaTime = (this.s_currentFrame - this.s_lastFrame) / 1000; 48 | // this.s_lastFrame = this.s_currentFrame; 49 | if (modifyLastFrameTime === true) { 50 | this.s_lastFrame = this.s_currentFrame; 51 | } 52 | } 53 | 54 | /** 55 | * メッセージを出力する 56 | * @param message 文字列 57 | * 58 | * 输出消息 59 | */ 60 | public static printMessage(message: string): void { 61 | console.log(message); 62 | } 63 | 64 | static lastUpdate = Date.now(); 65 | 66 | static s_currentFrame = 0.0; 67 | static s_lastFrame = 0.0; 68 | static s_deltaTime = 0.0; 69 | } 70 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/electron/use-input-subtitle.ts: -------------------------------------------------------------------------------- 1 | import { ChangeEvent, KeyboardEvent } from 'react'; 2 | import { useChatHistory } from '@/context/chat-history-context'; 3 | import { useVAD } from '@/context/vad-context'; 4 | import { useMicToggle } from '@/hooks/utils/use-mic-toggle'; 5 | import { useTextInput } from '@/hooks/footer/use-text-input'; 6 | import { useAiState, AiStateEnum } from '@/context/ai-state-context'; 7 | import { useInterrupt } from '@/hooks/utils/use-interrupt'; 8 | 9 | export function useInputSubtitle() { 10 | const { 11 | inputText: inputValue, 12 | setInputText: handleChange, 13 | handleKeyPress: handleKey, 14 | handleCompositionStart, 15 | handleCompositionEnd, 16 | handleSend, 17 | 18 | } = useTextInput(); 19 | 20 | const { messages } = useChatHistory(); 21 | const { startMic, autoStartMicOn } = useVAD(); 22 | const { handleMicToggle, micOn } = useMicToggle(); 23 | const { aiState, setAiState } = useAiState(); 24 | const { interrupt } = useInterrupt(); 25 | 26 | const lastAIMessage = messages 27 | .filter((msg) => msg.role === 'ai') 28 | .slice(-1) 29 | .map((msg) => msg.content)[0]; 30 | 31 | const hasAIMessages = messages.some((msg) => msg.role === 'ai'); 32 | 33 | const handleInterrupt = () => { 34 | interrupt(); 35 | if (autoStartMicOn) { 36 | startMic(); 37 | } 38 | }; 39 | 40 | const handleInputChange = (e: ChangeEvent) => { 41 | handleChange({ target: { value: e.target.value } } as ChangeEvent); 42 | setAiState(AiStateEnum.WAITING); 43 | }; 44 | 45 | const handleKeyPress = (e: KeyboardEvent) => { 46 | handleKey(e as any); 47 | }; 48 | 49 | return { 50 | inputValue, 51 | handleInputChange, 52 | handleKeyPress, 53 | handleCompositionStart, 54 | handleCompositionEnd, 55 | handleInterrupt, 56 | handleMicToggle, 57 | lastAIMessage, 58 | hasAIMessages, 59 | aiState, 60 | micOn, 61 | handleSend, 62 | }; 63 | } 64 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/sidebar/setting/use-live2d-settings.ts: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from 'react'; 2 | import { ModelInfo, useLive2DConfig } from '@/context/live2d-config-context'; 3 | 4 | export const useLive2dSettings = () => { 5 | const Live2DConfigContext = useLive2DConfig(); 6 | 7 | const initialModelInfo: ModelInfo = { 8 | url: '', 9 | kScale: 0.5, 10 | initialXshift: 0, 11 | initialYshift: 0, 12 | emotionMap: {}, 13 | scrollToResize: true, 14 | }; 15 | 16 | const [modelInfo, setModelInfoState] = useState( 17 | Live2DConfigContext?.modelInfo || initialModelInfo, 18 | ); 19 | const [originalModelInfo, setOriginalModelInfo] = useState( 20 | Live2DConfigContext?.modelInfo || initialModelInfo, 21 | ); 22 | 23 | useEffect(() => { 24 | if (Live2DConfigContext?.modelInfo) { 25 | if (JSON.stringify(Live2DConfigContext.modelInfo) !== JSON.stringify(originalModelInfo)) { 26 | setOriginalModelInfo(Live2DConfigContext.modelInfo); 27 | setModelInfoState(Live2DConfigContext.modelInfo); 28 | } 29 | } 30 | }, [Live2DConfigContext?.modelInfo]); 31 | 32 | useEffect(() => { 33 | if (Live2DConfigContext && modelInfo) { 34 | Live2DConfigContext.setModelInfo(modelInfo); 35 | } 36 | }, [modelInfo.pointerInteractive, modelInfo.scrollToResize]); 37 | 38 | const handleInputChange = (key: keyof ModelInfo, value: ModelInfo[keyof ModelInfo]): void => { 39 | setModelInfoState((prev) => ({ ...prev, [key]: value })); 40 | }; 41 | 42 | const handleSave = (): void => { 43 | if (Live2DConfigContext && modelInfo) { 44 | setOriginalModelInfo(modelInfo); 45 | } 46 | }; 47 | 48 | const handleCancel = (): void => { 49 | setModelInfoState(originalModelInfo); 50 | if (Live2DConfigContext && originalModelInfo) { 51 | Live2DConfigContext.setModelInfo(originalModelInfo); 52 | } 53 | }; 54 | 55 | return { 56 | modelInfo, 57 | handleInputChange, 58 | handleSave, 59 | handleCancel, 60 | }; 61 | }; 62 | -------------------------------------------------------------------------------- /src/renderer/src/context/browser-context.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable react/jsx-no-constructed-context-values */ 2 | import { 3 | createContext, useContext, useState, ReactNode, useCallback, 4 | } from 'react'; 5 | 6 | // Define browser view data structure 7 | export interface BrowserViewData { 8 | debuggerFullscreenUrl: string; 9 | debuggerUrl: string; 10 | pages: { 11 | id: string; 12 | url: string; 13 | faviconUrl: string; 14 | title: string; 15 | debuggerUrl: string; 16 | debuggerFullscreenUrl: string; 17 | }[]; 18 | wsUrl: string; 19 | sessionId?: string; // Optional for backward compatibility 20 | } 21 | 22 | // Define context interface 23 | interface BrowserContextType { 24 | browserViewData: BrowserViewData | null; 25 | setBrowserViewData: (data: BrowserViewData) => void; 26 | clearBrowserViewData: () => void; 27 | } 28 | 29 | // Create context with default values 30 | export const BrowserContext = createContext({ 31 | browserViewData: null, 32 | setBrowserViewData: () => {}, 33 | clearBrowserViewData: () => {}, 34 | }); 35 | 36 | // Provider component 37 | export function BrowserProvider({ children }: { children: ReactNode }) { 38 | const [browserViewData, setBrowserViewDataState] = useState(null); 39 | 40 | const setBrowserViewData = useCallback((data: BrowserViewData) => { 41 | setBrowserViewDataState(data); 42 | }, []); 43 | 44 | const clearBrowserViewData = useCallback(() => { 45 | setBrowserViewDataState(null); 46 | }, []); 47 | 48 | return ( 49 | 56 | {children} 57 | 58 | ); 59 | } 60 | 61 | // Custom hook for using the browser context 62 | export function useBrowser() { 63 | const context = useContext(BrowserContext); 64 | if (!context) { 65 | throw new Error('useBrowser must be used within a BrowserProvider'); 66 | } 67 | return context; 68 | } 69 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/footer/use-text-input.tsx: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useWebSocket } from '@/context/websocket-context'; 3 | import { useAiState } from '@/context/ai-state-context'; 4 | import { useInterrupt } from '@/components/canvas/live2d'; 5 | import { useChatHistory } from '@/context/chat-history-context'; 6 | import { useVAD } from '@/context/vad-context'; 7 | import { useMediaCapture } from '@/hooks/utils/use-media-capture'; 8 | 9 | export function useTextInput() { 10 | const [inputText, setInputText] = useState(''); 11 | const [isComposing, setIsComposing] = useState(false); 12 | const wsContext = useWebSocket(); 13 | const { aiState } = useAiState(); 14 | const { interrupt } = useInterrupt(); 15 | const { appendHumanMessage } = useChatHistory(); 16 | const { stopMic, autoStopMic } = useVAD(); 17 | const { captureAllMedia } = useMediaCapture(); 18 | 19 | const handleInputChange = (e: React.ChangeEvent) => { 20 | setInputText(e.target.value); 21 | }; 22 | 23 | const handleSend = async () => { 24 | if (!inputText.trim() || !wsContext) return; 25 | if (aiState === 'thinking-speaking') { 26 | interrupt(); 27 | } 28 | 29 | const images = await captureAllMedia(); 30 | 31 | appendHumanMessage(inputText.trim()); 32 | wsContext.sendMessage({ 33 | type: 'text-input', 34 | text: inputText.trim(), 35 | images, 36 | }); 37 | 38 | if (autoStopMic) stopMic(); 39 | setInputText(''); 40 | }; 41 | 42 | const handleKeyPress = (e: React.KeyboardEvent) => { 43 | if (isComposing) return; 44 | 45 | if (e.key === 'Enter' && !e.shiftKey) { 46 | e.preventDefault(); 47 | handleSend(); 48 | } 49 | }; 50 | 51 | const handleCompositionStart = () => setIsComposing(true); 52 | const handleCompositionEnd = () => setIsComposing(false); 53 | 54 | return { 55 | inputText, 56 | setInputText: handleInputChange, 57 | handleSend, 58 | handleKeyPress, 59 | handleCompositionStart, 60 | handleCompositionEnd, 61 | }; 62 | } 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Open-LLM-VTuber License 1.0 2 | 3 | Copyright © 2025 Open LLM Vtuber. All rights reserved. 4 | 5 | This software is licensed under the Apache License Version 2.0 (the "Apache License"), subject to the following Additional Conditions. 6 | 7 | Additional Conditions 8 | 9 | 1. Definitions 10 | 11 | "Software" refers to the Open-LLM-VTuber project and all of its associated components, including but not limited to source code, executable files, web applications, desktop clients, documentation, and accompanying assets. 12 | 13 | 2. Permitted Uses (No Additional License Required) 14 | 15 | You are granted permission to use the software freely, without requiring a commercial license, under the following circumstances: 16 | 17 | Non-commercial Uses: Any non-commercial activity including, but not limited to, personal projects, educational purposes, academic research, and non-profit initiatives. 18 | 19 | VTuber Streaming and Content Creation: Activities involving VTuber streaming, content creation, broadcasting, and monetization on platforms such as YouTube, Twitch, Bilibili, or other streaming services, provided revenue primarily derives from content creation and not from direct sale, distribution, or subscription to the software itself. 20 | 21 | 3. Uses Requiring a Commercial License 22 | 23 | You must obtain a separate commercial license from Open LLM Vtuber for any of the following uses: 24 | 25 | Monetized Access or Hosting Services: Providing paid access to or hosting of the software—whether original, modified, or derivative—including SaaS, subscription models, pay-per-use, or paid downloads and installations. 26 | 27 | Commercial Redistribution or Rebranding: Redistributing, repackaging, renaming, or otherwise commercially providing the software (original or modified versions) under any alternative brand, identity, or project name in exchange for payment or other commercial benefits. 28 | 29 | Commercial Embedding or Integration: Incorporating this software, wholly or partially, into any commercial software, hardware, or physical product that is sold, licensed, or otherwise distributed for a fee. -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/LICENSE.md: -------------------------------------------------------------------------------- 1 | ## Definitions 2 | 3 | ### Live2D Cubism Components 4 | 5 | Cubism Web Framework is included in Live2D Cubism Components. 6 | 7 | Cubism Web Framework は Live2D Cubism Components に含まれます。 8 | 9 | Cubism Web Framework 包括在 Live2D Cubism Components 中。 10 | 11 | ## Cubism SDK Release License 12 | 13 | *All business* users must obtain a Cubism SDK Release License. "Business" means an entity with the annual gross revenue more than ten million (10,000,000) JPY for the most recent fiscal year. 14 | 15 | * [Cubism SDK Release License](https://www.live2d.com/en/download/cubism-sdk/release-license/) 16 | 17 | 直近会計年度の売上高が 1000 万円以上の事業者様がご利用になる場合は、Cubism SDK リリースライセンス(出版許諾契約)に同意していただく必要がございます。 18 | 19 | * [Cubism SDK リリースライセンス](https://www.live2d.com/ja/download/cubism-sdk/release-license/) 20 | 21 | 如果您的企业在最近一个会计年度的销售额达到或超过1000万日元,您必须得到Cubism SDK的出版授权许可(出版许可协议)。 22 | 23 | * [Cubism SDK发行许可证](https://www.live2d.com/zh-CHS/download/cubism-sdk/release-license/) 24 | 25 | ## Live2D Open Software License 26 | 27 | Live2D Cubism Components is available under Live2D Open Software License. 28 | 29 | * [Live2D Open Software License](https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html) 30 | * [Live2D Open Software 使用許諾契約書](https://www.live2d.com/eula/live2d-open-software-license-agreement_jp.html) 31 | * [Live2D Open Software 使用授权协议](https://www.live2d.com/eula/live2d-open-software-license-agreement_cn.html) 32 | 33 | 34 | ## Live2D Proprietary Software License 35 | 36 | Live2D Cubism Core is available under Live2D Proprietary Software License. 37 | 38 | * [Live2D Proprietary Software License Agreement](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_en.html) 39 | * [Live2D Proprietary Software 使用許諾契約書](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_jp.html) 40 | * [Live2D Proprietary Software 使用授权协议](https://www.live2d.com/eula/live2d-proprietary-software-license-agreement_cn.html) 41 | 42 | 43 | --- 44 | 45 | Please contact us from [here](https://www.live2d.jp/contact/) for more license information. 46 | -------------------------------------------------------------------------------- /src/renderer/src/components/ui/color-mode.tsx: -------------------------------------------------------------------------------- 1 | 'use client'; 2 | 3 | import type { IconButtonProps } from '@chakra-ui/react'; 4 | import { ClientOnly, IconButton, Skeleton } from '@chakra-ui/react'; 5 | import { ThemeProvider, useTheme } from 'next-themes'; 6 | import type { ThemeProviderProps } from 'next-themes'; 7 | import * as React from 'react'; 8 | import { LuMoon, LuSun } from 'react-icons/lu'; 9 | 10 | export interface ColorModeProviderProps extends ThemeProviderProps {} 11 | 12 | export function ColorModeProvider(props: ColorModeProviderProps) { 13 | return ( 14 | 15 | ); 16 | } 17 | 18 | export function useColorMode() { 19 | const { resolvedTheme, setTheme } = useTheme(); 20 | const toggleColorMode = () => { 21 | setTheme(resolvedTheme === 'light' ? 'dark' : 'light'); 22 | }; 23 | return { 24 | colorMode: resolvedTheme, 25 | setColorMode: setTheme, 26 | toggleColorMode, 27 | }; 28 | } 29 | 30 | export function useColorModeValue(light: T, dark: T) { 31 | const { colorMode } = useColorMode(); 32 | return colorMode === 'light' ? light : dark; 33 | } 34 | 35 | export function ColorModeIcon() { 36 | const { colorMode } = useColorMode(); 37 | return colorMode === 'light' ? : ; 38 | } 39 | 40 | interface ColorModeButtonProps extends Omit {} 41 | 42 | export const ColorModeButton = React.forwardRef< 43 | HTMLButtonElement, 44 | ColorModeButtonProps 45 | >((props, ref) => { 46 | const { toggleColorMode } = useColorMode(); 47 | return ( 48 | }> 49 | 63 | 64 | 65 | 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /src/renderer/WebSDK/Framework/src/type/csmrectf.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright(c) Live2D Inc. All rights reserved. 3 | * 4 | * Use of this source code is governed by the Live2D Open Software license 5 | * that can be found at https://www.live2d.com/eula/live2d-open-software-license-agreement_en.html. 6 | */ 7 | 8 | /** 9 | * 矩形形状(座標・長さはfloat値)を定義するクラス 10 | */ 11 | export class csmRect { 12 | /** 13 | * コンストラクタ 14 | * @param x 左端X座標 15 | * @param y 上端Y座標 16 | * @param w 幅 17 | * @param h 高さ 18 | */ 19 | public constructor(x?: number, y?: number, w?: number, h?: number) { 20 | this.x = x; 21 | this.y = y; 22 | this.width = w; 23 | this.height = h; 24 | } 25 | 26 | /** 27 | * 矩形中央のX座標を取得する 28 | */ 29 | public getCenterX(): number { 30 | return this.x + 0.5 * this.width; 31 | } 32 | 33 | /** 34 | * 矩形中央のY座標を取得する 35 | */ 36 | public getCenterY(): number { 37 | return this.y + 0.5 * this.height; 38 | } 39 | 40 | /** 41 | * 右側のX座標を取得する 42 | */ 43 | public getRight(): number { 44 | return this.x + this.width; 45 | } 46 | 47 | /** 48 | * 下端のY座標を取得する 49 | */ 50 | public getBottom(): number { 51 | return this.y + this.height; 52 | } 53 | 54 | /** 55 | * 矩形に値をセットする 56 | * @param r 矩形のインスタンス 57 | */ 58 | public setRect(r: csmRect): void { 59 | this.x = r.x; 60 | this.y = r.y; 61 | this.width = r.width; 62 | this.height = r.height; 63 | } 64 | 65 | /** 66 | * 矩形中央を軸にして縦横を拡縮する 67 | * @param w 幅方向に拡縮する量 68 | * @param h 高さ方向に拡縮する量 69 | */ 70 | public expand(w: number, h: number) { 71 | this.x -= w; 72 | this.y -= h; 73 | this.width += w * 2.0; 74 | this.height += h * 2.0; 75 | } 76 | 77 | public x: number; // 左端X座標 78 | public y: number; // 上端Y座標 79 | public width: number; // 幅 80 | public height: number; // 高さ 81 | } 82 | 83 | // Namespace definition for compatibility. 84 | import * as $ from './csmrectf'; 85 | // eslint-disable-next-line @typescript-eslint/no-namespace 86 | export namespace Live2DCubismFramework { 87 | export const csmRect = $.csmRect; 88 | export type csmRect = $.csmRect; 89 | } 90 | -------------------------------------------------------------------------------- /src/renderer/src/utils/task-queue.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-promise-executor-return */ 2 | /* eslint-disable arrow-parens */ 3 | export class TaskQueue { 4 | private queue: (() => Promise)[] = []; 5 | 6 | private running = false; 7 | 8 | private taskInterval: number; 9 | 10 | private pendingComplete = false; 11 | 12 | private activeTasks = new Set>(); 13 | 14 | constructor(taskIntervalMs = 3000) { 15 | this.taskInterval = taskIntervalMs; 16 | } 17 | 18 | addTask(task: () => Promise) { 19 | this.queue.push(task); 20 | this.runNextTask(); 21 | } 22 | 23 | clearQueue() { 24 | this.queue = []; 25 | this.activeTasks.clear(); 26 | this.running = false; 27 | } 28 | 29 | private async runNextTask() { 30 | if (this.running || this.queue.length === 0) { 31 | if (this.queue.length === 0 && this.activeTasks.size === 0 && this.pendingComplete) { 32 | this.pendingComplete = false; 33 | await new Promise(resolve => setTimeout(resolve, this.taskInterval)); 34 | } 35 | return; 36 | } 37 | 38 | this.running = true; 39 | const task = this.queue.shift(); 40 | if (task) { 41 | const taskPromise = task(); 42 | this.activeTasks.add(taskPromise); 43 | 44 | try { 45 | await taskPromise; 46 | await new Promise(resolve => setTimeout(resolve, this.taskInterval)); 47 | } catch (error) { 48 | console.error('Task Queue Error', error); 49 | } finally { 50 | this.activeTasks.delete(taskPromise); 51 | this.running = false; 52 | this.runNextTask(); 53 | } 54 | } 55 | } 56 | 57 | public hasTask(): boolean { 58 | return this.queue.length > 0 || this.activeTasks.size > 0 || this.running; 59 | } 60 | 61 | public waitForCompletion(): Promise { 62 | this.pendingComplete = true; 63 | return new Promise((resolve) => { 64 | const check = () => { 65 | if (!this.hasTask()) { 66 | resolve(); 67 | } else { 68 | setTimeout(check, 100); 69 | } 70 | }; 71 | check(); 72 | }); 73 | } 74 | } 75 | 76 | export const audioTaskQueue = new TaskQueue(20); 77 | -------------------------------------------------------------------------------- /src/renderer/src/context/subtitle-context.tsx: -------------------------------------------------------------------------------- 1 | import { 2 | createContext, useState, useMemo, useContext, memo, 3 | } from 'react'; 4 | 5 | /** 6 | * Subtitle context state interface 7 | * @interface SubtitleState 8 | */ 9 | interface SubtitleState { 10 | /** Current subtitle text */ 11 | subtitleText: string 12 | 13 | /** Set subtitle text */ 14 | setSubtitleText: (text: string) => void 15 | 16 | /** Whether to show subtitle */ 17 | showSubtitle: boolean 18 | 19 | /** Toggle subtitle visibility */ 20 | setShowSubtitle: (show: boolean) => void 21 | } 22 | 23 | /** 24 | * Default values and constants 25 | */ 26 | const DEFAULT_SUBTITLE = { 27 | text: "Hi, I'm some random AI VTuber. Who the hell are ya? " 28 | + 'Ahh, you must be amazed by my awesomeness, right? right?', 29 | }; 30 | 31 | /** 32 | * Create the subtitle context 33 | */ 34 | export const SubtitleContext = createContext(null); 35 | 36 | /** 37 | * Subtitle Provider Component 38 | * Manages the subtitle display text state 39 | * 40 | * @param {Object} props - Provider props 41 | * @param {React.ReactNode} props.children - Child components 42 | */ 43 | export const SubtitleProvider = memo(({ children }: { children: React.ReactNode }) => { 44 | // State management 45 | const [subtitleText, setSubtitleText] = useState(DEFAULT_SUBTITLE.text); 46 | const [showSubtitle, setShowSubtitle] = useState(true); 47 | 48 | // Memoized context value 49 | const contextValue = useMemo( 50 | () => ({ 51 | subtitleText, 52 | setSubtitleText, 53 | showSubtitle, 54 | setShowSubtitle, 55 | }), 56 | [subtitleText, showSubtitle], 57 | ); 58 | 59 | return ( 60 | 61 | {children} 62 | 63 | ); 64 | }); 65 | 66 | /** 67 | * Custom hook to use the subtitle context 68 | * @throws {Error} If used outside of SubtitleProvider 69 | */ 70 | export function useSubtitle() { 71 | const context = useContext(SubtitleContext); 72 | 73 | if (!context) { 74 | throw new Error('useSubtitle must be used within a SubtitleProvider'); 75 | } 76 | 77 | return context; 78 | } 79 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/sidebar/use-group-drawer.tsx: -------------------------------------------------------------------------------- 1 | import { useState, useCallback } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useWebSocket } from '@/context/websocket-context'; 4 | import { toaster } from '@/components/ui/toaster'; 5 | 6 | export const useGroupDrawer = () => { 7 | const { t } = useTranslation(); 8 | const [isOpen, setIsOpen] = useState(false); 9 | const [inviteUid, setInviteUid] = useState(''); 10 | const { sendMessage } = useWebSocket(); 11 | 12 | // Request latest group information to update the display 13 | const requestGroupInfo = useCallback(() => { 14 | sendMessage({ 15 | type: 'request-group-info', 16 | }); 17 | }, [sendMessage]); 18 | 19 | const handleInvite = useCallback(async () => { 20 | if (!inviteUid.trim()) { 21 | toaster.create({ 22 | title: t('error.enterValidUuid'), 23 | type: 'error', 24 | duration: 2000, 25 | }); 26 | return; 27 | } 28 | 29 | sendMessage({ 30 | type: 'add-client-to-group', 31 | invitee_uid: inviteUid.trim(), 32 | }); 33 | setInviteUid(''); 34 | 35 | // Add a small delay to ensure server has processed the operation 36 | setTimeout(requestGroupInfo, 100); 37 | }, [inviteUid, sendMessage, requestGroupInfo, t]); 38 | 39 | const handleRemove = useCallback((targetUid: string) => { 40 | sendMessage({ 41 | type: 'remove-client-from-group', 42 | target_uid: targetUid, 43 | }); 44 | 45 | // Add a small delay to ensure server has processed the operation 46 | setTimeout(requestGroupInfo, 100); 47 | }, [sendMessage, requestGroupInfo]); 48 | 49 | const handleLeaveGroup = useCallback((selfUid: string) => { 50 | sendMessage({ 51 | type: 'remove-client-from-group', 52 | target_uid: selfUid, 53 | }); 54 | 55 | // Add a small delay to ensure server has processed the operation 56 | setTimeout(requestGroupInfo, 100); 57 | }, [sendMessage, requestGroupInfo]); 58 | 59 | return { 60 | isOpen, 61 | setIsOpen, 62 | inviteUid, 63 | setInviteUid, 64 | handleInvite, 65 | handleRemove, 66 | handleLeaveGroup, 67 | requestGroupInfo, 68 | }; 69 | }; 70 | -------------------------------------------------------------------------------- /electron.vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path'; 2 | import { defineConfig, externalizeDepsPlugin } from 'electron-vite'; 3 | import react from '@vitejs/plugin-react'; 4 | import { viteStaticCopy } from 'vite-plugin-static-copy' 5 | import { normalizePath } from 'vite'; 6 | 7 | export default defineConfig({ 8 | main: { 9 | plugins: [externalizeDepsPlugin()], 10 | }, 11 | preload: { 12 | plugins: [externalizeDepsPlugin()], 13 | }, 14 | renderer: { 15 | resolve: { 16 | alias: { 17 | '@': resolve('src/renderer/src'), 18 | "@framework": resolve("src/renderer/WebSDK/Framework/src"), 19 | "@cubismsdksamples": resolve("src/renderer/WebSDK/src"), 20 | "@motionsyncframework": resolve( 21 | "src/renderer/MotionSync/Framework/src", 22 | ), 23 | "@motionsync": resolve("src/renderer/MotionSync/src"), 24 | "/src": resolve("src/renderer/src"), 25 | }, 26 | }, 27 | plugins: [ 28 | viteStaticCopy({ 29 | targets: [ 30 | { 31 | src: normalizePath(resolve(__dirname, 'node_modules/@ricky0123/vad-web/dist/vad.worklet.bundle.min.js')), 32 | dest: './libs/', 33 | }, 34 | { 35 | src: normalizePath(resolve(__dirname, 'node_modules/@ricky0123/vad-web/dist/silero_vad_v5.onnx')), 36 | dest: './libs/', 37 | }, 38 | { 39 | src: normalizePath(resolve(__dirname, 'node_modules/@ricky0123/vad-web/dist/silero_vad_legacy.onnx')), 40 | dest: './libs/', 41 | }, 42 | { 43 | src: normalizePath(resolve(__dirname, 'node_modules/onnxruntime-web/dist/*.wasm')), 44 | dest: './libs/', 45 | }, 46 | { 47 | src: normalizePath(resolve(__dirname, 'src/renderer/WebSDK/Core/live2dcubismcore.js')), 48 | dest: './libs/' 49 | } 50 | ], 51 | }), 52 | react(), 53 | ], 54 | build: { 55 | rollupOptions: { 56 | onwarn(warning, warn) { 57 | if (warning.message.includes('onnxruntime')) { 58 | return; 59 | } 60 | warn(warning); 61 | }, 62 | }, 63 | }, 64 | }, 65 | }); 66 | -------------------------------------------------------------------------------- /src/renderer/src/hooks/sidebar/use-history-drawer.ts: -------------------------------------------------------------------------------- 1 | import { useState } from 'react'; 2 | import { useTranslation } from 'react-i18next'; 3 | import { useChatHistory } from '@/context/chat-history-context'; 4 | import { useWebSocket, HistoryInfo } from '@/context/websocket-context'; 5 | import { toaster } from '@/components/ui/toaster'; 6 | 7 | export const useHistoryDrawer = () => { 8 | const { t } = useTranslation(); 9 | const [open, setOpen] = useState(false); 10 | const { 11 | historyList, 12 | currentHistoryUid, 13 | setCurrentHistoryUid, 14 | setHistoryList, 15 | messages, 16 | updateHistoryList, 17 | } = useChatHistory(); 18 | const { sendMessage } = useWebSocket(); 19 | 20 | const fetchAndSetHistory = (uid: string) => { 21 | if (!uid || uid === currentHistoryUid) return; 22 | 23 | if (currentHistoryUid && messages.length > 0) { 24 | const latestMessage = messages[messages.length - 1]; 25 | updateHistoryList(currentHistoryUid, latestMessage); 26 | } 27 | 28 | setCurrentHistoryUid(uid); 29 | sendMessage({ 30 | type: 'fetch-and-set-history', 31 | history_uid: uid, 32 | }); 33 | }; 34 | 35 | const deleteHistory = (uid: string) => { 36 | if (uid === currentHistoryUid) { 37 | toaster.create({ 38 | title: t('error.cannotDeleteCurrentHistory'), 39 | type: 'warning', 40 | duration: 2000, 41 | }); 42 | return; 43 | } 44 | 45 | sendMessage({ 46 | type: 'delete-history', 47 | history_uid: uid, 48 | }); 49 | setHistoryList(historyList.filter((history) => history.uid !== uid)); 50 | }; 51 | 52 | const getLatestMessageContent = (history: HistoryInfo) => { 53 | if (history.uid === currentHistoryUid && messages.length > 0) { 54 | const latestMessage = messages[messages.length - 1]; 55 | return { 56 | content: latestMessage.content, 57 | timestamp: latestMessage.timestamp, 58 | }; 59 | } 60 | return { 61 | content: history.latest_message?.content || '', 62 | timestamp: history.timestamp, 63 | }; 64 | }; 65 | 66 | return { 67 | open, 68 | setOpen, 69 | historyList, 70 | currentHistoryUid, 71 | fetchAndSetHistory, 72 | deleteHistory, 73 | getLatestMessageContent, 74 | }; 75 | }; 76 | -------------------------------------------------------------------------------- /src/renderer/src/main.tsx: -------------------------------------------------------------------------------- 1 | import { createRoot } from 'react-dom/client'; 2 | import './index.css'; 3 | import App from './App'; 4 | import { LAppAdapter } from '../WebSDK/src/lappadapter'; 5 | import './i18n'; 6 | 7 | const originalConsoleWarn = console.warn; 8 | console.warn = (...args) => { 9 | if (typeof args[0] === 'string' && args[0].includes('onnxruntime')) { 10 | return; 11 | } 12 | originalConsoleWarn.apply(console, args); 13 | }; 14 | 15 | // Suppress specific console.error messages from @chatscope/chat-ui-kit-react 16 | const originalConsoleError = console.error; 17 | const errorMessagesToIgnore = ["Warning: Failed"]; 18 | console.error = (...args: any[]) => { 19 | if (typeof args[0] === 'string') { 20 | const shouldIgnore = errorMessagesToIgnore.some(msg => args[0].startsWith(msg)); 21 | if (shouldIgnore) { 22 | return; // Suppress the warning 23 | } 24 | } 25 | // Call the original console.error for other messages 26 | originalConsoleError.apply(console, args); 27 | }; 28 | 29 | if (typeof window !== 'undefined') { 30 | (window as any).getLAppAdapter = () => LAppAdapter.getInstance(); 31 | 32 | // Dynamically load the Live2D Core script 33 | const loadLive2DCore = () => { 34 | return new Promise((resolve, reject) => { 35 | const script = document.createElement('script'); 36 | script.src = './libs/live2dcubismcore.js'; // Path to the copied script 37 | script.onload = () => { 38 | console.log('Live2D Cubism Core loaded successfully.'); 39 | resolve(); 40 | }; 41 | script.onerror = (error) => { 42 | console.error('Failed to load Live2D Cubism Core:', error); 43 | reject(error); 44 | }; 45 | document.head.appendChild(script); 46 | }); 47 | }; 48 | 49 | // Load the script and then render the app 50 | loadLive2DCore() 51 | .then(() => { 52 | createRoot(document.getElementById('root')!).render( 53 | , 54 | ); 55 | }) 56 | .catch((error) => { 57 | console.error('Application failed to start due to script loading error:', error); 58 | // Optionally render an error message to the user 59 | const rootElement = document.getElementById('root'); 60 | if (rootElement) { 61 | rootElement.innerHTML = 'Error loading required components. Please check the console for details.'; 62 | } 63 | }); 64 | } 65 | -------------------------------------------------------------------------------- /src/renderer/src/components/sidebar/browser-panel.tsx: -------------------------------------------------------------------------------- 1 | /* eslint-disable jsx-a11y/iframe-has-title */ 2 | import { Box, Text } from "@chakra-ui/react"; 3 | import { FiGlobe } from "react-icons/fi"; 4 | import { useState } from "react"; 5 | import { useTranslation } from "react-i18next"; 6 | import { Tooltip } from "@/components/ui/tooltip"; 7 | import { sidebarStyles } from "./sidebar-styles"; 8 | import { useBrowser } from "@/context/browser-context"; 9 | 10 | function BrowserPlaceholder() { 11 | const { t } = useTranslation(); 12 | 13 | return ( 14 | 21 | 22 | 23 | {t('sidebar.noBrowserSession')} 24 | 25 | 26 | ); 27 | } 28 | 29 | function BrowserPanel(): JSX.Element { 30 | const { t } = useTranslation(); 31 | const { browserViewData } = useBrowser(); 32 | const [isHovering, setIsHovering] = useState(false); 33 | 34 | const handleMouseEnter = () => setIsHovering(true); 35 | const handleMouseLeave = () => setIsHovering(false); 36 | 37 | return ( 38 | 39 | 40 | {browserViewData && ( 41 | {t('sidebar.browserSession')} 42 | )} 43 | 44 | 45 | 54 | 60 | {browserViewData ? ( 61 |