diff --git a/lib/models/thought.dart b/lib/models/thought.dart index e0e8eb1f..88edea4d 100644 --- a/lib/models/thought.dart +++ b/lib/models/thought.dart @@ -27,6 +27,17 @@ class ThinkingThoughtRoleConverter int toJson(ThinkingThoughtRole object) => object.value; } +class ThinkingChunkTypeConverter + implements JsonConverter { + 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? data, + }) = _SnThinkingChunk; + + factory SnThinkingChunk.fromJson(Map 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 files, + @Default([]) List chunks, @ThinkingThoughtRoleConverter() required ThinkingThoughtRole role, required String sequenceId, SnThinkingSequence? sequence, diff --git a/lib/models/thought.freezed.dart b/lib/models/thought.freezed.dart index ad809447..3efe4ded 100644 --- a/lib/models/thought.freezed.dart +++ b/lib/models/thought.freezed.dart @@ -272,6 +272,274 @@ as String?, } +/// @nodoc +mixin _$SnThinkingChunk { + +@ThinkingChunkTypeConverter() ThinkingChunkType get type; Map? 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 get copyWith => _$SnThinkingChunkCopyWithImpl(this as SnThinkingChunk, _$identity); + + /// Serializes this SnThinkingChunk to a JSON map. + Map 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? 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?, + )); +} + +} + + +/// 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 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 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? 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 Function(@ThinkingChunkTypeConverter() ThinkingChunkType type, Map? 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 Function(@ThinkingChunkTypeConverter() ThinkingChunkType type, Map? 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? Function(@ThinkingChunkTypeConverter() ThinkingChunkType type, Map? 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? data}): _data = data; + factory _SnThinkingChunk.fromJson(Map json) => _$SnThinkingChunkFromJson(json); + +@override@ThinkingChunkTypeConverter() final ThinkingChunkType type; + final Map? _data; +@override Map? 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 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? 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?, + )); +} + + +} + + /// @nodoc mixin _$SnThinkingSequence { @@ -547,7 +815,7 @@ as DateTime?, /// @nodoc mixin _$SnThinkingThought { - String get id; String? get content; List 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 get files; List 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 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 files,@ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String? content, List files, List 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,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable +as List,chunks: null == chunks ? _self.chunks : chunks // ignore: cast_nullable_to_non_nullable +as List,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 Function( String id, String? content, List 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 Function( String id, String? content, List files, List 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 Function( String id, String? content, List files, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this; +@optionalTypeArgs TResult when(TResult Function( String id, String? content, List files, List 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? Function( String id, String? content, List files, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this; +@optionalTypeArgs TResult? whenOrNull(TResult? Function( String id, String? content, List files, List 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 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 files = const [], final List 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 json) => _$SnThinkingThoughtFromJson(json); @override final String id; @@ -767,6 +1036,13 @@ class _SnThinkingThought implements SnThinkingThought { return EqualUnmodifiableListView(_files); } + final List _chunks; +@override@JsonKey() List 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 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 files,@ThinkingThoughtRoleConverter() ThinkingThoughtRole role, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt + String id, String? content, List files, List 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,role: null == role ? _self.role : role // ignore: cast_nullable_to_non_nullable +as List,chunks: null == chunks ? _self._chunks : chunks // ignore: cast_nullable_to_non_nullable +as List,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 diff --git a/lib/models/thought.g.dart b/lib/models/thought.g.dart index a8aaa945..6f31ebe7 100644 --- a/lib/models/thought.g.dart +++ b/lib/models/thought.g.dart @@ -20,6 +20,20 @@ Map _$StreamThinkingRequestToJson( 'sequence_id': instance.sequenceId, }; +_SnThinkingChunk _$SnThinkingChunkFromJson(Map json) => + _SnThinkingChunk( + type: const ThinkingChunkTypeConverter().fromJson( + (json['type'] as num).toInt(), + ), + data: json['data'] as Map?, + ); + +Map _$SnThinkingChunkToJson(_SnThinkingChunk instance) => + { + 'type': const ThinkingChunkTypeConverter().toJson(instance.type), + 'data': instance.data, + }; + _SnThinkingSequence _$SnThinkingSequenceFromJson(Map json) => _SnThinkingSequence( id: json['id'] as String, @@ -52,6 +66,11 @@ _SnThinkingThought _$SnThinkingThoughtFromJson(Map json) => ?.map((e) => SnCloudFile.fromJson(e as Map)) .toList() ?? const [], + chunks: + (json['chunks'] as List?) + ?.map((e) => SnThinkingChunk.fromJson(e as Map)) + .toList() ?? + const [], role: const ThinkingThoughtRoleConverter().fromJson( (json['role'] as num).toInt(), ), @@ -75,6 +94,7 @@ Map _$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(), diff --git a/lib/screens/thought/think.dart b/lib/screens/thought/think.dart index 5ecc7775..e7dcab85 100644 --- a/lib/screens/thought/think.dart +++ b/lib/screens/thought/think.dart @@ -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, 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> 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(''); + final functionCalls = useState>([]); + final reasoningChunks = useState>([]); 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(); - 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'(.*)', - ).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 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(), + ), + ), + ], + ), + ), + ), + ), + ], + ), + ], ], ), ); diff --git a/lib/screens/thought/think.g.dart b/lib/screens/thought/think.g.dart new file mode 100644 index 00000000..0ba5d80e --- /dev/null +++ b/lib/screens/thought/think.g.dart @@ -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>> { + /// 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? _dependencies = null; + + @override + Iterable? get dependencies => _dependencies; + + static const Iterable? _allTransitiveDependencies = null; + + @override + Iterable? get allTransitiveDependencies => + _allTransitiveDependencies; + + @override + String? get name => r'thoughtSequenceProvider'; +} + +/// See also [thoughtSequence]. +class ThoughtSequenceProvider + extends AutoDisposeFutureProvider> { + /// 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> 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> 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> { + /// The parameter `sequenceId` of this provider. + String get sequenceId; +} + +class _ThoughtSequenceProviderElement + extends AutoDisposeFutureProviderElement> + 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