Skip to content

feat: arena assistant - LLM chatbot with live app data - #30

Merged
M1RK02 merged 14 commits into
developfrom
feature/assistant
Jul 10, 2026
Merged

feat: arena assistant - LLM chatbot with live app data#30
M1RK02 merged 14 commits into
developfrom
feature/assistant

Conversation

@Abdullahsaeed10

@Abdullahsaeed10 Abdullahsaeed10 commented Jul 9, 2026

Copy link
Copy Markdown
Collaborator

Description

Adds the Arena Assistant: an in-app chatbot (full-screen route /chatbot) that answers questions about empty tables, today's tournaments, queue position, and ELO — grounded in live Firestore data via the existing repositories (read-only, bounded queries, no listeners).

  • Uses a shared LLM key (Regolo) compiled in via --dart-define-from-file=dart_defines.json. The file is gitignored; copy app/dart_defines.example.jsonapp/dart_defines.json and paste the key (sent privately). Without it the app still builds and the assistant runs in a rule-based "limited mode" — CI and tests need no secrets.
  • Answers are personalized (name, degree program, per-sport ELO) via a per-session cached profile fetch (3 reads/session).
  • Settings UI for the key exists but is hidden in all builds (--dart-define=SHOW_ASSISTANT_SETTINGS=true re-enables it for dev).
  • Adds Firestore index (ownerType ASC, sportId ASC, elo ASC) on ratings to the manifest — needed by RatingRepository.getUserRank. Not yet deployed: until someone runs firebase deploy --only firestore:indexes --project campus-arena-app, rank numbers are silently omitted (handled gracefully).
  • Contract for the tournament vertical: the chatbot's tournament card "View" pushes /tournament/:id (RouteConstants.tournamentPath), currently a placeholder screen in app_router.dart — replace that one builder with the real detail screen.
  • Also fixes error-swallowing bare catch (_) in ChatCubit and ProfileSetupCubit.

Related Task

  • Vertical: B (Abdullah) — extra feature, not on the task board
  • Phase: 6–7 window
  • Reviewer: owner of the routing shell / tournament vertical (touches app_router.dart and defines the /tournament/:id contract)

Shared Contracts

  • None
  • Match document shape
  • Facility status field (facilities/{id}.status / .currentMatchId)
  • Queue position write (facilities/{id}/queue/{uid}.position)

The chatbot only reads through existing repositories; it never writes to any contract surface.

Shared Files

  • This PR does not touch them, OR it was opened the same day the changes were made and the other two devs were pinged.

Touches app_router.dart (adds /chatbot, /chatbot/settings, and the /tournament/:id placeholder — purely additive). Pinging both of you now.

Breaking Changes

  • No
  • Yes

All changes are additive: new feature folder, new route constants, new index manifest entry. No existing repository method, Cubit state, or model field changed.

Checklist

  • Branched off develop and rebased on develop before opening this PR (no merge commits). (Note: develop moved since branching — a merge from develop to resolve conflicts is coming; final merge will be squashed anyway.)
  • My code follows the Dart style guide (dart format applied).
  • I have not introduced any hardcoded user-facing strings (used ARB files if applicable). (Known gap: the limited-mode fallback answers and ELO explainer are English-only — flagged for the Phase 7 localization audit.)
  • CI pipeline is green (flutter analyze: 0 issues; flutter test test/features/chatbot: 58/58 pass locally).
  • If this PR adds a new sport, the ScoringStrategy contract remains unmodified. (No new sport.)
  • If this PR introduces a new Cubit, it emits an initial state synchronously. (ChatCubitconst ChatState().)
  • Firestore writes affecting ELO or queue position use atomic transactions. (This PR performs no Firestore writes.)
  • If this PR shows facility availability ("free"/"empty" status), the disclaimer footer is present via the availabilityDisclaimerShort ARB key — appended in code on every availability answer, never delegated to the LLM.

UI Changes (If applicable)

Implemented Design reference
(chat screen screenshot — attach) n/a — extra feature, no design-system screen exists

Abdullah Saeed added 10 commits July 10, 2026 07:44
- FacilityRepository.getFacilitiesByStatus, TournamentRepository.getTournamentsInWindow/getParticipantCount, RatingRepository.getUserRank, QueueRepository.getQueueLength/getUserQueueEntry - all one-shot, bounded (limit/count()), no listeners, for Spark-tier read discipline.
- ChatbotToolsService: read-only tool layer over the existing repositories (getEmptyTables, getFacilitiesByStatus, getTournamentsToday, getUserQueuePosition, getUserEloAndRank, explainHowEloWorks). Typed tool_results.dart result objects so the UI can render cards without re-parsing strings.
Deviation from the original chatbot plan (which assumed a single Gemini key via --dart-define): a runtime-configurable multi-provider LLM layer instead, so the API key/provider/model are user-supplied and persisted on-device rather than baked into the client binary.

