rastadidi commited on
Commit
08579f5
·
verified ·
1 Parent(s): 04ad660

Upload 2 files

Browse files
Files changed (2) hide show
  1. app.py +135 -47
  2. requirements.txt +1 -0
app.py CHANGED
@@ -3,6 +3,7 @@ import gradio as gr
3
  import pandas as pd
4
  import numpy as np
5
  import requests
 
6
  from datasets import load_dataset
7
 
8
  # --- Chargement des données ---
@@ -16,6 +17,8 @@ embeddings_norm = embeddings_matrix / (norms + 1e-10)
16
  EMBED_API = "https://api-inference.huggingface.co/models/BAAI/bge-m3"
17
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
18
 
 
 
19
 
20
  def encode_query(text: str):
21
  headers = {"Content-Type": "application/json"}
@@ -39,79 +42,134 @@ def encode_query(text: str):
39
  return None
40
 
41
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  def render_cards(results_df, scores=None):
43
  if results_df.empty:
44
  return "<p style='color:#888;text-align:center;padding:2rem'>Aucun résultat trouvé.</p>"
45
 
46
  cards = []
47
  for i, (_, row) in enumerate(results_df.iterrows()):
48
- titre = row.get("titre") or "Poste sans titre"
 
49
  direction = row.get("direction_interne") or ""
50
- grade = row.get("corps_grade") or ""
51
- lieu = row.get("lieu_travail") or ""
52
- immed = row.get("disponible_immediatement", False)
53
- url = row.get("url") or ""
54
- texte = (row.get("text") or "")[:200].strip()
55
- score = scores[i] if scores is not None else None
56
-
57
- immed_badge = (
58
  '<span style="background:#d1fae5;color:#065f46;font-size:11px;'
59
  'padding:2px 8px;border-radius:12px;font-weight:500">⚡ Immédiat</span>'
60
  if immed else ""
61
  )
62
- score_badge = (
63
- f'<span style="background:#ede9fe;color:#5b21b6;font-size:11px;'
64
- f'padding:2px 8px;border-radius:12px">{score:.0%}</span>'
65
- if score is not None else ""
 
 
66
  )
67
- meta_parts = [p for p in [direction, grade, lieu] if p]
68
- meta = " · ".join(meta_parts)
69
-
70
- link_btn = (
71
- f'<a href="{url}" target="_blank" style="display:inline-block;margin-top:10px;'
72
- f'padding:8px 16px;background:#2563eb;color:#fff;border-radius:8px;'
73
  f'text-decoration:none;font-size:13px;font-weight:500">Voir l\'annonce →</a>'
74
  if url else ""
75
  )
76
 
77
- card = f"""
78
  <div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;
79
- padding:14px 16px;margin-bottom:12px">
80
  <div style="display:flex;justify-content:space-between;align-items:flex-start;
81
- flex-wrap:wrap;gap:6px;margin-bottom:6px">
82
  <span style="font-size:15px;font-weight:600;color:#111;line-height:1.3;
83
  flex:1;min-width:0">{titre}</span>
84
- <div style="display:flex;gap:4px;flex-shrink:0">{score_badge}{immed_badge}</div>
 
 
85
  </div>
86
  <p style="font-size:12px;color:#6b7280;margin:0 0 6px">{meta}</p>
87
- <p style="font-size:13px;color:#374151;margin:0;line-height:1.5">{texte}…</p>
88
- {link_btn}
89
- </div>"""
90
- cards.append(card)
91
 
92
- header = f'<p style="font-size:13px;color:#6b7280;margin-bottom:12px">{len(results_df)} offre(s) trouvée(s)</p>'
93
  return header + "\n".join(cards)
94
 
95
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
96
  def search(query):
97
  if not query.strip():
98
- # Afficher toutes les offres par défaut
99
- return render_cards(df)
100
 
101
  q_vec = encode_query(query)
102
-
103
  if q_vec is not None:
104
  sims = embeddings_norm @ q_vec
105
  order = np.argsort(sims)[::-1]
106
  results = df.iloc[order].copy()
107
  scores = sims[order].tolist()
108
- # Garder seulement les résultats pertinents (similarité > 0.3)
109
  mask = [s > 0.3 for s in scores]
110
  results = results[mask]
