Skip to content

Facelift changes and model loading UX fix#1

Merged
priyavratuniyal merged 21 commits intomainfrom
feature/koshika-facelift
Mar 26, 2026
Merged

Facelift changes and model loading UX fix#1
priyavratuniyal merged 21 commits intomainfrom
feature/koshika-facelift

Conversation

@priyavratuniyal
Copy link
Copy Markdown
Owner

@priyavratuniyal priyavratuniyal commented Mar 24, 2026

  • new theme, new elements
  • model if downloaded, would load on app startup

Summary by CodeRabbit

  • New Features

    • Haptic feedback (light, selection, heavy)
    • Shimmer loading placeholders
    • New reusable UI components: card, icon container, status badge, trend painter
  • Visual & Design Updates

    • Unified design system: colors, typography, spacing, radii, elevations
    • Glassmorphic bottom navigation and chat input
    • Gradient pill CTAs, refreshed badges, charts, and list/item styles
  • Improvements

    • Switched to a single light theme
    • Background model loading on startup
    • Clearer user-facing error messages

Required for the Manrope + Inter dual-font system specified
in the Koshika design spec.
Add success/warning/info semantic colors, textPrimary/Secondary/Muted
aliases, surfaceContainerHighest, onErrorContainer, onTertiaryContainer,
and 10 health category colors with a categoryColor() lookup helper.
…and elevation tokens

Adds KoshikaTypography (Manrope headlines + Inter body with 15 Material
styles and 6 custom health styles), KoshikaSpacing (8dp grid), KoshikaRadius
(sm through pill), KoshikaElevation (tinted BoxShadow presets), and pre-built
card decorations and button styles.
…it scheme, wire fonts

Switch to light-only theme (ThemeMode.light), replace the seed-generated
ColorScheme with an explicit one mapped to AppColors tokens, wire
Manrope + Inter via KoshikaTypography.textTheme, set card elevation to 0,
and add pill/outlined button theme defaults.
KoshikaCard provides the standard card container with 24px radius,
asymmetric editorial padding, and no-border design. StatusBadge maps
BiomarkerFlag to semantic pill badges with icons. IconContainer wraps
icons in colored rounded-rect backgrounds for list items.
Adds ShimmerScope (shared AnimationController), ShimmerBox, ShimmerLine,
and ShimmerCircle for skeleton loading states. Uses a horizontal gradient
sweep on a 1500ms linear repeat cycle.
SimpleTrendLinePainter is a lightweight CustomPainter for compact sparklines
in dashboard category cards. Haptics wraps HapticFeedback with light,
selection, and heavy static methods.
Replace hardcoded colors, typography, spacing, and radii with
KoshikaTypography, KoshikaSpacing, KoshikaRadius, and KoshikaDecorations.
Hero card uses gradient decoration and heroMetric typography. Category
trend cards now use health category colors. Insights card uses
tertiaryContainer (insight) decoration. Section headers use sectionHeader
style. Border.all removed from insights card (no-line rule).
Replace hardcoded Colors.green/amber/orange/red/grey with AppColors
semantic tokens. Remove Dividers and Border.all (no-line rule). Use
KoshikaTypography, KoshikaDecorations, KoshikaRadius, and KoshikaSpacing.
Replace CircularProgressIndicator with shimmer skeleton loading. Use
heroMetric typography for the main biomarker value and StatusBadge pill
for flag display. Add LATEST RESULT and REFERENCE RANGE metric labels.
History list uses alternating row backgrounds instead of Divider
separators. All spacing, radii, and colors use design system tokens.
…design system

Replace hardcoded Colors.green/red/orange with AppColors.success/error/warning.
Use AppColors.secondary for chart line color. Replace Card with Container
using KoshikaDecorations.card. Remove dead isDark branch in gauge painter.
Zone labels use ALL-CAPS with design system-aligned styling.
…ip bubbles

AI bubbles get 4px left accent strip and KOSHIKA INTELLIGENCE label.
Chat input bar uses BackdropFilter for glassmorphic effect with privacy
label. All state views (download, loading, ready, error, empty) migrated
to design system tokens. Removed borders per no-line rule.
Reports list uses KoshikaCard containers instead of Card widgets.
Report detail uses metricLabel for category headers, removes Dividers.
Settings replaces ListTiles with custom SettingsRow using IconContainer,
model tiles use KoshikaDecorations.card with switch expressions, and
Dividers replaced with spacing per no-line rule.
Onboarding pages use design system typography and spacing. Action
button gets gradient pill decoration with medium elevation shadow.
Page indicator dots use AppColors tokens. Icon circles use primary
at 15% opacity for a lighter, editorial feel.
…cons

Custom bottom nav replaces NavigationBar with glassmorphic backdrop
blur (white @ 85%), animated icon transitions between outlined and
filled states, and primaryContainer pill indicator behind selected
item. Uses extendBody for content-behind-nav effect.
GemmaService and EmbeddingService now call installModel/installEmbedder
in initialize() when model files already exist on disk. This registers
the model as "active" with the flutter_gemma SDK so that getActiveModel/
getActiveEmbedder succeeds without requiring the user to re-download.
Previously, the services only checked isModelInstalled but never called
install(), causing "No active model set" errors on app re-open.

Chat screen now auto-loads the model on entry when status is ready,
removing the manual "Load Model" tap requirement on every launch.
Session restore on initState removed — chat always starts fresh.

