@@ -2,9 +2,12 @@ import 'dart:async';
22
33import 'package:flutter/material.dart' ;
44
5+ import 'package:flutter_gemma/flutter_gemma.dart' ;
6+
57import '../main.dart' ;
68import '../models/models.dart' ;
79import '../services/chat_context_builder.dart' ;
10+ import '../services/citation_extractor.dart' ;
811import '../widgets/chat_message_bubble.dart' ;
912
1013/// The AI Chat screen — transforms from download/load prompt into a
@@ -28,7 +31,7 @@ class _ChatScreenState extends State<ChatScreen> {
2831 final _messages = < ChatMessage > [];
2932 final _textController = TextEditingController ();
3033 final _scrollController = ScrollController ();
31- final _contextBuilder = ChatContextBuilder () ;
34+ late final ChatContextBuilder _contextBuilder ;
3235
3336 StreamSubscription <ModelInfo >? _statusSubscription;
3437 StreamSubscription <String >? _generationSubscription;
@@ -37,6 +40,7 @@ class _ChatScreenState extends State<ChatScreen> {
3740 @override
3841 void initState () {
3942 super .initState ();
43+ _contextBuilder = ChatContextBuilder (vectorStore: vectorStoreService);
4044 _statusSubscription = gemmaService.modelStatusStream.listen ((info) {
4145 if (mounted) {
4246 setState (() => _modelInfo = info);
@@ -55,7 +59,7 @@ class _ChatScreenState extends State<ChatScreen> {
5559
5660 // ─── Chat Logic ────────────────────────────────────────────────────
5761
58- void _sendMessage ([String ? overrideText]) {
62+ Future < void > _sendMessage ([String ? overrideText]) async {
5963 final text = (overrideText ?? _textController.text).trim ();
6064 if (text.isEmpty || ! mounted) return ;
6165 if (gemmaService.isGenerating) return ;
@@ -69,8 +73,13 @@ class _ChatScreenState extends State<ChatScreen> {
6973 });
7074 _scrollToBottom ();
7175
72- // Build context from lab data
73- final context = _contextBuilder.buildQueryContext (text);
76+ // Build context from lab data (async — may use semantic search)
77+ final context = await _contextBuilder.buildQueryContext (text);
78+ final retrievedDocs = List <RetrievalResult >.from (
79+ _contextBuilder.lastRetrievedDocs,
80+ );
81+
82+ if (! mounted) return ;
7483
7584 // Add placeholder assistant message (streaming)
7685 final assistantMsg = ChatMessage .assistantStreaming ();
@@ -101,13 +110,20 @@ class _ChatScreenState extends State<ChatScreen> {
101110 },
102111 onDone: () {
103112 if (mounted) {
113+ var finalContent = tokenBuffer.toString ();
114+ // Append citation footer if we have retrieved docs
115+ if (finalContent.isNotEmpty && retrievedDocs.isNotEmpty) {
116+ finalContent = CitationExtractor .appendSourceFooter (
117+ finalContent,
118+ retrievedDocs,
119+ );
120+ }
104121 setState (() {
105122 _messages[assistantIndex] = _messages[assistantIndex].copyWith (
106123 isStreaming: false ,
107- // If we got no tokens, show a fallback message
108- content: tokenBuffer.isEmpty
124+ content: finalContent.isEmpty
109125 ? 'I wasn\' t able to generate a response. Please try again.'
110- : null ,
126+ : finalContent ,
111127 );
112128 });
113129 _scrollToBottom ();
@@ -261,6 +277,7 @@ class _ChatScreenState extends State<ChatScreen> {
261277 textController: _textController,
262278 scrollController: _scrollController,
263279 isGenerating: gemmaService.isGenerating,
280+ isSemanticSearchActive: _contextBuilder.isSemanticSearchActive,
264281 onSend: _sendMessage,
265282 onStop: _stopGeneration,
266283 onSuggestionTap: (text) {
@@ -552,6 +569,7 @@ class _ChatView extends StatelessWidget {
552569 final TextEditingController textController;
553570 final ScrollController scrollController;
554571 final bool isGenerating;
572+ final bool isSemanticSearchActive;
555573 final VoidCallback onSend;
556574 final VoidCallback onStop;
557575 final ValueChanged <String >? onSuggestionTap;
@@ -561,6 +579,7 @@ class _ChatView extends StatelessWidget {
561579 required this .textController,
562580 required this .scrollController,
563581 required this .isGenerating,
582+ this .isSemanticSearchActive = false ,
564583 required this .onSend,
565584 required this .onStop,
566585 this .onSuggestionTap,
@@ -572,6 +591,39 @@ class _ChatView extends StatelessWidget {
572591
573592 return Column (
574593 children: [
594+ // Search mode indicator
595+ Container (
596+ width: double .infinity,
597+ padding: const EdgeInsets .symmetric (horizontal: 16 , vertical: 6 ),
598+ color: theme.colorScheme.surfaceContainerHighest.withValues (
599+ alpha: 0.5 ,
600+ ),
601+ child: Row (
602+ mainAxisAlignment: MainAxisAlignment .center,
603+ mainAxisSize: MainAxisSize .min,
604+ children: [
605+ Icon (
606+ isSemanticSearchActive ? Icons .auto_awesome : Icons .text_fields,
607+ size: 14 ,
608+ color: isSemanticSearchActive
609+ ? theme.colorScheme.primary
610+ : theme.colorScheme.onSurface.withValues (alpha: 0.5 ),
611+ ),
612+ const SizedBox (width: 4 ),
613+ Text (
614+ isSemanticSearchActive
615+ ? 'Semantic search active'
616+ : 'Keyword search (basic)' ,
617+ style: theme.textTheme.labelSmall? .copyWith (
618+ color: isSemanticSearchActive
619+ ? theme.colorScheme.primary
620+ : theme.colorScheme.onSurface.withValues (alpha: 0.5 ),
621+ ),
622+ ),
623+ ],
624+ ),
625+ ),
626+
575627 // Message list
576628 Expanded (
577629 child: messages.isEmpty
0 commit comments