feat: arena assistant - LLM chatbot with live app data - #30
Conversation
- 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).
…matching, queue lookup, DI wiring
05d64ed to
c9bb905
Compare
M1RK02
left a comment
There was a problem hiding this comment.
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, andexplainHowEloWorksare raw Dart literals. This is the exact thingbrain/task_board.md§C2 (line 226) specifies — "templated ARB answer ... localized limited-mode notice" — and C2 is checked done. Every user withoutdart_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 appendsavailabilityDisclaimerShortto the text, andFacilityStatusCardalways 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>inmain.dartis never read —ChatbotRepositorybuilds its own. Wiring it through so the provided instance is actually used. - Redundant Firestore index. The new
(ownerType, sportId, elo ASC)onratingsduplicates the existing(sportId, ownerType, elo DESC), whichrating_repository.dart:92already documents as servinggetUserRank(nodegreeProgram) — the dashboard runs that query today. Removing it. - N+1 enrichment.
getEmptyTables/getTournamentsTodayawait queue-length / participant-count per row sequentially. Parallelizing withFuture.wait(still bounded ≤25). - Timeout not distinguished.
LlmClientletsTimeoutExceptionfall through tounexpected_error. Mapping it to atimeoutreason 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.
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.
|
Pushed the fixes to Blocking
Bugs / nits
|
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
left a comment
There was a problem hiding this comment.
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.
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).--dart-define-from-file=dart_defines.json. The file is gitignored; copyapp/dart_defines.example.json→app/dart_defines.jsonand 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.--dart-define=SHOW_ASSISTANT_SETTINGS=truere-enables it for dev).(ownerType ASC, sportId ASC, elo ASC)onratingsto the manifest — needed byRatingRepository.getUserRank. Not yet deployed: until someone runsfirebase deploy --only firestore:indexes --project campus-arena-app, rank numbers are silently omitted (handled gracefully)./tournament/:id(RouteConstants.tournamentPath), currently a placeholder screen inapp_router.dart— replace that one builder with the real detail screen.catch (_)inChatCubitandProfileSetupCubit.Related Task
app_router.dartand defines the/tournament/:idcontract)Shared Contracts
facilities/{id}.status/.currentMatchId)facilities/{id}/queue/{uid}.position)The chatbot only reads through existing repositories; it never writes to any contract surface.
Shared Files
Touches
app_router.dart(adds/chatbot,/chatbot/settings, and the/tournament/:idplaceholder — purely additive). Pinging both of you now.Breaking Changes
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
developand rebased ondevelopbefore 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.)dart formatapplied).flutter analyze: 0 issues;flutter test test/features/chatbot: 58/58 pass locally).ScoringStrategycontract remains unmodified. (No new sport.)ChatCubit→const ChatState().)availabilityDisclaimerShortARB key — appended in code on every availability answer, never delegated to the LLM.UI Changes (If applicable)