gkdivya commited on
Commit
76ec835
·
verified ·
1 Parent(s): 87ecf4c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +57 -509
app.py CHANGED
@@ -1,421 +1,41 @@
1
- #!/usr/bin/env python3
2
  # app.py
3
- # --------------------------------------------------
4
- # Gradio app for state-specific school fuzzy matching
5
- #
6
- # - Loads school masters from Hugging Face dataset (Apf-AI4Good/Schools)
7
- # - Uses normalization patterns from patterns.json (no hardcoded patterns)
8
- # - Public "Search" tab for users
9
- # - "Admin" tab to view/edit/save patterns (password protected for saving)
10
- # --------------------------------------------------
11
-
12
  import os
13
- import re
14
- import json
15
  import pandas as pd
16
  import gradio as gr
17
- from rapidfuzz import process, fuzz
18
- from datasets import load_dataset
19
-
20
- # ====================================================
21
- # CONFIG: columns, states, HF dataset, admin
22
- # ====================================================
23
-
24
- # Expected columns in the master CSVs (must match your HF CSVs)
25
- MASTER_SCHOOL_COL = "School_Name__c"
26
- MASTER_DISTRICT_COL = "School_District__c"
27
- MASTER_BLOCK_COL = "School_Block__c" # optional
28
- MASTER_UDISE_COL = "School_Udise_Code__c"
29
- MASTER_STATE_COL = "School_State__c" # optional
30
-
31
- # Hugging Face dataset that holds all state CSVs
32
- HF_SCHOOLS_DATASET = "Apf-AI4Good/Schools"
33
-
34
- # Map state keys to CSV filenames inside that dataset
35
- STATE_HF_FILES = {
36
- "ARUNACHAL PRADESH": "Arunachal Pradesh.csv",
37
- "ASSAM": "Assam.csv",
38
- "BIHAR": "Bihar.csv",
39
- "CHHATTISGARH": "Chhattisgarh.csv",
40
- "JHARKHAND": "Jharkhand.csv",
41
- "MADHYA PRADESH": "Madhya Pradesh.csv",
42
- "MANIPUR": "Manipur.csv",
43
- "MEGHALAYA": "Meghalaya.csv",
44
- "MIZORAM": "Mizoram.csv",
45
- "NAGALAND": "Nagaland.csv",
46
- "ODISHA": "Odisha.csv",
47
- "PUDUCHERRY": "Puducherry.csv",
48
- "RAJASTHAN": "Rajasthan.csv",
49
- "SIKKIM": "Sikkim.csv",
50
- "TELANGANA": "Telangana.csv",
51
- "TRIPURA": "Tripura.csv",
52
- "UTTAR PRADESH": "Uttar Pradesh.csv",
53
- "UTTARAKHAND": "Uttarakhand.csv"
54
- }
55
-
56
-
57
- # Default state for dropdown
58
- DEFAULT_STATE_KEY = "ARUNACHAL PRADESH"
59
-
60
- # Number of candidates to show in the table
61
- MAX_CANDIDATES = 5
62
-
63
- # JSON file to store patterns
64
-
65
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
66
- PATTERN_FILE = os.path.join(BASE_DIR, "patterns.json")
67
-
68
- # Admin password (CHANGE THIS!)
69
- ADMIN_PASSWORD = "secret"
70
-
71
- # Global master df (for currently selected state)
72
- master_df: pd.DataFrame | None = None
73
-
74
- # Global pattern config (loaded from patterns.json)
75
- pattern_config: dict | None = None
76
-
77
-
78
- def load_pattern_config(debug=True) -> dict:
79
- """
80
- Load patterns.json from PATTERN_FILE.
81
- If missing, create empty structure.
82
- If debug=True, also return verbose info via prints.
83
- """
84
- if os.path.exists(PATTERN_FILE):
85
- try:
86
- with open(PATTERN_FILE, "r", encoding="utf-8") as f:
87
- cfg = json.load(f)
88
- if debug:
89
- print("DEBUG: Successfully loaded patterns.json")
90
- print("DEBUG: top-level keys:", list(cfg.keys()))
91
- # show counts safely
92
- gcount = len(cfg.get("global", []))
93
- skeys = list(cfg.get("states", {}).keys())
94
- print(f"DEBUG: global patterns={gcount}, state keys={skeys}")
95
- return cfg
96
- except Exception as e:
97
- print("ERROR: Failed to parse patterns.json:", e)
98
- # fallback to empty skeleton
99
- return {"global": [], "states": {}}
100
- else:
101
- # Not present — create it with empty structure and log
102
- skeleton = {"global": [], "states": {}}
103
- try:
104
- with open(PATTERN_FILE, "w", encoding="utf-8") as f:
105
- json.dump(skeleton, f, indent=2, ensure_ascii=False)
106
- print("DEBUG: patterns.json not found — created empty patterns.json at", PATTERN_FILE)
107
- except Exception as e:
108
- print("ERROR: Could not create patterns.json:", e)
109
- return skeleton
110
-
111
-
112
-
113
- def save_pattern_config(config: dict) -> None:
114
- with open(PATTERN_FILE, "w", encoding="utf-8") as f:
115
- json.dump(config, f, indent=2, ensure_ascii=False)
116
-
117
-
118
- def build_patterns_from_config(config: dict, state_key: str | None):
119
- """
120
- Convert JSON config into lists of (pattern, replacement) tuples
121
- for global and state-specific patterns.
122
- """
123
- global_list = [(p["pattern"], p["replacement"]) for p in config.get("global", [])]
124
-
125
- state_list = []
126
- if state_key:
127
- state_key_up = state_key.upper().strip()
128
- state_patterns = config.get("states", {}).get(state_key_up, [])
129
- state_list = [(p["pattern"], p["replacement"]) for p in state_patterns]
130
-
131
- return global_list, state_list
132
-
133
-
134
- # ====================================================
135
- # NORMALIZATION
136
- # ====================================================
137
-
138
- def normalize_with_patterns_dynamic(s: str, state_key: str | None) -> str:
139
- """Normalize using global + state patterns from pattern_config."""
140
- global pattern_config
141
-
142
- if not isinstance(s, str):
143
- return ""
144
- s = s.upper()
145
-
146
- if pattern_config is None:
147
- pattern_config = load_pattern_config()
148
-
149
- global_patterns, state_patterns = build_patterns_from_config(pattern_config, state_key)
150
-
151
- for pat, repl in global_patterns:
152
- s = re.sub(pat, repl, s)
153
- for pat, repl in state_patterns:
154
- s = re.sub(pat, repl, s)
155
-
156
- # Standard final cleaning
157
- s = re.sub(r"[^A-Z0-9]+", " ", s)
158
- s = re.sub(r"\s+", " ", s).strip()
159
- return s
160
-
161
-
162
- # ====================================================
163
- # DATA LOADING FROM HF
164
- # ====================================================
165
-
166
- def load_master_for_state(state_key: str | None):
167
- """
168
- Load the master CSV for a state from Hugging Face Datasets,
169
- set global master_df, and return District & Block dropdown configs.
170
- """
171
- global master_df
172
-
173
- if not state_key:
174
- master_df = None
175
- return (
176
- gr.Dropdown(choices=[], value=None),
177
- gr.Dropdown(choices=[], value=None),
178
- )
179
-
180
- state_key_norm = state_key.upper().strip()
181
- if state_key_norm not in STATE_HF_FILES:
182
- master_df = None
183
- return (
184
- gr.Dropdown(choices=[], value=None),
185
- gr.Dropdown(choices=[], value=None),
186
- )
187
-
188
- csv_filename = STATE_HF_FILES[state_key_norm]
189
-
190
- # Load only that CSV file from the HF dataset repo
191
- ds_dict = load_dataset(
192
- HF_SCHOOLS_DATASET,
193
- data_files={"train": csv_filename},
194
- )
195
- ds = ds_dict["train"]
196
-
197
- # Convert to pandas and standardize
198
- master_df = ds.to_pandas().fillna("")
199
-
200
- # District choices
201
- if MASTER_DISTRICT_COL in master_df.columns:
202
- districts = sorted(master_df[MASTER_DISTRICT_COL].dropna().unique().tolist())
203
- districts = ["All"] + districts
204
- else:
205
- districts = []
206
-
207
- # Initial blocks (will be refined when district changes)
208
- if MASTER_BLOCK_COL in master_df.columns:
209
- blocks = ["All"]
210
- else:
211
- blocks = []
212
-
213
- return (
214
- gr.Dropdown(choices=districts, value="All" if districts else None),
215
- gr.Dropdown(choices=blocks, value="All" if blocks else None),
216
- )
217
-
218
-
219
- def update_blocks(district: str | None):
220
- """
221
- Update Block dropdown when District changes.
222
- """
223
- global master_df
224
-
225
- if master_df is None or MASTER_BLOCK_COL not in master_df.columns:
226
- return gr.Dropdown(choices=["All"], value="All")
227
-
228
- df = master_df
229
- if (
230
- district
231
- and district != "All"
232
- and MASTER_DISTRICT_COL in df.columns
233
- ):
234
- df = df[df[MASTER_DISTRICT_COL] == district]
235
-
236
- blocks = sorted(df[MASTER_BLOCK_COL].dropna().unique().tolist())
237
- blocks = ["All"] + blocks if blocks else ["All"]
238
- return gr.Dropdown(choices=blocks, value="All")
239
-
240
-
241
- # ====================================================
242
- # FUZZY SEARCH
243
- # ====================================================
244
-
245
- def search_candidates(query_name: str, state_key: str | None, district: str | None, block: str | None):
246
- """
247
- Given school name + state + district + block, return:
248
- - candidates table (top N matches)
249
- - best-candidate table (single row)
250
- """
251
- global master_df
252
-
253
- if master_df is None:
254
- return pd.DataFrame(), pd.DataFrame()
255
-
256
- query_name = (query_name or "").strip()
257
- if not query_name:
258
- return pd.DataFrame(), pd.DataFrame()
259
-
260
- df = master_df
261
-
262
- # Filter by district
263
- if (
264
- district
265
- and district != "All"
266
- and MASTER_DISTRICT_COL in df.columns
267
- ):
268
- df = df[df[MASTER_DISTRICT_COL] == district]
269
-
270
- # Filter by block
271
- if (
272
- block
273
- and block != "All"
274
- and MASTER_BLOCK_COL in df.columns
275
- ):
276
- df = df[df[MASTER_BLOCK_COL] == block]
277
-
278
- if df.empty:
279
- return pd.DataFrame(), pd.DataFrame()
280
-
281
- state_for_patterns = (state_key or DEFAULT_STATE_KEY).upper().strip()
282
-
283
- choices = df[MASTER_SCHOOL_COL].astype(str)
284
 
