tfrere commited on
Commit
c750639
·
1 Parent(s): 69523a3

add url importer | improve yourbench error handling | refactor

Browse files
backend/pyproject.toml CHANGED
@@ -26,6 +26,7 @@ dependencies = [
26
  "PyPDF2>=3.0.0",
27
  "beautifulsoup4>=4.12.0",
28
  "evaluate>=0.4.0",
 
29
  ]
30
 
31
  [build-system]
 
26
  "PyPDF2>=3.0.0",
27
  "beautifulsoup4>=4.12.0",
28
  "evaluate>=0.4.0",
29
+ "requests>=2.31.0",
30
  ]
31
 
32
  [build-system]
backend/routes/cleanup.py CHANGED
@@ -26,8 +26,8 @@ async def cleanup_session(session_id: str):
26
  Dictionary with status and message
27
  """
28
  # Check if we are in development mode
29
- # if os.environ.get("ENVIRONEMENT", "").lower() == "development":
30
- if True:
31
  logging.info(f"[DEV MODE] Cleanup called for session: {session_id} - No action taken in development mode")
32
  return {
33
  "success": True,
 
26
  Dictionary with status and message
27
  """
28
  # Check if we are in development mode
29
+ if os.environ.get("ENVIRONEMENT", "").lower() == "development":
30
+ # if True:
31
  logging.info(f"[DEV MODE] Cleanup called for session: {session_id} - No action taken in development mode")
