💄 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);
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'<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];
}
} catch (e) { } catch (e) {
showErrorAlert('Failed to parse AI response'); showErrorAlert('Failed to parse AI response');
} }
@@ -163,51 +214,75 @@ 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) {
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), final key = Key('thought-${thought.id}');
padding: const EdgeInsets.all(12),
decoration: BoxDecoration( final thoughtWidget = Container(
color: margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
thought.role == ThinkingThoughtRole.assistant padding: const EdgeInsets.all(12),
? Theme.of(context).colorScheme.surfaceContainerHighest decoration: BoxDecoration(
: Theme.of(context).colorScheme.primaryContainer, color:
borderRadius: BorderRadius.circular(12), thought.role == ThinkingThoughtRole.assistant
), ? Theme.of(context).colorScheme.surfaceContainerHighest
child: Column( : Theme.of(context).colorScheme.primaryContainer,
crossAxisAlignment: CrossAxisAlignment.start, borderRadius: BorderRadius.circular(12),
children: [ ),
Row( child: Column(
children: [ crossAxisAlignment: CrossAxisAlignment.start,
Icon( children: [
thought.role == ThinkingThoughtRole.assistant Row(
? Symbols.smart_toy children: [
: Symbols.person, Icon(
size: 20, thought.role == ThinkingThoughtRole.assistant
), ? Symbols.smart_toy
const Gap(8), : Symbols.person,
Text( size: 20,
thought.role == ThinkingThoughtRole.assistant ),
? 'AI Assistant' const Gap(8),
: 'You', Text(
style: Theme.of(context).textTheme.titleSmall, 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,
), ),
], const Gap(8),
), if (thought.content != null)
); MarkdownTextContent(
content: thought.content!,
textStyle: Theme.of(context).textTheme.bodyMedium,
),
],
),
);
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),
@@ -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,42 +402,51 @@ 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: Row( child: Material(
children: [ elevation: 2,
Expanded( color: Theme.of(context).colorScheme.surfaceContainerHighest,
child: TextField( borderRadius: BorderRadius.circular(32),
controller: messageController, child: Padding(
decoration: InputDecoration( padding: const EdgeInsets.symmetric(vertical: 6, horizontal: 8),
hintText: 'Ask me anything...', child: Row(
border: OutlineInputBorder( crossAxisAlignment: CrossAxisAlignment.start,
borderRadius: BorderRadius.circular(24), children: [
), Expanded(
contentPadding: const EdgeInsets.symmetric( child: TextField(
horizontal: 16, controller: messageController,
vertical: 12, 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, IconButton(
textInputAction: TextInputAction.send, icon: Icon(isStreaming.value ? Symbols.stop : Icons.send),
onSubmitted: (_) => sendMessage(), 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),
),
],
), ),
), ),
], ],