285
- candidates_raw = process.extract(
286
- query_name,
287
- choices,
288
- scorer=fuzz.token_set_ratio,
289
- processor=lambda s: normalize_with_patterns_dynamic(s, state_for_patterns),
290
- limit=MAX_CANDIDATES,
291
- ) # (choice, score, key)
292
-
293
- if not candidates_raw:
294
- return pd.DataFrame(), pd.DataFrame()
295
-
296
- rows = []
297
- for choice_name, score, key in candidates_raw:
298
- try:
299
- row = df.loc[key]
300
- except Exception:
301
- continue
302
-
303
- rows.append({
304
- "School_Name": row.get(MASTER_SCHOOL_COL, ""),
305
- "State": row.get(MASTER_STATE_COL, "") if MASTER_STATE_COL in df.columns else state_for_patterns,
306
- "District": row.get(MASTER_DISTRICT_COL, "") if MASTER_DISTRICT_COL in df.columns else "",
307
- "Block": row.get(MASTER_BLOCK_COL, "") if MASTER_BLOCK_COL in df.columns else "",
308
- "UDISE_Code": row.get(MASTER_UDISE_COL, "") if MASTER_UDISE_COL in df.columns else "",
309
- "Score": score,
310
- })
311
-
312
- if not rows:
313
- return pd.DataFrame(), pd.DataFrame()
314
-
315
- candidates_df = pd.DataFrame(rows)
316
- best_df = candidates_df.head(1).copy()
317
- return candidates_df, best_df
318
-
319
-
320
- # ====================================================
321
- # ADMIN / PATTERN CALLBACKS (PASSWORD PROTECTED SAVE)
322
- # ====================================================
323
-
324
- def load_state_patterns_for_editor(selected_state: str | None, new_state_name: str | None):
325
- """
326
- Load state-specific patterns into the editor dataframe.
327
- Does NOT require password (view-only).
328
- """
329
- global pattern_config
330
- if pattern_config is None:
331
- pattern_config = load_pattern_config()
332
-
333
- key = None
334
- if new_state_name and new_state_name.strip():
335
- key = new_state_name.strip().upper()
336
- elif selected_state:
337
- key = selected_state.strip().upper()
338
-
339
- if not key:
340
- return pd.DataFrame(columns=["pattern", "replacement"])
341
-
342
- state_patterns = pattern_config.get("states", {}).get(key, [])
343
- if not state_patterns:
344
- return pd.DataFrame(columns=["pattern", "replacement"])
345
-
346
- return pd.DataFrame(state_patterns)
347
-
348
-
349
- def save_global_patterns_from_editor(df: pd.DataFrame, password: str):
350
- global pattern_config
351
- if password != ADMIN_PASSWORD:
352
- return "❌ Invalid admin password. Global patterns NOT saved."
353
-
354
- if pattern_config is None:
355
- pattern_config = load_pattern_config()
356
-
357
- pattern_config["global"] = df.fillna("").to_dict(orient="records")
358
- save_pattern_config(pattern_config)
359
- return "✅ Global patterns saved to patterns.json"
360
-
361
-
362
- def save_state_patterns_from_editor(selected_state: str | None, new_state_name: str | None, df: pd.DataFrame, password: str):
363
- global pattern_config
364
- if password != ADMIN_PASSWORD:
365
- return "❌ Invalid admin password. State patterns NOT saved."
366
-
367
- if pattern_config is None:
368
- pattern_config = load_pattern_config()
369
-
370
- key = None
371
- if new_state_name and new_state_name.strip():
372
- key = new_state_name.strip().upper()
373
- elif selected_state:
374
- key = selected_state.strip().upper()
375
-
376
- if not key:
377
- return "⚠️ Please select a state or type a new state key."
378
-
379
- pattern_config.setdefault("states", {})[key] = df.fillna("").to_dict(orient="records")
380
- save_pattern_config(pattern_config)
381
- return f"✅ Patterns for **{key}** saved to patterns.json"
382
-
383
-
384
- def load_global_patterns_for_editor():
385
- """Load global patterns into editor (view-only, no password)."""
386
- global pattern_config
387
- if pattern_config is None:
388
- pattern_config = load_pattern_config()
389
- return pd.DataFrame(pattern_config.get("global", []))
390
-
391
-
392
- # ====================================================
393
- # BUILD GRADIO UI
394
- # ====================================================
395
-
396
- # Initialize pattern_config at startup
397
- pattern_config = load_pattern_config()
398
-
399
- with gr.Blocks(title="State School Fuzzy Matcher") as demo:
400
  gr.Markdown(
401
  """
402
  # State School Fuzzy Matcher (RapidFuzz)
403
-
404
- **Search tab** (public):
405
- 1. Select **State** (loads master CSV from Hugging Face dataset).
406
- 2. (Optional) choose **District** and **Block**.
407
- 3. Type a **School Name** (as on marksheet).
408
- 4. See top fuzzy-match candidates and the best candidate.
409
-
410
- **Admin tab** (protected for saving):
411
- - View / edit **global** and **state-specific** normalization patterns.
412
- - Saving changes requires an **admin password**.
413
  """
414
  )