111
  scores = [s for s, m in zip(scores, mask) if m]
112
- return render_cards(results, scores)
113
  else:
114
- # Fallback mots-clés
115
  qwords = query.lower().split()
116
  def score_row(row):
117
  text = f"{row.get('titre','')} {row.get('text','')} {row.get('corps_grade','')}".lower()
@@ -119,20 +177,26 @@ def search(query):
119
  df2 = df.copy()
120
  df2["_s"] = df2.apply(score_row, axis=1)
121
  df2 = df2[df2["_s"] > 0].sort_values("_s", ascending=False)
122
- return render_cards(df2)
 
 
 
 
123
 
124
 
125
  with gr.Blocks(title="AVPS OPT-NC") as demo:
126
  gr.HTML("""<style>
127
- body { max-width: 600px; margin: 0 auto; }
128
- .gradio-container { padding: 0 !important; }
129
  footer { display: none !important; }
130
  #query textarea { font-size: 16px !important; line-height: 1.5 !important; }
131
  #search-btn { font-size: 16px !important; height: 48px !important; border-radius: 10px !important; }
 
 
 
 
132
  </style>""")
133
- gr.Markdown(
134
- "## 📋 Offres OPT-NC\nDécrivez votre profil ou saisissez des mots-clés.",
135
- )
136
 
137
  query_input = gr.Textbox(
138
  elem_id="query",
@@ -145,15 +209,39 @@ footer { display: none !important; }
145
  max_lines=8,
146
  show_label=False,
147
  )
148
-
149
  search_btn = gr.Button("🔍 Rechercher", variant="primary", elem_id="search-btn")
150
 
151
- results_html = gr.HTML()
152
-
153
- # Lancement au démarrage
154
- demo.load(fn=search, inputs=query_input, outputs=results_html)
155
- search_btn.click(fn=search, inputs=query_input, outputs=results_html)
156
- query_input.submit(fn=search, inputs=query_input, outputs=results_html)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
 
158
  if __name__ == "__main__":
159
  demo.launch()
 
3
  import pandas as pd
4
  import numpy as np
5
  import requests
6
+ import markdown as md_lib
7
  from datasets import load_dataset
8
 
9
  # --- Chargement des données ---
 
17
  EMBED_API = "https://api-inference.huggingface.co/models/BAAI/bge-m3"
18
  HF_TOKEN = os.environ.get("HF_TOKEN", "")
19
 
20
+ job_index = {str(row["id"]): dict(row) for _, row in df.iterrows()}
21
+
22
 
23
  def encode_query(text: str):
24
  headers = {"Content-Type": "application/json"}
 
42
  return None
43
 
44
 
45
+ def score_badge(score):
46
+ if score is None:
47
+ return ""
48
+ pct = int(round(score * 100))
49
+ bg = "#d1fae5" if pct >= 70 else ("#fef3c7" if pct >= 50 else "#f3f4f6")
50
+ color = "#065f46" if pct >= 70 else ("#92400e" if pct >= 50 else "#6b7280")
51
+ return (
52
+ f'<span style="background:{bg};color:{color};font-size:12px;font-weight:600;'
53
+ f'padding:3px 10px;border-radius:12px;white-space:nowrap">Score {pct}%</span>'
54
+ )
55
+
56
+
57
  def render_cards(results_df, scores=None):
58
  if results_df.empty:
59
  return "<p style='color:#888;text-align:center;padding:2rem'>Aucun résultat trouvé.</p>"
60
 
61
  cards = []
62
  for i, (_, row) in enumerate(results_df.iterrows()):
63
+ job_id = str(row.get("id") or "")
64
+ titre = row.get("titre") or "Poste sans titre"
65
  direction = row.get("direction_interne") or ""
66
+ grade = row.get("corps_grade") or ""
67
+ lieu = row.get("lieu_travail") or ""
68
+ immed = row.get("disponible_immediatement", False)
69
+ url = row.get("url") or ""
70
+ apercu = (row.get("text") or "")[:200].strip()
71
+ score = scores[i] if scores is not None else None
72
+
73
+ immed_html = (
74
  '<span style="background:#d1fae5;color:#065f46;font-size:11px;'
75
  'padding:2px 8px;border-radius:12px;font-weight:500">⚡ Immédiat</span>'
76
  if immed else ""
77
  )