- AssistantProvider enum (anthropic, openai, openrouter, gemini) with per-provider default models, all user-editable.
- AssistantSettingsModel + AssistantSettingsRepository, persisted via shared_preferences (added to pubspec.yaml alongside http, already present).
- LlmClient (package:http): OpenAI-compatible /chat/completions for openai + openrouter, Anthropic Messages API (x-api-key + anthropic-version), Gemini generateContent (key as query param). Low temperature throughout. testConnection() fires a minimal request and maps common HTTP failures (401/403 invalid key, 429 rate limit, network error) to short reason codes for localized UI feedback.
… prompt

- RuleBasedIntentMatcher: keyword classification (EN+IT) into empty tables / tournaments today / ELO explainer / queue position / general.
- ChatbotSystemPrompt: distills static app facts (ELO rules, not-a-booking-app framing) plus the current turn's tool results as the only source of live data, explicitly forbidding invented facilities/data.
- ChatbotRepository: orchestrates classify -> run tool(s) -> LLM-grounded answer if a key is configured, else deterministic rule-based fallback (limited mode) on missing key or LlmException. Hard rule enforced here in code (never trusted to the LLM or the templates): the localized availabilityDisclaimerShort is appended unconditionally whenever an availability tool fired this turn.
- ChatMessageModel: in-memory (non-persisted) chat message, optionally carrying FacilityToolResult/TournamentToolResult for rich card rendering.
Adds 42 new ARB keys (EN + IT) covering the chat screen, settings screen, suggested-question chips, limited-mode/error strings, and the four exact disclaimer strings from the plan (availabilityDisclaimerFull/Short, chatbotIntroDisclaimer, plus a one-line tutorialSlide3Disclaimer). Hand-edited app_localizations.dart / app_localizations_en.dart / app_localizations_it.dart to match — flutter gen-l10n is unavailable in this environment, so these were generated by script from the arb content and matched to the existing doc-comment/getter style key-for-key; not verified to compile.