415
 
416
- # ------------------------------------------------
417
- # TAB: SEARCH (PUBLIC)
418
- # ------------------------------------------------
419
  with gr.Tab("Search"):
420
  with gr.Row():
421
  state_dd = gr.Dropdown(
@@ -424,15 +44,11 @@ with gr.Blocks(title="State School Fuzzy Matcher") as demo:
424
  value=DEFAULT_STATE_KEY if DEFAULT_STATE_KEY in STATE_HF_FILES else None,
425
  interactive=True,
426
  )
427
-
428
  with gr.Row():
429
  district_dd = gr.Dropdown(label="District", choices=[], value=None, interactive=True)
430
  block_dd = gr.Dropdown(label="Block", choices=[], value=None, interactive=True)
431
 
432
- school_input = gr.Textbox(
433
- label="Input School Name",
434
- placeholder="Type school name from marksheet..."
435
- )
436
  search_btn = gr.Button("Find Candidates")
437
 
438
  gr.Markdown("### Candidates (top matches)")
@@ -449,119 +65,51 @@ with gr.Blocks(title="State School Fuzzy Matcher") as demo:
449
  interactive=False
450
  )
451
 
452
- # ------------------------------------------------
453
- # TAB: ADMIN (PASSWORD-PROTECTED SAVE)
454
- # ------------------------------------------------
455
  with gr.Tab("Admin"):
