Compare commits

...

3 Commits

Author SHA1 Message Date
62da279c71 Think based the post 2025-10-26 22:23:17 +08:00
fde6dbf891 🐛 Fix some bugs 2025-10-26 22:08:11 +08:00
613bf4fb42 💄 Optimize UX in AI thought 2025-10-26 22:04:34 +08:00
6 changed files with 1048 additions and 60 deletions

View File

@@ -44,6 +44,8 @@ sealed class StreamThinkingRequest with _$StreamThinkingRequest {
required String userMessage,
String? sequenceId,
@Default([]) List<String> accpetProposals,
List<String>? attachedPosts,
List<Map<String, dynamic>>? attachedMessages,
}) = _StreamThinkingRequest;
factory StreamThinkingRequest.fromJson(Map<String, dynamic> json) =>
@@ -80,6 +82,8 @@ sealed class SnThinkingSequence with _$SnThinkingSequence {
const factory SnThinkingSequence({
required String id,
String? topic,
@Default(0) int totalToken,
@Default(0) int paidToken,
required String accountId,
required DateTime createdAt,
required DateTime updatedAt,
@@ -98,6 +102,8 @@ sealed class SnThinkingThought with _$SnThinkingThought {
@Default([]) List<SnCloudFile> files,
@Default([]) List<SnThinkingChunk> chunks,
@ThinkingThoughtRoleConverter() required ThinkingThoughtRole role,
int? tokenCount,
String? modelName,
required String sequenceId,
SnThinkingSequence? sequence,
required DateTime createdAt,

View File

@@ -15,7 +15,7 @@ T _$identity<T>(T value) => value;
/// @nodoc
mixin _$StreamThinkingRequest {
String get userMessage; String? get sequenceId; List<String> get accpetProposals;
String get userMessage; String? get sequenceId; List<String> get accpetProposals; List<String>? get attachedPosts; List<Map<String, dynamic>>? get attachedMessages;
/// Create a copy of StreamThinkingRequest
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -28,16 +28,16 @@ $StreamThinkingRequestCopyWith<StreamThinkingRequest> get copyWith => _$StreamTh
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is StreamThinkingRequest&&(identical(other.userMessage, userMessage) || other.userMessage == userMessage)&&(identical(other.sequenceId, sequenceId) || other.sequenceId == sequenceId)&&const DeepCollectionEquality().equals(other.accpetProposals, accpetProposals));
return identical(this, other) || (other.runtimeType == runtimeType&&other is StreamThinkingRequest&&(identical(other.userMessage, userMessage) || other.userMessage == userMessage)&&(identical(other.sequenceId, sequenceId) || other.sequenceId == sequenceId)&&const DeepCollectionEquality().equals(other.accpetProposals, accpetProposals)&&const DeepCollectionEquality().equals(other.attachedPosts, attachedPosts)&&const DeepCollectionEquality().equals(other.attachedMessages, attachedMessages));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,userMessage,sequenceId,const DeepCollectionEquality().hash(accpetProposals));
int get hashCode => Object.hash(runtimeType,userMessage,sequenceId,const DeepCollectionEquality().hash(accpetProposals),const DeepCollectionEquality().hash(attachedPosts),const DeepCollectionEquality().hash(attachedMessages));
@override
String toString() {
return 'StreamThinkingRequest(userMessage: $userMessage, sequenceId: $sequenceId, accpetProposals: $accpetProposals)';
return 'StreamThinkingRequest(userMessage: $userMessage, sequenceId: $sequenceId, accpetProposals: $accpetProposals, attachedPosts: $attachedPosts, attachedMessages: $attachedMessages)';
}
@@ -48,7 +48,7 @@ abstract mixin class $StreamThinkingRequestCopyWith<$Res> {
factory $StreamThinkingRequestCopyWith(StreamThinkingRequest value, $Res Function(StreamThinkingRequest) _then) = _$StreamThinkingRequestCopyWithImpl;
@useResult
$Res call({
String userMessage, String? sequenceId, List<String> accpetProposals
String userMessage, String? sequenceId, List<String> accpetProposals, List<String>? attachedPosts, List<Map<String, dynamic>>? attachedMessages
});
@@ -65,12 +65,14 @@ class _$StreamThinkingRequestCopyWithImpl<$Res>
/// Create a copy of StreamThinkingRequest
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? userMessage = null,Object? sequenceId = freezed,Object? accpetProposals = null,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? userMessage = null,Object? sequenceId = freezed,Object? accpetProposals = null,Object? attachedPosts = freezed,Object? attachedMessages = freezed,}) {
return _then(_self.copyWith(
userMessage: null == userMessage ? _self.userMessage : userMessage // ignore: cast_nullable_to_non_nullable
as String,sequenceId: freezed == sequenceId ? _self.sequenceId : sequenceId // ignore: cast_nullable_to_non_nullable
as String?,accpetProposals: null == accpetProposals ? _self.accpetProposals : accpetProposals // ignore: cast_nullable_to_non_nullable
as List<String>,
as List<String>,attachedPosts: freezed == attachedPosts ? _self.attachedPosts : attachedPosts // ignore: cast_nullable_to_non_nullable
as List<String>?,attachedMessages: freezed == attachedMessages ? _self.attachedMessages : attachedMessages // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>?,
));
}
@@ -152,10 +154,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String userMessage, String? sequenceId, List<String> accpetProposals)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String userMessage, String? sequenceId, List<String> accpetProposals, List<String>? attachedPosts, List<Map<String, dynamic>>? attachedMessages)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _StreamThinkingRequest() when $default != null:
return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals);case _:
return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals,_that.attachedPosts,_that.attachedMessages);case _:
return orElse();
}
@@ -173,10 +175,10 @@ return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals);case _
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String userMessage, String? sequenceId, List<String> accpetProposals) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String userMessage, String? sequenceId, List<String> accpetProposals, List<String>? attachedPosts, List<Map<String, dynamic>>? attachedMessages) $default,) {final _that = this;
switch (_that) {
case _StreamThinkingRequest():
return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals);}
return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals,_that.attachedPosts,_that.attachedMessages);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -190,10 +192,10 @@ return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals);}
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String userMessage, String? sequenceId, List<String> accpetProposals)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String userMessage, String? sequenceId, List<String> accpetProposals, List<String>? attachedPosts, List<Map<String, dynamic>>? attachedMessages)? $default,) {final _that = this;
switch (_that) {
case _StreamThinkingRequest() when $default != null:
return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals);case _:
return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals,_that.attachedPosts,_that.attachedMessages);case _:
return null;
}
@@ -205,7 +207,7 @@ return $default(_that.userMessage,_that.sequenceId,_that.accpetProposals);case _
@JsonSerializable()
class _StreamThinkingRequest implements StreamThinkingRequest {
const _StreamThinkingRequest({required this.userMessage, this.sequenceId, final List<String> accpetProposals = const []}): _accpetProposals = accpetProposals;
const _StreamThinkingRequest({required this.userMessage, this.sequenceId, final List<String> accpetProposals = const [], final List<String>? attachedPosts, final List<Map<String, dynamic>>? attachedMessages}): _accpetProposals = accpetProposals,_attachedPosts = attachedPosts,_attachedMessages = attachedMessages;
factory _StreamThinkingRequest.fromJson(Map<String, dynamic> json) => _$StreamThinkingRequestFromJson(json);
@override final String userMessage;
@@ -217,6 +219,24 @@ class _StreamThinkingRequest implements StreamThinkingRequest {
return EqualUnmodifiableListView(_accpetProposals);
}
final List<String>? _attachedPosts;
@override List<String>? get attachedPosts {
final value = _attachedPosts;
if (value == null) return null;
if (_attachedPosts is EqualUnmodifiableListView) return _attachedPosts;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
final List<Map<String, dynamic>>? _attachedMessages;
@override List<Map<String, dynamic>>? get attachedMessages {
final value = _attachedMessages;
if (value == null) return null;
if (_attachedMessages is EqualUnmodifiableListView) return _attachedMessages;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(value);
}
/// Create a copy of StreamThinkingRequest
/// with the given fields replaced by the non-null parameter values.
@@ -231,16 +251,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _StreamThinkingRequest&&(identical(other.userMessage, userMessage) || other.userMessage == userMessage)&&(identical(other.sequenceId, sequenceId) || other.sequenceId == sequenceId)&&const DeepCollectionEquality().equals(other._accpetProposals, _accpetProposals));
return identical(this, other) || (other.runtimeType == runtimeType&&other is _StreamThinkingRequest&&(identical(other.userMessage, userMessage) || other.userMessage == userMessage)&&(identical(other.sequenceId, sequenceId) || other.sequenceId == sequenceId)&&const DeepCollectionEquality().equals(other._accpetProposals, _accpetProposals)&&const DeepCollectionEquality().equals(other._attachedPosts, _attachedPosts)&&const DeepCollectionEquality().equals(other._attachedMessages, _attachedMessages));
}
@JsonKey(includeFromJson: false, includeToJson: false)
@override
int get hashCode => Object.hash(runtimeType,userMessage,sequenceId,const DeepCollectionEquality().hash(_accpetProposals));
int get hashCode => Object.hash(runtimeType,userMessage,sequenceId,const DeepCollectionEquality().hash(_accpetProposals),const DeepCollectionEquality().hash(_attachedPosts),const DeepCollectionEquality().hash(_attachedMessages));
@override
String toString() {
return 'StreamThinkingRequest(userMessage: $userMessage, sequenceId: $sequenceId, accpetProposals: $accpetProposals)';
return 'StreamThinkingRequest(userMessage: $userMessage, sequenceId: $sequenceId, accpetProposals: $accpetProposals, attachedPosts: $attachedPosts, attachedMessages: $attachedMessages)';
}
@@ -251,7 +271,7 @@ abstract mixin class _$StreamThinkingRequestCopyWith<$Res> implements $StreamThi
factory _$StreamThinkingRequestCopyWith(_StreamThinkingRequest value, $Res Function(_StreamThinkingRequest) _then) = __$StreamThinkingRequestCopyWithImpl;
@override @useResult
$Res call({
String userMessage, String? sequenceId, List<String> accpetProposals
String userMessage, String? sequenceId, List<String> accpetProposals, List<String>? attachedPosts, List<Map<String, dynamic>>? attachedMessages
});
@@ -268,12 +288,14 @@ class __$StreamThinkingRequestCopyWithImpl<$Res>
/// Create a copy of StreamThinkingRequest
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? userMessage = null,Object? sequenceId = freezed,Object? accpetProposals = null,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? userMessage = null,Object? sequenceId = freezed,Object? accpetProposals = null,Object? attachedPosts = freezed,Object? attachedMessages = freezed,}) {
return _then(_StreamThinkingRequest(
userMessage: null == userMessage ? _self.userMessage : userMessage // ignore: cast_nullable_to_non_nullable
as String,sequenceId: freezed == sequenceId ? _self.sequenceId : sequenceId // ignore: cast_nullable_to_non_nullable
as String?,accpetProposals: null == accpetProposals ? _self._accpetProposals : accpetProposals // ignore: cast_nullable_to_non_nullable
as List<String>,
as List<String>,attachedPosts: freezed == attachedPosts ? _self._attachedPosts : attachedPosts // ignore: cast_nullable_to_non_nullable
as List<String>?,attachedMessages: freezed == attachedMessages ? _self._attachedMessages : attachedMessages // ignore: cast_nullable_to_non_nullable
as List<Map<String, dynamic>>?,
));
}
@@ -552,7 +574,7 @@ as Map<String, dynamic>?,
/// @nodoc
mixin _$SnThinkingSequence {
String get id; String? get topic; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
String get id; String? get topic; int get totalToken; int get paidToken; String get accountId; DateTime get createdAt; DateTime get updatedAt; DateTime? get deletedAt;
/// Create a copy of SnThinkingSequence
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@@ -565,16 +587,16 @@ $SnThinkingSequenceCopyWith<SnThinkingSequence> get copyWith => _$SnThinkingSequ
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is SnThinkingSequence&&(identical(other.id, id) || other.id == id)&&(identical(other.topic, topic) || other.topic == topic)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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 SnThinkingSequence&&(identical(other.id, id) || other.id == id)&&(identical(other.topic, topic) || other.topic == topic)&&(identical(other.totalToken, totalToken) || other.totalToken == totalToken)&&(identical(other.paidToken, paidToken) || other.paidToken == paidToken)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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,topic,accountId,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,topic,totalToken,paidToken,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnThinkingSequence(id: $id, topic: $topic, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnThinkingSequence(id: $id, topic: $topic, totalToken: $totalToken, paidToken: $paidToken, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -585,7 +607,7 @@ abstract mixin class $SnThinkingSequenceCopyWith<$Res> {
factory $SnThinkingSequenceCopyWith(SnThinkingSequence value, $Res Function(SnThinkingSequence) _then) = _$SnThinkingSequenceCopyWithImpl;
@useResult
$Res call({
String id, String? topic, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String? topic, int totalToken, int paidToken, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -602,11 +624,13 @@ class _$SnThinkingSequenceCopyWithImpl<$Res>
/// Create a copy of SnThinkingSequence
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? topic = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@pragma('vm:prefer-inline') @override $Res call({Object? id = null,Object? topic = freezed,Object? totalToken = null,Object? paidToken = null,Object? accountId = null,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,topic: freezed == topic ? _self.topic : topic // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,totalToken: null == totalToken ? _self.totalToken : totalToken // ignore: cast_nullable_to_non_nullable
as int,paidToken: null == paidToken ? _self.paidToken : paidToken // ignore: cast_nullable_to_non_nullable
as int,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
@@ -692,10 +716,10 @@ return $default(_that);case _:
/// }
/// ```
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? topic, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? topic, int totalToken, int paidToken, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,{required TResult orElse(),}) {final _that = this;
switch (_that) {
case _SnThinkingSequence() when $default != null:
return $default(_that.id,_that.topic,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.topic,_that.totalToken,_that.paidToken,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -713,10 +737,10 @@ return $default(_that.id,_that.topic,_that.accountId,_that.createdAt,_that.updat
/// }
/// ```
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? topic, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? topic, int totalToken, int paidToken, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt) $default,) {final _that = this;
switch (_that) {
case _SnThinkingSequence():
return $default(_that.id,_that.topic,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
return $default(_that.id,_that.topic,_that.totalToken,_that.paidToken,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -730,10 +754,10 @@ return $default(_that.id,_that.topic,_that.accountId,_that.createdAt,_that.updat
/// }
/// ```
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? topic, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? topic, int totalToken, int paidToken, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt)? $default,) {final _that = this;
switch (_that) {
case _SnThinkingSequence() when $default != null:
return $default(_that.id,_that.topic,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return $default(_that.id,_that.topic,_that.totalToken,_that.paidToken,_that.accountId,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -745,11 +769,13 @@ return $default(_that.id,_that.topic,_that.accountId,_that.createdAt,_that.updat
@JsonSerializable()
class _SnThinkingSequence implements SnThinkingSequence {
const _SnThinkingSequence({required this.id, this.topic, required this.accountId, required this.createdAt, required this.updatedAt, this.deletedAt});
const _SnThinkingSequence({required this.id, this.topic, this.totalToken = 0, this.paidToken = 0, required this.accountId, required this.createdAt, required this.updatedAt, this.deletedAt});
factory _SnThinkingSequence.fromJson(Map<String, dynamic> json) => _$SnThinkingSequenceFromJson(json);
@override final String id;
@override final String? topic;
@override@JsonKey() final int totalToken;
@override@JsonKey() final int paidToken;
@override final String accountId;
@override final DateTime createdAt;
@override final DateTime updatedAt;
@@ -768,16 +794,16 @@ Map<String, dynamic> toJson() {
@override
bool operator ==(Object other) {
return identical(this, other) || (other.runtimeType == runtimeType&&other is _SnThinkingSequence&&(identical(other.id, id) || other.id == id)&&(identical(other.topic, topic) || other.topic == topic)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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 _SnThinkingSequence&&(identical(other.id, id) || other.id == id)&&(identical(other.topic, topic) || other.topic == topic)&&(identical(other.totalToken, totalToken) || other.totalToken == totalToken)&&(identical(other.paidToken, paidToken) || other.paidToken == paidToken)&&(identical(other.accountId, accountId) || other.accountId == accountId)&&(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,topic,accountId,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,topic,totalToken,paidToken,accountId,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnThinkingSequence(id: $id, topic: $topic, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnThinkingSequence(id: $id, topic: $topic, totalToken: $totalToken, paidToken: $paidToken, accountId: $accountId, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -788,7 +814,7 @@ abstract mixin class _$SnThinkingSequenceCopyWith<$Res> implements $SnThinkingSe
factory _$SnThinkingSequenceCopyWith(_SnThinkingSequence value, $Res Function(_SnThinkingSequence) _then) = __$SnThinkingSequenceCopyWithImpl;
@override @useResult
$Res call({
String id, String? topic, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
String id, String? topic, int totalToken, int paidToken, String accountId, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -805,11 +831,13 @@ class __$SnThinkingSequenceCopyWithImpl<$Res>
/// Create a copy of SnThinkingSequence
/// with the given fields replaced by the non-null parameter values.
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? topic = freezed,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
@override @pragma('vm:prefer-inline') $Res call({Object? id = null,Object? topic = freezed,Object? totalToken = null,Object? paidToken = null,Object? accountId = null,Object? createdAt = null,Object? updatedAt = null,Object? deletedAt = freezed,}) {
return _then(_SnThinkingSequence(
id: null == id ? _self.id : id // ignore: cast_nullable_to_non_nullable
as String,topic: freezed == topic ? _self.topic : topic // ignore: cast_nullable_to_non_nullable
as String?,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String?,totalToken: null == totalToken ? _self.totalToken : totalToken // ignore: cast_nullable_to_non_nullable
as int,paidToken: null == paidToken ? _self.paidToken : paidToken // ignore: cast_nullable_to_non_nullable
as int,accountId: null == accountId ? _self.accountId : accountId // ignore: cast_nullable_to_non_nullable
as String,createdAt: null == createdAt ? _self.createdAt : createdAt // ignore: cast_nullable_to_non_nullable
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
as DateTime,deletedAt: freezed == deletedAt ? _self.deletedAt : deletedAt // ignore: cast_nullable_to_non_nullable
@@ -824,7 +852,7 @@ as DateTime?,
/// @nodoc
mixin _$SnThinkingThought {
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;
String get id; String? get content; List<SnCloudFile> get files; List<SnThinkingChunk> get chunks;@ThinkingThoughtRoleConverter() ThinkingThoughtRole get role; int? get tokenCount; String? get modelName; 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)
@@ -837,16 +865,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)&&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));
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.tokenCount, tokenCount) || other.tokenCount == tokenCount)&&(identical(other.modelName, modelName) || other.modelName == modelName)&&(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),const DeepCollectionEquality().hash(chunks),role,sequenceId,sequence,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,content,const DeepCollectionEquality().hash(files),const DeepCollectionEquality().hash(chunks),role,tokenCount,modelName,sequenceId,sequence,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnThinkingThought(id: $id, content: $content, files: $files, chunks: $chunks, role: $role, sequenceId: $sequenceId, sequence: $sequence, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnThinkingThought(id: $id, content: $content, files: $files, chunks: $chunks, role: $role, tokenCount: $tokenCount, modelName: $modelName, sequenceId: $sequenceId, sequence: $sequence, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -857,7 +885,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, List<SnThinkingChunk> chunks,@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, int? tokenCount, String? modelName, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -874,14 +902,16 @@ 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? chunks = 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? tokenCount = freezed,Object? modelName = freezed,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>,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 ThinkingThoughtRole,tokenCount: freezed == tokenCount ? _self.tokenCount : tokenCount // ignore: cast_nullable_to_non_nullable
as int?,modelName: freezed == modelName ? _self.modelName : modelName // ignore: cast_nullable_to_non_nullable
as String?,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
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable
@@ -980,10 +1010,10 @@ return $default(_that);case _:
/// }
/// ```
@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;
@optionalTypeArgs TResult maybeWhen<TResult extends Object?>(TResult Function( String id, String? content, List<SnCloudFile> files, List<SnThinkingChunk> chunks, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, int? tokenCount, String? modelName, 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.chunks,_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.tokenCount,_that.modelName,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return orElse();
}
@@ -1001,10 +1031,10 @@ return $default(_that.id,_that.content,_that.files,_that.chunks,_that.role,_that
/// }
/// ```
@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;
@optionalTypeArgs TResult when<TResult extends Object?>(TResult Function( String id, String? content, List<SnCloudFile> files, List<SnThinkingChunk> chunks, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, int? tokenCount, String? modelName, 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.chunks,_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.tokenCount,_that.modelName,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);}
}
/// A variant of `when` that fallback to returning `null`
///
@@ -1018,10 +1048,10 @@ return $default(_that.id,_that.content,_that.files,_that.chunks,_that.role,_that
/// }
/// ```
@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;
@optionalTypeArgs TResult? whenOrNull<TResult extends Object?>(TResult? Function( String id, String? content, List<SnCloudFile> files, List<SnThinkingChunk> chunks, @ThinkingThoughtRoleConverter() ThinkingThoughtRole role, int? tokenCount, String? modelName, 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.chunks,_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.tokenCount,_that.modelName,_that.sequenceId,_that.sequence,_that.createdAt,_that.updatedAt,_that.deletedAt);case _:
return null;
}
@@ -1033,7 +1063,7 @@ return $default(_that.id,_that.content,_that.files,_that.chunks,_that.role,_that
@JsonSerializable()
class _SnThinkingThought implements SnThinkingThought {
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;
const _SnThinkingThought({required this.id, this.content, final List<SnCloudFile> files = const [], final List<SnThinkingChunk> chunks = const [], @ThinkingThoughtRoleConverter() required this.role, this.tokenCount, this.modelName, 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;
@@ -1053,6 +1083,8 @@ class _SnThinkingThought implements SnThinkingThought {
}
@override@ThinkingThoughtRoleConverter() final ThinkingThoughtRole role;
@override final int? tokenCount;
@override final String? modelName;
@override final String sequenceId;
@override final SnThinkingSequence? sequence;
@override final DateTime createdAt;
@@ -1072,16 +1104,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)&&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));
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.tokenCount, tokenCount) || other.tokenCount == tokenCount)&&(identical(other.modelName, modelName) || other.modelName == modelName)&&(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),const DeepCollectionEquality().hash(_chunks),role,sequenceId,sequence,createdAt,updatedAt,deletedAt);
int get hashCode => Object.hash(runtimeType,id,content,const DeepCollectionEquality().hash(_files),const DeepCollectionEquality().hash(_chunks),role,tokenCount,modelName,sequenceId,sequence,createdAt,updatedAt,deletedAt);
@override
String toString() {
return 'SnThinkingThought(id: $id, content: $content, files: $files, chunks: $chunks, role: $role, sequenceId: $sequenceId, sequence: $sequence, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
return 'SnThinkingThought(id: $id, content: $content, files: $files, chunks: $chunks, role: $role, tokenCount: $tokenCount, modelName: $modelName, sequenceId: $sequenceId, sequence: $sequence, createdAt: $createdAt, updatedAt: $updatedAt, deletedAt: $deletedAt)';
}
@@ -1092,7 +1124,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, List<SnThinkingChunk> chunks,@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, int? tokenCount, String? modelName, String sequenceId, SnThinkingSequence? sequence, DateTime createdAt, DateTime updatedAt, DateTime? deletedAt
});
@@ -1109,14 +1141,16 @@ 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? chunks = 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? tokenCount = freezed,Object? modelName = freezed,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>,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 ThinkingThoughtRole,tokenCount: freezed == tokenCount ? _self.tokenCount : tokenCount // ignore: cast_nullable_to_non_nullable
as int?,modelName: freezed == modelName ? _self.modelName : modelName // ignore: cast_nullable_to_non_nullable
as String?,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
as DateTime,updatedAt: null == updatedAt ? _self.updatedAt : updatedAt // ignore: cast_nullable_to_non_nullable

View File

@@ -16,6 +16,14 @@ _StreamThinkingRequest _$StreamThinkingRequestFromJson(
?.map((e) => e as String)
.toList() ??
const [],
attachedPosts:
(json['attached_posts'] as List<dynamic>?)
?.map((e) => e as String)
.toList(),
attachedMessages:
(json['attached_messages'] as List<dynamic>?)
?.map((e) => e as Map<String, dynamic>)
.toList(),
);
Map<String, dynamic> _$StreamThinkingRequestToJson(
@@ -24,6 +32,8 @@ Map<String, dynamic> _$StreamThinkingRequestToJson(
'user_message': instance.userMessage,
'sequence_id': instance.sequenceId,
'accpet_proposals': instance.accpetProposals,
'attached_posts': instance.attachedPosts,
'attached_messages': instance.attachedMessages,
};
_SnThinkingChunk _$SnThinkingChunkFromJson(Map<String, dynamic> json) =>
@@ -44,6 +54,8 @@ _SnThinkingSequence _$SnThinkingSequenceFromJson(Map<String, dynamic> json) =>
_SnThinkingSequence(
id: json['id'] as String,
topic: json['topic'] as String?,
totalToken: (json['total_token'] as num?)?.toInt() ?? 0,
paidToken: (json['paid_token'] as num?)?.toInt() ?? 0,
accountId: json['account_id'] as String,
createdAt: DateTime.parse(json['created_at'] as String),
updatedAt: DateTime.parse(json['updated_at'] as String),
@@ -57,6 +69,8 @@ Map<String, dynamic> _$SnThinkingSequenceToJson(_SnThinkingSequence instance) =>
<String, dynamic>{
'id': instance.id,
'topic': instance.topic,
'total_token': instance.totalToken,
'paid_token': instance.paidToken,
'account_id': instance.accountId,
'created_at': instance.createdAt.toIso8601String(),
'updated_at': instance.updatedAt.toIso8601String(),
@@ -80,6 +94,8 @@ _SnThinkingThought _$SnThinkingThoughtFromJson(Map<String, dynamic> json) =>
role: const ThinkingThoughtRoleConverter().fromJson(
(json['role'] as num).toInt(),
),
tokenCount: (json['token_count'] as num?)?.toInt(),
modelName: json['model_name'] as String?,
sequenceId: json['sequence_id'] as String,
sequence:
json['sequence'] == null
@@ -102,6 +118,8 @@ Map<String, dynamic> _$SnThinkingThoughtToJson(_SnThinkingThought instance) =>
'files': instance.files.map((e) => e.toJson()).toList(),
'chunks': instance.chunks.map((e) => e.toJson()).toList(),
'role': const ThinkingThoughtRoleConverter().toJson(instance.role),
'token_count': instance.tokenCount,
'model_name': instance.modelName,
'sequence_id': instance.sequenceId,
'sequence': instance.sequence?.toJson(),
'created_at': instance.createdAt.toIso8601String(),

View File

@@ -22,6 +22,7 @@ import 'package:island/widgets/response.dart';
import 'package:island/utils/share_utils.dart';
import 'package:island/widgets/safety/abuse_report_helper.dart';
import 'package:island/widgets/share/share_sheet.dart';
import 'package:island/screens/thought/think_sheet.dart';
import 'package:material_symbols_icons/symbols.dart';
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:styled_widget/styled_widget.dart';
@@ -297,6 +298,16 @@ class PostActionButtons extends HookConsumerWidget {
),
);
actions.add(
FilledButton.tonalIcon(
onPressed: () {
ThoughtSheet.show(context, attachedPosts: [post.id]);
},
icon: const Icon(Symbols.smart_toy),
label: Text('aiThought'.tr()),
),
);
actions.add(
Row(
mainAxisSize: MainAxisSize.min,

View File

@@ -2,6 +2,7 @@ import "dart:convert";
import "package:dio/dio.dart";
import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:gap/gap.dart";
import "package:google_fonts/google_fonts.dart";
@@ -163,6 +164,8 @@ class ThoughtScreen extends HookConsumerWidget {
userMessage: userMessage,
sequenceId: selectedSequenceId.value,
accpetProposals: ['post_create'],
attachedMessages: [], // Message datas
attachedPosts: [], // ID list for posts
);
try {
@@ -382,6 +385,26 @@ class ThoughtScreen extends HookConsumerWidget {
],
),
),
if (thought.role == ThinkingThoughtRole.assistant)
SizedBox(
height: 20,
width: 20,
child: IconButton(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -4,
),
padding: EdgeInsets.zero,
iconSize: 16,
icon: Icon(Symbols.content_copy),
onPressed: () {
Clipboard.setData(
ClipboardData(text: thought.content ?? ''),
);
showSnackBar('copiedToClipboard'.tr());
},
),
),
],
),
const Gap(8),
@@ -405,6 +428,36 @@ class ThoughtScreen extends HookConsumerWidget {
),
],
),
if (thought.role == ThinkingThoughtRole.assistant &&
(thought.tokenCount != null || thought.modelName != null)) ...[
const Gap(8),
Row(
children: [
if (thought.modelName != null) ...[
const Icon(Symbols.neurology, size: 16),
const Gap(4),
Text(
'${thought.modelName}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Gap(16),
],
if (thought.tokenCount != null)
...([
const Icon(Symbols.token, size: 16),
const Gap(4),
Text(
'${thought.tokenCount} tokens',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
]),
],
),
],
if (proposals.isNotEmpty &&
thought.role == ThinkingThoughtRole.assistant) ...[
const Gap(12),

View File

@@ -0,0 +1,866 @@
import "dart:convert";
import "package:dio/dio.dart";
import "package:easy_localization/easy_localization.dart";
import "package:flutter/material.dart";
import "package:flutter/services.dart";
import "package:flutter_hooks/flutter_hooks.dart";
import "package:gap/gap.dart";
import "package:google_fonts/google_fonts.dart";
import "package:hooks_riverpod/hooks_riverpod.dart";
import "package:island/models/thought.dart";
import "package:island/pods/network.dart";
import "package:island/pods/userinfo.dart";
import "package:island/services/time.dart";
import "package:island/widgets/alert.dart";
import "package:island/widgets/content/markdown.dart";
import "package:island/widgets/content/sheet.dart";
import "package:island/widgets/post/compose_dialog.dart";
import "package:island/screens/posts/compose.dart";
import "package:material_symbols_icons/material_symbols_icons.dart";
import "package:styled_widget/styled_widget.dart";
import "package:super_sliver_list/super_sliver_list.dart";
import "package:markdown/markdown.dart" as markdown;
import "package:markdown_widget/markdown_widget.dart";
class ThoughtSheet extends HookConsumerWidget {
final List<Map<String, dynamic>> attachedMessages;
final List<String> attachedPosts;
const ThoughtSheet({
super.key,
this.attachedMessages = const [],
this.attachedPosts = const [],
});
static Future<void> show(
BuildContext context, {
List<Map<String, dynamic>> attachedMessages = const [],
List<String> attachedPosts = const [],
}) {
return showModalBottomSheet(
context: context,
isScrollControlled: true,
useSafeArea: true,
builder:
(context) => ThoughtSheet(
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
),
);
}
@override
Widget build(BuildContext context, WidgetRef ref) {
// Extract proposals from text content
List<Map<String, String>> extractProposals(String content) {
final proposalRegex = RegExp(
r'<proposal\s+type="([^"]+)">(.*?)<\/proposal>',
dotAll: true,
);
final matches = proposalRegex.allMatches(content);
return matches.map((match) {
return {'type': match.group(1)!, 'content': match.group(2)!};
}).toList();
}
void handleProposalAction(
BuildContext context,
Map<String, String> proposal,
) {
switch (proposal['type']) {
case 'post_create':
// Show post creation dialog with the proposal content
PostComposeDialog.show(
context,
initialState: PostComposeInitialState(
content: (proposal['content'] ?? '').trim(),
),
);
break;
default:
// Show a snackbar for unsupported proposal types
showSnackBar('Unsupported proposal type: ${proposal['type']}');
}
}
final localThoughts = useState<List<SnThinkingThought>>([]);
final currentTopic = useState<String?>('aiThought'.tr());
final messageController = useTextEditingController();
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(), []);
// Scroll to bottom when thoughts change or streaming state changes
useEffect(() {
if (localThoughts.value.isNotEmpty || isStreaming.value) {
WidgetsBinding.instance.addPostFrameCallback((_) {
scrollController.animateTo(
0,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
});
}
return null;
}, [localThoughts.value.length, isStreaming.value]);
void sendMessage() async {
if (messageController.text.trim().isEmpty) return;
final userMessage = messageController.text.trim();
// Add user message to local thoughts
final userInfo = ref.read(userInfoProvider);
final now = DateTime.now();
final userThought = SnThinkingThought(
id: 'user-${DateTime.now().millisecondsSinceEpoch}',
content: userMessage,
files: [],
role: ThinkingThoughtRole.user,
sequenceId: '',
createdAt: now,
updatedAt: now,
sequence: SnThinkingSequence(
id: '',
accountId: userInfo.value!.id,
createdAt: now,
updatedAt: now,
),
);
localThoughts.value = [userThought, ...localThoughts.value];
final request = StreamThinkingRequest(
userMessage: userMessage,
sequenceId: null,
accpetProposals: ['post_create'],
attachedMessages: attachedMessages,
attachedPosts: attachedPosts,
);
try {
isStreaming.value = true;
streamingText.value = '';
functionCalls.value = [];
reasoningChunks.value = [];
final apiClient = ref.read(apiClientProvider);
final response = await apiClient.post(
'/insight/thought',
data: request.toJson(),
options: Options(
responseType: ResponseType.stream,
sendTimeout: Duration(minutes: 1),
receiveTimeout: Duration(minutes: 1),
),
);
final stream = response.data.stream;
final lineBuffer = StringBuffer();
stream.listen(
(data) {
final chunk = utf8.decode(data);
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];
isStreaming.value = false;
}
} catch (e) {
// Ignore parsing errors for individual events
}
}
},
onDone: () {
if (isStreaming.value) {
isStreaming.value = false;
showErrorAlert('thoughtParseError'.tr());
}
},
onError: (error) {
isStreaming.value = false;
if (error is DioException && error.response?.data is ResponseBody) {
showErrorAlert('toughtParseError'.tr());
} else {
showErrorAlert(error);
}
},
);
messageController.clear();
FocusManager.instance.primaryFocus?.unfocus();
} catch (error) {
isStreaming.value = false;
showErrorAlert(error);
}
}
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}');
// Extract proposals from thought content
final proposals =
thought.content != null ? extractProposals(thought.content!) : [];
final thoughtWidget = Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color:
thought.role == ThinkingThoughtRole.assistant
? Theme.of(context).colorScheme.surfaceContainerHighest
: Theme.of(context).colorScheme.primaryContainer,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(
thought.role == ThinkingThoughtRole.assistant
? Symbols.smart_toy
: Symbols.person,
size: 20,
),
const Gap(8),
Expanded(
child: Row(
crossAxisAlignment: CrossAxisAlignment.baseline,
textBaseline: TextBaseline.ideographic,
spacing: 8,
children: [
Text(
thought.role == ThinkingThoughtRole.assistant
? 'thoughtAiName'.tr()
: 'thoughtUserName'.tr(),
style: Theme.of(context).textTheme.titleSmall,
),
Tooltip(
message: thought.createdAt.formatSystem(),
child: Text(
thought.createdAt.formatRelative(context),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color:
Theme.of(context).colorScheme.onSurfaceVariant,
),
),
),
],
),
),
if (thought.role == ThinkingThoughtRole.assistant)
SizedBox(
height: 20,
width: 20,
child: IconButton(
visualDensity: VisualDensity(
horizontal: -4,
vertical: -4,
),
padding: EdgeInsets.zero,
iconSize: 16,
icon: Icon(Symbols.content_copy),
onPressed: () {
Clipboard.setData(
ClipboardData(text: thought.content ?? ''),
);
showSnackBar('copiedToClipboard'.tr());
},
),
),
],
),
const Gap(8),
if (thought.chunks.isNotEmpty) ...[
buildChunkTiles(thought.chunks),
const Gap(8),
],
if (thought.content != null)
MarkdownTextContent(
isSelectable: true,
content: thought.content!,
extraBlockSyntaxList: [ProposalBlockSyntax()],
textStyle: Theme.of(context).textTheme.bodyMedium,
extraGenerators: [
ProposalGenerator(
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onSecondaryContainer,
borderColor: Theme.of(context).colorScheme.outline,
),
],
),
if (thought.role == ThinkingThoughtRole.assistant &&
(thought.tokenCount != null || thought.modelName != null)) ...[
const Gap(8),
Row(
children: [
if (thought.modelName != null) ...[
const Icon(Symbols.neurology, size: 16),
const Gap(4),
Text(
'${thought.modelName}',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
const Gap(16),
],
if (thought.tokenCount != null)
...([
const Icon(Symbols.token, size: 16),
const Gap(4),
Text(
'${thought.tokenCount} tokens',
style: Theme.of(context).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.onSurfaceVariant,
),
),
]),
],
),
],
if (proposals.isNotEmpty &&
thought.role == ThinkingThoughtRole.assistant) ...[
const Gap(12),
Wrap(
spacing: 8,
runSpacing: 8,
children:
proposals.map((proposal) {
return ElevatedButton.icon(
onPressed:
() => handleProposalAction(context, proposal),
icon: Icon(switch (proposal['type']) {
'post_create' => Symbols.add,
_ => Symbols.lightbulb,
}, size: 16),
label: Text(switch (proposal['type']) {
'post_create' => 'Create Post',
_ => proposal['type'] ?? 'Action',
}),
style: ElevatedButton.styleFrom(
backgroundColor:
Theme.of(context).colorScheme.primaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onPrimaryContainer,
padding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 8,
),
),
);
}).toList(),
),
],
],
),
);
return TweenAnimationBuilder<double>(
key: key,
tween: Tween<double>(begin: 0.0, end: 1.0),
duration: Duration(
milliseconds: 400 + (index % 5) * 50,
), // Staggered delay
curve: Curves.easeOutCubic,
builder: (context, animationValue, child) {
return Transform.translate(
offset: Offset(
0,
20 * (1 - animationValue),
), // Slide up from bottom
child: Opacity(opacity: animationValue, child: child),
);
},
child: thoughtWidget,
);
}
Widget streamingThoughtItem() => Container(
margin: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(12),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
Icon(Symbols.smart_toy, size: 20),
const Gap(8),
Text(
'thoughtAiName'.tr(),
style: Theme.of(context).textTheme.titleSmall,
),
const Spacer(),
SizedBox(
width: 16,
height: 16,
child: CircularProgressIndicator(strokeWidth: 2),
),
],
),
const Gap(8),
MarkdownTextContent(
content: streamingText.value,
textStyle: Theme.of(context).textTheme.bodyMedium,
extraBlockSyntaxList: [ProposalBlockSyntax()],
extraGenerators: [
ProposalGenerator(
backgroundColor:
Theme.of(context).colorScheme.secondaryContainer,
foregroundColor:
Theme.of(context).colorScheme.onSecondaryContainer,
borderColor: Theme.of(context).colorScheme.outline,
),
],
),
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(),
),
),
],
),
),
),
),
],
),
],
],
),
);
return SheetScaffold(
titleText: currentTopic.value ?? 'aiThought'.tr(),
child: Center(
child: Container(
constraints: BoxConstraints(maxWidth: 640),
child: Column(
children: [
Expanded(
child: SuperListView.builder(
listController: listController,
controller: scrollController,
padding: const EdgeInsets.only(top: 16, bottom: 16),
reverse: true,
itemCount:
localThoughts.value.length + (isStreaming.value ? 1 : 0),
itemBuilder: (context, index) {
if (isStreaming.value && index == 0) {
return streamingThoughtItem();
}
final thoughtIndex = isStreaming.value ? index - 1 : index;
final thought = localThoughts.value[thoughtIndex];
return thoughtItem(thought, thoughtIndex);
},
),
),
Column(
mainAxisSize: MainAxisSize.min,
children: [
if (attachedMessages.isNotEmpty || attachedPosts.isNotEmpty)
Container(
margin: const EdgeInsets.only(
left: 16,
right: 16,
bottom: 8,
),
child: Row(
children: [
Icon(
Symbols.attach_file,
size: 16,
color: Theme.of(context).colorScheme.primary,
),
const Gap(8),
Expanded(
child: Text(
[
if (attachedMessages.isNotEmpty)
'${attachedMessages.length} message${attachedMessages.length > 1 ? 's' : ''}',
if (attachedPosts.isNotEmpty)
'${attachedPosts.length} post${attachedPosts.length > 1 ? 's' : ''}',
].join(', '),
style: Theme.of(
context,
).textTheme.bodySmall?.copyWith(
color: Theme.of(context).colorScheme.primary,
fontWeight: FontWeight.w500,
),
),
),
IconButton(
icon: const Icon(Symbols.close, size: 16),
onPressed: () {
// Note: Since these are final parameters, we can't modify them directly
// This would require making the sheet stateful or using a callback
// For now, just show the indicator without remove functionality
},
style: IconButton.styleFrom(
minimumSize: const Size(24, 24),
padding: EdgeInsets.zero,
),
),
],
),
),
Container(
margin: EdgeInsets.only(
left: 16,
right: 16,
bottom: 16 + MediaQuery.of(context).padding.bottom,
),
child: Material(
elevation: 2,
color:
Theme.of(context).colorScheme.surfaceContainerHighest,
borderRadius: BorderRadius.circular(32),
child: Padding(
padding: const EdgeInsets.symmetric(
vertical: 6,
horizontal: 8,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Expanded(
child: TextField(
controller: messageController,
keyboardType: TextInputType.multiline,
enabled: !isStreaming.value,
decoration: InputDecoration(
hintText:
isStreaming.value
? 'thoughtStreamingHint'.tr()
: 'thoughtInputHint'.tr(),
border: InputBorder.none,
isDense: true,
contentPadding: const EdgeInsets.symmetric(
horizontal: 12,
vertical: 12,
),
),
maxLines: 5,
minLines: 1,
textInputAction: TextInputAction.send,
onSubmitted: (_) => sendMessage(),
),
),
IconButton(
icon: Icon(
isStreaming.value ? Symbols.stop : Icons.send,
),
color: Theme.of(context).colorScheme.primary,
onPressed: sendMessage,
),
],
),
),
),
),
],
),
],
),
),
),
);
}
}
class ProposalBlockSyntax extends markdown.BlockSyntax {
@override
RegExp get pattern => RegExp(r'^<proposal', caseSensitive: false);
@override
bool canParse(markdown.BlockParser parser) {
return pattern.hasMatch(parser.current.content);
}
@override
bool canEndBlock(markdown.BlockParser parser) {
return parser.current.content.contains('</proposal>');
}
@override
markdown.Node parse(markdown.BlockParser parser) {
final childLines = <String>[];
// Extract type from opening tag
final openingLine = parser.current.content;
final attrsMatch = RegExp(
r'<proposal(\s[^>]*)?>',
caseSensitive: false,
).firstMatch(openingLine);
final attrs = attrsMatch?.group(1) ?? '';
final typeMatch = RegExp(r'type="([^"]*)"').firstMatch(attrs);
final type = typeMatch?.group(1) ?? '';
// Collect all lines until closing tag
while (!parser.isDone) {
childLines.add(parser.current.content);
if (canEndBlock(parser)) {
parser.advance();
break;
}
parser.advance();
}
// Extract content between tags
final fullContent = childLines.join('\n');
final contentMatch = RegExp(
r'<proposal[^>]*>(.*?)</proposal>',
dotAll: true,
caseSensitive: false,
).firstMatch(fullContent);
final content = contentMatch?.group(1)?.trim() ?? '';
final element = markdown.Element('proposal', [markdown.Text(content)])
..attributes['type'] = type;
return element;
}
}
class ProposalGenerator extends SpanNodeGeneratorWithTag {
ProposalGenerator({
required Color backgroundColor,
required Color foregroundColor,
required Color borderColor,
}) : super(
tag: 'proposal',
generator: (
markdown.Element element,
MarkdownConfig config,
WidgetVisitor visitor,
) {
return ProposalSpanNode(
text: element.textContent,
type: element.attributes['type'] ?? '',
backgroundColor: backgroundColor,
foregroundColor: foregroundColor,
borderColor: borderColor,
);
},
);
}
class ProposalSpanNode extends SpanNode {
final String text;
final String type;
final Color backgroundColor;
final Color foregroundColor;
final Color borderColor;
ProposalSpanNode({
required this.text,
required this.type,
required this.backgroundColor,
required this.foregroundColor,
required this.borderColor,
});
@override
InlineSpan build() {
return WidgetSpan(
child: Container(
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 4),
decoration: BoxDecoration(
color: backgroundColor,
border: Border.all(color: borderColor, width: 1),
borderRadius: BorderRadius.circular(8),
),
child: Column(
mainAxisSize: MainAxisSize.min,
spacing: 6,
children: [
Row(
spacing: 6,
children: [
Icon(Symbols.lightbulb, size: 16, color: foregroundColor),
Text(
'SN-chan suggest you to ${type.split('_').reversed.join(' ')}',
).fontSize(13).opacity(0.8),
],
).padding(top: 3, bottom: 4),
Flexible(
child: Text(
text,
style: TextStyle(
color: foregroundColor,
fontSize: 14,
fontWeight: FontWeight.w500,
),
),
),
],
),
),
);
}
}