Raw exception strings replaced with user-friendly messages across
SplashScreen and SettingsScreen; raw errors go to debugPrint only.
Remove extendBody from HomeScreen — glassmorphic nav bar was obscuring
scrollable content on screens that need full height.

Set centerTitle: false in AppBarTheme so titles align left per the
editorial design spec.

Replace Row with Wrap in dashboard hero card stats so the three stat
items wrap to a second line on narrow screens instead of overflowing.
After services initialize, kick off loadModel() for both GemmaService
and EmbeddingService if their status is ready (i.e. already downloaded).
Calls are unawaited so navigation proceeds immediately while models warm
up in the background, eliminating the manual Load tap requirement.
@priyavratuniyal priyavratuniyal self-assigned this Mar 24, 2026
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Mar 26, 2026

📝 Walkthrough

Walkthrough

Adds a Koshika design system, migrates many screens/widgets to those tokens, switches the app to a fixed light theme with a glassmorphic bottom nav, adds shimmer/status/haptic utilities, and performs non-blocking background model re-load/re-install attempts during service initialization.

Changes

Cohort / File(s) Summary
Design system & colors
lib/theme/koshika_design_system.dart, lib/theme/app_colors.dart
New design-system tokens (typography via google_fonts, spacing, radii, elevations, decorations, button styles). AppColors extended with semantic/status/category colors and categoryColor() lookup.
App shell & navigation
lib/main.dart
Replaced adaptive light/dark themes with a single light theme; switched NavigationBar to a custom blurred/transparent bottom bar using ClipRect+BackdropFilter and animated _NavItem widgets.
Startup & model loading
lib/screens/splash_screen.dart, lib/services/gemma_service.dart, lib/services/embedding_service.dart
On init, services attempt non-blocking loadModel() when model status is ready; Gemma/Embedding attempt SDK re-installation/re-registration in try/catch and emit debugPrint on failures.
New widgets & utilities
lib/widgets/shimmer_loading.dart, lib/widgets/status_badge.dart, lib/widgets/icon_container.dart, lib/widgets/koshika_card.dart, lib/widgets/trend_line_painter.dart
Added shimmer placeholders, StatusBadge, IconContainer, reusable KoshikaCard, and SimpleTrendLinePainter sparkline painter.
Haptics utility
lib/services/haptic_feedback_service.dart
New Haptics utility class with light(), selection(), and heavy() static wrappers around HapticFeedback.
Screens — UI refactors
lib/screens/biomarker_detail_screen.dart, lib/screens/chat_screen.dart, lib/screens/dashboard_screen.dart, lib/screens/onboarding_screen.dart, lib/screens/report_detail_screen.dart, lib/screens/reports_screen.dart, lib/screens/settings_screen.dart
Replaced hardcoded spacing/typography/colors with Koshika tokens and AppColors; chat input and bottom nav use glassmorphic blur; list/cards replaced with KoshikaDecorations containers; sorting tweak in dashboard out-of-range list; settings refactor adds _SettingsRow.
Widgets — styling & behavior
lib/widgets/biomarker_trend_chart.dart, lib/widgets/chat_message_bubble.dart, lib/widgets/dashboard_summary_card.dart, lib/widgets/flag_badge.dart, lib/widgets/reference_range_gauge.dart, lib/widgets/..
Updated many widgets to use AppColors/Koshika tokens, adjusted flag color mappings, simplified painters/badges, replaced Cards with decorated Containers, and removed theme-dependent branching in some painters.
Dependency
pubspec.yaml
Added google_fonts: ^6.2.1 for typography in the design system.

Sequence Diagram(s)

mermaid
sequenceDiagram
participant Splash as SplashScreen
participant Gemma as GemmaService
participant Embed as EmbeddingService
participant SDK as FlutterGemma (SDK)
participant Nav as Navigator
Splash->>Gemma: initialize()
Splash->>Embed: initialize()
Gemma->>SDK: checkInstalledModel()
alt model installed
Gemma->>SDK: installModel().fromNetwork(...).install() (try/catch)
Gemma-->>Gemma: debugPrint on failure
end
Embed->>SDK: installEmbedder().fromNetwork(...).install() (try/catch)
Gemma-->>Splash: initialized
Embed-->>Splash: initialized
Splash->>Gemma: if currentModel.status == ready -> loadModel() (unawaited)
Splash->>Embed: if currentModel.status == ready -> loadModel() (unawaited)
Splash->>Nav: proceed with navigation (does not await model load)

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

"I hop through tokens, fonts, and light,
A shimmer here, a badge so bright,
Glass nav gleams while models load unseen,
Haptics tap — the UI feels keen,
I rabbit-hop — design made tight." 🐇✨

🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately captures both major aspects of the changeset: the comprehensive UI redesign with the new Koshika design system ("Facelift changes") and the model auto-loading improvement ("model loading UX fix").
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feature/koshika-facelift

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

- Wrap single-line if bodies in curly braces per lint rules
- Resolves curly_braces_in_flow_control_structures warnings in dashboard_screen.dart and app_colors.dart
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 6

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/main.dart (1)

27-31: ⚠️ Potential issue | 🟠 Major

Remove duplicate FlutterGemma.initialize() call in GemmaService.initialize().

