-
Notifications
You must be signed in to change notification settings - Fork 20
Add user chat feature with text input field #39
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
7a9cf25
e594202
fa5c05a
4e36141
68713f8
63dc277
af29c8f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,7 @@ docs/_static/audio2face | |
| # next.js | ||
| /.next/ | ||
| /out/ | ||
| package-lock.json | ||
|
|
||
| # production | ||
| /build | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| 'use client' | ||
|
|
||
| import React, { useState, useEffect, useRef } from 'react' | ||
| import { useTranslation } from 'react-i18next' | ||
|
|
||
| /** | ||
| * Props for the ChatModeMenu component. | ||
| */ | ||
| interface ChatModeMenuProps { | ||
| /** | ||
| * Current chat mode. | ||
| */ | ||
| chatMode: 'text' | 'voice' | ||
| /** | ||
| * Callback function called when a chat mode is selected. | ||
| * @param mode The selected chat mode. | ||
| */ | ||
| onModeSelect: (mode: 'text' | 'voice') => void | ||
| } | ||
|
|
||
| /** | ||
| * ChatModeMenu | ||
| * | ||
| * A menu component for selecting chat modes (Text Chat, Voice Chat). | ||
| * Displays a button that opens a popup menu with mode options. | ||
| * | ||
| * @param chatMode Current chat mode. | ||
| * @param onModeSelect Callback function called when a mode is selected. | ||
| * @returns JSX.Element The chat mode menu UI component. | ||
| */ | ||
| export default function ChatModeMenu({ chatMode, onModeSelect }: ChatModeMenuProps) { | ||
| const { t } = useTranslation('fronted') | ||
| const [showChatModeMenu, setShowChatModeMenu] = useState(false) | ||
| const chatModeMenuRef = useRef<HTMLDivElement>(null) | ||
|
|
||
| /** | ||
| * Close chat mode menu when clicking outside. | ||
| */ | ||
| useEffect(() => { | ||
| /** | ||
| * Handle click outside event to close the menu. | ||
| * | ||
| * @param event The mouse event. | ||
| * @returns void | ||
| */ | ||
| function handleClickOutside(event: MouseEvent) { | ||
| if ( | ||
| chatModeMenuRef.current && | ||
| !chatModeMenuRef.current.contains(event.target as Node) | ||
| ) { | ||
| setShowChatModeMenu(false) | ||
| } | ||
| } | ||
|
|
||
| if (showChatModeMenu) { | ||
| document.addEventListener('mousedown', handleClickOutside) | ||
| return () => document.removeEventListener('mousedown', handleClickOutside) | ||
| } | ||
| }, [showChatModeMenu]) | ||
|
|
||
| /** | ||
| * Get the display text for the current chat mode. | ||
| * | ||
| * @returns The translated text for the current chat mode. | ||
| */ | ||
| const getChatModeText = () => { | ||
| switch (chatMode) { | ||
| case 'text': | ||
| return t('chat.textChat') | ||
| case 'voice': | ||
| return t('chat.voiceChat') | ||
| default: | ||
| return t('chat.voiceChat') | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle chat mode selection. | ||
| * | ||
| * Calls the onModeSelect callback and closes the menu. | ||
| * | ||
| * @param mode The selected chat mode. | ||
| * @returns void | ||
| */ | ||
| const handleChatModeSelect = (mode: 'text' | 'voice') => { | ||
| onModeSelect(mode) | ||
| setShowChatModeMenu(false) | ||
| } | ||
|
|
||
| return ( | ||
| <div | ||
| ref={chatModeMenuRef} | ||
| style={{ | ||
| position: 'fixed', | ||
| bottom: '40px', | ||
| left: '20px', | ||
| zIndex: 1000, | ||
| }} | ||
| > | ||
| <button | ||
| className="chatSwitch-btn" | ||
| onClick={() => setShowChatModeMenu(!showChatModeMenu)} | ||
| > | ||
| {getChatModeText()} | ||
| </button> | ||
|
|
||
| {showChatModeMenu && ( | ||
| <div className="chat-mode-menu"> | ||
| <button | ||
| className={`chat-mode-menu-item ${chatMode === 'text' ? 'active' : ''}`} | ||
| onClick={() => handleChatModeSelect('text')} | ||
| > | ||
| {t('chat.textChat')} | ||
| </button> | ||
| <button | ||
| className={`chat-mode-menu-item ${chatMode === 'voice' ? 'active' : ''}`} | ||
| onClick={() => handleChatModeSelect('voice')} | ||
| > | ||
| {t('chat.voiceChat')} | ||
| </button> | ||
| </div> | ||
| )} | ||
| </div> | ||
| ) | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,91 @@ | ||
| 'use client' | ||
|
|
||
| import React, { useState } from 'react' | ||
| import { useTranslation } from 'react-i18next' | ||
| import { GlobalState } from '@/library/babylonjs/core' | ||
| import { | ||
| Conditions, | ||
| ConditionedMessage, | ||
| } from '@/library/babylonjs/runtime/fsm/conditions' | ||
|
|
||
| /** | ||
| * Props for the Chatbox component. | ||
| */ | ||
| interface ChatboxProps { | ||
| /** | ||
| * Global state for accessing BabylonJS scene and runtime. | ||
| */ | ||
| globalState?: GlobalState | ||
| } | ||
|
|
||
| /** | ||
| * Chatbox | ||
| * | ||
| * A text input component for sending chat messages. | ||
| * Displays a textarea and send button, positioned at the bottom center of the screen. | ||
| * | ||
| * @param globalState Global state for accessing BabylonJS scene and runtime. | ||
| * @returns JSX.Element The chatbox UI component. | ||
| */ | ||
| export default function Chatbox({ globalState }: ChatboxProps) { | ||
| const { t } = useTranslation('fronted') | ||
| const [chatMessage, setChatMessage] = useState('') | ||
|
|
||
| /** | ||
| * Handle sending chat message. | ||
| * | ||
| * Sends the current chat message to the state machine. If an animation is currently playing, | ||
| * it will interrupt the animation before sending the message. | ||
| * | ||
| * @returns void | ||
| */ | ||
| const handleSendMessage = () => { | ||
| if (chatMessage.trim()) { | ||
| console.log('[Handle Text Message]:', chatMessage) | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 日志可以移除或改为debug级别 |
||
| if (globalState?.runtime?.streamedAnimationPlaying()) { | ||
| globalState?.stateMachine?.putConditionedMessage( | ||
| new ConditionedMessage(Conditions.USER_TEXT_INTERRUPT_ANIMATION, null), | ||
| ) | ||
| } | ||
| globalState?.stateMachine?.putConditionedMessage( | ||
| new ConditionedMessage(Conditions.USER_TEXT_INPUT, { message: chatMessage }), | ||
| ) | ||
| setChatMessage('') | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Handle Enter key press in textarea. | ||
| * | ||
| * Sends the message when Enter is pressed without Shift. Prevents default behavior | ||
| * to avoid adding a new line. | ||
| * | ||
| * @param e The keyboard event from the textarea. | ||
| * @returns void | ||
| */ | ||
| const handleTextareaKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => { | ||
| if (e.key === 'Enter' && !e.shiftKey) { | ||
| e.preventDefault() | ||
| handleSendMessage() | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <div className="chatbox"> | ||
| <textarea | ||
| className="chatbox-textarea" | ||
| placeholder={t('chat.inputMessagePlaceholder')} | ||
| value={chatMessage} | ||
| onChange={e => setChatMessage(e.target.value)} | ||
| onKeyDown={handleTextareaKeyDown} | ||
| /> | ||
| <button | ||
| className="chatbox-send-btn" | ||
| onClick={handleSendMessage} | ||
| disabled={!chatMessage.trim()} | ||
| > | ||
| {t('chat.send')} | ||
| </button> | ||
| </div> | ||
| ) | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -19,6 +19,8 @@ import { AudioRecordState } from '@/data_structures/audioStreamState' | |
| import { WebSocketConnectionState } from '@/data_structures/webSocketState' | ||
| import * as orchestrator_v4 from '@/library/babylonjs/runtime/io/orchestrator_v4_pb' | ||
| import { uint8Array2ArrayBuffer } from '@/library/babylonjs/utils/array' | ||
| import Chatbox from '@/components/layout/Chatbox' | ||
| import ChatModeMenu from '@/components/layout/ChatModeMenu' | ||
|
|
||
| /** | ||
| * React context carrying BabylonJS canvas ref and global state. | ||
|
|
@@ -50,6 +52,7 @@ function BabylonJSProvider({ | |
| const [globalState, setGlobalState] = useState<GlobalState>() | ||
| const webSocketState = useWebSocket() | ||
| const [isSceneInitialized, setIsSceneInitialized] = useState(false) | ||
| const [chatMode, setChatMode] = useState<'text' | 'voice'>('voice') | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 是否考虑在浏览器中存储上一次使用的聊天模式?语言选项(中文或英文)应该是缓存的,下次进入时仍然会使用之前的选项 |
||
|
|
||
| // Add PCM queue to store audio data when websocket is not ready | ||
| const pcmQueue = useRef<ArrayBuffer[]>([]) | ||
|
|
@@ -274,9 +277,24 @@ function BabylonJSProvider({ | |
| } | ||
| }, [globalState, audioStreamState]) | ||
|
|
||
| /** | ||
| * Handle chat mode selection. | ||
| * Updates the local state and the record audio button visibility. | ||
| */ | ||
| const handleChatModeSelect = (mode: 'text' | 'voice') => { | ||
| setChatMode(mode) | ||
| const isVoiceMode = mode === 'voice' | ||
| if (globalState?.gui?.recordAudioButton) { | ||
| globalState.gui.recordAudioButton.isVisible = isVoiceMode | ||
| } | ||
| } | ||
|
|
||
| return ( | ||
| <BabylonJSContext.Provider value={{ canvas, globalState }}> | ||
| {children} | ||
| <ChatModeMenu chatMode={chatMode} onModeSelect={handleChatModeSelect} /> | ||
|
|
||
| {chatMode === 'text' && <Chatbox globalState={globalState} />} | ||
| </BabylonJSContext.Provider> | ||
| ) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -72,7 +72,16 @@ | |||
| "chat": "Chat", | ||||
| "chatList": "Chat List", | ||||
| "loginToChat": "Login to Chat", | ||||
| "myConversations": "My conversations" | ||||
| "myConversations": "My conversations", | ||||
| "textChat": "Text Chat", | ||||
| "voiceChat": "Voice Chat", | ||||
| "inputMessagePlaceholder": "Enter message...", | ||||
| "send": "Send", | ||||
| "autoMode": "Auto Mode", | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
此处没有被实际使用到 |
||||
| "confirm": "Confirm", | ||||
| "disconnect": "Disconnect", | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 缺少connect
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. confirm 其实是 connect,这是livestream 用的
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 这里评论的原因是发现中英不一致,英文有confirm没有connect,中文有connect没有confirm,需要统一 |
||||
| "notConnected": "Not Connected", | ||||
| "connected": "Connected" | ||||
| }, | ||||
| "sidebar": { | ||||
| "model": "3D model", | ||||
|
|
||||
| Original file line number | Diff line number | Diff line change | ||
|---|---|---|---|---|
|
|
@@ -72,7 +72,16 @@ | |||
| "chat": "开始对话", | ||||
| "chatList": "对话列表", | ||||
| "loginToChat": "登录以开始对话", | ||||
| "myConversations": "我的对话" | ||||
| "myConversations": "我的对话", | ||||
| "textChat": "文本聊天", | ||||
| "voiceChat": "语音聊天", | ||||
| "inputMessagePlaceholder": "输入消息...", | ||||
| "send": "发送", | ||||
| "autoMode": "自动模式", | ||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
此处没有被实际使用到 |
||||
| "connect": "连接", | ||||
| "disconnect": "断开连接", | ||||
| "notConnected": "未连接", | ||||
| "connected": "已连接" | ||||
| }, | ||||
| "sidebar": { | ||||
| "model": "3D模型", | ||||
|
|
||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
目前使用pnpm进行包管理,不会再出现npm build时生成的
package-lock.json