ArthurSrz commited on
Commit
ce4addb
·
verified ·
1 Parent(s): 77fdc03

redesign: polished card components — subtle shadows, refined badges, navy accents

Browse files
Files changed (1) hide show
  1. ui_components.py +101 -73
ui_components.py CHANGED
@@ -1,18 +1,53 @@
1
  """
2
  ui_components.py — HTML card builders for each legal source type + tab panel.
 
3
  """
4
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5
 
6
- def build_article_card(result: dict, related_decisions: list[dict] | None = None) -> str:
7
- code = result.get("code_name", "Code")
8
- num = result.get("num", result.get("id_legifrance", ""))
9
- snippet = (result.get("chunk_text") or "")[:200]
10
- date = (result.get("article_dateDebut") or "")[:10]
11
- lf_id = result.get("id_legifrance", "")
12
- url = f"https://www.legifrance.gouv.fr/codes/article_lc/{lf_id}" if lf_id else "#"
13
- etat = result.get("article_etat", "")
14
 
15
- etat_badge = f'<span style="font-size:11px;color:#6b7280;margin-left:6px">{etat}</span>' if etat else ""
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  cross_ref_html = ""
18
  if related_decisions:
@@ -23,31 +58,31 @@ def build_article_card(result: dict, related_decisions: list[dict] | None = None
23
  dec_jur = dec.get("jurisdiction", "")
24
  dec_snip = dec.get("chunk_text", "")[:120]
25
  mini_cards += f"""
26
- <div style="border-left:3px solid #6366f1;padding:6px 10px;margin-top:6px;font-size:12px;color:#374151">
 
27
  <strong>{dec_jur}</strong> · {dec_date}
28
- <div style="color:#6b7280;margin-top:2px">{dec_snip}…</div>
29
- <a href="{dec_url}" target="_blank" style="color:#6366f1;font-size:11px">→ Cour de cassation</a>
 
30
  </div>"""
31
  n = len(related_decisions)
32
  cross_ref_html = f"""
33
- <details style="margin-top:8px">
34
- <summary style="cursor:pointer;color:#6366f1;font-size:13px;font-weight:600">
35
- Voir les décisions ({n})
36
  </summary>
37
  {mini_cards}
38
  </details>"""
39
 
40
  return f"""
41
- <div data-article-id="{lf_id}" style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 16px;margin-bottom:10px;background:#fff">
42
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
43
- <span style="background:#dbeafe;color:#1d4ed8;font-size:11px;font-weight:700;padding:2px 8px;border-radius:12px">{code}</span>
44
- <strong style="font-size:14px">Art. {num}</strong>{etat_badge}
45
- </div>
46
- <p style="font-size:13px;color:#374151;margin:0 0 8px">{snippet}…</p>
47
- <div style="font-size:12px;color:#9ca3af">
48
- 📅 {date} &nbsp;|&nbsp;
49
- <a href="{url}" target="_blank" style="color:#2563eb">🔗 Légifrance</a>
50
  </div>
 
 
51
  {cross_ref_html}
52
  </div>"""
53
 
@@ -57,74 +92,61 @@ def build_decision_card(result: dict) -> str:
57
  chamber = result.get("chamber", "")
58
  date = result.get("date_decision", "")
59
  fiche = result.get("fiche_arret") or ""
60
- snippet = fiche[:200] if fiche else (result.get("chunk_text") or "")[:200]
61
  url = result.get("url_judilibre", "#")
62
  src_id = result.get("source_id", "")
63
 
64
- badge_label = f"{juris}" + (f" | {chamber}" if chamber else "")
65
 
66
  return f"""
67
- <div style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 16px;margin-bottom:10px;background:#fff">
68
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
69
- <span style="background:#fce7f3;color:#be185d;font-size:11px;font-weight:700;padding:2px 8px;border-radius:12px">{badge_label}</span>
70
- <strong style="font-size:13px">{date}</strong>
71
- {f'<span style="font-size:11px;color:#6b7280">n° {src_id}</span>' if src_id else ""}
72
- </div>
73
- <p style="font-size:13px;color:#374151;margin:0 0 8px">{snippet}…</p>
74
- <div style="font-size:12px;color:#9ca3af">
75
- 📅 {date} &nbsp;|&nbsp;
76
- <a href="{url}" target="_blank" style="color:#2563eb">🔗 Cour de cassation</a>
77
  </div>
 
 
78
  </div>"""
79
 
80
 
81
  def build_circulaire_card(result: dict) -> str:
82
  ministere = result.get("ministere", "")
83
  numero = result.get("numero", result.get("source_id", ""))
84
- objet = (result.get("objet") or result.get("chunk_text") or "")[:200]
85
  date = (result.get("date_parution") or "")[:10]
86
  url = result.get("url_legifrance", "#")
87
 
88
  return f"""
89
- <div style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 16px;margin-bottom:10px;background:#fff">
90
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
91
- <span style="background:#d1fae5;color:#065f46;font-size:11px;font-weight:700;padding:2px 8px;border-radius:12px">Ministère : {ministere}</span>
92
- <strong style="font-size:13px">Circ. n° {numero}</strong>
93
- </div>
94
- <p style="font-size:13px;color:#374151;margin:0 0 8px">{objet}…</p>
95
- <div style="font-size:12px;color:#9ca3af">
96
- 📅 {date} &nbsp;|&nbsp;
97
- <a href="{url}" target="_blank" style="color:#2563eb">🔗 Légifrance</a>
98
  </div>
 
 
99
  </div>"""
100
 
101
 
102
  def build_reponse_card(result: dict) -> str:
103
  ministere = result.get("ministere", "")
104
  num_q = result.get("numero_question", result.get("source_id", ""))
105
- question = (result.get("question_text") or result.get("chunk_text") or "")[:200]
106
  date = (result.get("date_reponse") or "")[:10]
107
  url = result.get("url_legifrance", "#")
108
 
109
  return f"""
110
- <div style="border:1px solid #e5e7eb;border-radius:8px;padding:12px 16px;margin-bottom:10px;background:#fff">
111
- <div style="display:flex;align-items:center;gap:8px;margin-bottom:6px">
112
- <span style="background:#fef3c7;color:#92400e;font-size:11px;font-weight:700;padding:2px 8px;border-radius:12px">{ministere}</span>
113
- <strong style="font-size:13px">Q. n° {num_q}</strong>
114
- </div>
115
- <p style="font-size:13px;color:#374151;margin:0 0 8px">{question}…</p>
116
- <div style="font-size:12px;color:#9ca3af">
117
- 📅 {date} &nbsp;|&nbsp;
118
- <a href="{url}" target="_blank" style="color:#2563eb">🔗 Légifrance</a>
119
  </div>
 
 
120
  </div>"""
121
 
122
 
123
  def build_tabs_html(results_dict: dict, loading_status: dict) -> str:
124
- """
125
- Build a 4-tab HTML panel. Each tab shows its source count in the label.
126
- If a source failed to load, shows 'Source temporairement indisponible'.
127
- """
128
  tabs_config = [
129
  ("articles", "Articles", build_article_card),
130
  ("jurisprudence", "Jurisprudence", build_decision_card),
@@ -138,21 +160,27 @@ def build_tabs_html(results_dict: dict, loading_status: dict) -> str:
138
  for i, (key, label, builder) in enumerate(tabs_config):
139
  results = results_dict.get(key, [])
140
  count = len(results)
141
- active = "active" if i == 0 else ""
 
 
 
 
 
142
 
143
  tab_buttons += f"""
144
  <button onclick="showTab('{key}')" id="tab-btn-{key}"
145
- style="padding:8px 16px;border:none;background:{'#eff6ff' if i==0 else 'transparent'};
146
- color:{'#1d4ed8' if i==0 else '#6b7280'};font-weight:{'700' if i==0 else '400'};
147
- border-bottom:{'2px solid #1d4ed8' if i==0 else '2px solid transparent'};
148
- cursor:pointer;font-size:14px;border-radius:4px 4px 0 0">
 
149
  {label} ({count})
150
  </button>"""
151
 
152
  if not loading_status.get(key, False):
153
- content = '<p style="color:#9ca3af;font-style:italic;padding:20px">Source temporairement indisponible</p>'
154
  elif not results:
155
- content = '<p style="color:#9ca3af;font-style:italic;padding:20px">Aucun résultat pour cette source.</p>'
156
  else:
157
  content = "".join(builder(r) for r in results)
158
 
@@ -168,17 +196,17 @@ def build_tabs_html(results_dict: dict, loading_status: dict) -> str:
168
  ['articles','jurisprudence','circulaires','reponses'].forEach(k => {
169
  document.getElementById('tab-' + k).style.display = (k === key) ? 'block' : 'none';
170
  var btn = document.getElementById('tab-btn-' + k);
171
- btn.style.background = (k === key) ? '#eff6ff' : 'transparent';
172
- btn.style.color = (k === key) ? '#1d4ed8' : '#6b7280';
173
- btn.style.fontWeight = (k === key) ? '700' : '400';
174
- btn.style.borderBottom = (k === key) ? '2px solid #1d4ed8' : '2px solid transparent';
175
  });
176
  }
177
  </script>"""
178
 
179
  return f"""
180
- <div style="font-family:system-ui,sans-serif">
181
- <div style="border-bottom:1px solid #e5e7eb;display:flex;gap:4px">
182
  {tab_buttons}
183
  </div>
184
  {tab_panels}
 
1
  """
2
  ui_components.py — HTML card builders for each legal source type + tab panel.
3
+ Doctrine.fr-inspired design: clean white cards, subtle borders, navy accents.
4
  """
5
 
6
+ # ── Shared card wrapper ──
7
+ _CARD_STYLE = (
8
+ "border:1px solid #e2e8f0;border-radius:10px;padding:16px 20px;"
9
+ "margin-bottom:12px;background:#ffffff;"
10
+ "box-shadow:0 1px 3px rgba(0,0,0,0.04);"
11
+ "transition:box-shadow 0.15s,border-color 0.15s"
12
+ )
13
+
14
+ # ── Badge colors per source ──
15
+ _BADGE = {
16
+ "article": ("background:#dbeafe;color:#1e40af", "Code"),
17
+ "decision": ("background:#fce7f3;color:#9d174d", "Jurisprudence"),
18
+ "circulaire": ("background:#d1fae5;color:#065f46", "Circulaire"),
19
+ "reponse": ("background:#fef3c7;color:#92400e", "Q&R"),
20
+ }
21
+
22
+
23
+ def _badge(style: str, text: str) -> str:
24
+ return (
25
+ f'<span style="{style};font-size:11px;font-weight:600;'
26
+ f'padding:3px 10px;border-radius:20px;letter-spacing:0.2px">{text}</span>'
27
+ )
28
 
 
 
 
 
 
 
 
 
29
 
30
+ def _meta_line(*parts: str) -> str:
31
+ items = [p for p in parts if p]
32
+ return (
33
+ '<div style="font-size:12px;color:#94a3b8;display:flex;align-items:center;gap:6px">'
34
+ + " · ".join(items)
35
+ + '</div>'
36
+ )
37
+
38
+
39
+ def build_article_card(result: dict, related_decisions: list[dict] | None = None) -> str:
40
+ code = result.get("code_name", "Code")
41
+ num = result.get("num", result.get("id_legifrance", ""))
42
+ snippet = (result.get("chunk_text") or "")[:250]
43
+ lf_id = result.get("id_legifrance", "")
44
+ url = f"https://www.legifrance.gouv.fr/codes/article_lc/{lf_id}" if lf_id else "#"
45
+ etat = result.get("article_etat", "")
46
+
47
+ etat_html = ""
48
+ if etat:
49
+ color = "#059669" if etat == "VIGUEUR" else "#94a3b8"
50
+ etat_html = f'<span style="font-size:11px;color:{color};font-weight:500">{etat}</span>'
51
 
52
  cross_ref_html = ""
53
  if related_decisions:
 
58
  dec_jur = dec.get("jurisdiction", "")
59
  dec_snip = dec.get("chunk_text", "")[:120]
60
  mini_cards += f"""
61
+ <div style="border-left:3px solid #1e3a5f;padding:8px 12px;margin-top:8px;
62
+ font-size:12px;color:#334155;background:#f8fafc;border-radius:0 6px 6px 0">
63
  <strong>{dec_jur}</strong> · {dec_date}
64
+ <div style="color:#64748b;margin-top:3px">{dec_snip}…</div>
65
+ <a href="{dec_url}" target="_blank"
66
+ style="color:#1e3a5f;font-size:11px;text-decoration:none;font-weight:500">Voir la decision →</a>
67
  </div>"""
68
  n = len(related_decisions)
69
  cross_ref_html = f"""
70
+ <details style="margin-top:10px">
71
+ <summary style="cursor:pointer;color:#1e3a5f;font-size:13px;font-weight:600">
72
+ {n} decision{"s" if n > 1 else ""} liee{"s" if n > 1 else ""}
73
  </summary>
74
  {mini_cards}
75
  </details>"""
76
 
77
  return f"""
78
+ <div style="{_CARD_STYLE}">
79
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
80
+ {_badge(*_BADGE["article"][:1], code)}
81
+ <strong style="font-size:15px;color:#1e293b;letter-spacing:-0.2px">Art. {num}</strong>
82
+ {etat_html}
 
 
 
 
83
  </div>
84
+ <p style="font-size:13.5px;color:#475569;margin:0 0 10px;line-height:1.65">{snippet}…</p>
85
+ {_meta_line(f'<a href="{url}" target="_blank" style="color:#1e3a5f;text-decoration:none;font-weight:500">Legifrance →</a>')}
86
  {cross_ref_html}
87
  </div>"""
88
 
 
92
  chamber = result.get("chamber", "")
93
  date = result.get("date_decision", "")
94
  fiche = result.get("fiche_arret") or ""
95
+ snippet = fiche[:250] if fiche else (result.get("chunk_text") or "")[:250]
96
  url = result.get("url_judilibre", "#")
97
  src_id = result.get("source_id", "")
98
 
99
+ label = juris + (f" · {chamber}" if chamber else "")
100
 
101
  return f"""
102
+ <div style="{_CARD_STYLE}">
103
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
104
+ {_badge(_BADGE["decision"][0], label)}
105
+ <strong style="font-size:14px;color:#1e293b">{date}</strong>
106
+ {f'<span style="font-size:11px;color:#94a3b8">n° {src_id}</span>' if src_id else ""}
 
 
 
 
 
107
  </div>
108
+ <p style="font-size:13.5px;color:#475569;margin:0 0 10px;line-height:1.65">{snippet}…</p>
109
+ {_meta_line(f'<a href="{url}" target="_blank" style="color:#1e3a5f;text-decoration:none;font-weight:500">Cour de cassation →</a>')}
110
  </div>"""
111
 
112
 
113
  def build_circulaire_card(result: dict) -> str:
114
  ministere = result.get("ministere", "")
115
  numero = result.get("numero", result.get("source_id", ""))
116
+ objet = (result.get("objet") or result.get("chunk_text") or "")[:250]
117
  date = (result.get("date_parution") or "")[:10]
118
  url = result.get("url_legifrance", "#")
119
 
120
  return f"""
121
+ <div style="{_CARD_STYLE}">
122
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
123
+ {_badge(_BADGE["circulaire"][0], f"Min. {ministere}")}
124
+ <strong style="font-size:14px;color:#1e293b">Circ. n° {numero}</strong>
 
 
 
 
 
125
  </div>
126
+ <p style="font-size:13.5px;color:#475569;margin:0 0 10px;line-height:1.65">{objet}…</p>
127
+ {_meta_line(date, f'<a href="{url}" target="_blank" style="color:#1e3a5f;text-decoration:none;font-weight:500">Legifrance →</a>')}
128
  </div>"""
129
 
130
 
131
  def build_reponse_card(result: dict) -> str:
132
  ministere = result.get("ministere", "")
133
  num_q = result.get("numero_question", result.get("source_id", ""))
134
+ question = (result.get("question_text") or result.get("chunk_text") or "")[:250]
135
  date = (result.get("date_reponse") or "")[:10]
136
  url = result.get("url_legifrance", "#")
137
 
138
  return f"""
139
+ <div style="{_CARD_STYLE}">
140
+ <div style="display:flex;align-items:center;gap:8px;margin-bottom:8px;flex-wrap:wrap">
141
+ {_badge(_BADGE["reponse"][0], ministere)}
142
+ <strong style="font-size:14px;color:#1e293b">Q. n° {num_q}</strong>
 
 
 
 
 
143
  </div>
144
+ <p style="font-size:13.5px;color:#475569;margin:0 0 10px;line-height:1.65">{question}…</p>
145
+ {_meta_line(date, f'<a href="{url}" target="_blank" style="color:#1e3a5f;text-decoration:none;font-weight:500">Legifrance →</a>')}
146
  </div>"""
147
 
148
 
149
  def build_tabs_html(results_dict: dict, loading_status: dict) -> str:
 
 
 
 
150
  tabs_config = [
151
  ("articles", "Articles", build_article_card),
152
  ("jurisprudence", "Jurisprudence", build_decision_card),
 
160
  for i, (key, label, builder) in enumerate(tabs_config):
161
  results = results_dict.get(key, [])
162
  count = len(results)
163
+
164
+ # Active tab styling
165
+ if i == 0:
166
+ bg, color, weight, border = "#e8f0fe", "#1e3a5f", "600", "2px solid #1e3a5f"
167
+ else:
168
+ bg, color, weight, border = "transparent", "#64748b", "400", "2px solid transparent"
169
 
170
  tab_buttons += f"""
171
  <button onclick="showTab('{key}')" id="tab-btn-{key}"
172
+ style="padding:10px 20px;border:none;background:{bg};
173
+ color:{color};font-weight:{weight};
174
+ border-bottom:{border};
175
+ cursor:pointer;font-size:14px;border-radius:6px 6px 0 0;
176
+ transition:all 0.15s;letter-spacing:-0.1px">
177
  {label} ({count})
178
  </button>"""
179
 
180
  if not loading_status.get(key, False):
181
+ content = '<p style="color:#94a3b8;font-style:italic;padding:24px 0">Source temporairement indisponible</p>'
182
  elif not results:
183
+ content = '<p style="color:#94a3b8;font-style:italic;padding:24px 0">Aucun resultat pour cette source.</p>'
184
  else:
185
  content = "".join(builder(r) for r in results)
186
 
 
196
  ['articles','jurisprudence','circulaires','reponses'].forEach(k => {
197
  document.getElementById('tab-' + k).style.display = (k === key) ? 'block' : 'none';
198
  var btn = document.getElementById('tab-btn-' + k);
199
+ btn.style.background = (k === key) ? '#e8f0fe' : 'transparent';
200
+ btn.style.color = (k === key) ? '#1e3a5f' : '#64748b';
201
+ btn.style.fontWeight = (k === key) ? '600' : '400';
202
+ btn.style.borderBottom = (k === key) ? '2px solid #1e3a5f' : '2px solid transparent';
203
  });
204
  }
205
  </script>"""
206
 
207
  return f"""
208
+ <div style="font-family:'Inter',system-ui,sans-serif">
209
+ <div style="border-bottom:1px solid #e2e8f0;display:flex;gap:2px;margin-bottom:4px">
210
  {tab_buttons}
211
  </div>
212
  {tab_panels}