Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
128 changes: 53 additions & 75 deletions lib/features/home/presentation/providers/model_setup_controller.dart
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,9 @@ import 'package:flutter_riverpod/flutter_riverpod.dart';

import 'package:kudlit_ph/features/home/presentation/providers/app_preferences_provider.dart';
import 'package:kudlit_ph/features/scanner/presentation/providers/yolo_model_selection_provider.dart';
import 'package:kudlit_ph/features/translator/data/datasources/local_gemma_datasource.dart';
import 'package:kudlit_ph/features/translator/domain/entities/ai_model_info.dart';
import 'package:kudlit_ph/features/translator/domain/entities/gemma_model_info.dart';
import 'package:kudlit_ph/features/translator/data/datasources/local_gemma_datasource.dart';
import 'package:kudlit_ph/features/translator/presentation/providers/ai_inference_provider.dart';
import 'package:kudlit_ph/features/translator/presentation/providers/ai_inference_state.dart';
import 'package:kudlit_ph/features/translator/presentation/providers/translator_providers.dart';
Expand Down Expand Up @@ -51,38 +51,28 @@ class ModelSetupController extends Notifier<ModelSetupState> {
state = state.copyWith(busy: true, clearError: true);

try {
final VisionModelSetupStatus visionStatus = await ref.read(
final VisionModelSetupStatus visionStatus = await ref.refresh(
visionModelSetupStatusProvider.future,
);
if (!visionStatus.ready) {
state = state.copyWith(busy: false, errorMessage: visionStatus.message);
return;
}

final AppPreferences prefs = await ref.read(
appPreferencesNotifierProvider.future,
final LocalGemmaReadiness gemmaReadiness = await ref.refresh(
localModelReadinessProvider.future,
);
final AiPreference nextPreference = kIsWeb
? AiPreference.cloud
: prefs.aiPreference;

if (!kIsWeb && nextPreference == AiPreference.local) {
final LocalGemmaReadiness gemmaReadiness = await ref.read(
localModelReadinessProvider.future,
if (!gemmaReadiness.installed || !gemmaReadiness.usable) {
state = state.copyWith(
busy: false,
errorMessage: 'Finish the offline downloads before continuing.',
);
if (!gemmaReadiness.installed || !gemmaReadiness.usable) {
state = state.copyWith(
busy: false,
errorMessage:
'Download the Gemma model before continuing with offline AI.',
);
return;
}
return;
}

await ref
.read(appPreferencesNotifierProvider.notifier)
.setAiPreference(nextPreference);
.setAiPreference(AiPreference.local);
await ref
.read(appPreferencesNotifierProvider.notifier)
.markModelsDownloaded();
Expand All @@ -99,31 +89,6 @@ class ModelSetupController extends Notifier<ModelSetupState> {

state = state.copyWith(busy: true, clearError: true);

if (kIsWeb) {
try {
final VisionModelSetupStatus visionStatus = await ref.refresh(
visionModelSetupStatusProvider.future,
);
if (!visionStatus.ready) {
state = state.copyWith(
busy: false,
errorMessage: visionStatus.message,
);
return;
}
await ref
.read(appPreferencesNotifierProvider.notifier)
.setAiPreference(AiPreference.cloud);
await ref
.read(appPreferencesNotifierProvider.notifier)
.markModelsDownloaded();
state = state.copyWith(busy: false, clearError: true);
} catch (e) {
state = state.copyWith(busy: false, errorMessage: e.toString());
}
return;
}

await ref
.read(aiInferenceNotifierProvider.notifier)
.triggerLocalDownload(llmModel);
Expand All @@ -137,40 +102,53 @@ class ModelSetupController extends Notifier<ModelSetupState> {
}

try {
final List<AiModelInfo> visionModels = await ref.read(
availableYoloModelsProvider.future,
);
final AiModelInfo? visionModel = visionModels.isEmpty
? null
: visionModels.first;
if (visionModel == null) {
state = state.copyWith(
busy: false,
errorMessage: 'No scanner model is configured yet.',
if (kIsWeb) {
final VisionModelSetupStatus visionStatus = await ref.refresh(
visionModelSetupStatusProvider.future,
);
return;
}
if (!visionStatus.ready) {
state = state.copyWith(
busy: false,
errorMessage: visionStatus.message,
);
return;
}
} else {
final List<AiModelInfo> visionModels = await ref.read(
availableYoloModelsProvider.future,
);
final AiModelInfo? visionModel = visionModels.isEmpty
? null
: visionModels.first;
if (visionModel == null) {
state = state.copyWith(
busy: false,
errorMessage: 'No scanner model is configured yet.',
);
return;
}

final String yoloUrl = resolveYoloModelUrl(visionModel);
if (yoloUrl.isEmpty) {
state = state.copyWith(
busy: false,
errorMessage:
'The selected scanner model does not have a download URL.',
final String yoloUrl = resolveYoloModelUrl(visionModel);
if (yoloUrl.isEmpty) {
state = state.copyWith(
busy: false,
errorMessage:
'The selected scanner model does not have a download URL.',
);
return;
}

await ref
.read(yoloModelCacheProvider)
.download(visionModel.id, yoloUrl, version: visionModel.version);
ref.invalidate(visionModelSetupStatusProvider);
ref.invalidate(yoloModelPathProvider);
unawaited(
ref
.read(yoloModelPathProvider(YoloModelScope.camera).future)
.catchError((Object _) => ''),
);
return;
}

await ref
.read(yoloModelCacheProvider)
.download(visionModel.id, yoloUrl, version: visionModel.version);
ref.invalidate(visionModelSetupStatusProvider);
ref.invalidate(yoloModelPathProvider);
unawaited(
ref
.read(yoloModelPathProvider(YoloModelScope.camera).future)
.catchError((Object _) => ''),
);
} catch (e) {
debugPrint('[ModelSetup] YOLO download failed: $e');
state = state.copyWith(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -278,7 +278,7 @@ class TranslateTextController extends Notifier<TranslateTextState> {
ref.read(appPreferencesNotifierProvider).value?.aiPreference ??
AiPreference.cloud;

if (kIsWeb || mode == AiPreference.cloud) {
if (mode == AiPreference.cloud) {
await _streamResponse(
stream: ref
.read(cloudGemmaDatasourceProvider)
Expand Down
26 changes: 11 additions & 15 deletions lib/features/home/presentation/screens/model_setup_screen.dart
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ class _ModelSetupScreenState extends ConsumerState<ModelSetupScreen> {
String _friendlyModelSetupError(String rawMessage) {
final String message = rawMessage.trim();
if (message.isEmpty) {
return 'Local AI setup is paused. You can use cloud AI and retry later.';
return 'Offline setup is paused. You can stay on internet mode and try again later.';
}

final String lower = message.toLowerCase();
Expand All @@ -73,7 +73,7 @@ class _ModelSetupScreenState extends ConsumerState<ModelSetupScreen> {
}

if (lower.contains('no ai models configured')) {
return 'The local model list is not available yet. You can use cloud AI for now.';
return 'Offline downloads are not available right now. You can stay on internet mode for now.';
}

final bool looksTechnical =
Expand All @@ -84,7 +84,7 @@ class _ModelSetupScreenState extends ConsumerState<ModelSetupScreen> {
lower.contains('uri=') ||
lower.contains('https://');
if (looksTechnical) {
return 'Local AI setup is paused. You can use cloud AI and retry later.';
return 'Offline setup is paused. You can stay on internet mode and try again later.';
}

return message;
Expand Down Expand Up @@ -368,7 +368,7 @@ class _SetupHeadline extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Power up Kudlit',
'Get ready to use Kudlit',
style: TextStyle(
color: KudlitColors.blue900,
fontSize: compact ? 24 : 28,
Expand All @@ -378,10 +378,8 @@ class _SetupHeadline extends StatelessWidget {
SizedBox(height: compact ? 8 : 10),
Text(
kIsWeb
? 'Kudlit checks the browser scanner model here before you start. '
'Gemma stays in cloud mode on web.'
: 'Kudlit uses on-device AI models for Baybayin recognition '
'and translation — no internet needed once downloaded.',
? 'Set up the downloads Kudlit needs before you start.'
: 'Download these once so key features can keep working even without internet.',
style: TextStyle(
color: KudlitColors.grey300,
fontSize: compact ? 13 : 15,
Expand Down Expand Up @@ -422,10 +420,8 @@ class _DownloadNotice extends StatelessWidget {
Expanded(
child: Text(
kIsWeb
? 'On web, the scanner model loads from Supabase Storage in '
'the browser. Gemma continues to use cloud AI.'
: 'AI model files are typically 1–5 GB. '
'Wi-Fi recommended. Download continues in the background.',
? 'The first setup happens in this browser and may take a while.'
: 'These downloads can be large. Wi-Fi is recommended.',
style: TextStyle(
color: KudlitColors.blue800,
fontSize: compact ? 10 : 11,
Expand Down Expand Up @@ -454,7 +450,7 @@ class _SetupActions extends StatelessWidget {

@override
Widget build(BuildContext context) {
final String label = busy ? 'Checking models' : 'Continue';
final String label = busy ? 'Checking setup' : 'Continue';

return Column(
crossAxisAlignment: CrossAxisAlignment.stretch,
Expand All @@ -463,7 +459,7 @@ class _SetupActions extends StatelessWidget {
button: true,
enabled: !busy,
label: label,
hint: 'Checks whether the required models are ready, then continues.',
hint: 'Checks whether everything is ready, then continues.',
child: FilledButton.icon(
onPressed: busy ? null : onContinue,
icon: busy
Expand Down Expand Up @@ -498,7 +494,7 @@ class _SetupActions extends StatelessWidget {
disabledForegroundColor: KudlitColors.grey500,
),
child: const Text(
'Not now - use cloud AI',
'Not now - stay on internet mode',
style: TextStyle(fontSize: 14, fontWeight: FontWeight.w600),
),
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,9 @@ class ButtyModelModeSelector extends ConsumerWidget {
final String helperText = switch (offlineStatusAsync) {
AsyncData(:final ButtyOfflineStatus value) =>
value.detail ?? 'Offline status unknown.',
AsyncError() => 'Offline check failed. Use cloud or retry later.',
_ => 'Checking whether the offline Gemma model is installed…',
AsyncError() =>
'Offline check failed. Stay on internet mode or try again later.',
_ => 'Checking whether offline replies are ready…',
};

final Widget pills = Container(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,6 @@ import 'settings_divider.dart';
import 'settings_section_label.dart';
import 'vision_download_tile.dart';

/// Settings section that shows download status and controls for both
/// on-device AI models:
///
/// - **Gemma 4 E2B** — LLM used by Butty (offline chat / feedback).
/// - **KudVis-1-Turbo** — YOLO TFLite used by the OCR / camera scanner.
///
class AiModelsSection extends StatelessWidget {
const AiModelsSection({super.key});

Expand All @@ -20,7 +14,7 @@ class AiModelsSection extends StatelessWidget {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
const SettingsSectionLabel(text: 'AI models'),
const SettingsSectionLabel(text: 'Offline downloads'),
SettingsCard(
children: <Widget>[
const _AiModelsIntro(),
Expand Down Expand Up @@ -65,7 +59,7 @@ class _AiModelsIntro extends StatelessWidget {
crossAxisAlignment: CrossAxisAlignment.start,
children: <Widget>[
Text(
'Local AI setup',
'Use Kudlit offline',
style: TextStyle(
fontSize: 14,
fontWeight: FontWeight.w800,
Expand All @@ -74,7 +68,7 @@ class _AiModelsIntro extends StatelessWidget {
),
const SizedBox(height: 3),
Text(
'Install once for offline Butty and scanner setup.',
'Set these up once to keep replies and camera reading available without internet.',
style: TextStyle(
fontSize: 12,
height: 1.3,
Expand Down
Loading
Loading