✨ Scroll gradiant to think as well
This commit is contained in:
		@@ -1,4 +1,5 @@
 | 
				
			|||||||
import "dart:convert";
 | 
					import "dart:convert";
 | 
				
			||||||
 | 
					import "dart:math" as math;
 | 
				
			||||||
import "package:dio/dio.dart";
 | 
					import "package:dio/dio.dart";
 | 
				
			||||||
import "package:easy_localization/easy_localization.dart";
 | 
					import "package:easy_localization/easy_localization.dart";
 | 
				
			||||||
import "package:flutter/material.dart";
 | 
					import "package:flutter/material.dart";
 | 
				
			||||||
@@ -57,6 +58,9 @@ class ThoughtScreen extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final listController = useMemoized(() => ListController(), []);
 | 
					    final listController = useMemoized(() => ListController(), []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Scroll animation notifiers
 | 
				
			||||||
 | 
					    final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Update local thoughts when provider data changes
 | 
					    // Update local thoughts when provider data changes
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      thoughts.whenData((data) {
 | 
					      thoughts.whenData((data) {
 | 
				
			||||||
@@ -86,6 +90,20 @@ class ThoughtScreen extends HookConsumerWidget {
 | 
				
			|||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }, [localThoughts.value.length, isStreaming.value]);
 | 
					    }, [localThoughts.value.length, isStreaming.value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add scroll listener for gradient animations
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      void onScroll() {
 | 
				
			||||||
 | 
					        // Update gradient animations
 | 
				
			||||||
 | 
					        final pixels = scrollController.position.pixels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Bottom gradient: appears when not at bottom (pixels > 0)
 | 
				
			||||||
 | 
					        bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      scrollController.addListener(onScroll);
 | 
				
			||||||
 | 
					      return () => scrollController.removeListener(onScroll);
 | 
				
			||||||
 | 
					    }, [scrollController]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void sendMessage() async {
 | 
					    void sendMessage() async {
 | 
				
			||||||
      if (messageController.text.trim().isEmpty) return;
 | 
					      if (messageController.text.trim().isEmpty) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -258,65 +276,120 @@ class ThoughtScreen extends HookConsumerWidget {
 | 
				
			|||||||
          const Gap(8),
 | 
					          const Gap(8),
 | 
				
			||||||
        ],
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
      body: Center(
 | 
					      body: Stack(
 | 
				
			||||||
        child: Container(
 | 
					        children: [
 | 
				
			||||||
          constraints: BoxConstraints(maxWidth: 640),
 | 
					          // Thoughts list
 | 
				
			||||||
          child: Column(
 | 
					          Center(
 | 
				
			||||||
            children: [
 | 
					            child: Container(
 | 
				
			||||||
              Expanded(
 | 
					              constraints: BoxConstraints(maxWidth: 640),
 | 
				
			||||||
                child: thoughts.when(
 | 
					              child: Column(
 | 
				
			||||||
                  data:
 | 
					                children: [
 | 
				
			||||||
                      (thoughtList) => SuperListView.builder(
 | 
					                  Expanded(
 | 
				
			||||||
                        listController: listController,
 | 
					                    child: thoughts.when(
 | 
				
			||||||
                        controller: scrollController,
 | 
					                      data:
 | 
				
			||||||
                        padding: const EdgeInsets.only(top: 16, bottom: 16),
 | 
					                          (thoughtList) => SuperListView.builder(
 | 
				
			||||||
                        reverse: true,
 | 
					                            listController: listController,
 | 
				
			||||||
                        itemCount:
 | 
					                            controller: scrollController,
 | 
				
			||||||
                            localThoughts.value.length +
 | 
					                            padding: EdgeInsets.only(
 | 
				
			||||||
                            (isStreaming.value ? 1 : 0),
 | 
					                              top: 16,
 | 
				
			||||||
                        itemBuilder: (context, index) {
 | 
					                              bottom:
 | 
				
			||||||
                          if (isStreaming.value && index == 0) {
 | 
					                                  MediaQuery.of(context).padding.bottom +
 | 
				
			||||||
                            return ThoughtItem(
 | 
					                                  80, // Leave space for thought input
 | 
				
			||||||
                              isStreaming: true,
 | 
					                            ),
 | 
				
			||||||
                              streamingText: streamingText.value,
 | 
					                            reverse: true,
 | 
				
			||||||
                              reasoningChunks: reasoningChunks.value,
 | 
					                            itemCount:
 | 
				
			||||||
                              streamingFunctionCalls: functionCalls.value,
 | 
					                                localThoughts.value.length +
 | 
				
			||||||
                            );
 | 
					                                (isStreaming.value ? 1 : 0),
 | 
				
			||||||
                          }
 | 
					                            itemBuilder: (context, index) {
 | 
				
			||||||
                          final thoughtIndex =
 | 
					                              if (isStreaming.value && index == 0) {
 | 
				
			||||||
                              isStreaming.value ? index - 1 : index;
 | 
					                                return ThoughtItem(
 | 
				
			||||||
                          final thought = localThoughts.value[thoughtIndex];
 | 
					                                  isStreaming: true,
 | 
				
			||||||
                          return ThoughtItem(
 | 
					                                  streamingText: streamingText.value,
 | 
				
			||||||
                            thought: thought,
 | 
					                                  reasoningChunks: reasoningChunks.value,
 | 
				
			||||||
                            thoughtIndex: thoughtIndex,
 | 
					                                  streamingFunctionCalls: functionCalls.value,
 | 
				
			||||||
                          );
 | 
					                                );
 | 
				
			||||||
                        },
 | 
					                              }
 | 
				
			||||||
 | 
					                              final thoughtIndex =
 | 
				
			||||||
 | 
					                                  isStreaming.value ? index - 1 : index;
 | 
				
			||||||
 | 
					                              final thought = localThoughts.value[thoughtIndex];
 | 
				
			||||||
 | 
					                              return ThoughtItem(
 | 
				
			||||||
 | 
					                                thought: thought,
 | 
				
			||||||
 | 
					                                thoughtIndex: thoughtIndex,
 | 
				
			||||||
 | 
					                              );
 | 
				
			||||||
 | 
					                            },
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                      loading:
 | 
				
			||||||
 | 
					                          () =>
 | 
				
			||||||
 | 
					                              const Center(child: CircularProgressIndicator()),
 | 
				
			||||||
 | 
					                      error:
 | 
				
			||||||
 | 
					                          (error, _) => ResponseErrorWidget(
 | 
				
			||||||
 | 
					                            error: error,
 | 
				
			||||||
 | 
					                            onRetry:
 | 
				
			||||||
 | 
					                                () =>
 | 
				
			||||||
 | 
					                                    selectedSequenceId.value != null
 | 
				
			||||||
 | 
					                                        ? ref.invalidate(
 | 
				
			||||||
 | 
					                                          thoughtSequenceProvider(
 | 
				
			||||||
 | 
					                                            selectedSequenceId.value!,
 | 
				
			||||||
 | 
					                                          ),
 | 
				
			||||||
 | 
					                                        )
 | 
				
			||||||
 | 
					                                        : null,
 | 
				
			||||||
 | 
					                          ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          // Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
 | 
				
			||||||
 | 
					          AnimatedBuilder(
 | 
				
			||||||
 | 
					            animation: bottomGradientNotifier.value,
 | 
				
			||||||
 | 
					            builder:
 | 
				
			||||||
 | 
					                (context, child) => Positioned(
 | 
				
			||||||
 | 
					                  left: 0,
 | 
				
			||||||
 | 
					                  right: 0,
 | 
				
			||||||
 | 
					                  bottom: 0,
 | 
				
			||||||
 | 
					                  child: Opacity(
 | 
				
			||||||
 | 
					                    opacity: bottomGradientNotifier.value.value,
 | 
				
			||||||
 | 
					                    child: Container(
 | 
				
			||||||
 | 
					                      height: math.min(
 | 
				
			||||||
 | 
					                        MediaQuery.of(context).size.height * 0.1,
 | 
				
			||||||
 | 
					                        128,
 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
                  loading:
 | 
					                      decoration: BoxDecoration(
 | 
				
			||||||
                      () => const Center(child: CircularProgressIndicator()),
 | 
					                        gradient: LinearGradient(
 | 
				
			||||||
                  error:
 | 
					                          begin: Alignment.bottomCenter,
 | 
				
			||||||
                      (error, _) => ResponseErrorWidget(
 | 
					                          end: Alignment.topCenter,
 | 
				
			||||||
                        error: error,
 | 
					                          colors: [
 | 
				
			||||||
                        onRetry:
 | 
					                            Theme.of(
 | 
				
			||||||
                            () =>
 | 
					                              context,
 | 
				
			||||||
                                selectedSequenceId.value != null
 | 
					                            ).colorScheme.surfaceContainer.withOpacity(0.8),
 | 
				
			||||||
                                    ? ref.invalidate(
 | 
					                            Theme.of(
 | 
				
			||||||
                                      thoughtSequenceProvider(
 | 
					                              context,
 | 
				
			||||||
                                        selectedSequenceId.value!,
 | 
					                            ).colorScheme.surfaceContainer.withOpacity(0.0),
 | 
				
			||||||
                                      ),
 | 
					                          ],
 | 
				
			||||||
                                    )
 | 
					                        ),
 | 
				
			||||||
                                    : null,
 | 
					 | 
				
			||||||
                      ),
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          // Thought Input positioned above gradient (higher z-index)
 | 
				
			||||||
 | 
					          Positioned(
 | 
				
			||||||
 | 
					            left: 0,
 | 
				
			||||||
 | 
					            right: 0,
 | 
				
			||||||
 | 
					            bottom: 0, // At the very bottom, above gradient
 | 
				
			||||||
 | 
					            child: Center(
 | 
				
			||||||
 | 
					              child: Container(
 | 
				
			||||||
 | 
					                constraints: BoxConstraints(maxWidth: 640),
 | 
				
			||||||
 | 
					                child: ThoughtInput(
 | 
				
			||||||
 | 
					                  messageController: messageController,
 | 
				
			||||||
 | 
					                  isStreaming: isStreaming.value,
 | 
				
			||||||
 | 
					                  onSend: sendMessage,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              ThoughtInput(
 | 
					            ),
 | 
				
			||||||
                messageController: messageController,
 | 
					 | 
				
			||||||
                isStreaming: isStreaming.value,
 | 
					 | 
				
			||||||
                onSend: sendMessage,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,5 @@
 | 
				
			|||||||
import "dart:convert";
 | 
					import "dart:convert";
 | 
				
			||||||
 | 
					import "dart:math" as math;
 | 
				
			||||||
import "package:dio/dio.dart";
 | 
					import "package:dio/dio.dart";
 | 
				
			||||||
import "package:easy_localization/easy_localization.dart";
 | 
					import "package:easy_localization/easy_localization.dart";
 | 
				
			||||||
import "package:flutter/material.dart";
 | 
					import "package:flutter/material.dart";
 | 
				
			||||||
@@ -54,6 +55,9 @@ class ThoughtSheet extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    final listController = useMemoized(() => ListController(), []);
 | 
					    final listController = useMemoized(() => ListController(), []);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Scroll animation notifiers
 | 
				
			||||||
 | 
					    final bottomGradientNotifier = useState(ValueNotifier<double>(0.0));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    // Scroll to bottom when thoughts change or streaming state changes
 | 
					    // Scroll to bottom when thoughts change or streaming state changes
 | 
				
			||||||
    useEffect(() {
 | 
					    useEffect(() {
 | 
				
			||||||
      if (localThoughts.value.isNotEmpty || isStreaming.value) {
 | 
					      if (localThoughts.value.isNotEmpty || isStreaming.value) {
 | 
				
			||||||
@@ -68,6 +72,20 @@ class ThoughtSheet extends HookConsumerWidget {
 | 
				
			|||||||
      return null;
 | 
					      return null;
 | 
				
			||||||
    }, [localThoughts.value.length, isStreaming.value]);
 | 
					    }, [localThoughts.value.length, isStreaming.value]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // Add scroll listener for gradient animations
 | 
				
			||||||
 | 
					    useEffect(() {
 | 
				
			||||||
 | 
					      void onScroll() {
 | 
				
			||||||
 | 
					        // Update gradient animations
 | 
				
			||||||
 | 
					        final pixels = scrollController.position.pixels;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        // Bottom gradient: appears when not at bottom (pixels > 0)
 | 
				
			||||||
 | 
					        bottomGradientNotifier.value.value = (pixels / 500.0).clamp(0.0, 1.0);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      scrollController.addListener(onScroll);
 | 
				
			||||||
 | 
					      return () => scrollController.removeListener(onScroll);
 | 
				
			||||||
 | 
					    }, [scrollController]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    void sendMessage() async {
 | 
					    void sendMessage() async {
 | 
				
			||||||
      if (messageController.text.trim().isEmpty) return;
 | 
					      if (messageController.text.trim().isEmpty) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -196,47 +214,103 @@ class ThoughtSheet extends HookConsumerWidget {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return SheetScaffold(
 | 
					    return SheetScaffold(
 | 
				
			||||||
      titleText: currentTopic.value ?? 'aiThought'.tr(),
 | 
					      titleText: currentTopic.value ?? 'aiThought'.tr(),
 | 
				
			||||||
      child: Center(
 | 
					      child: Stack(
 | 
				
			||||||
        child: Container(
 | 
					        children: [
 | 
				
			||||||
          constraints: BoxConstraints(maxWidth: 640),
 | 
					          // Thoughts list
 | 
				
			||||||
          child: Column(
 | 
					          Center(
 | 
				
			||||||
            children: [
 | 
					            child: Container(
 | 
				
			||||||
              Expanded(
 | 
					              constraints: BoxConstraints(maxWidth: 640),
 | 
				
			||||||
                child: SuperListView.builder(
 | 
					              child: Column(
 | 
				
			||||||
                  listController: listController,
 | 
					                children: [
 | 
				
			||||||
                  controller: scrollController,
 | 
					                  Expanded(
 | 
				
			||||||
                  padding: const EdgeInsets.only(top: 16, bottom: 16),
 | 
					                    child: SuperListView.builder(
 | 
				
			||||||
                  reverse: true,
 | 
					                      listController: listController,
 | 
				
			||||||
                  itemCount:
 | 
					                      controller: scrollController,
 | 
				
			||||||
                      localThoughts.value.length + (isStreaming.value ? 1 : 0),
 | 
					                      padding: EdgeInsets.only(
 | 
				
			||||||
                  itemBuilder: (context, index) {
 | 
					                        top: 16,
 | 
				
			||||||
                    if (isStreaming.value && index == 0) {
 | 
					                        bottom:
 | 
				
			||||||
                      return ThoughtItem(
 | 
					                            MediaQuery.of(context).padding.bottom +
 | 
				
			||||||
                        isStreaming: true,
 | 
					                            80, // Leave space for thought input
 | 
				
			||||||
                        streamingText: streamingText.value,
 | 
					                      ),
 | 
				
			||||||
                        reasoningChunks: reasoningChunks.value,
 | 
					                      reverse: true,
 | 
				
			||||||
                        streamingFunctionCalls: functionCalls.value,
 | 
					                      itemCount:
 | 
				
			||||||
                      );
 | 
					                          localThoughts.value.length +
 | 
				
			||||||
                    }
 | 
					                          (isStreaming.value ? 1 : 0),
 | 
				
			||||||
                    final thoughtIndex = isStreaming.value ? index - 1 : index;
 | 
					                      itemBuilder: (context, index) {
 | 
				
			||||||
                    final thought = localThoughts.value[thoughtIndex];
 | 
					                        if (isStreaming.value && index == 0) {
 | 
				
			||||||
                    return ThoughtItem(
 | 
					                          return ThoughtItem(
 | 
				
			||||||
                      thought: thought,
 | 
					                            isStreaming: true,
 | 
				
			||||||
                      thoughtIndex: thoughtIndex,
 | 
					                            streamingText: streamingText.value,
 | 
				
			||||||
                    );
 | 
					                            reasoningChunks: reasoningChunks.value,
 | 
				
			||||||
                  },
 | 
					                            streamingFunctionCalls: functionCalls.value,
 | 
				
			||||||
 | 
					                          );
 | 
				
			||||||
 | 
					                        }
 | 
				
			||||||
 | 
					                        final thoughtIndex =
 | 
				
			||||||
 | 
					                            isStreaming.value ? index - 1 : index;
 | 
				
			||||||
 | 
					                        final thought = localThoughts.value[thoughtIndex];
 | 
				
			||||||
 | 
					                        return ThoughtItem(
 | 
				
			||||||
 | 
					                          thought: thought,
 | 
				
			||||||
 | 
					                          thoughtIndex: thoughtIndex,
 | 
				
			||||||
 | 
					                        );
 | 
				
			||||||
 | 
					                      },
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ],
 | 
				
			||||||
 | 
					              ),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          // Bottom gradient - appears when scrolling towards newer thoughts (behind thought input)
 | 
				
			||||||
 | 
					          AnimatedBuilder(
 | 
				
			||||||
 | 
					            animation: bottomGradientNotifier.value,
 | 
				
			||||||
 | 
					            builder:
 | 
				
			||||||
 | 
					                (context, child) => Positioned(
 | 
				
			||||||
 | 
					                  left: 0,
 | 
				
			||||||
 | 
					                  right: 0,
 | 
				
			||||||
 | 
					                  bottom: 0,
 | 
				
			||||||
 | 
					                  child: Opacity(
 | 
				
			||||||
 | 
					                    opacity: bottomGradientNotifier.value.value,
 | 
				
			||||||
 | 
					                    child: Container(
 | 
				
			||||||
 | 
					                      height: math.min(
 | 
				
			||||||
 | 
					                        MediaQuery.of(context).size.height * 0.1,
 | 
				
			||||||
 | 
					                        128,
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                      decoration: BoxDecoration(
 | 
				
			||||||
 | 
					                        gradient: LinearGradient(
 | 
				
			||||||
 | 
					                          begin: Alignment.bottomCenter,
 | 
				
			||||||
 | 
					                          end: Alignment.topCenter,
 | 
				
			||||||
 | 
					                          colors: [
 | 
				
			||||||
 | 
					                            Theme.of(
 | 
				
			||||||
 | 
					                              context,
 | 
				
			||||||
 | 
					                            ).colorScheme.surfaceContainer.withOpacity(0.8),
 | 
				
			||||||
 | 
					                            Theme.of(
 | 
				
			||||||
 | 
					                              context,
 | 
				
			||||||
 | 
					                            ).colorScheme.surfaceContainer.withOpacity(0.0),
 | 
				
			||||||
 | 
					                          ],
 | 
				
			||||||
 | 
					                        ),
 | 
				
			||||||
 | 
					                      ),
 | 
				
			||||||
 | 
					                    ),
 | 
				
			||||||
 | 
					                  ),
 | 
				
			||||||
 | 
					                ),
 | 
				
			||||||
 | 
					          ),
 | 
				
			||||||
 | 
					          // Thought Input positioned above gradient (higher z-index)
 | 
				
			||||||
 | 
					          Positioned(
 | 
				
			||||||
 | 
					            left: 0,
 | 
				
			||||||
 | 
					            right: 0,
 | 
				
			||||||
 | 
					            bottom: 0, // At the very bottom, above gradient
 | 
				
			||||||
 | 
					            child: Center(
 | 
				
			||||||
 | 
					              child: Container(
 | 
				
			||||||
 | 
					                constraints: BoxConstraints(maxWidth: 640),
 | 
				
			||||||
 | 
					                child: ThoughtInput(
 | 
				
			||||||
 | 
					                  messageController: messageController,
 | 
				
			||||||
 | 
					                  isStreaming: isStreaming.value,
 | 
				
			||||||
 | 
					                  onSend: sendMessage,
 | 
				
			||||||
 | 
					                  attachedMessages: attachedMessages,
 | 
				
			||||||
 | 
					                  attachedPosts: attachedPosts,
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              ThoughtInput(
 | 
					            ),
 | 
				
			||||||
                messageController: messageController,
 | 
					 | 
				
			||||||
                isStreaming: isStreaming.value,
 | 
					 | 
				
			||||||
                onSend: sendMessage,
 | 
					 | 
				
			||||||
                attachedMessages: attachedMessages,
 | 
					 | 
				
			||||||
                attachedPosts: attachedPosts,
 | 
					 | 
				
			||||||
              ),
 | 
					 | 
				
			||||||
            ],
 | 
					 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
        ),
 | 
					        ],
 | 
				
			||||||
      ),
 | 
					      ),
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user