32
  return {
33
  "success": True,
backend/routes/download.py CHANGED
@@ -1,5 +1,5 @@
1
  from fastapi import APIRouter, HTTPException
2
- from fastapi.responses import StreamingResponse
3
  from huggingface_hub import hf_hub_download, snapshot_download
4
  import os
5
  import tempfile
@@ -7,6 +7,8 @@ import shutil
7
  import zipfile
8
  import io
9
  import logging
 
 
10
 
11
  router = APIRouter(tags=["download"])
12
 
@@ -71,4 +73,85 @@ async def download_dataset(session_id: str):
71
  raise HTTPException(
72
  status_code=500,
73
  detail=f"Erreur lors du téléchargement: {str(e)}"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  )
 
1
  from fastapi import APIRouter, HTTPException
2
+ from fastapi.responses import StreamingResponse, JSONResponse
3
  from huggingface_hub import hf_hub_download, snapshot_download
4
  import os
5
  import tempfile
 
7
  import zipfile
8
  import io
9
  import logging
10
+ import json
11
+ from datasets import load_dataset
12
 
13
  router = APIRouter(tags=["download"])
14
 
 
73
  raise HTTPException(
74
  status_code=500,
75
  detail=f"Erreur lors du téléchargement: {str(e)}"
76
+ )
77
+
78
+ @router.get("/download-questions/{session_id}")
79
+ async def download_questions(session_id: str):
80
+ """
81
+ Télécharge les questions générées pour une session au format JSON
82
+
83
+ Args:
84
+ session_id: Identifiant de la session
85
+
86
+ Returns:
87
+ Fichier JSON contenant les questions générées
88
+ """
89
+ try:
90
+ # Identifiant du repo HuggingFace
91
+ dataset_repo_id = f"yourbench/yourbench_{session_id}"
92
+
93
+ # Initialize questions list
94
+ all_questions = []
95
+
96
+ # Try to load single-shot questions
97
+ try:
98
+ single_dataset = load_dataset(dataset_repo_id, 'single_shot_questions')
99
+ if single_dataset and len(single_dataset['train']) > 0:
100
+ for idx in range(len(single_dataset['train'])):
101
+ all_questions.append({
102
+ "id": str(idx),
103
+ "question": single_dataset['train'][idx].get("question", ""),
104
+ "answer": single_dataset['train'][idx].get("self_answer", "No answer available"),
105
+ "type": "single_shot"
106
+ })
107
+ logging.info(f"Loaded {len(all_questions)} single-shot questions")
108
+ except Exception as e:
109
+ logging.error(f"Error loading single-shot questions: {str(e)}")
110
+
111
+ # Try to load multi-hop questions
112
+ try:
113
+ multi_dataset = load_dataset(dataset_repo_id, 'multi_hop_questions')
114
+ if multi_dataset and len(multi_dataset['train']) > 0:
115
+ start_idx = len(all_questions)
116
+ for idx in range(len(multi_dataset['train'])):
117
+ all_questions.append({
118
+ "id": str(start_idx + idx),
119
+ "question": multi_dataset['train'][idx].get("question", ""),
120
+ "answer": multi_dataset['train'][idx].get("self_answer", "No answer available"),
121
+ "type": "multi_hop"
122
+ })
123
+ logging.info(f"Loaded {len(multi_dataset['train'])} multi-hop questions")
124
+ except Exception as e:
125
+ logging.error(f"Error loading multi-hop questions: {str(e)}")
126
+
127
+ # If we couldn't load any questions, the dataset might not exist
128
+ if len(all_questions) == 0:
129
+ raise HTTPException(status_code=404, detail="Aucune question trouvée pour cette session")
130
+
131
+ # Convert questions to JSON
132
+ questions_json = json.dumps({
133
+ "session_id": session_id,
134
+ "questions": all_questions
135
+ }, ensure_ascii=False, indent=2)
136
+
137
+ # Create a BytesIO object with the JSON data
138
+ json_bytes = io.BytesIO(questions_json.encode('utf-8'))
139
+ json_bytes.seek(0)
140
+
141
+ # Return the JSON file for download
142
+ filename = f"yourbench_{session_id}_questions.json"
143
+ return StreamingResponse(
144
+ json_bytes,
145
+ media_type="application/json",
146
+ headers={"Content-Disposition": f"attachment; filename={filename}"}
147
+ )
148
+
149
+ except HTTPException:
150
+ # Re-raise HTTP exceptions
151
+ raise
152
+ except Exception as e:
153
+ logging.error(f"Erreur lors de la récupération des questions: {str(e)}")
154
+ raise HTTPException(
155
+ status_code=500,
156
+ detail=f"Erreur lors du téléchargement des questions: {str(e)}"
157
  )
backend/routes/questions.py CHANGED
@@ -50,26 +50,26 @@ async def get_benchmark_questions(session_id: str):
50
  except Exception as e:
51
  print(f"Error loading single-shot questions: {str(e)}")
52
 
53
- try:
54
- # Essayer de charger les questions multi-hop si nécessaire
55
- if len(questions) < 2:
56
- multi_dataset = load_dataset(dataset_repo_id, 'multi_hop_questions')
57
- if multi_dataset and len(multi_dataset['train']) > 0:
58
- # Prendre les questions multi-hop pour compléter, en évitant aussi la première
59
- start_idx = 1
60
- remaining = 2 - len(questions)
61
- max_questions = min(remaining, max(0, len(multi_dataset['train']) - start_idx))
62
- for i in range(max_questions):
63
- idx = start_idx + i
64
- questions.append({
65
- "id": str(idx),
66
- "question": multi_dataset['train'][idx].get("question", ""),
67
- "answer": multi_dataset['train'][idx].get("self_answer", "No answer available"),
68
- "type": "multi_hop"
69
- })
70
- print(f"Loaded {len(questions)} multi-hop questions")
71
- except Exception as e:
72
- print(f"Error loading multi-hop questions: {str(e)}")
73
 
74
  # If we couldn't load any questions, the dataset might not exist
75
  if len(questions) == 0:
 
50
  except Exception as e:
51
  print(f"Error loading single-shot questions: {str(e)}")
52
 
53
+ # try:
54
+ # # Essayer de charger les questions multi-hop si nécessaire
55
+ # if len(questions) < 2:
56
+ # multi_dataset = load_dataset(dataset_repo_id, 'multi_hop_questions')
57
+ # if multi_dataset and len(multi_dataset['train']) > 0:
58
+ # # Prendre les questions multi-hop pour compléter, en évitant aussi la première
59
+ # start_idx = 1
60
+ # remaining = 2 - len(questions)
61
+ # max_questions = min(remaining, max(0, len(multi_dataset['train']) - start_idx))
62
+ # for i in range(max_questions):
63
+ # idx = start_idx + i
64
+ # questions.append({
65
+ # "id": str(idx),
66
+ # "question": multi_dataset['train'][idx].get("question", ""),
67
+ # "answer": multi_dataset['train'][idx].get("self_answer", "No answer available"),
68
+ # "type": "multi_hop"
69
+ # })
70
+ # print(f"Loaded {len(questions)} multi-hop questions")
71
+ # except Exception as e:
72
+ # print(f"Error loading multi-hop questions: {str(e)}")
73
 
74
  # If we couldn't load any questions, the dataset might not exist
75
  if len(questions) == 0:
backend/routes/upload.py CHANGED
@@ -4,28 +4,31 @@ import shutil
4
  import uuid
5
  from bs4 import BeautifulSoup
6
  from PyPDF2 import PdfReader
 
 
 
7
 
8
  router = APIRouter(tags=["files"])
9
 
10
- # Définir le stockage des fichiers par session (importé dans main.py)
11
  session_files = {}
12
 
13
- # Dossier racine pour les uploads
14
  UPLOAD_ROOT = "uploaded_files"
15
  os.makedirs(UPLOAD_ROOT, exist_ok=True)
16
 
17
- # Longueur minimale pour tout fichier (en caractères)
18
  MIN_FILE_LENGTH = 500
19
 
20
  def validate_pdf(file_path: str) -> bool:
21
  """Validate if file is a valid PDF."""
22
  try:
23
  reader = PdfReader(file_path)
24
- # Vérifier que le PDF a au moins une page
25
  if len(reader.pages) == 0:
26
  return False
27
 
28
- # Extraire le texte pour vérifier la longueur
29
  text = ""
30
  for page in reader.pages:
31
  text += page.extract_text()
@@ -39,7 +42,7 @@ def validate_markdown(file_path: str) -> bool:
39
  try:
40
  with open(file_path, 'r', encoding='utf-8') as f:
41
  content = f.read()
42
- # Vérifier longueur minimale et présence d'éléments markdown
43
  return len(content) >= MIN_FILE_LENGTH and any(marker in content for marker in ['#', '-', '*', '`', '[', '>'])
44
  except:
45
  return False
@@ -49,7 +52,7 @@ def validate_html(file_path: str) -> bool:
49
  try:
50
  with open(file_path, 'r', encoding='utf-8') as f:
51
  content = f.read()
52
- # Vérifier longueur minimale et structure HTML
53
  if len(content) < MIN_FILE_LENGTH:
54
  return False
55
  BeautifulSoup(content, 'html.parser')
@@ -100,7 +103,7 @@ async def upload_file(file: UploadFile = File(...)):
100
  Returns:
101
  Dictionary with filename, status and session_id
102
  """
103
- # Vérifier si le fichier est un PDF, TXT, HTML ou MD
104
  if not file.filename.endswith(('.pdf', '.txt', '.html', '.md')):
105
  raise HTTPException(status_code=400, detail="Only PDF, TXT, HTML and MD files are accepted")
106
 
@@ -121,11 +124,11 @@ async def upload_file(file: UploadFile = File(...)):
121
  # Create the full path to save the file
122
  file_path = os.path.join(uploaded_files_dir, standardized_filename)
123
 
124
- # Sauvegarder le fichier
125
  with open(file_path, "wb") as buffer:
126
  shutil.copyfileobj(file.file, buffer)
127
 
128
- # Valider le fichier selon son type
129
  is_valid = False
130
  error_detail = ""
131
 
@@ -194,11 +197,83 @@ async def upload_file(file: UploadFile = File(...)):
194
  is_valid = False
195
 
196
  if not is_valid:
197
- # Supprimer le fichier invalide
198
  os.remove(file_path)
199
  raise HTTPException(status_code=400, detail=error_detail or f"Invalid {file_extension[1:].upper()} file")
200
 
201
  # Store file path for later use
202
  session_files[session_id] = file_path
203
 
204
- return {"filename": standardized_filename, "status": "uploaded", "session_id": session_id}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4
  import uuid
5
  from bs4 import BeautifulSoup
6
  from PyPDF2 import PdfReader
7
+ import requests
8
+ from fastapi import Form
9
+ from typing import Optional
10
 
11
  router = APIRouter(tags=["files"])
12
 
13
+ # Define file storage by session (imported in main.py)
14
  session_files = {}
15
 
16
+ # Root folder for uploads
17
  UPLOAD_ROOT = "uploaded_files"
18
  os.makedirs(UPLOAD_ROOT, exist_ok=True)
19
 
20
+ # Minimum length for any file (in characters)
21
  MIN_FILE_LENGTH = 500
22
 
23
  def validate_pdf(file_path: str) -> bool:
24
  """Validate if file is a valid PDF."""
25
  try:
26
  reader = PdfReader(file_path)
27
+ # Check that the PDF has at least one page
28
  if len(reader.pages) == 0:
29
  return False
30
 
31
+ # Extract text to check length
32
  text = ""
33
  for page in reader.pages:
34
  text += page.extract_text()
 
42
  try:
43
  with open(file_path, 'r', encoding='utf-8') as f:
44
  content = f.read()
45
+ # Check minimum length and presence of markdown elements
46
  return len(content) >= MIN_FILE_LENGTH and any(marker in content for marker in ['#', '-', '*', '`', '[', '>'])
47
  except:
48
  return False
 
52
  try:
53
  with open(file_path, 'r', encoding='utf-8') as f:
54
  content = f.read()
55
+ # Check minimum length and HTML structure
56
  if len(content) < MIN_FILE_LENGTH:
57
  return False
58
  BeautifulSoup(content, 'html.parser')
 
103
  Returns:
104
  Dictionary with filename, status and session_id
105
  """
106
+ # Check if the file is a PDF, TXT, HTML or MD
107
  if not file.filename.endswith(('.pdf', '.txt', '.html', '.md')):
108
  raise HTTPException(status_code=400, detail="Only PDF, TXT, HTML and MD files are accepted")
109
 
 
124
  # Create the full path to save the file
125
  file_path = os.path.join(uploaded_files_dir, standardized_filename)
126
 
127
+ # Save the file
128
  with open(file_path, "wb") as buffer:
129
  shutil.copyfileobj(file.file, buffer)
130
 
131
+ # Validate the file according to its type
132
  is_valid = False
133
  error_detail = ""
134
 
 
197
  is_valid = False
198
 
199
  if not is_valid:
200
+ # Delete the invalid file
201
  os.remove(file_path)
202
  raise HTTPException(status_code=400, detail=error_detail or f"Invalid {file_extension[1:].upper()} file")
203
 
204
  # Store file path for later use
205
  session_files[session_id] = file_path
206
 
207
+ return {"filename": standardized_filename, "status": "uploaded", "session_id": session_id}
208
+
209
+ @router.post("/upload-url")
210
+ async def upload_url(url: str = Form(...)):
211
+ """
212
+ Upload content from a URL, extract text and store it as a document
213
+
214
+ Args:
215
+ url: The URL to download content from
216
+
217
+ Returns:
218
+ Dictionary with status and session_id
219
+ """
220
+ try:
221
+ # Retrieve the content from the URL
222
+ response = requests.get(url, timeout=10)
223
+ response.raise_for_status() # Raise an exception if the HTTP status is not 200
224
+
225
+ # Extract text from HTML with BeautifulSoup
226
+ soup = BeautifulSoup(response.text, 'html.parser')
227
+
228
+ # Remove script and style tags
229
+ for script in soup(["script", "style"]):
230
+ script.extract()
231
+
232
+ # Extract the text
233
+ text = soup.get_text()
234
+
235
+ # Clean the text (remove multiple spaces and empty lines)
236
+ lines = (line.strip() for line in text.splitlines())
237
+ chunks = (phrase.strip() for line in lines for phrase in line.split(" "))
238
+ text = '\n'.join(chunk for chunk in chunks if chunk)
239
+
240
+ # Limit to 1000 characters if necessary
241
+ if len(text) > 25000:
242
+ text = text[:25000]
243
+
244
+ # Check if the text is long enough
245
+ if len(text.strip()) < MIN_FILE_LENGTH:
246
+ raise HTTPException(
247
+ status_code=400,
248
+ detail=f"The content is too short ({len(text.strip())} characters). Minimum required: {MIN_FILE_LENGTH} characters."
249
+ )
250
+
251
+ # Generate a session ID
252
+ session_id = str(uuid.uuid4())
253
+ # Create the directory structure for the session
254
+ session_dir = os.path.join(UPLOAD_ROOT, session_id)
255
+ uploaded_files_dir = os.path.join(session_dir, "uploaded_files")
256
+ os.makedirs(uploaded_files_dir, exist_ok=True)
257
+
258
+ # Path of the file to save
259
+ file_path = os.path.join(uploaded_files_dir, "document.txt")
260
+
261
+ # Save the text
262
+ with open(file_path, "w", encoding="utf-8") as f:
263
+ f.write(text)
264
+
265
+ # Store the file path for later use
266
+ session_files[session_id] = file_path
267
+
268
+ return {
269
+ "status": "uploaded",
270
+ "session_id": session_id,
271
+ "filename": "document.txt",
272
+ "text_length": len(text),
273
+ "source_url": url
274
+ }
275
+
276
+ except requests.exceptions.RequestException as e:
277
+ raise HTTPException(status_code=400, detail=f"Error retrieving the URL: {str(e)}")
278
+ except Exception as e:
279
+ raise HTTPException(status_code=500, detail=f"Error processing the URL: {str(e)}")
backend/tasks/create_bench.py CHANGED
@@ -140,6 +140,9 @@ class CreateBenchTask:
140
  self._add_log(f"[ERROR] {line}")
141
  elif "WARNING" in line:
142
  self._add_log(f"[WARN] {line}")
 
 
 
143
  else:
144
  # Detect completed stages
145
  if "Completed stage:" in line:
 
140
  self._add_log(f"[ERROR] {line}")
141
  elif "WARNING" in line:
142
  self._add_log(f"[WARN] {line}")
143
+ # Detect specific warning about no valid questions
144
+ if "No valid questions produced in single_shot_question_generation" in line:
145
+ self._add_log("[ERROR] Failed to generate benchmark: The document does not contain enough information to generate a meaningful benchmark. Please try with a more detailed document.")
146
  else:
147
  # Detect completed stages
148
  if "Completed stage:" in line:
backend/tasks/get_available_model_provider.py CHANGED
@@ -68,7 +68,7 @@ def test_provider(model_name: str, provider: str, verbose: bool = False) -> bool
68
  except Exception as e:
69
  if verbose:
70
  error_message = str(e)
71
- logger.error(f"Error with provider {provider}: {error_message}")
72
 
73
  # Log specific error types if we can identify them
74
  if "status_code=429" in error_message:
@@ -78,12 +78,12 @@ def test_provider(model_name: str, provider: str, verbose: bool = False) -> bool
78
  elif "status_code=503" in error_message:
79
  logger.warning(f"Provider {provider} service unavailable. Model may be loading or provider is down.")
80
  elif "timed out" in error_message.lower():
81
- logger.error(f"Timeout error with provider {provider} - request timed out after 10 seconds")
82
  return False
83
 
84
  except Exception as e:
85
  if verbose:
86
- logger.error(f"Error in test_provider: {str(e)}")
87
  return False
88
 
89
  def get_available_model_provider(model_name, verbose=False):
@@ -127,6 +127,9 @@ def get_available_model_provider(model_name, verbose=False):
127
  if test_provider(model_name, provider, verbose):
128
  return provider
129
 
 
 
 
130
  return None
131
 
132
  except Exception as e:
@@ -147,12 +150,19 @@ if __name__ == "__main__":
147
  ]
