styhero commited on
Commit
d3680f3
Β·
verified Β·
1 Parent(s): a224b91

Upload app.py with huggingface_hub

Browse files
Files changed (1) hide show
  1. app.py +331 -160
app.py CHANGED
@@ -8,21 +8,18 @@ from concurrent.futures import ThreadPoolExecutor, as_completed
8
  # ── Config ─────────────────────────────────────────────────────────────────────
9
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
10
  HF_DATASET = "hf://datasets/styhero/styhero_dataset/data"
11
- BASE_DIR = os.path.dirname(os.path.abspath(__file__))
12
- _LOCAL_M1 = os.path.join(BASE_DIR, "mobile1_index.parquet")
13
- _LOCAL_M2 = os.path.join(BASE_DIR, "mobile2_index.parquet")
14
- # Use local file if present (fast), otherwise read from HF dataset
15
- MOBILE1_IDX = _LOCAL_M1 if os.path.exists(_LOCAL_M1) else "hf://datasets/styhero/styhero_dataset/data/mobile1_index.parquet"
16
- MOBILE2_IDX = _LOCAL_M2 if os.path.exists(_LOCAL_M2) else "hf://datasets/styhero/styhero_dataset/data/mobile2_index.parquet"
17
-
18
 
19
  # ── Helpers ────────────────────────────────────────────────────────────────────
20
 
21
- def fmt_ms(ms: float) -> str:
22
- return f"{ms / 1000:.2f} s" if ms >= 1000 else f"{ms:.0f} ms"
23
-
24
 
25
- def clean(val) -> str:
26
  if val is None or (isinstance(val, float) and pd.isna(val)):
27
  return ""
28
  s = str(val).strip()
@@ -30,29 +27,26 @@ def clean(val) -> str:
30
  return ""
31
  return ", ".join(p.strip() for p in s.split("!") if p.strip())
32
 
33
-
34
- def sanitize_df(df: pd.DataFrame) -> pd.DataFrame:
35
  df = df.copy()
36
  for col in ["name", "fname", "email", "id", "address", "circle"]:
37
  if col in df.columns:
38
  df[col] = df[col].apply(clean)
39
  return df
40
 
41
-
42
  def new_con():
43
  con = duckdb.connect()
44
  con.execute("INSTALL httpfs; LOAD httpfs;")
45
  con.execute("CREATE SECRET IF NOT EXISTS hf_secret (TYPE HUGGINGFACE, TOKEN ?)", [HF_TOKEN])
46
- con.execute("SET threads = 4; SET enable_progress_bar = false;")
47
  return con
48
 
49
-
50
  # ── Queries ────────────────────────────────────────────────────────────────────
51
 
52
- def _query_one(index_path: str, col: str, value: str):
53
  con = new_con()
54
- t0 = time.perf_counter()
55
- df = con.execute(f"""
56
  SELECT mobile1, mobile2, row_group_id, row_start, row_end, filename
57
  FROM read_parquet('{index_path}')
58
  WHERE {col} = '{value}'
@@ -60,8 +54,7 @@ def _query_one(index_path: str, col: str, value: str):
60
  """).fetchdf()
61
  return df, (time.perf_counter() - t0) * 1000
62
 
63
-
64
- def query_both_parallel(target: str):
65
  results = {}
66
  with ThreadPoolExecutor(max_workers=2) as ex:
67
  futures = {
@@ -73,23 +66,95 @@ def query_both_parallel(target: str):
73
  results[futures[f]] = (df, ms)
74
  return results
75
 
76
-
77
- def _fetch_one(filename: str, row_start: int, row_end: int, search_col: str, target: str):
78
- con = new_con()
79
  hf_path = f"{HF_DATASET}/{filename.split('/')[-1]}"
80
- t0 = time.perf_counter()
81
- df = con.execute(f"""
82
  SELECT mobile1, mobile2, email, id, name, fname, address, circle
83
- FROM read_parquet('{hf_path}', file_row_number = true)
84
  WHERE file_row_number BETWEEN {row_start} AND {row_end}
85
  AND {search_col} = '{target}'
86
  """).fetchdf()
87
  return sanitize_df(df), (time.perf_counter() - t0) * 1000
88
 
 
89
 
90
- # ── Card renderer ──────────────────────────────────────────────────────────────
 
91
 
92
- def show_card(row: pd.Series, idx: int):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
93
  name = clean(row.get("name")) or "Unknown"
94
  fname = clean(row.get("fname")) or ""
95
  mobile1 = clean(row.get("mobile1")) or ""
@@ -101,7 +166,7 @@ def show_card(row: pd.Series, idx: int):
101
 
102
  copy_text = "\n".join(filter(None, [
103
  f"Name: {name}",
104
- f"Father: {fname}" if fname else "",
105
  f"Mobile1: {mobile1}" if mobile1 else "",
106
  f"Mobile2: {mobile2}" if mobile2 else "",
107
  f"Email: {email}" if email else "",
@@ -109,122 +174,198 @@ def show_card(row: pd.Series, idx: int):
109
  f"Address: {address}" if address else "",
110
  f"Circle: {circle}" if circle else "",
111
  ]))
 
112
 
113
- # Escape for JS string
114
- copy_safe = copy_text.replace("\\", "\\\\").replace("`", "\\`").replace("${", "\\${")
115
-
116
- card_html = f"""
117
- <!DOCTYPE html>
118
- <html>
119
- <head>
120
- <style>
121
- * {{ box-sizing: border-box; margin: 0; padding: 0; }}
122
- body {{ background: transparent; font-family: -apple-system, BlinkMacSystemFont, sans-serif; }}
123
- .card {{
124
- background: #1a1a2e;
125
- border: 1px solid #2d2d44;
126
- border-radius: 14px;
127
- padding: 18px 18px 18px 18px;
128
- position: relative;
129
- }}
130
- .copy-btn {{
131
- position: absolute;
132
- top: 14px;
133
- right: 14px;
134
- background: #3b3b6e;
135
- color: #a0a0ff;
136
- border: 1px solid #5050a0;
137
- border-radius: 8px;
138
- padding: 6px 14px;
139
- font-size: 0.82rem;
140
- cursor: pointer;
141
- white-space: nowrap;
142
- transition: background 0.15s;
143
- -webkit-tap-highlight-color: transparent;
144
- }}
145
- .copy-btn:hover, .copy-btn:active {{ background: #5050a0; color: #fff; }}
146
- .c-name {{
147
- font-size: 1.1rem;
148
- font-weight: 700;
149
- color: #e2e2f0;
150
- padding-right: 90px;
151
- }}
152
- .c-fname {{ font-size: 0.82rem; color: #777; margin: 3px 0 14px; }}
153
- .c-grid {{
154
- display: grid;
155
- grid-template-columns: repeat(auto-fit, minmax(180px, 1fr));
156
- gap: 10px 20px;
157
- }}
158
- .c-field {{ display: flex; flex-direction: column; }}
159
- .c-label {{
160
- font-size: 0.66rem;
161
- text-transform: uppercase;
162
- letter-spacing: 0.08em;
163
- color: #555;
164
- margin-bottom: 2px;
165
- }}
166
- .c-value {{ font-size: 0.9rem; color: #bbb; word-break: break-word; }}
167
- .c-full {{ grid-column: 1 / -1; }}
168
- </style>
169
- </head>
170
- <body>
171
- <div class="card">
172
- <button class="copy-btn" id="cb{idx}" onclick="
173
- navigator.clipboard.writeText(`{copy_safe}`)
174
- .then(() => {{ document.getElementById('cb{idx}').textContent='βœ“ Copied';
175
- setTimeout(()=>document.getElementById('cb{idx}').textContent='Copy',1500); }})
176
- .catch(() => document.getElementById('cb{idx}').textContent='Failed');
177
- ">Copy</button>
178
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
179
  <div class="c-name">{name}</div>
180
- <div class="c-fname">{"S/O: " + fname if fname else "&nbsp;"}</div>
181
-
182
- <div class="c-grid">
183
- {"" if not mobile1 else '<div class="c-field"><span class="c-label">Mobile 1</span><span class="c-value">' + mobile1 + '</span></div>'}
184
- {"" if not mobile2 else '<div class="c-field"><span class="c-label">Mobile 2</span><span class="c-value">' + mobile2 + '</span></div>'}
185
- {"" if not email else '<div class="c-field"><span class="c-label">Email</span><span class="c-value">' + email + '</span></div>'}
186
- {"" if not rid else '<div class="c-field"><span class="c-label">ID</span><span class="c-value">' + rid + '</span></div>'}
187
- {"" if not circle else '<div class="c-field"><span class="c-label">Circle</span><span class="c-value">' + circle + '</span></div>'}
188
- {"" if not address else '<div class="c-field c-full"><span class="c-label">Address</span><span class="c-value">' + address + '</span></div>'}
189
- </div>
190
  </div>
191
-
192
- <script>
193
- // Auto-resize iframe to card height
194
- const resize = () => {{
195
- const h = document.querySelector('.card').getBoundingClientRect().height;
196
- window.parent.document.querySelectorAll('iframe').forEach(f => {{
197
- if (f.contentWindow === window) f.style.height = (h + 8) + 'px';
198
- }});
199
- }};
200
- window.addEventListener('load', resize);
201
- window.addEventListener('resize', resize);
202
- </script>
203
- </body>
204
- </html>
205
- """
206
- st.components.v1.html(card_html, height=200, scrolling=False)
207
-
208
-
209
- # ── Page ───────────────────────────────────────────────────────────────────────
210
- st.set_page_config(page_title="Mobile Lookup", page_icon="πŸ“±", layout="centered")
 
 
 
 
 
 
 
 
211
 
212
  st.markdown("""
213
  <style>
214
- /* tighten top padding */
215
- .block-container { padding-top: 2rem; padding-bottom: 2rem; }
216
- /* hide streamlit menu / footer */
217
- #MainMenu, footer { visibility: hidden; }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
218
  </style>
219
  """, unsafe_allow_html=True)
220
 
221
- st.markdown("## πŸ“± Mobile Lookup")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
222
 
223
  with st.form("search_form"):
224
- number = st.text_input(
225
- "", placeholder="Enter mobile number β€” e.g. 919876543210",
226
- label_visibility="collapsed"
227
- )
228
  submitted = st.form_submit_button("Search", use_container_width=True, type="primary")
229
 
230
  if not submitted or not number.strip():
@@ -232,25 +373,36 @@ if not submitted or not number.strip():
232
 
233
  target = number.strip()
234
 
235
- # ── Index lookup ───────────────────────────────────────────────────────────────
236
- with st.spinner("Searching…"):
237
- t0 = time.perf_counter()
238
- idx_results = query_both_parallel(target)
239
- idx_ms = (time.perf_counter() - t0) * 1000
 
 
 
 
 
240
 
241
  m1_df, m1_ms = idx_results["mobile1"]
242
  m2_df, m2_ms = idx_results["mobile2"]
243
 
244
  if m1_df.empty and m2_df.empty:
245
- st.warning(f"No records found for **{target}**")
 
 
 
 
 
246
  st.stop()
247
 
248
- # ── Row-group fetch ────────────────────────────────────────────────────────────
 
249
  fetch_tasks = []
250
  for src_col, src_df in [("mobile1", m1_df), ("mobile2", m2_df)]:
251
  if src_df.empty:
252
  continue
253
- for _, rg in src_df[["filename", "row_group_id", "row_start", "row_end"]].drop_duplicates().iterrows():
254
  fetch_tasks.append({
255
  "filename": str(rg["filename"]),
256
  "row_start": int(rg["row_start"]),
@@ -258,33 +410,52 @@ for src_col, src_df in [("mobile1", m1_df), ("mobile2", m2_df)]:
258
  "search_col": src_col,
259
  })
260
 
261
- with st.spinner("Fetching records…"):
262
- t0 = time.perf_counter()
263
- all_dfs, errors = [], []
264
- with ThreadPoolExecutor(max_workers=min(8, len(fetch_tasks))) as ex:
265
- futures = {
266
- ex.submit(_fetch_one, t["filename"], t["row_start"], t["row_end"], t["search_col"], target): t
267
- for t in fetch_tasks
268
- }
269
- for f in as_completed(futures):
270
- try:
271
- df, _ = f.result()
272
- if not df.empty:
273
- all_dfs.append(df)
274
- except Exception as e:
275
- errors.append(str(e))
276
- fetch_ms = (time.perf_counter() - t0) * 1000
 
 
277
 
278
  for err in errors:
279
  st.error(err)
280
 
281
  if not all_dfs:
282
- st.warning("No matching rows found.")
 
 
 
 
283
  st.stop()
284
 
285
  combined = pd.concat(all_dfs, ignore_index=True).drop_duplicates()
286
 
287
- st.caption(f"**{len(combined)}** record(s) Β· {fmt_ms(idx_ms)} index Β· {fmt_ms(fetch_ms)} fetch")
 
 
 
 
 
 
 
 
 
 
 
 
 
288
 
289
  for i, (_, row) in enumerate(combined.iterrows()):
290
  show_card(row, i)
 
8
  # ── Config ─────────────────────────────────────────────────────────────────────
9
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
10
  HF_DATASET = "hf://datasets/styhero/styhero_dataset/data"
11
+ BASE_DIR = os.path.dirname(os.path.abspath(__file__))
12
+ _L1 = os.path.join(BASE_DIR, "mobile1_index.parquet")
13
+ _L2 = os.path.join(BASE_DIR, "mobile2_index.parquet")
14
+ MOBILE1_IDX = _L1 if os.path.exists(_L1) else "hf://datasets/styhero/styhero_dataset/data/mobile1_index.parquet"
15
+ MOBILE2_IDX = _L2 if os.path.exists(_L2) else "hf://datasets/styhero/styhero_dataset/data/mobile2_index.parquet"
 
 
16
 
17
  # ── Helpers ────────────────────────────────────────────────────────────────────
18
 
19
+ def fmt_ms(ms):
20
+ return f"{ms/1000:.2f}s" if ms >= 1000 else f"{ms:.0f}ms"
 
21
 
22
+ def clean(val):
23
  if val is None or (isinstance(val, float) and pd.isna(val)):
24
  return ""
25
  s = str(val).strip()
 
27
  return ""
28
  return ", ".join(p.strip() for p in s.split("!") if p.strip())
29
 
30
+ def sanitize_df(df):
 
31
  df = df.copy()
32
  for col in ["name", "fname", "email", "id", "address", "circle"]:
33
  if col in df.columns:
34
  df[col] = df[col].apply(clean)
35
  return df
36
 
 
37
  def new_con():
38
  con = duckdb.connect()
39
  con.execute("INSTALL httpfs; LOAD httpfs;")
40
  con.execute("CREATE SECRET IF NOT EXISTS hf_secret (TYPE HUGGINGFACE, TOKEN ?)", [HF_TOKEN])
41
+ con.execute("SET threads=4; SET enable_progress_bar=false;")
42
  return con
43
 
 
44
  # ── Queries ────────────────────────────────────────────────────────────────────
45
 
46
+ def _query_one(index_path, col, value):
47
  con = new_con()
48
+ t0 = time.perf_counter()
49
+ df = con.execute(f"""
50
  SELECT mobile1, mobile2, row_group_id, row_start, row_end, filename
51
  FROM read_parquet('{index_path}')
52
  WHERE {col} = '{value}'
 
54
  """).fetchdf()
55
  return df, (time.perf_counter() - t0) * 1000
56
 
57
+ def query_both_parallel(target):
 
58
  results = {}
59
  with ThreadPoolExecutor(max_workers=2) as ex:
60
  futures = {
 
66
  results[futures[f]] = (df, ms)
67
  return results
68
 
69
+ def _fetch_one(filename, row_start, row_end, search_col, target):
70
+ con = new_con()
 
71
  hf_path = f"{HF_DATASET}/{filename.split('/')[-1]}"
72
+ t0 = time.perf_counter()
73
+ df = con.execute(f"""
74
  SELECT mobile1, mobile2, email, id, name, fname, address, circle
75
+ FROM read_parquet('{hf_path}', file_row_number=true)
76
  WHERE file_row_number BETWEEN {row_start} AND {row_end}
77
  AND {search_col} = '{target}'
78
  """).fetchdf()
79
  return sanitize_df(df), (time.perf_counter() - t0) * 1000
80
 
81
+ # ── SVG icons ──────────────────────────────────────────────────────────────────
82
 
83
+ def ico(path, color="#8b8bff", size=14):
84
+ return f'<svg width="{size}" height="{size}" viewBox="0 0 24 24" fill="none" stroke="{color}" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" style="flex-shrink:0;margin-top:1px">{path}</svg>'
85
 
86
+ ICO_PHONE = ico('<rect x="5" y="2" width="14" height="20" rx="2"/><line x1="12" y1="18" x2="12.01" y2="18"/>')
87
+ ICO_EMAIL = ico('<rect x="2" y="4" width="20" height="16" rx="2"/><path d="m22 7-8.97 5.7a1.94 1.94 0 0 1-2.06 0L2 7"/>')
88
+ ICO_ID = ico('<rect x="2" y="7" width="20" height="14" rx="2"/><path d="M16 21V5a2 2 0 0 0-2-2h-4a2 2 0 0 0-2 2v16"/>')
89
+ ICO_PIN = ico('<path d="M20 10c0 6-8 12-8 12s-8-6-8-12a8 8 0 0 1 16 0Z"/><circle cx="12" cy="10" r="3"/>')
90
+ ICO_CIRCLE = ico('<circle cx="12" cy="12" r="10"/><path d="M12 8v4l3 3"/>')
91
+ ICO_PERSON = ico('<path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/><circle cx="12" cy="7" r="4"/>')
92
+
93
+ # ── Skeleton HTML ──────────────────────────────────────────────────────────────
94
+
95
+ SKELETON_CSS = """
96
+ <style>
97
+ @keyframes shimmer {
98
+ 0% { background-position: -600px 0 }
99
+ 100% { background-position: 600px 0 }
100
+ }
101
+ .sk {
102
+ border-radius: 6px;
103
+ background: linear-gradient(90deg,rgba(255,255,255,.05) 25%,rgba(255,255,255,.12) 50%,rgba(255,255,255,.05) 75%);
104
+ background-size: 1200px 100%;
105
+ animation: shimmer 1.5s infinite linear;
106
+ }
107
+ .sk-card {
108
+ background: rgba(255,255,255,.05);
109
+ border: 1px solid rgba(255,255,255,.1);
110
+ border-radius: 16px;
111
+ padding: 20px;
112
+ margin-bottom: 14px;
113
+ }
114
+ .sk-row { display:flex; gap:12px; align-items:center; margin-bottom:10px; }
115
+ .sk-circle { width:36px;height:36px;border-radius:50%; flex-shrink:0; }
116
+ .sk-line { height:14px; }
117
+ .sk-grid { display:grid; grid-template-columns:1fr 1fr; gap:12px; margin-top:14px; }
118
+ .sk-field { display:flex; flex-direction:column; gap:5px; }
119
+ .sk-label { height:9px; width:40%; }
120
+ .sk-value { height:13px; width:75%; }
121
+ </style>
122
+ """
123
+
124
+ def skeleton_card():
125
+ return """
126
+ <div class="sk-card">
127
+ <div class="sk-row">
128
+ <div class="sk sk-circle"></div>
129
+ <div style="flex:1;display:flex;flex-direction:column;gap:6px">
130
+ <div class="sk sk-line" style="width:55%"></div>
131
+ <div class="sk sk-line" style="width:35%;height:10px"></div>
132
+ </div>
133
+ </div>
134
+ <div class="sk-grid">
135
+ <div class="sk-field"><div class="sk sk-label"></div><div class="sk sk-value"></div></div>
136
+ <div class="sk-field"><div class="sk sk-label"></div><div class="sk sk-value"></div></div>
137
+ <div class="sk-field"><div class="sk sk-label"></div><div class="sk sk-value" style="width:85%"></div></div>
138
+ <div class="sk-field"><div class="sk sk-label"></div><div class="sk sk-value" style="width:60%"></div></div>
139
+ <div class="sk-field" style="grid-column:1/-1"><div class="sk sk-label"></div><div class="sk sk-value" style="width:90%"></div></div>
140
+ </div>
141
+ </div>"""
142
+
143
+ SKELETONS_HTML = SKELETON_CSS + "".join(skeleton_card() for _ in range(3))
144
+
145
+ # ── Result card ────────────────────────────────────────────────────────────────
146
+
147
+ def field_html(ico_svg, label, value, full=False):
148
+ if not value:
149
+ return ""
150
+ span = 'style="grid-column:1/-1"' if full else ""
151
+ return f"""
152
+ <div class="c-field" {span}>
153
+ <div class="c-label">{ico_svg}<span>{label}</span></div>
154
+ <div class="c-value">{value}</div>
155
+ </div>"""
156
+
157
+ def show_card(row, idx):
158
  name = clean(row.get("name")) or "Unknown"
159
  fname = clean(row.get("fname")) or ""
160
  mobile1 = clean(row.get("mobile1")) or ""
 
166
 
167
  copy_text = "\n".join(filter(None, [
168
  f"Name: {name}",
169
+ f"Father: {fname}" if fname else "",
170
  f"Mobile1: {mobile1}" if mobile1 else "",
171
  f"Mobile2: {mobile2}" if mobile2 else "",
172
  f"Email: {email}" if email else "",
 
174
  f"Address: {address}" if address else "",
175
  f"Circle: {circle}" if circle else "",
176
  ]))
177
+ copy_safe = copy_text.replace("\\","\\\\").replace("`","\\`").replace("${","\\${")
178
 
179
+ initials = (name[0] if name and name != "Unknown" else "?").upper()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
180
 
181
+ card = f"""<!DOCTYPE html><html><head><meta charset="utf-8">
182
+ <style>
183
+ *{{box-sizing:border-box;margin:0;padding:0}}
184
+ body{{background:transparent;font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",sans-serif}}
185
+ .card{{
186
+ background:rgba(255,255,255,.07);
187
+ backdrop-filter:blur(20px);
188
+ -webkit-backdrop-filter:blur(20px);
189
+ border:1px solid rgba(255,255,255,.13);
190
+ border-radius:16px;
191
+ padding:18px;
192
+ position:relative;
193
+ }}
194
+ .copy-btn{{
195
+ position:absolute;top:14px;right:14px;
196
+ background:rgba(124,111,255,.15);
197
+ color:#a0a0ff;
198
+ border:1px solid rgba(124,111,255,.3);
199
+ border-radius:8px;padding:5px 14px;
200
+ font-size:.76rem;font-weight:600;cursor:pointer;
201
+ transition:background .15s,color .15s;
202
+ -webkit-tap-highlight-color:transparent;
203
+ }}
204
+ .copy-btn:hover,.copy-btn:active{{background:linear-gradient(135deg,#7c6fff,#c084fc);color:#fff;border-color:transparent}}
205
+ .header{{display:flex;align-items:center;gap:12px;padding-right:90px;margin-bottom:14px}}
206
+ .avatar{{
207
+ width:40px;height:40px;border-radius:50%;flex-shrink:0;
208
+ background:linear-gradient(135deg,#7c6fff,#c084fc);
209
+ display:flex;align-items:center;justify-content:center;
210
+ font-size:1.1rem;font-weight:700;color:#fff;
211
+ }}
212
+ .c-name{{font-size:1.05rem;font-weight:700;color:#f0f0ff}}
213
+ .c-fname{{font-size:.78rem;color:rgba(200,200,255,.45);margin-top:2px}}
214
+ .c-grid{{
215
+ display:grid;
216
+ grid-template-columns:repeat(auto-fit,minmax(170px,1fr));
217
+ gap:10px 18px;
218
+ }}
219
+ .c-field{{display:flex;flex-direction:column;gap:3px}}
220
+ .c-label{{
221
+ display:flex;align-items:center;gap:4px;
222
+ font-size:.62rem;text-transform:uppercase;letter-spacing:.09em;
223
+ color:rgba(200,200,255,.4);
224
+ }}
225
+ .c-value{{font-size:.87rem;color:rgba(240,240,255,.82);word-break:break-word}}
226
+ </style></head><body>
227
+ <div class="card">
228
+ <button class="copy-btn" id="cb{idx}" onclick="
229
+ navigator.clipboard.writeText(`{copy_safe}`)
230
+ .then(()=>{{document.getElementById('cb{idx}').textContent='βœ“ Copied';
231
+ setTimeout(()=>document.getElementById('cb{idx}').textContent='Copy',1500)}})
232
+ .catch(()=>document.getElementById('cb{idx}').textContent='Failed')
233
+ ">Copy</button>
234
+
235
+ <div class="header">
236
+ <div class="avatar">{initials}</div>
237
+ <div>
238
  <div class="c-name">{name}</div>
239
+ {"<div class='c-fname'>S/O: " + fname + "</div>" if fname else ""}
 
 
 
 
 
 
 
 
 
240
  </div>
241
+ </div>
242
+
243
+ <div class="c-grid">
244
+ {field_html(ICO_PHONE, "Mobile 1", mobile1)}
245
+ {field_html(ICO_PHONE, "Mobile 2", mobile2)}
246
+ {field_html(ICO_EMAIL, "Email", email)}
247
+ {field_html(ICO_ID, "ID", rid)}
248
+ {field_html(ICO_CIRCLE,"Circle", circle)}
249
+ {field_html(ICO_PIN, "Address", address, full=True)}
250
+ </div>
251
+ </div>
252
+ <script>
253
+ const resize=()=>{{
254
+ const h=document.querySelector('.card').getBoundingClientRect().height;
255
+ window.parent.document.querySelectorAll('iframe').forEach(f=>{{
256
+ if(f.contentWindow===window) f.style.height=(h+6)+'px';
257
+ }});
258
+ }};
259
+ window.addEventListener('load',resize);
260
+ window.addEventListener('resize',resize);
261
+ </script>
262
+ </body></html>"""
263
+
264
+ st.components.v1.html(card, height=220, scrolling=False)
265
+
266
+ # ── Global CSS ─────────────────────────────────────────────────────────────────
267
+
268
+ st.set_page_config(page_title="Mobile Lookup", layout="centered")
269
 
270
  st.markdown("""
271
  <style>
272
+ /* Background */
273
+ .stApp {
274
+ background: linear-gradient(140deg,#0f0c29 0%,#24243e 50%,#302b63 100%) !important;
275
+ background-attachment: fixed !important;
276
+ }
277
+ /* Hide default chrome */
278
+ #MainMenu, footer, header { visibility: hidden; }
279
+ .block-container { padding: 2rem 1.5rem 3rem !important; max-width: 720px !important; }
280
+
281
+ /* Form glass panel */
282
+ [data-testid="stForm"] {
283
+ background: rgba(255,255,255,.07) !important;
284
+ backdrop-filter: blur(20px) !important;
285
+ -webkit-backdrop-filter: blur(20px) !important;
286
+ border: 1px solid rgba(255,255,255,.13) !important;
287
+ border-radius: 16px !important;
288
+ padding: 1.25rem !important;
289
+ box-shadow: 0 8px 32px rgba(0,0,0,.35) !important;
290
+ }
291
+
292
+ /* Input */
293
+ .stTextInput input {
294
+ background: rgba(255,255,255,.05) !important;
295
+ border: 1px solid rgba(255,255,255,.15) !important;
296
+ border-radius: 10px !important;
297
+ color: #f0f0ff !important;
298
+ font-size: 1rem !important;
299
+ padding: 0.6rem 1rem !important;
300
+ }
301
+ .stTextInput input:focus {
302
+ border-color: #7c6fff !important;
303
+ box-shadow: 0 0 0 2px rgba(124,111,255,.2) !important;
304
+ }
305
+ .stTextInput input::placeholder { color: rgba(200,200,255,.35) !important; }
306
+ .stTextInput label { display: none !important; }
307
+
308
+ /* Submit button */
309
+ .stFormSubmitButton button {
310
+ background: linear-gradient(135deg,#7c6fff,#c084fc) !important;
311
+ color: #fff !important;
312
+ border: none !important;
313
+ border-radius: 10px !important;
314
+ font-weight: 600 !important;
315
+ font-size: .95rem !important;
316
+ padding: .55rem 0 !important;
317
+ width: 100% !important;
318
+ transition: opacity .15s !important;
319
+ }
320
+ .stFormSubmitButton button:hover { opacity: .85 !important; }
321
+
322
+ /* Status messages */
323
+ .stAlert {
324
+ background: rgba(255,255,255,.06) !important;
325
+ border-radius: 10px !important;
326
+ backdrop-filter: blur(12px) !important;
327
+ }
328
+
329
+ /* Caption */
330
+ .stCaption { color: rgba(200,200,255,.4) !important; font-size: .78rem !important; }
331
+
332
+ /* Spinner */
333
+ .stSpinner > div { border-top-color: #7c6fff !important; }
334
  </style>
335
  """, unsafe_allow_html=True)
336
 
337
+ # ── Header ──────────────────────────────────────────────────────────────────────
338
+
339
+ st.markdown("""
340
+ <div style="display:flex;align-items:center;gap:12px;margin-bottom:1.5rem">
341
+ <div style="
342
+ width:42px;height:42px;border-radius:12px;flex-shrink:0;
343
+ background:linear-gradient(135deg,#7c6fff,#c084fc);
344
+ display:flex;align-items:center;justify-content:center;
345
+ ">
346
+ <svg width="22" height="22" viewBox="0 0 24 24" fill="none" stroke="#fff"
347
+ stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
348
+ <rect x="5" y="2" width="14" height="20" rx="2"/>
349
+ <line x1="12" y1="18" x2="12.01" y2="18"/>
350
+ </svg>
351
+ </div>
352
+ <div>
353
+ <div style="font-size:1.4rem;font-weight:800;
354
+ background:linear-gradient(90deg,#7c6fff,#c084fc);
355
+ -webkit-background-clip:text;-webkit-text-fill-color:transparent;
356
+ background-clip:text;line-height:1.2">Mobile Lookup</div>
357
+ <div style="font-size:.75rem;color:rgba(200,200,255,.4);margin-top:2px">
358
+ Search across HuggingFace parquet via DuckDB
359
+ </div>
360
+ </div>
361
+ </div>
362
+ """, unsafe_allow_html=True)
363
+
364
+ # ── Search form ─────────────────────────────────────────────────────────────────
365
 
366
  with st.form("search_form"):
367
+ number = st.text_input("", placeholder="Enter mobile number β€” e.g. 919876543210",
368
+ label_visibility="collapsed")
 
 
369
  submitted = st.form_submit_button("Search", use_container_width=True, type="primary")
370
 
371
  if not submitted or not number.strip():
 
373
 
374
  target = number.strip()
375
 
376
+ # ── Skeleton while loading ──────────────────────────────────────────────────────
377
+
378
+ sk_placeholder = st.empty()
379
+ sk_placeholder.markdown(SKELETONS_HTML, unsafe_allow_html=True)
380
+
381
+ # ── Index lookup ────────────────────────────────────────────────────────────────
382
+
383
+ t0 = time.perf_counter()
384
+ idx_results = query_both_parallel(target)
385
+ idx_ms = (time.perf_counter() - t0) * 1000
386
 
387
  m1_df, m1_ms = idx_results["mobile1"]
388
  m2_df, m2_ms = idx_results["mobile2"]
389
 
390
  if m1_df.empty and m2_df.empty:
391
+ sk_placeholder.empty()
392
+ st.markdown("""
393
+ <div style="background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25);
394
+ border-radius:10px;padding:12px 16px;color:#fbbf24;font-size:.9rem">
395
+ No records found for <strong>{}</strong>
396
+ </div>""".format(target), unsafe_allow_html=True)
397
  st.stop()
398
 
399
+ # ── Row-group fetch ─────────────────────────────────────────────────────────────
400
+
401
  fetch_tasks = []
402
  for src_col, src_df in [("mobile1", m1_df), ("mobile2", m2_df)]:
403
  if src_df.empty:
404
  continue
405
+ for _, rg in src_df[["filename","row_group_id","row_start","row_end"]].drop_duplicates().iterrows():
406
  fetch_tasks.append({
407
  "filename": str(rg["filename"]),
408
  "row_start": int(rg["row_start"]),
 
410
  "search_col": src_col,
411
  })
412
 
413
+ all_dfs, errors = [], []
414
+ with ThreadPoolExecutor(max_workers=min(8, len(fetch_tasks))) as ex:
415
+ futures = {
416
+ ex.submit(_fetch_one, t["filename"], t["row_start"], t["row_end"], t["search_col"], target): t
417
+ for t in fetch_tasks
418
+ }
419
+ for f in as_completed(futures):
420
+ try:
421
+ df, _ = f.result()
422
+ if not df.empty:
423
+ all_dfs.append(df)
424
+ except Exception as e:
425
+ errors.append(str(e))
426
+
427
+ fetch_ms = (time.perf_counter() - t0) * 1000
428
+
429
+ # Clear skeleton
430
+ sk_placeholder.empty()
431
 
432
  for err in errors:
433
  st.error(err)
434
 
435
  if not all_dfs:
436
+ st.markdown("""
437
+ <div style="background:rgba(251,191,36,.08);border:1px solid rgba(251,191,36,.25);
438
+ border-radius:10px;padding:12px 16px;color:#fbbf24;font-size:.9rem">
439
+ No matching rows found in remote parquet.
440
+ </div>""", unsafe_allow_html=True)
441
  st.stop()
442
 
443
  combined = pd.concat(all_dfs, ignore_index=True).drop_duplicates()
444
 
445
+ # ── Results ─────────────────────────────────────────────────────────────────────
446
+
447
+ st.markdown(f"""
448
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:.75rem">
449
+ <div style="
450
+ background:rgba(74,222,128,.12);border:1px solid rgba(74,222,128,.25);
451
+ border-radius:20px;padding:4px 14px;
452
+ color:#4ade80;font-size:.8rem;font-weight:600
453
+ ">{len(combined)} record(s)</div>
454
+ <div style="color:rgba(200,200,255,.35);font-size:.75rem">
455
+ {fmt_ms(idx_ms)} index Β· {fmt_ms(fetch_ms)} total
456
+ </div>
457
+ </div>
458
+ """, unsafe_allow_html=True)
459
 
460
  for i, (_, row) in enumerate(combined.iterrows()):
461
  show_card(row, i)