FlutterGemma.initialize(maxDownloadRetries: 5) is called twice:

  1. In main() at line 29 (awaited, before runApp())
  2. In gemma_service.dart line 71 (not awaited, inside initialize())

The first initialization in main() is sufficient and completes before the app starts. The second call in gemmaService.initialize() is redundant. Remove the FlutterGemma.initialize() call from gemmaService.initialize() to avoid double initialization of the SDK.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/main.dart` around lines 27 - 31, Remove the redundant SDK initialization:
delete the FlutterGemma.initialize(...) call from GemmaService.initialize() so
the SDK is only initialized once in main() where Future<void> main() awaits
FlutterGemma.initialize(maxDownloadRetries: 5) before runApp; keep the
initialization in main() and ensure GemmaService.initialize() no longer calls or
attempts to re-initialize FlutterGemma.initialize().
🧹 Nitpick comments (7)
lib/widgets/dashboard_summary_card.dart (2)

50-65: Consider using design tokens instead of Colors.white.

Lines 54 and 63 use Colors.white directly, which is inconsistent with the design-system-driven approach used elsewhere in this PR. Consider using an appropriate AppColors token (e.g., AppColors.onPrimaryContainer or a dedicated surface color) to maintain consistency and ease future theme adjustments.

♻️ Suggested token usage
               Text(
                 'Health Overview',
                 style: theme.textTheme.titleMedium?.copyWith(
                   fontWeight: FontWeight.bold,
-                  color: Colors.white,
+                  color: AppColors.onPrimaryContainer,
                 ),
               ),
             ],
           ),
           const SizedBox(height: KoshikaSpacing.base),
           Text(
             '$totalTracked Biomarkers Tracked',
             style: KoshikaTypography.sectionHeader.copyWith(
-              color: Colors.white,
+              color: AppColors.onPrimaryContainer,
             ),
           ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/widgets/dashboard_summary_card.dart` around lines 50 - 65, Replace the
hardcoded Colors.white usages in the Text styles with the appropriate design
token from your design system (e.g., AppColors.onPrimaryContainer) to keep
theming consistent: update the color passed into
theme.textTheme.titleMedium?.copyWith(...) and
KoshikaTypography.sectionHeader.copyWith(...) to use the AppColors token, and
add the necessary import for AppColors at the top of
dashboard_summary_card.dart; ensure both Text style usages (the "Health
Overview" title and the '$totalTracked Biomarkers Tracked' subtitle) use the
same chosen token.

161-166: Consider using design-system typography for the label.

Line 166 uses an inline TextStyle(fontSize: 12, color: color) while the count on line 163 uses KoshikaTypography.statusText. For consistency, consider using a design-system typography token for the label as well.

♻️ Suggested refactor
           Text(
             '$count',
             style: KoshikaTypography.statusText.copyWith(color: color),
           ),
           const SizedBox(width: KoshikaSpacing.xs),
-          Text(label, style: TextStyle(fontSize: 12, color: color)),
+          Text(label, style: KoshikaTypography.statusText.copyWith(color: color)),
         ],
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/widgets/dashboard_summary_card.dart` around lines 161 - 166, The label
Text currently uses an inline TextStyle(fontSize: 12, color: color); replace
that inline style with the design-system typography token from KoshikaTypography
to match the count's usage (e.g., use an appropriate token such as
KoshikaTypography.label, KoshikaTypography.bodySmall, or the project’s
equivalent) and apply the color via copyWith(color: color); update the Text
widget that renders label so it uses KoshikaTypography.<token>.copyWith(color:
color) instead of the inline TextStyle.
lib/screens/onboarding_screen.dart (1)

161-168: Hardcoded color in gradient breaks design system consistency.

The gradient uses a hardcoded teal color Color(0xFF0D9488) (Line 164) alongside AppColors.primary. This circumvents the design system that the PR establishes. Consider adding this color to AppColors (e.g., AppColors.primaryGradientEnd or AppColors.teal) for consistency and easier future theming.

🎨 Extract gradient color to design system

In lib/theme/app_colors.dart, add:

static const Color primaryGradientEnd = Color(0xFF0D9488);

Then update the gradient:

                   child: DecoratedBox(
                     decoration: BoxDecoration(
                       gradient: const LinearGradient(
-                        colors: [AppColors.primary, Color(0xFF0D9488)],
+                        colors: [AppColors.primary, AppColors.primaryGradientEnd],
                       ),
                       borderRadius: KoshikaRadius.pill,
                       boxShadow: KoshikaElevation.medium,
                     ),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/screens/onboarding_screen.dart` around lines 161 - 168, The gradient in
the DecoratedBox currently uses a hardcoded Color(0xFF0D9488) next to
AppColors.primary; add a named constant to the design system (e.g., in AppColors
add static const Color primaryGradientEnd = Color(0xFF0D9488)) and update the
gradient colors list in onboarding_screen.dart to use
AppColors.primaryGradientEnd instead of the hardcoded value so the gradient
follows the AppColors theme (look for DecoratedBox and KoshikaRadius.pill to
locate the usage).
lib/widgets/trend_line_painter.dart (1)

79-83: List reference comparison in shouldRepaint may not behave as expected.

The comparison values != oldDelegate.values compares list references, not contents. This means:

  • If a new list with identical values is passed, it will trigger a repaint (slightly inefficient but safe)
  • If the same list instance is mutated in place, changes won't be detected (unlikely usage pattern but risky)