Also appends the short disclaimer as a muted caption on the onboarding mini-tutorial's map/queue slide (Task C4 placement 4).
- ChatCubit/ChatState: initial/sending/answered/error lifecycle, in-memory-only messages (no persistence across restarts), limited-mode tracking driven by whether an API key is configured.
- ChatScreen (route /chatbot, full-screen, pushed outside the shell): app bar with settings gear + overflow 'Clear chat', limited-mode pill linking to settings, dismissible first-open disclaimer banner (chatbotIntroDisclaimer), suggested-question chips, message list with typing indicator, rounded input + 48px circular send button disabled while sending.
- Widgets: MessageBubble (user -> primaryContainer right-aligned, assistant -> surfaceContainer left-aligned, error bubble with retry), FacilityStatusCard (status dot, sport icon, queue count, Open Map, short-disclaimer footer), TournamentCard (name/sport/time, x/max participants, status chip, View), SuggestedChips, DisclaimerBanner, TypingIndicator (animated 3-dot).
- AssistantSettingsCubit/State + AssistantSettingsScreen (route /chatbot/settings): provider dropdown, obscured API key field with show/hide toggle, user-editable model field (prompts to reset to the new provider's default on provider change), Test Connection with loading/success/error feedback, 48px Save button. Works fully with no key saved (chat stays in limited/rule-based mode) with a banner linking here.
- Wiring: route_constants.dart + app_router.dart (both routes registered, /chatbot auth-guarded the same way courtPath/challengePath already are), main.dart provides ChatbotToolsService/AssistantSettingsRepository/ChatbotRepository via MultiRepositoryProvider, Home screen gets a minimal FAB entry point (small surgical diff, dashboard rebuild is a separate in-progress phase).
- RuleBasedIntentMatcher: classifies EN+IT sample phrases into each of the 4 intents plus the general fallback.
- ChatbotToolsService: mocked-repository tests for getEmptyTables, getTournamentsToday, getUserQueuePosition, getUserEloAndRank, and the static explainHowEloWorks (asserts zero repository interactions).
- ChatbotRepository: no-key -> rule-based, LLM-configured-but-errors -> rule-based fallback, LLM-configured-and-succeeds -> LLM text used; hard disclaimer-rule tests proving availabilityDisclaimerShort is appended on both the rule-based path and the LLM path (even when the LLM's own text omits it), and NOT appended when no availability tool fired.
- ChatCubit: limited-mode detection on init, sendMessage emits user bubble then answer, blank input is a no-op, repository errors surface as a retryable error bubble, clearChat resets state.
- ChatScreen widget tests: suggested chips + intro disclaimer banner visible on first open, limited-mode pill shown with no key configured, sending a message renders a user bubble and the assistant reply, a facility card renders with its disclaimer footer visible.
- Repository tests extended: FacilityRepository.getFacilitiesByStatus, TournamentRepository.getTournamentsInWindow/getParticipantCount, RatingRepository.getUserRank, QueueRepository.getQueueLength/getUserQueueEntry (all against fake_cloud_firestore, asserting the bounded limit/count() behavior).
@M1RK02
M1RK02 force-pushed the feature/assistant branch from 05d64ed to c9bb905 Compare July 10, 2026 06:20
@M1RK02
M1RK02 self-requested a review July 10, 2026 06:29
M1RK02
M1RK02 previously requested changes Jul 10, 2026

@M1RK02 M1RK02 left a comment

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

Solid feature — read-only tool discipline, graceful LLM degradation, and the in-code disclaimer guarantee are all well done. Requesting changes for tracking; I'll push the fixes to this branch myself, no action needed on your end. Recording them here so the history is clear.

Blocking

  • Rule-based / limited-mode answers are hardcoded English. _ruleBasedAnswer, the queue/ELO fallback text, and explainHowEloWorks are raw Dart literals. This is the exact thing brain/task_board.md §C2 (line 226) specifies — "templated ARB answer ... localized limited-mode notice" — and C2 is checked done. Every user without dart_defines.json (i.e. the default build, and CI) hits this path, so it's the common case, not an edge. Moving all of it to ARB (EN+IT).

Bugs / nits

  • Disclaimer rendered twice. ChatbotRepository.answer() unconditionally appends availabilityDisclaimerShort to the text, and FacilityStatusCard always renders the same short disclaimer as a footer — so any answer that lists free tables shows it twice in one bubble. Keeping the tested repo-level append (the hard guarantee) and dropping the card's duplicate footer.
  • Dead DI registration. RepositoryProvider<ChatbotToolsService> in main.dart is never read — ChatbotRepository builds its own. Wiring it through so the provided instance is actually used.
  • Redundant Firestore index. The new (ownerType, sportId, elo ASC) on ratings duplicates the existing (sportId, ownerType, elo DESC), which rating_repository.dart:92 already documents as serving getUserRank (no degreeProgram) — the dashboard runs that query today. Removing it.
  • N+1 enrichment. getEmptyTables / getTournamentsToday await queue-length / participant-count per row sequentially. Parallelizing with Future.wait (still bounded ≤25).
  • Timeout not distinguished. LlmClient lets TimeoutException fall through to unexpected_error. Mapping it to a timeout reason for a clearer Test-Connection message.

Note: the PR body's /tournament/:id placeholder concern is already moot — develop merged the real tournament detail screen and RouteConstants.tournamentDetail resolves correctly on this branch.

M1RK02 added 3 commits July 10, 2026 09:42
Rule-based/limited-mode answers were hardcoded English, violating the
i18n rule and task_board C2 ("templated ARB answer"). Move them all to
ARB (EN+IT) and pass AppLocalizations into ChatbotRepository.answer();
the English toolContext stays as LLM grounding only.

Also localize two user-facing data tokens the cards printed raw:
- TournamentCard now uses the shared TournamentStatusPill instead of
  Text(tournament.status), which rendered "in_progress" untranslated.
- FacilityStatusCard shows a people icon + count instead of "x2".

Map LlmClient TimeoutException to a distinct 'timeout' reason with its
own localized Test-Connection message (chatbotErrorTimeout).

Drop the duplicate short-disclaimer footer from FacilityStatusCard: the
repository already appends it unconditionally to the message text above
the card, so it was rendering twice in one bubble. Tests updated.
- main.dart: ChatbotRepository now reads the root-provided
  ChatbotToolsService and AssistantSettingsRepository instead of
  constructing its own, so the registered providers are actually used.
- firestore.indexes.json: remove the (ownerType, sportId, elo ASC)
  ratings index. RatingRepository.getUserRank (no degreeProgram) is
  already served by the existing (sportId, ownerType, elo) index, which
  the dashboard uses in production — the new one was a duplicate.
- ChatbotToolsService: fan out the per-row queue-length and
  participant-count reads with Future.wait (still bounded <= maxResults)
  instead of one sequential round-trip each.
Core (C1/C2) and UI (C3/C4) are delivered by the assistant feature and
now meet their specs (C2's rule-based path produces localized ARB
answers). Marked done per the PR review.
@M1RK02
M1RK02 requested a review from ChristianPrendin July 10, 2026 07:42
@M1RK02

M1RK02 commented Jul 10, 2026

Copy link
Copy Markdown
Owner

Pushed the fixes to feature/assistant (dd194de, 79bbef5, 0c65b46) — all review items addressed. dart format clean, flutter analyze clean, 735/735 tests pass.

Blocking

  • Rule-based / limited-mode answers are now localized. All fallback templates, the queue/ELO answers, and the ELO explainer moved to ARB (EN+IT). ChatbotRepository.answer() takes AppLocalizations; the English toolContext remains LLM grounding only (the model already answers in the user's language). This is what task_board §C2 asked for.

Bugs / nits

  • Disclaimer no longer doubles. Dropped the always-on footer from FacilityStatusCard; the repo's unconditional text-append (the tested hard rule) is the single source now.
  • Two more raw tokens localized while I was in there: TournamentCard printed tournament.status ("in_progress") untranslated — now uses the shared TournamentStatusPill; the facility queue count was a hardcoded x2 — now a people-icon + count.
  • DI wired. ChatbotRepository reads the root-provided ChatbotToolsService / AssistantSettingsRepository instead of building its own.
  • Redundant index removed. Dropped (ownerType, sportId, elo ASC)getUserRank (no degreeProgram) is already served by the existing (sportId, ownerType, elo) index.
  • N+1 parallelized. getEmptyTables / getTournamentsToday fan out their per-row reads with Future.wait (still bounded ≤ 25).
  • Timeout mapped. LlmClient TimeoutException → distinct timeout reason with its own localized message.

⚠️ Shared contract — Firestore index manifest

79bbef5 removes a ratings composite index from app/firestore.indexes.json. It's redundant (see above), so no redeploy is required to keep rank working — but flagging it since the manifest is a shared surface. If anyone runs firebase deploy --only firestore:indexes, that index will be deleted server-side, which is fine and intended.

@M1RK02
M1RK02 dismissed their stale review July 10, 2026 07:44

I modified the code myself and flagged Christian to review it.

The rule-based ELO line printed elo + match count but dropped the rank,
while the LLM toolContext included it — a content asymmetry between the
grounded and limited-mode paths. Add a chatbotAnswerYourEloRanked ARB
variant (EN+IT) and branch on rank nullability so limited mode reaches
parity. Tests cover both the ranked and unranked branches.

@ChristianPrendin ChristianPrendin left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Reviewed Mirko's three fix commits (dd194de, 79bbef5, 0c65b46) against the code. All correct:

  • Localized fallback answers move to ARB (EN+IT); hand-edited generated l10n verified consistent across all three files, placeholder types match.
  • Disclaimer dedup on FacilityStatusCard is safe: the card only renders on the emptyTables intent, which always sets availabilityToolFired, so the repository unconditionally appends the disclaimer above the card. Verified by the 'shows exactly once' widget test.
  • TournamentStatusPill correctly replaces the raw status Text.
  • Timeout mapping is reachable (.timeout() is applied per call) and mapped in the settings screen.
  • DI wiring: providers registered before ChatbotRepository, context.read resolves.
  • Firestore index removal verified redundant: getUserRank (sportId==, ownerType==, elo>) is served by the retained (sportId, ownerType, elo DESC) index; no query uses elo ASC or isLessThan.
  • Future.wait fan-out reconstructs results index-aligned, still bounded by maxResults.

One minor content asymmetry noted (limited-mode ELO answer dropped the rank the LLM path included) has been fixed in 19c74e3 with a chatbotAnswerYourEloRanked ARB variant + tests.

flutter analyze clean, 60 chatbot tests green.

@M1RK02 M1RK02 changed the title feat(chatbot): Arena Assistant - LLM chat with live app data feat: arena assistant - LLM chatbot with live app data Jul 10, 2026
@M1RK02
M1RK02 merged commit 223134d into develop Jul 10, 2026
1 check passed
@M1RK02
M1RK02 deleted the feature/assistant branch July 10, 2026 10:19
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.

3 participants