feat: add AI recommendations flow for tenders
Implement company AI onboarding/recommendation models, services, repository, and tender UI integration with supporting tests. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
import 'package:tm_app/core/constants/pref_keys.dart';
|
||||
import 'package:tm_app/core/utils/logger.dart';
|
||||
import 'package:tm_app/core/utils/result.dart';
|
||||
import 'package:tm_app/data/services/company_ai_service.dart';
|
||||
import 'package:tm_app/data/services/model/onboarding_response/onboarding_response.dart';
|
||||
import 'package:tm_app/data/services/model/recommend_response/recommend_response.dart';
|
||||
|
||||
/// Outcome of [CompanyAiRepository.maybeStartOnboarding].
|
||||
enum AiOnboardingOutcome {
|
||||
/// Onboarding was started against the AI service.
|
||||
started,
|
||||
|
||||
/// Skipped: the company has no documents and no website yet.
|
||||
skippedNoData,
|
||||
|
||||
/// Skipped: this profile version was already onboarded.
|
||||
skippedUnchanged,
|
||||
|
||||
/// The onboarding request failed (network/server). Safe to retry later.
|
||||
failed,
|
||||
}
|
||||
|
||||
/// Coordinates AI onboarding and recommendation calls.
|
||||
///
|
||||
/// Onboarding is asynchronous on the AI side and only needs to run when the
|
||||
/// company's documents or website change, so this repository fingerprints that
|
||||
/// data in [SharedPreferences] and skips redundant calls — mirroring the
|
||||
/// `onCompanyProfileReady` guidance in the mobile integration guide.
|
||||
class CompanyAiRepository {
|
||||
CompanyAiRepository({
|
||||
required CompanyAiService companyAiService,
|
||||
required SharedPreferences prefs,
|
||||
}) : _companyAiService = companyAiService,
|
||||
_prefs = prefs;
|
||||
|
||||
final CompanyAiService _companyAiService;
|
||||
final SharedPreferences _prefs;
|
||||
|
||||
/// Starts onboarding only when the company has data to index and its
|
||||
/// fingerprint differs from the last successful run.
|
||||
Future<AiOnboardingOutcome> maybeStartOnboarding({
|
||||
required List<String> documentFileIds,
|
||||
required String? website,
|
||||
}) async {
|
||||
final hasWebsite = website != null && website.trim().isNotEmpty;
|
||||
if (documentFileIds.isEmpty && !hasWebsite) {
|
||||
return AiOnboardingOutcome.skippedNoData;
|
||||
}
|
||||
|
||||
final fingerprint = _fingerprint(documentFileIds, website);
|
||||
if (fingerprint == _prefs.getString(PrefKeys.onboardingFingerprint)) {
|
||||
return AiOnboardingOutcome.skippedUnchanged;
|
||||
}
|
||||
|
||||
final result = await _companyAiService.startOnboarding();
|
||||
switch (result) {
|
||||
case Ok<OnboardingResponse>():
|
||||
await _prefs.setString(PrefKeys.onboardingFingerprint, fingerprint);
|
||||
await _prefs.setInt(
|
||||
PrefKeys.onboardingStartedAt,
|
||||
DateTime.now().millisecondsSinceEpoch,
|
||||
);
|
||||
AppLogger().info('✅ AI onboarding started for fingerprint $fingerprint');
|
||||
return AiOnboardingOutcome.started;
|
||||
case Error<OnboardingResponse>():
|
||||
// Don't persist the fingerprint so the next profile load retries.
|
||||
AppLogger().logWarning('⚠️ AI onboarding failed: ${result.error}');
|
||||
return AiOnboardingOutcome.failed;
|
||||
}
|
||||
}
|
||||
|
||||
Future<Result<RecommendResponse>> getRecommendations() {
|
||||
return _companyAiService.getRecommendations();
|
||||
}
|
||||
|
||||
/// Epoch milliseconds of the last started onboarding, or null if none.
|
||||
int? lastOnboardingStartedAt() =>
|
||||
_prefs.getInt(PrefKeys.onboardingStartedAt);
|
||||
|
||||
/// Stable, order-independent fingerprint of the data the AI service indexes.
|
||||
/// Stored verbatim (it is not sensitive) so it compares reliably across app
|
||||
/// launches, unlike [String.hashCode].
|
||||
String _fingerprint(List<String> documentFileIds, String? website) {
|
||||
final ids = [...documentFileIds]..sort();
|
||||
return '${ids.join(',')}|${website?.trim() ?? ''}';
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user