File size: 12,627 Bytes
41366c7
 
3399231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
c04d3f1
 
 
 
 
 
 
 
3399231
 
41366c7
 
5c1cd6f
41366c7
5c1cd6f
 
 
 
 
 
 
 
 
 
3399231
 
 
 
 
5c1cd6f
41366c7
5c1cd6f
 
 
3399231
41366c7
 
5c1cd6f
 
 
 
 
 
 
 
 
 
 
 
3399231
 
 
 
 
41366c7
 
5c1cd6f
 
 
3399231
41366c7
 
5c1cd6f
 
 
 
 
3399231
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c1cd6f
 
3399231
 
41366c7
 
5c1cd6f
 
 
3399231
41366c7
 
5c1cd6f
 
 
 
 
 
 
 
 
 
3399231
 
 
 
 
 
5c1cd6f
 
 
 
 
 
 
 
 
 
 
 
 
3399231
 
 
 
 
 
5c1cd6f
 
 
3399231
 
 
5c1cd6f
 
 
d1c696f
 
5c1cd6f
 
 
 
 
 
 
d1c696f
 
 
 
 
 
 
3399231
 
5c1cd6f
 
 
 
 
3399231
41366c7
 
442508a
 
 
 
 
5c1cd6f
 
 
 
 
 
 
 
41366c7
5c1cd6f
41366c7
3399231
 
 
 
41366c7
 
5c1cd6f
 
 
3399231
41366c7
 
5c1cd6f
41366c7
5c1cd6f
 
 
671cbc8
 
5c1cd6f
 
 
671cbc8
 
5c1cd6f
 
 
 
671cbc8
 
5c1cd6f
 
41366c7
 
 
3399231
 
 
 
 
 
 
 
 
5c1cd6f
 
 
3399231
 
5c1cd6f
 
 
 
41366c7
3399231
41366c7
 
721846b
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c1cd6f
41366c7
3399231
 
 
 
 
41366c7
 
 
 
3399231
41366c7
 
 
 
 
 
 
 
 
 
3399231
 
 
41366c7
 
 
 
3399231
 
 
 
41366c7
 
 
 
 
5c1cd6f
 
 
41366c7
 
5c1cd6f
3399231
 
 
 
41366c7
 
5c1cd6f
41366c7
 
 
d1c696f
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5c1cd6f
 
 
c04d3f1
41366c7
 
 
 
5c1cd6f
 
 
c04d3f1
41366c7
 
 
 
5c1cd6f
 
 
c04d3f1
41366c7
 
 
 
5c1cd6f
 
 
c04d3f1
41366c7
 
 
 
5c1cd6f
 
 
c04d3f1
41366c7
 
 
 
 
 
 
c04d3f1
41366c7
 
 
5c1cd6f
721846b
 
 
c04d3f1
721846b
 
 
 
5c1cd6f
 
 
c04d3f1
5c1cd6f
 
 
 
 
 
 
c04d3f1
5c1cd6f
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
import { useQuery } from '@tanstack/react-query'

// Local fixtures β€” no backend wiring. Every React Query hook in this file
// resolves against these fixtures via a small simulated network delay so the
// UI renders with populated content.
import {
  mandisResponse,
  marketPricesResponse,
  priceForecastsResponse,
  sellRecommendationsResponse,
  priceConflictsResponse,
  modelInfoResponse,
  deliveryLogsResponse,
  pipelineRunsResponse,
  pipelineStatsResponse,
} from './mockData'

const BASE_URL = import.meta.env.VITE_API_URL ?? ''

async function fetchJson<T>(path: string): Promise<T> {
  const res = await fetch(`${BASE_URL}${path}`)
  if (!res.ok) throw new Error(`API error: ${res.status} ${res.statusText}`)
  return res.json() as Promise<T>
}

function mock<T>(value: T, delayMs = 260): Promise<T> {
  return new Promise((resolve) => setTimeout(() => resolve(value), delayMs))
}

// ── Types ────────────────────────────────────────────────────────────────────

export interface Mandi {
  mandi_id: string
  name: string
  district: string
  latitude: number
  longitude: number
  market_type: string
  enam_integrated: boolean
  reporting_quality: string
  commodities_traded: string[]
  // Real backend fields (present in static api/mandis.ts and HF Space `src/api.py`):
  state?: string
  avg_daily_arrivals_tonnes?: number
  // Only populated by the offline mock fixture β€” real backend does not return this.
  last_updated?: string
}

export interface MandisResponse {
  mandis: Mandi[]
  total: number
  source?: string
}

export interface MarketPrice {
  mandi_id: string
  mandi_name: string
  commodity_id: string
  commodity_name: string
  price_rs: number
  agmarknet_price_rs: number | null
  enam_price_rs: number | null
  reconciled_price_rs: number
  confidence: number
  price_trend: string
  date: string
  // Present when backend spreads `full_data` JSONB (pipeline-authored rows);
  // absent from the older-row fallback in api/market-prices.ts.
  category?: string
  source_used?: string
  reasoning?: string
}

