Skip to content

Repository files navigation

Seen — Flutter client

Behavioral annotation client for the Seen backend. The app renders a hidden-object scene from a passive-data context, lets the user pick clues that felt familiar, asks an AI (or fixed fallback) to clarify what each clue meant, and then surfaces transparent 14-day patterns.

The point of this codebase's architecture is that UI changes and logic changes never have to touch the same file. See "Where to change what" below.

Running

The backend URL and function key are injected at build time via --dart-define so the key is never committed to source:

flutter run \
  --dart-define=SEEN_API_BASE_URL=https://seen-backend-func-32653.azurewebsites.net/api \
  --dart-define=SEEN_API_FUNCTION_KEY=<your-function-key>

Get the key from Azure:

az functionapp keys list --name seen-backend-func-32653 \
  --resource-group seen-backend-rg --query "functionKeys.default" -o tsv

Leave both blank and the app still works — every backend call has a deterministic local fallback so the demo never breaks because the network broke.

Architecture at a glance

lib/
├── main.dart                              # ProviderScope + MaterialApp entry
│
├── core/                                  # Foundation, no imports from below
│   ├── config/api_config.dart             # baseUrl + functionKey from --dart-define
│   ├── errors/failures.dart               # sealed Failure hierarchy
│   └── theme/app_theme.dart               # AppColors + GlassContainer + ThemeData
│
├── data/                                  # External world
│   ├── models/                            # DTOs matching backend contract 1:1
│   │   ├── daily_context.dart
│   │   ├── interpreted_signal.dart
│   │   ├── clue.dart                      # backend fields + UI-only extras (icon/x/y)
│   │   ├── clue_selection.dart
│   │   ├── daily_entry.dart
│   │   ├── follow_up_question.dart
│   │   ├── pattern_observation.dart
│   │   ├── scene_composition.dart
│   │   └── demo_profile.dart
│   ├── local/                             # Static/seed data
│   │   ├── clue_catalog.dart              # the 12 clues + visual metadata
│   │   ├── demo_profiles.dart             # profile A/B/C
│   │   └── historical_dataset.dart        # 14-day synthetic generator
│   ├── remote/
│   │   ├── api_client.dart                # Dio + function-key interceptor
│   │   └── seen_api.dart                  # typed endpoints, throws Failure
│   └── repositories/seen_repository_impl.dart
│
├── domain/                                # Pure business logic
│   ├── repositories/seen_repository.dart  # abstract interface
│   └── engine/                            # deterministic engines (also offline fallbacks)
│       ├── signal_engine.dart
│       ├── scoring_engine.dart
│       ├── pattern_engine.dart
│       ├── summary_engine.dart
│       └── fallback_questions.dart
│
└── presentation/                          # UI + state controllers
    ├── providers/api_providers.dart       # config → client → api → repository wiring
    ├── controllers/                       # Riverpod notifiers
    │   ├── app_navigation_controller.dart # mode / step / walkthrough
    │   ├── profile_controller.dart        # active demo profile + ad-hoc tweaks
    │   ├── day_flow_controller.dart       # signals + scene + selections + hint
    │   ├── follow_up_controller.dart      # AsyncValue<FollowUpQuestion> per clue
    │   ├── summary_controller.dart        # AsyncValue<DailyEntry> for commit
    │   └── historical_entries_controller.dart
    ├── screens/
    │   ├── main_layout.dart               # header + stepper + mode switch
    │   ├── walkthrough_screen.dart        # 4-page intro
    │   ├── context_preview_screen.dart    # screen 1
    │   ├── scene_screen.dart              # screen 2
    │   ├── daily_summary_screen.dart      # screen 3
    │   ├── patterns_screen.dart           # screen 4
    │   └── therapist_portal.dart
    └── widgets/
        ├── scene_canvas.dart              # the hidden-object canvas + painters
        ├── question_sheet.dart            # the AI/fallback follow-up sheet
        └── simulator.dart                 # telemetry sliders (optional dev tool)

Dependency rule

Each layer only imports from itself or from layers below it:

presentation → domain → data → core
                            ↘ ↑
                              core

No widget calls the API directly. No controller instantiates Dio. Screens read from providers; controllers read from the repository; the repository mediates the local engine and the HTTP API.

Where to change what

I want to change… Files to touch
Colors, typography, spacing core/theme/app_theme.dart
A screen's look (any of the four patient screens) presentation/screens/<screen>.dart only
The hidden-object canvas visuals presentation/widgets/scene_canvas.dart
The follow-up modal sheet look presentation/widgets/question_sheet.dart
The walkthrough intro pages presentation/screens/walkthrough_screen.dart
Add/remove a clue data/local/clue_catalog.dart
Change the 3 demo profiles data/local/demo_profiles.dart
Change signal interpretation rules domain/engine/signal_engine.dart
Change clue scoring / scene composition domain/engine/scoring_engine.dart
Change how patterns are calculated domain/engine/pattern_engine.dart
Change the offline fallback question by category domain/engine/fallback_questions.dart
Change what the backend gets sent data/remote/seen_api.dart
Swap the backend entirely (mock server, offline demo…) provide a new SeenRepository and override seenRepositoryProvider
Add a new endpoint add a method in seen_api.dart + a method in the repo interface + impl
Add a new screen new file under presentation/screens/, wire into main_layout.dart
Change the max clues per day kMaxSelectionsPerDay in day_flow_controller.dart (server also enforces)

State management: why Riverpod

  • Compile-time safe DI — the only way to get a repository into a widget is via ref.watch(seenRepositoryProvider); you can't accidentally pass the wrong shape or forget to construct one.
  • AsyncValue<T> for the AI calls — widgets pattern-match loading/data/error rather than juggling FutureBuilder + local state.
  • overrideWith(...) in tests — every provider is swappable, so widget tests never need real HTTP.
  • Auto-dispose semantics — the follow-up question per clue lives only while its sheet is open.

The day-flow state machine

DayFlowController is the single controller responsible for "what does today look like right now". It watches the active profile and re-derives signals + scene deterministically. Selections and hint level are the only mutable state; everything else is a pure function of the current context.

activeProfileProvider  ──▶  DayFlowController.build()
                              │
                              ├─▶ interpretSignals(context)
                              ├─▶ composeScene(signals)
                              └─▶ selections=[], hintLevel=0

user taps clue ──▶ SceneCanvas.onClueTapped
                     └─▶ ClueQuestionSheet
                           └─▶ ref.watch(followUpQuestionProvider(clue))
                                 └─▶ SeenRepository.followUpQuestion(...)
                                       ├─▶ SeenApi.followUpQuestion (backend)
                                       └─▶ FallbackQuestions.forCategory (offline)
                           └─▶ ref.read(dayFlow…).addSelection(...)

user taps "Complete Daily Entry" ──▶ SceneScreen
                                       └─▶ ref.read(dailySummary…).commit()
                                             └─▶ SeenRepository.completeDay(...)

Backend contract

All models mirror the backend TypeScript types exactly. See seen-backend/README.md for the source of truth. The frontend adds four UI-only fields to Clue (icon, x, y, question, options) which Clue.toJson() deliberately omits — the backend AI never sees them (this respects the "AI receives only the minimum structured context" rule from the requirements guide).

Testing

flutter test

The included smoke test overrides walkthroughDoneProvider to skip the intro and asserts the main layout renders. Add more tests by overriding the relevant providers with mocks — no need to stub the whole world.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

Packages

Contributors

Languages