For a lightweight sparkline painter, this is acceptable, but worth noting for maintainability.

🔧 Optional: Use deep equality for more precise repaint control
+import 'package:flutter/foundation.dart';
+
 `@override`
 bool shouldRepaint(SimpleTrendLinePainter oldDelegate) =>
-    values != oldDelegate.values ||
+    !listEquals(values, oldDelegate.values) ||
     lineColor != oldDelegate.lineColor ||
     lineWidth != oldDelegate.lineWidth ||
     showFill != oldDelegate.showFill;
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/widgets/trend_line_painter.dart` around lines 79 - 83, The shouldRepaint
implementation currently compares list references (values != oldDelegate.values)
which misses in-place mutations and overtriggers for equivalent new lists;
update SimpleTrendLinePainter.shouldRepaint to compare list contents instead
(e.g., use Flutter's listEquals from foundation or manually compare lengths and
elements) and keep the existing comparisons for lineColor, lineWidth, and
showFill; ensure you reference values, shouldRepaint, and SimpleTrendLinePainter
when making the change.
lib/screens/reports_screen.dart (1)

381-390: Silent exception swallowing during file deletion.

The catch block at Lines 383-388 silently ignores all exceptions when deleting the PDF file. While this prevents crashes, it could hide issues like permission problems or disk errors that might be worth logging.

🔧 Consider logging the error for debugging
                     onDismissed: (_) {
                       objectbox.deleteReport(report.id);
                       try {
                         final file = File(report.pdfPath);
                         if (file.existsSync()) {
                           file.deleteSync();
                         }
-                      } catch (_) {}
+                      } catch (e) {
+                        debugPrint('Failed to delete PDF file: $e');
+                      }
                       setState(() {});
                     },
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/screens/reports_screen.dart` around lines 381 - 390, The onDismissed
handler currently swallows all exceptions during PDF deletion; update the catch
to capture the exception (and optionally the stack) and log it instead of
ignoring it so permission/disk errors are visible—specifically modify the
try/catch around File(report.pdfPath).existsSync()/deleteSync() in the
onDismissed block (which also calls objectbox.deleteReport(report.id) and
setState()) to log the error via your app logger or debugPrint including
contextual info (report.id and report.pdfPath) and consider showing a
non-blocking user-facing message if deletion fails.
lib/screens/settings_screen.dart (1)

454-582: Extract the shared model-status card before these drift.

_ModelStatusTile and _EmbeddingModelTile now duplicate the same layout, status mapping, progress/error rendering, and action row. A single configurable widget would keep future model-state/UI changes from diverging.

