Scroll gradiant to think as well

This commit is contained in:
2025-11-02 23:55:00 +08:00
parent 848439f664
commit dd17b2b9c1
2 changed files with 239 additions and 92 deletions

View File

@@ -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,7 +276,10 @@ class ThoughtScreen extends HookConsumerWidget {
const Gap(8), const Gap(8),
], ],
), ),
body: Center( body: Stack(
children: [
// Thoughts list
Center(
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
child: Column( child: Column(
@@ -269,7 +290,12 @@ class ThoughtScreen extends HookConsumerWidget {
(thoughtList) => SuperListView.builder( (thoughtList) => SuperListView.builder(
listController: listController, listController: listController,
controller: scrollController, controller: scrollController,
padding: const EdgeInsets.only(top: 16, bottom: 16), padding: EdgeInsets.only(
top: 16,
bottom:
MediaQuery.of(context).padding.bottom +
80, // Leave space for thought input
),
reverse: true, reverse: true,
itemCount: itemCount:
localThoughts.value.length + localThoughts.value.length +
@@ -293,7 +319,8 @@ class ThoughtScreen extends HookConsumerWidget {
}, },
), ),
loading: loading:
() => const Center(child: CircularProgressIndicator()), () =>
const Center(child: CircularProgressIndicator()),
error: error:
(error, _) => ResponseErrorWidget( (error, _) => ResponseErrorWidget(
error: error, error: error,
@@ -309,15 +336,61 @@ class ThoughtScreen extends HookConsumerWidget {
), ),
), ),
), ),
ThoughtInput(
messageController: messageController,
isStreaming: isStreaming.value,
onSend: sendMessage,
),
], ],
), ),
), ),
), ),
// 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,
),
),
),
),
],
),
); );
} }
} }

View File

@@ -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,7 +214,10 @@ class ThoughtSheet extends HookConsumerWidget {
return SheetScaffold( return SheetScaffold(
titleText: currentTopic.value ?? 'aiThought'.tr(), titleText: currentTopic.value ?? 'aiThought'.tr(),
child: Center( child: Stack(
children: [
// Thoughts list
Center(
child: Container( child: Container(
constraints: BoxConstraints(maxWidth: 640), constraints: BoxConstraints(maxWidth: 640),
child: Column( child: Column(
@@ -205,10 +226,16 @@ class ThoughtSheet extends HookConsumerWidget {
child: SuperListView.builder( child: SuperListView.builder(
listController: listController, listController: listController,
controller: scrollController, controller: scrollController,
padding: const EdgeInsets.only(top: 16, bottom: 16), padding: EdgeInsets.only(
top: 16,
bottom:
MediaQuery.of(context).padding.bottom +
80, // Leave space for thought input
),
reverse: true, reverse: true,
itemCount: itemCount:
localThoughts.value.length + (isStreaming.value ? 1 : 0), localThoughts.value.length +
(isStreaming.value ? 1 : 0),
itemBuilder: (context, index) { itemBuilder: (context, index) {
if (isStreaming.value && index == 0) { if (isStreaming.value && index == 0) {
return ThoughtItem( return ThoughtItem(
@@ -218,7 +245,8 @@ class ThoughtSheet extends HookConsumerWidget {
streamingFunctionCalls: functionCalls.value, streamingFunctionCalls: functionCalls.value,
); );
} }
final thoughtIndex = isStreaming.value ? index - 1 : index; final thoughtIndex =
isStreaming.value ? index - 1 : index;
final thought = localThoughts.value[thoughtIndex]; final thought = localThoughts.value[thoughtIndex];
return ThoughtItem( return ThoughtItem(
thought: thought, thought: thought,
@@ -227,17 +255,63 @@ class ThoughtSheet extends HookConsumerWidget {
}, },
), ),
), ),
ThoughtInput( ],
),
),
),
// 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, messageController: messageController,
isStreaming: isStreaming.value, isStreaming: isStreaming.value,
onSend: sendMessage, onSend: sendMessage,
attachedMessages: attachedMessages, attachedMessages: attachedMessages,
attachedPosts: attachedPosts, attachedPosts: attachedPosts,
), ),
),
),
),
], ],
), ),
),
),
); );
} }
} }