feat(voice): 优化语音识别功能及增强可用性提示
- 新增语音服务可用性状态管理,动态检测浏览器支持情况 - 调整初始输入模式为键盘输入,避免误触启动语音识别 - 合并消息列表时保留已有调试消息,避免信息丢失 - VoiceInput组件新增调试日志回调,增强调试信息输出 - 细化语音识别状态机,区分idle、starting和listening状态 - 优化权限拒绝错误处理,针对iOS给出具体操作提示 - 改进语音识别启动流程,针对iOS增加软启动延迟避免冲突 - 支持多种指针和触摸事件管理,防止冲突导致的识别异常 - 按钮UI根据状态动态调整,显示不同提示文本和样式 - 禁用语音不可用时的按钮操作,避免误操作 - 增加语音识别启动超时保护,防止长时间挂起 - 优化用户交互体验,加入振动反馈及事件防抖处理
This commit is contained in:
69
App.tsx
69
App.tsx
@@ -121,7 +121,8 @@ const App: React.FC = () => {
|
||||
const [isProcessing, setIsProcessing] = useState(false);
|
||||
const [editingTransaction, setEditingTransaction] = useState<Transaction | null>(null);
|
||||
const [isEditingFromChat, setIsEditingFromChat] = useState(false);
|
||||
const [inputMode, setInputMode] = useState<'voice' | 'keyboard'>('voice');
|
||||
const [inputMode, setInputMode] = useState<'voice' | 'keyboard'>('keyboard');
|
||||
const [isVoiceServiceAvailable, setIsVoiceServiceAvailable] = useState(true);
|
||||
const [token, setToken] = useState<string | null>(null);
|
||||
const [userInfo, setUserInfo] = useState<any>(null);
|
||||
|
||||
@@ -151,6 +152,12 @@ const App: React.FC = () => {
|
||||
useEffect(() => {
|
||||
console.log('当前URL:', window.location.href);
|
||||
|
||||
// 检查浏览器是否支持语音识别
|
||||
const SpeechRecognition = (window as any).SpeechRecognition || (window as any).webkitSpeechRecognition;
|
||||
if (!SpeechRecognition) {
|
||||
setIsVoiceServiceAvailable(false);
|
||||
}
|
||||
|
||||
// 检查是否存在token,如果不存在则调用登录接口
|
||||
checkAndLogin().then((userInfo) => {
|
||||
// 登录成功后加载数据
|
||||
@@ -264,33 +271,46 @@ const App: React.FC = () => {
|
||||
relatedTransactionId: msg.transaction_id || msg.relatedTransactionId
|
||||
}));
|
||||
// 更新本地缓存
|
||||
setMessages(messagesFromDB);
|
||||
setMessages(prev => {
|
||||
// 如果已经有消息了(比如调试消息),保留它们并合并
|
||||
const existingDebugMessages = prev.filter(m => m.id.startsWith('debug-'));
|
||||
return [...messagesFromDB, ...existingDebugMessages];
|
||||
});
|
||||
console.log('消息列表已从数据库加载:', messagesFromDB.length);
|
||||
} else {
|
||||
// 如果数据库中没有消息,显示初始欢迎消息
|
||||
const aiName = aiSettings.name;
|
||||
setMessages([{
|
||||
id: 'init',
|
||||
role: 'assistant',
|
||||
content: `我是${aiName}。✨\n想记账?快说!🎤`,
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
setMessages(prev => {
|
||||
const existingDebugMessages = prev.filter(m => m.id.startsWith('debug-'));
|
||||
return [{
|
||||
id: 'init',
|
||||
role: 'assistant',
|
||||
content: `我是${aiName}。✨\n想记账?快说!🎤`,
|
||||
timestamp: Date.now()
|
||||
}, ...existingDebugMessages];
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('加载消息列表失败:', error);
|
||||
// 失败时尝试从本地存储加载
|
||||
const storedMessages = localStorage.getItem('cui_messages');
|
||||
if (storedMessages) {
|
||||
setMessages(JSON.parse(storedMessages));
|
||||
setMessages(prev => {
|
||||
const existingDebugMessages = prev.filter(m => m.id.startsWith('debug-'));
|
||||
return [...JSON.parse(storedMessages), ...existingDebugMessages];
|
||||
});
|
||||
} else {
|
||||
// 显示初始欢迎消息
|
||||
const aiName = aiSettings.name;
|
||||
setMessages([{
|
||||
id: 'init',
|
||||
role: 'assistant',
|
||||
content: `我是${aiName}。✨\n想记账?快说!🎤`,
|
||||
timestamp: Date.now()
|
||||
}]);
|
||||
setMessages(prev => {
|
||||
const existingDebugMessages = prev.filter(m => m.id.startsWith('debug-'));
|
||||
return [{
|
||||
id: 'init',
|
||||
role: 'assistant',
|
||||
content: `我是${aiName}。✨\n想记账?快说!🎤`,
|
||||
timestamp: Date.now()
|
||||
}, ...existingDebugMessages];
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -482,6 +502,11 @@ const App: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDebugLog = React.useCallback((content: string) => {
|
||||
// 调试信息仅打印到控制台,不再添加到消息列表
|
||||
console.log(`🔧 [调试信息] ${content}`);
|
||||
}, []);
|
||||
|
||||
const handleSendMessage = React.useCallback(async (text: string, imageBase64?: string, mimeType?: string) => {
|
||||
if (!text.trim() && !imageBase64) return;
|
||||
|
||||
@@ -799,7 +824,10 @@ const App: React.FC = () => {
|
||||
<div className="absolute bottom-20 left-0 right-0 p-4 bg-gradient-to-t from-gray-100 via-gray-100 to-transparent z-40">
|
||||
<div className="max-w-4xl mx-auto flex flex-col gap-3">
|
||||
<div className="flex items-center gap-3">
|
||||
<button onClick={() => setInputMode(inputMode === 'voice' ? 'keyboard' : 'voice')} className="p-3 bg-white text-gray-400 rounded-full shadow-lg hover:bg-gray-50 transition-all active:scale-90">
|
||||
<button
|
||||
onClick={() => setInputMode(inputMode === 'voice' ? 'keyboard' : 'voice')}
|
||||
className="p-3 bg-white text-gray-400 rounded-full shadow-lg hover:bg-gray-50 transition-all active:scale-90"
|
||||
>
|
||||
{inputMode === 'voice' ? (
|
||||
<svg className="w-6 h-6" fill="none" viewBox="0 0 24 24" stroke="currentColor"><path strokeLinecap="round" strokeLinejoin="round" strokeWidth={2} d="M11 5H6a2 2 0 00-2 2v11a2 2 0 002 2h11a2 2 0 002-2v-5m-1.414-9.414a2 2 0 112.828 2.828L11.828 15H9v-2.828l8.586-8.586z" /></svg>
|
||||
) : (
|
||||
@@ -815,7 +843,14 @@ const App: React.FC = () => {
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<VoiceInput onTranscript={(text) => handleSendMessage(text)} isProcessing={isProcessing} isFullWidth />
|
||||
<VoiceInput
|
||||
onTranscript={(text) => handleSendMessage(text)}
|
||||
isProcessing={isProcessing}
|
||||
isFullWidth
|
||||
onDebug={handleDebugLog}
|
||||
onAvailabilityChange={(isAvailable) => setIsVoiceServiceAvailable(isAvailable)}
|
||||
isServiceAvailable={isVoiceServiceAvailable}
|
||||
/>
|
||||
)}
|
||||
|
||||
<input type="file" accept="image/*" className="hidden" ref={fileInputRef} onChange={handleFileUpload} />
|
||||
|
||||
Reference in New Issue
Block a user