Gankit12 commited on
Commit
fb6ccf7
·
1 Parent(s): 0cdf1a1
backend/app/services/voice_assistant_service.py CHANGED
@@ -99,8 +99,8 @@ def _build_weather_context(forecast_data: Dict[str, Any], time_ref: Optional[str
99
  return "\n".join(lines)
100
 
101
 
102
- def _build_apmc_context(prices_data: Dict[str, Any]) -> str:
103
- """Build APMC price context string from API response."""
104
  records = prices_data.get("records", [])
105
  if not records:
106
  return "No price data available."
@@ -127,7 +127,7 @@ def _detect_intent(message: str) -> str:
127
  "tomorrow", "today", "next week", "this week",
128
  "मौसम", "बारिश", "तापमान", "कल", "आज",
129
  ]
130
- apmc_keywords = [
131
  "price", "mandi", "apmc", "market", "cost", "rate", "sell",
132
  "भाव", "मंडी", "दाम", "कीमत", "बेच",
133
  ]
@@ -138,14 +138,60 @@ def _detect_intent(message: str) -> str:
138
 
139
  if any(kw in lower for kw in weather_keywords):
140
  return "weather"
141
- if any(kw in lower for kw in apmc_keywords):
142
- return "apmc"
143
  if any(kw in lower for kw in disease_keywords):
144
  return "disease"
145
 
146
  return "general"
147
 
148
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
149
  def _detect_time_ref(message: str) -> Optional[str]:
150
  """Detect time reference in message."""
151
  lower = message.lower()
@@ -282,6 +328,7 @@ async def process_voice_query(
282
  loc = location if location and location.get("taluka") else DEFAULT_LOCATION
283
  intent = _detect_intent(message)
284
  time_ref = _detect_time_ref(message)
 
285
  context_parts = [f"User message: {message}"]
286
  navigate_to = None
287
  fetched_data = None
@@ -305,20 +352,29 @@ async def process_voice_query(
305
  )
306
  navigate_to = "/weather"
307
 
308
- elif intent == "apmc":
309
- # Try to extract commodity from the message
310
- prices = await _fetch_apmc_prices()
311
  if prices:
312
- apmc_ctx = _build_apmc_context(prices)
313
- context_parts.append(f"\n{apmc_ctx}")
 
 
314
  navigate_to = "/apmc"
315
- fetched_data = {"type": "apmc"}
 
 
 
316
  else:
317
  context_parts.append(
318
- "\nAPMC price data is currently unavailable. "
319
  "Suggest the user check the APMC prices page."
320
  )
321
  navigate_to = "/apmc"
 
 
 
 
322
 
323
  elif intent == "disease":
324
  context_parts.append(
@@ -357,7 +413,7 @@ async def process_voice_query(
357
  "en": f"I could not fetch the weather details right now. Please check the weather page for {loc.get('taluka', 'your area')}.",
358
  "hi": f"अभी मौसम की जानकारी नहीं मिल पाई। कृपया {loc.get('taluka', 'अपने क्षेत्र')} का मौसम पेज देखें।",
359
  },
360
- "apmc": {
361
  "en": "I could not fetch the APMC prices right now. Please check the APMC prices page.",
362
  "hi": "अभी APMC भाव नहीं मिल पाए। कृपया APMC भाव पेज देखें।",
363
  },
 
99
  return "\n".join(lines)
100
 
101
 
102
+ def _build_mandi_context(prices_data: Dict[str, Any]) -> str:
103
+ """Build mandi/APMC price context string from API response."""
104
  records = prices_data.get("records", [])
105
  if not records:
106
  return "No price data available."
 
127
  "tomorrow", "today", "next week", "this week",
128
  "मौसम", "बारिश", "तापमान", "कल", "आज",
129
  ]
130
+ mandi_keywords = [
131
  "price", "mandi", "apmc", "market", "cost", "rate", "sell",
132
  "भाव", "मंडी", "दाम", "कीमत", "बेच",
133
  ]
 
138
 
139
  if any(kw in lower for kw in weather_keywords):
140
  return "weather"
141
+ if any(kw in lower for kw in mandi_keywords):
142
+ return "mandi"
143
  if any(kw in lower for kw in disease_keywords):
144
  return "disease"
145
 
146
  return "general"
147
 
148
 
149
+ # Commodity names the system can extract from voice queries
150
+ _COMMODITY_NAMES = [
151
+ "wheat", "paddy", "rice", "cotton", "sugarcane", "tomato", "potato",
152
+ "onion", "chilli", "maize", "corn", "pulses", "oilseeds", "millets",
153
+ "groundnut", "cumin", "soybean", "mustard", "bajra", "jowar", "ragi",
154
+ "barley", "gram", "tur", "moong", "urad", "masoor", "arhar",
155
+ ]
156
+
157
+ _COMMODITY_HI_MAP = {
158
+ "गेहूं": "Wheat",
159
+ "धान": "Paddy",
160
+ "चावल": "Paddy",
161
+ "कपास": "Cotton",
162
+ "गन्ना": "Sugarcane",
163
+ "टमाटर": "Tomato",
164
+ "आलू": "Potato",
165
+ "प्याज": "Onion",
166
+ "मिर्च": "Chilli",
167
+ "मक्का": "Maize",
168
+ "दालें": "Pulses",
169
+ "दाल": "Pulses",
170
+ "तिलहन": "Oilseeds",
171
+ "बाजरा": "Millets",
172
+ "ज्वार": "Millets",
173
+ "मूंगफली": "Groundnut",
174
+ "जीरा": "Cumin",
175
+ "सोयाबीन": "Soybean",
176
+ "सरसों": "Mustard",
177
+ }
178
+
179
+
180
+ def _extract_commodity(message: str) -> Optional[str]:
181
+ """Extract a commodity/crop name from the user message."""
182
+ lower = message.lower()
183
+
184
+ for name in _COMMODITY_NAMES:
185
+ if name in lower:
186
+ return name.capitalize()
187
+
188
+ for hindi, english in _COMMODITY_HI_MAP.items():
189
+ if hindi in message:
190
+ return english
191
+
192
+ return None
193
+
194
+
195
  def _detect_time_ref(message: str) -> Optional[str]:
196
  """Detect time reference in message."""
197
  lower = message.lower()
 
328
  loc = location if location and location.get("taluka") else DEFAULT_LOCATION
329
  intent = _detect_intent(message)
330
  time_ref = _detect_time_ref(message)
331
+ commodity = _extract_commodity(message)
332
  context_parts = [f"User message: {message}"]
333
  navigate_to = None
334
  fetched_data = None
 
352
  )
353
  navigate_to = "/weather"
354
 
355
+ elif intent == "mandi":
356
+ # Fetch prices, optionally filtered by commodity
357
+ prices = await _fetch_mandi_prices(commodity=commodity)
358
  if prices:
359
+ mandi_ctx = _build_mandi_context(prices)
360
+ context_parts.append(f"\n{mandi_ctx}")
361
+ if commodity:
362
+ context_parts.append(f"\nUser is specifically asking about: {commodity}")
363
  navigate_to = "/apmc"
364
+ fetched_data = {
365
+ "type": "mandi",
366
+ "commodity": commodity,
367
+ }
368
  else:
369
  context_parts.append(
370
+ "\nMandi price data is currently unavailable. "
371
  "Suggest the user check the APMC prices page."
372
  )
373
  navigate_to = "/apmc"
374
+ fetched_data = {
375
+ "type": "mandi",
376
+ "commodity": commodity,
377
+ }
378
 
379
  elif intent == "disease":
380
  context_parts.append(
 
413
  "en": f"I could not fetch the weather details right now. Please check the weather page for {loc.get('taluka', 'your area')}.",
414
  "hi": f"अभी मौसम की जानकारी नहीं मिल पाई। कृपया {loc.get('taluka', 'अपने क्षेत्र')} का मौसम पेज देखें।",
415
  },
416
+ "mandi": {
417
  "en": "I could not fetch the APMC prices right now. Please check the APMC prices page.",
418
  "hi": "अभी APMC भाव नहीं मिल पाए। कृपया APMC भाव पेज देखें।",
419
  },
frontend/src/components/voice/VoiceButton.jsx CHANGED
@@ -72,6 +72,32 @@ const DATA_INTENTS = new Set([
72
  INTENTS.BEST_APMC,
73
  ]);
74
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
  function VoiceButton({ className = "" }) {
76
  const navigate = useNavigate();
77
  const { language } = useApp();
@@ -150,16 +176,19 @@ function VoiceButton({ className = "" }) {
150
  speak(aiResult.response, language, settings).catch(() => {});
151
  }
152
 
153
- // Navigate after speaking if a route is suggested
154
- if (aiResult.navigate_to) {
155
- navigate(aiResult.navigate_to);
 
 
156
  }
157
  })
