yangzhitao commited on
Commit
ae66b87
·
1 Parent(s): fa763b7

feat: enhance backend status indicator and refactor app structure

Browse files

- Introduced a dedicated backend status indicator with visual states (undefined, healthy, unhealthy) using HTML and CSS.
- Refactored app.py to streamline the Gradio interface and integrate the new status indicator.
- Added JavaScript functionality for automatic backend health updates every 30 seconds.
- Removed deprecated backend status logic and CSS from custom.css, consolidating styles into a new backend-status-indicator.css file.
- Updated the backend status retrieval logic to utilize new HTML templates for better maintainability.

app.py CHANGED
@@ -19,7 +19,12 @@ from src.about import (
19
  TITLE,
20
  )
21
  from src.backend.app import create_app
22
- from src.display.css_html_js import backend_status_js, custom_css
 
 
 
 
 
23
  from src.display.utils import (
24
  BASE_COLS,
25
  BENCHMARK_COLS,
@@ -40,58 +45,6 @@ def restart_space():
40
  API.restart_space(repo_id=settings.REPO_ID)
41
 
42
 
43
- def get_backend_status_undefined_html() -> str:
44
- """
45
- 返回未定义状态(首次检查前)的 HTML
46
- """
47
- return """
48
- <div id="backend-status-indicator">
49
- <span class="backend-status-light undefined"></span>
50
- <span>Backend Status: Checking...</span>
51
- </div>
52
- """
53
-
54
-
55
- def check_backend_health() -> tuple[bool, str]:
56
- """
57
- 查询后端健康状态
58
- 返回: (is_healthy, status_html)
59
- """
60
- try:
61
- response = requests.get(f"http://localhost:{settings.BACKEND_PORT}/api/v1/health/", timeout=2)
62
- if response.status_code == 200:
63
- data = response.json()
64
- if data.get("code") == 0:
65
- return (
66
- True,
67
- """
68
- <div id="backend-status-indicator">
69
- <span class="backend-status-light healthy"></span>
70
- <span>Backend Status: Healthy</span>
71
- </div>
72
- """,
73
- )
74
- return (
75
- False,
76
- """
77
- <div id="backend-status-indicator">
78
- <span class="backend-status-light unhealthy"></span>
79
- <span>Backend Status: Unhealthy</span>
80
- </div>
81
- """,
82
- )
83
- except Exception:
84
- return (
85
- False,
86
- """
87
- <div id="backend-status-indicator">
88
- <span class="backend-status-light unhealthy"></span>
89
- <span>Backend Status: Unavailable</span>
90
- </div>
91
- """,
92
- )
93
-
94
-
95
  print("///// --- Settings --- /////", settings.model_dump())
96
 
97
  # Space initialisation
@@ -311,187 +264,216 @@ def init_leaderboard_tabs(dataframe: pd.DataFrame, cols: list[str]):
311
  return leaderboard
312
 
313
 
314
- demo = gr.Blocks(css_paths=[custom_css])
315
- with demo:
316
- gr.HTML(TITLE)
317
- gr.Markdown(INTRODUCTION_TEXT, elem_classes="markdown-text")
318
-
319
- with gr.Tabs(elem_classes="tab-buttons") as tabs:
320
- for i, benchmark in enumerate[str](sorted(BENCHMARKS)):
321
- with gr.TabItem(f"🏅 {benchmark}", elem_id="llm-benchmark-tab-table", id=i):
322
- benchmark_cols = [
323
- BENCHMARK_COL for BENCHMARK_COL in BENCHMARK_COLS if BENCHMARK_COL.startswith(benchmark)
324
- ]
325
- cols = BASE_COLS + benchmark_cols
326
- BENCHMARK_DF = get_leaderboard_df(
327
- settings.EVAL_RESULTS_PATH,
328
- settings.EVAL_REQUESTS_PATH,
329
- cols,
330
- benchmark_cols,
331
- )
332
- leaderboard = init_leaderboard_tabs(BENCHMARK_DF, benchmark_cols)
333
-
334
- with gr.TabItem("📝 About", elem_id="about-tab", id=len(BENCHMARKS)):
335
- gr.Markdown(LLM_BENCHMARKS_TEXT, elem_classes="markdown-text")
336
-
337
- with gr.TabItem("🚀 Submit here! ", elem_id="submit-tab", id=len(BENCHMARKS) + 1):
338
- # Backend status indicator - 初始状态为 undefined
339
- backend_status = gr.HTML(value=get_backend_status_undefined_html(), elem_id="backend-status-container")
340
-
341
- # 定时更新后端状态
342
- def update_backend_status():
343
- return check_backend_health()[1]
344
-
345
- # 创建一个隐藏的按钮用于定时触发更新
346
- status_trigger = gr.Button(visible=False, elem_id="backend-status-trigger-btn")
347
 
348
- # 绑定按钮点击事件来更新状态
349
- status_trigger.click(
350
- fn=update_backend_status,
351
- inputs=None,
352
- outputs=backend_status,
353
- )
354
 
355
- # 加载外部 JavaScript 文件
356
- js_content = backend_status_js.read_text(encoding="utf-8")
357
- status_trigger_js_html = f'<script>{js_content}</script>'
358
- gr.HTML(status_trigger_js_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
359
 
360
- # 页面加载时立即检查一次
361
- demo.load(
362
- fn=update_backend_status,
363
- inputs=None,
364
- outputs=backend_status,
365
- )
366
 
367
- with gr.Column():
368
  with gr.Row():
369
- gr.Markdown(EVALUATION_QUEUE_TEXT, elem_classes="markdown-text")
 
 
 
 
 
 
370
 
371
- with gr.Column():
372
- with gr.Accordion(
373
- f"✅ Finished Evaluations ({len(finished_eval_queue_df)})",
374
- open=False,
375
- ):
376
- with gr.Row():
377
- finished_eval_table = grc.Dataframe(
378
- value=finished_eval_queue_df,
379
- headers=EVAL_COLS,
380
- datatype=EVAL_TYPES,
381
- row_count=5,
382
- )
383
- with gr.Accordion(
384
- f"🔄 Running Evaluation Queue ({len(running_eval_queue_df)})",
385
- open=False,
386
- ):
387
- with gr.Row():
388
- running_eval_table = grc.Dataframe(
389
- value=running_eval_queue_df,
390
- headers=EVAL_COLS,
391
- datatype=EVAL_TYPES,
392
- row_count=5,
393
- )
394
-
395
- with gr.Accordion(
396
- f"⏳ Pending Evaluation Queue ({len(pending_eval_queue_df)})",
397
- open=False,
398
- ):
399
- with gr.Row():
400
- pending_eval_table = grc.Dataframe(
401
- value=pending_eval_queue_df,
402
- headers=EVAL_COLS,
403
- datatype=EVAL_TYPES,
404
- row_count=5,
405
- )
406
- with gr.Row():
407
- gr.Markdown("# ✉️✨ Submit your model here!", elem_classes="markdown-text")
408
-
409
- with gr.Row():
410
- search_name = gr.Textbox(label="search model name", placeholder="user/model_name")
411
-
412
- with gr.Row():
413
- table = gr.Dataframe(
414
- headers=["Model Name", "Pipeline", "Downloads", "Likes"],
415
- datatype=["str", "str", "number", "number"],
416
- interactive=False,
417
- wrap=True,
418
- label="click model name to select",
 
 
 
 
 
 
 
 
 
 
 
 
419
  )
420
 
421
- with gr.Row():
422
- with gr.Column():
423
- model_name_textbox = gr.Textbox(label="Model name", placeholder="user/model_name")
424
- revision_name_textbox = gr.Textbox(label="Revision commit", placeholder="main")
425
- model_type = gr.Dropdown(
426
- choices=[t.to_str(" : ") for t in ModelType if t != ModelType.Unknown],
427
- label="Model type",
428
- multiselect=False,
429
- value=None,
430
- interactive=True,
431
- )
432
 
433
- def search_models(query):
434
- if not query.strip():
435
- return []
436
- models = API.list_models(search=query, limit=10)
437
- results = []
438
- for m in models:
439
- results.append([m.id, m.pipeline_tag or "N/A", m.downloads or 0, m.likes or 0])
440
- return results
 
 
 
 
 
 
 
 
 
 
 
 
 
 
441
 
442
- def on_select(evt: gr.SelectData, data):
443
- row_idx = evt.index[0] # 获取点击行号
444
- if row_idx < len(data):
445
- return data.iloc[row_idx, 0] # 返回模型名
446
- return ""
447
 
448
- search_name.change(fn=search_models, inputs=search_name, outputs=table)
449
- table.select(fn=on_select, inputs=table, outputs=model_name_textbox)
 
 
 
450
 
451
- with gr.Column():
452
- precision = gr.Dropdown(
453
- choices=[i.value.name for i in Precision if i != Precision.Unknown],
454
- label="Precision",
455
- multiselect=False,
456
- value="float16",
457
- interactive=True,
458
- )
459
- weight_type = gr.Dropdown(
460
- choices=[i.value.name for i in WeightType],
461
- label="Weights type",
462
- multiselect=False,
463
- value="Original",
464
- interactive=True,
465
- )
466
- base_model_name_textbox = gr.Textbox(label="Base model (for delta or adapter weights)")
467
-
468
- submit_button = gr.Button("Submit Eval")
469
- submission_result = gr.Markdown()
470
- submit_button.click(
471
- add_new_eval,
472
- [
473
- model_name_textbox,
474
- base_model_name_textbox,
475
- revision_name_textbox,
476
- precision,
477
- weight_type,
478
- model_type,
479
- ],
480
- submission_result,
481
- )
482
 
483
- with gr.Row():
484
- with gr.Accordion("📙 Citation", open=False):
485
- citation_button = gr.Textbox(
486
- value=CITATION_BUTTON_TEXT,
487
- label=CITATION_BUTTON_LABEL,
488
- lines=20,
489
- elem_id="citation-button",
490
- show_copy_button=True,
491
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
492
 
493
 
494
  if __name__ == "__main__":
 
 
495
  # Backend server - 在单独的线程中运行
496
  app = create_app()
497
 
 
19
  TITLE,
20
  )
21
  from src.backend.app import create_app
22
+ from src.display.css_html_js import (
23
+ backend_status_indicator_css,
24
+ backend_status_indicator_html,
25
+ backend_status_js,
26
+ custom_css,
27
+ )
28
  from src.display.utils import (
29
  BASE_COLS,
30
  BENCHMARK_COLS,
 
45
  API.restart_space(repo_id=settings.REPO_ID)
46
 
47
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
48
  print("///// --- Settings --- /////", settings.model_dump())
49
 
50
  # Space initialisation
 
264
  return leaderboard
265
 
266
 
267
+ def main():
268
+ demo = gr.Blocks(css_paths=[custom_css, backend_status_indicator_css])
269
+ with demo:
270
+ gr.HTML(TITLE)
271
+ gr.Markdown(INTRODUCTION_TEXT, elem_classes="markdown-text")
272
+
273
+ with gr.Tabs(elem_classes="tab-buttons") as _tabs:
274
+ for i, benchmark in enumerate[str](sorted(BENCHMARKS)):
275
+ with gr.TabItem(f"🏅 {benchmark}", elem_id="llm-benchmark-tab-table", id=i):
276
+ benchmark_cols = [
277
+ BENCHMARK_COL for BENCHMARK_COL in BENCHMARK_COLS if BENCHMARK_COL.startswith(benchmark)
278
+ ]
279
+ cols = BASE_COLS + benchmark_cols
280
+ BENCHMARK_DF = get_leaderboard_df(
281
+ settings.EVAL_RESULTS_PATH,
282
+ settings.EVAL_REQUESTS_PATH,
283
+ cols,
284
+ benchmark_cols,
285
+ )
286
+ _leaderboard = init_leaderboard_tabs(BENCHMARK_DF, benchmark_cols)
 
 
 
 
 
 
 
 
 
 
 
 
 
287
 
288
+ with gr.TabItem("📝 About", elem_id="about-tab", id=len(BENCHMARKS)):
289
+ gr.Markdown(LLM_BENCHMARKS_TEXT, elem_classes="markdown-text")
 
 
 
 
290
 
291
+ with gr.TabItem("🚀 Submit here! ", elem_id="submit-tab", id=len(BENCHMARKS) + 1):
292
+ with gr.Column():
293
+ with gr.Row():
294
+ gr.Markdown(EVALUATION_QUEUE_TEXT, elem_classes="markdown-text")
295
+
296
+ with gr.Column():
297
+ with gr.Accordion(
298
+ f"✅ Finished Evaluations ({len(finished_eval_queue_df)})",
299
+ open=False,
300
+ ):
301
+ with gr.Row():
302
+ _finished_eval_table = grc.Dataframe(
303
+ value=finished_eval_queue_df,
304
+ headers=EVAL_COLS,
305
+ datatype=EVAL_TYPES,
306
+ row_count=5,
307
+ )
308
+ with gr.Accordion(
309
+ f"🔄 Running Evaluation Queue ({len(running_eval_queue_df)})",
310
+ open=False,
311
+ ):
312
+ with gr.Row():
313
+ _running_eval_table = grc.Dataframe(
314
+ value=running_eval_queue_df,
315
+ headers=EVAL_COLS,
316
+ datatype=EVAL_TYPES,
317
+ row_count=5,
318
+ )
319
+
320
+ with gr.Accordion(
321
+ f"⏳ Pending Evaluation Queue ({len(pending_eval_queue_df)})",
322
+ open=False,
323
+ ):
324
+ with gr.Row():
325
+ _pending_eval_table = grc.Dataframe(
326
+ value=pending_eval_queue_df,
327
+ headers=EVAL_COLS,
328
+ datatype=EVAL_TYPES,
329
+ row_count=5,
330
+ )
331
+ with gr.Row():
332
+ gr.Markdown("# ✉️✨ Submit your model here!", elem_classes="markdown-text")
333
 
334
+ with gr.Row():
335
+ search_name = gr.Textbox(label="search model name", placeholder="user/model_name")
 
 
 
 
336
 
 
337
  with gr.Row():
338
+ table = gr.Dataframe(
339
+ headers=["Model Name", "Pipeline", "Downloads", "Likes"],
340
+ datatype=["str", "str", "number", "number"],
341
+ interactive=False,
342
+ wrap=True,
343
+ label="click model name to select",
344
+ )
345
 
346
+ with gr.Row():
347
+ with gr.Column():
348
+ model_name_textbox = gr.Textbox(label="Model name", placeholder="user/model_name")
349
+ revision_name_textbox = gr.Textbox(label="Revision commit", placeholder="main")
350
+ model_type = gr.Dropdown(
351
+ choices=[t.to_str(" : ") for t in ModelType if t != ModelType.Unknown],
352
+ label="Model type",
353
+ multiselect=False,
354
+ value=None,
355
+ interactive=True,
356
+ )
357
+
358
+ def search_models(query):
359
+ if not query.strip():
360
+ return []
361
+ models = API.list_models(search=query, limit=10)
362
+ results = []
363
+ for m in models:
364
+ results.append([m.id, m.pipeline_tag or "N/A", m.downloads or 0, m.likes or 0])
365
+ return results
366
+
367
+ def on_select(evt: gr.SelectData, data):
368
+ row_idx = evt.index[0] # 获取点击行号
369
+ if row_idx < len(data):
370
+ return data.iloc[row_idx, 0] # 返回模型名
371
+ return ""
372
+
373
+ search_name.change(fn=search_models, inputs=search_name, outputs=table)
374
+ table.select(fn=on_select, inputs=table, outputs=model_name_textbox)
375
+
376
+ with gr.Column():
377
+ precision = gr.Dropdown(
378
+ choices=[i.value.name for i in Precision if i != Precision.Unknown],
379
+ label="Precision",
380
+ multiselect=False,
381
+ value="float16",
382
+ interactive=True,
383
+ )
384
+ weight_type = gr.Dropdown(
385
+ choices=[i.value.name for i in WeightType],
386
+ label="Weights type",
387
+ multiselect=False,
388
+ value="Original",
389
+ interactive=True,
390
+ )
391
+ base_model_name_textbox = gr.Textbox(label="Base model (for delta or adapter weights)")
392
+
393
+ submit_button = gr.Button("Submit Eval")
394
+ submission_result = gr.Markdown()
395
+ submit_button.click(
396
+ add_new_eval,
397
+ [
398
+ model_name_textbox,
399
+ base_model_name_textbox,
400
+ revision_name_textbox,
401
+ precision,
402
+ weight_type,
403
+ model_type,
404
+ ],
405
+ submission_result,
406
  )
407
 
408
+ with gr.Row():
409
+ with gr.Accordion("📙 Citation", open=False):
410
+ _citation_button = gr.Textbox(
411
+ value=CITATION_BUTTON_TEXT,
412
+ label=CITATION_BUTTON_LABEL,
413
+ lines=20,
414
+ elem_id="citation-button",
415
+ show_copy_button=True,
416
+ )
 
 
417
 
418
+ # Backend status indicator
419
+ backend_status = gr.HTML(
420
+ value=get_backend_status_undefined_html(),
421
+ elem_id="backend-status-container",
422
+ )
423
+ # trigger button to bind the click event
424
+ status_trigger = gr.Button(elem_id="backend-status-trigger-btn", visible=False)
425
+ status_trigger.click(
426
+ fn=lambda: check_backend_health()[1],
427
+ inputs=None,
428
+ outputs=backend_status,
429
+ )
430
+ # load external JavaScript file
431
+ js_content = backend_status_js()
432
+ status_trigger_js_html = f'<script>{js_content}</script>'
433
+ gr.HTML(status_trigger_js_html, visible=False)
434
+ demo.load(
435
+ fn=lambda: check_backend_health()[1],
436
+ inputs=None,
437
+ outputs=backend_status,
438
+ )
439
+ return demo
440
 
 
 
 
 
 
441
 
442
+ def get_backend_status_undefined_html() -> str:
443
+ """
444
+ 返回未定义状态(首次检查前)的 HTML
445
+ """
446
+ return backend_status_indicator_html("undefined")
447
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
448
 
449
+ def check_backend_health() -> tuple[bool, str]:
450
+ """
451
+ 查询后端健康状态
452
+ 返回: (is_healthy, status_html)
453
+ """
454
+ try:
455
+ response = requests.get(f"http://localhost:{settings.BACKEND_PORT}/api/v1/health/", timeout=2)
456
+ if response.status_code == 200:
457
+ data = response.json()
458
+ if data.get("code") == 0:
459
+ return (
460
+ True,
461
+ backend_status_indicator_html("healthy"),
462
+ )
463
+ return (
464
+ False,
465
+ backend_status_indicator_html("unhealthy"),
466
+ )
467
+ except Exception:
468
+ return (
469
+ False,
470
+ backend_status_indicator_html("unhealthy"),
471
+ )
472
 
473
 
474
  if __name__ == "__main__":
475
+ demo = main()
476
+
477
  # Backend server - 在单独的线程中运行
478
  app = create_app()
479
 
src/assets/css/backend-status-indicator.css ADDED
@@ -0,0 +1,41 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ /* Backend status indicator - breathing light animation */
2
+ #backend-status-indicator {
3
+ display: flex;
4
+ align-items: center;
5
+ gap: 10px;
6
+ padding: 10px;
7
+ }
8
+
9
+ .backend-status-light {
10
+ width: 12px;
11
+ height: 12px;
12
+ border-radius: 75%;
13
+ display: inline-block;
14
+ animation: breathing 2s ease-in-out infinite;
15
+ }
16
+
17
+ .backend-status-light.undefined {
18
+ background-color: #6b7280;
19
+ box-shadow: 0 0 10px rgba(107, 114, 128, 0.5);
20
+ }
21
+
22
+ .backend-status-light.healthy {
23
+ background-color: #10b981;
24
+ box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
25
+ }
26
+
27
+ .backend-status-light.unhealthy {
28
+ background-color: #ef4444;
29
+ box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
30
+ }
31
+
32
+ @keyframes breathing {
33
+ 0%, 100% {
34
+ opacity: 1;
35
+ transform: scale(1);
36
+ }
37
+ 50% {
38
+ opacity: 0.6;
39
+ transform: scale(1.1);
40
+ }
41
+ }
src/assets/css/custom.css CHANGED
@@ -105,45 +105,3 @@
105
  overflow: hidden !important;
106
  text-overflow: ellipsis !important;
107
  }
108
-
109
- /* Backend status indicator - breathing light animation */
110
- #backend-status-indicator {
111
- display: flex;
112
- align-items: center;
113
- gap: 10px;
114
- padding: 10px;
115
- }
116
-
117
- .backend-status-light {
118
- width: 16px;
119
- height: 16px;
120
- border-radius: 50%;
121
- display: inline-block;
122
- animation: breathing 2s ease-in-out infinite;
123
- }
124
-
125
- .backend-status-light.undefined {
126
- background-color: #6b7280;
127
- box-shadow: 0 0 10px rgba(107, 114, 128, 0.5);
128
- }
129
-
130
- .backend-status-light.healthy {
131
- background-color: #10b981;
132
- box-shadow: 0 0 10px rgba(16, 185, 129, 0.5);
133
- }
134
-
135
- .backend-status-light.unhealthy {
136
- background-color: #ef4444;
137
- box-shadow: 0 0 10px rgba(239, 68, 68, 0.5);
138
- }
139
-
140
- @keyframes breathing {
141
- 0%, 100% {
142
- opacity: 1;
143
- transform: scale(1);
144
- }
145
- 50% {
146
- opacity: 0.6;
147
- transform: scale(1.1);
148
- }
149
- }
 
