Spaces:
Sleeping
Sleeping
thibaud frere
commited on
Commit
·
be09b12
1
Parent(s):
482813c
update
Browse files- Dockerfile +10 -3
- backend/app.py +83 -0
- frontend/index.html +15 -5
- frontend/main.js +77 -0
Dockerfile
CHANGED
|
@@ -1,11 +1,18 @@
|
|
| 1 |
FROM python:3.12-slim
|
| 2 |
WORKDIR /app
|
| 3 |
|
| 4 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
| 5 |
COPY frontend/ /app/frontend/
|
| 6 |
|
|
|
|
|
|
|
|
|
|
| 7 |
# Exposer le port requis par Spaces
|
| 8 |
EXPOSE 7860
|
| 9 |
|
| 10 |
-
#
|
| 11 |
-
CMD ["python", "-
|
|
|
|
| 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 {
|
|
|
|
| 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 |
-
<
|
|
|
|
|
|
|
|
|
|
| 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 |
|