Skip to content

Commit d5f0311

Browse files
committed
Fix Mandi rates, Saved Reports native UI, notifications, and fellow farmers
1 parent 4d179dc commit d5f0311

6 files changed

Lines changed: 83 additions & 90 deletions

File tree

app/src/main/java/com/example/DiseaseScannerScreen.kt

Lines changed: 16 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -53,13 +53,15 @@ data class Store(val name: String, val url: String, val icon: String)
5353
data class CropScanData(
5454
val status: String, val name: String, val confidence: Int, val severity: String,
5555
val symptoms: String, val cause: String, val treatment: String, val prevention: String,
56+
val details: String,
5657
val products: List<Pair<Pair<String, String>, List<Store>>> // Pair(Name to Dose, Stores)
5758
)
5859

5960
data class SoilScanData(
6061
val soilType: String, val estimatedPH: String, val texture: String,
6162
val organicMatter: String, val deficiencies: List<String>,
6263
val suitableCrops: List<String>, val improvements: String,
64+
val details: String,
6365
val fertilizers: List<Pair<Pair<String, String>, List<Store>>>
6466
)
6567

@@ -82,7 +84,8 @@ fun parseCropJson(raw: String): CropScanData? = runCatching {
8284
}
8385
} ?: emptyList()
8486
CropScanData(j.optString("status"), j.optString("name"), j.optInt("confidence"), j.optString("severity"),
85-
j.optString("symptoms"), j.optString("cause"), j.optString("treatment"), j.optString("prevention"), prods)
87+
j.optString("symptoms"), j.optString("cause"), j.optString("treatment"), j.optString("prevention"),
88+
j.optString("details"), prods)
8689
}.getOrNull()
8790

