feat: add local-first private AI digest workflow
Migrate app code into canonical feature slices, add phone-only AI digest scheduling and review, wire local notification/background task support, and cover the flow with tests.
This commit is contained in:
@@ -0,0 +1,107 @@
|
||||
import 'package:flutter_riverpod/flutter_riverpod.dart';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
|
||||
const String _llmApiKeyStorageKey = 'llm_api_key';
|
||||
const String _llmProviderStorageKey = 'llm_provider';
|
||||
|
||||
enum LlmProvider { openai, anthropic, google }
|
||||
|
||||
class LlmConfigState {
|
||||
const LlmConfigState({
|
||||
this.apiKeyConfigured = false,
|
||||
this.provider = LlmProvider.openai,
|
||||
this.isConfigured = false,
|
||||
});
|
||||
|
||||
final bool apiKeyConfigured;
|
||||
final LlmProvider provider;
|
||||
final bool isConfigured;
|
||||
|
||||
LlmConfigState copyWith({
|
||||
bool? apiKeyConfigured,
|
||||
LlmProvider? provider,
|
||||
bool? isConfigured,
|
||||
}) {
|
||||
return LlmConfigState(
|
||||
apiKeyConfigured: apiKeyConfigured ?? this.apiKeyConfigured,
|
||||
provider: provider ?? this.provider,
|
||||
isConfigured: isConfigured ?? this.isConfigured,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class LlmConfigNotifier extends Notifier<LlmConfigState> {
|
||||
Future<void>? _initializeFuture;
|
||||
|
||||
@override
|
||||
LlmConfigState build() {
|
||||
return const LlmConfigState();
|
||||
}
|
||||
|
||||
Future<void> initialize() async {
|
||||
final Future<void>? inFlight = _initializeFuture;
|
||||
if (inFlight != null) {
|
||||
return inFlight;
|
||||
}
|
||||
_initializeFuture = _initializeFromStorage();
|
||||
return _initializeFuture;
|
||||
}
|
||||
|
||||
Future<void> _initializeFromStorage() async {
|
||||
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
|
||||
final String? storedKey = await secureStorage.read(
|
||||
key: _llmApiKeyStorageKey,
|
||||
);
|
||||
final String? storedProvider = await secureStorage.read(
|
||||
key: _llmProviderStorageKey,
|
||||
);
|
||||
|
||||
state = LlmConfigState(
|
||||
apiKeyConfigured: storedKey != null && storedKey.isNotEmpty,
|
||||
provider: storedProvider != null
|
||||
? LlmProvider.values.firstWhere(
|
||||
(LlmProvider p) => p.name == storedProvider,
|
||||
orElse: () => LlmProvider.openai,
|
||||
)
|
||||
: LlmProvider.openai,
|
||||
isConfigured: storedKey != null && storedKey.isNotEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setApiKey(String apiKey) async {
|
||||
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
|
||||
if (apiKey.isEmpty) {
|
||||
await secureStorage.delete(key: _llmApiKeyStorageKey);
|
||||
} else {
|
||||
await secureStorage.write(key: _llmApiKeyStorageKey, value: apiKey);
|
||||
}
|
||||
state = state.copyWith(
|
||||
apiKeyConfigured: apiKey.isNotEmpty,
|
||||
isConfigured: apiKey.isNotEmpty,
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> setProvider(LlmProvider provider) async {
|
||||
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
|
||||
await secureStorage.write(
|
||||
key: _llmProviderStorageKey,
|
||||
value: provider.name,
|
||||
);
|
||||
state = state.copyWith(provider: provider);
|
||||
}
|
||||
|
||||
Future<String?> getApiKey() async {
|
||||
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
|
||||
return secureStorage.read(key: _llmApiKeyStorageKey);
|
||||
}
|
||||
|
||||
Future<void> clearApiKey() async {
|
||||
const FlutterSecureStorage secureStorage = FlutterSecureStorage();
|
||||
await secureStorage.delete(key: _llmApiKeyStorageKey);
|
||||
state = state.copyWith(apiKeyConfigured: false, isConfigured: false);
|
||||
}
|
||||
}
|
||||
|
||||
final llmConfigProvider = NotifierProvider<LlmConfigNotifier, LlmConfigState>(
|
||||
LlmConfigNotifier.new,
|
||||
);
|
||||
Reference in New Issue
Block a user