Rochane commited on
Commit
005becb
·
1 Parent(s): e2741fa

Add file upload RAG: app.js + style.css

Browse files
Files changed (2) hide show
  1. static/app.js +167 -2
  2. static/style.css +112 -0
static/app.js CHANGED
@@ -13,7 +13,8 @@
13
  phase: 0,
14
  history: [], // {role, content}
15
  timestamps: [], // epoch ms for every message (user & assistant alternating)
16
- analysisResult: null
 
17
  };
18
 
19
  var PHASE_NAMES = [
@@ -35,6 +36,7 @@
35
 
36
  var modeBadge = document.getElementById("mode-badge");
37
  var topicBadge = document.getElementById("topic-badge");
 
38
  var phaseDots = document.getElementById("phase-dots");
39
  var phaseLabels = document.getElementById("phase-labels");
40
  var messagesEl = document.getElementById("messages");
@@ -52,6 +54,12 @@
52
  var btnExport = document.getElementById("btn-export");
53
  var btnNewSession = document.getElementById("btn-new-session");
54
 
 
 
 
 
 
 
55
  /* ===== Screen navigation ===== */
56
  function showScreen(screen) {
57
  setupScreen.classList.remove("active");
@@ -99,6 +107,133 @@
99
  if (on) messagesEl.scrollTop = messagesEl.scrollHeight;
100
  }
101
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  /* ===== API calls ===== */
103
  function sendMessage(text) {
104
  state.history.push({ role: "user", content: text });
@@ -117,7 +252,7 @@
117
  mode: state.mode,
118
  topic: state.topic,
119
  phase: state.phase,
120
- history: state.history.slice(0, -1) // send history before this message
121
  })
122
  })
123
  .then(function (res) { return res.json(); })
@@ -232,10 +367,13 @@
232
  state.history = [];
233
  state.timestamps = [];
234
  state.analysisResult = null;
 
235
 
236
  topicInput.value = "";
237
  chatInput.value = "";
238
  messagesEl.querySelectorAll(".message").forEach(function (el) { el.remove(); });
 
 
239
 
240
  modeBtns.forEach(function (btn) {
241
  btn.classList.toggle("selected", btn.dataset.mode === "TUTOR");
@@ -245,9 +383,25 @@
245
  btnEnd.disabled = false;
246
  btnSend.disabled = false;
247
 
 
 
 
248
  showScreen(setupScreen);
249
  }
250
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  /* ===== Event listeners ===== */
252
 
253
  // Mode selection
@@ -273,6 +427,14 @@
273
  modeBadge.textContent = state.mode === "TUTOR" ? "Tuteur" : "Critique";
274
  topicBadge.textContent = topic;
275
 
 
 
 
 
 
 
 
 
276
  renderPhaseIndicator();
277
  showScreen(chatScreen);
278
  chatInput.focus();
@@ -309,4 +471,7 @@
309
  // New session from analysis screen
310
  btnNewSession.addEventListener("click", resetSession);
311
 
 
 
 
312
  })();
 
13
  phase: 0,
14
  history: [], // {role, content}
15
  timestamps: [], // epoch ms for every message (user & assistant alternating)
16
+ analysisResult: null,
17
+ uploadedDocs: [] // filenames uploaded this session
18
  };
19
 