8891
fun parseSoilJson(raw: String): SoilScanData? = runCatching {
@@ -106,7 +109,7 @@ fun parseSoilJson(raw: String): SoilScanData? = runCatching {
106109
}
107110
} ?: emptyList()
108111
SoilScanData(j.optString("soilType"), j.optString("estimatedPH"), j.optString("texture"),
109-
j.optString("organicMatter"), defs, crops, j.optString("improvements"), ferts)
112+
j.optString("organicMatter"), defs, crops, j.optString("improvements"), j.optString("details"), ferts)
110113
}.getOrNull()
111114
@Composable
112115
fun DiseaseScannerScreen(modifier: Modifier = Modifier) {
@@ -473,83 +476,17 @@ fun ScanResultView(modifier: Modifier, raw: String, mode: ScanMode, accent: Colo
473476

474477
fun saveReportToDownloads(context: android.content.Context, content: String) {
475478
try {
476-
// Format the raw JSON into a readable text report
477-
val formattedContent = runCatching {
478-
val d = parseCropJson(content)
479-
if (d != null) {
480-
"""
481-
NuKropAI Scan Report
482-
====================
483-
Status: ${d.status}
484-
Diagnosis: ${d.name}
485-
Confidence: ${d.confidence}%
486-
Severity: ${d.severity}
487-
488-
SYMPTOMS
489-
--------
490-
${d.symptoms}
491-
492-
CAUSE
493-
-----
494-
${d.cause}
495-
496-
TREATMENT PLAN
497-
--------------
498-
${d.treatment}
499-
500-
PREVENTION
501-
----------
502-
${d.prevention}
503-
504-
PRODUCTS RECOMMENDED
505-
--------------------
506-
${d.products.joinToString("\n") { (info, stores) ->
507-
"- ${info.first} (Dose: ${info.second})\n Buy: ${stores.firstOrNull()?.url ?: "Search locally"}"
508-
}}
509-
""".trimIndent()
510-
} else {
511-
val s = parseSoilJson(content)
512-
if (s != null) {
513-
"""
514-
NuKropAI Soil Report
515-
====================
516-
Soil Type: ${s.soilType}
517-
Est. pH: ${s.estimatedPH}
518-
Texture: ${s.texture}
519-
Organic Matter: ${s.organicMatter}
520-
521-
DEFICIENCIES
522-
------------
523-
${s.deficiencies.joinToString(", ")}
524-
525-
IMPROVEMENTS
526-
------------
527-
${s.improvements}
528-
529-
SUITABLE CROPS
530-
--------------
531-
${s.suitableCrops.joinToString(", ")}
532-
533-
FERTILIZERS
534-
-----------
535-
${s.fertilizers.joinToString("\n") { (info, stores) ->
536-
"- ${info.first} (Dose: ${info.second})\n Buy: ${stores.firstOrNull()?.url ?: "Search locally"}"
537-
}}
538-
""".trimIndent()
539-
} else content
540-
}
541-
}.getOrDefault(content)
542-
479+
val cleanRaw = content.replace(Regex("<think>.*?</think>", RegexOption.DOT_MATCHES_ALL), "").trim()
543480
val resolver = context.contentResolver
544481
val contentValues = ContentValues().apply {
545-
put(MediaStore.MediaColumns.DISPLAY_NAME, "NuKropAI_Report_${System.currentTimeMillis()}.txt")
546-
put(MediaStore.MediaColumns.MIME_TYPE, "text/plain")
482+
put(MediaStore.MediaColumns.DISPLAY_NAME, "NuKropAI_Report_${System.currentTimeMillis()}.json")
483+
put(MediaStore.MediaColumns.MIME_TYPE, "application/json")
547484
put(MediaStore.MediaColumns.RELATIVE_PATH, Environment.DIRECTORY_DOWNLOADS)
548485
}
549486
val uri = resolver.insert(MediaStore.Files.getContentUri("external"), contentValues)
550487
if (uri != null) {
551488
resolver.openOutputStream(uri)?.use { os ->
552-
os.write(formattedContent.toByteArray())
489+
os.write(cleanRaw.toByteArray())
553490
}
554491
Toast.makeText(context, "Saved to Downloads!", Toast.LENGTH_SHORT).show()
555492
} else {
@@ -585,9 +522,10 @@ fun CropResultUI(d: CropScanData, accent: Color, context: android.content.Contex
585522
}
586523
}
587524

588-
if (d.symptoms.isNotEmpty()) ResultBlock("👁️ Symptoms Observed", d.symptoms)
589-
if (d.treatment.isNotEmpty()) ResultBlock("💊 Treatment Plan", d.treatment)
525+
if (d.symptoms.isNotEmpty()) ResultBlock("🦠 Symptoms Observed", d.symptoms)
526+
if (d.treatment.isNotEmpty()) ResultBlock("💉 Treatment Plan", d.treatment)
590527
if (d.prevention.isNotEmpty()) ResultBlock("🛡️ Prevention", d.prevention)
528+
if (d.details.isNotEmpty()) ResultBlock("ℹ️ Detailed Insights", d.details)
591529
if (d.products.isNotEmpty()) {
592530
Text("🛒 Buy Recommended Products", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = NuKropText,
593531
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp))
@@ -610,9 +548,10 @@ fun SoilResultUI(d: SoilScanData, accent: Color, context: android.content.Contex
610548
}
611549
}
612550
}
613-
if (d.deficiencies.isNotEmpty()) ResultBlock("⚠️  Likely Deficiencies", d.deficiencies.joinToString(""))
614-
if (d.suitableCrops.isNotEmpty()) ResultBlock("🌾 Best Crops to Grow", d.suitableCrops.joinToString(", "))
615-
if (d.improvements.isNotEmpty()) ResultBlock("💡 Improvement Tips", d.improvements)
551+
if (d.deficiencies.isNotEmpty()) ResultBlock("📉 Likely Deficiencies", d.deficiencies.joinToString(""))
552+
if (d.suitableCrops.isNotEmpty()) ResultBlock("🌱 Best Crops to Grow", d.suitableCrops.joinToString(", "))
553+
if (d.improvements.isNotEmpty()) ResultBlock("🛠 Improvement Tips", d.improvements)
554+
if (d.details.isNotEmpty()) ResultBlock("ℹ️ Detailed Insights", d.details)
616555
if (d.fertilizers.isNotEmpty()) {
617556
Text("🛒 Recommended Fertilizers", fontSize = 15.sp, fontWeight = FontWeight.Bold, color = NuKropText,
618557
modifier = Modifier.padding(horizontal = 16.dp, vertical = 10.dp))

app/src/main/java/com/example/GeminiVisionService.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -200,6 +200,7 @@ Respond ONLY in this exact JSON format (no markdown, no extra text):
200200
"symptoms": "Very brief symptom description",
201201
"cause": "Exact causative organism",
202202
"treatment": "Precise chemical or organic treatment plan",
203+
"details": "Detailed biological information, lifecycle, or advanced agronomic insights about this condition (3-4 sentences).",
203204
"products": [
204205
{
205206
"name": "REAL brand name pesticide",
@@ -225,6 +226,7 @@ Respond ONLY in this exact JSON format (no markdown, no extra text):
225226
"organicMatter": "Medium",
226227
"deficiencies": ["Nitrogen"],
227228
"improvements": "Add organic compost",
229+
"details": "Detailed analysis of soil profile, structure, and advanced agronomical insights (3-4 sentences).",
228230
"suitableCrops": ["Wheat", "Soybean"],
229231
"fertilizers": [
230232
{

app/src/main/java/com/example/HomeScreen.kt

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,9 @@ fun HomeScreen(
106106
Text(now, fontSize = 10.sp, color = Color.White.copy(alpha = 0.7f), maxLines = 1, overflow = TextOverflow.Ellipsis)
107107
}
108108
}
109+
IconButton(onClick = { /* TODO Notifications */ }, modifier = Modifier.size(44.dp).clip(CircleShape).background(Color(0x33FFFFFF))) {
110+
Icon(Icons.Filled.Notifications, contentDescription = "Notifications", tint = Color.White, modifier = Modifier.size(22.dp))
111+
}
109112
}
110113

111114
// Weather Pill inside Hero
@@ -241,7 +244,7 @@ fun HomeScreen(
241244
Text("Growing Wheat • 2.5 km away", color = NuKropTextMuted, fontSize = 11.sp)
242245
}
243246
}
244-
Button(onClick = {}, colors = ButtonDefaults.buttonColors(containerColor = NuKropAccent.copy(alpha=0.15f)), contentPadding = PaddingValues(0.dp), modifier = Modifier.size(70.dp, 30.dp), shape = RoundedCornerShape(8.dp)) {
247+
Button(onClick = onNavigateToChat, colors = ButtonDefaults.buttonColors(containerColor = NuKropAccent.copy(alpha=0.15f)), contentPadding = PaddingValues(0.dp), modifier = Modifier.size(70.dp, 30.dp), shape = RoundedCornerShape(8.dp)) {
245248
Text("Connect", color = NuKropAccent, fontSize = 11.sp, fontWeight = FontWeight.Bold)
246249
}
247250
}

app/src/main/java/com/example/MandiApiService.kt

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -140,13 +140,15 @@ object MandiApiService {
140140
commodity: String
141141
): Result<List<MandiRecord>> = withContext(Dispatchers.IO) {
142142

143-
// Try Direct Government API
144-
val govResult = fetchDirectFromGovApi(state, commodity)
145-
if (govResult.isSuccess && govResult.getOrDefault(emptyList()).isNotEmpty()) {
143+
// Try Direct Government API with 3-second timeout
144+
val govResult = kotlinx.coroutines.withTimeoutOrNull(3000) {
145+
fetchDirectFromGovApi(state, commodity)
146+
}
147+
if (govResult != null && govResult.isSuccess && govResult.getOrDefault(emptyList()).isNotEmpty()) {
146148
return@withContext govResult
147149
}
148150

149-
// If Gov API fails or is empty, use AI to fetch realistic market rates
151+
// If Gov API fails, times out, or is empty, use AI to fetch realistic market rates
150152
try {
151153
val prompt = """
152154
You are a real-time agriculture data provider. The user wants current market rates for $commodity in $state, India.
@@ -158,7 +160,11 @@ object MandiApiService {
158160
val aiResponse = GeminiVisionService.chatQuery(prompt)
159161
if (aiResponse.isSuccess) {
160162
val jsonStr = aiResponse.getOrNull() ?: ""
161-
val cleanJson = jsonStr.replace("```json", "").replace("```", "").trim()
163+
val cleanRaw = jsonStr.replace(Regex("<think>.*?</think>", RegexOption.DOT_MATCHES_ALL), "").trim()
164+
val start = cleanRaw.indexOf('[')
165+
val end = cleanRaw.lastIndexOf(']')
166+
if (start == -1 || end == -1) throw Exception("No JSON array found in AI response")
167+
val cleanJson = cleanRaw.substring(start, end + 1)
162168
val jsonArray = org.json.JSONArray(cleanJson)
163169
val aiRecords = mutableListOf<MandiRecord>()
164170
for (i in 0 until jsonArray.length()) {
@@ -178,14 +184,14 @@ object MandiApiService {
178184
)
179185
}
180186
if (aiRecords.isNotEmpty()) {
181-
return@withContext Result.success(aiRecords)
187+
return@withContext Result.success(aiRecords.toList())
182188
}
183189
}
184190
} catch (e: Exception) {
185191
e.printStackTrace()
186192
}
187193

188-
return@withContext govResult // return original error/empty if AI fails
194+
return@withContext govResult ?: Result.failure(Exception("Gov API Timeout and AI fallback failed"))
189195
}
190196

191197
private suspend fun fetchDirectFromGovApi(

app/src/main/java/com/example/SavedReportsScreen.kt

Lines changed: 48 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ import androidx.compose.foundation.clickable
1111
import androidx.compose.foundation.layout.*
1212
import androidx.compose.foundation.lazy.LazyColumn
1313
import androidx.compose.foundation.lazy.items
14+
import androidx.compose.foundation.rememberScrollState
15+
import androidx.compose.foundation.verticalScroll
1416
import androidx.compose.foundation.shape.RoundedCornerShape
1517
import androidx.compose.material.icons.Icons
1618
import androidx.compose.material.icons.automirrored.filled.ArrowBack
@@ -85,6 +87,47 @@ fun SavedReportsScreen(onNavigateBack: () -> Unit) {
8587
}
8688
}
8789

90+
var selectedReportContent by remember { mutableStateOf<String?>(null) }
91+
var selectedReportName by remember { mutableStateOf<String>("") }
92+
93+
if (selectedReportContent != null) {
94+
Column(
95+
modifier = Modifier
96+
.fillMaxSize()
97+
.background(Color(0xFF0D1208))
98+
.padding(bottom = 100.dp) // padding for nav bar
99+
) {
100+
Row(
101+
modifier = Modifier
102+
.fillMaxWidth()
103+
.statusBarsPadding()
104+
.padding(16.dp),
105+
verticalAlignment = Alignment.CenterVertically
106+
) {
107+
IconButton(onClick = { selectedReportContent = null }) {
108+
Icon(Icons.AutoMirrored.Filled.ArrowBack, contentDescription = "Back", tint = NuKropText)
109+
}
110+
Spacer(Modifier.width(8.dp))
111+
Text(selectedReportName, fontSize = 20.sp, fontWeight = FontWeight.Bold, color = NuKropText)
112+
}
113+
rememberScrollState().let { scrollState ->
114+
Column(Modifier.fillMaxSize().verticalScroll(scrollState)) {
115+
val crop = parseCropJson(selectedReportContent!!)
116+
val soil = parseSoilJson(selectedReportContent!!)
117+
if (crop != null && (crop.status.isNotEmpty() || crop.name.isNotEmpty())) {
118+
CropResultUI(crop, NuKropBadgeGreen, context) // Using a fallback accent color
119+
} else if (soil != null && soil.soilType.isNotEmpty()) {
120+
SoilResultUI(soil, NuKropBadgeGreen, context)
121+
} else {
122+
// Fallback text view if it's the old .txt report format
123+
RawResultFallback(selectedReportContent!!)
124+
}
125+
}
126+
}
127+
}
128+
return
129+
}
130+
88131
Column(
89132
modifier = Modifier
90133
.fillMaxSize()
@@ -128,13 +171,13 @@ fun SavedReportsScreen(onNavigateBack: () -> Unit) {
128171
items(reports) { report ->
129172
ReportCard(report) {
130173
try {
131-
val intent = Intent(Intent.ACTION_VIEW).apply {
132-
setDataAndType(report.uri, "text/plain")
133-
addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION)
174+
context.contentResolver.openInputStream(report.uri)?.use { stream ->
175+
val text = stream.bufferedReader().use { it.readText() }
176+
selectedReportContent = text
177+
selectedReportName = report.name
134178
}
135-
context.startActivity(Intent.createChooser(intent, "Open Report"))
136179
} catch (e: Exception) {
137-
// Handle if no viewer available
180+
// ignore or toast
138181
}
139182
}
140183
}

web/public/NuKropAI_v1.0.apk

5.44 KB
Binary file not shown.

0 commit comments

Comments
 (0)