Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in / Register
Toggle navigation
F
FinSim
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
CI / CD
CI / CD
Pipelines
Jobs
Schedules
Charts
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Jobs
Commits
Issue Boards
Open sidebar
Fares
FinSim
Commits
57b01db5
Commit
57b01db5
authored
Apr 08, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 15 files via Son of Anton
parent
cf85f694
Changes
15
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
15 changed files
with
1069 additions
and
18 deletions
+1069
-18
router.dart
finsim_flutter/lib/core/config/router.dart
+17
-13
kb_remote_datasource.dart
...knowledge_base/data/datasources/kb_remote_datasource.dart
+36
-0
kb_repository_impl.dart
.../knowledge_base/data/repositories/kb_repository_impl.dart
+37
-0
kb_repository.dart
...res/knowledge_base/domain/repositories/kb_repository.dart
+9
-0
kb_cubit.dart
...b/features/knowledge_base/presentation/bloc/kb_cubit.dart
+47
-0
kb_state.dart
...b/features/knowledge_base/presentation/bloc/kb_state.dart
+52
-0
kb_list_screen.dart
...s/knowledge_base/presentation/screens/kb_list_screen.dart
+163
-0
kb_upload_screen.dart
...knowledge_base/presentation/screens/kb_upload_screen.dart
+279
-0
leaderboard_remote_datasource.dart
...board/data/datasources/leaderboard_remote_datasource.dart
+42
-0
leaderboard_repository_impl.dart
...rboard/data/repositories/leaderboard_repository_impl.dart
+18
-0
leaderboard_repository.dart
...aderboard/domain/repositories/leaderboard_repository.dart
+5
-0
leaderboard_cubit.dart
...ures/leaderboard/presentation/bloc/leaderboard_cubit.dart
+24
-0
leaderboard_state.dart
...ures/leaderboard/presentation/bloc/leaderboard_state.dart
+29
-0
leaderboard_screen.dart
.../leaderboard/presentation/screens/leaderboard_screen.dart
+278
-0
settings_screen.dart
finsim_flutter/lib/shared/settings_screen.dart
+33
-5
No files found.
finsim_flutter/lib/core/config/router.dart
View file @
57b01db5
...
@@ -17,10 +17,12 @@ import '../../features/generator/presentation/screens/generator_results_screen.d
...
@@ -17,10 +17,12 @@ import '../../features/generator/presentation/screens/generator_results_screen.d
import
'../../features/generator/data/datasources/generator_remote_datasource.dart'
;
import
'../../features/generator/data/datasources/generator_remote_datasource.dart'
;
import
'../../features/scenarios/presentation/screens/scenario_browser_screen.dart'
;
import
'../../features/scenarios/presentation/screens/scenario_browser_screen.dart'
;
import
'../../features/scenarios/presentation/screens/scenario_detail_screen.dart'
;
import
'../../features/scenarios/presentation/screens/scenario_detail_screen.dart'
;
import
'../../features/knowledge_base/presentation/screens/kb_list_screen.dart'
;
import
'../../features/knowledge_base/presentation/screens/kb_upload_screen.dart'
;
import
'../../features/leaderboard/presentation/screens/leaderboard_screen.dart'
;
import
'../../shared/bottom_nav_shell.dart'
;
import
'../../shared/bottom_nav_shell.dart'
;
import
'../../shared/more_menu_screen.dart'
;
import
'../../shared/more_menu_screen.dart'
;
import
'../../shared/settings_screen.dart'
;
import
'../../shared/settings_screen.dart'
;
import
'../../shared/placeholder_screen.dart'
;
class
AppRouter
{
class
AppRouter
{
final
AuthCubit
authCubit
;
final
AuthCubit
authCubit
;
...
@@ -81,7 +83,7 @@ class AppRouter {
...
@@ -81,7 +83,7 @@ class AppRouter {
),
),
],
],
),
),
// ── Tab 3: Quiz ──
✅ PHASE 4
// ── Tab 3: Quiz ──
StatefulShellBranch
(
StatefulShellBranch
(
routes:
[
routes:
[
GoRoute
(
GoRoute
(
...
@@ -97,7 +99,7 @@ class AppRouter {
...
@@ -97,7 +99,7 @@ class AppRouter {
path:
'/app/more'
,
path:
'/app/more'
,
builder:
(
_
,
__
)
=>
const
MoreMenuScreen
(),
builder:
(
_
,
__
)
=>
const
MoreMenuScreen
(),
routes:
[
routes:
[
//
── Scenarios ── ✅ PHASE 4
//
Scenarios
GoRoute
(
GoRoute
(
path:
'scenarios'
,
path:
'scenarios'
,
builder:
(
_
,
__
)
=>
const
ScenarioBrowserScreen
(),
builder:
(
_
,
__
)
=>
const
ScenarioBrowserScreen
(),
...
@@ -111,6 +113,7 @@ class AppRouter {
...
@@ -111,6 +113,7 @@ class AppRouter {
),
),
],
],
),
),
// Generator
GoRoute
(
GoRoute
(
path:
'generator'
,
path:
'generator'
,
builder:
(
_
,
__
)
=>
const
GeneratorScreen
(),
builder:
(
_
,
__
)
=>
const
GeneratorScreen
(),
...
@@ -125,22 +128,23 @@ class AppRouter {
...
@@ -125,22 +128,23 @@ class AppRouter {
),
),
],
],
),
),
// Knowledge Base
GoRoute
(
GoRoute
(
path:
'kb'
,
path:
'kb'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
builder:
(
_
,
__
)
=>
const
KBListScreen
(),
icon:
'🧠'
,
routes:
[
title:
'Knowledge Base'
,
GoRoute
(
subtitle:
'Coming in Phase 5'
,
path:
'upload'
,
),
builder:
(
_
,
__
)
=>
const
KBUploadScreen
(),
),
],
),
),
// Leaderboard
GoRoute
(
GoRoute
(
path:
'leaderboard'
,
path:
'leaderboard'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
builder:
(
_
,
__
)
=>
const
LeaderboardScreen
(),
icon:
'🏆'
,
title:
'Leaderboard'
,
subtitle:
'Coming in Phase 5'
,
),
),
),
// Settings
GoRoute
(
GoRoute
(
path:
'settings'
,
path:
'settings'
,
builder:
(
_
,
__
)
=>
const
SettingsScreen
(),
builder:
(
_
,
__
)
=>
const
SettingsScreen
(),
...
...
finsim_flutter/lib/features/knowledge_base/data/datasources/kb_remote_datasource.dart
View file @
57b01db5
...
@@ -11,4 +11,40 @@ class KBRemoteDatasource {
...
@@ -11,4 +11,40 @@ class KBRemoteDatasource {
final
data
=
response
.
data
as
Map
<
String
,
dynamic
>;
final
data
=
response
.
data
as
Map
<
String
,
dynamic
>;
return
List
<
String
>.
from
(
data
[
'knowledge_bases'
]
??
[]);
return
List
<
String
>.
from
(
data
[
'knowledge_bases'
]
??
[]);
}
}
Future
<
String
>
uploadToKB
({
required
String
kbName
,
List
<
String
>?
urls
,
List
<
String
>?
filePaths
,
List
<
String
>?
fileNames
,
List
<
List
<
int
>>?
fileBytes
,
})
async
{
final
formData
=
FormData
();
formData
.
fields
.
add
(
MapEntry
(
'kb_name'
,
kbName
));
if
(
urls
!=
null
&&
urls
.
isNotEmpty
)
{
formData
.
fields
.
add
(
MapEntry
(
'urls'
,
urls
.
join
(
','
)));
}
if
(
fileBytes
!=
null
&&
fileNames
!=
null
)
{
for
(
var
i
=
0
;
i
<
fileBytes
.
length
;
i
++)
{
formData
.
files
.
add
(
MapEntry
(
'files'
,
MultipartFile
.
fromBytes
(
fileBytes
[
i
],
filename:
fileNames
[
i
],
),
));
}
}
final
response
=
await
_dio
.
post
(
ApiEndpoints
.
ragUpload
,
data:
formData
,
options:
Options
(
contentType:
'multipart/form-data'
),
);
final
data
=
response
.
data
as
Map
<
String
,
dynamic
>;
return
data
[
'message'
]
as
String
?
??
'Upload complete'
;
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/knowledge_base/data/repositories/kb_repository_impl.dart
0 → 100644
View file @
57b01db5
import
'package:dio/dio.dart'
;
import
'../../domain/repositories/kb_repository.dart'
;
import
'../datasources/kb_remote_datasource.dart'
;
class
KBRepositoryImpl
implements
KBRepository
{
final
KBRemoteDatasource
remote
;
KBRepositoryImpl
(
this
.
remote
);
@override
Future
<
List
<
String
>>
getKnowledgeBases
()
async
{
try
{
return
await
remote
.
getKnowledgeBases
();
}
on
DioException
catch
(
e
)
{
throw
e
.
error
??
e
;
}
}
@override
Future
<
String
>
uploadToKB
({
required
String
kbName
,
List
<
String
>?
urls
,
List
<
String
>?
fileNames
,
List
<
List
<
int
>>?
fileBytes
,
})
async
{
try
{
return
await
remote
.
uploadToKB
(
kbName:
kbName
,
urls:
urls
,
fileNames:
fileNames
,
fileBytes:
fileBytes
,
);
}
on
DioException
catch
(
e
)
{
throw
e
.
error
??
e
;
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/knowledge_base/domain/repositories/kb_repository.dart
0 → 100644
View file @
57b01db5
abstract
class
KBRepository
{
Future
<
List
<
String
>>
getKnowledgeBases
();
Future
<
String
>
uploadToKB
({
required
String
kbName
,
List
<
String
>?
urls
,
List
<
String
>?
fileNames
,
List
<
List
<
int
>>?
fileBytes
,
});
}
\ No newline at end of file
finsim_flutter/lib/features/knowledge_base/presentation/bloc/kb_cubit.dart
0 → 100644
View file @
57b01db5
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../../../core/network/dio_client.dart'
;
import
'../../data/datasources/kb_remote_datasource.dart'
;
import
'../../data/repositories/kb_repository_impl.dart'
;
import
'kb_state.dart'
;
class
KBCubit
extends
Cubit
<
KBState
>
{
final
KBRepositoryImpl
_repo
;
KBCubit
()
:
_repo
=
KBRepositoryImpl
(
KBRemoteDatasource
(
DioClient
.
instance
)),
super
(
const
KBInitial
());
Future
<
void
>
loadKnowledgeBases
()
async
{
emit
(
const
KBLoading
());
try
{
final
kbs
=
await
_repo
.
getKnowledgeBases
();
emit
(
KBLoaded
(
kbs
));
}
catch
(
e
)
{
emit
(
KBError
(
e
.
toString
()));
}
}
Future
<
void
>
upload
({
required
String
kbName
,
List
<
String
>?
urls
,
List
<
String
>?
fileNames
,
List
<
List
<
int
>>?
fileBytes
,
})
async
{
emit
(
const
KBUploading
());
try
{
final
message
=
await
_repo
.
uploadToKB
(
kbName:
kbName
,
urls:
urls
,
fileNames:
fileNames
,
fileBytes:
fileBytes
,
);
emit
(
KBUploadSuccess
(
message
));
}
catch
(
e
)
{
emit
(
KBUploadError
(
e
.
toString
()));
}
}
void
resetToLoaded
(
List
<
String
>
kbs
)
{
emit
(
KBLoaded
(
kbs
));
}
}
\ No newline at end of file
finsim_flutter/lib/features/knowledge_base/presentation/bloc/kb_state.dart
0 → 100644
View file @
57b01db5
import
'package:equatable/equatable.dart'
;
sealed
class
KBState
extends
Equatable
{
const
KBState
();
@override
List
<
Object
?>
get
props
=>
[];
}
class
KBInitial
extends
KBState
{
const
KBInitial
();
}
class
KBLoading
extends
KBState
{
const
KBLoading
();
}
class
KBLoaded
extends
KBState
{
final
List
<
String
>
knowledgeBases
;
const
KBLoaded
(
this
.
knowledgeBases
);
@override
List
<
Object
?>
get
props
=>
[
knowledgeBases
];
}
class
KBError
extends
KBState
{
final
String
message
;
const
KBError
(
this
.
message
);
@override
List
<
Object
?>
get
props
=>
[
message
];
}
class
KBUploading
extends
KBState
{
const
KBUploading
();
}
class
KBUploadSuccess
extends
KBState
{
final
String
message
;
const
KBUploadSuccess
(
this
.
message
);
@override
List
<
Object
?>
get
props
=>
[
message
];
}
class
KBUploadError
extends
KBState
{
final
String
message
;
const
KBUploadError
(
this
.
message
);
@override
List
<
Object
?>
get
props
=>
[
message
];
}
\ No newline at end of file
finsim_flutter/lib/features/knowledge_base/presentation/screens/kb_list_screen.dart
0 → 100644
View file @
57b01db5
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'package:go_router/go_router.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_spacing.dart'
;
import
'../../../../core/theme/app_radius.dart'
;
import
'../../../../core/widgets/fs_button.dart'
;
import
'../../../../core/widgets/fs_empty_state.dart'
;
import
'../../../../core/widgets/fs_error_state.dart'
;
import
'../../../../core/widgets/fs_shimmer.dart'
;
import
'../bloc/kb_cubit.dart'
;
import
'../bloc/kb_state.dart'
;
class
KBListScreen
extends
StatefulWidget
{
const
KBListScreen
({
super
.
key
});
@override
State
<
KBListScreen
>
createState
()
=>
_KBListScreenState
();
}
class
_KBListScreenState
extends
State
<
KBListScreen
>
{
late
final
KBCubit
_cubit
;
@override
void
initState
()
{
super
.
initState
();
_cubit
=
KBCubit
()..
loadKnowledgeBases
();
}
@override
void
dispose
()
{
_cubit
.
close
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
.
value
(
value:
_cubit
,
child:
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Knowledge Base'
),
actions:
[
IconButton
(
icon:
const
Icon
(
Icons
.
refresh
,
size:
20
),
onPressed:
()
=>
_cubit
.
loadKnowledgeBases
(),
tooltip:
'Refresh'
,
),
],
),
floatingActionButton:
FloatingActionButton
.
extended
(
onPressed:
()
=>
context
.
push
(
'/app/more/kb/upload'
),
backgroundColor:
AppColors
.
accent
,
icon:
const
Icon
(
Icons
.
upload_file
,
color:
Colors
.
white
),
label:
Text
(
'Upload'
,
style:
AppTextStyles
.
labelMedium
.
copyWith
(
color:
Colors
.
white
)),
),
body:
BlocBuilder
<
KBCubit
,
KBState
>(
builder:
(
context
,
state
)
{
if
(
state
is
KBLoading
)
{
return
Padding
(
padding:
AppSpacing
.
screenAll
,
child:
FSShimmerList
(
itemCount:
5
),
);
}
if
(
state
is
KBError
)
{
return
FSErrorState
(
message:
state
.
message
,
onRetry:
()
=>
_cubit
.
loadKnowledgeBases
(),
);
}
if
(
state
is
KBLoaded
)
{
if
(
state
.
knowledgeBases
.
isEmpty
)
{
return
const
FSEmptyState
(
icon:
'🧠'
,
title:
'No Knowledge Bases'
,
subtitle:
'Upload PDFs, documents, or URLs to create a knowledge base that grounds AI responses.'
,
);
}
return
RefreshIndicator
(
color:
AppColors
.
accent
,
backgroundColor:
AppColors
.
panel
,
onRefresh:
()
=>
_cubit
.
loadKnowledgeBases
(),
child:
ListView
.
separated
(
padding:
AppSpacing
.
screenAll
,
itemCount:
state
.
knowledgeBases
.
length
,
separatorBuilder:
(
_
,
__
)
=>
const
SizedBox
(
height:
10
),
itemBuilder:
(
context
,
index
)
{
final
kb
=
state
.
knowledgeBases
[
index
];
return
_buildKBCard
(
kb
);
},
),
);
}
return
const
SizedBox
.
shrink
();
},
),
),
);
}
Widget
_buildKBCard
(
String
name
)
{
return
Container
(
padding:
AppSpacing
.
cardInner
,
decoration:
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Row
(
children:
[
Container
(
width:
44
,
height:
44
,
decoration:
BoxDecoration
(
color:
AppColors
.
purpleBg
,
borderRadius:
AppRadius
.
smAll
,
),
alignment:
Alignment
.
center
,
child:
const
Text
(
'🧠'
,
style:
TextStyle
(
fontSize:
22
)),
),
const
SizedBox
(
width:
14
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
name
,
style:
AppTextStyles
.
titleSmall
),
const
SizedBox
(
height:
2
),
Text
(
'ChromaDB Collection'
,
style:
AppTextStyles
.
caption
,
),
],
),
),
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
10
,
vertical:
4
),
decoration:
BoxDecoration
(
color:
AppColors
.
successBg
,
borderRadius:
AppRadius
.
pill
,
),
child:
Text
(
'ACTIVE'
,
style:
TextStyle
(
color:
AppColors
.
successLight
,
fontSize:
9
,
fontWeight:
FontWeight
.
w700
,
letterSpacing:
0.5
,
),
),
),
],
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/knowledge_base/presentation/screens/kb_upload_screen.dart
0 → 100644
View file @
57b01db5
This diff is collapsed.
Click to expand it.
finsim_flutter/lib/features/leaderboard/data/datasources/leaderboard_remote_datasource.dart
0 → 100644
View file @
57b01db5
import
'package:dio/dio.dart'
;
import
'../../../../core/network/api_endpoints.dart'
;
class
LeaderboardEntryModel
{
final
String
username
;
final
int
totalScore
;
final
int
quizzesTaken
;
final
String
?
avatar
;
final
int
avgScore
;
const
LeaderboardEntryModel
({
required
this
.
username
,
required
this
.
totalScore
,
required
this
.
quizzesTaken
,
this
.
avatar
,
required
this
.
avgScore
,
});
factory
LeaderboardEntryModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
LeaderboardEntryModel
(
username:
json
[
'username'
]
as
String
,
totalScore:
(
json
[
'total_score'
]
as
num
).
toInt
(),
quizzesTaken:
(
json
[
'quizzes_taken'
]
as
num
).
toInt
(),
avatar:
json
[
'avatar'
]
as
String
?,
avgScore:
(
json
[
'avg_score'
]
as
num
).
toInt
(),
);
}
}
class
LeaderboardRemoteDatasource
{
final
Dio
_dio
;
LeaderboardRemoteDatasource
(
this
.
_dio
);
Future
<
List
<
LeaderboardEntryModel
>>
getLeaderboard
()
async
{
final
response
=
await
_dio
.
get
(
ApiEndpoints
.
leaderboard
);
final
data
=
response
.
data
as
Map
<
String
,
dynamic
>;
return
(
data
[
'leaderboard'
]
as
List
<
dynamic
>)
.
map
((
e
)
=>
LeaderboardEntryModel
.
fromJson
(
e
as
Map
<
String
,
dynamic
>))
.
toList
();
}
}
\ No newline at end of file
finsim_flutter/lib/features/leaderboard/data/repositories/leaderboard_repository_impl.dart
0 → 100644
View file @
57b01db5
import
'package:dio/dio.dart'
;
import
'../../domain/repositories/leaderboard_repository.dart'
;
import
'../datasources/leaderboard_remote_datasource.dart'
;
class
LeaderboardRepositoryImpl
implements
LeaderboardRepository
{
final
LeaderboardRemoteDatasource
remote
;
LeaderboardRepositoryImpl
(
this
.
remote
);
@override
Future
<
List
<
LeaderboardEntryModel
>>
getLeaderboard
()
async
{
try
{
return
await
remote
.
getLeaderboard
();
}
on
DioException
catch
(
e
)
{
throw
e
.
error
??
e
;
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/leaderboard/domain/repositories/leaderboard_repository.dart
0 → 100644
View file @
57b01db5
import
'../../data/datasources/leaderboard_remote_datasource.dart'
;
abstract
class
LeaderboardRepository
{
Future
<
List
<
LeaderboardEntryModel
>>
getLeaderboard
();
}
\ No newline at end of file
finsim_flutter/lib/features/leaderboard/presentation/bloc/leaderboard_cubit.dart
0 → 100644
View file @
57b01db5
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../../../core/network/dio_client.dart'
;
import
'../../data/datasources/leaderboard_remote_datasource.dart'
;
import
'../../data/repositories/leaderboard_repository_impl.dart'
;
import
'leaderboard_state.dart'
;
class
LeaderboardCubit
extends
Cubit
<
LeaderboardState
>
{
final
LeaderboardRepositoryImpl
_repo
;
LeaderboardCubit
()
:
_repo
=
LeaderboardRepositoryImpl
(
LeaderboardRemoteDatasource
(
DioClient
.
instance
)),
super
(
const
LeaderboardLoading
());
Future
<
void
>
loadLeaderboard
()
async
{
emit
(
const
LeaderboardLoading
());
try
{
final
entries
=
await
_repo
.
getLeaderboard
();
emit
(
LeaderboardLoaded
(
entries
));
}
catch
(
e
)
{
emit
(
LeaderboardError
(
e
.
toString
()));
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/leaderboard/presentation/bloc/leaderboard_state.dart
0 → 100644
View file @
57b01db5
import
'package:equatable/equatable.dart'
;
import
'../../data/datasources/leaderboard_remote_datasource.dart'
;
sealed
class
LeaderboardState
extends
Equatable
{
const
LeaderboardState
();
@override
List
<
Object
?>
get
props
=>
[];
}
class
LeaderboardLoading
extends
LeaderboardState
{
const
LeaderboardLoading
();
}
class
LeaderboardLoaded
extends
LeaderboardState
{
final
List
<
LeaderboardEntryModel
>
entries
;
const
LeaderboardLoaded
(
this
.
entries
);
@override
List
<
Object
?>
get
props
=>
[
entries
.
length
];
}
class
LeaderboardError
extends
LeaderboardState
{
final
String
message
;
const
LeaderboardError
(
this
.
message
);
@override
List
<
Object
?>
get
props
=>
[
message
];
}
\ No newline at end of file
finsim_flutter/lib/features/leaderboard/presentation/screens/leaderboard_screen.dart
0 → 100644
View file @
57b01db5
import
'package:flutter/material.dart'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_spacing.dart'
;
import
'../../../../core/theme/app_radius.dart'
;
import
'../../../../core/theme/app_gradients.dart'
;
import
'../../../../core/widgets/fs_shimmer.dart'
;
import
'../../../../core/widgets/fs_empty_state.dart'
;
import
'../../../../core/widgets/fs_error_state.dart'
;
import
'../../data/datasources/leaderboard_remote_datasource.dart'
;
import
'../bloc/leaderboard_cubit.dart'
;
import
'../bloc/leaderboard_state.dart'
;
class
LeaderboardScreen
extends
StatefulWidget
{
const
LeaderboardScreen
({
super
.
key
});
@override
State
<
LeaderboardScreen
>
createState
()
=>
_LeaderboardScreenState
();
}
class
_LeaderboardScreenState
extends
State
<
LeaderboardScreen
>
{
late
final
LeaderboardCubit
_cubit
;
@override
void
initState
()
{
super
.
initState
();
_cubit
=
LeaderboardCubit
()..
loadLeaderboard
();
}
@override
void
dispose
()
{
_cubit
.
close
();
super
.
dispose
();
}
@override
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
.
value
(
value:
_cubit
,
child:
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Leaderboard'
),
actions:
[
IconButton
(
icon:
const
Icon
(
Icons
.
refresh
,
size:
20
),
onPressed:
()
=>
_cubit
.
loadLeaderboard
(),
),
],
),
body:
BlocBuilder
<
LeaderboardCubit
,
LeaderboardState
>(
builder:
(
context
,
state
)
{
if
(
state
is
LeaderboardLoading
)
{
return
Padding
(
padding:
AppSpacing
.
screenAll
,
child:
FSShimmerList
(
itemCount:
8
),
);
}
if
(
state
is
LeaderboardError
)
{
return
FSErrorState
(
message:
state
.
message
,
onRetry:
()
=>
_cubit
.
loadLeaderboard
(),
);
}
if
(
state
is
LeaderboardLoaded
)
{
if
(
state
.
entries
.
isEmpty
)
{
return
const
FSEmptyState
(
icon:
'🏆'
,
title:
'No Rankings Yet'
,
subtitle:
'Be the first to take a quiz and claim the top spot!'
,
);
}
return
RefreshIndicator
(
color:
AppColors
.
accent
,
backgroundColor:
AppColors
.
panel
,
onRefresh:
()
=>
_cubit
.
loadLeaderboard
(),
child:
ListView
.
builder
(
padding:
AppSpacing
.
screenAll
,
itemCount:
state
.
entries
.
length
+
1
,
// +1 for podium
itemBuilder:
(
context
,
index
)
{
if
(
index
==
0
)
{
return
_buildPodium
(
state
.
entries
);
}
final
entry
=
state
.
entries
[
index
-
1
];
final
rank
=
index
;
return
_buildRow
(
entry
,
rank
);
},
),
);
}
return
const
SizedBox
.
shrink
();
},
),
),
);
}
Widget
_buildPodium
(
List
<
LeaderboardEntryModel
>
entries
)
{
if
(
entries
.
length
<
3
)
return
const
SizedBox
.
shrink
();
return
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
20
),
padding:
const
EdgeInsets
.
symmetric
(
vertical:
24
),
child:
Row
(
mainAxisAlignment:
MainAxisAlignment
.
center
,
crossAxisAlignment:
CrossAxisAlignment
.
end
,
children:
[
// 2nd place
_podiumItem
(
entries
[
1
],
2
,
80
,
AppColors
.
textSecondary
),
const
SizedBox
(
width:
12
),
// 1st place
_podiumItem
(
entries
[
0
],
1
,
110
,
AppColors
.
warning
),
const
SizedBox
(
width:
12
),
// 3rd place
_podiumItem
(
entries
[
2
],
3
,
60
,
AppColors
.
cyan
),
],
),
);
}
Widget
_podiumItem
(
LeaderboardEntryModel
entry
,
int
rank
,
double
height
,
Color
accent
)
{
final
medals
=
[
''
,
'🥇'
,
'🥈'
,
'🥉'
];
return
Column
(
mainAxisSize:
MainAxisSize
.
min
,
children:
[
// Avatar
Container
(
width:
rank
==
1
?
56
:
44
,
height:
rank
==
1
?
56
:
44
,
decoration:
BoxDecoration
(
gradient:
rank
==
1
?
AppGradients
.
primaryButton
:
LinearGradient
(
colors:
[
accent
,
accent
.
withOpacity
(
0.6
)]),
shape:
BoxShape
.
circle
,
border:
Border
.
all
(
color:
accent
.
withOpacity
(
0.5
),
width:
rank
==
1
?
3
:
2
,
),
),
alignment:
Alignment
.
center
,
child:
Text
(
entry
.
username
.
isNotEmpty
?
entry
.
username
[
0
].
toUpperCase
()
:
'?'
,
style:
AppTextStyles
.
titleMedium
.
copyWith
(
color:
Colors
.
white
,
fontSize:
rank
==
1
?
20
:
16
,
),
),
),
const
SizedBox
(
height:
6
),
Text
(
medals
[
rank
],
style:
const
TextStyle
(
fontSize:
18
)),
const
SizedBox
(
height:
4
),
Text
(
entry
.
username
,
style:
AppTextStyles
.
labelMedium
,
maxLines:
1
,
overflow:
TextOverflow
.
ellipsis
,
),
Text
(
'
${entry.totalScore}
pts'
,
style:
AppTextStyles
.
caption
.
copyWith
(
color:
accent
),
),
const
SizedBox
(
height:
8
),
// Podium block
Container
(
width:
80
,
height:
height
,
decoration:
BoxDecoration
(
gradient:
LinearGradient
(
begin:
Alignment
.
topCenter
,
end:
Alignment
.
bottomCenter
,
colors:
[
accent
.
withOpacity
(
0.3
),
accent
.
withOpacity
(
0.08
)],
),
borderRadius:
const
BorderRadius
.
only
(
topLeft:
Radius
.
circular
(
8
),
topRight:
Radius
.
circular
(
8
),
),
border:
Border
.
all
(
color:
accent
.
withOpacity
(
0.2
)),
),
alignment:
Alignment
.
center
,
child:
Text
(
'#
$rank
'
,
style:
AppTextStyles
.
headlineMedium
.
copyWith
(
color:
accent
.
withOpacity
(
0.6
),
),
),
),
],
);
}
Widget
_buildRow
(
LeaderboardEntryModel
entry
,
int
rank
)
{
final
isTop3
=
rank
<=
3
;
return
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
8
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
14
,
vertical:
12
),
decoration:
BoxDecoration
(
color:
isTop3
?
AppColors
.
accentGlow
:
AppColors
.
panel
,
border:
Border
.
all
(
color:
isTop3
?
AppColors
.
accent
.
withOpacity
(
0.2
)
:
AppColors
.
border
,
),
borderRadius:
AppRadius
.
smAll
,
),
child:
Row
(
children:
[
// Rank
SizedBox
(
width:
32
,
child:
Text
(
'#
$rank
'
,
style:
AppTextStyles
.
titleSmall
.
copyWith
(
color:
isTop3
?
AppColors
.
warningLight
:
AppColors
.
textTertiary
,
fontWeight:
FontWeight
.
w800
,
),
),
),
const
SizedBox
(
width:
10
),
// Avatar
Container
(
width:
36
,
height:
36
,
decoration:
BoxDecoration
(
color:
AppColors
.
panelLight
,
shape:
BoxShape
.
circle
,
),
alignment:
Alignment
.
center
,
child:
Text
(
entry
.
username
.
isNotEmpty
?
entry
.
username
[
0
].
toUpperCase
()
:
'?'
,
style:
AppTextStyles
.
labelLarge
.
copyWith
(
color:
AppColors
.
accentLight
,
),
),
),
const
SizedBox
(
width:
12
),
// Name + stats
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
entry
.
username
,
style:
AppTextStyles
.
titleSmall
),
Text
(
'
${entry.quizzesTaken}
quizzes · avg
${entry.avgScore}
%'
,
style:
AppTextStyles
.
caption
,
),
],
),
),
// Score
Text
(
'
${entry.totalScore}
'
,
style:
AppTextStyles
.
titleMedium
.
copyWith
(
color:
AppColors
.
accentLight
,
fontWeight:
FontWeight
.
w800
,
),
),
const
SizedBox
(
width:
4
),
Text
(
'pts'
,
style:
AppTextStyles
.
caption
),
],
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/shared/settings_screen.dart
View file @
57b01db5
...
@@ -14,12 +14,36 @@ class SettingsScreen extends StatelessWidget {
...
@@ -14,12 +14,36 @@ class SettingsScreen extends StatelessWidget {
@override
@override
Widget
build
(
BuildContext
context
)
{
Widget
build
(
BuildContext
context
)
{
final
user
=
context
.
read
<
AuthCubit
>().
currentUser
;
return
Scaffold
(
return
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Settings'
)),
appBar:
AppBar
(
title:
const
Text
(
'Settings'
)),
body:
ListView
(
body:
ListView
(
padding:
AppSpacing
.
screenAll
,
padding:
AppSpacing
.
screenAll
,
children:
[
children:
[
// ── Server Info ──
// ── Account ──
_SectionHeader
(
'Account'
),
AppSpacing
.
vSm
,
Container
(
padding:
AppSpacing
.
cardInner
,
decoration:
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Column
(
children:
[
_infoRow
(
'Username'
,
user
?.
username
??
'-'
),
const
Divider
(
height:
20
),
_infoRow
(
'Email'
,
user
?.
email
??
'-'
),
const
Divider
(
height:
20
),
_infoRow
(
'Role'
,
user
?.
role
.
toUpperCase
()
??
'USER'
),
],
),
),
AppSpacing
.
vLg
,
// ── Server ──
_SectionHeader
(
'Server Configuration'
),
_SectionHeader
(
'Server Configuration'
),
AppSpacing
.
vSm
,
AppSpacing
.
vSm
,
Container
(
Container
(
...
@@ -63,13 +87,14 @@ class SettingsScreen extends StatelessWidget {
...
@@ -63,13 +87,14 @@ class SettingsScreen extends StatelessWidget {
borderRadius:
AppRadius
.
mdAll
,
borderRadius:
AppRadius
.
mdAll
,
),
),
child:
Column
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
children:
[
_infoRow
(
'App'
,
'
${AppConfig.appName}
v
${AppConfig.appVersion}
'
),
_infoRow
(
'App'
,
'
${AppConfig.appName}
v
${AppConfig.appVersion}
'
),
const
Divider
(
height:
20
),
const
Divider
(
height:
20
),
_infoRow
(
'Platform'
,
'Flutter + FastAPI + LangGraph'
),
_infoRow
(
'Platform'
,
'Flutter + FastAPI + LangGraph'
),
const
Divider
(
height:
20
),
const
Divider
(
height:
20
),
_infoRow
(
'AI Model'
,
'LLaMA 3.3 70B (Groq)'
),
_infoRow
(
'AI Model'
,
'LLaMA 3.3 70B (Groq)'
),
const
Divider
(
height:
20
),
_infoRow
(
'Data'
,
'yfinance + SerpAPI + ChromaDB'
),
],
],
),
),
),
),
...
@@ -93,9 +118,12 @@ class SettingsScreen extends StatelessWidget {
...
@@ -93,9 +118,12 @@ class SettingsScreen extends StatelessWidget {
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
mainAxisAlignment:
MainAxisAlignment
.
spaceBetween
,
children:
[
children:
[
Text
(
label
,
style:
AppTextStyles
.
bodySmall
),
Text
(
label
,
style:
AppTextStyles
.
bodySmall
),
Text
(
value
,
Flexible
(
style:
AppTextStyles
.
labelMedium
child:
Text
(
value
,
.
copyWith
(
color:
AppColors
.
textSecondary
)),
style:
AppTextStyles
.
labelMedium
.
copyWith
(
color:
AppColors
.
textSecondary
),
textAlign:
TextAlign
.
end
),
),
],
],
);
);
}
}
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment