Spaces:
Sleeping
Sleeping
Upload app.py with huggingface_hub
Browse files
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
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 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
|
| 22 |
-
return f"{ms
|
| 23 |
-
|
| 24 |
|
| 25 |
-
def clean(val)
|
| 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
|
| 47 |
return con
|
| 48 |
|
| 49 |
-
|
| 50 |
# ββ Queries ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
| 51 |
|
| 52 |
-
def _query_one(index_path
|
| 53 |
con = new_con()
|
| 54 |
-
t0
|
| 55 |
-
df
|
| 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 |
-
|
| 78 |
-
con = new_con()
|
| 79 |
hf_path = f"{HF_DATASET}/{filename.split('/')[-1]}"
|
| 80 |
-
t0
|
| 81 |
-
df
|
| 82 |
SELECT mobile1, mobile2, email, id, name, fname, address, circle
|
| 83 |
-
FROM read_parquet('{hf_path}', file_row_number
|
| 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 |
-
|
|
|
|
| 91 |
|
| 92 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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}"
|
| 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 |
-
|
| 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=
|
| 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 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
|
| 203 |
-
|
| 204 |
-
|
| 205 |
-
|
| 206 |
-
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 211 |
|
| 212 |
st.markdown("""
|
| 213 |
<style>
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
</style>
|
| 219 |
""", unsafe_allow_html=True)
|
| 220 |
|
| 221 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 222 |
|
| 223 |
with st.form("search_form"):
|
| 224 |
-
number = st.text_input(
|
| 225 |
-
|
| 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 |
-
# ββ
|
| 236 |
-
|
| 237 |
-
|
| 238 |
-
|
| 239 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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",
|
| 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 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
| 271 |
-
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
|
|
|
|
|
|
| 277 |
|
| 278 |
for err in errors:
|
| 279 |
st.error(err)
|
| 280 |
|
| 281 |
if not all_dfs:
|
| 282 |
-
st.
|
|
|
|
|
|
|
|
|
|
|
|
|
| 283 |
st.stop()
|
| 284 |
|
| 285 |
combined = pd.concat(all_dfs, ignore_index=True).drop_duplicates()
|
| 286 |
|
| 287 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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)
|