💄 Optimize the AI thought
This commit is contained in:
@@ -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),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
|||||||
Reference in New Issue
Block a user