158
  .catch(() => {
159
  // Fallback: navigate and speak the basic response
160
  setCommandResult(result);
161
  if (result.route) {
162
- navigate(result.route);
 
163
  }
164
  const settings = getVoiceSettings();
165
  if (settings.autoSpeak) {
 
72
  INTENTS.BEST_APMC,
73
  ]);
74
 
75
+ /**
76
+ * Build route state to pass entity data to target pages.
77
+ * This enables auto-filling search fields and triggering data fetches.
78
+ */
79
+ function _buildRouteState(parsedResult, aiResult, userLocation) {
80
+ const state = { fromVoice: true };
81
+ const entities = parsedResult?.entities || {};
82
+
83
+ // Pass commodity for APMC page
84
+ if (entities.crops && entities.crops.length > 0) {
85
+ state.commodity = entities.crops[0];
86
+ }
87
+
88
+ // Pass location for weather page
89
+ if (userLocation && userLocation.taluka) {
90
+ state.location = userLocation;
91
+ }
92
+
93
+ // Also extract commodity from backend response if available
94
+ if (aiResult?.data?.commodity) {
95
+ state.commodity = aiResult.data.commodity;
96
+ }
97
+
98
+ return state;
99
+ }
100
+
101
  function VoiceButton({ className = "" }) {
102
  const navigate = useNavigate();
103
  const { language } = useApp();
 
176
  speak(aiResult.response, language, settings).catch(() => {});
177
  }
178
 
179
+ // Navigate with entity data as route state
180
+ const targetRoute = aiResult.navigate_to || result.route;
181
+ if (targetRoute) {
182
+ const routeState = _buildRouteState(result, aiResult, location);
183
+ navigate(targetRoute, { state: routeState });
184
  }
185
  })
