Initial commit

This commit is contained in:
Kazimierz Ciołek
2026-02-19 02:49:29 +01:00
commit 782986a632
148 changed files with 29230 additions and 0 deletions

View File

@@ -0,0 +1,109 @@
import 'package:riverpod_annotation/riverpod_annotation.dart';
import 'package:trainhub_flutter/injection.dart';
import 'package:trainhub_flutter/domain/repositories/chat_repository.dart';
import 'package:trainhub_flutter/presentation/chat/chat_state.dart';
part 'chat_controller.g.dart';
@riverpod
class ChatController extends _$ChatController {
late final ChatRepository _repo;
@override
Future<ChatState> build() async {
_repo = getIt<ChatRepository>();
final sessions = await _repo.getAllSessions();
return ChatState(sessions: sessions);
}
Future<void> createSession() async {
final session = await _repo.createSession();
final sessions = await _repo.getAllSessions();
state = AsyncValue.data(
ChatState(sessions: sessions, activeSession: session),
);
}
Future<void> loadSession(String id) async {
final session = await _repo.getSession(id);
if (session == null) return;
final messages = await _repo.getMessages(id);
final current = state.valueOrNull ?? const ChatState();
state = AsyncValue.data(
current.copyWith(activeSession: session, messages: messages),
);
}
Future<void> deleteSession(String id) async {
await _repo.deleteSession(id);
final sessions = await _repo.getAllSessions();
final current = state.valueOrNull ?? const ChatState();
state = AsyncValue.data(
current.copyWith(
sessions: sessions,
activeSession:
current.activeSession?.id == id ? null : current.activeSession,
messages: current.activeSession?.id == id ? [] : current.messages,
),
);
}
Future<void> sendMessage(String content) async {
final current = state.valueOrNull;
if (current == null) return;
String sessionId;
if (current.activeSession == null) {
final session = await _repo.createSession();
sessionId = session.id;
final sessions = await _repo.getAllSessions();
state = AsyncValue.data(
current.copyWith(sessions: sessions, activeSession: session),
);
} else {
sessionId = current.activeSession!.id;
}
await _repo.addMessage(
sessionId: sessionId,
role: 'user',
content: content,
);
final messagesAfterUser = await _repo.getMessages(sessionId);
state = AsyncValue.data(
state.valueOrNull!.copyWith(messages: messagesAfterUser, isTyping: true),
);
await Future<void>.delayed(const Duration(seconds: 1));
final String response = _getMockResponse(content);
await _repo.addMessage(
sessionId: sessionId,
role: 'assistant',
content: response,
);
final messagesAfterAi = await _repo.getMessages(sessionId);
if (messagesAfterAi.length <= 2) {
final title = content.length > 30
? '${content.substring(0, 30)}...'
: content;
await _repo.updateSessionTitle(sessionId, title);
}
final sessions = await _repo.getAllSessions();
state = AsyncValue.data(
state.valueOrNull!.copyWith(
messages: messagesAfterAi,
isTyping: false,
sessions: sessions,
),
);
}
String _getMockResponse(String input) {
final String lower = input.toLowerCase();
if (lower.contains('plan') || lower.contains('program')) {
return "I can help you design a training plan! What are your goals? Strength, hypertrophy, or endurance?";
} else if (lower.contains('squat') || lower.contains('bench')) {
return "Compound movements are great. Remember to maintain proper form. For squats, keep your chest up and knees tracking over toes.";
} else if (lower.contains('nutrition') || lower.contains('eat')) {
return "Nutrition is key. Aim for 1.6-2.2g of protein per kg of bodyweight if you're training hard.";
}
return "I'm your AI training assistant. How can I help you today?";
}
}

View File

