Files
sure/mobile/lib/main.dart
Lazy Bone f52b3fceb6 feat: implement mobile AI chat feature and fix duplicate response issue (#610)
Backend fixes:
- Fix duplicate AssistantResponseJob triggering causing duplicate AI responses
- UserMessage model already handles job triggering via after_create_commit callback
- Remove redundant job enqueue in chats_controller and messages_controller

Mobile app features:
- Implement complete AI chat interface and conversation management
- Add Chat, Message, and ToolCall data models
- Add ChatProvider for state management with polling mechanism
- Add ChatService to handle all chat-related API requests
- Add chat list screen (ChatListScreen)
- Add conversation detail screen (ChatConversationScreen)
- Refactor navigation structure with bottom navigation bar (MainNavigationScreen)
- Add settings screen (SettingsScreen)
- Optimize TransactionsProvider to support account filtering

Technical details:
- Implement message polling mechanism for real-time AI responses
- Support chat creation, deletion, retry and other operations
- Integrate Material Design 3 design language
- Improve user experience and error handling

Co-authored-by: dwvwdv <dwvwdv@protonmail.com>
2026-01-11 12:45:33 +01:00

216 lines
6.2 KiB
Dart

import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'providers/auth_provider.dart';
import 'providers/accounts_provider.dart';
import 'providers/transactions_provider.dart';
import 'providers/chat_provider.dart';
import 'screens/backend_config_screen.dart';
import 'screens/login_screen.dart';
import 'screens/main_navigation_screen.dart';
import 'services/api_config.dart';
import 'services/connectivity_service.dart';
import 'services/log_service.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await ApiConfig.initialize();
// Add initial log entry
LogService.instance.info('App', 'Sure Finance app starting...');
runApp(const SureApp());
}
class SureApp extends StatelessWidget {
const SureApp({super.key});
@override
Widget build(BuildContext context) {
return MultiProvider(
providers: [
ChangeNotifierProvider(create: (_) => LogService.instance),
ChangeNotifierProvider(create: (_) => ConnectivityService()),
ChangeNotifierProvider(create: (_) => AuthProvider()),
ChangeNotifierProvider(create: (_) => ChatProvider()),
ChangeNotifierProxyProvider<ConnectivityService, AccountsProvider>(
create: (_) => AccountsProvider(),
update: (_, connectivityService, accountsProvider) {
if (accountsProvider == null) {
final provider = AccountsProvider();
provider.setConnectivityService(connectivityService);
return provider;
} else {
accountsProvider.setConnectivityService(connectivityService);
return accountsProvider;
}
},
),
ChangeNotifierProxyProvider<ConnectivityService, TransactionsProvider>(
create: (_) => TransactionsProvider(),
update: (_, connectivityService, transactionsProvider) {
if (transactionsProvider == null) {
final provider = TransactionsProvider();
provider.setConnectivityService(connectivityService);
return provider;
} else {
transactionsProvider.setConnectivityService(connectivityService);
return transactionsProvider;
}
},
),
],
child: MaterialApp(
title: 'Sure Finance',
debugShowCheckedModeBanner: false,
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.light,
),
useMaterial3: true,
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
darkTheme: ThemeData(
colorScheme: ColorScheme.fromSeed(
seedColor: const Color(0xFF6366F1),
brightness: Brightness.dark,
),
useMaterial3: true,
appBarTheme: const AppBarTheme(
centerTitle: true,
elevation: 0,
),
cardTheme: CardThemeData(
elevation: 2,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
inputDecorationTheme: InputDecorationTheme(
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(12),
),
filled: true,
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
minimumSize: const Size(double.infinity, 50),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
),
),
),
),
themeMode: ThemeMode.system,
routes: {
'/config': (context) => const BackendConfigScreen(),
'/login': (context) => const LoginScreen(),
'/home': (context) => const MainNavigationScreen(),
},
home: const AppWrapper(),
),
);
}
}
class AppWrapper extends StatefulWidget {
const AppWrapper({super.key});
@override
State<AppWrapper> createState() => _AppWrapperState();
}
class _AppWrapperState extends State<AppWrapper> {
bool _isCheckingConfig = true;
bool _hasBackendUrl = false;
@override
void initState() {
super.initState();
_checkBackendConfig();
}
Future<void> _checkBackendConfig() async {
final hasUrl = await ApiConfig.initialize();
if (mounted) {
setState(() {
_hasBackendUrl = hasUrl;
_isCheckingConfig = false;
});
}
}
void _onBackendConfigSaved() {
setState(() {
_hasBackendUrl = true;
});
}
void _goToBackendConfig() {
setState(() {
_hasBackendUrl = false;
});
}
@override
Widget build(BuildContext context) {
if (_isCheckingConfig) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
if (!_hasBackendUrl) {
return BackendConfigScreen(
onConfigSaved: _onBackendConfigSaved,
);
}
return Consumer<AuthProvider>(
builder: (context, authProvider, _) {
// Only show loading spinner during initial auth check
if (authProvider.isInitializing) {
return const Scaffold(
body: Center(
child: CircularProgressIndicator(),
),
);
}
if (authProvider.isAuthenticated) {
return const MainNavigationScreen();
}
return LoginScreen(
onGoToSettings: _goToBackendConfig,
);
},
);
}
}