148
 
149
  providers = []
 
150
 
151
  for model in models:
152
  provider = get_available_model_provider(model, verbose=True)
153
- providers.append((model, provider))
 
 
 
154
 
155
  for model, provider in providers:
156
  print(f"Model: {model}, Provider: {provider}")
157
-
 
 
 
158
  print(f"Total Providers {len(providers)}: {providers}")
 
68
  except Exception as e:
69
  if verbose:
70
  error_message = str(e)
71
+ logger.warning(f"Error with provider {provider}: {error_message}")
72
 
73
  # Log specific error types if we can identify them
74
  if "status_code=429" in error_message:
 
78
  elif "status_code=503" in error_message:
79
  logger.warning(f"Provider {provider} service unavailable. Model may be loading or provider is down.")
80
  elif "timed out" in error_message.lower():
81
+ logger.warning(f"Timeout error with provider {provider} - request timed out after 10 seconds")
82
  return False
83
 
84
  except Exception as e:
85
  if verbose:
86
+ logger.warning(f"Error in test_provider: {str(e)}")
87
  return False
88
 
89
  def get_available_model_provider(model_name, verbose=False):
 
127
  if test_provider(model_name, provider, verbose):
128
  return provider
129
 
130
+ # If we've tried all providers and none worked, log this but don't raise an exception
131
+ if verbose:
132
+ logger.error(f"No available providers for {model_name}")
133
  return None
134
 
135
  except Exception as e:
 
150
  ]
151
 
152
  providers = []
153
+ unavailable_models = []
154
 
155
  for model in models:
156
  provider = get_available_model_provider(model, verbose=True)
157
+ if provider:
158
+ providers.append((model, provider))
159
+ else:
160
+ unavailable_models.append(model)
161
 
162
  for model, provider in providers:
163
  print(f"Model: {model}, Provider: {provider}")
164
+
165
+ if unavailable_models:
166
+ print(f"Models with no available providers: {', '.join(unavailable_models)}")
167
+
168
  print(f"Total Providers {len(providers)}: {providers}")