Also applies to: 605-744

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/screens/settings_screen.dart` around lines 454 - 582, The two widgets
_ModelStatusTile and _EmbeddingModelTile duplicate the same UI and status logic
(layout, status icon/label/color getters, progress/error rendering, and action
row); refactor by extracting a single reusable widget (e.g., ModelStatusCard)
that accepts a ModelInfo (or the existing modelInfo), callbacks
(onDownload/onLoad/onUnload) and optional config (labels/icons if needed), then
replace _ModelStatusTile and _EmbeddingModelTile with thin wrappers that build
ModelStatusCard; move the shared getters (_statusIcon, _statusLabel,
_statusColor), progress/error rendering and action-row rendering into
ModelStatusCard so changes apply in one place while retaining modelInfo-based
behavior.
lib/theme/koshika_design_system.dart (1)

162-190: Nit: Comment says "8dp base grid" but values include non-8dp multiples.

Values like xs=4, md=12, lg=20, contentPadding=20 don't follow an 8dp grid. Consider updating the comment to "4dp base grid" or "4dp/8dp hybrid grid" for accuracy.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/theme/koshika_design_system.dart` around lines 162 - 190, The comment
"8dp base grid" is inaccurate since values in KoshikaSpacing (e.g., xs, sm, md,
lg, xl, xxl, xxxl and screen/content paddings like contentPadding and
cardPaddingAsymmetric) use 4dp increments and non-8 multiples; update the header
comment to reflect the actual spacing system (for example "4dp base grid" or
"4dp/8dp hybrid grid") so it accurately documents KoshikaSpacing and its
constants.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/screens/biomarker_detail_screen.dart`:
- Around line 75-117: The skeleton inside ShimmerScope is a tall Column that can
overflow; make it scrollable by wrapping the Column (the child of Padding) in a
scrollable widget such as SingleChildScrollView (or ListView) so the
ShimmerScope + Padding remain but the inner content can scroll; update the
widget tree around the Column (affecting ShimmerScope, the Padding's child, and
all ShimmerBox/ShimmerLine widgets) to use a SingleChildScrollView (optionally
with physics: AlwaysScrollableScrollPhysics and a Column inside) so the loading
skeleton no longer causes RenderFlex overflow on small/landscape viewports.

In `@lib/screens/settings_screen.dart`:
- Around line 387-430: The interactive row currently uses GestureDetector;
replace it with a Material widget wrapping an InkWell to enable keyboard focus,
hover and ripple feedback: wrap the Container's content in Material (color:
AppColors.surfaceContainerLowest, borderRadius: KoshikaRadius.lg) and move the
onTap to an InkWell (set borderRadius to KoshikaRadius.lg and keep
splash/highlight behavior), keeping padding/margin and child Row intact; ensure
any null checks for trailing/subtitle remain and that semantics/focus are
preserved by providing InkWell's onTap (and optionally focusNode/hoverColor if
needed).

In `@lib/widgets/biomarker_trend_chart.dart`:
- Around line 127-131: The dot color sets dotColor based on (result.flag ==
BiomarkerFlag.normal || result.flag == BiomarkerFlag.unknown) so the tooltip's
flag color logic must match that rule; update the tooltip color branch (the code
computing the tooltip flag color, e.g., tooltipFlagColor or similar) to treat
BiomarkerFlag.unknown the same as BiomarkerFlag.normal (use AppColors.success
for both) instead of rendering unknown as AppColors.error, ensuring consistency
between the dotColor and tooltip display for result.flag values.

In `@lib/widgets/chat_message_bubble.dart`:
- Around line 46-69: The left accent Container currently renders as a separate
short strip (with only minHeight: 40) and therefore doesn't span multi-line
bubbles; instead, remove that separate accent Container used when (!isUser &&
!isError) and add a left border to the bubble Container's BoxDecoration (the
Container with padding and decoration named bubbleColor) by setting
decoration.border = Border(left: BorderSide(color: AppColors.primary, width:
4)); ensure you only apply this border when (!isUser && !isError) and keep the
existing borderRadius and bubbleColor intact so the accent aligns with the full
bubble height for multiline messages.

In `@lib/widgets/shimmer_loading.dart`:
- Around line 48-53: The current of(BuildContext context) method relies on an
assert that is stripped in release builds, causing a null-check crash when the
_ShimmerInherited is missing; change it to explicitly check for null and throw a
FlutterError with a clear message (e.g., "ShimmerScope not found in widget
tree") instead of using assert so release builds fail fast and provide a helpful
error; update the method that references _ShimmerInherited and returns
inherited!.controller to perform a null check and throw the FlutterError when
inherited is null.

In `@pubspec.yaml`:
- Around line 23-25: The pubspec.yaml currently pins the google_fonts dependency
to ^6.2.1; update the dependency declaration for google_fonts to the current
stable version (e.g., ^8.0.2) in pubspec.yaml, run flutter pub get and the test
suite to catch any breaking API changes, and if you must keep ^6.2.1 instead,
add a brief comment in pubspec.yaml explaining the compatibility reason;
reference the google_fonts entry when making the change.

---

Outside diff comments:
In `@lib/main.dart`:
- Around line 27-31: Remove the redundant SDK initialization: delete the
FlutterGemma.initialize(...) call from GemmaService.initialize() so the SDK is
only initialized once in main() where Future<void> main() awaits
FlutterGemma.initialize(maxDownloadRetries: 5) before runApp; keep the
initialization in main() and ensure GemmaService.initialize() no longer calls or
attempts to re-initialize FlutterGemma.initialize().

---

Nitpick comments:
In `@lib/screens/onboarding_screen.dart`:
- Around line 161-168: The gradient in the DecoratedBox currently uses a
hardcoded Color(0xFF0D9488) next to AppColors.primary; add a named constant to
the design system (e.g., in AppColors add static const Color primaryGradientEnd
= Color(0xFF0D9488)) and update the gradient colors list in
onboarding_screen.dart to use AppColors.primaryGradientEnd instead of the
hardcoded value so the gradient follows the AppColors theme (look for
DecoratedBox and KoshikaRadius.pill to locate the usage).

In `@lib/screens/reports_screen.dart`:
- Around line 381-390: The onDismissed handler currently swallows all exceptions
during PDF deletion; update the catch to capture the exception (and optionally
the stack) and log it instead of ignoring it so permission/disk errors are
visible—specifically modify the try/catch around
File(report.pdfPath).existsSync()/deleteSync() in the onDismissed block (which
also calls objectbox.deleteReport(report.id) and setState()) to log the error
via your app logger or debugPrint including contextual info (report.id and
report.pdfPath) and consider showing a non-blocking user-facing message if
deletion fails.

In `@lib/screens/settings_screen.dart`:
- Around line 454-582: The two widgets _ModelStatusTile and _EmbeddingModelTile
duplicate the same UI and status logic (layout, status icon/label/color getters,
progress/error rendering, and action row); refactor by extracting a single
reusable widget (e.g., ModelStatusCard) that accepts a ModelInfo (or the
existing modelInfo), callbacks (onDownload/onLoad/onUnload) and optional config
(labels/icons if needed), then replace _ModelStatusTile and _EmbeddingModelTile
with thin wrappers that build ModelStatusCard; move the shared getters
(_statusIcon, _statusLabel, _statusColor), progress/error rendering and
action-row rendering into ModelStatusCard so changes apply in one place while
retaining modelInfo-based behavior.

In `@lib/theme/koshika_design_system.dart`:
- Around line 162-190: The comment "8dp base grid" is inaccurate since values in
KoshikaSpacing (e.g., xs, sm, md, lg, xl, xxl, xxxl and screen/content paddings
like contentPadding and cardPaddingAsymmetric) use 4dp increments and non-8
multiples; update the header comment to reflect the actual spacing system (for
example "4dp base grid" or "4dp/8dp hybrid grid") so it accurately documents
KoshikaSpacing and its constants.

In `@lib/widgets/dashboard_summary_card.dart`:
- Around line 50-65: Replace the hardcoded Colors.white usages in the Text
styles with the appropriate design token from your design system (e.g.,
AppColors.onPrimaryContainer) to keep theming consistent: update the color
passed into theme.textTheme.titleMedium?.copyWith(...) and
KoshikaTypography.sectionHeader.copyWith(...) to use the AppColors token, and
add the necessary import for AppColors at the top of
dashboard_summary_card.dart; ensure both Text style usages (the "Health
Overview" title and the '$totalTracked Biomarkers Tracked' subtitle) use the
same chosen token.
- Around line 161-166: The label Text currently uses an inline
TextStyle(fontSize: 12, color: color); replace that inline style with the
design-system typography token from KoshikaTypography to match the count's usage
(e.g., use an appropriate token such as KoshikaTypography.label,
KoshikaTypography.bodySmall, or the project’s equivalent) and apply the color
via copyWith(color: color); update the Text widget that renders label so it uses
KoshikaTypography.<token>.copyWith(color: color) instead of the inline
TextStyle.

In `@lib/widgets/trend_line_painter.dart`:
- Around line 79-83: The shouldRepaint implementation currently compares list
references (values != oldDelegate.values) which misses in-place mutations and
overtriggers for equivalent new lists; update
SimpleTrendLinePainter.shouldRepaint to compare list contents instead (e.g., use
Flutter's listEquals from foundation or manually compare lengths and elements)
and keep the existing comparisons for lineColor, lineWidth, and showFill; ensure
you reference values, shouldRepaint, and SimpleTrendLinePainter when making the
change.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: eb8e9bfb-34ed-4b24-82ff-72bc2f904cc4

📥 Commits

Reviewing files that changed from the base of the PR and between 65627f0 and 8aac329.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (25)
  • lib/main.dart
  • lib/screens/biomarker_detail_screen.dart
  • lib/screens/chat_screen.dart
  • lib/screens/dashboard_screen.dart
  • lib/screens/onboarding_screen.dart
  • lib/screens/report_detail_screen.dart
  • lib/screens/reports_screen.dart
  • lib/screens/settings_screen.dart
  • lib/screens/splash_screen.dart
  • lib/services/embedding_service.dart
  • lib/services/gemma_service.dart
  • lib/services/haptic_feedback_service.dart
  • lib/theme/app_colors.dart
  • lib/theme/koshika_design_system.dart
  • lib/widgets/biomarker_trend_chart.dart
  • lib/widgets/chat_message_bubble.dart
  • lib/widgets/dashboard_summary_card.dart
  • lib/widgets/flag_badge.dart
  • lib/widgets/icon_container.dart
  • lib/widgets/koshika_card.dart
  • lib/widgets/reference_range_gauge.dart
  • lib/widgets/shimmer_loading.dart
  • lib/widgets/status_badge.dart
  • lib/widgets/trend_line_painter.dart
  • pubspec.yaml

Comment thread lib/screens/biomarker_detail_screen.dart
Comment thread lib/screens/settings_screen.dart Outdated
Comment thread lib/widgets/biomarker_trend_chart.dart
Comment thread lib/widgets/chat_message_bubble.dart Outdated
Comment thread lib/widgets/shimmer_loading.dart
Comment thread pubspec.yaml
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
lib/screens/dashboard_screen.dart (1)

829-843: ⚠️ Potential issue | 🟡 Minor

Latest mini-bar highlight can be wrong for duplicate heights.

On Line 830–832, barHeights.indexOf(h) uses first-match lookup, so if values repeat, the true last bar may not be marked as latest.

Suggested fix
-              children: barHeights.map((h) {
-                final isLatest =
-                    barHeights.last == h &&
-                    barHeights.indexOf(h) == barHeights.length - 1;
+              children: barHeights.asMap().entries.map((entry) {
+                final index = entry.key;
+                final h = entry.value;
+                final isLatest = index == barHeights.length - 1;
                 return Expanded(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/screens/dashboard_screen.dart` around lines 829 - 843, The logic that
