🐛 Fixes and optimizations
This commit is contained in:
@@ -1,10 +1,11 @@
|
||||
import "dart:async";
|
||||
import "dart:convert";
|
||||
import "package:dio/dio.dart";
|
||||
import "package:easy_localization/easy_localization.dart";
|
||||
import "package:flutter/material.dart";
|
||||
import "package:flutter_hooks/flutter_hooks.dart";
|
||||
import "package:gap/gap.dart";
|
||||
import "package:google_fonts/google_fonts.dart";
|
||||
import "package:riverpod_annotation/riverpod_annotation.dart";
|
||||
import "package:hooks_riverpod/hooks_riverpod.dart";
|
||||
import "package:island/models/thought.dart";
|
||||
import "package:island/pods/network.dart";
|
||||
@@ -18,19 +19,21 @@ import "package:island/widgets/thought/thought_sequence_list.dart";
|
||||
import "package:material_symbols_icons/material_symbols_icons.dart";
|
||||
import "package:super_sliver_list/super_sliver_list.dart";
|
||||
|
||||
final thoughtSequenceProvider =
|
||||
FutureProvider.family<List<SnThinkingThought>, String>((
|
||||
ref,
|
||||
sequenceId,
|
||||
) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final response = await apiClient.get(
|
||||
'/insight/thought/sequences/$sequenceId',
|
||||
);
|
||||
return (response.data as List)
|
||||
.map((e) => SnThinkingThought.fromJson(e))
|
||||
.toList();
|
||||
});
|
||||
part 'think.g.dart';
|
||||
|
||||
@riverpod
|
||||
Future<List<SnThinkingThought>> thoughtSequence(
|
||||
Ref ref,
|
||||
String sequenceId,
|
||||
) async {
|
||||
final apiClient = ref.watch(apiClientProvider);
|
||||
final response = await apiClient.get(
|
||||
'/insight/thought/sequences/$sequenceId',
|
||||
);
|
||||
return (response.data as List)
|
||||
.map((e) => SnThinkingThought.fromJson(e))
|
||||
.toList();
|
||||
}
|
||||
|
||||
class ThoughtScreen extends HookConsumerWidget {
|
||||
const ThoughtScreen({super.key});
|
||||
@@ -50,6 +53,8 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
final scrollController = useScrollController();
|
||||
final isStreaming = useState(false);
|
||||
final streamingText = useState<String>('');
|
||||
final functionCalls = useState<List<String>>([]);
|
||||
final reasoningChunks = useState<List<String>>([]);
|
||||
|
||||
final listController = useMemoized(() => ListController(), []);
|
||||
|
||||
@@ -124,6 +129,8 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
try {
|
||||
isStreaming.value = true;
|
||||
streamingText.value = '';
|
||||
functionCalls.value = [];
|
||||
reasoningChunks.value = [];
|
||||
|
||||
final apiClient = ref.read(apiClientProvider);
|
||||
final response = await apiClient.post(
|
||||
@@ -137,65 +144,66 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
);
|
||||
|
||||
final stream = response.data.stream;
|
||||
final completer = Completer<String>();
|
||||
final buffer = StringBuffer();
|
||||
final lineBuffer = StringBuffer();
|
||||
|
||||
stream.listen(
|
||||
(data) {
|
||||
final chunk = utf8.decode(data);
|
||||
buffer.write(chunk);
|
||||
streamingText.value = buffer.toString();
|
||||
lineBuffer.write(chunk);
|
||||
final lines = lineBuffer.toString().split('\n');
|
||||
lineBuffer.clear();
|
||||
lineBuffer.write(lines.last); // keep incomplete line
|
||||
|
||||
for (final line in lines.sublist(0, lines.length - 1)) {
|
||||
if (line.trim().isEmpty) continue;
|
||||
try {
|
||||
if (line.startsWith('data: ')) {
|
||||
final jsonStr = line.substring(6);
|
||||
final event = jsonDecode(jsonStr);
|
||||
final type = event['type'];
|
||||
final eventData = event['data'];
|
||||
if (type == 'text') {
|
||||
streamingText.value += eventData;
|
||||
} else if (type == 'function_call') {
|
||||
functionCalls.value = [
|
||||
...functionCalls.value,
|
||||
JsonEncoder.withIndent(' ').convert(eventData),
|
||||
];
|
||||
} else if (type == 'reasoning') {
|
||||
reasoningChunks.value = [
|
||||
...reasoningChunks.value,
|
||||
eventData,
|
||||
];
|
||||
}
|
||||
} else if (line.startsWith('topic: ')) {
|
||||
final jsonStr = line.substring(7);
|
||||
final event = jsonDecode(jsonStr);
|
||||
currentTopic.value = event['data'];
|
||||
} else if (line.startsWith('thought: ')) {
|
||||
final jsonStr = line.substring(9);
|
||||
final event = jsonDecode(jsonStr);
|
||||
final aiThought = SnThinkingThought.fromJson(event['data']);
|
||||
localThoughts.value = [aiThought, ...localThoughts.value];
|
||||
if (selectedSequenceId.value == null &&
|
||||
aiThought.sequenceId.isNotEmpty) {
|
||||
selectedSequenceId.value = aiThought.sequenceId;
|
||||
}
|
||||
isStreaming.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
// Ignore parsing errors for individual events
|
||||
}
|
||||
}
|
||||
},
|
||||
onDone: () {
|
||||
completer.complete(buffer.toString());
|
||||
isStreaming.value = false;
|
||||
// Parse the response and add AI thought
|
||||
try {
|
||||
final lines =
|
||||
buffer
|
||||
.toString()
|
||||
.split('\n')
|
||||
.where((line) => line.trim().isNotEmpty)
|
||||
.toList();
|
||||
final lastLine = lines.last;
|
||||
final responseJson = jsonDecode(lastLine);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
// Add AI thought to conversation
|
||||
localThoughts.value = [aiThought, ...localThoughts.value];
|
||||
|
||||
// Update selected sequence ID if it was null (new conversation)
|
||||
if (selectedSequenceId.value == null &&
|
||||
aiThought.sequenceId.isNotEmpty) {
|
||||
selectedSequenceId.value = aiThought.sequenceId;
|
||||
}
|
||||
|
||||
// Update current topic if found (AI responses don't include sequence to prevent backend loops)
|
||||
if (topic != null) {
|
||||
currentTopic.value = topic;
|
||||
}
|
||||
} catch (e) {
|
||||
if (isStreaming.value) {
|
||||
isStreaming.value = false;
|
||||
showErrorAlert('thoughtParseError'.tr());
|
||||
}
|
||||
},
|
||||
onError: (error) {
|
||||
completer.completeError(error);
|
||||
isStreaming.value = false;
|
||||
// Handle streaming response errors differently
|
||||
if (error is DioException && error.response?.data is ResponseBody) {
|
||||
// For streaming responses, show a generic error message
|
||||
showErrorAlert('toughtParseError'.tr());
|
||||
} else {
|
||||
showErrorAlert(error);
|
||||
@@ -211,6 +219,74 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
}
|
||||
}
|
||||
|
||||
Widget buildChunkTiles(List<SnThinkingChunk> chunks) {
|
||||
return Column(
|
||||
children: [
|
||||
...chunks
|
||||
.where((chunk) => chunk.type == ThinkingChunkType.reasoning)
|
||||
.map(
|
||||
(chunk) => Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: Theme(
|
||||
data: Theme.of(
|
||||
context,
|
||||
).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
title: Text(
|
||||
'Reasoning',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
chunk.data?['content'] ?? '',
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...chunks
|
||||
.where((chunk) => chunk.type == ThinkingChunkType.functionCall)
|
||||
.map(
|
||||
(chunk) => Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: Theme(
|
||||
data: Theme.of(
|
||||
context,
|
||||
).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: Text(
|
||||
'Function Call',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SelectableText(
|
||||
JsonEncoder.withIndent(' ').convert(chunk.data),
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget thoughtItem(SnThinkingThought thought, int index) {
|
||||
final key = Key('thought-${thought.id}');
|
||||
|
||||
@@ -244,7 +320,7 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
children: [
|
||||
Text(
|
||||
thought.role == ThinkingThoughtRole.assistant
|
||||
? 'toughtAiName'.tr()
|
||||
? 'thoughtAiName'.tr()
|
||||
: 'thoughtUserName'.tr(),
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
@@ -266,6 +342,10 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
],
|
||||
),
|
||||
const Gap(8),
|
||||
if (thought.chunks.isNotEmpty) ...[
|
||||
buildChunkTiles(thought.chunks),
|
||||
const Gap(8),
|
||||
],
|
||||
if (thought.content != null)
|
||||
MarkdownTextContent(
|
||||
isSelectable: true,
|
||||
@@ -327,6 +407,71 @@ class ThoughtScreen extends HookConsumerWidget {
|
||||
content: streamingText.value,
|
||||
textStyle: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
if (reasoningChunks.value.isNotEmpty ||
|
||||
functionCalls.value.isNotEmpty) ...[
|
||||
const Gap(8),
|
||||
Column(
|
||||
children: [
|
||||
...reasoningChunks.value.map(
|
||||
(chunk) => Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: Theme(
|
||||
data: Theme.of(
|
||||
context,
|
||||
).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
title: Text(
|
||||
'Reasoning',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: Text(
|
||||
chunk,
|
||||
style: Theme.of(context).textTheme.bodyMedium,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
...functionCalls.value.map(
|
||||
(call) => Card(
|
||||
margin: const EdgeInsets.only(bottom: 8),
|
||||
child: Theme(
|
||||
data: Theme.of(
|
||||
context,
|
||||
).copyWith(dividerColor: Colors.transparent),
|
||||
child: ExpansionTile(
|
||||
shape: RoundedRectangleBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
visualDensity: VisualDensity.compact,
|
||||
title: Text(
|
||||
'Function Call',
|
||||
style: Theme.of(context).textTheme.titleSmall,
|
||||
),
|
||||
children: [
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(8.0),
|
||||
child: SelectableText(
|
||||
call,
|
||||
style: GoogleFonts.robotoMono(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
156
lib/screens/thought/think.g.dart
Normal file
156
lib/screens/thought/think.g.dart
Normal file
@@ -0,0 +1,156 @@
|
||||
// GENERATED CODE - DO NOT MODIFY BY HAND
|
||||
|
||||
part of 'think.dart';
|
||||
|
||||
// **************************************************************************
|
||||
// RiverpodGenerator
|
||||
// **************************************************************************
|
||||
|
||||
String _$thoughtSequenceHash() => r'2a93c0a04f9a720ba474c02a36502940fb7f3ed7';
|
||||
|
||||
/// Copied from Dart SDK
|
||||
class _SystemHash {
|
||||
_SystemHash._();
|
||||
|
||||
static int combine(int hash, int value) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + value);
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x0007ffff & hash) << 10));
|
||||
return hash ^ (hash >> 6);
|
||||
}
|
||||
|
||||
static int finish(int hash) {
|
||||
// ignore: parameter_assignments
|
||||
hash = 0x1fffffff & (hash + ((0x03ffffff & hash) << 3));
|
||||
// ignore: parameter_assignments
|
||||
hash = hash ^ (hash >> 11);
|
||||
return 0x1fffffff & (hash + ((0x00003fff & hash) << 15));
|
||||
}
|
||||
}
|
||||
|
||||
/// See also [thoughtSequence].
|
||||
@ProviderFor(thoughtSequence)
|
||||
const thoughtSequenceProvider = ThoughtSequenceFamily();
|
||||
|
||||
/// See also [thoughtSequence].
|
||||
class ThoughtSequenceFamily
|
||||
extends Family<AsyncValue<List<SnThinkingThought>>> {
|
||||
/// See also [thoughtSequence].
|
||||
const ThoughtSequenceFamily();
|
||||
|
||||
/// See also [thoughtSequence].
|
||||
ThoughtSequenceProvider call(String sequenceId) {
|
||||
return ThoughtSequenceProvider(sequenceId);
|
||||
}
|
||||
|
||||
@override
|
||||
ThoughtSequenceProvider getProviderOverride(
|
||||
covariant ThoughtSequenceProvider provider,
|
||||
) {
|
||||
return call(provider.sequenceId);
|
||||
}
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _dependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get dependencies => _dependencies;
|
||||
|
||||
static const Iterable<ProviderOrFamily>? _allTransitiveDependencies = null;
|
||||
|
||||
@override
|
||||
Iterable<ProviderOrFamily>? get allTransitiveDependencies =>
|
||||
_allTransitiveDependencies;
|
||||
|
||||
@override
|
||||
String? get name => r'thoughtSequenceProvider';
|
||||
}
|
||||
|
||||
/// See also [thoughtSequence].
|
||||
class ThoughtSequenceProvider
|
||||
extends AutoDisposeFutureProvider<List<SnThinkingThought>> {
|
||||
/// See also [thoughtSequence].
|
||||
ThoughtSequenceProvider(String sequenceId)
|
||||
: this._internal(
|
||||
(ref) => thoughtSequence(ref as ThoughtSequenceRef, sequenceId),
|
||||
from: thoughtSequenceProvider,
|
||||
name: r'thoughtSequenceProvider',
|
||||
debugGetCreateSourceHash:
|
||||
const bool.fromEnvironment('dart.vm.product')
|
||||
? null
|
||||
: _$thoughtSequenceHash,
|
||||
dependencies: ThoughtSequenceFamily._dependencies,
|
||||
allTransitiveDependencies:
|
||||
ThoughtSequenceFamily._allTransitiveDependencies,
|
||||
sequenceId: sequenceId,
|
||||
);
|
||||
|
||||
ThoughtSequenceProvider._internal(
|
||||
super._createNotifier, {
|
||||
required super.name,
|
||||
required super.dependencies,
|
||||
required super.allTransitiveDependencies,
|
||||
required super.debugGetCreateSourceHash,
|
||||
required super.from,
|
||||
required this.sequenceId,
|
||||
}) : super.internal();
|
||||
|
||||
final String sequenceId;
|
||||
|
||||
@override
|
||||
Override overrideWith(
|
||||
FutureOr<List<SnThinkingThought>> Function(ThoughtSequenceRef provider)
|
||||
create,
|
||||
) {
|
||||
return ProviderOverride(
|
||||
origin: this,
|
||||
override: ThoughtSequenceProvider._internal(
|
||||
(ref) => create(ref as ThoughtSequenceRef),
|
||||
from: from,
|
||||
name: null,
|
||||
dependencies: null,
|
||||
allTransitiveDependencies: null,
|
||||
debugGetCreateSourceHash: null,
|
||||
sequenceId: sequenceId,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
AutoDisposeFutureProviderElement<List<SnThinkingThought>> createElement() {
|
||||
return _ThoughtSequenceProviderElement(this);
|
||||
}
|
||||
|
||||
@override
|
||||
bool operator ==(Object other) {
|
||||
return other is ThoughtSequenceProvider && other.sequenceId == sequenceId;
|
||||
}
|
||||
|
||||
@override
|
||||
int get hashCode {
|
||||
var hash = _SystemHash.combine(0, runtimeType.hashCode);
|
||||
hash = _SystemHash.combine(hash, sequenceId.hashCode);
|
||||
|
||||
return _SystemHash.finish(hash);
|
||||
}
|
||||
}
|
||||
|
||||
@Deprecated('Will be removed in 3.0. Use Ref instead')
|
||||
// ignore: unused_element
|
||||
mixin ThoughtSequenceRef
|
||||
on AutoDisposeFutureProviderRef<List<SnThinkingThought>> {
|
||||
/// The parameter `sequenceId` of this provider.
|
||||
String get sequenceId;
|
||||
}
|
||||
|
||||
class _ThoughtSequenceProviderElement
|
||||
extends AutoDisposeFutureProviderElement<List<SnThinkingThought>>
|
||||
with ThoughtSequenceRef {
|
||||
_ThoughtSequenceProviderElement(super.provider);
|
||||
|
||||
@override
|
||||
String get sequenceId => (origin as ThoughtSequenceProvider).sequenceId;
|
||||
}
|
||||
|
||||
// ignore_for_file: type=lint
|
||||
// ignore_for_file: subtype_of_sealed_class, invalid_use_of_internal_member, invalid_use_of_visible_for_testing_member, deprecated_member_use_from_same_package
|
||||
Reference in New Issue
Block a user