diff --git a/.flutter-plugins-dependencies b/.flutter-plugins-dependencies index aa06683..cd1a086 100644 --- a/.flutter-plugins-dependencies +++ b/.flutter-plugins-dependencies @@ -1 +1 @@ -{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/nathan/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/nathan/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_auth-6.5.1/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_core-4.9.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/nathan/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_mobile","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_mobile-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.3.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/nathan/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/nathan/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/nathan/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/nathan/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/nathan/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/nathan/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"firebase_auth","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_auth-6.5.1/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_core-4.9.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/nathan/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.35/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/nathan/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_mobile","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_mobile-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_android","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_android-7.2.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/nathan/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/nathan/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/nathan/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/nathan/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.23/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/nathan/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/nathan/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.29/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/nathan/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/nathan/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_auth-6.5.1/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_core-4.9.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/nathan/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_desktop","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_desktop-0.2.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.3.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/nathan/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/nathan/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/nathan/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/nathan/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/nathan/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/nathan/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_desktop","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_desktop-0.2.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/nathan/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/nathan/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/nathan/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/nathan/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/nathan/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/nathan/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/nathan/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_auth-6.5.1/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_core-4.9.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/nathan/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_desktop","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_desktop-0.2.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/nathan/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/nathan/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/nathan/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/nathan/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/nathan/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/nathan/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/nathan/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","dependencies":[],"dev_dependency":false},{"name":"firebase_auth_web","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_auth_web-6.2.1/","dependencies":["firebase_core_web"],"dev_dependency":false},{"name":"firebase_core_web","path":"/home/nathan/.pub-cache/hosted/pub.dev/firebase_core_web-3.7.0/","dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/nathan/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_web","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_web-0.1.0/","dependencies":["google_sign_in_web"],"dev_dependency":false},{"name":"google_sign_in_web","path":"/home/nathan/.pub-cache/hosted/pub.dev/google_sign_in_web-1.1.3/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/nathan/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/nathan/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.3/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"firebase_auth","dependencies":["firebase_auth_web","firebase_core"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_tts","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_all_platforms","dependencies":["google_sign_in_all_platforms_desktop","google_sign_in_all_platforms_mobile","google_sign_in_all_platforms_web"]},{"name":"google_sign_in_all_platforms_desktop","dependencies":["url_launcher"]},{"name":"google_sign_in_all_platforms_mobile","dependencies":["google_sign_in"]},{"name":"google_sign_in_all_platforms_web","dependencies":["google_sign_in","google_sign_in_web","google_sign_in_all_platforms_mobile"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqlite3_flutter_libs","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-06-05 17:44:56.859820","version":"3.44.0","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file +{"info":"This is a generated file; do not edit or check into version control.","plugins":{"ios":[{"name":"connectivity_plus","path":"/home/chh/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/chh/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_auth-6.5.1/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_core-4.9.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/chh/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_mobile","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_mobile-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.3.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/chh/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/chh/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/chh/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_ios","path":"/home/chh/.pub-cache/hosted/pub.dev/url_launcher_ios-6.4.1/","native_build":true,"dependencies":[],"dev_dependency":false}],"android":[{"name":"connectivity_plus","path":"/home/chh/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/chh/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":true,"dependencies":["flutter_plugin_android_lifecycle"],"dev_dependency":false},{"name":"firebase_auth","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_auth-6.5.1/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_core-4.9.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_plugin_android_lifecycle","path":"/home/chh/.pub-cache/hosted/pub.dev/flutter_plugin_android_lifecycle-2.0.35/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/chh/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_mobile","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_mobile-0.2.1/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_android","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_android-7.2.11/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/chh/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"jni_flutter","path":"/home/chh/.pub-cache/hosted/pub.dev/jni_flutter-1.0.1/","native_build":true,"dependencies":["jni"],"dev_dependency":false},{"name":"path_provider_android","path":"/home/chh/.pub-cache/hosted/pub.dev/path_provider_android-2.3.1/","native_build":false,"dependencies":["jni","jni_flutter"],"dev_dependency":false},{"name":"shared_preferences_android","path":"/home/chh/.pub-cache/hosted/pub.dev/shared_preferences_android-2.4.23/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/chh/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_android","path":"/home/chh/.pub-cache/hosted/pub.dev/url_launcher_android-6.3.29/","native_build":true,"dependencies":[],"dev_dependency":false}],"macos":[{"name":"connectivity_plus","path":"/home/chh/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/chh/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_auth-6.5.1/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_core-4.9.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/chh/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_desktop","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_desktop-0.2.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_ios","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_ios-6.3.0/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_foundation","path":"/home/chh/.pub-cache/hosted/pub.dev/path_provider_foundation-2.6.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_foundation","path":"/home/chh/.pub-cache/hosted/pub.dev/shared_preferences_foundation-2.5.6/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/chh/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","shared_darwin_source":true,"native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_macos","path":"/home/chh/.pub-cache/hosted/pub.dev/url_launcher_macos-3.2.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"linux":[{"name":"connectivity_plus","path":"/home/chh/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/chh/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_desktop","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_desktop-0.2.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/chh/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_linux","path":"/home/chh/.pub-cache/hosted/pub.dev/path_provider_linux-2.2.1/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_linux","path":"/home/chh/.pub-cache/hosted/pub.dev/shared_preferences_linux-2.4.1/","native_build":false,"dependencies":["path_provider_linux"],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/chh/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_linux","path":"/home/chh/.pub-cache/hosted/pub.dev/url_launcher_linux-3.2.2/","native_build":true,"dependencies":[],"dev_dependency":false}],"windows":[{"name":"connectivity_plus","path":"/home/chh/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/chh/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"firebase_auth","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_auth-6.5.1/","native_build":true,"dependencies":["firebase_core"],"dev_dependency":false},{"name":"firebase_core","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_core-4.9.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/chh/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_desktop","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_desktop-0.2.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"jni","path":"/home/chh/.pub-cache/hosted/pub.dev/jni-1.0.0/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"path_provider_windows","path":"/home/chh/.pub-cache/hosted/pub.dev/path_provider_windows-2.3.0/","native_build":false,"dependencies":[],"dev_dependency":false},{"name":"shared_preferences_windows","path":"/home/chh/.pub-cache/hosted/pub.dev/shared_preferences_windows-2.4.1/","native_build":false,"dependencies":["path_provider_windows"],"dev_dependency":false},{"name":"sqlite3_flutter_libs","path":"/home/chh/.pub-cache/hosted/pub.dev/sqlite3_flutter_libs-0.5.42/","native_build":true,"dependencies":[],"dev_dependency":false},{"name":"url_launcher_windows","path":"/home/chh/.pub-cache/hosted/pub.dev/url_launcher_windows-3.1.5/","native_build":true,"dependencies":[],"dev_dependency":false}],"web":[{"name":"connectivity_plus","path":"/home/chh/.pub-cache/hosted/pub.dev/connectivity_plus-6.1.5/","dependencies":[],"dev_dependency":false},{"name":"file_picker","path":"/home/chh/.pub-cache/hosted/pub.dev/file_picker-8.3.7/","dependencies":[],"dev_dependency":false},{"name":"firebase_auth_web","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_auth_web-6.2.1/","dependencies":["firebase_core_web"],"dev_dependency":false},{"name":"firebase_core_web","path":"/home/chh/.pub-cache/hosted/pub.dev/firebase_core_web-3.7.0/","dependencies":[],"dev_dependency":false},{"name":"flutter_tts","path":"/home/chh/.pub-cache/hosted/pub.dev/flutter_tts-4.2.5/","dependencies":[],"dev_dependency":false},{"name":"google_sign_in_all_platforms_web","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_all_platforms_web-0.1.0/","dependencies":["google_sign_in_web"],"dev_dependency":false},{"name":"google_sign_in_web","path":"/home/chh/.pub-cache/hosted/pub.dev/google_sign_in_web-1.1.3/","dependencies":[],"dev_dependency":false},{"name":"shared_preferences_web","path":"/home/chh/.pub-cache/hosted/pub.dev/shared_preferences_web-2.4.3/","dependencies":[],"dev_dependency":false},{"name":"url_launcher_web","path":"/home/chh/.pub-cache/hosted/pub.dev/url_launcher_web-2.4.3/","dependencies":[],"dev_dependency":false}]},"dependencyGraph":[{"name":"connectivity_plus","dependencies":[]},{"name":"file_picker","dependencies":["flutter_plugin_android_lifecycle"]},{"name":"firebase_auth","dependencies":["firebase_auth_web","firebase_core"]},{"name":"firebase_auth_web","dependencies":["firebase_core","firebase_core_web"]},{"name":"firebase_core","dependencies":["firebase_core_web"]},{"name":"firebase_core_web","dependencies":[]},{"name":"flutter_plugin_android_lifecycle","dependencies":[]},{"name":"flutter_tts","dependencies":[]},{"name":"google_sign_in","dependencies":["google_sign_in_android","google_sign_in_ios","google_sign_in_web"]},{"name":"google_sign_in_all_platforms","dependencies":["google_sign_in_all_platforms_desktop","google_sign_in_all_platforms_mobile","google_sign_in_all_platforms_web"]},{"name":"google_sign_in_all_platforms_desktop","dependencies":["url_launcher"]},{"name":"google_sign_in_all_platforms_mobile","dependencies":["google_sign_in"]},{"name":"google_sign_in_all_platforms_web","dependencies":["google_sign_in","google_sign_in_web","google_sign_in_all_platforms_mobile"]},{"name":"google_sign_in_android","dependencies":[]},{"name":"google_sign_in_ios","dependencies":[]},{"name":"google_sign_in_web","dependencies":[]},{"name":"jni","dependencies":[]},{"name":"jni_flutter","dependencies":["jni"]},{"name":"path_provider","dependencies":["path_provider_android","path_provider_foundation","path_provider_linux","path_provider_windows"]},{"name":"path_provider_android","dependencies":["jni","jni_flutter"]},{"name":"path_provider_foundation","dependencies":[]},{"name":"path_provider_linux","dependencies":[]},{"name":"path_provider_windows","dependencies":[]},{"name":"shared_preferences","dependencies":["shared_preferences_android","shared_preferences_foundation","shared_preferences_linux","shared_preferences_web","shared_preferences_windows"]},{"name":"shared_preferences_android","dependencies":[]},{"name":"shared_preferences_foundation","dependencies":[]},{"name":"shared_preferences_linux","dependencies":["path_provider_linux"]},{"name":"shared_preferences_web","dependencies":[]},{"name":"shared_preferences_windows","dependencies":["path_provider_windows"]},{"name":"sqlite3_flutter_libs","dependencies":[]},{"name":"url_launcher","dependencies":["url_launcher_android","url_launcher_ios","url_launcher_linux","url_launcher_macos","url_launcher_web","url_launcher_windows"]},{"name":"url_launcher_android","dependencies":[]},{"name":"url_launcher_ios","dependencies":[]},{"name":"url_launcher_linux","dependencies":[]},{"name":"url_launcher_macos","dependencies":[]},{"name":"url_launcher_web","dependencies":[]},{"name":"url_launcher_windows","dependencies":[]}],"date_created":"2026-06-06 02:05:50.623480","version":"3.44.1","swift_package_manager_enabled":{"ios":false,"macos":false}} \ No newline at end of file diff --git a/lib/screens/home_screen.dart b/lib/screens/home_screen.dart index d05c0da..0a63efd 100644 --- a/lib/screens/home_screen.dart +++ b/lib/screens/home_screen.dart @@ -4,6 +4,7 @@ import '../admin/widgets/admin_app_bar_action.dart'; import '../models/app_user.dart'; import '../models/guess_score_summary.dart'; import '../models/incoming_question.dart'; +import '../models/question_source_preference.dart'; import '../utils/guess_slot_format.dart'; import '../models/sync_result.dart'; import '../models/user_profile.dart'; @@ -190,6 +191,8 @@ class HomeScreen extends StatelessWidget { return Column( crossAxisAlignment: CrossAxisAlignment.stretch, children: [ + if (panelOpen) + _QuestionSourceConfigTopBar(sources: sources), if (showQuestionPanel) Expanded( child: Padding( @@ -625,3 +628,103 @@ class _SyncStatusChip extends StatelessWidget { ); } } + +class _QuestionSourceConfigTopBar extends StatelessWidget { + const _QuestionSourceConfigTopBar({ + required this.sources, + }); + + final List sources; + + @override + Widget build(BuildContext context) { + final Map mix = + QuestionSourceConfigRepository.effectiveMix(sources); + final String mixText = mix.isEmpty + ? 'No sources enabled' + : mix.entries.map((MapEntry entry) { + final QuestionSourcePreference source = sources.firstWhere( + (QuestionSourcePreference s) => s.id == entry.key, + ); + return '${source.label}: ${entry.value}%'; + }).join(', '); + + return Container( + key: const Key('question-source-config-top-bar'), + decoration: BoxDecoration( + color: AppColors.surfaceElevated, + border: Border( + bottom: BorderSide( + color: AppColors.accent.withValues(alpha: 0.15), + width: 1, + ), + ), + ), + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Row( + children: [ + const Icon( + Icons.tune_outlined, + color: AppColors.accent, + size: 20, + ), + const SizedBox(width: 10), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + 'Question source configuration', + style: TextStyle( + color: AppColors.textPrimary, + fontSize: 13, + fontWeight: FontWeight.w600, + ), + ), + const SizedBox(height: 2), + Text( + mixText, + style: const TextStyle( + color: AppColors.textSecondary, + fontSize: 11, + ), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + TextButton.icon( + key: const Key('question-source-config-top-bar-button'), + onPressed: () => QuestionSourceSettingsSheet.show(context), + icon: const Icon( + Icons.settings_outlined, + size: 14, + color: AppColors.accent, + ), + label: const Text( + 'Configure', + style: TextStyle( + color: AppColors.accent, + fontSize: 12, + fontWeight: FontWeight.bold, + ), + ), + style: TextButton.styleFrom( + padding: const EdgeInsets.symmetric(horizontal: 12, vertical: 6), + minimumSize: Size.zero, + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + backgroundColor: AppColors.accent.withValues(alpha: 0.08), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(6), + side: BorderSide( + color: AppColors.accent.withValues(alpha: 0.2), + ), + ), + ), + ), + ], + ), + ); + } +} diff --git a/lib/services/questions_hub_service.dart b/lib/services/questions_hub_service.dart index 18b2a9e..2ac9175 100644 --- a/lib/services/questions_hub_service.dart +++ b/lib/services/questions_hub_service.dart @@ -126,6 +126,16 @@ class QuestionsHubService { return; } final IncomingQuestion question = IncomingQuestion.fromJson(json); + + if (questionActionBusy.value) { + debugPrint('ReceiveQuestion: ignored push because action is busy'); + return; + } + if (pendingQuestion.value != null && pendingQuestion.value!.id != question.id) { + debugPrint('ReceiveQuestion: ignored push because another question is active'); + return; + } + _applyIncoming(question); } @@ -307,8 +317,21 @@ class QuestionsHubService { if (refreshed.isEmpty) { if (result.nextQuestion != null && _acceptsQuestionSource(result.nextQuestion!)) { + IncomingQuestion nextQ = result.nextQuestion!; + if (nextQ.needsPrepare) { + try { + final IncomingQuestion? prepared = await _api.prepareSpellingQuestion( + questionId: nextQ.id, + ); + if (prepared != null) { + nextQ = prepared; + } + } catch (e) { + debugPrint('Failed to prepare next question in defer: $e'); + } + } _setActiveQuestion( - result.nextQuestion!, + nextQ, unansweredCount: result.unansweredCount, ); } else if (_shouldKeepPanelOpenWithoutQuestion()) { @@ -334,6 +357,19 @@ class QuestionsHubService { next = result.nextQuestion!; } + if (next.needsPrepare) { + try { + final IncomingQuestion? prepared = await _api.prepareSpellingQuestion( + questionId: next.id, + ); + if (prepared != null) { + next = prepared; + } + } catch (e) { + debugPrint('Failed to prepare next question in defer: $e'); + } + } + _setActiveQuestion( next, unansweredCount: result.unansweredCount, @@ -372,10 +408,15 @@ class QuestionsHubService { return; } + num effectiveAnswer = answer; + if (question.isSpellingFollowUp && answer == 0) { + effectiveAnswer = 1; + } + await _submitAnswer( answeredQuestionId: question.id, question: question, - answer: answer, + answer: effectiveAnswer, ); } @@ -386,6 +427,7 @@ class QuestionsHubService { String? answerText, }) async { questionActionBusy.value = true; + final DateTime startTime = DateTime.now(); try { final QuestionSubmitResult? result = await _api.submitAnswer( questionId: answeredQuestionId, @@ -396,14 +438,35 @@ class QuestionsHubService { return; } + if (question.usesSpellingWordTile) { + final int elapsedMs = DateTime.now().difference(startTime).inMilliseconds; + final int remainingMs = 1000 - elapsedMs; + if (remainingMs > 0) { + await Future.delayed(Duration(milliseconds: remainingMs)); + } + } + if (question.isMarketHistory) { guessScoreSummary.value = result.score; } if (result.nextQuestion != null && _acceptsQuestionSource(result.nextQuestion!)) { + IncomingQuestion nextQ = result.nextQuestion!; + if (nextQ.needsPrepare) { + try { + final IncomingQuestion? prepared = await _api.prepareSpellingQuestion( + questionId: nextQ.id, + ); + if (prepared != null) { + nextQ = prepared; + } + } catch (e) { + debugPrint('Failed to prepare next question in submit: $e'); + } + } _setActiveQuestion( - result.nextQuestion!, + nextQ, unansweredCount: result.unansweredCount, ); return; diff --git a/lib/widgets/question_presentation_router.dart b/lib/widgets/question_presentation_router.dart index a961808..c9f9fbc 100644 --- a/lib/widgets/question_presentation_router.dart +++ b/lib/widgets/question_presentation_router.dart @@ -44,9 +44,11 @@ class QuestionPresentationRouter extends StatelessWidget { return SwipeQuestionTile( key: ValueKey(question.id), questionId: question.id, + questionText: question.text, busy: busy, onSwipeRight: onAnswer, onSwipeLeft: onDefer, + showGuidGlyph: !question.isSpelling, ); } } diff --git a/lib/widgets/spelling_question_tile.dart b/lib/widgets/spelling_question_tile.dart index 6b67170..9277e5b 100644 --- a/lib/widgets/spelling_question_tile.dart +++ b/lib/widgets/spelling_question_tile.dart @@ -195,12 +195,23 @@ class _SpellingQuestionTileState extends State { _showingFeedback = true; }); - await Future.delayed(const Duration(seconds: 1)); - if (!mounted) { - return; + final DateTime startTime = DateTime.now(); + try { + await widget.onSubmitAnswer(answer); + } catch (_) { + final int elapsedMs = DateTime.now().difference(startTime).inMilliseconds; + final int remainingMs = 1000 - elapsedMs; + if (remainingMs > 0) { + await Future.delayed(Duration(milliseconds: remainingMs)); + } + if (mounted) { + setState(() { + _showingFeedback = false; + _answerCorrect = null; + }); + } + rethrow; } - - await widget.onSubmitAnswer(answer); } @override diff --git a/lib/widgets/swipe_question_tile.dart b/lib/widgets/swipe_question_tile.dart index 98ae7f2..80ba6e8 100644 --- a/lib/widgets/swipe_question_tile.dart +++ b/lib/widgets/swipe_question_tile.dart @@ -33,12 +33,14 @@ class SwipeQuestionTile extends StatefulWidget { required this.questionId, required this.onSwipeRight, required this.onSwipeLeft, + this.questionText, this.busy = false, this.swipeAxisToleranceDegrees = 25, this.showGuidGlyph = true, }); final String questionId; + final String? questionText; final Future Function(num answer) onSwipeRight; final Future Function() onSwipeLeft; final bool busy; @@ -82,7 +84,7 @@ class _SwipeQuestionTileState extends State /// Updated each build from the tile height so ±10 reaches near the track edges. double _maxVerticalDrag = 120; - bool get _atZero => _snappedSliderValue == 0; + bool get _atZero => widget.showGuidGlyph ? _snappedSliderValue == 0 : false; bool get _holdSaveEnabled => !widget.busy && !_acting && !_atZero; @@ -326,6 +328,9 @@ class _SwipeQuestionTileState extends State _snappedSliderValue / _sliderMax * _maxVerticalDrag; void _applyVerticalInputDelta(double dy) { + if (!widget.showGuidGlyph) { + return; + } setState(() { _verticalOffset -= dy; _verticalOffset = _verticalOffset.clamp( @@ -468,7 +473,7 @@ class _SwipeQuestionTileState extends State /// Steps the slider by [delta] integers, clamped to [-10, 10]. void _stepSliderBy(int delta) { - if (widget.busy || _acting || delta == 0) { + if (widget.busy || _acting || delta == 0 || !widget.showGuidGlyph) { return; } final int next = (_snappedSliderValue + delta).clamp(_sliderMin, _sliderMax); @@ -743,6 +748,26 @@ class _SwipeQuestionTileState extends State ), ), ), + ) + else if (widget.questionText != null) + Positioned( + top: 8, + bottom: 8, + left: _centerSideInset, + right: _centerSideInset, + child: Center( + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 16), + child: Text( + widget.questionText!, + style: Theme.of(context).textTheme.titleMedium?.copyWith( + fontWeight: FontWeight.w600, + color: AppColors.textPrimary, + ), + textAlign: TextAlign.center, + ), + ), + ), ), Positioned( left: 0, diff --git a/server/bin/server.dart b/server/bin/server.dart index 0e73b67..3ea0378 100644 --- a/server/bin/server.dart +++ b/server/bin/server.dart @@ -101,6 +101,7 @@ Future main() async { marketDataDb: marketDataDb, tradingConfigDb: tradingConfigDb, tradingStateDb: tradingStateDb, + questionSourcePreferencesDb: questionSourcePreferencesDb, marketHistoryQuery: MarketHistoryQuery(connection: db.connection), marketHistoryEnv: mh, guardrails: Guardrails(allowLive: env.alpaca.allowLive), diff --git a/server/lib/pipeline/question_pipeline.dart b/server/lib/pipeline/question_pipeline.dart index 10121b9..e1295ed 100644 --- a/server/lib/pipeline/question_pipeline.dart +++ b/server/lib/pipeline/question_pipeline.dart @@ -198,10 +198,11 @@ class QuestionPipeline { } final List prefs = await preferencesDb.getResolved(firebaseUid); + final Map mix = QuestionSourcePreferences.effectiveMix(prefs); final Set allowed = prefs .where( (ResolvedQuestionSourcePreference s) => - s.enabled && !s.comingSoon, + s.enabled && !s.comingSoon && (mix[s.id] ?? 0) > 0, ) .map((ResolvedQuestionSourcePreference s) => s.id) .toSet(); @@ -229,7 +230,7 @@ class QuestionPipeline { ); } - if (!await _canEnqueue(firebaseUid)) { + if (pipelineKey != PipelineKeys.spelling && !await _canEnqueue(firebaseUid)) { return; } @@ -730,8 +731,8 @@ class QuestionPipeline { /// question is immediately dispatchable and the client never needs a separate /// prepare call. Falls back to a [spelling_pending] placeholder only when no /// catalog words are available. - Future _startSpellingPipeline(String firebaseUid) async { - if (!await _canEnqueue(firebaseUid)) { + Future _startSpellingPipeline(String firebaseUid, {bool force = false}) async { + if (!force && !await _canEnqueue(firebaseUid)) { return; } if (!await _isSpellingEnabled(firebaseUid)) { @@ -843,7 +844,7 @@ class QuestionPipeline { ); case PipelineSteps.spellingFollowUp: if (userResponse > 0) { - await _startSpellingPipeline(firebaseUid); + await _startSpellingPipeline(firebaseUid, force: true); } else { await _setIdle(firebaseUid, PipelineKeys.spelling); } diff --git a/server/lib/question_dispatcher.dart b/server/lib/question_dispatcher.dart index 4486f8d..69fb181 100644 --- a/server/lib/question_dispatcher.dart +++ b/server/lib/question_dispatcher.dart @@ -1,3 +1,5 @@ +import 'dart:math'; + import 'pipeline/question_pipeline.dart'; import 'question_service.dart'; import 'question_source_mix.dart'; @@ -14,15 +16,18 @@ class QuestionDispatcher { required QuestionSourcePreferencesDb preferencesDb, this.debugLoggingEnabled = false, this.maxQueuedQuestions = 3, + Random? random, }) : _questionsDb = questionsDb, _questionService = questionService, _questionPipeline = questionPipeline, - _preferencesDb = preferencesDb; + _preferencesDb = preferencesDb, + _random = random ?? Random(); final QuestionsDb _questionsDb; final QuestionService _questionService; final QuestionPipeline _questionPipeline; final QuestionSourcePreferencesDb _preferencesDb; + final Random _random; final bool debugLoggingEnabled; final int maxQueuedQuestions; @@ -99,12 +104,25 @@ class QuestionDispatcher { required Map answeredQuestion, }) async { _log('nextQuestionAfterAnswer($firebaseUid)'); + final String answeredSourceId = QuestionsDb.resolveSourceId(answeredQuestion); + var rows = await _listEnabledUnansweredOrdered(firebaseUid); if (rows.isEmpty) { _log('Queue empty, falling back to dispatchNextQuestion'); return dispatchNextQuestion(firebaseUid); } + // Pipeline continuity: If we just answered a spelling question and another + // spelling question is available (e.g. follow-up or next word quiz), return it + // immediately. Pipelines should not be interrupted by mix changes. + if (answeredSourceId == 'spelling') { + for (final Map row in rows) { + if (QuestionsDb.resolveSourceId(row) == 'spelling') { + return _toClientPayload(row, rows.length); + } + } + } + final List prefs = await _preferencesDb.getResolved(firebaseUid); final Map mix = QuestionSourceMix.effectiveMixFromResolved(prefs); @@ -118,9 +136,8 @@ class QuestionDispatcher { firebaseUid, effectiveMixPercent: mix, deliveredCounts: delivered, - afterAnsweringSourceId: - QuestionsDb.resolveSourceId(answeredQuestion), allowedSourceIds: allowed, + random: _random, ); if (row == null) { return dispatchNextQuestion(firebaseUid); @@ -187,6 +204,7 @@ class QuestionDispatcher { final String primary = QuestionSourceMix.pickNextSourceId( effectiveMixPercent: mix, deliveredCounts: delivered, + random: _random, ); _log('_enqueueOneFromMix: picked primary=$primary (mix=$mix, delivered=$delivered)'); final List order = _sourceTryOrderForEnqueue( @@ -229,6 +247,7 @@ class QuestionDispatcher { firebaseUid, effectiveMixPercent: mix, allowedSourceIds: allowed, + random: _random, ); } diff --git a/server/lib/question_source_mix.dart b/server/lib/question_source_mix.dart index a1cc66f..931745b 100644 --- a/server/lib/question_source_mix.dart +++ b/server/lib/question_source_mix.dart @@ -1,44 +1,42 @@ +import 'dart:math'; + import 'question_source_preferences.dart'; import 'questions_db.dart'; -/// Picks the next question source to match configured mix percentages over time. +/// Picks the next question source randomly matching configured mix percentages. abstract final class QuestionSourceMix { - /// Source id with the largest deficit vs configured effective mix (sequential balance). + /// Source id selected using weighted random selection based on configured mix ratios. static String pickNextSourceId({ required Map effectiveMixPercent, required Map deliveredCounts, + Random? random, }) { if (effectiveMixPercent.isEmpty) { return 'market_history'; } - final int totalDelivered = - deliveredCounts.values.fold(0, (int a, int b) => a + b); - if (totalDelivered == 0) { - return effectiveMixPercent.entries - .reduce( - ( - MapEntry a, - MapEntry b, - ) => - a.value >= b.value ? a : b, - ) - .key; + final int totalWeight = + effectiveMixPercent.values.fold(0, (int a, int b) => a + b); + if (totalWeight <= 0) { + return effectiveMixPercent.keys.first; } - String? pick; - var maxDeficit = double.negativeInfinity; + // If one source has 100% of the active weight, bypass the random roll completely. for (final MapEntry entry in effectiveMixPercent.entries) { - final double target = entry.value / 100.0; - final double actual = - (deliveredCounts[entry.key] ?? 0) / totalDelivered; - final double deficit = target - actual; - if (deficit > maxDeficit) { - maxDeficit = deficit; - pick = entry.key; + if (entry.value == totalWeight) { + return entry.key; } } - return pick ?? effectiveMixPercent.keys.first; + + final Random rng = random ?? Random(); + var roll = rng.nextInt(totalWeight); + for (final MapEntry entry in effectiveMixPercent.entries) { + roll -= entry.value; + if (roll < 0) { + return entry.key; + } + } + return effectiveMixPercent.keys.first; } static Map effectiveMixFromResolved( diff --git a/server/lib/questions_db.dart b/server/lib/questions_db.dart index 7f4a60b..4c56368 100644 --- a/server/lib/questions_db.dart +++ b/server/lib/questions_db.dart @@ -1,4 +1,5 @@ import 'dart:convert'; +import 'dart:math'; import 'package:postgres/postgres.dart'; import 'package:uuid/uuid.dart'; @@ -273,8 +274,8 @@ class QuestionsDb { String assignedUserId, { required Map effectiveMixPercent, Map? deliveredCounts, - String? afterAnsweringSourceId, Set? allowedSourceIds, + Random? random, }) async { List> rows = await listUnansweredQuestions(assignedUserId); @@ -285,19 +286,12 @@ class QuestionsDb { return null; } - if (afterAnsweringSourceId == 'spelling') { - for (final Map row in rows) { - if (resolveSourceId(row) == 'spelling') { - return row; - } - } - } - final Map delivered = deliveredCounts ?? await countDeliveredBySourceId(assignedUserId); final String preferred = QuestionSourceMix.pickNextSourceId( effectiveMixPercent: effectiveMixPercent, deliveredCounts: delivered, + random: random, ); final List> ordered = QuestionSourceMix.orderUnansweredByPreferredSource( @@ -312,6 +306,7 @@ class QuestionsDb { String assignedUserId, { required Map effectiveMixPercent, Set? allowedSourceIds, + Random? random, }) async { List> rows = await listUnansweredQuestions(assignedUserId); @@ -326,6 +321,7 @@ class QuestionsDb { final String preferred = QuestionSourceMix.pickNextSourceId( effectiveMixPercent: effectiveMixPercent, deliveredCounts: delivered, + random: random, ); return QuestionSourceMix.orderUnansweredByPreferredSource( rows, diff --git a/server/lib/trading/trading_pipeline.dart b/server/lib/trading/trading_pipeline.dart index 8ed8467..2ba63f6 100644 --- a/server/lib/trading/trading_pipeline.dart +++ b/server/lib/trading/trading_pipeline.dart @@ -15,6 +15,7 @@ import 'symbol_obfuscator.dart'; import 'trading_config.dart'; import 'trading_config_db.dart'; import 'user_trading_state_db.dart'; +import '../question_source_preferences_db.dart'; /// Result of one [TradingPipeline.evaluate] cycle for a single user. class TradingEvaluationResult { @@ -39,6 +40,7 @@ class TradingPipeline { required MarketDataDb marketDataDb, required TradingConfigDb tradingConfigDb, required UserTradingStateDb tradingStateDb, + QuestionSourcePreferencesDb? questionSourcePreferencesDb, RuleEngine? ruleEngine, MarketHistoryQuery? marketHistoryQuery, MarketHistoryEnv? marketHistoryEnv, @@ -51,6 +53,7 @@ class TradingPipeline { _marketDataDb = marketDataDb, _tradingConfigDb = tradingConfigDb, _tradingStateDb = tradingStateDb, + _questionSourcePreferencesDb = questionSourcePreferencesDb, _ruleEngine = ruleEngine ?? RuleEngine(clock: clock), _marketHistoryQuery = marketHistoryQuery, _marketHistoryEnv = marketHistoryEnv ?? MarketHistoryEnv.fromMap({}), @@ -64,6 +67,7 @@ class TradingPipeline { final MarketDataDb _marketDataDb; final TradingConfigDb _tradingConfigDb; final UserTradingStateDb _tradingStateDb; + final QuestionSourcePreferencesDb? _questionSourcePreferencesDb; final RuleEngine _ruleEngine; final MarketHistoryQuery? _marketHistoryQuery; final MarketHistoryEnv _marketHistoryEnv; @@ -85,6 +89,15 @@ class TradingPipeline { final List fired = []; final List skipped = []; + if (_questionSourcePreferencesDb != null && + !await _questionSourcePreferencesDb!.isSourceEnabled(firebaseUid, 'market_history')) { + return TradingEvaluationResult( + questionsCreated: 0, + rulesFired: fired, + rulesSkipped: skipped, + ); + } + final EffectiveTradingConfig? config = await _tradingConfigDb.resolveEffectiveConfig(firebaseUid); if (config == null || !config.enabled) { diff --git a/server/test/helpers/test_db.dart b/server/test/helpers/test_db.dart index 29d629b..b48fe47 100644 --- a/server/test/helpers/test_db.dart +++ b/server/test/helpers/test_db.dart @@ -1,4 +1,5 @@ import 'dart:io'; +import 'dart:math'; import 'package:cyberhybridhub_server/db.dart'; import 'package:cyberhybridhub_server/question_source_preferences_db.dart'; @@ -138,13 +139,14 @@ class TestDb { testMode: testMode, ); - QuestionDispatcher questionDispatcher({bool testMode = true}) { + QuestionDispatcher questionDispatcher({bool testMode = true, Random? random}) { final QuestionPipeline pipeline = questionPipeline(testMode: testMode); final QuestionDispatcher dispatcher = QuestionDispatcher( questionsDb: questionsDb, questionService: questionService(), questionPipeline: pipeline, preferencesDb: questionSourcePreferencesDb, + random: random ?? Random(1), ); pipeline.attachQuestionDispatcher(dispatcher); return dispatcher; diff --git a/server/test/question_source_mix_test.dart b/server/test/question_source_mix_test.dart index 12e5c16..ba458a5 100644 --- a/server/test/question_source_mix_test.dart +++ b/server/test/question_source_mix_test.dart @@ -1,53 +1,48 @@ +import 'dart:math'; + import 'package:cyberhybridhub_server/question_source_mix.dart'; import 'package:cyberhybridhub_server/questions_db.dart'; import 'package:test/test.dart'; void main() { group('QuestionSourceMix.pickNextSourceId', () { - test('picks highest share when nothing delivered yet', () { + test('weighted random selection works deterministically with seeded random', () { + final Map mix = { + 'market_history': 70, + 'spelling': 30, + }; + + var marketCount = 0; + var spellingCount = 0; + final Random random = Random(42); + + for (var i = 0; i < 200; i++) { + final String pick = QuestionSourceMix.pickNextSourceId( + effectiveMixPercent: mix, + deliveredCounts: {}, + random: random, + ); + if (pick == 'market_history') { + marketCount++; + } else if (pick == 'spelling') { + spellingCount++; + } + } + + expect(marketCount, greaterThan(120)); + expect(spellingCount, greaterThan(40)); + expect(marketCount + spellingCount, 200); + }); + + test('always picks the only active source', () { expect( QuestionSourceMix.pickNextSourceId( - effectiveMixPercent: { - 'market_history': 70, - 'spelling': 30, - }, + effectiveMixPercent: {'spelling': 100}, deliveredCounts: {}, ), - 'market_history', - ); - }); - - test('picks spelling when market is ahead of target mix', () { - expect( - QuestionSourceMix.pickNextSourceId( - effectiveMixPercent: { - 'market_history': 50, - 'spelling': 50, - }, - deliveredCounts: { - 'market_history': 4, - 'spelling': 0, - }, - ), 'spelling', ); }); - - test('picks market when spelling is ahead of target mix', () { - expect( - QuestionSourceMix.pickNextSourceId( - effectiveMixPercent: { - 'market_history': 70, - 'spelling': 30, - }, - deliveredCounts: { - 'market_history': 1, - 'spelling': 3, - }, - ), - 'market_history', - ); - }); }); group('QuestionSourceMix.orderUnansweredByPreferredSource', () { diff --git a/test/question_source_settings_test.dart b/test/question_source_settings_test.dart index 82baa34..2f4c356 100644 --- a/test/question_source_settings_test.dart +++ b/test/question_source_settings_test.dart @@ -140,4 +140,52 @@ void main() { ); expect(find.byKey(const Key('question-source-settings-dialog')), findsNothing); }); + + testWidgets('Question Source config top bar appears when panel is open and configures source preferences', ( + WidgetTester tester, + ) async { + final QuestionsHubService hub = QuestionsHubService.instance; + hub.hasPendingQuestion.value = true; + hub.pendingQuestionCount.value = 1; + hub.questionPanelOpen.value = true; + + addTearDown(() { + hub.hasPendingQuestion.value = false; + hub.pendingQuestionCount.value = 0; + hub.questionPanelOpen.value = false; + QuestionSourceConfigRepository.instance.resetToDefaults(); + }); + + await tester.pumpWidget( + MaterialApp( + theme: buildAppTheme(), + home: const HomeScreen( + user: AppUser(uid: 'u1', displayName: 'Test User'), + ), + ), + ); + + // Verify config top bar is displayed when panel is open. + expect(find.byKey(const Key('question-source-config-top-bar')), findsOneWidget); + expect(find.text('Question source configuration'), findsOneWidget); + expect(find.text('Market question: 50%, Spelling: 50%'), findsOneWidget); + + // Click on the Configure button to open settings dialog. + expect(find.byKey(const Key('question-source-config-top-bar-button')), findsOneWidget); + await tester.tap(find.byKey(const Key('question-source-config-top-bar-button'))); + await tester.pumpAndSettle(); + + // Verify settings dialog is open. + expect(find.byKey(const Key('question-source-settings-dialog')), findsOneWidget); + + // Close settings dialog. + await tester.tap(find.byKey(const Key('question-source-settings-close'))); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('question-source-settings-dialog')), findsNothing); + + // Close panel and verify top bar is gone. + hub.closeQuestionPanel(); + await tester.pumpAndSettle(); + expect(find.byKey(const Key('question-source-config-top-bar')), findsNothing); + }); } diff --git a/test/widgets/question_presentation_router_test.dart b/test/widgets/question_presentation_router_test.dart index b2f84e3..ac89198 100644 --- a/test/widgets/question_presentation_router_test.dart +++ b/test/widgets/question_presentation_router_test.dart @@ -41,6 +41,10 @@ void main() { ); expect(find.byType(SwipeQuestionTile), findsOneWidget); + expect( + tester.widget(find.byType(SwipeQuestionTile)).showGuidGlyph, + true, + ); expect(find.byType(SpellingQuestionTile), findsNothing); }); @@ -66,6 +70,10 @@ void main() { ); expect(find.byType(SwipeQuestionTile), findsOneWidget); + expect( + tester.widget(find.byType(SwipeQuestionTile)).showGuidGlyph, + false, + ); expect(find.byType(SpellingQuestionTile), findsNothing); }); diff --git a/test/widgets/spelling_question_tile_test.dart b/test/widgets/spelling_question_tile_test.dart index 03a95df..65e31fb 100644 --- a/test/widgets/spelling_question_tile_test.dart +++ b/test/widgets/spelling_question_tile_test.dart @@ -29,7 +29,7 @@ void main() { await tester.tap(find.byKey(const Key('spelling-submit-answer'))); await tester.pump(); - expect(submitCount, 0); + expect(submitCount, 1); final Text feedback = tester.widget( find.byKey(const Key('spelling-feedback-word')), );