export interface MarketPricesResponse {
  market_prices: MarketPrice[]
  total: number
  source?: string
}

export interface PriceForecast {
  mandi_id: string
  mandi_name: string
  commodity_id: string
  commodity_name: string
  // Backend api/price-forecast.ts initialises these to `null` and only fills
  // 7d/14d/30d when the corresponding horizon row exists in Neon.
  current_price_rs: number | null
  price_7d: number | null
  price_14d: number | null
  price_30d: number | null
  ci_lower_7d: number | null
  ci_upper_7d: number | null
  // Backend api/price-forecast.ts only produces 7d CI bands. 14d/30d bands
  // exist in the Python `PriceForecast` dataclass but are not surfaced through
  // the `price_forecasts` SELECT. Mock fixture provides them; real API will
  // leave them undefined.
  ci_lower_14d?: number
  ci_upper_14d?: number
  ci_lower_30d?: number
  ci_upper_30d?: number
  direction: string
  confidence: number
  // Mock-only; not returned by backend.
  seasonal_index?: number
}

export interface PriceForecastsResponse {
  price_forecasts: PriceForecast[]
  total: number
  source?: string
}

export interface SellOption {
  mandi_id: string
  mandi_name: string
  sell_timing: string
  market_price_rs: number
  transport_cost_rs: number
  storage_loss_rs: number
  mandi_fee_rs: number
  net_price_rs: number
  distance_km: number
  // Python `recommendation_to_dict` emits `confidence` and `price_source`
  // but does NOT emit `drive_time_min`. Older fallback rows in
  // api/sell-recommendations.ts also drop `confidence`.
  drive_time_min?: number
  confidence?: number
  price_source?: string
}

export interface CreditReadiness {
  readiness: 'strong' | 'moderate' | 'not_yet'
  expected_revenue_rs: number
  min_revenue_rs: number
  max_advisable_input_loan_rs: number
  revenue_confidence: number
  loan_to_revenue_pct: number
  strengths: string[]
  risks: string[]
  advice_en: string
  advice_ta: string
  // KCC / DPI extras from Python `credit_readiness_to_dict` β€” absent in mock.
  kcc_limit_rs?: number | null
  kcc_outstanding_rs?: number | null
  kcc_headroom_rs?: number | null
  kcc_repayment_status?: string | null
  dpi_checked?: boolean
}

export interface SellRecommendation {
  // Fallback branch of api/sell-recommendations.ts emits `farmer_id`; newer
  // `full_data` rows include it too.
  farmer_id?: string
  farmer_name: string
  commodity_id: string
  commodity_name: string
  // Python layer emits a `quantity_quintals` key regardless of region; under
  // Kenya the value is bags. Pages use `RegionCopy.quantityNoun` for the label.
  quantity_quintals: number
  farmer_lat: number
  farmer_lon: number
  best_option: SellOption
  all_options: SellOption[]
  potential_gain_rs: number
  recommendation_text: string
  // Phase 1.4 rename: the broker agent emits `recommendation_local` as the
  // local-language translation, paired with `local_language_code` (ISO 639-1,
  // "ta" for Tamil / India, "sw" for Swahili / Kenya). Use
  // `LANGUAGE_NAMES[code]` from lib/region.ts to render a display name
  // rather than hardcoding the language.
  recommendation_local?: string
  local_language_code?: string
  // Backend explicitly returns `null` (not undefined) from the fallback path.
  credit_readiness?: CreditReadiness | null
}

export interface SellRecommendationsResponse {
  sell_recommendations: SellRecommendation[]
  total: number
  source?: string
}

export interface InvestigationStep {
  tool: string
  finding: string
}

export interface PriceConflict {
  mandi_id: string
  mandi_name: string
  commodity_id: string
  commodity_name: string
  agmarknet_price: number
  enam_price: number
  delta_pct: number
  resolution: string
  reconciled_price: number
  reasoning: string
  // api/price-conflicts.ts adds this alongside `investigation_steps` when
  // enriching raw JSONB conflicts from pipeline_runs.
  confidence?: number
  investigation_steps?: InvestigationStep[] | null
}

export interface PriceConflictsResponse {
  price_conflicts: PriceConflict[]
  total: number
  source?: string
}

// ── Raw / Extracted / Reconciled responses ────────────────────────────────────

export interface RawInputsResponse {
  raw_inputs: Record<string, unknown>
  sources: string[]
}

export interface ExtractedDataResponse {
  extracted_data: Record<string, unknown>
  total_mandis: number
}

export interface ReconciledDataResponse {
  reconciled_data: Record<string, unknown>
  total_mandis: number
  total_conflicts: number
}

// ── Model info ───────────────────────────────────────────────────────────────

export interface ModelInfoResponse {
  model_metrics: {
    model_type: string
    // Backend api/model-info.ts returns these as literal `null` when the
    // static stub has no values. Once the port wires real metrics from Neon
    // they'll be numbers, but the type must tolerate `null` today.
    rmse?: number | null
    mae?: number | null
    r2?: number | null
    directional_accuracy?: number | null
    train_samples?: number | null
    test_samples?: number | null
    features?: string[]
    feature_importances?: Record<string, number>
  }
  // Mock-only; backend api/model-info.ts does not emit this block. Kept as
  // optional so pages can render a Claude/Chronos stack summary when present.
  ml_stack?: {
    primary_model: { type: string; features: number; metrics: Record<string, number> }
    agents: Record<string, string>
    [key: string]: unknown
  }
  source?: string
}