frontend/src/components/Benchmark/CreateForm.jsx CHANGED
@@ -1,4 +1,4 @@
1
- import React, { useState, useRef } from "react";
2
  import {
3
  Box,
4
  Paper,
@@ -14,6 +14,8 @@ import {
14
  DialogTitle,
15
  DialogContent,
16
  DialogActions,
 
 
17
  } from "@mui/material";
18
  import { alpha } from "@mui/material/styles";
19
  import CloudUploadIcon from "@mui/icons-material/CloudUpload";
@@ -25,9 +27,11 @@ import MenuBookIcon from "@mui/icons-material/MenuBook";
25
  import DownloadIcon from "@mui/icons-material/Download";
26
  import VisibilityIcon from "@mui/icons-material/Visibility";
27
  import CloseIcon from "@mui/icons-material/Close";
 
28
  import { useThemeMode } from "../../hooks/useThemeMode";
29
  import getTheme from "../../config/theme";
30
  import API_CONFIG from "../../config/api";
 
31
 
32
  /**
33
  * Component for creating a new benchmark, including file upload and generation initiation
@@ -39,20 +43,45 @@ import API_CONFIG from "../../config/api";
39
  function CreateForm({ onStartGeneration }) {
40
  const { mode } = useThemeMode();
41
  const theme = getTheme(mode);
42
- const [isDragging, setIsDragging] = useState(false);
43
- const [uploadStatus, setUploadStatus] = useState(null);
44
- const [isLoading, setIsLoading] = useState(false);
45
- const [sessionId, setSessionId] = useState(null);
46
  const [openSnackbar, setOpenSnackbar] = useState(false);
47
- const [selectedDocument, setSelectedDocument] = useState(null);
48
- const [isDefaultDocument, setIsDefaultDocument] = useState(false);
49
  const [isDownloading, setIsDownloading] = useState(false);
50
  const [documentContent, setDocumentContent] = useState("");
51
  const [openContentModal, setOpenContentModal] = useState(false);
52
  const [isLoadingContent, setIsLoadingContent] = useState(false);
53
  const [modalDocument, setModalDocument] = useState(null);
54
- const fileInputRef = useRef(null);
55
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
56
  const defaultDocuments = [
57
  {
58
  id: "the-bitter-lesson",
@@ -78,143 +107,45 @@ function CreateForm({ onStartGeneration }) {
78
  setOpenSnackbar(false);
79
  };
80
 
81
- const handleDragOver = (e) => {
82
- e.preventDefault();
83
- setIsDragging(true);
84
- };
85
-
86
- const handleDragLeave = () => {
87
- setIsDragging(false);
88
- };
89
-
90
- const handleClick = () => {
91
- fileInputRef.current.click();
92
- };
93
-
94
- const handleFileChange = (e) => {
95
- const file = e.target.files[0];
96
- if (!file) return;
97
-
98
- // Check if it's a PDF, TXT, HTML or MD
99
- if (
100
- !file.name.endsWith(".pdf") &&
101
- !file.name.endsWith(".txt") &&
102
- !file.name.endsWith(".html") &&
103
- !file.name.endsWith(".md")
104
- ) {
105
- setUploadStatus({
106
- success: false,
107
- message: "Only PDF, TXT, HTML and MD files are accepted",
108
- });
109
- setOpenSnackbar(true);
110
- return;
111
- }
112
-
113
- // Check file size limit (3MB = 3145728 bytes)
114
- if (file.size > 1048576 * 2) {
115
- setUploadStatus({
116
- success: false,
117
- message: "File size exceeds the 2MB limit",
118
- });
119
- setOpenSnackbar(true);
120
- return;
121
- }
122
-
123
- handleFileUpload(file);
124
- };
125
-
126
- const handleFileUpload = async (file) => {
127
- setIsLoading(true);
128
- setUploadStatus(null);
129
- setIsDefaultDocument(false);
130
- setSelectedDocument(null);
131
 
132
  try {
133
- const formData = new FormData();
134
- formData.append("file", file);
135
-
136
- const response = await fetch(`${API_CONFIG.BASE_URL}/upload`, {
137
- method: "POST",
138
- body: formData,
139
- });
140
-
141
- const result = await response.json();
142
-
143
- if (response.ok) {
144
- setUploadStatus({
145
- success: true,
146
- message: "File uploaded successfully",
147
- });
148
- setOpenSnackbar(true);
149
- setSessionId(result.session_id);
150
- setSelectedDocument({ name: file.name });
151
  } else {
152
- setUploadStatus({
153
- success: false,
154
- message: result.detail || "Upload failed",
155
- });
156
- setOpenSnackbar(true);
157
  }
158
- } catch (error) {
159
- setUploadStatus({
160
- success: false,
161
- message: "Server connection error",
162
- });
163
- setOpenSnackbar(true);
164
- } finally {
165
- setIsLoading(false);
166
- }
167
- };
168
 
169
- const handleDrop = async (e) => {
170
- e.preventDefault();
171
- setIsDragging(false);
172
-
173
- const file = e.dataTransfer.files[0];
174
- if (!file) {
175
- setUploadStatus({ success: false, message: "No file detected" });
176
- setOpenSnackbar(true);
177
- return;
178
- }
179
 
180
- // Check if it's a PDF, TXT, HTML or MD
181
- if (
182
- !file.name.endsWith(".pdf") &&
183
- !file.name.endsWith(".txt") &&
184
- !file.name.endsWith(".html") &&
185
- !file.name.endsWith(".md")
186
- ) {
187
- setUploadStatus({
188
- success: false,
189
- message: "Only PDF, TXT, HTML and MD files are accepted",
190
- });
191
- setOpenSnackbar(true);
192
- return;
193
- }
194
 
195
- // Check file size limit (3MB = 3145728 bytes)
196
- if (file.size > 1048576 * 3) {
 
 
197
  setUploadStatus({
198
  success: false,
199
- message: "File size exceeds the 3MB limit",
200
  });
201
- setOpenSnackbar(true);
202
- return;
203
  }
204
-
205
- handleFileUpload(file);
206
- };
207
-
208
- const handleDefaultDocClick = (doc) => {
209
- setSelectedDocument(doc);
210
- setSessionId(doc.id);
211
- setIsDefaultDocument(true);
212
  };
213
 
214
- const handleGenerateClick = () => {
215
- if (onStartGeneration && sessionId) {
216
- onStartGeneration(sessionId, isDefaultDocument);
217
- }
 
 
 
218
  };
219
 
220
  const handleDownloadDocument = async (doc) => {
@@ -247,54 +178,11 @@ function CreateForm({ onStartGeneration }) {
247
  success: false,
248
  message: "Error downloading document",
249
  });
250
- setOpenSnackbar(true);
251
  } finally {
252
  setIsDownloading(false);
253
  }
254
  };
255
 
256
- const handleViewDocument = async (doc) => {
257
- setIsLoadingContent(true);
258
-
259
- try {
260
- let extension = "";
261
- if (doc.id === "the-bitter-lesson") {
262
- extension = "html";
263
- } else if (doc.id === "hurricane-faq") {
264
- extension = "md";
265
- } else {
266
- extension = "txt";
267
- }
268
-
269
- // Mettre à jour l'état du document pour la modale
270
- setModalDocument(doc);
271
-
272
- const response = await fetch(`/${doc.id}.${extension}`);
273
- const text = await response.text();
274
-
275
- setDocumentContent(text);
276
- setOpenContentModal(true);
277
- } catch (error) {
278
- console.error("Error loading document content:", error);
279
- setUploadStatus({
280
- success: false,
281
- message: "Error loading document content",
282
- });
283
- setOpenSnackbar(true);
284
- } finally {
285
- setIsLoadingContent(false);
286
- }
287
- };
288
-
289
- const handleCloseContentModal = () => {
290
- setOpenContentModal(false);
291
- // Réinitialiser après la fermeture de la modale
292
- setTimeout(() => {
293
- setDocumentContent("");
294
- setModalDocument(null);
295
- }, 300);
296
- };
297
-
298
  return (
299
  <Box sx={{ mt: -2 }}>
300
  <Typography
@@ -303,10 +191,11 @@ function CreateForm({ onStartGeneration }) {
303
  align="center"
304
  sx={{ mb: 2, color: "text.secondary" }}
305
  >
306
- To create a benchmark, choose a sample document
 
307
  </Typography>
308
 
309
- <Grid container spacing={2} sx={{ mb: 2 }}>
310
  {defaultDocuments.map((doc) => (
311
  <Grid item xs={12} sm={4} key={doc.id}>
312
  <Box
@@ -379,81 +268,149 @@ function CreateForm({ onStartGeneration }) {
379
  ))}
380
  </Grid>
381
 
382
- <Typography
383
- variant="subtitle1"
384
- component="div"
385
- align="center"
386
- sx={{ mb: 2, color: "text.secondary" }}
387
- >
388
- Or upload your own ...
389
- </Typography>
390
-
391
  <Box
392
  sx={{
393
- p: 4,
394
- mt: 2,
395
- mb: 2,
396
- borderRadius: 1.5,
397
- border:
398
- selectedDocument?.name && !isDefaultDocument
399
- ? `2px solid ${theme.palette.primary.main}`
400
- : isDragging
401
- ? `2px dashed ${theme.palette.primary.main}`
402
- : `2px dashed ${theme.palette.divider}`,
403
- backgroundColor: isDragging
404
- ? alpha(theme.palette.action.hover, 0.5)
405
- : "transparent",
406
  display: "flex",
407
- flexDirection: "column",
408
- alignItems: "center",
409
- justifyContent: "center",
410
- minHeight: 180,
411
- cursor: "pointer",
412
- transition: "all 0.3s ease",
413
  }}
414
- onDragOver={handleDragOver}
415
- onDragLeave={handleDragLeave}
416
- onDrop={handleDrop}
417
- onClick={handleClick}
418
  >
419
- <input
420
- type="file"
421
- ref={fileInputRef}
422
- onChange={handleFileChange}
423
- accept=".pdf,.txt,.html,.md"
424
- style={{ display: "none" }}
425
- />
426
- {selectedDocument?.name && !isDefaultDocument ? (
427
- <>
428
- <InsertDriveFileIcon
429
- sx={{ fontSize: 50, color: "primary.main", mb: 1 }}
430
- />
431
- <Typography variant="h6" component="div" gutterBottom>
432
- {selectedDocument.name}
433
- </Typography>
434
- <Typography variant="body2" color="text.secondary">
435
- Click to upload a different file
436
- </Typography>
437
- </>
438
- ) : (
439
- <>
440
- {isLoading ? (
441
- <CircularProgress size={50} sx={{ mb: 1 }} />
442
- ) : (
443
- <CloudUploadIcon
 
 
 
 
 
 
 
 
 
 
 
 
 
 
444
  sx={{ fontSize: 50, color: "primary.main", mb: 1 }}
445
  />
446
- )}
447
- <Typography variant="h6" component="div" gutterBottom>
448
- {isLoading
449
- ? "Uploading your file..."
450
- : "Drag and drop your file here or click to browse"}
451
- </Typography>
452
- <Typography variant="body2" color="text.secondary">
453
- Accepted formats: PDF, TXT, HTML, MD
454
- </Typography>
455
- </>
456
- )}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
457
  </Box>
458
 
459
  <Box sx={{ display: "flex", justifyContent: "center" }}>
@@ -465,19 +422,27 @@ function CreateForm({ onStartGeneration }) {
465
  disabled={!sessionId}
466
  sx={{ mt: 2 }}
467
  >
468
- Generate Benchmark
 
 
 
 
 
 
469
  </Button>
470
  </Box>
471
 
472
  <Snackbar
473
  open={openSnackbar}
474
- autoHideDuration={6000}
475
  onClose={handleCloseSnackbar}
476
  anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
477
  >
478
  <Alert
479
  onClose={handleCloseSnackbar}
480
  severity={uploadStatus?.success ? "success" : "error"}
 
 
481
  sx={{ width: "100%" }}
482
  >
483
  {uploadStatus?.message}
 
1
+ import React, { useState, useEffect } from "react";
2
  import {
3
  Box,
4
  Paper,
 
14
  DialogTitle,
15
  DialogContent,
16
  DialogActions,
17
+ TextField,
18
+ Divider,
19
  } from "@mui/material";
20
  import { alpha } from "@mui/material/styles";
21
  import CloudUploadIcon from "@mui/icons-material/CloudUpload";
 
27
  import DownloadIcon from "@mui/icons-material/Download";
28
  import VisibilityIcon from "@mui/icons-material/Visibility";
29
  import CloseIcon from "@mui/icons-material/Close";
30
+ import LinkIcon from "@mui/icons-material/Link";
31
  import { useThemeMode } from "../../hooks/useThemeMode";
32
  import getTheme from "../../config/theme";
33
  import API_CONFIG from "../../config/api";
34
+ import { useDocumentSelection } from "./hooks/useDocumentSelection";
35
 
36
  /**
37
  * Component for creating a new benchmark, including file upload and generation initiation
 
43
  function CreateForm({ onStartGeneration }) {
44
  const { mode } = useThemeMode();
45
  const theme = getTheme(mode);
46
+
47
+ // États pour la visualisation du document
 
 
48
  const [openSnackbar, setOpenSnackbar] = useState(false);
 
 
49
  const [isDownloading, setIsDownloading] = useState(false);
50
  const [documentContent, setDocumentContent] = useState("");
51
  const [openContentModal, setOpenContentModal] = useState(false);
52
  const [isLoadingContent, setIsLoadingContent] = useState(false);
53
  const [modalDocument, setModalDocument] = useState(null);
 
54
 
55
+ // Utiliser le hook personnalisé pour la gestion des documents
56
+ const {
57
+ isDragging,
58
+ isLoading,
59
+ sessionId,
60
+ selectedDocument,
61
+ isDefaultDocument,
62
+ urlInput,
63
+ urlSelected,
64
+ uploadStatus,
65
+ fileInputRef,
66
+ handleDragOver,
67
+ handleDragLeave,
68
+ handleClick,
69
+ handleFileChange,
70
+ handleDrop,
71
+ handleDefaultDocClick,
72
+ handleGenerateClick,
73
+ handleUrlInputChange,
74
+ setUploadStatus,
75
+ } = useDocumentSelection(onStartGeneration);
76
+
77
+ // Afficher le snackbar quand uploadStatus change
78
+ useEffect(() => {
79
+ if (uploadStatus) {
80
+ setOpenSnackbar(true);
81
+ }
82
+ }, [uploadStatus]);
83
+
84
+ // Liste des documents par défaut
85
  const defaultDocuments = [
86
  {
87
  id: "the-bitter-lesson",
 
107
  setOpenSnackbar(false);
108
  };
109
 
110
+ const handleViewDocument = async (doc) => {
111
+ setIsLoadingContent(true);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
112
 
113
  try {
114
+ let extension = "";
115
+ if (doc.id === "the-bitter-lesson") {
116
+ extension = "html";
117
+ } else if (doc.id === "hurricane-faq") {
118
+ extension = "md";
 
 
 
 
 
 
 
 
 
 
 
 
 
119
  } else {
120
+ extension = "txt";
 
 
 
 
121
  }
 
 
 
 
 
 
 
 
 
 
122
 
123
+ // Mettre à jour l'état du document pour la modale
124
+ setModalDocument(doc);
 
 
 
 
 
 
 
 
125
 
126
+ const response = await fetch(`/${doc.id}.${extension}`);
127
+ const text = await response.text();
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
+ setDocumentContent(text);
130
+ setOpenContentModal(true);
131
+ } catch (error) {
132
+ console.error("Error loading document content:", error);
133
  setUploadStatus({
134
  success: false,
135
+ message: "Error loading document content",
136
  });
137
+ } finally {
138
+ setIsLoadingContent(false);
139
  }
 
 
 
 
 
 
 
 
140
  };
141
 
142
+ const handleCloseContentModal = () => {
143
+ setOpenContentModal(false);
144
+ // Réinitialiser après la fermeture de la modale
145
+ setTimeout(() => {
146
+ setDocumentContent("");
147
+ setModalDocument(null);
148
+ }, 300);
149
  };
150
 
151
  const handleDownloadDocument = async (doc) => {
 
178
  success: false,
179
  message: "Error downloading document",
180
  });
 
181
  } finally {
182
  setIsDownloading(false);
183
  }
184
  };
185
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
186
  return (
187
  <Box sx={{ mt: -2 }}>
188
  <Typography
 
191
  align="center"
192
  sx={{ mb: 2, color: "text.secondary" }}
193
  >
194
+ To create a benchmark, choose a sample document or upload your own
195
+ file/URL
196
  </Typography>
197
 
198
+ <Grid container spacing={2} sx={{ mb: 0 }}>
199
  {defaultDocuments.map((doc) => (
200
  <Grid item xs={12} sm={4} key={doc.id}>
201
  <Box
 
268
  ))}
269
  </Grid>
270
 
 
 
 
 
 
 
 
 
 
271
  <Box
272
  sx={{
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  display: "flex",
274
+ flexDirection: { xs: "column", md: "row" },
275
+ gap: 2,
276
+ mb: 2,
 
 
 
277
  }}
 
 
 
 
278
  >
279
+ {/* Zone de glisser-déposer pour les fichiers */}
280
+ <Box
281
+ sx={{
282
+ flex: 1,
283
+ p: 4,
284
+ mt: 2,
285
+ borderRadius: 1.5,
286
+ border:
287
+ selectedDocument?.name && !isDefaultDocument && !urlSelected
288
+ ? `2px solid ${theme.palette.primary.main}`
289
+ : isDragging
290
+ ? `2px dashed ${theme.palette.primary.main}`
291
+ : `2px dashed ${theme.palette.divider}`,
292
+ backgroundColor: isDragging
293
+ ? alpha(theme.palette.action.hover, 0.5)
294
+ : "transparent",
295
+ display: "flex",
296
+ flexDirection: "column",
297
+ alignItems: "center",
298
+ justifyContent: "center",
299
+ minHeight: 180,
300
+ cursor: "pointer",
301
+ transition: "all 0.3s ease",
302
+ }}
303
+ onDragOver={handleDragOver}
304
+ onDragLeave={handleDragLeave}
305
+ onDrop={handleDrop}
306
+ onClick={handleClick}
307
+ >
308
+ <input
309
+ type="file"
310
+ ref={fileInputRef}
311
+ onChange={handleFileChange}
312
+ accept=".pdf,.txt,.html,.md"
313
+ style={{ display: "none" }}
314
+ />
315
+ {selectedDocument?.name && !isDefaultDocument && !urlSelected ? (
316
+ <>
317
+ <InsertDriveFileIcon
318
  sx={{ fontSize: 50, color: "primary.main", mb: 1 }}
319
  />
320
+ <Typography variant="h6" component="div" gutterBottom>
321
+ {selectedDocument.name}
322
+ </Typography>
323
+ <Typography variant="body2" color="text.secondary">
324
+ Click to upload a different file
325
+ </Typography>
326
+ </>
327
+ ) : (
328
+ <>
329
+ {isLoading && !urlSelected ? (
330
+ <CircularProgress size={50} sx={{ mb: 1 }} />
331
+ ) : (
332
+ <CloudUploadIcon
333
+ sx={{ fontSize: 50, color: "primary.main", mb: 1 }}
334
+ />
335
+ )}
336
+ <Typography variant="h6" component="div" gutterBottom>
337
+ {isLoading && !urlSelected
338
+ ? "Uploading your file..."
339
+ : "Drag and drop your file here or click to browse"}
340
+ </Typography>
341
+ <Typography variant="body2" color="text.secondary">
342
+ Accepted formats: PDF, TXT, HTML, MD
343
+ </Typography>
344
+ </>
345
+ )}
346
+ </Box>
347
+
348
+ {/* Champ d'entrée URL */}
349
+ <Box
350
+ sx={{
351
+ flex: 1,
352
+ p: 4,
353
+ mt: 2,
354
+ borderRadius: 1.5,
355
+ border: urlSelected
356
+ ? `2px solid ${theme.palette.primary.main}`
357
+ : `2px dashed ${theme.palette.divider}`,
358
+ display: "flex",
359
+ flexDirection: "column",
360
+ alignItems: "center",
361
+ justifyContent: "center",
362
+ minHeight: 180,
363
+ transition: "all 0.3s ease",
364
+ }}
365
+ >
366
+ {selectedDocument?.name && urlSelected ? (
367
+ <>
368
+ <LinkIcon sx={{ fontSize: 50, color: "primary.main", mb: 1 }} />
369
+ <Typography
370
+ variant="h6"
371
+ component="div"
372
+ gutterBottom
373
+ noWrap
374
+ sx={{
375
+ maxWidth: "100%",
376
+ textOverflow: "ellipsis",
377
+ overflow: "hidden",
378
+ }}
379
+ >
380
+ {selectedDocument?.domain || "URL processed"}
381
+ </Typography>
382
+ <TextField
383
+ fullWidth
384
+ variant="outlined"
385
+ label="Enter a new URL"
386
+ placeholder="https://example.com"
387
+ value={urlInput}
388
+ onChange={handleUrlInputChange}
389
+ disabled={isLoading}
390
+ />
391
+ </>
392
+ ) : (
393
+ <>
394
+ {isLoading && urlSelected ? (
395
+ <CircularProgress size={50} sx={{ mb: 1 }} />
396
+ ) : (
397
+ <LinkIcon sx={{ fontSize: 50, color: "primary.main", mb: 1 }} />
398
+ )}
399
+ <Typography variant="h6" component="div" gutterBottom>
400
+ {isLoading && urlSelected ? "Processing URL..." : "Enter a URL"}
401
+ </Typography>
402
+ <TextField
403
+ fullWidth
404
+ variant="outlined"
405
+ label="Website URL"
406
+ placeholder="https://example.com"
407
+ value={urlInput}
408
+ onChange={handleUrlInputChange}
409
+ disabled={isLoading}
410
+ />
411
+ </>
412
+ )}
413
+ </Box>
414
  </Box>
