💄 Optimize the AI thought

This commit is contained in:
2025-10-25 23:20:10 +08:00
parent aa180a1358
commit e3be691596

View File

@@ -52,6 +52,7 @@ class ThoughtScreen extends HookConsumerWidget {
: const AsyncValue<List<SnThinkingThought>>.data([]); : const AsyncValue<List<SnThinkingThought>>.data([]);
final localThoughts = useState<List<SnThinkingThought>>([]); final localThoughts = useState<List<SnThinkingThought>>([]);
final currentTopic = useState<String?>('AI Thought');
final messageController = useTextEditingController(); final messageController = useTextEditingController();
final scrollController = useScrollController(); final scrollController = useScrollController();
@@ -62,7 +63,15 @@ class ThoughtScreen extends HookConsumerWidget {
// Update local thoughts when provider data changes // Update local thoughts when provider data changes
useEffect(() { 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; return null;
}, [thoughts]); }, [thoughts]);
@@ -138,13 +147,55 @@ class ThoughtScreen extends HookConsumerWidget {
isStreaming.value = false; isStreaming.value = false;
// Parse the response and add AI thought // Parse the response and add AI thought
try { try {
final lines = buffer.toString().split('\n'); final lines =
final lastLine = lines.lastWhere( buffer
(line) => line.trim().isNotEmpty, .toString()
); .split('\n')
.where((line) => line.trim().isNotEmpty)
.toList();
final lastLine = lines.last;
final responseJson = jsonDecode(lastLine); final responseJson = jsonDecode(lastLine);
final aiThought = SnThinkingThought.fromJson(responseJson); final aiThought = SnThinkingThought.fromJson(responseJson);
// Check for topic in second last line
String? topic;
if (lines.length >= 2) {
final secondLastLine = lines[lines.length - 2];
final topicMatch = RegExp(
r'<topic>(.*)</topic>',
).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]; localThoughts.value = [aiThought, ...localThoughts.value];
}
} catch (e) { } catch (e) {
showErrorAlert('Failed to parse AI response'); showErrorAlert('Failed to parse AI response');
} }
@@ -163,13 +214,17 @@ class ThoughtScreen extends HookConsumerWidget {
); );
messageController.clear(); messageController.clear();
FocusManager.instance.primaryFocus?.unfocus();
} catch (error) { } catch (error) {
isStreaming.value = false; isStreaming.value = false;
showErrorAlert(error); showErrorAlert(error);
} }
} }
Widget thoughtItem(SnThinkingThought thought) => Container( Widget thoughtItem(SnThinkingThought thought, int index) {
final key = Key('thought-${thought.id}');
final thoughtWidget = Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
decoration: BoxDecoration( decoration: BoxDecoration(
@@ -209,6 +264,26 @@ class ThoughtScreen extends HookConsumerWidget {
), ),
); );
return TweenAnimationBuilder<double>(
key: key,
tween: Tween<double>(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( Widget streamingThoughtItem() => Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
padding: const EdgeInsets.all(12), padding: const EdgeInsets.all(12),
@@ -246,7 +321,7 @@ class ThoughtScreen extends HookConsumerWidget {
return AppScaffold( return AppScaffold(
appBar: AppBar( appBar: AppBar(
title: const Text('AI Thought'), title: Text(currentTopic.value ?? 'AI Thought'),
actions: [ actions: [
IconButton( IconButton(
icon: const Icon(Symbols.history), icon: const Icon(Symbols.history),
@@ -295,7 +370,7 @@ class ThoughtScreen extends HookConsumerWidget {
(thoughtList) => SuperListView.builder( (thoughtList) => SuperListView.builder(
listController: listController, listController: listController,
controller: scrollController, controller: scrollController,
padding: const EdgeInsets.only(bottom: 16), padding: const EdgeInsets.only(top: 16),
reverse: true, reverse: true,
itemCount: itemCount:
localThoughts.value.length + localThoughts.value.length +
@@ -307,7 +382,7 @@ class ThoughtScreen extends HookConsumerWidget {
final thoughtIndex = final thoughtIndex =
isStreaming.value ? index - 1 : index; isStreaming.value ? index - 1 : index;
final thought = localThoughts.value[thoughtIndex]; final thought = localThoughts.value[thoughtIndex];
return thoughtItem(thought); return thoughtItem(thought, thoughtIndex);
}, },
), ),
loading: () => const Center(child: CircularProgressIndicator()), loading: () => const Center(child: CircularProgressIndicator()),
@@ -327,44 +402,53 @@ class ThoughtScreen extends HookConsumerWidget {
), ),
), ),
Container( Container(
padding: const EdgeInsets.all(16), margin: EdgeInsets.only(
decoration: BoxDecoration( left: 16,
color: Theme.of(context).scaffoldBackgroundColor, right: 16,
border: Border( bottom: 16 + MediaQuery.of(context).padding.bottom,
top: BorderSide(
color: Theme.of(context).dividerColor,
width: 1,
),
),
), ),
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( child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [ children: [
Expanded( Expanded(
child: TextField( child: TextField(
controller: messageController, controller: messageController,
keyboardType: TextInputType.multiline,
enabled: !isStreaming.value,
decoration: InputDecoration( decoration: InputDecoration(
hintText: 'Ask me anything...', hintText:
border: OutlineInputBorder( isStreaming.value
borderRadius: BorderRadius.circular(24), ? 'AI is thinking...'
), : 'Ask me anything...',
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric( contentPadding: const EdgeInsets.symmetric(
horizontal: 16, horizontal: 12,
vertical: 12, vertical: 12,
), ),
), ),
maxLines: null, maxLines: 5,
minLines: 1,
textInputAction: TextInputAction.send, textInputAction: TextInputAction.send,
onSubmitted: (_) => sendMessage(), onSubmitted: (_) => sendMessage(),
), ),
), ),
const Gap(8), IconButton(
IconButton.filled( icon: Icon(isStreaming.value ? Symbols.stop : Icons.send),
onPressed: isStreaming.value ? null : sendMessage, color: Theme.of(context).colorScheme.primary,
icon: Icon(isStreaming.value ? Symbols.stop : Symbols.send), onPressed: sendMessage,
), ),
], ],
), ),
), ),
),
),
], ],
), ),
); );