456
- gr.Markdown(
457
- """
458
- ### Admin – Pattern configuration
459
-
460
- - **Global patterns**: applied to all states.
461
- - **State-specific patterns**: applied only for that state key.
462
-
463
- You can **view and edit** patterns freely, but **saving** requires the admin password.
464
- """
465
- )
466
 
467
- admin_pwd = gr.Textbox(
468
- label="Admin Password",
469
- type="password",
470
- placeholder="Enter admin password to save changes",
471
- )
472
-
473
- # ---- Global patterns editor ----
474
- gr.Markdown("#### Global Patterns")
475
  global_df = gr.Dataframe(
476
  value=load_global_patterns_for_editor(),
477
  headers=["pattern", "replacement"],
478
  datatype=["str", "str"],
479
  interactive=True,
480
- row_count=(len(pattern_config.get("global", [])) or 1),
481
  )
482
  save_global_btn = gr.Button("💾 Save Global Patterns")
483
  global_status = gr.Markdown("")
484
 
485
- # ---- State patterns editor ----
486
- gr.Markdown("#### State-specific Patterns")
487
-
488
- existing_state_keys = sorted(pattern_config.get("states", {}).keys())
489
  with gr.Row():
490
- state_pattern_dd = gr.Dropdown(
491
- label="Existing state key",
492
- choices=existing_state_keys,
493
- value=existing_state_keys[0] if existing_state_keys else None,
494
- interactive=True,
495
- )
496
- new_state_tb = gr.Textbox(
497
- label="Or type new state key",
498
- placeholder="e.g. KARNATAKA or TAMIL NADU",
499
- )
500
-
501
- state_df = gr.Dataframe(
502
- value=pd.DataFrame(columns=["pattern", "replacement"]),
503
- headers=["pattern", "replacement"],
504
- datatype=["str", "str"],
505
- interactive=True,
506
- row_count=3,
507
- )
508
  load_state_btn = gr.Button("Load patterns for selected / new state")
