RecordButton.tsx 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159
  1. import { useState, useRef } from 'react'
  2. import { useAppStore } from '@/stores/appStore'
  3. interface RecordButtonProps {
  4. onPress: () => void
  5. onRelease: () => void
  6. disabled?: boolean
  7. }
  8. export default function RecordButton({ onPress, onRelease, disabled = false }: RecordButtonProps) {
  9. const isRecording = useAppStore((state) => state.isRecording)
  10. const isPlaying = useAppStore((state) => state.isPlaying)
  11. const isConnected = useAppStore((state) => state.isConnected)
  12. const animationState = useAppStore((state) => state.animationState)
  13. // 本地状态:用于立即响应按钮按下/松开
  14. const [isPressed, setIsPressed] = useState(false)
  15. const isPressing = useRef(false)
  16. const handleMouseDown = (e?: React.MouseEvent) => {
  17. if (e) e.preventDefault()
  18. console.log('按钮按下 - state:', animationState, 'disabled:', disabled, 'isConnected:', isConnected)
  19. // 允许录音(只要不在播放中)
  20. if (!disabled && !isPlaying && isConnected) {
  21. console.log('触发onPress')
  22. setIsPressed(true)
  23. isPressing.current = true
  24. onPress()
  25. }
  26. }
  27. const handleMouseUp = () => {
  28. console.log('按钮松开 - isRecording:', isRecording, 'isPressing:', isPressing.current)
  29. // 立即更新本地状态
  30. setIsPressed(false)
  31. if (isPressing.current) {
  32. console.log('触发onRelease')
  33. isPressing.current = false
  34. onRelease()
  35. }
  36. }
  37. const handleTouchStart = (e: React.TouchEvent) => {
  38. e.preventDefault() // 防止触发鼠标事件和默认行为
  39. console.log('触摸开始 - state:', animationState)
  40. // 允许录音
  41. if (!disabled && !isPlaying && isConnected) {
  42. setIsPressed(true)
  43. isPressing.current = true
  44. onPress()
  45. }
  46. }
  47. const handleTouchEnd = (e: React.TouchEvent) => {
  48. e.preventDefault()
  49. console.log('触摸结束')
  50. setIsPressed(false)
  51. if (isPressing.current) {
  52. isPressing.current = false
  53. onRelease()
  54. }
  55. }
  56. const handleTouchCancel = (e: React.TouchEvent) => {
  57. e.preventDefault()
  58. console.log('触摸取消')
  59. setIsPressed(false)
  60. if (isPressing.current) {
  61. isPressing.current = false
  62. onRelease()
  63. }
  64. }
  65. // 阻止长按弹出右键菜单
  66. const handleContextMenu = (e: React.MouseEvent) => {
  67. e.preventDefault()
  68. console.log('阻止右键菜单')
  69. }
  70. const getButtonStyle = () => {
  71. if (disabled || !isConnected) {
  72. return 'bg-gray-400 cursor-not-allowed'
  73. }
  74. // 使用本地状态isPressed来立即响应
  75. if (isPressed || isRecording) {
  76. return 'bg-red-500 hover:bg-red-600 scale-110 shadow-2xl'
  77. }
  78. if (isPlaying) {
  79. return 'bg-purple-400 cursor-not-allowed'
  80. }
  81. return 'bg-blue-500 hover:bg-blue-600 active:scale-95'
  82. }
  83. const getButtonText = () => {
  84. if (!isConnected) return '连接中...'
  85. if (isPressed || isRecording) return '松开停止'
  86. if (animationState === 'PROCESSING') return 'AI思考中...'
  87. if (animationState === 'SPEAKING' || isPlaying) return 'AI回复中...'
  88. return '按住说话'
  89. }
  90. return (
  91. <div className="flex flex-col items-center space-y-4">
  92. {/* 录音按钮 */}
  93. <button
  94. className={`
  95. w-32 h-32 rounded-full text-white font-bold text-lg
  96. transition-all duration-200 transform
  97. flex items-center justify-center
  98. select-none
  99. ${getButtonStyle()}
  100. `}
  101. style={{
  102. WebkitTouchCallout: 'none',
  103. WebkitUserSelect: 'none',
  104. touchAction: 'none',
  105. }}
  106. onMouseDown={handleMouseDown}
  107. onMouseUp={handleMouseUp}
  108. onMouseLeave={handleMouseUp}
  109. onTouchStart={handleTouchStart}
  110. onTouchEnd={handleTouchEnd}
  111. onTouchCancel={handleTouchCancel}
  112. onContextMenu={handleContextMenu}
  113. disabled={disabled || !isConnected}
  114. >
  115. <div className="flex flex-col items-center">
  116. <span className="text-3xl mb-1">🎤</span>
  117. <span className="text-sm">{getButtonText()}</span>
  118. </div>
  119. </button>
  120. {/* 使用提示 */}
  121. <div className="text-center text-sm text-gray-600 max-w-xs">
  122. {!isConnected ? (
  123. <p className="text-orange-500">正在连接服务器...</p>
  124. ) : (
  125. <>
  126. <p className="mb-1">按住按钮开始说话</p>
  127. <p className="text-xs text-gray-500">松开按钮听取AI回复</p>
  128. </>
  129. )}
  130. </div>
  131. {/* 录音状态指示器 */}
  132. {(isPressed || isRecording) && (
  133. <div className="flex items-center space-x-2 text-red-500 animate-pulse">
  134. <div className="w-3 h-3 bg-red-500 rounded-full"></div>
  135. <span className="text-sm font-medium">录音中...</span>
  136. </div>
  137. )}
  138. </div>
  139. )
  140. }