105
  overflow: hidden !important;
106
  text-overflow: ellipsis !important;
107
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
src/assets/html/backend-status-indicator.healthy.html ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <div id="backend-status-indicator">
2
+ <span class="backend-status-light healthy"></span>
3
+ <span>Backend Status: Healthy</span>
4
+ </div>
src/assets/html/backend-status-indicator.undefined.html ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <div id="backend-status-indicator">
2
+ <span class="backend-status-light undefined"></span>
3
+ <span>Backend Status: Checking...</span>
4
+ </div>
src/assets/html/backend-status-indicator.unhealthy.html ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ <div id="backend-status-indicator">
2
+ <span class="backend-status-light unhealthy"></span>
3
+ <span>Backend Status: Unhealthy</span>
4
+ </div>
src/assets/js/{backend_status.js → backend-status.js} RENAMED
File without changes
src/display/css_html_js.py CHANGED
@@ -1,13 +1,54 @@
 
 
1
  from pathlib import Path
 
2
 
3
- custom_css = Path("src/assets/css/custom.css")
4
- backend_status_js = Path("src/assets/js/backend_status.js")
5
 
6
- # FIXME: seems deprecated
7
- get_window_url_params = """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
8
  function(url_params) {
9
  const params = new URLSearchParams(window.location.search);
10
  url_params = Object.fromEntries(params);
11
  return url_params;
12
  }
13
  """
 
 
 
 
