qyle commited on
Commit
006a3d6
·
verified ·
1 Parent(s): 5486587

comments, profile info, user guid, gemini conservative/creative

Browse files
README.md CHANGED
@@ -74,3 +74,54 @@ Since `docker` is the selected SDK, the space builds the app using the `Dockerfi
74
  To update the code in the space, click on `+ Contribute` button in the upper-right corner of the Files page, then click `Upload Files`. Follow the instructions to commit/upload your local files to the space.
75
 
76
  You could add the Git repo as a remote to your local Git repository, but it would add unnecessary complexity. HuggingFace is stricter than Gitlab concerning best Git practices. You would have to configure `git-xet` and delete the `.env` file and the binary file in `rag_data` from the Git history to be able to push your changes. The `.env` file has not been added to the space. The environment variables are stored in the settings page.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
74
  To update the code in the space, click on `+ Contribute` button in the upper-right corner of the Files page, then click `Upload Files`. Follow the instructions to commit/upload your local files to the space.
75
 
76
  You could add the Git repo as a remote to your local Git repository, but it would add unnecessary complexity. HuggingFace is stricter than Gitlab concerning best Git practices. You would have to configure `git-xet` and delete the `.env` file and the binary file in `rag_data` from the Git history to be able to push your changes. The `.env` file has not been added to the space. The environment variables are stored in the settings page.
77
+
78
+
79
+ ## Load testing
80
+ [k6](https://k6.io/open-source/) is an open-source tool for performing load testing. Test cases are defined in JavaScript files and can be run using the command `k6 run <filename>.js`.
81
+
82
+ ### k6 installation
83
+ On Debian/Ubuntu:
84
+ ```
85
+ sudo apt-get update
86
+ sudo apt-get install k6
87
+ ```
88
+ On Windows:
89
+ ```
90
+ winget install k6 --source winget
91
+ ```
92
+ On Docker:
93
+ ```
94
+ docker pull grafana/k6
95
+ ```
96
+ For more options, see [Install k6](https://grafana.com/docs/k6/latest/set-up/install-k6/).
97
+
98
+ ### Test scenarios
99
+ The test cases are defined in the folder `/tests/stress_tests/`:
100
+ - `chat_session.js` simulates 80 users sending three messages to one specific model.
101
+ - `website_spike.js` simulates 80 users connecting to the application home web page.
102
+
103
+
104
+ #### Chat session test scenario
105
+ The chat session scenario must be run by specifying the model type and the URL of the server. For example, the following command simulates 80 users making three requests at `https://<username>-champ-bot.hf.space/chat` to the model `champ`:
106
+ ```
107
+ k6 run chat_session.js -e MODEL_TYPE=champ -e URL=https://<username>-champ-bot.hf.space/chat
108
+ ```
109
+ The possible values for `MODEL_TYPE` are `champ`, `google`, and `openai`.
110
+
111
+ To find your HuggingFace Space backend URL, follow these steps:
112
+ 1. Go to your space
113
+ 2. Click on the **three dots** in the top right corner
114
+ 3. Select **Embed this Space**
115
+ 4. Look for the **Direct URL** in the code snippet.
116
+
117
+ Typically, the URL follows this format: `https://<username>-<space-name>.hf.space`.
118
+ To test locally, simply use `http://localhost:8000/chat`
119
+
120
+ The file `message_examples.txt` contains 250 pediatric medical prompts (generated by Gemini). `chat_session.js` uses this file to simulate real user messages.
121
+
122
+ #### Website spike test scenario
123
+ The website spike scenario must be run by specifying the website URL which is simply the HuggingFace Space URL:
124
+ ```
125
+ k6 run website_spike.js -e URL=https://huggingface.co/spaces/<username>/champ-bot
126
+ ```
127
+
champ/__pycache__/rag.cpython-313.pyc CHANGED
Binary files a/champ/__pycache__/rag.cpython-313.pyc and b/champ/__pycache__/rag.cpython-313.pyc differ
 
champ/rag.py CHANGED
@@ -1,4 +1,6 @@
1
  # app/champ/rag.py
 
 
2
  from pathlib import Path
3
 
4
  from langchain_community.vectorstores import FAISS as LCFAISS
@@ -12,7 +14,7 @@ def load_vector_store(
12
  hf_token: str = HF_TOKEN,
13
  rag_relpath: str = "rag_data/FAISS_ALLEN_20260129",
14
  embedding_model: str = "BAAI/bge-large-en-v1.5",
15
- device: str = "cpu",
16
  ) -> LCFAISS:
17
  rag_path = base_dir / rag_relpath
18
 
 
1
  # app/champ/rag.py
2
+ import torch
3
+
4
  from pathlib import Path
5
 
6
  from langchain_community.vectorstores import FAISS as LCFAISS
 
14
  hf_token: str = HF_TOKEN,
15
  rag_relpath: str = "rag_data/FAISS_ALLEN_20260129",
16
  embedding_model: str = "BAAI/bge-large-en-v1.5",
17
+ device: str = "cuda" if torch.cuda.is_available() else "cpu",
18
  ) -> LCFAISS:
19
  rag_path = base_dir / rag_relpath
20
 
main.py CHANGED
@@ -35,10 +35,12 @@ load_dotenv()
35
  # -------------------- Config --------------------
36
  DEV = os.getenv("ENV", None) == "dev"
37
 
 
38
  MODEL_MAP = {
39
  "champ": "champ-model/placeholder",
40
  "openai": "gpt-5-mini-2025-08-07",
41
- "google": "gemini-2.5-flash-lite",
 
42
  }
43
 
44
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
@@ -67,11 +69,24 @@ class ChatRequest(BaseModel):
67
  user_id: str
68
  session_id: str
69
  messages: List[ChatMessage]
70
- temperature: float = 0.7
71
  model_type: str
72
- # max_new_tokens: int = 256
73
  consent: bool = False
74
  system_prompt: Optional[str] = None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
75
 
76
 
77
  # -------------------- Helpers --------------------
@@ -103,9 +118,7 @@ print(f"CUDA available: {torch.cuda.is_available()}")
103
  champ = ChampService(base_dir=BASE_DIR, hf_token=HF_TOKEN)
104
 
105
 
106
- async def _call_openai(
107
- model_id: str, msgs: list[dict], temperature: float
108
- ) -> AsyncGenerator[str, None]:
109
  # We are streaming the output because the model answers tend to be very long and slow to generate
