Przeglądaj źródła

fix(flutter): display streaming AI reply (liveReply) in conversation list

- Added liveReply param to ConversationList widget
- _LiveAiBubble widget shows streaming AI response with pulsing dots
- Clear liveReply when final ReplyMessage arrives
- Pass liveReply from recording_page to ConversationList
wenhongquan 3 tygodni temu
rodzic
commit
18e3e4d341

+ 1 - 0
flutter_asr_client/lib/pages/recording/notifiers/recording_notifier.dart

@@ -158,6 +158,7 @@ final class RecordingNotifier extends AutoDisposeAsyncNotifier<RecordingState> {
         case UtteranceMessage(:final text):
           _finalizeBubble(text);
         case ReplyMessage(:final text):
+          _updateState((s) => s.copyWith(liveReply: null));
           _addAiReply(text);
         case ReplyPartialMessage(:final text):
           _updateState((s) => s.copyWith(liveReply: text));

+ 1 - 0
flutter_asr_client/lib/pages/recording/recording_page.dart

@@ -227,6 +227,7 @@ class _RecordingBody extends ConsumerWidget {
                 ConversationList(
                   items: state.items,
                   liveUtterance: state.liveUtterance,
+                  liveReply: state.liveReply,
                   isRecording: state.isRecording,
                 ),
               ],

+ 47 - 0
flutter_asr_client/lib/pages/recording/widgets/conversation_list.dart

@@ -8,11 +8,13 @@ class ConversationList extends StatelessWidget {
     super.key,
     required this.items,
     this.liveUtterance,
+    this.liveReply,
     required this.isRecording,
   });
 
   final List<ConversationItem> items;
   final String? liveUtterance;
+  final String? liveReply;
   final bool isRecording;
 
   @override
@@ -28,6 +30,8 @@ class ConversationList extends StatelessWidget {
           ),
         if (liveUtterance != null && liveUtterance!.trim().isNotEmpty)
           _LiveBubble(text: liveUtterance!, isRecording: isRecording),
+        if (liveReply != null && liveReply!.trim().isNotEmpty)
+          _LiveAiBubble(text: liveReply!),
       ],
     );
   }
@@ -172,3 +176,46 @@ class _AiBubble extends StatelessWidget {
     );
   }
 }
+
+class _LiveAiBubble extends StatelessWidget {
+  const _LiveAiBubble({required this.text});
+
+  final String text;
+
+  @override
+  Widget build(BuildContext context) {
+    return Align(
+      alignment: Alignment.centerLeft,
+      child: ConstrainedBox(
+        constraints: BoxConstraints(
+          maxWidth: MediaQuery.of(context).size.width * 0.78,
+        ),
+        child: Container(
+          padding: const EdgeInsets.symmetric(horizontal: 13, vertical: 9),
+          decoration: BoxDecoration(
+            color: AppColors.card,
+            borderRadius: const BorderRadius.only(
+              topLeft: Radius.circular(4),
+              topRight: Radius.circular(16),
+              bottomRight: Radius.circular(16),
+            ),
+            border: Border.all(color: AppColors.line),
+          ),
+          child: Row(
+            mainAxisSize: MainAxisSize.min,
+            children: [
+              Flexible(
+                child: Text(
+                  text,
+                  style: const TextStyle(fontSize: 13.5, color: AppColors.ink, height: 1.4),
+                ),
+              ),
+              const SizedBox(width: 6),
+              const _PulsingDots(),
+            ],
+          ),
+        ),
+      ),
+    );
+  }
+}