20
  var PHASE_NAMES = [
 
36
 
37
  var modeBadge = document.getElementById("mode-badge");
38
  var topicBadge = document.getElementById("topic-badge");
39
+ var docsBadge = document.getElementById("docs-badge");
40
  var phaseDots = document.getElementById("phase-dots");
41
  var phaseLabels = document.getElementById("phase-labels");
42
  var messagesEl = document.getElementById("messages");
 
54
  var btnExport = document.getElementById("btn-export");
55
  var btnNewSession = document.getElementById("btn-new-session");
56
 
57
+ // Upload refs
58
+ var uploadZone = document.getElementById("upload-zone");
59
+ var fileInput = document.getElementById("file-input");
60
+ var uploadList = document.getElementById("upload-list");
61
+ var uploadStatus = document.getElementById("upload-status");
62
+
63
  /* ===== Screen navigation ===== */
64
  function showScreen(screen) {
65
  setupScreen.classList.remove("active");
 
107
  if (on) messagesEl.scrollTop = messagesEl.scrollHeight;
108
  }
109
 
110
+ /* ===== File Upload ===== */
111
+
112
+ function formatFileSize(bytes) {
113
+ if (bytes < 1024) return bytes + " o";
114
+ if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + " Ko";
115
+ return (bytes / (1024 * 1024)).toFixed(1) + " Mo";
116
+ }
117
+
118
+ function renderUploadList() {
119
+ uploadList.innerHTML = "";
120
+ state.uploadedDocs.forEach(function (doc) {
121
+ var item = document.createElement("div");
122
+ item.className = "upload-item";
123
+
124
+ var icon = doc.filename.toLowerCase().endsWith(".pdf") ? "PDF" :
125
+ doc.filename.toLowerCase().endsWith(".pptx") ? "PPT" :
126
+ doc.filename.toLowerCase().endsWith(".ppt") ? "PPT" : "TXT";
127
+
128
+ item.innerHTML =
129
+ '<span class="upload-item-icon">' + icon + '</span>' +
130
+ '<span class="upload-item-name">' + doc.filename + '</span>' +
131
+ '<span class="upload-item-chunks">' + doc.chunks + ' chunks</span>' +
132
+ '<button class="upload-item-delete" data-filename="' + doc.filename + '">X</button>';
133
+ uploadList.appendChild(item);
134
+ });
135
+
136
+ // Bind delete buttons
137
+ uploadList.querySelectorAll(".upload-item-delete").forEach(function (btn) {
138
+ btn.addEventListener("click", function () {
139
+ deleteDoc(btn.dataset.filename);
140
+ });
141
+ });
142
+ }
143
+
144
+ function uploadFiles(fileList) {
145
+ if (!fileList || fileList.length === 0) return;
146
+
147
+ var formData = new FormData();
148
+ for (var i = 0; i < fileList.length; i++) {
149
+ formData.append("files", fileList[i]);
150
+ }
151
+
152
+ uploadStatus.textContent = "Upload en cours...";
153
+ uploadStatus.className = "upload-status uploading";
154
+ uploadZone.classList.add("uploading");
155
+
156
+ fetch("/api/upload", {
157
+ method: "POST",
158
+ body: formData
159
+ })
160
+ .then(function (res) { return res.json(); })
161
+ .then(function (data) {
162
+ uploadZone.classList.remove("uploading");
163
+ var ok = 0;
164
+ var errors = [];
165
+
166
+ (data.results || []).forEach(function (r) {
167
+ if (r.status === "ok") {
168
+ ok++;
169
+ state.uploadedDocs.push({ filename: r.filename, chunks: r.chunks });
170
+ } else {
171
+ errors.push(r.filename + ": " + (r.message || "erreur"));
172
+ }
173
+ });
174
+
175
+ (data.skipped || []).forEach(function (s) {
176
+ errors.push(s.filename + ": " + s.reason);
177
+ });
178
+
179
+ if (ok > 0 && errors.length === 0) {
180
+ uploadStatus.textContent = ok + " fichier(s) ajoute(s) au corpus";
181
+ uploadStatus.className = "upload-status success";
182
+ } else if (ok > 0 && errors.length > 0) {
183
+ uploadStatus.textContent = ok + " OK, " + errors.length + " erreur(s): " + errors.join("; ");
184
+ uploadStatus.className = "upload-status warning";
185
+ } else {
186
+ uploadStatus.textContent = "Erreur: " + errors.join("; ");
187
+ uploadStatus.className = "upload-status error";
188
+ }
189
+
190
+ renderUploadList();
191
+ })
192
+ .catch(function () {
193
+ uploadZone.classList.remove("uploading");
194
+ uploadStatus.textContent = "Erreur de connexion. Reessaye.";
195
+ uploadStatus.className = "upload-status error";
196
+ });
197
+ }
198
+
199
+ function deleteDoc(filename) {
200
+ fetch("/api/documents/" + encodeURIComponent(filename), { method: "DELETE" })
201
+ .then(function (res) { return res.json(); })
202
+ .then(function () {
203
+ state.uploadedDocs = state.uploadedDocs.filter(function (d) {
204
+ return d.filename !== filename;
205
+ });
206
+ renderUploadList();
207
+ uploadStatus.textContent = filename + " supprime";
208
+ uploadStatus.className = "upload-status success";
209
+ });
210
+ }
211
+
212
+ // Upload zone events
213
+ uploadZone.addEventListener("click", function () {
214
+ fileInput.click();
215
+ });
216
+
217
+ fileInput.addEventListener("change", function () {
218
+ uploadFiles(fileInput.files);
219
+ fileInput.value = "";
220
+ });
221
+
222
+ uploadZone.addEventListener("dragover", function (e) {
223
+ e.preventDefault();
224
+ uploadZone.classList.add("dragover");
225
+ });
226
+
227
+ uploadZone.addEventListener("dragleave", function () {
228
+ uploadZone.classList.remove("dragover");
229
+ });
230
+
231
+ uploadZone.addEventListener("drop", function (e) {
232
+ e.preventDefault();
233
+ uploadZone.classList.remove("dragover");
234
+ uploadFiles(e.dataTransfer.files);
235
+ });
236
+
237
  /* ===== API calls ===== */
238
  function sendMessage(text) {
239
  state.history.push({ role: "user", content: text });
 
252
  mode: state.mode,
253
  topic: state.topic,
254
  phase: state.phase,
255
+ history: state.history.slice(0, -1)
256
  })