110
  stream = await openai_client.responses.create(
111
  model=model_id, input=msgs, stream=True
@@ -156,10 +169,13 @@ def call_llm(
156
  msgs = convert_messages(req.messages)
157
 
158
  if req.model_type == "openai":
159
- return _call_openai(model_id, msgs, req.temperature)
160
 
161
- if req.model_type == "google":
162
- return _call_gemini(model_id, msgs, req.temperature), {}
 
 
 
163
 
164
  # If you later add HF models via hf_client, handle here.
165
  raise ValueError(f"Unhandled model_type: {req.model_type}")
@@ -215,9 +231,12 @@ async def chat_endpoint(payload: ChatRequest, background_tasks: BackgroundTasks)
215
  data={
216
  "model_type": payload.model_type,
217
  "consent": payload.consent,
218
- "temperature": payload.temperature,
219
  "messages": payload.messages[-1].dict(),
220
  "reply": reply,
 
 
 
 
221
  "triage_meta": {},
222
  },
223
  )
@@ -235,8 +254,11 @@ async def chat_endpoint(payload: ChatRequest, background_tasks: BackgroundTasks)
235
  "error": str(e),
236
  "model_type": payload.model_type,
237
  "consent": payload.consent,
238
- "temperature": payload.temperature,
239
  "messages": payload.messages[-1].dict(),
 
 
 
 
240
  },
241
  )
242
 
@@ -247,10 +269,33 @@ async def chat_endpoint(payload: ChatRequest, background_tasks: BackgroundTasks)
247
  data={
248
  "model_type": payload.model_type,
249
  "consent": payload.consent,
250
- "temperature": payload.temperature,
251
  "messages": payload.messages[-1].dict(),
252
  "reply": reply,
 
 
 
 
253
  **(triage_meta or {}),
254
  },
255
  )
256
  return {"reply": reply}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
  # -------------------- Config --------------------
36
  DEV = os.getenv("ENV", None) == "dev"
37
 
38
+ # The "Google" models are differentiated by their temperature.
39
  MODEL_MAP = {
40
  "champ": "champ-model/placeholder",
41
  "openai": "gpt-5-mini-2025-08-07",
42
+ "google-conservative": "gemini-2.5-flash-lite",
43
+ "google-creative": "gemini-2.5-flash-lite",
44
  }
45
 
46
  OPENAI_API_KEY = os.getenv("OPENAI_API_KEY")
 
69
  user_id: str
70
  session_id: str
71
  messages: List[ChatMessage]
 
72
  model_type: str
 
73
  consent: bool = False
74
  system_prompt: Optional[str] = None
75
+ age_group: str
76
+ gender: str
77
+ roles: List[str]
78
+ participant_id: str
79
+
80
+
81
+ class CommentRequest(BaseModel):
82
+ user_id: str
83
+ session_id: str
84
+ comment: str
85
+ consent: bool = False
86
+ age_group: str
87
+ gender: str
88
+ roles: List[str]
89
+ participant_id: str
90
 
91
 
92
  # -------------------- Helpers --------------------
 
118
  champ = ChampService(base_dir=BASE_DIR, hf_token=HF_TOKEN)
119
 
120
 
121
+ async def _call_openai(model_id: str, msgs: list[dict]) -> AsyncGenerator[str, None]:
 
 
122
  # We are streaming the output because the model answers tend to be very long and slow to generate
123
  stream = await openai_client.responses.create(
124
  model=model_id, input=msgs, stream=True
 
169
  msgs = convert_messages(req.messages)
170
 
171
  if req.model_type == "openai":
172
+ return _call_openai(model_id, msgs)
173
 
174
+ if req.model_type == "google-conservative":
175
+ return _call_gemini(model_id, msgs, temperature=0.2), {}
176
+
177
+ if req.model_type == "google-creative":
178
+ return _call_gemini(model_id, msgs, temperature=1.0), {}
179
 
180
  # If you later add HF models via hf_client, handle here.
181
  raise ValueError(f"Unhandled model_type: {req.model_type}")
 
231
  data={
232
  "model_type": payload.model_type,
233
  "consent": payload.consent,
 
234
  "messages": payload.messages[-1].dict(),
235
  "reply": reply,
236
+ "age_group": payload.age_group,
237
+ "gender": payload.gender,
238
+ "roles": payload.roles,
239
+ "participant_id": payload.participant_id,
240
  "triage_meta": {},
241
  },
242
  )
 
254
  "error": str(e),
255
  "model_type": payload.model_type,
256
  "consent": payload.consent,
 
257
  "messages": payload.messages[-1].dict(),
258
+ "age_group": payload.age_group,
259
+ "gender": payload.gender,
260
+ "roles": payload.roles,
261
+ "participant_id": payload.participant_id,
262
  },
263
  )
264
 
 
269
  data={
270
  "model_type": payload.model_type,
271
  "consent": payload.consent,
 
272
  "messages": payload.messages[-1].dict(),
273
  "reply": reply,
274
+ "age_group": payload.age_group,
275
+ "gender": payload.gender,
276
+ "roles": payload.roles,
277
+ "participant_id": payload.participant_id,
278
  **(triage_meta or {}),
279
  },
280
  )
281
  return {"reply": reply}
282
+
283
+
284
+ @app.post("/comment")
285
+ def comment_endpoint(payload: CommentRequest, background_tasks: BackgroundTasks):
286
+ if not payload.comment:
287
+ return JSONResponse({"error": "No comment provided"}, status_code=400)
288
+
289
+ background_tasks.add_task(
290
+ log_event,
291
+ user_id=payload.user_id,
292
+ session_id=payload.session_id,
293
+ data={
294
+ "consent": payload.consent,
295
+ "comment": payload.comment,
296
+ "age_group": payload.age_group,
297
+ "gender": payload.gender,
298
+ "roles": payload.roles,
299
+ "participant_id": payload.participant_id,
300
+ },
301
+ )
static/app.js CHANGED
@@ -2,27 +2,46 @@ const chatWindow = document.getElementById('chatWindow');
2
  const userInput = document.getElementById('userInput');
3
  const sendBtn = document.getElementById('sendBtn');
4
  const statusEl = document.getElementById('status');
 
5
 
6
  const systemPresetSelect = document.getElementById('systemPreset');
7
- const tempSlider = document.getElementById('tempSlider');
8
- const tempValue = document.getElementById('tempValue');
9
- // const maxTokensSlider = document.getElementById("maxTokensSlider");
10
- // const maxTokensValue = document.getElementById("maxTokensValue");
11
  const clearBtn = document.getElementById('clearBtn');
12
 
13
- const consentOverlay = document.getElementById('consentOverlay');
 
14
  const consentCheckbox = document.getElementById('consentCheckbox');
15
  const consentBtn = document.getElementById('consentBtn');
16
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  // Local in-browser chat history
18
  // We store for each model its chat history.
19
- // We store the temperature of the google model as it can change.
20
  const modelChats = {};