509
  save_state_btn = gr.Button("💾 Save State Patterns")
510
  state_status = gr.Markdown("")
511
 
512
- # ====================================================
513
- # WIRING EVENTS
514
- # ====================================================
515
-
516
- # Load master when state changes
517
- state_dd.change(
518
- fn=load_master_for_state,
519
- inputs=state_dd,
520
- outputs=[district_dd, block_dd],
521
- )
522
-
523
- # Initial load for default state (if any)
524
- demo.load(
525
- fn=load_master_for_state,
526
- inputs=state_dd,
527
- outputs=[district_dd, block_dd],
528
- )
529
-
530
- # Update blocks when district changes
531
- district_dd.change(
532
- fn=update_blocks,
533
- inputs=district_dd,
534
- outputs=block_dd,
535
- )
536
-
537
- # Search button
538
- search_btn.click(
539
- fn=search_candidates,
540
- inputs=[school_input, state_dd, district_dd, block_dd],
541
- outputs=[candidates_table, best_table],
542
- )
543
-
544
- # Admin: save global patterns (password checked inside)
545
- save_global_btn.click(
546
- fn=save_global_patterns_from_editor,
547
- inputs=[global_df, admin_pwd],
548
- outputs=global_status,
549
- )
550
-
551
- # Admin: load state patterns into editor
552
- load_state_btn.click(
553
- fn=load_state_patterns_for_editor,
554
- inputs=[state_pattern_dd, new_state_tb],
555
- outputs=state_df,
556
- )
557
-
558
- # Admin: save state patterns (password checked inside)
559
- save_state_btn.click(
560
- fn=save_state_patterns_from_editor,
561
- inputs=[state_pattern_dd, new_state_tb, state_df, admin_pwd],
562
- outputs=state_status,
563
- )
564
-
565
 
