diff --git a/lib/core/widgets/intro_overlay_builder.dart b/lib/core/widgets/intro_overlay_builder.dart new file mode 100644 index 000000000..aab3dae94 --- /dev/null +++ b/lib/core/widgets/intro_overlay_builder.dart @@ -0,0 +1,72 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_intro/flutter_intro.dart'; +import 'package:flutter_screenutil/flutter_screenutil.dart'; + +class IntroOverlayBuilder { + static Widget aiBuilder( + StepWidgetParams params, + String text, { + String buttonText = '下一步', + bool alignment = true, + Function? onNext, + Function? onFinish, + }) { + return Column( + crossAxisAlignment: + alignment ? CrossAxisAlignment.start : CrossAxisAlignment.end, + children: [ + Padding( + padding: EdgeInsets.only(bottom: 10.w), + child: Text( + text, + style: TextStyle( + fontSize: 15.sp, + color: Colors.white, + fontWeight: FontWeight.bold, + ), + ), + ), + Row( + children: [ + if (onNext != null) + _button( + buttonText, + () { + params.onNext!(); + onNext(); + }, + ), + if (onFinish != null) + _button( + buttonText, + () { + params.onFinish(); + onFinish(); + }, + ), + ], + ), + ], + ); + } + + static Widget _button(String text, Function() onTap) { + return GestureDetector( + onTap: onTap, + child: Container( + padding: EdgeInsets.symmetric(horizontal: 10.w, vertical: 5.w), + decoration: BoxDecoration( + borderRadius: BorderRadius.circular(20.r), + border: Border.all(color: Colors.white), + ), + child: Text( + text, + style: TextStyle( + fontSize: 14.sp, + color: Colors.white, + ), + ), + ), + ); + } +} diff --git a/lib/core/widgets/select_location_map.dart b/lib/core/widgets/select_location_map.dart deleted file mode 100644 index c783901ed..000000000 --- a/lib/core/widgets/select_location_map.dart +++ /dev/null @@ -1,15 +0,0 @@ -import 'package:flutter/material.dart'; - -class SelectLocationMap extends StatefulWidget { - const SelectLocationMap({super.key}); - - @override - State createState() => _SelectLocationMapState(); -} - -class _SelectLocationMapState extends State { - @override - Widget build(BuildContext context) { - return const Placeholder(); - } -} diff --git a/lib/data/services/novice_guide_service.dart b/lib/data/services/novice_guide_service.dart index aeb3baa39..12faf1a2c 100644 --- a/lib/data/services/novice_guide_service.dart +++ b/lib/data/services/novice_guide_service.dart @@ -8,6 +8,47 @@ class NoviceGuideService extends GetxService { bool isHomeGuide = true; bool isAIGuide = true; + int aiStep = 0; + + List aiStepsList = [ + { + 'start': '对小言说:“今天天气怎么样”', + 'input': '今天天气怎么样', + 'send': '小言会为你实时为你播报天气', + 'end': '小言能实时为你播报天气,试试其他指令吧', + }, + { + 'start': '对小言说:“请播放一组摇滚乐”', + 'input': '请播放一组摇滚乐', + 'send': '小言会为你搜寻到一组音乐', + 'end': '小言已成功为您搜寻到一组音乐,试试其他指令吧', + }, + { + 'start': '对小言说:“导航到深圳机场”', + 'input': '导航到深圳机场', + 'send': '小言会为你规划路线', + 'end': '小言已成功为您导航到深圳机场,试试其他指令吧', + }, + { + 'start': '对小言说:“明天下午3点创建会议提醒”', + 'input': '明天下午3点创建会议提醒', + 'send': '小言会为你创建会议提醒', + 'end': '小言已成功为您创建会议提醒,试试其他指令吧', + }, + { + 'start': '对小言说:“拨打妈妈的电话”', + 'input': '拨打妈妈的电话', + 'send': '小言会为你拨打电话', + 'end': '小言已成功为您拨打电话,试试其他指令吧', + }, + { + 'start': '对小言说:“给爸爸发短信说晚上回家吃饭’”', + 'input': '给爸爸发短信说晚上回家吃饭’', + 'send': '小言会为你发送短信', + 'end': '短信已发送,语音操控更便捷', + }, + ]; + @override void onInit() { super.onInit(); @@ -15,8 +56,9 @@ class NoviceGuideService extends GetxService { // isAIGuide = _storage.read('ai_guide') ?? false; } - void startHomeGuide() { + void startHomeGuide() async { if (!isHomeGuide) { + await Future.delayed(const Duration(milliseconds: 200)); isHomeGuide = true; _storage.write('home_guide', true); Intro.of(Get.context!).start(group: 'home'); diff --git a/lib/modules/agent/views/agent_view.dart b/lib/modules/agent/views/agent_view.dart index d989cd037..849baa4cc 100644 --- a/lib/modules/agent/views/agent_view.dart +++ b/lib/modules/agent/views/agent_view.dart @@ -4,6 +4,7 @@ import 'dart:math'; import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; +import '../../../core/widgets/intro_overlay_builder.dart'; import '../controllers/agent_controller.dart'; import '../controllers/message_action_controller.dart'; import 'message_bubble.dart'; @@ -197,47 +198,79 @@ class _AgentViewState extends State with WidgetsBindingObserver { children: [ // 聊天内容 Expanded( - child: Obx( - () => Align( - alignment: Alignment.topCenter, - child: ListView.builder( - controller: controller.scrollController, - shrinkWrap: true, - reverse: true, - padding: EdgeInsets.symmetric( - vertical: 16.h, horizontal: 16.w), - itemCount: controller.messages.length + - (controller.hasMoreMessages.value ? 1 : 0), - itemBuilder: (context, index) { - // 如果是最后一项且有更多消息,显示加载指示器 - if (index == controller.messages.length && - controller.hasMoreMessages.value) { - return _buildLoadMoreIndicator(); - } - - final message = controller - .messages[controller.messages.length - 1 - index]; - return Padding( - padding: EdgeInsets.only(bottom: 8.h), - child: GestureDetector( - onLongPressStart: - (LongPressStartDetails details) { - messageActionController.onLongPressMessage( - context, isDarkMode, details, message); - }, - child: MessageBubble( - isUser: message.isUser, - message: message.text, - timestamp: message.timestamp, - isRecognizing: message.isRecognizing, - hasImage: message.hasImage, - imagePath: message.imagePath, - meta: message.meta, - ), - )); + child: Stack( + children: [ + IntroStepBuilder( + group: 'ai', + order: 3, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + controller.noviceGuideService.aiStepsList[ + controller.noviceGuideService.aiStep]['end'], + onFinish: () async { + if (controller.noviceGuideService.aiStep < 5) { + await Future.delayed( + const Duration(milliseconds: 300)); + controller.noviceGuideService.aiStep++; + Intro.of(Get.context!) + .start(group: 'ai', reset: true); + } + }, + ); }, + builder: (BuildContext context, GlobalKey key) => Align( + alignment: Alignment.bottomCenter, + child: SizedBox( + key: key, + width: double.infinity, + height: 430.w, + ), + ), ), - ), + Obx( + () => Align( + alignment: Alignment.topCenter, + child: ListView.builder( + controller: controller.scrollController, + shrinkWrap: true, + reverse: true, + padding: EdgeInsets.symmetric( + vertical: 16.h, horizontal: 16.w), + itemCount: controller.messages.length + + (controller.hasMoreMessages.value ? 1 : 0), + itemBuilder: (context, index) { + // 如果是最后一项且有更多消息,显示加载指示器 + if (index == controller.messages.length && + controller.hasMoreMessages.value) { + return _buildLoadMoreIndicator(); + } + + final message = controller.messages[ + controller.messages.length - 1 - index]; + return Padding( + padding: EdgeInsets.only(bottom: 8.h), + child: GestureDetector( + onLongPressStart: + (LongPressStartDetails details) { + messageActionController.onLongPressMessage( + context, isDarkMode, details, message); + }, + child: MessageBubble( + isUser: message.isUser, + message: message.text, + timestamp: message.timestamp, + isRecognizing: message.isRecognizing, + hasImage: message.hasImage, + imagePath: message.imagePath, + meta: message.meta, + ), + )); + }, + ), + ), + ), + ], ), ), @@ -392,7 +425,24 @@ class _AgentViewState extends State with WidgetsBindingObserver { return IntroStepBuilder( group: 'ai', order: 1, - text: '对小言说“今天天气怎么样”', + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + controller.noviceGuideService + .aiStepsList[ + controller.noviceGuideService + .aiStep]['start'], + buttonText: '确定输入', + onNext: () { + controller.textController.text = + controller.noviceGuideService + .aiStepsList[ + controller + .noviceGuideService + .aiStep]['input']; + }, + ); + }, onWidgetLoad: () { controller.noviceGuideService .startAIGuide(); @@ -630,111 +680,143 @@ class _AgentViewState extends State with WidgetsBindingObserver { SizedBox(width: 8.w), // 动态按钮区域:展开按钮 + 发送按钮 或 图片选择按钮 - Obx(() { - final hasText = controller.hasTextInput.value; - final hasImage = - controller.isImageInputActive.value; - final shouldShowSend = hasText || hasImage; - final shouldShowExpand = - controller.shouldShowExpandButton.value; + Builder(builder: (context) { + StepWidgetParams? widgetParams; + return IntroStepBuilder( + group: 'ai', + order: 2, + onHighlightWidgetTap: () { + widgetParams!.onNext!(); + controller.sendMessage(); + }, + overlayBuilder: (params) { + widgetParams = params; + return IntroOverlayBuilder.aiBuilder( + params, + '点击发送,${controller.noviceGuideService.aiStepsList[controller.noviceGuideService.aiStep]['send']}', + alignment: false, + ); + }, + builder: + (BuildContext context, GlobalKey key) => + Obx( + key: key, + () { + final hasText = + controller.hasTextInput.value; + final hasImage = + controller.isImageInputActive.value; + final shouldShowSend = + hasText || hasImage; + final shouldShowExpand = controller + .shouldShowExpandButton.value; - if (shouldShowSend) { - // 显示发送按钮(可能还有展开按钮) - return Column( - mainAxisSize: MainAxisSize.min, - children: [ - // 展开按钮(在发送按钮上方) - if (shouldShowExpand && hasText) - Padding( - padding: - EdgeInsets.only(bottom: 40.h), - child: GestureDetector( + if (shouldShowSend) { + // 显示发送按钮(可能还有展开按钮) + return Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 展开按钮(在发送按钮上方) + if (shouldShowExpand && hasText) + Padding( + padding: EdgeInsets.only( + bottom: 40.h), + child: GestureDetector( + onTap: () { + // 打开底部弹出编辑模式 + controller + .openFullScreenEdit(); + showModalBottomSheet( + context: context, + isScrollControlled: true, + backgroundColor: + Colors.transparent, + builder: (context) => + const FullScreenEditor(), + ).then((_) { + // 弹出页关闭时的回调 + controller + .closeFullScreenEdit(); + }); + }, + child: Container( + width: 32.w, + height: 32.w, + decoration: BoxDecoration( + color: isDarkMode + ? Colors.grey[700] + : Colors.grey[300], + borderRadius: + BorderRadius.circular( + 16.w), + ), + child: Icon( + Icons.expand_less, + color: isDarkMode + ? Colors.grey[300] + : Colors.grey[700], + size: 20.sp, + ), + ), + ), + ), + // 发送按钮 + GestureDetector( + onTap: () => + controller.sendMessage(), + child: Container( + width: 44.w, + height: 44.w, + decoration: BoxDecoration( + color: primaryColor, + borderRadius: + BorderRadius.circular( + 22.w), + ), + child: Icon( + Icons.send, + color: Colors.white, + size: 22.sp, + ), + ), + ), + ], + ); + } else { + // 显示'+'选择按钮 + return Obx(() { + final isExpanded = controller + .isPlusOptionsExpanded.value; + return GestureDetector( onTap: () { - // 打开底部弹出编辑模式 - controller.openFullScreenEdit(); - showModalBottomSheet( - context: context, - isScrollControlled: true, - backgroundColor: - Colors.transparent, - builder: (context) => - const FullScreenEditor(), - ).then((_) { - // 弹出页关闭时的回调 - controller - .closeFullScreenEdit(); - }); + controller.togglePlusOptions(); }, child: Container( - width: 32.w, - height: 32.w, + width: 44.w, + height: 44.w, decoration: BoxDecoration( color: isDarkMode - ? Colors.grey[700] - : Colors.grey[300], + ? Colors.grey[800] + : Colors.grey[200], borderRadius: - BorderRadius.circular(16.w), + BorderRadius.circular(22.w), ), child: Icon( - Icons.expand_less, + isExpanded + ? Icons.close + : Icons.add, color: isDarkMode ? Colors.grey[300] : Colors.grey[700], - size: 20.sp, + size: 22.sp, ), ), - ), - ), - // 发送按钮 - GestureDetector( - onTap: () => controller.sendMessage(), - child: Container( - width: 44.w, - height: 44.w, - decoration: BoxDecoration( - color: primaryColor, - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - Icons.send, - color: Colors.white, - size: 22.sp, - ), - ), - ), - ], - ); - } else { - // 显示'+'选择按钮 - return Obx(() { - final isExpanded = - controller.isPlusOptionsExpanded.value; - return GestureDetector( - onTap: () { - controller.togglePlusOptions(); - }, - child: Container( - width: 44.w, - height: 44.w, - decoration: BoxDecoration( - color: isDarkMode - ? Colors.grey[800] - : Colors.grey[200], - borderRadius: - BorderRadius.circular(22.w), - ), - child: Icon( - isExpanded ? Icons.close : Icons.add, - color: isDarkMode - ? Colors.grey[300] - : Colors.grey[700], - size: 22.sp, - ), - ), - ); - }); - } + ); + }); + } + }, + ), + ); }), ], ), diff --git a/lib/modules/home/views/home_view.dart b/lib/modules/home/views/home_view.dart index f842b6a23..6660fbafa 100644 --- a/lib/modules/home/views/home_view.dart +++ b/lib/modules/home/views/home_view.dart @@ -2,6 +2,7 @@ import 'package:flutter/material.dart'; import 'package:flutter_intro/flutter_intro.dart'; import 'package:get/get.dart'; import 'package:flutter_screenutil/flutter_screenutil.dart'; +import '../../../core/widgets/intro_overlay_builder.dart'; import '../controllers/home_controller.dart'; import '../../../routes/app_routes.dart'; import '../../../core/widgets/common_bottom_nav.dart'; @@ -84,31 +85,46 @@ class HomeView extends GetView { _buildAppBar(isDarkMode), // 顶部图片 + _buildEarbudsImage(), + + SizedBox(height: 20.h), + + // 设备状态卡片 IntroStepBuilder( group: 'home', order: 1, - text: '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', onWidgetLoad: () { controller.noviceGuideService.startHomeGuide(); }, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '欢迎使用Voitrans!首先,让我们唤醒你的专属 AI—— 小言', + onNext: () {}, + ); + }, builder: (BuildContext context, GlobalKey key) => SizedBox( key: key, - child: _buildEarbudsImage(), + child: _buildDeviceStatusCard(isDarkMode), ), ), + SizedBox(height: 20.h), - // 设备状态卡片 IntroStepBuilder( group: 'home', order: 2, - text: '对着麦克风说“小言小言”', + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '对着麦克风说“小言小言”', + onNext: () {}, + ); + }, builder: (BuildContext context, GlobalKey key) => SizedBox( key: key, - child: _buildDeviceStatusCard(isDarkMode), ), ), - SizedBox(height: 20.h), // 热门AI体验 _buildSectionTitle(isDarkMode), @@ -645,11 +661,16 @@ class HomeView extends GetView { child: IntroStepBuilder( group: 'home', order: 3, - text: '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', onHighlightWidgetTap: () { Intro.of(Get.context!).dispose(); controller.exploreAIFeatures(); }, + overlayBuilder: (params) { + return IntroOverlayBuilder.aiBuilder( + params, + '太棒了!你已成功唤醒小言,接下来试试让它帮你做件事吧', + ); + }, builder: (BuildContext context, GlobalKey key) => _buildFeatureCard( key: key, title: 'aiAssistant'.tr, // AI全能助手 diff --git a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift index a07e91f72..1067a9211 100644 --- a/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift +++ b/local_plugins/ble_service/ios/ble_service/Sources/ble_service/SwiftOpusAudioProcessor.swift @@ -26,22 +26,32 @@ class SwiftOpusAudioProcessor: NSObject { cleanup() } + /// 线程安全的解码器初始化 private func initializeOpusDecoder() -> Bool { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + if decoder != nil { return true } var error: Int32 = 0 - decoder = opus_decoder_create(sampleRate, channels, &error) + let newDecoder = opus_decoder_create(sampleRate, channels, &error) - guard error == OPUS_OK, decoder != nil else { + guard error == OPUS_OK, newDecoder != nil else { + print("Opus解码器创建失败,错误代码: \(error)") return false } + decoder = newDecoder return true } + /// 线程安全的解码器销毁 private func destroyOpusDecoder() { + objc_sync_enter(self) + defer { objc_sync_exit(self) } + if let decoder = decoder { opus_decoder_destroy(decoder) self.decoder = nil @@ -112,23 +122,44 @@ class SwiftOpusAudioProcessor: NSObject { } } - // 简化的 Opus 解码方法,重点是容错处理 + /// 改进的 Opus 解码方法,增强容错和线程安全 private func decodeOpusPacket(_ packet: Data) { - guard let decoder = decoder else { return } + // 添加线程安全锁 + objc_sync_enter(self) + defer { objc_sync_exit(self) } + // 验证解码器状态 + guard let decoder = decoder, isDecodingActive else { + print("解码器未初始化或已停止") + return + } + let maxSamples = Int(maxFrameSize) var pcmBuffer = [opus_int16](repeating: 0, count: maxSamples * Int(channels)) let decodedSamples = packet.withUnsafeBytes { packetPtr in - let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress - return opus_decode( - decoder, - uint8Ptr, - opus_int32(packet.count), - &pcmBuffer, - Int32(maxSamples), - 0 - ) + // 确保指针有效 + guard let uint8Ptr = packetPtr.bindMemory(to: UInt8.self).baseAddress else { + print("无法获取数据包指针") + return opus_int32(-1) + } + + // 添加异常捕获机制 + var result: opus_int32 = -1 + + // 使用 autoreleasepool 确保内存管理 + autoreleasepool { + result = opus_decode( + decoder, + uint8Ptr, + opus_int32(packet.count), + &pcmBuffer, + Int32(maxSamples), + 0 + ) + } + + return result } // 增强的错误处理:检查各种错误情况 @@ -237,4 +268,4 @@ class SwiftOpusAudioProcessor: NSObject { func getCurrentConfig() -> (sampleRate: Int32, channels: Int32) { return (sampleRate, channels) } -} +}