thibaud frere commited on
Commit
be09b12
·
1 Parent(s): 482813c
Files changed (4) hide show
  1. Dockerfile +10 -3
  2. backend/app.py +83 -0
  3. frontend/index.html +15 -5
  4. frontend/main.js +77 -0
Dockerfile CHANGED
@@ -1,11 +1,18 @@
1
  FROM python:3.12-slim
2
  WORKDIR /app
3
 
4
- # Copier les fichiers du frontend statique
 
 
 
 
5
  COPY frontend/ /app/frontend/
6
 
 
 
 
7
  # Exposer le port requis par Spaces
8
  EXPOSE 7860
9
 
10
- # Lancer un serveur HTTP statique sur le port 7860
11
- CMD ["python", "-m", "http.server", "7860", "--directory", "frontend"]
 
1
  FROM python:3.12-slim
2
  WORKDIR /app
3
 
4
+ # Dépendances backend
5
+ RUN pip install --no-cache-dir flask
6
+
7
+ # Copier le code
8
+ COPY backend/ /app/backend/
9
  COPY frontend/ /app/frontend/
10
 
11
+ # Variables d'environnement
12
+ ENV PORT=7860 FLASK_ENV=production PYTHONUNBUFFERED=1
13
+
14
  # Exposer le port requis par Spaces
15
  EXPOSE 7860
16
 
17
+ # Démarrer Flask (sert le frontend statique et l'API)
18
+ CMD ["python", "-c", "from backend.app import run; run()"]
backend/app.py ADDED
@@ -0,0 +1,83 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import json
2
+ import logging
3
+ import os
4
+ import time
5
+ from flask import Flask, request, jsonify, send_from_directory
6
+
7
+
8
+ app = Flask(
9
+ __name__,
10
+ static_folder=os.path.join(os.path.dirname(__file__), "../frontend"),
11
+ static_url_path="",
12
+ )
13
+
14
+ logging.basicConfig(level=logging.INFO, format="[%(levelname)s] %(message)s")
15
+ logger = logging.getLogger(__name__)
16
+
17
+
18
+ @app.get("/")
19
+ def index():
20
+ return send_from_directory(app.static_folder, "index.html")
21
+
22
+
23
+ @app.post("/api/start")
24
+ def start_demo():
25
+ try:
26
+ data = request.get_json(force=True, silent=False)
27
+ except Exception as exc:
28
+ logger.exception("Invalid JSON body")
29
+ return jsonify({"ok": False, "error": "invalid_json", "detail": str(exc)}), 400
30
+
31
+ # Log brut du payload reçu
32
+ logger.info("/api/start payload:\n%s", json.dumps(data, indent=2, ensure_ascii=False))
33
+
34
+ # Log de métadonnées utiles sur la requête et un résumé des champs
35
+ client_ip = (request.headers.get("X-Forwarded-For") or request.remote_addr or "-").split(",")[0].strip()
36
+ user_agent = request.headers.get("User-Agent", "-")
37
+ referer = request.headers.get("Referer", "-")
38
+ content_type = request.content_type
39
+ content_length = request.content_length
40
+
41
+ mcp_text = data.get("mcp") if isinstance(data, dict) else None
42
+ mcp_len = len(mcp_text) if isinstance(mcp_text, str) else 0
43
+ mcp_is_json = False
44
+ if isinstance(mcp_text, str):
45
+ try:
46
+ json.loads(mcp_text)
47
+ mcp_is_json = True
48
+ except Exception:
49
+ mcp_is_json = False
50
+
51
+ summary = {
52
+ "client": {
53
+ "ip": client_ip,
54
+ "user_agent": user_agent,
55
+ "referer": referer,
56
+ "content_type": content_type,
57
+ "content_length": content_length,
58
+ },
59
+ "received_fields": {
60
+ "model": data.get("model") if isinstance(data, dict) else None,
61
+ "provider": data.get("provider") if isinstance(data, dict) else None,
62
+ "user": data.get("user") if isinstance(data, dict) else None,
63
+ "mcp_length": mcp_len,
64
+ "mcp_is_json": mcp_is_json,
65
+ },
66
+ }
67
+ logger.info("/api/start summary: %s", json.dumps(summary, ensure_ascii=False))
68
+
69
+ # Simuler un démarrage de démo
70
+ time.sleep(2)
71
+ iframe_url = "https://example.com/demo?session=demo123"
72
+ return jsonify({"ok": True, "received": True, "iframe_url": iframe_url}), 200
73
+
74
+
75
+ def run():
76
+ port = int(os.environ.get("PORT", "7860"))
77
+ app.run(host="0.0.0.0", port=port)
78
+
79
+
80
+ if __name__ == "__main__":
81
+ run()
82
+
83
+
frontend/index.html CHANGED
@@ -7,7 +7,8 @@
7
  <meta name="description" content="Demo login Hugging Face (frontend only)" />
8
  <style>
9
  :root { color-scheme: light dark; }
10
- body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; }
 
11
  .toolbar { position: sticky; top: 0; z-index: 10; width: 100%; box-sizing: border-box; padding: 12px 16px; background: #f8fafc; border-bottom: 1px solid #e5e7eb; display: grid; grid-template-columns: auto 1fr 1fr 1fr auto; gap: 12px; align-items: center; }
12
  .toolbar input[type="text"], .toolbar input[type="file"] { width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff; color: #111827; }
13
  .toolbar button { padding: 10px 14px; border: 1px solid #e5e7eb; border-radius: 10px; background: #111827; color: #fff; font-weight: 600; cursor: pointer; }
@@ -16,6 +17,10 @@
16
  .login-btn:hover { background: #f9fafb; }
17
  button { display: inline-flex; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid #e5e7eb; border-radius: 10px; background: #fff; color: #111827; cursor: pointer; font-weight: 600; }
18
  button:hover { background: #f9fafb; }
 
 
 
 
19
  </style>
20
  </head>
21
  <body>
@@ -24,12 +29,17 @@
24
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M12 2a7 7 0 0 1 7 7v1h1a2 2 0 1 1 0 4h-1.05A6.002 6.002 0 0 1 12 20a6.002 6.002 0 0 1-6.95-6H4a2 2 0 1 1 0-4h1V9a7 7 0 0 1 7-7Z" fill="#FFB000"/></svg>
25
  <span id="loginLabel">Login avec Hugging Face</span>
26
  </button>
27
- <input id="modelInput" type="text" placeholder="Choose your model" />
28
- <input id="providerInput" type="text" placeholder="Choose your provider" />
29
- <input id="mcpFile" type="file" accept=".json" />
 
 
 
30
  <button id="startDemoBtn" type="button">Start demo</button>
31
  </div>
32
-
 
 
33
 
34
  <script type="module" src="/main.js"></script>
35
  </body>
 
7
  <meta name="description" content="Demo login Hugging Face (frontend only)" />
8
  <style>
9
  :root { color-scheme: light dark; }
10
+ html, body { height: 100%; }
11
+ body { font-family: system-ui, -apple-system, Segoe UI, Roboto, Helvetica, Arial, sans-serif; margin: 0; padding: 0; min-height: 100vh; display: grid; grid-template-rows: auto 1fr; }
12
  .toolbar { position: sticky; top: 0; z-index: 10; width: 100%; box-sizing: border-box; padding: 12px 16px; background: #f8fafc; border-bottom: 1px solid #e5e7eb; display: grid; grid-template-columns: auto 1fr 1fr 1fr auto; gap: 12px; align-items: center; }
13
  .toolbar input[type="text"], .toolbar input[type="file"] { width: 100%; box-sizing: border-box; padding: 10px 12px; border: 1px solid #e5e7eb; border-radius: 8px; background: #fff; color: #111827; }
14
  .toolbar button { padding: 10px 14px; border: 1px solid #e5e7eb; border-radius: 10px; background: #111827; color: #fff; font-weight: 600; cursor: pointer; }
 
17
  .login-btn:hover { background: #f9fafb; }
18
  button { display: inline-flex; align-items: center; gap: 10px; padding: 10px 14px; border: 1px solid #e5e7eb; border-radius: 10px; background: #fff; color: #111827; cursor: pointer; font-weight: 600; }
19
  button:hover { background: #f9fafb; }
20
+ .filecol { display: flex; flex-direction: column; gap: 6px; }
21
+ .hint { font-size: 12px; color: #475569; }
22
+ .iframe-wrap { height: 100%; box-sizing: border-box; padding: 16px; background: #f8fafc;}
23
+ .iframe { width: 100%; height: 100%; border: 1px solid #e5e7eb; border-radius: 12px; }
24
  </style>
25
  </head>
26
  <body>
 
29
  <svg width="20" height="20" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg" aria-hidden="true"><path d="M12 2a7 7 0 0 1 7 7v1h1a2 2 0 1 1 0 4h-1.05A6.002 6.002 0 0 1 12 20a6.002 6.002 0 0 1-6.95-6H4a2 2 0 1 1 0-4h1V9a7 7 0 0 1 7-7Z" fill="#FFB000"/></svg>
30
  <span id="loginLabel">Login avec Hugging Face</span>
31
  </button>
32
+ <input id="modelInput" type="text" placeholder="Choose your model" value="meta-llama/Llama-3.3-70B-Instruct" />
33
+ <input id="providerInput" type="text" placeholder="Choose your provider" value="nebius" />
34
+ <div class="filecol">
35
+ <input id="mcpFile" type="file" accept=".json" />
36
+ <div class="hint">Upload your MCP file (optional). HF_TOKEN will be replaced by your token on the fly if present</div>
37
+ </div>
38
  <button id="startDemoBtn" type="button">Start demo</button>
39
  </div>
40
+ <div class="iframe-wrap">
41
+ <iframe id="resultFrame" class="iframe" hidden></iframe>
42
+ </div>
43
 
44
  <script type="module" src="/main.js"></script>
45
  </body>
frontend/main.js CHANGED
@@ -236,6 +236,83 @@ document.getElementById("loginBtn").addEventListener("click", () => {
236
  preloadDefaultMcp(),
237
  ]);
238
  if (!handled) updateUI("Login avec Hugging Face");
 
239
  })();
240
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
241
 
 
236
  preloadDefaultMcp(),
237
  ]);
238
  if (!handled) updateUI("Login avec Hugging Face");
239
+ await validateForm();
240
  })();
241
 
242
+ // Form handling: validate and submit to backend
243
+ function getSelectedFileAsText(input) {
244
+ return new Promise((resolve, reject) => {
245
+ if (!input || !input.files || input.files.length === 0) return resolve(null);
246
+ const file = input.files[0];
247
+ const reader = new FileReader();
248
+ reader.onerror = () => reject(new Error("Lecture du fichier échouée"));
249
+ reader.onload = () => resolve(String(reader.result || ""));
250
+ reader.readAsText(file);
251
+ });
252
+ }
253
+
254
+ function getLoginLabel() {
255
+ const el = document.getElementById("loginLabel");
256
+ return el ? el.textContent || "" : "";
257
+ }
258
+
259
+ function isLikelyEmail(value) {
260
+ return /@/.test(value || "");
261
+ }
262
+
263
+ async function validateForm() {
264
+ const model = document.getElementById("modelInput").value.trim();
265
+ const provider = document.getElementById("providerInput").value.trim();
266
+ const fileInput = document.getElementById("mcpFile");
267
+ const hasFile = fileInput && fileInput.files && fileInput.files.length > 0;
268
+ const user = getLoginLabel();
269
+ const loggedIn = isLikelyEmail(user) || (user && user !== "Login avec Hugging Face" && !user.startsWith("Erreur"));
270
+ const ok = Boolean(model && provider && hasFile && loggedIn);
271
+ document.getElementById("startDemoBtn").disabled = !ok;
272
+ return ok;
273
+ }
274
+
275
+ async function gatherPayload() {
276
+ const model = document.getElementById("modelInput").value.trim();
277
+ const provider = document.getElementById("providerInput").value.trim();
278
+ const fileInput = document.getElementById("mcpFile");
279
+ const mcpText = await getSelectedFileAsText(fileInput);
280
+ const user = getLoginLabel();
281
+ return { model, provider, user, mcp: mcpText };
282
+ }
283
+
284
+ async function submitStart() {
285
+ if (!(await validateForm())) return;
286
+ const btn = document.getElementById("startDemoBtn");
287
+ const prev = btn.textContent;
288
+ btn.disabled = true;
289
+ btn.textContent = "Starting…";
290
+ try {
291
+ const payload = await gatherPayload();
292
+ const res = await fetch("/api/start", {
293
+ method: "POST",
294
+ headers: { "Content-Type": "application/json" },
295
+ body: JSON.stringify(payload),
296
+ });
297
+ if (res.ok) {
298
+ const data = await res.json();
299
+ if (data && data.iframe_url) {
300
+ const frame = document.getElementById("resultFrame");
301
+ frame.hidden = false;
302
+ frame.src = data.iframe_url;
303
+ }
304
+ }
305
+ btn.textContent = prev;
306
+ } catch (e) {
307
+ btn.textContent = prev;
308
+ } finally {
309
+ await validateForm();
310
+ }
311
+ }
312
+
313
+ document.getElementById("modelInput").addEventListener("input", validateForm);
314
+ document.getElementById("providerInput").addEventListener("input", validateForm);
315
+ document.getElementById("mcpFile").addEventListener("change", validateForm);
316
+ document.getElementById("startDemoBtn").addEventListener("click", submitStart);
317
+
318