78
+ meta = " · ".join(p for p in [direction, grade, lieu] if p)
79
+ detail_btn = (
80
+ f'<button onclick="showDetail(\'{job_id}\')" '
81
+ f'style="padding:7px 14px;background:#f3f4f6;color:#374151;border:none;'
82
+ f'border-radius:8px;font-size:13px;cursor:pointer;font-weight:500">'
83
+ f'📄 Détail</button>'
84
  )
85
+ annonce_btn = (
86
+ f'<a href="{url}" target="_blank" '
87
+ f'style="padding:7px 14px;background:#2563eb;color:#fff;border-radius:8px;'
 
 
 
88
  f'text-decoration:none;font-size:13px;font-weight:500">Voir l\'annonce →</a>'
89
  if url else ""
90
  )
91
 
92
+ cards.append(f"""
93
  <div style="background:#fff;border:1px solid #e5e7eb;border-radius:12px;
94
+ padding:14px 16px;margin-bottom:10px">
95
  <div style="display:flex;justify-content:space-between;align-items:flex-start;
96
+ flex-wrap:wrap;gap:6px;margin-bottom:4px">
97
  <span style="font-size:15px;font-weight:600;color:#111;line-height:1.3;
98
  flex:1;min-width:0">{titre}</span>
99
+ <div style="display:flex;gap:4px;align-items:center;flex-shrink:0">
100
+ {score_badge(score)}{immed_html}
101
+ </div>
102
  </div>
103
  <p style="font-size:12px;color:#6b7280;margin:0 0 6px">{meta}</p>
104
+ <p style="font-size:13px;color:#374151;margin:0 0 10px;line-height:1.5">{apercu}…</p>
105
+ <div style="display:flex;gap:8px;flex-wrap:wrap">{detail_btn}{annonce_btn}</div>
106
+ </div>""")
 
107
 
108
+ header = f'<p style="font-size:13px;color:#6b7280;margin-bottom:10px">{len(results_df)} offre(s) trouvée(s)</p>'
109
  return header + "\n".join(cards)
110
 
111
 
112
+ def render_detail(job_id: str):
113
+ row = job_index.get(job_id)
114
+ if not row:
115
+ return "<p>Annonce introuvable.</p>"
116
+
117
+ titre = row.get("titre") or "Poste sans titre"
118
+ direction = row.get("direction_interne") or ""
119
+ grade = row.get("corps_grade") or ""
120
+ lieu = row.get("lieu_travail") or ""
121
+ immed = row.get("disponible_immediatement", False)
122
+ url = row.get("url") or ""
123
+ url_pdf = row.get("url_pdf") or ""
124
+ texte_md = row.get("text") or ""
125
+
126
+ # Markdown → HTML
127
+ body_html = md_lib.markdown(texte_md, extensions=["nl2br", "sane_lists"])
128
+
129
+ immed_html = (
130
+ '<span style="background:#d1fae5;color:#065f46;font-size:12px;'
131
+ 'padding:3px 10px;border-radius:12px;font-weight:500">⚡ Disponible immédiatement</span>'
132
+ if immed else ""
133
+ )
134
+ meta = " · ".join(p for p in [direction, grade, lieu] if p)
135
+ btns = ""
136
+ if url:
137
+ btns += (f'<a href="{url}" target="_blank" style="padding:8px 16px;background:#2563eb;'
138
+ f'color:#fff;border-radius:8px;text-decoration:none;font-size:13px;'
139
+ f'font-weight:500;margin-right:8px">🌐 Voir l\'annonce</a>')
140
+ if url_pdf:
141
+ btns += (f'<a href="{url_pdf}" target="_blank" style="padding:8px 16px;'
142
+ f'background:#f3f4f6;color:#374151;border-radius:8px;text-decoration:none;'
143
+ f'font-size:13px;font-weight:500">📎 PDF</a>')
144
+
145
+ return f"""
146
+ <div style="padding:4px 0">
147
+ <h2 style="font-size:17px;font-weight:700;color:#111;margin:0 0 6px;line-height:1.3">{titre}</h2>
148
+ <p style="font-size:12px;color:#6b7280;margin:0 0 8px">{meta}</p>
149
+ {immed_html}
150
+ <div style="margin:16px 0">{btns}</div>
151
+ <hr style="border:none;border-top:1px solid #e5e7eb;margin:12px 0">
152
+ <div style="font-size:14px;color:#374151;line-height:1.7">
153
+ {body_html}
154
+ </div>
155
+ </div>"""
156
+
157
+
158
  def search(query):
