mirror of
https://github.com/Dvorinka/1356.git
synced 2026-06-05 04:22:55 +00:00
feat: Complete Phase 1 - Full Flutter app implementation with comprehensive features
Version: 1.1.0 Major changes: - Implemented complete Flutter app structure with all core features - Added comprehensive UI screens for auth, countdown, goals, profile, settings, and social features - Integrated Supabase backend with authentication and data repositories - Added offline support with Hive caching and local storage - Implemented comprehensive routing with go_router - Added location services with Google Maps integration - Implemented notifications and home widget support - Added voice recording capabilities and AI chat features - Created comprehensive test suite and documentation - Added Android and iOS platform configurations - Implemented achievements system and social features - Added calendar integration and bucket list functionality This represents a complete Phase 1 milestone with 3,775 additions across 31 files.
This commit is contained in:
@@ -0,0 +1,469 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import '../application/ai_chat_controller.dart';
|
||||
|
||||
class AIChatScreen extends ConsumerStatefulWidget {
|
||||
const AIChatScreen({super.key});
|
||||
|
||||
@override
|
||||
ConsumerState<AIChatScreen> createState() => _AIChatScreenState();
|
||||
}
|
||||
|
||||
class _AIChatScreenState extends ConsumerState<AIChatScreen> {
|
||||
final TextEditingController _textController = TextEditingController();
|
||||
final ScrollController _scrollController = ScrollController();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_textController.dispose();
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
void _scrollToBottom() {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
if (_scrollController.hasClients) {
|
||||
_scrollController.animateTo(
|
||||
_scrollController.position.maxScrollExtent,
|
||||
duration: const Duration(milliseconds: 300),
|
||||
curve: Curves.easeOut,
|
||||
);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final state = ref.watch(aiChatControllerProvider);
|
||||
final controller = ref.read(aiChatControllerProvider.notifier);
|
||||
|
||||
// Auto-scroll when new messages arrive
|
||||
ref.listen<AIChatState>(aiChatControllerProvider, (previous, next) {
|
||||
if (next.messages.length > (previous?.messages.length ?? 0)) {
|
||||
_scrollToBottom();
|
||||
}
|
||||
});
|
||||
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('AI Life Coach'),
|
||||
backgroundColor: Theme.of(context).colorScheme.surface,
|
||||
foregroundColor: Theme.of(context).colorScheme.onSurface,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: const Icon(Icons.clear_all),
|
||||
onPressed: () => controller.clearMessages(),
|
||||
tooltip: 'Clear chat',
|
||||
),
|
||||
],
|
||||
),
|
||||
body: Column(
|
||||
children: [
|
||||
// Messages list
|
||||
Expanded(
|
||||
child: state.messages.isEmpty
|
||||
? _buildEmptyState(context)
|
||||
: _buildMessagesList(state.messages),
|
||||
),
|
||||
|
||||
// Error message
|
||||
if (state.error != null) _buildErrorMessage(state.error!, controller),
|
||||
|
||||
// Transcription indicator
|
||||
if (state.currentTranscription != null)
|
||||
_buildTranscriptionIndicator(state.currentTranscription!),
|
||||
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 4),
|
||||
child: Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
state.privacyModeEnabled
|
||||
? 'Privacy mode on · Only share countdown day and time remaining with the AI.'
|
||||
: 'Privacy mode off · Also share your username and goal progress for more personal coaching.',
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.onSurface
|
||||
.withValues(alpha:0.7),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Switch.adaptive(
|
||||
value: state.privacyModeEnabled,
|
||||
onChanged: controller.setPrivacyMode,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Input area
|
||||
_buildInputArea(state, controller),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildEmptyState(BuildContext context) {
|
||||
return Center(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.psychology,
|
||||
size: 80,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
Text(
|
||||
'Your AI Life Coach',
|
||||
style: Theme.of(context).textTheme.headlineMedium?.copyWith(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
'Ask for goal inspiration, motivation, or life advice',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onSurface.withValues(alpha:0.7),
|
||||
),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32),
|
||||
child: Column(
|
||||
children: [
|
||||
_buildSuggestionChip('Give me bucket list ideas'),
|
||||
const SizedBox(height: 8),
|
||||
_buildSuggestionChip('How to stay motivated?'),
|
||||
const SizedBox(height: 8),
|
||||
_buildSuggestionChip('Help me set meaningful goals'),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildSuggestionChip(String text) {
|
||||
return InkWell(
|
||||
onTap: () {
|
||||
_textController.text = text;
|
||||
ref.read(aiChatControllerProvider.notifier).sendMessage(text);
|
||||
_textController.clear();
|
||||
},
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessagesList(List messages) {
|
||||
return ListView.builder(
|
||||
controller: _scrollController,
|
||||
padding: const EdgeInsets.all(16),
|
||||
itemCount: messages.length,
|
||||
itemBuilder: (context, index) {
|
||||
final message = messages[index];
|
||||
final isUser = message.role == 'user';
|
||||
|
||||
return _buildMessageBubble(message, isUser);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildMessageBubble(message, bool isUser) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16),
|
||||
child: Row(
|
||||
mainAxisAlignment: isUser ? MainAxisAlignment.end : MainAxisAlignment.start,
|
||||
children: [
|
||||
if (!isUser) ...[
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.primary,
|
||||
child: Icon(
|
||||
Icons.psychology,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onPrimary,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
Flexible(
|
||||
child: Container(
|
||||
constraints: BoxConstraints(
|
||||
maxWidth: MediaQuery.of(context).size.width * 0.75,
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
|
||||
decoration: BoxDecoration(
|
||||
color: isUser
|
||||
? Theme.of(context).colorScheme.primary
|
||||
: Theme.of(context).colorScheme.surfaceContainerHighest,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: Text(
|
||||
message.content,
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: isUser
|
||||
? Theme.of(context).colorScheme.onPrimary
|
||||
: Theme.of(context).colorScheme.onSurfaceVariant,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
if (isUser) ...[
|
||||
const SizedBox(width: 8),
|
||||
CircleAvatar(
|
||||
radius: 16,
|
||||
backgroundColor: Theme.of(context).colorScheme.secondary,
|
||||
child: Icon(
|
||||
Icons.person,
|
||||
size: 20,
|
||||
color: Theme.of(context).colorScheme.onSecondary,
|
||||
),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildErrorMessage(String error, controller) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.all(16),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(
|
||||
Icons.error_outline,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Expanded(
|
||||
child: Text(
|
||||
error,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.close, size: 16),
|
||||
onPressed: controller.clearError,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildTranscriptionIndicator(String text) {
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.primaryContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
text,
|
||||
style: Theme.of(context).textTheme.bodySmall?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onPrimaryContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildInputArea(AIChatState state, controller) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.surface,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Theme.of(context).shadowColor.withValues(alpha:0.1),
|
||||
blurRadius: 8,
|
||||
offset: const Offset(0, -2),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
// Voice recording indicator
|
||||
if (state.isRecording) ...[
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: Theme.of(context).colorScheme.errorContainer,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
Icons.mic,
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
'Recording... Tap to stop',
|
||||
style: Theme.of(context).textTheme.bodyMedium?.copyWith(
|
||||
color: Theme.of(context).colorScheme.onErrorContainer,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
],
|
||||
|
||||
// Input row
|
||||
Row(
|
||||
children: [
|
||||
// Voice button
|
||||
Container
|
||||
(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: state.isRecording
|
||||
? Theme.of(context).colorScheme.error.withValues(alpha:0.12)
|
||||
: Theme.of(context).colorScheme.primary.withValues(alpha:0.08),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed: state.isRecording
|
||||
? () => controller.stopRecording()
|
||||
: () => controller.startRecording(),
|
||||
icon: Icon(
|
||||
state.isRecording ? Icons.stop : Icons.mic,
|
||||
color: state.isRecording
|
||||
? Theme.of(context).colorScheme.error
|
||||
: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
tooltip:
|
||||
state.isRecording ? 'Stop recording' : 'Start voice input',
|
||||
),
|
||||
),
|
||||
|
||||
// Text field
|
||||
Expanded(
|
||||
child: TextField(
|
||||
controller: _textController,
|
||||
enabled: !state.isLoading && !state.isRecording,
|
||||
decoration: InputDecoration(
|
||||
hintText: state.isRecording
|
||||
? 'Recording voice...'
|
||||
: 'Ask for advice or inspiration...',
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
),
|
||||
enabledBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.outline,
|
||||
),
|
||||
),
|
||||
focusedBorder: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
borderSide: BorderSide(
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
contentPadding: const EdgeInsets.symmetric(
|
||||
horizontal: 16,
|
||||
vertical: 12,
|
||||
),
|
||||
),
|
||||
maxLines: null,
|
||||
textInputAction: TextInputAction.send,
|
||||
onSubmitted: (value) {
|
||||
if (value.trim().isNotEmpty && !state.isLoading) {
|
||||
controller.sendMessage(value);
|
||||
_textController.clear();
|
||||
}
|
||||
},
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(width: 8),
|
||||
|
||||
// Send button
|
||||
Container(
|
||||
decoration: BoxDecoration(
|
||||
shape: BoxShape.circle,
|
||||
color: Theme.of(context)
|
||||
.colorScheme
|
||||
.primary
|
||||
.withValues(alpha:state.isLoading ||
|
||||
_textController.text.trim().isEmpty
|
||||
? 0.06
|
||||
: 0.12),
|
||||
),
|
||||
child: IconButton(
|
||||
onPressed:
|
||||
state.isLoading || _textController.text.trim().isEmpty
|
||||
? null
|
||||
: () {
|
||||
controller.sendMessage(_textController.text);
|
||||
_textController.clear();
|
||||
},
|
||||
icon: state.isLoading
|
||||
? SizedBox(
|
||||
width: 20,
|
||||
height: 20,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
valueColor: AlwaysStoppedAnimation<Color>(
|
||||
Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
),
|
||||
)
|
||||
: Icon(
|
||||
Icons.send,
|
||||
color: Theme.of(context).colorScheme.primary,
|
||||
),
|
||||
tooltip: 'Send message',
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user