import 'dart:convert'; import 'package:flutter_dotenv/flutter_dotenv.dart'; import 'package:google_generative_ai/google_generative_ai.dart'; import 'package:http/http.dart' as http; /// Unified AI text-enhancement service. /// /// Tries Gemini (free-tier flash/lite models) first, with automatic /// 429-retry across all discovered models. If every Gemini model fails /// (quota exhausted or any unrecoverable error) it seamlessly falls back /// to the DeepSeek API. /// /// Usage: /// ```dart /// final result = await AiService().enhanceText( /// myText, /// promptInstruction: 'Fix grammar and translate to English …', /// ); /// ``` class AiService { static final AiService _instance = AiService._internal(); factory AiService() => _instance; late final String _geminiApiKey; late final String _deepseekApiKey; /// Cached Gemini model IDs (flash / lite, generateContent-capable). List _geminiModels = []; AiService._internal() { final gKey = dotenv.env['GEMINI_API_KEY']; if (gKey == null || gKey.isEmpty) { throw Exception('GEMINI_API_KEY not found in .env'); } _geminiApiKey = gKey; final dsKey = dotenv.env['DEEPSEEK_API_KEY']; if (dsKey == null || dsKey.isEmpty) { throw Exception('DEEPSEEK_API_KEY not found in .env'); } _deepseekApiKey = dsKey; } // --------------------------------------------------------------------------- // Public API // --------------------------------------------------------------------------- /// Fixes spelling / grammar, improves clarity, and translates [text] to /// professional English. /// /// Supply [promptInstruction] to give the model field-specific context /// (e.g. "This is an IT helpdesk ticket subject …"). If omitted a /// sensible generic instruction is used. /// /// Order of preference: /// 1. Gemini flash / lite models (auto-retry on 429 across all models) /// 2. DeepSeek `deepseek-chat` (fallback on total Gemini failure) /// /// Throws only if **both** providers fail. /// [onFallbackToDeepSeek] is called (from the same isolate) just before /// switching to the DeepSeek provider, so callers can update UI accordingly. /// /// This method never throws — if both providers fail it returns [text] unchanged. Future enhanceText( String text, { String? promptInstruction, void Function()? onFallbackToDeepSeek, }) async { if (text.trim().isEmpty) return text; final instruction = promptInstruction ?? 'Fix spelling and grammar, improve clarity, and translate to ' 'professional English. Return ONLY the improved text, ' 'no explanations:'; final prompt = '$instruction\n\n"$text"'; // --- 1. Try Gemini --- try { return await _geminiGenerate(prompt, fallback: text); } catch (_) { // All Gemini models failed — fall through to DeepSeek. onFallbackToDeepSeek?.call(); } // --- 2. Fallback: DeepSeek --- try { return await _deepseekGenerate(prompt, fallback: text); } catch (_) { // Both providers failed — return original text unchanged. return text; } } // --------------------------------------------------------------------------- // Gemini // --------------------------------------------------------------------------- Future> _getGeminiModels() async { if (_geminiModels.isNotEmpty) return _geminiModels; try { final uri = Uri.parse( 'https://generativelanguage.googleapis.com/v1beta/models' '?key=$_geminiApiKey', ); final res = await http.get(uri); if (res.statusCode == 200) { final data = jsonDecode(res.body) as Map; final rawModels = (data['models'] as List?) ?? []; final discovered = []; for (final m in rawModels) { final fullName = m['name'] as String? ?? ''; final lower = fullName.toLowerCase(); final methods = (m['supportedGenerationMethods'] as List?) ?? []; if (methods.contains('generateContent') && (lower.contains('flash') || lower.contains('lite'))) { final id = fullName.startsWith('models/') ? fullName.substring('models/'.length) : fullName; discovered.add(id); } } discovered.sort((a, b) => b.compareTo(a)); _geminiModels = discovered; } } catch (_) { // Fall through to hard-coded list. } if (_geminiModels.isEmpty) { _geminiModels = [ 'gemini-2.5-flash-lite', 'gemini-2.5-flash', 'gemini-2.0-flash', 'gemini-1.5-flash', ]; } return _geminiModels; } Future _geminiGenerate( String prompt, { required String fallback, }) async { final models = await _getGeminiModels(); Object? lastError; for (final modelId in models) { try { final model = GenerativeModel(model: modelId, apiKey: _geminiApiKey); final response = await model.generateContent([Content.text(prompt)]); return response.text ?? fallback; } catch (e) { lastError = e; // Try the next model regardless of error type. } } throw Exception('All Gemini models failed. Last error: $lastError'); } // --------------------------------------------------------------------------- // DeepSeek (OpenAI-compatible REST) // --------------------------------------------------------------------------- Future _deepseekGenerate( String prompt, { required String fallback, }) async { const url = 'https://api.deepseek.com/chat/completions'; final body = jsonEncode({ 'model': 'deepseek-chat', 'messages': [ {'role': 'user', 'content': prompt}, ], }); final res = await http.post( Uri.parse(url), headers: { 'Content-Type': 'application/json', 'Authorization': 'Bearer $_deepseekApiKey', }, body: body, ); if (res.statusCode == 200) { final data = jsonDecode(res.body) as Map; final choices = data['choices'] as List?; final content = choices?.firstOrNull?['message']?['content'] as String?; return content?.trim() ?? fallback; } throw Exception( 'DeepSeek request failed (HTTP ${res.statusCode}): ${res.body}', ); } }