Files
OpenCode 68a9071cf1 feat: reconcile local master with origin (exa/hot-platform hunters, leaks ledger, latest results)
Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
2026-08-07 12:53:47 +08:00

1 line
92 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "ahmetkoca32/zerowaste", "files": {"lib/core/constants/app_constants.dart": {"af2308540008e59dff25a5bd3ecc65f720015430": {"t": 1785745913.7683146, "n": 1003, "h": "98361e25aba17d5b", "c": "import 'package:flutter_dotenv/flutter_dotenv.dart';\n\n/// Global app constants.\nabstract final class AppConstants {\n AppConstants._();\n\n /// Base URL for API (switch when moving to remote).\n static const String recipesBaseUrl = '';\n\n /// Local recipes asset path (used when [recipesBaseUrl] is empty).\n static const String recipesAssetPath = 'assets/data/recipes.json';\n\n /// DeepSeek API base URL.\n static const String deepSeekBaseUrl = 'https://api.deepseek.com';\n\n /// DeepSeek chat completions path (OpenAI-compatible).\n static const String deepSeekChatPath = '/v1/chat/completions';\n\n /// DeepSeek model for chat.\n static const String deepSeekModel = 'deepseek-chat';\n\n /// API key for DeepSeek. Prefer .env (DEEPSEEK_API_KEY); fallback to --dart-define.\n static String get deepSeekApiKey =>\n dotenv.env['DEEPSEEK_API_KEY']?.trim() ??\n String.fromEnvironment('DEEPSEEK_API_KEY', defaultValue: '');\n\n /// App display name.\n static const String appName = 'Atıksız Mutfak';\n}\n"}, "671cca96b78d421ad57b4c538484c9b47a772454": {"t": 1785757864.201365, "n": 1003, "h": "98361e25aba17d5b", "c": "import 'package:flutter_dotenv/flutter_dotenv.dart';\n\n/// Global app constants.\nabstract final class AppConstants {\n AppConstants._();\n\n /// Base URL for API (switch when moving to remote).\n static const String recipesBaseUrl = '';\n\n /// Local recipes asset path (used when [recipesBaseUrl] is empty).\n static const String recipesAssetPath = 'assets/data/recipes.json';\n\n /// DeepSeek API base URL.\n static const String deepSeekBaseUrl = 'https://api.deepseek.com';\n\n /// DeepSeek chat completions path (OpenAI-compatible).\n static const String deepSeekChatPath = '/v1/chat/completions';\n\n /// DeepSeek model for chat.\n static const String deepSeekModel = 'deepseek-chat';\n\n /// API key for DeepSeek. Prefer .env (DEEPSEEK_API_KEY); fallback to --dart-define.\n static String get deepSeekApiKey =>\n dotenv.env['DEEPSEEK_API_KEY']?.trim() ??\n String.fromEnvironment('DEEPSEEK_API_KEY', defaultValue: '');\n\n /// App display name.\n static const String appName = 'Atıksız Mutfak';\n}\n"}, "6828319678f6eebce1d4d4d55e8d32d0a7e65e18": {"t": 1785757865.9951882, "n": 1006, "h": "bc0073506efb473e", "c": "import 'package:flutter_dotenv/flutter_dotenv.dart';\n\n/// Global app constants.\nabstract final class AppConstants {\n AppConstants._();\n\n /// Base URL for API (switch when moving to remote).\n static const String recipesBaseUrl = '';\n\n /// Local recipes asset path (used when [recipesBaseUrl] is empty).\n static const String recipesAssetPath = 'assets/data/recipes.json';\n\n /// DeepSeek API base URL.\n static const String deepSeekBaseUrl = 'https://api.deepseek.com';\n\n /// DeepSeek chat completions path (OpenAI-compatible).\n static const String deepSeekChatPath = '/v1/chat/completions';\n\n /// DeepSeek model for chat.\n static const String deepSeekModel = 'deepseek-chat';\n\n /// API key for DeepSeek. Prefer .env (DEEPSEEK_API_KEY); fallback to --dart-define.\n static String get deepSeekApiKey =>\n dotenv.env['DEEPSEEK_API_KEY']?.trim() ??\n String.fromEnvironment('DEEPSEEK_API_KEY', defaultValue: '');\n\n /// App display name.\n static const String appName = 'Sıfır Atık Mutfak';\n}\n"}, "d9c1014dbf8689a703c2e5251b766cb04a5c045e": {"t": 1785757867.2833712, "n": 1005, "h": "86b8f95308486782", "c": "import 'package:flutter_dotenv/flutter_dotenv.dart';\n\n/// Global app constants.\nabstract final class AppConstants {\n AppConstants._();\n\n /// Base URL for API (switch when moving to remote).\n static const String recipesBaseUrl = '';\n\n /// Local recipes asset path (used when [recipesBaseUrl] is empty).\n static const String recipesAssetPath = 'assets/data/recipes.json';\n\n /// DeepSeek API base URL.\n static const String deepSeekBaseUrl = 'https://api.deepseek.com';\n\n /// DeepSeek chat completions path (OpenAI-compatible).\n static const String deepSeekChatPath = '/v1/chat/completions';\n\n /// DeepSeek model for chat.\n static const String deepSeekModel = 'deepseek-chat';\n\n /// API key for DeepSeek. Prefer .env (DEEPSEEK_API_KEY); fallback to --dart-define.\n static String get deepSeekApiKey =>\n dotenv.env['DEEPSEEK_API_KEY']?.trim() ??\n String.fromEnvironment('DEEPSEEK_API_KEY', defaultValue: '');\n\n /// App display name.\n static const String appName = 'ZeroWaste Mutfak';\n}\n"}, "f06ec4cab9cef7a8e2dfc294bb45d0c057eb8cd1": {"t": 1786067503.9653156, "n": 1003, "h": "98361e25aba17d5b", "c": "import 'package:flutter_dotenv/flutter_dotenv.dart';\n\n/// Global app constants.\nabstract final class AppConstants {\n AppConstants._();\n\n /// Base URL for API (switch when moving to remote).\n static const String recipesBaseUrl = '';\n\n /// Local recipes asset path (used when [recipesBaseUrl] is empty).\n static const String recipesAssetPath = 'assets/data/recipes.json';\n\n /// DeepSeek API base URL.\n static const String deepSeekBaseUrl = 'https://api.deepseek.com';\n\n /// DeepSeek chat completions path (OpenAI-compatible).\n static const String deepSeekChatPath = '/v1/chat/completions';\n\n /// DeepSeek model for chat.\n static const String deepSeekModel = 'deepseek-chat';\n\n /// API key for DeepSeek. Prefer .env (DEEPSEEK_API_KEY); fallback to --dart-define.\n static String get deepSeekApiKey =>\n dotenv.env['DEEPSEEK_API_KEY']?.trim() ??\n String.fromEnvironment('DEEPSEEK_API_KEY', defaultValue: '');\n\n /// App display name.\n static const String appName = 'Atıksız Mutfak';\n}\n"}, "3be0962556864cdc1158a33dd7b347a644cc25c7": {"t": 1786068343.230961, "n": 1003, "h": "98361e25aba17d5b", "c": "import 'package:flutter_dotenv/flutter_dotenv.dart';\n\n/// Global app constants.\nabstract final class AppConstants {\n AppConstants._();\n\n /// Base URL for API (switch when moving to remote).\n static const String recipesBaseUrl = '';\n\n /// Local recipes asset path (used when [recipesBaseUrl] is empty).\n static const String recipesAssetPath = 'assets/data/recipes.json';\n\n /// DeepSeek API base URL.\n static const String deepSeekBaseUrl = 'https://api.deepseek.com';\n\n /// DeepSeek chat completions path (OpenAI-compatible).\n static const String deepSeekChatPath = '/v1/chat/completions';\n\n /// DeepSeek model for chat.\n static const String deepSeekModel = 'deepseek-chat';\n\n /// API key for DeepSeek. Prefer .env (DEEPSEEK_API_KEY); fallback to --dart-define.\n static String get deepSeekApiKey =>\n dotenv.env['DEEPSEEK_API_KEY']?.trim() ??\n String.fromEnvironment('DEEPSEEK_API_KEY', defaultValue: '');\n\n /// App display name.\n static const String appName = 'Atıksız Mutfak';\n}\n"}}, "lib/core/services/deep_seek_service.dart": {"af2308540008e59dff25a5bd3ecc65f720015430": {"t": 1785745924.965519, "n": 14398, "h": "3b8471ed9501568c", "c": "import 'dart:async';\n\nimport 'package:dio/dio.dart';\n\nimport '../constants/app_constants.dart';\n\n/// Thrown when DeepSeek API key is missing or invalid.\nclass DeepSeekAuthException implements Exception {\n DeepSeekAuthException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Invalid or missing DeepSeek API key';\n}\n\n/// Thrown when the request times out after waiting for a server response.\nclass DeepSeekTimeoutException implements Exception {\n DeepSeekTimeoutException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Request timed out';\n}\n\n/// Thrown when we can't even reach the server (DNS, no internet, blocked).\n/// Distinct from [DeepSeekTimeoutException] which means we connected but the\n/// server didn't reply in time.\nclass DeepSeekConnectionException implements Exception {\n DeepSeekConnectionException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Could not reach the server';\n}\n\n/// Thrown when the API returns an error.\nclass DeepSeekApiException implements Exception {\n DeepSeekApiException(this.statusCode, [this.message]);\n final int statusCode;\n final String? message;\n @override\n String toString() => message ?? 'API error (status: $statusCode)';\n}\n\n/// Service for DeepSeek chat API: recipe generation and mascot chat.\nclass DeepSeekService {\n DeepSeekService(this._dio, {String? apiKey}) : _apiKey = apiKey ?? AppConstants.deepSeekApiKey;\n\n final Dio _dio;\n final String _apiKey;\n\n static String _recipeSystemPrompt(String languageCode) {\n if (languageCode == 'en') {\n return '''\nYou are EcoChef, a professional chef. Create a creative recipe from the user's ingredients. Use a friendly, casual tone.\n\nLANGUAGE — highest priority:\n- Write the entire recipe in English only. Never use Turkish words or headers.\n- The recipe title (text after \"## Title:\") MUST be in English — e.g. \"Tomato & Egg Skillet\", NOT \"Domatesli Omlet\".\n- Use \"## Title:\" only. NEVER use \"## Başlık:\" or any Turkish section headers.\n\nNever add intro phrases, greetings, \"sure\", \"here is your recipe\", or similar. Start directly with the recipe.\n\nUse exactly this format:\n\n## Title: [recipe name]\n\n**Short Description:** [1-2 sentence description]\n\n**Ingredients:**\n- [ingredient 1]\n- [ingredient 2]\n\n**Steps:**\n1. [step 1]\n2. [step 2]\n\nKeep it practical and home-kitchen friendly. Add zero-waste tips when possible (optional).\n''';\n }\n\n return '''\nYou are EcoChef, a professional chef. Create a creative recipe from the user's ingredients. Use a friendly, casual tone.\n\nLANGUAGE — highest priority:\n- Write the entire recipe in Turkish only. Never use English words or headers.\n\nNever add intro phrases, greetings, \"elbette\", \"işte tarifiniz\", \"karşınızda\", or similar. Start directly with the recipe.\n\nUse exactly this format:\n\n## Başlık: [tarif adı]\n\n**Kısa Açıklama:** [1-2 cümlelik açıklama]\n\n**Malzemeler:**\n- [malzeme 1]\n- [malzeme 2]\n\n**Yapılış Adımları:**\n1. [adım 1]\n2. [adım 2]\n\nKeep it practical and home-kitchen friendly. Add zero-waste tips when possible (optional).\n''';\n }\n\n static const String _mascotSystemPrompt = '''\nYou are EcoChef, the mascot of Zero-Waste Kitchen (Atıksız Mutfak). You are warm, eco-conscious, and an expert on reducing food waste, using leftovers, and sustainable cooking.\n\nLANGUAGE — highest priority, never violate:\n- Read the user's latest message and reply entirely in that same language only.\n- English in → English out. Every word must be English. No Turkish words, greetings, or phrases.\n- Turkish in → Turkish out. Every word must be Turkish.\n- Never default to Turkish because of your brand name, this prompt, or the app locale.\n- Do not mix languages in one reply. Do not translate the user's question unless they ask.\n- If a [LANGUAGE RULE] tag is present in the user message, follow it exactly.\n\nStyle: short, helpful, encouraging, friendly. Stay on zero-waste cooking and kitchen topics.\n''';\n\n /// Retry budget for transient network failures. The first attempt is\n /// immediate, retries wait [_retryBackoffs] before firing. Keep this small\n /// — the user is staring at a loading indicator.\n static const List<Duration> _retryBackoffs = [\n Duration(milliseconds: 500),\n Duration(milliseconds: 1500),\n ];\n\n /// How many prior+current bubbles to send to the model (user + assistant).\n static const int mascotHistoryLimit = 20;\n\n /// Soft-cap long EcoChef replies in history to keep token use bounded.\n static const int _maxHistoryAssistantChars = 1200;\n\n /// Sends a chat request to DeepSeek and returns the assistant message content.\n Future<String> _chat({\n required String systemPrompt,\n required List<Map<String, String>> conversation,\n }) async {\n if (_apiKey.isEmpty) {\n throw DeepSeekAuthException('DEEPSEEK_API_KEY is not set.');\n }\n\n // Total attempts = initial + retries. We only retry on network-layer\n // failures (no socket, DNS, timeouts) — never on 4xx/5xx, since those\n // are either programmer error or paid-for failures we shouldn't double-charge for.\n final maxAttempts = _retryBackoffs.length + 1;\n Object? lastError;\n\n for (int attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await _postOnce(\n systemPrompt: systemPrompt,\n conversation: conversation,\n );\n } on DeepSeekTimeoutException catch (e) {\n lastError = e;\n } on DeepSeekConnectionException catch (e) {\n lastError = e;\n }\n\n // Wait before the next attempt (skip wait after the last attempt).\n if (attempt < maxAttempts - 1) {\n await Future<void>.delayed(_retryBackoffs[attempt]);\n }\n }\n\n // All attempts exhausted — surface the most recent failure.\n throw lastError!;\n }\n\n /// One round-trip; never retries. Wrapped by [_chat] for the retry loop.\n Future<String> _postOnce({\n required String systemPrompt,\n required List<Map<String, String>> conversation,\n }) async {\n try {\n final response = await _dio.post<Map<String, dynamic>>(\n AppConstants.deepSeekChatPath,\n data: <String, dynamic>{\n 'model': AppConstants.deepSeekModel,\n 'messages': <Map<String, String>>[\n <String, String>{'role': 'system', 'content': systemPrompt},\n ...conversation,\n ],\n 'max_tokens': 1024,\n },\n options: Options(\n headers: <String, String>{\n 'Authorization': 'Bearer $_apiKey',\n },\n // All three timeouts are overridden here. Previously connectTimeout\n // fell back to the 30s default from NetworkService, which masqueraded\n // as \"60s\" in the per-request config.\n connectTimeout: const Duration(seconds: 60),\n sendTimeout: const Duration(seconds: 60),\n receiveTimeout: const Duration(seconds: 60),\n ),\n );\n final data = response.data;\n if (data == null) throw DeepSeekApiException(response.statusCode ?? -1, 'Empty response');\n final choices = data['choices'] as List<dynamic>?;\n final content = choices?.isNotEmpty == true\n ? (choices!.first as Map<String, dynamic>)['message'] as Map<String, dynamic>?\n : null;\n final text = content?['content'] as String?;\n if (text == null || text.isEmpty) {\n throw DeepSeekApiException(response.statusCode ?? -1, 'No content in response');\n }\n return text.trim();\n } on DioException catch (e) {\n // Connection-level: server was never reached. Worth retrying.\n // connectionError covers DNS, socket, refused, host unreachable — Dio's\n // cross-platform shorthand for \"we never got bytes flowing\".\n if (e.type == DioExceptionType.connectionError) {\n throw DeepSeekConnectionException(e.message);\n }\n // Timeouts — these split into \"couldn't open socket\" (connectTimeout,\n // also retry-worthy) and \"server slow to respond\" (send/receive). We\n // bucket the first as a connection error and the others as proper timeouts.\n if (e.type == DioExceptionType.connectionTimeout) {\n throw DeepSeekConnectionException(e.message);\n }\n if (e.type == DioExceptionType.sendTimeout ||\n e.type == DioExceptionType.receiveTimeout) {\n throw DeepSeekTimeoutException(e.message);\n }\n final statusCode = e.response?.statusCode;\n if (statusCode == 401 || statusCode == 403) {\n throw DeepSeekAuthException(\n statusCode == 401 ? 'Invalid API key.' : 'Access forbidden. Check your API key.',\n );\n }\n throw DeepSeekApiException(\n statusCode ?? -1,\n e.response?.data?.toString() ?? e.message ?? 'Network error',\n );\n }\n }\n\n /// Generates a recipe using the given ingredients and optional cuisine style.\n Future<String> generateRecipe(\n List<String> ingredients, {\n String? cuisine,\n String languageCode = 'tr',\n }) async {\n final isEnglish = languageCode == 'en';\n if (ingredients.isEmpty) {\n return isEnglish\n ? 'Please add at least one ingredient.'\n : 'Lütfen en az bir malzeme ekleyin.';\n }\n final list = ingredients.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();\n if (list.isEmpty) {\n return isEnglish\n ? 'Please add at least one ingredient.'\n : 'Lütfen en az bir malzeme ekleyin.';\n }\n\n final languageRule = isEnglish\n ? 'Write the entire recipe in English only. The recipe title must be in English. Use \"## Title:\" as the first header — never \"## Başlık:\".'\n : 'Tarifin tamamını yalnızca Türkçe yaz. Tüm başlıklar, malzemeler ve adımlar Türkçe olmalı.';\n\n var userContent = isEnglish\n ? 'Create a recipe using only these ingredients:\\n${list.join('\\n')}\\n\\n'\n 'Ingredients may be in any language, but the recipe title, description, '\n 'ingredient names in the list, and steps must all be in English. '\n 'Give the dish an English name (e.g. \"Tomato & Egg Skillet\", not \"Domatesli Omlet\").'\n : 'Sadece şu malzemelerle bir tarif oluştur:\\n${list.join('\\n')}';\n if (cuisine != null && cuisine.isNotEmpty) {\n userContent += isEnglish\n ? '\\n\\nPreferred cuisine: $cuisine. Match this cuisine\\'s flavors and techniques.'\n : '\\n\\nTercih edilen mutfak: $cuisine. Tarif bu mutfağın lezzet ve tekniklerine uygun olsun.';\n }\n\n userContent = '''\n[LANGUAGE RULE: $languageRule]\n\n$userContent''';\n\n return _chat(\n systemPrompt: _recipeSystemPrompt(languageCode),\n conversation: [\n <String, String>{'role': 'user', 'content': userContent},\n ],\n );\n }\n\n /// Chat with the Atıksız mascot (friendly, eco-conscious, zero-waste expert).\n ///\n /// [priorTurns] is the conversation **before** [message] (oldest → newest).\n /// The last [mascotHistoryLimit] bubbles (prior + current) are sent to the model.\n Future<String> chatWithMascot(\n String message, {\n List<({String text, bool isUser})> priorTurns = const [],\n }) async {\n final trimmed = message.trim();\n if (trimmed.isEmpty) {\n return 'Ask me anything about zero-waste cooking and kitchen tips! / Sıfır atık mutfak ve ipuçları hakkında ne olursa olsun sorabilirsin!';\n }\n\n final languageRule = _languageRuleForMessage(trimmed);\n final latestUserContent = '''\n[LANGUAGE RULE: $languageRule]\n\nUser message:\n$trimmed''';\n\n final turns = <({String text, bool isUser})>[\n ...priorTurns,\n (text: trimmed, isUser: true),\n ];\n final window = turns.length > mascotHistoryLimit\n ? turns.sublist(turns.length - mascotHistoryLimit)\n : turns;\n\n final conversation = <Map<String, String>>[];\n for (var i = 0; i < window.length; i++) {\n final turn = window[i];\n final isLast = i == window.length - 1;\n if (turn.isUser) {\n conversation.add({\n 'role': 'user',\n 'content': isLast ? latestUserContent : turn.text,\n });\n } else {\n conversation.add({\n 'role': 'assistant',\n 'content': _truncateHistoryText(turn.text),\n });\n }\n }\n\n return _chat(\n systemPrompt: _mascotSystemPrompt,\n conversation: conversation,\n );\n }\n\n static String _truncateHistoryText(String text) {\n if (text.length <= _maxHistoryAssistantChars) return text;\n return '${text.substring(0, _maxHistoryAssistantChars)}…';\n }\n\n /// Returns an explicit reply-language instruction derived from the user's text.\n static String _languageRuleForMessage(String message) {\n switch (_detectMessageLanguage(message)) {\n case _MessageLanguage.english:\n return 'The user wrote in English. Reply ONLY in English. Do not use any Turkish.';\n case _MessageLanguage.turkish:\n return 'Kullanıcı Türkçe yazdı. Yanıtını YALNIZCA Türkçe ver. İngilizce kullanma.';\n case _MessageLanguage.unknown:\n return 'Detect the language of the user message and reply entirely in that same language only.';\n }\n }\n\n static _MessageLanguage _detectMessageLanguage(String message) {\n final lower = message.toLowerCase();\n\n if (RegExp(r'[ğüşöçı]').hasMatch(lower)) {\n return _MessageLanguage.turkish;\n }\n\n const turkishWords = [\n 'merhaba', 'nasıl', 'neden', 'ne ', ' bir ', 'için', 'olan', 'var',\n 'yok', 'yemek', 'mutfak', 'atık', 'tarif', 'pişir', 'lütfen',\n 'teşekkür', 'evet', 'hayır', 'artık', 'malzeme', 'soğan', 'domates',\n ];\n const englishWords = [\n 'hello', 'how', 'what', 'the', 'is', 'are', 'you', 'can', 'could',\n 'would', 'please', 'thanks', 'thank', 'yes', 'no', 'why', 'when',\n 'where', 'food', 'waste', 'recipe', 'cook', 'kitchen', 'leftover',\n 'ingredient', 'help', 'good', 'morning', 'evening', 'hey', 'hi ',\n ];\n\n var trScore = 0;\n var enScore = 0;\n for (final word in turkishWords) {\n if (lower.contains(word)) trScore++;\n }\n for (final word in englishWords) {\n if (RegExp('\\\\b${RegExp.escape(word.trim())}\\\\b').hasMatch(lower)) {\n enScore++;\n }\n }\n\n if (trScore > enScore) return _MessageLanguage.turkish;\n if (enScore > trScore) return _MessageLanguage.english;\n if (enScore > 0) return _MessageLanguage.english;\n\n return _MessageLanguage.unknown;\n }\n}\n\nenum _MessageLanguage { english, turkish, unknown }\n"}, "5675c091bb11492f2532030162c40fdfe65e74ac": {"t": 1785757864.240281, "n": 14398, "h": "3b8471ed9501568c", "c": "import 'dart:async';\n\nimport 'package:dio/dio.dart';\n\nimport '../constants/app_constants.dart';\n\n/// Thrown when DeepSeek API key is missing or invalid.\nclass DeepSeekAuthException implements Exception {\n DeepSeekAuthException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Invalid or missing DeepSeek API key';\n}\n\n/// Thrown when the request times out after waiting for a server response.\nclass DeepSeekTimeoutException implements Exception {\n DeepSeekTimeoutException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Request timed out';\n}\n\n/// Thrown when we can't even reach the server (DNS, no internet, blocked).\n/// Distinct from [DeepSeekTimeoutException] which means we connected but the\n/// server didn't reply in time.\nclass DeepSeekConnectionException implements Exception {\n DeepSeekConnectionException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Could not reach the server';\n}\n\n/// Thrown when the API returns an error.\nclass DeepSeekApiException implements Exception {\n DeepSeekApiException(this.statusCode, [this.message]);\n final int statusCode;\n final String? message;\n @override\n String toString() => message ?? 'API error (status: $statusCode)';\n}\n\n/// Service for DeepSeek chat API: recipe generation and mascot chat.\nclass DeepSeekService {\n DeepSeekService(this._dio, {String? apiKey}) : _apiKey = apiKey ?? AppConstants.deepSeekApiKey;\n\n final Dio _dio;\n final String _apiKey;\n\n static String _recipeSystemPrompt(String languageCode) {\n if (languageCode == 'en') {\n return '''\nYou are EcoChef, a professional chef. Create a creative recipe from the user's ingredients. Use a friendly, casual tone.\n\nLANGUAGE — highest priority:\n- Write the entire recipe in English only. Never use Turkish words or headers.\n- The recipe title (text after \"## Title:\") MUST be in English — e.g. \"Tomato & Egg Skillet\", NOT \"Domatesli Omlet\".\n- Use \"## Title:\" only. NEVER use \"## Başlık:\" or any Turkish section headers.\n\nNever add intro phrases, greetings, \"sure\", \"here is your recipe\", or similar. Start directly with the recipe.\n\nUse exactly this format:\n\n## Title: [recipe name]\n\n**Short Description:** [1-2 sentence description]\n\n**Ingredients:**\n- [ingredient 1]\n- [ingredient 2]\n\n**Steps:**\n1. [step 1]\n2. [step 2]\n\nKeep it practical and home-kitchen friendly. Add zero-waste tips when possible (optional).\n''';\n }\n\n return '''\nYou are EcoChef, a professional chef. Create a creative recipe from the user's ingredients. Use a friendly, casual tone.\n\nLANGUAGE — highest priority:\n- Write the entire recipe in Turkish only. Never use English words or headers.\n\nNever add intro phrases, greetings, \"elbette\", \"işte tarifiniz\", \"karşınızda\", or similar. Start directly with the recipe.\n\nUse exactly this format:\n\n## Başlık: [tarif adı]\n\n**Kısa Açıklama:** [1-2 cümlelik açıklama]\n\n**Malzemeler:**\n- [malzeme 1]\n- [malzeme 2]\n\n**Yapılış Adımları:**\n1. [adım 1]\n2. [adım 2]\n\nKeep it practical and home-kitchen friendly. Add zero-waste tips when possible (optional).\n''';\n }\n\n static const String _mascotSystemPrompt = '''\nYou are EcoChef, the mascot of Zero-Waste Kitchen (Atıksız Mutfak). You are warm, eco-conscious, and an expert on reducing food waste, using leftovers, and sustainable cooking.\n\nLANGUAGE — highest priority, never violate:\n- Read the user's latest message and reply entirely in that same language only.\n- English in → English out. Every word must be English. No Turkish words, greetings, or phrases.\n- Turkish in → Turkish out. Every word must be Turkish.\n- Never default to Turkish because of your brand name, this prompt, or the app locale.\n- Do not mix languages in one reply. Do not translate the user's question unless they ask.\n- If a [LANGUAGE RULE] tag is present in the user message, follow it exactly.\n\nStyle: short, helpful, encouraging, friendly. Stay on zero-waste cooking and kitchen topics.\n''';\n\n /// Retry budget for transient network failures. The first attempt is\n /// immediate, retries wait [_retryBackoffs] before firing. Keep this small\n /// — the user is staring at a loading indicator.\n static const List<Duration> _retryBackoffs = [\n Duration(milliseconds: 500),\n Duration(milliseconds: 1500),\n ];\n\n /// How many prior+current bubbles to send to the model (user + assistant).\n static const int mascotHistoryLimit = 20;\n\n /// Soft-cap long EcoChef replies in history to keep token use bounded.\n static const int _maxHistoryAssistantChars = 1200;\n\n /// Sends a chat request to DeepSeek and returns the assistant message content.\n Future<String> _chat({\n required String systemPrompt,\n required List<Map<String, String>> conversation,\n }) async {\n if (_apiKey.isEmpty) {\n throw DeepSeekAuthException('DEEPSEEK_API_KEY is not set.');\n }\n\n // Total attempts = initial + retries. We only retry on network-layer\n // failures (no socket, DNS, timeouts) — never on 4xx/5xx, since those\n // are either programmer error or paid-for failures we shouldn't double-charge for.\n final maxAttempts = _retryBackoffs.length + 1;\n Object? lastError;\n\n for (int attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await _postOnce(\n systemPrompt: systemPrompt,\n conversation: conversation,\n );\n } on DeepSeekTimeoutException catch (e) {\n lastError = e;\n } on DeepSeekConnectionException catch (e) {\n lastError = e;\n }\n\n // Wait before the next attempt (skip wait after the last attempt).\n if (attempt < maxAttempts - 1) {\n await Future<void>.delayed(_retryBackoffs[attempt]);\n }\n }\n\n // All attempts exhausted — surface the most recent failure.\n throw lastError!;\n }\n\n /// One round-trip; never retries. Wrapped by [_chat] for the retry loop.\n Future<String> _postOnce({\n required String systemPrompt,\n required List<Map<String, String>> conversation,\n }) async {\n try {\n final response = await _dio.post<Map<String, dynamic>>(\n AppConstants.deepSeekChatPath,\n data: <String, dynamic>{\n 'model': AppConstants.deepSeekModel,\n 'messages': <Map<String, String>>[\n <String, String>{'role': 'system', 'content': systemPrompt},\n ...conversation,\n ],\n 'max_tokens': 1024,\n },\n options: Options(\n headers: <String, String>{\n 'Authorization': 'Bearer $_apiKey',\n },\n // All three timeouts are overridden here. Previously connectTimeout\n // fell back to the 30s default from NetworkService, which masqueraded\n // as \"60s\" in the per-request config.\n connectTimeout: const Duration(seconds: 60),\n sendTimeout: const Duration(seconds: 60),\n receiveTimeout: const Duration(seconds: 60),\n ),\n );\n final data = response.data;\n if (data == null) throw DeepSeekApiException(response.statusCode ?? -1, 'Empty response');\n final choices = data['choices'] as List<dynamic>?;\n final content = choices?.isNotEmpty == true\n ? (choices!.first as Map<String, dynamic>)['message'] as Map<String, dynamic>?\n : null;\n final text = content?['content'] as String?;\n if (text == null || text.isEmpty) {\n throw DeepSeekApiException(response.statusCode ?? -1, 'No content in response');\n }\n return text.trim();\n } on DioException catch (e) {\n // Connection-level: server was never reached. Worth retrying.\n // connectionError covers DNS, socket, refused, host unreachable — Dio's\n // cross-platform shorthand for \"we never got bytes flowing\".\n if (e.type == DioExceptionType.connectionError) {\n throw DeepSeekConnectionException(e.message);\n }\n // Timeouts — these split into \"couldn't open socket\" (connectTimeout,\n // also retry-worthy) and \"server slow to respond\" (send/receive). We\n // bucket the first as a connection error and the others as proper timeouts.\n if (e.type == DioExceptionType.connectionTimeout) {\n throw DeepSeekConnectionException(e.message);\n }\n if (e.type == DioExceptionType.sendTimeout ||\n e.type == DioExceptionType.receiveTimeout) {\n throw DeepSeekTimeoutException(e.message);\n }\n final statusCode = e.response?.statusCode;\n if (statusCode == 401 || statusCode == 403) {\n throw DeepSeekAuthException(\n statusCode == 401 ? 'Invalid API key.' : 'Access forbidden. Check your API key.',\n );\n }\n throw DeepSeekApiException(\n statusCode ?? -1,\n e.response?.data?.toString() ?? e.message ?? 'Network error',\n );\n }\n }\n\n /// Generates a recipe using the given ingredients and optional cuisine style.\n Future<String> generateRecipe(\n List<String> ingredients, {\n String? cuisine,\n String languageCode = 'tr',\n }) async {\n final isEnglish = languageCode == 'en';\n if (ingredients.isEmpty) {\n return isEnglish\n ? 'Please add at least one ingredient.'\n : 'Lütfen en az bir malzeme ekleyin.';\n }\n final list = ingredients.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();\n if (list.isEmpty) {\n return isEnglish\n ? 'Please add at least one ingredient.'\n : 'Lütfen en az bir malzeme ekleyin.';\n }\n\n final languageRule = isEnglish\n ? 'Write the entire recipe in English only. The recipe title must be in English. Use \"## Title:\" as the first header — never \"## Başlık:\".'\n : 'Tarifin tamamını yalnızca Türkçe yaz. Tüm başlıklar, malzemeler ve adımlar Türkçe olmalı.';\n\n var userContent = isEnglish\n ? 'Create a recipe using only these ingredients:\\n${list.join('\\n')}\\n\\n'\n 'Ingredients may be in any language, but the recipe title, description, '\n 'ingredient names in the list, and steps must all be in English. '\n 'Give the dish an English name (e.g. \"Tomato & Egg Skillet\", not \"Domatesli Omlet\").'\n : 'Sadece şu malzemelerle bir tarif oluştur:\\n${list.join('\\n')}';\n if (cuisine != null && cuisine.isNotEmpty) {\n userContent += isEnglish\n ? '\\n\\nPreferred cuisine: $cuisine. Match this cuisine\\'s flavors and techniques.'\n : '\\n\\nTercih edilen mutfak: $cuisine. Tarif bu mutfağın lezzet ve tekniklerine uygun olsun.';\n }\n\n userContent = '''\n[LANGUAGE RULE: $languageRule]\n\n$userContent''';\n\n return _chat(\n systemPrompt: _recipeSystemPrompt(languageCode),\n conversation: [\n <String, String>{'role': 'user', 'content': userContent},\n ],\n );\n }\n\n /// Chat with the Atıksız mascot (friendly, eco-conscious, zero-waste expert).\n ///\n /// [priorTurns] is the conversation **before** [message] (oldest → newest).\n /// The last [mascotHistoryLimit] bubbles (prior + current) are sent to the model.\n Future<String> chatWithMascot(\n String message, {\n List<({String text, bool isUser})> priorTurns = const [],\n }) async {\n final trimmed = message.trim();\n if (trimmed.isEmpty) {\n return 'Ask me anything about zero-waste cooking and kitchen tips! / Sıfır atık mutfak ve ipuçları hakkında ne olursa olsun sorabilirsin!';\n }\n\n final languageRule = _languageRuleForMessage(trimmed);\n final latestUserContent = '''\n[LANGUAGE RULE: $languageRule]\n\nUser message:\n$trimmed''';\n\n final turns = <({String text, bool isUser})>[\n ...priorTurns,\n (text: trimmed, isUser: true),\n ];\n final window = turns.length > mascotHistoryLimit\n ? turns.sublist(turns.length - mascotHistoryLimit)\n : turns;\n\n final conversation = <Map<String, String>>[];\n for (var i = 0; i < window.length; i++) {\n final turn = window[i];\n final isLast = i == window.length - 1;\n if (turn.isUser) {\n conversation.add({\n 'role': 'user',\n 'content': isLast ? latestUserContent : turn.text,\n });\n } else {\n conversation.add({\n 'role': 'assistant',\n 'content': _truncateHistoryText(turn.text),\n });\n }\n }\n\n return _chat(\n systemPrompt: _mascotSystemPrompt,\n conversation: conversation,\n );\n }\n\n static String _truncateHistoryText(String text) {\n if (text.length <= _maxHistoryAssistantChars) return text;\n return '${text.substring(0, _maxHistoryAssistantChars)}…';\n }\n\n /// Returns an explicit reply-language instruction derived from the user's text.\n static String _languageRuleForMessage(String message) {\n switch (_detectMessageLanguage(message)) {\n case _MessageLanguage.english:\n return 'The user wrote in English. Reply ONLY in English. Do not use any Turkish.';\n case _MessageLanguage.turkish:\n return 'Kullanıcı Türkçe yazdı. Yanıtını YALNIZCA Türkçe ver. İngilizce kullanma.';\n case _MessageLanguage.unknown:\n return 'Detect the language of the user message and reply entirely in that same language only.';\n }\n }\n\n static _MessageLanguage _detectMessageLanguage(String message) {\n final lower = message.toLowerCase();\n\n if (RegExp(r'[ğüşöçı]').hasMatch(lower)) {\n return _MessageLanguage.turkish;\n }\n\n const turkishWords = [\n 'merhaba', 'nasıl', 'neden', 'ne ', ' bir ', 'için', 'olan', 'var',\n 'yok', 'yemek', 'mutfak', 'atık', 'tarif', 'pişir', 'lütfen',\n 'teşekkür', 'evet', 'hayır', 'artık', 'malzeme', 'soğan', 'domates',\n ];\n const englishWords = [\n 'hello', 'how', 'what', 'the', 'is', 'are', 'you', 'can', 'could',\n 'would', 'please', 'thanks', 'thank', 'yes', 'no', 'why', 'when',\n 'where', 'food', 'waste', 'recipe', 'cook', 'kitchen', 'leftover',\n 'ingredient', 'help', 'good', 'morning', 'evening', 'hey', 'hi ',\n ];\n\n var trScore = 0;\n var enScore = 0;\n for (final word in turkishWords) {\n if (lower.contains(word)) trScore++;\n }\n for (final word in englishWords) {\n if (RegExp('\\\\b${RegExp.escape(word.trim())}\\\\b').hasMatch(lower)) {\n enScore++;\n }\n }\n\n if (trScore > enScore) return _MessageLanguage.turkish;\n if (enScore > trScore) return _MessageLanguage.english;\n if (enScore > 0) return _MessageLanguage.english;\n\n return _MessageLanguage.unknown;\n }\n}\n\nenum _MessageLanguage { english, turkish, unknown }\n"}, "f06ec4cab9cef7a8e2dfc294bb45d0c057eb8cd1": {"t": 1785757866.071912, "n": 12906, "h": "4c3f96403d478b6d", "c": "import 'dart:async';\n\nimport 'package:dio/dio.dart';\n\nimport '../constants/app_constants.dart';\n\n/// Thrown when DeepSeek API key is missing or invalid.\nclass DeepSeekAuthException implements Exception {\n DeepSeekAuthException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Invalid or missing DeepSeek API key';\n}\n\n/// Thrown when the request times out after waiting for a server response.\nclass DeepSeekTimeoutException implements Exception {\n DeepSeekTimeoutException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Request timed out';\n}\n\n/// Thrown when we can't even reach the server (DNS, no internet, blocked).\n/// Distinct from [DeepSeekTimeoutException] which means we connected but the\n/// server didn't reply in time.\nclass DeepSeekConnectionException implements Exception {\n DeepSeekConnectionException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Could not reach the server';\n}\n\n/// Thrown when the API returns an error.\nclass DeepSeekApiException implements Exception {\n DeepSeekApiException(this.statusCode, [this.message]);\n final int statusCode;\n final String? message;\n @override\n String toString() => message ?? 'API error (status: $statusCode)';\n}\n\n/// Service for DeepSeek chat API: recipe generation and mascot chat.\nclass DeepSeekService {\n DeepSeekService(this._dio, {String? apiKey}) : _apiKey = apiKey ?? AppConstants.deepSeekApiKey;\n\n final Dio _dio;\n final String _apiKey;\n\n static String _recipeSystemPrompt(String languageCode) {\n if (languageCode == 'en') {\n return '''\nYou are EcoChef, a professional chef. Create a creative recipe from the user's ingredients. Use a friendly, casual tone.\n\nLANGUAGE — highest priority:\n- Write the entire recipe in English only. Never use Turkish words or headers.\n- The recipe title (text after \"## Title:\") MUST be in English — e.g. \"Tomato & Egg Skillet\", NOT \"Domatesli Omlet\".\n- Use \"## Title:\" only. NEVER use \"## Başlık:\" or any Turkish section headers.\n\nNever add intro phrases, greetings, \"sure\", \"here is your recipe\", or similar. Start directly with the recipe.\n\nUse exactly this format:\n\n## Title: [recipe name]\n\n**Short Description:** [1-2 sentence description]\n\n**Ingredients:**\n- [ingredient 1]\n- [ingredient 2]\n\n**Steps:**\n1. [step 1]\n2. [step 2]\n\nKeep it practical and home-kitchen friendly. Add zero-waste tips when possible (optional).\n''';\n }\n\n return '''\nYou are EcoChef, a professional chef. Create a creative recipe from the user's ingredients. Use a friendly, casual tone.\n\nLANGUAGE — highest priority:\n- Write the entire recipe in Turkish only. Never use English words or headers.\n\nNever add intro phrases, greetings, \"elbette\", \"işte tarifiniz\", \"karşınızda\", or similar. Start directly with the recipe.\n\nUse exactly this format:\n\n## Başlık: [tarif adı]\n\n**Kısa Açıklama:** [1-2 cümlelik açıklama]\n\n**Malzemeler:**\n- [malzeme 1]\n- [malzeme 2]\n\n**Yapılış Adımları:**\n1. [adım 1]\n2. [adım 2]\n\nKeep it practical and home-kitchen friendly. Add zero-waste tips when possible (optional).\n''';\n }\n\n static const String _mascotSystemPrompt = '''\nYou are EcoChef, the mascot of Zero-Waste Kitchen (Atıksız Mutfak). You are warm, eco-conscious, and an expert on reducing food waste, using leftovers, and sustainable cooking.\n\nLANGUAGE — highest priority, never violate:\n- Read the user's latest message and reply entirely in that same language only.\n- English in → English out. Every word must be English. No Turkish words, greetings, or phrases.\n- Turkish in → Turkish out. Every word must be Turkish.\n- Never default to Turkish because of your brand name, this prompt, or the app locale.\n- Do not mix languages in one reply. Do not translate the user's question unless they ask.\n- If a [LANGUAGE RULE] tag is present in the user message, follow it exactly.\n\nStyle: short, helpful, encouraging, friendly. Stay on zero-waste cooking and kitchen topics.\n''';\n\n /// Retry budget for transient network failures. The first attempt is\n /// immediate, retries wait [_retryBackoffs] before firing. Keep this small\n /// — the user is staring at a loading indicator.\n static const List<Duration> _retryBackoffs = [\n Duration(milliseconds: 500),\n Duration(milliseconds: 1500),\n ];\n\n /// Sends a chat request to DeepSeek and returns the assistant message content.\n Future<String> _chat({\n required String systemPrompt,\n required String userContent,\n }) async {\n if (_apiKey.isEmpty) {\n throw DeepSeekAuthException('DEEPSEEK_API_KEY is not set.');\n }\n\n // Total attempts = initial + retries. We only retry on network-layer\n // failures (no socket, DNS, timeouts) — never on 4xx/5xx, since those\n // are either programmer error or paid-for failures we shouldn't double-charge for.\n final maxAttempts = _retryBackoffs.length + 1;\n Object? lastError;\n\n for (int attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await _postOnce(\n systemPrompt: systemPrompt,\n userContent: userContent,\n );\n } on DeepSeekTimeoutException catch (e) {\n lastError = e;\n } on DeepSeekConnectionException catch (e) {\n lastError = e;\n }\n\n // Wait before the next attempt (skip wait after the last attempt).\n if (attempt < maxAttempts - 1) {\n await Future<void>.delayed(_retryBackoffs[attempt]);\n }\n }\n\n // All attempts exhausted — surface the most recent failure.\n throw lastError!;\n }\n\n /// One round-trip; never retries. Wrapped by [_chat] for the retry loop.\n Future<String> _postOnce({\n required String systemPrompt,\n required String userContent,\n }) async {\n try {\n final response = await _dio.post<Map<String, dynamic>>(\n AppConstants.deepSeekChatPath,\n data: <String, dynamic>{\n 'model': AppConstants.deepSeekModel,\n 'messages': <Map<String, String>>[\n <String, String>{'role': 'system', 'content': systemPrompt},\n <String, String>{'role': 'user', 'content': userContent},\n ],\n 'max_tokens': 1024,\n },\n options: Options(\n headers: <String, String>{\n 'Authorization': 'Bearer $_apiKey',\n },\n // All three timeouts are overridden here. Previously connectTimeout\n // fell back to the 30s default from NetworkService, which masqueraded\n // as \"60s\" in the per-request config.\n connectTimeout: const Duration(seconds: 60),\n sendTimeout: const Duration(seconds: 60),\n receiveTimeout: const Duration(seconds: 60),\n ),\n );\n final data = response.data;\n if (data == null) throw DeepSeekApiException(response.statusCode ?? -1, 'Empty response');\n final choices = data['choices'] as List<dynamic>?;\n final content = choices?.isNotEmpty == true\n ? (choices!.first as Map<String, dynamic>)['message'] as Map<String, dynamic>?\n : null;\n final text = content?['content'] as String?;\n if (text == null || text.isEmpty) {\n throw DeepSeekApiException(response.statusCode ?? -1, 'No content in response');\n }\n return text.trim();\n } on DioException catch (e) {\n // Connection-level: server was never reached. Worth retrying.\n // connectionError covers DNS, socket, refused, host unreachable — Dio's\n // cross-platform shorthand for \"we never got bytes flowing\".\n if (e.type == DioExceptionType.connectionError) {\n throw DeepSeekConnectionException(e.message);\n }\n // Timeouts — these split into \"couldn't open socket\" (connectTimeout,\n // also retry-worthy) and \"server slow to respond\" (send/receive). We\n // bucket the first as a connection error and the others as proper timeouts.\n if (e.type == DioExceptionType.connectionTimeout) {\n throw DeepSeekConnectionException(e.message);\n }\n if (e.type == DioExceptionType.sendTimeout ||\n e.type == DioExceptionType.receiveTimeout) {\n throw DeepSeekTimeoutException(e.message);\n }\n final statusCode = e.response?.statusCode;\n if (statusCode == 401 || statusCode == 403) {\n throw DeepSeekAuthException(\n statusCode == 401 ? 'Invalid API key.' : 'Access forbidden. Check your API key.',\n );\n }\n throw DeepSeekApiException(\n statusCode ?? -1,\n e.response?.data?.toString() ?? e.message ?? 'Network error',\n );\n }\n }\n\n /// Generates a recipe using the given ingredients and optional cuisine style.\n Future<String> generateRecipe(\n List<String> ingredients, {\n String? cuisine,\n String languageCode = 'tr',\n }) async {\n final isEnglish = languageCode == 'en';\n if (ingredients.isEmpty) {\n return isEnglish\n ? 'Please add at least one ingredient.'\n : 'Lütfen en az bir malzeme ekleyin.';\n }\n final list = ingredients.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();\n if (list.isEmpty) {\n return isEnglish\n ? 'Please add at least one ingredient.'\n : 'Lütfen en az bir malzeme ekleyin.';\n }\n\n final languageRule = isEnglish\n ? 'Write the entire recipe in English only. The recipe title must be in English. Use \"## Title:\" as the first header — never \"## Başlık:\".'\n : 'Tarifin tamamını yalnızca Türkçe yaz. Tüm başlıklar, malzemeler ve adımlar Türkçe olmalı.';\n\n var userContent = isEnglish\n ? 'Create a recipe using only these ingredients:\\n${list.join('\\n')}\\n\\n'\n 'Ingredients may be in any language, but the recipe title, description, '\n 'ingredient names in the list, and steps must all be in English. '\n 'Give the dish an English name (e.g. \"Tomato & Egg Skillet\", not \"Domatesli Omlet\").'\n : 'Sadece şu malzemelerle bir tarif oluştur:\\n${list.join('\\n')}';\n if (cuisine != null && cuisine.isNotEmpty) {\n userContent += isEnglish\n ? '\\n\\nPreferred cuisine: $cuisine. Match this cuisine\\'s flavors and techniques.'\n : '\\n\\nTercih edilen mutfak: $cuisine. Tarif bu mutfağın lezzet ve tekniklerine uygun olsun.';\n }\n\n userContent = '''\n[LANGUAGE RULE: $languageRule]\n\n$userContent''';\n\n return _chat(\n systemPrompt: _recipeSystemPrompt(languageCode),\n userContent: userContent,\n );\n }\n\n /// Chat with the Atıksız mascot (friendly, eco-conscious, zero-waste expert).\n Future<String> chatWithMascot(String message) async {\n final trimmed = message.trim();\n if (trimmed.isEmpty) {\n return 'Ask me anything about zero-waste cooking and kitchen tips! / Sıfır atık mutfak ve ipuçları hakkında ne olursa olsun sorabilirsin!';\n }\n\n final languageRule = _languageRuleForMessage(trimmed);\n final userContent = '''\n[LANGUAGE RULE: $languageRule]\n\nUser message:\n$trimmed''';\n\n return _chat(systemPrompt: _mascotSystemPrompt, userContent: userContent);\n }\n\n /// Returns an explicit reply-language instruction derived from the user's text.\n static String _languageRuleForMessage(String message) {\n switch (_detectMessageLanguage(message)) {\n case _MessageLanguage.english:\n return 'The user wrote in English. Reply ONLY in English. Do not use any Turkish.';\n case _MessageLanguage.turkish:\n return 'Kullanıcı Türkçe yazdı. Yanıtını YALNIZCA Türkçe ver. İngilizce kullanma.';\n case _MessageLanguage.unknown:\n return 'Detect the language of the user message and reply entirely in that same language only.';\n }\n }\n\n static _MessageLanguage _detectMessageLanguage(String message) {\n final lower = message.toLowerCase();\n\n if (RegExp(r'[ğüşöçı]').hasMatch(lower)) {\n return _MessageLanguage.turkish;\n }\n\n const turkishWords = [\n 'merhaba', 'nasıl', 'neden', 'ne ', ' bir ', 'için', 'olan', 'var',\n 'yok', 'yemek', 'mutfak', 'atık', 'tarif', 'pişir', 'lütfen',\n 'teşekkür', 'evet', 'hayır', 'artık', 'malzeme', 'soğan', 'domates',\n ];\n const englishWords = [\n 'hello', 'how', 'what', 'the', 'is', 'are', 'you', 'can', 'could',\n 'would', 'please', 'thanks', 'thank', 'yes', 'no', 'why', 'when',\n 'where', 'food', 'waste', 'recipe', 'cook', 'kitchen', 'leftover',\n 'ingredient', 'help', 'good', 'morning', 'evening', 'hey', 'hi ',\n ];\n\n var trScore = 0;\n var enScore = 0;\n for (final word in turkishWords) {\n if (lower.contains(word)) trScore++;\n }\n for (final word in englishWords) {\n if (RegExp('\\\\b${RegExp.escape(word.trim())}\\\\b').hasMatch(lower)) {\n enScore++;\n }\n }\n\n if (trScore > enScore) return _MessageLanguage.turkish;\n if (enScore > trScore) return _MessageLanguage.english;\n if (enScore > 0) return _MessageLanguage.english;\n\n return _MessageLanguage.unknown;\n }\n}\n\nenum _MessageLanguage { english, turkish, unknown }\n"}, "1e8c6b4ca2dc563a7754f8e98ea91cc1c1e84310": {"t": 1785757867.6008854, "n": 8214, "h": "c0328803f1bff2a8", "c": "import 'dart:async';\n\nimport 'package:dio/dio.dart';\n\nimport '../constants/app_constants.dart';\n\n/// Thrown when DeepSeek API key is missing or invalid.\nclass DeepSeekAuthException implements Exception {\n DeepSeekAuthException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Invalid or missing DeepSeek API key';\n}\n\n/// Thrown when the request times out after waiting for a server response.\nclass DeepSeekTimeoutException implements Exception {\n DeepSeekTimeoutException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Request timed out';\n}\n\n/// Thrown when we can't even reach the server (DNS, no internet, blocked).\n/// Distinct from [DeepSeekTimeoutException] which means we connected but the\n/// server didn't reply in time.\nclass DeepSeekConnectionException implements Exception {\n DeepSeekConnectionException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Could not reach the server';\n}\n\n/// Thrown when the API returns an error.\nclass DeepSeekApiException implements Exception {\n DeepSeekApiException(this.statusCode, [this.message]);\n final int statusCode;\n final String? message;\n @override\n String toString() => message ?? 'API error (status: $statusCode)';\n}\n\n/// Service for DeepSeek chat API: recipe generation and mascot chat.\nclass DeepSeekService {\n DeepSeekService(this._dio, {String? apiKey}) : _apiKey = apiKey ?? AppConstants.deepSeekApiKey;\n\n final Dio _dio;\n final String _apiKey;\n\n static const String _recipeSystemPrompt = '''\nSen profesyonel bir şefsin adın EcoChef. Kullanıcının verdiği malzemelerle yaratıcı bir tarif oluştur. Resmi bir dilden ziyade arkadaş canlısı bir dil kullan.\n\nASLA giriş cümlesi, selamlama, \"elbette\", \"işte tarifiniz\", \"karşınızda\" veya benzeri hiçbir tanıtım metni ekleme. Doğrudan tarife geç.\n\nYanıtını aşağıdaki sabit formatta, başlıkla başlayarak yaz:\n\n## Başlık: [tarif adı]\n\n**Kısa Açıklama:** [1-2 cümlelik açıklama]\n\n**Malzemeler:**\n- [malzeme 1]\n- [malzeme 2]\n\n**Yapılış Adımları:**\n1. [adım 1]\n2. [adım 2]\n\nPratik ve ev mutfağına uygun olsun. Mümkünse sıfır atık ipuçları ver (isteğe bağlı).\n''';\n\n static const String _mascotSystemPrompt = '''\nSen Atıksız Mutfak maskotusun (EcoChef). Çevre bilincine sahip, sıcak ve sıfır atık yemek konusunda uzmansın.\nGıda israfını azaltma, artakalanları kullanma ve sürdürülebilir pişirme hakkında kısa, faydalı ipuçları ver.\nKullanıcının mesajının dilini algıla ve aynı dilde yanıt ver. Kısa, teşvik edici ve samimi bir dil kullan.\n''';\n\n /// Retry budget for transient network failures. The first attempt is\n /// immediate, retries wait [_retryBackoffs] before firing. Keep this small\n /// — the user is staring at a loading indicator.\n static const List<Duration> _retryBackoffs = [\n Duration(milliseconds: 500),\n Duration(milliseconds: 1500),\n ];\n\n /// Sends a chat request to DeepSeek and returns the assistant message content.\n Future<String> _chat({\n required String systemPrompt,\n required String userContent,\n }) async {\n if (_apiKey.isEmpty) {\n throw DeepSeekAuthException('DEEPSEEK_API_KEY is not set.');\n }\n\n // Total attempts = initial + retries. We only retry on network-layer\n // failures (no socket, DNS, timeouts) — never on 4xx/5xx, since those\n // are either programmer error or paid-for failures we shouldn't double-charge for.\n final maxAttempts = _retryBackoffs.length + 1;\n Object? lastError;\n\n for (int attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await _postOnce(\n systemPrompt: systemPrompt,\n userContent: userContent,\n );\n } on DeepSeekTimeoutException catch (e) {\n lastError = e;\n } on DeepSeekConnectionException catch (e) {\n lastError = e;\n }\n\n // Wait before the next attempt (skip wait after the last attempt).\n if (attempt < maxAttempts - 1) {\n await Future<void>.delayed(_retryBackoffs[attempt]);\n }\n }\n\n // All attempts exhausted — surface the most recent failure.\n throw lastError!;\n }\n\n /// One round-trip; never retries. Wrapped by [_chat] for the retry loop.\n Future<String> _postOnce({\n required String systemPrompt,\n required String userContent,\n }) async {\n try {\n final response = await _dio.post<Map<String, dynamic>>(\n AppConstants.deepSeekChatPath,\n data: <String, dynamic>{\n 'model': AppConstants.deepSeekModel,\n 'messages': <Map<String, String>>[\n <String, String>{'role': 'system', 'content': systemPrompt},\n <String, String>{'role': 'user', 'content': userContent},\n ],\n 'max_tokens': 1024,\n },\n options: Options(\n headers: <String, String>{\n 'Authorization': 'Bearer $_apiKey',\n },\n // All three timeouts are overridden here. Previously connectTimeout\n // fell back to the 30s default from NetworkService, which masqueraded\n // as \"60s\" in the per-request config.\n connectTimeout: const Duration(seconds: 60),\n sendTimeout: const Duration(seconds: 60),\n receiveTimeout: const Duration(seconds: 60),\n ),\n );\n final data = response.data;\n if (data == null) throw DeepSeekApiException(response.statusCode ?? -1, 'Empty response');\n final choices = data['choices'] as List<dynamic>?;\n final content = choices?.isNotEmpty == true\n ? (choices!.first as Map<String, dynamic>)['message'] as Map<String, dynamic>?\n : null;\n final text = content?['content'] as String?;\n if (text == null || text.isEmpty) {\n throw DeepSeekApiException(response.statusCode ?? -1, 'No content in response');\n }\n return text.trim();\n } on DioException catch (e) {\n // Connection-level: server was never reached. Worth retrying.\n // connectionError covers DNS, socket, refused, host unreachable — Dio's\n // cross-platform shorthand for \"we never got bytes flowing\".\n if (e.type == DioExceptionType.connectionError) {\n throw DeepSeekConnectionException(e.message);\n }\n // Timeouts — these split into \"couldn't open socket\" (connectTimeout,\n // also retry-worthy) and \"server slow to respond\" (send/receive). We\n // bucket the first as a connection error and the others as proper timeouts.\n if (e.type == DioExceptionType.connectionTimeout) {\n throw DeepSeekConnectionException(e.message);\n }\n if (e.type == DioExceptionType.sendTimeout ||\n e.type == DioExceptionType.receiveTimeout) {\n throw DeepSeekTimeoutException(e.message);\n }\n final statusCode = e.response?.statusCode;\n if (statusCode == 401 || statusCode == 403) {\n throw DeepSeekAuthException(\n statusCode == 401 ? 'Invalid API key.' : 'Access forbidden. Check your API key.',\n );\n }\n throw DeepSeekApiException(\n statusCode ?? -1,\n e.response?.data?.toString() ?? e.message ?? 'Network error',\n );\n }\n }\n\n /// Generates a recipe using the given ingredients and optional cuisine style.\n Future<String> generateRecipe(List<String> ingredients, {String? cuisine}) async {\n if (ingredients.isEmpty) {\n return 'Lütfen en az bir malzeme ekleyin.';\n }\n final list = ingredients.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();\n if (list.isEmpty) return 'Lütfen en az bir malzeme ekleyin.';\n var userContent = 'Sadece şu malzemelerle bir tarif oluştur:\\n${list.join('\\n')}';\n if (cuisine != null && cuisine.isNotEmpty) {\n userContent += '\\n\\nTercih edilen mutfak: $cuisine. Tarif bu mutfağın lezzet ve tekniklerine uygun olsun.';\n }\n return _chat(systemPrompt: _recipeSystemPrompt, userContent: userContent);\n }\n\n /// Chat with the Atıksız mascot (friendly, eco-conscious, zero-waste expert).\n Future<String> chatWithMascot(String message) async {\n final trimmed = message.trim();\n if (trimmed.isEmpty) return 'Atıksız mutfak veya ipuçları hakkında ne olursa olsun sorabilirsiniz!';\n return _chat(systemPrompt: _mascotSystemPrompt, userContent: trimmed);\n }\n}\n"}, "e914dcf25aec5587ef477bf5f0af1af36ed96026": {"t": 1785757868.955924, "n": 8180, "h": "8d2260d998b45c2c", "c": "import 'dart:async';\n\nimport 'package:dio/dio.dart';\n\nimport '../constants/app_constants.dart';\n\n/// Thrown when DeepSeek API key is missing or invalid.\nclass DeepSeekAuthException implements Exception {\n DeepSeekAuthException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Invalid or missing DeepSeek API key';\n}\n\n/// Thrown when the request times out after waiting for a server response.\nclass DeepSeekTimeoutException implements Exception {\n DeepSeekTimeoutException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Request timed out';\n}\n\n/// Thrown when we can't even reach the server (DNS, no internet, blocked).\n/// Distinct from [DeepSeekTimeoutException] which means we connected but the\n/// server didn't reply in time.\nclass DeepSeekConnectionException implements Exception {\n DeepSeekConnectionException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Could not reach the server';\n}\n\n/// Thrown when the API returns an error.\nclass DeepSeekApiException implements Exception {\n DeepSeekApiException(this.statusCode, [this.message]);\n final int statusCode;\n final String? message;\n @override\n String toString() => message ?? 'API error (status: $statusCode)';\n}\n\n/// Service for DeepSeek chat API: recipe generation and mascot chat.\nclass DeepSeekService {\n DeepSeekService(this._dio, {String? apiKey}) : _apiKey = apiKey ?? AppConstants.deepSeekApiKey;\n\n final Dio _dio;\n final String _apiKey;\n\n static const String _recipeSystemPrompt = '''\nSen profesyonel bir şefsin adın EcoChef. Kullanıcının verdiği malzemelerle yaratıcı bir tarif oluştur. Resmi bir dilden ziyade arkadaş canlısı bir dil kullan.\n\nASLA giriş cümlesi, selamlama, \"elbette\", \"işte tarifiniz\", \"karşınızda\" veya benzeri hiçbir tanıtım metni ekleme. Doğrudan tarife geç.\n\nYanıtını aşağıdaki sabit formatta, başlıkla başlayarak yaz:\n\n## Başlık: [tarif adı]\n\n**Kısa Açıklama:** [1-2 cümlelik açıklama]\n\n**Malzemeler:**\n- [malzeme 1]\n- [malzeme 2]\n\n**Yapılış Adımları:**\n1. [adım 1]\n2. [adım 2]\n\nPratik ve ev mutfağına uygun olsun. Mümkünse sıfır atık ipuçları ver (isteğe bağlı).\n''';\n\n static const String _mascotSystemPrompt = '''\nSen Atıksız Mutfak maskotusun (EcoChef). Çevre bilincine sahip, sıcak ve sıfır atık yemek konusunda uzmansın.\nGıda israfını azaltma, artakalanları kullanma ve sürdürülebilir pişirme hakkında kısa, faydalı ipuçları ver.\nTüm yanıtlarını Türkçe yaz. Kısa, teşvik edici ve samimi bir dil kullan.\n''';\n\n /// Retry budget for transient network failures. The first attempt is\n /// immediate, retries wait [_retryBackoffs] before firing. Keep this small\n /// — the user is staring at a loading indicator.\n static const List<Duration> _retryBackoffs = [\n Duration(milliseconds: 500),\n Duration(milliseconds: 1500),\n ];\n\n /// Sends a chat request to DeepSeek and returns the assistant message content.\n Future<String> _chat({\n required String systemPrompt,\n required String userContent,\n }) async {\n if (_apiKey.isEmpty) {\n throw DeepSeekAuthException('DEEPSEEK_API_KEY is not set.');\n }\n\n // Total attempts = initial + retries. We only retry on network-layer\n // failures (no socket, DNS, timeouts) — never on 4xx/5xx, since those\n // are either programmer error or paid-for failures we shouldn't double-charge for.\n final maxAttempts = _retryBackoffs.length + 1;\n Object? lastError;\n\n for (int attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await _postOnce(\n systemPrompt: systemPrompt,\n userContent: userContent,\n );\n } on DeepSeekTimeoutException catch (e) {\n lastError = e;\n } on DeepSeekConnectionException catch (e) {\n lastError = e;\n }\n\n // Wait before the next attempt (skip wait after the last attempt).\n if (attempt < maxAttempts - 1) {\n await Future<void>.delayed(_retryBackoffs[attempt]);\n }\n }\n\n // All attempts exhausted — surface the most recent failure.\n throw lastError!;\n }\n\n /// One round-trip; never retries. Wrapped by [_chat] for the retry loop.\n Future<String> _postOnce({\n required String systemPrompt,\n required String userContent,\n }) async {\n try {\n final response = await _dio.post<Map<String, dynamic>>(\n AppConstants.deepSeekChatPath,\n data: <String, dynamic>{\n 'model': AppConstants.deepSeekModel,\n 'messages': <Map<String, String>>[\n <String, String>{'role': 'system', 'content': systemPrompt},\n <String, String>{'role': 'user', 'content': userContent},\n ],\n 'max_tokens': 1024,\n },\n options: Options(\n headers: <String, String>{\n 'Authorization': 'Bearer $_apiKey',\n },\n // All three timeouts are overridden here. Previously connectTimeout\n // fell back to the 30s default from NetworkService, which masqueraded\n // as \"60s\" in the per-request config.\n connectTimeout: const Duration(seconds: 60),\n sendTimeout: const Duration(seconds: 60),\n receiveTimeout: const Duration(seconds: 60),\n ),\n );\n final data = response.data;\n if (data == null) throw DeepSeekApiException(response.statusCode ?? -1, 'Empty response');\n final choices = data['choices'] as List<dynamic>?;\n final content = choices?.isNotEmpty == true\n ? (choices!.first as Map<String, dynamic>)['message'] as Map<String, dynamic>?\n : null;\n final text = content?['content'] as String?;\n if (text == null || text.isEmpty) {\n throw DeepSeekApiException(response.statusCode ?? -1, 'No content in response');\n }\n return text.trim();\n } on DioException catch (e) {\n // Connection-level: server was never reached. Worth retrying.\n // connectionError covers DNS, socket, refused, host unreachable — Dio's\n // cross-platform shorthand for \"we never got bytes flowing\".\n if (e.type == DioExceptionType.connectionError) {\n throw DeepSeekConnectionException(e.message);\n }\n // Timeouts — these split into \"couldn't open socket\" (connectTimeout,\n // also retry-worthy) and \"server slow to respond\" (send/receive). We\n // bucket the first as a connection error and the others as proper timeouts.\n if (e.type == DioExceptionType.connectionTimeout) {\n throw DeepSeekConnectionException(e.message);\n }\n if (e.type == DioExceptionType.sendTimeout ||\n e.type == DioExceptionType.receiveTimeout) {\n throw DeepSeekTimeoutException(e.message);\n }\n final statusCode = e.response?.statusCode;\n if (statusCode == 401 || statusCode == 403) {\n throw DeepSeekAuthException(\n statusCode == 401 ? 'Invalid API key.' : 'Access forbidden. Check your API key.',\n );\n }\n throw DeepSeekApiException(\n statusCode ?? -1,\n e.response?.data?.toString() ?? e.message ?? 'Network error',\n );\n }\n }\n\n /// Generates a recipe using the given ingredients and optional cuisine style.\n Future<String> generateRecipe(List<String> ingredients, {String? cuisine}) async {\n if (ingredients.isEmpty) {\n return 'Lütfen en az bir malzeme ekleyin.';\n }\n final list = ingredients.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();\n if (list.isEmpty) return 'Lütfen en az bir malzeme ekleyin.';\n var userContent = 'Sadece şu malzemelerle bir tarif oluştur:\\n${list.join('\\n')}';\n if (cuisine != null && cuisine.isNotEmpty) {\n userContent += '\\n\\nTercih edilen mutfak: $cuisine. Tarif bu mutfağın lezzet ve tekniklerine uygun olsun.';\n }\n return _chat(systemPrompt: _recipeSystemPrompt, userContent: userContent);\n }\n\n /// Chat with the Atıksız mascot (friendly, eco-conscious, zero-waste expert).\n Future<String> chatWithMascot(String message) async {\n final trimmed = message.trim();\n if (trimmed.isEmpty) return 'Atıksız mutfak veya ipuçları hakkında ne olursa olsun sorabilirsiniz!';\n return _chat(systemPrompt: _mascotSystemPrompt, userContent: trimmed);\n }\n}\n"}, "11800a99c935384eb45e936c61d36bc69b44edaf": {"t": 1785757870.1747975, "n": 5047, "h": "f6a605f4ea3cd3be", "c": "import 'package:dio/dio.dart';\n\nimport '../constants/app_constants.dart';\n\n/// Thrown when DeepSeek API key is missing or invalid.\nclass DeepSeekAuthException implements Exception {\n DeepSeekAuthException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Invalid or missing DeepSeek API key';\n}\n\n/// Thrown when the request times out.\nclass DeepSeekTimeoutException implements Exception {\n DeepSeekTimeoutException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Request timed out';\n}\n\n/// Thrown when the API returns an error.\nclass DeepSeekApiException implements Exception {\n DeepSeekApiException(this.statusCode, [this.message]);\n final int statusCode;\n final String? message;\n @override\n String toString() => message ?? 'API error (status: $statusCode)';\n}\n\n/// Service for DeepSeek chat API: recipe generation and mascot chat.\nclass DeepSeekService {\n DeepSeekService(this._dio, {String? apiKey}) : _apiKey = apiKey ?? AppConstants.deepSeekApiKey;\n\n final Dio _dio;\n final String _apiKey;\n\n static const String _recipeSystemPrompt = '''\nSen profesyonel bir şefsin. Kullanıcının verdiği malzemelerle yaratıcı bir tarif oluştur.\nYanıtını Türkçe yaz. Tarifte başlık, kısa açıklama, malzemeler listesi ve adım adım yapılış olsun.\nPratik ve ev mutfağına uygun olsun. Mümkünse sıfır atık ipuçları ver.\n''';\n\n static const String _mascotSystemPrompt = '''\nSen ZeroWaste Mutfak maskotusun (EcoChef). Çevre bilincine sahip, sıcak ve sıfır atık yemek konusunda uzmansın.\nGıda israfını azaltma, artakalanları kullanma ve sürdürülebilir pişirme hakkında kısa, faydalı ipuçları ver.\nTüm yanıtlarını Türkçe yaz. Kısa, teşvik edici ve samimi bir dil kullan.\n''';\n\n /// Sends a chat request to DeepSeek and returns the assistant message content.\n Future<String> _chat({\n required String systemPrompt,\n required String userContent,\n }) async {\n if (_apiKey.isEmpty) {\n throw DeepSeekAuthException('DEEPSEEK_API_KEY is not set.');\n }\n\n try {\n final response = await _dio.post<Map<String, dynamic>>(\n AppConstants.deepSeekChatPath,\n data: <String, dynamic>{\n 'model': AppConstants.deepSeekModel,\n 'messages': <Map<String, String>>[\n <String, String>{'role': 'system', 'content': systemPrompt},\n <String, String>{'role': 'user', 'content': userContent},\n ],\n 'max_tokens': 1024,\n },\n options: Options(\n headers: <String, String>{\n 'Authorization': 'Bearer $_apiKey',\n },\n sendTimeout: const Duration(seconds: 60),\n receiveTimeout: const Duration(seconds: 60),\n ),\n );\n final data = response.data;\n if (data == null) throw DeepSeekApiException(response.statusCode ?? -1, 'Empty response');\n final choices = data['choices'] as List<dynamic>?;\n final content = choices?.isNotEmpty == true\n ? (choices!.first as Map<String, dynamic>)['message'] as Map<String, dynamic>?\n : null;\n final text = content?['content'] as String?;\n if (text == null || text.isEmpty) {\n throw DeepSeekApiException(response.statusCode ?? -1, 'No content in response');\n }\n return text.trim();\n } on DioException catch (e) {\n if (e.type == DioExceptionType.connectionTimeout ||\n e.type == DioExceptionType.sendTimeout ||\n e.type == DioExceptionType.receiveTimeout) {\n throw DeepSeekTimeoutException(e.message);\n }\n final statusCode = e.response?.statusCode;\n if (statusCode == 401 || statusCode == 403) {\n throw DeepSeekAuthException(\n statusCode == 401 ? 'Invalid API key.' : 'Access forbidden. Check your API key.',\n );\n }\n throw DeepSeekApiException(\n statusCode ?? -1,\n e.response?.data?.toString() ?? e.message ?? 'Network error',\n );\n }\n }\n\n /// Generates a recipe using the given ingredients and optional cuisine style.\n Future<String> generateRecipe(List<String> ingredients, {String? cuisine}) async {\n if (ingredients.isEmpty) {\n return 'Lütfen en az bir malzeme ekleyin.';\n }\n final list = ingredients.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();\n if (list.isEmpty) return 'Lütfen en az bir malzeme ekleyin.';\n var userContent = 'Sadece şu malzemelerle bir tarif oluştur:\\n${list.join('\\n')}';\n if (cuisine != null && cuisine.isNotEmpty) {\n userContent += '\\n\\nTercih edilen mutfak: $cuisine. Tarif bu mutfağın lezzet ve tekniklerine uygun olsun.';\n }\n return _chat(systemPrompt: _recipeSystemPrompt, userContent: userContent);\n }\n\n /// Chat with the ZeroWaste mascot (friendly, eco-conscious, zero-waste expert).\n Future<String> chatWithMascot(String message) async {\n final trimmed = message.trim();\n if (trimmed.isEmpty) return 'Sıfır atık mutfak veya ipuçları hakkında ne olursa olsun sorabilirsiniz!';\n return _chat(systemPrompt: _mascotSystemPrompt, userContent: trimmed);\n }\n}\n"}, "d9c1014dbf8689a703c2e5251b766cb04a5c045e": {"t": 1785757871.6279285, "n": 5045, "h": "3065cf521417fd20", "c": "import 'package:dio/dio.dart';\n\nimport '../constants/app_constants.dart';\n\n/// Thrown when DeepSeek API key is missing or invalid.\nclass DeepSeekAuthException implements Exception {\n DeepSeekAuthException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Invalid or missing DeepSeek API key';\n}\n\n/// Thrown when the request times out.\nclass DeepSeekTimeoutException implements Exception {\n DeepSeekTimeoutException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Request timed out';\n}\n\n/// Thrown when the API returns an error.\nclass DeepSeekApiException implements Exception {\n DeepSeekApiException(this.statusCode, [this.message]);\n final int statusCode;\n final String? message;\n @override\n String toString() => message ?? 'API error (status: $statusCode)';\n}\n\n/// Service for DeepSeek chat API: recipe generation and mascot chat.\nclass DeepSeekService {\n DeepSeekService(this._dio, {String? apiKey}) : _apiKey = apiKey ?? AppConstants.deepSeekApiKey;\n\n final Dio _dio;\n final String _apiKey;\n\n static const String _recipeSystemPrompt = '''\nSen profesyonel bir şefsin. Kullanıcının verdiği malzemelerle yaratıcı bir tarif oluştur.\nYanıtını Türkçe yaz. Tarifte başlık, kısa açıklama, malzemeler listesi ve adım adım yapılış olsun.\nPratik ve ev mutfağına uygun olsun. Mümkünse sıfır atık ipuçları ver.\n''';\n\n static const String _mascotSystemPrompt = '''\nSen ZeroWaste Mutfak maskotusun (Leafy). Çevre bilincine sahip, sıcak ve sıfır atık yemek konusunda uzmansın.\nGıda israfını azaltma, artakalanları kullanma ve sürdürülebilir pişirme hakkında kısa, faydalı ipuçları ver.\nTüm yanıtlarını Türkçe yaz. Kısa, teşvik edici ve samimi bir dil kullan.\n''';\n\n /// Sends a chat request to DeepSeek and returns the assistant message content.\n Future<String> _chat({\n required String systemPrompt,\n required String userContent,\n }) async {\n if (_apiKey.isEmpty) {\n throw DeepSeekAuthException('DEEPSEEK_API_KEY is not set.');\n }\n\n try {\n final response = await _dio.post<Map<String, dynamic>>(\n AppConstants.deepSeekChatPath,\n data: <String, dynamic>{\n 'model': AppConstants.deepSeekModel,\n 'messages': <Map<String, String>>[\n <String, String>{'role': 'system', 'content': systemPrompt},\n <String, String>{'role': 'user', 'content': userContent},\n ],\n 'max_tokens': 1024,\n },\n options: Options(\n headers: <String, String>{\n 'Authorization': 'Bearer $_apiKey',\n },\n sendTimeout: const Duration(seconds: 60),\n receiveTimeout: const Duration(seconds: 60),\n ),\n );\n final data = response.data;\n if (data == null) throw DeepSeekApiException(response.statusCode ?? -1, 'Empty response');\n final choices = data['choices'] as List<dynamic>?;\n final content = choices?.isNotEmpty == true\n ? (choices!.first as Map<String, dynamic>)['message'] as Map<String, dynamic>?\n : null;\n final text = content?['content'] as String?;\n if (text == null || text.isEmpty) {\n throw DeepSeekApiException(response.statusCode ?? -1, 'No content in response');\n }\n return text.trim();\n } on DioException catch (e) {\n if (e.type == DioExceptionType.connectionTimeout ||\n e.type == DioExceptionType.sendTimeout ||\n e.type == DioExceptionType.receiveTimeout) {\n throw DeepSeekTimeoutException(e.message);\n }\n final statusCode = e.response?.statusCode;\n if (statusCode == 401 || statusCode == 403) {\n throw DeepSeekAuthException(\n statusCode == 401 ? 'Invalid API key.' : 'Access forbidden. Check your API key.',\n );\n }\n throw DeepSeekApiException(\n statusCode ?? -1,\n e.response?.data?.toString() ?? e.message ?? 'Network error',\n );\n }\n }\n\n /// Generates a recipe using the given ingredients and optional cuisine style.\n Future<String> generateRecipe(List<String> ingredients, {String? cuisine}) async {\n if (ingredients.isEmpty) {\n return 'Lütfen en az bir malzeme ekleyin.';\n }\n final list = ingredients.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();\n if (list.isEmpty) return 'Lütfen en az bir malzeme ekleyin.';\n var userContent = 'Sadece şu malzemelerle bir tarif oluştur:\\n${list.join('\\n')}';\n if (cuisine != null && cuisine.isNotEmpty) {\n userContent += '\\n\\nTercih edilen mutfak: $cuisine. Tarif bu mutfağın lezzet ve tekniklerine uygun olsun.';\n }\n return _chat(systemPrompt: _recipeSystemPrompt, userContent: userContent);\n }\n\n /// Chat with the ZeroWaste mascot (friendly, eco-conscious, zero-waste expert).\n Future<String> chatWithMascot(String message) async {\n final trimmed = message.trim();\n if (trimmed.isEmpty) return 'Sıfır atık mutfak veya ipuçları hakkında ne olursa olsun sorabilirsiniz!';\n return _chat(systemPrompt: _mascotSystemPrompt, userContent: trimmed);\n }\n}\n"}, "3be0962556864cdc1158a33dd7b347a644cc25c7": {"t": 1786068343.7093413, "n": 14398, "h": "3b8471ed9501568c", "c": "import 'dart:async';\n\nimport 'package:dio/dio.dart';\n\nimport '../constants/app_constants.dart';\n\n/// Thrown when DeepSeek API key is missing or invalid.\nclass DeepSeekAuthException implements Exception {\n DeepSeekAuthException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Invalid or missing DeepSeek API key';\n}\n\n/// Thrown when the request times out after waiting for a server response.\nclass DeepSeekTimeoutException implements Exception {\n DeepSeekTimeoutException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Request timed out';\n}\n\n/// Thrown when we can't even reach the server (DNS, no internet, blocked).\n/// Distinct from [DeepSeekTimeoutException] which means we connected but the\n/// server didn't reply in time.\nclass DeepSeekConnectionException implements Exception {\n DeepSeekConnectionException([this.message]);\n final String? message;\n @override\n String toString() => message ?? 'Could not reach the server';\n}\n\n/// Thrown when the API returns an error.\nclass DeepSeekApiException implements Exception {\n DeepSeekApiException(this.statusCode, [this.message]);\n final int statusCode;\n final String? message;\n @override\n String toString() => message ?? 'API error (status: $statusCode)';\n}\n\n/// Service for DeepSeek chat API: recipe generation and mascot chat.\nclass DeepSeekService {\n DeepSeekService(this._dio, {String? apiKey}) : _apiKey = apiKey ?? AppConstants.deepSeekApiKey;\n\n final Dio _dio;\n final String _apiKey;\n\n static String _recipeSystemPrompt(String languageCode) {\n if (languageCode == 'en') {\n return '''\nYou are EcoChef, a professional chef. Create a creative recipe from the user's ingredients. Use a friendly, casual tone.\n\nLANGUAGE — highest priority:\n- Write the entire recipe in English only. Never use Turkish words or headers.\n- The recipe title (text after \"## Title:\") MUST be in English — e.g. \"Tomato & Egg Skillet\", NOT \"Domatesli Omlet\".\n- Use \"## Title:\" only. NEVER use \"## Başlık:\" or any Turkish section headers.\n\nNever add intro phrases, greetings, \"sure\", \"here is your recipe\", or similar. Start directly with the recipe.\n\nUse exactly this format:\n\n## Title: [recipe name]\n\n**Short Description:** [1-2 sentence description]\n\n**Ingredients:**\n- [ingredient 1]\n- [ingredient 2]\n\n**Steps:**\n1. [step 1]\n2. [step 2]\n\nKeep it practical and home-kitchen friendly. Add zero-waste tips when possible (optional).\n''';\n }\n\n return '''\nYou are EcoChef, a professional chef. Create a creative recipe from the user's ingredients. Use a friendly, casual tone.\n\nLANGUAGE — highest priority:\n- Write the entire recipe in Turkish only. Never use English words or headers.\n\nNever add intro phrases, greetings, \"elbette\", \"işte tarifiniz\", \"karşınızda\", or similar. Start directly with the recipe.\n\nUse exactly this format:\n\n## Başlık: [tarif adı]\n\n**Kısa Açıklama:** [1-2 cümlelik açıklama]\n\n**Malzemeler:**\n- [malzeme 1]\n- [malzeme 2]\n\n**Yapılış Adımları:**\n1. [adım 1]\n2. [adım 2]\n\nKeep it practical and home-kitchen friendly. Add zero-waste tips when possible (optional).\n''';\n }\n\n static const String _mascotSystemPrompt = '''\nYou are EcoChef, the mascot of Zero-Waste Kitchen (Atıksız Mutfak). You are warm, eco-conscious, and an expert on reducing food waste, using leftovers, and sustainable cooking.\n\nLANGUAGE — highest priority, never violate:\n- Read the user's latest message and reply entirely in that same language only.\n- English in → English out. Every word must be English. No Turkish words, greetings, or phrases.\n- Turkish in → Turkish out. Every word must be Turkish.\n- Never default to Turkish because of your brand name, this prompt, or the app locale.\n- Do not mix languages in one reply. Do not translate the user's question unless they ask.\n- If a [LANGUAGE RULE] tag is present in the user message, follow it exactly.\n\nStyle: short, helpful, encouraging, friendly. Stay on zero-waste cooking and kitchen topics.\n''';\n\n /// Retry budget for transient network failures. The first attempt is\n /// immediate, retries wait [_retryBackoffs] before firing. Keep this small\n /// — the user is staring at a loading indicator.\n static const List<Duration> _retryBackoffs = [\n Duration(milliseconds: 500),\n Duration(milliseconds: 1500),\n ];\n\n /// How many prior+current bubbles to send to the model (user + assistant).\n static const int mascotHistoryLimit = 20;\n\n /// Soft-cap long EcoChef replies in history to keep token use bounded.\n static const int _maxHistoryAssistantChars = 1200;\n\n /// Sends a chat request to DeepSeek and returns the assistant message content.\n Future<String> _chat({\n required String systemPrompt,\n required List<Map<String, String>> conversation,\n }) async {\n if (_apiKey.isEmpty) {\n throw DeepSeekAuthException('DEEPSEEK_API_KEY is not set.');\n }\n\n // Total attempts = initial + retries. We only retry on network-layer\n // failures (no socket, DNS, timeouts) — never on 4xx/5xx, since those\n // are either programmer error or paid-for failures we shouldn't double-charge for.\n final maxAttempts = _retryBackoffs.length + 1;\n Object? lastError;\n\n for (int attempt = 0; attempt < maxAttempts; attempt++) {\n try {\n return await _postOnce(\n systemPrompt: systemPrompt,\n conversation: conversation,\n );\n } on DeepSeekTimeoutException catch (e) {\n lastError = e;\n } on DeepSeekConnectionException catch (e) {\n lastError = e;\n }\n\n // Wait before the next attempt (skip wait after the last attempt).\n if (attempt < maxAttempts - 1) {\n await Future<void>.delayed(_retryBackoffs[attempt]);\n }\n }\n\n // All attempts exhausted — surface the most recent failure.\n throw lastError!;\n }\n\n /// One round-trip; never retries. Wrapped by [_chat] for the retry loop.\n Future<String> _postOnce({\n required String systemPrompt,\n required List<Map<String, String>> conversation,\n }) async {\n try {\n final response = await _dio.post<Map<String, dynamic>>(\n AppConstants.deepSeekChatPath,\n data: <String, dynamic>{\n 'model': AppConstants.deepSeekModel,\n 'messages': <Map<String, String>>[\n <String, String>{'role': 'system', 'content': systemPrompt},\n ...conversation,\n ],\n 'max_tokens': 1024,\n },\n options: Options(\n headers: <String, String>{\n 'Authorization': 'Bearer $_apiKey',\n },\n // All three timeouts are overridden here. Previously connectTimeout\n // fell back to the 30s default from NetworkService, which masqueraded\n // as \"60s\" in the per-request config.\n connectTimeout: const Duration(seconds: 60),\n sendTimeout: const Duration(seconds: 60),\n receiveTimeout: const Duration(seconds: 60),\n ),\n );\n final data = response.data;\n if (data == null) throw DeepSeekApiException(response.statusCode ?? -1, 'Empty response');\n final choices = data['choices'] as List<dynamic>?;\n final content = choices?.isNotEmpty == true\n ? (choices!.first as Map<String, dynamic>)['message'] as Map<String, dynamic>?\n : null;\n final text = content?['content'] as String?;\n if (text == null || text.isEmpty) {\n throw DeepSeekApiException(response.statusCode ?? -1, 'No content in response');\n }\n return text.trim();\n } on DioException catch (e) {\n // Connection-level: server was never reached. Worth retrying.\n // connectionError covers DNS, socket, refused, host unreachable — Dio's\n // cross-platform shorthand for \"we never got bytes flowing\".\n if (e.type == DioExceptionType.connectionError) {\n throw DeepSeekConnectionException(e.message);\n }\n // Timeouts — these split into \"couldn't open socket\" (connectTimeout,\n // also retry-worthy) and \"server slow to respond\" (send/receive). We\n // bucket the first as a connection error and the others as proper timeouts.\n if (e.type == DioExceptionType.connectionTimeout) {\n throw DeepSeekConnectionException(e.message);\n }\n if (e.type == DioExceptionType.sendTimeout ||\n e.type == DioExceptionType.receiveTimeout) {\n throw DeepSeekTimeoutException(e.message);\n }\n final statusCode = e.response?.statusCode;\n if (statusCode == 401 || statusCode == 403) {\n throw DeepSeekAuthException(\n statusCode == 401 ? 'Invalid API key.' : 'Access forbidden. Check your API key.',\n );\n }\n throw DeepSeekApiException(\n statusCode ?? -1,\n e.response?.data?.toString() ?? e.message ?? 'Network error',\n );\n }\n }\n\n /// Generates a recipe using the given ingredients and optional cuisine style.\n Future<String> generateRecipe(\n List<String> ingredients, {\n String? cuisine,\n String languageCode = 'tr',\n }) async {\n final isEnglish = languageCode == 'en';\n if (ingredients.isEmpty) {\n return isEnglish\n ? 'Please add at least one ingredient.'\n : 'Lütfen en az bir malzeme ekleyin.';\n }\n final list = ingredients.map((e) => e.trim()).where((e) => e.isNotEmpty).toList();\n if (list.isEmpty) {\n return isEnglish\n ? 'Please add at least one ingredient.'\n : 'Lütfen en az bir malzeme ekleyin.';\n }\n\n final languageRule = isEnglish\n ? 'Write the entire recipe in English only. The recipe title must be in English. Use \"## Title:\" as the first header — never \"## Başlık:\".'\n : 'Tarifin tamamını yalnızca Türkçe yaz. Tüm başlıklar, malzemeler ve adımlar Türkçe olmalı.';\n\n var userContent = isEnglish\n ? 'Create a recipe using only these ingredients:\\n${list.join('\\n')}\\n\\n'\n 'Ingredients may be in any language, but the recipe title, description, '\n 'ingredient names in the list, and steps must all be in English. '\n 'Give the dish an English name (e.g. \"Tomato & Egg Skillet\", not \"Domatesli Omlet\").'\n : 'Sadece şu malzemelerle bir tarif oluştur:\\n${list.join('\\n')}';\n if (cuisine != null && cuisine.isNotEmpty) {\n userContent += isEnglish\n ? '\\n\\nPreferred cuisine: $cuisine. Match this cuisine\\'s flavors and techniques.'\n : '\\n\\nTercih edilen mutfak: $cuisine. Tarif bu mutfağın lezzet ve tekniklerine uygun olsun.';\n }\n\n userContent = '''\n[LANGUAGE RULE: $languageRule]\n\n$userContent''';\n\n return _chat(\n systemPrompt: _recipeSystemPrompt(languageCode),\n conversation: [\n <String, String>{'role': 'user', 'content': userContent},\n ],\n );\n }\n\n /// Chat with the Atıksız mascot (friendly, eco-conscious, zero-waste expert).\n ///\n /// [priorTurns] is the conversation **before** [message] (oldest → newest).\n /// The last [mascotHistoryLimit] bubbles (prior + current) are sent to the model.\n Future<String> chatWithMascot(\n String message, {\n List<({String text, bool isUser})> priorTurns = const [],\n }) async {\n final trimmed = message.trim();\n if (trimmed.isEmpty) {\n return 'Ask me anything about zero-waste cooking and kitchen tips! / Sıfır atık mutfak ve ipuçları hakkında ne olursa olsun sorabilirsin!';\n }\n\n final languageRule = _languageRuleForMessage(trimmed);\n final latestUserContent = '''\n[LANGUAGE RULE: $languageRule]\n\nUser message:\n$trimmed''';\n\n final turns = <({String text, bool isUser})>[\n ...priorTurns,\n (text: trimmed, isUser: true),\n ];\n final window = turns.length > mascotHistoryLimit\n ? turns.sublist(turns.length - mascotHistoryLimit)\n : turns;\n\n final conversation = <Map<String, String>>[];\n for (var i = 0; i < window.length; i++) {\n final turn = window[i];\n final isLast = i == window.length - 1;\n if (turn.isUser) {\n conversation.add({\n 'role': 'user',\n 'content': isLast ? latestUserContent : turn.text,\n });\n } else {\n conversation.add({\n 'role': 'assistant',\n 'content': _truncateHistoryText(turn.text),\n });\n }\n }\n\n return _chat(\n systemPrompt: _mascotSystemPrompt,\n conversation: conversation,\n );\n }\n\n static String _truncateHistoryText(String text) {\n if (text.length <= _maxHistoryAssistantChars) return text;\n return '${text.substring(0, _maxHistoryAssistantChars)}…';\n }\n\n /// Returns an explicit reply-language instruction derived from the user's text.\n static String _languageRuleForMessage(String message) {\n switch (_detectMessageLanguage(message)) {\n case _MessageLanguage.english:\n return 'The user wrote in English. Reply ONLY in English. Do not use any Turkish.';\n case _MessageLanguage.turkish:\n return 'Kullanıcı Türkçe yazdı. Yanıtını YALNIZCA Türkçe ver. İngilizce kullanma.';\n case _MessageLanguage.unknown:\n return 'Detect the language of the user message and reply entirely in that same language only.';\n }\n }\n\n static _MessageLanguage _detectMessageLanguage(String message) {\n final lower = message.toLowerCase();\n\n if (RegExp(r'[ğüşöçı]').hasMatch(lower)) {\n return _MessageLanguage.turkish;\n }\n\n const turkishWords = [\n 'merhaba', 'nasıl', 'neden', 'ne ', ' bir ', 'için', 'olan', 'var',\n 'yok', 'yemek', 'mutfak', 'atık', 'tarif', 'pişir', 'lütfen',\n 'teşekkür', 'evet', 'hayır', 'artık', 'malzeme', 'soğan', 'domates',\n ];\n const englishWords = [\n 'hello', 'how', 'what', 'the', 'is', 'are', 'you', 'can', 'could',\n 'would', 'please', 'thanks', 'thank', 'yes', 'no', 'why', 'when',\n 'where', 'food', 'waste', 'recipe', 'cook', 'kitchen', 'leftover',\n 'ingredient', 'help', 'good', 'morning', 'evening', 'hey', 'hi ',\n ];\n\n var trScore = 0;\n var enScore = 0;\n for (final word in turkishWords) {\n if (lower.contains(word)) trScore++;\n }\n for (final word in englishWords) {\n if (RegExp('\\\\b${RegExp.escape(word.trim())}\\\\b').hasMatch(lower)) {\n enScore++;\n }\n }\n\n if (trScore > enScore) return _MessageLanguage.turkish;\n if (enScore > trScore) return _MessageLanguage.english;\n if (enScore > 0) return _MessageLanguage.english;\n\n return _MessageLanguage.unknown;\n }\n}\n\nenum _MessageLanguage { english, turkish, unknown }\n"}}}}