1
+ import warnings
2
+ from functools import lru_cache
3
  from pathlib import Path
4
+ from typing import Literal
5
 
6
+ PROJECT_ROOT = Path(__file__).parents[2]
 
7
 
8
+ CSS_FOLDER = PROJECT_ROOT / "src/assets/css"
9
+ if not CSS_FOLDER.exists():
10
+ warnings.warn(f"CSS folder not found: {CSS_FOLDER.as_posix()!r}. Please check the folder exists.", stacklevel=2)
11
+ JS_FOLDER = PROJECT_ROOT / "src/assets/js"
12
+ if not JS_FOLDER.exists():
13
+ warnings.warn(
14
+ f"JavaScript folder not found: {JS_FOLDER.as_posix()!r}. Please check the folder exists.", stacklevel=2
15
+ )
16
+ HTML_FOLDER = PROJECT_ROOT / "src/assets/html"
17
+ if not HTML_FOLDER.exists():
18
+ warnings.warn(f"HTML folder not found: {HTML_FOLDER.as_posix()!r}. Please check the folder exists.", stacklevel=2)
19
+
20
+
21
+ custom_css = CSS_FOLDER / "custom.css"
22
+
23
+
24
+ # ------------------------------------------------------------
25
+ # Backend status indicator
26
+ # ------------------------------------------------------------
27
+ @lru_cache(maxsize=1)
28
+ def backend_status_js() -> str:
29
+ file_path = JS_FOLDER / "backend-status.js"
30
+ if not file_path.exists():
31
+ raise FileNotFoundError(f"JavaScript file not found: {file_path.as_posix()!r}. Please check the file exists.")
32
+ return file_path.read_text(encoding="utf-8")
33
+
34
+
35
+ @lru_cache(maxsize=3)
36
+ def backend_status_indicator_html(status: Literal["healthy", "unhealthy", "undefined"]) -> str:
37
+ file_path = HTML_FOLDER / f"backend-status-indicator.{status}.html"
38
+ if not file_path.exists():
39
+ raise FileNotFoundError(f"HTML file not found: {file_path.as_posix()!r}. Please check the file exists.")
40
+ return file_path.read_text(encoding="utf-8")
41
+
42
+
43
+ @lru_cache(maxsize=1)
44
+ def get_window_url_params() -> str:
45
+ return """
46
  function(url_params) {
47
  const params = new URLSearchParams(window.location.search);
48
  url_params = Object.fromEntries(params);
49
  return url_params;
50
  }
51
  """
52
+
53
+
54
+ backend_status_indicator_css = CSS_FOLDER / "backend-status-indicator.css"