257
  })
258
  .then(function (res) { return res.json(); })
 
367
  state.history = [];
368
  state.timestamps = [];
369
  state.analysisResult = null;
370
+ state.uploadedDocs = [];
371
 
372
  topicInput.value = "";
373
  chatInput.value = "";
374
  messagesEl.querySelectorAll(".message").forEach(function (el) { el.remove(); });
375
+ uploadList.innerHTML = "";
376
+ uploadStatus.textContent = "";
377
 
378
  modeBtns.forEach(function (btn) {
379
  btn.classList.toggle("selected", btn.dataset.mode === "TUTOR");
 
383
  btnEnd.disabled = false;
384
  btnSend.disabled = false;
385
 
386
+ // Load existing documents
387
+ loadDocumentList();
388
+
389
  showScreen(setupScreen);
390
  }
391
 
392
+ /* ===== Load existing documents on page load ===== */
393
+ function loadDocumentList() {
394
+ fetch("/api/documents")
395
+ .then(function (res) { return res.json(); })
396
+ .then(function (data) {
397
+ state.uploadedDocs = (data.documents || []).map(function (d) {
398
+ return { filename: d.filename, chunks: "?" };
399
+ });
400
+ renderUploadList();
401
+ })
402
+ .catch(function () {});
403
+ }
404
+
405
  /* ===== Event listeners ===== */
406
 
407
  // Mode selection
 
427
  modeBadge.textContent = state.mode === "TUTOR" ? "Tuteur" : "Critique";
428
  topicBadge.textContent = topic;
429
 
430
+ // Show doc count badge
431
+ if (state.uploadedDocs.length > 0) {
432
+ docsBadge.textContent = state.uploadedDocs.length + " doc(s)";
433
+ docsBadge.style.display = "inline-block";
434
+ } else {
435
+ docsBadge.style.display = "none";
436
+ }
437
+
438
  renderPhaseIndicator();
439
  showScreen(chatScreen);
440
  chatInput.focus();
 
471
  // New session from analysis screen
472
  btnNewSession.addEventListener("click", resetSession);
473
 
474
+ // Load existing docs on startup
475
+ loadDocumentList();
476
+
477
  })();