415
 
416
  <Box sx={{ display: "flex", justifyContent: "center" }}>
 
422
  disabled={!sessionId}
423
  sx={{ mt: 2 }}
424
  >
425
+ {!sessionId
426
+ ? "Select a document first"
427
+ : isDefaultDocument
428
+ ? `Generate Benchmark from "${selectedDocument?.name}"`
429
+ : urlSelected
430
+ ? "Generate Benchmark from URL"
431
+ : "Generate Benchmark from File"}
432
  </Button>
433
  </Box>
434
 
435
  <Snackbar
436
  open={openSnackbar}
437
+ autoHideDuration={5000}
438
  onClose={handleCloseSnackbar}
439
  anchorOrigin={{ vertical: "bottom", horizontal: "right" }}
440
  >
441
  <Alert
442
  onClose={handleCloseSnackbar}
443
  severity={uploadStatus?.success ? "success" : "error"}
444
+ variant="outlined"
445
+ elevation={1}
446
  sx={{ width: "100%" }}
447
  >
448
  {uploadStatus?.message}
frontend/src/components/Benchmark/DefaultDocumentCard.jsx ADDED
@@ -0,0 +1,104 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import {
3
+ Box,
4
+ Typography,
5
+ CircularProgress,
6
+ IconButton,
7
+ Tooltip,
8
+ } from "@mui/material";
9
+ import { alpha } from "@mui/material/styles";
10
+ import VisibilityIcon from "@mui/icons-material/Visibility";
11
+
12
+ /**
13
+ * Composant pour afficher une carte de document par défaut
14
+ * @param {Object} props - Propriétés du composant
15
+ * @returns {JSX.Element} - Carte de document par défaut
16
+ */
17
+ const DefaultDocumentCard = ({
18
+ doc,
19
+ theme,
20
+ selectedDocument,
21
+ isDefaultDocument,
22
+ urlSelected,
23
+ isLoadingContent,
24
+ handleDefaultDocClick,
25
+ handleViewDocument,
26
+ resetSelection,
27
+ }) => {
28
+ // Vérifier si cette carte est actuellement sélectionnée
29
+ const isSelected = selectedDocument?.id === doc.id;
30
+
31
+ // Vérifier si une autre méthode est active (URL ou fichier)
32
+ const otherMethodActive =
33
+ (selectedDocument && !isDefaultDocument) || (urlSelected && !isSelected);
34
+
35
+ return (
36
+ <Box
37
+ elevation={2}
38
+ sx={{
39
+ p: 2,
40
+ display: "flex",
41
+ flexDirection: "column",
42
+ borderRadius: 1.5,
43
+ alignItems: "center",
44
+ cursor: "pointer",
45
+ transition: "all 0.2s ease",
46
+ height: "100%",
47
+ position: "relative",
48
+ border: isSelected
49
+ ? `2px solid ${theme.palette.primary.main}`
50
+ : `2px solid ${theme.palette.divider}`,
51
+ "&:hover": {
52
+ transform: "translateY(-2px)",
53
+ boxShadow: 3,
54
+ },
55
+ }}
56
+ onClick={() => handleDefaultDocClick(doc)}
57
+ >
58
+ <Tooltip title="View content">
59
+ <IconButton
60
+ onClick={(e) => {
61
+ e.stopPropagation();
62
+ handleViewDocument(doc);
63
+ }}
64
+ sx={{
65
+ position: "absolute",
66
+ top: 4,
67
+ right: 4,
68
+ color: "text.secondary",
69
+ opacity: 0.4,
70
+ "&:hover": {
71
+ opacity: 0.8,
72
+ backgroundColor: alpha(theme.palette.primary.main, 0.05),
73
+ },
74
+ padding: 0.3,
75
+ "& .MuiSvgIcon-root": {
76
+ fontSize: 16,
77
+ },
78
+ }}
79
+ disabled={isLoadingContent}
80
+ >
81
+ {isLoadingContent && selectedDocument?.id === doc.id ? (
82
+ <CircularProgress size={14} />
83
+ ) : (
84
+ <VisibilityIcon />
85
+ )}
86
+ </IconButton>
87
+ </Tooltip>
88
+ <Box sx={{ color: "primary.main", mb: 1 }}>{doc.icon}</Box>
89
+ <Typography variant="subtitle1" component="div" gutterBottom>
90
+ {doc.name}
91
+ </Typography>
92
+ <Typography
93
+ variant="body2"
94
+ color="text.secondary"
95
+ align="center"
96
+ sx={{ flexGrow: 1 }}
97
+ >
98
+ {doc.description}
99
+ </Typography>
100
+ </Box>
101
+ );
102
+ };
103
+
104
+ export default DefaultDocumentCard;
frontend/src/components/Benchmark/DocumentViewerDialog.jsx ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import {
3
+ Dialog,
4
+ DialogTitle,
5
+ DialogContent,
6
+ Box,
7
+ Typography,
8
+ CircularProgress,
9
+ IconButton,
10
+ Tooltip,
11
+ } from "@mui/material";
12
+ import DownloadIcon from "@mui/icons-material/Download";
13
+ import CloseIcon from "@mui/icons-material/Close";
14
+
15
+ /**
16
+ * Composant pour afficher un document dans une boîte de dialogue
17
+ * @param {Object} props - Propriétés du composant
18
+ * @returns {JSX.Element} - Dialogue pour visualiser un document
19
+ */
20
+ const DocumentViewerDialog = ({
21
+ open,
22
+ onClose,
23
+ document,
24
+ content,
25
+ isLoading,
26
+ isDownloading,
27
+ handleDownload,
28
+ }) => {
29
+ // Déterminer le type de document
30
+ const getDocumentType = () => {
31
+ if (!document) return "";
32
+
33
+ if (document.id === "the-bitter-lesson") return "HTML";
34
+ if (document.id === "hurricane-faq") return "Markdown";
35
+ return "Text";
36
+ };
37
+
38
+ return (
39
+ <Dialog
40
+ open={open}
41
+ onClose={onClose}
42
+ maxWidth="md"
43
+ fullWidth
44
+ aria-labelledby="document-content-dialog-title"
45
+ >
46
+ <DialogTitle id="document-content-dialog-title">
47
+ <Box
48
+ sx={{
49
+ display: "flex",
50
+ justifyContent: "space-between",
51
+ alignItems: "flex-start",
52
+ }}
53
+ >
54
+ <Box>
55
+ {document && (
56
+ <Typography variant="h6" sx={{ fontWeight: 600 }}>
57
+ {document.name}
58
+ </Typography>
59
+ )}
60
+ <Typography variant="body2" color="text.secondary">
61
+ {document && getDocumentType()}
62
+ </Typography>
63
+ </Box>
64
+ <Box sx={{ display: "flex", gap: 1 }}>
65
+ {document && (
66
+ <Tooltip title="Download document">
67
+ <IconButton
68
+ edge="end"
69
+ color="inherit"
70
+ onClick={() => handleDownload(document)}
71
+ disabled={isDownloading}
72
+ aria-label="download"
73
+ sx={{
74
+ color: "text.secondary",
75
+ opacity: 0.4,
76
+ "&:hover": {
77
+ opacity: 0.8,
78
+ },
79
+ }}
80
+ >
81
+ {isDownloading ? (
82
+ <CircularProgress size={20} />
83
+ ) : (
84
+ <DownloadIcon />
85
+ )}
86
+ </IconButton>
87
+ </Tooltip>
88
+ )}
89
+ <IconButton
90
+ edge="end"
91
+ color="inherit"
92
+ onClick={onClose}
93
+ aria-label="close"
94
+ >
95
+ <CloseIcon />
96
+ </IconButton>
97
+ </Box>
98
+ </Box>
99
+ </DialogTitle>
100
+ <DialogContent
101
+ dividers
102
+ sx={{
103
+ padding: 0,
104
+ }}
105
+ >
106
+ {isLoading ? (
107
+ <Box sx={{ display: "flex", justifyContent: "center", my: 4 }}>
108
+ <CircularProgress />
109
+ </Box>
110
+ ) : (
111
+ <Box
112
+ sx={{
113
+ maxHeight: "60vh",
114
+ overflow: "auto",
115
+ whiteSpace: "pre-wrap",
116
+ fontFamily: "monospace",
117
+ fontSize: "0.875rem",
118
+ p: 2.5,
119
+ }}
120
+ >
121
+ {content}
122
+ </Box>
123
+ )}
124
+ </DialogContent>
125
+ </Dialog>
126
+ );
127
+ };
128
+
129
+ export default DocumentViewerDialog;
frontend/src/components/Benchmark/Generator.jsx CHANGED
@@ -244,6 +244,25 @@ const Generator = ({ sessionId, isDefaultDocument, onComplete }) => {
244
  );
