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
8f5af7a6
Commit
8f5af7a6
authored
Apr 08, 2026
by
Administrator
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
Update 25 files via Son of Anton
parent
7636a1d3
Changes
25
Expand all
Hide whitespace changes
Inline
Side-by-side
Showing
25 changed files
with
1885 additions
and
24 deletions
+1885
-24
router.dart
finsim_flutter/lib/core/config/router.dart
+21
-21
error_interceptor.dart
finsim_flutter/lib/core/network/error_interceptor.dart
+12
-3
akinator_remote_datasource.dart
...akinator/data/datasources/akinator_remote_datasource.dart
+82
-0
akinator_repository_impl.dart
.../akinator/data/repositories/akinator_repository_impl.dart
+28
-0
akinator_repository.dart
...res/akinator/domain/repositories/akinator_repository.dart
+10
-0
akinator_cubit.dart
...b/features/akinator/presentation/bloc/akinator_cubit.dart
+128
-0
akinator_state.dart
...b/features/akinator/presentation/bloc/akinator_state.dart
+92
-0
akinator_screen.dart
...atures/akinator/presentation/screens/akinator_screen.dart
+249
-0
akinator_banner.dart
...atures/akinator/presentation/widgets/akinator_banner.dart
+54
-0
akinator_message_card.dart
.../akinator/presentation/widgets/akinator_message_card.dart
+115
-0
panel_mode_toggle.dart
...ures/akinator/presentation/widgets/panel_mode_toggle.dart
+69
-0
chat_remote_datasource.dart
...eatures/chat/data/datasources/chat_remote_datasource.dart
+44
-0
chat_repository_impl.dart
...features/chat/data/repositories/chat_repository_impl.dart
+26
-0
chat_repository.dart
...ib/features/chat/domain/repositories/chat_repository.dart
+9
-0
chat_cubit.dart
...utter/lib/features/chat/presentation/bloc/chat_cubit.dart
+124
-0
chat_state.dart
...utter/lib/features/chat/presentation/bloc/chat_state.dart
+65
-0
chat_screen.dart
...r/lib/features/chat/presentation/screens/chat_screen.dart
+200
-0
generator_remote_datasource.dart
...nerator/data/datasources/generator_remote_datasource.dart
+60
-0
generator_repository_impl.dart
...enerator/data/repositories/generator_repository_impl.dart
+30
-0
generator_repository.dart
...s/generator/domain/repositories/generator_repository.dart
+11
-0
generator_cubit.dart
...features/generator/presentation/bloc/generator_cubit.dart
+53
-0
generator_state.dart
...features/generator/presentation/bloc/generator_state.dart
+40
-0
generator_results_screen.dart
...erator/presentation/screens/generator_results_screen.dart
+131
-0
generator_screen.dart
...ures/generator/presentation/screens/generator_screen.dart
+218
-0
kb_remote_datasource.dart
...knowledge_base/data/datasources/kb_remote_datasource.dart
+14
-0
No files found.
finsim_flutter/lib/core/config/router.dart
View file @
8f5af7a6
...
...
@@ -9,6 +9,11 @@ import '../../features/auth/presentation/screens/login_screen.dart';
import
'../../features/auth/presentation/screens/register_screen.dart'
;
import
'../../features/dashboard/presentation/screens/dashboard_screen.dart'
;
import
'../../features/dashboard/presentation/screens/profile_screen.dart'
;
import
'../../features/chat/presentation/screens/chat_screen.dart'
;
import
'../../features/akinator/presentation/screens/akinator_screen.dart'
;
import
'../../features/generator/presentation/screens/generator_screen.dart'
;
import
'../../features/generator/presentation/screens/generator_results_screen.dart'
;
import
'../../features/generator/data/datasources/generator_remote_datasource.dart'
;
import
'../../shared/bottom_nav_shell.dart'
;
import
'../../shared/more_menu_screen.dart'
;
import
'../../shared/settings_screen.dart'
;
...
...
@@ -55,29 +60,21 @@ class AppRouter {
),
],
),
// ── Tab 1: Chat ──
// ── Tab 1: Chat ──
✅ PHASE 3
StatefulShellBranch
(
routes:
[
GoRoute
(
path:
'/app/chat'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'🤖'
,
title:
'AI Investment Advisor'
,
subtitle:
'Coming in Phase 3'
,
),
builder:
(
_
,
__
)
=>
const
ChatScreen
(),
),
],
),
// ── Tab 2: Akinator ──
// ── Tab 2: Akinator ──
✅ PHASE 3
StatefulShellBranch
(
routes:
[
GoRoute
(
path:
'/app/akinator'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'🔮'
,
title:
'Akinator 2.0'
,
subtitle:
'Coming in Phase 3'
,
),
builder:
(
_
,
__
)
=>
const
AkinatorScreen
(),
),
],
),
...
...
@@ -109,13 +106,20 @@ class AppRouter {
subtitle:
'Coming in Phase 4'
,
),
),
// ── Generator ── ✅ PHASE 3
GoRoute
(
path:
'generator'
,
builder:
(
_
,
__
)
=>
const
PlaceholderScreen
(
icon:
'⚡'
,
title:
'Scenario Generator'
,
subtitle:
'Coming in Phase 3'
,
),
builder:
(
_
,
__
)
=>
const
GeneratorScreen
(),
routes:
[
GoRoute
(
path:
'results'
,
builder:
(
_
,
state
)
{
final
result
=
state
.
extra
as
GenerateResultModel
;
return
GeneratorResultsScreen
(
result:
result
);
},
),
],
),
GoRoute
(
path:
'kb'
,
...
...
@@ -154,24 +158,20 @@ class AppRouter {
final
isSplash
=
location
==
'/splash'
;
final
isAuthScreen
=
location
==
'/login'
||
location
==
'/register'
;
// Still loading — stay on splash
if
(
authState
is
AuthInitial
||
authState
is
AuthLoading
)
{
return
isSplash
?
null
:
'/splash'
;
}
// Authenticated — get off auth screens
if
(
authState
is
Authenticated
)
{
if
(
isSplash
||
isAuthScreen
)
return
'/app/dashboard'
;
return
null
;
}
// Not authenticated — must be on auth screen
if
(
isSplash
||
isAuthScreen
)
return
null
;
return
'/login'
;
}
}
/// Converts AuthCubit's stream into a Listenable for GoRouter.
class
_GoRouterAuthRefresh
extends
ChangeNotifier
{
late
final
StreamSubscription
<
dynamic
>
_sub
;
...
...
finsim_flutter/lib/core/network/error_interceptor.dart
View file @
8f5af7a6
...
...
@@ -21,6 +21,18 @@ class ErrorInterceptor extends Interceptor {
@override
void
onError
(
DioException
err
,
ErrorInterceptorHandler
handler
)
{
// Already typed by onResponse — pass through
final
existing
=
err
.
error
;
if
(
existing
is
RateLimitException
||
existing
is
AuthException
||
existing
is
NotFoundException
||
existing
is
ConflictException
||
existing
is
NetworkException
||
existing
is
ServerException
)
{
handler
.
next
(
err
);
return
;
}
final
statusCode
=
err
.
response
?.
statusCode
;
switch
(
statusCode
)
{
...
...
@@ -33,7 +45,6 @@ class ErrorInterceptor extends Interceptor {
),
);
return
;
case
404
:
handler
.
reject
(
DioException
(
...
...
@@ -43,7 +54,6 @@ class ErrorInterceptor extends Interceptor {
),
);
return
;
case
409
:
handler
.
reject
(
DioException
(
...
...
@@ -53,7 +63,6 @@ class ErrorInterceptor extends Interceptor {
),
);
return
;
case
429
:
final
retryAfter
=
err
.
response
?.
headers
.
value
(
'retry-after'
);
final
waitSeconds
=
...
...
finsim_flutter/lib/features/akinator/data/datasources/akinator_remote_datasource.dart
0 → 100644
View file @
8f5af7a6
import
'package:dio/dio.dart'
;
import
'../../../../core/network/api_endpoints.dart'
;
class
AkinatorResponseModel
{
final
String
sessionId
;
final
String
reply
;
final
int
confidenceScore
;
final
Map
<
String
,
dynamic
>?
newsSentiment
;
final
Map
<
String
,
String
>
jargonDefinitions
;
final
String
?
memo
;
final
String
?
whatifResult
;
final
String
?
critiqueNote
;
final
bool
ragUsed
;
const
AkinatorResponseModel
({
required
this
.
sessionId
,
required
this
.
reply
,
this
.
confidenceScore
=
0
,
this
.
newsSentiment
,
this
.
jargonDefinitions
=
const
{},
this
.
memo
,
this
.
whatifResult
,
this
.
critiqueNote
,
this
.
ragUsed
=
false
,
});
factory
AkinatorResponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
// Parse jargon_definitions — API sends Map<String, dynamic>
final
rawJargon
=
json
[
'jargon_definitions'
];
final
jargon
=
<
String
,
String
>{};
if
(
rawJargon
is
Map
)
{
rawJargon
.
forEach
((
k
,
v
)
=>
jargon
[
k
.
toString
()]
=
v
.
toString
());
}
return
AkinatorResponseModel
(
sessionId:
(
json
[
'session_id'
]
as
String
?)
??
''
,
reply:
(
json
[
'reply'
]
as
String
?)
??
''
,
confidenceScore:
(
json
[
'confidence_score'
]
as
num
?)?.
toInt
()
??
0
,
newsSentiment:
json
[
'news_sentiment'
]
as
Map
<
String
,
dynamic
>?,
jargonDefinitions:
jargon
,
memo:
json
[
'memo'
]
as
String
?,
whatifResult:
json
[
'whatif_result'
]
as
String
?,
critiqueNote:
json
[
'critique_note'
]
as
String
?,
ragUsed:
(
json
[
'rag_used'
]
as
bool
?)
??
false
,
);
}
String
?
get
sentimentLabel
{
if
(
newsSentiment
==
null
)
return
null
;
return
newsSentiment
![
'label'
]
as
String
?;
}
int
get
sentimentScore
{
if
(
newsSentiment
==
null
)
return
50
;
return
(
newsSentiment
![
'score'
]
as
num
?)?.
toInt
()
??
50
;
}
}
class
AkinatorRemoteDatasource
{
final
Dio
_dio
;
AkinatorRemoteDatasource
(
this
.
_dio
);
Future
<
AkinatorResponseModel
>
predict
({
required
String
sessionId
,
required
String
message
,
bool
panelMode
=
false
,
String
?
knowledgeBaseId
,
})
async
{
final
response
=
await
_dio
.
post
(
ApiEndpoints
.
akinator
,
data:
{
'session_id'
:
sessionId
,
'message'
:
message
,
'panel_mode'
:
panelMode
,
if
(
knowledgeBaseId
!=
null
)
'knowledge_base_id'
:
knowledgeBaseId
,
},
);
return
AkinatorResponseModel
.
fromJson
(
response
.
data
as
Map
<
String
,
dynamic
>);
}
}
\ No newline at end of file
finsim_flutter/lib/features/akinator/data/repositories/akinator_repository_impl.dart
0 → 100644
View file @
8f5af7a6
import
'package:dio/dio.dart'
;
import
'../../domain/repositories/akinator_repository.dart'
;
import
'../datasources/akinator_remote_datasource.dart'
;
class
AkinatorRepositoryImpl
implements
AkinatorRepository
{
final
AkinatorRemoteDatasource
remote
;
AkinatorRepositoryImpl
(
this
.
remote
);
@override
Future
<
AkinatorResponseModel
>
predict
({
required
String
sessionId
,
required
String
message
,
bool
panelMode
=
false
,
String
?
knowledgeBaseId
,
})
async
{
try
{
return
await
remote
.
predict
(
sessionId:
sessionId
,
message:
message
,
panelMode:
panelMode
,
knowledgeBaseId:
knowledgeBaseId
,
);
}
on
DioException
catch
(
e
)
{
throw
e
.
error
??
e
;
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/akinator/domain/repositories/akinator_repository.dart
0 → 100644
View file @
8f5af7a6
import
'../../data/datasources/akinator_remote_datasource.dart'
;
abstract
class
AkinatorRepository
{
Future
<
AkinatorResponseModel
>
predict
({
required
String
sessionId
,
required
String
message
,
bool
panelMode
,
String
?
knowledgeBaseId
,
});
}
\ No newline at end of file
finsim_flutter/lib/features/akinator/presentation/bloc/akinator_cubit.dart
0 → 100644
View file @
8f5af7a6
import
'dart:async'
;
import
'dart:math'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../../../core/error/exceptions.dart'
;
import
'../../../../core/network/dio_client.dart'
;
import
'../../../knowledge_base/data/datasources/kb_remote_datasource.dart'
;
import
'../../data/datasources/akinator_remote_datasource.dart'
;
import
'../../data/repositories/akinator_repository_impl.dart'
;
import
'akinator_state.dart'
;
class
AkinatorCubit
extends
Cubit
<
AkinatorState
>
{
final
AkinatorRepositoryImpl
_repo
;
Timer
?
_rateLimitTimer
;
AkinatorCubit
()
:
_repo
=
AkinatorRepositoryImpl
(
AkinatorRemoteDatasource
(
DioClient
.
instance
)),
super
(
AkinatorState
(
sessionId:
'ak_
${Random().nextInt(999999)}
'
,
messages:
[
AkinatorMessage
(
id:
'welcome'
,
text:
'🔮 **Akinator 2.0 — Multi-Agent Prediction Engine**
\n\n
'
'I run **4 expert analyst agents** simultaneously:
\n\n
'
'📈 **Market Data Analyst** — Live prices, ratios & returns
\n
'
'📰 **News & Sentiment Analyst** — Breaking news & market mood
\n
'
'⚠️ **Risk Assessment Analyst** — Volatility & downside risk
\n
'
'💼 **Portfolio Strategy Advisor** — Allocation & strategy
\n\n
'
'_Try: "Should I invest in AAPL?" or "What if I invested
\$
10K in Tesla 3 years ago?"_'
,
timestamp:
DateTime
.
now
(),
),
],
));
Future
<
void
>
loadKnowledgeBases
()
async
{
try
{
final
kbs
=
await
KBRemoteDatasource
(
DioClient
.
instance
).
getKnowledgeBases
();
emit
(
state
.
copyWith
(
knowledgeBases:
kbs
));
}
catch
(
_
)
{}
}
void
selectKB
(
String
?
kbId
)
{
emit
(
state
.
copyWith
(
selectedKbId:
()
=>
kbId
));
}
void
togglePanelMode
()
{
emit
(
state
.
copyWith
(
panelMode:
!
state
.
panelMode
));
}
Future
<
void
>
sendQuery
(
String
text
)
async
{
if
(
text
.
trim
().
isEmpty
||
state
.
isTyping
||
state
.
isRateLimited
)
return
;
final
userMsg
=
AkinatorMessage
(
id:
'user_
${DateTime.now().millisecondsSinceEpoch}
'
,
text:
text
.
trim
(),
isUser:
true
,
timestamp:
DateTime
.
now
(),
);
emit
(
state
.
copyWith
(
messages:
[...
state
.
messages
,
userMsg
],
isTyping:
true
,
));
try
{
final
response
=
await
_repo
.
predict
(
sessionId:
state
.
sessionId
,
message:
text
.
trim
(),
panelMode:
state
.
panelMode
,
knowledgeBaseId:
state
.
selectedKbId
,
);
final
botMsg
=
AkinatorMessage
(
id:
'bot_
${DateTime.now().millisecondsSinceEpoch}
'
,
text:
response
.
reply
,
confidenceScore:
response
.
confidenceScore
,
sentimentLabel:
response
.
sentimentLabel
,
sentimentScore:
response
.
sentimentScore
,
critiqueNote:
response
.
critiqueNote
,
jargonDefinitions:
response
.
jargonDefinitions
,
memo:
response
.
memo
,
whatifResult:
response
.
whatifResult
,
ragUsed:
response
.
ragUsed
,
timestamp:
DateTime
.
now
(),
);
emit
(
state
.
copyWith
(
messages:
[...
state
.
messages
,
botMsg
],
isTyping:
false
,
));
}
on
RateLimitException
catch
(
e
)
{
emit
(
state
.
copyWith
(
isTyping:
false
));
_startRateLimitCountdown
(
e
.
waitSeconds
);
}
catch
(
e
)
{
final
errorMsg
=
AkinatorMessage
(
id:
'error_
${DateTime.now().millisecondsSinceEpoch}
'
,
text:
'❌ Error:
${e.toString()}
'
,
timestamp:
DateTime
.
now
(),
);
emit
(
state
.
copyWith
(
messages:
[...
state
.
messages
,
errorMsg
],
isTyping:
false
,
));
}
}
void
_startRateLimitCountdown
(
int
seconds
)
{
_rateLimitTimer
?.
cancel
();
emit
(
state
.
copyWith
(
rateLimitSeconds:
()
=>
seconds
));
_rateLimitTimer
=
Timer
.
periodic
(
const
Duration
(
seconds:
1
),
(
timer
)
{
final
remaining
=
(
state
.
rateLimitSeconds
??
0
)
-
1
;
if
(
remaining
<=
0
)
{
timer
.
cancel
();
emit
(
state
.
copyWith
(
rateLimitSeconds:
()
=>
null
));
}
else
{
emit
(
state
.
copyWith
(
rateLimitSeconds:
()
=>
remaining
));
}
});
}
@override
Future
<
void
>
close
()
{
_rateLimitTimer
?.
cancel
();
return
super
.
close
();
}
}
\ No newline at end of file
finsim_flutter/lib/features/akinator/presentation/bloc/akinator_state.dart
0 → 100644
View file @
8f5af7a6
import
'package:equatable/equatable.dart'
;
class
AkinatorMessage
extends
Equatable
{
final
String
id
;
final
String
text
;
final
bool
isUser
;
final
bool
isTyping
;
final
int
confidenceScore
;
final
String
?
sentimentLabel
;
final
int
sentimentScore
;
final
String
?
critiqueNote
;
final
Map
<
String
,
String
>
jargonDefinitions
;
final
String
?
memo
;
final
String
?
whatifResult
;
final
bool
ragUsed
;
final
DateTime
timestamp
;
const
AkinatorMessage
({
required
this
.
id
,
required
this
.
text
,
this
.
isUser
=
false
,
this
.
isTyping
=
false
,
this
.
confidenceScore
=
0
,
this
.
sentimentLabel
,
this
.
sentimentScore
=
50
,
this
.
critiqueNote
,
this
.
jargonDefinitions
=
const
{},
this
.
memo
,
this
.
whatifResult
,
this
.
ragUsed
=
false
,
required
this
.
timestamp
,
});
bool
get
hasMetadata
=>
!
isUser
&&
!
isTyping
&&
(
confidenceScore
>
0
||
sentimentLabel
!=
null
||
(
critiqueNote
!=
null
&&
critiqueNote
!.
isNotEmpty
)
||
jargonDefinitions
.
isNotEmpty
||
(
memo
!=
null
&&
memo
!.
isNotEmpty
));
@override
List
<
Object
?>
get
props
=>
[
id
];
}
class
AkinatorState
extends
Equatable
{
final
List
<
AkinatorMessage
>
messages
;
final
bool
isTyping
;
final
bool
panelMode
;
final
int
?
rateLimitSeconds
;
final
String
?
selectedKbId
;
final
String
sessionId
;
final
List
<
String
>
knowledgeBases
;
const
AkinatorState
({
this
.
messages
=
const
[],
this
.
isTyping
=
false
,
this
.
panelMode
=
false
,
this
.
rateLimitSeconds
,
this
.
selectedKbId
,
required
this
.
sessionId
,
this
.
knowledgeBases
=
const
[],
});
bool
get
isRateLimited
=>
rateLimitSeconds
!=
null
&&
rateLimitSeconds
!
>
0
;
AkinatorState
copyWith
({
List
<
AkinatorMessage
>?
messages
,
bool
?
isTyping
,
bool
?
panelMode
,
int
?
Function
()?
rateLimitSeconds
,
String
?
Function
()?
selectedKbId
,
List
<
String
>?
knowledgeBases
,
})
{
return
AkinatorState
(
messages:
messages
??
this
.
messages
,
isTyping:
isTyping
??
this
.
isTyping
,
panelMode:
panelMode
??
this
.
panelMode
,
rateLimitSeconds:
rateLimitSeconds
!=
null
?
rateLimitSeconds
()
:
this
.
rateLimitSeconds
,
selectedKbId:
selectedKbId
!=
null
?
selectedKbId
()
:
this
.
selectedKbId
,
sessionId:
sessionId
,
knowledgeBases:
knowledgeBases
??
this
.
knowledgeBases
,
);
}
@override
List
<
Object
?>
get
props
=>
[
messages
,
isTyping
,
panelMode
,
rateLimitSeconds
,
selectedKbId
,
knowledgeBases
];
}
\ No newline at end of file
finsim_flutter/lib/features/akinator/presentation/screens/akinator_screen.dart
0 → 100644
View file @
8f5af7a6
This diff is collapsed.
Click to expand it.
finsim_flutter/lib/features/akinator/presentation/widgets/akinator_banner.dart
0 → 100644
View file @
8f5af7a6
import
'package:flutter/material.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_radius.dart'
;
import
'../../../../core/theme/app_gradients.dart'
;
import
'../../../../core/widgets/fs_badge.dart'
;
class
AkinatorBanner
extends
StatelessWidget
{
const
AkinatorBanner
({
super
.
key
});
@override
Widget
build
(
BuildContext
context
)
{
return
Container
(
margin:
const
EdgeInsets
.
fromLTRB
(
12
,
8
,
12
,
4
),
padding:
const
EdgeInsets
.
all
(
16
),
decoration:
BoxDecoration
(
gradient:
AppGradients
.
akinatorBanner
,
border:
Border
.
all
(
color:
AppColors
.
purple
.
withOpacity
(
0.25
)),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
ShaderMask
(
shaderCallback:
(
b
)
=>
AppGradients
.
akinatorButton
.
createShader
(
b
),
child:
Text
(
'🔮 Akinator 2.0'
,
style:
AppTextStyles
.
titleLarge
.
copyWith
(
color:
Colors
.
white
,
fontWeight:
FontWeight
.
w800
,
),
),
),
const
SizedBox
(
height:
6
),
Text
(
'4 expert AI analysts working in parallel — live data, news, risk & strategy.'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
textSecondary
),
),
const
SizedBox
(
height:
10
),
const
Wrap
(
spacing:
6
,
runSpacing:
6
,
children:
[
FSBadge
(
text:
'📈 Market Data'
,
variant:
FSBadgeVariant
.
purple
),
FSBadge
(
text:
'📰 News'
,
variant:
FSBadgeVariant
.
blue
),
FSBadge
(
text:
'⚠️ Risk'
,
variant:
FSBadgeVariant
.
orange
),
FSBadge
(
text:
'💼 Strategy'
,
variant:
FSBadgeVariant
.
green
),
],
),
],
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/akinator/presentation/widgets/akinator_message_card.dart
0 → 100644
View file @
8f5af7a6
import
'package:flutter/material.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
import
'../../../../core/theme/app_radius.dart'
;
import
'../../../../core/widgets/chat_bubble.dart'
;
import
'../../../../core/widgets/confidence_meter.dart'
;
import
'../../../../core/widgets/sentiment_badge.dart'
;
import
'../../../../core/widgets/jargon_panel.dart'
;
import
'../../../../core/widgets/memo_card.dart'
;
import
'../bloc/akinator_state.dart'
;
class
AkinatorMessageCard
extends
StatelessWidget
{
final
AkinatorMessage
message
;
const
AkinatorMessageCard
({
super
.
key
,
required
this
.
message
});
@override
Widget
build
(
BuildContext
context
)
{
if
(
message
.
isUser
)
{
return
ChatBubble
(
text:
message
.
text
,
isUser:
true
);
}
if
(
message
.
isTyping
)
{
return
const
ChatBubble
(
text:
''
,
isTyping:
true
,
isAkinator:
true
);
}
return
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// Main bubble
ChatBubble
(
text:
message
.
text
,
isAkinator:
true
,
ragUsed:
message
.
ragUsed
,
),
// Metadata
if
(
message
.
hasMetadata
)
...[
const
SizedBox
(
height:
6
),
Padding
(
padding:
const
EdgeInsets
.
only
(
left:
4
,
right:
60
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
// Confidence + Sentiment row
if
(
message
.
confidenceScore
>
0
||
message
.
sentimentLabel
!=
null
)
Wrap
(
spacing:
8
,
runSpacing:
6
,
children:
[
if
(
message
.
confidenceScore
>
0
)
ConfidenceMeter
(
score:
message
.
confidenceScore
,
size:
48
,
strokeWidth:
4
,
),
if
(
message
.
sentimentLabel
!=
null
)
SentimentBadge
(
label:
message
.
sentimentLabel
!,
score:
message
.
sentimentScore
,
),
],
),
// Critique
if
(
message
.
critiqueNote
!=
null
&&
message
.
critiqueNote
!.
isNotEmpty
)
...[
const
SizedBox
(
height:
8
),
Container
(
padding:
const
EdgeInsets
.
all
(
10
),
decoration:
BoxDecoration
(
color:
AppColors
.
warningBg
,
border:
Border
.
all
(
color:
AppColors
.
warning
.
withOpacity
(
0.2
)),
borderRadius:
AppRadius
.
smAll
,
),
child:
Row
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
const
Text
(
'⚠️'
,
style:
TextStyle
(
fontSize:
14
)),
const
SizedBox
(
width:
6
),
Expanded
(
child:
Text
(
message
.
critiqueNote
!,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
warningLight
,
height:
1.5
,
),
),
),
],
),
),
],
// Jargon
if
(
message
.
jargonDefinitions
.
isNotEmpty
)
...[
const
SizedBox
(
height:
8
),
JargonPanel
(
definitions:
message
.
jargonDefinitions
),
],
// Memo
if
(
message
.
memo
!=
null
&&
message
.
memo
!.
isNotEmpty
)
...[
const
SizedBox
(
height:
8
),
MemoCard
(
memo:
message
.
memo
!),
],
],
),
),
],
],
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/akinator/presentation/widgets/panel_mode_toggle.dart
0 → 100644
View file @
8f5af7a6
import
'package:flutter/material.dart'
;
import
'../../../../core/theme/app_colors.dart'
;
import
'../../../../core/theme/app_text_styles.dart'
;
class
PanelModeToggle
extends
StatelessWidget
{
final
bool
isOn
;
final
ValueChanged
<
bool
>
onChanged
;
const
PanelModeToggle
({
super
.
key
,
required
this
.
isOn
,
required
this
.
onChanged
,
});
@override
Widget
build
(
BuildContext
context
)
{
return
Container
(
margin:
const
EdgeInsets
.
symmetric
(
horizontal:
12
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
14
,
vertical:
10
),
decoration:
BoxDecoration
(
border:
Border
(
top:
BorderSide
(
color:
AppColors
.
purple
.
withOpacity
(
0.15
)),
bottom:
BorderSide
(
color:
AppColors
.
purple
.
withOpacity
(
0.15
)),
),
),
child:
Row
(
children:
[
const
Text
(
'🎭'
,
style:
TextStyle
(
fontSize:
18
)),
const
SizedBox
(
width:
10
),
Expanded
(
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Panel Discussion Mode'
,
style:
AppTextStyles
.
labelMedium
),
Text
(
'10 investor personas debate before predicting'
,
style:
AppTextStyles
.
caption
,
),
],
),
),
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
8
,
vertical:
2
),
decoration:
BoxDecoration
(
color:
isOn
?
AppColors
.
purple
.
withOpacity
(
0.12
)
:
Colors
.
transparent
,
borderRadius:
BorderRadius
.
circular
(
12
),
),
child:
Text
(
isOn
?
'🎭 ON'
:
'OFF'
,
style:
TextStyle
(
color:
isOn
?
AppColors
.
purpleLight
:
AppColors
.
textTertiary
,
fontSize:
11
,
fontWeight:
FontWeight
.
w700
,
),
),
),
const
SizedBox
(
width:
6
),
Switch
(
value:
isOn
,
onChanged:
onChanged
,
),
],
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/chat/data/datasources/chat_remote_datasource.dart
0 → 100644
View file @
8f5af7a6
import
'package:dio/dio.dart'
;
import
'../../../../core/network/api_endpoints.dart'
;
class
ChatResponseModel
{
final
String
sessionId
;
final
String
reply
;
final
bool
ragUsed
;
const
ChatResponseModel
({
required
this
.
sessionId
,
required
this
.
reply
,
this
.
ragUsed
=
false
,
});
factory
ChatResponseModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
ChatResponseModel
(
sessionId:
(
json
[
'session_id'
]
as
String
?)
??
''
,
reply:
(
json
[
'reply'
]
as
String
?)
??
''
,
ragUsed:
(
json
[
'rag_used'
]
as
bool
?)
??
false
,
);
}
}
class
ChatRemoteDatasource
{
final
Dio
_dio
;
ChatRemoteDatasource
(
this
.
_dio
);
Future
<
ChatResponseModel
>
sendMessage
({
required
String
sessionId
,
required
String
message
,
String
?
knowledgeBaseId
,
})
async
{
final
response
=
await
_dio
.
post
(
ApiEndpoints
.
chat
,
data:
{
'session_id'
:
sessionId
,
'message'
:
message
,
if
(
knowledgeBaseId
!=
null
)
'knowledge_base_id'
:
knowledgeBaseId
,
},
);
return
ChatResponseModel
.
fromJson
(
response
.
data
as
Map
<
String
,
dynamic
>);
}
}
\ No newline at end of file
finsim_flutter/lib/features/chat/data/repositories/chat_repository_impl.dart
0 → 100644
View file @
8f5af7a6
import
'package:dio/dio.dart'
;
import
'../../domain/repositories/chat_repository.dart'
;
import
'../datasources/chat_remote_datasource.dart'
;
class
ChatRepositoryImpl
implements
ChatRepository
{
final
ChatRemoteDatasource
remote
;
ChatRepositoryImpl
(
this
.
remote
);
@override
Future
<
ChatResponseModel
>
sendMessage
({
required
String
sessionId
,
required
String
message
,
String
?
knowledgeBaseId
,
})
async
{
try
{
return
await
remote
.
sendMessage
(
sessionId:
sessionId
,
message:
message
,
knowledgeBaseId:
knowledgeBaseId
,
);
}
on
DioException
catch
(
e
)
{
throw
e
.
error
??
e
;
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/chat/domain/repositories/chat_repository.dart
0 → 100644
View file @
8f5af7a6
import
'../../data/datasources/chat_remote_datasource.dart'
;
abstract
class
ChatRepository
{
Future
<
ChatResponseModel
>
sendMessage
({
required
String
sessionId
,
required
String
message
,
String
?
knowledgeBaseId
,
});
}
\ No newline at end of file
finsim_flutter/lib/features/chat/presentation/bloc/chat_cubit.dart
0 → 100644
View file @
8f5af7a6
import
'dart:async'
;
import
'dart:math'
;
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../../../core/error/exceptions.dart'
;
import
'../../../../core/network/dio_client.dart'
;
import
'../../../knowledge_base/data/datasources/kb_remote_datasource.dart'
;
import
'../../data/datasources/chat_remote_datasource.dart'
;
import
'../../data/repositories/chat_repository_impl.dart'
;
import
'chat_state.dart'
;
class
ChatCubit
extends
Cubit
<
ChatState
>
{
final
ChatRepositoryImpl
_repo
;
Timer
?
_rateLimitTimer
;
ChatCubit
()
:
_repo
=
ChatRepositoryImpl
(
ChatRemoteDatasource
(
DioClient
.
instance
)),
super
(
ChatState
(
sessionId:
'sess_
${Random().nextInt(999999)}
'
,
messages:
[
ChatMessage
(
id:
'welcome'
,
text:
'👋 Hello! I
\'
m your FinSim Investment Advisor. I can help you with:
\n\n
'
'• Real-time market data & prices
\n
'
'• Investment strategy questions
\n
'
'• Practice MCQ scenarios
\n
'
'• Financial concept explanations
\n\n
'
'What would you like to explore?'
,
isUser:
false
,
timestamp:
DateTime
.
now
(),
),
],
));
Future
<
void
>
loadKnowledgeBases
()
async
{
try
{
final
kbs
=
await
KBRemoteDatasource
(
DioClient
.
instance
).
getKnowledgeBases
();
emit
(
state
.
copyWith
(
knowledgeBases:
kbs
));
}
catch
(
_
)
{}
}
void
selectKB
(
String
?
kbId
)
{
emit
(
state
.
copyWith
(
selectedKbId:
()
=>
kbId
));
}
Future
<
void
>
sendMessage
(
String
text
)
async
{
if
(
text
.
trim
().
isEmpty
||
state
.
isTyping
||
state
.
isRateLimited
)
return
;
final
userMsg
=
ChatMessage
(
id:
'user_
${DateTime.now().millisecondsSinceEpoch}
'
,
text:
text
.
trim
(),
isUser:
true
,
timestamp:
DateTime
.
now
(),
);
emit
(
state
.
copyWith
(
messages:
[...
state
.
messages
,
userMsg
],
isTyping:
true
,
));
try
{
final
response
=
await
_repo
.
sendMessage
(
sessionId:
state
.
sessionId
,
message:
text
.
trim
(),
knowledgeBaseId:
state
.
selectedKbId
,
);
final
botMsg
=
ChatMessage
(
id:
'bot_
${DateTime.now().millisecondsSinceEpoch}
'
,
text:
response
.
reply
,
isUser:
false
,
ragUsed:
response
.
ragUsed
,
timestamp:
DateTime
.
now
(),
);
emit
(
state
.
copyWith
(
messages:
[...
state
.
messages
,
botMsg
],
isTyping:
false
,
));
}
on
RateLimitException
catch
(
e
)
{
emit
(
state
.
copyWith
(
isTyping:
false
));
_startRateLimitCountdown
(
e
.
waitSeconds
);
}
catch
(
e
)
{
final
errorMsg
=
ChatMessage
(
id:
'error_
${DateTime.now().millisecondsSinceEpoch}
'
,
text:
'❌ Error:
${e.toString()}
'
,
isUser:
false
,
timestamp:
DateTime
.
now
(),
);
emit
(
state
.
copyWith
(
messages:
[...
state
.
messages
,
errorMsg
],
isTyping:
false
,
));
}
}
void
_startRateLimitCountdown
(
int
seconds
)
{
_rateLimitTimer
?.
cancel
();
emit
(
state
.
copyWith
(
rateLimitSeconds:
()
=>
seconds
));
_rateLimitTimer
=
Timer
.
periodic
(
const
Duration
(
seconds:
1
),
(
timer
)
{
final
remaining
=
(
state
.
rateLimitSeconds
??
0
)
-
1
;
if
(
remaining
<=
0
)
{
timer
.
cancel
();
emit
(
state
.
copyWith
(
rateLimitSeconds:
()
=>
null
));
}
else
{
emit
(
state
.
copyWith
(
rateLimitSeconds:
()
=>
remaining
));
}
});
}
void
clearChat
()
{
emit
(
ChatState
(
sessionId:
'sess_
${Random().nextInt(999999)}
'
,
knowledgeBases:
state
.
knowledgeBases
,
messages:
[
state
.
messages
.
first
],
// keep welcome message
));
}
@override
Future
<
void
>
close
()
{
_rateLimitTimer
?.
cancel
();
return
super
.
close
();
}
}
\ No newline at end of file
finsim_flutter/lib/features/chat/presentation/bloc/chat_state.dart
0 → 100644
View file @
8f5af7a6
import
'package:equatable/equatable.dart'
;
class
ChatMessage
extends
Equatable
{
final
String
id
;
final
String
text
;
final
bool
isUser
;
final
bool
isTyping
;
final
bool
ragUsed
;
final
DateTime
timestamp
;
const
ChatMessage
({
required
this
.
id
,
required
this
.
text
,
this
.
isUser
=
false
,
this
.
isTyping
=
false
,
this
.
ragUsed
=
false
,
required
this
.
timestamp
,
});
@override
List
<
Object
?>
get
props
=>
[
id
];
}
class
ChatState
extends
Equatable
{
final
List
<
ChatMessage
>
messages
;
final
bool
isTyping
;
final
int
?
rateLimitSeconds
;
final
String
?
selectedKbId
;
final
String
sessionId
;
final
List
<
String
>
knowledgeBases
;
const
ChatState
({
this
.
messages
=
const
[],
this
.
isTyping
=
false
,
this
.
rateLimitSeconds
,
this
.
selectedKbId
,
required
this
.
sessionId
,
this
.
knowledgeBases
=
const
[],
});
bool
get
isRateLimited
=>
rateLimitSeconds
!=
null
&&
rateLimitSeconds
!
>
0
;
ChatState
copyWith
({
List
<
ChatMessage
>?
messages
,
bool
?
isTyping
,
int
?
Function
()?
rateLimitSeconds
,
String
?
Function
()?
selectedKbId
,
List
<
String
>?
knowledgeBases
,
})
{
return
ChatState
(
messages:
messages
??
this
.
messages
,
isTyping:
isTyping
??
this
.
isTyping
,
rateLimitSeconds:
rateLimitSeconds
!=
null
?
rateLimitSeconds
()
:
this
.
rateLimitSeconds
,
selectedKbId:
selectedKbId
!=
null
?
selectedKbId
()
:
this
.
selectedKbId
,
sessionId:
sessionId
,
knowledgeBases:
knowledgeBases
??
this
.
knowledgeBases
,
);
}
@override
List
<
Object
?>
get
props
=>
[
messages
,
isTyping
,
rateLimitSeconds
,
selectedKbId
,
knowledgeBases
];
}
\ No newline at end of file
finsim_flutter/lib/features/chat/presentation/screens/chat_screen.dart
0 → 100644
View file @
8f5af7a6
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_radius.dart'
;
import
'../../../../core/widgets/chat_bubble.dart'
;
import
'../../../../core/widgets/rate_limit_timer.dart'
;
import
'../../../../core/widgets/kb_selector.dart'
;
import
'../bloc/chat_cubit.dart'
;
import
'../bloc/chat_state.dart'
;
class
ChatScreen
extends
StatefulWidget
{
const
ChatScreen
({
super
.
key
});
@override
State
<
ChatScreen
>
createState
()
=>
_ChatScreenState
();
}
class
_ChatScreenState
extends
State
<
ChatScreen
>
{
late
final
ChatCubit
_cubit
;
final
_inputCtrl
=
TextEditingController
();
final
_scrollCtrl
=
ScrollController
();
@override
void
initState
()
{
super
.
initState
();
_cubit
=
ChatCubit
()..
loadKnowledgeBases
();
}
@override
void
dispose
()
{
_inputCtrl
.
dispose
();
_scrollCtrl
.
dispose
();
_cubit
.
close
();
super
.
dispose
();
}
void
_send
()
{
final
text
=
_inputCtrl
.
text
.
trim
();
if
(
text
.
isEmpty
)
return
;
_inputCtrl
.
clear
();
_cubit
.
sendMessage
(
text
);
}
void
_scrollToBottom
()
{
WidgetsBinding
.
instance
.
addPostFrameCallback
((
_
)
{
if
(
_scrollCtrl
.
hasClients
)
{
_scrollCtrl
.
animateTo
(
_scrollCtrl
.
position
.
maxScrollExtent
,
duration:
const
Duration
(
milliseconds:
300
),
curve:
Curves
.
easeOut
,
);
}
});
}
@override
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
.
value
(
value:
_cubit
,
child:
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'AI Investment Advisor'
),
actions:
[
IconButton
(
icon:
const
Icon
(
Icons
.
delete_outline
,
size:
20
),
onPressed:
()
=>
_cubit
.
clearChat
(),
tooltip:
'Clear chat'
,
),
],
),
body:
Column
(
children:
[
// ── Messages ──
Expanded
(
child:
BlocConsumer
<
ChatCubit
,
ChatState
>(
listener:
(
_
,
__
)
=>
_scrollToBottom
(),
builder:
(
context
,
state
)
{
return
ListView
.
builder
(
controller:
_scrollCtrl
,
padding:
const
EdgeInsets
.
all
(
12
),
itemCount:
state
.
messages
.
length
+
(
state
.
isTyping
?
1
:
0
)
+
(
state
.
isRateLimited
?
1
:
0
),
itemBuilder:
(
context
,
index
)
{
// Rate limit timer at the end
if
(
state
.
isRateLimited
&&
index
==
state
.
messages
.
length
+
(
state
.
isTyping
?
1
:
0
))
{
return
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
8
),
child:
RateLimitTimer
(
totalSeconds:
state
.
rateLimitSeconds
!,
),
);
}
// Typing indicator
if
(
state
.
isTyping
&&
index
==
state
.
messages
.
length
)
{
return
const
Padding
(
padding:
EdgeInsets
.
only
(
top:
8
),
child:
ChatBubble
(
text:
''
,
isTyping:
true
),
);
}
final
msg
=
state
.
messages
[
index
];
return
Padding
(
padding:
const
EdgeInsets
.
only
(
top:
8
),
child:
ChatBubble
(
text:
msg
.
text
,
isUser:
msg
.
isUser
,
ragUsed:
msg
.
ragUsed
,
),
);
},
);
},
),
),
// ── Input Bar ──
BlocBuilder
<
ChatCubit
,
ChatState
>(
builder:
(
context
,
state
)
{
return
Container
(
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
12
,
vertical:
8
),
decoration:
const
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
(
top:
BorderSide
(
color:
AppColors
.
border
)),
),
child:
SafeArea
(
top:
false
,
child:
Row
(
children:
[
// KB Selector
SizedBox
(
width:
100
,
child:
KBSelector
(
knowledgeBases:
state
.
knowledgeBases
,
selectedKB:
state
.
selectedKbId
,
onChanged:
_cubit
.
selectKB
,
compact:
true
,
),
),
const
SizedBox
(
width:
8
),
// Input
Expanded
(
child:
TextField
(
controller:
_inputCtrl
,
enabled:
!
state
.
isTyping
&&
!
state
.
isRateLimited
,
style:
AppTextStyles
.
bodyMedium
,
decoration:
InputDecoration
(
hintText:
'Ask about markets...'
,
contentPadding:
const
EdgeInsets
.
symmetric
(
horizontal:
14
,
vertical:
10
),
border:
OutlineInputBorder
(
borderRadius:
AppRadius
.
smAll
,
borderSide:
BorderSide
.
none
,
),
filled:
true
,
fillColor:
AppColors
.
bgSecondary
,
),
onSubmitted:
(
_
)
=>
_send
(),
),
),
const
SizedBox
(
width:
8
),
// Send
Material
(
color:
state
.
isTyping
||
state
.
isRateLimited
?
AppColors
.
accent
.
withOpacity
(
0.4
)
:
AppColors
.
accent
,
borderRadius:
AppRadius
.
smAll
,
child:
InkWell
(
onTap:
state
.
isTyping
||
state
.
isRateLimited
?
null
:
_send
,
borderRadius:
AppRadius
.
smAll
,
child:
const
SizedBox
(
width:
44
,
height:
44
,
child:
Icon
(
Icons
.
send_rounded
,
color:
Colors
.
white
,
size:
20
),
),
),
),
],
),
),
);
},
),
],
),
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/generator/data/datasources/generator_remote_datasource.dart
0 → 100644
View file @
8f5af7a6
import
'package:dio/dio.dart'
;
import
'../../../../core/network/api_endpoints.dart'
;
class
GenerateResultModel
{
final
String
status
;
final
String
stockSymbol
;
final
String
?
message
;
final
int
eventsAnalyzed
;
final
int
scenariosGenerated
;
final
int
scenariosSavedToDb
;
const
GenerateResultModel
({
required
this
.
status
,
this
.
stockSymbol
=
''
,
this
.
message
,
this
.
eventsAnalyzed
=
0
,
this
.
scenariosGenerated
=
0
,
this
.
scenariosSavedToDb
=
0
,
});
factory
GenerateResultModel
.
fromJson
(
Map
<
String
,
dynamic
>
json
)
{
return
GenerateResultModel
(
status:
(
json
[
'status'
]
as
String
?)
??
'unknown'
,
stockSymbol:
(
json
[
'stock_symbol'
]
as
String
?)
??
'Mixed Basket'
,
message:
json
[
'message'
]
as
String
?,
eventsAnalyzed:
(
json
[
'events_analyzed'
]
as
num
?)?.
toInt
()
??
0
,
scenariosGenerated:
(
json
[
'scenarios_generated'
]
as
num
?)?.
toInt
()
??
0
,
scenariosSavedToDb:
(
json
[
'scenarios_saved_to_db'
]
as
num
?)?.
toInt
()
??
0
,
);
}
bool
get
hasScenarios
=>
scenariosGenerated
>
0
;
}
class
GeneratorRemoteDatasource
{
final
Dio
_dio
;
GeneratorRemoteDatasource
(
this
.
_dio
);
Future
<
GenerateResultModel
>
generate
({
required
String
stockSymbol
,
required
int
zscoreWindow
,
required
double
zscoreTriggerMin
,
required
double
zscoreTriggerMax
,
String
?
knowledgeBaseId
,
})
async
{
final
response
=
await
_dio
.
post
(
ApiEndpoints
.
generate
,
data:
{
'stock_symbol'
:
stockSymbol
,
'zscore_window'
:
zscoreWindow
,
'zscore_trigger_min'
:
zscoreTriggerMin
,
'zscore_trigger_max'
:
zscoreTriggerMax
,
if
(
knowledgeBaseId
!=
null
)
'knowledge_base_id'
:
knowledgeBaseId
,
},
);
return
GenerateResultModel
.
fromJson
(
response
.
data
as
Map
<
String
,
dynamic
>);
}
}
\ No newline at end of file
finsim_flutter/lib/features/generator/data/repositories/generator_repository_impl.dart
0 → 100644
View file @
8f5af7a6
import
'package:dio/dio.dart'
;
import
'../../domain/repositories/generator_repository.dart'
;
import
'../datasources/generator_remote_datasource.dart'
;
class
GeneratorRepositoryImpl
implements
GeneratorRepository
{
final
GeneratorRemoteDatasource
remote
;
GeneratorRepositoryImpl
(
this
.
remote
);
@override
Future
<
GenerateResultModel
>
generate
({
required
String
stockSymbol
,
required
int
zscoreWindow
,
required
double
zscoreTriggerMin
,
required
double
zscoreTriggerMax
,
String
?
knowledgeBaseId
,
})
async
{
try
{
return
await
remote
.
generate
(
stockSymbol:
stockSymbol
,
zscoreWindow:
zscoreWindow
,
zscoreTriggerMin:
zscoreTriggerMin
,
zscoreTriggerMax:
zscoreTriggerMax
,
knowledgeBaseId:
knowledgeBaseId
,
);
}
on
DioException
catch
(
e
)
{
throw
e
.
error
??
e
;
}
}
}
\ No newline at end of file
finsim_flutter/lib/features/generator/domain/repositories/generator_repository.dart
0 → 100644
View file @
8f5af7a6
import
'../../data/datasources/generator_remote_datasource.dart'
;
abstract
class
GeneratorRepository
{
Future
<
GenerateResultModel
>
generate
({
required
String
stockSymbol
,
required
int
zscoreWindow
,
required
double
zscoreTriggerMin
,
required
double
zscoreTriggerMax
,
String
?
knowledgeBaseId
,
});
}
\ No newline at end of file
finsim_flutter/lib/features/generator/presentation/bloc/generator_cubit.dart
0 → 100644
View file @
8f5af7a6
import
'package:flutter_bloc/flutter_bloc.dart'
;
import
'../../../../core/network/dio_client.dart'
;
import
'../../../knowledge_base/data/datasources/kb_remote_datasource.dart'
;
import
'../../data/datasources/generator_remote_datasource.dart'
;
import
'../../data/repositories/generator_repository_impl.dart'
;
import
'generator_state.dart'
;
class
GeneratorCubit
extends
Cubit
<
GeneratorState
>
{
final
GeneratorRepositoryImpl
_repo
;
List
<
String
>
_kbs
=
[];
GeneratorCubit
()
:
_repo
=
GeneratorRepositoryImpl
(
GeneratorRemoteDatasource
(
DioClient
.
instance
)),
super
(
const
GeneratorInitial
());
Future
<
void
>
loadKnowledgeBases
()
async
{
try
{
_kbs
=
await
KBRemoteDatasource
(
DioClient
.
instance
).
getKnowledgeBases
();
if
(
state
is
GeneratorInitial
)
{
emit
(
GeneratorInitial
(
knowledgeBases:
_kbs
));
}
}
catch
(
_
)
{}
}
List
<
String
>
get
knowledgeBases
=>
_kbs
;
Future
<
void
>
generate
({
required
String
stockSymbol
,
required
int
zscoreWindow
,
required
double
zscoreTriggerMin
,
required
double
zscoreTriggerMax
,
String
?
knowledgeBaseId
,
})
async
{
emit
(
const
GeneratorRunning
());
try
{
final
result
=
await
_repo
.
generate
(
stockSymbol:
stockSymbol
,
zscoreWindow:
zscoreWindow
,
zscoreTriggerMin:
zscoreTriggerMin
,
zscoreTriggerMax:
zscoreTriggerMax
,
knowledgeBaseId:
knowledgeBaseId
,
);
emit
(
GeneratorSuccess
(
result
));
}
catch
(
e
)
{
emit
(
GeneratorError
(
e
.
toString
()));
}
}
void
reset
()
{
emit
(
GeneratorInitial
(
knowledgeBases:
_kbs
));
}
}
\ No newline at end of file
finsim_flutter/lib/features/generator/presentation/bloc/generator_state.dart
0 → 100644
View file @
8f5af7a6
import
'package:equatable/equatable.dart'
;
import
'../../data/datasources/generator_remote_datasource.dart'
;
sealed
class
GeneratorState
extends
Equatable
{
const
GeneratorState
();
@override
List
<
Object
?>
get
props
=>
[];
}
class
GeneratorInitial
extends
GeneratorState
{
final
List
<
String
>
knowledgeBases
;
const
GeneratorInitial
({
this
.
knowledgeBases
=
const
[]});
@override
List
<
Object
?>
get
props
=>
[
knowledgeBases
];
}
class
GeneratorRunning
extends
GeneratorState
{
const
GeneratorRunning
();
}
class
GeneratorSuccess
extends
GeneratorState
{
final
GenerateResultModel
result
;
const
GeneratorSuccess
(
this
.
result
);
@override
List
<
Object
?>
get
props
=>
[
result
.
scenariosGenerated
];
}
class
GeneratorError
extends
GeneratorState
{
final
String
message
;
const
GeneratorError
(
this
.
message
);
@override
List
<
Object
?>
get
props
=>
[
message
];
}
\ No newline at end of file
finsim_flutter/lib/features/generator/presentation/screens/generator_results_screen.dart
0 → 100644
View file @
8f5af7a6
import
'package:flutter/material.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/theme/app_gradients.dart'
;
import
'../../../../core/widgets/fs_button.dart'
;
import
'../../data/datasources/generator_remote_datasource.dart'
;
class
GeneratorResultsScreen
extends
StatelessWidget
{
final
GenerateResultModel
result
;
const
GeneratorResultsScreen
({
super
.
key
,
required
this
.
result
});
@override
Widget
build
(
BuildContext
context
)
{
return
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Generation Complete'
)),
body:
ListView
(
padding:
AppSpacing
.
screenAll
,
children:
[
// Success icon
Center
(
child:
Container
(
width:
80
,
height:
80
,
decoration:
BoxDecoration
(
color:
AppColors
.
successBg
,
shape:
BoxShape
.
circle
,
border:
Border
.
all
(
color:
AppColors
.
success
.
withOpacity
(
0.3
),
width:
2
),
),
child:
const
Center
(
child:
Text
(
'✅'
,
style:
TextStyle
(
fontSize:
36
)),
),
),
),
AppSpacing
.
vLg
,
Center
(
child:
ShaderMask
(
shaderCallback:
(
b
)
=>
AppGradients
.
scoreGood
.
createShader
(
b
),
child:
Text
(
'Scenarios Generated!'
,
style:
AppTextStyles
.
headlineLarge
.
copyWith
(
color:
Colors
.
white
),
),
),
),
AppSpacing
.
vXl
,
// Stats
...
_buildStatRow
(
'📈'
,
'Symbol'
,
result
.
stockSymbol
),
...
_buildStatRow
(
'🔍'
,
'Events Analyzed'
,
result
.
eventsAnalyzed
.
toString
()),
...
_buildStatRow
(
'🧠'
,
'Scenarios Generated'
,
result
.
scenariosGenerated
.
toString
()),
...
_buildStatRow
(
'💾'
,
'Saved to Database'
,
result
.
scenariosSavedToDb
.
toString
()),
if
(
result
.
message
!=
null
)
...[
AppSpacing
.
vMd
,
Container
(
padding:
AppSpacing
.
cardInner
,
decoration:
BoxDecoration
(
color:
AppColors
.
infoBg
,
borderRadius:
AppRadius
.
smAll
,
),
child:
Text
(
result
.
message
!,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
infoLight
)),
),
],
AppSpacing
.
vXl
,
if
(
result
.
hasScenarios
)
...[
FSButton
(
label:
'📚 Browse Scenarios'
,
onPressed:
()
=>
context
.
go
(
'/app/more/scenarios'
),
variant:
FSButtonVariant
.
gradient
,
),
AppSpacing
.
vSm
,
FSButton
(
label:
'📝 Start Quiz'
,
onPressed:
()
=>
context
.
go
(
'/app/quiz'
),
variant:
FSButtonVariant
.
secondary
,
),
],
AppSpacing
.
vSm
,
FSButton
(
label:
'⚡ Generate More'
,
onPressed:
()
=>
context
.
pop
(),
variant:
FSButtonVariant
.
ghost
,
),
],
),
);
}
List
<
Widget
>
_buildStatRow
(
String
icon
,
String
label
,
String
value
)
{
return
[
Container
(
margin:
const
EdgeInsets
.
only
(
bottom:
10
),
padding:
const
EdgeInsets
.
symmetric
(
horizontal:
16
,
vertical:
14
),
decoration:
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
smAll
,
),
child:
Row
(
children:
[
Text
(
icon
,
style:
const
TextStyle
(
fontSize:
20
)),
const
SizedBox
(
width:
12
),
Expanded
(
child:
Text
(
label
,
style:
AppTextStyles
.
bodyMedium
),
),
Text
(
value
,
style:
AppTextStyles
.
titleMedium
.
copyWith
(
color:
AppColors
.
accentLight
),
),
],
),
),
];
}
}
\ No newline at end of file
finsim_flutter/lib/features/generator/presentation/screens/generator_screen.dart
0 → 100644
View file @
8f5af7a6
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_text_field.dart'
;
import
'../../../../core/widgets/kb_selector.dart'
;
import
'../bloc/generator_cubit.dart'
;
import
'../bloc/generator_state.dart'
;
class
GeneratorScreen
extends
StatefulWidget
{
const
GeneratorScreen
({
super
.
key
});
@override
State
<
GeneratorScreen
>
createState
()
=>
_GeneratorScreenState
();
}
class
_GeneratorScreenState
extends
State
<
GeneratorScreen
>
{
late
final
GeneratorCubit
_cubit
;
final
_symbolCtrl
=
TextEditingController
();
final
_windowCtrl
=
TextEditingController
(
text:
'100'
);
final
_zMinCtrl
=
TextEditingController
(
text:
'-2.5'
);
final
_zMaxCtrl
=
TextEditingController
(
text:
'2.5'
);
String
?
_selectedKb
;
@override
void
initState
()
{
super
.
initState
();
_cubit
=
GeneratorCubit
()..
loadKnowledgeBases
();
}
@override
void
dispose
()
{
_symbolCtrl
.
dispose
();
_windowCtrl
.
dispose
();
_zMinCtrl
.
dispose
();
_zMaxCtrl
.
dispose
();
_cubit
.
close
();
super
.
dispose
();
}
void
_generate
()
{
_cubit
.
generate
(
stockSymbol:
_symbolCtrl
.
text
.
trim
(),
zscoreWindow:
int
.
tryParse
(
_windowCtrl
.
text
)
??
100
,
zscoreTriggerMin:
double
.
tryParse
(
_zMinCtrl
.
text
)
??
-
2.5
,
zscoreTriggerMax:
double
.
tryParse
(
_zMaxCtrl
.
text
)
??
2.5
,
knowledgeBaseId:
_selectedKb
,
);
}
@override
Widget
build
(
BuildContext
context
)
{
return
BlocProvider
.
value
(
value:
_cubit
,
child:
Scaffold
(
appBar:
AppBar
(
title:
const
Text
(
'Scenario Generator'
)),
body:
BlocConsumer
<
GeneratorCubit
,
GeneratorState
>(
listener:
(
context
,
state
)
{
if
(
state
is
GeneratorSuccess
)
{
context
.
push
(
'/app/more/generator/results'
,
extra:
state
.
result
);
}
},
builder:
(
context
,
state
)
{
final
isRunning
=
state
is
GeneratorRunning
;
return
ListView
(
padding:
AppSpacing
.
screenAll
,
children:
[
Text
(
'Generate investment scenarios from real market data.'
,
style:
AppTextStyles
.
bodySmall
),
AppSpacing
.
vLg
,
// Config card
Container
(
padding:
AppSpacing
.
cardInnerLarge
,
decoration:
BoxDecoration
(
color:
AppColors
.
panel
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
mdAll
,
),
child:
Column
(
crossAxisAlignment:
CrossAxisAlignment
.
start
,
children:
[
Text
(
'Configuration'
,
style:
AppTextStyles
.
titleMedium
),
AppSpacing
.
vMd
,
FSTextField
(
label:
'Stock Symbol'
,
hint:
'Leave empty for mixed basket (SPY, AAPL, TSLA...)'
,
controller:
_symbolCtrl
,
enabled:
!
isRunning
,
),
AppSpacing
.
vMd
,
Row
(
children:
[
Expanded
(
child:
FSTextField
(
label:
'Z-Score Min'
,
controller:
_zMinCtrl
,
keyboardType:
const
TextInputType
.
numberWithOptions
(
decimal:
true
,
signed:
true
),
enabled:
!
isRunning
,
),
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
FSTextField
(
label:
'Z-Score Max'
,
controller:
_zMaxCtrl
,
keyboardType:
const
TextInputType
.
numberWithOptions
(
decimal:
true
,
signed:
true
),
enabled:
!
isRunning
,
),
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
FSTextField
(
label:
'Window'
,
controller:
_windowCtrl
,
keyboardType:
TextInputType
.
number
,
enabled:
!
isRunning
,
),
),
],
),
AppSpacing
.
vMd
,
Text
(
'STYLE KNOWLEDGE BASE'
,
style:
AppTextStyles
.
labelSmall
.
copyWith
(
color:
AppColors
.
textSecondary
,
letterSpacing:
1
)),
const
SizedBox
(
height:
6
),
KBSelector
(
knowledgeBases:
_cubit
.
knowledgeBases
,
selectedKB:
_selectedKb
,
onChanged:
(
v
)
=>
setState
(()
=>
_selectedKb
=
v
),
),
],
),
),
AppSpacing
.
vLg
,
FSButton
(
label:
'⚡ Generate Scenarios'
,
isLoading:
isRunning
,
onPressed:
isRunning
?
null
:
_generate
,
variant:
FSButtonVariant
.
gradient
,
),
AppSpacing
.
vLg
,
// Status
Container
(
padding:
AppSpacing
.
cardInner
,
decoration:
BoxDecoration
(
color:
AppColors
.
bgSecondary
,
border:
Border
.
all
(
color:
AppColors
.
border
),
borderRadius:
AppRadius
.
smAll
,
),
child:
_buildStatus
(
state
),
),
],
);
},
),
),
);
}
Widget
_buildStatus
(
GeneratorState
state
)
{
if
(
state
is
GeneratorRunning
)
{
return
Row
(
children:
[
SizedBox
(
width:
16
,
height:
16
,
child:
CircularProgressIndicator
(
strokeWidth:
2
,
valueColor:
AlwaysStoppedAnimation
(
AppColors
.
warning
),
),
),
const
SizedBox
(
width:
12
),
Expanded
(
child:
Text
(
'⏳ Fetching market data & running AI analysis...
\n
This may take 15-30 seconds.'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
warningLight
),
),
),
],
);
}
if
(
state
is
GeneratorError
)
{
return
Text
(
'❌
${state.message}
'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
errorLight
),
);
}
return
Text
(
'Ready. Configure parameters and tap Generate.
\n\n
'
'The pipeline will:
\n
'
'1. Fetch 5 years of stock data (yfinance)
\n
'
'2. Calculate rolling Z-Scores (Polars)
\n
'
'3. Detect market anomalies
\n
'
'4. Generate MCQ scenarios (Groq AI)
\n
'
'5. Save to database'
,
style:
AppTextStyles
.
bodySmall
.
copyWith
(
color:
AppColors
.
textTertiary
,
height:
1.6
,
),
);
}
}
\ No newline at end of file
finsim_flutter/lib/features/knowledge_base/data/datasources/kb_remote_datasource.dart
0 → 100644
View file @
8f5af7a6
import
'package:dio/dio.dart'
;
import
'../../../../core/network/api_endpoints.dart'
;
class
KBRemoteDatasource
{
final
Dio
_dio
;
KBRemoteDatasource
(
this
.
_dio
);
Future
<
List
<
String
>>
getKnowledgeBases
()
async
{
final
response
=
await
_dio
.
get
(
ApiEndpoints
.
ragKbs
);
final
data
=
response
.
data
as
Map
<
String
,
dynamic
>;
return
List
<
String
>.
from
(
data
[
'knowledge_bases'
]
??
[]);
}
}
\ No newline at end of file
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