@@ -0,0 +1,26 @@
// GENERATED CODE - DO NOT MODIFY BY HAND
part of 'chat_controller.dart';
// **************************************************************************
// RiverpodGenerator
// **************************************************************************
String _$chatControllerHash() => r'44a3d0e906eaad16f7a9c292fe847b8bd144c835';
/// See also [ChatController].
@ProviderFor(ChatController)
final chatControllerProvider =
AutoDisposeAsyncNotifierProvider<ChatController, ChatState>.internal(
ChatController.new,
name: r'chatControllerProvider',
debugGetCreateSourceHash: const bool.fromEnvironment('dart.vm.product')
? null
: _$chatControllerHash,
dependencies: null,
allTransitiveDependencies: null,
);
typedef _$ChatController = AutoDisposeAsyncNotifier<ChatState>;
// 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

View File

@@ -0,0 +1,766 @@
import 'dart:math' as math;
import 'package:auto_route/auto_route.dart';
import 'package:flutter/material.dart';
import 'package:flutter_riverpod/flutter_riverpod.dart';
import 'package:trainhub_flutter/core/constants/ui_constants.dart';
import 'package:trainhub_flutter/core/theme/app_colors.dart';
import 'package:trainhub_flutter/domain/entities/chat_message.dart';
import 'package:trainhub_flutter/domain/entities/chat_session.dart';
import 'package:trainhub_flutter/presentation/chat/chat_controller.dart';
import 'package:trainhub_flutter/presentation/chat/chat_state.dart';
@RoutePage()
class ChatPage extends ConsumerStatefulWidget {
const ChatPage({super.key});
@override
ConsumerState<ChatPage> createState() => _ChatPageState();
}
class _ChatPageState extends ConsumerState<ChatPage> {
final TextEditingController _inputController = TextEditingController();
final ScrollController _scrollController = ScrollController();
final FocusNode _inputFocusNode = FocusNode();
String? _hoveredSessionId;
@override
void dispose() {
_inputController.dispose();
_scrollController.dispose();
_inputFocusNode.dispose();
super.dispose();
}
void _scrollToBottom() {
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_scrollController.hasClients) {
_scrollController.animateTo(
_scrollController.position.maxScrollExtent,
duration: const Duration(milliseconds: 300),
curve: Curves.easeOut,
);
}
});
}
void _sendMessage(ChatController controller) {
final text = _inputController.text.trim();
if (text.isNotEmpty) {
controller.sendMessage(text);
_inputController.clear();
_inputFocusNode.requestFocus();
}
}
String _formatTimestamp(String timestamp) {
try {
final dt = DateTime.parse(timestamp);
final now = DateTime.now();
final hour = dt.hour.toString().padLeft(2, '0');
final minute = dt.minute.toString().padLeft(2, '0');
if (dt.year == now.year &&
dt.month == now.month &&
dt.day == now.day) {
return '$hour:$minute';
}
return '${dt.day}/${dt.month} $hour:$minute';
} catch (_) {
return '';
}
}
@override
Widget build(BuildContext context) {
final state = ref.watch(chatControllerProvider);
final controller = ref.read(chatControllerProvider.notifier);
ref.listen(chatControllerProvider, (prev, next) {
if (next.hasValue &&
(prev?.value?.messages.length ?? 0) <
next.value!.messages.length) {
_scrollToBottom();
}
if (next.hasValue && next.value!.isTyping && !(prev?.value?.isTyping ?? false)) {
_scrollToBottom();
}
});
return Scaffold(
backgroundColor: AppColors.surface,
body: Row(
children: [
// --- Side Panel ---
_buildSidePanel(state, controller),
// --- Main Chat Area ---
Expanded(
child: _buildChatArea(state, controller),
),
],
),
);
}
// ---------------------------------------------------------------------------
// Side Panel
// ---------------------------------------------------------------------------
Widget _buildSidePanel(
AsyncValue<ChatState> asyncState,
ChatController controller,
) {
return Container(
width: 250,
decoration: const BoxDecoration(
color: AppColors.surfaceContainer,
border: Border(
right: BorderSide(color: AppColors.border, width: 1),
),
),
child: Column(
children: [
// New Chat button
Padding(
padding: const EdgeInsets.all(UIConstants.spacing12),
child: SizedBox(
width: double.infinity,
child: _NewChatButton(onPressed: controller.createSession),
),
),
const Divider(height: 1, color: AppColors.border),
// Session list
Expanded(
child: asyncState.when(
data: (data) {
if (data.sessions.isEmpty) {
return Center(
child: Padding(
padding: const EdgeInsets.all(UIConstants.spacing24),
child: Text(
'No conversations yet',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 13,
),
),
),
);
}
return ListView.builder(
padding: const EdgeInsets.symmetric(
vertical: UIConstants.spacing8,
),
itemCount: data.sessions.length,
itemBuilder: (context, index) {
final session = data.sessions[index];
final isActive =
session.id == data.activeSession?.id;
return _buildSessionTile(
session: session,
isActive: isActive,
controller: controller,
);
},
);
},
error: (_, __) => Center(
child: Text(
'Error loading sessions',
style: TextStyle(color: AppColors.textMuted, fontSize: 13),
),
),
loading: () => const Center(
child: SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.textMuted,
),
),
),
),
),
],
),
);
}
Widget _buildSessionTile({
required ChatSessionEntity session,
required bool isActive,
required ChatController controller,
}) {
final isHovered = _hoveredSessionId == session.id;
return MouseRegion(
onEnter: (_) => setState(() => _hoveredSessionId = session.id),
onExit: (_) => setState(() {
if (_hoveredSessionId == session.id) {
_hoveredSessionId = null;
}
}),
child: GestureDetector(
onTap: () => controller.loadSession(session.id),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
margin: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing8,
vertical: 2,
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing8,
),
decoration: BoxDecoration(
color: isActive
? AppColors.zinc700.withValues(alpha: 0.7)
: isHovered
? AppColors.zinc800.withValues(alpha: 0.6)
: Colors.transparent,
borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius),
border: isActive
? Border.all(
color: AppColors.accent.withValues(alpha: 0.3),
width: 1,
)
: null,
),
child: Row(
children: [
Icon(
Icons.chat_bubble_outline_rounded,
size: 14,
color: isActive ? AppColors.accent : AppColors.textMuted,
),
const SizedBox(width: UIConstants.spacing8),
Expanded(
child: Text(
session.title ?? 'New Chat',
maxLines: 1,
overflow: TextOverflow.ellipsis,
style: TextStyle(
color: isActive
? AppColors.textPrimary
: AppColors.textSecondary,
fontSize: 13,
fontWeight:
isActive ? FontWeight.w500 : FontWeight.normal,
),
),
),
// Delete button appears on hover
AnimatedOpacity(
duration: const Duration(milliseconds: 150),
opacity: isHovered ? 1.0 : 0.0,
child: IgnorePointer(
ignoring: !isHovered,
child: SizedBox(
width: 24,
height: 24,
child: IconButton(
padding: EdgeInsets.zero,
iconSize: 14,
splashRadius: 14,
icon: const Icon(
Icons.delete_outline_rounded,
color: AppColors.textMuted,
),
onPressed: () => controller.deleteSession(session.id),
tooltip: 'Delete',
),
),
),
),
],
),
),
),
);
}
// ---------------------------------------------------------------------------
// Chat Area
// ---------------------------------------------------------------------------
Widget _buildChatArea(
AsyncValue<ChatState> asyncState,
ChatController controller,
) {
return Column(
children: [
// Messages
Expanded(
child: asyncState.when(
data: (data) {
if (data.messages.isEmpty) {
return _buildEmptyState();
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing24,
vertical: UIConstants.spacing16,
),
itemCount:
data.messages.length + (data.isTyping ? 1 : 0),
itemBuilder: (context, index) {
if (index == data.messages.length) {
return const _TypingIndicator();
}
final msg = data.messages[index];
return _MessageBubble(
message: msg,
formattedTime: _formatTimestamp(msg.createdAt),
);
},
);
},
error: (e, _) => Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(
Icons.error_outline_rounded,
color: AppColors.destructive,
size: 40,
),
const SizedBox(height: UIConstants.spacing12),
Text(
'Something went wrong',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 14,
),
),
],
),
),
loading: () => const Center(
child: SizedBox(
width: 24,
height: 24,
child: CircularProgressIndicator(
strokeWidth: 2,
color: AppColors.textMuted,
),
),
),
),
),
// Input area
_buildInputBar(asyncState, controller),
],
);
}
Widget _buildEmptyState() {
return Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Container(
width: 64,
height: 64,
decoration: BoxDecoration(
color: AppColors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(16),
),
child: const Icon(
Icons.auto_awesome_outlined,
size: 32,
color: AppColors.textMuted,
),
),
const SizedBox(height: UIConstants.spacing16),
const Text(
'Ask me anything about your training!',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 15,
fontWeight: FontWeight.w500,
),
),
const SizedBox(height: UIConstants.spacing8),
const Text(
'Start a conversation to get personalized advice.',
style: TextStyle(
color: AppColors.textMuted,
fontSize: 13,
),
),
],
),
);
}
// ---------------------------------------------------------------------------
// Input Bar
// ---------------------------------------------------------------------------
Widget _buildInputBar(
AsyncValue<ChatState> asyncState,
ChatController controller,
) {
final isTyping = asyncState.valueOrNull?.isTyping ?? false;
return Container(
decoration: const BoxDecoration(
color: AppColors.surface,
border: Border(
top: BorderSide(color: AppColors.border, width: 1),
),
),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
child: Row(
crossAxisAlignment: CrossAxisAlignment.end,
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
border: Border.all(color: AppColors.border, width: 1),
),
child: TextField(
controller: _inputController,
focusNode: _inputFocusNode,
style: const TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
),
maxLines: 4,
minLines: 1,
decoration: InputDecoration(
hintText: 'Type a message...',
hintStyle: TextStyle(
color: AppColors.textMuted,
fontSize: 14,
),
contentPadding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
border: InputBorder.none,
enabledBorder: InputBorder.none,
focusedBorder: InputBorder.none,
),
onSubmitted: (_) => _sendMessage(controller),
textInputAction: TextInputAction.send,
),
),
),
const SizedBox(width: UIConstants.spacing8),
SizedBox(
width: 40,
height: 40,
child: Material(
color: isTyping ? AppColors.zinc700 : AppColors.accent,
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
child: InkWell(
borderRadius:
BorderRadius.circular(UIConstants.borderRadius),
onTap: isTyping ? null : () => _sendMessage(controller),
child: Icon(
Icons.arrow_upward_rounded,
color: isTyping ? AppColors.textMuted : AppColors.zinc950,
size: 20,
),
),
),
),
],
),
);
}
}
// =============================================================================
// New Chat Button
// =============================================================================
class _NewChatButton extends StatefulWidget {
const _NewChatButton({required this.onPressed});
final VoidCallback onPressed;
@override
State<_NewChatButton> createState() => _NewChatButtonState();
}
class _NewChatButtonState extends State<_NewChatButton> {
bool _isHovered = false;
@override
Widget build(BuildContext context) {
return MouseRegion(
onEnter: (_) => setState(() => _isHovered = true),
onExit: (_) => setState(() => _isHovered = false),
child: AnimatedContainer(
duration: const Duration(milliseconds: 150),
decoration: BoxDecoration(
color: _isHovered
? AppColors.zinc700
: AppColors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(UIConstants.smallBorderRadius),
border: Border.all(color: AppColors.border, width: 1),
),
child: Material(
color: Colors.transparent,
child: InkWell(
borderRadius:
BorderRadius.circular(UIConstants.smallBorderRadius),
onTap: widget.onPressed,
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing12,
vertical: UIConstants.spacing8,
),
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(
Icons.add_rounded,
size: 16,
color: AppColors.textSecondary,
),
SizedBox(width: UIConstants.spacing8),
Text(
'New Chat',
style: TextStyle(
color: AppColors.textSecondary,
fontSize: 13,
fontWeight: FontWeight.w500,
),
),
],
),
),
),
),
),
);
}
}
// =============================================================================
// Message Bubble
// =============================================================================
class _MessageBubble extends StatelessWidget {
const _MessageBubble({
required this.message,
required this.formattedTime,
});
final ChatMessageEntity message;
final String formattedTime;
@override
Widget build(BuildContext context) {
final isUser = message.isUser;
final maxWidth = MediaQuery.of(context).size.width * 0.55;
return Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing12),
child: Row(
mainAxisAlignment:
isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (!isUser) ...[
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: AppColors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.auto_awesome_rounded,
size: 14,
color: AppColors.accent,
),
),
const SizedBox(width: UIConstants.spacing8),
],
Flexible(
child: Column(
crossAxisAlignment:
isUser ? CrossAxisAlignment.end : CrossAxisAlignment.start,
children: [
Container(
constraints: BoxConstraints(maxWidth: maxWidth),
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
decoration: BoxDecoration(
color: isUser
? AppColors.zinc700
: AppColors.surfaceContainer,
borderRadius: BorderRadius.only(
topLeft: const Radius.circular(16),
topRight: const Radius.circular(16),
bottomLeft:
isUser ? const Radius.circular(16) : const Radius.circular(4),
bottomRight:
isUser ? const Radius.circular(4) : const Radius.circular(16),
),
border: isUser
? null
: Border.all(color: AppColors.border, width: 1),
),
child: SelectableText(
message.content,
style: TextStyle(
color: AppColors.textPrimary,
fontSize: 14,
height: 1.5,
),
),
),
const SizedBox(height: 4),
Padding(
padding: const EdgeInsets.symmetric(horizontal: 4),
child: Text(
formattedTime,
style: const TextStyle(
color: AppColors.textMuted,
fontSize: 11,
),
),
),
],
),
),
if (isUser) ...[
const SizedBox(width: UIConstants.spacing8),
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: AppColors.accent.withValues(alpha: 0.15),
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.person_rounded,
size: 14,
color: AppColors.accent,
),
),
],
],
),
);
}
}
// =============================================================================
// Typing Indicator (3 animated bouncing dots)
// =============================================================================
class _TypingIndicator extends StatefulWidget {
const _TypingIndicator();
@override
State<_TypingIndicator> createState() => _TypingIndicatorState();
}
class _TypingIndicatorState extends State<_TypingIndicator>
with SingleTickerProviderStateMixin {
late final AnimationController _controller;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
)..repeat();
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.only(bottom: UIConstants.spacing12),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 28,
height: 28,
decoration: BoxDecoration(
color: AppColors.surfaceContainerHigh,
borderRadius: BorderRadius.circular(8),
),
child: const Icon(
Icons.auto_awesome_rounded,
size: 14,
color: AppColors.accent,
),
),
const SizedBox(width: UIConstants.spacing8),
Container(
padding: const EdgeInsets.symmetric(
horizontal: UIConstants.spacing16,
vertical: UIConstants.spacing12,
),
decoration: BoxDecoration(
color: AppColors.surfaceContainer,
borderRadius: const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
bottomLeft: Radius.circular(4),
bottomRight: Radius.circular(16),
),
border: Border.all(color: AppColors.border, width: 1),
),
child: AnimatedBuilder(
animation: _controller,
builder: (context, _) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (index) {
// Stagger each dot by 0.2 of the animation cycle
final delay = index * 0.2;
final t = (_controller.value - delay) % 1.0;
// Bounce: use a sin curve over the first half, rest at 0
final bounce =
t < 0.5 ? math.sin(t * math.pi * 2) * 4.0 : 0.0;
return Padding(
padding: EdgeInsets.only(
left: index == 0 ? 0 : 4,
),
child: Transform.translate(
offset: Offset(0, -bounce.abs()),
child: Container(
width: 7,
height: 7,
decoration: BoxDecoration(
color: AppColors.textMuted,
shape: BoxShape.circle,
),
),
),
);
}),
);
},
),
),
],
),
);
}
}

View File

@@ -0,0 +1,15 @@
import 'package:freezed_annotation/freezed_annotation.dart';
import 'package:trainhub_flutter/domain/entities/chat_session.dart';
import 'package:trainhub_flutter/domain/entities/chat_message.dart';
part 'chat_state.freezed.dart';
@freezed
class ChatState with _$ChatState {
const factory ChatState({
@Default([]) List<ChatSessionEntity> sessions,
ChatSessionEntity? activeSession,
@Default([]) List<ChatMessageEntity> messages,
@Default(false) bool isTyping,
}) = _ChatState;
}

View File

@@ -0,0 +1,261 @@
// coverage:ignore-file
// GENERATED CODE - DO NOT MODIFY BY HAND
// ignore_for_file: type=lint
// ignore_for_file: unused_element, deprecated_member_use, deprecated_member_use_from_same_package, use_function_type_syntax_for_parameters, unnecessary_const, avoid_init_to_null, invalid_override_different_default_values_named, prefer_expression_function_bodies, annotate_overrides, invalid_annotation_target, unnecessary_question_mark
part of 'chat_state.dart';
// **************************************************************************
// FreezedGenerator
// **************************************************************************
T _$identity<T>(T value) => value;
final _privateConstructorUsedError = UnsupportedError(
'It seems like you constructed your class using `MyClass._()`. This constructor is only meant to be used by freezed and you are not supposed to need it nor use it.\nPlease check the documentation here for more information: https://github.com/rrousselGit/freezed#adding-getters-and-methods-to-our-models',
);
/// @nodoc
mixin _$ChatState {
List<ChatSessionEntity> get sessions => throw _privateConstructorUsedError;
ChatSessionEntity? get activeSession => throw _privateConstructorUsedError;
List<ChatMessageEntity> get messages => throw _privateConstructorUsedError;
bool get isTyping => throw _privateConstructorUsedError;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
$ChatStateCopyWith<ChatState> get copyWith =>
throw _privateConstructorUsedError;
}
/// @nodoc
abstract class $ChatStateCopyWith<$Res> {
factory $ChatStateCopyWith(ChatState value, $Res Function(ChatState) then) =
_$ChatStateCopyWithImpl<$Res, ChatState>;
@useResult
$Res call({
List<ChatSessionEntity> sessions,
ChatSessionEntity? activeSession,
List<ChatMessageEntity> messages,
bool isTyping,
});
$ChatSessionEntityCopyWith<$Res>? get activeSession;
}
/// @nodoc
class _$ChatStateCopyWithImpl<$Res, $Val extends ChatState>
implements $ChatStateCopyWith<$Res> {
_$ChatStateCopyWithImpl(this._value, this._then);
// ignore: unused_field
final $Val _value;
// ignore: unused_field
final $Res Function($Val) _then;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? sessions = null,
Object? activeSession = freezed,
Object? messages = null,
Object? isTyping = null,
}) {
return _then(
_value.copyWith(
sessions: null == sessions
? _value.sessions
: sessions // ignore: cast_nullable_to_non_nullable
as List<ChatSessionEntity>,
activeSession: freezed == activeSession
? _value.activeSession
: activeSession // ignore: cast_nullable_to_non_nullable
as ChatSessionEntity?,
messages: null == messages
? _value.messages
: messages // ignore: cast_nullable_to_non_nullable
as List<ChatMessageEntity>,
isTyping: null == isTyping
? _value.isTyping
: isTyping // ignore: cast_nullable_to_non_nullable
as bool,
)
as $Val,
);
}
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@override
@pragma('vm:prefer-inline')
$ChatSessionEntityCopyWith<$Res>? get activeSession {
if (_value.activeSession == null) {
return null;
}
return $ChatSessionEntityCopyWith<$Res>(_value.activeSession!, (value) {
return _then(_value.copyWith(activeSession: value) as $Val);
});
}
}
/// @nodoc
abstract class _$$ChatStateImplCopyWith<$Res>
implements $ChatStateCopyWith<$Res> {
factory _$$ChatStateImplCopyWith(
_$ChatStateImpl value,
$Res Function(_$ChatStateImpl) then,
) = __$$ChatStateImplCopyWithImpl<$Res>;
@override
@useResult
$Res call({
List<ChatSessionEntity> sessions,
ChatSessionEntity? activeSession,
List<ChatMessageEntity> messages,
bool isTyping,
});
@override
$ChatSessionEntityCopyWith<$Res>? get activeSession;
}
/// @nodoc
class __$$ChatStateImplCopyWithImpl<$Res>
extends _$ChatStateCopyWithImpl<$Res, _$ChatStateImpl>
implements _$$ChatStateImplCopyWith<$Res> {
__$$ChatStateImplCopyWithImpl(
_$ChatStateImpl _value,
$Res Function(_$ChatStateImpl) _then,
) : super(_value, _then);
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@pragma('vm:prefer-inline')
@override
$Res call({
Object? sessions = null,
Object? activeSession = freezed,
Object? messages = null,
Object? isTyping = null,
}) {
return _then(
_$ChatStateImpl(
sessions: null == sessions
? _value._sessions
: sessions // ignore: cast_nullable_to_non_nullable
as List<ChatSessionEntity>,
activeSession: freezed == activeSession
? _value.activeSession
: activeSession // ignore: cast_nullable_to_non_nullable
as ChatSessionEntity?,
messages: null == messages
? _value._messages
: messages // ignore: cast_nullable_to_non_nullable
as List<ChatMessageEntity>,
isTyping: null == isTyping
? _value.isTyping
: isTyping // ignore: cast_nullable_to_non_nullable
as bool,
),
);
}
}
/// @nodoc
class _$ChatStateImpl implements _ChatState {
const _$ChatStateImpl({
final List<ChatSessionEntity> sessions = const [],
this.activeSession,
final List<ChatMessageEntity> messages = const [],
this.isTyping = false,
}) : _sessions = sessions,
_messages = messages;
final List<ChatSessionEntity> _sessions;
@override
@JsonKey()
List<ChatSessionEntity> get sessions {
if (_sessions is EqualUnmodifiableListView) return _sessions;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_sessions);
}
@override
final ChatSessionEntity? activeSession;
final List<ChatMessageEntity> _messages;
@override
@JsonKey()
List<ChatMessageEntity> get messages {
if (_messages is EqualUnmodifiableListView) return _messages;
// ignore: implicit_dynamic_type
return EqualUnmodifiableListView(_messages);
}
@override
@JsonKey()
final bool isTyping;
@override
String toString() {
return 'ChatState(sessions: $sessions, activeSession: $activeSession, messages: $messages, isTyping: $isTyping)';
}
@override
bool operator ==(Object other) {
return identical(this, other) ||
(other.runtimeType == runtimeType &&
other is _$ChatStateImpl &&
const DeepCollectionEquality().equals(other._sessions, _sessions) &&
(identical(other.activeSession, activeSession) ||
other.activeSession == activeSession) &&
const DeepCollectionEquality().equals(other._messages, _messages) &&
(identical(other.isTyping, isTyping) ||
other.isTyping == isTyping));
}
@override
int get hashCode => Object.hash(
runtimeType,
const DeepCollectionEquality().hash(_sessions),
activeSession,
const DeepCollectionEquality().hash(_messages),
isTyping,
);
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@JsonKey(includeFromJson: false, includeToJson: false)
@override
@pragma('vm:prefer-inline')
_$$ChatStateImplCopyWith<_$ChatStateImpl> get copyWith =>
__$$ChatStateImplCopyWithImpl<_$ChatStateImpl>(this, _$identity);
}
abstract class _ChatState implements ChatState {
const factory _ChatState({
final List<ChatSessionEntity> sessions,
final ChatSessionEntity? activeSession,
final List<ChatMessageEntity> messages,
final bool isTyping,
}) = _$ChatStateImpl;
@override
List<ChatSessionEntity> get sessions;
@override
ChatSessionEntity? get activeSession;
@override
List<ChatMessageEntity> get messages;
@override
bool get isTyping;
/// Create a copy of ChatState
/// with the given fields replaced by the non-null parameter values.
@override
@JsonKey(includeFromJson: false, includeToJson: false)
_$$ChatStateImplCopyWith<_$ChatStateImpl> get copyWith =>
throw _privateConstructorUsedError;
}