// ── Delivery log types ──────────────────────────────────────────────────────

export interface DeliveryLog {
  farmer_id: string
  farmer_name: string
  phone: string
  channel: string
  sms_text: string
  sms_text_local: string
  status: string
  error: string | null
  created_at: string
}

export interface DeliveryLogsResponse {
  delivery_logs: DeliveryLog[]
  total: number
}

// ── Pipeline types ───────────────────────────────────────────────────────────

export interface PipelineStepDetails {
  data_source_mode?: 'live' | 'demo'
  [key: string]: unknown
}

export interface PipelineStep {
  step: string
  status: string
  duration_s: number
  details?: PipelineStepDetails
}

export interface PipelineRun {
  run_id: string
  started_at: string
  ended_at: string
  status: string
  duration_s: number
  steps: PipelineStep[]
  total_cost_usd: number
  // Extra columns surfaced by api/pipeline-runs.ts; mock fixture omits them.
  mandis_processed?: number
  commodities_tracked?: number
}

export interface PipelineRunsResponse {
  runs: PipelineRun[]
  // Backend api/pipeline-runs.ts does NOT return `total` β€” it only emits
  // `{ runs, source }`. Kept optional so the mock can still populate it.
  total?: number
  source?: string
}

export interface PipelineStats {
  total_runs: number
  success_rate: number
  mandis_monitored: number
  commodities_tracked: number
  price_conflicts_found: number
  total_cost_usd: number
  last_run: string | null
  data_sources: string[]
  // Additional fields returned by api/pipeline-stats.ts.
  successful_runs?: number
  avg_cost_per_run_usd?: number
  source?: string
}

// ── Query hooks ──────────────────────────────────────────────────────────────

const STALE_5MIN = 5 * 60 * 1000

// ── Health / region endpoint ────────────────────────────────────────────────

export interface HealthResponse {
  status: string
  // Present when backend exposes region (added in Phase 1.5 β€” both the Vercel
  // `/api/health` and the HF Space `/health` emit it).
  region?: 'kenya' | 'india'
  pipeline_data?: boolean
  source?: string
  error?: string
}

export function useHealth() {
  return useQuery<HealthResponse>({
    queryKey: ['health'],
    queryFn: () => fetchJson<HealthResponse>('/api/health'),
    staleTime: STALE_5MIN,
  })
}

export function useMandis() {
  return useQuery<MandisResponse>({
    queryKey: ['mandis'],
    queryFn: () => fetchJson<MandisResponse>('/api/mandis'),
    staleTime: STALE_5MIN,
  })
}

export function useMarketPrices() {
  return useQuery<MarketPricesResponse>({
    queryKey: ['market-prices'],
    queryFn: () => fetchJson<MarketPricesResponse>('/api/market-prices'),
    staleTime: STALE_5MIN,
  })
}

export function usePriceForecasts() {
  return useQuery<PriceForecastsResponse>({
    queryKey: ['price-forecast'],
    queryFn: () => fetchJson<PriceForecastsResponse>('/api/price-forecast'),
    staleTime: STALE_5MIN,
  })
}

export function useSellRecommendations() {
  return useQuery<SellRecommendationsResponse>({
    queryKey: ['sell-recommendations'],
    queryFn: () => fetchJson<SellRecommendationsResponse>('/api/sell-recommendations'),
    staleTime: STALE_5MIN,
  })
}

export function usePriceConflicts() {
  return useQuery<PriceConflictsResponse>({
    queryKey: ['price-conflicts'],
    queryFn: () => fetchJson<PriceConflictsResponse>('/api/price-conflicts'),
    staleTime: STALE_5MIN,
  })
}

export function useModelInfo() {
  return useQuery<ModelInfoResponse>({
    queryKey: ['model-info'],
    queryFn: () => fetchJson<ModelInfoResponse>('/api/model-info'),
    staleTime: STALE_5MIN,
  })
}

export function useDeliveryLogs() {
  return useQuery<DeliveryLogsResponse>({
    queryKey: ['delivery-logs'],
    queryFn: () => fetchJson<DeliveryLogsResponse>('/api/delivery-logs'),
    staleTime: STALE_5MIN,
  })
}

export function usePipelineRuns() {
  return useQuery<PipelineRunsResponse>({
    queryKey: ['pipeline-runs'],
    queryFn: () => fetchJson<PipelineRunsResponse>('/api/pipeline-runs'),
    staleTime: STALE_5MIN,
  })
}

export function usePipelineStats() {
  return useQuery<PipelineStats>({
    queryKey: ['pipeline-stats'],
    queryFn: () => fetchJson<PipelineStats>('/api/pipeline-stats'),
    staleTime: STALE_5MIN,
  })
}