sets isLatest uses barHeights.indexOf(h), which returns the first match and
fails when heights repeat; change the iteration to preserve the index and use
that index to detect the last bar (e.g., iterate via barHeights.asMap().entries
or List.generate so you have both index and value) and set isLatest to (index ==
barHeights.length - 1) — update the mapping closure where barHeights.map((h) {
... }) is used so it references the index instead of indexOf(h).
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@lib/screens/dashboard_screen.dart`:
- Around line 223-231: The comparator currently returns 0 for non-critical rows
causing unstable ordering; update the comparator (the block comparing a.flag and
b.flag) so when neither side is critical it falls back to a deterministic
secondary key: first compare severity (e.g., a.severity vs b.severity, using
compareTo or numeric subtraction so higher severity sorts earlier), and if
severities are equal then compare a.name.toLowerCase() with b.name.toLowerCase()
for a stable alphabetical tiebreaker. Ensure the comparator returns the integer
result of the severity comparison or the name compareTo instead of 0.

---

Outside diff comments:
In `@lib/screens/dashboard_screen.dart`:
- Around line 829-843: The logic that sets isLatest uses barHeights.indexOf(h),
which returns the first match and fails when heights repeat; change the
iteration to preserve the index and use that index to detect the last bar (e.g.,
iterate via barHeights.asMap().entries or List.generate so you have both index
and value) and set isLatest to (index == barHeights.length - 1) — update the
mapping closure where barHeights.map((h) { ... }) is used so it references the
index instead of indexOf(h).
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 43f95833-b050-4506-a903-a44428661c5b

📥 Commits

Reviewing files that changed from the base of the PR and between 8aac329 and 519dd4c.

⛔ Files ignored due to path filters (1)
  • pubspec.lock is excluded by !**/*.lock
📒 Files selected for processing (2)
  • lib/screens/dashboard_screen.dart
  • lib/theme/app_colors.dart

Comment thread lib/screens/dashboard_screen.dart Outdated
- wrap shimmer skeleton in ListView to prevent overflow on landscape/small screens
- add secondary name sort to make out-of-range list order deterministic
- align unknown flag tooltip color with dot color (both now green)
- replace GestureDetector with Material+InkWell in settings rows for ripple and a11y
- use left BorderSide on chat bubble instead of separate accent strip widget
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (2)
lib/screens/dashboard_screen.dart (1)

222-231: ⚠️ Potential issue | 🟠 Major

Keep severity ordering before the name tiebreaker.

Lines 290-310 only render outOfRangeResults.take(3). With the current comparator, a borderline item can displace a high/low one purely because its name sorts earlier.

📌 Suggested change
+    int severityRank(BiomarkerFlag flag) {
+      switch (flag) {
+        case BiomarkerFlag.critical:
+          return 0;
+        case BiomarkerFlag.high:
+        case BiomarkerFlag.low:
+          return 1;
+        case BiomarkerFlag.borderline:
+          return 2;
+        case BiomarkerFlag.normal:
+        case BiomarkerFlag.unknown:
+          return 3;
+      }
+    }
+
     outOfRangeResults.sort((a, b) {
-      if (a.flag == BiomarkerFlag.critical &&
-          b.flag != BiomarkerFlag.critical) {
-        return -1;
-      }
-      if (b.flag == BiomarkerFlag.critical &&
-          a.flag != BiomarkerFlag.critical) {
-        return 1;
-      }
-      return a.displayName.compareTo(b.displayName);
+      final bySeverity = severityRank(a.flag).compareTo(severityRank(b.flag));
+      if (bySeverity != 0) return bySeverity;
+      return a.displayName.toLowerCase().compareTo(
+        b.displayName.toLowerCase(),
+      );
     });
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/screens/dashboard_screen.dart` around lines 222 - 231, The comparator
passed to outOfRangeResults.sort currently mixes severity and name ordering and
allows name to override severity; change the comparator in the
outOfRangeResults.sort block so it first maps a.flag and b.flag to a severity
rank (e.g., critical > high/low > borderline > normal) and compares those ranks,
and only if the ranks are equal fall back to comparing
a.displayName.compareTo(b.displayName); reference the comparator used on
outOfRangeResults and the BiomarkerFlag enum/constants when implementing the
severity-to-rank mapping.
lib/widgets/biomarker_trend_chart.dart (1)

