{"repo": "ahmaddzulkarnainyoo-dev/winatra-ai", "files": {"android/app/src/main/kotlin/com/example/winatra_ai/WinatraService.kt": {"f666cd5c72f9e3514945e4c0fef876b155a16ff7": {"t": 1786067752.6490183, "n": 36188, "h": "704801f9a9768a0a", "c": "package com.example.winatra_ai\n\nimport android.app.*\nimport android.content.ClipboardManager\nimport android.content.Context\nimport android.content.Intent\nimport android.os.Build\nimport android.os.IBinder\nimport android.util.Log\nimport android.widget.Toast\nimport androidx.core.app.NotificationCompat\nimport com.google.firebase.auth.FirebaseAuth\nimport com.google.firebase.firestore.FirebaseFirestore\nimport kotlinx.coroutines.*\nimport kotlinx.coroutines.tasks.await\nimport okhttp3.*\nimport okhttp3.MediaType.Companion.toMediaType\nimport okhttp3.RequestBody.Companion.toRequestBody\nimport org.json.JSONObject\nimport java.text.SimpleDateFormat\nimport java.util.Date\nimport java.util.Locale\nimport kotlin.random.Random\n\nclass WinatraService : Service() {\n\n companion object {\n const val CHANNEL_ID = \"winatra_channel\"\n const val RESULT_CHANNEL_ID = \"winatra_result_channel\"\n const val NOTIF_ID = 1\n const val RESULT_NOTIF_ID = 100\n const val ACTION_ANSWER = \"ACTION_ANSWER\"\n const val ACTION_CHANGE_MODE = \"ACTION_CHANGE_MODE\"\n const val ACTION_CLIPBOARD_RESULT = \"CLIPBOARD_RESULT\"\n const val ACTION_ACCESSIBILITY_RESULT = \"ACCESSIBILITY_RESULT\"\n const val ACTION_EXPLAIN = \"ACTION_EXPLAIN\"\n const val ACTION_COPY_ANSWER = \"ACTION_COPY_ANSWER\"\n const val PREFS_NAME = \"winatra_prefs\"\n const val KEY_MODE = \"mode\"\n const val PREF_KEY_LAST_QUOTA_RESET = \"last_quota_reset\"\n const val DEFAULT_DAILY_QUOTA = 8\n const val TAG = \"WinatraService\"\n const val DEEPSEEK_WEIGHT = 96\n\n data class ApiEndpoint(val key: String, val baseUrl: String, val type: String)\n\n // Hanya DeepSeek endpoints — Groq dihapus total\n private val API_ENDPOINTS = listOf(\n ApiEndpoint(\"sk-3f82fc1304a1486b99ae94d4bb9b59b4\", \"https://api.deepseek.com/v1\", \"DeepSeek\"),\n )\n\n // ========== SPECIALIZATION PROMPTS MAPPING ==========\n private val SPECIALIZATION_PROMPTS = mapOf(\n \"general\" to \"Anda adalah asisten AI yang membantu menjawab pertanyaan dengan jelas, akurat, dan mendalam.\",\n // Mata Kuliah Universitas\n \"fisika\" to \"Anda adalah ahli Fisika berpengalaman. Jawab dengan rumus yang relevan, penjelasan konsep yang mendalam, dan contoh aplikasi nyata. Sertakan satuan dan dimensi ketika diperlukan.\",\n \"kimia\" to \"Anda adalah ahli Kimia berpengalaman. Jelaskan dengan persamaan reaksi, struktur molekul, dan mekanisme reaksi. Berikan contoh praktis dalam kehidupan sehari-hari.\",\n \"biologi\" to \"Anda adalah ahli Biologi berpengalaman. Jelaskan proses biologis dengan detail, sertakan nama-nama ilmiah (Latin), dan contoh organisme konkret. Bahas mekanisme sel dan genetika jika relevan.\",\n \"matematika\" to \"Anda adalah tutor Matematika berpengalaman. Berikan solusi langkah demi langkah, tunjukkan setiap proses perhitungan, dan jelaskan konsep dasarnya. Sertakan visualisasi atau contoh numerik.\",\n \"statistika\" to \"Anda adalah ahli Statistika. Jelaskan dengan konsep probabilitas, distribusi, dan metode analisis. Gunakan notasi statistik yang tepat dan berikan contoh penerapan data real.\",\n \"ilmu_komputer\" to \"Anda adalah ahli Ilmu Komputer. Jelaskan dengan algoritma, kompleksitas waktu (Big O), pseudocode, dan contoh implementasi. Bahas best practices dan trade-offs.\",\n \"teknik_sipil\" to \"Anda adalah insinyur Teknik Sipil berpengalaman. Jelaskan dengan prinsip struktur, beban, material, dan standar konstruksi Indonesia. Sertakan perhitungan teknis jika relevan.\",\n \"teknik_elektro\" to \"Anda adalah insinyur Teknik Elektro. Jelaskan dengan rangkaian listrik, tegangan, arus, frekuensi, dan teori Thevenin-Norton jika relevan. Gunakan diagram skematik mental.\",\n \"teknik_mesin\" to \"Anda adalah insinyur Teknik Mesin. Jelaskan dengan prinsip mekanika, termodinamika, keseimbangan gaya, dan material properties. Sertakan perhitungan kekuatan bahan jika perlu.\",\n \"hukum\" to \"Anda adalah praktisi Hukum Indonesia berpengalaman. Jawab berdasarkan peraturan perundang-undangan, kasus preseden, dan asas-asas hukum. Sebutkan pasal dan sumber peraturan yang relevan.\",\n \"ekonomi\" to \"Anda adalah ekonom berpengalaman. Jelaskan dengan teori mikro-makro, penawaran-permintaan, elastisitas, dan contoh ekonomi Indonesia. Bahas dampak kebijakan nyata.\",\n \"akuntansi\" to \"Anda adalah akuntan profesional. Jelaskan dengan prinsip debit-kredit, jurnal, buku besar, dan laporan keuangan sesuai standar PSAK. Sertakan jurnal contoh jika relevan.\",\n \"manajemen\" to \"Anda adalah manajer berpengalaman. Jelaskan dengan fungsi manajemen (planning, organizing, leading, controlling), teori organisasi, dan best practices industri.\",\n \"psikologi\" to \"Anda adalah psikolog berpengalaman. Jelaskan dengan teori psikologi, penelitian empiris, dan aplikasi praktis. Sebutkan tokoh psikolog yang relevan dan teori mereka.\",\n \"sosiologi\" to \"Anda adalah sosiolog berpengalaman. Jelaskan dengan teori sosial, struktur masyarakat, interaksi sosial, dan konteks budaya Indonesia.\",\n \"filsafat\" to \"Anda adalah filsuf berpengalaman. Jelaskan dengan argumen logis, contoh pemikiran dari berbagai aliran filsafat, dan implikasi filosofis.\",\n \"sejarah\" to \"Anda adalah sejarawan berpengalaman. Jelaskan dengan konteks historis, fakta akurat, kronologi, dan analisis sebab-akibat. Sertakan tokoh sejarah relevan.\",\n \"geografi\" to \"Anda adalah ahli Geografi. Jelaskan dengan aspek fisik (alam) dan sosial (manusia), lokasi, peta mental, iklim, dan dampak geografis.\",\n \"linguistik\" to \"Anda adalah ahli Linguistik. Jelaskan dengan teori bahasa, struktur gramatika, semantik, dan fonologi. Berikan contoh analisis bahasa konkret.\",\n \"sastra\" to \"Anda adalah kritikus sastra berpengalaman. Jelaskan dengan analisis teks, gaya bahasa, makna filosofis, dan konteks budaya pengarang.\",\n \"pendidikan\" to \"Anda adalah pendidik berpengalaman. Jelaskan dengan teori belajar, metode mengajar, kurikulum, dan pedagogis yang efektif.\",\n \"kesehatan_masyarakat\" to \"Anda adalah ahli Kesehatan Masyarakat. Jelaskan dengan epidemiologi, pencegahan penyakit, promosi kesehatan, dan kebijakan kesehatan.\",\n \"kedokteran\" to \"Anda adalah dokter berpengalaman. Jawab dengan patofisiologi, diagnosis diferensial, terapi farmakologi, dan evidence-based medicine. CATATAN: Konsultasi dokter sungguhan untuk diagnosis nyata.\",\n \"farmasi\" to \"Anda adalah apoteker berpengalaman. Jelaskan dengan farmakokinetik, farmakodinamik, indikasi obat, efek samping, dan interaksi obat.\",\n \"keperawatan\" to \"Anda adalah perawat berpengalaman. Jelaskan dengan asuhan keperawatan, standar perawatan, asepsis, dan patient care.\",\n \"arsitektur\" to \"Anda adalah arsitek berpengalaman. Jelaskan dengan desain ruang, estetika, fungsi bangunan, material, dan standar desain Indonesia.\",\n \"seni_rupa\" to \"Anda adalah seniman dan kritikus seni rupa. Jelaskan dengan teori seni, elemen desain, teknik, aliran seni, dan konteks budaya.\",\n \"desain_komunikasi_visual\" to \"Anda adalah desainer komunikasi visual berpengalaman. Jelaskan dengan prinsip desain, tipografi, warna, layout, dan psikologi visual.\",\n \"hubungan_internasional\" to \"Anda adalah ahli Hubungan Internasional. Jelaskan dengan teori HI, kebijakan luar negeri, diplomasi, dan dinamika global.\",\n \"ilmu_politik\" to \"Anda adalah ahli Ilmu Politik. Jelaskan dengan teori politik, sistem pemerintahan, kepemimpinan, dan dinamika kekuasaan.\",\n \"komunikasi\" to \"Anda adalah ahli Komunikasi. Jelaskan dengan teori komunikasi, proses komunikasi, media massa, dan strategi komunikasi.\",\n \"jurnalistik\" to \"Anda adalah jurnalis berpengalaman. Jelaskan dengan kode etik jurnalistik, penulisan berita, investigasi, dan newsworthiness.\",\n\n // Mata Pelajaran SMA/SMP/SD\n \"matematika_wajib\" to \"Anda adalah guru Matematika Wajib berpengalaman. Jelaskan dengan konsep dasar, rumus, dan contoh soal berlevel SMA. Berikan langkah solusi yang mudah dipahami.\",\n \"matematika_peminatan\" to \"Anda adalah guru Matematika Peminatan berpengalaman. Jelaskan kalkulus, trigonometri lanjut, atau aljabar dengan detail dan contoh aplikasi.\",\n \"fisika_sma\" to \"Anda adalah guru Fisika SMA berpengalaman. Jelaskan dengan rumus, diagram gaya, dan contoh fenomena sehari-hari yang mudah dipahami siswa.\",\n \"kimia_sma\" to \"Anda adalah guru Kimia SMA berpengalaman. Jelaskan dengan persamaan reaksi, struktur atom, dan eksperimen laboratorium level SMA.\",\n \"biologi_sma\" to \"Anda adalah guru Biologi SMA berpengalaman. Jelaskan dengan mekanisme biologi, ekosistem, dan contoh organisme konkret yang mudah dipahami.\",\n \"ekonomi_sma\" to \"Anda adalah guru Ekonomi SMA berpengalaman. Jelaskan dengan penawaran-permintaan, konsumsi, produksi, dan ekonomi Indonesia level SMA.\",\n \"geografi_sma\" to \"Anda adalah guru Geografi SMA berpengalaman. Jelaskan dengan peta, lokasi, iklim, dan fenomena geografis dengan contoh konkret.\",\n \"sejarah_sma\" to \"Anda adalah guru Sejarah SMA berpengalaman. Jelaskan dengan kronologi, tokoh sejarah, dan dampak peristiwa dengan analisis yang mudah dipahami.\",\n \"sosiologi_sma\" to \"Anda adalah guru Sosiologi SMA berpengalaman. Jelaskan dengan struktur sosial, interaksi masyarakat, dan dinamika budaya.\",\n \"antropologi_sma\" to \"Anda adalah guru Antropologi SMA berpengalaman. Jelaskan dengan budaya, tradisi, etnis, dan keragaman budaya Indonesia.\",\n \"bahasa_indonesia\" to \"Anda adalah guru Bahasa Indonesia berpengalaman. Jelaskan dengan tata bahasa, puisi, prosa, analisis teks, dan EYD yang benar.\",\n \"bahasa_inggris\" to \"Anda adalah guru Bahasa Inggris berpengalaman. Jelaskan dengan grammar, vocabulary, pronunciation, dan contoh dialog sehari-hari.\",\n \"bahasa_arab\" to \"Anda adalah guru Bahasa Arab berpengalaman. Jelaskan dengan kaidah Bahasa Arab, vocabulary, dan contoh kalimat level SMA.\",\n \"pkn\" to \"Anda adalah guru PKN berpengalaman. Jelaskan dengan nilai-nilai Pancasila, UUD 1945, hak-kewajiban warga negara, dan sistem pemerintahan.\",\n \"agama_islam\" to \"Anda adalah guru Agama Islam berpengalaman. Jelaskan dengan ayat Al-Quran, hadis, akidah, ibadah, akhlak, dan hukum Islam.\",\n \"agama_kristen\" to \"Anda adalah guru Agama Kristen berpengalaman. Jelaskan dengan Alkitab, teologi Kristen, dan ajaran iman Kristen.\",\n \"agama_katolik\" to \"Anda adalah guru Agama Katolik berpengalaman. Jelaskan dengan Alkitab, katekismus, dan ajaran Gereja Katolik.\",\n \"agama_hindu\" to \"Anda adalah guru Agama Hindu berpengalaman. Jelaskan dengan Weda, Upanisad, dan filosofi Hindu.\",\n \"agama_buddha\" to \"Anda adalah guru Agama Buddha berpengalaman. Jelaskan dengan Tri Pitaka, Empat Kebenaran Mulia, dan ajaran Buddha.\",\n \"seni_budaya\" to \"Anda adalah guru Seni Budaya berpengalaman. Jelaskan dengan seni rupa, musik, tari, teater, dan warisan budaya Indonesia.\",\n \"prakarya\" to \"Anda adalah guru Prakarya berpengalaman. Jelaskan dengan teknik kerajinan, rekayasa, budi daya, dan proses produksi level SMA.\",\n \"pjok\" to \"Anda adalah guru PJOK berpengalaman. Jelaskan dengan teknik olahraga, kebugaran, kesehatan, dan peraturan permainan.\",\n \"informatika_tik\" to \"Anda adalah guru Informatika/TIK berpengalaman. Jelaskan dengan dasar komputer, pemrograman level pemula, dan literasi digital.\",\n \"kewirausahaan\" to \"Anda adalah guru Kewirausahaan berpengalaman. Jelaskan dengan peluang bisnis, rencana usaha, dan jiwa entrepreneur.\"\n )\n }\n\n private val scope = CoroutineScope(Dispatchers.IO + SupervisorJob())\n private val client = OkHttpClient.Builder()\n .connectTimeout(30, java.util.concurrent.TimeUnit.SECONDS)\n .readTimeout(60, java.util.concurrent.TimeUnit.SECONDS)\n .build()\n private var autoSolveEnabled = false\n private var clipboardListener: ClipboardManager.OnPrimaryClipChangedListener? = null\n\n override fun onCreate() {\n super.onCreate()\n createNotificationChannels()\n setupClipboardListener()\n showPersistentNotification()\n }\n\n override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {\n when (intent?.action) {\n ACTION_CHANGE_MODE -> toggleMode()\n ACTION_CLIPBOARD_RESULT -> {\n if (FirebaseAuth.getInstance().currentUser == null) {\n showResultNotification(\"Winatra AI\", \"Silakan login terlebih dahulu.\")\n return START_STICKY\n }\n val question = intent.getStringExtra(ClipboardActivity.EXTRA_QUESTION) ?: \"\"\n val modeType = intent.getStringExtra(ClipboardActivity.EXTRA_MODE_TYPE) ?: \"answer\"\n processClipboardResult(question, modeType)\n }\n ACTION_EXPLAIN -> {\n val question = intent.getStringExtra(\"question\") ?: \"\"\n val answer = intent.getStringExtra(\"answer\") ?: \"\"\n scope.launch { handleExplain(question, answer) }\n }\n ACTION_COPY_ANSWER -> {\n val answer = intent.getStringExtra(\"answer\") ?: \"\"\n if (answer.isNotEmpty()) {\n val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n clipboard.setPrimaryClip(android.content.ClipData.newPlainText(\"answer\", answer))\n Toast.makeText(this, \"Jawaban disalin\", Toast.LENGTH_SHORT).show()\n }\n }\n ACTION_ACCESSIBILITY_RESULT -> {\n val text = intent.getStringExtra(\"accessibility_text\") ?: \"\"\n processAccessibilityText(text)\n }\n \"SET_AUTO_SOLVE\" -> {\n updateAutoSolve(intent.getBooleanExtra(\"enabled\", false))\n showPersistentNotification()\n }\n \"SYNC_MODE\" -> {\n setMode(intent.getStringExtra(\"mode\") ?: getMode())\n showPersistentNotification()\n }\n \"SET_OFFLINE_MODE\" -> {\n setOfflineMode(intent.getBooleanExtra(\"enabled\", false))\n showPersistentNotification()\n }\n else -> showPersistentNotification()\n }\n return START_STICKY\n }\n\n override fun onBind(intent: Intent?): IBinder? = null\n\n override fun onDestroy() {\n clipboardListener?.let {\n val clipboard = getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager\n clipboard.removePrimaryClipChangedListener(it)\n }\n scope.cancel()\n super.onDestroy()\n }\n\n private fun createNotificationChannels() {\n if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {\n val manager = getSystemService(NotificationManager::class.java)\n manager.createNotificationChannel(NotificationChannel(CHANNEL_ID, \"Winatra AI Service\", NotificationManager.IMPORTANCE_LOW).apply {\n description = \"Winatra AI Shortcut Service\"\n setShowBadge(false)\n })\n manager.createNotificationChannel(NotificationChannel(RESULT_CHANNEL_ID, \"Winatra AI Jawaban\", NotificationManager.IMPORTANCE_HIGH).apply {\n description = \"Notifikasi hasil jawaban AI\"\n setShowBadge(true)\n enableVibration(true)\n setSound(null, null)\n })\n }\n }\n\n private fun getMode() = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getString(KEY_MODE, \"Essay\") ?: \"Essay\"\n private fun setMode(mode: String) = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putString(KEY_MODE, mode).apply()\n private fun isOfflineMode() = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getBoolean(\"offline_mode_enabled\", false)\n private fun setOfflineMode(enabled: Boolean) = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putBoolean(\"offline_mode_enabled\", enabled).apply()\n \n private fun toggleMode() {\n val current = getMode()\n setMode(if (current == \"Essay\") \"PG\" else \"Essay\")\n showPersistentNotification()\n }\n\n private fun setupClipboardListener() {\n autoSolveEnabled = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getBoolean(\"auto_solve\", false)\n clipboardListener = ClipboardManager.OnPrimaryClipChangedListener {\n if (autoSolveEnabled) {\n try {\n val text = (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).primaryClip?.getItemAt(0)?.text?.toString()?.trim() ?: \"\"\n if (text.isNotEmpty() && text.endsWith(\"?\")) {\n startActivity(Intent(this, ClipboardActivity::class.java).apply {\n addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_SINGLE_TOP)\n })\n }\n } catch (e: Exception) { Log.e(TAG, \"Clipboard error\", e) }\n }\n }\n (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager).addPrimaryClipChangedListener(clipboardListener)\n }\n\n private fun updateAutoSolve(enabled: Boolean) {\n autoSolveEnabled = enabled\n getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit().putBoolean(\"auto_solve\", enabled).apply()\n }\n\n private fun showPersistentNotification() {\n val mode = getMode()\n val autoStatus = if (autoSolveEnabled) \"ON\" else \"OFF\"\n val notification = NotificationCompat.Builder(this, CHANNEL_ID)\n .setContentTitle(\"Winatra AI Shortcut\")\n .setContentText(\"Mode: $mode | Auto: $autoStatus | Copy teks lalu tekan Jawab\")\n .setSmallIcon(android.R.drawable.ic_menu_search)\n .setOngoing(true)\n .addAction(0, \"Ganti Mode\", PendingIntent.getService(this, 1, Intent(this, WinatraService::class.java).apply { action = ACTION_CHANGE_MODE }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))\n .addAction(0, \"Jawab\", PendingIntent.getActivity(this, 0, Intent(this, ClipboardActivity::class.java).apply { addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))\n .addAction(android.R.drawable.ic_menu_send, \"Tanya\", PendingIntent.getActivity(this, 3, Intent(this, ClipboardActivity::class.java).apply { putExtra(\"mode\", \"ask\") }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))\n .build()\n startForeground(NOTIF_ID, notification)\n }\n\n private fun showResultNotification(title: String, body: String, withExplain: Boolean = false, question: String = \"\", answer: String = \"\") {\n val builder = NotificationCompat.Builder(this, RESULT_CHANNEL_ID)\n .setContentTitle(title).setContentText(body).setStyle(NotificationCompat.BigTextStyle().bigText(body))\n .setSmallIcon(android.R.drawable.ic_dialog_info).setAutoCancel(true)\n if (withExplain && question.isNotEmpty() && answer.isNotEmpty()) {\n builder.addAction(0, \"Kenapa?\", PendingIntent.getService(this, 2, Intent(this, WinatraService::class.java).apply {\n action = ACTION_EXPLAIN\n putExtra(\"question\", question)\n putExtra(\"answer\", answer)\n }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))\n }\n (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(RESULT_NOTIF_ID, builder.build())\n }\n\n // ---------- LIMIT & PREMIUM (kuota gratis 7) ----------\n private fun checkAndShowLimit(): Boolean {\n val remaining = getSharedPreferences(PREFS_NAME, MODE_PRIVATE).getInt(\"remaining_quota\", -1)\n if (remaining <= 0 && remaining != -1) {\n showResultNotification(\"Winatra AI\", \"Maaf, kuota harian habis. Hubungi admin untuk langganan.\\n• 5k/hari\\n• 15k/minggu\\n• 30k/bulan\")\n return false\n }\n return true\n }\n\n private suspend fun resetDailyQuotaIfNeeded() {\n val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)\n val today = SimpleDateFormat(\"yyyy-MM-dd\", Locale.US).format(Date())\n if (prefs.getString(PREF_KEY_LAST_QUOTA_RESET, \"\") == today) return\n prefs.edit().putString(PREF_KEY_LAST_QUOTA_RESET, today).putInt(\"remaining_quota\", DEFAULT_DAILY_QUOTA).apply()\n syncRemainingQuotaToFirestore(DEFAULT_DAILY_QUOTA)\n }\n\n private suspend fun syncRemainingQuotaToFirestore(quota: Int) {\n FirebaseAuth.getInstance().currentUser?.let { user ->\n try {\n FirebaseFirestore.getInstance().collection(\"users\").document(user.uid).update(\"remainingQuota\", quota).await()\n } catch (e: Exception) { Log.e(TAG, \"Firestore sync failed\", e) }\n }\n }\n\n private fun decrementRemainingQuota() {\n val prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)\n val current = prefs.getInt(\"remaining_quota\", 0)\n if (current > 0) {\n prefs.edit().putInt(\"remaining_quota\", current - 1).apply()\n scope.launch { syncRemainingQuotaToFirestore(current - 1) }\n }\n }\n\n private suspend fun isUserPremium(): Boolean = withContext(Dispatchers.IO) {\n try {\n val user = FirebaseAuth.getInstance().currentUser ?: return@withContext false\n val doc = FirebaseFirestore.getInstance().collection(\"users\").document(user.uid).get().await()\n if (!doc.exists()) return@withContext false\n val isPremiumFlag = doc.getBoolean(\"isPremium\") ?: false\n if (!isPremiumFlag) return@withContext false\n val expiry = doc.getTimestamp(\"premiumExpiry\")?.toDate()\n expiry == null || expiry.after(Date())\n } catch (e: Exception) { false }\n }\n\n // ---------- DEEPSEEK API (hanya DeepSeek, tanpa Groq) ----------\n private suspend fun callAIWithFallback(question: String, mode: String): String {\n val deepseekEndpoint = API_ENDPOINTS.first { it.type == \"DeepSeek\" }\n Log.i(TAG, \"Calling DeepSeek API (Q: ${question.take(50)})\")\n \n // Coba hingga 3 kali jika gagal\n var attemptCount = 0\n val maxAttempts = 3\n while (attemptCount < maxAttempts) {\n attemptCount++\n val result = performApiRequest(deepseekEndpoint, question, mode)\n if (result != null && !result.startsWith(\"Error:\")) {\n Log.i(TAG, \"✓ DeepSeek success after $attemptCount attempts\")\n return result\n } else {\n Log.w(TAG, \"✗ DeepSeek attempt $attemptCount failed: ${result?.take(30)}\")\n }\n }\n Log.e(TAG, \"✗ DeepSeek failed after $maxAttempts attempts!\")\n return \"Error: All API keys failed.\"\n }\n\n private suspend fun performApiRequest(endpoint: ApiEndpoint, question: String, mode: String): String? {\n val specialization = getSharedPreferences(PREFS_NAME, MODE_PRIVATE)\n .getString(\"user_specialization\", \"general\") ?: \"general\"\n \n val specializationPrompt = SPECIALIZATION_PROMPTS[specialization] \n ?: SPECIALIZATION_PROMPTS[\"general\"]!!\n \n val systemPrompt = if (mode == \"PG\") {\n \"Jawab HANYA dengan satu huruf: A, B, C, atau D. Tidak perlu penjelasan.\"\n } else {\n \"$specializationPrompt\\n\\nBerikan jawaban yang lengkap dan jelas dalam Bahasa Indonesia.\"\n }\n\n if (endpoint.key.startsWith(\"BUILD_\")) {\n Log.e(TAG, \"❌ INVALID KEY DETECTED: ${endpoint.key} (placeholder not replaced!)\")\n return \"Error: placeholder_key\"\n }\n\n val json = JSONObject().apply {\n put(\"model\", \"deepseek-chat\")\n put(\"messages\", org.json.JSONArray().apply {\n put(JSONObject().apply { put(\"role\", \"system\"); put(\"content\", systemPrompt) })\n put(JSONObject().apply { put(\"role\", \"user\"); put(\"content\", question) })\n })\n put(\"max_tokens\", if (mode == \"PG\") 10 else 500)\n put(\"temperature\", 0.3)\n }\n\n val url = \"${endpoint.baseUrl}/chat/completions\"\n val request = Request.Builder()\n .url(url)\n .addHeader(\"Authorization\", \"Bearer ${endpoint.key}\")\n .addHeader(\"Content-Type\", \"application/json\")\n .post(json.toString().toRequestBody(\"application/json\".toMediaType()))\n .build()\n\n return try {\n Log.d(TAG, \"→ Calling DeepSeek at $url (specialization=$specialization)\")\n val response = client.newCall(request).execute()\n val body = response.body?.string() ?: \"\"\n if (response.isSuccessful) {\n var answer = JSONObject(body).getJSONArray(\"choices\")\n .getJSONObject(0).getJSONObject(\"message\").getString(\"content\").trim()\n if (mode == \"PG\") {\n val firstValid = answer.uppercase().firstOrNull { it in 'A'..'D' }\n answer = if (firstValid != null) firstValid.toString() else \"?\"\n }\n Log.d(TAG, \"← Got response from DeepSeek: ${answer.take(50)}...\")\n answer\n } else {\n val errorMsg = when (response.code) {\n 429, 503 -> \"rate_limit\"\n 401, 403 -> \"auth_failed (key invalid?)\"\n else -> response.code.toString()\n }\n Log.w(TAG, \"✗ DeepSeek returned ${response.code}: $errorMsg. Body: ${body.take(100)}\")\n \"Error: $errorMsg\"\n }\n } catch (e: java.net.SocketTimeoutException) {\n Log.w(TAG, \"✗ DeepSeek timeout: ${e.message}\")\n \"Error: timeout\"\n } catch (e: java.io.IOException) {\n Log.w(TAG, \"✗ DeepSeek network error: ${e.message}\")\n \"Error: network\"\n } catch (e: Exception) {\n Log.e(TAG, \"✗ DeepSeek exception: ${e.message}\", e)\n \"Error: ${e.message}\"\n }\n }\n\n // ---------- EXPLANATION ----------\n private suspend fun callExplanationWithFallback(question: String, answer: String): String {\n val deepseekEndpoint = API_ENDPOINTS.first { it.type == \"DeepSeek\" }\n return performExplanationRequest(deepseekEndpoint, question, answer) ?: \"Error: All API keys failed for explanation.\"\n }\n\n private suspend fun performExplanationRequest(endpoint: ApiEndpoint, question: String, answer: String): String? {\n val json = JSONObject().apply {\n put(\"model\", \"deepseek-chat\")\n put(\"messages\", org.json.JSONArray().apply {\n put(JSONObject().apply { put(\"role\", \"system\"); put(\"content\", \"Jelaskan secara singkat mengapa jawaban yang benar adalah $answer. Berikan alasan logis dalam Bahasa Indonesia.\") })\n put(JSONObject().apply { put(\"role\", \"user\"); put(\"content\", \"Pertanyaan: $question\") })\n })\n put(\"max_tokens\", 300)\n put(\"temperature\", 0.5)\n }\n val request = Request.Builder()\n .url(\"${endpoint.baseUrl}/chat/completions\")\n .addHeader(\"Authorization\", \"Bearer ${endpoint.key}\")\n .addHeader(\"Content-Type\", \"application/json\")\n .post(json.toString().toRequestBody(\"application/json\".toMediaType()))\n .build()\n return try {\n val response = client.newCall(request).execute()\n val body = response.body?.string() ?: \"\"\n if (response.isSuccessful) {\n JSONObject(body).getJSONArray(\"choices\")\n .getJSONObject(0).getJSONObject(\"message\").getString(\"content\").trim()\n } else { \"Error: ${response.code}\" }\n } catch (e: Exception) { \"Error: ${e.message}\" }\n }\n\n // ---------- PROSES UTAMA ----------\n private fun processClipboardResult(question: String, modeType: String = \"answer\") {\n if (FirebaseAuth.getInstance().currentUser == null) {\n showResultNotification(\"Winatra AI\", \"Silakan login terlebih dahulu.\")\n return\n }\n if (question.isEmpty()) {\n showResultNotification(\"Winatra AI\", \"Clipboard kosong! Salin pertanyaan dulu.\")\n return\n }\n \n val isOffline = isOfflineMode()\n \n if (isOffline) {\n showOfflineNotification(question, modeType)\n return\n }\n \n // Tampilkan notifikasi \"Berpikir...\"\n showThinkingNotification()\n scope.launch {\n resetDailyQuotaIfNeeded()\n val isPremium = isUserPremium()\n if (!isPremium && !checkAndShowLimit()) return@launch\n \n var answer = \"\"\n var usedBridge = false\n if (FlutterBridge.isReady()) {\n try {\n answer = FlutterBridge.getAnswer(question)\n if (!answer.startsWith(\"Error:\")) {\n usedBridge = true\n }\n } catch (e: Exception) {\n Log.e(TAG, \"FlutterBridge failed: ${e.message}\")\n }\n }\n \n if (!usedBridge) {\n Log.w(TAG, \"Falling back to direct API call (bridge not available or failed)\")\n answer = callAIWithFallback(question, if (modeType == \"discussion\") \"Essay\" else getMode())\n }\n \n withContext(Dispatchers.Main) {\n // Cancel thinking notification dan tampilkan hasil\n (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).cancel(RESULT_NOTIF_ID + 3)\n \n if (answer.startsWith(\"Error:\")) {\n val msg = when {\n answer.contains(\"rate_limit\") -> \"Layanan padat, coba lagi nanti.\"\n answer.contains(\"auth_failed\") -> \"Masalah kunci API, laporkan ke pengembang.\"\n answer.contains(\"timeout\") -> \"Koneksi lambat, coba lagi.\"\n answer.contains(\"network\") -> \"Tidak ada koneksi internet.\"\n else -> \"Layanan sibuk, coba lagi nanti.\"\n }\n showResultNotification(\"Winatra AI\", msg)\n return@withContext\n }\n if (!isPremium) decrementRemainingQuota()\n if (modeType == \"discussion\") {\n showDiscussionNotification(question, answer)\n } else {\n if (getMode() == \"PG\") {\n showResultNotification(\"Jawaban PG\", \"Jawaban: $answer\", true, question, answer)\n } else {\n (getSystemService(Context.CLIPBOARD_SERVICE) as ClipboardManager)\n .setPrimaryClip(android.content.ClipData.newPlainText(\"answer\", answer))\n showResultNotification(\"Jawaban dari Winatra\", answer)\n }\n }\n }\n }\n }\n\n private fun showThinkingNotification() {\n val builder = NotificationCompat.Builder(this, RESULT_CHANNEL_ID)\n .setContentTitle(\"Winatra AI\")\n .setContentText(\"Berpikir...\")\n .setStyle(NotificationCompat.BigTextStyle().bigText(\"Menganalisis pertanyaan Anda...\"))\n .setSmallIcon(android.R.drawable.ic_menu_search)\n .setOngoing(true)\n .setAutoCancel(false)\n (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(RESULT_NOTIF_ID + 3, builder.build())\n }\n\n private fun showOfflineNotification(question: String, modeType: String) {\n val shortQuestion = question.take(60) + if (question.length > 60) \"...\" else \"\"\n val builder = NotificationCompat.Builder(this, RESULT_CHANNEL_ID)\n .setContentTitle(\"🔒 Mode Offline Aktif\")\n .setContentText(\"Tanya: $shortQuestion\")\n .setStyle(NotificationCompat.BigTextStyle().bigText(\"Buka app untuk menjawab dengan AI lokal + dokumen Anda.\\n\\nPertanyaan: $question\"))\n .setSmallIcon(android.R.drawable.ic_dialog_info)\n .setAutoCancel(true)\n .setContentIntent(\n PendingIntent.getActivity(\n this, \n (System.currentTimeMillis() % 10000).toInt(),\n Intent(this, MainActivity::class.java).apply {\n addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)\n putExtra(\"offline_question\", question)\n putExtra(\"open_offline_screen\", true)\n },\n PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE\n )\n )\n (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(RESULT_NOTIF_ID + 2, builder.build())\n }\n\n private fun processAccessibilityText(text: String) {\n if (FirebaseAuth.getInstance().currentUser == null) {\n showResultNotification(\"Winatra AI\", \"Silakan login terlebih dahulu.\")\n return\n }\n if (text.isBlank()) {\n showResultNotification(\"Winatra AI\", \"Tidak ada teks yang dapat dibaca.\")\n return\n }\n showResultNotification(\"Winatra AI\", \"⏳ Memproses teks aksesibilitas...\")\n scope.launch {\n resetDailyQuotaIfNeeded()\n val isPremium = isUserPremium()\n if (!isPremium && !checkAndShowLimit()) return@launch\n val answer = callAIWithFallback(text, getMode())\n withContext(Dispatchers.Main) {\n if (answer.startsWith(\"Error:\")) {\n val msg = when {\n answer.contains(\"rate_limit\") -> \"Layanan padat, coba lagi nanti.\"\n answer.contains(\"auth_failed\") -> \"Masalah kunci API, laporkan ke pengembang.\"\n answer.contains(\"timeout\") -> \"Koneksi lambat, coba lagi.\"\n answer.contains(\"network\") -> \"Tidak ada koneksi internet.\"\n else -> \"Layanan sibuk, coba lagi nanti.\"\n }\n showResultNotification(\"Winatra AI\", msg)\n return@withContext\n }\n if (!isPremium) decrementRemainingQuota()\n showResultNotification(\"Jawaban Asisten Belajar\", answer)\n }\n }\n }\n\n private fun showDiscussionNotification(question: String, answer: String) {\n val builder = NotificationCompat.Builder(this, RESULT_CHANNEL_ID)\n .setContentTitle(\"💬 Diskusi AI\")\n .setContentText(\"Pertanyaan: ${question.take(60)}...\\nKlik untuk lihat jawaban\")\n .setStyle(NotificationCompat.BigTextStyle().bigText(answer))\n .setSmallIcon(android.R.drawable.ic_dialog_info)\n .setAutoCancel(true)\n .addAction(0, \"Salin\", PendingIntent.getService(this, 4, Intent(this, WinatraService::class.java).apply {\n action = ACTION_COPY_ANSWER\n putExtra(\"answer\", answer)\n }, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE))\n (getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager).notify(RESULT_NOTIF_ID + 1, builder.build())\n }\n\n private suspend fun handleExplain(question: String, answer: String) {\n showResultNotification(\"Winatra AI\", \"📖 Menyiapkan penjelasan...\")\n val explanation = callExplanationWithFallback(question, answer)\n withContext(Dispatchers.Main) {\n if (explanation.startsWith(\"Error:\")) {\n showResultNotification(\"Penjelasan\", \"Penjelasan tidak tersedia saat ini.\")\n } else {\n showResultNotification(\"Penjelasan\", explanation)\n }\n }\n }\n}"}}, ".history/lib/services/ai_service_20260706232753.dart": {"f666cd5c72f9e3514945e4c0fef876b155a16ff7": {"t": 1786068436.5832982, "n": 5794, "h": "f110f3e91998c84c", "c": "import 'dart:convert';\nimport 'package:http/http.dart' as http;\nimport 'package:onenm_local_llm/onenm_local_llm.dart';\nimport 'remote_config_service.dart';\n\n/// Unified AI service that works both online (DeepSeek API) and offline (local LLM).\nclass AIService {\n static final AIService _instance = AIService._internal();\n factory AIService() => _instance;\n AIService._internal();\n\n OneNm? _localAI;\n bool _isLocalInitialized = false;\n String _localStatus = 'Not initialized';\n\n /// Get current local AI status message\n String get localStatus => _localStatus;\n\n /// Whether local AI is ready\n bool get isLocalReady => _isLocalInitialized;\n\n /// Initialize local LLM (offline mode)\n Future initLocal({OneNmProgressCallback? onProgress}) async {\n if (_isLocalInitialized) return;\n\n try {\n _localStatus = 'Memeriksa model...';\n onProgress?.call(_localStatus);\n\n // Use Qwen2.5 1.5B — more capable than TinyLlama\n _localAI = OneNm(\n model: OneNmModel.qwen25,\n settings: const GenerationSettings(\n temperature: 0.7,\n maxTokens: 512,\n topK: 40,\n topP: 0.9,\n repeatPenalty: 1.1,\n ),\n onProgress: (status) {\n _localStatus = status;\n onProgress?.call(status);\n },\n );\n\n _localStatus = 'Memuat model...';\n onProgress?.call(_localStatus);\n await _localAI!.initialize();\n\n _localStatus = 'Model siap!';\n _isLocalInitialized = true;\n onProgress?.call(_localStatus);\n } catch (e) {\n _localStatus = 'Gagal memuat model: $e';\n _isLocalInitialized = false;\n rethrow;\n }\n }\n\n /// Send a message to AI (online or offline based on [isOffline])\n Future chat({\n required String message,\n required bool isOffline,\n String? systemPrompt,\n }) async {\n if (isOffline) {\n return _chatLocal(message, systemPrompt: systemPrompt);\n } else {\n return _chatOnline(message, systemPrompt: systemPrompt);\n }\n }\n\n /// Generate answer with context from The Brain (for keyboard/notification services)\n /// This method automatically searches all documents in The Brain for relevant context.\n Future generateAnswer({\n required String query,\n required bool isOffline,\n String context = '',\n }) async {\n final systemPrompt = context.isNotEmpty\n ? '''Kamu adalah Winatra AI, asisten belajar yang membantu dan ramah.\nJAWABLAH DALAM BAHASA INDONESIA.\nGunakan konteks berikut untuk menjawab pertanyaan:\n\n$context\n\nBerikan jawaban yang jelas, informatif, dan mudah dipahami.\nJangan mengulangi perintah atau instruksi dalam jawabanmu.\nJawab langsung pertanyaannya tanpa basa-basi.'''\n : null;\n\n return await chat(\n message: query,\n isOffline: isOffline,\n systemPrompt: systemPrompt,\n );\n }\n\n /// Chat using local LLM\n Future _chatLocal(String message, {String? systemPrompt}) async {\n if (!_isLocalInitialized || _localAI == null) {\n throw Exception('Model lokal belum siap. Silakan tunggu atau gunakan mode online.');\n }\n\n final systemMsg = systemPrompt ?? _buildLocalSystemPrompt();\n final reply = await _localAI!.chat(message, systemPrompt: systemMsg);\n return reply;\n }\n\n /// Build a good system prompt for local model\n String _buildLocalSystemPrompt() {\n return '''Kamu adalah Winatra AI, asisten belajar yang membantu dan ramah.\nJAWABLAH DALAM BAHASA INDONESIA.\nBerikan jawaban yang jelas, informatif, dan mudah dipahami.\nJika ditanya tentang suatu konsep, jelaskan dengan detail minimal 3-4 kalimat.\nJangan mengulangi perintah atau instruksi dalam jawabanmu.\nJawab langsung pertanyaannya tanpa basa-basi.''' ;\n }\n\n /// Chat using DeepSeek API (online)\n Future _chatOnline(String message, {String? systemPrompt}) async {\n try {\n final remoteConfig = RemoteConfigService();\n final apiKey = remoteConfig.getDeepSeekPrimary();\n \n if (apiKey.isEmpty) {\n throw Exception('API key tidak tersedia. Periksa koneksi internet atau hubungi admin.');\n }\n\n final systemMsg = systemPrompt ?? _buildOnlineSystemPrompt();\n\n final response = await http.post(\n Uri.parse('https://api.deepseek.com/v1/chat/completions'),\n headers: {\n 'Authorization': 'Bearer $apiKey',\n 'Content-Type': 'application/json',\n },\n body: jsonEncode({\n 'model': 'deepseek-chat',\n 'messages': [\n {'role': 'system', 'content': systemMsg},\n {'role': 'user', 'content': message},\n ],\n 'temperature': 0.7,\n 'max_tokens': 1024,\n }),\n );\n\n if (response.statusCode == 200) {\n final data = jsonDecode(response.body);\n final content = data['choices']?[0]?['message']?['content'] ?? '';\n return content.toString().trim();\n } else {\n throw Exception('DeepSeek API error (${response.statusCode}): ${response.body}');\n }\n } catch (e) {\n if (e is Exception) rethrow;\n throw Exception('Gagal terhubung ke server: $e');\n }\n }\n\n String _buildOnlineSystemPrompt() {\n return '''Kamu adalah Winatra AI, asisten belajar yang membantu dan ramah.\nJAWABLAH DALAM BAHASA INDONESIA.\nBerikan jawaban yang jelas, informatif, dan mudah dipahami.\nJika ditanya tentang suatu konsep, jelaskan dengan detail.\nJangan mengulangi perintah atau instruksi dalam jawabanmu.\nJawab langsung pertanyaannya.''' ;\n }\n\n /// Clear local conversation history\n void clearLocalHistory() {\n _localAI?.clearHistory();\n }\n\n /// Dispose local AI resources\n Future disposeLocal() async {\n try {\n await _localAI?.dispose();\n } catch (_) {}\n _localAI = null;\n _isLocalInitialized = false;\n _localStatus = 'Disposed';\n }\n}"}}}}