566
  if __name__ == "__main__":
567
  demo.launch()
 
 
1
  # app.py
 
 
 
 
 
 
 
 
 
2
  import os
 
 
3
  import pandas as pd
4
  import gradio as gr
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
+ # Import functions from other modules
7
+ from searchschool import (
8
+ STATE_HF_FILES,
9
+ DEFAULT_STATE_KEY,
10
+ load_master_for_state,
11
+ update_blocks,
12
+ search_candidates,
13
+ MASTER_SCHOOL_COL,
14
+ )
15
+ from admin_patterns import (
16
+ load_pattern_config,
17
+ load_global_patterns_for_editor,
18
+ load_state_patterns_for_editor,
19
+ save_global_patterns_from_editor,
20
+ save_state_patterns_from_editor,
21
+ refresh_pattern_config,
22
+ show_patterns_file_info,
23
+ )
24
+
25
+ # Admin password from environment (set as HF Space secret ADMIN_PASSWORD)
26
+ ADMIN_PASSWORD = os.getenv("ADMIN_PASSWORD", None)
27
+ if not ADMIN_PASSWORD:
28
+ print(" ADMIN_PASSWORD environment variable not set. Set ADMIN_PASSWORD secret in HF Space settings.")
29
+
30
+ # Build Gradio UI
31
+ with gr.Blocks(title="State School Fuzzy Matcher (modular)") as demo:
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  gr.Markdown(
33
  """
34
  # State School Fuzzy Matcher (RapidFuzz)
 
 
 
 
 
 
 
 
 
 
35
  """
36
  )
37
 
38
+ # ----- SEARCH TAB -----
 
 
39
  with gr.Tab("Search"):
40
  with gr.Row():
41
  state_dd = gr.Dropdown(
 
44
  value=DEFAULT_STATE_KEY if DEFAULT_STATE_KEY in STATE_HF_FILES else None,
45
  interactive=True,
46
  )
 
47
  with gr.Row():
48
  district_dd = gr.Dropdown(label="District", choices=[], value=None, interactive=True)
49
  block_dd = gr.Dropdown(label="Block", choices=[], value=None, interactive=True)
