om4r932 commited on
Commit
8b6f532
·
1 Parent(s): ba94174

Beta version

Browse files
Files changed (6) hide show
  1. README.md +3 -5
  2. app.py +138 -0
  3. requirements.txt +8 -0
  4. static/script.js +304 -0
  5. static/styles.css +511 -0
  6. templates/index.html +119 -0
README.md CHANGED
@@ -1,11 +1,9 @@
1
  ---
2
  title: RAGnarok
3
  emoji: 📘
4
- colorFrom: yellow
5
- colorTo: red
6
- sdk: gradio
7
- sdk_version: 5.33.2
8
- app_file: app.py
9
  pinned: true
10
  license: gpl-3.0
11
  short_description: Chat with the specs
 
1
  ---
2
  title: RAGnarok
3
  emoji: 📘
4
+ colorFrom: red
5
+ colorTo: blue
6
+ sdk: docker
 
 
7
  pinned: true
8
  license: gpl-3.0
9
  short_description: Chat with the specs
app.py ADDED
@@ -0,0 +1,138 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from typing import List, Dict, Any
2
+ import zipfile
3
+ import os
4
+ import warnings
5
+ from openai import OpenAI
6
+ from dotenv import load_dotenv
7
+ import bm25s
8
+ from fastapi.staticfiles import StaticFiles
9
+ from nltk.stem import WordNetLemmatizer
10
+ import nltk
11
+ from fastapi import FastAPI
12
+ from fastapi.responses import FileResponse
13
+ from fastapi.middleware.cors import CORSMiddleware
14
+ import numpy as np
15
+ from pydantic import BaseModel
16
+ from sklearn.preprocessing import MinMaxScaler
17
+
18
+ load_dotenv()
19
+
20
+ nltk.download('wordnet')
21
+ if os.path.exists("bm25s.zip"):
22
+ with zipfile.ZipFile("bm25s.zip", 'r') as zip_ref:
23
+ zip_ref.extractall(".")
24
+ bm25_engine = bm25s.BM25.load("3gpp_bm25_docs", load_corpus=True)
25
+ lemmatizer = WordNetLemmatizer()
26
+ llm = OpenAI(api_key=os.environ.get("GEMINI"), base_url="https://generativelanguage.googleapis.com/v1beta/openai/")
27
+
28
+ warnings.filterwarnings("ignore")
29
+
30
+ app = FastAPI(title="RAGnarok",
31
+ description="API to search specifications for RAG")
32
+
33
+ app.mount("/static", StaticFiles(directory="static"), name="static")
34
+
35
+ origins = [
36
+ "*",
37
+ ]
38
+
39
+ app.add_middleware(
40
+ CORSMiddleware,
41
+ allow_origins=origins,
42
+ allow_credentials=True,
43
+ allow_methods=["*"],
44
+ allow_headers=["*"],
45
+ )
46
+
47
+ class SearchRequest(BaseModel):
48
+ keyword: str
49
+ threshold: int
50
+
51
+ class SearchResponse(BaseModel):
52
+ results: List[Dict[str, Any]]
53
+
54
+ class ChatRequest(BaseModel):
55
+ messages: List[Dict[str, str]]
56
+ model: str
57
+
58
+ class ChatResponse(BaseModel):
59
+ response: str
60
+
61
+ @app.get("/")
62
+ async def main_menu():
63
+ return FileResponse(os.path.join("templates", "index.html"))
64
+
65
+ @app.post("/chat", response_model=ChatResponse)
66
+ def question_the_sources(req: ChatRequest):
67
+ model = req.model
68
+ resp = llm.chat.completions.create(
69
+ messages=req.messages,
70
+ model=model
71
+ )
72
+ return ChatResponse(response=resp.choices[0].message.content)
73
+
74
+ @app.post("/search", response_model=SearchResponse)
75
+ def search_specifications(req: SearchRequest):
76
+ keywords = req.keyword
77
+ threshold = req.threshold
78
+ query = lemmatizer.lemmatize(keywords)
79
+ results_out = []
80
+ query_tokens = bm25s.tokenize(query)
81
+ results, scores = bm25_engine.retrieve(query_tokens, k=len(bm25_engine.corpus))
82
+
83
+ def calculate_boosted_score(metadata, score, query):
84
+ title = {lemmatizer.lemmatize(metadata['title']).lower()}
85
+ q = {query.lower()}
86
+ spec_id_presence = 0.5 if len(q & {metadata['id']}) > 0 else 0
87
+ booster = len(q & title) * 0.5
88
+ return score + spec_id_presence + booster
89
+
90
+ spec_scores = {}
91
+ spec_indices = {}
92
+ spec_details = {}
93
+
94
+ for i in range(results.shape[1]):
95
+ doc = results[0, i]
96
+ score = scores[0, i]
97
+ spec = doc["metadata"]["id"]
98
+
99
+ boosted_score = calculate_boosted_score(doc['metadata'], score, query)
100
+
101
+ if spec not in spec_scores or boosted_score > spec_scores[spec]:
102
+ spec_scores[spec] = boosted_score
103
+ spec_indices[spec] = i
104
+ spec_details[spec] = {
105
+ 'original_score': score,
106
+ 'boosted_score': boosted_score,
107
+ 'doc': doc
108
+ }
109
+
110
+ def normalize_scores(scores_dict):
111
+ if not scores_dict:
112
+ return {}
113
+
114
+ scores_array = np.array(list(scores_dict.values())).reshape(-1, 1)
115
+ scaler = MinMaxScaler()
116
+ normalized_scores = scaler.fit_transform(scores_array).flatten()
117
+
118
+ normalized_dict = {}
119
+ for i, spec in enumerate(scores_dict.keys()):
120
+ normalized_dict[spec] = normalized_scores[i]
121
+
122
+ return normalized_dict
123
+
124
+ normalized_scores = normalize_scores(spec_scores)
125
+
126
+ for spec in spec_details:
127
+ spec_details[spec]["normalized_score"] = normalized_scores[spec]
128
+
129
+ unique_specs = sorted(normalized_scores.keys(), key=lambda x: normalized_scores[x], reverse=True)
130
+
131
+ for rank, spec in enumerate(unique_specs, 1):
132
+ details = spec_details[spec]
133
+ metadata = details['doc']['metadata']
134
+ if details['normalized_score'] < threshold / 100:
135
+ break
136
+ results_out.append({'id': metadata['id'], 'title': metadata['title'], 'section': metadata['section_title'], 'content': details['doc']['text'], 'similarity': int(details['normalized_score']*100)})
137
+
138
+ return SearchResponse(results=results_out)
requirements.txt ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ openai
2
+ fastapi
3
+ uvicorn[standard]
4
+ python-dotenv
5
+ bm25s[full]
6
+ ntlk
7
+ numpy
8
+ scikit-learn
static/script.js ADDED
@@ -0,0 +1,304 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ class DocumentSearchChatBot {
2
+ constructor() {
3
+ this.selectedDocuments = new Set();
4
+ this.searchResults = [];
5
+ this.chatHistory = [];
6
+ this.initializeEventListeners();
7
+ this.updateThresholdDisplay();
8
+ }
9
+
10
+ initializeEventListeners() {
11
+ // Search form
12
+ document.getElementById('search-form').addEventListener('submit', (e) => {
13
+ e.preventDefault();
14
+ this.performSearch();
15
+ });
16
+
17
+ // Threshold slider
18
+ document.getElementById('threshold').addEventListener('input', (e) => {
19
+ this.updateThresholdDisplay();
20
+ });
21
+
22
+ // Selection controls
23
+ document.getElementById('select-all').addEventListener('click', () => {
24
+ this.selectAllDocuments();
25
+ });
26
+
27
+ document.getElementById('unselect-all').addEventListener('click', () => {
28
+ this.unselectAllDocuments();
29
+ });
30
+
31
+ // Chat launch
32
+ document.getElementById('start-chat').addEventListener('click', () => {
33
+ this.startChat();
34
+ });
35
+
36
+ // Chat form
37
+ document.getElementById('chat-form').addEventListener('submit', (e) => {
38
+ e.preventDefault();
39
+ this.sendChatMessage();
40
+ });
41
+
42
+ // Back to search
43
+ document.getElementById('back-to-search').addEventListener('click', () => {
44
+ this.backToSearch();
45
+ });
46
+
47
+ // Modal close
48
+ document.querySelector('.modal-close').addEventListener('click', () => {
49
+ this.closeModal();
50
+ });
51
+
52
+ // Close modal on background click
53
+ document.getElementById('modal').addEventListener('click', (e) => {
54
+ if (e.target.id === 'modal') {
55
+ this.closeModal();
56
+ }
57
+ });
58
+ }
59
+
60
+ updateThresholdDisplay() {
61
+ const threshold = document.getElementById('threshold').value;
62
+ document.getElementById('threshold-value').textContent = threshold;
63
+ }
64
+
65
+ async performSearch() {
66
+ const keyword = document.getElementById('keyword').value.trim();
67
+ const threshold = parseFloat(document.getElementById('threshold').value);
68
+
69
+ if (!keyword) return;
70
+
71
+ this.showLoading();
72
+
73
+ try {
74
+ const response = await fetch('/search', {
75
+ method: 'POST',
76
+ headers: {
77
+ 'Content-Type': 'application/json',
78
+ },
79
+ body: JSON.stringify({
80
+ keyword: keyword,
81
+ threshold: threshold
82
+ })
83
+ });
84
+
85
+ if (!response.ok) {
86
+ throw new Error(`HTTP error! status: ${response.status}`);
87
+ }
88
+
89
+ const data = await response.json();
90
+ this.searchResults = data.results || [];
91
+ this.displayResults();
92
+
93
+ } catch (error) {
94
+ console.error('Erreur lors de la recherche:', error);
95
+ this.showError('Erreur lors de la recherche. Veuillez réessayer.');
96
+ } finally {
97
+ this.hideLoading();
98
+ }
99
+ }
100
+
101
+ displayResults() {
102
+ const resultsContainer = document.getElementById('results-container');
103
+ const resultsSection = document.getElementById('results-section');
104
+
105
+ if (this.searchResults.length === 0) {
106
+ resultsContainer.innerHTML = '<p class="no-results">Aucun résultat trouvé.</p>';
107
+ resultsSection.classList.remove('hidden');
108
+ return;
109
+ }
110
+
111
+ resultsContainer.innerHTML = '';
112
+
113
+ this.searchResults.forEach((result, index) => {
114
+ const card = this.createResultCard(result, index);
115
+ resultsContainer.appendChild(card);
116
+ let viewBtn = card.querySelector('.view-btn');
117
+ viewBtn.addEventListener('click', () => {
118
+ this.showDocumentContent(result.id, result.section, result.content);
119
+ });
120
+ });
121
+
122
+ resultsSection.classList.remove('hidden');
123
+ this.updateChatButtonState();
124
+ }
125
+
126
+ createResultCard(result, index) {
127
+ const card = document.createElement('div');
128
+ card.className = 'result-card';
129
+ card.dataset.index = index;
130
+
131
+ card.innerHTML = `
132
+ <div class="card-header">
133
+ <div class="card-info">
134
+ <div class="card-id">ID: ${result.id}</div>
135
+ <div class="card-title">${result.title}</div>
136
+ <div class="card-section">${result.section}</div>
137
+ </div>
138
+ <div class="similarity-score">${result.similarity}%</div>
139
+ </div>
140
+ <div class="card-actions">
141
+ <button class="view-btn" data-id="${result.id}" data-section="${result.section}" data-content="${result.content}">
142
+ <span class="material-icons">visibility</span>
143
+ Voir le contenu
144
+ </button>
145
+ <input type="checkbox" class="select-checkbox" onchange="app.toggleDocumentSelection(${index})">
146
+ </div>
147
+ `;
148
+
149
+ return card;
150
+ }
151
+
152
+ toggleDocumentSelection(index) {
153
+ const card = document.querySelector(`[data-index="${index}"]`);
154
+ const checkbox = card.querySelector('.select-checkbox');
155
+
156
+ if (checkbox.checked) {
157
+ this.selectedDocuments.add(index);
158
+ card.classList.add('selected');
159
+ } else {
160
+ this.selectedDocuments.delete(index);
161
+ card.classList.remove('selected');
162
+ }
163
+
164
+ this.updateChatButtonState();
165
+ }
166
+
167
+ selectAllDocuments() {
168
+ const checkboxes = document.querySelectorAll('.select-checkbox');
169
+ checkboxes.forEach((checkbox, index) => {
170
+ checkbox.checked = true;
171
+ this.selectedDocuments.add(index);
172
+ checkbox.closest('.result-card').classList.add('selected');
173
+ });
174
+ this.updateChatButtonState();
175
+ }
176
+
177
+ unselectAllDocuments() {
178
+ const checkboxes = document.querySelectorAll('.select-checkbox');
179
+ checkboxes.forEach((checkbox, index) => {
180
+ checkbox.checked = false;
181
+ this.selectedDocuments.delete(index);
182
+ checkbox.closest('.result-card').classList.remove('selected');
183
+ });
184
+ this.updateChatButtonState();
185
+ }
186
+
187
+ updateChatButtonState() {
188
+ const chatButton = document.getElementById('start-chat');
189
+ chatButton.disabled = this.selectedDocuments.size === 0;
190
+ }
191
+
192
+ showDocumentContent(id, section, content) {
193
+ // Simuler le contenu du document (remplacer par un appel API réel)
194
+ document.getElementById('modal-title').textContent = `Specification n°${id} - ${section}`;
195
+ document.getElementById('modal-body').textContent = content;
196
+ document.getElementById('modal').style.display = 'block';
197
+ }
198
+
199
+ closeModal() {
200
+ document.getElementById('modal').style.display = 'none';
201
+ }
202
+
203
+ startChat() {
204
+
205
+ if (this.selectedDocuments.size === 0) return;
206
+
207
+ const selectedDocs = Array.from(this.selectedDocuments).map(index => {
208
+ const doc = this.searchResults[index];
209
+ return `(${doc.id} ${doc.title} ${doc.section} ${doc.content || ""})`
210
+ }).join("\n");
211
+ this.chatHistory = [{"role": "system", "content": `You are a helpful AI assistant. You will answer any questions related to the following specifications: ${selectedDocs}`}];
212
+
213
+ document.getElementById('search-section').classList.add('hidden');
214
+ document.getElementById('results-section').classList.add('hidden');
215
+ document.getElementById('chat-section').classList.remove('hidden');
216
+
217
+ // Ajouter un message de bienvenue
218
+ this.addChatMessage('bot', `Bonjour ! Je suis prêt à répondre à vos questions sur les ${this.selectedDocuments.size} document(s) sélectionné(s). Que souhaitez-vous savoir ?`);
219
+ }
220
+
221
+ backToSearch() {
222
+ document.getElementById('chat-section').classList.add('hidden');
223
+ document.getElementById('search-section').classList.remove('hidden');
224
+ document.getElementById('results-section').classList.remove('hidden');
225
+
226
+ // Vider les messages du chat
227
+ document.getElementById('chat-messages').innerHTML = '';
228
+ }
229
+
230
+ async sendChatMessage() {
231
+ const input = document.getElementById('chat-input');
232
+ const message = input.value.trim();
233
+ const model = document.getElementById('model-select').value;
234
+
235
+ if (!message) return;
236
+
237
+ // Ajouter le message de l'utilisateur
238
+ this.addChatMessage('user', message);
239
+ input.value = '';
240
+
241
+ this.chatHistory.push({"role": "user", "content": message});
242
+
243
+ // Désactiver le formulaire pendant l'envoi
244
+ const form = document.getElementById('chat-form');
245
+ const submitBtn = form.querySelector('button');
246
+ submitBtn.disabled = true;
247
+
248
+ try {
249
+ const response = await fetch('/chat', {
250
+ method: 'POST',
251
+ headers: {
252
+ 'Content-Type': 'application/json'
253
+ },
254
+ body: JSON.stringify({
255
+ messages: this.chatHistory,
256
+ model: model,
257
+ })
258
+ });
259
+
260
+ if (!response.ok) {
261
+ this.chatHistory.push({"role": "assistant", "content": `HTTP error! status: ${response.status}`})
262
+ throw new Error(`HTTP error! status: ${response.status}`);
263
+ }
264
+
265
+ const data = await response.json();
266
+ this.addChatMessage('bot', data.response);
267
+ this.chatHistory.push({"role": "assistant", "content": data.response})
268
+
269
+ } catch (error) {
270
+ console.error('Erreur lors de l\'envoi du message:', error);
271
+ this.addChatMessage('bot', 'Désolé, une erreur s\'est produite. Veuillez réessayer.');
272
+ } finally {
273
+ submitBtn.disabled = false;
274
+ }
275
+ }
276
+
277
+ addChatMessage(sender, message) {
278
+ const messagesContainer = document.getElementById('chat-messages');
279
+ const messageDiv = document.createElement('div');
280
+ messageDiv.className = `message ${sender}`;
281
+ messageDiv.textContent = message;
282
+ messagesContainer.appendChild(messageDiv);
283
+ messagesContainer.scrollTop = messagesContainer.scrollHeight;
284
+ }
285
+
286
+ showLoading() {
287
+ document.getElementById('loading').classList.remove('hidden');
288
+ }
289
+
290
+ hideLoading() {
291
+ document.getElementById('loading').classList.add('hidden');
292
+ }
293
+
294
+ showError(message) {
295
+ // Vous pouvez implémenter une notification d'erreur plus sophistiquée
296
+ alert(message);
297
+ }
298
+ }
299
+
300
+ // Initialiser l'application
301
+ const app = new DocumentSearchChatBot();
302
+
303
+ // Fonction globale pour les événements onclick dans le HTML
304
+ window.app = app;
static/styles.css ADDED
@@ -0,0 +1,511 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ * {
2
+ margin: 0;
3
+ padding: 0;
4
+ box-sizing: border-box;
5
+ }
6
+
7
+ body {
8
+ font-family: 'Roboto', sans-serif;
9
+ background-color: #f8f9fa;
10
+ color: #202124;
11
+ line-height: 1.6;
12
+ }
13
+
14
+ .container {
15
+ max-width: 1200px;
16
+ margin: 0 auto;
17
+ padding: 20px;
18
+ }
19
+
20
+ /* Header */
21
+ .header {
22
+ text-align: center;
23
+ margin-bottom: 40px;
24
+ }
25
+
26
+ .logo {
27
+ display: flex;
28
+ align-items: center;
29
+ justify-content: center;
30
+ gap: 10px;
31
+ }
32
+
33
+ .logo .material-icons {
34
+ font-size: 32px;
35
+ color: #4285f4;
36
+ }
37
+
38
+ .logo h1 {
39
+ color: #202124;
40
+ font-weight: 400;
41
+ font-size: 28px;
42
+ }
43
+
44
+ /* Search Section */
45
+ .search-section {
46
+ background: white;
47
+ border-radius: 8px;
48
+ padding: 40px;
49
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
50
+ margin-bottom: 30px;
51
+ }
52
+
53
+ .search-container h2 {
54
+ text-align: center;
55
+ margin-bottom: 30px;
56
+ color: #202124;
57
+ font-weight: 400;
58
+ }
59
+
60
+ .search-form {
61
+ max-width: 600px;
62
+ margin: 0 auto;
63
+ }
64
+
65
+ .input-group {
66
+ position: relative;
67
+ margin-bottom: 20px;
68
+ }
69
+
70
+ .input-group input {
71
+ width: 100%;
72
+ padding: 16px 50px 16px 16px;
73
+ border: 2px solid #dadce0;
74
+ border-radius: 24px;
75
+ font-size: 16px;
76
+ outline: none;
77
+ transition: border-color 0.3s;
78
+ }
79
+
80
+ .input-group input:focus {
81
+ border-color: #4285f4;
82
+ }
83
+
84
+ .input-group .material-icons {
85
+ position: absolute;
86
+ right: 16px;
87
+ top: 50%;
88
+ transform: translateY(-50%);
89
+ color: #5f6368;
90
+ }
91
+
92
+ .threshold-group {
93
+ display: flex;
94
+ align-items: center;
95
+ gap: 15px;
96
+ margin-bottom: 30px;
97
+ justify-content: center;
98
+ }
99
+
100
+ .threshold-group label {
101
+ font-weight: 500;
102
+ color: #5f6368;
103
+ }
104
+
105
+ .threshold-group input[type="range"] {
106
+ width: 200px;
107
+ accent-color: #4285f4;
108
+ }
109
+
110
+ #threshold-value {
111
+ font-weight: 500;
112
+ color: #4285f4;
113
+ min-width: 30px;
114
+ }
115
+
116
+ .search-btn {
117
+ display: flex;
118
+ align-items: center;
119
+ gap: 8px;
120
+ background: #4285f4;
121
+ color: white;
122
+ border: none;
123
+ padding: 12px 24px;
124
+ border-radius: 24px;
125
+ font-size: 16px;
126
+ cursor: pointer;
127
+ margin: 0 auto;
128
+ transition: background-color 0.3s;
129
+ }
130
+
131
+ .search-btn:hover {
132
+ background: #3367d6;
133
+ }
134
+
135
+ /* Results Section */
136
+ .results-section {
137
+ background: white;
138
+ border-radius: 8px;
139
+ padding: 30px;
140
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
141
+ margin-bottom: 30px;
142
+ }
143
+
144
+ .results-header {
145
+ display: flex;
146
+ justify-content: space-between;
147
+ align-items: center;
148
+ margin-bottom: 20px;
149
+ flex-wrap: wrap;
150
+ gap: 15px;
151
+ }
152
+
153
+ .results-header h3 {
154
+ color: #202124;
155
+ font-weight: 500;
156
+ }
157
+
158
+ .selection-controls {
159
+ display: flex;
160
+ gap: 10px;
161
+ }
162
+
163
+ .control-btn {
164
+ display: flex;
165
+ align-items: center;
166
+ gap: 5px;
167
+ background: #f8f9fa;
168
+ border: 1px solid #dadce0;
169
+ padding: 8px 16px;
170
+ border-radius: 20px;
171
+ cursor: pointer;
172
+ font-size: 14px;
173
+ transition: background-color 0.3s;
174
+ }
175
+
176
+ .control-btn:hover {
177
+ background: #e8f0fe;
178
+ }
179
+
180
+ .results-container {
181
+ display: grid;
182
+ grid-template-columns: repeat(auto-fill, minmax(350px, 1fr));
183
+ gap: 20px;
184
+ margin-bottom: 30px;
185
+ }
186
+
187
+ .result-card {
188
+ border: 1px solid #dadce0;
189
+ border-radius: 8px;
190
+ padding: 20px;
191
+ background: white;
192
+ transition: all 0.3s;
193
+ cursor: pointer;
194
+ }
195
+
196
+ .result-card:hover {
197
+ box-shadow: 0 4px 12px rgba(0,0,0,0.15);
198
+ transform: translateY(-2px);
199
+ }
200
+
201
+ .result-card.selected {
202
+ border-color: #4285f4;
203
+ background: #e8f0fe;
204
+ }
205
+
206
+ .card-header {
207
+ display: flex;
208
+ justify-content: between;
209
+ align-items: flex-start;
210
+ margin-bottom: 15px;
211
+ }
212
+
213
+ .card-info {
214
+ flex: 1;
215
+ }
216
+
217
+ .card-id {
218
+ font-size: 12px;
219
+ color: #5f6368;
220
+ font-weight: 500;
221
+ }
222
+
223
+ .card-title {
224
+ font-size: 16px;
225
+ font-weight: 500;
226
+ color: #202124;
227
+ margin: 5px 0;
228
+ }
229
+
230
+ .card-section {
231
+ font-size: 14px;
232
+ color: #5f6368;
233
+ margin-bottom: 10px;
234
+ }
235
+
236
+ .similarity-score {
237
+ background: #e8f0fe;
238
+ color: #4285f4;
239
+ padding: 4px 8px;
240
+ border-radius: 12px;
241
+ font-size: 12px;
242
+ font-weight: 500;
243
+ margin-left: 10px;
244
+ }
245
+
246
+ .card-actions {
247
+ display: flex;
248
+ justify-content: space-between;
249
+ align-items: center;
250
+ margin-top: 15px;
251
+ }
252
+
253
+ .view-btn {
254
+ background: #4285f4;
255
+ color: white;
256
+ border: none;
257
+ padding: 8px 16px;
258
+ border-radius: 16px;
259
+ cursor: pointer;
260
+ font-size: 14px;
261
+ display: flex;
262
+ align-items: center;
263
+ gap: 5px;
264
+ }
265
+
266
+ .view-btn:hover {
267
+ background: #3367d6;
268
+ }
269
+
270
+ .select-checkbox {
271
+ width: 20px;
272
+ height: 20px;
273
+ accent-color: #4285f4;
274
+ }
275
+
276
+ .chat-launch {
277
+ text-align: center;
278
+ }
279
+
280
+ .chat-btn {
281
+ display: flex;
282
+ align-items: center;
283
+ gap: 10px;
284
+ background: #34a853;
285
+ color: white;
286
+ border: none;
287
+ padding: 16px 32px;
288
+ border-radius: 24px;
289
+ font-size: 16px;
290
+ cursor: pointer;
291
+ margin: 0 auto;
292
+ transition: background-color 0.3s;
293
+ }
294
+
295
+ .chat-btn:hover:not(:disabled) {
296
+ background: #2d8f47;
297
+ }
298
+
299
+ .chat-btn:disabled {
300
+ background: #dadce0;
301
+ cursor: not-allowed;
302
+ }
303
+
304
+ /* Chat Section */
305
+ .chat-section {
306
+ background: white;
307
+ border-radius: 8px;
308
+ padding: 30px;
309
+ box-shadow: 0 2px 10px rgba(0,0,0,0.1);
310
+ height: 600px;
311
+ display: flex;
312
+ flex-direction: column;
313
+ }
314
+
315
+ .chat-header {
316
+ display: flex;
317
+ justify-content: space-between;
318
+ align-items: center;
319
+ margin-bottom: 20px;
320
+ padding-bottom: 15px;
321
+ border-bottom: 1px solid #dadce0;
322
+ }
323
+
324
+ .back-btn {
325
+ display: flex;
326
+ align-items: center;
327
+ gap: 5px;
328
+ background: #f8f9fa;
329
+ border: 1px solid #dadce0;
330
+ padding: 8px 16px;
331
+ border-radius: 20px;
332
+ cursor: pointer;
333
+ font-size: 14px;
334
+ }
335
+
336
+ .chat-container {
337
+ flex: 1;
338
+ display: flex;
339
+ flex-direction: column;
340
+ }
341
+
342
+ .chat-messages {
343
+ flex: 1;
344
+ overflow-y: auto;
345
+ padding: 20px 0;
346
+ border-bottom: 1px solid #dadce0;
347
+ margin-bottom: 20px;
348
+ }
349
+
350
+ .message {
351
+ margin-bottom: 15px;
352
+ padding: 12px 16px;
353
+ border-radius: 18px;
354
+ max-width: 80%;
355
+ }
356
+
357
+ .message.user {
358
+ background: #4285f4;
359
+ color: white;
360
+ margin-left: auto;
361
+ }
362
+
363
+ .message.bot {
364
+ background: #f1f3f4;
365
+ color: #202124;
366
+ }
367
+
368
+ .chat-input-group {
369
+ display: flex;
370
+ gap: 10px;
371
+ align-items: center;
372
+ }
373
+
374
+ .chat-input-group input {
375
+ flex: 1;
376
+ padding: 12px 16px;
377
+ border: 1px solid #dadce0;
378
+ border-radius: 24px;
379
+ outline: none;
380
+ }
381
+
382
+ .chat-input-group select {
383
+ padding: 12px;
384
+ border: 1px solid #dadce0;
385
+ border-radius: 20px;
386
+ outline: none;
387
+ }
388
+
389
+ .chat-input-group button {
390
+ background: #4285f4;
391
+ color: white;
392
+ border: none;
393
+ padding: 12px;
394
+ border-radius: 50%;
395
+ cursor: pointer;
396
+ display: flex;
397
+ align-items: center;
398
+ justify-content: center;
399
+ }
400
+
401
+ /* Modal */
402
+ .modal {
403
+ display: none;
404
+ position: fixed;
405
+ top: 0;
406
+ left: 0;
407
+ width: 100%;
408
+ height: 100%;
409
+ background: rgba(0,0,0,0.5);
410
+ z-index: 1000;
411
+ }
412
+
413
+ .modal-content {
414
+ position: absolute;
415
+ top: 50%;
416
+ left: 50%;
417
+ transform: translate(-50%, -50%);
418
+ background: white;
419
+ border-radius: 8px;
420
+ max-width: 800px;
421
+ max-height: 80vh;
422
+ width: 90%;
423
+ overflow: hidden;
424
+ }
425
+
426
+ .modal-header {
427
+ display: flex;
428
+ justify-content: space-between;
429
+ align-items: center;
430
+ padding: 20px;
431
+ border-bottom: 1px solid #dadce0;
432
+ }
433
+
434
+ .modal-close {
435
+ background: none;
436
+ border: none;
437
+ cursor: pointer;
438
+ padding: 5px;
439
+ border-radius: 50%;
440
+ }
441
+
442
+ .modal-body {
443
+ padding: 20px;
444
+ max-height: 60vh;
445
+ overflow-y: auto;
446
+ line-height: 1.6;
447
+ }
448
+
449
+ /* Loading */
450
+ .loading {
451
+ position: fixed;
452
+ top: 0;
453
+ left: 0;
454
+ width: 100%;
455
+ height: 100%;
456
+ background: rgba(255,255,255,0.9);
457
+ display: flex;
458
+ flex-direction: column;
459
+ justify-content: center;
460
+ align-items: center;
461
+ z-index: 999;
462
+ }
463
+
464
+ .spinner {
465
+ width: 40px;
466
+ height: 40px;
467
+ border: 4px solid #f3f3f3;
468
+ border-top: 4px solid #4285f4;
469
+ border-radius: 50%;
470
+ animation: spin 1s linear infinite;
471
+ margin-bottom: 20px;
472
+ }
473
+
474
+ @keyframes spin {
475
+ 0% { transform: rotate(0deg); }
476
+ 100% { transform: rotate(360deg); }
477
+ }
478
+
479
+ /* Utility classes */
480
+ .hidden {
481
+ display: none !important;
482
+ }
483
+
484
+ /* Responsive */
485
+ @media (max-width: 768px) {
486
+ .container {
487
+ padding: 10px;
488
+ }
489
+
490
+ .search-section {
491
+ padding: 20px;
492
+ }
493
+
494
+ .results-container {
495
+ grid-template-columns: 1fr;
496
+ }
497
+
498
+ .results-header {
499
+ flex-direction: column;
500
+ align-items: stretch;
501
+ }
502
+
503
+ .selection-controls {
504
+ justify-content: center;
505
+ }
506
+
507
+ .threshold-group {
508
+ flex-direction: column;
509
+ gap: 10px;
510
+ }
511
+ }
templates/index.html ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="fr">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>Document Search ChatBot</title>
7
+ <link rel="preconnect" href="https://fonts.googleapis.com">
8
+ <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
+ <link href="https://fonts.googleapis.com/css2?family=Roboto:wght@300;400;500;700&display=swap" rel="stylesheet">
10
+ <link href="https://fonts.googleapis.com/icon?family=Material+Icons" rel="stylesheet">
11
+ <link rel="stylesheet" href="/static/styles.css">
12
+ </head>
13
+ <body>
14
+ <div class="container">
15
+ <!-- Header -->
16
+ <header class="header">
17
+ <div class="logo">
18
+ <span class="material-icons">search</span>
19
+ <h1>Document Search</h1>
20
+ </div>
21
+ </header>
22
+
23
+ <!-- Search Section -->
24
+ <section id="search-section" class="search-section">
25
+ <div class="search-container">
26
+ <h2>Rechercher des documents techniques</h2>
27
+ <form id="search-form" class="search-form">
28
+ <div class="input-group">
29
+ <input type="text" id="keyword" placeholder="Entrez vos mots-clés..." required>
30
+ <span class="material-icons">search</span>
31
+ </div>
32
+ <div class="threshold-group">
33
+ <label for="threshold">Seuil de similarité :</label>
34
+ <input type="range" id="threshold" min="0" max="100" step="1" value="70">
35
+ <span id="threshold-value"></span>
36
+ </div>
37
+ <button type="submit" class="search-btn">
38
+ <span class="material-icons">search</span>
39
+ Rechercher
40
+ </button>
41
+ </form>
42
+ </div>
43
+ </section>
44
+
45
+ <!-- Results Section -->
46
+ <section id="results-section" class="results-section hidden">
47
+ <div class="results-header">
48
+ <h3>Résultats de recherche</h3>
49
+ <div class="selection-controls">
50
+ <button id="select-all" class="control-btn">
51
+ <span class="material-icons">select_all</span>
52
+ Tout sélectionner
53
+ </button>
54
+ <button id="unselect-all" class="control-btn">
55
+ <span class="material-icons">deselect</span>
56
+ Tout désélectionner
57
+ </button>
58
+ </div>
59
+ </div>
60
+ <div id="results-container" class="results-container"></div>
61
+ <div class="chat-launch">
62
+ <button id="start-chat" class="chat-btn" disabled>
63
+ <span class="material-icons">chat</span>
64
+ Démarrer le ChatBot
65
+ </button>
66
+ </div>
67
+ </section>
68
+
69
+ <!-- ChatBot Section -->
70
+ <section id="chat-section" class="chat-section hidden">
71
+ <div class="chat-header">
72
+ <h3>ChatBot Assistant</h3>
73
+ <button id="back-to-search" class="back-btn">
74
+ <span class="material-icons">arrow_back</span>
75
+ Retour à la recherche
76
+ </button>
77
+ </div>
78
+ <div class="chat-container">
79
+ <div id="chat-messages" class="chat-messages"></div>
80
+ <form id="chat-form" class="chat-form">
81
+ <div class="chat-input-group">
82
+ <input type="text" id="chat-input" placeholder="Posez votre question..." required>
83
+ <select id="model-select">
84
+ <option value="gemini-2.5-flash-preview-05-20">Gemini 2.5 Flash Preview</option>
85
+ <option value="gemini-2.0-flash">Gemini 2.0 Flash</option>
86
+ <option value="gemma-3-27b-it">Gemma 3</option>
87
+ <option value="gemma-3n-e4b-it">Gemma 3n</option>
88
+ </select>
89
+ <button type="submit">
90
+ <span class="material-icons">send</span>
91
+ </button>
92
+ </div>
93
+ </form>
94
+ </div>
95
+ </section>
96
+ </div>
97
+
98
+ <!-- Modal for document content -->
99
+ <div id="modal" class="modal">
100
+ <div class="modal-content">
101
+ <div class="modal-header">
102
+ <h4 id="modal-title"></h4>
103
+ <button class="modal-close">
104
+ <span class="material-icons">close</span>
105
+ </button>
106
+ </div>
107
+ <div id="modal-body" class="modal-body"></div>
108
+ </div>
109
+ </div>
110
+
111
+ <!-- Loading overlay -->
112
+ <div id="loading" class="loading hidden">
113
+ <div class="spinner"></div>
114
+ <p>Recherche en cours...</p>
115
+ </div>
116
+
117
+ <script src="/static/script.js"></script>
118
+ </body>
119
+ </html>