245
 
246
  if (hasError) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
  const errorMessage =
248
  generationLogs.find(
249
  (log) =>
 
244
  );
245
 
246
  if (hasError) {
247
+ // Vérifier d'abord le cas spécifique du document avec info insuffisante
248
+ const insufficientInfoMessage = generationLogs.find((log) =>
249
+ log.includes(
250
+ "Failed to generate benchmark: The document does not contain enough information"
251
+ )
252
+ );
253
+
254
+ if (insufficientInfoMessage) {
255
+ setError(
256
+ "Your document doesn't contain enough information to generate a benchmark. Please try with a more comprehensive document that includes richer content."
257
+ );
258
+ notifyGenerationComplete(
259
+ false,
260
+ null,
261
+ "Insufficient document information"
262
+ );
263
+ return;
264
+ }
265
+
266
  const errorMessage =
267
  generationLogs.find(
268
  (log) =>
frontend/src/components/Benchmark/hooks/useBenchmarkLogs.js CHANGED
@@ -19,6 +19,21 @@ export const useBenchmarkLogs = (sessionId, isDefault, onComplete) => {
19
  const [generationComplete, setGenerationComplete] = useState(false);
20
 
21
  const checkForErrors = (logs) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
  // Check for rate limiting errors
23
  const hasRateLimitError = logs.some(
24
  (log) =>
@@ -65,6 +80,22 @@ export const useBenchmarkLogs = (sessionId, isDefault, onComplete) => {
65
  };
66
  }
67
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
68
  return { hasError: false };
69
  };
70
 
 
19
  const [generationComplete, setGenerationComplete] = useState(false);
20
 
21
  const checkForErrors = (logs) => {
22
+ // Check for document with insufficient information error
23
+ const hasInsufficientInfoError = logs.some((log) =>
24
+ log.includes(
25
+ "Failed to generate benchmark: The document does not contain enough information"
26
+ )
27
+ );
28
+
29
+ if (hasInsufficientInfoError) {
30
+ return {
31
+ hasError: true,
32
+ error:
33
+ "Your document doesn't contain enough information to generate a benchmark. Please try with a more comprehensive document that includes richer content.",
34
+ };
35
+ }
36
+
37
  // Check for rate limiting errors
38
  const hasRateLimitError = logs.some(
39
  (log) =>
 
80
  };
81
  }
82
 
83
+ // Check for insufficient content errors
84
+ const hasInsufficientContentError = logs.some(
85
+ (log) =>
86
+ log.includes(
87
+ "No valid questions produced in single_shot_question_generation"
88
+ ) || log.includes("No parseable JSON found")
89
+ );
90
+
91
+ if (hasInsufficientContentError) {
92
+ return {
93
+ hasError: true,
94
+ error:
95
+ "The document does not contain enough information to generate a meaningful benchmark. Please try with a more detailed document.",
96
+ };
97
+ }
98
+
99
  return { hasError: false };
100
  };
101
 
frontend/src/components/Benchmark/hooks/useDocumentSelection.js ADDED
@@ -0,0 +1,322 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { useState, useRef } from "react";
2
+ import API_CONFIG from "../../../config/api";
3
+
4
+ /**
5
+ * Hook personnalisé pour gérer la sélection et l'upload de documents
6
+ * @returns {Object} - Méthodes et états pour la gestion des documents
7
+ */
8
+ export const useDocumentSelection = (onStartGeneration) => {
9
+ const [isDragging, setIsDragging] = useState(false);
10
+ const [uploadStatus, setUploadStatus] = useState(null);
11
+ const [isLoading, setIsLoading] = useState(false);
12
+ const [sessionId, setSessionId] = useState(null);
13
+ const [selectedDocument, setSelectedDocument] = useState(null);
14
+ const [isDefaultDocument, setIsDefaultDocument] = useState(false);
15
+ const [urlInput, setUrlInput] = useState("");
16
+ const [urlSelected, setUrlSelected] = useState(false);
17
+ const fileInputRef = useRef(null);
18
+
19
+ const handleDragOver = (e) => {
20
+ e.preventDefault();
21
+ setIsDragging(true);
22
+ };
23
+
24
+ const handleDragLeave = () => {
25
+ setIsDragging(false);
26
+ };
27
+
28
+ const handleClick = () => {
29
+ // Lors d'un clic sur la zone de fichier, seulement ouvrir le dialogue de sélection
30
+ // mais ne pas réinitialiser les autres options avant que l'utilisateur ne sélectionne réellement un fichier
31
+ fileInputRef.current.click();
32
+ };
33
+
34
+ const handleFileChange = (e) => {
35
+ const file = e.target.files[0];
36
+ if (!file) return;
37
+
38
+ // Maintenant que l'utilisateur a choisi un fichier, réinitialiser les autres options
39
+ setUrlInput("");
40
+ setUrlSelected(false);
41
+ setIsDefaultDocument(false);
42
+ setSessionId(null); // Reset session ID until upload completes
43
+
44
+ // Check if it's a PDF, TXT, HTML or MD
45
+ if (
46
+ !file.name.endsWith(".pdf") &&
47
+ !file.name.endsWith(".txt") &&
48
+ !file.name.endsWith(".html") &&
49
+ !file.name.endsWith(".md")
50
+ ) {
51
+ setUploadStatus({
52
+ success: false,
53
+ message: "Only PDF, TXT, HTML and MD files are accepted",
54
+ });
55
+ return { success: false, error: "Invalid file format" };
56
+ }
57
+
58
+ // Check file size limit (3MB = 3145728 bytes)
59
+ if (file.size > 1048576 * 2) {
60
+ setUploadStatus({
61
+ success: false,
62
+ message: "File size exceeds the 2MB limit",
63
+ });
64
+ return { success: false, error: "File too large" };
65
+ }
66
+
67
+ handleFileUpload(file);
68
+ return { success: true };
69
+ };
70
+
71
+ const handleFileUpload = async (file) => {
72
+ setIsLoading(true);
73
+ setUploadStatus(null);
74
+
75
+ // Réinitialiser les sélections précédentes
76
+ setSelectedDocument(null);
77
+
78
+ try {
79
+ const formData = new FormData();
80
+ formData.append("file", file);
81
+
82
+ const response = await fetch(`${API_CONFIG.BASE_URL}/upload`, {
83
+ method: "POST",
84
+ body: formData,
85
+ });
86
+
87
+ const result = await response.json();
88
+
89
+ if (response.ok) {
90
+ setUploadStatus({
91
+ success: true,
92
+ message: "File uploaded successfully",
93
+ });
94
+ setSessionId(result.session_id);
95
+ setSelectedDocument({ name: file.name });
96
+ // Fichier uploadé avec succès, donc on désactive les autres options
97
+ setIsDefaultDocument(false);
98
+ setUrlSelected(false);
99
+ return { success: true };
100
+ } else {
101
+ setUploadStatus({
102
+ success: false,
103
+ message: result.detail || "Upload failed",
104
+ });
105
+ return { success: false, error: result.detail || "Upload failed" };
106
+ }
107
+ } catch (error) {
108
+ setUploadStatus({
109
+ success: false,
110
+ message: "Server connection error",
111
+ });
112
+ return { success: false, error: "Server connection error" };
113
+ } finally {
114
+ setIsLoading(false);
115
+ }
116
+ };
117
+
118
+ const handleDrop = async (e) => {
119
+ e.preventDefault();
120
+ setIsDragging(false);
121
+
122
+ // Réinitialiser les autres options
123
+ setUrlInput("");
124
+ setUrlSelected(false);
125
+ setIsDefaultDocument(false);
126
+ fileInputRef.current.value = "";
127
+ setSessionId(null); // Reset session ID until upload completes
128
+
129
+ const file = e.dataTransfer.files[0];
130
+ if (!file) {
131
+ setUploadStatus({
132
+ success: false,
133
+ message: "No file detected",
134
+ });
135
+ return { success: false, error: "No file detected" };
136
+ }
137
+
138
+ // Check if it's a PDF, TXT, HTML or MD
139
+ if (
140
+ !file.name.endsWith(".pdf") &&
141
+ !file.name.endsWith(".txt") &&
142
+ !file.name.endsWith(".html") &&
143
+ !file.name.endsWith(".md")
144
+ ) {
145
+ setUploadStatus({
146
+ success: false,
147
+ message: "Only PDF, TXT, HTML and MD files are accepted",
148
+ });
149
+ return { success: false, error: "Invalid file format" };
150
+ }
151
+
152
+ // Check file size limit (3MB = 3145728 bytes)
153
+ if (file.size > 1048576 * 3) {
154
+ setUploadStatus({
155
+ success: false,
156
+ message: "File size exceeds the 3MB limit",
157
+ });
158
+ return { success: false, error: "File too large" };
159
+ }
160
+
161
+ handleFileUpload(file);
162
+ return { success: true };
163
+ };
164
+
165
+ const handleDefaultDocClick = (doc) => {
166
+ // Réinitialiser les autres options
167
+ setUrlInput("");
168
+ setUrlSelected(false);
169
+ fileInputRef.current.value = "";
170
+
171
+ // Set the selected document
172
+ setSelectedDocument(doc);
173
+ if (doc) {
174
+ setSessionId(doc.id);
175
+ setIsDefaultDocument(true);
176
+ } else {
177
+ // Si on désélectionne
178
+ setIsDefaultDocument(false);
179
+ setSessionId(null);
180
+ }
181
+ };
182
+
183
+ const handleGenerateClick = () => {
184
+ if (onStartGeneration && sessionId) {
185
+ onStartGeneration(sessionId, isDefaultDocument);
186
+ } else if (!sessionId) {
187
+ setUploadStatus({
188
+ success: false,
189
+ message: "Please select or upload a document first",
190
+ });
191
+ }
192
+ };
193
+
194
+ const handleUrlInputChange = async (e) => {
195
+ const url = e.target.value;
196
+ setUrlInput(url);
197
+
198
+ // Ne pas réinitialiser les autres options tant que l'URL n'est pas valide
199
+ // Vérification simple d'URL valide avec regex
200
+ const urlRegex =
201
+ /^(https?:\/\/)?([\da-z.-]+)\.([a-z.]{2,6})([/\w .-]*)*\/?$/;
202
+
203
+ if (url && urlRegex.test(url)) {
204
+ // URL valide, maintenant réinitialiser les autres options
205
+ setIsDefaultDocument(false);
206
+ fileInputRef.current.value = "";
207
+
208
+ setUrlSelected(true);
209
+
210
+ // Si l'URL semble valide et a une longueur suffisante, on la traite
211
+ if (url.length > 10) {
212
+ await handleUrlUpload(url);
213
+ }
214
+ } else if (url && url.length > 5) {
215
+ // Si l'URL n'est pas valide mais que l'utilisateur a saisi quelque chose
216
+ setUrlSelected(false);
217
+ setUploadStatus({
218
+ success: false,
219
+ message: "Please enter a valid URL",
220
+ });
221
+ } else {
222
+ // Si le champ est vide ou presque vide, réinitialiser complètement
223
+ setUrlSelected(false);
224
+
225
+ // Si on avait déjà une URL sélectionnée avant et qu'on vide le champ
226
+ // il faut aussi réinitialiser le document sélectionné et la session
227
+ if (urlSelected) {
228
+ setSelectedDocument(null);
229
+ setSessionId(null);
230
+ }
231
+ }
232
+ };
233
+
234
+ const handleUrlUpload = async (url) => {
235
+ setIsLoading(true);
236
+ setUploadStatus(null);
237
+
238
+ // Réinitialiser les sélections précédentes
239
+ setSelectedDocument(null);
240
+ setIsDefaultDocument(false);
241
+
242
+ try {
243
+ // Création d'un FormData pour envoyer l'URL
244
+ const formData = new FormData();
245
+ formData.append("url", url);
246
+
247
+ const response = await fetch(`${API_CONFIG.BASE_URL}/upload-url`, {
248
+ method: "POST",
249
+ body: formData,
250
+ });
251
+
252
+ const result = await response.json();
253
+
254
+ if (response.ok) {
255
+ setUploadStatus({
256
+ success: true,
257
+ message: "Content from URL uploaded successfully",
258
+ });
259
+ setSessionId(result.session_id);
260
+
261
+ // Extraire le domaine racine de l'URL
262
+ let domain = url;
263
+ try {
264
+ // Utiliser URL API pour extraire le hostname
265
+ const urlObj = new URL(
266
+ url.startsWith("http") ? url : `https://${url}`
267
+ );
268
+ domain = urlObj.hostname;
269
+ } catch (e) {
270
+ console.log("Error parsing URL, using original:", e);
271
+ }
272
+
273
+ setSelectedDocument({ name: url, domain: domain });
274
+ setUrlSelected(true);
275
+ return { success: true };
276
+ } else {
277
+ setUploadStatus({
278
+ success: false,
279
+ message: result.detail || "URL processing failed",
280
+ });
281
+ return {
282
+ success: false,
283
+ error: result.detail || "URL processing failed",
284
+ };
285
+ }
286
+ } catch (error) {
287
+ setUploadStatus({
288
+ success: false,
289
+ message: "Server connection error",
290
+ });
291
+ return { success: false, error: "Server connection error" };
292
+ } finally {
293
+ setIsLoading(false);
294
+ }
295
+ };
296
+
297
+ return {
298
+ // États
299
+ isDragging,
300
+ isLoading,
301
+ sessionId,
302
+ selectedDocument,
303
+ isDefaultDocument,
304
+ urlInput,
305
+ urlSelected,
306
+ uploadStatus,
307
+ fileInputRef,
308
+
309
+ // Gestionnaires d'événements
310
+ handleDragOver,
311
+ handleDragLeave,
312
+ handleClick,
313
+ handleFileChange,
314
+ handleDrop,
315
+ handleDefaultDocClick,
316
+ handleGenerateClick,
317
+ handleUrlInputChange,
318
+
319
+ // Setters pour les mises à jour externes
320
+ setUploadStatus,
321
+ };
322
+ };