🐛 Fixes and optimizations

This commit is contained in:
2025-10-26 18:45:14 +08:00
parent 50672795f3
commit aa2df1e847
5 changed files with 717 additions and 82 deletions

View File

@@ -27,6 +27,17 @@ class ThinkingThoughtRoleConverter
int toJson(ThinkingThoughtRole object) => object.value;
}
class ThinkingChunkTypeConverter
implements JsonConverter<ThinkingChunkType, int> {
const ThinkingChunkTypeConverter();
@override
ThinkingChunkType fromJson(int json) => ThinkingChunkType.fromValue(json);
@override
int toJson(ThinkingChunkType object) => object.value;
}
@freezed
sealed class StreamThinkingRequest with _$StreamThinkingRequest {
const factory StreamThinkingRequest({
@@ -38,6 +49,31 @@ sealed class StreamThinkingRequest with _$StreamThinkingRequest {
_$StreamThinkingRequestFromJson(json);
}
enum ThinkingChunkType {
text(0),
reasoning(1),
functionCall(2),
unknown(3);
const ThinkingChunkType(this.value);
final int value;
static ThinkingChunkType fromValue(int value) {
return values.firstWhere((e) => e.value == value);
}
}
@freezed
sealed class SnThinkingChunk with _$SnThinkingChunk {
const factory SnThinkingChunk({
@ThinkingChunkTypeConverter() required ThinkingChunkType type,
Map<String, dynamic>? data,
}) = _SnThinkingChunk;
factory SnThinkingChunk.fromJson(Map<String, dynamic> json) =>
_$SnThinkingChunkFromJson(json);
}
@freezed
sealed class SnThinkingSequence with _$SnThinkingSequence {
const factory SnThinkingSequence({
@@ -59,6 +95,7 @@ sealed class SnThinkingThought with _$SnThinkingThought {
required String id,
String? content,
@Default([]) List<SnCloudFile> files,
@Default([]) List<SnThinkingChunk> chunks,
@ThinkingThoughtRoleConverter() required ThinkingThoughtRole role,
required String sequenceId,
SnThinkingSequence? sequence,

View File

@@ -272,6 +272,274 @@ as String?,
}
/// @nodoc
mixin _$SnThinkingChunk {
@ThinkingChunkTypeConverter() ThinkingChunkType get type; Map<String, dynamic>? get data;
/// Create a copy of SnThinkingChunk
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
$SnThinkingChunkCopyWith<SnThinkingChunk> get copyWith => _$SnThinkingChunkCopyWithImpl<SnThinkingChunk>(this as SnThinkingChunk, _$identity);
/// Serializes this SnThinkingChunk to a JSON map.
Map<String, dynamic> toJson();
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnThinkingChunk&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other.data, data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(data));
@override
String toString() {
return 'SnThinkingChunk(type: $type, data: $data)';
}
}
/// @nodoc
abstract mixin class $SnThinkingChunkCopyWith<$Res> {
factory $SnThinkingChunkCopyWith(SnThinkingChunk value, $Res Function(SnThinkingChunk) _then) = _$SnThinkingChunkCopyWithImpl;
@useResult
$Res call({
@ThinkingChunkTypeConverter() ThinkingChunkType type, Map<String, dynamic>? data
});
}
/// @nodoc
class _$SnThinkingChunkCopyWithImpl<$Res>
implements $SnThinkingChunkCopyWith<$Res> {
_$SnThinkingChunkCopyWithImpl(this._self, this._then);
final SnThinkingChunk _self;
final $Res Function(SnThinkingChunk) _then;
/// Create a copy of SnThinkingChunk
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? type = null,Object? data = freezed,}) {
return _then(_self.copyWith(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as ThinkingChunkType,data: freezed == data ? _self.data : data // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
));
}
}
/// Adds pattern-matching-related methods to [SnThinkingChunk].
extension SnThinkingChunkPatterns on SnThinkingChunk {
/// A variant of `map` that fallback to returning `orElse`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeMap<TResult extends Object?>(TResult Function( _SnThinkingChunk value)? $default,{required TResult orElse(),}){
final _that = this;
switch (_that) {
case _SnThinkingChunk() when $default != null:
return $default(_that);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// Callbacks receives the raw object, upcasted.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case final Subclass2 value:
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult map<TResult extends Object?>(TResult Function( _SnThinkingChunk value) $default,){
final _that = this;
switch (_that) {
case _SnThinkingChunk():
return $default(_that);}
}
/// A variant of `map` that fallback to returning `null`.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case final Subclass value:
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? mapOrNull<TResult extends Object?>(TResult? Function( _SnThinkingChunk value)? $default,){
final _that = this;
switch (_that) {
case _SnThinkingChunk() when $default != null:
return $default(_that);case _:
return null;
}
}
/// A variant of `when` that fallback to an `orElse` callback.
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return orElse();
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function(@ThinkingChunkTypeConverter() ThinkingChunkType type, Map<String, dynamic>? data)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnThinkingChunk() when $default != null:
return $default(_that.type,_that.data);case _:
return orElse();
}
}
/// A `switch`-like method, using callbacks.
///
/// As opposed to `map`, this offers destructuring.
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case Subclass2(:final field2):
/// return ...;
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function(@ThinkingChunkTypeConverter() ThinkingChunkType type, Map<String, dynamic>? data) $default,) {final _that = this;
switch (_that) {
case _SnThinkingChunk():
return $default(_that.type,_that.data);}
}
/// A variant of `when` that fallback to returning `null`
///
/// It is equivalent to doing:
/// ```dart
/// switch (sealedClass) {
/// case Subclass(:final field):
/// return ...;
/// case _:
/// return null;
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function(@ThinkingChunkTypeConverter() ThinkingChunkType type, Map<String, dynamic>? data)? $default,) {final _that = this;
switch (_that) {
case _SnThinkingChunk() when $default != null:
return $default(_that.type,_that.data);case _:
return null;
}
}
}
/// @nodoc
@JsonSerializable()
class _SnThinkingChunk implements SnThinkingChunk {
const _SnThinkingChunk({@ThinkingChunkTypeConverter() required this.type, final Map<String, dynamic>? data}): _data = data;
factory _SnThinkingChunk.fromJson(Map<String, dynamic> json) => _$SnThinkingChunkFromJson(json);
@override@ThinkingChunkTypeConverter() final ThinkingChunkType type;
final Map<String, dynamic>? _data;
@override Map<String, dynamic>? get data {
final value = _data;
if (value == null) return null;
if (_data is EqualUnmodifiableMapView) return _data;
// ignore: implicit_dynamic_type
return EqualUnmodifiableMapView(value);
}
/// Create a copy of SnThinkingChunk
/// with the given fields replaced by the non-null parameter values.
@override @JsonKey(includeFromJson: false, includeToJson: false)
@pragma('vm:prefer-inline')
_$SnThinkingChunkCopyWith<_SnThinkingChunk> get copyWith => __$SnThinkingChunkCopyWithImpl<_SnThinkingChunk>(this, _$identity);
@override
Map<String, dynamic> toJson() {
return _$SnThinkingChunkToJson(this, );
}
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnThinkingChunk&&(identical(other.type, type) || other.type == type)&&const DeepCollectionEquality().equals(other._data, _data));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,type,const DeepCollectionEquality().hash(_data));
@override
String toString() {
return 'SnThinkingChunk(type: $type, data: $data)';
}
}
/// @nodoc
abstract mixin class _$SnThinkingChunkCopyWith<$Res> implements $SnThinkingChunkCopyWith<$Res> {
factory _$SnThinkingChunkCopyWith(_SnThinkingChunk value, $Res Function(_SnThinkingChunk) _then) = __$SnThinkingChunkCopyWithImpl;
@override @useResult
$Res call({
@ThinkingChunkTypeConverter() ThinkingChunkType type, Map<String, dynamic>? data
});
}
/// @nodoc
class __$SnThinkingChunkCopyWithImpl<$Res>
implements _$SnThinkingChunkCopyWith<$Res> {
__$SnThinkingChunkCopyWithImpl(this._self, this._then);
final _SnThinkingChunk _self;
final $Res Function(_SnThinkingChunk) _then;
/// Create a copy of SnThinkingChunk
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? type = null,Object? data = freezed,}) {
return _then(_SnThinkingChunk(
type: null == type ? _self.type : type // ignore: cast_nullable_to_non_nullable
as ThinkingChunkType,data: freezed == data ? _self._data : data // ignore: cast_nullable_to_non_nullable
as Map<String, dynamic>?,
));
}
}
/// @nodoc
mixin _$SnThinkingSequence {
@@ -547,7 +815,7 @@ as DateTime?,
/// @nodoc
mixin _$SnThinkingThought {
String get id; String? get content; List<SnCloudFile> get files;@ThinkingThoughtRoleConverter() ThinkingThoughtRole get role; String get sequenceId; SnThinkingSequence? get sequence; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String? get content; List<SnCloudFile> get files; List<SnThinkingChunk> get chunks;@ThinkingThoughtRoleConverter() ThinkingThoughtRole get role; String get sequenceId; SnThinkingSequence? get sequence; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnThinkingThought
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -560,16 +828,16 @@ $SnThinkingThoughtCopyWith<SnThinkingThought> get copyWith => _$SnThinkingThough
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnThinkingThought&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other.files, files)&&(identical(other.role, role) || other.role == role)&&(identical(other.sequenceId, sequenceId) || other.sequenceId == sequenceId)&&(identical(other.sequence, sequence) || other.sequence == sequence)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnThinkingThought&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other.files, files)&&const DeepCollectionEquality().equals(other.chunks, chunks)&&(identical(other.role, role) || other.role == role)&&(identical(other.sequenceId, sequenceId) || other.sequenceId == sequenceId)&&(identical(other.sequence, sequence) || other.sequence == sequence)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,content,const DeepCollectionEquality().hash(files),role,sequenceId,sequence,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,content,const DeepCollectionEquality().hash(files),const DeepCollectionEquality().hash(chunks),role,sequenceId,sequence,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnThinkingThought(id: $id, content: $content, files: $files, role: $role, sequenceId: $sequenceId, sequence: $sequence, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnThinkingThought(id: $id, content: $content, files: $files, chunks: $chunks, role: $role, sequenceId: $sequenceId, sequence: $sequence, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -580,7 +848,7 @@ abstract mixin class $SnThinkingThoughtCopyWith<$Res> {
factory $SnThinkingThoughtCopyWith(SnThinkingThought value, $Res Function(SnThinkingThought) _then) = _$SnThinkingThoughtCopyWithImpl;
@useResult
$Res call({
String id, String? content, List<SnCloudFile> files,@ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String? content, List<SnCloudFile> files, List<SnThinkingChunk> chunks,@ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -597,12 +865,13 @@ class _$SnThinkingThoughtCopyWithImpl<$Res>
/// Create a copy of SnThinkingThought
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? content = freezed,Object? files = null,Object? role = null,Object? sequenceId = null,Object? sequence = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? content = freezed,Object? files = null,Object? chunks = null,Object? role = null,Object? sequenceId = null,Object? sequence = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_self.copyWith(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,files: null == files ? _self.files : files // ignore: cast_nullable_to_non_nullable
as List<SnCloudFile>,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as List<SnCloudFile>,chunks: null == chunks ? _self.chunks : chunks // ignore: cast_nullable_to_non_nullable
as List<SnThinkingChunk>,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as ThinkingThoughtRole,sequenceId: null == sequenceId ? _self.sequenceId : sequenceId // ignore: cast_nullable_to_non_nullable
as String,sequence: freezed == sequence ? _self.sequence : sequence // ignore: cast_nullable_to_non_nullable
as SnThinkingSequence?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
@@ -702,10 +971,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? content, List<SnCloudFile> files, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? content, List<SnCloudFile> files, List<SnThinkingChunk> chunks, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnThinkingThought() when $default != null:
return $default(_that.id,_that.content,_that.files,_that.role,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.content,_that.files,_that.chunks,_that.role,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -723,10 +992,10 @@ return $default(_that.id,_that.content,_that.files,_that.role,_that.sequenceId,_
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? content, List<SnCloudFile> files, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? content, List<SnCloudFile> files, List<SnThinkingChunk> chunks, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnThinkingThought():
return $default(_that.id,_that.content,_that.files,_that.role,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.content,_that.files,_that.chunks,_that.role,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -740,10 +1009,10 @@ return $default(_that.id,_that.content,_that.files,_that.role,_that.sequenceId,_
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? content, List<SnCloudFile> files, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? content, List<SnCloudFile> files, List<SnThinkingChunk> chunks, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnThinkingThought() when $default != null:
return $default(_that.id,_that.content,_that.files,_that.role,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.content,_that.files,_that.chunks,_that.role,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -755,7 +1024,7 @@ return $default(_that.id,_that.content,_that.files,_that.role,_that.sequenceId,_
@JsonSerializable()
class _SnThinkingThought implements SnThinkingThought {
const _SnThinkingThought({required this.id, this.content, final List<SnCloudFile> files = const [], @ThinkingThoughtRoleConverter() required this.role, required this.sequenceId, this.sequence, required this.createdAt, required this.updatedAt, this.deletedAt}): _files = files;
const _SnThinkingThought({required this.id, this.content, final List<SnCloudFile> files = const [], final List<SnThinkingChunk> chunks = const [], @ThinkingThoughtRoleConverter() required this.role, required this.sequenceId, this.sequence, required this.createdAt, required this.updatedAt, this.deletedAt}): _files = files,_chunks = chunks;
factory _SnThinkingThought.fromJson(Map<String, dynamic> json) => _$SnThinkingThoughtFromJson(json);
@override final String id;
@@ -767,6 +1036,13 @@ class _SnThinkingThought implements SnThinkingThought {
return EqualUnmodifiableListView(_files);
}
final List<SnThinkingChunk> _chunks;
@override@JsonKey() List<SnThinkingChunk> get chunks {
if (_chunks is EqualUnmodifiableListView) return _chunks;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_chunks);
}
@override@ThinkingThoughtRoleConverter() final ThinkingThoughtRole role;
@override final String sequenceId;
@override final SnThinkingSequence? sequence;
@@ -787,16 +1063,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnThinkingThought&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other._files, _files)&&(identical(other.role, role) || other.role == role)&&(identical(other.sequenceId, sequenceId) || other.sequenceId == sequenceId)&&(identical(other.sequence, sequence) || other.sequence == sequence)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnThinkingThought&&(identical(other.id, id) || other.id == id)&&(identical(other.content, content) || other.content == content)&&const DeepCollectionEquality().equals(other._files, _files)&&const DeepCollectionEquality().equals(other._chunks, _chunks)&&(identical(other.role, role) || other.role == role)&&(identical(other.sequenceId, sequenceId) || other.sequenceId == sequenceId)&&(identical(other.sequence, sequence) || other.sequence == sequence)&&(identical(other.createdAt, createdAt) || other.createdAt == createdAt)&&(identical(other.updatedAt, updatedAt) || other.updatedAt == updatedAt)&&(identical(other.deletedAt, deletedAt) || other.deletedAt == deletedAt));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,id,content,const DeepCollectionEquality().hash(_files),role,sequenceId,sequence,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,content,const DeepCollectionEquality().hash(_files),const DeepCollectionEquality().hash(_chunks),role,sequenceId,sequence,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnThinkingThought(id: $id, content: $content, files: $files, role: $role, sequenceId: $sequenceId, sequence: $sequence, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnThinkingThought(id: $id, content: $content, files: $files, chunks: $chunks, role: $role, sequenceId: $sequenceId, sequence: $sequence, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -807,7 +1083,7 @@ abstract mixin class _$SnThinkingThoughtCopyWith<$Res> implements $SnThinkingTho
factory _$SnThinkingThoughtCopyWith(_SnThinkingThought value, $Res Function(_SnThinkingThought) _then) = __$SnThinkingThoughtCopyWithImpl;
@override @useResult
$Res call({
String id, String? content, List<SnCloudFile> files,@ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String? content, List<SnCloudFile> files, List<SnThinkingChunk> chunks,@ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -824,12 +1100,13 @@ class __$SnThinkingThoughtCopyWithImpl<$Res>
/// Create a copy of SnThinkingThought
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? content = freezed,Object? files = null,Object? role = null,Object? sequenceId = null,Object? sequence = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? content = freezed,Object? files = null,Object? chunks = null,Object? role = null,Object? sequenceId = null,Object? sequence = freezed,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnThinkingThought(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,content: freezed == content ? _self.content : content // ignore: cast_nullable_to_non_nullable
as String?,files: null == files ? _self._files : files // ignore: cast_nullable_to_non_nullable
as List<SnCloudFile>,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as List<SnCloudFile>,chunks: null == chunks ? _self._chunks : chunks // ignore: cast_nullable_to_non_nullable
as List<SnThinkingChunk>,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable
as ThinkingThoughtRole,sequenceId: null == sequenceId ? _self.sequenceId : sequenceId // ignore: cast_nullable_to_non_nullable
as String,sequence: freezed == sequence ? _self.sequence : sequence // ignore: cast_nullable_to_non_nullable
as SnThinkingSequence?,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable

View File

@@ -20,6 +20,20 @@ Map<String, dynamic> _$StreamThinkingRequestToJson(
'sequence_id': instance.sequenceId,
};
_SnThinkingChunk _$SnThinkingChunkFromJson(Map<String, dynamic> json) =>
_SnThinkingChunk(
type: const ThinkingChunkTypeConverter().fromJson(
(json['type'] as num).toInt(),
),
data: json['data'] as Map<String, dynamic>?,
);
Map<String, dynamic> _$SnThinkingChunkToJson(_SnThinkingChunk instance) =>
<String, dynamic>{
'type': const ThinkingChunkTypeConverter().toJson(instance.type),
'data': instance.data,
};
_SnThinkingSequence _$SnThinkingSequenceFromJson(Map<String, dynamic> json) =>
_SnThinkingSequence(
id: json['id'] as String,
@@ -52,6 +66,11 @@ _SnThinkingThought _$SnThinkingThoughtFromJson(Map<String, dynamic> json) =>
?.map((e) => SnCloudFile.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
chunks:
(json['chunks'] as List<dynamic>?)
?.map((e) => SnThinkingChunk.fromJson(e as Map<String, dynamic>))
.toList() ??
const [],
role: const ThinkingThoughtRoleConverter().fromJson(
(json['role'] as num).toInt(),
),
@@ -75,6 +94,7 @@ Map<String, dynamic> _$SnThinkingThoughtToJson(_SnThinkingThought instance) =>
'id': instance.id,
'content': instance.content,
'files': instance.files.map((e) => e.toJson()).toList(),
'chunks': instance.chunks.map((e) => e.toJson()).toList(),
'role': const ThinkingThoughtRoleConverter().toJson(instance.role),
'sequence_id': instance.sequenceId,
'sequence': instance.sequence?.toJson(),

View File

@@ -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,11 +19,13 @@ 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 {
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',
@@ -30,7 +33,7 @@ final thoughtSequenceProvider =
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();
},
onDone: () {
completer.complete(buffer.toString());
isStreaming.value = false;
// Parse the response and add AI thought
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 {
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);
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,
];
}
}
// Add AI thought to conversation
} 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];
// 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;
isStreaming.value = false;
}
} catch (e) {
// Ignore parsing errors for individual events
}
}
},
onDone: () {
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(),
),
),
],
),
),
),
),
],
),
],
],
),
);

View 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