127-131: ⚠️ Potential issue | 🟠 Major

Don't render unknown as a healthy result.

BiomarkerFlag.unknown is indeterminate, but both branches color it with AppColors.success. That makes the chart show an unknown value as normal, while lib/widgets/status_badge.dart and lib/widgets/flag_badge.dart render it neutrally.

🎯 Suggested change
-          final dotColor =
-              (result.flag == BiomarkerFlag.normal ||
-                  result.flag == BiomarkerFlag.unknown)
-              ? AppColors.success
-              : AppColors.error;
+          final dotColor = result.flag == BiomarkerFlag.normal
+              ? AppColors.success
+              : result.flag == BiomarkerFlag.unknown
+                  ? AppColors.textMuted
+                  : AppColors.error;-                    final flagColor =
-                        (result.flag == BiomarkerFlag.normal ||
-                            result.flag == BiomarkerFlag.unknown)
-                        ? AppColors.success
-                        : AppColors.error;
+                    final flagColor = result.flag == BiomarkerFlag.normal
+                        ? AppColors.success
+                        : result.flag == BiomarkerFlag.unknown
+                            ? AppColors.textMuted
+                            : AppColors.error;

Also applies to: 243-247

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/widgets/biomarker_trend_chart.dart` around lines 127 - 131, The dotColor
logic in biomarker_trend_chart currently treats BiomarkerFlag.unknown as
success; update the conditional in the dotColor assignment (and the similar
block at the other occurrence) to only map BiomarkerFlag.normal to
AppColors.success, map BiomarkerFlag.unknown to the neutral color used by
StatusBadge/FlagBadge (e.g., AppColors.neutral), and map other non-normal flags
to AppColors.error so unknown values render neutrally instead of as healthy.
🧹 Nitpick comments (2)
lib/screens/settings_screen.dart (1)

560-584: Consider extracting shared status logic to reduce duplication.

The _statusIcon, _statusLabel, and _statusColor getters are identical between _ModelStatusTile and _EmbeddingModelTile. This duplication could be consolidated.

♻️ Suggested approach: extract to a mixin or extension
// Add this extension near the top of the file or in a shared location
extension ModelStatusVisuals on ModelStatus {
  IconData get icon => switch (this) {
    ModelStatus.notDownloaded => Icons.cloud_download_outlined,
    ModelStatus.downloading => Icons.downloading,
    ModelStatus.ready => Icons.check_circle_outline,
    ModelStatus.loading => Icons.hourglass_top,
    ModelStatus.loaded => Icons.check_circle,
    ModelStatus.error => Icons.error_outline,
  };

  String get label => switch (this) {
    ModelStatus.notDownloaded => 'Not Downloaded',
    ModelStatus.downloading => 'Downloading...',
    ModelStatus.ready => 'Ready',
    ModelStatus.loading => 'Loading...',
    ModelStatus.loaded => 'Active',
    ModelStatus.error => 'Error',
  };

  Color get color => switch (this) {
    ModelStatus.notDownloaded => AppColors.onSurfaceVariant,
    ModelStatus.downloading || ModelStatus.loading => AppColors.statusBusy,
    ModelStatus.ready => AppColors.statusReady,
    ModelStatus.loaded => AppColors.statusActive,
    ModelStatus.error => AppColors.error,
  };
}

Then in both tiles, replace:

-  final statusColor = _statusColor;
+  final statusColor = modelInfo.status.color;
...
-  Icon(_statusIcon, color: statusColor, size: 20),
+  Icon(modelInfo.status.icon, color: statusColor, size: 20),
...
-  _statusLabel,
+  modelInfo.status.label,

Also applies to: 722-746

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/screens/settings_screen.dart` around lines 560 - 584, Extract the
duplicated status logic from _statusIcon, _statusLabel, and _statusColor into a
single extension or mixin on ModelStatus (e.g. ModelStatusVisuals) that exposes
icon, label, and color getters; update both _ModelStatusTile and
_EmbeddingModelTile to call modelInfo.status.icon / .label / .color instead of
the local getters to remove duplication and keep behavior identical.
lib/widgets/chat_message_bubble.dart (1)