static/style.css CHANGED
@@ -127,6 +127,118 @@ body {
127
  .btn-primary:hover { opacity: 0.9; }
128
  .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
129
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
130
  /* ===== Chat Screen ===== */
131
  #chat-screen {
132
  height: 100vh;
 
127
  .btn-primary:hover { opacity: 0.9; }
128
  .btn-primary:disabled { opacity: 0.5; cursor: not-allowed; }
129
 
130
+ /* ===== Upload Zone ===== */
131
+ .upload-zone {
132
+ width: 100%;
133
+ border: 2px dashed var(--border);
134
+ border-radius: var(--radius);
135
+ padding: 28px 20px;
136
+ text-align: center;
137
+ cursor: pointer;
138
+ transition: all 0.2s;
139
+ background: var(--bg-secondary);
140
+ }
141
+
142
+ .upload-zone:hover, .upload-zone.dragover {
143
+ border-color: var(--accent);
144
+ background: var(--bg-tertiary);
145
+ }
146
+
147
+ .upload-zone.uploading {
148
+ opacity: 0.6;
149
+ pointer-events: none;
150
+ }
151
+
152
+ .upload-icon {
153
+ font-size: 2rem;
154
+ color: var(--accent);
155
+ margin-bottom: 8px;
156
+ font-weight: 300;
157
+ }
158
+
159
+ .upload-text {
160
+ font-size: 0.92rem;
161
+ color: var(--text-primary);
162
+ margin-bottom: 4px;
163
+ }
164
+
165
+ .upload-hint {
166
+ font-size: 0.78rem;
167
+ color: var(--text-secondary);
168
+ }
169
+
170
+ .upload-list {
171
+ width: 100%;
172
+ display: flex;
173
+ flex-direction: column;
174
+ gap: 6px;
175
+ }
176
+
177
+ .upload-item {
178
+ display: flex;
179
+ align-items: center;
180
+ gap: 10px;
181
+ padding: 10px 14px;
182
+ background: var(--bg-secondary);
183
+ border: 1px solid var(--border);
184
+ border-radius: var(--radius-sm);
185
+ font-size: 0.85rem;
186
+ }
187
+
188
+ .upload-item-icon {
189
+ background: var(--accent);
190
+ color: #fff;
191
+ padding: 2px 8px;
192
+ border-radius: 4px;
193
+ font-size: 0.7rem;
194
+ font-weight: 700;
195
+ flex-shrink: 0;
196
+ }
197
+
198
+ .upload-item-name {
199
+ flex: 1;
200
+ overflow: hidden;
201
+ text-overflow: ellipsis;
202
+ white-space: nowrap;
203
+ }
204
+
205
+ .upload-item-chunks {
206
+ color: var(--text-secondary);
207
+ font-size: 0.78rem;
208
+ flex-shrink: 0;
209
+ }
210
+
211
+ .upload-item-delete {
212
+ background: transparent;
213
+ border: 1px solid var(--border);
214
+ color: var(--text-secondary);
215
+ border-radius: 4px;
216
+ cursor: pointer;
217
+ padding: 2px 8px;
218
+ font-size: 0.75rem;
219
+ transition: all 0.2s;
220
+ }
221
+
222
+ .upload-item-delete:hover {
223
+ border-color: var(--danger);
224
+ color: var(--danger);
225
+ }
226
+
227
+ .upload-status {
228
+ font-size: 0.82rem;
229
+ min-height: 1.2em;
230
+ }
231
+
232
+ .upload-status.success { color: var(--success); }
233
+ .upload-status.warning { color: var(--warning); }
234
+ .upload-status.error { color: var(--danger); }
235
+ .upload-status.uploading { color: var(--accent-light); }
236
+
237
+ .badge-docs {
238
+ background: var(--success);
239
+ color: #fff;
240
+ }
241
+
242
  /* ===== Chat Screen ===== */
243
  #chat-screen {
244
  height: 100vh;