50
 
51
+ school_input = gr.Textbox(label="Input School Name", placeholder="Type school name from marksheet...")
 
 
 
52
  search_btn = gr.Button("Find Candidates")
53
 
54
  gr.Markdown("### Candidates (top matches)")
 
65
  interactive=False
66
  )
67
 
68
+ # ----- ADMIN TAB -----
 
 
69
  with gr.Tab("Admin"):
70
+ gr.Markdown("### Admin — Patterns (password required to save)")
71
+ admin_pwd = gr.Textbox(label="Admin Password", type="password", placeholder="Enter admin password to save changes")
 
 
 
 
 
 
 
 
72
 
73
+ # Global patterns editor (load fresh)
 
 
 
 
 
 
 
74
  global_df = gr.Dataframe(
75
  value=load_global_patterns_for_editor(),
76
  headers=["pattern", "replacement"],
77
  datatype=["str", "str"],
78
  interactive=True,
79
+ row_count=(len(load_pattern_config().get("global", [])) or 1),
80
  )
81
  save_global_btn = gr.Button("💾 Save Global Patterns")
82
  global_status = gr.Markdown("")
83
 
84
+ # State-specific patterns editor
85
+ existing_state_keys = sorted(load_pattern_config().get("states", {}).keys())
 
 
86
  with gr.Row():
87
+ state_pattern_dd = gr.Dropdown(label="Existing state key", choices=existing_state_keys, value=existing_state_keys[0] if existing_state_keys else None, interactive=True)
88
+ new_state_tb = gr.Textbox(label="Or type new state key", placeholder="e.g. KARNATAKA")
89
+ state_df = gr.Dataframe(value=pd.DataFrame(columns=["pattern", "replacement"]), headers=["pattern", "replacement"], datatype=["str", "str"], interactive=True, row_count=3)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  load_state_btn = gr.Button("Load patterns for selected / new state")
91
  save_state_btn = gr.Button("💾 Save State Patterns")
92
  state_status = gr.Markdown("")
93
 
94
+ # Debug controls
95
+ refresh_btn = gr.Button("🔄 Refresh patterns.json from disk")
96
+ refresh_status = gr.Markdown("")
97
+ show_file_btn = gr.Button("🗂 Show patterns.json file info (path + preview)")
98
+ debug_info_md = gr.Markdown("")
99
+
100
+ # ----- Wiring -----
101
+ state_dd.change(fn=load_master_for_state, inputs=state_dd, outputs=[district_dd, block_dd])
102
+ demo.load(fn=load_master_for_state, inputs=state_dd, outputs=[district_dd, block_dd])
103
+ district_dd.change(fn=update_blocks, inputs=district_dd, outputs=block_dd)
104
+ search_btn.click(fn=search_candidates, inputs=[school_input, state_dd, district_dd, block_dd], outputs=[candidates_table, best_table])
105
+
106
+ # Admin wiring (password passed to save functions)
107
+ save_global_btn.click(fn=lambda df, pwd: save_global_patterns_from_editor(df, pwd, ADMIN_PASSWORD), inputs=[global_df, admin_pwd], outputs=global_status)
108
+ load_state_btn.click(fn=load_state_patterns_for_editor, inputs=[state_pattern_dd, new_state_tb], outputs=state_df)
109
+ save_state_btn.click(fn=lambda state_sel, new_state, df, pwd: save_state_patterns_from_editor(state_sel, new_state, df, pwd, ADMIN_PASSWORD), inputs=[state_pattern_dd, new_state_tb, state_df, admin_pwd], outputs=state_status)
110
+
111
+ refresh_btn.click(fn=refresh_pattern_config, inputs=None, outputs=refresh_status)
112
+ show_file_btn.click(fn=show_patterns_file_info, inputs=None, outputs=debug_info_md)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  if __name__ == "__main__":
115
  demo.launch()