VoiceHelper前端模块深度解析
本文档详细介绍VoiceHelper智能语音助手系统的前端模块技术实现,涵盖Next.js应用架构、实时通信机制、多端适配策略等核心技术。
2. 前端模块深度解析
2.1 Next.js应用架构
// 前端应用主入口
// 文件路径: frontend/app/layout.tsx
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="zh-CN">
<body className={inter.className}>
<Providers>
<div className="min-h-screen bg-gradient-to-br from-blue-50 to-indigo-100">
<Header />
<main className="container mx-auto px-4 py-8">
{children}
</main>
<Footer />
</div>
<Toaster />
</Providers>
</body>
</html>
)
}
// 实时通信Hook
// 文件路径: frontend/hooks/useWebSocket.ts
export function useWebSocket(url: string) {
const [socket, setSocket] = useState<WebSocket | null>(null)
const [connectionStatus, setConnectionStatus] = useState<ConnectionStatus>('Disconnected')
const [messageHistory, setMessageHistory] = useState<MessageEvent[]>([])
const sendMessage = useCallback((message: any) => {
if (socket && socket.readyState === WebSocket.OPEN) {
socket.send(JSON.stringify(message))
}
}, [socket])
useEffect(() => {
const ws = new WebSocket(url)
ws.onopen = () => {
setConnectionStatus('Connected')
setSocket(ws)
}
ws.onmessage = (event) => {
const message = JSON.parse(event.data)
setMessageHistory(prev => [...prev, message])
}
ws.onclose = () => {
setConnectionStatus('Disconnected')
setSocket(null)
}
return () => {
ws.close()
}
}, [url])
return { socket, connectionStatus, messageHistory, sendMessage }
}
2.2 实时通信机制
VoiceHelper前端采用WebSocket实现实时通信,支持流式对话和多模态交互。
2.2.1 WebSocket连接建立流程
sequenceDiagram
participant User as 用户
participant Frontend as 前端应用
participant Gateway as API网关
participant ChatService as 对话服务
User->>Frontend: 打开应用
Frontend->>Gateway: 建立WebSocket连接
Gateway->>ChatService: 验证用户身份
ChatService-->>Gateway: 返回连接确认
Gateway-->>Frontend: 连接建立成功
Frontend-->>User: 显示在线状态
2.2.2 消息处理流程
sequenceDiagram
participant User as 用户
participant Frontend as 前端应用
participant Gateway as API网关
participant ChatService as 对话服务
User->>Frontend: 发送消息
Frontend->>Frontend: 消息预处理
Frontend->>Gateway: 通过WebSocket发送
Gateway->>ChatService: 转发到对话服务
ChatService->>ChatService: 处理业务逻辑
ChatService-->>Gateway: 返回处理结果
Gateway-->>Frontend: 推送响应数据
Frontend-->>User: 更新界面显示
2.2.3 流式响应处理
sequenceDiagram
participant Frontend as 前端应用
participant Gateway as API网关
participant RAGEngine as RAG引擎
participant LLM as 大模型
Frontend->>Gateway: 发送查询请求
Gateway->>RAGEngine: 转发到RAG引擎
RAGEngine->>LLM: 构建提示词
loop 流式生成
LLM-->>RAGEngine: 生成内容片段
RAGEngine-->>Gateway: 转发数据块
Gateway-->>Frontend: 实时推送
Frontend->>Frontend: 增量更新UI
end
LLM-->>RAGEngine: 生成完成
RAGEngine-->>Gateway: 发送结束标记
Gateway-->>Frontend: 通知响应结束
2.3 多端适配策略
// 多端适配配置
// 文件路径: frontend/lib/platform.ts
export class PlatformAdapter {
private platform: Platform
constructor() {
this.platform = this.detectPlatform()
}
detectPlatform(): Platform {
if (typeof window === 'undefined') return 'server'
const userAgent = window.navigator.userAgent
if (/MicroMessenger/i.test(userAgent)) return 'wechat'
if (/Mobile|Android|iPhone|iPad/i.test(userAgent)) return 'mobile'
if (/Electron/i.test(userAgent)) return 'desktop'
return 'web'
}
getApiConfig(): ApiConfig {
const baseConfigs = {
web: {
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 30000,
enableWebSocket: true,
},
mobile: {
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 15000,
enableWebSocket: true,
},
wechat: {
baseURL: process.env.NEXT_PUBLIC_API_URL,
timeout: 10000,
enableWebSocket: false, // 微信小程序使用轮询
},
desktop: {
baseURL: 'http://localhost:8080',
timeout: 60000,
enableWebSocket: true,
}
}
return baseConfigs[this.platform] || baseConfigs.web
}
}
2.4 语音交互组件
// 语音录制组件
// 文件路径: frontend/components/VoiceRecorder.tsx
export function VoiceRecorder() {
const [isRecording, setIsRecording] = useState(false)
const [audioBlob, setAudioBlob] = useState<Blob | null>(null)
const [transcript, setTranscript] = useState('')
const mediaRecorderRef = useRef<MediaRecorder | null>(null)
const audioChunksRef = useRef<Blob[]>([])
const startRecording = async () => {
try {
const stream = await navigator.mediaDevices.getUserMedia({
audio: {
echoCancellation: true,
noiseSuppression: true,
sampleRate: 44100
}
})
const mediaRecorder = new MediaRecorder(stream, {
mimeType: 'audio/webm;codecs=opus'
})
mediaRecorderRef.current = mediaRecorder
audioChunksRef.current = []
mediaRecorder.ondataavailable = (event) => {
if (event.data.size > 0) {
audioChunksRef.current.push(event.data)
}
}
mediaRecorder.onstop = () => {
const audioBlob = new Blob(audioChunksRef.current, {
type: 'audio/webm;codecs=opus'
})
setAudioBlob(audioBlob)
processAudio(audioBlob)
}
mediaRecorder.start(100) // 每100ms收集一次数据
setIsRecording(true)
} catch (error) {
console.error('录音启动失败:', error)
}
}
const stopRecording = () => {
if (mediaRecorderRef.current && isRecording) {
mediaRecorderRef.current.stop()
setIsRecording(false)
}
}
const processAudio = async (audioBlob: Blob) => {
try {
const formData = new FormData()
formData.append('audio', audioBlob, 'recording.webm')
const response = await fetch('/api/voice/transcribe', {
method: 'POST',
body: formData
})
const result = await response.json()
setTranscript(result.transcript)
} catch (error) {
console.error('语音转文字失败:', error)
}
}
return (
<div className="voice-recorder">
<button
onMouseDown={startRecording}
onMouseUp={stopRecording}
onTouchStart={startRecording}
onTouchEnd={stopRecording}
className={`voice-button ${isRecording ? 'recording' : ''}`}
>
{isRecording ? '🎤 录音中...' : '🎤 按住说话'}
</button>
{transcript && (
<div className="transcript">
<p>识别结果: {transcript}</p>
</div>
)}
</div>
)
}
2.5 流式对话组件
// 流式对话组件
// 文件路径: frontend/components/StreamingChat.tsx
export function StreamingChat() {
const [messages, setMessages] = useState<Message[]>([])
const [isStreaming, setIsStreaming] = useState(false)
const [currentMessage, setCurrentMessage] = useState('')
const { socket, connectionStatus, sendMessage } = useWebSocket(
process.env.NEXT_PUBLIC_WS_URL || 'ws://localhost:8080/ws'
)
const sendUserMessage = async (content: string) => {
const userMessage: Message = {
id: generateId(),
role: 'user',
content,
timestamp: new Date()
}
setMessages(prev => [...prev, userMessage])
setIsStreaming(true)
setCurrentMessage('')
// 发送消息到后端
sendMessage({
type: 'chat',
content,
sessionId: getCurrentSessionId()
})
}
useEffect(() => {
if (socket) {
socket.onmessage = (event) => {
const data = JSON.parse(event.data)
if (data.type === 'stream_start') {
setCurrentMessage('')
} else if (data.type === 'stream_chunk') {
setCurrentMessage(prev => prev + data.content)
} else if (data.type === 'stream_end') {
const assistantMessage: Message = {
id: generateId(),
role: 'assistant',
content: currentMessage,
timestamp: new Date()
}
setMessages(prev => [...prev, assistantMessage])
setCurrentMessage('')
setIsStreaming(false)
}
}
}
}, [socket, currentMessage])
return (
<div className="streaming-chat">
<div className="messages-container">
{messages.map((message) => (
<MessageBubble key={message.id} message={message} />
))}
{isStreaming && (
<div className="streaming-message">
<MessageBubble
message={{
id: 'streaming',
role: 'assistant',
content: currentMessage,
timestamp: new Date()
}}
isStreaming={true}
/>
</div>
)}
</div>
<div className="input-container">
<VoiceRecorder onTranscript={sendUserMessage} />
<TextInput onSend={sendUserMessage} />
</div>
</div>
)
}
2.6 状态管理架构
// Redux Store配置
// 文件路径: frontend/store/index.ts
export const store = configureStore({
reducer: {
chat: chatSlice.reducer,
user: userSlice.reducer,
settings: settingsSlice.reducer,
voice: voiceSlice.reducer,
},
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware({
serializableCheck: {
ignoredActions: [FLUSH, REHYDRATE, PAUSE, PERSIST, PURGE, REGISTER],
},
}).concat(api.middleware),
})
// 聊天状态管理
// 文件路径: frontend/store/slices/chatSlice.ts
export const chatSlice = createSlice({
name: 'chat',
initialState: {
messages: [] as Message[],
currentSession: null as Session | null,
isStreaming: false,
connectionStatus: 'disconnected' as ConnectionStatus,
},
reducers: {
addMessage: (state, action: PayloadAction<Message>) => {
state.messages.push(action.payload)
},
updateMessage: (state, action: PayloadAction<{id: string, content: string}>) => {
const message = state.messages.find(m => m.id === action.payload.id)
if (message) {
message.content = action.payload.content
}
},
setStreaming: (state, action: PayloadAction<boolean>) => {
state.isStreaming = action.payload
},
setConnectionStatus: (state, action: PayloadAction<ConnectionStatus>) => {
state.connectionStatus = action.payload
},
clearMessages: (state) => {
state.messages = []
}
}
})
2.7 性能优化策略
// 虚拟滚动组件
// 文件路径: frontend/components/VirtualizedMessageList.tsx
export function VirtualizedMessageList({ messages }: { messages: Message[] }) {
const [containerHeight, setContainerHeight] = useState(0)
const [scrollTop, setScrollTop] = useState(0)
const itemHeight = 80 // 每条消息的预估高度
const visibleCount = Math.ceil(containerHeight / itemHeight) + 2
const startIndex = Math.floor(scrollTop / itemHeight)
const endIndex = Math.min(startIndex + visibleCount, messages.length)
const visibleMessages = messages.slice(startIndex, endIndex)
return (
<div
className="message-list"
style={{ height: containerHeight, overflow: 'auto' }}
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
>
<div style={{ height: messages.length * itemHeight, position: 'relative' }}>
{visibleMessages.map((message, index) => (
<div
key={message.id}
style={{
position: 'absolute',
top: (startIndex + index) * itemHeight,
height: itemHeight,
width: '100%'
}}
>
<MessageBubble message={message} />
</div>
))}
</div>
</div>
)
}
// 懒加载组件
// 文件路径: frontend/components/LazyComponent.tsx
export function LazyComponent({
children,
fallback = <div>Loading...</div>
}: {
children: React.ReactNode
fallback?: React.ReactNode
}) {
const [isVisible, setIsVisible] = useState(false)
const ref = useRef<HTMLDivElement>(null)
useEffect(() => {
const observer = new IntersectionObserver(
([entry]) => {
if (entry.isIntersecting) {
setIsVisible(true)
observer.disconnect()
}
},
{ threshold: 0.1 }
)
if (ref.current) {
observer.observe(ref.current)
}
return () => observer.disconnect()
}, [])
return (
<div ref={ref}>
{isVisible ? children : fallback}
</div>
)
}
2.8 错误处理和监控
// 错误边界组件
// 文件路径: frontend/components/ErrorBoundary.tsx
export class ErrorBoundary extends React.Component<
{ children: React.ReactNode },
{ hasError: boolean; error?: Error }
> {
constructor(props: { children: React.ReactNode }) {
super(props)
this.state = { hasError: false }
}
static getDerivedStateFromError(error: Error) {
return { hasError: true, error }
}
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
// 发送错误到监控服务
console.error('Error caught by boundary:', error, errorInfo)
// 发送到错误监控服务
if (typeof window !== 'undefined') {
fetch('/api/errors', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
error: error.message,
stack: error.stack,
componentStack: errorInfo.componentStack,
timestamp: new Date().toISOString(),
userAgent: navigator.userAgent,
url: window.location.href
})
})
}
}
render() {
if (this.state.hasError) {
return (
<div className="error-fallback">
<h2>出现了一些问题</h2>
<p>我们正在努力修复这个问题,请稍后再试。</p>
<button onClick={() => this.setState({ hasError: false })}>
重试
</button>
</div>
)
}
return this.props.children
}
}
// 性能监控Hook
// 文件路径: frontend/hooks/usePerformanceMonitor.ts
export function usePerformanceMonitor() {
useEffect(() => {
// 监控页面加载性能
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
if (entry.entryType === 'navigation') {
const navEntry = entry as PerformanceNavigationTiming
// 发送性能数据到监控服务
fetch('/api/performance', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
type: 'navigation',
data: {
domContentLoaded: navEntry.domContentLoadedEventEnd - navEntry.domContentLoadedEventStart,
loadComplete: navEntry.loadEventEnd - navEntry.loadEventStart,
firstPaint: performance.getEntriesByName('first-paint')[0]?.startTime,
firstContentfulPaint: performance.getEntriesByName('first-contentful-paint')[0]?.startTime,
largestContentfulPaint: performance.getEntriesByName('largest-contentful-paint')[0]?.startTime
}
})
})
}
}
})
observer.observe({ entryTypes: ['navigation', 'paint', 'largest-contentful-paint'] })
return () => observer.disconnect()
}, [])
}