68-68: Consider localizing hardcoded labels.

'KOSHIKA INTELLIGENCE' and 'Error' are user-facing strings; if l10n is used in the app, move these to localization keys.

Also applies to: 90-90

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@lib/widgets/chat_message_bubble.dart` at line 68, Replace the hardcoded
user-facing strings in the ChatMessageBubble widget (the label "KOSHIKA
INTELLIGENCE" and the "Error" text) with localized resources: locate the
occurrences in the ChatMessageBubble class/build method and use the app's
localization accessor (e.g., AppLocalizations.of(context).koshikaIntelligence or
context.l10n.koshikaIntelligence) instead of the literal strings, add
corresponding keys/entries to the ARB/i18n resource files, and update any
imports to reference the localization class so the UI reads the localized
values.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Duplicate comments:
In `@lib/screens/dashboard_screen.dart`:
- Around line 222-231: The comparator passed to outOfRangeResults.sort currently
mixes severity and name ordering and allows name to override severity; change
the comparator in the outOfRangeResults.sort block so it first maps a.flag and
b.flag to a severity rank (e.g., critical > high/low > borderline > normal) and
compares those ranks, and only if the ranks are equal fall back to comparing
a.displayName.compareTo(b.displayName); reference the comparator used on
outOfRangeResults and the BiomarkerFlag enum/constants when implementing the
severity-to-rank mapping.

In `@lib/widgets/biomarker_trend_chart.dart`:
- Around line 127-131: The dotColor logic in biomarker_trend_chart currently
treats BiomarkerFlag.unknown as success; update the conditional in the dotColor
assignment (and the similar block at the other occurrence) to only map
BiomarkerFlag.normal to AppColors.success, map BiomarkerFlag.unknown to the
neutral color used by StatusBadge/FlagBadge (e.g., AppColors.neutral), and map
other non-normal flags to AppColors.error so unknown values render neutrally
instead of as healthy.

---

Nitpick comments:
In `@lib/screens/settings_screen.dart`:
- Around line 560-584: Extract the duplicated status logic from _statusIcon,
_statusLabel, and _statusColor into a single extension or mixin on ModelStatus
(e.g. ModelStatusVisuals) that exposes icon, label, and color getters; update
both _ModelStatusTile and _EmbeddingModelTile to call modelInfo.status.icon /
.label / .color instead of the local getters to remove duplication and keep
behavior identical.

In `@lib/widgets/chat_message_bubble.dart`:
- Line 68: Replace the hardcoded user-facing strings in the ChatMessageBubble
widget (the label "KOSHIKA INTELLIGENCE" and the "Error" text) with localized
resources: locate the occurrences in the ChatMessageBubble class/build method
and use the app's localization accessor (e.g.,
AppLocalizations.of(context).koshikaIntelligence or
context.l10n.koshikaIntelligence) instead of the literal strings, add
corresponding keys/entries to the ARB/i18n resource files, and update any
imports to reference the localization class so the UI reads the localized
values.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 7698b18e-dc14-41a0-8fd7-729bd66cf558

📥 Commits

Reviewing files that changed from the base of the PR and between 519dd4c and f5c966f.

📒 Files selected for processing (5)
  • lib/screens/biomarker_detail_screen.dart
  • lib/screens/dashboard_screen.dart
  • lib/screens/settings_screen.dart
  • lib/widgets/biomarker_trend_chart.dart
  • lib/widgets/chat_message_bubble.dart

@priyavratuniyal priyavratuniyal merged commit 6f533bc into main Mar 26, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant