Ryadg commited on
Commit
97e5b2d
Β·
1 Parent(s): 22e60a6

feat: Off-Brand badge - custom HTML UI with JS bridge to hidden Gradio components

Browse files
Files changed (4) hide show
  1. Dockerfile +0 -17
  2. README.md +4 -1
  3. app.py +120 -13
  4. main.py +0 -128
Dockerfile DELETED
@@ -1,17 +0,0 @@
1
- FROM python:3.12-slim
2
-
3
- WORKDIR /app
4
-
5
- RUN apt-get update && apt-get install -y git curl && rm -rf /var/lib/apt/lists/*
6
-
7
- COPY requirements.txt .
8
- RUN pip install --no-cache-dir -r requirements.txt
9
-
10
- COPY . .
11
-
12
- RUN useradd -m -u 1000 user && chown -R user /app
13
- USER user
14
-
15
- EXPOSE 7860
16
-
17
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "7860", "--workers", "1"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
README.md CHANGED
@@ -3,7 +3,10 @@ title: PaperProf
3
  emoji: πŸ“„
4
  colorFrom: purple
5
  colorTo: blue
6
- sdk: docker
 
 
 
7
  pinned: false
8
  ---
9
 
 
3
  emoji: πŸ“„
4
  colorFrom: purple
5
  colorTo: blue
6
+ sdk: gradio
7
+ sdk_version: 6.16.0
8
+ python_version: '3.12'
9
+ app_file: app.py
10
  pinned: false
11
  ---
12
 
app.py CHANGED
@@ -11,6 +11,34 @@ from core.chunker import chunk_text
11
  from core.questioner import generate_question
12
  from core.evaluator import evaluate_answer
13
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
14
  # ---------------------------------------------------------------------------
15
  # Internationalisation
16
  # ---------------------------------------------------------------------------
@@ -185,9 +213,88 @@ HEADER = """
185
  # UI
186
  # ---------------------------------------------------------------------------
187
 
188
- with gr.Blocks(title="PaperProf") as demo:
189
-
190
- gr.HTML(HEADER)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
191
 
192
  chunks_state = gr.State([])
193
  chunk_state = gr.State("")
@@ -195,21 +302,21 @@ with gr.Blocks(title="PaperProf") as demo:
195
  total_state = gr.State(0)
196
 
197
  with gr.Row():
198
- pdf_input = gr.File(label="πŸ“Ž Cours PDF", file_types=[".pdf"], scale=3)
199
- load_btn = gr.Button("Load PDF", variant="primary", scale=2)
200
- language_selector = gr.Radio(["English", "Français"], value="English", label="🌐 Language")
201
 
202
  with gr.Row():
203
- load_status = gr.Textbox(label="Status", interactive=False, scale=4, elem_id="status-box")
204
- score_box = gr.Textbox(label="Score", value="β€”", interactive=False, scale=1, elem_id="score-box")
205
 
206
  with gr.Row():
207
- question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="question-box")
208
- new_q_btn = gr.Button("New Question", variant="secondary", scale=1)
209
 
210
- answer_box = gr.Textbox(label="✏️ Your Answer", lines=4, placeholder="Write your answer here…")
211
- submit_btn = gr.Button("Submit Answer", variant="primary")
212
- feedback_box = gr.Textbox(label="πŸ’¬ Feedback", interactive=False, lines=7, elem_id="feedback-box")
213
 
214
  # Traduction UI quand on change la langue
215
  language_selector.change(
 
11
  from core.questioner import generate_question
12
  from core.evaluator import evaluate_answer
13
 
14
+ CSS = """
15
+ /* Hide all Gradio chrome */
16
+ footer { display: none !important; }
17
+ header { display: none !important; }
18
+ .gradio-container {
19
+ padding: 0 !important;
20
+ margin: 0 !important;
21
+ max-width: 100% !important;
22
+ background: #0A0F1E !important;
23
+ }
24
+ .main { padding: 0 !important; }
25
+ #component-0 { padding: 0 !important; }
26
+ .gap { gap: 0 !important; }
27
+ .contain { padding: 0 !important; }
28
+ * { box-sizing: border-box; }
29
+ body {
30
+ background: #0A0F1E !important;
31
+ margin: 0 !important;
32
+ }
33
+ /* Hide all visible Gradio components - only show our custom HTML */
34
+ .gr-file, .gr-textbox, .gr-button, .gr-radio {
35
+ display: none !important;
36
+ }
37
+ """
38
+
39
+ import pathlib
40
+ CUSTOM_HTML = pathlib.Path("ui/index.html").read_text()
41
+
42
  # ---------------------------------------------------------------------------
43
  # Internationalisation
44
  # ---------------------------------------------------------------------------
 
213
  # UI
214
  # ---------------------------------------------------------------------------
215
 
216
+ with gr.Blocks(title="PaperProf", css=CSS) as demo:
217
+
218
+ gr.HTML(CUSTOM_HTML)
219
+
220
+ gr.HTML("""
221
+ <script>
222
+ // Bridge: custom UI β†’ hidden Gradio components
223
+ // We intercept clicks on our custom buttons and trigger hidden Gradio ones
224
+
225
+ document.addEventListener('DOMContentLoaded', function() {
226
+
227
+ // Helper: wait for an element to exist
228
+ function waitFor(selector, callback, maxTries=50) {
229
+ let tries = 0;
230
+ const interval = setInterval(() => {
231
+ const el = document.querySelector(selector);
232
+ if (el) { clearInterval(interval); callback(el); }
233
+ if (++tries >= maxTries) clearInterval(interval);
234
+ }, 200);
235
+ }
236
+
237
+ // Load PDF button
238
+ waitFor('#custom-load-btn', (btn) => {
239
+ btn.addEventListener('click', () => {
240
+ // Transfer file from custom input to hidden Gradio file component
241
+ const fileInput = document.querySelector('#custom-file-input input[type=file]');
242
+ const hiddenBtn = document.querySelector('#hidden-load-btn button');
243
+ if (hiddenBtn) hiddenBtn.click();
244
+ });
245
+ });
246
+
247
+ // New Question button
248
+ waitFor('#custom-question-btn', (btn) => {
249
+ btn.addEventListener('click', () => {
250
+ const hiddenBtn = document.querySelector('#hidden-question-btn button');
251
+ if (hiddenBtn) hiddenBtn.click();
252
+ });
253
+ });
254
+
255
+ // Submit Answer button
256
+ waitFor('#custom-submit-btn', (btn) => {
257
+ btn.addEventListener('click', () => {
258
+ // Copy answer from custom textarea to hidden Gradio textbox
259
+ const customAnswer = document.querySelector('#custom-answer-input');
260
+ const hiddenAnswer = document.querySelector('#hidden-answer-input textarea');
261
+ if (customAnswer && hiddenAnswer) {
262
+ hiddenAnswer.value = customAnswer.value;
263
+ hiddenAnswer.dispatchEvent(new Event('input', {bubbles: true}));
264
+ }
265
+ const hiddenBtn = document.querySelector('#hidden-submit-btn button');
266
+ if (hiddenBtn) hiddenBtn.click();
267
+ });
268
+ });
269
+
270
+ // Watch for Gradio output changes and update custom UI
271
+ const observer = new MutationObserver(() => {
272
+ // Update question display
273
+ const questionEl = document.querySelector('#hidden-question-output textarea');
274
+ const customQuestion = document.querySelector('#custom-question-display');
275
+ if (questionEl && customQuestion) {
276
+ customQuestion.textContent = questionEl.value;
277
+ }
278
+
279
+ // Update feedback display
280
+ const feedbackEl = document.querySelector('#hidden-feedback-output textarea');
281
+ const customFeedback = document.querySelector('#custom-feedback-display');
282
+ if (feedbackEl && customFeedback) {
283
+ customFeedback.textContent = feedbackEl.value;
284
+ }
285
+
286
+ // Update status
287
+ const statusEl = document.querySelector('#hidden-status-output textarea');
288
+ const customStatus = document.querySelector('#custom-status-display');
289
+ if (statusEl && customStatus) {
290
+ customStatus.textContent = statusEl.value;
291
+ }
292
+ });
293
+
294
+ observer.observe(document.body, { childList: true, subtree: true, characterData: true });
295
+ });
296
+ </script>
297
+ """)
298
 
299
  chunks_state = gr.State([])
300
  chunk_state = gr.State("")
 
302
  total_state = gr.State(0)
303
 
304
  with gr.Row():
305
+ pdf_input = gr.File(label="πŸ“Ž Cours PDF", file_types=[".pdf"], scale=3, visible=False)
306
+ load_btn = gr.Button("Load PDF", variant="primary", scale=2, visible=False, elem_id="hidden-load-btn")
307
+ language_selector = gr.Radio(["English", "Français"], value="English", label="🌐 Language", visible=False)
308
 
309
  with gr.Row():
310
+ load_status = gr.Textbox(label="Status", interactive=False, scale=4, elem_id="hidden-status-output", visible=False)
311
+ score_box = gr.Textbox(label="Score", value="β€”", interactive=False, scale=1, elem_id="score-box", visible=False)
312
 
313
  with gr.Row():
314
+ question_box = gr.Textbox(label="❓ Question", interactive=False, lines=3, scale=3, elem_id="hidden-question-output", visible=False)
315
+ new_q_btn = gr.Button("New Question", variant="secondary", scale=1, visible=False, elem_id="hidden-question-btn")
316
 
317
+ answer_box = gr.Textbox(label="✏️ Your Answer", lines=4, placeholder="Write your answer here…", visible=False, elem_id="hidden-answer-input")
318
+ submit_btn = gr.Button("Submit Answer", variant="primary", visible=False, elem_id="hidden-submit-btn")
319
+ feedback_box = gr.Textbox(label="πŸ’¬ Feedback", interactive=False, lines=7, elem_id="hidden-feedback-output", visible=False)
320
 
321
  # Traduction UI quand on change la langue
322
  language_selector.change(
main.py DELETED
@@ -1,128 +0,0 @@
1
- """
2
- main.py β€” FastAPI entry point for PaperProf (Docker / HuggingFace Spaces).
3
-
4
- Routes:
5
- GET / β†’ custom HTML frontend
6
- POST /api/load β†’ parse PDF, return chunks (CPU)
7
- POST /api/question β†’ generate question from chunk (GPU via @spaces.GPU)
8
- POST /api/evaluate β†’ evaluate student answer (GPU via @spaces.GPU)
9
- * /gradio/* β†’ Gradio interface as fallback
10
- """
11
-
12
- import os
13
- import pathlib
14
- import tempfile
15
-
16
- import gradio as gr
17
- from fastapi import FastAPI, File, HTTPException, Request, UploadFile
18
- from fastapi.responses import HTMLResponse
19
-
20
- from app import demo
21
- from core.parser import extract_text
22
- from core.chunker import chunk_text
23
- from core.questioner import generate_question
24
- from core.evaluator import evaluate_answer
25
-
26
- try:
27
- import spaces
28
- except ImportError:
29
- class spaces: # noqa: N801 β€” local dev fallback
30
- @staticmethod
31
- def GPU(duration=60):
32
- def wrap(fn): return fn
33
- return wrap
34
-
35
- # ─────────────────────────────────────────────────────────────────────────────
36
- # FastAPI app
37
- # ─────────────────────────────────────────────────────────────────────────────
38
-
39
- fastapi_app = FastAPI(title="PaperProf", docs_url=None, redoc_url=None)
40
-
41
-
42
- @fastapi_app.get("/", response_class=HTMLResponse)
43
- async def root():
44
- return pathlib.Path("ui/index.html").read_text(encoding="utf-8")
45
-
46
-
47
- # ─────────────────────────────────────────────────────────────────────────────
48
- # /api/load β€” parse uploaded PDF into text chunks (no GPU needed)
49
- # ─────────────────────────────────────────────────────────────────────────────
50
-
51
- @fastapi_app.post("/api/load")
52
- async def api_load(file: UploadFile = File(...)):
53
- if not (file.filename or "").lower().endswith(".pdf"):
54
- raise HTTPException(400, "Only PDF files are supported.")
55
- content = await file.read()
56
- with tempfile.NamedTemporaryFile(suffix=".pdf", delete=False) as tmp:
57
- tmp.write(content)
58
- tmp_path = tmp.name
59
- try:
60
- text = extract_text(tmp_path)
61
- chunks = chunk_text(text)
62
- if not chunks:
63
- raise HTTPException(400, "No text found in PDF (scanned or too short?).")
64
- return {"chunks": chunks, "count": len(chunks)}
65
- except HTTPException:
66
- raise
67
- except ValueError as exc:
68
- raise HTTPException(400, str(exc))
69
- except Exception as exc:
70
- raise HTTPException(500, f"Unexpected error: {exc}")
71
- finally:
72
- os.unlink(tmp_path)
73
-
74
-
75
- # ─────────────────────────────────────────────────────────────────────────────
76
- # /api/question β€” generate a study question from a chunk (GPU)
77
- # ─────────────────────────────────────────────────────────────────────────────
78
-
79
- @spaces.GPU(duration=60)
80
- def _gen_question(chunk: str, language: str, difficulty: str) -> str:
81
- return generate_question(chunk, language, difficulty)
82
-
83
-
84
- @fastapi_app.post("/api/question")
85
- async def api_question(request: Request):
86
- body = await request.json()
87
- chunk = body.get("chunk", "")
88
- language = body.get("language", "English")
89
- difficulty = body.get("difficulty", "Normal")
90
- if not chunk:
91
- raise HTTPException(400, "chunk is required.")
92
- try:
93
- question = _gen_question(chunk, language, difficulty)
94
- return {"question": question}
95
- except Exception as exc:
96
- raise HTTPException(500, str(exc))
97
-
98
-
99
- # ─────────────────────────────────────────────────────────────────────────────
100
- # /api/evaluate β€” evaluate student answer against source chunk (GPU)
101
- # ─────────────────────────────────────────────────────────────────────────────
102
-
103
- @spaces.GPU(duration=120)
104
- def _eval_answer(question: str, chunk: str, answer: str, language: str) -> str:
105
- return evaluate_answer(question, chunk, answer, language)
106
-
107
-
108
- @fastapi_app.post("/api/evaluate")
109
- async def api_evaluate(request: Request):
110
- body = await request.json()
111
- question = body.get("question", "")
112
- chunk = body.get("chunk", "")
113
- answer = body.get("answer", "")
114
- language = body.get("language", "English")
115
- if not (question and chunk and answer):
116
- raise HTTPException(400, "question, chunk, and answer are required.")
117
- try:
118
- feedback = _eval_answer(question, chunk, answer, language)
119
- return {"feedback": feedback}
120
- except Exception as exc:
121
- raise HTTPException(500, str(exc))
122
-
123
-
124
- # ─────────────────────────────────────────────────────────────────────────────
125
- # Mount Gradio at /gradio as fallback and export the combined ASGI app
126
- # ─────────────────────────────────────────────────────────────────────────────
127
-
128
- app = gr.mount_gradio_app(fastapi_app, demo, path="/gradio")