Spaces:
Running
Running
update to lean_finder_v1_updated endpoint, UI change
Browse files- .gitignore +6 -0
- README.md +5 -0
- app.py +1176 -950
- requirements.txt +2 -1
.gitignore
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
.env
|
| 2 |
+
__pycache__/
|
| 3 |
+
*.pyc
|
| 4 |
+
.venv/
|
| 5 |
+
venv/
|
| 6 |
+
.DS_Store
|
README.md
CHANGED
|
@@ -14,3 +14,8 @@ short_description: Code search for Lean 4
|
|
| 14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 15 |
|
| 16 |
arxiv.org/abs/2510.15940
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
| 15 |
|
| 16 |
arxiv.org/abs/2510.15940
|
| 17 |
+
|
| 18 |
+
## Acknowledgements
|
| 19 |
+
|
| 20 |
+
Lean Finder is built on open-source projects like [Jixia](https://github.com/frenzymath/jixia) and [LeanDojo](https://github.com/lean-dojo/leandojo). We were inspired by [LeanSearch](https://leansearch.net/), and we use LeanSearch in our Arena mode to let users compare results. Thanks to the creators of these awesome tools!
|
| 21 |
+
|
app.py
CHANGED
|
@@ -1,1186 +1,1412 @@
|
|
| 1 |
-
import os, json, datetime, threading, requests, random,
|
| 2 |
from typing import List, Dict, Any
|
|
|
|
| 3 |
import gradio as gr
|
| 4 |
import gspread
|
| 5 |
from google.oauth2.service_account import Credentials
|
| 6 |
from gspread.exceptions import WorksheetNotFound
|
| 7 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 8 |
|
| 9 |
ENDPOINT_ID_A = os.getenv("ENDPOINT_ID_A")
|
| 10 |
ENDPOINT_ID_B = os.getenv("ENDPOINT_ID_B")
|
| 11 |
-
HF_TOKEN
|
| 12 |
SERVICE_ACCOUNT_INFO = os.getenv("GCP_SERVICE_ACCOUNT_JSON")
|
|
|
|
| 13 |
SCOPES = [
|
| 14 |
"https://www.googleapis.com/auth/spreadsheets",
|
| 15 |
"https://www.googleapis.com/auth/drive",
|
| 16 |
]
|
| 17 |
|
| 18 |
-
credentials = Credentials.from_service_account_info(
|
| 19 |
-
json.loads(SERVICE_ACCOUNT_INFO), scopes=SCOPES
|
| 20 |
-
)
|
| 21 |
if not ENDPOINT_ID_A:
|
| 22 |
-
raise ValueError("
|
| 23 |
if not ENDPOINT_ID_B:
|
| 24 |
raise ValueError("ENDPOINT_ID_B is not set")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 27 |
headers = {"Accept": "application/json", "Content-Type": "application/json"}
|
| 28 |
if HF_TOKEN:
|
| 29 |
headers["Authorization"] = f"Bearer {HF_TOKEN}"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
try:
|
| 31 |
r = requests.post(ENDPOINT_ID_A, json=payload, headers=headers, timeout=60)
|
| 32 |
r.raise_for_status()
|
| 33 |
-
return r.json()
|
| 34 |
-
except requests.exceptions.RequestException
|
| 35 |
-
raise RuntimeError("
|
| 36 |
|
| 37 |
-
|
|
|
|
| 38 |
payload = {"query": [query], "num_results": str(k)}
|
| 39 |
try:
|
| 40 |
r = requests.post(ENDPOINT_ID_B, json=payload, timeout=60)
|
| 41 |
r.raise_for_status()
|
| 42 |
data = r.json()
|
| 43 |
return data[0] if isinstance(data, list) and data else []
|
| 44 |
-
except requests.exceptions.RequestException
|
| 45 |
-
raise RuntimeError("
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
|
| 47 |
-
gc = gspread.authorize(credentials)
|
| 48 |
-
worksheet = gc.open("arena_votes").sheet1
|
| 49 |
-
vote_worksheet = gc.open("arena_votes").worksheet("individual_votes")
|
| 50 |
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 52 |
|
| 53 |
-
VOTE_UI = ["Retriever A better", "Retriever B better", "Tie", "Both are bad"]
|
| 54 |
|
| 55 |
-
def _save_vote(choice: str, query: str,
|
| 56 |
-
|
| 57 |
-
|
| 58 |
if not choice:
|
| 59 |
-
return gr.update(value="
|
|
|
|
| 60 |
if choice == "Retriever A better":
|
| 61 |
-
|
| 62 |
elif choice == "Retriever B better":
|
| 63 |
-
|
| 64 |
else:
|
| 65 |
-
|
| 66 |
|
| 67 |
payload = {
|
| 68 |
-
"retriever_a":
|
| 69 |
-
"retriever_b":
|
| 70 |
-
"
|
|
|
|
| 71 |
}
|
| 72 |
row = [
|
| 73 |
-
datetime.datetime.
|
| 74 |
query,
|
| 75 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 76 |
json.dumps(payload, ensure_ascii=False),
|
| 77 |
-
"User Preference"
|
| 78 |
]
|
| 79 |
with SHEET_LOCK:
|
| 80 |
-
|
| 81 |
-
return gr.update(value="**Vote recorded β thanks!**", visible=True)
|
|
|
|
| 82 |
|
| 83 |
-
def _save_individual_vote(
|
|
|
|
|
|
|
| 84 |
row = [
|
| 85 |
-
datetime.datetime.
|
| 86 |
query,
|
| 87 |
-
|
|
|
|
| 88 |
vote_decision,
|
| 89 |
system,
|
| 90 |
rank,
|
| 91 |
-
json.dumps(payload, ensure_ascii=False)
|
| 92 |
]
|
| 93 |
try:
|
| 94 |
with SHEET_LOCK:
|
| 95 |
-
|
| 96 |
return "Vote recorded!"
|
| 97 |
except Exception as e:
|
| 98 |
-
return f"Error writing to Google Sheets: {
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
| 102 |
return ""
|
| 103 |
-
|
| 104 |
-
text = re.sub(r'`([^`]+)`', r'<code>\1</code>', text)
|
| 105 |
-
return text
|
| 106 |
-
|
| 107 |
-
def _render_system1_results(res: List[Dict[str, Any]], title: str = "Retriever A", query: str = ""):
|
| 108 |
-
if not res:
|
| 109 |
-
return f"<p>No results from {title}.</p>"
|
| 110 |
-
|
| 111 |
-
def make_row(i: int, r: Dict[str, Any]) -> str:
|
| 112 |
-
formal_text = r['formal_statement']
|
| 113 |
-
informal_text = r['informal_statement']
|
| 114 |
-
|
| 115 |
-
formal_escaped = html.escape(formal_text)
|
| 116 |
-
doc_url = r.get('url', '')
|
| 117 |
-
doc_button = f'<button class="doc-button" onclick="window.open(\'{doc_url}\', \'_blank\')" title="View documentation">Doc</button>' if doc_url else ''
|
| 118 |
-
|
| 119 |
-
formal_cell = (
|
| 120 |
-
f'<div class="copy-container">'
|
| 121 |
-
f'<code style="white-space:pre-wrap">{formal_escaped}</code>'
|
| 122 |
-
f'<div class="button-container">'
|
| 123 |
-
f'<div class="left-buttons">'
|
| 124 |
-
f'{doc_button}'
|
| 125 |
-
f'<button class="copy-button" data-copy-text="{formal_escaped}" onclick="copyToClipboard(this.getAttribute(\'data-copy-text\'), this)">Copy</button>'
|
| 126 |
-
f'</div>'
|
| 127 |
-
f'<div class="right-buttons">'
|
| 128 |
-
f'<button class="vote-button upvote" data-formal="{formal_escaped}" data-query="{html.escape(query)}" data-system="System1" data-rank="{i}" onclick="voteOnResultSafe(this, \'Upvote\')">π</button>'
|
| 129 |
-
f'<button class="vote-button downvote" data-formal="{formal_escaped}" data-query="{html.escape(query)}" data-system="System1" data-rank="{i}" onclick="voteOnResultSafe(this, \'Downvote\')">π</button>'
|
| 130 |
-
f'</div>'
|
| 131 |
-
f'</div>'
|
| 132 |
-
f'</div>'
|
| 133 |
-
)
|
| 134 |
-
|
| 135 |
-
informal_cell = (
|
| 136 |
-
f'<div class="copy-container">'
|
| 137 |
-
f'<span style="white-space:pre-wrap">{_process_informal_statement(informal_text)}</span>'
|
| 138 |
-
f'<div class="button-container">'
|
| 139 |
-
f'<button class="copy-button" data-copy-text="{informal_text}" onclick="copyToClipboard(this.getAttribute(\'data-copy-text\'), this)">Copy</button>'
|
| 140 |
-
f'</div>'
|
| 141 |
-
f'</div>'
|
| 142 |
-
)
|
| 143 |
-
|
| 144 |
-
return f"<tr><td>{i}</td><td>{formal_cell}</td><td>{informal_cell}</td></tr>"
|
| 145 |
-
|
| 146 |
-
rows = "\n".join(make_row(i, r) for i, r in enumerate(res, 1))
|
| 147 |
return (
|
| 148 |
-
f
|
| 149 |
-
"
|
| 150 |
-
"<th>Formal statement</th><th>Informal statement</th></tr></thead>"
|
| 151 |
-
f"<tbody>{rows}</tbody></table>"
|
| 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 |
-
f'<
|
| 179 |
-
f'
|
| 180 |
-
f'
|
| 181 |
-
f'</div>'
|
| 182 |
-
f'</div>'
|
| 183 |
-
f'</div>'
|
| 184 |
)
|
| 185 |
-
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 192 |
f'</div>'
|
| 193 |
)
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
return (
|
| 199 |
-
f
|
| 200 |
-
|
| 201 |
-
|
| 202 |
-
f
|
| 203 |
)
|
| 204 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 205 |
INSTRUCTIONS_MD = """
|
| 206 |
-
##
|
| 207 |
-
|
| 208 |
-
|
| 209 |
-
|
| 210 |
-
|
| 211 |
-
|
| 212 |
-
|
| 213 |
-
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
**
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
```
|
| 221 |
-
|
| 222 |
-
|
| 223 |
-
Enter the proof state of a theorem. For better results, enter a proof state followed by how you want to transform the proof state.\n
|
| 224 |
-
**Example:**
|
| 225 |
-
```
|
| 226 |
-
K : Type u_1\nE : Type u_2\ninstβ : RCLike K\nzβ z : K\nβ’ |re z| β€ βzβ\nTransform the goal from proving that the absolute value of the real part of a complex number is less than or equal to its norm, to proving that the square of the real part is less than or equal to the squared norm of the number.
|
| 227 |
-
```
|
| 228 |
-
|
| 229 |
-
### 4. Statement definition
|
| 230 |
-
Enter any fragment or the whole statement definition, and find statements that match the entered content. Note that the query syntax doesn't need to be perfect.\n
|
| 231 |
-
**Example:**
|
| 232 |
-
```
|
| 233 |
-
theorem restrict Ioi: restrict Ioi e = restrict Ici e
|
| 234 |
-
```
|
| 235 |
"""
|
| 236 |
|
| 237 |
-
|
| 238 |
CUSTOM_CSS = """
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 242 |
}
|
| 243 |
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 249 |
|
| 250 |
-
|
| 251 |
-
|
| 252 |
-
|
| 253 |
-
|
| 254 |
-
padding:
|
| 255 |
-
font-size:0.9em;
|
| 256 |
-
font-family:Monaco, Consolas, "Courier New", monospace;
|
| 257 |
}
|
| 258 |
|
| 259 |
-
/*
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 260 |
@media (prefers-color-scheme: dark) {
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
background:#333;
|
| 268 |
-
border:1px solid #555;
|
| 269 |
-
color:#e0e0e0;
|
| 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 |
-
justify-content:center !important;
|
| 316 |
-
gap:1rem !important;
|
| 317 |
-
width:100% !important;
|
| 318 |
-
margin:1rem auto !important;
|
| 319 |
-
padding:0 !important;
|
| 320 |
}
|
|
|
|
| 321 |
|
| 322 |
-
/*
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
| 326 |
-
|
| 327 |
-
|
| 328 |
-
|
| 329 |
-
|
| 330 |
-
|
| 331 |
-
|
|
|
|
| 332 |
}
|
| 333 |
|
| 334 |
-
|
| 335 |
-
width:60px !important;
|
| 336 |
-
height:60px !important;
|
| 337 |
-
flex:0 0 60px !important;
|
| 338 |
-
overflow:hidden;
|
| 339 |
-
}
|
| 340 |
|
| 341 |
-
|
| 342 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 343 |
}
|
| 344 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 345 |
|
| 346 |
-
|
| 347 |
-
|
| 348 |
-
|
| 349 |
-
|
| 350 |
-
|
| 351 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 352 |
}
|
| 353 |
|
| 354 |
-
.
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
line-height:1.
|
| 362 |
-
text-align:center !important;
|
| 363 |
}
|
| 364 |
|
| 365 |
-
.
|
| 366 |
-
|
|
|
|
|
|
|
| 367 |
font-weight: 600;
|
| 368 |
-
|
| 369 |
-
padding: 0
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
min-height: 1.5em;
|
| 400 |
}
|
| 401 |
|
| 402 |
-
.
|
| 403 |
-
|
| 404 |
-
|
| 405 |
-
|
| 406 |
-
|
| 407 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 408 |
cursor: pointer;
|
| 409 |
-
transition:
|
| 410 |
-
color: #666;
|
| 411 |
-
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
| 412 |
-
line-height: 1.2;
|
| 413 |
white-space: nowrap;
|
| 414 |
-
|
| 415 |
-
|
|
|
|
| 416 |
}
|
| 417 |
-
|
| 418 |
-
|
| 419 |
-
|
|
|
|
| 420 |
}
|
| 421 |
|
| 422 |
-
.
|
| 423 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 424 |
}
|
| 425 |
|
| 426 |
-
|
| 427 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 428 |
}
|
| 429 |
-
|
| 430 |
-
|
| 431 |
-
|
| 432 |
-
|
| 433 |
-
|
| 434 |
-
|
| 435 |
-
|
| 436 |
-
|
| 437 |
-
|
|
|
|
|
|
|
|
|
|
| 438 |
}
|
| 439 |
|
| 440 |
-
|
| 441 |
-
|
|
|
|
|
|
|
|
|
|
| 442 |
}
|
| 443 |
|
| 444 |
-
/*
|
| 445 |
-
|
| 446 |
-
|
| 447 |
-
|
| 448 |
-
max-height: 400px !important;
|
| 449 |
}
|
| 450 |
-
|
| 451 |
-
|
| 452 |
-
|
| 453 |
-
|
| 454 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 455 |
}
|
| 456 |
-
|
| 457 |
-
|
| 458 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 459 |
background: transparent;
|
| 460 |
-
border:
|
| 461 |
-
|
| 462 |
-
|
| 463 |
-
|
| 464 |
-
|
| 465 |
-
|
| 466 |
-
transition: all 0.2s ease;
|
| 467 |
-
opacity: 0.6;
|
| 468 |
-
vertical-align: baseline;
|
| 469 |
-
box-sizing: border-box;
|
| 470 |
}
|
| 471 |
|
| 472 |
-
.
|
| 473 |
-
|
| 474 |
-
|
|
|
|
|
|
|
| 475 |
}
|
| 476 |
-
|
| 477 |
-
|
| 478 |
-
|
| 479 |
-
|
|
|
|
|
|
|
|
|
|
| 480 |
}
|
| 481 |
|
| 482 |
-
|
| 483 |
-
|
| 484 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 485 |
}
|
| 486 |
-
|
| 487 |
-
|
| 488 |
-
|
| 489 |
-
|
| 490 |
-
|
| 491 |
-
|
| 492 |
-
|
| 493 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
cursor: pointer;
|
| 495 |
-
|
| 496 |
-
|
| 497 |
-
font-
|
| 498 |
-
|
| 499 |
-
white-space: nowrap;
|
| 500 |
-
vertical-align: baseline;
|
| 501 |
-
box-sizing: border-box;
|
| 502 |
}
|
| 503 |
-
|
| 504 |
-
|
| 505 |
-
|
|
|
|
|
|
|
|
|
|
| 506 |
color: white;
|
|
|
|
| 507 |
}
|
| 508 |
|
| 509 |
-
/*
|
| 510 |
-
|
| 511 |
-
|
| 512 |
-
margin-top: 8px;
|
| 513 |
-
align-items: baseline;
|
| 514 |
-
justify-content: space-between;
|
| 515 |
}
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
|
| 519 |
-
|
| 520 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 521 |
}
|
| 522 |
-
|
| 523 |
-
|
| 524 |
-
|
| 525 |
-
gap: 8px;
|
| 526 |
-
align-items: baseline;
|
| 527 |
}
|
|
|
|
|
|
|
| 528 |
|
| 529 |
-
|
| 530 |
-
|
| 531 |
-
|
|
|
|
|
|
|
| 532 |
}
|
| 533 |
|
| 534 |
-
|
| 535 |
-
|
| 536 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 537 |
}
|
| 538 |
|
| 539 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 540 |
opacity: 1;
|
| 541 |
-
|
| 542 |
}
|
|
|
|
| 543 |
|
| 544 |
-
.vote-button.upvote.voted {
|
| 545 |
-
background: #28a745;
|
| 546 |
-
color: white;
|
| 547 |
-
}
|
| 548 |
|
| 549 |
-
|
| 550 |
-
|
| 551 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 552 |
}
|
| 553 |
-
|
| 554 |
-
|
| 555 |
-
|
| 556 |
-
|
| 557 |
-
|
| 558 |
-
|
| 559 |
-
|
| 560 |
-
|
| 561 |
-
|
| 562 |
-
|
| 563 |
-
|
| 564 |
-
|
| 565 |
-
|
| 566 |
-
|
| 567 |
-
|
| 568 |
-
|
| 569 |
-
|
| 570 |
-
}
|
| 571 |
-
|
| 572 |
-
/* Progress bar dark mode */
|
| 573 |
-
.gradio-container .progress-bar {
|
| 574 |
-
background: #2a2a2a !important;
|
| 575 |
-
border: 1px solid #555 !important;
|
| 576 |
-
}
|
| 577 |
-
|
| 578 |
-
.gradio-container .progress-bar .progress-text {
|
| 579 |
-
color: #e0e0e0 !important;
|
| 580 |
-
}
|
| 581 |
-
|
| 582 |
-
.gradio-container .progress-bar .progress-level {
|
| 583 |
-
background: #444 !important;
|
| 584 |
-
}
|
| 585 |
-
|
| 586 |
-
/* Copy button dark mode */
|
| 587 |
-
.copy-button {
|
| 588 |
-
background: rgba(255, 255, 255, 0.1);
|
| 589 |
-
color: #ccc;
|
| 590 |
-
border-color: #555;
|
| 591 |
-
}
|
| 592 |
-
|
| 593 |
-
.copy-button:hover {
|
| 594 |
-
background: rgba(255, 255, 255, 0.2);
|
| 595 |
-
color: #fff;
|
| 596 |
-
}
|
| 597 |
-
|
| 598 |
-
.copy-button:active {
|
| 599 |
-
background: rgba(255, 255, 255, 0.3);
|
| 600 |
-
color: #fff;
|
| 601 |
-
}
|
| 602 |
-
|
| 603 |
-
/* Query input dark mode */
|
| 604 |
-
#query_input textarea {
|
| 605 |
-
background: #2a2a2a !important;
|
| 606 |
-
color: #e0e0e0 !important;
|
| 607 |
-
}
|
| 608 |
-
|
| 609 |
-
#query_input textarea:focus {
|
| 610 |
-
border-color: #0066ff !important;
|
| 611 |
-
box-shadow: 0 0 0 2px rgba(0, 102, 255, 0.2) !important;
|
| 612 |
-
}
|
| 613 |
-
|
| 614 |
-
/* Vote buttons dark mode */
|
| 615 |
-
.vote-button {
|
| 616 |
-
border-color: #555 !important;
|
| 617 |
-
background: #2a2a2a !important;
|
| 618 |
-
}
|
| 619 |
-
|
| 620 |
-
.vote-button.upvote {
|
| 621 |
-
color: #4ade80 !important;
|
| 622 |
-
border-color: #4ade80 !important;
|
| 623 |
-
}
|
| 624 |
-
|
| 625 |
-
.vote-button.downvote {
|
| 626 |
-
color: #f87171 !important;
|
| 627 |
-
border-color: #f87171 !important;
|
| 628 |
-
}
|
| 629 |
-
|
| 630 |
-
/* Doc button dark mode */
|
| 631 |
-
.doc-button {
|
| 632 |
-
background: rgba(96, 165, 250, 0.1) !important;
|
| 633 |
-
border-color: #60a5fa !important;
|
| 634 |
-
color: #60a5fa !important;
|
| 635 |
-
}
|
| 636 |
-
|
| 637 |
-
.doc-button:hover {
|
| 638 |
-
background: #60a5fa !important;
|
| 639 |
-
color: #000 !important;
|
| 640 |
-
}
|
| 641 |
-
|
| 642 |
-
.vote-button.upvote:hover {
|
| 643 |
-
background: #4ade80 !important;
|
| 644 |
-
color: #000 !important;
|
| 645 |
}
|
| 646 |
-
|
| 647 |
-
|
| 648 |
-
|
| 649 |
-
|
| 650 |
-
|
| 651 |
-
|
| 652 |
-
|
| 653 |
-
|
| 654 |
-
|
| 655 |
-
|
| 656 |
-
|
| 657 |
-
|
| 658 |
-
|
| 659 |
-
|
| 660 |
-
|
| 661 |
-
|
| 662 |
-
/* Vote feedback styling */
|
| 663 |
-
#vote_feedback {
|
| 664 |
-
margin-top: 1rem;
|
| 665 |
-
text-align: center;
|
| 666 |
-
}
|
| 667 |
-
|
| 668 |
-
#vote_feedback .markdown {
|
| 669 |
-
background: #d4edda !important;
|
| 670 |
-
border: 1px solid #c3e6cb !important;
|
| 671 |
-
border-radius: 8px !important;
|
| 672 |
-
padding: 12px 16px !important;
|
| 673 |
-
color: #155724 !important;
|
| 674 |
-
font-weight: 500 !important;
|
| 675 |
-
margin: 0 !important;
|
| 676 |
-
transition: opacity 0.5s ease !important;
|
| 677 |
-
opacity: 1 !important;
|
| 678 |
-
}
|
| 679 |
-
|
| 680 |
-
/* Error styling for vote feedback */
|
| 681 |
-
#vote_feedback .markdown:has-text("ERROR"),
|
| 682 |
-
#vote_feedback .markdown[data-error="true"] {
|
| 683 |
-
background: #f8d7da !important;
|
| 684 |
-
border-color: #f5c6cb !important;
|
| 685 |
-
color: #721c24 !important;
|
| 686 |
}
|
| 687 |
-
|
| 688 |
-
|
| 689 |
-
|
| 690 |
-
|
| 691 |
-
|
| 692 |
-
|
| 693 |
-
|
| 694 |
-
|
| 695 |
-
|
| 696 |
-
|
| 697 |
-
|
| 698 |
-
|
| 699 |
-
|
| 700 |
-
|
| 701 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 702 |
}
|
| 703 |
}
|
| 704 |
}
|
| 705 |
-
"""
|
| 706 |
|
| 707 |
-
|
| 708 |
-
|
| 709 |
-
|
| 710 |
-
|
| 711 |
-
|
| 712 |
-
|
| 713 |
-
|
| 714 |
-
|
| 715 |
-
|
| 716 |
-
|
| 717 |
-
|
| 718 |
-
|
| 719 |
-
|
| 720 |
-
|
| 721 |
-
|
| 722 |
-
|
| 723 |
-
|
| 724 |
-
|
| 725 |
-
|
| 726 |
-
|
| 727 |
-
let mathExpressions = [];
|
| 728 |
-
|
| 729 |
-
// Find inline math $...$
|
| 730 |
-
let pos = 0;
|
| 731 |
-
while (pos < text.length) {
|
| 732 |
-
const start = text.indexOf('$', pos);
|
| 733 |
-
if (start === -1) break;
|
| 734 |
-
const end = text.indexOf('$', start + 1);
|
| 735 |
-
if (end === -1) break;
|
| 736 |
-
|
| 737 |
-
const isDisplayMath = (start > 0 && text[start - 1] === '$') ||
|
| 738 |
-
(end < text.length - 1 && text[end + 1] === '$');
|
| 739 |
-
|
| 740 |
-
if (!isDisplayMath) {
|
| 741 |
-
mathExpressions.push({
|
| 742 |
-
start: start,
|
| 743 |
-
end: end + 1,
|
| 744 |
-
content: text.substring(start + 1, end),
|
| 745 |
-
type: 'inline'
|
| 746 |
-
});
|
| 747 |
-
pos = end + 1;
|
| 748 |
-
} else {
|
| 749 |
-
pos = start + 1;
|
| 750 |
-
}
|
| 751 |
-
}
|
| 752 |
-
|
| 753 |
-
// Find display math $$...$$
|
| 754 |
-
pos = 0;
|
| 755 |
-
while (pos < text.length) {
|
| 756 |
-
const start = text.indexOf('$$', pos);
|
| 757 |
-
if (start === -1) break;
|
| 758 |
-
const end = text.indexOf('$$', start + 2);
|
| 759 |
-
if (end === -1) break;
|
| 760 |
-
|
| 761 |
-
mathExpressions.push({
|
| 762 |
-
start: start,
|
| 763 |
-
end: end + 2,
|
| 764 |
-
content: text.substring(start + 2, end),
|
| 765 |
-
type: 'display'
|
| 766 |
-
});
|
| 767 |
-
pos = end + 2;
|
| 768 |
-
}
|
| 769 |
-
|
| 770 |
-
mathExpressions.sort((a, b) => a.start - b.start);
|
| 771 |
-
|
| 772 |
-
// Remove overlapping expressions
|
| 773 |
-
let filtered = [];
|
| 774 |
-
for (let expr of mathExpressions) {
|
| 775 |
-
let shouldAdd = true;
|
| 776 |
-
for (let j = 0; j < filtered.length; j++) {
|
| 777 |
-
const existing = filtered[j];
|
| 778 |
-
if (!(expr.end <= existing.start || expr.start >= existing.end)) {
|
| 779 |
-
if (expr.type === 'display' && existing.type === 'inline') {
|
| 780 |
-
filtered.splice(j, 1);
|
| 781 |
-
j--;
|
| 782 |
-
} else {
|
| 783 |
-
shouldAdd = false;
|
| 784 |
-
break;
|
| 785 |
-
}
|
| 786 |
-
}
|
| 787 |
-
}
|
| 788 |
-
if (shouldAdd) filtered.push(expr);
|
| 789 |
-
}
|
| 790 |
-
|
| 791 |
-
if (filtered.length > 0) {
|
| 792 |
-
let newContent = document.createDocumentFragment();
|
| 793 |
-
let lastPos = 0;
|
| 794 |
-
|
| 795 |
-
filtered.forEach(function(expr) {
|
| 796 |
-
if (expr.start > lastPos) {
|
| 797 |
-
newContent.appendChild(document.createTextNode(text.substring(lastPos, expr.start)));
|
| 798 |
-
}
|
| 799 |
-
|
| 800 |
-
try {
|
| 801 |
-
const mathElement = document.createElement('span');
|
| 802 |
-
mathElement.innerHTML = katex.renderToString(expr.content, {
|
| 803 |
-
throwOnError: false,
|
| 804 |
-
displayMode: expr.type === 'display'
|
| 805 |
-
});
|
| 806 |
-
newContent.appendChild(mathElement);
|
| 807 |
-
} catch (e) {
|
| 808 |
-
const fallback = expr.type === 'display' ? '$$' + expr.content + '$$' : '$' + expr.content + '$';
|
| 809 |
-
newContent.appendChild(document.createTextNode(fallback));
|
| 810 |
-
}
|
| 811 |
-
|
| 812 |
-
lastPos = expr.end;
|
| 813 |
-
});
|
| 814 |
-
|
| 815 |
-
if (lastPos < text.length) {
|
| 816 |
-
newContent.appendChild(document.createTextNode(text.substring(lastPos)));
|
| 817 |
-
}
|
| 818 |
-
|
| 819 |
-
span.innerHTML = '';
|
| 820 |
-
span.appendChild(newContent);
|
| 821 |
}
|
| 822 |
}
|
| 823 |
-
});
|
| 824 |
-
}
|
| 825 |
-
|
| 826 |
-
setTimeout(renderKatex, 1000);
|
| 827 |
-
setTimeout(renderKatex, 3000);
|
| 828 |
-
setTimeout(renderKatex, 5000);
|
| 829 |
-
|
| 830 |
-
function copyToClipboard(text, button) {
|
| 831 |
-
if (navigator.clipboard && window.isSecureContext) {
|
| 832 |
-
navigator.clipboard.writeText(text).then(function() {
|
| 833 |
-
showCopySuccess(button);
|
| 834 |
-
}).catch(function() {
|
| 835 |
-
fallbackCopy(text, button);
|
| 836 |
-
});
|
| 837 |
-
} else {
|
| 838 |
-
fallbackCopy(text, button);
|
| 839 |
}
|
| 840 |
-
}
|
| 841 |
-
|
| 842 |
-
function
|
| 843 |
-
const
|
| 844 |
-
|
| 845 |
-
|
| 846 |
-
|
| 847 |
-
|
| 848 |
-
document.body.appendChild(textArea);
|
| 849 |
-
textArea.focus();
|
| 850 |
-
textArea.select();
|
| 851 |
-
|
| 852 |
-
try {
|
| 853 |
-
document.execCommand('copy');
|
| 854 |
-
showCopySuccess(button);
|
| 855 |
-
} catch (err) {
|
| 856 |
-
showCopyError(button);
|
| 857 |
}
|
| 858 |
-
|
| 859 |
-
document.body.removeChild(textArea);
|
| 860 |
-
}
|
| 861 |
-
|
| 862 |
-
function showCopySuccess(button) {
|
| 863 |
-
const originalText = button.innerHTML;
|
| 864 |
-
button.innerHTML = 'β';
|
| 865 |
-
button.style.color = '#28a745';
|
| 866 |
-
setTimeout(function() {
|
| 867 |
-
button.innerHTML = originalText;
|
| 868 |
-
button.style.color = '';
|
| 869 |
-
}, 1000);
|
| 870 |
-
}
|
| 871 |
-
|
| 872 |
-
function showCopyError(button) {
|
| 873 |
-
const originalText = button.innerHTML;
|
| 874 |
-
button.innerHTML = 'β';
|
| 875 |
-
button.style.color = '#dc3545';
|
| 876 |
-
setTimeout(function() {
|
| 877 |
-
button.innerHTML = originalText;
|
| 878 |
-
button.style.color = '';
|
| 879 |
-
}, 1000);
|
| 880 |
}
|
| 881 |
-
|
| 882 |
-
|
| 883 |
-
|
| 884 |
-
|
| 885 |
-
|
| 886 |
-
|
| 887 |
-
|
| 888 |
-
|
| 889 |
-
|
| 890 |
-
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
| 894 |
-
|
| 895 |
-
|
| 896 |
-
|
| 897 |
-
button.innerHTML = voteDecision === 'Upvote' ? 'πβ' : 'πβ';
|
| 898 |
-
setTimeout(function() {
|
| 899 |
-
button.innerHTML = originalText;
|
| 900 |
-
}, 2000);
|
| 901 |
-
|
| 902 |
-
const voteData = JSON.stringify({
|
| 903 |
-
formal_statement: formalStatement,
|
| 904 |
-
vote_decision: voteDecision,
|
| 905 |
-
query: query,
|
| 906 |
-
system: system,
|
| 907 |
-
rank: rank
|
| 908 |
-
});
|
| 909 |
-
|
| 910 |
-
const voteContainer = document.querySelector('#vote_trigger_input');
|
| 911 |
-
if (voteContainer) {
|
| 912 |
-
const input = voteContainer.querySelector('textarea') || voteContainer.querySelector('input[type="text"]');
|
| 913 |
-
if (input) {
|
| 914 |
-
input.value = voteData;
|
| 915 |
-
input.dispatchEvent(new Event('input', { bubbles: true, cancelable: true }));
|
| 916 |
-
input.dispatchEvent(new Event('change', { bubbles: true, cancelable: true }));
|
| 917 |
-
|
| 918 |
-
setTimeout(function() {
|
| 919 |
-
const feedbackElement = document.querySelector('#vote_feedback .markdown');
|
| 920 |
-
if (feedbackElement) {
|
| 921 |
-
if (feedbackElement.textContent.includes('ERROR')) {
|
| 922 |
-
feedbackElement.setAttribute('data-error', 'true');
|
| 923 |
-
} else {
|
| 924 |
-
feedbackElement.removeAttribute('data-error');
|
| 925 |
-
}
|
| 926 |
-
}
|
| 927 |
-
}, 100);
|
| 928 |
-
|
| 929 |
-
setTimeout(function() {
|
| 930 |
-
const feedbackElement = document.querySelector('#vote_feedback .markdown');
|
| 931 |
-
if (feedbackElement && feedbackElement.textContent.trim()) {
|
| 932 |
-
feedbackElement.style.opacity = '0';
|
| 933 |
-
setTimeout(function() {
|
| 934 |
-
const input = voteContainer.querySelector('textarea') || voteContainer.querySelector('input[type="text"]');
|
| 935 |
-
if (input) {
|
| 936 |
-
input.value = '';
|
| 937 |
-
input.dispatchEvent(new Event('input', { bubbles: true }));
|
| 938 |
-
}
|
| 939 |
-
}, 500);
|
| 940 |
-
}
|
| 941 |
-
}, 4000);
|
| 942 |
}
|
| 943 |
-
}
|
| 944 |
}
|
| 945 |
-
|
| 946 |
-
|
| 947 |
-
|
| 948 |
-
|
| 949 |
-
tempButton.setAttribute('data-query', query);
|
| 950 |
-
tempButton.setAttribute('data-system', system);
|
| 951 |
-
tempButton.setAttribute('data-rank', rank);
|
| 952 |
-
tempButton.className = button.className;
|
| 953 |
-
tempButton.parentElement = button.parentElement;
|
| 954 |
-
voteOnResultSafe(tempButton, voteDecision);
|
| 955 |
}
|
| 956 |
-
|
| 957 |
-
|
| 958 |
-
|
| 959 |
-
|
| 960 |
-
}
|
| 961 |
-
}, 3000);
|
| 962 |
-
</script>
|
| 963 |
-
"""
|
| 964 |
-
) as demo:
|
| 965 |
-
with gr.Column(elem_id="lf_header"):
|
| 966 |
-
gr.Image(
|
| 967 |
-
value="lean_finder_logo.png",
|
| 968 |
-
show_label=False,
|
| 969 |
-
interactive=False,
|
| 970 |
-
container=False,
|
| 971 |
-
show_download_button=False,
|
| 972 |
-
height=60,
|
| 973 |
-
elem_id="lf_logo",
|
| 974 |
-
)
|
| 975 |
-
gr.HTML('<h1 class="lf-title">Lean Finder: Semantic Search for Mathlib That Understands User Intents</h1>')
|
| 976 |
|
| 977 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 978 |
gr.Markdown(INSTRUCTIONS_MD)
|
| 979 |
|
| 980 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 981 |
|
|
|
|
| 982 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 983 |
|
| 984 |
-
with gr.Row():
|
| 985 |
-
query_box = gr.Textbox(label="Query", lines=4, max_lines=20,
|
| 986 |
-
placeholder="Type your query here β¦",
|
| 987 |
-
elem_id="query_input")
|
| 988 |
-
topk_slider = gr.Slider(label="Number of results", minimum=1, maximum=50, step=1, value=5)
|
| 989 |
with gr.Column():
|
| 990 |
-
|
| 991 |
-
mode_description = gr.Markdown("Arena mode: Compare retrieval results from Lean Finder with another retriever and vote.\n By voting, you agree that your responses may be used to help improve Lean Finder. We only collect the voting results; no identifiable information is stored.")
|
| 992 |
-
|
| 993 |
-
run_btn = gr.Button("Retrieve")
|
| 994 |
-
|
| 995 |
-
with gr.Row(elem_id="vote_area"):
|
| 996 |
-
with gr.Column(elem_id="vote_radio_col"):
|
| 997 |
vote_radio = gr.Radio(
|
| 998 |
VOTE_UI,
|
| 999 |
-
label="Which
|
| 1000 |
-
|
| 1001 |
-
elem_id="vote_radio"
|
| 1002 |
)
|
| 1003 |
submit_btn = gr.Button(
|
| 1004 |
"Submit vote",
|
| 1005 |
-
visible=False,
|
| 1006 |
elem_id="submit_btn",
|
| 1007 |
-
variant="primary"
|
| 1008 |
)
|
| 1009 |
vote_status = gr.Markdown("", visible=False, elem_id="vote_status")
|
| 1010 |
|
| 1011 |
-
results_html = gr.HTML(
|
| 1012 |
-
|
| 1013 |
-
|
| 1014 |
-
|
| 1015 |
-
|
| 1016 |
-
|
| 1017 |
-
|
| 1018 |
-
gap: 0.4rem;
|
| 1019 |
-
padding: 0.4rem 0.8rem;
|
| 1020 |
-
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 1021 |
-
color: white;
|
| 1022 |
-
text-decoration: none;
|
| 1023 |
-
border-radius: 20px;
|
| 1024 |
-
font-weight: 500;
|
| 1025 |
-
font-size: 0.85rem;
|
| 1026 |
-
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
|
| 1027 |
-
transition: all 0.3s ease;
|
| 1028 |
-
border: none;
|
| 1029 |
-
" onmouseover="this.style.transform='translateY(-1px)'; this.style.boxShadow='0 4px 12px rgba(102, 126, 234, 0.35)'"
|
| 1030 |
-
onmouseout="this.style.transform='translateY(0px)'; this.style.boxShadow='0 2px 8px rgba(102, 126, 234, 0.25)'">
|
| 1031 |
-
<span style="font-size: 1rem;">π</span>
|
| 1032 |
-
Read our paper for details about Lean Finder
|
| 1033 |
-
</a>
|
| 1034 |
-
</div>
|
| 1035 |
-
''', elem_id="paper_link")
|
| 1036 |
-
|
| 1037 |
-
# Hidden component for individual vote triggers
|
| 1038 |
-
vote_trigger = gr.Textbox(visible=False, elem_id="vote_trigger_input")
|
| 1039 |
vote_feedback = gr.Markdown("", visible=True, elem_id="vote_feedback")
|
| 1040 |
|
| 1041 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1042 |
st_query = gr.State("")
|
| 1043 |
-
|
| 1044 |
-
|
| 1045 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1046 |
|
| 1047 |
-
def retrieve(query: str, k: int, mode: str, progress=gr.Progress()):
|
| 1048 |
-
query = query.strip()
|
| 1049 |
if not query:
|
| 1050 |
-
|
| 1051 |
-
|
|
|
|
|
|
|
|
|
|
| 1052 |
|
|
|
|
| 1053 |
try:
|
| 1054 |
-
|
| 1055 |
except RuntimeError:
|
| 1056 |
-
|
| 1057 |
-
|
| 1058 |
-
|
| 1059 |
-
|
| 1060 |
-
|
| 1061 |
-
|
| 1062 |
-
|
| 1063 |
-
|
| 1064 |
-
|
| 1065 |
-
|
| 1066 |
-
|
| 1067 |
-
if elapsed_time - last_progress_update >= 10:
|
| 1068 |
-
if elapsed_time < progress_phase1_duration:
|
| 1069 |
-
progress(elapsed_time / progress_phase1_duration, desc="Please wait about 2 minutes. The Lean Finder service is starting up. Results will be displayed automatically once Lean Finder is ready.")
|
| 1070 |
-
else:
|
| 1071 |
-
progress(1.0, desc="Experiencing a slow start. Please wait a little longer β the content will be displayed once Lean Finder is ready.")
|
| 1072 |
-
last_progress_update = elapsed_time
|
| 1073 |
-
|
| 1074 |
-
time.sleep(1)
|
| 1075 |
try:
|
| 1076 |
-
|
| 1077 |
break
|
| 1078 |
except RuntimeError:
|
| 1079 |
-
|
| 1080 |
-
|
| 1081 |
-
|
| 1082 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1083 |
|
| 1084 |
if mode == "Normal":
|
| 1085 |
-
|
| 1086 |
-
|
| 1087 |
-
|
|
|
|
|
|
|
| 1088 |
|
|
|
|
| 1089 |
try:
|
| 1090 |
-
|
|
|
|
| 1091 |
except RuntimeError:
|
| 1092 |
-
|
| 1093 |
-
|
| 1094 |
-
|
| 1095 |
-
|
| 1096 |
-
|
| 1097 |
-
|
| 1098 |
-
|
| 1099 |
-
|
| 1100 |
-
|
| 1101 |
-
|
| 1102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1103 |
else:
|
| 1104 |
-
|
| 1105 |
-
|
| 1106 |
-
|
| 1107 |
-
|
| 1108 |
-
|
| 1109 |
-
|
| 1110 |
-
|
| 1111 |
-
|
| 1112 |
-
|
| 1113 |
)
|
| 1114 |
-
show_radio = gr.update(visible=True, value=None)
|
| 1115 |
-
hide_status = gr.update(visible=False, value="")
|
| 1116 |
-
show_btn = gr.update(visible=True)
|
| 1117 |
-
return page, query, ret_a_json, ret_b_json, show_radio, show_btn, hide_status, sys1_is_a
|
| 1118 |
|
| 1119 |
run_btn.click(
|
| 1120 |
retrieve,
|
| 1121 |
-
inputs=[query_box, topk_slider, mode_sel],
|
| 1122 |
-
outputs=[results_html, st_query,
|
| 1123 |
-
|
|
|
|
| 1124 |
)
|
| 1125 |
|
| 1126 |
-
def
|
| 1127 |
if mode == "Arena":
|
| 1128 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1129 |
else:
|
| 1130 |
-
|
| 1131 |
-
|
|
|
|
| 1132 |
return (
|
| 1133 |
-
""
|
|
|
|
| 1134 |
gr.update(visible=False, value=None),
|
| 1135 |
gr.update(visible=False),
|
| 1136 |
gr.update(visible=False, value=""),
|
| 1137 |
-
|
| 1138 |
)
|
| 1139 |
|
| 1140 |
mode_sel.change(
|
| 1141 |
-
|
| 1142 |
inputs=mode_sel,
|
| 1143 |
-
outputs=[results_html, vote_radio,
|
| 1144 |
)
|
| 1145 |
|
| 1146 |
submit_btn.click(
|
| 1147 |
_save_vote,
|
| 1148 |
-
inputs=[vote_radio, st_query,
|
| 1149 |
-
|
|
|
|
| 1150 |
)
|
| 1151 |
-
|
| 1152 |
-
def handle_individual_vote(vote_data: str, query: str,
|
|
|
|
|
|
|
| 1153 |
if not vote_data:
|
| 1154 |
return ""
|
| 1155 |
try:
|
| 1156 |
data = json.loads(vote_data)
|
| 1157 |
-
|
|
|
|
|
|
|
| 1158 |
payload = {
|
| 1159 |
-
"retriever_a":
|
| 1160 |
-
"retriever_b":
|
| 1161 |
-
"
|
| 1162 |
-
"
|
|
|
|
| 1163 |
}
|
| 1164 |
-
|
| 1165 |
_save_individual_vote(
|
| 1166 |
data["formal_statement"],
|
| 1167 |
data["vote_decision"],
|
| 1168 |
data["query"],
|
| 1169 |
-
|
| 1170 |
data["rank"],
|
| 1171 |
-
|
|
|
|
| 1172 |
)
|
| 1173 |
-
return f"**{data['vote_decision']}
|
| 1174 |
except (json.JSONDecodeError, KeyError) as e:
|
| 1175 |
-
return f"Error saving vote: {
|
| 1176 |
except Exception as e:
|
| 1177 |
-
return f"ERROR: {
|
| 1178 |
-
|
| 1179 |
vote_trigger.change(
|
| 1180 |
handle_individual_vote,
|
| 1181 |
-
inputs=[vote_trigger, st_query,
|
| 1182 |
-
|
|
|
|
| 1183 |
)
|
| 1184 |
|
|
|
|
| 1185 |
if __name__ == "__main__":
|
| 1186 |
-
demo.launch()
|
|
|
|
| 1 |
+
import os, json, datetime, threading, requests, random, html, time
|
| 2 |
from typing import List, Dict, Any
|
| 3 |
+
|
| 4 |
import gradio as gr
|
| 5 |
import gspread
|
| 6 |
from google.oauth2.service_account import Credentials
|
| 7 |
from gspread.exceptions import WorksheetNotFound
|
| 8 |
+
try:
|
| 9 |
+
from dotenv import load_dotenv
|
| 10 |
+
load_dotenv()
|
| 11 |
+
except ImportError:
|
| 12 |
+
pass # In deployment the env vars are already set
|
| 13 |
|
| 14 |
ENDPOINT_ID_A = os.getenv("ENDPOINT_ID_A")
|
| 15 |
ENDPOINT_ID_B = os.getenv("ENDPOINT_ID_B")
|
| 16 |
+
HF_TOKEN = os.getenv("HF_TOKEN", "")
|
| 17 |
SERVICE_ACCOUNT_INFO = os.getenv("GCP_SERVICE_ACCOUNT_JSON")
|
| 18 |
+
|
| 19 |
SCOPES = [
|
| 20 |
"https://www.googleapis.com/auth/spreadsheets",
|
| 21 |
"https://www.googleapis.com/auth/drive",
|
| 22 |
]
|
| 23 |
|
|
|
|
|
|
|
|
|
|
| 24 |
if not ENDPOINT_ID_A:
|
| 25 |
+
raise ValueError("ENDPOINT_ID_A is not set")
|
| 26 |
if not ENDPOINT_ID_B:
|
| 27 |
raise ValueError("ENDPOINT_ID_B is not set")
|
| 28 |
+
if not SERVICE_ACCOUNT_INFO:
|
| 29 |
+
raise ValueError("GCP_SERVICE_ACCOUNT_JSON is not set")
|
| 30 |
+
|
| 31 |
+
credentials = Credentials.from_service_account_info(
|
| 32 |
+
json.loads(SERVICE_ACCOUNT_INFO), scopes=SCOPES
|
| 33 |
+
)
|
| 34 |
|
| 35 |
+
# Mathlib versions supported by the new endpoint
|
| 36 |
+
MATHLIB_VERSIONS = ["v4.19.0", "v4.24.0", "v4.28.0"]
|
| 37 |
+
DEFAULT_VERSION = "v4.19.0"
|
| 38 |
+
|
| 39 |
+
LEAN_FINDER = "Lean Finder"
|
| 40 |
+
LEAN_SEARCH = "LeanSearch"
|
| 41 |
+
APP_SOURCE = "lean finder web"
|
| 42 |
+
|
| 43 |
+
|
| 44 |
+
# Retriever calls
|
| 45 |
+
|
| 46 |
+
def _call_lean_finder(query: str, k: int, version: str) -> List[Dict[str, Any]]:
|
| 47 |
headers = {"Accept": "application/json", "Content-Type": "application/json"}
|
| 48 |
if HF_TOKEN:
|
| 49 |
headers["Authorization"] = f"Bearer {HF_TOKEN}"
|
| 50 |
+
payload = {
|
| 51 |
+
"inputs": query,
|
| 52 |
+
"top_k": k,
|
| 53 |
+
"version": version,
|
| 54 |
+
"source": APP_SOURCE,
|
| 55 |
+
}
|
| 56 |
try:
|
| 57 |
r = requests.post(ENDPOINT_ID_A, json=payload, headers=headers, timeout=60)
|
| 58 |
r.raise_for_status()
|
| 59 |
+
return r.json().get("results", []) or []
|
| 60 |
+
except requests.exceptions.RequestException:
|
| 61 |
+
raise RuntimeError("Lean Finder service is unreachable.")
|
| 62 |
|
| 63 |
+
|
| 64 |
+
def _call_lean_search(query: str, k: int) -> List[Dict[str, Any]]:
|
| 65 |
payload = {"query": [query], "num_results": str(k)}
|
| 66 |
try:
|
| 67 |
r = requests.post(ENDPOINT_ID_B, json=payload, timeout=60)
|
| 68 |
r.raise_for_status()
|
| 69 |
data = r.json()
|
| 70 |
return data[0] if isinstance(data, list) and data else []
|
| 71 |
+
except requests.exceptions.RequestException:
|
| 72 |
+
raise RuntimeError("LeanSearch service is unreachable.")
|
| 73 |
+
|
| 74 |
+
|
| 75 |
+
# Normalize results from both retrievers into a common schema
|
| 76 |
+
|
| 77 |
+
def _norm_lean_finder(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 78 |
+
out = []
|
| 79 |
+
for r in results:
|
| 80 |
+
out.append({
|
| 81 |
+
"formal_name": (r.get("formal_name") or "").strip(),
|
| 82 |
+
"informal_name": (r.get("informal_name") or "").strip(),
|
| 83 |
+
"kind": (r.get("kind") or "").strip(),
|
| 84 |
+
"type": (r.get("type") or "").strip(),
|
| 85 |
+
"informal_description": (r.get("informal_description") or "").strip(),
|
| 86 |
+
"path": (r.get("path") or "").strip(),
|
| 87 |
+
"score": float(r.get("score") or 0.0),
|
| 88 |
+
"source": LEAN_FINDER,
|
| 89 |
+
})
|
| 90 |
+
return out
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
def _norm_lean_search(results: List[Dict[str, Any]]) -> List[Dict[str, Any]]:
|
| 94 |
+
out = []
|
| 95 |
+
for e in results:
|
| 96 |
+
r = e.get("result", {}) or {}
|
| 97 |
+
formal_name = ".".join(r.get("name") or [])
|
| 98 |
+
module = r.get("module_name") or []
|
| 99 |
+
path = "/".join(module) if module else ""
|
| 100 |
+
type_str = (r.get("type") or r.get("signature") or "").strip()
|
| 101 |
+
# LeanSearch returns a distance (lower is better, ~cosine). Convert to a
|
| 102 |
+
# similarity-ish score for display only.
|
| 103 |
+
try:
|
| 104 |
+
distance = float(e.get("distance") or 1.0)
|
| 105 |
+
except (TypeError, ValueError):
|
| 106 |
+
distance = 1.0
|
| 107 |
+
score = max(0.0, min(1.0, 1.0 - distance))
|
| 108 |
+
out.append({
|
| 109 |
+
"formal_name": formal_name.strip(),
|
| 110 |
+
"informal_name": (r.get("informal_name") or "").strip(),
|
| 111 |
+
"kind": (r.get("kind") or "").strip(),
|
| 112 |
+
"type": type_str,
|
| 113 |
+
"informal_description": (r.get("informal_description") or "").strip(),
|
| 114 |
+
"path": path,
|
| 115 |
+
"score": score,
|
| 116 |
+
"source": LEAN_SEARCH,
|
| 117 |
+
})
|
| 118 |
+
return out
|
| 119 |
+
|
| 120 |
+
|
| 121 |
+
# Google Sheets β new sheet names
|
| 122 |
+
|
| 123 |
+
gc = gspread.authorize(credentials)
|
| 124 |
+
SHEET_LOCK = threading.Lock()
|
| 125 |
+
_spreadsheet = gc.open("arena_votes")
|
| 126 |
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
+
def _get_or_create_sheet(name: str, header: List[str]):
|
| 129 |
+
try:
|
| 130 |
+
ws = _spreadsheet.worksheet(name)
|
| 131 |
+
# Ensure header exists
|
| 132 |
+
try:
|
| 133 |
+
first = ws.row_values(1)
|
| 134 |
+
except Exception:
|
| 135 |
+
first = []
|
| 136 |
+
if not first:
|
| 137 |
+
ws.append_row(header, value_input_option="RAW")
|
| 138 |
+
return ws
|
| 139 |
+
except WorksheetNotFound:
|
| 140 |
+
ws = _spreadsheet.add_worksheet(title=name, rows=2000, cols=max(12, len(header)))
|
| 141 |
+
ws.append_row(header, value_input_option="RAW")
|
| 142 |
+
return ws
|
| 143 |
+
|
| 144 |
+
|
| 145 |
+
comparison_ws = _get_or_create_sheet(
|
| 146 |
+
"leansearch_comparison_updated",
|
| 147 |
+
["timestamp", "query", "version", "ui_choice", "winner", "a_source", "b_source", "payload", "type"],
|
| 148 |
+
)
|
| 149 |
+
individual_ws = _get_or_create_sheet(
|
| 150 |
+
"individual_votes_updated",
|
| 151 |
+
["timestamp", "query", "version", "formal_name", "vote_decision", "system", "rank", "payload"],
|
| 152 |
+
)
|
| 153 |
+
|
| 154 |
+
|
| 155 |
+
VOTE_UI = ["Retriever A better", "Retriever B better", "Tie", "Both are bad"]
|
| 156 |
|
|
|
|
| 157 |
|
| 158 |
+
def _save_vote(choice: str, query: str, version: str,
|
| 159 |
+
ret_a: List[Dict[str, Any]], ret_b: List[Dict[str, Any]],
|
| 160 |
+
a_source: str, b_source: str):
|
| 161 |
if not choice:
|
| 162 |
+
return gr.update(value="β οΈ Please pick a winner first.", visible=True)
|
| 163 |
+
|
| 164 |
if choice == "Retriever A better":
|
| 165 |
+
winner = a_source
|
| 166 |
elif choice == "Retriever B better":
|
| 167 |
+
winner = b_source
|
| 168 |
else:
|
| 169 |
+
winner = choice # "Tie" or "Both are bad"
|
| 170 |
|
| 171 |
payload = {
|
| 172 |
+
"retriever_a": ret_a,
|
| 173 |
+
"retriever_b": ret_b,
|
| 174 |
+
"a_source": a_source,
|
| 175 |
+
"b_source": b_source,
|
| 176 |
}
|
| 177 |
row = [
|
| 178 |
+
datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds"),
|
| 179 |
query,
|
| 180 |
+
version,
|
| 181 |
+
choice,
|
| 182 |
+
winner,
|
| 183 |
+
a_source,
|
| 184 |
+
b_source,
|
| 185 |
json.dumps(payload, ensure_ascii=False),
|
| 186 |
+
"User Preference",
|
| 187 |
]
|
| 188 |
with SHEET_LOCK:
|
| 189 |
+
comparison_ws.append_row(row, value_input_option="RAW")
|
| 190 |
+
return gr.update(value="β
**Vote recorded β thanks!**", visible=True)
|
| 191 |
+
|
| 192 |
|
| 193 |
+
def _save_individual_vote(formal_name: str, vote_decision: str, query: str,
|
| 194 |
+
system: str, rank: int, version: str,
|
| 195 |
+
payload: Dict[str, Any]) -> str:
|
| 196 |
row = [
|
| 197 |
+
datetime.datetime.now(datetime.timezone.utc).replace(tzinfo=None).isoformat(timespec="seconds"),
|
| 198 |
query,
|
| 199 |
+
version,
|
| 200 |
+
formal_name,
|
| 201 |
vote_decision,
|
| 202 |
system,
|
| 203 |
rank,
|
| 204 |
+
json.dumps(payload, ensure_ascii=False),
|
| 205 |
]
|
| 206 |
try:
|
| 207 |
with SHEET_LOCK:
|
| 208 |
+
individual_ws.append_row(row, value_input_option="RAW")
|
| 209 |
return "Vote recorded!"
|
| 210 |
except Exception as e:
|
| 211 |
+
return f"Error writing to Google Sheets: {e}"
|
| 212 |
+
|
| 213 |
+
|
| 214 |
+
# Rendering
|
| 215 |
+
|
| 216 |
+
KIND_COLORS = {
|
| 217 |
+
"theorem": ("#dbeafe", "#1d4ed8"),
|
| 218 |
+
"lemma": ("#dbeafe", "#1d4ed8"),
|
| 219 |
+
"def": ("#d1fae5", "#047857"),
|
| 220 |
+
"definition": ("#d1fae5", "#047857"),
|
| 221 |
+
"structure": ("#ede9fe", "#6d28d9"),
|
| 222 |
+
"class": ("#ede9fe", "#6d28d9"),
|
| 223 |
+
"inductive": ("#ffedd5", "#c2410c"),
|
| 224 |
+
"instance": ("#fce7f3", "#be185d"),
|
| 225 |
+
"abbrev": ("#cffafe", "#0e7490"),
|
| 226 |
+
"opaque": ("#e5e7eb", "#374151"),
|
| 227 |
+
"axiom": ("#fef3c7", "#a16207"),
|
| 228 |
+
}
|
| 229 |
|
| 230 |
+
|
| 231 |
+
def _kind_chip(kind: str) -> str:
|
| 232 |
+
label = (kind or "").strip()
|
| 233 |
+
if not label:
|
| 234 |
return ""
|
| 235 |
+
bg, fg = KIND_COLORS.get(label.lower(), ("#e5e7eb", "#374151"))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 236 |
return (
|
| 237 |
+
f'<span class="kind-chip" '
|
| 238 |
+
f'style="background:{bg};color:{fg};">{html.escape(label)}</span>'
|
|
|
|
|
|
|
| 239 |
)
|
| 240 |
|
| 241 |
+
|
| 242 |
+
def _result_card(idx: int, r: Dict[str, Any], system_label: str, query: str,
|
| 243 |
+
arena_mode: bool, version: str = "") -> str:
|
| 244 |
+
formal_name = r.get("formal_name", "")
|
| 245 |
+
informal_name = r.get("informal_name", "")
|
| 246 |
+
kind = r.get("kind", "")
|
| 247 |
+
type_str = r.get("type", "")
|
| 248 |
+
informal_desc = r.get("informal_description", "")
|
| 249 |
+
path = (r.get("path") or "").strip()
|
| 250 |
+
|
| 251 |
+
formal_escaped = html.escape(formal_name)
|
| 252 |
+
type_escaped = html.escape(type_str)
|
| 253 |
+
query_escaped = html.escape(query)
|
| 254 |
+
system_escaped = html.escape(system_label)
|
| 255 |
+
|
| 256 |
+
doc_url = (
|
| 257 |
+
f"https://leanprover-community.github.io/mathlib4_docs/find/"
|
| 258 |
+
f"?pattern={requests.utils.quote(formal_name)}#doc"
|
| 259 |
+
if formal_name else ""
|
| 260 |
+
)
|
| 261 |
+
|
| 262 |
+
btns: List[str] = []
|
| 263 |
+
if doc_url:
|
| 264 |
+
btns.append(
|
| 265 |
+
f'<button class="card-btn" '
|
| 266 |
+
f'onclick="window.open(\'{doc_url}\', \'_blank\')" '
|
| 267 |
+
f'title="Open in mathlib docs">Doc</button>'
|
|
|
|
|
|
|
|
|
|
| 268 |
)
|
| 269 |
+
btns.append(
|
| 270 |
+
f'<button class="card-btn" data-copy-text="{formal_escaped}" '
|
| 271 |
+
f'onclick="copyToClipboard(this.getAttribute(\'data-copy-text\'), this)" '
|
| 272 |
+
f'title="Copy name">Copy</button>'
|
| 273 |
+
)
|
| 274 |
+
|
| 275 |
+
vote_cluster = ""
|
| 276 |
+
if arena_mode:
|
| 277 |
+
vote_cluster = (
|
| 278 |
+
f'<div class="vote-cluster">'
|
| 279 |
+
f'<button class="vote-btn upvote" data-formal="{formal_escaped}" '
|
| 280 |
+
f'data-query="{query_escaped}" data-system="{system_escaped}" '
|
| 281 |
+
f'data-rank="{idx}" onclick="voteOnResultSafe(this, \'Upvote\')" '
|
| 282 |
+
f'title="Useful">π</button>'
|
| 283 |
+
f'<button class="vote-btn downvote" data-formal="{formal_escaped}" '
|
| 284 |
+
f'data-query="{query_escaped}" data-system="{system_escaped}" '
|
| 285 |
+
f'data-rank="{idx}" onclick="voteOnResultSafe(this, \'Downvote\')" '
|
| 286 |
+
f'title="Not useful">π</button>'
|
| 287 |
f'</div>'
|
| 288 |
)
|
| 289 |
+
|
| 290 |
+
informal_name_html = (
|
| 291 |
+
f'<div class="informal-name"><span class="math-content">'
|
| 292 |
+
f'{html.escape(informal_name)}</span></div>'
|
| 293 |
+
if informal_name else ""
|
| 294 |
+
)
|
| 295 |
+
|
| 296 |
+
informal_desc_html = ""
|
| 297 |
+
if informal_desc:
|
| 298 |
+
if len(informal_desc) > 320:
|
| 299 |
+
preview = informal_desc[:320].rstrip() + "β¦"
|
| 300 |
+
informal_desc_html = (
|
| 301 |
+
f'<div class="informal-desc collapsible">'
|
| 302 |
+
f'<span class="math-content desc-preview">{html.escape(preview)}</span>'
|
| 303 |
+
f'<span class="math-content desc-full" hidden>{html.escape(informal_desc)}</span>'
|
| 304 |
+
f'<button class="link-btn read-more" onclick="toggleDesc(this)">Read more</button>'
|
| 305 |
+
f'</div>'
|
| 306 |
+
)
|
| 307 |
+
else:
|
| 308 |
+
informal_desc_html = (
|
| 309 |
+
f'<div class="informal-desc">'
|
| 310 |
+
f'<span class="math-content">{html.escape(informal_desc)}</span>'
|
| 311 |
+
f'</div>'
|
| 312 |
+
)
|
| 313 |
+
|
| 314 |
+
type_section_html = (
|
| 315 |
+
f'<pre class="formal-type"><code>{type_escaped}</code></pre>'
|
| 316 |
+
if type_str else ""
|
| 317 |
+
)
|
| 318 |
+
path_html = ""
|
| 319 |
+
if path:
|
| 320 |
+
path_dotted = html.escape(path.replace("/", "."))
|
| 321 |
+
github_url = f"https://github.com/leanprover-community/mathlib4/tree/{version}/{path}.lean" if version else ""
|
| 322 |
+
if github_url:
|
| 323 |
+
path_html = f'<div class="module-path">In: <a href="{github_url}" target="_blank" class="module-path-link">{path_dotted}</a></div>'
|
| 324 |
+
else:
|
| 325 |
+
path_html = f'<div class="module-path">In: {path_dotted}</div>'
|
| 326 |
+
|
| 327 |
+
return (
|
| 328 |
+
f'<article class="result-card">'
|
| 329 |
+
f' <div class="card-header">'
|
| 330 |
+
f' <span class="rank-badge">{idx}</span>'
|
| 331 |
+
f' {_kind_chip(kind)}'
|
| 332 |
+
f' <code class="formal-name">{formal_escaped}</code>'
|
| 333 |
+
f' <div class="header-actions">{"".join(btns)}{vote_cluster}</div>'
|
| 334 |
+
f' </div>'
|
| 335 |
+
f' {informal_name_html}'
|
| 336 |
+
f' {informal_desc_html}'
|
| 337 |
+
f' {type_section_html}'
|
| 338 |
+
f' {path_html}'
|
| 339 |
+
f'</article>'
|
| 340 |
+
)
|
| 341 |
+
|
| 342 |
+
|
| 343 |
+
def _render_column(results: List[Dict[str, Any]], col_title: str,
|
| 344 |
+
system_label: str, query: str, arena_mode: bool,
|
| 345 |
+
version: str = "", col_css: str = "") -> str:
|
| 346 |
+
if not results:
|
| 347 |
+
body = '<p class="empty">No results.</p>'
|
| 348 |
+
else:
|
| 349 |
+
body = "".join(_result_card(i, r, system_label, query, arena_mode, version)
|
| 350 |
+
for i, r in enumerate(results, 1))
|
| 351 |
+
cls = f'results-column {col_css}'.strip()
|
| 352 |
return (
|
| 353 |
+
f'<div class="{cls}">'
|
| 354 |
+
f' <div class="col-title">{html.escape(col_title)}</div>'
|
| 355 |
+
f' <div class="cards-stack">{body}</div>'
|
| 356 |
+
f'</div>'
|
| 357 |
)
|
| 358 |
|
| 359 |
+
|
| 360 |
+
def _render_single(results: List[Dict[str, Any]], query: str, version: str = "") -> str:
|
| 361 |
+
if not results:
|
| 362 |
+
return '<div class="empty-state">No matching Lean statements found.</div>'
|
| 363 |
+
cards = "".join(_result_card(i, r, LEAN_FINDER, query, arena_mode=False, version=version)
|
| 364 |
+
for i, r in enumerate(results, 1))
|
| 365 |
+
return f'<div class="results-single"><div class="cards-stack">{cards}</div></div>'
|
| 366 |
+
|
| 367 |
+
|
| 368 |
+
def _render_arena(ret_a: List[Dict[str, Any]], ret_b: List[Dict[str, Any]],
|
| 369 |
+
a_source: str, b_source: str, query: str, version: str = "") -> str:
|
| 370 |
+
a_html = _render_column(ret_a, "Retriever A", a_source, query, arena_mode=True, version=version, col_css="column-a")
|
| 371 |
+
b_html = _render_column(ret_b, "Retriever B", b_source, query, arena_mode=True, version=version, col_css="column-b")
|
| 372 |
+
return f'<div class="arena-grid">{a_html}{b_html}</div>'
|
| 373 |
+
|
| 374 |
+
|
| 375 |
+
# Static content
|
| 376 |
+
|
| 377 |
INSTRUCTIONS_MD = """
|
| 378 |
+
### Lean Finder accepts four kinds of queries
|
| 379 |
+
|
| 380 |
+
1. **Informalized statement** β A natural-language rendering of a Lean statement.
|
| 381 |
+
> *Let L/K be a field extension and let x, y β L be algebraic elements over K with the same minimal polynomial. Then the K-algebra isomorphism algEquiv between the simple field extensions K(x) and K(y) maps the generator x of K(x) to the generator y of K(y); i.e. algEquiv(x) = y.*
|
| 382 |
+
|
| 383 |
+
2. **User question** β Free-form questions about Lean / Mathlib.
|
| 384 |
+
> *I'm working with algebraic elements over a field extension β¦ Does this imply that the minimal polynomials of `x` and `y` are equal?*
|
| 385 |
+
|
| 386 |
+
3. **Proof state** β Paste a proof state, optionally followed by what you want to do next.
|
| 387 |
+
> *K : Type u_1, E : Type u_2, instβ : RCLike K, zβ z : K β’ |re z| β€ βzβ. Transform the goal from proving that the absolute value of the real part is β€ the norm, to proving that the square of the real part is β€ the squared norm.*
|
| 388 |
+
|
| 389 |
+
4. **Statement fragment** β A snippet or rough draft of the Lean definition you're looking for.
|
| 390 |
+
> *theorem restrict Ioi: restrict Ioi e = restrict Ici e*
|
| 391 |
+
|
| 392 |
+
**Mathlib version** β pick from `v4.19.0`, `v4.24.0`, or `v4.28.0`. Lean Finder
|
| 393 |
+
queries the index matching that version so the formal names it returns actually
|
| 394 |
+
exist in your build.
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
"""
|
| 396 |
|
| 397 |
+
|
| 398 |
CUSTOM_CSS = """
|
| 399 |
+
:root {
|
| 400 |
+
--bg: #ffffff;
|
| 401 |
+
--fg: #111827;
|
| 402 |
+
--muted: #6b7280;
|
| 403 |
+
--border: #e5e7eb;
|
| 404 |
+
--card-bg: #ffffff;
|
| 405 |
+
--card-border: #e5e7eb;
|
| 406 |
+
--card-hover-border: #c7d2fe;
|
| 407 |
+
--primary: #4f46e5;
|
| 408 |
+
--primary-2: #7c3aed;
|
| 409 |
+
--primary-soft: #eef2ff;
|
| 410 |
+
--code-bg: #f9fafb;
|
| 411 |
+
--success: #10b981;
|
| 412 |
+
--danger: #ef4444;
|
| 413 |
}
|
| 414 |
|
| 415 |
+
@media (prefers-color-scheme: dark) {
|
| 416 |
+
:root {
|
| 417 |
+
--bg: #0b0b0d;
|
| 418 |
+
--fg: #e5e7eb;
|
| 419 |
+
--muted: #9ca3af;
|
| 420 |
+
--border: #27272a;
|
| 421 |
+
--card-bg: #141417;
|
| 422 |
+
--card-border: #27272a;
|
| 423 |
+
--card-hover-border: #4f46e5;
|
| 424 |
+
--primary: #818cf8;
|
| 425 |
+
--primary-2: #a78bfa;
|
| 426 |
+
--primary-soft: #1e1b4b;
|
| 427 |
+
--code-bg: #0f0f12;
|
| 428 |
+
--success: #34d399;
|
| 429 |
+
--danger: #f87171;
|
| 430 |
+
}
|
| 431 |
+
}
|
| 432 |
+
.dark {
|
| 433 |
+
--bg: #0b0b0d;
|
| 434 |
+
--fg: #e5e7eb;
|
| 435 |
+
--muted: #9ca3af;
|
| 436 |
+
--border: #27272a;
|
| 437 |
+
--card-bg: #141417;
|
| 438 |
+
--card-border: #27272a;
|
| 439 |
+
--card-hover-border: #4f46e5;
|
| 440 |
+
--primary: #818cf8;
|
| 441 |
+
--primary-2: #a78bfa;
|
| 442 |
+
--primary-soft: #1e1b4b;
|
| 443 |
+
--code-bg: #0f0f12;
|
| 444 |
+
--success: #34d399;
|
| 445 |
+
--danger: #f87171;
|
| 446 |
+
}
|
| 447 |
|
| 448 |
+
html, body { margin: 0; padding: 0; width: 100%; }
|
| 449 |
+
.gradio-container, .gradio-container .block {
|
| 450 |
+
max-width: 1280px !important;
|
| 451 |
+
margin: 0 auto !important;
|
| 452 |
+
padding: 0 1rem;
|
|
|
|
|
|
|
| 453 |
}
|
| 454 |
|
| 455 |
+
/* Hero β 3-column grid keeps the logo+title centered while the badge floats right */
|
| 456 |
+
.lf-hero {
|
| 457 |
+
display: grid;
|
| 458 |
+
grid-template-columns: 1fr auto 1fr;
|
| 459 |
+
align-items: center;
|
| 460 |
+
gap: 1rem;
|
| 461 |
+
padding: 1.6rem 1.4rem;
|
| 462 |
+
margin: 0.5rem 0 1.1rem;
|
| 463 |
+
background: linear-gradient(135deg, rgba(102,126,234,0.08) 0%, rgba(118,75,162,0.08) 100%);
|
| 464 |
+
border: 1px solid var(--border);
|
| 465 |
+
border-radius: 18px;
|
| 466 |
+
}
|
| 467 |
+
.lf-hero-main {
|
| 468 |
+
grid-column: 2;
|
| 469 |
+
display: flex;
|
| 470 |
+
align-items: center;
|
| 471 |
+
gap: 1.1rem;
|
| 472 |
+
min-width: 0;
|
| 473 |
+
}
|
| 474 |
+
.lf-hero .lf-logo {
|
| 475 |
+
width: 76px; height: 76px; flex: 0 0 76px;
|
| 476 |
+
border-radius: 14px;
|
| 477 |
+
object-fit: contain;
|
| 478 |
+
background: white;
|
| 479 |
+
padding: 8px;
|
| 480 |
+
box-shadow: 0 2px 10px rgba(0,0,0,0.06);
|
| 481 |
+
}
|
| 482 |
+
.lf-hero-text { display: flex; flex-direction: column; justify-content: center; min-width: 0; }
|
| 483 |
+
.lf-title {
|
| 484 |
+
margin: 0 !important;
|
| 485 |
+
padding: 0 !important;
|
| 486 |
+
font-size: clamp(2rem, 4.5vw, 2.85rem) !important;
|
| 487 |
+
font-weight: 800 !important;
|
| 488 |
+
line-height: 1.1 !important;
|
| 489 |
+
letter-spacing: -0.02em !important;
|
| 490 |
+
color: #4f46e5; /* fallback for browsers that don't support gradient text */
|
| 491 |
+
background: linear-gradient(135deg, #4f46e5 0%, #7c3aed 100%);
|
| 492 |
+
-webkit-background-clip: text;
|
| 493 |
+
background-clip: text;
|
| 494 |
+
-webkit-text-fill-color: transparent;
|
| 495 |
+
display: inline-block;
|
| 496 |
+
}
|
| 497 |
@media (prefers-color-scheme: dark) {
|
| 498 |
+
.lf-title {
|
| 499 |
+
color: #a78bfa;
|
| 500 |
+
background: linear-gradient(135deg, #818cf8 0%, #c4b5fd 100%);
|
| 501 |
+
-webkit-background-clip: text;
|
| 502 |
+
background-clip: text;
|
| 503 |
+
-webkit-text-fill-color: transparent;
|
|
|
|
|
|
|
|
|
|
| 504 |
}
|
| 505 |
}
|
| 506 |
+
.dark .lf-title {
|
| 507 |
+
color: #a78bfa;
|
| 508 |
+
background: linear-gradient(135deg, #818cf8 0%, #c4b5fd 100%);
|
| 509 |
+
-webkit-background-clip: text;
|
| 510 |
+
background-clip: text;
|
| 511 |
+
-webkit-text-fill-color: transparent;
|
| 512 |
+
}
|
| 513 |
+
.lf-tagline {
|
| 514 |
+
margin: 0.35rem 0 0 0;
|
| 515 |
+
color: var(--muted);
|
| 516 |
+
font-size: 0.92rem;
|
| 517 |
+
font-weight: 400;
|
| 518 |
+
line-height: 1.4;
|
| 519 |
+
}
|
| 520 |
+
.lf-hero-badge {
|
| 521 |
+
grid-column: 3;
|
| 522 |
+
justify-self: end;
|
| 523 |
+
display: inline-flex;
|
| 524 |
+
align-items: center;
|
| 525 |
+
gap: 0.4rem;
|
| 526 |
+
padding: 0.5rem 0.95rem;
|
| 527 |
+
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
| 528 |
+
color: white;
|
| 529 |
+
text-decoration: none;
|
| 530 |
+
border-radius: 999px;
|
| 531 |
+
font-weight: 500;
|
| 532 |
+
font-size: 0.82rem;
|
| 533 |
+
box-shadow: 0 2px 8px rgba(102, 126, 234, 0.25);
|
| 534 |
+
transition: transform .15s ease, box-shadow .15s ease;
|
| 535 |
+
white-space: nowrap;
|
| 536 |
+
}
|
| 537 |
+
.lf-hero-badge:hover {
|
| 538 |
+
transform: translateY(-1px);
|
| 539 |
+
box-shadow: 0 6px 16px rgba(102, 126, 234, 0.35);
|
| 540 |
+
}
|
| 541 |
+
@media (max-width: 760px) {
|
| 542 |
+
.lf-hero {
|
| 543 |
+
grid-template-columns: 1fr;
|
| 544 |
+
text-align: center;
|
| 545 |
+
gap: 0.75rem;
|
| 546 |
+
}
|
| 547 |
+
.lf-hero-main { grid-column: 1; flex-direction: column; }
|
| 548 |
+
.lf-hero-badge { grid-column: 1; justify-self: center; }
|
| 549 |
}
|
| 550 |
|
| 551 |
+
/* Query input */
|
| 552 |
+
#query_input textarea {
|
| 553 |
+
resize: vertical !important;
|
| 554 |
+
min-height: 120px !important;
|
| 555 |
+
max-height: 360px !important;
|
| 556 |
+
font-size: 0.95rem !important;
|
| 557 |
+
line-height: 1.5 !important;
|
| 558 |
+
padding: 12px !important;
|
| 559 |
+
border-radius: 10px !important;
|
| 560 |
}
|
| 561 |
|
| 562 |
+
/* Controls row β keep compact, even baseline */
|
| 563 |
+
#controls_row { gap: 0.75rem; align-items: flex-end; margin-top: 0.4rem; }
|
| 564 |
+
#controls_row label { font-size: 0.8rem !important; font-weight: 500 !important; color: var(--muted) !important; }
|
| 565 |
+
#controls_row label.selected, #controls_row label.selected span { color: white !important; }
|
| 566 |
+
#controls_row .gr-form { background: transparent !important; border: none !important; }
|
| 567 |
+
|
| 568 |
+
/* Search button - the only purple gradient in this region */
|
| 569 |
+
#search_btn { margin: 0.9rem 0 0.6rem; }
|
| 570 |
+
#search_btn button {
|
| 571 |
+
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-2) 100%) !important;
|
| 572 |
+
color: white !important;
|
| 573 |
+
border: none !important;
|
| 574 |
+
padding: 0.85rem 1.4rem !important;
|
| 575 |
+
font-weight: 600 !important;
|
| 576 |
+
font-size: 1rem !important;
|
| 577 |
+
border-radius: 12px !important;
|
| 578 |
+
box-shadow: 0 4px 14px rgba(79, 70, 229, 0.28) !important;
|
| 579 |
+
transition: transform .12s ease, box-shadow .15s ease !important;
|
| 580 |
+
width: 100% !important;
|
| 581 |
}
|
| 582 |
+
#search_btn button:hover {
|
| 583 |
+
transform: translateY(-1px) !important;
|
| 584 |
+
box-shadow: 0 8px 20px rgba(79, 70, 229, 0.38) !important;
|
| 585 |
}
|
| 586 |
|
| 587 |
+
#mode_description { color: var(--muted); font-size: 0.85rem; margin: 0.2rem 0 0.4rem; }
|
| 588 |
+
#mode_description p { margin: 0; }
|
| 589 |
+
|
| 590 |
+
/* Result cards */
|
| 591 |
+
.results-single, .arena-grid { margin-top: 1rem; }
|
| 592 |
+
.arena-grid {
|
| 593 |
+
display: grid;
|
| 594 |
+
grid-template-columns: 1fr 1fr;
|
| 595 |
+
gap: 1rem;
|
| 596 |
+
}
|
| 597 |
+
@media (max-width: 860px) {
|
| 598 |
+
.arena-grid { grid-template-columns: 1fr; }
|
| 599 |
}
|
| 600 |
|
| 601 |
+
.results-column { display: flex; flex-direction: column; min-width: 0; }
|
| 602 |
|
| 603 |
+
/* Vertical divider between arena columns */
|
| 604 |
+
.results-column.column-b { border-left: 2px solid #6366f1; padding-left: 1rem; }
|
| 605 |
+
@media (prefers-color-scheme: dark) {
|
| 606 |
+
.results-column.column-b { border-left-color: #818cf8; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 607 |
}
|
| 608 |
+
.dark .results-column.column-b { border-left-color: #818cf8; }
|
| 609 |
|
| 610 |
+
/* Subtle, non-gradient column titles */
|
| 611 |
+
.col-title {
|
| 612 |
+
font-size: 0.78rem;
|
| 613 |
+
margin: 0 0 0.7rem 0;
|
| 614 |
+
padding: 0.4rem 0;
|
| 615 |
+
color: var(--muted);
|
| 616 |
+
font-weight: 600;
|
| 617 |
+
text-align: center;
|
| 618 |
+
letter-spacing: 0.12em;
|
| 619 |
+
text-transform: uppercase;
|
| 620 |
+
border-bottom: 1px solid var(--border);
|
| 621 |
}
|
| 622 |
|
| 623 |
+
.cards-stack { display: flex; flex-direction: column; gap: 0.7rem; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 624 |
|
| 625 |
+
.result-card {
|
| 626 |
+
background: var(--card-bg);
|
| 627 |
+
border: 1px solid var(--card-border);
|
| 628 |
+
border-radius: 12px;
|
| 629 |
+
padding: 0.85rem 1rem;
|
| 630 |
+
transition: border-color .15s ease, box-shadow .15s ease, transform .12s ease;
|
| 631 |
+
box-shadow: 0 1px 2px rgba(0,0,0,0.04);
|
| 632 |
+
min-width: 0;
|
| 633 |
+
}
|
| 634 |
+
.result-card:hover {
|
| 635 |
+
border-color: var(--card-hover-border);
|
| 636 |
+
box-shadow: 0 4px 14px rgba(79, 70, 229, 0.08);
|
| 637 |
}
|
| 638 |
|
| 639 |
+
.card-header {
|
| 640 |
+
display: flex;
|
| 641 |
+
align-items: center;
|
| 642 |
+
gap: 0.55rem;
|
| 643 |
+
min-width: 0;
|
| 644 |
+
}
|
| 645 |
|
| 646 |
+
.rank-badge {
|
| 647 |
+
display: inline-flex;
|
| 648 |
+
align-items: center;
|
| 649 |
+
justify-content: center;
|
| 650 |
+
width: 1.6rem;
|
| 651 |
+
height: 1.6rem;
|
| 652 |
+
border-radius: 50%;
|
| 653 |
+
background: var(--primary-soft);
|
| 654 |
+
color: var(--primary);
|
| 655 |
+
font-weight: 700;
|
| 656 |
+
font-size: 0.78rem;
|
| 657 |
+
flex-shrink: 0;
|
| 658 |
}
|
| 659 |
|
| 660 |
+
.kind-chip {
|
| 661 |
+
font-size: 0.65rem;
|
| 662 |
+
font-weight: 700;
|
| 663 |
+
padding: 0.18rem 0.55rem;
|
| 664 |
+
border-radius: 999px;
|
| 665 |
+
letter-spacing: 0.04em;
|
| 666 |
+
text-transform: uppercase;
|
| 667 |
+
line-height: 1.4;
|
|
|
|
| 668 |
}
|
| 669 |
|
| 670 |
+
code.formal-name {
|
| 671 |
+
background: transparent;
|
| 672 |
+
border: none;
|
| 673 |
+
color: var(--fg);
|
| 674 |
font-weight: 600;
|
| 675 |
+
font-size: 0.92rem;
|
| 676 |
+
padding: 0;
|
| 677 |
+
word-break: break-all;
|
| 678 |
+
flex: 1 1 0;
|
| 679 |
+
min-width: 0;
|
| 680 |
+
font-family: 'JetBrains Mono', ui-monospace, Menlo, Monaco, Consolas, 'Courier New', monospace;
|
| 681 |
}
|
| 682 |
|
| 683 |
+
.module-path {
|
| 684 |
+
margin-top: 0.3rem;
|
| 685 |
+
font-size: 0.75rem;
|
| 686 |
+
color: var(--muted);
|
| 687 |
+
font-family: 'JetBrains Mono', ui-monospace, Menlo, Monaco, Consolas, monospace;
|
| 688 |
+
opacity: 0.75;
|
| 689 |
+
letter-spacing: 0.01em;
|
| 690 |
+
}
|
| 691 |
+
.module-path-link {
|
| 692 |
+
color: var(--primary);
|
| 693 |
+
opacity: 0.55;
|
| 694 |
+
text-decoration: underline;
|
| 695 |
+
text-decoration-color: rgba(79, 70, 229, 0.25);
|
| 696 |
+
text-underline-offset: 2px;
|
| 697 |
+
transition: opacity .12s ease, text-decoration-color .12s ease;
|
| 698 |
+
}
|
| 699 |
+
.module-path-link:hover {
|
| 700 |
+
opacity: 0.85;
|
| 701 |
+
text-decoration-color: var(--primary);
|
| 702 |
}
|
| 703 |
|
| 704 |
+
.header-actions {
|
| 705 |
+
display: flex;
|
| 706 |
+
gap: 0.3rem;
|
| 707 |
+
margin-left: auto;
|
| 708 |
+
align-items: center;
|
| 709 |
+
flex-wrap: wrap;
|
|
|
|
| 710 |
}
|
| 711 |
|
| 712 |
+
.card-btn {
|
| 713 |
+
display: inline-flex;
|
| 714 |
+
align-items: center;
|
| 715 |
+
justify-content: center;
|
| 716 |
+
gap: 0.35rem;
|
| 717 |
+
height: 32px;
|
| 718 |
+
padding: 0 0.8rem;
|
| 719 |
+
background: var(--bg);
|
| 720 |
+
border: 1px solid var(--border);
|
| 721 |
+
color: var(--muted);
|
| 722 |
+
border-radius: 7px;
|
| 723 |
+
font-size: 0.82rem;
|
| 724 |
+
line-height: 1;
|
| 725 |
cursor: pointer;
|
| 726 |
+
transition: all .12s ease;
|
|
|
|
|
|
|
|
|
|
| 727 |
white-space: nowrap;
|
| 728 |
+
font-family: inherit;
|
| 729 |
+
font-weight: 500;
|
| 730 |
+
vertical-align: middle;
|
| 731 |
}
|
| 732 |
+
.card-btn:hover {
|
| 733 |
+
border-color: var(--primary);
|
| 734 |
+
color: var(--primary);
|
| 735 |
+
background: var(--primary-soft);
|
| 736 |
}
|
| 737 |
|
| 738 |
+
.vote-cluster {
|
| 739 |
+
display: inline-flex;
|
| 740 |
+
align-items: center;
|
| 741 |
+
gap: 0.25rem;
|
| 742 |
+
margin-left: 0.4rem;
|
| 743 |
+
padding-left: 0.55rem;
|
| 744 |
+
border-left: 1px solid var(--border);
|
| 745 |
}
|
| 746 |
|
| 747 |
+
.vote-btn {
|
| 748 |
+
display: inline-flex;
|
| 749 |
+
align-items: center;
|
| 750 |
+
justify-content: center;
|
| 751 |
+
height: 28px;
|
| 752 |
+
min-width: 36px;
|
| 753 |
+
padding: 0 0.4rem;
|
| 754 |
+
background: var(--bg);
|
| 755 |
+
border: 1px solid var(--border);
|
| 756 |
+
color: var(--muted);
|
| 757 |
+
border-radius: 7px;
|
| 758 |
+
font-size: 0.95rem;
|
| 759 |
+
line-height: 1;
|
| 760 |
+
cursor: pointer;
|
| 761 |
+
transition: all .12s ease;
|
| 762 |
+
vertical-align: middle;
|
| 763 |
}
|
| 764 |
+
.vote-btn:hover { transform: translateY(-1px); }
|
| 765 |
+
.vote-btn.upvote:hover { border-color: var(--success); color: var(--success); background: rgba(16,185,129,0.08); }
|
| 766 |
+
.vote-btn.downvote:hover { border-color: var(--danger); color: var(--danger); background: rgba(239,68,68,0.08); }
|
| 767 |
+
.vote-btn.voted.upvote { background: var(--success); color: white; border-color: var(--success); }
|
| 768 |
+
.vote-btn.voted.downvote { background: var(--danger); color: white; border-color: var(--danger); }
|
| 769 |
+
|
| 770 |
+
.informal-name {
|
| 771 |
+
margin-top: 0.55rem;
|
| 772 |
+
font-size: 0.92rem;
|
| 773 |
+
color: var(--fg);
|
| 774 |
+
font-weight: 500;
|
| 775 |
+
line-height: 1.45;
|
| 776 |
}
|
| 777 |
|
| 778 |
+
.informal-desc {
|
| 779 |
+
margin-top: 0.4rem;
|
| 780 |
+
font-size: 0.86rem;
|
| 781 |
+
line-height: 1.55;
|
| 782 |
+
color: var(--muted);
|
| 783 |
}
|
| 784 |
|
| 785 |
+
/* Dark-mode: ensure informal text is bright enough on dark card backgrounds */
|
| 786 |
+
@media (prefers-color-scheme: dark) {
|
| 787 |
+
.informal-name { color: #e5e7eb; }
|
| 788 |
+
.informal-desc { color: #a1a1aa; }
|
|
|
|
| 789 |
}
|
| 790 |
+
.dark .informal-name { color: #e5e7eb; }
|
| 791 |
+
.dark .informal-desc { color: #a1a1aa; }
|
| 792 |
+
.informal-desc.collapsible .desc-full[hidden] { display: none; }
|
| 793 |
+
.link-btn {
|
| 794 |
+
background: none;
|
| 795 |
+
border: none;
|
| 796 |
+
color: var(--primary);
|
| 797 |
+
cursor: pointer;
|
| 798 |
+
font-size: 0.78rem;
|
| 799 |
+
font-weight: 500;
|
| 800 |
+
padding: 0 0 0 4px;
|
| 801 |
+
text-decoration: underline;
|
| 802 |
+
font-family: inherit;
|
| 803 |
}
|
| 804 |
+
.link-btn:hover { text-decoration: none; }
|
| 805 |
+
|
| 806 |
+
/* Formal type β always visible, calm look */
|
| 807 |
+
.formal-type {
|
| 808 |
+
background: var(--code-bg);
|
| 809 |
+
border: 1px solid var(--border);
|
| 810 |
+
border-radius: 8px;
|
| 811 |
+
padding: 0.65rem 0.8rem;
|
| 812 |
+
margin: 0.7rem 0 0;
|
| 813 |
+
overflow-x: auto;
|
| 814 |
+
font-size: 0.78rem;
|
| 815 |
+
line-height: 1.5;
|
| 816 |
+
}
|
| 817 |
+
.formal-type code {
|
| 818 |
background: transparent;
|
| 819 |
+
border: none;
|
| 820 |
+
padding: 0;
|
| 821 |
+
white-space: pre-wrap;
|
| 822 |
+
word-break: break-word;
|
| 823 |
+
color: var(--fg);
|
| 824 |
+
font-family: 'JetBrains Mono', ui-monospace, Menlo, Monaco, Consolas, monospace;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 825 |
}
|
| 826 |
|
| 827 |
+
.empty {
|
| 828 |
+
color: var(--muted);
|
| 829 |
+
text-align: center;
|
| 830 |
+
padding: 1rem;
|
| 831 |
+
font-size: 0.9rem;
|
| 832 |
}
|
| 833 |
+
.empty-state {
|
| 834 |
+
text-align: center;
|
| 835 |
+
padding: 2.5rem 1rem;
|
| 836 |
+
color: var(--muted);
|
| 837 |
+
border: 1px dashed var(--border);
|
| 838 |
+
border-radius: 12px;
|
| 839 |
+
margin-top: 0.75rem;
|
| 840 |
}
|
| 841 |
|
| 842 |
+
/* Vote area β warm accent so it reads as a separate "action" zone */
|
| 843 |
+
#vote_area {
|
| 844 |
+
margin: 1rem 0 0.3rem !important;
|
| 845 |
+
padding: 0.95rem 1.1rem !important;
|
| 846 |
+
border: 1px solid rgba(245, 158, 11, 0.35) !important;
|
| 847 |
+
border-radius: 14px !important;
|
| 848 |
+
background: linear-gradient(180deg, rgba(254, 243, 199, 0.55) 0%, rgba(254, 243, 199, 0.12) 100%) !important;
|
| 849 |
+
box-shadow: 0 1px 2px rgba(0,0,0,0.03);
|
| 850 |
}
|
| 851 |
+
@media (prefers-color-scheme: dark) {
|
| 852 |
+
#vote_area {
|
| 853 |
+
background: linear-gradient(180deg, rgba(120, 53, 15, 0.22) 0%, rgba(120, 53, 15, 0.05) 100%) !important;
|
| 854 |
+
border-color: rgba(245, 158, 11, 0.30) !important;
|
| 855 |
+
}
|
| 856 |
+
}
|
| 857 |
+
.dark #vote_area {
|
| 858 |
+
background: linear-gradient(180deg, rgba(120, 53, 15, 0.22) 0%, rgba(120, 53, 15, 0.05) 100%) !important;
|
| 859 |
+
border-color: rgba(245, 158, 11, 0.30) !important;
|
| 860 |
+
}
|
| 861 |
+
.vote-area-label {
|
| 862 |
+
font-size: 0.78rem;
|
| 863 |
+
font-weight: 700;
|
| 864 |
+
color: #92400e;
|
| 865 |
+
letter-spacing: 0.08em;
|
| 866 |
+
text-transform: uppercase;
|
| 867 |
+
margin-bottom: 0.55rem;
|
| 868 |
+
}
|
| 869 |
+
@media (prefers-color-scheme: dark) {
|
| 870 |
+
.vote-area-label { color: #fbbf24; }
|
| 871 |
+
}
|
| 872 |
+
.dark .vote-area-label { color: #fbbf24; }
|
| 873 |
+
#vote_radio .gr-radio { display: flex; gap: 0.45rem; flex-wrap: wrap; }
|
| 874 |
+
#vote_radio label {
|
| 875 |
+
padding: 6px 14px;
|
| 876 |
+
border: 1px solid var(--border);
|
| 877 |
+
background: var(--card-bg);
|
| 878 |
+
border-radius: 8px;
|
| 879 |
cursor: pointer;
|
| 880 |
+
user-select: none;
|
| 881 |
+
transition: all .15s ease;
|
| 882 |
+
font-size: 0.86rem;
|
| 883 |
+
color: var(--fg);
|
|
|
|
|
|
|
|
|
|
| 884 |
}
|
| 885 |
+
@media (prefers-color-scheme: dark) {
|
| 886 |
+
#vote_radio label { color: #e5e7eb; background: #1c1c20; border-color: #3f3f46; }
|
| 887 |
+
}
|
| 888 |
+
.dark #vote_radio label { color: #e5e7eb; background: #1c1c20; border-color: #3f3f46; }
|
| 889 |
+
#vote_radio input[type="radio"]:checked + label {
|
| 890 |
+
background: #f59e0b;
|
| 891 |
color: white;
|
| 892 |
+
border-color: #f59e0b;
|
| 893 |
}
|
| 894 |
|
| 895 |
+
/* Dark-mode kind chips β boost contrast */
|
| 896 |
+
@media (prefers-color-scheme: dark) {
|
| 897 |
+
.kind-chip { filter: brightness(1.15) saturate(1.1); }
|
|
|
|
|
|
|
|
|
|
| 898 |
}
|
| 899 |
+
.dark .kind-chip { filter: brightness(1.15) saturate(1.1); }
|
| 900 |
+
#submit_btn button {
|
| 901 |
+
padding: 0.55rem 1.4rem !important;
|
| 902 |
+
font-weight: 600 !important;
|
| 903 |
+
font-size: 0.9rem !important;
|
| 904 |
+
border-radius: 8px !important;
|
| 905 |
+
background: #f59e0b !important;
|
| 906 |
+
color: white !important;
|
| 907 |
+
border: none !important;
|
| 908 |
+
margin-top: 0.5rem !important;
|
| 909 |
+
width: auto !important;
|
| 910 |
+
align-self: flex-start;
|
| 911 |
+
box-shadow: 0 2px 8px rgba(245, 158, 11, 0.25) !important;
|
| 912 |
}
|
| 913 |
+
#submit_btn button:hover {
|
| 914 |
+
background: #d97706 !important;
|
| 915 |
+
box-shadow: 0 4px 12px rgba(245, 158, 11, 0.35) !important;
|
|
|
|
|
|
|
| 916 |
}
|
| 917 |
+
#vote_status { margin-top: 0.5rem; text-align: center; }
|
| 918 |
+
#vote_trigger_input { display: none !important; }
|
| 919 |
|
| 920 |
+
/* Misc */
|
| 921 |
+
.gr-accordion-header {
|
| 922 |
+
font-size: 1.0rem;
|
| 923 |
+
font-weight: 600;
|
| 924 |
+
cursor: pointer;
|
| 925 |
}
|
| 926 |
|
| 927 |
+
/* Progress overlay center */
|
| 928 |
+
div.gradio-modal[aria-label="progress"] {
|
| 929 |
+
position: fixed !important;
|
| 930 |
+
top: 50% !important; left: 50% !important;
|
| 931 |
+
transform: translate(-50%, -50%) !important;
|
| 932 |
+
z-index: 2000 !important;
|
| 933 |
+
width: clamp(260px, 70vw, 440px) !important;
|
| 934 |
+
max-height: 160px !important;
|
| 935 |
+
padding: 20px 24px !important;
|
| 936 |
+
background: var(--card-bg) !important;
|
| 937 |
+
border: 1px solid var(--border) !important;
|
| 938 |
+
border-radius: 12px !important;
|
| 939 |
+
box-shadow: 0 8px 28px rgba(0,0,0,.18) !important;
|
| 940 |
}
|
| 941 |
|
| 942 |
+
/* Acknowledgements */
|
| 943 |
+
.acknowledgements {
|
| 944 |
+
margin-top: 2rem;
|
| 945 |
+
padding: 1rem 0 0.5rem;
|
| 946 |
+
border-top: 1px solid var(--border);
|
| 947 |
+
text-align: center;
|
| 948 |
+
}
|
| 949 |
+
.ack-title {
|
| 950 |
+
font-size: 0.72rem;
|
| 951 |
+
font-weight: 600;
|
| 952 |
+
text-transform: uppercase;
|
| 953 |
+
letter-spacing: 0.1em;
|
| 954 |
+
color: var(--muted);
|
| 955 |
+
margin-bottom: 0.35rem;
|
| 956 |
+
}
|
| 957 |
+
.ack-body {
|
| 958 |
+
font-size: 0.78rem;
|
| 959 |
+
color: var(--muted);
|
| 960 |
+
line-height: 1.6;
|
| 961 |
+
}
|
| 962 |
+
.ack-body a {
|
| 963 |
+
color: var(--primary);
|
| 964 |
+
text-decoration: none;
|
| 965 |
+
opacity: 0.75;
|
| 966 |
+
transition: opacity .12s ease;
|
| 967 |
+
}
|
| 968 |
+
.ack-body a:hover {
|
| 969 |
opacity: 1;
|
| 970 |
+
text-decoration: underline;
|
| 971 |
}
|
| 972 |
+
"""
|
| 973 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 974 |
|
| 975 |
+
HEAD_HTML = """
|
| 976 |
+
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.css">
|
| 977 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=JetBrains+Mono:wght@400;500&display=swap" rel="stylesheet">
|
| 978 |
+
<script src="https://cdn.jsdelivr.net/npm/katex@0.16.8/dist/katex.min.js"></script>
|
| 979 |
+
<style>
|
| 980 |
+
body, .gradio-container { font-family: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; }
|
| 981 |
+
</style>
|
| 982 |
+
<script>
|
| 983 |
+
function renderKatex() {
|
| 984 |
+
if (typeof katex === 'undefined') { setTimeout(renderKatex, 400); return; }
|
| 985 |
+
document.querySelectorAll('.math-content:not([data-katex-processed])').forEach(function(el) {
|
| 986 |
+
el.setAttribute('data-katex-processed', 'true');
|
| 987 |
+
const text = el.textContent;
|
| 988 |
+
if (!text || (text.indexOf('$') === -1 && text.indexOf('\\\\(') === -1)) return;
|
| 989 |
+
let html = '';
|
| 990 |
+
let i = 0;
|
| 991 |
+
while (i < text.length) {
|
| 992 |
+
if (text[i] === '$' && text[i+1] === '$') {
|
| 993 |
+
const end = text.indexOf('$$', i + 2);
|
| 994 |
+
if (end === -1) { html += text.substring(i); break; }
|
| 995 |
+
try {
|
| 996 |
+
html += katex.renderToString(text.substring(i + 2, end), { throwOnError: false, displayMode: true });
|
| 997 |
+
} catch (e) { html += '$$' + escapeHtml(text.substring(i + 2, end)) + '$$'; }
|
| 998 |
+
i = end + 2;
|
| 999 |
+
} else if (text[i] === '$') {
|
| 1000 |
+
const end = text.indexOf('$', i + 1);
|
| 1001 |
+
if (end === -1) { html += escapeHtml(text.substring(i)); break; }
|
| 1002 |
+
try {
|
| 1003 |
+
html += katex.renderToString(text.substring(i + 1, end), { throwOnError: false });
|
| 1004 |
+
} catch (e) { html += '$' + escapeHtml(text.substring(i + 1, end)) + '$'; }
|
| 1005 |
+
i = end + 1;
|
| 1006 |
+
} else {
|
| 1007 |
+
html += escapeHtml(text[i]);
|
| 1008 |
+
i++;
|
| 1009 |
+
}
|
| 1010 |
+
}
|
| 1011 |
+
el.innerHTML = html;
|
| 1012 |
+
});
|
| 1013 |
}
|
| 1014 |
+
function escapeHtml(s) { return s.replace(/[&<>"']/g, function(c) { return ({'&':'&','<':'<','>':'>','"':'"',"'":'''})[c]; }); }
|
| 1015 |
+
setInterval(renderKatex, 1200);
|
| 1016 |
+
|
| 1017 |
+
function copyToClipboard(text, button) {
|
| 1018 |
+
const ok = (b) => {
|
| 1019 |
+
const t = b.innerHTML; b.innerHTML = 'β Copied'; b.style.color = '#10b981';
|
| 1020 |
+
setTimeout(() => { b.innerHTML = t; b.style.color = ''; }, 1200);
|
| 1021 |
+
};
|
| 1022 |
+
if (navigator.clipboard && window.isSecureContext) {
|
| 1023 |
+
navigator.clipboard.writeText(text).then(() => ok(button)).catch(() => fb(text, button));
|
| 1024 |
+
} else { fb(text, button); }
|
| 1025 |
+
function fb(t, b) {
|
| 1026 |
+
const ta = document.createElement('textarea');
|
| 1027 |
+
ta.value = t; ta.style.position = 'fixed'; ta.style.left = '-9999px';
|
| 1028 |
+
document.body.appendChild(ta); ta.select();
|
| 1029 |
+
try { document.execCommand('copy'); ok(b); } catch(e) {}
|
| 1030 |
+
document.body.removeChild(ta);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1031 |
}
|
| 1032 |
+
}
|
| 1033 |
+
|
| 1034 |
+
function toggleDesc(btn) {
|
| 1035 |
+
const parent = btn.parentElement;
|
| 1036 |
+
const preview = parent.querySelector('.desc-preview');
|
| 1037 |
+
const full = parent.querySelector('.desc-full');
|
| 1038 |
+
const showingFull = !full.hasAttribute('hidden');
|
| 1039 |
+
if (showingFull) {
|
| 1040 |
+
full.setAttribute('hidden', '');
|
| 1041 |
+
preview.removeAttribute('hidden');
|
| 1042 |
+
btn.textContent = 'Read more';
|
| 1043 |
+
} else {
|
| 1044 |
+
full.removeAttribute('hidden');
|
| 1045 |
+
preview.setAttribute('hidden', '');
|
| 1046 |
+
btn.textContent = 'Show less';
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1047 |
}
|
| 1048 |
+
}
|
| 1049 |
+
|
| 1050 |
+
function voteOnResultSafe(button, voteDecision) {
|
| 1051 |
+
const formal = button.getAttribute('data-formal');
|
| 1052 |
+
const query = button.getAttribute('data-query');
|
| 1053 |
+
const system = button.getAttribute('data-system');
|
| 1054 |
+
const rank = parseInt(button.getAttribute('data-rank'));
|
| 1055 |
+
button.classList.add('voted');
|
| 1056 |
+
const sibling = button.parentElement.querySelector(
|
| 1057 |
+
'.vote-btn.' + (voteDecision === 'Upvote' ? 'downvote' : 'upvote')
|
| 1058 |
+
);
|
| 1059 |
+
if (sibling) { sibling.style.opacity = '0.35'; sibling.style.pointerEvents = 'none'; }
|
| 1060 |
+
const data = JSON.stringify({
|
| 1061 |
+
formal_statement: formal, vote_decision: voteDecision,
|
| 1062 |
+
query: query, system: system, rank: rank
|
| 1063 |
+
});
|
| 1064 |
+
const container = document.querySelector('#vote_trigger_input');
|
| 1065 |
+
if (container) {
|
| 1066 |
+
const input = container.querySelector('textarea') || container.querySelector('input[type="text"]');
|
| 1067 |
+
if (input) {
|
| 1068 |
+
input.value = data;
|
| 1069 |
+
input.dispatchEvent(new Event('input', { bubbles: true }));
|
| 1070 |
+
input.dispatchEvent(new Event('change', { bubbles: true }));
|
| 1071 |
+
setTimeout(function() {
|
| 1072 |
+
const inp = container.querySelector('textarea') || container.querySelector('input[type="text"]');
|
| 1073 |
+
if (inp) { inp.value=''; inp.dispatchEvent(new Event('input',{bubbles:true})); }
|
| 1074 |
+
}, 2500);
|
| 1075 |
}
|
| 1076 |
}
|
| 1077 |
}
|
|
|
|
| 1078 |
|
| 1079 |
+
/* --- Auto-scroll to vote area after search results load --- */
|
| 1080 |
+
function lf_scrollToVote() {
|
| 1081 |
+
const voteArea = document.querySelector('#vote_area');
|
| 1082 |
+
const resultsEl = document.querySelector('.arena-grid') || document.querySelector('.results-single');
|
| 1083 |
+
const target = voteArea && voteArea.offsetParent !== null ? voteArea : resultsEl;
|
| 1084 |
+
if (target) {
|
| 1085 |
+
setTimeout(function() {
|
| 1086 |
+
target.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
| 1087 |
+
}, 120);
|
| 1088 |
+
}
|
| 1089 |
+
}
|
| 1090 |
+
// Observe the results HTML container for changes (Gradio replaces innerHTML)
|
| 1091 |
+
(function() {
|
| 1092 |
+
const observer = new MutationObserver(function(mutations) {
|
| 1093 |
+
for (const m of mutations) {
|
| 1094 |
+
if (m.type === 'childList' && m.addedNodes.length) {
|
| 1095 |
+
const container = m.target.closest ? m.target : m.target.parentElement;
|
| 1096 |
+
if (container && (container.querySelector('.arena-grid') || container.querySelector('.results-single'))) {
|
| 1097 |
+
lf_scrollToVote();
|
| 1098 |
+
break;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1099 |
}
|
| 1100 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1101 |
}
|
| 1102 |
+
});
|
| 1103 |
+
// Start observing once Gradio has mounted
|
| 1104 |
+
function startObserving() {
|
| 1105 |
+
const app = document.querySelector('.gradio-container');
|
| 1106 |
+
if (app) {
|
| 1107 |
+
observer.observe(app, { childList: true, subtree: true });
|
| 1108 |
+
} else {
|
| 1109 |
+
setTimeout(startObserving, 500);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1110 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1111 |
}
|
| 1112 |
+
startObserving();
|
| 1113 |
+
})();
|
| 1114 |
+
|
| 1115 |
+
/* --- Enter key to search --- */
|
| 1116 |
+
(function() {
|
| 1117 |
+
function attachEnter() {
|
| 1118 |
+
const container = document.querySelector('#query_input');
|
| 1119 |
+
if (!container) { setTimeout(attachEnter, 500); return; }
|
| 1120 |
+
const textarea = container.querySelector('textarea');
|
| 1121 |
+
if (!textarea) { setTimeout(attachEnter, 500); return; }
|
| 1122 |
+
textarea.addEventListener('keydown', function(e) {
|
| 1123 |
+
// Enter (without Shift) triggers search
|
| 1124 |
+
if (e.key === 'Enter' && !e.shiftKey) {
|
| 1125 |
+
e.preventDefault();
|
| 1126 |
+
const btn = document.querySelector('#search_btn button');
|
| 1127 |
+
if (btn) btn.click();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1128 |
}
|
| 1129 |
+
});
|
| 1130 |
}
|
| 1131 |
+
if (document.readyState === 'loading') {
|
| 1132 |
+
document.addEventListener('DOMContentLoaded', attachEnter);
|
| 1133 |
+
} else {
|
| 1134 |
+
attachEnter();
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1135 |
}
|
| 1136 |
+
})();
|
| 1137 |
+
</script>
|
| 1138 |
+
"""
|
| 1139 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1140 |
|
| 1141 |
+
with gr.Blocks(title="Lean Finder β Semantic Search for Mathlib",
|
| 1142 |
+
css=CUSTOM_CSS, head=HEAD_HTML, theme=gr.themes.Soft(
|
| 1143 |
+
primary_hue="indigo",
|
| 1144 |
+
neutral_hue="slate",
|
| 1145 |
+
)) as demo:
|
| 1146 |
+
|
| 1147 |
+
gr.HTML(
|
| 1148 |
+
'<div class="lf-hero">'
|
| 1149 |
+
' <div class="lf-hero-main">'
|
| 1150 |
+
' <img class="lf-logo" src="/file=lean_finder_logo.png" alt="Lean Finder logo">'
|
| 1151 |
+
' <div class="lf-hero-text">'
|
| 1152 |
+
' <div class="lf-title">Lean Finder</div>'
|
| 1153 |
+
' <div class="lf-tagline">Semantic Search For Mathlib That Understands User Intents</div>'
|
| 1154 |
+
' </div>'
|
| 1155 |
+
' </div>'
|
| 1156 |
+
' <a class="lf-hero-badge" href="https://arxiv.org/pdf/2510.15940" target="_blank">'
|
| 1157 |
+
' Paper Link'
|
| 1158 |
+
' </a>'
|
| 1159 |
+
'</div>'
|
| 1160 |
+
)
|
| 1161 |
+
|
| 1162 |
+
with gr.Accordion("π What can I search for? (4 query types + Mathlib versions)", open=False):
|
| 1163 |
gr.Markdown(INSTRUCTIONS_MD)
|
| 1164 |
|
| 1165 |
+
query_box = gr.Textbox(
|
| 1166 |
+
label="Your query",
|
| 1167 |
+
lines=4, max_lines=18,
|
| 1168 |
+
placeholder="Describe the Lean statement you're looking for, ask a question, paste a proof state, or sketch a definitionβ¦",
|
| 1169 |
+
elem_id="query_input",
|
| 1170 |
+
show_copy_button=True,
|
| 1171 |
+
)
|
| 1172 |
+
|
| 1173 |
+
with gr.Row(elem_id="controls_row", equal_height=True):
|
| 1174 |
+
version_sel = gr.Dropdown(
|
| 1175 |
+
MATHLIB_VERSIONS,
|
| 1176 |
+
value=DEFAULT_VERSION,
|
| 1177 |
+
label="Mathlib version",
|
| 1178 |
+
scale=1, min_width=140, container=True,
|
| 1179 |
+
)
|
| 1180 |
+
topk_slider = gr.Slider(
|
| 1181 |
+
label="Results",
|
| 1182 |
+
minimum=1, maximum=20, step=1, value=5,
|
| 1183 |
+
scale=2, min_width=180,
|
| 1184 |
+
)
|
| 1185 |
+
mode_sel = gr.Radio(
|
| 1186 |
+
["Arena", "Normal"], value="Arena", label="Mode",
|
| 1187 |
+
scale=2, min_width=180,
|
| 1188 |
+
)
|
| 1189 |
|
| 1190 |
+
run_btn = gr.Button("Search", elem_id="search_btn", variant="primary")
|
| 1191 |
|
| 1192 |
+
mode_description = gr.Markdown(
|
| 1193 |
+
"_**Arena mode** β Lean Finder is compared side-by-side with another model. "
|
| 1194 |
+
"The two models are shown as **Retriever A** and **Retriever B** in random order so the comparison stays fair. "
|
| 1195 |
+
"Vote for the better set overall, or π/π individual results. "
|
| 1196 |
+
"Your votes help us improve Lean Finder β no personally identifiable information is stored._",
|
| 1197 |
+
elem_id="mode_description",
|
| 1198 |
+
)
|
| 1199 |
|
| 1200 |
+
with gr.Row(elem_id="vote_area", visible=False) as vote_row:
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1201 |
with gr.Column():
|
| 1202 |
+
gr.HTML('<div class="vote-area-label">Cast your vote</div>')
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1203 |
vote_radio = gr.Radio(
|
| 1204 |
VOTE_UI,
|
| 1205 |
+
label="Which set of results did better overall?",
|
| 1206 |
+
elem_id="vote_radio",
|
|
|
|
| 1207 |
)
|
| 1208 |
submit_btn = gr.Button(
|
| 1209 |
"Submit vote",
|
|
|
|
| 1210 |
elem_id="submit_btn",
|
|
|
|
| 1211 |
)
|
| 1212 |
vote_status = gr.Markdown("", visible=False, elem_id="vote_status")
|
| 1213 |
|
| 1214 |
+
results_html = gr.HTML(
|
| 1215 |
+
'<div class="empty-state">Type a query above and hit <b>Search</b> '
|
| 1216 |
+
'to see Lean statements from Mathlib.</div>'
|
| 1217 |
+
)
|
| 1218 |
+
|
| 1219 |
+
# Hidden vote bridge
|
| 1220 |
+
vote_trigger = gr.Textbox(visible=True, elem_id="vote_trigger_input")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1221 |
vote_feedback = gr.Markdown("", visible=True, elem_id="vote_feedback")
|
| 1222 |
|
| 1223 |
+
gr.HTML(
|
| 1224 |
+
'<div class="acknowledgements">'
|
| 1225 |
+
' <div class="ack-title">Acknowledgements</div>'
|
| 1226 |
+
' <div class="ack-body">'
|
| 1227 |
+
' Lean Finder is built on open-source projects like '
|
| 1228 |
+
' <a href="https://github.com/frenzymath/jixia" target="_blank">Jixia</a> and '
|
| 1229 |
+
' <a href="https://github.com/lean-dojo/leandojo" target="_blank">LeanDojo</a>. '
|
| 1230 |
+
' We were inspired by '
|
| 1231 |
+
' <a href="https://leansearch.net/" target="_blank">LeanSearch</a>, and we '
|
| 1232 |
+
' use LeanSearch in our Arena mode to let users compare results. '
|
| 1233 |
+
' Thanks to the creators of these awesome tools!'
|
| 1234 |
+
' </div>'
|
| 1235 |
+
'</div>'
|
| 1236 |
+
)
|
| 1237 |
+
|
| 1238 |
+
# Session state
|
| 1239 |
st_query = gr.State("")
|
| 1240 |
+
st_version = gr.State(DEFAULT_VERSION)
|
| 1241 |
+
st_ret_a = gr.State([])
|
| 1242 |
+
st_ret_b = gr.State([])
|
| 1243 |
+
st_a_source = gr.State(LEAN_FINDER)
|
| 1244 |
+
st_b_source = gr.State(LEAN_SEARCH)
|
| 1245 |
+
|
| 1246 |
+
def retrieve(query: str, k: int, version: str, mode: str, progress=gr.Progress()):
|
| 1247 |
+
query = (query or "").strip()
|
| 1248 |
+
hide_radio = gr.update(visible=False, value=None)
|
| 1249 |
+
hide_status = gr.update(visible=False, value="")
|
| 1250 |
+
hide_row = gr.update(visible=False)
|
| 1251 |
|
|
|
|
|
|
|
| 1252 |
if not query:
|
| 1253 |
+
return (
|
| 1254 |
+
'<div class="empty-state">Please enter a query.</div>',
|
| 1255 |
+
"", version, [], [], LEAN_FINDER, LEAN_SEARCH,
|
| 1256 |
+
hide_radio, hide_status, hide_row,
|
| 1257 |
+
)
|
| 1258 |
|
| 1259 |
+
# Call Lean Finder, with cold-start retry loop preserved
|
| 1260 |
try:
|
| 1261 |
+
lf_raw = _call_lean_finder(query, k, version)
|
| 1262 |
except RuntimeError:
|
| 1263 |
+
progress(0, desc="Lean Finder is warming up. Results will appear in ~1β2 min.")
|
| 1264 |
+
start = time.time()
|
| 1265 |
+
lf_raw = None
|
| 1266 |
+
timeout_s = 300
|
| 1267 |
+
phase1 = 120
|
| 1268 |
+
while time.time() - start < timeout_s:
|
| 1269 |
+
elapsed = time.time() - start
|
| 1270 |
+
if elapsed < phase1:
|
| 1271 |
+
progress(elapsed / phase1, desc="Lean Finder is warming up. Results will appear in ~1β2 min.")
|
| 1272 |
+
else:
|
| 1273 |
+
progress(1.0, desc="Slow cold start β almost readyβ¦")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1274 |
try:
|
| 1275 |
+
lf_raw = _call_lean_finder(query, k, version)
|
| 1276 |
break
|
| 1277 |
except RuntimeError:
|
| 1278 |
+
time.sleep(2)
|
| 1279 |
+
if lf_raw is None:
|
| 1280 |
+
err = (
|
| 1281 |
+
'<div class="empty-state" style="color:#b91c1c;border-color:#fecaca;background:#fef2f2;">'
|
| 1282 |
+
'β οΈ Lean Finder is currently unavailable. Please try again in a moment '
|
| 1283 |
+
'or contact <a href="mailto:mike_lu@sfu.ca">mike_lu@sfu.ca</a>.'
|
| 1284 |
+
'</div>'
|
| 1285 |
+
)
|
| 1286 |
+
return err, query, version, [], [], LEAN_FINDER, LEAN_SEARCH, hide_radio, hide_status, hide_row
|
| 1287 |
+
|
| 1288 |
+
lf = _norm_lean_finder(lf_raw)
|
| 1289 |
|
| 1290 |
if mode == "Normal":
|
| 1291 |
+
return (
|
| 1292 |
+
_render_single(lf, query, version),
|
| 1293 |
+
query, version, lf, [], LEAN_FINDER, LEAN_SEARCH,
|
| 1294 |
+
hide_radio, hide_status, hide_row,
|
| 1295 |
+
)
|
| 1296 |
|
| 1297 |
+
# Arena mode
|
| 1298 |
try:
|
| 1299 |
+
ls_raw = _call_lean_search(query, k)
|
| 1300 |
+
ls = _norm_lean_search(ls_raw)
|
| 1301 |
except RuntimeError:
|
| 1302 |
+
notice = (
|
| 1303 |
+
'<div class="empty-state" style="border-color:#fde68a;background:#fffbeb;color:#92400e;">'
|
| 1304 |
+
'β οΈ LeanSearch is unavailable right now β showing Lean Finder results only.'
|
| 1305 |
+
'</div>'
|
| 1306 |
+
)
|
| 1307 |
+
return (
|
| 1308 |
+
notice + _render_single(lf, query, version),
|
| 1309 |
+
query, version, lf, [], LEAN_FINDER, LEAN_SEARCH,
|
| 1310 |
+
hide_radio, hide_status, hide_row,
|
| 1311 |
+
)
|
| 1312 |
+
|
| 1313 |
+
# Random shuffle of A/B so the user can't tell which is which
|
| 1314 |
+
lf_is_a = random.choice([True, False])
|
| 1315 |
+
if lf_is_a:
|
| 1316 |
+
ret_a, ret_b = lf, ls
|
| 1317 |
+
a_src, b_src = LEAN_FINDER, LEAN_SEARCH
|
| 1318 |
else:
|
| 1319 |
+
ret_a, ret_b = ls, lf
|
| 1320 |
+
a_src, b_src = LEAN_SEARCH, LEAN_FINDER
|
| 1321 |
+
|
| 1322 |
+
return (
|
| 1323 |
+
_render_arena(ret_a, ret_b, a_src, b_src, query, version),
|
| 1324 |
+
query, version, ret_a, ret_b, a_src, b_src,
|
| 1325 |
+
gr.update(visible=True, value=None),
|
| 1326 |
+
gr.update(visible=False, value=""),
|
| 1327 |
+
gr.update(visible=True),
|
| 1328 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1329 |
|
| 1330 |
run_btn.click(
|
| 1331 |
retrieve,
|
| 1332 |
+
inputs=[query_box, topk_slider, version_sel, mode_sel],
|
| 1333 |
+
outputs=[results_html, st_query, st_version, st_ret_a, st_ret_b,
|
| 1334 |
+
st_a_source, st_b_source,
|
| 1335 |
+
vote_radio, vote_status, vote_row],
|
| 1336 |
)
|
| 1337 |
|
| 1338 |
+
def _on_mode_change(mode):
|
| 1339 |
if mode == "Arena":
|
| 1340 |
+
desc = (
|
| 1341 |
+
"_**Arena mode** β Lean Finder vs. another model, shown in random order as Retriever A / B. "
|
| 1342 |
+
"Vote for the better set or π/π individual results. "
|
| 1343 |
+
"Votes help improve Lean Finder β we do not collect IP addresses or any personally identifiable information._"
|
| 1344 |
+
)
|
| 1345 |
else:
|
| 1346 |
+
desc = (
|
| 1347 |
+
"_Normal mode β Lean Finder only. Quick results, no voting._"
|
| 1348 |
+
)
|
| 1349 |
return (
|
| 1350 |
+
'<div class="empty-state">Type a query above and hit <b>Search</b> '
|
| 1351 |
+
'to see Lean statements from Mathlib.</div>',
|
| 1352 |
gr.update(visible=False, value=None),
|
| 1353 |
gr.update(visible=False),
|
| 1354 |
gr.update(visible=False, value=""),
|
| 1355 |
+
desc,
|
| 1356 |
)
|
| 1357 |
|
| 1358 |
mode_sel.change(
|
| 1359 |
+
_on_mode_change,
|
| 1360 |
inputs=mode_sel,
|
| 1361 |
+
outputs=[results_html, vote_radio, vote_row, vote_status, mode_description],
|
| 1362 |
)
|
| 1363 |
|
| 1364 |
submit_btn.click(
|
| 1365 |
_save_vote,
|
| 1366 |
+
inputs=[vote_radio, st_query, st_version, st_ret_a, st_ret_b,
|
| 1367 |
+
st_a_source, st_b_source],
|
| 1368 |
+
outputs=vote_status,
|
| 1369 |
)
|
| 1370 |
+
|
| 1371 |
+
def handle_individual_vote(vote_data: str, query: str, version: str,
|
| 1372 |
+
ret_a: List[Dict[str, Any]], ret_b: List[Dict[str, Any]],
|
| 1373 |
+
a_source: str, b_source: str) -> str:
|
| 1374 |
if not vote_data:
|
| 1375 |
return ""
|
| 1376 |
try:
|
| 1377 |
data = json.loads(vote_data)
|
| 1378 |
+
# The on-card "system" attribute holds the real source name (Lean Finder / LeanSearch)
|
| 1379 |
+
# because we already pass it through when rendering arena cards.
|
| 1380 |
+
system = data.get("system", "")
|
| 1381 |
payload = {
|
| 1382 |
+
"retriever_a": ret_a,
|
| 1383 |
+
"retriever_b": ret_b,
|
| 1384 |
+
"a_source": a_source,
|
| 1385 |
+
"b_source": b_source,
|
| 1386 |
+
"individual_vote": data,
|
| 1387 |
}
|
|
|
|
| 1388 |
_save_individual_vote(
|
| 1389 |
data["formal_statement"],
|
| 1390 |
data["vote_decision"],
|
| 1391 |
data["query"],
|
| 1392 |
+
system,
|
| 1393 |
data["rank"],
|
| 1394 |
+
version,
|
| 1395 |
+
payload,
|
| 1396 |
)
|
| 1397 |
+
return f"β
**{data['vote_decision']}** recorded for rank {data['rank']}. Thanks!"
|
| 1398 |
except (json.JSONDecodeError, KeyError) as e:
|
| 1399 |
+
return f"Error saving vote: {e}"
|
| 1400 |
except Exception as e:
|
| 1401 |
+
return f"ERROR: {e}"
|
| 1402 |
+
|
| 1403 |
vote_trigger.change(
|
| 1404 |
handle_individual_vote,
|
| 1405 |
+
inputs=[vote_trigger, st_query, st_version, st_ret_a, st_ret_b,
|
| 1406 |
+
st_a_source, st_b_source],
|
| 1407 |
+
outputs=vote_feedback,
|
| 1408 |
)
|
| 1409 |
|
| 1410 |
+
|
| 1411 |
if __name__ == "__main__":
|
| 1412 |
+
demo.launch(allowed_paths=["lean_finder_logo.png"])
|
requirements.txt
CHANGED
|
@@ -1,3 +1,4 @@
|
|
| 1 |
huggingface_hub>=0.26.0,<1
|
| 2 |
gspread
|
| 3 |
-
google-auth
|
|
|
|
|
|
| 1 |
huggingface_hub>=0.26.0,<1
|
| 2 |
gspread
|
| 3 |
+
google-auth
|
| 4 |
+
requests
|