186
  .catch(() => {
187
  // Fallback: navigate and speak the basic response
188
  setCommandResult(result);
189
  if (result.route) {
190
+ const routeState = _buildRouteState(result, null, location);
191
+ navigate(result.route, { state: routeState });
192
  }
193
  const settings = getVoiceSettings();
194
  if (settings.autoSpeak) {
frontend/src/components/voice/VoiceChat.jsx CHANGED
@@ -368,13 +368,32 @@ function VoiceChat({ isOpen, onClose, language }) {
368
  }
369
 
370
  if (aiResult.navigate_to) {
371
- navigate(aiResult.navigate_to);
 
 
 
 
 
 
 
 
 
 
 
372
  }
373
  })
374
  .catch(() => {
375
  // Fallback: use local response if backend fails
376
  if (localResult.action === ACTIONS.NAVIGATE && localResult.route) {
377
- navigate(localResult.route);
 
 
 
 
 
 
 
 
378
  }
379
  if (voiceSettings.autoSpeak && localResult.response) {
380
  speak(localResult.response, lang, voiceSettings).catch(() => {});
 
368
  }
369
 
370
  if (aiResult.navigate_to) {
371
+ const routeState = { fromVoice: true };
372
+ const entities = localResult.entities || {};
373
+ if (entities.crops && entities.crops.length > 0) {
374
+ routeState.commodity = entities.crops[0];
375
+ }
376
+ if (aiResult.data?.commodity) {
377
+ routeState.commodity = aiResult.data.commodity;
378
+ }
379
+ if (location && location.taluka) {
380
+ routeState.location = location;
381
+ }
382
+ navigate(aiResult.navigate_to, { state: routeState });
383
  }
384
  })
385
  .catch(() => {
386
  // Fallback: use local response if backend fails
387
  if (localResult.action === ACTIONS.NAVIGATE && localResult.route) {
388
+ const routeState = { fromVoice: true };
389
+ const entities = localResult.entities || {};
390
+ if (entities.crops && entities.crops.length > 0) {
391
+ routeState.commodity = entities.crops[0];
392
+ }
393
+ if (location && location.taluka) {
394
+ routeState.location = location;
395
+ }
396
+ navigate(localResult.route, { state: routeState });
397
  }
398
  if (voiceSettings.autoSpeak && localResult.response) {
399
  speak(localResult.response, lang, voiceSettings).catch(() => {});
frontend/src/pages/APMCPricePage.jsx CHANGED
@@ -18,6 +18,7 @@
18
  */
19
 
20
  import { useState, useCallback, useRef, useEffect, useMemo } from "react";
 
21
  import PropTypes from "prop-types";
22
  import {
23
  BuildingStorefrontIcon,
@@ -59,6 +60,7 @@ const INITIAL_FILTERS = {
59
  function APMCPricePage() {
60
  const { language } = useApp();
61
  const { location } = useLocation();
 
62
 
63
  // Data state
64
  const [commodities, setCommodities] = useState([]);
@@ -74,6 +76,7 @@ function APMCPricePage() {
74
  const [error, setError] = useState(null);
75
 
76
  const requestIdRef = useRef(0);
 
77
 
78
  // Fetch commodities on mount
79
  useEffect(() => {
@@ -213,6 +216,17 @@ function APMCPricePage() {
213
  [fetchPriceData],
214
  );
215
 
 
 
 
 
 
 
 
 
 
 
 
216
  const handleRefresh = useCallback(() => {
217
  if (selectedCommodity) {
218
  fetchPriceData(selectedCommodity, true);
 
18
  */
19
 
20
  import { useState, useCallback, useRef, useEffect, useMemo } from "react";
21
+ import { useLocation as useRouterLocation } from "react-router-dom";
22
  import PropTypes from "prop-types";
23
  import {
24
  BuildingStorefrontIcon,
 
60
  function APMCPricePage() {
61
  const { language } = useApp();
62
  const { location } = useLocation();
63
+ const routerLocation = useRouterLocation();
64
 
65
  // Data state
66
  const [commodities, setCommodities] = useState([]);
 
76
  const [error, setError] = useState(null);
77
 
78
  const requestIdRef = useRef(0);
79
+ const voiceAutoSearchDone = useRef(false);
80
 
81
  // Fetch commodities on mount
82
  useEffect(() => {
 
216
  [fetchPriceData],
217
  );
218
 
219
+ // Auto-search when navigated from voice assistant with a commodity
220
+ useEffect(() => {
221
+ const voiceCommodity = routerLocation.state?.commodity;
222
+ if (voiceCommodity && !voiceAutoSearchDone.current) {
223
+ voiceAutoSearchDone.current = true;
224
+ setSelectedCommodity(voiceCommodity);
225
+ setFilters(INITIAL_FILTERS);
226
+ fetchPriceData(voiceCommodity);
227
+ }
228
+ }, [routerLocation.state, fetchPriceData]);
229
+
230
  const handleRefresh = useCallback(() => {
231
  if (selectedCommodity) {
232
  fetchPriceData(selectedCommodity, true);
frontend/src/pages/WeatherPage.jsx CHANGED
@@ -10,6 +10,7 @@
10
  */
11
 
12
  import { useState, useCallback, useEffect, useRef } from "react";
 
13
  import { motion, AnimatePresence } from "framer-motion";
14
  import PropTypes from "prop-types";
15
  import {
@@ -31,6 +32,7 @@ import {
31
  Select,
32
  } from "@components/common";
33
  import useApp from "@hooks/useApp";
 
34
  import api, { API_V1 } from "@services/api";
35
  import { getForecast, getAlerts } from "@services/weatherApi";
36
  import { SUPPORTED_CROPS, WEATHER_CACHE_DURATION_MS } from "@utils/constants";
@@ -136,8 +138,17 @@ FarmingAdviceSection.propTypes = {
136
  // WeatherPage
137
  // ---------------------------------------------------------------------------
138
 
 
 
 
 
 
 
 
139
  function WeatherPage() {
140
  const { language } = useApp();
 
 
141
 
142
  // Active location (the one for which data is displayed)
143
  const [activeLocation, setActiveLocation] = useState(null);
@@ -156,6 +167,7 @@ function WeatherPage() {
156
  const [lastRefresh, setLastRefresh] = useState(null);
157
 
158
  const requestIdRef = useRef(0);
 
159
 
160
  // -------------------------------------------------------------------
161
  // Fetch weather data (cache-first)
@@ -233,6 +245,34 @@ function WeatherPage() {
233
  [fetchWeatherData],
234
  );
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  const handleRefresh = useCallback(() => {
237
  if (activeLocation) {
238
  fetchWeatherData(activeLocation, { forceRefresh: true });
 
10
  */
11
 
12
  import { useState, useCallback, useEffect, useRef } from "react";
13
+ import { useLocation as useRouterLocation } from "react-router-dom";
14
  import { motion, AnimatePresence } from "framer-motion";
15
  import PropTypes from "prop-types";
16
  import {
 
32
  Select,
33
  } from "@components/common";
34
  import useApp from "@hooks/useApp";
35
+ import useLocation from "@hooks/useLocation";
36
  import api, { API_V1 } from "@services/api";
37
  import { getForecast, getAlerts } from "@services/weatherApi";
38
  import { SUPPORTED_CROPS, WEATHER_CACHE_DURATION_MS } from "@utils/constants";
 
138
  // WeatherPage
139
  // ---------------------------------------------------------------------------
140
 
141
+ /** Default location used when no user location is set. */
142
+ const DEFAULT_WEATHER_LOCATION = {
143
+ state: "Gujarat",
144
+ district: "Rajkot",
145
+ taluka: "Jetpur",
146
+ };
147
+
148
  function WeatherPage() {
149
  const { language } = useApp();
150
+ const locationCtx = useLocation();
151
+ const routerLocation = useRouterLocation();
152
 
153
  // Active location (the one for which data is displayed)
154
  const [activeLocation, setActiveLocation] = useState(null);
 
167
  const [lastRefresh, setLastRefresh] = useState(null);
168
 
169
  const requestIdRef = useRef(0);
170
+ const voiceAutoFetchDone = useRef(false);
171
 
172
  // -------------------------------------------------------------------
173
  // Fetch weather data (cache-first)
 
245
  [fetchWeatherData],
246
  );
247
 
248
+ // Auto-fetch weather when navigated from voice assistant or if user has saved location
249
+ useEffect(() => {
250
+ if (voiceAutoFetchDone.current || activeLocation) return;
251
+ voiceAutoFetchDone.current = true;
252
+
253
+ // Priority 1: Route state from voice navigation
254
+ const voiceLocation = routerLocation.state?.location;
255
+ if (voiceLocation && voiceLocation.taluka) {
256
+ fetchWeatherData(voiceLocation);
257
+ return;
258
+ }
259
+
260
+ // Priority 2: User's saved location from LocationContext
261
+ if (locationCtx.hasLocation) {
262
+ fetchWeatherData({
263
+ state: locationCtx.state,
264
+ district: locationCtx.district,
265
+ taluka: locationCtx.taluka,
266
+ });
267
+ return;
268
+ }
269
+
270
+ // Priority 3: If navigated via voice (fromVoice flag), use default
271
+ if (routerLocation.state?.fromVoice) {
272
+ fetchWeatherData(DEFAULT_WEATHER_LOCATION);
273
+ }
274
+ }, [routerLocation.state, locationCtx, activeLocation, fetchWeatherData]);
275
+
276
  const handleRefresh = useCallback(() => {
277
  if (activeLocation) {
278
  fetchWeatherData(activeLocation, { forceRefresh: true });
frontend/src/services/api.js CHANGED
@@ -37,7 +37,7 @@ export const API_V1 = "/api/v1";
37
  /** @param {string} path - Path starting with /api/v1/... */
38
  export function apiUrl(path) {
39
  if (path.startsWith("http")) return path;
40
- if (isDev) return path;
41
  return `${API_BASE_URL}${path}`;
42
  }
43
 
 
37
  /** @param {string} path - Path starting with /api/v1/... */
38
  export function apiUrl(path) {
39
  if (path.startsWith("http")) return path;
40
+ if (isDev || !API_BASE_URL) return path;
41
  return `${API_BASE_URL}${path}`;
42
  }
43
 
frontend/src/utils/constants.js CHANGED
@@ -9,8 +9,9 @@
9
  // API
10
  // ---------------------------------------------------------------------------
11
 
 
12
  export const API_BASE_URL =
13
- import.meta.env.VITE_API_BASE_URL || "http://127.0.0.1:8000";
14
 
15
  export const API_PREFIX = "/api";
16
 
 
9
  // API
10
  // ---------------------------------------------------------------------------
11
 
12
+ // Use nullish coalescing to allow empty string as valid value (same-origin API)
13
  export const API_BASE_URL =
14
+ import.meta.env.VITE_API_BASE_URL ?? "http://127.0.0.1:8000";
15
 
16
  export const API_PREFIX = "/api";
17