159
  if not query.strip():
160
+ return render_cards(df), ""
 
161
 
162
  q_vec = encode_query(query)
 
163
  if q_vec is not None:
164
  sims = embeddings_norm @ q_vec
165
  order = np.argsort(sims)[::-1]
166
  results = df.iloc[order].copy()
167
  scores = sims[order].tolist()
 
168
  mask = [s > 0.3 for s in scores]
169
  results = results[mask]
170
  scores = [s for s, m in zip(scores, mask) if m]
171
+ return render_cards(results, scores), ""
172
  else:
 
173
  qwords = query.lower().split()
174
  def score_row(row):
175
  text = f"{row.get('titre','')} {row.get('text','')} {row.get('corps_grade','')}".lower()
 
177
  df2 = df.copy()
178
  df2["_s"] = df2.apply(score_row, axis=1)
179
  df2 = df2[df2["_s"] > 0].sort_values("_s", ascending=False)
180
+ return render_cards(df2), ""
181
+
182
+
183
+ def show_detail(job_id: str):
184
+ return render_detail(job_id)
185
 
186
 
187
  with gr.Blocks(title="AVPS OPT-NC") as demo:
188
  gr.HTML("""<style>
189
+ .gradio-container { max-width: 1100px !important; }
 
190
  footer { display: none !important; }
191
  #query textarea { font-size: 16px !important; line-height: 1.5 !important; }
192
  #search-btn { font-size: 16px !important; height: 48px !important; border-radius: 10px !important; }
193
+ @media (max-width: 640px) {
194
+ #detail-panel { display: none !important; }
195
+ #results-col { width: 100% !important; }
196
+ }
197
  </style>""")
198
+
199
+ gr.Markdown("## 📋 Offres OPT-NC\nDécrivez votre profil ou saisissez des mots-clés.")
 
200
 
201
  query_input = gr.Textbox(
202
  elem_id="query",
 
209
  max_lines=8,
210
  show_label=False,
211
  )
 
212
  search_btn = gr.Button("🔍 Rechercher", variant="primary", elem_id="search-btn")
213
 
214
+ with gr.Row():
215
+ with gr.Column(scale=1, elem_id="results-col"):
216
+ results_html = gr.HTML()
217
+ with gr.Column(scale=1, elem_id="detail-panel", visible=True):
218
+ detail_html = gr.HTML(
219
+ '<div style="padding:2rem;color:#9ca3af;text-align:center;'
220
+ 'border:1px dashed #e5e7eb;border-radius:12px;margin-top:4px">'
221
+ '← Cliquez sur "Détail" pour afficher une annonce</div>'
222
+ )
223
+
224
+ # Bouton Détail : appel via gr.Text caché qui transmet l'id
225
+ selected_id = gr.Textbox(visible=False)
226
+
227
+ # JS : le bouton "Détail" dans le HTML met à jour selected_id
228
+ gr.HTML("""<script>
229
+ function showDetail(jobId) {
230
+ const input = document.querySelector('#selected-id textarea') ||
231
+ document.querySelector('#selected-id input');
232
+ if (input) {
233
+ input.value = jobId;
234
+ input.dispatchEvent(new Event('input', { bubbles: true }));
235
+ }
236
+ }
237
+ </script>""")
238
+
239
+ selected_id.change(fn=show_detail, inputs=selected_id, outputs=detail_html)
240
+
241
+ inputs = [query_input]
242
+ search_btn.click(fn=search, inputs=inputs, outputs=[results_html, detail_html])
243
+ query_input.submit(fn=search, inputs=inputs, outputs=[results_html, detail_html])
244
+ demo.load(fn=search, inputs=inputs, outputs=[results_html, detail_html])
245
 
246
  if __name__ == "__main__":
247
  demo.launch()
requirements.txt CHANGED
@@ -2,3 +2,4 @@ gradio>=6.0.0
2
  datasets
3
  pandas
4
  numpy
 
 
2
  datasets
3
  pandas
4
  numpy
5
+ markdown