21
  modelChats["champ"] = {"messages": []}
22
  modelChats["openai"] = {"messages": []}
23
- modelChats["google"] = {"messages": [], "temperature": 0.2}
 
24
 
25
  let consentGranted = false;
 
 
 
 
 
 
26
  let sessionId = 'session-' + crypto.randomUUID(); // Unique session ID, generated once per page load
27
  document.body.classList.add('no-scroll');
28
 
@@ -42,11 +61,6 @@ function renderMessages() {
42
  chatWindow.scrollTop = chatWindow.scrollHeight;
43
  }
44
 
45
- function updateSlidersUI() {
46
- tempValue.textContent = tempSlider.value;
47
- // maxTokensValue.textContent = maxTokensSlider.value;
48
- }
49
-
50
  function getMachineId() {
51
  let machineId = localStorage.getItem('MachineId');
52
 
@@ -73,18 +87,16 @@ async function sendMessage() {
73
  statusEl.textContent = 'Thinking...';
74
  statusEl.className = 'status status-info';
75
 
76
- const temperature = parseFloat(tempSlider.value);
77
- // const maxTokens = parseInt(maxTokensSlider.value, 10);
78
- // const systemPrompt = systemPresetSelect.value;
79
-
80
  const payload = {
81
  user_id: getMachineId(),
82
  session_id: sessionId,
83
  messages: modelChats[modelType]["messages"].map((m) => ({ role: m.role, content: m.content })),
84
- temperature,
85
- // max_new_tokens: maxTokens,
86
  model_type: modelType,
87
  consent: consentGranted,
 
 
 
 
88
  };
89
 
90
  try {
@@ -131,7 +143,6 @@ async function sendMessage() {
131
  statusEl.textContent = 'Ready';
132
  statusEl.className = 'status status-ok';
133
  } catch (err) {
134
- console.error(err);
135
  statusEl.textContent = 'Network error.';
136
  statusEl.className = 'status status-error';
137
  }
@@ -146,12 +157,6 @@ function clearConversation() {
146
 
147
  resetSession();
148
  modelChats[modelType]["messages"] = [];
149
- // If the model is google, we also have to clear the temperature
150
- if (modelType === "google") {
151
- modelChats["google"]["temperature"] = 0.2;
152
- tempSlider.value = 0.2
153
- updateSlidersUI();
154
- }
155
  renderMessages();
156
  statusEl.textContent = 'Conversation cleared. Start a new chat!';
157
  statusEl.className = 'status status-ok';
@@ -168,9 +173,41 @@ consentCheckbox.addEventListener('change', () => {
168
 
169
  // Handle the consent acceptance
170
  consentBtn.addEventListener('click', () => {
171
- consentOverlay.style.display = 'none'; // Hide overlay
172
- document.body.classList.remove('no-scroll'); // NEW: re-enable scrolling
173
  consentGranted = true; // Mark consent as granted
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
174
  });
175
 
176
  sendBtn.addEventListener('click', sendMessage);
@@ -182,47 +219,84 @@ userInput.addEventListener('keydown', (e) => {
182
  sendMessage();
183
  }
184
  });
185
-
186
- tempSlider.addEventListener('input', () => {
187
- if (!tempSlider.disabled) {
188
- updateSlidersUI();
189
- const modelType = systemPresetSelect.value;
190
- modelChats[modelType]["temperature"] = tempSlider.value;
191
  }
192
  });
193
- // maxTokensSlider.addEventListener("input", updateSlidersUI);
 
194
  clearBtn.addEventListener('click', clearConversation);
195
 
196
  systemPresetSelect.addEventListener('change', () => {
197
- updateTempControlForModel(); // 👈 add this
198
  renderMessages();
199
  statusEl.textContent = 'Model changed.';
200
  statusEl.className = 'status status-ok';
201
  });
202
 
203
- // initial UI state
204
- updateTempControlForModel();
205
- function updateTempControlForModel() {
206
- const model = systemPresetSelect.value;
207
-
208
- if (model === 'champ') {
209
- // Fix CHAMP temperature and disable slider
210
- tempSlider.disabled = true;
211
- tempSlider.value = '0.2';
212
- tempValue.textContent = '0.2 (fixed)';
213
- tempSlider.classList.add('disabled');
214
- } else if (model === 'openai' ){
215
- // GPT-5 models: temperature not supported
216
- tempSlider.disabled = true;
217
- tempValue.textContent = 'N/A (not supported for GPT-5 models)';;
218
- tempSlider.classList.add('disabled');
219
- } else {
220
- // Enable slider for other models
221
- tempSlider.disabled = false;
222
- tempSlider.classList.remove('disabled');
223
- tempSlider.value = modelChats[model]["temperature"];
224
- updateSlidersUI(); // refresh displayed value
225
- }
226
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
227
  statusEl.textContent = 'Ready';
228
- statusEl.className = 'status status-ok';
 
 
 
 
 
2
  const userInput = document.getElementById('userInput');
3
  const sendBtn = document.getElementById('sendBtn');
4
  const statusEl = document.getElementById('status');
5
+ const statusComment = document.getElementById('commentStatus');
6
 
7
  const systemPresetSelect = document.getElementById('systemPreset');
 
 
 
 
8
  const clearBtn = document.getElementById('clearBtn');
9
 
10
+ const welcomePopup = document.getElementById('welcomePopup');
11
+
12
  const consentCheckbox = document.getElementById('consentCheckbox');
13
  const consentBtn = document.getElementById('consentBtn');
14
 
15
+ const profileBtn = document.getElementById('profileBtn');
16
+ const genderInputs = document.querySelectorAll('input[name="gender"]');
17
+ const roleInputs = document.querySelectorAll('input[name="role"]');
18
+ const participantInput = document.getElementById('participant-id');
19
+
20
+ const popupSlider = document.getElementById('mainSlider');
21
+
22
+ const leaveCommentText = document.getElementById('leave-comment');
23
+ const commentOverlay = document.getElementById('comment-overlay');
24
+
25
+ const closeCommentBtn = document.getElementById('closeCommentBtn');
26
+ const cancelCommentBtn = document.getElementById('cancelCommentBtn');
27
+ const sendCommentBtn = document.getElementById('sendCommentBtn');
28
+ const commentInput = document.getElementById('commentInput');
29
+
30
  // Local in-browser chat history
31
  // We store for each model its chat history.
 
32
  const modelChats = {};
33
  modelChats["champ"] = {"messages": []}
34
  modelChats["openai"] = {"messages": []}
35
+ modelChats["google-conservative"] = {"messages": []}
36
+ modelChats["google-creative"] = {"messages": []}
37
 
38
  let consentGranted = false;
39
+
40
+ let ageGroup = '';
41
+ let gender = '';
42
+ let roles = [];
43
+ let participantId = '';
44
+
45
  let sessionId = 'session-' + crypto.randomUUID(); // Unique session ID, generated once per page load
46
  document.body.classList.add('no-scroll');
47
 
 
61
  chatWindow.scrollTop = chatWindow.scrollHeight;
62
  }
63
 
 
 
 
 
 
64
  function getMachineId() {
65
  let machineId = localStorage.getItem('MachineId');
66
 
 
87
  statusEl.textContent = 'Thinking...';
88
  statusEl.className = 'status status-info';
89
 
 
 
 
 
90
  const payload = {
91
  user_id: getMachineId(),
92
  session_id: sessionId,
93
  messages: modelChats[modelType]["messages"].map((m) => ({ role: m.role, content: m.content })),
 
 
94
  model_type: modelType,
95
  consent: consentGranted,
96
+ age_group: ageGroup,
97
+ gender,
98
+ roles,
99
+ participant_id: participantId
100
  };
101
 
102
  try {
 
143
  statusEl.textContent = 'Ready';
144
  statusEl.className = 'status status-ok';
145
  } catch (err) {
 
146
  statusEl.textContent = 'Network error.';
147
  statusEl.className = 'status status-error';
148
  }
 
157
 
158
  resetSession();
159
  modelChats[modelType]["messages"] = [];
 
 
 
 
 
 
160
  renderMessages();
161
  statusEl.textContent = 'Conversation cleared. Start a new chat!';
162
  statusEl.className = 'status status-ok';
 
173
 
174
  // Handle the consent acceptance
175
  consentBtn.addEventListener('click', () => {
 
 
176
  consentGranted = true; // Mark consent as granted
177
+ popupSlider.style.transform = `translateX(-468px)`;
178
+ });
179
+
180
+ // When the profile is changed, enable or disable the button
181
+ function checkProfileValidity () {
182
+ // 1. Check if any gender is selected
183
+ const genderSelected = Array.from(genderInputs).some(input => input.checked);
184
+
185
+ // 2. Check if at least one role checkbox is selected
186
+ const roleSelected = Array.from(roleInputs).some(input => input.checked);
187
+
188
+ // 3. Check if the participant id field has a value
189
+ const participantIdEntered = participantInput.value.trim().length > 0;
190
+
191
+ // 4. Enable button only if both are true
192
+ if (genderSelected && roleSelected && participantIdEntered) {
193
+ profileBtn.disabled = false;
194
+ } else {
195
+ profileBtn.disabled = true;
196
+ }
197
+ }
198
+ // Add the listener to all gender radio buttons and role checkboxes
199
+ genderInputs.forEach(input => input.addEventListener('change', checkProfileValidity));
200
+ roleInputs.forEach(input => input.addEventListener('change', checkProfileValidity));
201
+ participantInput.addEventListener('input', checkProfileValidity);
202
+
203
+ profileBtn.addEventListener('click', () => {
204
+ welcomePopup.style.display = 'none'; // Hide overlay
205
+ document.body.classList.remove('no-scroll'); // NEW: re-enable scrolling
206
+
207
+ ageGroup = document.getElementById('age-group').value;
208
+ gender = document.querySelector('input[name="gender"]:checked')?.value;
209
+ roles = Array.from(document.querySelectorAll('input[name="role"]:checked')).map(input => input.value);
210
+ participantId = participantInput.value.trim();
211
  });
212
 
213
  sendBtn.addEventListener('click', sendMessage);
 
219
  sendMessage();
220
  }
221
  });
222
+ commentInput.addEventListener('keydown', (e) => {
223
+ if (e.key === 'Enter' && !e.shiftKey) {
224
+ e.preventDefault();
225
+ sendComment();
 
 
226
  }
227
  });
228
+
229
+
230
  clearBtn.addEventListener('click', clearConversation);
231
 
232
  systemPresetSelect.addEventListener('change', () => {
 
233
  renderMessages();
234
  statusEl.textContent = 'Model changed.';
235
  statusEl.className = 'status status-ok';
236
  });
237
 
238
+ // Comments
239
+ function openCommentOverlay(e) {
240
+ e.preventDefault();
241
+ // Let the stylesheet take over
242
+ commentOverlay.style.display = '';
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
243
  }
244
+ leaveCommentText.addEventListener('click', openCommentOverlay);
245
+
246
+ // Cancelling or closing the comment overlay simply hides the comment popup
247
+ closeCommentBtn.addEventListener('click', () => {
248
+ commentOverlay.style.display = 'none';
249
+ });
250
+ cancelCommentBtn.addEventListener('click', () => {
251
+ commentOverlay.style.display = 'none';
252
+ });
253
+
254
+ async function sendComment() {
255
+ const comment = commentInput.value;
256
+ if (!comment) return;
257
+
258
+ const payload = {
259
+ user_id: getMachineId(),
260
+ session_id: sessionId,
261
+ comment,
262
+ consent: consentGranted,
263
+ age_group: ageGroup,
264
+ gender,
265
+ roles,
266
+ participant_id: participantId
267
+ };
268
+
269
+ statusComment.textContent = 'Sending...';
270
+ statusComment.className = 'status-info';
271
+
272
+ try {
273
+ const res = await fetch('/comment', {
274
+ method: 'POST',
275
+ headers: { 'Content-Type': 'application/json' },
276
+ body: JSON.stringify(payload),
277
+ });
278
+
279
+ if (!res.ok) {
280
+ statusComment.textContent = 'Error from the server';
281
+ statusComment.className = 'status-error';
282
+ return;
283
+ }
284
+
285
+ commentInput.value = '';
286
+
287
+ statusComment.textContent = 'Comment sent!'
288
+ statusComment.className = 'status-ok'
289
+ } catch (err) {
290
+ statusComment.textContent = 'Network error';
291
+ statusComment.className = 'status-error'
292
+ }
293
+
294
+ };
295
+ sendCommentBtn.addEventListener('click', sendComment);
296
+
297
  statusEl.textContent = 'Ready';
298
+ statusEl.className = 'status-ok';
299
+
300
+ statusComment.textContent = 'Ready';
301
+ statusComment.className = 'status-ok';
302
+
static/style.css CHANGED
@@ -12,6 +12,10 @@ body.no-scroll {
12
  overflow: hidden;
13
  }
14
 
 
 
 
 
15
  .chat-container {
16
  max-width: 900px;
17
  margin: 40px auto;
@@ -151,10 +155,13 @@ body.no-scroll {
151
  background: #3453e6;
152
  }
153
 
154
- /* Status text */
155
- .status {
156
  margin-top: 6px;
157
  font-size: 0.85rem;
 
 
 
158
  }
159
 
160
  .status-info {
@@ -192,15 +199,15 @@ body.no-scroll {
192
  }
193
 
194
  /* CONSENT OVERLAY FIXED VERSION */
195
- .consent-overlay {
196
  position: fixed;
197
  top: 0;
198
  left: 0;
199
  width: 100%;
200
  height: 100%;
201
 
202
- background: rgba(0, 0, 0, 0.55); /* CHANGED: darker for visibility */
203
- backdrop-filter: blur(4px);
204
 
205
  display: flex;
206
  align-items: center;
@@ -209,8 +216,24 @@ body.no-scroll {
209
  z-index: 9999; /* NEW: ensure nothing covers this */
210
  }
211
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
212
  /* Dark theme overlay box */
213
- .consent-box {
 
214
  background: #141b2f; /* CHANGED: match theme */
215
  color: #f5f5f5; /* NEW: readable on dark bg */
216
  padding: 24px;
@@ -225,8 +248,129 @@ body.no-scroll {
225
  margin: 16px 0;
226
  gap: 10px;
227
  }
 
228
  /* Disable look for CHAMP fixed temperature */
229
  input[type='range'].disabled {
230
  opacity: 0.6;
231
  cursor: not-allowed;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
232
  }
 
12
  overflow: hidden;
13
  }
14
 
15
+ a {
16
+ color: #4da6ff;
17
+ }
18
+
19
  .chat-container {
20
  max-width: 900px;
21
  margin: 40px auto;
 
155
  background: #3453e6;
156
  }
157
 
158
+ /* Status and comment text */
159
+ .status-comment {
160
  margin-top: 6px;
161
  font-size: 0.85rem;
162
+
163
+ display: flex;
164
+ justify-content: space-between;
165
  }
166
 
167
  .status-info {
 
199
  }
200
 
201
  /* CONSENT OVERLAY FIXED VERSION */
202
+ .popup-overlay {
203
  position: fixed;
204
  top: 0;
205
  left: 0;
206
  width: 100%;
207
  height: 100%;
208
 
209
+ background: rgba(0, 0, 0, 0.8); /* CHANGED: darker for visibility */
210
+ /* backdrop-filter: blur(4px); */ /* removed blur for performance */
211
 
212
  display: flex;
213
  align-items: center;
 
216
  z-index: 9999; /* NEW: ensure nothing covers this */
217
  }
218
 
219
+ .slider {
220
+ display: flex;
221
+ width: 840px;
222
+ transition: transform 0.5s cubic-bezier(0.25, 1, 0.5, 1);
223
+
224
+ /* Performance Boosters */
225
+ will-change: transform;
226
+ perspective: 1000px;
227
+ }
228
+
229
+ .popup-window {
230
+ width: 468px;
231
+ overflow: hidden;
232
+ }
233
+
234
  /* Dark theme overlay box */
235
+ .popup-step {
236
+ flex-shrink: 0;
237
  background: #141b2f; /* CHANGED: match theme */
238
  color: #f5f5f5; /* NEW: readable on dark bg */
239
  padding: 24px;
 
248
  margin: 16px 0;
249
  gap: 10px;
250
  }
251
+
252
  /* Disable look for CHAMP fixed temperature */
253
  input[type='range'].disabled {
254
  opacity: 0.6;
255
  cursor: not-allowed;
256
+ }
257
+
258
+ /* Comment area */
259
+ .comment-area {
260
+ position: relative;
261
+ display: flex;
262
+ flex-direction: column;
263
+ gap: 16px;
264
+ background: #1a2238;
265
+ padding: 24px;
266
+ border-radius: 15px;
267
+ width: 90%;
268
+ max-width: 450px;
269
+ border: 1px solid #2c3554;
270
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.4);
271
+ }
272
+
273
+ .comment-area h2 {
274
+ margin: 0 0 8px 0;
275
+ font-size: 1.5rem;
276
+ color: #f5f5f5;
277
+ font-weight: 600;
278
+ }
279
+
280
+ .comment-area textarea {
281
+ width: 425px;
282
+ min-height: 120px;
283
+ border-radius: 10px;
284
+ border: 1px solid #2c3554;
285
+ background: #0d1324;
286
+ color: #f5f5f5;
287
+ padding: 12px;
288
+ resize: vertical;
289
+ font-size: 1rem;
290
+ font-family: inherit;
291
+ transition: border-color 0.2s ease;
292
+ }
293
+
294
+ .comment-area textarea:focus {
295
+ outline: none;
296
+ border-color: #4a5f8f;
297
+ }
298
+
299
+ .comment-area textarea::placeholder {
300
+ color: #6b7280;
301
+ }
302
+
303
+ /* Button container */
304
+ .comment-area .button-group {
305
+ display: flex;
306
+ gap: 12px;
307
+ justify-content: flex-end;
308
+ }
309
+
310
+ /* Cancel button */
311
+ .comment-area .cancelBtn {
312
+ background: transparent;
313
+ color: #9ca3af;
314
+ border: 1px solid #2c3554;
315
+ padding: 10px 24px;
316
+ border-radius: 8px;
317
+ font-size: 0.95rem;
318
+ font-weight: 500;
319
+ cursor: pointer;
320
+ transition: all 0.2s ease;
321
+ }
322
+
323
+ .comment-area .cancelBtn:hover {
324
+ background: #0d1324;
325
+ color: #f5f5f5;
326
+ border-color: #4a5f8f;
327
+ }
328
+
329
+ /* Send button */
330
+ .comment-area .sendBtn {
331
+ background: #3b82f6;
332
+ color: #ffffff;
333
+ padding: 10px 24px;
334
+ border-radius: 8px;
335
+ border: none;
336
+ font-size: 0.95rem;
337
+ font-weight: 500;
338
+ cursor: pointer;
339
+ transition: all 0.2s ease;
340
+ }
341
+
342
+ .comment-area .sendBtn:hover {
343
+ background: #2563eb;
344
+ }
345
+
346
+ .comment-area .sendBtn:active {
347
+ transform: scale(0.98);
348
+ }
349
+
350
+ /* Close button (X) */
351
+ .comment-area .closeBtn {
352
+ position: absolute;
353
+ top: 15px;
354
+ right: 15px;
355
+ width: 28px; /* Explicit small width */
356
+ height: 28px; /* Explicit small height */
357
+ padding: 0;
358
+ background: transparent;
359
+ color: #6b7280;
360
+ border: none;
361
+ border-radius: 4px;
362
+ font-size: 20px;
363
+ line-height: 28px; /* Center the × vertically */
364
+ text-align: center; /* Center the × horizontally */
365
+ cursor: pointer;
366
+ transition: all 0.2s ease;
367
+ }
368
+
369
+ .comment-area .close-btn:hover {
370
+ background: #0d1324;
371
+ color: #f5f5f5;
372
+ }
373
+
374
+ .comment-area .close-btn:active {
375
+ transform: scale(0.95);
376
  }
templates/index.html CHANGED
@@ -19,6 +19,11 @@
19
  remember to avoid sharing any sensitive or private details during the
20
  conversation.
21
  </p>
 
 
 
 
 
22
  </header>
23
 
24
  <!-- Controls bar -->
@@ -29,45 +34,79 @@
29
  <option value="champ" selected>CHAMP</option>
30
  <!-- champ is our model -->
31
  <option value="openai">GPT-5.2</option>
32
- <option value="google">Gemini-3</option>
 
33
  </select>
34
  </div>
35
 
36
- <div class="control-group">
37
- <label for="tempSlider">
38
- Temperature:
39
- <span id="tempValue">0.7</span>
40
- </label>
41
- <input
42
- type="range"
43
- id="tempSlider"
44
- min="0.0"
45
- max="1.0"
46
- step="0.1"
47
- value="0.7"
48
- />
49
- </div>
50
-
51
  <button id="clearBtn" class="secondary-button">Clear</button>
52
  </div>
53
 
54
- <div id="consentOverlay" class="consent-overlay">
55
- <div class="consent-box">
56
- <h2>Before you continue</h2>
57
- <p>
58
- By using this demo you agree that your messages will be shared with
59
- us for processing. Do not provide sensitive or private details.
60
- </p>
61
-
62
- <label class="consent-check">
63
- <input type="checkbox" id="consentCheckbox" />
64
- I understand and agree
65
- </label>
66
-
67
- <button id="consentBtn" class="primary-button" disabled>
68
- Agree and Continue
69
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
70
  </div>
 
71
  </div>
72
 
73
  <!-- Chat window -->
@@ -85,8 +124,27 @@
85
  <button id="sendBtn">Send</button>
86
  </footer>
87
 
88
- <!-- Status line -->
89
- <div id="status" class="status"></div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
90
  </div>
91
 
92
  <script src="/static/app.js"></script>
 
19
  remember to avoid sharing any sensitive or private details during the
20
  conversation.
21
  </p>
22
+ <p class="subtitle">User guide:
23
+ <a href="https://docs.google.com/document/d/1-2UIpKbh1BdAmgCaF4QdcaZ4H5fwkQkKRigHz47EejY/edit?usp=sharing" target="_blank">
24
+ CHAMP Model Comparison – Participant Testing Guide
25
+ </a>
26
+ </p>
27
  </header>
28
 
29
  <!-- Controls bar -->
 
34
  <option value="champ" selected>CHAMP</option>
35
  <!-- champ is our model -->
36
  <option value="openai">GPT-5.2</option>
37
+ <option value="google-conservative">Gemini-3 (Conservative)</option>
38
+ <option value="google-creative">Gemini-3 (Creative)</option>
39
  </select>
40
  </div>
41
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
42
  <button id="clearBtn" class="secondary-button">Clear</button>
43
  </div>
44
 
45
+ <div id="welcomePopup" class="popup-overlay">
46
+ <div class="popup-window">
47
+ <div class="slider" id="mainSlider">
48
+ <div class="consent-box popup-step">
49
+ <h2>Before you continue</h2>
50
+ <p>
51
+ By using this demo you agree that your messages will be shared with
52
+ us for processing. Do not provide sensitive or private details.
53
+ </p>
54
+
55
+ <label class="consent-check">
56
+ <input type="checkbox" id="consentCheckbox" />
57
+ I understand and agree
58
+ </label>
59
+
60
+ <button id="consentBtn" class="primary-button" disabled>
61
+ Agree and Continue
62
+ </button>
63
+ </div>
64
+
65
+ <div class="profile popup-step">
66
+ <h2>Profile information</h2>
67
+ <p>We collect this information to help us understand how different groups of users interact with the system.
68
+ This data allows us to improve the experience and ensure the tool is effective for everyone.</p>
69
+ <label for="age-group">Age group</label>
70
+ <select id="age-group">
71
+ <option value="0-18" selected>0-18</option>
72
+ <option value="18-24">18-24</option>
73
+ <option value="25-34">25-34</option>
74
+ <option value="35-44">35-44</option>
75
+ <option value="45-54">45-54</option>
76
+ <option value="55-64">55-64</option>
77
+ <option value="65+">65+</option>
78
+ </select>
79
+
80
+ <p>Gender</p>
81
+ <input type="radio" id="male" name="gender" value="M">
82
+ <label for="male">Male</label>
83
+
84
+ <input type="radio" id="female" name="gender" value="F">
85
+ <label for="female">Female</label>
86
+
87
+ <p>Role</p>
88
+
89
+ <input type="checkbox" id="patient" name="role" value="patient">
90
+ <label for="patient">Patient</label><br>
91
+ <input type="checkbox" id="clinician" name="role" value="clinician">
92
+ <label for="clinician">Clinician</label><br>
93
+ <input type="checkbox" id="computer-scientist" name="role" value="computer-scientist">
94
+ <label for="computer-scientist">Computer scientist</label><br>
95
+ <input type="checkbox" id="researcher" name="role" value="researcher">
96
+ <label for="researcher">Researcher</label><br>
97
+ <input type="checkbox" id="other" name="role" value="other">
98
+ <label for="other">Other</label><br>
99
+ <br>
100
+ <label for="participant-id">Participant Id</label>
101
+ <input type="text" id="participant-id" name="participant-id"><br>
102
+ <br>
103
+ <button id="profileBtn" class="primary-button" disabled>
104
+ Continue
105
+ </button>
106
+ </div>
107
+ </div>
108
  </div>
109
+
110
  </div>
111
 
112
  <!-- Chat window -->
 
124
  <button id="sendBtn">Send</button>
125
  </footer>
126
 
127
+ <!-- Status/Comment line -->
128
+ <div class="status-comment">
129
+ <span id="status"></span>
130
+ <span id="leave-comment"><a href="#">Leave a comment</a></span>
131
+ </div>
132
+
133
+ <div id="comment-overlay" class="popup-overlay" style="display:none">
134
+ <div class="popup-step comment-area">
135
+ <button id="closeCommentBtn" class="closeBtn" aria-label="Close">×</button>
136
+ <h2>Leave a comment</h2>
137
+ <textarea
138
+ id="commentInput"
139
+ rows="2"
140
+ placeholder="Type your comment and click Send..."
141
+ ></textarea>
142
+ <div id="commentStatus" class="comment-status"></div>
143
+ <button id="cancelCommentBtn" class="cancelBtn">Cancel</button>
144
+ <button id="sendCommentBtn" class="sendBtn">Send</button>
145
+ </div>
146
+ </div>
147
+
148
  </div>
149
 
150
  <script src="/static/app.js"></script>
tests/stress_tests/chat_session.js CHANGED
@@ -1,5 +1,4 @@
1
- // This file aims to test a case where 80 users send 3 messages to ONE model (very pessimistic).
2
- // This test aims to help us identify the most appropriate plan for each model.
3
 
4
  import http from 'k6/http';
5
  import { sleep, check } from 'k6';
@@ -16,8 +15,8 @@ export const options = {
16
  scenarios: {
17
  my_spike_test: {
18
  executor: 'per-vu-iterations',
19
- vus: 80, // 80 total users
20
- iterations: 1, // Each user runs the script exactly once
21
  },
22
  },
23
  };
@@ -27,8 +26,7 @@ export default function () {
27
  // sending their messages at the exact same time.
28
  sleep(Math.random() * 10);
29
 
30
- // const url = 'https://marvin-cusm-chatbot-champ-chatbot.hf.space/chat'
31
- const url = 'http://localhost:8000/chat'
32
 
33
  const conversation = [];
34
  // Each VU sends 3 messages
@@ -48,24 +46,30 @@ export default function () {
48
  const params = { headers: { 'Content-Type': 'application/json' } };
49
 
50
  const res = http.post(url, JSON.stringify(payload), params);
 
51
  check(res, {'status is 200': (r) => r.status === 200})
52
 
53
  let reply = '';
54
  if (res.status === 200) {
55
  // k6 does not support streaming response bodies. It waits for the entire response until
56
  // the stream is 'done'. Therefore, we do not need to read the chunks one by one.
57
- const data = res.json();
 
 
 
 
 
 
 
58
  reply = data.reply || '(No reply)';
59
  conversation.push({ role: 'assistant', content: reply });
60
  } else {
61
- console.log(res.status);
62
- console.log(res.body);
63
  conversation.push({ role: 'assistant', content: '(No reply)' });
64
  }
65
 
66
- // Users usually wait before sending another message.
67
- // It takes time to read the response and write another message.
68
-
69
  // Simulate reading speed: ~200ms per word in the reply + 2s thinking time
70
  const readingTime = (reply.split(' ').length * 0.2) + 2;
71
  // Cap it so it doesn't wait forever, but add some randomness (jitter)
@@ -75,8 +79,31 @@ export default function () {
75
  }
76
  }
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
 
79
- // CHAMP (GROK) - BASIC CPU - 80VUs (~$1.20)
 
 
 
 
 
 
 
 
 
80
  // █ TOTAL RESULTS
81
 
82
  // checks_total.......: 240 2.408279/s
@@ -101,8 +128,95 @@ export default function () {
101
  // data_received..................: 547 kB 5.5 kB/s
102
  // data_sent......................: 345 kB 3.5 kB/s
103
 
 
 
 
104
 
105
 
 
 
106
 
107
- // running (01m39.7s), 00/80 VUs, 80 complete and 0 interrupted iterations
108
- // my_spike_test ✓ [======================================] 80 VUs 01m39.7s/10m0s 80/80 iters, 1 per VU
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ // This file aims to test a scenario where 80 users send 3 messages to ONE model (pessimistic).
 
2
 
3
  import http from 'k6/http';
4
  import { sleep, check } from 'k6';
 
15
  scenarios: {
16
  my_spike_test: {
17
  executor: 'per-vu-iterations',
18
+ vus: 80, // 80 total users
19
+ iterations: 1, // Each user runs the function exactly once.
20
  },
21
  },
22
  };
 
26
  // sending their messages at the exact same time.
27
  sleep(Math.random() * 10);
28
 
29
+ const url = __ENV.URL
 
30
 
31
  const conversation = [];
32
  // Each VU sends 3 messages
 
46
  const params = { headers: { 'Content-Type': 'application/json' } };
47
 
48
  const res = http.post(url, JSON.stringify(payload), params);
49
+
50
  check(res, {'status is 200': (r) => r.status === 200})
51
 
52
  let reply = '';
53
  if (res.status === 200) {
54
  // k6 does not support streaming response bodies. It waits for the entire response until
55
  // the stream is 'done'. Therefore, we do not need to read the chunks one by one.
56
+ let data = "";
57
+ try {
58
+ data = res.json();
59
+ } catch (error) {
60
+ // However, if the response contains streamed data, it is not in JSON format.
61
+ // We would have to read the body to access that data.
62
+ data = res.body;
63
+ }
64
  reply = data.reply || '(No reply)';
65
  conversation.push({ role: 'assistant', content: reply });
66
  } else {
67
+ console.error(res.status);
68
+ console.error(res.body);
69
  conversation.push({ role: 'assistant', content: '(No reply)' });
70
  }
71
 
72
+ // Simulating reading time, thinking time and writing time.
 
 
73
  // Simulate reading speed: ~200ms per word in the reply + 2s thinking time
74
  const readingTime = (reply.split(' ').length * 0.2) + 2;
75
  // Cap it so it doesn't wait forever, but add some randomness (jitter)
 
79
  }
80
  }
81
 
82
+ // TEST RESULT ANALYSIS
83
+
84
+ // CHAMP
85
+ // The bottleneck associated with CHAMP's performance is the HuggingFace Space hardware.
86
+ // CHAMP actually requires a lot of computing power, because of the FAISS retrieval system.
87
+ // Using a T4 GPU for accelerated retrieval has halved the average request duration, from 11.65s
88
+ // to 5.56s.
89
+ // The performance of CHAMP could be improved by selecting a more powerful GPU (such as L4 or A10G)
90
+ // or by running a simpler vector search algorithm. The current algorithm (maximal marginal relevance)
91
+ // optimizes for similarity to query and diversity among selected documents. A basic similarity search
92
+ // should be faster to run.
93
+
94
+ // Google (Gemini)
95
+ // The average request duration is 1.13s with a maximum of 3.44s. This level of performance is acceptable.
96
 
97
+ // OpenAI (GPT-5-mini)
98
+ // Previous tests used GPT-5-nano. However, this model had strict rate limits and tests would exceed them
99
+ // which caused them to fail. The model was switched to GPT-5-mini.
100
+ // The performance is quite poor, with an average request duration of 22.73s and a maximum duration of 59.62s.
101
+ // The bottleneck lies within the OpenAI API, which leaves us with little room for optimization.
102
+
103
+
104
+ // RAW TEST RESULTS
105
+
106
+ // CHAMP (GROK) - BASIC CPU - 80VUs
107
  // █ TOTAL RESULTS
108
 
109
  // checks_total.......: 240 2.408279/s
 
128
  // data_received..................: 547 kB 5.5 kB/s
129
  // data_sent......................: 345 kB 3.5 kB/s
130
 
131
+ // running (01m39.7s), 00/80 VUs, 80 complete and 0 interrupted iterations
132
+ // my_spike_test ✓ [======================================] 80 VUs 01m39.7s/10m0s 80/80 iters, 1 per VU
133
+
134
 
135
 
136
+ // CHAMP (GROK) - T4 small - 80VUs
137
+ // █ TOTAL RESULTS
138
 
139
+ // checks_total.......: 240 3.02291/s
140
+ // checks_succeeded...: 100.00% 240 out of 240
141
+ // checks_failed......: 0.00% 0 out of 240
142
+
143
+ // ✓ status is 200
144
+
145
+ // HTTP
146
+ // http_req_duration..............: avg=5.56s min=31.95ms med=5.74s max=9.78s p(90)=7.92s p(95)=8.92s
147
+ // { expected_response:true }...: avg=5.56s min=31.95ms med=5.74s max=9.78s p(90)=7.92s p(95)=8.92s
148
+ // http_req_failed................: 0.00% 0 out of 240
149
+ // http_reqs......................: 240 3.02291/s
150
+
151
+ // EXECUTION
152
+ // iteration_duration.............: avg=1m5s min=35.54s med=1m7s max=1m19s p(90)=1m16s p(95)=1m17s
153
+ // iterations.....................: 80 1.007637/s
154
+ // vus............................: 1 min=1 max=80
155
+ // vus_max........................: 80 min=80 max=80
156
+
157
+ // NETWORK
158
+ // data_received..................: 539 kB 6.8 kB/s
159
+ // data_sent......................: 335 kB 4.2 kB/s
160
+
161
+ // running (01m19.4s), 00/80 VUs, 80 complete and 0 interrupted iterations
162
+ // my_spike_test ✓ [======================================] 80 VUs 01m19.4s/10m0s 80/80 iters, 1 per VU
163
+
164
+
165
+
166
+ // GEMINI - T4 small - 80VUs
167
+ // █ TOTAL RESULTS
168
+
169
+ // checks_total.......: 240 3.611159/s
170
+ // checks_succeeded...: 100.00% 240 out of 240
171
+ // checks_failed......: 0.00% 0 out of 240
172
+
173
+ // ✓ status is 200
174
+
175
+ // HTTP
176
+ // http_req_duration..............: avg=1.13s min=266.67ms med=991.5ms max=3.44s p(90)=2.08s p(95)=2.44s
177
+ // { expected_response:true }...: avg=1.13s min=266.67ms med=991.5ms max=3.44s p(90)=2.08s p(95)=2.44s
178
+ // http_req_failed................: 0.00% 0 out of 240
179
+ // http_reqs......................: 240 3.611159/s
180
+
181
+ // EXECUTION
182
+ // iteration_duration.............: avg=48.54s min=15.63s med=53.61s max=1m6s p(90)=1m1s p(95)=1m3s
183
+ // iterations.....................: 80 1.20372/s
184
+ // vus............................: 1 min=1 max=80
185
+ // vus_max........................: 80 min=80 max=80
186
+
187
+ // NETWORK
188
+ // data_received..................: 621 kB 9.3 kB/s
189
+ // data_sent......................: 416 kB 6.3 kB/s
190
+
191
+ // running (01m06.5s), 00/80 VUs, 80 complete and 0 interrupted iterdations
192
+ // my_spike_test ✓ [======================================] 80 VUs 01m06.5s/10m0s 80/80 iters, 1 per VU
193
+
194
+
195
+
196
+ // GPT-5-mini - T4 small - 80VUs
197
+ // █ TOTAL RESULTS
198
+
199
+ // checks_total.......: 240 2.010998/s
200
+ // checks_succeeded...: 100.00% 240 out of 240
201
+ // checks_failed......: 0.00% 0 out of 240
202
+
203
+ // ✓ status is 200
204
+
205
+ // HTTP
206
+ // http_req_duration..............: avg=22.73s min=7.66s med=20.73s max=59.62s p(90)=32.1s p(95)=38.59s
207
+ // { expected_response:true }...: avg=22.73s min=7.66s med=20.73s max=59.62s p(90)=32.1s p(95)=38.59s
208
+ // http_req_failed................: 0.00% 0 out of 240
209
+ // http_reqs......................: 240 2.010998/s
210
+
211
+ // EXECUTION
212
+ // iteration_duration.............: avg=1m20s min=51.07s med=1m18s max=1m59s p(90)=1m35s p(95)=1m49s
213
+ // iterations.....................: 80 0.670333/s
214
+ // vus............................: 1 min=1 max=80
215
+ // vus_max........................: 80 min=80 max=80
216
+
217
+ // NETWORK
218
+ // data_received..................: 4.9 MB 41 kB/s
219
+ // data_sent......................: 247 kB 2.1 kB/s
220
+
221
+ // running (01m59.3s), 00/80 VUs, 80 complete and 0 interrupted iterations
222
+ // my_spike_test ✓ [======================================] 80 VUs 01m59.3s/10m0s 80/80 iters, 1 per VU
tests/stress_tests/website_spike.js CHANGED
@@ -1,5 +1,4 @@
1
  // This file aims to test a case where 80 users connect to the website at the same time.
2
- // This test aims to help us identify the most appropriate HuggingFace hardware.
3
  import http from 'k6/http';
4
  import { check } from 'k6';
5
 
@@ -15,12 +14,11 @@ export const options = {
15
  };
16
 
17
  export default function () {
 
18
  // User lands on the site
19
- const res = http.get('https://huggingface.co/spaces/marvin-cusm-chatbot/champ-chatbot');
20
 
21
  check(res, { 'status is 200': (r) => r.status === 200 });
22
  }
23
 
24
- // iteration_duration.............: avg=4.91s min=3.01s med=5.11s max=7.36s p(90)=5.37s p(95)=5.87s
25
- // It might take up to 7 seconds for a page to load
26
- // We might want to upgrade
 
1
  // This file aims to test a case where 80 users connect to the website at the same time.
 
2
  import http from 'k6/http';
3
  import { check } from 'k6';
4
 
 
14
  };
15
 
16
  export default function () {
17
+ const url = __ENV.URL;
18
  // User lands on the site
19
+ const res = http.get(url);
20
 
21
  check(res, { 'status is 200': (r) => r.status === 200 });
22
  }
23
 
24
+ // http_req_duration..............: avg=1.28s min=628.16ms med=1.35s max=2.21s p(90)=1.7s p(95)=1.77s