Commit 2a9d7f25 authored by Administrator's avatar Administrator

Update 41 files via Son of Anton

parent 98138631
BASE_URL=http://localhost:8000
\ No newline at end of file
import 'package:flutter/material.dart';
import 'core/config/router.dart';
import 'core/theme/app_theme.dart';
class FinSimApp extends StatelessWidget {
const FinSimApp({super.key});
@override
Widget build(BuildContext context) {
return MaterialApp.router(
title: 'FinSim',
debugShowCheckedModeBanner: false,
theme: AppTheme.dark,
routerConfig: appRouter,
);
}
}
\ No newline at end of file
import 'package:flutter_dotenv/flutter_dotenv.dart';
class AppConfig {
AppConfig._();
static String get baseUrl =>
dotenv.env['BASE_URL'] ?? 'http://localhost:8000';
static const String appName = 'FinSim';
static const String appVersion = '1.0.0';
static const String appTagline = 'AI-Powered Investment Simulation';
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:go_router/go_router.dart';
import '../../shared/phase1_showcase_screen.dart';
/// GoRouter configuration — Phase 1 skeleton.
/// Phase 2 will replace this with auth guards + full route tree.
final GoRouter appRouter = GoRouter(
initialLocation: '/',
routes: [
GoRoute(
path: '/',
builder: (context, state) => const Phase1ShowcaseScreen(),
),
],
);
\ No newline at end of file
class ServerException implements Exception {
final String message;
const ServerException(this.message);
@override
String toString() => message;
}
class NetworkException implements Exception {
final String message;
const NetworkException(this.message);
@override
String toString() => message;
}
class AuthException implements Exception {
final String message;
const AuthException(this.message);
@override
String toString() => message;
}
class RateLimitException implements Exception {
final int waitSeconds;
const RateLimitException({required this.waitSeconds});
@override
String toString() => 'Rate limited — wait $waitSeconds seconds';
}
class NotFoundException implements Exception {
final String message;
const NotFoundException(this.message);
@override
String toString() => message;
}
class ConflictException implements Exception {
final String message;
const ConflictException(this.message);
@override
String toString() => message;
}
\ No newline at end of file
import 'package:equatable/equatable.dart';
sealed class Failure extends Equatable {
final String message;
const Failure(this.message);
@override
List<Object?> get props => [message];
}
class ServerFailure extends Failure {
const ServerFailure(super.message);
}
class NetworkFailure extends Failure {
const NetworkFailure(super.message);
}
class AuthFailure extends Failure {
const AuthFailure(super.message);
}
class RateLimitFailure extends Failure {
final int waitSeconds;
const RateLimitFailure({required this.waitSeconds})
: super('Rate limit reached.');
@override
List<Object?> get props => [message, waitSeconds];
}
class NotFoundFailure extends Failure {
const NotFoundFailure(super.message);
}
class ConflictFailure extends Failure {
const ConflictFailure(super.message);
}
\ No newline at end of file
class ApiEndpoints {
ApiEndpoints._();
// Auth
static const String login = '/api/login';
static const String register = '/api/register';
static const String me = '/api/me';
static const String logout = '/api/logout';
// Dashboard / Stats
static const String stats = '/api/stats';
static const String leaderboard = '/api/leaderboard';
// Scenarios
static const String scenarios = '/api/scenarios';
static String scenarioDetail(String id) => '/api/scenarios/$id';
static const String filters = '/api/filters';
// Generator
static const String generate = '/api/generate';
// Chat
static const String chat = '/api/chat';
// Akinator
static const String akinator = '/api/akinator';
// Quiz
static const String quizStart = '/api/quiz/start';
static String quizSubmit(int attemptId) => '/api/quiz/$attemptId/submit';
// Knowledge Base (RAG)
static const String ragKbs = '/api/rag/kbs';
static const String ragUpload = '/api/rag/upload';
}
\ No newline at end of file
import 'package:dio/dio.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class AuthInterceptor extends Interceptor {
static const _storage = FlutterSecureStorage();
static const _tokenKey = 'auth_token';
static const _noAuthPaths = ['/api/login', '/api/register'];
@override
Future<void> onRequest(
RequestOptions options,
RequestInterceptorHandler handler,
) async {
if (_noAuthPaths.contains(options.path)) {
return handler.next(options);
}
final token = await _storage.read(key: _tokenKey);
if (token != null && token.isNotEmpty) {
options.headers['Authorization'] = 'Bearer $token';
}
handler.next(options);
}
/// Save token to secure storage
static Future<void> saveToken(String token) async {
await _storage.write(key: _tokenKey, value: token);
}
/// Read token from secure storage
static Future<String?> getToken() async {
return await _storage.read(key: _tokenKey);
}
/// Delete token from secure storage
static Future<void> clearToken() async {
await _storage.delete(key: _tokenKey);
}
}
\ No newline at end of file
import 'package:dio/dio.dart';
import '../config/app_config.dart';
import 'auth_interceptor.dart';
import 'error_interceptor.dart';
import 'logging_interceptor.dart';
class DioClient {
DioClient._();
static Dio? _instance;
static Dio get instance {
_instance ??= _createDio();
return _instance!;
}
static Dio _createDio() {
final dio = Dio(
BaseOptions(
baseUrl: AppConfig.baseUrl,
connectTimeout: const Duration(seconds: 15),
receiveTimeout: const Duration(seconds: 120),
sendTimeout: const Duration(seconds: 30),
headers: {
'Content-Type': 'application/json',
'Accept': 'application/json',
},
),
);
dio.interceptors.addAll([
AuthInterceptor(),
ErrorInterceptor(),
LoggingInterceptor(),
]);
return dio;
}
static void reset() {
_instance?.close();
_instance = null;
}
}
\ No newline at end of file
import 'package:dio/dio.dart';
import '../error/exceptions.dart';
class ErrorInterceptor extends Interceptor {
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
final data = response.data;
if (data is Map && data['rate_limited'] == true) {
final waitSeconds = (data['wait_seconds'] as num?)?.toInt() ?? 30;
handler.reject(
DioException(
requestOptions: response.requestOptions,
error: RateLimitException(waitSeconds: waitSeconds),
type: DioExceptionType.unknown,
),
);
return;
}
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
final statusCode = err.response?.statusCode;
switch (statusCode) {
case 401:
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: const AuthException('Session expired. Please log in again.'),
type: DioExceptionType.unknown,
),
);
return;
case 404:
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: NotFoundException(_extractDetail(err)),
type: DioExceptionType.unknown,
),
);
return;
case 409:
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: ConflictException(_extractDetail(err)),
type: DioExceptionType.unknown,
),
);
return;
case 429:
final retryAfter = err.response?.headers.value('retry-after');
final waitSeconds =
retryAfter != null ? int.tryParse(retryAfter) ?? 30 : 30;
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: RateLimitException(waitSeconds: waitSeconds),
type: DioExceptionType.unknown,
),
);
return;
}
if (err.type == DioExceptionType.connectionTimeout ||
err.type == DioExceptionType.receiveTimeout ||
err.type == DioExceptionType.sendTimeout) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: const NetworkException('Connection timed out. Please try again.'),
type: DioExceptionType.unknown,
),
);
return;
}
if (err.type == DioExceptionType.connectionError) {
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: const NetworkException('No internet connection.'),
type: DioExceptionType.unknown,
),
);
return;
}
handler.reject(
DioException(
requestOptions: err.requestOptions,
error: ServerException(_extractDetail(err)),
type: DioExceptionType.unknown,
),
);
}
String _extractDetail(DioException err) {
final data = err.response?.data;
if (data is Map && data.containsKey('detail')) {
return data['detail'].toString();
}
return err.message ?? 'An unknown error occurred.';
}
}
\ No newline at end of file
import 'package:dio/dio.dart';
import 'package:flutter/foundation.dart';
class LoggingInterceptor extends Interceptor {
@override
void onRequest(RequestOptions options, RequestInterceptorHandler handler) {
if (kDebugMode) {
debugPrint('→ ${options.method} ${options.path}');
}
handler.next(options);
}
@override
void onResponse(Response response, ResponseInterceptorHandler handler) {
if (kDebugMode) {
debugPrint('← ${response.statusCode} ${response.requestOptions.path}');
}
handler.next(response);
}
@override
void onError(DioException err, ErrorInterceptorHandler handler) {
if (kDebugMode) {
debugPrint(
'✗ ${err.response?.statusCode ?? 'ERR'} '
'${err.requestOptions.path}: ${err.message}',
);
}
handler.next(err);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
class AppColors {
AppColors._();
// ── Backgrounds ──
static const Color bg = Color(0xFF06080F);
static const Color bgSecondary = Color(0xFF0C1120);
static const Color panel = Color(0xFF111827);
static const Color panelLight = Color(0xFF1A2236);
static const Color panelLighter = Color(0xFF1F2A40);
// ── Borders ──
static const Color border = Color(0xFF1E293B);
static const Color borderLight = Color(0xFF2D3A52);
// ── Text ──
static const Color textPrimary = Color(0xFFF1F5F9);
static const Color textSecondary = Color(0xFF94A3B8);
static const Color textTertiary = Color(0xFF64748B);
static const Color textMuted = Color(0xFF475569);
// ── Accent / Brand ──
static const Color accent = Color(0xFF6366F1);
static const Color accentLight = Color(0xFF818CF8);
static const Color accentLighter = Color(0xFFA5B4FC);
static const Color accentGlow = Color(0x1F6366F1);
// ── Semantic ──
static const Color success = Color(0xFF10B981);
static const Color successLight = Color(0xFF34D399);
static const Color successBg = Color(0x1A10B981);
static const Color error = Color(0xFFEF4444);
static const Color errorLight = Color(0xFFF87171);
static const Color errorBg = Color(0x1AEF4444);
static const Color warning = Color(0xFFF59E0B);
static const Color warningLight = Color(0xFFFBBF24);
static const Color warningBg = Color(0x1AF59E0B);
static const Color info = Color(0xFF3B82F6);
static const Color infoLight = Color(0xFF60A5FA);
static const Color infoBg = Color(0x1A3B82F6);
// ── Special (Akinator) ──
static const Color purple = Color(0xFF8B5CF6);
static const Color purpleLight = Color(0xFFA78BFA);
static const Color purpleBg = Color(0x1A8B5CF6);
static const Color pink = Color(0xFFEC4899);
static const Color pinkLight = Color(0xFFF472B6);
static const Color pinkBg = Color(0x1AEC4899);
static const Color cyan = Color(0xFF06B6D4);
static const Color cyanBg = Color(0x1A06B6D4);
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'app_colors.dart';
class AppGradients {
AppGradients._();
static const LinearGradient primaryButton = LinearGradient(
colors: [AppColors.accent, Color(0xFFA855F7)],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
);
static const LinearGradient akinatorButton = LinearGradient(
colors: [AppColors.purple, AppColors.pink],
begin: Alignment.centerLeft,
end: Alignment.centerRight,
);
static const LinearGradient akinatorBanner = LinearGradient(
colors: [
Color(0x268B5CF6),
Color(0x1FEC4899),
Color(0x1AF59E0B),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
static const LinearGradient textAccent = LinearGradient(
colors: [AppColors.accentLight, Color(0xFFA78BFA)],
);
static const LinearGradient scoreGood = LinearGradient(
colors: [AppColors.success, Color(0xFF06B6D4)],
);
static const LinearGradient scoreOk = LinearGradient(
colors: [AppColors.warning, Color(0xFFF97316)],
);
static const LinearGradient scoreBad = LinearGradient(
colors: [AppColors.error, Color(0xFFDC2626)],
);
static const LinearGradient rateLimitBg = LinearGradient(
colors: [
Color(0x1AF59E0B),
Color(0x14EF4444),
],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
);
}
\ No newline at end of file
import 'package:flutter/material.dart';
class AppRadius {
AppRadius._();
static const double sm = 8;
static const double md = 12;
static const double lg = 16;
static const double xl = 24;
static const double full = 9999;
static final BorderRadius smAll = BorderRadius.circular(sm);
static final BorderRadius mdAll = BorderRadius.circular(md);
static final BorderRadius lgAll = BorderRadius.circular(lg);
static final BorderRadius xlAll = BorderRadius.circular(xl);
static final BorderRadius pill = BorderRadius.circular(full);
// Chat bubble radii
static final BorderRadius bubbleUser = const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
bottomLeft: Radius.circular(16),
bottomRight: Radius.circular(4),
);
static final BorderRadius bubbleBot = const BorderRadius.only(
topLeft: Radius.circular(16),
topRight: Radius.circular(16),
bottomLeft: Radius.circular(4),
bottomRight: Radius.circular(16),
);
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'app_colors.dart';
class AppShadows {
AppShadows._();
static List<BoxShadow> get card => [
BoxShadow(
color: Colors.black.withOpacity(0.25),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
static List<BoxShadow> get cardHover => [
BoxShadow(
color: Colors.black.withOpacity(0.35),
blurRadius: 24,
offset: const Offset(0, 8),
),
];
static List<BoxShadow> get accentGlow => [
BoxShadow(
color: AppColors.accent.withOpacity(0.2),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
static List<BoxShadow> get purpleGlow => [
BoxShadow(
color: AppColors.purple.withOpacity(0.2),
blurRadius: 16,
offset: const Offset(0, 4),
),
];
}
\ No newline at end of file
import 'package:flutter/material.dart';
class AppSpacing {
AppSpacing._();
static const double xs = 4;
static const double sm = 8;
static const double md = 16;
static const double lg = 24;
static const double xl = 32;
static const double xxl = 48;
static const double xxxl = 64;
// Named EdgeInsets presets
static const EdgeInsets screenH = EdgeInsets.symmetric(horizontal: 20);
static const EdgeInsets screenAll = EdgeInsets.all(20);
static const EdgeInsets cardInner = EdgeInsets.all(16);
static const EdgeInsets cardInnerLarge = EdgeInsets.all(20);
// SizedBox shortcuts
static const SizedBox vXs = SizedBox(height: xs);
static const SizedBox vSm = SizedBox(height: sm);
static const SizedBox vMd = SizedBox(height: md);
static const SizedBox vLg = SizedBox(height: lg);
static const SizedBox vXl = SizedBox(height: xl);
static const SizedBox vXxl = SizedBox(height: xxl);
static const SizedBox hXs = SizedBox(width: xs);
static const SizedBox hSm = SizedBox(width: sm);
static const SizedBox hMd = SizedBox(width: md);
static const SizedBox hLg = SizedBox(width: lg);
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
class AppTextStyles {
AppTextStyles._();
// ── Display ──
static TextStyle get displayLarge => GoogleFonts.inter(
fontSize: 48,
fontWeight: FontWeight.w900,
height: 1.1,
color: AppColors.textPrimary,
);
static TextStyle get displayMedium => GoogleFonts.inter(
fontSize: 36,
fontWeight: FontWeight.w800,
height: 1.15,
color: AppColors.textPrimary,
);
// ── Headlines ──
static TextStyle get headlineLarge => GoogleFonts.inter(
fontSize: 24,
fontWeight: FontWeight.w800,
height: 1.2,
color: AppColors.textPrimary,
);
static TextStyle get headlineMedium => GoogleFonts.inter(
fontSize: 20,
fontWeight: FontWeight.w700,
height: 1.3,
color: AppColors.textPrimary,
);
// ── Titles ──
static TextStyle get titleLarge => GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
);
static TextStyle get titleMedium => GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
);
static TextStyle get titleSmall => GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
);
// ── Body ──
static TextStyle get bodyLarge => GoogleFonts.inter(
fontSize: 16,
fontWeight: FontWeight.w400,
height: 1.6,
color: AppColors.textPrimary,
);
static TextStyle get bodyMedium => GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w400,
height: 1.6,
color: AppColors.textPrimary,
);
static TextStyle get bodySmall => GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w400,
height: 1.5,
color: AppColors.textSecondary,
);
// ── Labels ──
static TextStyle get labelLarge => GoogleFonts.inter(
fontSize: 14,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
);
static TextStyle get labelMedium => GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w600,
color: AppColors.textPrimary,
);
static TextStyle get labelSmall => GoogleFonts.inter(
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
color: AppColors.textSecondary,
);
// ── Caption ──
static TextStyle get caption => GoogleFonts.inter(
fontSize: 11,
fontWeight: FontWeight.w500,
color: AppColors.textTertiary,
);
// ── Mono ──
static TextStyle get mono => GoogleFonts.jetBrainsMono(
fontSize: 13,
fontWeight: FontWeight.w500,
color: AppColors.textPrimary,
);
static TextStyle get monoSmall => GoogleFonts.jetBrainsMono(
fontSize: 11,
fontWeight: FontWeight.w400,
color: AppColors.textSecondary,
);
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:google_fonts/google_fonts.dart';
import 'app_colors.dart';
class AppTheme {
AppTheme._();
static ThemeData get dark {
return ThemeData(
brightness: Brightness.dark,
scaffoldBackgroundColor: AppColors.bg,
primaryColor: AppColors.accent,
colorScheme: const ColorScheme.dark(
primary: AppColors.accent,
secondary: AppColors.accentLight,
surface: AppColors.panel,
error: AppColors.error,
onPrimary: Colors.white,
onSecondary: Colors.white,
onSurface: AppColors.textPrimary,
onError: Colors.white,
),
fontFamily: GoogleFonts.inter().fontFamily,
appBarTheme: AppBarTheme(
backgroundColor: AppColors.panel,
elevation: 0,
scrolledUnderElevation: 0,
centerTitle: false,
surfaceTintColor: Colors.transparent,
titleTextStyle: GoogleFonts.inter(
fontSize: 18,
fontWeight: FontWeight.w700,
color: AppColors.textPrimary,
),
iconTheme: const IconThemeData(color: AppColors.textPrimary),
),
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
backgroundColor: AppColors.panel,
selectedItemColor: AppColors.accentLight,
unselectedItemColor: AppColors.textTertiary,
type: BottomNavigationBarType.fixed,
elevation: 0,
selectedLabelStyle: TextStyle(fontSize: 11, fontWeight: FontWeight.w600),
unselectedLabelStyle: TextStyle(fontSize: 11, fontWeight: FontWeight.w500),
),
cardTheme: CardTheme(
color: AppColors.panel,
elevation: 0,
margin: EdgeInsets.zero,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12),
side: const BorderSide(color: AppColors.border, width: 1),
),
),
inputDecorationTheme: InputDecorationTheme(
filled: true,
fillColor: AppColors.bgSecondary,
contentPadding: const EdgeInsets.symmetric(horizontal: 16, vertical: 14),
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.border),
),
enabledBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.border),
),
focusedBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.accent, width: 1.5),
),
errorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.error),
),
focusedErrorBorder: OutlineInputBorder(
borderRadius: BorderRadius.circular(8),
borderSide: const BorderSide(color: AppColors.error, width: 1.5),
),
hintStyle: GoogleFonts.inter(fontSize: 14, color: AppColors.textTertiary),
labelStyle: GoogleFonts.inter(
fontSize: 12,
fontWeight: FontWeight.w500,
color: AppColors.textSecondary,
),
errorStyle: GoogleFonts.inter(fontSize: 11, color: AppColors.error),
),
elevatedButtonTheme: ElevatedButtonThemeData(
style: ElevatedButton.styleFrom(
backgroundColor: AppColors.accent,
foregroundColor: Colors.white,
disabledBackgroundColor: AppColors.accent.withOpacity(0.5),
disabledForegroundColor: Colors.white.withOpacity(0.5),
minimumSize: const Size(double.infinity, 48),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
elevation: 0,
textStyle: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.w600),
),
),
outlinedButtonTheme: OutlinedButtonThemeData(
style: OutlinedButton.styleFrom(
foregroundColor: AppColors.textPrimary,
minimumSize: const Size(double.infinity, 48),
side: const BorderSide(color: AppColors.border),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
textStyle: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.w600),
),
),
textButtonTheme: TextButtonThemeData(
style: TextButton.styleFrom(
foregroundColor: AppColors.accentLight,
textStyle: GoogleFonts.inter(fontSize: 14, fontWeight: FontWeight.w600),
),
),
dividerTheme: const DividerThemeData(
color: AppColors.border,
thickness: 1,
space: 1,
),
snackBarTheme: SnackBarThemeData(
backgroundColor: AppColors.panelLight,
contentTextStyle: GoogleFonts.inter(fontSize: 14, color: AppColors.textPrimary),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
behavior: SnackBarBehavior.floating,
),
progressIndicatorTheme: const ProgressIndicatorThemeData(
color: AppColors.accent,
linearTrackColor: AppColors.border,
),
switchTheme: SwitchThemeData(
thumbColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) return Colors.white;
return AppColors.textTertiary;
}),
trackColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) return AppColors.accent;
return AppColors.panelLight;
}),
trackOutlineColor: WidgetStateProperty.resolveWith((states) {
if (states.contains(WidgetState.selected)) return Colors.transparent;
return AppColors.border;
}),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
extension BuildContextX on BuildContext {
ThemeData get theme => Theme.of(this);
TextTheme get textTheme => Theme.of(this).textTheme;
ColorScheme get colorScheme => Theme.of(this).colorScheme;
MediaQueryData get mq => MediaQuery.of(this);
double get screenWidth => mq.size.width;
double get screenHeight => mq.size.height;
EdgeInsets get padding => mq.padding;
bool get isSmallScreen => screenWidth < 380;
void showSnackBar(String message, {bool isError = false}) {
ScaffoldMessenger.of(this).hideCurrentSnackBar();
ScaffoldMessenger.of(this).showSnackBar(
SnackBar(
content: Text(message),
backgroundColor: isError ? AppColors.error : AppColors.panelLight,
duration: Duration(seconds: isError ? 4 : 2),
),
);
}
}
extension StringX on String {
String get capitalize {
if (isEmpty) return this;
return '${this[0].toUpperCase()}${substring(1)}';
}
String get initials {
if (isEmpty) return '?';
final words = trim().split(' ');
if (words.length >= 2) {
return '${words[0][0]}${words[1][0]}'.toUpperCase();
}
return this[0].toUpperCase();
}
}
\ No newline at end of file
import 'package:intl/intl.dart';
class Formatters {
Formatters._();
static String number(num value) {
return NumberFormat('#,##0').format(value);
}
static String decimal(num value, [int decimals = 2]) {
return NumberFormat('#,##0.${'0' * decimals}').format(value);
}
static String currency(num value, [String symbol = '\$']) {
return '$symbol${NumberFormat('#,##0.00').format(value)}';
}
static String percentage(num value) {
return '${value.toStringAsFixed(1)}%';
}
static String date(String? isoDate) {
if (isoDate == null || isoDate.isEmpty) return '-';
try {
final dt = DateTime.parse(isoDate);
return DateFormat('MMM d, yyyy').format(dt);
} catch (_) {
return isoDate;
}
}
static String dateTime(String? isoDate) {
if (isoDate == null || isoDate.isEmpty) return '-';
try {
final dt = DateTime.parse(isoDate);
return DateFormat('MMM d, yyyy · h:mm a').format(dt);
} catch (_) {
return isoDate;
}
}
static String timeAgo(String? isoDate) {
if (isoDate == null || isoDate.isEmpty) return '-';
try {
final dt = DateTime.parse(isoDate);
final diff = DateTime.now().difference(dt);
if (diff.inDays > 30) return DateFormat('MMM d').format(dt);
if (diff.inDays > 0) return '${diff.inDays}d ago';
if (diff.inHours > 0) return '${diff.inHours}h ago';
if (diff.inMinutes > 0) return '${diff.inMinutes}m ago';
return 'Just now';
} catch (_) {
return isoDate;
}
}
static String countdown(int totalSeconds) {
if (totalSeconds <= 0) return '0s';
final h = totalSeconds ~/ 3600;
final m = (totalSeconds % 3600) ~/ 60;
final s = totalSeconds % 60;
if (h > 0) return '${h}h ${m.toString().padLeft(2, '0')}m ${s.toString().padLeft(2, '0')}s';
if (m > 0) return '${m}m ${s.toString().padLeft(2, '0')}s';
return '${s}s';
}
}
\ No newline at end of file
class Validators {
Validators._();
static String? email(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Email is required';
}
if (!value.contains('@') || !value.contains('.')) {
return 'Enter a valid email address';
}
return null;
}
static String? password(String? value) {
if (value == null || value.isEmpty) {
return 'Password is required';
}
if (value.length < 4) {
return 'Password must be at least 4 characters';
}
return null;
}
static String? username(String? value) {
if (value == null || value.trim().isEmpty) {
return 'Username is required';
}
if (value.trim().length < 2) {
return 'Username must be at least 2 characters';
}
return null;
}
static String? required(String? value, [String fieldName = 'This field']) {
if (value == null || value.trim().isEmpty) {
return '$fieldName is required';
}
return null;
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter_markdown/flutter_markdown.dart';
import '../theme/app_colors.dart';
import '../theme/app_radius.dart';
import '../theme/app_text_styles.dart';
class ChatBubble extends StatelessWidget {
final String text;
final bool isUser;
final bool isTyping;
final bool isAkinator;
final bool ragUsed;
final Widget? bottomWidget;
const ChatBubble({
super.key,
required this.text,
this.isUser = false,
this.isTyping = false,
this.isAkinator = false,
this.ragUsed = false,
this.bottomWidget,
});
@override
Widget build(BuildContext context) {
final maxWidth = MediaQuery.of(context).size.width * 0.80;
return Align(
alignment: isUser ? Alignment.centerRight : Alignment.centerLeft,
child: Container(
constraints: BoxConstraints(maxWidth: maxWidth),
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 10),
decoration: BoxDecoration(
color: isUser
? AppColors.accent
: isAkinator
? AppColors.panel.withOpacity(0.95)
: AppColors.panel,
border: isUser
? null
: Border.all(
color: isAkinator
? AppColors.purple.withOpacity(0.2)
: AppColors.border,
),
borderRadius: isUser ? AppRadius.bubbleUser : AppRadius.bubbleBot,
gradient: isAkinator && !isUser
? LinearGradient(
colors: [AppColors.panel, AppColors.purple.withOpacity(0.06)],
begin: Alignment.topLeft,
end: Alignment.bottomRight,
)
: null,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
if (ragUsed && !isUser)
Container(
margin: const EdgeInsets.only(bottom: 8),
padding: const EdgeInsets.symmetric(horizontal: 8, vertical: 2),
decoration: BoxDecoration(
color: AppColors.purpleBg,
borderRadius: AppRadius.pill,
),
child: Text(
'🧠 RAG ACTIVATED',
style: TextStyle(
color: AppColors.purpleLight,
fontSize: 9,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
),
if (isTyping)
_TypingIndicator()
else if (isUser)
Text(text, style: AppTextStyles.bodyMedium.copyWith(color: Colors.white))
else
MarkdownBody(
data: text,
selectable: true,
styleSheet: _markdownStyle(),
),
if (bottomWidget != null) ...[
const SizedBox(height: 8),
bottomWidget!,
],
],
),
),
);
}
MarkdownStyleSheet _markdownStyle() {
return MarkdownStyleSheet(
p: AppTextStyles.bodyMedium.copyWith(height: 1.6),
h1: AppTextStyles.titleLarge.copyWith(color: AppColors.accentLight),
h2: AppTextStyles.titleMedium.copyWith(color: AppColors.accentLight),
h3: AppTextStyles.titleSmall.copyWith(color: AppColors.accentLight),
strong: AppTextStyles.bodyMedium.copyWith(fontWeight: FontWeight.w700),
em: AppTextStyles.bodyMedium.copyWith(
fontStyle: FontStyle.italic,
color: AppColors.textSecondary,
),
listBullet: AppTextStyles.bodyMedium,
blockquoteDecoration: BoxDecoration(
border: Border(left: BorderSide(color: AppColors.accent, width: 3)),
),
blockquotePadding: const EdgeInsets.only(left: 12),
code: AppTextStyles.monoSmall.copyWith(
backgroundColor: AppColors.purpleBg,
),
codeblockDecoration: BoxDecoration(
color: AppColors.bgSecondary,
border: Border.all(color: AppColors.border),
borderRadius: AppRadius.smAll,
),
tableBorder: TableBorder.all(color: AppColors.border, width: 0.5),
tableHead: AppTextStyles.labelMedium.copyWith(color: AppColors.accentLight),
tableBody: AppTextStyles.bodySmall,
horizontalRuleDecoration: BoxDecoration(
border: Border(top: BorderSide(color: AppColors.border)),
),
);
}
}
class _TypingIndicator extends StatefulWidget {
@override
State<_TypingIndicator> createState() => _TypingIndicatorState();
}
class _TypingIndicatorState extends State<_TypingIndicator>
with TickerProviderStateMixin {
late final List<AnimationController> _controllers;
@override
void initState() {
super.initState();
_controllers = List.generate(3, (i) {
return AnimationController(
vsync: this,
duration: const Duration(milliseconds: 600),
)..repeat(reverse: true);
});
for (var i = 0; i < 3; i++) {
Future.delayed(Duration(milliseconds: i * 200), () {
if (mounted) _controllers[i].forward();
});
}
}
@override
void dispose() {
for (final c in _controllers) {
c.dispose();
}
super.dispose();
}
@override
Widget build(BuildContext context) {
return Row(
mainAxisSize: MainAxisSize.min,
children: List.generate(3, (i) {
return AnimatedBuilder(
animation: _controllers[i],
builder: (_, __) {
return Container(
margin: const EdgeInsets.symmetric(horizontal: 2),
width: 8,
height: 8,
decoration: BoxDecoration(
color: AppColors.textTertiary.withOpacity(
0.3 + (_controllers[i].value * 0.7),
),
shape: BoxShape.circle,
),
);
},
);
}),
);
}
}
\ No newline at end of file
import 'dart:math' as math;
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
class ConfidenceMeter extends StatefulWidget {
final int score;
final double size;
final double strokeWidth;
const ConfidenceMeter({
super.key,
required this.score,
this.size = 56,
this.strokeWidth = 5,
});
@override
State<ConfidenceMeter> createState() => _ConfidenceMeterState();
}
class _ConfidenceMeterState extends State<ConfidenceMeter>
with SingleTickerProviderStateMixin {
late AnimationController _controller;
late Animation<double> _animation;
@override
void initState() {
super.initState();
_controller = AnimationController(
vsync: this,
duration: const Duration(milliseconds: 1200),
);
_animation = Tween<double>(begin: 0, end: widget.score / 100)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
_controller.forward();
}
@override
void didUpdateWidget(ConfidenceMeter old) {
super.didUpdateWidget(old);
if (old.score != widget.score) {
_animation = Tween<double>(begin: _animation.value, end: widget.score / 100)
.animate(CurvedAnimation(parent: _controller, curve: Curves.easeOutCubic));
_controller.forward(from: 0);
}
}
@override
void dispose() {
_controller.dispose();
super.dispose();
}
Color get _color {
if (widget.score >= 75) return AppColors.success;
if (widget.score >= 50) return AppColors.warning;
return AppColors.error;
}
@override
Widget build(BuildContext context) {
return SizedBox(
width: widget.size,
height: widget.size,
child: AnimatedBuilder(
animation: _animation,
builder: (_, __) => CustomPaint(
painter: _RingPainter(
progress: _animation.value,
color: _color,
strokeWidth: widget.strokeWidth,
),
child: Center(
child: Text(
'${widget.score}%',
style: AppTextStyles.labelMedium.copyWith(
fontWeight: FontWeight.w800,
color: _color,
fontSize: widget.size * 0.22,
),
),
),
),
),
);
}
}
class _RingPainter extends CustomPainter {
final double progress;
final Color color;
final double strokeWidth;
_RingPainter({required this.progress, required this.color, required this.strokeWidth});
@override
void paint(Canvas canvas, Size size) {
final center = Offset(size.width / 2, size.height / 2);
final radius = (size.width - strokeWidth) / 2;
// Background ring
canvas.drawCircle(
center,
radius,
Paint()
..color = AppColors.border
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth,
);
// Progress arc
canvas.drawArc(
Rect.fromCircle(center: center, radius: radius),
-math.pi / 2,
2 * math.pi * progress,
false,
Paint()
..color = color
..style = PaintingStyle.stroke
..strokeWidth = strokeWidth
..strokeCap = StrokeCap.round,
);
}
@override
bool shouldRepaint(_RingPainter old) =>
old.progress != progress || old.color != color;
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_radius.dart';
enum FSBadgeVariant { green, red, orange, blue, purple }
class FSBadge extends StatelessWidget {
final String text;
final FSBadgeVariant variant;
const FSBadge({
super.key,
required this.text,
this.variant = FSBadgeVariant.blue,
});
factory FSBadge.risk(String level) {
final v = switch (level.toLowerCase()) {
'high' => FSBadgeVariant.red,
'medium' => FSBadgeVariant.orange,
'low' => FSBadgeVariant.green,
_ => FSBadgeVariant.blue,
};
return FSBadge(text: level, variant: v);
}
factory FSBadge.difficulty(String level) {
final v = switch (level.toLowerCase()) {
'hard' => FSBadgeVariant.red,
'medium' => FSBadgeVariant.orange,
'easy' => FSBadgeVariant.green,
_ => FSBadgeVariant.blue,
};
return FSBadge(text: level, variant: v);
}
@override
Widget build(BuildContext context) {
final (bg, fg) = switch (variant) {
FSBadgeVariant.green => (AppColors.successBg, AppColors.successLight),
FSBadgeVariant.red => (AppColors.errorBg, AppColors.errorLight),
FSBadgeVariant.orange => (AppColors.warningBg, AppColors.warningLight),
FSBadgeVariant.blue => (AppColors.infoBg, AppColors.infoLight),
FSBadgeVariant.purple => (AppColors.purpleBg, AppColors.purpleLight),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 3),
decoration: BoxDecoration(
color: bg,
borderRadius: AppRadius.pill,
),
child: Text(
text.toUpperCase(),
style: TextStyle(
color: fg,
fontSize: 10,
fontWeight: FontWeight.w700,
letterSpacing: 0.5,
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_radius.dart';
import '../theme/app_gradients.dart';
enum FSButtonVariant { primary, secondary, ghost, danger, gradient, akinator }
class FSButton extends StatelessWidget {
final String label;
final VoidCallback? onPressed;
final FSButtonVariant variant;
final bool isLoading;
final bool fullWidth;
final IconData? icon;
final double height;
const FSButton({
super.key,
required this.label,
this.onPressed,
this.variant = FSButtonVariant.primary,
this.isLoading = false,
this.fullWidth = true,
this.icon,
this.height = 48,
});
@override
Widget build(BuildContext context) {
final isDisabled = onPressed == null || isLoading;
if (variant == FSButtonVariant.gradient || variant == FSButtonVariant.akinator) {
return _buildGradientButton(isDisabled);
}
return SizedBox(
width: fullWidth ? double.infinity : null,
height: height,
child: _buildStandardButton(isDisabled),
);
}
Widget _buildGradientButton(bool isDisabled) {
final gradient = variant == FSButtonVariant.akinator
? AppGradients.akinatorButton
: AppGradients.primaryButton;
return SizedBox(
width: fullWidth ? double.infinity : null,
height: height,
child: Opacity(
opacity: isDisabled ? 0.5 : 1.0,
child: DecoratedBox(
decoration: BoxDecoration(
gradient: gradient,
borderRadius: AppRadius.smAll,
),
child: Material(
color: Colors.transparent,
child: InkWell(
onTap: isDisabled ? null : onPressed,
borderRadius: AppRadius.smAll,
child: Center(child: _buildChild(Colors.white)),
),
),
),
),
);
}
Widget _buildStandardButton(bool isDisabled) {
switch (variant) {
case FSButtonVariant.primary:
return ElevatedButton(
onPressed: isDisabled ? null : onPressed,
child: _buildChild(Colors.white),
);
case FSButtonVariant.secondary:
return OutlinedButton(
onPressed: isDisabled ? null : onPressed,
child: _buildChild(AppColors.textPrimary),
);
case FSButtonVariant.ghost:
return TextButton(
onPressed: isDisabled ? null : onPressed,
child: _buildChild(AppColors.textSecondary),
);
case FSButtonVariant.danger:
return ElevatedButton(
onPressed: isDisabled ? null : onPressed,
style: ElevatedButton.styleFrom(backgroundColor: AppColors.error),
child: _buildChild(Colors.white),
);
default:
return ElevatedButton(
onPressed: isDisabled ? null : onPressed,
child: _buildChild(Colors.white),
);
}
}
Widget _buildChild(Color textColor) {
if (isLoading) {
return SizedBox(
width: 20,
height: 20,
child: CircularProgressIndicator(
strokeWidth: 2,
valueColor: AlwaysStoppedAnimation(textColor),
),
);
}
if (icon != null) {
return Row(
mainAxisSize: MainAxisSize.min,
children: [
Icon(icon, size: 18, color: textColor),
const SizedBox(width: 8),
Text(label, style: TextStyle(color: textColor, fontWeight: FontWeight.w600, fontSize: 14)),
],
);
}
return Text(label, style: TextStyle(color: textColor, fontWeight: FontWeight.w600, fontSize: 14));
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_spacing.dart';
import 'fs_button.dart';
class FSEmptyState extends StatelessWidget {
final String icon;
final String title;
final String? subtitle;
final String? actionLabel;
final VoidCallback? onAction;
const FSEmptyState({
super.key,
required this.icon,
required this.title,
this.subtitle,
this.actionLabel,
this.onAction,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: AppSpacing.screenAll,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(icon, style: const TextStyle(fontSize: 48)),
AppSpacing.vMd,
Text(
title,
style: AppTextStyles.titleMedium,
textAlign: TextAlign.center,
),
if (subtitle != null) ...[
AppSpacing.vSm,
Text(
subtitle!,
style: AppTextStyles.bodySmall.copyWith(color: AppColors.textTertiary),
textAlign: TextAlign.center,
),
],
if (actionLabel != null && onAction != null) ...[
AppSpacing.vLg,
FSButton(
label: actionLabel!,
onPressed: onAction,
fullWidth: false,
),
],
],
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_spacing.dart';
import 'fs_button.dart';
class FSErrorState extends StatelessWidget {
final String message;
final VoidCallback? onRetry;
const FSErrorState({
super.key,
required this.message,
this.onRetry,
});
@override
Widget build(BuildContext context) {
return Center(
child: Padding(
padding: AppSpacing.screenAll,
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('😵', style: TextStyle(fontSize: 48)),
AppSpacing.vMd,
Text(
'Something went wrong',
style: AppTextStyles.titleMedium,
textAlign: TextAlign.center,
),
AppSpacing.vSm,
Text(
message,
style: AppTextStyles.bodySmall.copyWith(color: AppColors.error),
textAlign: TextAlign.center,
maxLines: 4,
overflow: TextOverflow.ellipsis,
),
if (onRetry != null) ...[
AppSpacing.vLg,
FSButton(
label: 'Try Again',
onPressed: onRetry,
variant: FSButtonVariant.secondary,
fullWidth: false,
),
],
],
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:shimmer/shimmer.dart';
import '../theme/app_colors.dart';
import '../theme/app_radius.dart';
class FSShimmer extends StatelessWidget {
final double width;
final double height;
final double borderRadius;
const FSShimmer({
super.key,
this.width = double.infinity,
required this.height,
this.borderRadius = 8,
});
/// Card-shaped shimmer placeholder
factory FSShimmer.card({double height = 120}) => FSShimmer(height: height, borderRadius: 12);
/// Text line shimmer
factory FSShimmer.line({double width = 200, double height = 14}) =>
FSShimmer(width: width, height: height, borderRadius: 4);
/// Circle shimmer (for avatars)
factory FSShimmer.circle({double size = 40}) =>
FSShimmer(width: size, height: size, borderRadius: size / 2);
@override
Widget build(BuildContext context) {
return Shimmer.fromColors(
baseColor: AppColors.panelLight,
highlightColor: AppColors.panelLighter,
child: Container(
width: width,
height: height,
decoration: BoxDecoration(
color: AppColors.panelLight,
borderRadius: BorderRadius.circular(borderRadius),
),
),
);
}
}
/// A preset shimmer layout for list items
class FSShimmerList extends StatelessWidget {
final int itemCount;
const FSShimmerList({super.key, this.itemCount = 5});
@override
Widget build(BuildContext context) {
return ListView.separated(
physics: const NeverScrollableScrollPhysics(),
shrinkWrap: true,
itemCount: itemCount,
separatorBuilder: (_, __) => const SizedBox(height: 12),
itemBuilder: (_, __) => const FSShimmer(height: 80, borderRadius: 12),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
class FSTextField extends StatelessWidget {
final String label;
final String? hint;
final TextEditingController? controller;
final String? Function(String?)? validator;
final bool obscureText;
final Widget? suffixIcon;
final Widget? prefixIcon;
final TextInputType keyboardType;
final TextInputAction textInputAction;
final ValueChanged<String>? onChanged;
final VoidCallback? onEditingComplete;
final int maxLines;
final bool enabled;
final FocusNode? focusNode;
const FSTextField({
super.key,
required this.label,
this.hint,
this.controller,
this.validator,
this.obscureText = false,
this.suffixIcon,
this.prefixIcon,
this.keyboardType = TextInputType.text,
this.textInputAction = TextInputAction.next,
this.onChanged,
this.onEditingComplete,
this.maxLines = 1,
this.enabled = true,
this.focusNode,
});
@override
Widget build(BuildContext context) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label.toUpperCase(),
style: AppTextStyles.labelSmall.copyWith(
color: AppColors.textSecondary,
letterSpacing: 1,
),
),
const SizedBox(height: 6),
TextFormField(
controller: controller,
validator: validator,
obscureText: obscureText,
keyboardType: keyboardType,
textInputAction: textInputAction,
onChanged: onChanged,
onEditingComplete: onEditingComplete,
maxLines: maxLines,
enabled: enabled,
focusNode: focusNode,
style: AppTextStyles.bodyMedium,
decoration: InputDecoration(
hintText: hint,
suffixIcon: suffixIcon,
prefixIcon: prefixIcon,
),
),
],
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_radius.dart';
class GivensTableWidget extends StatelessWidget {
final Map<String, dynamic> data;
const GivensTableWidget({super.key, required this.data});
@override
Widget build(BuildContext context) {
final entries = data.entries
.where((e) => e.value != null && e.value.toString().isNotEmpty)
.toList();
if (entries.isEmpty) return const SizedBox.shrink();
return Wrap(
spacing: 8,
runSpacing: 8,
children: entries.map((e) {
return Container(
constraints: const BoxConstraints(minWidth: 140),
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 8),
decoration: BoxDecoration(
color: AppColors.bgSecondary,
borderRadius: AppRadius.smAll,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_formatKey(e.key),
style: AppTextStyles.caption.copyWith(
fontSize: 9,
letterSpacing: 0.5,
color: AppColors.textTertiary,
),
),
const SizedBox(height: 2),
Text(
e.value.toString(),
style: AppTextStyles.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
),
);
}).toList(),
);
}
String _formatKey(String key) {
// Convert camelCase/snake_case to Title Case
return key
.replaceAllMapped(RegExp(r'([a-z])([A-Z])'), (m) => '${m[1]} ${m[2]}')
.replaceAll('_', ' ')
.toUpperCase();
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_radius.dart';
class JargonPanel extends StatelessWidget {
final Map<String, String> definitions;
const JargonPanel({super.key, required this.definitions});
@override
Widget build(BuildContext context) {
if (definitions.isEmpty) return const SizedBox.shrink();
return Container(
decoration: BoxDecoration(
color: AppColors.purpleBg,
border: Border.all(color: AppColors.purple.withOpacity(0.15)),
borderRadius: AppRadius.smAll,
),
child: Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 0),
childrenPadding: const EdgeInsets.fromLTRB(12, 0, 12, 8),
title: Text(
'📚 Financial Terms Explained (${definitions.length} terms)',
style: AppTextStyles.labelMedium.copyWith(color: AppColors.purpleLight),
),
children: definitions.entries.map((e) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 4),
child: Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
SizedBox(
width: 90,
child: Text(
e.key,
style: AppTextStyles.labelMedium.copyWith(color: AppColors.purpleLight),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(e.value, style: AppTextStyles.bodySmall),
),
],
),
);
}).toList(),
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_radius.dart';
import '../theme/app_text_styles.dart';
class KBSelector extends StatelessWidget {
final List<String> knowledgeBases;
final String? selectedKB;
final ValueChanged<String?> onChanged;
final bool compact;
const KBSelector({
super.key,
required this.knowledgeBases,
this.selectedKB,
required this.onChanged,
this.compact = false,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10),
decoration: BoxDecoration(
color: AppColors.bgSecondary,
border: Border.all(color: AppColors.border),
borderRadius: AppRadius.smAll,
),
child: DropdownButtonHideUnderline(
child: DropdownButton<String>(
value: selectedKB,
hint: Text(
compact ? 'No KB' : 'No KB Context',
style: AppTextStyles.bodySmall.copyWith(color: AppColors.textTertiary),
),
dropdownColor: AppColors.panelLight,
icon: Icon(Icons.expand_more, size: 18, color: AppColors.textTertiary),
isDense: compact,
isExpanded: !compact,
style: AppTextStyles.bodySmall,
items: [
DropdownMenuItem<String>(
value: null,
child: Text(compact ? 'No KB' : 'No KB Context',
style: AppTextStyles.bodySmall.copyWith(color: AppColors.textTertiary)),
),
...knowledgeBases.map((kb) => DropdownMenuItem(
value: kb,
child: Text('🧠 $kb', style: AppTextStyles.bodySmall),
)),
],
onChanged: onChanged,
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_radius.dart';
class MemoCard extends StatelessWidget {
final String memo;
const MemoCard({super.key, required this.memo});
@override
Widget build(BuildContext context) {
if (memo.isEmpty) return const SizedBox.shrink();
return Theme(
data: Theme.of(context).copyWith(dividerColor: Colors.transparent),
child: ExpansionTile(
tilePadding: EdgeInsets.zero,
childrenPadding: EdgeInsets.zero,
title: Text(
'📋 Investment Memo',
style: AppTextStyles.labelMedium.copyWith(color: AppColors.accentLight),
),
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: AppColors.bgSecondary,
border: Border.all(color: AppColors.border),
borderRadius: AppRadius.smAll,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(memo, style: AppTextStyles.monoSmall.copyWith(height: 1.6)),
const SizedBox(height: 8),
Align(
alignment: Alignment.centerRight,
child: GestureDetector(
onTap: () {
Clipboard.setData(ClipboardData(text: memo));
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Memo copied to clipboard')),
);
},
child: Text(
'Copy',
style: AppTextStyles.labelSmall.copyWith(color: AppColors.accentLight),
),
),
),
],
),
),
],
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_radius.dart';
enum OptionState { idle, selected, correct, wrong }
class OptionButton extends StatelessWidget {
final String label;
final String text;
final OptionState state;
final VoidCallback? onTap;
const OptionButton({
super.key,
required this.label,
required this.text,
this.state = OptionState.idle,
this.onTap,
});
@override
Widget build(BuildContext context) {
final (bg, borderColor, labelColor) = switch (state) {
OptionState.idle => (AppColors.panelLight, AppColors.border, AppColors.accentLight),
OptionState.selected => (AppColors.accentGlow, AppColors.accent, AppColors.accentLight),
OptionState.correct => (AppColors.successBg, AppColors.success, AppColors.success),
OptionState.wrong => (AppColors.errorBg, AppColors.error, AppColors.error),
};
final trailingIcon = switch (state) {
OptionState.correct => Icons.check_circle_rounded,
OptionState.wrong => Icons.cancel_rounded,
_ => null,
};
return Material(
color: bg,
borderRadius: AppRadius.smAll,
child: InkWell(
onTap: onTap,
borderRadius: AppRadius.smAll,
child: AnimatedContainer(
duration: const Duration(milliseconds: 250),
curve: Curves.easeInOut,
padding: const EdgeInsets.symmetric(horizontal: 14, vertical: 12),
decoration: BoxDecoration(
border: Border.all(color: borderColor, width: 2),
borderRadius: AppRadius.smAll,
),
child: Row(
children: [
Text(
'$label)',
style: AppTextStyles.titleSmall.copyWith(
color: labelColor,
fontWeight: FontWeight.w800,
),
),
const SizedBox(width: 10),
Expanded(
child: Text(text, style: AppTextStyles.bodyMedium),
),
if (trailingIcon != null)
Icon(trailingIcon, color: labelColor, size: 22),
],
),
),
),
);
}
}
\ No newline at end of file
import 'dart:async';
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_radius.dart';
import '../theme/app_gradients.dart';
import '../utils/formatters.dart';
class RateLimitTimer extends StatefulWidget {
final int totalSeconds;
final VoidCallback? onComplete;
const RateLimitTimer({super.key, required this.totalSeconds, this.onComplete});
@override
State<RateLimitTimer> createState() => _RateLimitTimerState();
}
class _RateLimitTimerState extends State<RateLimitTimer> {
late int _remaining;
Timer? _timer;
@override
void initState() {
super.initState();
_remaining = widget.totalSeconds;
_timer = Timer.periodic(const Duration(seconds: 1), (_) {
if (_remaining <= 1) {
_timer?.cancel();
setState(() => _remaining = 0);
widget.onComplete?.call();
} else {
setState(() => _remaining--);
}
});
}
@override
void dispose() {
_timer?.cancel();
super.dispose();
}
@override
Widget build(BuildContext context) {
final isDone = _remaining <= 0;
return Container(
padding: const EdgeInsets.all(16),
decoration: BoxDecoration(
gradient: AppGradients.rateLimitBg,
border: Border.all(color: AppColors.warning.withOpacity(0.25)),
borderRadius: AppRadius.lgAll,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
'⏳ Rate Limit Reached',
style: AppTextStyles.titleSmall.copyWith(color: AppColors.warning),
),
const SizedBox(height: 4),
if (isDone)
Text(
'✅ You can chat again now!',
style: AppTextStyles.titleMedium.copyWith(color: AppColors.success),
)
else ...[
Text(
'API cooldown — you can chat again in:',
style: AppTextStyles.bodySmall,
),
const SizedBox(height: 8),
Text(
Formatters.countdown(_remaining),
style: AppTextStyles.displayMedium.copyWith(
fontWeight: FontWeight.w900,
fontFeatures: [const FontFeature.tabularFigures()],
),
),
],
],
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_radius.dart';
import '../theme/app_spacing.dart';
import 'fs_badge.dart';
class ScenarioCard extends StatelessWidget {
final String title;
final String? description;
final String? riskLevel;
final String? difficulty;
final String? eventType;
final VoidCallback? onTap;
const ScenarioCard({
super.key,
required this.title,
this.description,
this.riskLevel,
this.difficulty,
this.eventType,
this.onTap,
});
@override
Widget build(BuildContext context) {
return Material(
color: AppColors.panel,
borderRadius: AppRadius.mdAll,
child: InkWell(
onTap: onTap,
borderRadius: AppRadius.mdAll,
child: Container(
padding: AppSpacing.cardInner,
decoration: BoxDecoration(
border: Border.all(color: AppColors.border),
borderRadius: AppRadius.mdAll,
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
title,
style: AppTextStyles.titleSmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
if (description != null) ...[
AppSpacing.vXs,
Text(
description!,
style: AppTextStyles.bodySmall,
maxLines: 2,
overflow: TextOverflow.ellipsis,
),
],
AppSpacing.vSm,
Wrap(
spacing: 6,
runSpacing: 6,
children: [
if (riskLevel != null) FSBadge.risk(riskLevel!),
if (difficulty != null) FSBadge.difficulty(difficulty!),
if (eventType != null)
FSBadge(
text: eventType!,
variant: eventType!.toLowerCase() == 'major'
? FSBadgeVariant.red
: FSBadgeVariant.blue,
),
],
),
],
),
),
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_radius.dart';
class SentimentBadge extends StatelessWidget {
final String label;
final int score;
const SentimentBadge({
super.key,
required this.label,
required this.score,
});
@override
Widget build(BuildContext context) {
final (icon, bg, fg) = switch (label.toLowerCase()) {
'bullish' => ('📈', AppColors.successBg, AppColors.successLight),
'bearish' => ('📉', AppColors.errorBg, AppColors.errorLight),
'mixed' => ('📊', AppColors.infoBg, AppColors.infoLight),
_ => ('➖', AppColors.panelLight, AppColors.textTertiary),
};
return Container(
padding: const EdgeInsets.symmetric(horizontal: 10, vertical: 4),
decoration: BoxDecoration(color: bg, borderRadius: AppRadius.pill),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(icon, style: const TextStyle(fontSize: 12)),
const SizedBox(width: 4),
Text(
'$label ($score/100)',
style: TextStyle(color: fg, fontSize: 11, fontWeight: FontWeight.w600),
),
],
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import '../theme/app_colors.dart';
import '../theme/app_text_styles.dart';
import '../theme/app_radius.dart';
import '../theme/app_gradients.dart';
class StatCard extends StatelessWidget {
final String icon;
final String value;
final String label;
const StatCard({
super.key,
required this.icon,
required this.value,
required this.label,
});
@override
Widget build(BuildContext context) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 12),
decoration: BoxDecoration(
color: AppColors.panel,
border: Border.all(color: AppColors.border),
borderRadius: AppRadius.mdAll,
),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text(icon, style: const TextStyle(fontSize: 24)),
const SizedBox(height: 8),
ShaderMask(
shaderCallback: (bounds) =>
AppGradients.textAccent.createShader(bounds),
child: Text(
value,
style: AppTextStyles.headlineLarge.copyWith(
color: Colors.white,
fontWeight: FontWeight.w900,
),
),
),
const SizedBox(height: 4),
Text(
label.toUpperCase(),
style: AppTextStyles.caption.copyWith(letterSpacing: 0.5),
textAlign: TextAlign.center,
),
],
),
);
}
}
\ No newline at end of file
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
import 'package:flutter_dotenv/flutter_dotenv.dart';
import 'app.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
await dotenv.load(fileName: '.env');
SystemChrome.setSystemUIOverlayStyle(
const SystemUiOverlayStyle(
statusBarColor: Colors.transparent,
statusBarIconBrightness: Brightness.light,
statusBarBrightness: Brightness.dark,
),
);
SystemChrome.setPreferredOrientations([
DeviceOrientation.portraitUp,
DeviceOrientation.portraitDown,
]);
runApp(const FinSimApp());
}
\ No newline at end of file
This diff is collapsed.
name: finsim_flutter
description: FinSim — AI-Powered Investment Simulation & Education Platform
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.5.0
dependencies:
flutter:
sdk: flutter
# State Management
flutter_bloc: ^8.1.6
equatable: ^2.0.7
# Networking
dio: ^5.7.0
json_annotation: ^4.9.0
freezed_annotation: ^2.4.4
# Navigation
go_router: ^14.6.2
# Local Storage
flutter_secure_storage: ^9.2.4
shared_preferences: ^2.3.5
# UI / Animation
flutter_markdown: ^0.7.6
flutter_animate: ^4.5.2
shimmer: ^3.0.0
fl_chart: ^0.69.2
google_fonts: ^6.2.1
# Utilities
file_picker: ^8.1.7
fluttertoast: ^8.2.12
url_launcher: ^6.3.1
flutter_dotenv: ^5.2.1
intl: ^0.19.0
dev_dependencies:
flutter_test:
sdk: flutter
build_runner: ^2.4.14
freezed: ^2.5.7
json_serializable: ^6.8.0
bloc_test: ^9.1.7
mocktail: ^1.0.4
flutter_lints: ^5.0.0
flutter:
uses-material-design: true
assets:
- .env
\ No newline at end of file
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment