From e3be6915968bfee860e84451bfb60ec19646a302 Mon Sep 17 00:00:00 2001 From: LittleSheep Date: Sat, 25 Oct 2025 23:20:10 +0800 Subject: [PATCH] :lipstick: Optimize the AI thought --- lib/screens/thought/think.dart | 242 ++++++++++++++++++++++----------- 1 file changed, 163 insertions(+), 79 deletions(-) diff --git a/lib/screens/thought/think.dart b/lib/screens/thought/think.dart index 7ebc67b6..fdcf61d7 100644 --- a/lib/screens/thought/think.dart +++ b/lib/screens/thought/think.dart @@ -52,6 +52,7 @@ class ThoughtScreen extends HookConsumerWidget { : const AsyncValue>.data([]); final localThoughts = useState>([]); + final currentTopic = useState('AI Thought'); final messageController = useTextEditingController(); final scrollController = useScrollController(); @@ -62,7 +63,15 @@ class ThoughtScreen extends HookConsumerWidget { // Update local thoughts when provider data changes useEffect(() { - thoughts.whenData((data) => localThoughts.value = data); + thoughts.whenData((data) { + localThoughts.value = data; + // Update topic from the first thought's sequence + if (data.isNotEmpty && data.first.sequence?.topic != null) { + currentTopic.value = data.first.sequence!.topic; + } else { + currentTopic.value = 'AI Thought'; + } + }); return null; }, [thoughts]); @@ -138,13 +147,55 @@ class ThoughtScreen extends HookConsumerWidget { isStreaming.value = false; // Parse the response and add AI thought try { - final lines = buffer.toString().split('\n'); - final lastLine = lines.lastWhere( - (line) => line.trim().isNotEmpty, - ); + final lines = + buffer + .toString() + .split('\n') + .where((line) => line.trim().isNotEmpty) + .toList(); + final lastLine = lines.last; final responseJson = jsonDecode(lastLine); final aiThought = SnThinkingThought.fromJson(responseJson); - localThoughts.value = [aiThought, ...localThoughts.value]; + + // Check for topic in second last line + String? topic; + if (lines.length >= 2) { + final secondLastLine = lines[lines.length - 2]; + final topicMatch = RegExp( + r'(.*)', + ).firstMatch(secondLastLine); + if (topicMatch != null) { + topic = topicMatch.group(1); + } + } + + // Update sequence topic if found + if (topic != null && aiThought.sequence != null) { + final updatedSequence = aiThought.sequence!.copyWith( + topic: topic, + ); + final updatedThought = aiThought.copyWith( + sequence: updatedSequence, + ); + localThoughts.value = [updatedThought, ...localThoughts.value]; + + // Also update topic in existing thoughts with same sequenceId + localThoughts.value = + localThoughts.value.map((thought) { + if (thought.sequenceId == aiThought.sequenceId && + thought.sequence != null) { + return thought.copyWith( + sequence: thought.sequence!.copyWith(topic: topic), + ); + } + return thought; + }).toList(); + + // Update current topic + currentTopic.value = topic; + } else { + localThoughts.value = [aiThought, ...localThoughts.value]; + } } catch (e) { showErrorAlert('Failed to parse AI response'); } @@ -163,51 +214,75 @@ class ThoughtScreen extends HookConsumerWidget { ); messageController.clear(); + FocusManager.instance.primaryFocus?.unfocus(); } catch (error) { isStreaming.value = false; showErrorAlert(error); } } - Widget thoughtItem(SnThinkingThought thought) => Container( - margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), - padding: const EdgeInsets.all(12), - decoration: BoxDecoration( - color: - thought.role == ThinkingThoughtRole.assistant - ? Theme.of(context).colorScheme.surfaceContainerHighest - : Theme.of(context).colorScheme.primaryContainer, - borderRadius: BorderRadius.circular(12), - ), - child: Column( - crossAxisAlignment: CrossAxisAlignment.start, - children: [ - Row( - children: [ - Icon( - thought.role == ThinkingThoughtRole.assistant - ? Symbols.smart_toy - : Symbols.person, - size: 20, - ), - const Gap(8), - Text( - thought.role == ThinkingThoughtRole.assistant - ? 'AI Assistant' - : 'You', - style: Theme.of(context).textTheme.titleSmall, - ), - ], - ), - const Gap(8), - if (thought.content != null) - MarkdownTextContent( - content: thought.content!, - textStyle: Theme.of(context).textTheme.bodyMedium, + Widget thoughtItem(SnThinkingThought thought, int index) { + final key = Key('thought-${thought.id}'); + + final thoughtWidget = Container( + margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + padding: const EdgeInsets.all(12), + decoration: BoxDecoration( + color: + thought.role == ThinkingThoughtRole.assistant + ? Theme.of(context).colorScheme.surfaceContainerHighest + : Theme.of(context).colorScheme.primaryContainer, + borderRadius: BorderRadius.circular(12), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Row( + children: [ + Icon( + thought.role == ThinkingThoughtRole.assistant + ? Symbols.smart_toy + : Symbols.person, + size: 20, + ), + const Gap(8), + Text( + thought.role == ThinkingThoughtRole.assistant + ? 'AI Assistant' + : 'You', + style: Theme.of(context).textTheme.titleSmall, + ), + ], ), - ], - ), - ); + const Gap(8), + if (thought.content != null) + MarkdownTextContent( + content: thought.content!, + textStyle: Theme.of(context).textTheme.bodyMedium, + ), + ], + ), + ); + + return TweenAnimationBuilder( + key: key, + tween: Tween(begin: 0.0, end: 1.0), + duration: Duration( + milliseconds: 400 + (index % 5) * 50, + ), // Staggered delay + curve: Curves.easeOutCubic, + builder: (context, animationValue, child) { + return Transform.translate( + offset: Offset( + 0, + 20 * (1 - animationValue), + ), // Slide up from bottom + child: Opacity(opacity: animationValue, child: child), + ); + }, + child: thoughtWidget, + ); + } Widget streamingThoughtItem() => Container( margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), @@ -246,7 +321,7 @@ class ThoughtScreen extends HookConsumerWidget { return AppScaffold( appBar: AppBar( - title: const Text('AI Thought'), + title: Text(currentTopic.value ?? 'AI Thought'), actions: [ IconButton( icon: const Icon(Symbols.history), @@ -295,7 +370,7 @@ class ThoughtScreen extends HookConsumerWidget { (thoughtList) => SuperListView.builder( listController: listController, controller: scrollController, - padding: const EdgeInsets.only(bottom: 16), + padding: const EdgeInsets.only(top: 16), reverse: true, itemCount: localThoughts.value.length + @@ -307,7 +382,7 @@ class ThoughtScreen extends HookConsumerWidget { final thoughtIndex = isStreaming.value ? index - 1 : index; final thought = localThoughts.value[thoughtIndex]; - return thoughtItem(thought); + return thoughtItem(thought, thoughtIndex); }, ), loading: () => const Center(child: CircularProgressIndicator()), @@ -327,42 +402,51 @@ class ThoughtScreen extends HookConsumerWidget { ), ), Container( - padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: Theme.of(context).scaffoldBackgroundColor, - border: Border( - top: BorderSide( - color: Theme.of(context).dividerColor, - width: 1, - ), - ), + margin: EdgeInsets.only( + left: 16, + right: 16, + bottom: 16 + MediaQuery.of(context).padding.bottom, ), - child: Row( - children: [ - Expanded( - child: TextField( - controller: messageController, - decoration: InputDecoration( - hintText: 'Ask me anything...', - border: OutlineInputBorder( - borderRadius: BorderRadius.circular(24), - ), - contentPadding: const EdgeInsets.symmetric( - horizontal: 16, - vertical: 12, + child: Material( + elevation: 2, + color: Theme.of(context).colorScheme.surfaceContainerHighest, + borderRadius: BorderRadius.circular(32), + child: Padding( + padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8), + child: Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Expanded( + child: TextField( + controller: messageController, + keyboardType: TextInputType.multiline, + enabled: !isStreaming.value, + decoration: InputDecoration( + hintText: + isStreaming.value + ? 'AI is thinking...' + : 'Ask me anything...', + border: InputBorder.none, + isDense: true, + contentPadding: const EdgeInsets.symmetric( + horizontal: 12, + vertical: 12, + ), + ), + maxLines: 5, + minLines: 1, + textInputAction: TextInputAction.send, + onSubmitted: (_) => sendMessage(), ), ), - maxLines: null, - textInputAction: TextInputAction.send, - onSubmitted: (_) => sendMessage(), - ), + IconButton( + icon: Icon(isStreaming.value ? Symbols.stop : Icons.send), + color: Theme.of(context).colorScheme.primary, + onPressed: sendMessage, + ), + ], ), - const Gap(8), - IconButton.filled( - onPressed: isStreaming.value ? null : sendMessage, - icon: Icon(isStreaming.value ? Symbols.stop : Symbols.send), - ), - ], + ), ), ), ],