lojol469-cmd commited on
Commit
b54b689
·
1 Parent(s): 30b5e11

Déploiement de l'agent Kibali sur Hugging Face

Browse files
.dockerignore CHANGED
@@ -1,10 +1,14 @@
1
- __pycache__
 
 
 
 
2
  *.pyc
3
  *.pt
4
  *.pkl
5
- model_cache
6
- data
7
- tools
8
- kibali_env
9
- frontend
10
  *.jar
 
 
 
 
 
 
1
+ # --- À EXCLURE (Trop lourd ou inutile) ---
2
+ kibali_env/
3
+ model_cache/
4
+ frontend/
5
+ __pycache__/
6
  *.pyc
7
  *.pt
8
  *.pkl
 
 
 
 
 
9
  *.jar
10
+ .git/
11
+ .gitignore
12
+
13
+ # --- À NE PAS EXCLURE ---
14
+ # Assure-toi que "tools" n'est PAS écrit ici !
.gitattributes CHANGED
@@ -1 +1,3 @@
1
  model_cache/** filter=lfs diff=lfs merge=lfs -text
 
 
 
1
  model_cache/** filter=lfs diff=lfs merge=lfs -text
2
+ data/** filter=lfs diff=lfs merge=lfs -text
3
+ *.pyc filter=lfs diff=lfs merge=lfs -text
CARTABON1/server.js CHANGED
@@ -1,3 +1,4 @@
 
1
  import express from "express";
2
  import mongoose from "mongoose";
3
  import dotenv from "dotenv";
@@ -5,9 +6,11 @@ import cors from "cors";
5
  import http from "http";
6
  import { Server } from "socket.io";
7
  import bcrypt from "bcrypt";
 
8
  import multer from "multer";
9
  import path from "path";
10
  import fs from "fs";
 
11
 
12
  dotenv.config();
13
 
@@ -16,17 +19,17 @@ const server = http.createServer(app);
16
  const io = new Server(server, {
17
  cors: {
18
  origin: "*",
19
- methods: ["GET", "POST"]
20
  }
21
  });
22
 
23
- app.use(express.json());
24
  app.use(cors());
 
25
 
26
  if (!fs.existsSync("uploads")) {
27
  fs.mkdirSync("uploads");
28
  }
29
- app.use("/uploads", express.static(path.join(process.cwd(), "uploads")));
30
 
31
  const storage = multer.diskStorage({
32
  destination: (req, file, cb) => cb(null, "uploads/"),
@@ -39,28 +42,25 @@ const upload = multer({ storage });
39
  // =========================
40
  mongoose
41
  .connect(process.env.MONGO_URI)
42
- .then(() => console.log("MongoDB connecté ✅"))
43
- .catch(err => console.error("Erreur MongoDB ❌:", err));
44
 
45
  // =========================
46
- // User Schema (ajout photo de profil)
47
  // =========================
48
  const userSchema = new mongoose.Schema({
49
  name: { type: String, required: true },
50
- email: { type: String, required: true, unique: true },
51
- password: { type: String }, // null pour Google login
52
- googleId: { type: String }, // pour OAuth Google
53
- photoUrl: { type: String }, // photo Google
54
  latitude: { type: Number, default: 0 },
55
  longitude: { type: Number, default: 0 },
 
 
56
  createdAt: { type: Date, default: Date.now }
57
  });
58
 
59
  const User = mongoose.model("User", userSchema);
60
 
61
- // =========================
62
- // Marker Schema (inchangé)
63
- // =========================
64
  const markerSchema = new mongoose.Schema({
65
  latitude: { type: Number, required: true },
66
  longitude: { type: Number, required: true },
@@ -69,109 +69,147 @@ const markerSchema = new mongoose.Schema({
69
  color: { type: String, default: "#ff0000" },
70
  photos: { type: [String], default: [] },
71
  videos: { type: [String], default: [] },
72
- createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User", default: null },
73
  createdAt: { type: Date, default: Date.now }
74
  });
75
  const Marker = mongoose.model("Marker", markerSchema);
76
 
77
  // =========================
78
- // Routes
79
  // =========================
80
- app.get("/", (req, res) => res.send("API Temps Réel fonctionne ✅"));
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
81
 
82
- // Lister tous les utilisateurs (sans mot de passe)
83
- app.get("/users", async (req, res) => {
 
 
84
  try {
85
- const users = await User.find({}, "-__v -password");
86
- res.json(users);
87
- } catch (err) {
88
- res.status(500).json({ error: err.message });
89
- }
90
- });
91
 
92
- // Inscription classique
93
- app.post("/register", async (req, res) => {
94
- try {
95
- const { name, email, password } = req.body;
96
- const existingUser = await User.findOne({ email });
97
- if (existingUser) return res.status(400).json({ message: "Email déjà utilisé" });
98
 
99
- const hashedPassword = await bcrypt.hash(password, 10);
100
- const newUser = await User.create({ name, email, password: hashedPassword });
 
 
101
 
102
- res.status(201).json({
103
- user: { _id: newUser._id, name: newUser.name, email: newUser.email, photoUrl: newUser.photoUrl }
104
- });
105
  } catch (err) {
106
- res.status(500).json({ message: err.message });
 
107
  }
108
  });
109
 
110
- // Connexion classique
111
- app.post("/login", async (req, res) => {
112
  try {
113
- const { email, password } = req.body;
114
  const user = await User.findOne({ email });
115
- if (!user || !user.password) return res.status(400).json({ message: "Email ou mot de passe incorrect" });
116
 
117
- const isMatch = await bcrypt.compare(password, user.password);
118
- if (!isMatch) return res.status(400).json({ message: "Email ou mot de passe incorrect" });
 
 
 
 
 
 
 
 
 
119
 
120
  res.json({
121
- user: { _id: user._id, name: user.name, email: user.email, photoUrl: user.photoUrl }
 
 
 
 
 
 
 
122
  });
123
  } catch (err) {
124
- res.status(500).json({ message: err.message });
 
125
  }
126
  });
127
 
128
- // Connexion Google (à appeler après OAuth réussi côté frontend)
129
- app.post("/auth/google", async (req, res) => {
130
- try {
131
- const { googleId, name, email, photoUrl } = req.body;
132
-
133
- let user = await User.findOne({ googleId });
134
- if (!user) {
135
- user = await User.findOne({ email });
136
- if (user) {
137
- // Compte existant avec email lier Google
138
- user.googleId = googleId;
139
- user.photoUrl = photoUrl;
140
- user.name = name;
141
- await user.save();
142
- } else {
143
- // Nouveau compte Google
144
- user = await User.create({
145
- googleId,
146
- name,
147
- email,
148
- photoUrl,
149
- password: null // pas de mot de passe local
150
- });
151
- }
152
- } else {
153
- // Mise à jour photo/nom si changé
154
- user.name = name;
155
- user.photoUrl = photoUrl;
156
- await user.save();
157
- }
158
 
159
- res.json({
160
- user: { _id: user._id, name: user.name, email: user.email, photoUrl: user.photoUrl }
161
- });
 
 
 
 
 
 
 
162
  } catch (err) {
163
  res.status(500).json({ message: err.message });
164
  }
165
  });
166
 
167
- // ... (routes markers inchangées)
168
- app.post("/markers", upload.fields([{ name: "photos", maxCount: 10 }, { name: "videos", maxCount: 10 }]), async (req, res) => {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
169
  try {
170
- const { latitude, longitude, title, comment, color, userId } = req.body;
171
  const photos = req.files.photos ? req.files.photos.map(f => `/uploads/${f.filename}`) : [];
172
  const videos = req.files.videos ? req.files.videos.map(f => `/uploads/${f.filename}`) : [];
173
 
174
- const newMarker = await Marker.create({
175
  latitude: parseFloat(latitude),
176
  longitude: parseFloat(longitude),
177
  title,
@@ -179,34 +217,38 @@ app.post("/markers", upload.fields([{ name: "photos", maxCount: 10 }, { name: "v
179
  color,
180
  photos,
181
  videos,
182
- createdBy: userId || null
183
  });
184
 
185
- io.emit("newMarker", newMarker);
186
- res.status(201).json(newMarker);
 
187
  } catch (err) {
188
- console.error("Erreur addMarker:", err);
189
  res.status(500).json({ message: err.message });
190
  }
191
  });
192
 
193
- app.patch("/markers/:id", upload.fields([{ name: "photos", maxCount: 10 }, { name: "videos", maxCount: 10 }]), async (req, res) => {
 
 
 
194
  try {
195
- const { title, comment, color } = req.body;
196
  const marker = await Marker.findById(req.params.id);
197
  if (!marker) return res.status(404).json({ message: "Marqueur non trouvé" });
198
 
199
- if (title !== undefined) marker.title = title;
200
- if (comment !== undefined) marker.comment = comment;
201
- if (color !== undefined) marker.color = color;
202
- if (req.files.photos) marker.photos = [...marker.photos, ...req.files.photos.map(f => `/uploads/${f.filename}`)];
203
- if (req.files.videos) marker.videos = [...marker.videos, ...req.files.videos.map(f => `/uploads/${f.filename}`)];
 
204
 
205
  await marker.save();
206
- io.emit("updatedMarker", marker);
207
- res.json(marker);
 
208
  } catch (err) {
209
- console.error("Erreur editMarker:", err);
210
  res.status(500).json({ message: err.message });
211
  }
212
  });
@@ -215,43 +257,39 @@ app.patch("/markers/:id", upload.fields([{ name: "photos", maxCount: 10 }, { nam
215
  // Socket.IO
216
  // =========================
217
  io.on("connection", (socket) => {
218
- console.log("Nouvel utilisateur connecté:", socket.id);
219
 
220
- const sendAllMarkers = async () => {
221
- try {
222
- const markers = await Marker.find();
223
- socket.emit("allMarkers", markers);
224
- } catch (err) {
225
- console.error("Erreur envoi marqueurs:", err);
226
- }
227
- };
228
- sendAllMarkers();
229
 
 
230
  socket.on("updatePosition", async ({ userId, latitude, longitude }) => {
231
- if (typeof latitude !== "number" || typeof longitude !== "number") return;
232
-
233
  try {
234
  const user = await User.findByIdAndUpdate(userId, { latitude, longitude }, { new: true });
235
  if (user) {
236
  io.emit("positionsUpdate", {
237
  userId: user._id,
238
  name: user.name,
239
- latitude: user.latitude,
240
- longitude: user.longitude
241
  });
242
  }
243
  } catch (err) {
244
- console.error("Erreur updatePosition:", err);
245
  }
246
  });
247
 
248
  socket.on("disconnect", () => {
249
- console.log("Utilisateur déconnecté:", socket.id);
250
  });
251
  });
252
 
253
  // =========================
254
- // Start Server
255
  // =========================
256
- const PORT = process.env.PORT || 5000;
257
- server.listen(PORT, () => console.log(`Server démarré sur le port ${PORT} 🚀`));
 
 
 
1
+ // server.js (ou index.js)
2
  import express from "express";
3
  import mongoose from "mongoose";
4
  import dotenv from "dotenv";
 
6
  import http from "http";
7
  import { Server } from "socket.io";
8
  import bcrypt from "bcrypt";
9
+ import jwt from "jsonwebtoken";
10
  import multer from "multer";
11
  import path from "path";
12
  import fs from "fs";
13
+ import crypto from "crypto";
14
 
15
  dotenv.config();
16
 
 
19
  const io = new Server(server, {
20
  cors: {
21
  origin: "*",
22
+ methods: ["GET", "POST", "PATCH"]
23
  }
24
  });
25
 
26
+ app.use(express.json({ limit: "50mb" }));
27
  app.use(cors());
28
+ app.use("/uploads", express.static(path.join(process.cwd(), "uploads")));
29
 
30
  if (!fs.existsSync("uploads")) {
31
  fs.mkdirSync("uploads");
32
  }
 
33
 
34
  const storage = multer.diskStorage({
35
  destination: (req, file, cb) => cb(null, "uploads/"),
 
42
  // =========================
43
  mongoose
44
  .connect(process.env.MONGO_URI)
45
+ .then(() => console.log("MongoDB connecté avec succès ✅"))
46
+ .catch(err => console.error("Erreur connexion MongoDB ❌:", err));
47
 
48
  // =========================
49
+ // Modèles
50
  // =========================
51
  const userSchema = new mongoose.Schema({
52
  name: { type: String, required: true },
53
+ email: { type: String, required: true, unique: true, lowercase: true },
54
+ password: { type: String }, // hashé ou null si OTP uniquement
 
 
55
  latitude: { type: Number, default: 0 },
56
  longitude: { type: Number, default: 0 },
57
+ otp: { type: String }, // OTP temporaire
58
+ otpExpires: { type: Date }, // expiration OTP
59
  createdAt: { type: Date, default: Date.now }
60
  });
61
 
62
  const User = mongoose.model("User", userSchema);
63
 
 
 
 
64
  const markerSchema = new mongoose.Schema({
65
  latitude: { type: Number, required: true },
66
  longitude: { type: Number, required: true },
 
69
  color: { type: String, default: "#ff0000" },
70
  photos: { type: [String], default: [] },
71
  videos: { type: [String], default: [] },
72
+ createdBy: { type: mongoose.Schema.Types.ObjectId, ref: "User" },
73
  createdAt: { type: Date, default: Date.now }
74
  });
75
  const Marker = mongoose.model("Marker", markerSchema);
76
 
77
  // =========================
78
+ // Helpers
79
  // =========================
80
+ const generateOTP = () => Math.floor(100000 + Math.random() * 900000).toString();
81
+
82
+ const sendOTPByEmail = async (email, otp) => {
83
+ // À intégrer avec Nodemailer, SMTP, ou service comme Brevo/SendGrid
84
+ console.log(`📧 OTP pour ${email} : ${otp}`); // Simulation en console
85
+ // Exemple avec Nodemailer (à décommenter et configurer)
86
+ /*
87
+ const transporter = nodemailer.createTransport({
88
+ host: "smtp.gmail.com",
89
+ port: 587,
90
+ auth: { user: "tonemail@gmail.com", pass: "app-password" }
91
+ });
92
+ await transporter.sendMail({
93
+ from: "Kibali <no-reply@kibali.ga>",
94
+ to: email,
95
+ subject: "Votre code OTP Kibali",
96
+ text: `Votre code de connexion est : ${otp}\nValable 10 minutes.`
97
+ });
98
+ */
99
+ };
100
 
101
+ // =========================
102
+ // Routes Auth avec OTP
103
+ // =========================
104
+ app.post("/auth/request-otp", async (req, res) => {
105
  try {
106
+ const { email, name } = req.body;
107
+ if (!email) return res.status(400).json({ message: "Email requis" });
 
 
 
 
108
 
109
+ let user = await User.findOne({ email });
110
+ if (!user) {
111
+ user = await User.create({ email, name: name || email.split("@")[0], password: null });
112
+ }
 
 
113
 
114
+ const otp = generateOTP();
115
+ user.otp = otp;
116
+ user.otpExpires = Date.now() + 10 * 60 * 1000; // 10 minutes
117
+ await user.save();
118
 
119
+ await sendOTPByEmail(email, otp);
120
+
121
+ res.json({ message: "OTP envoyé à votre email" });
122
  } catch (err) {
123
+ console.error(err);
124
+ res.status(500).json({ message: "Erreur serveur" });
125
  }
126
  });
127
 
128
+ app.post("/auth/verify-otp", async (req, res) => {
 
129
  try {
130
+ const { email, otp } = req.body;
131
  const user = await User.findOne({ email });
 
132
 
133
+ if (!user || !user.otp || user.otp !== otp || user.otpExpires < Date.now()) {
134
+ return res.status(400).json({ message: "OTP invalide ou expiré" });
135
+ }
136
+
137
+ // OTP valide → générer JWT
138
+ const token = jwt.sign({ userId: user._id }, process.env.JWT_SECRET, { expiresIn: "7d" });
139
+
140
+ // Nettoyer OTP
141
+ user.otp = undefined;
142
+ user.otpExpires = undefined;
143
+ await user.save();
144
 
145
  res.json({
146
+ token,
147
+ user: {
148
+ _id: user._id,
149
+ name: user.name,
150
+ email: user.email,
151
+ latitude: user.latitude,
152
+ longitude: user.longitude
153
+ }
154
  });
155
  } catch (err) {
156
+ console.error(err);
157
+ res.status(500).json({ message: "Erreur serveur" });
158
  }
159
  });
160
 
161
+ // =========================
162
+ // Routes protégées (vérification JWT)
163
+ // =========================
164
+ const authenticateToken = (req, res, next) => {
165
+ const authHeader = req.headers["authorization"];
166
+ const token = authHeader && authHeader.split(" ")[1];
167
+ if (!token) return res.status(401).json({ message: "Accès refusé" });
168
+
169
+ jwt.verify(token, process.env.JWT_SECRET, (err, payload) => {
170
+ if (err) return res.status(403).json({ message: "Token invalide" });
171
+ req.userId = payload.userId;
172
+ next();
173
+ });
174
+ };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
175
 
176
+ // Mise à jour position utilisateur
177
+ app.post("/update-position", authenticateToken, async (req, res) => {
178
+ try {
179
+ const { latitude, longitude } = req.body;
180
+ const user = await User.findByIdAndUpdate(
181
+ req.userId,
182
+ { latitude, longitude },
183
+ { new: true }
184
+ );
185
+ res.json({ latitude: user.latitude, longitude: user.longitude });
186
  } catch (err) {
187
  res.status(500).json({ message: err.message });
188
  }
189
  });
190
 
191
+ // =========================
192
+ // Routes Markers
193
+ // =========================
194
+ app.get("/markers", async (req, res) => {
195
+ try {
196
+ const markers = await Marker.find().populate("createdBy", "name email");
197
+ res.json(markers);
198
+ } catch (err) {
199
+ res.status(500).json({ error: err.message });
200
+ }
201
+ });
202
+
203
+ app.post("/markers", authenticateToken, upload.fields([
204
+ { name: "photos", maxCount: 10 },
205
+ { name: "videos", maxCount: 10 }
206
+ ]), async (req, res) => {
207
  try {
208
+ const { latitude, longitude, title, comment, color } = req.body;
209
  const photos = req.files.photos ? req.files.photos.map(f => `/uploads/${f.filename}`) : [];
210
  const videos = req.files.videos ? req.files.videos.map(f => `/uploads/${f.filename}`) : [];
211
 
212
+ const marker = await Marker.create({
213
  latitude: parseFloat(latitude),
214
  longitude: parseFloat(longitude),
215
  title,
 
217
  color,
218
  photos,
219
  videos,
220
+ createdBy: req.userId
221
  });
222
 
223
+ const populated = await marker.populate("createdBy", "name email");
224
+ io.emit("newMarker", populated);
225
+ res.status(201).json(populated);
226
  } catch (err) {
227
+ console.error(err);
228
  res.status(500).json({ message: err.message });
229
  }
230
  });
231
 
232
+ app.patch("/markers/:id", authenticateToken, upload.fields([
233
+ { name: "photos", maxCount: 10 },
234
+ { name: "videos", maxCount: 10 }
235
+ ]), async (req, res) => {
236
  try {
 
237
  const marker = await Marker.findById(req.params.id);
238
  if (!marker) return res.status(404).json({ message: "Marqueur non trouvé" });
239
 
240
+ const updates = req.body;
241
+ if (updates.title !== undefined) marker.title = updates.title;
242
+ if (updates.comment !== undefined) marker.comment = updates.comment;
243
+ if (updates.color !== undefined) marker.color = updates.color;
244
+ if (req.files.photos) marker.photos.push(...req.files.photos.map(f => `/uploads/${f.filename}`));
245
+ if (req.files.videos) marker.videos.push(...req.files.videos.map(f => `/uploads/${f.filename}`));
246
 
247
  await marker.save();
248
+ const populated = await marker.populate("createdBy", "name email");
249
+ io.emit("updatedMarker", populated);
250
+ res.json(populated);
251
  } catch (err) {
 
252
  res.status(500).json({ message: err.message });
253
  }
254
  });
 
257
  // Socket.IO
258
  // =========================
259
  io.on("connection", (socket) => {
260
+ console.log("Client connecté:", socket.id);
261
 
262
+ // Envoyer tous les marqueurs au nouveau client
263
+ Marker.find().populate("createdBy", "name email").then(markers => {
264
+ socket.emit("allMarkers", markers);
265
+ });
 
 
 
 
 
266
 
267
+ // Mise à jour position en temps réel
268
  socket.on("updatePosition", async ({ userId, latitude, longitude }) => {
 
 
269
  try {
270
  const user = await User.findByIdAndUpdate(userId, { latitude, longitude }, { new: true });
271
  if (user) {
272
  io.emit("positionsUpdate", {
273
  userId: user._id,
274
  name: user.name,
275
+ latitude,
276
+ longitude
277
  });
278
  }
279
  } catch (err) {
280
+ console.error("Erreur position:", err);
281
  }
282
  });
283
 
284
  socket.on("disconnect", () => {
285
+ console.log("Client déconnecté:", socket.id);
286
  });
287
  });
288
 
289
  // =========================
290
+ // Démarrage
291
  // =========================
292
+ const PORT = process.env.PORT || 8000;
293
+ server.listen(PORT, () => {
294
+ console.log(`🚀 Serveur Kibali démarré sur http://localhost:${PORT}`);
295
+ });
Dockerfile CHANGED
@@ -1,15 +1,36 @@
1
- FROM python:3.11-slim
 
 
 
 
 
 
2
 
 
 
3
  WORKDIR /app
4
 
5
- # Copier seulement ce qui est nécessaire
 
 
 
 
 
 
 
 
 
6
  COPY requirements.txt .
7
- RUN pip install --no-cache-dir -r requirements.txt
 
 
 
8
 
9
- COPY main.py app.py llm.py agent.py memory_faiss.py kibali_logo.svg ./
 
10
 
11
- # Exposer le port
12
  EXPOSE 8000
13
 
14
- # Lancer FastAPI
15
- CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
 
1
+ # --- STAGE 1 : Build du Frontend (Vite) ---
2
+ FROM node:18-alpine AS build-frontend
3
+ WORKDIR /app/frontend
4
+ COPY kibali-ui/package*.json ./
5
+ RUN npm install
6
+ COPY kibali-ui/ ./
7
+ RUN npm run build
8
 
9
+ # --- STAGE 2 : Backend + Serveur Statique ---
10
+ FROM nvidia/cuda:12.1.0-runtime-ubuntu22.04
11
  WORKDIR /app
12
 
13
+ # Installation de Python
14
+ RUN apt-get update && apt-get install -y \
15
+ python3-pip \
16
+ python3-dev \
17
+ && rm -rf /var/lib/apt/lists/*
18
+
19
+ # Installation de PyTorch
20
+ RUN pip3 install --no-cache-dir torch torchvision torchaudio --index-url https://download.pytorch.org/whl/cu121
21
+
22
+ # Installation des dépendances
23
  COPY requirements.txt .
24
+ RUN pip3 install --no-cache-dir -r requirements.txt
25
+
26
+ # On récupère le dossier 'dist' de Vite et on le renomme 'static'
27
+ COPY --from=build-frontend /app/frontend/dist ./static
28
 
29
+ # Copie du code Python
30
+ COPY . .
31
 
32
+ ENV PYTHONUNBUFFERED=1
33
  EXPOSE 8000
34
 
35
+ # Commande corrigée pour Ubuntu (python3)
36
+ CMD ["python3", "-m", "uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]
docker-compose.yml CHANGED
@@ -5,6 +5,7 @@ services:
5
  build: .
6
  image: kibali-engine:latest
7
  container_name: kibali-engine
 
8
  deploy:
9
  resources:
10
  reservations:
@@ -13,19 +14,12 @@ services:
13
  count: 1
14
  capabilities: [gpu]
15
  volumes:
16
- - /home/belikan/geoscan/agent_kibali:/app
 
 
 
 
17
  ports:
18
  - "8000:8000"
19
- command: python main.py
20
-
21
- kibali-ui:
22
- build: .
23
- image: kibali-ui:latest
24
- container_name: kibali-ui
25
- ports:
26
- - "8501:8501"
27
- depends_on:
28
- - kibali-engine
29
- volumes:
30
- - /home/belikan/geoscan/agent_kibali:/app
31
- command: streamlit run app.py --server.port 8501 --server.address 0.0.0.0
 
5
  build: .
6
  image: kibali-engine:latest
7
  container_name: kibali-engine
8
+ # Configuration GPU vitale pour l'IA
9
  deploy:
10
  resources:
11
  reservations:
 
14
  count: 1
15
  capabilities: [gpu]
16
  volumes:
17
+ # On ne monte que les dossiers de données persistantes
18
+ - ./model_cache:/app/model_cache
19
+ - ./data:/app/data
20
+ # On évite de monter tout le dossier /app pour ne pas écraser
21
+ # le build de Vite fait pendant le Dockerfile
22
  ports:
23
  - "8000:8000"
24
+ # Lancement de l'API avec uvicorn
25
+ command: python3 -m uvicorn main:app --host 0.0.0.0 --port 8000
 
 
 
 
 
 
 
 
 
 
 
kibali-ui/package-lock.json CHANGED
@@ -9,13 +9,22 @@
9
  "version": "0.0.0",
10
  "dependencies": {
11
  "axios": "^1.13.2",
 
 
 
 
12
  "firebase": "^12.7.0",
 
13
  "leaflet": "^1.9.4",
14
  "lucide-react": "^0.562.0",
 
 
15
  "react": "^19.2.0",
16
  "react-dom": "^19.2.0",
17
  "react-leaflet": "^5.0.0",
18
- "react-markdown": "^10.1.0"
 
 
19
  },
20
  "devDependencies": {
21
  "@eslint/js": "^9.39.1",
@@ -1654,6 +1663,15 @@
1654
  "@jridgewell/sourcemap-codec": "^1.4.14"
1655
  }
1656
  },
 
 
 
 
 
 
 
 
 
1657
  "node_modules/@protobufjs/aspromise": {
1658
  "version": "1.1.2",
1659
  "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
@@ -2044,6 +2062,12 @@
2044
  "win32"
2045
  ]
2046
  },
 
 
 
 
 
 
2047
  "node_modules/@types/babel__core": {
2048
  "version": "7.20.5",
2049
  "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
@@ -2089,6 +2113,15 @@
2089
  "@babel/types": "^7.28.2"
2090
  }
2091
  },
 
 
 
 
 
 
 
 
 
2092
  "node_modules/@types/debug": {
2093
  "version": "4.1.12",
2094
  "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
@@ -2178,6 +2211,21 @@
2178
  "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
2179
  "license": "MIT"
2180
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2181
  "node_modules/@ungap/structured-clone": {
2182
  "version": "1.3.0",
2183
  "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
@@ -2205,6 +2253,44 @@
2205
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
2206
  }
2207
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2208
  "node_modules/acorn": {
2209
  "version": "8.15.0",
2210
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
@@ -2269,6 +2355,12 @@
2269
  "url": "https://github.com/chalk/ansi-styles?sponsor=1"
2270
  }
2271
  },
 
 
 
 
 
 
2272
  "node_modules/argparse": {
2273
  "version": "2.0.1",
2274
  "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
@@ -2347,6 +2439,15 @@
2347
  "dev": true,
2348
  "license": "MIT"
2349
  },
 
 
 
 
 
 
 
 
 
2350
  "node_modules/baseline-browser-mapping": {
2351
  "version": "2.9.11",
2352
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
@@ -2357,6 +2458,44 @@
2357
  "baseline-browser-mapping": "dist/cli.js"
2358
  }
2359
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2360
  "node_modules/brace-expansion": {
2361
  "version": "1.1.12",
2362
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
@@ -2402,6 +2541,47 @@
2402
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
2403
  }
2404
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2405
  "node_modules/call-bind-apply-helpers": {
2406
  "version": "1.0.2",
2407
  "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
@@ -2415,6 +2595,22 @@
2415
  "node": ">= 0.4"
2416
  }
2417
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2418
  "node_modules/callsites": {
2419
  "version": "3.1.0",
2420
  "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
@@ -2574,6 +2770,43 @@
2574
  "dev": true,
2575
  "license": "MIT"
2576
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2577
  "node_modules/convert-source-map": {
2578
  "version": "2.0.0",
2579
  "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
@@ -2581,6 +2814,37 @@
2581
  "dev": true,
2582
  "license": "MIT"
2583
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2584
  "node_modules/cross-spawn": {
2585
  "version": "7.0.6",
2586
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
@@ -2648,6 +2912,15 @@
2648
  "node": ">=0.4.0"
2649
  }
2650
  },
 
 
 
 
 
 
 
 
 
2651
  "node_modules/dequal": {
2652
  "version": "2.0.3",
2653
  "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
@@ -2670,6 +2943,18 @@
2670
  "url": "https://github.com/sponsors/wooorm"
2671
  }
2672
  },
 
 
 
 
 
 
 
 
 
 
 
 
2673
  "node_modules/dunder-proto": {
2674
  "version": "1.0.1",
2675
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -2684,6 +2969,21 @@
2684
  "node": ">= 0.4"
2685
  }
2686
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2687
  "node_modules/electron-to-chromium": {
2688
  "version": "1.5.267",
2689
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
@@ -2697,6 +2997,79 @@
2697
  "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
2698
  "license": "MIT"
2699
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2700
  "node_modules/es-define-property": {
2701
  "version": "1.0.1",
2702
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
@@ -2793,6 +3166,12 @@
2793
  "node": ">=6"
2794
  }
2795
  },
 
 
 
 
 
 
2796
  "node_modules/escape-string-regexp": {
2797
  "version": "4.0.0",
2798
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
@@ -3000,6 +3379,83 @@
3000
  "node": ">=0.10.0"
3001
  }
3002
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3003
  "node_modules/extend": {
3004
  "version": "3.0.2",
3005
  "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
@@ -3070,6 +3526,27 @@
3070
  "node": ">=16.0.0"
3071
  }
3072
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3073
  "node_modules/find-up": {
3074
  "version": "5.0.0",
3075
  "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
@@ -3180,6 +3657,15 @@
3180
  "node": ">= 6"
3181
  }
3182
  },
 
 
 
 
 
 
 
 
 
3183
  "node_modules/fraction.js": {
3184
  "version": "5.3.4",
3185
  "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
@@ -3194,6 +3680,15 @@
3194
  "url": "https://github.com/sponsors/rawify"
3195
  }
3196
  },
 
 
 
 
 
 
 
 
 
3197
  "node_modules/fsevents": {
3198
  "version": "2.3.3",
3199
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
@@ -3428,17 +3923,53 @@
3428
  "url": "https://opencollective.com/unified"
3429
  }
3430
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3431
  "node_modules/http-parser-js": {
3432
  "version": "0.5.10",
3433
  "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
3434
  "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
3435
  "license": "MIT"
3436
  },
3437
- "node_modules/idb": {
3438
- "version": "7.1.1",
3439
- "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
3440
- "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
3441
- "license": "ISC"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3442
  },
3443
  "node_modules/ignore": {
3444
  "version": "5.3.2",
@@ -3477,12 +4008,27 @@
3477
  "node": ">=0.8.19"
3478
  }
3479
  },
 
 
 
 
 
 
3480
  "node_modules/inline-style-parser": {
3481
  "version": "0.2.7",
3482
  "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
3483
  "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
3484
  "license": "MIT"
3485
  },
 
 
 
 
 
 
 
 
 
3486
  "node_modules/is-alphabetical": {
3487
  "version": "2.0.1",
3488
  "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
@@ -3571,6 +4117,12 @@
3571
  "url": "https://github.com/sponsors/sindresorhus"
3572
  }
3573
  },
 
 
 
 
 
 
3574
  "node_modules/isexe": {
3575
  "version": "2.0.0",
3576
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
@@ -3645,6 +4197,70 @@
3645
  "node": ">=6"
3646
  }
3647
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3648
  "node_modules/keyv": {
3649
  "version": "4.5.4",
3650
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
@@ -3697,6 +4313,42 @@
3697
  "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
3698
  "license": "MIT"
3699
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3700
  "node_modules/lodash.merge": {
3701
  "version": "4.6.2",
3702
  "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
@@ -3704,6 +4356,12 @@
3704
  "dev": true,
3705
  "license": "MIT"
3706
  },
 
 
 
 
 
 
3707
  "node_modules/long": {
3708
  "version": "5.3.2",
3709
  "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
@@ -3901,6 +4559,33 @@
3901
  "url": "https://opencollective.com/unified"
3902
  }
3903
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
3904
  "node_modules/micromark": {
3905
  "version": "4.0.2",
3906
  "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
@@ -4377,12 +5062,171 @@
4377
  "node": "*"
4378
  }
4379
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4380
  "node_modules/ms": {
4381
  "version": "2.1.3",
4382
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
4383
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
4384
  "license": "MIT"
4385
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4386
  "node_modules/nanoid": {
4387
  "version": "3.3.11",
4388
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
@@ -4409,6 +5253,35 @@
4409
  "dev": true,
4410
  "license": "MIT"
4411
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4412
  "node_modules/node-releases": {
4413
  "version": "2.0.27",
4414
  "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
@@ -4416,6 +5289,48 @@
4416
  "dev": true,
4417
  "license": "MIT"
4418
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4419
  "node_modules/optionator": {
4420
  "version": "0.9.4",
4421
  "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
@@ -4504,6 +5419,15 @@
4504
  "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
4505
  "license": "MIT"
4506
  },
 
 
 
 
 
 
 
 
 
4507
  "node_modules/path-exists": {
4508
  "version": "4.0.0",
4509
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
@@ -4524,6 +5448,16 @@
4524
  "node": ">=8"
4525
  }
4526
  },
 
 
 
 
 
 
 
 
 
 
4527
  "node_modules/picocolors": {
4528
  "version": "1.1.1",
4529
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
@@ -4624,6 +5558,19 @@
4624
  "node": ">=12.0.0"
4625
  }
4626
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
4627
  "node_modules/proxy-from-env": {
4628
  "version": "1.1.0",
4629
  "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
@@ -4634,12 +5581,50 @@
4634
  "version": "2.3.1",
4635
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
4636
  "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
4637
- "dev": true,
4638
  "license": "MIT",
4639
  "engines": {
4640
  "node": ">=6"
4641
  }
4642
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4643
  "node_modules/react": {
4644
  "version": "19.2.3",
4645
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
@@ -4712,6 +5697,20 @@
4712
  "node": ">=0.10.0"
4713
  }
4714
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4715
  "node_modules/remark-parse": {
4716
  "version": "11.0.0",
4717
  "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
@@ -4806,6 +5805,22 @@
4806
  "fsevents": "~2.3.2"
4807
  }
4808
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4809
  "node_modules/safe-buffer": {
4810
  "version": "5.2.1",
4811
  "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
@@ -4826,6 +5841,12 @@
4826
  ],
4827
  "license": "MIT"
4828
  },
 
 
 
 
 
 
4829
  "node_modules/scheduler": {
4830
  "version": "0.27.0",
4831
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
@@ -4842,6 +5863,82 @@
4842
  "semver": "bin/semver.js"
4843
  }
4844
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4845
  "node_modules/shebang-command": {
4846
  "version": "2.0.0",
4847
  "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -4865,6 +5962,162 @@
4865
  "node": ">=8"
4866
  }
4867
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4868
  "node_modules/source-map-js": {
4869
  "version": "1.2.1",
4870
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
@@ -4885,6 +6138,41 @@
4885
  "url": "https://github.com/sponsors/wooorm"
4886
  }
4887
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4888
  "node_modules/string-width": {
4889
  "version": "4.2.3",
4890
  "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -4993,6 +6281,27 @@
4993
  "url": "https://github.com/sponsors/SuperchupuDev"
4994
  }
4995
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
4996
  "node_modules/trim-lines": {
4997
  "version": "3.0.1",
4998
  "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
@@ -5032,6 +6341,51 @@
5032
  "node": ">= 0.8.0"
5033
  }
5034
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5035
  "node_modules/undici-types": {
5036
  "version": "7.16.0",
5037
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
@@ -5125,6 +6479,15 @@
5125
  "url": "https://opencollective.com/unified"
5126
  }
5127
  },
 
 
 
 
 
 
 
 
 
5128
  "node_modules/update-browserslist-db": {
5129
  "version": "1.2.3",
5130
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
@@ -5166,6 +6529,21 @@
5166
  "punycode": "^2.1.0"
5167
  }
5168
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5169
  "node_modules/vfile": {
5170
  "version": "6.0.3",
5171
  "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
@@ -5275,6 +6653,15 @@
5275
  "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
5276
  "license": "Apache-2.0"
5277
  },
 
 
 
 
 
 
 
 
 
5278
  "node_modules/websocket-driver": {
5279
  "version": "0.7.4",
5280
  "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
@@ -5298,6 +6685,19 @@
5298
  "node": ">=0.8.0"
5299
  }
5300
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
5301
  "node_modules/which": {
5302
  "version": "2.0.2",
5303
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -5341,6 +6741,50 @@
5341
  "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
5342
  }
5343
  },
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
5344
  "node_modules/y18n": {
5345
  "version": "5.0.8",
5346
  "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
 
9
  "version": "0.0.0",
10
  "dependencies": {
11
  "axios": "^1.13.2",
12
+ "bcrypt": "^6.0.0",
13
+ "cors": "^2.8.5",
14
+ "dotenv": "^17.2.3",
15
+ "express": "^5.2.1",
16
  "firebase": "^12.7.0",
17
+ "jsonwebtoken": "^9.0.3",
18
  "leaflet": "^1.9.4",
19
  "lucide-react": "^0.562.0",
20
+ "mongoose": "^9.1.0",
21
+ "multer": "^2.0.2",
22
  "react": "^19.2.0",
23
  "react-dom": "^19.2.0",
24
  "react-leaflet": "^5.0.0",
25
+ "react-markdown": "^10.1.0",
26
+ "socket.io": "^4.8.3",
27
+ "socket.io-client": "^4.8.3"
28
  },
29
  "devDependencies": {
30
  "@eslint/js": "^9.39.1",
 
1663
  "@jridgewell/sourcemap-codec": "^1.4.14"
1664
  }
1665
  },
1666
+ "node_modules/@mongodb-js/saslprep": {
1667
+ "version": "1.4.4",
1668
+ "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.4.4.tgz",
1669
+ "integrity": "sha512-p7X/ytJDIdwUfFL/CLOhKgdfJe1Fa8uw9seJYvdOmnP9JBWGWHW69HkOixXS6Wy9yvGf1MbhcS6lVmrhy4jm2g==",
1670
+ "license": "MIT",
1671
+ "dependencies": {
1672
+ "sparse-bitfield": "^3.0.3"
1673
+ }
1674
+ },
1675
  "node_modules/@protobufjs/aspromise": {
1676
  "version": "1.1.2",
1677
  "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz",
 
2062
  "win32"
2063
  ]
2064
  },
2065
+ "node_modules/@socket.io/component-emitter": {
2066
+ "version": "3.1.2",
2067
+ "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz",
2068
+ "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==",
2069
+ "license": "MIT"
2070
+ },
2071
  "node_modules/@types/babel__core": {
2072
  "version": "7.20.5",
2073
  "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz",
 
2113
  "@babel/types": "^7.28.2"
2114
  }
2115
  },
2116
+ "node_modules/@types/cors": {
2117
+ "version": "2.8.19",
2118
+ "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.19.tgz",
2119
+ "integrity": "sha512-mFNylyeyqN93lfe/9CSxOGREz8cpzAhH+E93xJ4xWQf62V8sQ/24reV2nyzUWM6H6Xji+GGHpkbLe7pVoUEskg==",
2120
+ "license": "MIT",
2121
+ "dependencies": {
2122
+ "@types/node": "*"
2123
+ }
2124
+ },
2125
  "node_modules/@types/debug": {
2126
  "version": "4.1.12",
2127
  "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz",
 
2211
  "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==",
2212
  "license": "MIT"
2213
  },
2214
+ "node_modules/@types/webidl-conversions": {
2215
+ "version": "7.0.3",
2216
+ "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz",
2217
+ "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==",
2218
+ "license": "MIT"
2219
+ },
2220
+ "node_modules/@types/whatwg-url": {
2221
+ "version": "13.0.0",
2222
+ "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-13.0.0.tgz",
2223
+ "integrity": "sha512-N8WXpbE6Wgri7KUSvrmQcqrMllKZ9uxkYWMt+mCSGwNc0Hsw9VQTW7ApqI4XNrx6/SaM2QQJCzMPDEXE058s+Q==",
2224
+ "license": "MIT",
2225
+ "dependencies": {
2226
+ "@types/webidl-conversions": "*"
2227
+ }
2228
+ },
2229
  "node_modules/@ungap/structured-clone": {
2230
  "version": "1.3.0",
2231
  "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz",
 
2253
  "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0"
2254
  }
2255
  },
2256
+ "node_modules/accepts": {
2257
+ "version": "2.0.0",
2258
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-2.0.0.tgz",
2259
+ "integrity": "sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==",
2260
+ "license": "MIT",
2261
+ "dependencies": {
2262
+ "mime-types": "^3.0.0",
2263
+ "negotiator": "^1.0.0"
2264
+ },
2265
+ "engines": {
2266
+ "node": ">= 0.6"
2267
+ }
2268
+ },
2269
+ "node_modules/accepts/node_modules/mime-db": {
2270
+ "version": "1.54.0",
2271
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
2272
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
2273
+ "license": "MIT",
2274
+ "engines": {
2275
+ "node": ">= 0.6"
2276
+ }
2277
+ },
2278
+ "node_modules/accepts/node_modules/mime-types": {
2279
+ "version": "3.0.2",
2280
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
2281
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
2282
+ "license": "MIT",
2283
+ "dependencies": {
2284
+ "mime-db": "^1.54.0"
2285
+ },
2286
+ "engines": {
2287
+ "node": ">=18"
2288
+ },
2289
+ "funding": {
2290
+ "type": "opencollective",
2291
+ "url": "https://opencollective.com/express"
2292
+ }
2293
+ },
2294
  "node_modules/acorn": {
2295
  "version": "8.15.0",
2296
  "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz",
 
2355
  "url": "https://github.com/chalk/ansi-styles?sponsor=1"
2356
  }
2357
  },
2358
+ "node_modules/append-field": {
2359
+ "version": "1.0.0",
2360
+ "resolved": "https://registry.npmjs.org/append-field/-/append-field-1.0.0.tgz",
2361
+ "integrity": "sha512-klpgFSWLW1ZEs8svjfb7g4qWY0YS5imI82dTg+QahUvJ8YqAY0P10Uk8tTyh9ZGuYEZEMaeJYCF5BFuX552hsw==",
2362
+ "license": "MIT"
2363
+ },
2364
  "node_modules/argparse": {
2365
  "version": "2.0.1",
2366
  "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
 
2439
  "dev": true,
2440
  "license": "MIT"
2441
  },
2442
+ "node_modules/base64id": {
2443
+ "version": "2.0.0",
2444
+ "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz",
2445
+ "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==",
2446
+ "license": "MIT",
2447
+ "engines": {
2448
+ "node": "^4.5.0 || >= 5.9"
2449
+ }
2450
+ },
2451
  "node_modules/baseline-browser-mapping": {
2452
  "version": "2.9.11",
2453
  "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz",
 
2458
  "baseline-browser-mapping": "dist/cli.js"
2459
  }
2460
  },
2461
+ "node_modules/bcrypt": {
2462
+ "version": "6.0.0",
2463
+ "resolved": "https://registry.npmjs.org/bcrypt/-/bcrypt-6.0.0.tgz",
2464
+ "integrity": "sha512-cU8v/EGSrnH+HnxV2z0J7/blxH8gq7Xh2JFT6Aroax7UohdmiJJlxApMxtKfuI7z68NvvVcmR78k2LbT6efhRg==",
2465
+ "hasInstallScript": true,
2466
+ "license": "MIT",
2467
+ "dependencies": {
2468
+ "node-addon-api": "^8.3.0",
2469
+ "node-gyp-build": "^4.8.4"
2470
+ },
2471
+ "engines": {
2472
+ "node": ">= 18"
2473
+ }
2474
+ },
2475
+ "node_modules/body-parser": {
2476
+ "version": "2.2.1",
2477
+ "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz",
2478
+ "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==",
2479
+ "license": "MIT",
2480
+ "dependencies": {
2481
+ "bytes": "^3.1.2",
2482
+ "content-type": "^1.0.5",
2483
+ "debug": "^4.4.3",
2484
+ "http-errors": "^2.0.0",
2485
+ "iconv-lite": "^0.7.0",
2486
+ "on-finished": "^2.4.1",
2487
+ "qs": "^6.14.0",
2488
+ "raw-body": "^3.0.1",
2489
+ "type-is": "^2.0.1"
2490
+ },
2491
+ "engines": {
2492
+ "node": ">=18"
2493
+ },
2494
+ "funding": {
2495
+ "type": "opencollective",
2496
+ "url": "https://opencollective.com/express"
2497
+ }
2498
+ },
2499
  "node_modules/brace-expansion": {
2500
  "version": "1.1.12",
2501
  "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz",
 
2541
  "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7"
2542
  }
2543
  },
2544
+ "node_modules/bson": {
2545
+ "version": "7.0.0",
2546
+ "resolved": "https://registry.npmjs.org/bson/-/bson-7.0.0.tgz",
2547
+ "integrity": "sha512-Kwc6Wh4lQ5OmkqqKhYGKIuELXl+EPYSCObVE6bWsp1T/cGkOCBN0I8wF/T44BiuhHyNi1mmKVPXk60d41xZ7kw==",
2548
+ "license": "Apache-2.0",
2549
+ "engines": {
2550
+ "node": ">=20.19.0"
2551
+ }
2552
+ },
2553
+ "node_modules/buffer-equal-constant-time": {
2554
+ "version": "1.0.1",
2555
+ "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
2556
+ "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
2557
+ "license": "BSD-3-Clause"
2558
+ },
2559
+ "node_modules/buffer-from": {
2560
+ "version": "1.1.2",
2561
+ "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
2562
+ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
2563
+ "license": "MIT"
2564
+ },
2565
+ "node_modules/busboy": {
2566
+ "version": "1.6.0",
2567
+ "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz",
2568
+ "integrity": "sha512-8SFQbg/0hQ9xy3UNTB0YEnsNBbWfhf7RtnzpL7TkBiTBRfrQ9Fxcnz7VJsleJpyp6rVLvXiuORqjlHi5q+PYuA==",
2569
+ "dependencies": {
2570
+ "streamsearch": "^1.1.0"
2571
+ },
2572
+ "engines": {
2573
+ "node": ">=10.16.0"
2574
+ }
2575
+ },
2576
+ "node_modules/bytes": {
2577
+ "version": "3.1.2",
2578
+ "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz",
2579
+ "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==",
2580
+ "license": "MIT",
2581
+ "engines": {
2582
+ "node": ">= 0.8"
2583
+ }
2584
+ },
2585
  "node_modules/call-bind-apply-helpers": {
2586
  "version": "1.0.2",
2587
  "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz",
 
2595
  "node": ">= 0.4"
2596
  }
2597
  },
2598
+ "node_modules/call-bound": {
2599
+ "version": "1.0.4",
2600
+ "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz",
2601
+ "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==",
2602
+ "license": "MIT",
2603
+ "dependencies": {
2604
+ "call-bind-apply-helpers": "^1.0.2",
2605
+ "get-intrinsic": "^1.3.0"
2606
+ },
2607
+ "engines": {
2608
+ "node": ">= 0.4"
2609
+ },
2610
+ "funding": {
2611
+ "url": "https://github.com/sponsors/ljharb"
2612
+ }
2613
+ },
2614
  "node_modules/callsites": {
2615
  "version": "3.1.0",
2616
  "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz",
 
2770
  "dev": true,
2771
  "license": "MIT"
2772
  },
2773
+ "node_modules/concat-stream": {
2774
+ "version": "2.0.0",
2775
+ "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
2776
+ "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
2777
+ "engines": [
2778
+ "node >= 6.0"
2779
+ ],
2780
+ "license": "MIT",
2781
+ "dependencies": {
2782
+ "buffer-from": "^1.0.0",
2783
+ "inherits": "^2.0.3",
2784
+ "readable-stream": "^3.0.2",
2785
+ "typedarray": "^0.0.6"
2786
+ }
2787
+ },
2788
+ "node_modules/content-disposition": {
2789
+ "version": "1.0.1",
2790
+ "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-1.0.1.tgz",
2791
+ "integrity": "sha512-oIXISMynqSqm241k6kcQ5UwttDILMK4BiurCfGEREw6+X9jkkpEe5T9FZaApyLGGOnFuyMWZpdolTXMtvEJ08Q==",
2792
+ "license": "MIT",
2793
+ "engines": {
2794
+ "node": ">=18"
2795
+ },
2796
+ "funding": {
2797
+ "type": "opencollective",
2798
+ "url": "https://opencollective.com/express"
2799
+ }
2800
+ },
2801
+ "node_modules/content-type": {
2802
+ "version": "1.0.5",
2803
+ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz",
2804
+ "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==",
2805
+ "license": "MIT",
2806
+ "engines": {
2807
+ "node": ">= 0.6"
2808
+ }
2809
+ },
2810
  "node_modules/convert-source-map": {
2811
  "version": "2.0.0",
2812
  "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz",
 
2814
  "dev": true,
2815
  "license": "MIT"
2816
  },
2817
+ "node_modules/cookie": {
2818
+ "version": "0.7.2",
2819
+ "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz",
2820
+ "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==",
2821
+ "license": "MIT",
2822
+ "engines": {
2823
+ "node": ">= 0.6"
2824
+ }
2825
+ },
2826
+ "node_modules/cookie-signature": {
2827
+ "version": "1.2.2",
2828
+ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.2.2.tgz",
2829
+ "integrity": "sha512-D76uU73ulSXrD1UXF4KE2TMxVVwhsnCgfAyTg9k8P6KGZjlXKrOLe4dJQKI3Bxi5wjesZoFXJWElNWBjPZMbhg==",
2830
+ "license": "MIT",
2831
+ "engines": {
2832
+ "node": ">=6.6.0"
2833
+ }
2834
+ },
2835
+ "node_modules/cors": {
2836
+ "version": "2.8.5",
2837
+ "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz",
2838
+ "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==",
2839
+ "license": "MIT",
2840
+ "dependencies": {
2841
+ "object-assign": "^4",
2842
+ "vary": "^1"
2843
+ },
2844
+ "engines": {
2845
+ "node": ">= 0.10"
2846
+ }
2847
+ },
2848
  "node_modules/cross-spawn": {
2849
  "version": "7.0.6",
2850
  "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz",
 
2912
  "node": ">=0.4.0"
2913
  }
2914
  },
2915
+ "node_modules/depd": {
2916
+ "version": "2.0.0",
2917
+ "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
2918
+ "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==",
2919
+ "license": "MIT",
2920
+ "engines": {
2921
+ "node": ">= 0.8"
2922
+ }
2923
+ },
2924
  "node_modules/dequal": {
2925
  "version": "2.0.3",
2926
  "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz",
 
2943
  "url": "https://github.com/sponsors/wooorm"
2944
  }
2945
  },
2946
+ "node_modules/dotenv": {
2947
+ "version": "17.2.3",
2948
+ "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-17.2.3.tgz",
2949
+ "integrity": "sha512-JVUnt+DUIzu87TABbhPmNfVdBDt18BLOWjMUFJMSi/Qqg7NTYtabbvSNJGOJ7afbRuv9D/lngizHtP7QyLQ+9w==",
2950
+ "license": "BSD-2-Clause",
2951
+ "engines": {
2952
+ "node": ">=12"
2953
+ },
2954
+ "funding": {
2955
+ "url": "https://dotenvx.com"
2956
+ }
2957
+ },
2958
  "node_modules/dunder-proto": {
2959
  "version": "1.0.1",
2960
  "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
 
2969
  "node": ">= 0.4"
2970
  }
2971
  },
2972
+ "node_modules/ecdsa-sig-formatter": {
2973
+ "version": "1.0.11",
2974
+ "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
2975
+ "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
2976
+ "license": "Apache-2.0",
2977
+ "dependencies": {
2978
+ "safe-buffer": "^5.0.1"
2979
+ }
2980
+ },
2981
+ "node_modules/ee-first": {
2982
+ "version": "1.1.1",
2983
+ "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz",
2984
+ "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==",
2985
+ "license": "MIT"
2986
+ },
2987
  "node_modules/electron-to-chromium": {
2988
  "version": "1.5.267",
2989
  "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz",
 
2997
  "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==",
2998
  "license": "MIT"
2999
  },
3000
+ "node_modules/encodeurl": {
3001
+ "version": "2.0.0",
3002
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
3003
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
3004
+ "license": "MIT",
3005
+ "engines": {
3006
+ "node": ">= 0.8"
3007
+ }
3008
+ },
3009
+ "node_modules/engine.io": {
3010
+ "version": "6.6.5",
3011
+ "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.5.tgz",
3012
+ "integrity": "sha512-2RZdgEbXmp5+dVbRm0P7HQUImZpICccJy7rN7Tv+SFa55pH+lxnuw6/K1ZxxBfHoYpSkHLAO92oa8O4SwFXA2A==",
3013
+ "license": "MIT",
3014
+ "dependencies": {
3015
+ "@types/cors": "^2.8.12",
3016
+ "@types/node": ">=10.0.0",
3017
+ "accepts": "~1.3.4",
3018
+ "base64id": "2.0.0",
3019
+ "cookie": "~0.7.2",
3020
+ "cors": "~2.8.5",
3021
+ "debug": "~4.4.1",
3022
+ "engine.io-parser": "~5.2.1",
3023
+ "ws": "~8.18.3"
3024
+ },
3025
+ "engines": {
3026
+ "node": ">=10.2.0"
3027
+ }
3028
+ },
3029
+ "node_modules/engine.io-client": {
3030
+ "version": "6.6.4",
3031
+ "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.6.4.tgz",
3032
+ "integrity": "sha512-+kjUJnZGwzewFDw951CDWcwj35vMNf2fcj7xQWOctq1F2i1jkDdVvdFG9kM/BEChymCH36KgjnW0NsL58JYRxw==",
3033
+ "license": "MIT",
3034
+ "dependencies": {
3035
+ "@socket.io/component-emitter": "~3.1.0",
3036
+ "debug": "~4.4.1",
3037
+ "engine.io-parser": "~5.2.1",
3038
+ "ws": "~8.18.3",
3039
+ "xmlhttprequest-ssl": "~2.1.1"
3040
+ }
3041
+ },
3042
+ "node_modules/engine.io-parser": {
3043
+ "version": "5.2.3",
3044
+ "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz",
3045
+ "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==",
3046
+ "license": "MIT",
3047
+ "engines": {
3048
+ "node": ">=10.0.0"
3049
+ }
3050
+ },
3051
+ "node_modules/engine.io/node_modules/accepts": {
3052
+ "version": "1.3.8",
3053
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
3054
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
3055
+ "license": "MIT",
3056
+ "dependencies": {
3057
+ "mime-types": "~2.1.34",
3058
+ "negotiator": "0.6.3"
3059
+ },
3060
+ "engines": {
3061
+ "node": ">= 0.6"
3062
+ }
3063
+ },
3064
+ "node_modules/engine.io/node_modules/negotiator": {
3065
+ "version": "0.6.3",
3066
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
3067
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
3068
+ "license": "MIT",
3069
+ "engines": {
3070
+ "node": ">= 0.6"
3071
+ }
3072
+ },
3073
  "node_modules/es-define-property": {
3074
  "version": "1.0.1",
3075
  "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz",
 
3166
  "node": ">=6"
3167
  }
3168
  },
3169
+ "node_modules/escape-html": {
3170
+ "version": "1.0.3",
3171
+ "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
3172
+ "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==",
3173
+ "license": "MIT"
3174
+ },
3175
  "node_modules/escape-string-regexp": {
3176
  "version": "4.0.0",
3177
  "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz",
 
3379
  "node": ">=0.10.0"
3380
  }
3381
  },
3382
+ "node_modules/etag": {
3383
+ "version": "1.8.1",
3384
+ "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz",
3385
+ "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==",
3386
+ "license": "MIT",
3387
+ "engines": {
3388
+ "node": ">= 0.6"
3389
+ }
3390
+ },
3391
+ "node_modules/express": {
3392
+ "version": "5.2.1",
3393
+ "resolved": "https://registry.npmjs.org/express/-/express-5.2.1.tgz",
3394
+ "integrity": "sha512-hIS4idWWai69NezIdRt2xFVofaF4j+6INOpJlVOLDO8zXGpUVEVzIYk12UUi2JzjEzWL3IOAxcTubgz9Po0yXw==",
3395
+ "license": "MIT",
3396
+ "dependencies": {
3397
+ "accepts": "^2.0.0",
3398
+ "body-parser": "^2.2.1",
3399
+ "content-disposition": "^1.0.0",
3400
+ "content-type": "^1.0.5",
3401
+ "cookie": "^0.7.1",
3402
+ "cookie-signature": "^1.2.1",
3403
+ "debug": "^4.4.0",
3404
+ "depd": "^2.0.0",
3405
+ "encodeurl": "^2.0.0",
3406
+ "escape-html": "^1.0.3",
3407
+ "etag": "^1.8.1",
3408
+ "finalhandler": "^2.1.0",
3409
+ "fresh": "^2.0.0",
3410
+ "http-errors": "^2.0.0",
3411
+ "merge-descriptors": "^2.0.0",
3412
+ "mime-types": "^3.0.0",
3413
+ "on-finished": "^2.4.1",
3414
+ "once": "^1.4.0",
3415
+ "parseurl": "^1.3.3",
3416
+ "proxy-addr": "^2.0.7",
3417
+ "qs": "^6.14.0",
3418
+ "range-parser": "^1.2.1",
3419
+ "router": "^2.2.0",
3420
+ "send": "^1.1.0",
3421
+ "serve-static": "^2.2.0",
3422
+ "statuses": "^2.0.1",
3423
+ "type-is": "^2.0.1",
3424
+ "vary": "^1.1.2"
3425
+ },
3426
+ "engines": {
3427
+ "node": ">= 18"
3428
+ },
3429
+ "funding": {
3430
+ "type": "opencollective",
3431
+ "url": "https://opencollective.com/express"
3432
+ }
3433
+ },
3434
+ "node_modules/express/node_modules/mime-db": {
3435
+ "version": "1.54.0",
3436
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
3437
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
3438
+ "license": "MIT",
3439
+ "engines": {
3440
+ "node": ">= 0.6"
3441
+ }
3442
+ },
3443
+ "node_modules/express/node_modules/mime-types": {
3444
+ "version": "3.0.2",
3445
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
3446
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
3447
+ "license": "MIT",
3448
+ "dependencies": {
3449
+ "mime-db": "^1.54.0"
3450
+ },
3451
+ "engines": {
3452
+ "node": ">=18"
3453
+ },
3454
+ "funding": {
3455
+ "type": "opencollective",
3456
+ "url": "https://opencollective.com/express"
3457
+ }
3458
+ },
3459
  "node_modules/extend": {
3460
  "version": "3.0.2",
3461
  "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
 
3526
  "node": ">=16.0.0"
3527
  }
3528
  },
3529
+ "node_modules/finalhandler": {
3530
+ "version": "2.1.1",
3531
+ "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-2.1.1.tgz",
3532
+ "integrity": "sha512-S8KoZgRZN+a5rNwqTxlZZePjT/4cnm0ROV70LedRHZ0p8u9fRID0hJUZQpkKLzro8LfmC8sx23bY6tVNxv8pQA==",
3533
+ "license": "MIT",
3534
+ "dependencies": {
3535
+ "debug": "^4.4.0",
3536
+ "encodeurl": "^2.0.0",
3537
+ "escape-html": "^1.0.3",
3538
+ "on-finished": "^2.4.1",
3539
+ "parseurl": "^1.3.3",
3540
+ "statuses": "^2.0.1"
3541
+ },
3542
+ "engines": {
3543
+ "node": ">= 18.0.0"
3544
+ },
3545
+ "funding": {
3546
+ "type": "opencollective",
3547
+ "url": "https://opencollective.com/express"
3548
+ }
3549
+ },
3550
  "node_modules/find-up": {
3551
  "version": "5.0.0",
3552
  "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz",
 
3657
  "node": ">= 6"
3658
  }
3659
  },
3660
+ "node_modules/forwarded": {
3661
+ "version": "0.2.0",
3662
+ "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
3663
+ "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==",
3664
+ "license": "MIT",
3665
+ "engines": {
3666
+ "node": ">= 0.6"
3667
+ }
3668
+ },
3669
  "node_modules/fraction.js": {
3670
  "version": "5.3.4",
3671
  "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-5.3.4.tgz",
 
3680
  "url": "https://github.com/sponsors/rawify"
3681
  }
3682
  },
3683
+ "node_modules/fresh": {
3684
+ "version": "2.0.0",
3685
+ "resolved": "https://registry.npmjs.org/fresh/-/fresh-2.0.0.tgz",
3686
+ "integrity": "sha512-Rx/WycZ60HOaqLKAi6cHRKKI7zxWbJ31MhntmtwMoaTeF7XFH9hhBp8vITaMidfljRQ6eYWCKkaTK+ykVJHP2A==",
3687
+ "license": "MIT",
3688
+ "engines": {
3689
+ "node": ">= 0.8"
3690
+ }
3691
+ },
3692
  "node_modules/fsevents": {
3693
  "version": "2.3.3",
3694
  "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
 
3923
  "url": "https://opencollective.com/unified"
3924
  }
3925
  },
3926
+ "node_modules/http-errors": {
3927
+ "version": "2.0.1",
3928
+ "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.1.tgz",
3929
+ "integrity": "sha512-4FbRdAX+bSdmo4AUFuS0WNiPz8NgFt+r8ThgNWmlrjQjt1Q7ZR9+zTlce2859x4KSXrwIsaeTqDoKQmtP8pLmQ==",
3930
+ "license": "MIT",
3931
+ "dependencies": {
3932
+ "depd": "~2.0.0",
3933
+ "inherits": "~2.0.4",
3934
+ "setprototypeof": "~1.2.0",
3935
+ "statuses": "~2.0.2",
3936
+ "toidentifier": "~1.0.1"
3937
+ },
3938
+ "engines": {
3939
+ "node": ">= 0.8"
3940
+ },
3941
+ "funding": {
3942
+ "type": "opencollective",
3943
+ "url": "https://opencollective.com/express"
3944
+ }
3945
+ },
3946
  "node_modules/http-parser-js": {
3947
  "version": "0.5.10",
3948
  "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz",
3949
  "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==",
3950
  "license": "MIT"
3951
  },
3952
+ "node_modules/iconv-lite": {
3953
+ "version": "0.7.1",
3954
+ "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.7.1.tgz",
3955
+ "integrity": "sha512-2Tth85cXwGFHfvRgZWszZSvdo+0Xsqmw8k8ZwxScfcBneNUraK+dxRxRm24nszx80Y0TVio8kKLt5sLE7ZCLlw==",
3956
+ "license": "MIT",
3957
+ "dependencies": {
3958
+ "safer-buffer": ">= 2.1.2 < 3.0.0"
3959
+ },
3960
+ "engines": {
3961
+ "node": ">=0.10.0"
3962
+ },
3963
+ "funding": {
3964
+ "type": "opencollective",
3965
+ "url": "https://opencollective.com/express"
3966
+ }
3967
+ },
3968
+ "node_modules/idb": {
3969
+ "version": "7.1.1",
3970
+ "resolved": "https://registry.npmjs.org/idb/-/idb-7.1.1.tgz",
3971
+ "integrity": "sha512-gchesWBzyvGHRO9W8tzUWFDycow5gwjvFKfyV9FF32Y7F50yZMp7mP+T2mJIWFx49zicqyC4uefHM17o6xKIVQ==",
3972
+ "license": "ISC"
3973
  },
3974
  "node_modules/ignore": {
3975
  "version": "5.3.2",
 
4008
  "node": ">=0.8.19"
4009
  }
4010
  },
4011
+ "node_modules/inherits": {
4012
+ "version": "2.0.4",
4013
+ "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz",
4014
+ "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==",
4015
+ "license": "ISC"
4016
+ },
4017
  "node_modules/inline-style-parser": {
4018
  "version": "0.2.7",
4019
  "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz",
4020
  "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==",
4021
  "license": "MIT"
4022
  },
4023
+ "node_modules/ipaddr.js": {
4024
+ "version": "1.9.1",
4025
+ "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz",
4026
+ "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==",
4027
+ "license": "MIT",
4028
+ "engines": {
4029
+ "node": ">= 0.10"
4030
+ }
4031
+ },
4032
  "node_modules/is-alphabetical": {
4033
  "version": "2.0.1",
4034
  "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz",
 
4117
  "url": "https://github.com/sponsors/sindresorhus"
4118
  }
4119
  },
4120
+ "node_modules/is-promise": {
4121
+ "version": "4.0.0",
4122
+ "resolved": "https://registry.npmjs.org/is-promise/-/is-promise-4.0.0.tgz",
4123
+ "integrity": "sha512-hvpoI6korhJMnej285dSg6nu1+e6uxs7zG3BYAm5byqDsgJNWwxzM6z6iZiAgQR4TJ30JmBTOwqZUw3WlyH3AQ==",
4124
+ "license": "MIT"
4125
+ },
4126
  "node_modules/isexe": {
4127
  "version": "2.0.0",
4128
  "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz",
 
4197
  "node": ">=6"
4198
  }
4199
  },
4200
+ "node_modules/jsonwebtoken": {
4201
+ "version": "9.0.3",
4202
+ "resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
4203
+ "integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
4204
+ "license": "MIT",
4205
+ "dependencies": {
4206
+ "jws": "^4.0.1",
4207
+ "lodash.includes": "^4.3.0",
4208
+ "lodash.isboolean": "^3.0.3",
4209
+ "lodash.isinteger": "^4.0.4",
4210
+ "lodash.isnumber": "^3.0.3",
4211
+ "lodash.isplainobject": "^4.0.6",
4212
+ "lodash.isstring": "^4.0.1",
4213
+ "lodash.once": "^4.0.0",
4214
+ "ms": "^2.1.1",
4215
+ "semver": "^7.5.4"
4216
+ },
4217
+ "engines": {
4218
+ "node": ">=12",
4219
+ "npm": ">=6"
4220
+ }
4221
+ },
4222
+ "node_modules/jsonwebtoken/node_modules/semver": {
4223
+ "version": "7.7.3",
4224
+ "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.3.tgz",
4225
+ "integrity": "sha512-SdsKMrI9TdgjdweUSR9MweHA4EJ8YxHn8DFaDisvhVlUOe4BF1tLD7GAj0lIqWVl+dPb/rExr0Btby5loQm20Q==",
4226
+ "license": "ISC",
4227
+ "bin": {
4228
+ "semver": "bin/semver.js"
4229
+ },
4230
+ "engines": {
4231
+ "node": ">=10"
4232
+ }
4233
+ },
4234
+ "node_modules/jwa": {
4235
+ "version": "2.0.1",
4236
+ "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
4237
+ "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
4238
+ "license": "MIT",
4239
+ "dependencies": {
4240
+ "buffer-equal-constant-time": "^1.0.1",
4241
+ "ecdsa-sig-formatter": "1.0.11",
4242
+ "safe-buffer": "^5.0.1"
4243
+ }
4244
+ },
4245
+ "node_modules/jws": {
4246
+ "version": "4.0.1",
4247
+ "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
4248
+ "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
4249
+ "license": "MIT",
4250
+ "dependencies": {
4251
+ "jwa": "^2.0.1",
4252
+ "safe-buffer": "^5.0.1"
4253
+ }
4254
+ },
4255
+ "node_modules/kareem": {
4256
+ "version": "3.0.0",
4257
+ "resolved": "https://registry.npmjs.org/kareem/-/kareem-3.0.0.tgz",
4258
+ "integrity": "sha512-RKhaOBSPN8L7y4yAgNhDT2602G5FD6QbOIISbjN9D6mjHPeqeg7K+EB5IGSU5o81/X2Gzm3ICnAvQW3x3OP8HA==",
4259
+ "license": "Apache-2.0",
4260
+ "engines": {
4261
+ "node": ">=18.0.0"
4262
+ }
4263
+ },
4264
  "node_modules/keyv": {
4265
  "version": "4.5.4",
4266
  "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz",
 
4313
  "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
4314
  "license": "MIT"
4315
  },
4316
+ "node_modules/lodash.includes": {
4317
+ "version": "4.3.0",
4318
+ "resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
4319
+ "integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
4320
+ "license": "MIT"
4321
+ },
4322
+ "node_modules/lodash.isboolean": {
4323
+ "version": "3.0.3",
4324
+ "resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
4325
+ "integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
4326
+ "license": "MIT"
4327
+ },
4328
+ "node_modules/lodash.isinteger": {
4329
+ "version": "4.0.4",
4330
+ "resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
4331
+ "integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
4332
+ "license": "MIT"
4333
+ },
4334
+ "node_modules/lodash.isnumber": {
4335
+ "version": "3.0.3",
4336
+ "resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
4337
+ "integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
4338
+ "license": "MIT"
4339
+ },
4340
+ "node_modules/lodash.isplainobject": {
4341
+ "version": "4.0.6",
4342
+ "resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
4343
+ "integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
4344
+ "license": "MIT"
4345
+ },
4346
+ "node_modules/lodash.isstring": {
4347
+ "version": "4.0.1",
4348
+ "resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
4349
+ "integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
4350
+ "license": "MIT"
4351
+ },
4352
  "node_modules/lodash.merge": {
4353
  "version": "4.6.2",
4354
  "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz",
 
4356
  "dev": true,
4357
  "license": "MIT"
4358
  },
4359
+ "node_modules/lodash.once": {
4360
+ "version": "4.1.1",
4361
+ "resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
4362
+ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
4363
+ "license": "MIT"
4364
+ },
4365
  "node_modules/long": {
4366
  "version": "5.3.2",
4367
  "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz",
 
4559
  "url": "https://opencollective.com/unified"
4560
  }
4561
  },
4562
+ "node_modules/media-typer": {
4563
+ "version": "1.1.0",
4564
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-1.1.0.tgz",
4565
+ "integrity": "sha512-aisnrDP4GNe06UcKFnV5bfMNPBUw4jsLGaWwWfnH3v02GnBuXX2MCVn5RbrWo0j3pczUilYblq7fQ7Nw2t5XKw==",
4566
+ "license": "MIT",
4567
+ "engines": {
4568
+ "node": ">= 0.8"
4569
+ }
4570
+ },
4571
+ "node_modules/memory-pager": {
4572
+ "version": "1.5.0",
4573
+ "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz",
4574
+ "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==",
4575
+ "license": "MIT"
4576
+ },
4577
+ "node_modules/merge-descriptors": {
4578
+ "version": "2.0.0",
4579
+ "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-2.0.0.tgz",
4580
+ "integrity": "sha512-Snk314V5ayFLhp3fkUREub6WtjBfPdCPY1Ln8/8munuLuiYhsABgBVWsozAG+MWMbVEvcdcpbi9R7ww22l9Q3g==",
4581
+ "license": "MIT",
4582
+ "engines": {
4583
+ "node": ">=18"
4584
+ },
4585
+ "funding": {
4586
+ "url": "https://github.com/sponsors/sindresorhus"
4587
+ }
4588
+ },
4589
  "node_modules/micromark": {
4590
  "version": "4.0.2",
4591
  "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz",
 
5062
  "node": "*"
5063
  }
5064
  },
5065
+ "node_modules/minimist": {
5066
+ "version": "1.2.8",
5067
+ "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz",
5068
+ "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==",
5069
+ "license": "MIT",
5070
+ "funding": {
5071
+ "url": "https://github.com/sponsors/ljharb"
5072
+ }
5073
+ },
5074
+ "node_modules/mkdirp": {
5075
+ "version": "0.5.6",
5076
+ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz",
5077
+ "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==",
5078
+ "license": "MIT",
5079
+ "dependencies": {
5080
+ "minimist": "^1.2.6"
5081
+ },
5082
+ "bin": {
5083
+ "mkdirp": "bin/cmd.js"
5084
+ }
5085
+ },
5086
+ "node_modules/mongodb": {
5087
+ "version": "7.0.0",
5088
+ "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-7.0.0.tgz",
5089
+ "integrity": "sha512-vG/A5cQrvGGvZm2mTnCSz1LUcbOPl83hfB6bxULKQ8oFZauyox/2xbZOoGNl+64m8VBrETkdGCDBdOsCr3F3jg==",
5090
+ "license": "Apache-2.0",
5091
+ "dependencies": {
5092
+ "@mongodb-js/saslprep": "^1.3.0",
5093
+ "bson": "^7.0.0",
5094
+ "mongodb-connection-string-url": "^7.0.0"
5095
+ },
5096
+ "engines": {
5097
+ "node": ">=20.19.0"
5098
+ },
5099
+ "peerDependencies": {
5100
+ "@aws-sdk/credential-providers": "^3.806.0",
5101
+ "@mongodb-js/zstd": "^7.0.0",
5102
+ "gcp-metadata": "^7.0.1",
5103
+ "kerberos": "^7.0.0",
5104
+ "mongodb-client-encryption": ">=7.0.0 <7.1.0",
5105
+ "snappy": "^7.3.2",
5106
+ "socks": "^2.8.6"
5107
+ },
5108
+ "peerDependenciesMeta": {
5109
+ "@aws-sdk/credential-providers": {
5110
+ "optional": true
5111
+ },
5112
+ "@mongodb-js/zstd": {
5113
+ "optional": true
5114
+ },
5115
+ "gcp-metadata": {
5116
+ "optional": true
5117
+ },
5118
+ "kerberos": {
5119
+ "optional": true
5120
+ },
5121
+ "mongodb-client-encryption": {
5122
+ "optional": true
5123
+ },
5124
+ "snappy": {
5125
+ "optional": true
5126
+ },
5127
+ "socks": {
5128
+ "optional": true
5129
+ }
5130
+ }
5131
+ },
5132
+ "node_modules/mongodb-connection-string-url": {
5133
+ "version": "7.0.0",
5134
+ "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-7.0.0.tgz",
5135
+ "integrity": "sha512-irhhjRVLE20hbkRl4zpAYLnDMM+zIZnp0IDB9akAFFUZp/3XdOfwwddc7y6cNvF2WCEtfTYRwYbIfYa2kVY0og==",
5136
+ "license": "Apache-2.0",
5137
+ "dependencies": {
5138
+ "@types/whatwg-url": "^13.0.0",
5139
+ "whatwg-url": "^14.1.0"
5140
+ },
5141
+ "engines": {
5142
+ "node": ">=20.19.0"
5143
+ }
5144
+ },
5145
+ "node_modules/mongoose": {
5146
+ "version": "9.1.0",
5147
+ "resolved": "https://registry.npmjs.org/mongoose/-/mongoose-9.1.0.tgz",
5148
+ "integrity": "sha512-RVCApwqD6q+O3rsnypmiL1K5+mkN5DwA7BO5a5ofCKh/EZB9FKvcQ4EiqHNmRye3cXhz5DmQ/aVyfBFkXnUbrg==",
5149
+ "license": "MIT",
5150
+ "dependencies": {
5151
+ "kareem": "3.0.0",
5152
+ "mongodb": "~7.0",
5153
+ "mpath": "0.9.0",
5154
+ "mquery": "6.0.0",
5155
+ "ms": "2.1.3",
5156
+ "sift": "17.1.3"
5157
+ },
5158
+ "engines": {
5159
+ "node": ">=20.19.0"
5160
+ },
5161
+ "funding": {
5162
+ "type": "opencollective",
5163
+ "url": "https://opencollective.com/mongoose"
5164
+ }
5165
+ },
5166
+ "node_modules/mpath": {
5167
+ "version": "0.9.0",
5168
+ "resolved": "https://registry.npmjs.org/mpath/-/mpath-0.9.0.tgz",
5169
+ "integrity": "sha512-ikJRQTk8hw5DEoFVxHG1Gn9T/xcjtdnOKIU1JTmGjZZlg9LST2mBLmcX3/ICIbgJydT2GOc15RnNy5mHmzfSew==",
5170
+ "license": "MIT",
5171
+ "engines": {
5172
+ "node": ">=4.0.0"
5173
+ }
5174
+ },
5175
+ "node_modules/mquery": {
5176
+ "version": "6.0.0",
5177
+ "resolved": "https://registry.npmjs.org/mquery/-/mquery-6.0.0.tgz",
5178
+ "integrity": "sha512-b2KQNsmgtkscfeDgkYMcWGn9vZI9YoXh802VDEwE6qc50zxBFQ0Oo8ROkawbPAsXCY1/Z1yp0MagqsZStPWJjw==",
5179
+ "license": "MIT",
5180
+ "engines": {
5181
+ "node": ">=20.19.0"
5182
+ }
5183
+ },
5184
  "node_modules/ms": {
5185
  "version": "2.1.3",
5186
  "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
5187
  "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
5188
  "license": "MIT"
5189
  },
5190
+ "node_modules/multer": {
5191
+ "version": "2.0.2",
5192
+ "resolved": "https://registry.npmjs.org/multer/-/multer-2.0.2.tgz",
5193
+ "integrity": "sha512-u7f2xaZ/UG8oLXHvtF/oWTRvT44p9ecwBBqTwgJVq0+4BW1g8OW01TyMEGWBHbyMOYVHXslaut7qEQ1meATXgw==",
5194
+ "license": "MIT",
5195
+ "dependencies": {
5196
+ "append-field": "^1.0.0",
5197
+ "busboy": "^1.6.0",
5198
+ "concat-stream": "^2.0.0",
5199
+ "mkdirp": "^0.5.6",
5200
+ "object-assign": "^4.1.1",
5201
+ "type-is": "^1.6.18",
5202
+ "xtend": "^4.0.2"
5203
+ },
5204
+ "engines": {
5205
+ "node": ">= 10.16.0"
5206
+ }
5207
+ },
5208
+ "node_modules/multer/node_modules/media-typer": {
5209
+ "version": "0.3.0",
5210
+ "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz",
5211
+ "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==",
5212
+ "license": "MIT",
5213
+ "engines": {
5214
+ "node": ">= 0.6"
5215
+ }
5216
+ },
5217
+ "node_modules/multer/node_modules/type-is": {
5218
+ "version": "1.6.18",
5219
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz",
5220
+ "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==",
5221
+ "license": "MIT",
5222
+ "dependencies": {
5223
+ "media-typer": "0.3.0",
5224
+ "mime-types": "~2.1.24"
5225
+ },
5226
+ "engines": {
5227
+ "node": ">= 0.6"
5228
+ }
5229
+ },
5230
  "node_modules/nanoid": {
5231
  "version": "3.3.11",
5232
  "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz",
 
5253
  "dev": true,
5254
  "license": "MIT"
5255
  },
5256
+ "node_modules/negotiator": {
5257
+ "version": "1.0.0",
5258
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz",
5259
+ "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==",
5260
+ "license": "MIT",
5261
+ "engines": {
5262
+ "node": ">= 0.6"
5263
+ }
5264
+ },
5265
+ "node_modules/node-addon-api": {
5266
+ "version": "8.5.0",
5267
+ "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-8.5.0.tgz",
5268
+ "integrity": "sha512-/bRZty2mXUIFY/xU5HLvveNHlswNJej+RnxBjOMkidWfwZzgTbPG1E3K5TOxRLOR+5hX7bSofy8yf1hZevMS8A==",
5269
+ "license": "MIT",
5270
+ "engines": {
5271
+ "node": "^18 || ^20 || >= 21"
5272
+ }
5273
+ },
5274
+ "node_modules/node-gyp-build": {
5275
+ "version": "4.8.4",
5276
+ "resolved": "https://registry.npmjs.org/node-gyp-build/-/node-gyp-build-4.8.4.tgz",
5277
+ "integrity": "sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==",
5278
+ "license": "MIT",
5279
+ "bin": {
5280
+ "node-gyp-build": "bin.js",
5281
+ "node-gyp-build-optional": "optional.js",
5282
+ "node-gyp-build-test": "build-test.js"
5283
+ }
5284
+ },
5285
  "node_modules/node-releases": {
5286
  "version": "2.0.27",
5287
  "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz",
 
5289
  "dev": true,
5290
  "license": "MIT"
5291
  },
5292
+ "node_modules/object-assign": {
5293
+ "version": "4.1.1",
5294
+ "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
5295
+ "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==",
5296
+ "license": "MIT",
5297
+ "engines": {
5298
+ "node": ">=0.10.0"
5299
+ }
5300
+ },
5301
+ "node_modules/object-inspect": {
5302
+ "version": "1.13.4",
5303
+ "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz",
5304
+ "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==",
5305
+ "license": "MIT",
5306
+ "engines": {
5307
+ "node": ">= 0.4"
5308
+ },
5309
+ "funding": {
5310
+ "url": "https://github.com/sponsors/ljharb"
5311
+ }
5312
+ },
5313
+ "node_modules/on-finished": {
5314
+ "version": "2.4.1",
5315
+ "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz",
5316
+ "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==",
5317
+ "license": "MIT",
5318
+ "dependencies": {
5319
+ "ee-first": "1.1.1"
5320
+ },
5321
+ "engines": {
5322
+ "node": ">= 0.8"
5323
+ }
5324
+ },
5325
+ "node_modules/once": {
5326
+ "version": "1.4.0",
5327
+ "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz",
5328
+ "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==",
5329
+ "license": "ISC",
5330
+ "dependencies": {
5331
+ "wrappy": "1"
5332
+ }
5333
+ },
5334
  "node_modules/optionator": {
5335
  "version": "0.9.4",
5336
  "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz",
 
5419
  "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==",
5420
  "license": "MIT"
5421
  },
5422
+ "node_modules/parseurl": {
5423
+ "version": "1.3.3",
5424
+ "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
5425
+ "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==",
5426
+ "license": "MIT",
5427
+ "engines": {
5428
+ "node": ">= 0.8"
5429
+ }
5430
+ },
5431
  "node_modules/path-exists": {
5432
  "version": "4.0.0",
5433
  "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz",
 
5448
  "node": ">=8"
5449
  }
5450
  },
5451
+ "node_modules/path-to-regexp": {
5452
+ "version": "8.3.0",
5453
+ "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-8.3.0.tgz",
5454
+ "integrity": "sha512-7jdwVIRtsP8MYpdXSwOS0YdD0Du+qOoF/AEPIt88PcCFrZCzx41oxku1jD88hZBwbNUIEfpqvuhjFaMAqMTWnA==",
5455
+ "license": "MIT",
5456
+ "funding": {
5457
+ "type": "opencollective",
5458
+ "url": "https://opencollective.com/express"
5459
+ }
5460
+ },
5461
  "node_modules/picocolors": {
5462
  "version": "1.1.1",
5463
  "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
 
5558
  "node": ">=12.0.0"
5559
  }
5560
  },
5561
+ "node_modules/proxy-addr": {
5562
+ "version": "2.0.7",
5563
+ "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
5564
+ "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==",
5565
+ "license": "MIT",
5566
+ "dependencies": {
5567
+ "forwarded": "0.2.0",
5568
+ "ipaddr.js": "1.9.1"
5569
+ },
5570
+ "engines": {
5571
+ "node": ">= 0.10"
5572
+ }
5573
+ },
5574
  "node_modules/proxy-from-env": {
5575
  "version": "1.1.0",
5576
  "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz",
 
5581
  "version": "2.3.1",
5582
  "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz",
5583
  "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==",
 
5584
  "license": "MIT",
5585
  "engines": {
5586
  "node": ">=6"
5587
  }
5588
  },
5589
+ "node_modules/qs": {
5590
+ "version": "6.14.1",
5591
+ "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.1.tgz",
5592
+ "integrity": "sha512-4EK3+xJl8Ts67nLYNwqw/dsFVnCf+qR7RgXSK9jEEm9unao3njwMDdmsdvoKBKHzxd7tCYz5e5M+SnMjdtXGQQ==",
5593
+ "license": "BSD-3-Clause",
5594
+ "dependencies": {
5595
+ "side-channel": "^1.1.0"
5596
+ },
5597
+ "engines": {
5598
+ "node": ">=0.6"
5599
+ },
5600
+ "funding": {
5601
+ "url": "https://github.com/sponsors/ljharb"
5602
+ }
5603
+ },
5604
+ "node_modules/range-parser": {
5605
+ "version": "1.2.1",
5606
+ "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz",
5607
+ "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==",
5608
+ "license": "MIT",
5609
+ "engines": {
5610
+ "node": ">= 0.6"
5611
+ }
5612
+ },
5613
+ "node_modules/raw-body": {
5614
+ "version": "3.0.2",
5615
+ "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-3.0.2.tgz",
5616
+ "integrity": "sha512-K5zQjDllxWkf7Z5xJdV0/B0WTNqx6vxG70zJE4N0kBs4LovmEYWJzQGxC9bS9RAKu3bgM40lrd5zoLJ12MQ5BA==",
5617
+ "license": "MIT",
5618
+ "dependencies": {
5619
+ "bytes": "~3.1.2",
5620
+ "http-errors": "~2.0.1",
5621
+ "iconv-lite": "~0.7.0",
5622
+ "unpipe": "~1.0.0"
5623
+ },
5624
+ "engines": {
5625
+ "node": ">= 0.10"
5626
+ }
5627
+ },
5628
  "node_modules/react": {
5629
  "version": "19.2.3",
5630
  "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz",
 
5697
  "node": ">=0.10.0"
5698
  }
5699
  },
5700
+ "node_modules/readable-stream": {
5701
+ "version": "3.6.2",
5702
+ "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
5703
+ "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==",
5704
+ "license": "MIT",
5705
+ "dependencies": {
5706
+ "inherits": "^2.0.3",
5707
+ "string_decoder": "^1.1.1",
5708
+ "util-deprecate": "^1.0.1"
5709
+ },
5710
+ "engines": {
5711
+ "node": ">= 6"
5712
+ }
5713
+ },
5714
  "node_modules/remark-parse": {
5715
  "version": "11.0.0",
5716
  "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz",
 
5805
  "fsevents": "~2.3.2"
5806
  }
5807
  },
5808
+ "node_modules/router": {
5809
+ "version": "2.2.0",
5810
+ "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz",
5811
+ "integrity": "sha512-nLTrUKm2UyiL7rlhapu/Zl45FwNgkZGaCpZbIHajDYgwlJCOzLSk+cIPAnsEqV955GjILJnKbdQC1nVPz+gAYQ==",
5812
+ "license": "MIT",
5813
+ "dependencies": {
5814
+ "debug": "^4.4.0",
5815
+ "depd": "^2.0.0",
5816
+ "is-promise": "^4.0.0",
5817
+ "parseurl": "^1.3.3",
5818
+ "path-to-regexp": "^8.0.0"
5819
+ },
5820
+ "engines": {
5821
+ "node": ">= 18"
5822
+ }
5823
+ },
5824
  "node_modules/safe-buffer": {
5825
  "version": "5.2.1",
5826
  "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
 
5841
  ],
5842
  "license": "MIT"
5843
  },
5844
+ "node_modules/safer-buffer": {
5845
+ "version": "2.1.2",
5846
+ "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz",
5847
+ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==",
5848
+ "license": "MIT"
5849
+ },
5850
  "node_modules/scheduler": {
5851
  "version": "0.27.0",
5852
  "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
 
5863
  "semver": "bin/semver.js"
5864
  }
5865
  },
5866
+ "node_modules/send": {
5867
+ "version": "1.2.1",
5868
+ "resolved": "https://registry.npmjs.org/send/-/send-1.2.1.tgz",
5869
+ "integrity": "sha512-1gnZf7DFcoIcajTjTwjwuDjzuz4PPcY2StKPlsGAQ1+YH20IRVrBaXSWmdjowTJ6u8Rc01PoYOGHXfP1mYcZNQ==",
5870
+ "license": "MIT",
5871
+ "dependencies": {
5872
+ "debug": "^4.4.3",
5873
+ "encodeurl": "^2.0.0",
5874
+ "escape-html": "^1.0.3",
5875
+ "etag": "^1.8.1",
5876
+ "fresh": "^2.0.0",
5877
+ "http-errors": "^2.0.1",
5878
+ "mime-types": "^3.0.2",
5879
+ "ms": "^2.1.3",
5880
+ "on-finished": "^2.4.1",
5881
+ "range-parser": "^1.2.1",
5882
+ "statuses": "^2.0.2"
5883
+ },
5884
+ "engines": {
5885
+ "node": ">= 18"
5886
+ },
5887
+ "funding": {
5888
+ "type": "opencollective",
5889
+ "url": "https://opencollective.com/express"
5890
+ }
5891
+ },
5892
+ "node_modules/send/node_modules/mime-db": {
5893
+ "version": "1.54.0",
5894
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
5895
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
5896
+ "license": "MIT",
5897
+ "engines": {
5898
+ "node": ">= 0.6"
5899
+ }
5900
+ },
5901
+ "node_modules/send/node_modules/mime-types": {
5902
+ "version": "3.0.2",
5903
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
5904
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
5905
+ "license": "MIT",
5906
+ "dependencies": {
5907
+ "mime-db": "^1.54.0"
5908
+ },
5909
+ "engines": {
5910
+ "node": ">=18"
5911
+ },
5912
+ "funding": {
5913
+ "type": "opencollective",
5914
+ "url": "https://opencollective.com/express"
5915
+ }
5916
+ },
5917
+ "node_modules/serve-static": {
5918
+ "version": "2.2.1",
5919
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-2.2.1.tgz",
5920
+ "integrity": "sha512-xRXBn0pPqQTVQiC8wyQrKs2MOlX24zQ0POGaj0kultvoOCstBQM5yvOhAVSUwOMjQtTvsPWoNCHfPGwaaQJhTw==",
5921
+ "license": "MIT",
5922
+ "dependencies": {
5923
+ "encodeurl": "^2.0.0",
5924
+ "escape-html": "^1.0.3",
5925
+ "parseurl": "^1.3.3",
5926
+ "send": "^1.2.0"
5927
+ },
5928
+ "engines": {
5929
+ "node": ">= 18"
5930
+ },
5931
+ "funding": {
5932
+ "type": "opencollective",
5933
+ "url": "https://opencollective.com/express"
5934
+ }
5935
+ },
5936
+ "node_modules/setprototypeof": {
5937
+ "version": "1.2.0",
5938
+ "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz",
5939
+ "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==",
5940
+ "license": "ISC"
5941
+ },
5942
  "node_modules/shebang-command": {
5943
  "version": "2.0.0",
5944
  "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
 
5962
  "node": ">=8"
5963
  }
5964
  },
5965
+ "node_modules/side-channel": {
5966
+ "version": "1.1.0",
5967
+ "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz",
5968
+ "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==",
5969
+ "license": "MIT",
5970
+ "dependencies": {
5971
+ "es-errors": "^1.3.0",
5972
+ "object-inspect": "^1.13.3",
5973
+ "side-channel-list": "^1.0.0",
5974
+ "side-channel-map": "^1.0.1",
5975
+ "side-channel-weakmap": "^1.0.2"
5976
+ },
5977
+ "engines": {
5978
+ "node": ">= 0.4"
5979
+ },
5980
+ "funding": {
5981
+ "url": "https://github.com/sponsors/ljharb"
5982
+ }
5983
+ },
5984
+ "node_modules/side-channel-list": {
5985
+ "version": "1.0.0",
5986
+ "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz",
5987
+ "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==",
5988
+ "license": "MIT",
5989
+ "dependencies": {
5990
+ "es-errors": "^1.3.0",
5991
+ "object-inspect": "^1.13.3"
5992
+ },
5993
+ "engines": {
5994
+ "node": ">= 0.4"
5995
+ },
5996
+ "funding": {
5997
+ "url": "https://github.com/sponsors/ljharb"
5998
+ }
5999
+ },
6000
+ "node_modules/side-channel-map": {
6001
+ "version": "1.0.1",
6002
+ "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz",
6003
+ "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==",
6004
+ "license": "MIT",
6005
+ "dependencies": {
6006
+ "call-bound": "^1.0.2",
6007
+ "es-errors": "^1.3.0",
6008
+ "get-intrinsic": "^1.2.5",
6009
+ "object-inspect": "^1.13.3"
6010
+ },
6011
+ "engines": {
6012
+ "node": ">= 0.4"
6013
+ },
6014
+ "funding": {
6015
+ "url": "https://github.com/sponsors/ljharb"
6016
+ }
6017
+ },
6018
+ "node_modules/side-channel-weakmap": {
6019
+ "version": "1.0.2",
6020
+ "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz",
6021
+ "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==",
6022
+ "license": "MIT",
6023
+ "dependencies": {
6024
+ "call-bound": "^1.0.2",
6025
+ "es-errors": "^1.3.0",
6026
+ "get-intrinsic": "^1.2.5",
6027
+ "object-inspect": "^1.13.3",
6028
+ "side-channel-map": "^1.0.1"
6029
+ },
6030
+ "engines": {
6031
+ "node": ">= 0.4"
6032
+ },
6033
+ "funding": {
6034
+ "url": "https://github.com/sponsors/ljharb"
6035
+ }
6036
+ },
6037
+ "node_modules/sift": {
6038
+ "version": "17.1.3",
6039
+ "resolved": "https://registry.npmjs.org/sift/-/sift-17.1.3.tgz",
6040
+ "integrity": "sha512-Rtlj66/b0ICeFzYTuNvX/EF1igRbbnGSvEyT79McoZa/DeGhMyC5pWKOEsZKnpkqtSeovd5FL/bjHWC3CIIvCQ==",
6041
+ "license": "MIT"
6042
+ },
6043
+ "node_modules/socket.io": {
6044
+ "version": "4.8.3",
6045
+ "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.3.tgz",
6046
+ "integrity": "sha512-2Dd78bqzzjE6KPkD5fHZmDAKRNe3J15q+YHDrIsy9WEkqttc7GY+kT9OBLSMaPbQaEd0x1BjcmtMtXkfpc+T5A==",
6047
+ "license": "MIT",
6048
+ "dependencies": {
6049
+ "accepts": "~1.3.4",
6050
+ "base64id": "~2.0.0",
6051
+ "cors": "~2.8.5",
6052
+ "debug": "~4.4.1",
6053
+ "engine.io": "~6.6.0",
6054
+ "socket.io-adapter": "~2.5.2",
6055
+ "socket.io-parser": "~4.2.4"
6056
+ },
6057
+ "engines": {
6058
+ "node": ">=10.2.0"
6059
+ }
6060
+ },
6061
+ "node_modules/socket.io-adapter": {
6062
+ "version": "2.5.6",
6063
+ "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.6.tgz",
6064
+ "integrity": "sha512-DkkO/dz7MGln0dHn5bmN3pPy+JmywNICWrJqVWiVOyvXjWQFIv9c2h24JrQLLFJ2aQVQf/Cvl1vblnd4r2apLQ==",
6065
+ "license": "MIT",
6066
+ "dependencies": {
6067
+ "debug": "~4.4.1",
6068
+ "ws": "~8.18.3"
6069
+ }
6070
+ },
6071
+ "node_modules/socket.io-client": {
6072
+ "version": "4.8.3",
6073
+ "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.8.3.tgz",
6074
+ "integrity": "sha512-uP0bpjWrjQmUt5DTHq9RuoCBdFJF10cdX9X+a368j/Ft0wmaVgxlrjvK3kjvgCODOMMOz9lcaRzxmso0bTWZ/g==",
6075
+ "license": "MIT",
6076
+ "dependencies": {
6077
+ "@socket.io/component-emitter": "~3.1.0",
6078
+ "debug": "~4.4.1",
6079
+ "engine.io-client": "~6.6.1",
6080
+ "socket.io-parser": "~4.2.4"
6081
+ },
6082
+ "engines": {
6083
+ "node": ">=10.0.0"
6084
+ }
6085
+ },
6086
+ "node_modules/socket.io-parser": {
6087
+ "version": "4.2.5",
6088
+ "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.5.tgz",
6089
+ "integrity": "sha512-bPMmpy/5WWKHea5Y/jYAP6k74A+hvmRCQaJuJB6I/ML5JZq/KfNieUVo/3Mh7SAqn7TyFdIo6wqYHInG1MU1bQ==",
6090
+ "license": "MIT",
6091
+ "dependencies": {
6092
+ "@socket.io/component-emitter": "~3.1.0",
6093
+ "debug": "~4.4.1"
6094
+ },
6095
+ "engines": {
6096
+ "node": ">=10.0.0"
6097
+ }
6098
+ },
6099
+ "node_modules/socket.io/node_modules/accepts": {
6100
+ "version": "1.3.8",
6101
+ "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz",
6102
+ "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==",
6103
+ "license": "MIT",
6104
+ "dependencies": {
6105
+ "mime-types": "~2.1.34",
6106
+ "negotiator": "0.6.3"
6107
+ },
6108
+ "engines": {
6109
+ "node": ">= 0.6"
6110
+ }
6111
+ },
6112
+ "node_modules/socket.io/node_modules/negotiator": {
6113
+ "version": "0.6.3",
6114
+ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz",
6115
+ "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==",
6116
+ "license": "MIT",
6117
+ "engines": {
6118
+ "node": ">= 0.6"
6119
+ }
6120
+ },
6121
  "node_modules/source-map-js": {
6122
  "version": "1.2.1",
6123
  "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
 
6138
  "url": "https://github.com/sponsors/wooorm"
6139
  }
6140
  },
6141
+ "node_modules/sparse-bitfield": {
6142
+ "version": "3.0.3",
6143
+ "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz",
6144
+ "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==",
6145
+ "license": "MIT",
6146
+ "dependencies": {
6147
+ "memory-pager": "^1.0.2"
6148
+ }
6149
+ },
6150
+ "node_modules/statuses": {
6151
+ "version": "2.0.2",
6152
+ "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.2.tgz",
6153
+ "integrity": "sha512-DvEy55V3DB7uknRo+4iOGT5fP1slR8wQohVdknigZPMpMstaKJQWhwiYBACJE3Ul2pTnATihhBYnRhZQHGBiRw==",
6154
+ "license": "MIT",
6155
+ "engines": {
6156
+ "node": ">= 0.8"
6157
+ }
6158
+ },
6159
+ "node_modules/streamsearch": {
6160
+ "version": "1.1.0",
6161
+ "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz",
6162
+ "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==",
6163
+ "engines": {
6164
+ "node": ">=10.0.0"
6165
+ }
6166
+ },
6167
+ "node_modules/string_decoder": {
6168
+ "version": "1.3.0",
6169
+ "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
6170
+ "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
6171
+ "license": "MIT",
6172
+ "dependencies": {
6173
+ "safe-buffer": "~5.2.0"
6174
+ }
6175
+ },
6176
  "node_modules/string-width": {
6177
  "version": "4.2.3",
6178
  "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
 
6281
  "url": "https://github.com/sponsors/SuperchupuDev"
6282
  }
6283
  },
6284
+ "node_modules/toidentifier": {
6285
+ "version": "1.0.1",
6286
+ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz",
6287
+ "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==",
6288
+ "license": "MIT",
6289
+ "engines": {
6290
+ "node": ">=0.6"
6291
+ }
6292
+ },
6293
+ "node_modules/tr46": {
6294
+ "version": "5.1.1",
6295
+ "resolved": "https://registry.npmjs.org/tr46/-/tr46-5.1.1.tgz",
6296
+ "integrity": "sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==",
6297
+ "license": "MIT",
6298
+ "dependencies": {
6299
+ "punycode": "^2.3.1"
6300
+ },
6301
+ "engines": {
6302
+ "node": ">=18"
6303
+ }
6304
+ },
6305
  "node_modules/trim-lines": {
6306
  "version": "3.0.1",
6307
  "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz",
 
6341
  "node": ">= 0.8.0"
6342
  }
6343
  },
6344
+ "node_modules/type-is": {
6345
+ "version": "2.0.1",
6346
+ "resolved": "https://registry.npmjs.org/type-is/-/type-is-2.0.1.tgz",
6347
+ "integrity": "sha512-OZs6gsjF4vMp32qrCbiVSkrFmXtG/AZhY3t0iAMrMBiAZyV9oALtXO8hsrHbMXF9x6L3grlFuwW2oAz7cav+Gw==",
6348
+ "license": "MIT",
6349
+ "dependencies": {
6350
+ "content-type": "^1.0.5",
6351
+ "media-typer": "^1.1.0",
6352
+ "mime-types": "^3.0.0"
6353
+ },
6354
+ "engines": {
6355
+ "node": ">= 0.6"
6356
+ }
6357
+ },
6358
+ "node_modules/type-is/node_modules/mime-db": {
6359
+ "version": "1.54.0",
6360
+ "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz",
6361
+ "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==",
6362
+ "license": "MIT",
6363
+ "engines": {
6364
+ "node": ">= 0.6"
6365
+ }
6366
+ },
6367
+ "node_modules/type-is/node_modules/mime-types": {
6368
+ "version": "3.0.2",
6369
+ "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz",
6370
+ "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==",
6371
+ "license": "MIT",
6372
+ "dependencies": {
6373
+ "mime-db": "^1.54.0"
6374
+ },
6375
+ "engines": {
6376
+ "node": ">=18"
6377
+ },
6378
+ "funding": {
6379
+ "type": "opencollective",
6380
+ "url": "https://opencollective.com/express"
6381
+ }
6382
+ },
6383
+ "node_modules/typedarray": {
6384
+ "version": "0.0.6",
6385
+ "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
6386
+ "integrity": "sha512-/aCDEGatGvZ2BIk+HmLf4ifCJFwvKFNb9/JeZPMulfgFracn9QFcAf5GO8B/mweUjSoblS5In0cWhqpfs/5PQA==",
6387
+ "license": "MIT"
6388
+ },
6389
  "node_modules/undici-types": {
6390
  "version": "7.16.0",
6391
  "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz",
 
6479
  "url": "https://opencollective.com/unified"
6480
  }
6481
  },
6482
+ "node_modules/unpipe": {
6483
+ "version": "1.0.0",
6484
+ "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz",
6485
+ "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==",
6486
+ "license": "MIT",
6487
+ "engines": {
6488
+ "node": ">= 0.8"
6489
+ }
6490
+ },
6491
  "node_modules/update-browserslist-db": {
6492
  "version": "1.2.3",
6493
  "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz",
 
6529
  "punycode": "^2.1.0"
6530
  }
6531
  },
6532
+ "node_modules/util-deprecate": {
6533
+ "version": "1.0.2",
6534
+ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz",
6535
+ "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==",
6536
+ "license": "MIT"
6537
+ },
6538
+ "node_modules/vary": {
6539
+ "version": "1.1.2",
6540
+ "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz",
6541
+ "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
6542
+ "license": "MIT",
6543
+ "engines": {
6544
+ "node": ">= 0.8"
6545
+ }
6546
+ },
6547
  "node_modules/vfile": {
6548
  "version": "6.0.3",
6549
  "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz",
 
6653
  "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==",
6654
  "license": "Apache-2.0"
6655
  },
6656
+ "node_modules/webidl-conversions": {
6657
+ "version": "7.0.0",
6658
+ "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz",
6659
+ "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==",
6660
+ "license": "BSD-2-Clause",
6661
+ "engines": {
6662
+ "node": ">=12"
6663
+ }
6664
+ },
6665
  "node_modules/websocket-driver": {
6666
  "version": "0.7.4",
6667
  "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz",
 
6685
  "node": ">=0.8.0"
6686
  }
6687
  },
6688
+ "node_modules/whatwg-url": {
6689
+ "version": "14.2.0",
6690
+ "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-14.2.0.tgz",
6691
+ "integrity": "sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==",
6692
+ "license": "MIT",
6693
+ "dependencies": {
6694
+ "tr46": "^5.1.0",
6695
+ "webidl-conversions": "^7.0.0"
6696
+ },
6697
+ "engines": {
6698
+ "node": ">=18"
6699
+ }
6700
+ },
6701
  "node_modules/which": {
6702
  "version": "2.0.2",
6703
  "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
 
6741
  "url": "https://github.com/chalk/wrap-ansi?sponsor=1"
6742
  }
6743
  },
6744
+ "node_modules/wrappy": {
6745
+ "version": "1.0.2",
6746
+ "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
6747
+ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==",
6748
+ "license": "ISC"
6749
+ },
6750
+ "node_modules/ws": {
6751
+ "version": "8.18.3",
6752
+ "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz",
6753
+ "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==",
6754
+ "license": "MIT",
6755
+ "engines": {
6756
+ "node": ">=10.0.0"
6757
+ },
6758
+ "peerDependencies": {
6759
+ "bufferutil": "^4.0.1",
6760
+ "utf-8-validate": ">=5.0.2"
6761
+ },
6762
+ "peerDependenciesMeta": {
6763
+ "bufferutil": {
6764
+ "optional": true
6765
+ },
6766
+ "utf-8-validate": {
6767
+ "optional": true
6768
+ }
6769
+ }
6770
+ },
6771
+ "node_modules/xmlhttprequest-ssl": {
6772
+ "version": "2.1.2",
6773
+ "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.1.2.tgz",
6774
+ "integrity": "sha512-TEU+nJVUUnA4CYJFLvK5X9AOeH4KvDvhIfm0vV1GaQRtchnG0hgK5p8hw/xjv8cunWYCsiPCSDzObPyhEwq3KQ==",
6775
+ "engines": {
6776
+ "node": ">=0.4.0"
6777
+ }
6778
+ },
6779
+ "node_modules/xtend": {
6780
+ "version": "4.0.2",
6781
+ "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
6782
+ "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
6783
+ "license": "MIT",
6784
+ "engines": {
6785
+ "node": ">=0.4"
6786
+ }
6787
+ },
6788
  "node_modules/y18n": {
6789
  "version": "5.0.8",
6790
  "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz",
kibali-ui/package.json CHANGED
@@ -11,13 +11,22 @@
11
  },
12
  "dependencies": {
13
  "axios": "^1.13.2",
 
 
 
 
14
  "firebase": "^12.7.0",
 
15
  "leaflet": "^1.9.4",
16
  "lucide-react": "^0.562.0",
 
 
17
  "react": "^19.2.0",
18
  "react-dom": "^19.2.0",
19
  "react-leaflet": "^5.0.0",
20
- "react-markdown": "^10.1.0"
 
 
21
  },
22
  "devDependencies": {
23
  "@eslint/js": "^9.39.1",
 
11
  },
12
  "dependencies": {
13
  "axios": "^1.13.2",
14
+ "bcrypt": "^6.0.0",
15
+ "cors": "^2.8.5",
16
+ "dotenv": "^17.2.3",
17
+ "express": "^5.2.1",
18
  "firebase": "^12.7.0",
19
+ "jsonwebtoken": "^9.0.3",
20
  "leaflet": "^1.9.4",
21
  "lucide-react": "^0.562.0",
22
+ "mongoose": "^9.1.0",
23
+ "multer": "^2.0.2",
24
  "react": "^19.2.0",
25
  "react-dom": "^19.2.0",
26
  "react-leaflet": "^5.0.0",
27
+ "react-markdown": "^10.1.0",
28
+ "socket.io": "^4.8.3",
29
+ "socket.io-client": "^4.8.3"
30
  },
31
  "devDependencies": {
32
  "@eslint/js": "^9.39.1",
kibali-ui/src/App.jsx CHANGED
@@ -2,12 +2,11 @@ import React, { useState, useEffect, useRef } from 'react';
2
  import axios from 'axios';
3
  import {
4
  Send, FileText, Upload, Globe, MapPin,
5
- Loader2, Trash2, Image as ImageIcon, Search, Brain, ChevronDown, ChevronUp, User, LogOut
 
6
  } from 'lucide-react';
7
- import { auth, googleProvider } from './firebase'; // Import Firebase
8
- import { signInWithPopup, signOut, onAuthStateChanged } from "firebase/auth";
9
 
10
- const API_BASE = "http://localhost:8000"; // Ton backend Node
11
  const LOGO_PATH = "/kibali_logo.svg";
12
 
13
  function App() {
@@ -15,129 +14,223 @@ function App() {
15
  const [input, setInput] = useState("");
16
  const [loading, setLoading] = useState(false);
17
  const [uploading, setUploading] = useState(false);
18
- const [status, setStatus] = useState(null);
19
- const [showThinking, setShowThinking] = useState(true);
20
- const [currentUser, setCurrentUser] = useState(null); // Utilisateur Firebase
 
 
 
 
 
21
  const scrollRef = useRef(null);
 
22
 
23
- // Écouter les changements d'authentification Firebase
24
  useEffect(() => {
25
- const unsubscribe = onAuthStateChanged(auth, async (firebaseUser) => {
26
- if (firebaseUser) {
27
- // Utilisateur connecté via Google
28
- try {
29
- // Envoyer les infos à ton backend pour créer/lier le compte
30
- const res = await axios.post(`${API_BASE}/auth/google`, {
31
- googleId: firebaseUser.uid,
32
- name: firebaseUser.displayName,
33
- email: firebaseUser.email,
34
- photoUrl: firebaseUser.photoURL
35
- });
36
-
37
- const user = res.data.user;
38
- setCurrentUser(user);
39
- localStorage.setItem("user", JSON.stringify(user)); // backup local
40
- } catch (err) {
41
- console.error("Erreur synchronisation avec backend:", err);
42
- // Option : déconnexion si échec critique
43
- }
44
- } else {
45
- setCurrentUser(null);
46
- localStorage.removeItem("user");
47
  }
48
- });
49
-
50
- return () => unsubscribe();
51
  }, []);
52
 
53
- // Charger l'utilisateur depuis localStorage au démarrage (si refresh)
54
- useEffect(() => {
55
- const saved = localStorage.getItem("user");
56
- if (saved && !currentUser) {
57
- setCurrentUser(JSON.parse(saved));
58
- }
59
- }, []);
60
-
61
- useEffect(() => {
62
- scrollRef.current?.scrollIntoView({ behavior: "smooth" });
63
- }, [messages, loading]);
64
-
65
- const handleGoogleLogin = async () => {
66
  try {
67
- await signInWithPopup(auth, googleProvider);
68
- // onAuthStateChanged gérera la suite
69
  } catch (error) {
70
- alert("Échec de la connexion Google : " + error.message);
71
  }
72
  };
73
 
74
- const handleLogout = async () => {
75
- try {
76
- await signOut(auth);
77
- setMessages([]);
78
- // Firebase effacera l'état → onAuthStateChanged mettra currentUser à null
79
- } catch (error) {
80
- alert("Erreur déconnexion");
81
- }
82
- };
83
 
 
84
  const handleSend = async () => {
85
- if (!currentUser) {
86
- alert("Veuillez vous connecter avec Google pour discuter.");
87
- return;
88
- }
89
  if (!input.trim() || loading) return;
90
 
91
- const userMsg = { role: "user", content: input };
92
  setMessages(prev => [...prev, userMsg]);
93
  setInput("");
94
  setLoading(true);
95
 
96
  try {
97
- // À adapter selon ton vrai endpoint chat
98
  const response = await axios.post(`${API_BASE}/chat`, {
99
  messages: [...messages, userMsg],
100
- latitude: currentUser.latitude || 0.4061,
101
- longitude: currentUser.longitude || 9.4673,
102
  city: "Libreville",
103
  thinking_mode: true
 
 
104
  });
105
 
106
- const aiResponse = response.data.response || "Réponse simulée connecter)";
107
  const aiImages = response.data.images || [];
 
108
 
109
  setMessages(prev => [...prev, {
110
  role: "assistant",
111
  content: aiResponse,
112
  images: aiImages,
113
- tools_used: ["Recherche Web", "Analyse Géo", "Base Documentaire"]
 
114
  }]);
 
 
 
115
  } catch (error) {
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
116
  setMessages(prev => [...prev, {
117
  role: "assistant",
118
- content: "Erreur : impossible de contacter le serveur IA."
 
 
119
  }]);
120
  } finally {
121
  setLoading(false);
122
  }
123
  };
124
 
 
125
  const handleFileUpload = async (e) => {
126
- // ... (inchangé)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
127
  };
128
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
129
  const styles = {
130
  container: { display: 'flex', height: '100vh', backgroundColor: '#020617', color: '#f8fafc', fontFamily: 'system-ui, -apple-system, sans-serif' },
131
- sidebar: { width: '320px', backgroundColor: '#0f172a', borderRight: '1px solid #1e293b', display: 'flex', flexDirection: 'column', zIndex: 10 },
132
  main: { flex: 1, display: 'flex', flexDirection: 'column', position: 'relative', background: 'radial-gradient(circle at 50% 0%, #1e293b 0%, #020617 100%)' },
133
  messageArea: { flex: 1, overflowY: 'auto', padding: '2rem 10% 2rem 10%' },
134
  userBubble: { backgroundColor: '#059669', color: 'white', padding: '1rem 1.25rem', borderRadius: '1.25rem 1.25rem 0.25rem 1.25rem', boxShadow: '0 4px 15px rgba(5, 150, 105, 0.2)' },
135
  aiBubble: { backgroundColor: '#1e293b', border: '1px solid #334155', color: '#f1f5f9', padding: '1rem 1.25rem', borderRadius: '1.25rem 1.25rem 1.25rem 0.25rem', boxShadow: '0 10px 30px rgba(0,0,0,0.5)' },
 
 
136
  inputContainer: { padding: '2rem', background: 'linear-gradient(to top, #020617 70%, transparent)' },
137
  inputWrapper: { display: 'flex', alignItems: 'center', backgroundColor: 'rgba(30, 41, 59, 0.7)', backdropFilter: 'blur(20px)', border: '1px solid #334155', borderRadius: '1.5rem', padding: '0.6rem 1.2rem', maxWidth: '900px', margin: '0 auto' },
138
  inputField: { flex: 1, background: 'transparent', border: 'none', color: 'white', padding: '0.8rem', outline: 'none', fontSize: '1rem' },
139
  sendBtn: { backgroundColor: '#10b981', color: 'white', border: 'none', borderRadius: '1rem', width: '45px', height: '45px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.2s' },
140
- toolBadge: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '11px', backgroundColor: '#020617', padding: '6px 12px', borderRadius: '10px', border: '1px solid #1e293b', color: '#94a3b8' }
 
141
  };
142
 
143
  return (
@@ -146,141 +239,322 @@ function App() {
146
  <aside style={styles.sidebar}>
147
  <div style={{ padding: '2rem', borderBottom: '1px solid #1e293b', display: 'flex', alignItems: 'center', gap: '1rem' }}>
148
  <div style={{ width: '45px', height: '45px', backgroundColor: '#1e293b', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #334155' }}>
149
- <img src={LOGO_PATH} alt="K" style={{ width: '30px', height: '30px', objectFit: 'contain' }} onError={(e) => e.target.style.display='none'} />
150
  </div>
151
  <div>
152
- <h1 style={{ fontSize: '1.3rem', fontWeight: '900', margin: 0, letterSpacing: '-0.5px' }}>Kibali <span style={{ color: '#10b981' }}>AI</span></h1>
 
 
 
 
 
153
  </div>
154
  </div>
155
 
156
- <div style={{ padding: '2rem', flex: 1, display: 'flex', flexDirection: 'column', gap: '2.5rem' }}>
157
- {/* Profil ou bouton connexion */}
158
- {!currentUser ? (
159
- <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', gap: '1rem' }}>
160
- <p style={{ color: '#64748b', fontSize: '14px', textAlign: 'center' }}>
161
- Connectez-vous avec Google pour utiliser Kibali
162
- </p>
163
- <button
164
- onClick={handleGoogleLogin}
165
- style={{
166
- backgroundColor: '#ffffff',
167
- color: '#000',
168
- padding: '12px 24px',
169
- borderRadius: '12px',
170
- border: 'none',
171
- cursor: 'pointer',
172
- fontWeight: '600',
173
- display: 'flex',
174
- alignItems: 'center',
175
- gap: '10px',
176
- boxShadow: '0 4px 15px rgba(0,0,0,0.3)'
177
- }}
178
- >
179
- <img src="https://upload.wikimedia.org/wikipedia/commons/5/53/Google_%22G%22_Logo.svg" alt="G" width="20" />
180
- Se connecter avec Google
181
- </button>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
  </div>
183
- ) : (
184
- <div style={{ display: 'flex', alignItems: 'center', gap: '12px', backgroundColor: '#020617', padding: '12px', borderRadius: '12px', border: '1px solid #1e293b' }}>
185
- <img
186
- src={currentUser.photoUrl || '/default-avatar.png'}
187
- alt="Profil"
188
- style={{ width: '40px', height: '40px', borderRadius: '50%', objectFit: 'cover' }}
189
- />
190
- <div style={{ flex: 1 }}>
191
- <p style={{ margin: 0, fontWeight: '600', fontSize: '14px' }}>{currentUser.name}</p>
192
- <p style={{ margin: 0, fontSize: '11px', color: '#64748b' }}>{currentUser.email}</p>
 
 
 
193
  </div>
194
- <button onClick={handleLogout} style={{ background: 'none', border: 'none', cursor: 'pointer', color: '#ef4444' }}>
195
- <LogOut size={18} />
196
- </button>
197
  </div>
198
- )}
199
 
200
- {/* Le reste de ta sidebar (documents, stats, etc.) */}
201
- <div>
202
- <h2 style={{ fontSize: '11px', color: '#475569', textTransform: 'uppercase', letterSpacing: '1.5px', marginBottom: '1rem', fontWeight: '800' }}>Documents</h2>
203
- <label style={{ display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', height: '140px', border: '2px dashed #1e293b', borderRadius: '1.5rem', cursor: 'pointer', transition: 'all 0.3s', backgroundColor: '#020617' }}>
204
- {uploading ? <Loader2 className="animate-spin" color="#10b981" size={32} /> : <Upload color="#10b981" size={32} />}
205
- <span style={{ fontSize: '12px', marginTop: '12px', color: '#64748b', fontWeight: '500' }}>Importer rapports PDF</span>
206
- <input type="file" hidden multiple accept=".pdf" onChange={handleFileUpload} />
207
- </label>
208
- </div>
 
 
 
 
 
 
 
209
 
210
- {/* Statistiques */}
211
- <div style={{ backgroundColor: 'rgba(2, 6, 23, 0.5)', borderRadius: '1.5rem', padding: '1.5rem', border: '1px solid #1e293b' }}>
212
- <h3 style={{ fontSize: '11px', color: '#475569', textTransform: 'uppercase', marginBottom: '1.2rem', fontWeight: '800' }}>Statistiques</h3>
213
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px', marginBottom: '1rem' }}>
214
- <span style={{ color: '#64748b' }}>Connaissances</span>
215
- <span style={{ color: '#10b981', fontWeight: 'bold' }}>{status?.doc_chunks || 0} segments</span>
 
 
216
  </div>
217
- <div style={{ display: 'flex', justifyContent: 'space-between', fontSize: '13px' }}>
218
- <span style={{ color: '#64748b' }}>Position</span>
219
- <span style={{ color: '#f1f5f9', display: 'flex', alignItems: 'center', gap: '6px', fontWeight: '600' }}>
220
- <MapPin size={14} color="#ef4444"/> Libreville
221
- </span>
 
 
 
 
 
 
222
  </div>
223
  </div>
224
  </div>
225
 
226
- <button onClick={() => setMessages([])} style={{ margin: '2rem', background: '#020617', border: '1px solid #1e293b', color: '#475569', padding: '14px', borderRadius: '14px', cursor: 'pointer', display: 'flex', alignItems: 'center', justifyContent: 'center', gap: '10px', fontWeight: '700', fontSize: '12px' }}>
227
- <Trash2 size={16} /> RÉINITIALISER LE CHAT
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
228
  </button>
229
  </aside>
230
 
231
- {/* MAIN CONTENT - reste identique, avec photo utilisateur dans les bulles */}
232
  <main style={styles.main}>
233
  <div style={styles.messageArea}>
234
- {messages.length === 0 && !currentUser && (
235
  <div style={{ height: '80%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }}>
236
- <div style={{ width: '100px', height: '100px', backgroundColor: '#0f172a', borderRadius: '30px', display: 'flex', alignItems: 'center', justifyContent: 'center', marginBottom: '2rem', border: '1px solid #334155', boxShadow: '0 20px 50px rgba(0,0,0,0.5)' }}>
237
- <img src={LOGO_PATH} alt="Kibali" style={{ width: '60px' }} />
 
 
 
 
 
 
 
 
 
 
 
238
  </div>
239
- <h2 style={{ fontSize: '2.5rem', fontWeight: '900', marginBottom: '0.8rem', letterSpacing: '-1px' }}>Connectez-vous pour commencer</h2>
 
 
240
  <p style={{ color: '#64748b', maxWidth: '450px', lineHeight: '1.7', fontSize: '1.1rem' }}>
241
- Utilisez votre compte Google pour accéder à Kibali AI.
242
  </p>
 
 
 
 
 
 
 
 
 
 
243
  </div>
244
  )}
245
 
246
  {messages.map((m, i) => (
247
  <div key={i} style={{ display: 'flex', justifyContent: m.role === 'user' ? 'flex-end' : 'flex-start', marginBottom: '3rem' }}>
248
  <div style={{ display: 'flex', gap: '1.2rem', maxWidth: '85%', flexDirection: m.role === 'user' ? 'row-reverse' : 'row' }}>
249
- <div style={{ width: '45px', height: '45px', borderRadius: '14px', display: 'flex', alignItems: 'center', justifyContent: 'center', backgroundColor: m.role === 'user' ? '#059669' : '#1e293b', flexShrink: 0, border: '1px solid #334155', overflow: 'hidden' }}>
 
 
 
 
 
 
 
 
 
 
250
  {m.role === 'user' ? (
251
- currentUser?.photoUrl ? (
252
- <img src={currentUser.photoUrl} alt="Vous" style={{ width: '100%', height: '100%', objectFit: 'cover' }} />
253
- ) : (
254
- <User size={24} color="white" />
255
- )
256
  ) : (
257
- <img src={LOGO_PATH} style={{ width: '28px' }} alt="K" />
258
  )}
259
  </div>
260
 
261
- <div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem' }}>
262
- {m.role === 'assistant' && (
263
- <div style={{ backgroundColor: 'rgba(30, 41, 59, 0.3)', borderRadius: '12px', padding: '8px 12px', border: '1px solid #1e293b' }}>
264
- <button onClick={() => setShowThinking(!showThinking)} style={{ background: 'none', border: 'none', color: '#64748b', fontSize: '11px', display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer', fontWeight: '700', textTransform: 'uppercase' }}>
265
- <Brain size={14} color="#10b981"/> Renseignements utilisés {showThinking ? <ChevronUp size={14}/> : <ChevronDown size={14}/>}
 
 
 
 
 
 
 
266
  </button>
267
- {showThinking && (
268
- <div style={{ display: 'flex', gap: '8px', marginTop: '10px', flexWrap: 'wrap' }}>
269
- <div style={styles.toolBadge}><Globe size={13} color="#3b82f6"/> Web Search</div>
270
- <div style={styles.toolBadge}><FileText size={13} color="#10b981"/> PDF Vault</div>
271
- <div style={styles.toolBadge}><MapPin size={13} color="#ef4444"/> Geo-Context</div>
272
- </div>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  )}
274
  </div>
275
  )}
276
- <div style={m.role === 'user' ? styles.userBubble : styles.aiBubble}>
277
- <p style={{ margin: 0, fontSize: '1.05rem', lineHeight: '1.6', fontWeight: '400' }}>{m.content}</p>
 
 
 
 
 
 
 
 
 
 
 
 
278
  </div>
 
 
279
  {m.images && m.images.length > 0 && (
280
- <div style={{ display: 'flex', gap: '12px', marginTop: '12px', overflowX: 'auto', paddingBottom: '10px' }}>
281
  {m.images.map((img, idx) => (
282
- <div key={idx} style={{ flexShrink: 0, position: 'relative', borderRadius: '1.2rem', overflow: 'hidden', border: '2px solid #334155' }}>
283
- <img src={img} alt="contexte" style={{ height: '180px', width: '280px', objectFit: 'cover' }} />
 
 
 
 
 
 
 
 
284
  </div>
285
  ))}
286
  </div>
@@ -292,12 +566,29 @@ function App() {
292
 
293
  {loading && (
294
  <div style={{ display: 'flex', gap: '1.2rem', alignItems: 'center' }}>
295
- <div style={{ width: '45px', height: '45px', borderRadius: '14px', backgroundColor: '#1e293b', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #334155' }}>
 
 
 
 
 
 
 
 
 
296
  <Loader2 className="animate-spin" color="#10b981" size={24} />
297
  </div>
298
- <div style={{ color: '#64748b', fontSize: '15px', fontWeight: '600' }}>Kibali analyse la situation...</div>
 
 
 
 
 
 
 
299
  </div>
300
  )}
 
301
  <div ref={scrollRef} />
302
  </div>
303
 
@@ -306,23 +597,61 @@ function App() {
306
  <Search size={22} color="#475569" style={{ marginRight: '10px' }} />
307
  <input
308
  style={styles.inputField}
309
- placeholder={currentUser ? "Posez votre question..." : "Connectez-vous pour discuter"}
310
  value={input}
311
  onChange={(e) => setInput(e.target.value)}
312
- onKeyDown={(e) => e.key === 'Enter' && handleSend()}
313
- disabled={!currentUser}
 
 
 
 
 
314
  />
315
  <button
316
- style={{ ...styles.sendBtn, opacity: currentUser && input.trim() ? 1 : 0.4 }}
 
 
 
 
 
317
  onClick={handleSend}
318
- disabled={!currentUser || loading || !input.trim()}
 
 
 
 
 
 
 
 
 
 
319
  >
320
- <Send size={20} />
321
  </button>
322
  </div>
323
- <p style={{ textAlign: 'center', color: '#1e293b', fontSize: '10px', marginTop: '1.2rem', fontWeight: '800', letterSpacing: '2px' }}>
324
- GABONESE SOVEREIGN ARTIFICIAL INTELLIGENCE SYSTEM KIBALI-1 powered by SETRAF-GABON
325
- </p>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
326
  </div>
327
  </main>
328
  </div>
 
2
  import axios from 'axios';
3
  import {
4
  Send, FileText, Upload, Globe, MapPin,
5
+ Loader2, Trash2, Image as ImageIcon, Search, Brain, ChevronDown, ChevronUp, User,
6
+ Database, Clock, TrendingUp, AlertCircle, CheckCircle, Zap
7
  } from 'lucide-react';
 
 
8
 
9
+ const API_BASE = "http://localhost:8000";
10
  const LOGO_PATH = "/kibali_logo.svg";
11
 
12
  function App() {
 
14
  const [input, setInput] = useState("");
15
  const [loading, setLoading] = useState(false);
16
  const [uploading, setUploading] = useState(false);
17
+ const [status, setStatus] = useState({
18
+ doc_chunks: 0,
19
+ memory_entries: 0,
20
+ current_subject: null,
21
+ subject_message_count: 0
22
+ });
23
+ const [showThinking, setShowThinking] = useState({});
24
+ const [uploadProgress, setUploadProgress] = useState(null);
25
  const scrollRef = useRef(null);
26
+ const pollingInterval = useRef(null);
27
 
28
+ // Récupération du status backend au démarrage et périodiquement
29
  useEffect(() => {
30
+ fetchStatus();
31
+ pollingInterval.current = setInterval(fetchStatus, 10000); // Toutes les 10 secondes
32
+
33
+ return () => {
34
+ if (pollingInterval.current) {
35
+ clearInterval(pollingInterval.current);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
36
  }
37
+ };
 
 
38
  }, []);
39
 
40
+ const fetchStatus = async () => {
 
 
 
 
 
 
 
 
 
 
 
 
41
  try {
42
+ const response = await axios.get(`${API_BASE}/status`, { timeout: 5000 });
43
+ setStatus(response.data);
44
  } catch (error) {
45
+ console.error("Erreur récupération status:", error);
46
  }
47
  };
48
 
49
+ // Auto-scroll vers le bas
50
+ useEffect(() => {
51
+ scrollRef.current?.scrollIntoView({ behavior: "smooth" });
52
+ }, [messages, loading]);
 
 
 
 
 
53
 
54
+ // Envoi du message
55
  const handleSend = async () => {
 
 
 
 
56
  if (!input.trim() || loading) return;
57
 
58
+ const userMsg = { role: "user", content: input.trim() };
59
  setMessages(prev => [...prev, userMsg]);
60
  setInput("");
61
  setLoading(true);
62
 
63
  try {
 
64
  const response = await axios.post(`${API_BASE}/chat`, {
65
  messages: [...messages, userMsg],
66
+ latitude: 0.4061,
67
+ longitude: 9.4673,
68
  city: "Libreville",
69
  thinking_mode: true
70
+ }, {
71
+ timeout: 120000 // 120 secondes pour les requêtes complexes
72
  });
73
 
74
+ const aiResponse = response.data.response || "Réponse reçue du serveur.";
75
  const aiImages = response.data.images || [];
76
+ const contextInfo = response.data.context_info || {};
77
 
78
  setMessages(prev => [...prev, {
79
  role: "assistant",
80
  content: aiResponse,
81
  images: aiImages,
82
+ context_info: contextInfo,
83
+ timestamp: new Date().toISOString()
84
  }]);
85
+
86
+ // Mise à jour du status après la réponse
87
+ fetchStatus();
88
  } catch (error) {
89
+ console.error("Erreur lors de l'appel au backend:", error);
90
+ let errorMsg = "Erreur : impossible de contacter le serveur IA.";
91
+
92
+ if (error.code === 'ERR_NETWORK') {
93
+ errorMsg = "⚠️ Serveur injoignable. Vérifiez que votre backend est lancé sur http://localhost:8000";
94
+ } else if (error.code === 'ECONNABORTED') {
95
+ errorMsg = "⏱️ Timeout : la requête a pris trop de temps. Le modèle est peut-être surchargé.";
96
+ } else if (error.response?.status === 400) {
97
+ errorMsg = `❌ Erreur de requête : ${error.response.data.detail || 'Format invalide'}`;
98
+ } else if (error.response?.status === 500) {
99
+ errorMsg = "🔥 Erreur serveur interne. Vérifiez les logs du backend.";
100
+ } else if (error.response?.data?.detail) {
101
+ errorMsg = `Erreur serveur : ${error.response.data.detail}`;
102
+ }
103
+
104
  setMessages(prev => [...prev, {
105
  role: "assistant",
106
+ content: errorMsg,
107
+ error: true,
108
+ timestamp: new Date().toISOString()
109
  }]);
110
  } finally {
111
  setLoading(false);
112
  }
113
  };
114
 
115
+ // Upload de fichiers PDF avec feedback de progression
116
  const handleFileUpload = async (e) => {
117
+ const files = Array.from(e.target.files);
118
+ if (!files.length) return;
119
+
120
+ setUploading(true);
121
+ setUploadProgress({ current: 0, total: files.length });
122
+
123
+ const formData = new FormData();
124
+ files.forEach(file => formData.append('files', file));
125
+
126
+ try {
127
+ const res = await axios.post(`${API_BASE}/upload`, formData, {
128
+ headers: { 'Content-Type': 'multipart/form-data' },
129
+ timeout: 180000, // 3 minutes pour les gros PDFs
130
+ onUploadProgress: (progressEvent) => {
131
+ const percentCompleted = Math.round((progressEvent.loaded * 100) / progressEvent.total);
132
+ setUploadProgress(prev => ({ ...prev, percent: percentCompleted }));
133
+ }
134
+ });
135
+
136
+ const chunksAdded = res.data.chunks_added || 0;
137
+ const filesProcessed = res.data.files_processed || 0;
138
+ const totalChunks = res.data.total_doc_chunks || 0;
139
+
140
+ // Mise à jour du status immédiate
141
+ setStatus(prev => ({ ...prev, doc_chunks: totalChunks }));
142
+
143
+ setMessages(prev => [...prev, {
144
+ role: "assistant",
145
+ content: `✅ **Import réussi !**\n\n📄 ${filesProcessed} document(s) traité(s)\n📊 ${chunksAdded} nouveaux segments ajoutés\n💾 Total en base : **${totalChunks} chunks**\n\nVous pouvez maintenant poser des questions sur ces documents !`,
146
+ success: true,
147
+ timestamp: new Date().toISOString()
148
+ }]);
149
+
150
+ // Refresh complet du status
151
+ fetchStatus();
152
+ } catch (err) {
153
+ console.error("Erreur upload:", err);
154
+ let errorMsg = "❌ Échec de l'import des documents.";
155
+
156
+ if (err.code === 'ECONNABORTED') {
157
+ errorMsg += " Timeout : les fichiers sont peut-être trop volumineux.";
158
+ } else if (err.response?.data?.detail) {
159
+ errorMsg += ` Détails : ${err.response.data.detail}`;
160
+ }
161
+
162
+ setMessages(prev => [...prev, {
163
+ role: "assistant",
164
+ content: errorMsg,
165
+ error: true,
166
+ timestamp: new Date().toISOString()
167
+ }]);
168
+ } finally {
169
+ setUploading(false);
170
+ setUploadProgress(null);
171
+ e.target.value = '';
172
+ }
173
  };
174
 
175
+ // Réinitialisation du chat ET de la mémoire
176
+ const handleReset = async () => {
177
+ if (!window.confirm("Voulez-vous réinitialiser la conversation ET effacer la mémoire du modèle ?")) {
178
+ return;
179
+ }
180
+
181
+ try {
182
+ // Appeler l'endpoint de clear memory
183
+ await axios.post(`${API_BASE}/clear-memory`, {}, { timeout: 5000 });
184
+ setMessages([]);
185
+ fetchStatus();
186
+
187
+ setMessages([{
188
+ role: "assistant",
189
+ content: "✅ Conversation et mémoire réinitialisées avec succès.",
190
+ success: true,
191
+ timestamp: new Date().toISOString()
192
+ }]);
193
+ } catch (error) {
194
+ console.error("Erreur lors de la réinitialisation:", error);
195
+ setMessages([{
196
+ role: "assistant",
197
+ content: "⚠️ Chat réinitialisé, mais impossible de contacter le serveur pour effacer la mémoire.",
198
+ error: true
199
+ }]);
200
+ }
201
+ };
202
+
203
+ // Toggle thinking pour chaque message
204
+ const toggleThinking = (msgIndex) => {
205
+ setShowThinking(prev => ({ ...prev, [msgIndex]: !prev[msgIndex] }));
206
+ };
207
+
208
+ // Formatage du temps relatif
209
+ const formatTimeAgo = (timestamp) => {
210
+ if (!timestamp) return "";
211
+ const seconds = Math.floor((new Date() - new Date(timestamp)) / 1000);
212
+ if (seconds < 60) return "à l'instant";
213
+ if (seconds < 3600) return `il y a ${Math.floor(seconds / 60)}m`;
214
+ if (seconds < 86400) return `il y a ${Math.floor(seconds / 3600)}h`;
215
+ return `il y a ${Math.floor(seconds / 86400)}j`;
216
+ };
217
+
218
+ // Styles centralisés
219
  const styles = {
220
  container: { display: 'flex', height: '100vh', backgroundColor: '#020617', color: '#f8fafc', fontFamily: 'system-ui, -apple-system, sans-serif' },
221
+ sidebar: { width: '340px', backgroundColor: '#0f172a', borderRight: '1px solid #1e293b', display: 'flex', flexDirection: 'column', overflowY: 'auto' },
222
  main: { flex: 1, display: 'flex', flexDirection: 'column', position: 'relative', background: 'radial-gradient(circle at 50% 0%, #1e293b 0%, #020617 100%)' },
223
  messageArea: { flex: 1, overflowY: 'auto', padding: '2rem 10% 2rem 10%' },
224
  userBubble: { backgroundColor: '#059669', color: 'white', padding: '1rem 1.25rem', borderRadius: '1.25rem 1.25rem 0.25rem 1.25rem', boxShadow: '0 4px 15px rgba(5, 150, 105, 0.2)' },
225
  aiBubble: { backgroundColor: '#1e293b', border: '1px solid #334155', color: '#f1f5f9', padding: '1rem 1.25rem', borderRadius: '1.25rem 1.25rem 1.25rem 0.25rem', boxShadow: '0 10px 30px rgba(0,0,0,0.5)' },
226
+ successBubble: { backgroundColor: '#064e3b', border: '1px solid #059669', color: '#d1fae5' },
227
+ errorBubble: { backgroundColor: '#7f1d1d', border: '1px solid #991b1b', color: '#fca5a5' },
228
  inputContainer: { padding: '2rem', background: 'linear-gradient(to top, #020617 70%, transparent)' },
229
  inputWrapper: { display: 'flex', alignItems: 'center', backgroundColor: 'rgba(30, 41, 59, 0.7)', backdropFilter: 'blur(20px)', border: '1px solid #334155', borderRadius: '1.5rem', padding: '0.6rem 1.2rem', maxWidth: '900px', margin: '0 auto' },
230
  inputField: { flex: 1, background: 'transparent', border: 'none', color: 'white', padding: '0.8rem', outline: 'none', fontSize: '1rem' },
231
  sendBtn: { backgroundColor: '#10b981', color: 'white', border: 'none', borderRadius: '1rem', width: '45px', height: '45px', display: 'flex', alignItems: 'center', justifyContent: 'center', cursor: 'pointer', transition: 'all 0.2s' },
232
+ toolBadge: { display: 'flex', alignItems: 'center', gap: '6px', fontSize: '11px', backgroundColor: '#020617', padding: '6px 12px', borderRadius: '10px', border: '1px solid #1e293b', color: '#94a3b8' },
233
+ statCard: { backgroundColor: 'rgba(2, 6, 23, 0.5)', borderRadius: '1rem', padding: '1rem', border: '1px solid #1e293b', marginBottom: '0.8rem' }
234
  };
235
 
236
  return (
 
239
  <aside style={styles.sidebar}>
240
  <div style={{ padding: '2rem', borderBottom: '1px solid #1e293b', display: 'flex', alignItems: 'center', gap: '1rem' }}>
241
  <div style={{ width: '45px', height: '45px', backgroundColor: '#1e293b', borderRadius: '12px', display: 'flex', alignItems: 'center', justifyContent: 'center', border: '1px solid #334155' }}>
242
+ <img src={LOGO_PATH} alt="K" style={{ width: '30px', height: '30px', objectFit: 'contain' }} onError={(e) => e.target.style.display = 'none'} />
243
  </div>
244
  <div>
245
+ <h1 style={{ fontSize: '1.3rem', fontWeight: '900', margin: 0, letterSpacing: '-0.5px' }}>
246
+ Kibali <span style={{ color: '#10b981' }}>AI</span>
247
+ </h1>
248
+ <p style={{ fontSize: '10px', color: '#475569', margin: 0, marginTop: '4px', fontWeight: '700' }}>
249
+ v1.0 • {status.status === 'ready' ? '🟢 ONLINE' : '🔴 OFFLINE'}
250
+ </p>
251
  </div>
252
  </div>
253
 
254
+ <div style={{ padding: '2rem', flex: 1, display: 'flex', flexDirection: 'column', gap: '2rem' }}>
255
+ {/* Upload de documents */}
256
+ <div>
257
+ <h2 style={{ fontSize: '11px', color: '#475569', textTransform: 'uppercase', letterSpacing: '1.5px', marginBottom: '1rem', fontWeight: '800' }}>
258
+ 📚 Documents
259
+ </h2>
260
+ <label style={{
261
+ display: 'flex',
262
+ flexDirection: 'column',
263
+ alignItems: 'center',
264
+ justifyContent: 'center',
265
+ height: '130px',
266
+ border: uploading ? '2px solid #10b981' : '2px dashed #1e293b',
267
+ borderRadius: '1.5rem',
268
+ cursor: uploading ? 'not-allowed' : 'pointer',
269
+ transition: 'all 0.3s',
270
+ backgroundColor: uploading ? 'rgba(16, 185, 129, 0.05)' : '#020617',
271
+ position: 'relative',
272
+ overflow: 'hidden'
273
+ }}>
274
+ {uploading ? (
275
+ <>
276
+ <Loader2 className="animate-spin" color="#10b981" size={32} />
277
+ <span style={{ fontSize: '12px', marginTop: '12px', color: '#10b981', fontWeight: '600' }}>
278
+ {uploadProgress?.percent ? `${uploadProgress.percent}%` : 'Traitement...'}
279
+ </span>
280
+ {uploadProgress?.total && (
281
+ <span style={{ fontSize: '10px', color: '#64748b', marginTop: '4px' }}>
282
+ {uploadProgress.current}/{uploadProgress.total} fichiers
283
+ </span>
284
+ )}
285
+ </>
286
+ ) : (
287
+ <>
288
+ <Upload color="#10b981" size={32} />
289
+ <span style={{ fontSize: '12px', marginTop: '12px', color: '#64748b', fontWeight: '500' }}>
290
+ Importer rapports PDF
291
+ </span>
292
+ <span style={{ fontSize: '10px', color: '#475569', marginTop: '4px' }}>
293
+ Plusieurs fichiers acceptés
294
+ </span>
295
+ </>
296
+ )}
297
+ <input type="file" hidden multiple accept=".pdf" onChange={handleFileUpload} disabled={uploading} />
298
+ </label>
299
+ </div>
300
+
301
+ {/* Statistiques enrichies */}
302
+ <div>
303
+ <h3 style={{ fontSize: '11px', color: '#475569', textTransform: 'uppercase', marginBottom: '1rem', fontWeight: '800', letterSpacing: '1.5px' }}>
304
+ 📊 Statistiques
305
+ </h3>
306
+
307
+ {/* Base de connaissances */}
308
+ <div style={styles.statCard}>
309
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
310
+ <span style={{ color: '#64748b', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
311
+ <Database size={14} color="#10b981" /> Base documentaire
312
+ </span>
313
+ <span style={{ color: '#10b981', fontWeight: 'bold', fontSize: '15px' }}>
314
+ {status.doc_chunks || 0}
315
+ </span>
316
+ </div>
317
+ <div style={{ fontSize: '10px', color: '#475569', marginTop: '6px' }}>
318
+ segments vectorisés
319
+ </div>
320
  </div>
321
+
322
+ {/* Mémoire conversationnelle */}
323
+ <div style={styles.statCard}>
324
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
325
+ <span style={{ color: '#64748b', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
326
+ <Brain size={14} color="#3b82f6" /> Mémoire active
327
+ </span>
328
+ <span style={{ color: '#3b82f6', fontWeight: 'bold', fontSize: '15px' }}>
329
+ {status.memory_entries || 0}
330
+ </span>
331
+ </div>
332
+ <div style={{ fontSize: '10px', color: '#475569', marginTop: '6px' }}>
333
+ échanges mémorisés
334
  </div>
 
 
 
335
  </div>
 
336
 
337
+ {/* Contexte conversationnel */}
338
+ {status.current_subject && (
339
+ <div style={styles.statCard}>
340
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
341
+ <span style={{ color: '#64748b', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
342
+ <TrendingUp size={14} color="#f59e0b" /> Sujet actuel
343
+ </span>
344
+ <span style={{ color: '#f59e0b', fontWeight: 'bold', fontSize: '15px' }}>
345
+ {status.subject_message_count}
346
+ </span>
347
+ </div>
348
+ <div style={{ fontSize: '10px', color: '#475569', marginTop: '6px', overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>
349
+ {status.current_subject}
350
+ </div>
351
+ </div>
352
+ )}
353
 
354
+ {/* Localisation */}
355
+ <div style={styles.statCard}>
356
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
357
+ <span style={{ color: '#64748b', fontSize: '12px' }}>Position</span>
358
+ <span style={{ color: '#f1f5f9', display: 'flex', alignItems: 'center', gap: '6px', fontWeight: '600', fontSize: '13px' }}>
359
+ <MapPin size={14} color="#ef4444" /> Libreville
360
+ </span>
361
+ </div>
362
  </div>
363
+
364
+ {/* Santé du système */}
365
+ <div style={styles.statCard}>
366
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
367
+ <span style={{ color: '#64748b', fontSize: '12px', display: 'flex', alignItems: 'center', gap: '6px' }}>
368
+ <Zap size={14} color={status.torch_cuda_available ? '#10b981' : '#f59e0b'} /> GPU
369
+ </span>
370
+ <span style={{ color: status.torch_cuda_available ? '#10b981' : '#f59e0b', fontWeight: 'bold', fontSize: '11px' }}>
371
+ {status.torch_cuda_available ? 'ACTIVÉ' : 'CPU ONLY'}
372
+ </span>
373
+ </div>
374
  </div>
375
  </div>
376
  </div>
377
 
378
+ <button
379
+ onClick={handleReset}
380
+ style={{
381
+ margin: '2rem',
382
+ background: '#020617',
383
+ border: '1px solid #1e293b',
384
+ color: '#475569',
385
+ padding: '14px',
386
+ borderRadius: '14px',
387
+ cursor: 'pointer',
388
+ display: 'flex',
389
+ alignItems: 'center',
390
+ justifyContent: 'center',
391
+ gap: '10px',
392
+ fontWeight: '700',
393
+ fontSize: '12px',
394
+ transition: 'all 0.2s'
395
+ }}
396
+ onMouseEnter={(e) => {
397
+ e.target.style.borderColor = '#ef4444';
398
+ e.target.style.color = '#ef4444';
399
+ }}
400
+ onMouseLeave={(e) => {
401
+ e.target.style.borderColor = '#1e293b';
402
+ e.target.style.color = '#475569';
403
+ }}
404
+ >
405
+ <Trash2 size={16} /> RÉINITIALISER TOUT
406
  </button>
407
  </aside>
408
 
409
+ {/* MAIN CONTENT */}
410
  <main style={styles.main}>
411
  <div style={styles.messageArea}>
412
+ {messages.length === 0 && (
413
  <div style={{ height: '80%', display: 'flex', flexDirection: 'column', alignItems: 'center', justifyContent: 'center', textAlign: 'center' }}>
414
+ <div style={{
415
+ width: '100px',
416
+ height: '100px',
417
+ backgroundColor: '#0f172a',
418
+ borderRadius: '30px',
419
+ display: 'flex',
420
+ alignItems: 'center',
421
+ justifyContent: 'center',
422
+ marginBottom: '2rem',
423
+ border: '1px solid #334155',
424
+ boxShadow: '0 20px 50px rgba(0,0,0,0.5)'
425
+ }}>
426
+ <img src={LOGO_PATH} alt="Kibali" style={{ width: '60px' }} onError={(e) => e.target.style.display = 'none'} />
427
  </div>
428
+ <h2 style={{ fontSize: '2.5rem', fontWeight: '900', marginBottom: '0.8rem', letterSpacing: '-1px' }}>
429
+ Bienvenue sur Kibali AI
430
+ </h2>
431
  <p style={{ color: '#64748b', maxWidth: '450px', lineHeight: '1.7', fontSize: '1.1rem' }}>
432
+ Assistant IA expert du Gabon avec mémoire contextuelle et analyse documentaire avancée.
433
  </p>
434
+ <div style={{ display: 'flex', gap: '1rem', marginTop: '2rem' }}>
435
+ <div style={{ textAlign: 'center' }}>
436
+ <div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#10b981' }}>{status.doc_chunks}</div>
437
+ <div style={{ fontSize: '12px', color: '#64748b' }}>Chunks PDF</div>
438
+ </div>
439
+ <div style={{ textAlign: 'center' }}>
440
+ <div style={{ fontSize: '2rem', fontWeight: 'bold', color: '#3b82f6' }}>{status.memory_entries}</div>
441
+ <div style={{ fontSize: '12px', color: '#64748b' }}>Mémoires</div>
442
+ </div>
443
+ </div>
444
  </div>
445
  )}
446
 
447
  {messages.map((m, i) => (
448
  <div key={i} style={{ display: 'flex', justifyContent: m.role === 'user' ? 'flex-end' : 'flex-start', marginBottom: '3rem' }}>
449
  <div style={{ display: 'flex', gap: '1.2rem', maxWidth: '85%', flexDirection: m.role === 'user' ? 'row-reverse' : 'row' }}>
450
+ <div style={{
451
+ width: '45px',
452
+ height: '45px',
453
+ borderRadius: '14px',
454
+ display: 'flex',
455
+ alignItems: 'center',
456
+ justifyContent: 'center',
457
+ backgroundColor: m.role === 'user' ? '#059669' : m.error ? '#7f1d1d' : m.success ? '#064e3b' : '#1e293b',
458
+ flexShrink: 0,
459
+ border: '1px solid #334155'
460
+ }}>
461
  {m.role === 'user' ? (
462
+ <User size={24} color="white" />
463
+ ) : m.error ? (
464
+ <AlertCircle size={24} color="#ef4444" />
465
+ ) : m.success ? (
466
+ <CheckCircle size={24} color="#10b981" />
467
  ) : (
468
+ <img src={LOGO_PATH} style={{ width: '28px' }} alt="K" onError={(e) => e.target.style.display = 'none'} />
469
  )}
470
  </div>
471
 
472
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '0.8rem', width: '100%' }}>
473
+ {/* Informations contextuelles */}
474
+ {m.role === 'assistant' && !m.error && !m.success && m.context_info && (
475
+ <div style={{ backgroundColor: 'rgba(30, 41, 59, 0.3)', borderRadius: '12px', padding: '10px 14px', border: '1px solid #1e293b' }}>
476
+ <button
477
+ onClick={() => toggleThinking(i)}
478
+ style={{ background: 'none', border: 'none', color: '#64748b', fontSize: '11px', display: 'flex', alignItems: 'center', gap: '6px', cursor: 'pointer', fontWeight: '700', textTransform: 'uppercase', width: '100%', justifyContent: 'space-between' }}
479
+ >
480
+ <span style={{ display: 'flex', alignItems: 'center', gap: '6px' }}>
481
+ <Brain size={14} color="#10b981" /> Intelligence contextuelle
482
+ </span>
483
+ {showThinking[i] ? <ChevronUp size={14} /> : <ChevronDown size={14} />}
484
  </button>
485
+
486
+ {showThinking[i] && (
487
+ <>
488
+ <div style={{ display: 'flex', gap: '8px', marginTop: '12px', flexWrap: 'wrap' }}>
489
+ <div style={styles.toolBadge}>
490
+ <Globe size={13} color="#3b82f6" /> Web Search
491
+ {m.context_info.web_results > 0 && <span style={{ color: '#10b981' }}>• {m.context_info.web_results}</span>}
492
+ </div>
493
+ <div style={styles.toolBadge}>
494
+ <FileText size={13} color="#10b981" /> PDF Vault
495
+ {m.context_info.rag_sources?.length > 0 && <span style={{ color: '#10b981' }}>• {m.context_info.rag_sources.length}</span>}
496
+ </div>
497
+ <div style={styles.toolBadge}>
498
+ <Brain size={13} color="#f59e0b" /> Mémoire
499
+ {m.context_info.memory_used > 0 && <span style={{ color: '#10b981' }}>• {m.context_info.memory_used}</span>}
500
+ </div>
501
+ <div style={styles.toolBadge}>
502
+ <MapPin size={13} color="#ef4444" /> Geo-Context
503
+ </div>
504
+ </div>
505
+
506
+ {/* Détails du contexte */}
507
+ <div style={{ marginTop: '12px', fontSize: '11px', color: '#64748b', padding: '10px', backgroundColor: '#020617', borderRadius: '8px' }}>
508
+ {m.context_info.subject_keywords?.length > 0 && (
509
+ <div style={{ marginBottom: '8px' }}>
510
+ <strong style={{ color: '#94a3b8' }}>Mots-clés du sujet :</strong> {m.context_info.subject_keywords.join(', ')}
511
+ </div>
512
+ )}
513
+ {m.context_info.message_count > 0 && (
514
+ <div style={{ marginBottom: '8px' }}>
515
+ <strong style={{ color: '#94a3b8' }}>Messages sur ce sujet :</strong> {m.context_info.message_count}
516
+ </div>
517
+ )}
518
+ {m.context_info.rag_sources?.length > 0 && (
519
+ <div>
520
+ <strong style={{ color: '#94a3b8' }}>Sources documentaires :</strong> {m.context_info.rag_sources.join(', ')}
521
+ </div>
522
+ )}
523
+ </div>
524
+ </>
525
  )}
526
  </div>
527
  )}
528
+
529
+ {/* Bulle de message */}
530
+ <div style={{
531
+ ...(m.role === 'user' ? styles.userBubble : m.error ? styles.errorBubble : m.success ? styles.successBubble : styles.aiBubble),
532
+ position: 'relative'
533
+ }}>
534
+ <p style={{ margin: 0, fontSize: '1.05rem', lineHeight: '1.6', fontWeight: '400', whiteSpace: 'pre-wrap' }}>
535
+ {m.content}
536
+ </p>
537
+ {m.timestamp && (
538
+ <div style={{ fontSize: '10px', color: 'rgba(255,255,255,0.4)', marginTop: '8px', display: 'flex', alignItems: 'center', gap: '4px' }}>
539
+ <Clock size={10} /> {formatTimeAgo(m.timestamp)}
540
+ </div>
541
+ )}
542
  </div>
543
+
544
+ {/* Images */}
545
  {m.images && m.images.length > 0 && (
546
+ <div style={{ display: 'flex', gap: '12px', marginTop: '8px', overflowX: 'auto', paddingBottom: '10px' }}>
547
  {m.images.map((img, idx) => (
548
+ <div key={idx} style={{ flexShrink: 0, position: 'relative', borderRadius: '1.2rem', overflow: 'hidden', border: '2px solid #334155', backgroundColor: '#0f172a' }}>
549
+ <img
550
+ src={img}
551
+ alt={`Image ${idx + 1}`}
552
+ style={{ height: '180px', width: '280px', objectFit: 'cover' }}
553
+ onError={(e) => {
554
+ e.target.style.display = 'none';
555
+ e.target.parentElement.innerHTML = '<div style="height:180px;width:280px;display:flex;align-items:center;justify-content:center;color:#64748b;font-size:12px;"><ImageIcon size={24} /> Image indisponible</div>';
556
+ }}
557
+ />
558
  </div>
559
  ))}
560
  </div>
 
566
 
567
  {loading && (
568
  <div style={{ display: 'flex', gap: '1.2rem', alignItems: 'center' }}>
569
+ <div style={{
570
+ width: '45px',
571
+ height: '45px',
572
+ borderRadius: '14px',
573
+ backgroundColor: '#1e293b',
574
+ display: 'flex',
575
+ alignItems: 'center',
576
+ justifyContent: 'center',
577
+ border: '1px solid #334155'
578
+ }}>
579
  <Loader2 className="animate-spin" color="#10b981" size={24} />
580
  </div>
581
+ <div style={{ display: 'flex', flexDirection: 'column', gap: '6px' }}>
582
+ <div style={{ color: '#f1f5f9', fontSize: '15px', fontWeight: '600' }}>
583
+ Kibali analyse votre demande...
584
+ </div>
585
+ <div style={{ color: '#64748b', fontSize: '12px' }}>
586
+ Recherche multi-sources • Traitement vectoriel • Génération contextuelle
587
+ </div>
588
+ </div>
589
  </div>
590
  )}
591
+
592
  <div ref={scrollRef} />
593
  </div>
594
 
 
597
  <Search size={22} color="#475569" style={{ marginRight: '10px' }} />
598
  <input
599
  style={styles.inputField}
600
+ placeholder="Posez votre question sur le Gabon, vos documents ou l'actualité..."
601
  value={input}
602
  onChange={(e) => setInput(e.target.value)}
603
+ onKeyDown={(e) => {
604
+ if (e.key === 'Enter' && !e.shiftKey) {
605
+ e.preventDefault();
606
+ handleSend();
607
+ }
608
+ }}
609
+ disabled={loading}
610
  />
611
  <button
612
+ style={{
613
+ ...styles.sendBtn,
614
+ opacity: input.trim() && !loading ? 1 : 0.4,
615
+ cursor: input.trim() && !loading ? 'pointer' : 'not-allowed',
616
+ transform: input.trim() && !loading ? 'scale(1)' : 'scale(0.95)'
617
+ }}
618
  onClick={handleSend}
619
+ disabled={loading || !input.trim()}
620
+ onMouseEnter={(e) => {
621
+ if (input.trim() && !loading) {
622
+ e.target.style.backgroundColor = '#059669';
623
+ e.target.style.transform = 'scale(1.05)';
624
+ }
625
+ }}
626
+ onMouseLeave={(e) => {
627
+ e.target.style.backgroundColor = '#10b981';
628
+ e.target.style.transform = 'scale(1)';
629
+ }}
630
  >
631
+ {loading ? <Loader2 className="animate-spin" size={20} /> : <Send size={20} />}
632
  </button>
633
  </div>
634
+
635
+ <div style={{ textAlign: 'center', marginTop: '1.5rem', display: 'flex', flexDirection: 'column', gap: '0.5rem', alignItems: 'center' }}>
636
+ <p style={{
637
+ color: '#334155',
638
+ fontSize: '10px',
639
+ fontWeight: '800',
640
+ letterSpacing: '2px',
641
+ margin: 0
642
+ }}>
643
+ GABONESE SOVEREIGN ARTIFICIAL INTELLIGENCE
644
+ </p>
645
+ <div style={{ display: 'flex', gap: '1rem', fontSize: '9px', color: '#1e293b', fontWeight: '700' }}>
646
+ <span>SYSTEM KIBALI-1</span>
647
+ <span>•</span>
648
+ <span>SETRAF-GABON</span>
649
+ <span>•</span>
650
+ <span style={{ color: status.torch_cuda_available ? '#10b981' : '#f59e0b' }}>
651
+ {status.torch_cuda_available ? '⚡ GPU ACCELERATED' : '⚙️ CPU MODE'}
652
+ </span>
653
+ </div>
654
+ </div>
655
  </div>
656
  </main>
657
  </div>
main.py CHANGED
@@ -12,6 +12,9 @@ from threading import Thread
12
  import os
13
  from io import BytesIO
14
  import logging
 
 
 
15
 
16
  # --- CONFIGURATION LOGGING ---
17
  logging.basicConfig(level=logging.INFO)
@@ -30,30 +33,13 @@ except ImportError:
30
  raise ImportError("Installe pypdf ou PyPDF2 : pip install pypdf")
31
 
32
  # --- OUTILS PERSONNALISÉS ---
33
- try:
34
- from tools.web import web_search
35
- except ImportError:
36
- logger.warning("Module tools.web non trouvé, utilisation d'une fonction stub")
37
- def web_search(query):
38
- return {"results": [], "images": []}
39
-
40
- try:
41
- from tools.todo import execute_reflection_plan
42
- except ImportError:
43
- logger.warning("Module tools.todo non trouvé")
44
- def execute_reflection_plan(*args, **kwargs):
45
- return None
46
-
47
- try:
48
- from tools.geo import get_geo_context
49
- except ImportError:
50
- logger.warning("Module tools.geo non trouvé")
51
- def get_geo_context(*args, **kwargs):
52
- return {}
53
 
54
  app = FastAPI(title="Kibali AI API", version="1.0")
55
 
56
- # --- SERVEUR STATIQUE (logo, etc.) ---
57
  script_dir = os.path.dirname(os.path.abspath(__file__))
58
  static_dir = os.path.join(script_dir, "static")
59
  os.makedirs(static_dir, exist_ok=True)
@@ -68,58 +54,99 @@ app.add_middleware(
68
  allow_headers=["*"],
69
  )
70
 
71
- # --- CHARGEMENT DES MODÈLES DEPUIS HUGGING FACE HUB ---
72
- MODEL_REPO = "BelikanM/kibali-final-merged" # Ton modèle sur HF Hub
73
-
74
  logger.info("Chargement du modèle d'embedding...")
75
- try:
76
- embed_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
77
- logger.info("Modèle d'embedding chargé avec succès")
78
- except Exception as e:
79
- logger.error(f"Erreur chargement embedding model : {e}")
80
- raise
 
 
 
 
 
 
81
 
82
- logger.info(f"Chargement du tokenizer depuis {MODEL_REPO}...")
83
- try:
84
- tokenizer = AutoTokenizer.from_pretrained(MODEL_REPO, trust_remote_code=True)
85
- if tokenizer.pad_token is None:
86
- tokenizer.pad_token = tokenizer.eos_token
87
- logger.info("Tokenizer chargé avec succès")
88
- except Exception as e:
89
- logger.error(f"Erreur chargement tokenizer : {e}")
90
- raise
91
-
92
- logger.info(f"Chargement du modèle LLM depuis {MODEL_REPO}...")
93
- try:
94
- bnb_config = BitsAndBytesConfig(
95
- load_in_4bit=True,
96
- bnb_4bit_use_double_quant=True,
97
- bnb_4bit_quant_type="nf4",
98
- bnb_4bit_compute_dtype=torch.float16
99
- )
100
-
101
- model = AutoModelForCausalLM.from_pretrained(
102
- MODEL_REPO,
103
- quantization_config=bnb_config,
104
- device_map="auto",
105
- torch_dtype=torch.float16,
106
- trust_remote_code=True,
107
- low_cpu_mem_usage=True
108
- )
109
- logger.info(f"Modèle LLM chargé avec succès sur {model.device}")
110
- except Exception as e:
111
- logger.error(f"Erreur chargement modèle LLM : {e}")
112
- raise
113
-
114
- # --- BASES VECTORIELLES (séparées) ---
115
  dimension = 384
116
- # Index pour les documents PDF
117
  doc_index = faiss.IndexFlatL2(dimension)
118
  doc_chunks: List[str] = []
 
119
 
120
- # Index séparé pour la mémoire conversationnelle
121
  memory_index = faiss.IndexFlatL2(dimension)
122
  memory_texts: List[str] = []
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  # --- MODÈLES PYDANTIC ---
125
  class Message(BaseModel):
@@ -136,10 +163,10 @@ class ChatRequest(BaseModel):
136
  class ChatResponse(BaseModel):
137
  response: str
138
  images: List[str] = []
 
139
 
140
  # --- UTILITAIRES ---
141
  def extract_text_from_pdf(pdf_bytes: bytes) -> str:
142
- """Extrait le texte d'un PDF depuis des bytes"""
143
  text = ""
144
  try:
145
  pdf_file = BytesIO(pdf_bytes)
@@ -148,41 +175,105 @@ def extract_text_from_pdf(pdf_bytes: bytes) -> str:
148
  page_text = page.extract_text()
149
  if page_text:
150
  text += page_text + "\n"
 
151
  except Exception as e:
152
  logger.error(f"Erreur extraction PDF : {e}")
153
- return text
154
 
155
  def chunk_text(text: str, chunk_size: int = 400, overlap: int = 50) -> List[str]:
156
- """Découpe un texte en chunks avec overlap"""
 
157
  words = text.split()
158
  chunks = []
159
  i = 0
160
  while i < len(words):
161
- chunk = " ".join(words[i:i + chunk_size])
162
- chunks.append(chunk)
 
 
163
  i += chunk_size - overlap
164
- if i >= len(words) and len(chunks) > 0:
165
  break
166
  return chunks
167
 
168
- # --- ROUTES ---
169
- @app.get("/")
170
- async def root():
171
- """Route racine - information API"""
172
- return {
173
- "message": "Kibali AI API - Prêt",
174
- "version": "1.0",
175
- "endpoints": {
176
- "status": "/status",
177
- "chat": "/chat",
178
- "upload": "/upload-pdfs",
179
- "docs": "/docs"
180
- }
 
 
 
 
181
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
182
 
 
183
  @app.get("/status")
184
  async def status():
185
- """Statut de l'API et des ressources chargées"""
186
  return {
187
  "status": "ready",
188
  "doc_chunks": len(doc_chunks),
@@ -190,188 +281,215 @@ async def status():
190
  "pdf_library": PDF_READER,
191
  "model_device": str(model.device),
192
  "torch_cuda_available": torch.cuda.is_available(),
193
- "model_repo": MODEL_REPO
 
194
  }
195
 
196
  @app.post("/chat", response_model=ChatResponse)
197
  async def chat(request: ChatRequest):
198
- """Endpoint principal pour le chat avec Kibali"""
199
- try:
200
- user_message = request.messages[-1].content.strip()
201
- if not user_message:
202
- raise HTTPException(status_code=400, detail="Message vide")
203
-
204
- geo = {
205
- "latitude": request.latitude,
206
- "longitude": request.longitude,
207
- "city": request.city or "Libreville"
208
- }
209
-
210
- # 1. RAG Documents (PDF uploadés)
211
- rag_context = ""
212
- if doc_index.ntotal > 0 and len(doc_chunks) > 0:
213
- try:
214
- query_emb = embed_model.encode([user_message], normalize_embeddings=True).astype('float32')
215
- D, I = doc_index.search(query_emb, k=5)
216
- relevant_chunks = [doc_chunks[i] for i in I[0] if 0 <= i < len(doc_chunks)]
217
- rag_context = "\n\n".join([f"Document : {chunk[:1000]}" for chunk in relevant_chunks])
218
- except Exception as e:
219
- logger.error(f"Erreur RAG documents : {e}")
220
-
221
- # 2. Mémoire conversationnelle (historique du chat indexé vectoriellement)
222
- memory_context = ""
223
- if memory_index.ntotal > 0 and len(memory_texts) > 0:
224
- try:
225
- query_emb = embed_model.encode([user_message], normalize_embeddings=True).astype('float32')
226
- D, I = memory_index.search(query_emb, k=5)
227
- relevant_mem = [memory_texts[i] for i in I[0] if 0 <= i < len(memory_texts)]
228
- memory_context = "\n\n".join(relevant_mem)
229
- except Exception as e:
230
- logger.error(f"Erreur RAG mémoire : {e}")
231
-
232
- # 3. Recherche Web (pour éviter les hallucinations avec infos récentes)
233
- web_context = ""
234
- web_images = []
235
- try:
236
- search_results = web_search(user_message + " Gabon")
237
- web_context = "\n".join([f"- {r['content'][:500]}" for r in search_results.get("results", [])[:6]])
238
- web_images = search_results.get("images", [])[:4]
239
- except Exception as e:
240
- logger.error(f"Erreur recherche web : {e}")
 
 
 
 
 
 
 
 
 
 
 
241
 
242
- # 4. Prompt final renforcé pour précision, cohérence et réduction des hallucinations
243
- system_prompt = f"""Tu es Kibali, un assistant IA chaleureux, précis et expert du Gabon, basé à {geo['city']}.
244
  Réponds toujours en français, de façon naturelle, concise et factuelle.
245
- Utilise uniquement les informations fournies dans les contextes ci-dessous ou ta connaissance vérifiée.
246
- Si tu n'es pas sûr d'une information, dis-le clairement plutôt que d'inventer.
247
- Ne répète jamais une réponse précédente et n'ajoute pas d'informations inutiles."""
248
 
249
- full_prompt = f"""### INSTRUCTIONS STRICTES :
 
 
 
 
 
 
 
 
 
 
 
 
250
  {system_prompt}
251
 
252
- ### CONTEXTE DOCUMENTS (sources fiables uploadées) :
253
- {rag_context}
254
 
255
- ### HISTORIQUE DE LA CONVERSATION (contexte du fil du chat) :
256
- {memory_context}
257
 
258
- ### INFORMATIONS RÉCENTES DU WEB (vérifiées) :
259
- {web_context}
260
 
261
- ### QUESTION DE L'UTILISATEUR :
262
  {user_message}
263
 
264
- ### RÉPONSE (en français uniquement, directe, précise, sans répétition ni hallucination) :
265
  """
266
 
267
- inputs = tokenizer(full_prompt, return_tensors="pt", truncation=True, max_length=8192).to(model.device)
268
-
269
- streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True, timeout=120.0)
270
-
271
- def generate_stream():
272
- try:
273
- model.generate(
274
- **inputs,
275
- streamer=streamer,
276
- max_new_tokens=1024,
277
- temperature=0.6,
278
- do_sample=True,
279
- top_p=0.85,
280
- top_k=50,
281
- repetition_penalty=1.2,
282
- length_penalty=0.8
283
- )
284
- except Exception as e:
285
- logger.error(f"Erreur génération : {e}")
286
-
287
- thread = Thread(target=generate_stream)
288
- thread.start()
289
-
290
- response_text = ""
291
- try:
292
- for new_text in streamer:
293
- if new_text is None:
294
- break
295
- response_text += new_text
296
- except Exception as e:
297
- logger.error(f"Erreur streaming : {e}")
298
- response_text = "Désolé, une erreur est survenue pendant la génération."
299
 
300
- response_text = response_text.strip()
301
 
302
- # 5. Mise à jour automatique de la base vectorielle de mémoire
303
- if response_text and response_text != "Désolé, une erreur est survenue pendant la génération.":
304
- try:
305
- memory_entry = f"Utilisateur : {user_message}\nKibali : {response_text}"
306
- memory_texts.append(memory_entry)
307
- mem_emb = embed_model.encode([memory_entry], normalize_embeddings=True).astype('float32')
308
- memory_index.add(mem_emb)
309
- except Exception as e:
310
- logger.error(f"Erreur mise à jour mémoire : {e}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
311
 
312
- return ChatResponse(response=response_text, images=web_images)
313
 
314
- except HTTPException as he:
315
- raise he
316
- except Exception as e:
317
- logger.error(f"Erreur inattendue dans /chat : {e}")
318
- raise HTTPException(status_code=500, detail=f"Erreur serveur : {str(e)}")
319
 
320
- @app.post("/upload-pdfs")
321
- async def upload_pdfs(files: List[UploadFile] = File(...)):
322
- """Upload et indexation de fichiers PDF"""
323
- added = 0
324
- errors = []
325
-
326
  for file in files:
327
  if not file.filename.lower().endswith(".pdf"):
328
- errors.append(f"{file.filename} : n'est pas un PDF")
329
  continue
330
-
331
  try:
332
  content = await file.read()
333
  text = extract_text_from_pdf(content)
334
-
335
- if text.strip():
336
- chunks = chunk_text(text)
337
- if chunks:
338
- embeddings = embed_model.encode(chunks, normalize_embeddings=True).astype('float32')
339
- doc_index.add(embeddings)
340
- doc_chunks.extend(chunks)
341
- added += len(chunks)
342
- logger.info(f"PDF {file.filename} indexé : {len(chunks)} chunks")
343
- else:
344
- errors.append(f"{file.filename} : aucun texte extrait")
345
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  except Exception as e:
347
- logger.error(f"Erreur upload {file.filename} : {e}")
348
- errors.append(f"{file.filename} : {str(e)}")
349
-
350
- return {
351
- "status": "success" if added > 0 else "partial" if errors else "failed",
352
- "chunks_added": added,
353
- "total_doc_chunks": len(doc_chunks),
354
- "errors": errors if errors else None
355
- }
356
 
357
- @app.get("/health")
358
- async def health():
359
- """Health check pour monitoring"""
360
  return {
361
- "status": "healthy",
362
- "model_loaded": model is not None,
363
- "embed_model_loaded": embed_model is not None
 
364
  }
365
 
366
- # --- MESSAGE AU DEMARRAGE ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
367
  @app.on_event("startup")
368
  async def startup_event():
369
  logger.info("🚀 Kibali AI API démarrée avec succès !")
370
- logger.info(f"Modèle : {MODEL_REPO}")
371
- logger.info(f"Device : {model.device}")
372
- logger.info(f"CUDA disponible : {torch.cuda.is_available()}")
373
- logger.info("Accès : http://localhost:8000 | Docs : http://localhost:8000/docs")
374
-
375
- @app.on_event("shutdown")
376
- async def shutdown_event():
377
- logger.info("👋 Arrêt de Kibali AI API")
 
12
  import os
13
  from io import BytesIO
14
  import logging
15
+ from datetime import datetime
16
+ import json
17
+ import hashlib
18
 
19
  # --- CONFIGURATION LOGGING ---
20
  logging.basicConfig(level=logging.INFO)
 
33
  raise ImportError("Installe pypdf ou PyPDF2 : pip install pypdf")
34
 
35
  # --- OUTILS PERSONNALISÉS ---
36
+ from tools.web import web_search
37
+ from tools.todo import execute_reflection_plan
38
+ from tools.geo import get_geo_context
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
  app = FastAPI(title="Kibali AI API", version="1.0")
41
 
42
+ # --- SERVEUR STATIQUE ---
43
  script_dir = os.path.dirname(os.path.abspath(__file__))
44
  static_dir = os.path.join(script_dir, "static")
45
  os.makedirs(static_dir, exist_ok=True)
 
54
  allow_headers=["*"],
55
  )
56
 
57
+ # --- CHARGEMENT DES MODÈLES ---
58
+ MODEL_PATH = "/home/belikan/geoscan/agent_kibali/model_cache"
 
59
  logger.info("Chargement du modèle d'embedding...")
60
+ embed_model = SentenceTransformer('paraphrase-multilingual-MiniLM-L12-v2')
61
+ logger.info("Chargement du tokenizer et du modèle LLM...")
62
+ tokenizer = AutoTokenizer.from_pretrained(MODEL_PATH, local_files_only=True)
63
+ if tokenizer.pad_token is None:
64
+ tokenizer.pad_token = tokenizer.eos_token
65
+
66
+ bnb_config = BitsAndBytesConfig(
67
+ load_in_4bit=True,
68
+ bnb_4bit_use_double_quant=True,
69
+ bnb_4bit_quant_type="nf4",
70
+ bnb_4bit_compute_dtype=torch.float16
71
+ )
72
 
73
+ model = AutoModelForCausalLM.from_pretrained(
74
+ MODEL_PATH,
75
+ quantization_config=bnb_config,
76
+ device_map="auto",
77
+ torch_dtype=torch.float16,
78
+ trust_remote_code=True,
79
+ low_cpu_mem_usage=True
80
+ )
81
+ logger.info(f"Modèle chargé sur {model.device}")
82
+
83
+ # --- BASES VECTORIELLES GLOBALES ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
84
  dimension = 384
 
85
  doc_index = faiss.IndexFlatL2(dimension)
86
  doc_chunks: List[str] = []
87
+ doc_metadata: List[dict] = [] # Métadonnées des chunks (source, timestamp, etc.)
88
 
 
89
  memory_index = faiss.IndexFlatL2(dimension)
90
  memory_texts: List[str] = []
91
+ memory_metadata: List[dict] = [] # Métadonnées des mémoires (timestamp, sujet, score)
92
+
93
+ # --- GESTION DU CONTEXTE CONVERSATIONNEL ---
94
+ class ConversationContext:
95
+ def __init__(self):
96
+ self.current_subject = None
97
+ self.subject_embedding = None
98
+ self.subject_start_time = None
99
+ self.message_count = 0
100
+ self.subject_keywords = []
101
+
102
+ def update_subject(self, message: str, embedding: np.ndarray):
103
+ """Détecte et met à jour le sujet actuel de la conversation"""
104
+ keywords = self._extract_keywords(message)
105
+
106
+ # Détection de changement de sujet
107
+ if self.subject_embedding is not None:
108
+ similarity = np.dot(embedding.flatten(), self.subject_embedding.flatten())
109
+ if similarity < 0.6: # Seuil de changement de sujet
110
+ logger.info(f"Changement de sujet détecté (similarité: {similarity:.2f})")
111
+ self._archive_current_subject()
112
+ self.current_subject = message
113
+ self.subject_embedding = embedding
114
+ self.subject_start_time = datetime.now()
115
+ self.message_count = 1
116
+ self.subject_keywords = keywords
117
+ else:
118
+ self.message_count += 1
119
+ self.subject_keywords.extend(keywords)
120
+ self.subject_keywords = list(set(self.subject_keywords))[:10] # Top 10
121
+ else:
122
+ self.current_subject = message
123
+ self.subject_embedding = embedding
124
+ self.subject_start_time = datetime.now()
125
+ self.message_count = 1
126
+ self.subject_keywords = keywords
127
+
128
+ def _extract_keywords(self, text: str) -> List[str]:
129
+ """Extrait les mots-clés importants du texte"""
130
+ stopwords = {'le', 'la', 'les', 'un', 'une', 'des', 'de', 'du', 'et', 'ou',
131
+ 'est', 'sont', 'à', 'au', 'en', 'pour', 'dans', 'sur', 'avec'}
132
+ words = text.lower().split()
133
+ keywords = [w for w in words if len(w) > 3 and w not in stopwords]
134
+ return keywords[:5]
135
+
136
+ def _archive_current_subject(self):
137
+ """Archive le sujet actuel avant de passer au suivant"""
138
+ if self.current_subject and memory_index.ntotal > 0:
139
+ # Créer un résumé du sujet archivé
140
+ summary = {
141
+ "subject": self.current_subject[:200],
142
+ "keywords": self.subject_keywords,
143
+ "message_count": self.message_count,
144
+ "duration": (datetime.now() - self.subject_start_time).seconds,
145
+ "archived_at": datetime.now().isoformat()
146
+ }
147
+ logger.info(f"Sujet archivé: {summary['keywords']}")
148
+
149
+ conversation_ctx = ConversationContext()
150
 
151
  # --- MODÈLES PYDANTIC ---
152
  class Message(BaseModel):
 
163
  class ChatResponse(BaseModel):
164
  response: str
165
  images: List[str] = []
166
+ context_info: Optional[dict] = None
167
 
168
  # --- UTILITAIRES ---
169
  def extract_text_from_pdf(pdf_bytes: bytes) -> str:
 
170
  text = ""
171
  try:
172
  pdf_file = BytesIO(pdf_bytes)
 
175
  page_text = page.extract_text()
176
  if page_text:
177
  text += page_text + "\n"
178
+ return text.strip()
179
  except Exception as e:
180
  logger.error(f"Erreur extraction PDF : {e}")
181
+ return ""
182
 
183
  def chunk_text(text: str, chunk_size: int = 400, overlap: int = 50) -> List[str]:
184
+ if not text.strip():
185
+ return []
186
  words = text.split()
187
  chunks = []
188
  i = 0
189
  while i < len(words):
190
+ chunk_words = words[i:i + chunk_size]
191
+ chunk = " ".join(chunk_words)
192
+ if chunk.strip():
193
+ chunks.append(chunk.strip())
194
  i += chunk_size - overlap
195
+ if i >= len(words) and len(chunk_words) < overlap:
196
  break
197
  return chunks
198
 
199
+ def add_to_memory_realtime(user_msg: str, ai_response: str, subject_keywords: List[str]):
200
+ """Ajoute une entrée mémoire en temps réel avec métadonnées enrichies"""
201
+ timestamp = datetime.now().isoformat()
202
+
203
+ # Créer une entrée mémoire enrichie
204
+ memory_entry = f"""[{timestamp}]
205
+ Sujet: {', '.join(subject_keywords)}
206
+ Utilisateur: {user_msg}
207
+ Kibali: {ai_response}"""
208
+
209
+ # Métadonnées
210
+ metadata = {
211
+ "timestamp": timestamp,
212
+ "subject_keywords": subject_keywords,
213
+ "user_length": len(user_msg),
214
+ "ai_length": len(ai_response),
215
+ "hash": hashlib.md5(memory_entry.encode()).hexdigest()
216
  }
217
+
218
+ # Éviter les doublons
219
+ if metadata["hash"] not in [m.get("hash") for m in memory_metadata]:
220
+ memory_texts.append(memory_entry)
221
+ memory_metadata.append(metadata)
222
+
223
+ # Ajout vectoriel
224
+ mem_emb = embed_model.encode([memory_entry], normalize_embeddings=True).astype('float32')
225
+ memory_index.add(mem_emb)
226
+
227
+ logger.info(f"Mémoire ajoutée en temps réel: {subject_keywords} (total: {len(memory_texts)})")
228
+ return True
229
+ return False
230
+
231
+ def retrieve_adaptive_memory(query: str, k: int = 5) -> tuple:
232
+ """Récupère la mémoire de façon adaptative selon le contexte"""
233
+ if memory_index.ntotal == 0:
234
+ return [], []
235
+
236
+ query_emb = embed_model.encode([query], normalize_embeddings=True).astype('float32')
237
+
238
+ # Recherche de base
239
+ k_search = min(k * 2, memory_index.ntotal) # Chercher plus pour filtrer ensuite
240
+ D, I = memory_index.search(query_emb, k=k_search)
241
+
242
+ # Filtrage intelligent avec scoring
243
+ results = []
244
+ for dist, idx in zip(D[0], I[0]):
245
+ if 0 <= idx < len(memory_texts):
246
+ metadata = memory_metadata[idx] if idx < len(memory_metadata) else {}
247
+
248
+ # Score de pertinence
249
+ recency_score = 1.0 / (1 + (datetime.now() - datetime.fromisoformat(metadata.get("timestamp", datetime.now().isoformat()))).seconds / 3600)
250
+ similarity_score = 1.0 / (1 + dist)
251
+
252
+ # Bonus si les mots-clés du sujet actuel correspondent
253
+ keyword_bonus = 0
254
+ if conversation_ctx.subject_keywords:
255
+ text_lower = memory_texts[idx].lower()
256
+ keyword_bonus = sum(1 for kw in conversation_ctx.subject_keywords if kw in text_lower) * 0.1
257
+
258
+ total_score = similarity_score * 0.6 + recency_score * 0.3 + keyword_bonus
259
+
260
+ results.append({
261
+ "text": memory_texts[idx],
262
+ "score": total_score,
263
+ "metadata": metadata
264
+ })
265
+
266
+ # Trier par score et prendre les top k
267
+ results = sorted(results, key=lambda x: x["score"], reverse=True)[:k]
268
+
269
+ texts = [r["text"] for r in results]
270
+ scores = [r["score"] for r in results]
271
+
272
+ return texts, scores
273
 
274
+ # --- ROUTES ---
275
  @app.get("/status")
276
  async def status():
 
277
  return {
278
  "status": "ready",
279
  "doc_chunks": len(doc_chunks),
 
281
  "pdf_library": PDF_READER,
282
  "model_device": str(model.device),
283
  "torch_cuda_available": torch.cuda.is_available(),
284
+ "current_subject": conversation_ctx.current_subject[:100] if conversation_ctx.current_subject else None,
285
+ "subject_message_count": conversation_ctx.message_count
286
  }
287
 
288
  @app.post("/chat", response_model=ChatResponse)
289
  async def chat(request: ChatRequest):
290
+ user_message = request.messages[-1].content.strip()
291
+ if not user_message:
292
+ raise HTTPException(status_code=400, detail="Message vide")
293
+
294
+ geo = {
295
+ "latitude": request.latitude,
296
+ "longitude": request.longitude,
297
+ "city": request.city or "Libreville"
298
+ }
299
+
300
+ # Mise à jour du contexte conversationnel en temps réel
301
+ user_emb = embed_model.encode([user_message], normalize_embeddings=True).astype('float32')
302
+ conversation_ctx.update_subject(user_message, user_emb)
303
+
304
+ # 1. RAG Documents PDF
305
+ rag_context = ""
306
+ rag_sources = []
307
+ if doc_index.ntotal > 0 and len(doc_chunks) > 0:
308
+ D, I = doc_index.search(user_emb, k=5)
309
+ relevant_chunks = []
310
+ for idx in I[0]:
311
+ if 0 <= idx < len(doc_chunks):
312
+ relevant_chunks.append(doc_chunks[idx][:1000])
313
+ if idx < len(doc_metadata):
314
+ rag_sources.append(doc_metadata[idx].get("source", "PDF"))
315
+ if relevant_chunks:
316
+ rag_context = "\n\n".join([f"Document : {chunk}" for chunk in relevant_chunks])
317
+
318
+ # 2. Mémoire conversationnelle adaptative
319
+ memory_context = ""
320
+ memory_texts_filtered, memory_scores = retrieve_adaptive_memory(user_message, k=5)
321
+ if memory_texts_filtered:
322
+ memory_context = "\n\n".join([f"Mémoire (score: {score:.2f}): {text}"
323
+ for text, score in zip(memory_texts_filtered, memory_scores)])
324
+ logger.info(f"Mémoire récupérée: {len(memory_texts_filtered)} entrées (scores: {[f'{s:.2f}' for s in memory_scores]})")
325
+
326
+ # 3. Réflexion stratégique
327
+ if request.thinking_mode:
328
+ execute_reflection_plan(
329
+ user_message,
330
+ geo_info=geo,
331
+ messages=request.messages,
332
+ current_subject=conversation_ctx.current_subject,
333
+ subject_keywords=conversation_ctx.subject_keywords
334
+ )
335
+
336
+ # 4. Recherche Web enrichie
337
+ search_query = user_message
338
+ if conversation_ctx.subject_keywords:
339
+ search_query = f"{user_message} {' '.join(conversation_ctx.subject_keywords[:3])} Gabon"
340
+
341
+ search_results = web_search(search_query)
342
+ web_context = "\n".join([f"- {r['content'][:500]}" for r in search_results.get("results", [])[:6]])
343
+ web_images = search_results.get("images", [])[:4]
344
 
345
+ # 5. Prompt enrichi avec verrouillage contextuel
346
+ system_prompt = f"""Tu es Kibali, un assistant IA chaleureux, précis et expert du Gabon, basé à {geo['city']}.
347
  Réponds toujours en français, de façon naturelle, concise et factuelle.
 
 
 
348
 
349
+ CONTEXTE CONVERSATIONNEL ACTUEL:
350
+ - Sujet en cours: {', '.join(conversation_ctx.subject_keywords) if conversation_ctx.subject_keywords else 'Nouveau sujet'}
351
+ - Nombre de messages sur ce sujet: {conversation_ctx.message_count}
352
+
353
+ PRIORITÉ DES SOURCES:
354
+ 1. Documents uploadés (PDF Vault) - Source la plus fiable
355
+ 2. Mémoire conversationnelle récente et pertinente
356
+ 3. Informations Web actualisées
357
+
358
+ Si une information vient d'un document uploadé, mentionne-le brièvement.
359
+ Adapte-toi aux changements brusques de sujet en restant cohérent."""
360
+
361
+ full_prompt = f"""### INSTRUCTIONS STRICTES :
362
  {system_prompt}
363
 
364
+ ### CONTEXTE DOCUMENTS (PDF Vault) :
365
+ {rag_context if rag_context else "Aucun document pertinent trouvé."}
366
 
367
+ ### HISTORIQUE PERTINENT (Mémoire adaptative) :
368
+ {memory_context if memory_context else "Pas d'historique pertinent."}
369
 
370
+ ### INFORMATIONS WEB RÉCENTES :
371
+ {web_context if web_context else "Pas d'informations web disponibles."}
372
 
373
+ ### QUESTION :
374
  {user_message}
375
 
376
+ ### RÉPONSE (en français uniquement) :
377
  """
378
 
379
+ inputs = tokenizer(full_prompt, return_tensors="pt", truncation=True, max_length=8192).to(model.device)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
380
 
381
+ streamer = TextIteratorStreamer(tokenizer, skip_prompt=True, skip_special_tokens=True, timeout=120.0)
382
 
383
+ def generate_stream():
384
+ try:
385
+ model.generate(
386
+ **inputs,
387
+ streamer=streamer,
388
+ max_new_tokens=1024,
389
+ temperature=0.6,
390
+ do_sample=True,
391
+ top_p=0.85,
392
+ top_k=50,
393
+ repetition_penalty=1.2,
394
+ length_penalty=0.8
395
+ )
396
+ except Exception as e:
397
+ logger.error(f"Erreur génération : {e}")
398
+
399
+ thread = Thread(target=generate_stream)
400
+ thread.start()
401
+
402
+ response_text = ""
403
+ for new_text in streamer:
404
+ if new_text is not None:
405
+ response_text += new_text
406
+ response_text = response_text.strip()
407
+
408
+ # Ajout en temps réel à la mémoire
409
+ if response_text:
410
+ add_to_memory_realtime(
411
+ user_message,
412
+ response_text,
413
+ conversation_ctx.subject_keywords
414
+ )
415
+
416
+ # Informations contextuelles
417
+ context_info = {
418
+ "subject_keywords": conversation_ctx.subject_keywords,
419
+ "message_count": conversation_ctx.message_count,
420
+ "memory_used": len(memory_texts_filtered),
421
+ "rag_sources": list(set(rag_sources)),
422
+ "web_results": len(search_results.get("results", []))
423
+ }
424
 
425
+ return ChatResponse(response=response_text, images=web_images, context_info=context_info)
426
 
427
+ @app.post("/upload")
428
+ async def upload(files: List[UploadFile] = File(...)):
429
+ total_added = 0
430
+ processed_files = 0
 
431
 
 
 
 
 
 
 
432
  for file in files:
433
  if not file.filename.lower().endswith(".pdf"):
 
434
  continue
435
+
436
  try:
437
  content = await file.read()
438
  text = extract_text_from_pdf(content)
439
+
440
+ if not text:
441
+ logger.warning(f"Aucun texte extrait de {file.filename}")
442
+ continue
443
+
444
+ chunks = chunk_text(text)
445
+ if not chunks:
446
+ continue
447
+
448
+ # Métadonnées pour chaque chunk
449
+ timestamp = datetime.now().isoformat()
450
+ for chunk in chunks:
451
+ doc_metadata.append({
452
+ "source": file.filename,
453
+ "timestamp": timestamp,
454
+ "length": len(chunk)
455
+ })
456
+
457
+ embeddings = embed_model.encode(chunks, normalize_embeddings=True).astype('float32')
458
+ doc_index.add(embeddings)
459
+ doc_chunks.extend(chunks)
460
+
461
+ total_added += len(chunks)
462
+ processed_files += 1
463
+
464
+ logger.info(f"Upload réussi : {file.filename} → {len(chunks)} chunks ajoutés")
465
+
466
  except Exception as e:
467
+ logger.error(f"Erreur lors du traitement de {file.filename} : {e}")
 
 
 
 
 
 
 
 
468
 
 
 
 
469
  return {
470
+ "status": "success",
471
+ "files_processed": processed_files,
472
+ "chunks_added": total_added,
473
+ "total_doc_chunks": len(doc_chunks)
474
  }
475
 
476
+ @app.post("/upload-pdfs")
477
+ async def upload_pdfs(files: List[UploadFile] = File(...)):
478
+ return await upload(files)
479
+
480
+ @app.post("/clear-memory")
481
+ async def clear_memory():
482
+ """Efface la mémoire conversationnelle"""
483
+ global memory_index, memory_texts, memory_metadata
484
+ memory_index = faiss.IndexFlatL2(dimension)
485
+ memory_texts = []
486
+ memory_metadata = []
487
+ conversation_ctx.__init__()
488
+ return {"status": "memory_cleared", "message": "Mémoire conversationnelle effacée"}
489
+
490
+ # --- DEMARRAGE ---
491
  @app.on_event("startup")
492
  async def startup_event():
493
  logger.info("🚀 Kibali AI API démarrée avec succès !")
494
+ logger.info(f"Accès : http://localhost:8000 | Docs : http://localhost:8000/docs")
495
+ logger.info(f"Mémoire adaptative et réflexion contextuelle activées ✓")
 
 
 
 
 
 
requirements.txt CHANGED
@@ -1,21 +1,22 @@
1
- torch==2.11.0.dev20251229+cu130
2
- torchvision==0.25.0.dev20251229+cu130
3
- torchaudio==2.10.0.dev20251229+cu130
4
- triton==3.6.0+git9844da95
5
  transformers==4.41.2
6
  bitsandbytes>=0.41.0
7
  accelerate
8
  sentence-transformers
9
  faiss-gpu
 
 
10
  fastapi
11
  uvicorn[standard]
12
  pydantic
13
  python-multipart
14
- fastapi.middleware.cors
15
- fastapi.staticfiles
16
  pypdf>=3.0.0
17
- numpy<2
18
  folium
19
  duckduckgo-search
20
  huggingface_hub==0.23.4
21
- spaces
 
1
+ # --- Core IA (Versions Stables) ---
2
+ # Note: On ne met pas de version figée pour torch ici,
3
+ # car on l'installe via l'URL spécifique dans le Dockerfile.
 
4
  transformers==4.41.2
5
  bitsandbytes>=0.41.0
6
  accelerate
7
  sentence-transformers
8
  faiss-gpu
9
+
10
+ # --- Serveur & API ---
11
  fastapi
12
  uvicorn[standard]
13
  pydantic
14
  python-multipart
15
+
16
+ # --- Outils & Data ---
17
  pypdf>=3.0.0
18
+ numpy<2.0.0
19
  folium
20
  duckduckgo-search
21
  huggingface_hub==0.23.4
22
+ spaces
tools/todo.py CHANGED
@@ -1,41 +1,253 @@
1
- # tools/todo.py
2
  import streamlit as st
3
  import time
 
 
4
 
5
- def execute_reflection_plan(prompt, geo_info=None, messages=[]):
6
- """
7
- Phase de réflexion structurée avec verrouillage du sujet conversationnel.
8
- """
9
- if geo_info is None: geo_info = {}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- # 1. ANALYSE DU SUJET (Contextualisation)
12
- # Si le prompt est court, on récupère le sujet du dernier message
13
- subject = prompt
14
- is_continuation = len(prompt.split()) < 5 or any(x in prompt.lower() for x in ["ils", "elles", "donc", "alors", "ceux-là"])
15
 
16
- if is_continuation and len(messages) > 0:
17
- # On extrait le sujet principal du dernier échange pour "nourrir" la réflexion
18
- subject = f"{prompt} (contexte: {messages[-1]['content'][:50]}...)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
 
20
- location = f"{geo_info.get('city', 'Libreville')}, {geo_info.get('country', 'Gabon')}"
21
- method = geo_info.get('method', 'Inconnue')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
22
 
23
- with st.status(f"🧠 Kibali Thinking Engine", expanded=True) as status:
24
- st.write(f"🌍 **Localisation active :** {location}")
25
-
26
- if is_continuation:
27
- st.write(f"🔗 **Liaison contextuelle :** Analyse du sujet précédent détectée.")
28
-
29
- steps = [
30
- f"Identification de l'entité : Recherche d'informations sur '{subject}'.",
31
- "Extraction de la mémoire sémantique FAISS pour éviter les répétitions.",
32
- "Requête Web enrichie : Combinaison du sujet récent + question actuelle pour les images.",
33
- f"Vérification de la pertinence culturelle et temporelle pour le Gabon."
34
- ]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
35
 
36
- for i, step in enumerate(steps):
37
- st.write(f"{i+1}. {step}")
38
- time.sleep(0.15)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
 
40
- status.update(label="✅ Stratégie de réponse validée", state="complete", expanded=False)
41
- return True
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import streamlit as st
2
  import time
3
+ from typing import List, Optional
4
+ import re
5
 
6
+ def analyze_query_type(prompt: str) -> dict:
7
+ """Analyse le type de requête pour adapter la stratégie de réflexion"""
8
+ prompt_lower = prompt.lower()
9
+
10
+ analysis = {
11
+ "type": "general",
12
+ "needs_web": False,
13
+ "needs_memory": False,
14
+ "needs_docs": False,
15
+ "complexity": "simple",
16
+ "temporal": False,
17
+ "geographical": False
18
+ }
19
+
20
+ # Détection de questions temporelles
21
+ temporal_keywords = ["aujourd'hui", "maintenant", "récent", "actuel", "dernier", "2024", "2025"]
22
+ if any(kw in prompt_lower for kw in temporal_keywords):
23
+ analysis["temporal"] = True
24
+ analysis["needs_web"] = True
25
+
26
+ # Détection géographique
27
+ geo_keywords = ["gabon", "libreville", "port-gentil", "franceville", "oyem", "où", "localisation"]
28
+ if any(kw in prompt_lower for kw in geo_keywords):
29
+ analysis["geographical"] = True
30
+
31
+ # Détection de questions sur documents
32
+ doc_keywords = ["selon le document", "d'après le pdf", "dans le fichier", "uploadé"]
33
+ if any(kw in prompt_lower for kw in doc_keywords):
34
+ analysis["needs_docs"] = True
35
+ analysis["type"] = "document_query"
36
+
37
+ # Détection de continuation de conversation
38
+ continuation_keywords = ["ils", "elles", "lui", "leur", "donc", "alors", "ensuite", "aussi", "également"]
39
+ if any(kw in prompt_lower for kw in continuation_keywords) or len(prompt.split()) < 5:
40
+ analysis["needs_memory"] = True
41
+ analysis["type"] = "continuation"
42
+
43
+ # Détection de complexité
44
+ if len(prompt.split()) > 15 or "?" in prompt and prompt.count("?") > 1:
45
+ analysis["complexity"] = "complex"
46
+ elif "pourquoi" in prompt_lower or "comment" in prompt_lower or "expliquer" in prompt_lower:
47
+ analysis["complexity"] = "medium"
48
+
49
+ # Questions nécessitant le web
50
+ web_keywords = ["actualité", "news", "prix", "cours", "météo", "horaire"]
51
+ if any(kw in prompt_lower for kw in web_keywords):
52
+ analysis["needs_web"] = True
53
+ analysis["type"] = "real_time"
54
+
55
+ return analysis
56
+
57
+ def detect_subject_shift(prompt: str, current_subject: str, subject_keywords: List[str]) -> dict:
58
+ """Détecte un changement de sujet et évalue la force du changement"""
59
+ if not current_subject or not subject_keywords:
60
+ return {
61
+ "shift_detected": False,
62
+ "shift_strength": 0.0,
63
+ "new_subject_detected": True,
64
+ "reason": "Premier message ou pas de sujet actuel"
65
+ }
66
 
67
+ prompt_lower = prompt.lower()
 
 
 
68
 
69
+ # Calcul de l'overlap des mots-clés
70
+ prompt_words = set(re.findall(r'\b\w{4,}\b', prompt_lower))
71
+ keyword_overlap = len(prompt_words.intersection(set(subject_keywords)))
72
+ overlap_ratio = keyword_overlap / max(len(subject_keywords), 1)
73
+
74
+ # Détection de marqueurs de changement de sujet
75
+ shift_markers = ["maintenant", "sinon", "autre chose", "parlons de", "passons à", "nouveau sujet"]
76
+ has_shift_marker = any(marker in prompt_lower for marker in shift_markers)
77
+
78
+ # Calcul de la force du changement
79
+ shift_strength = 0.0
80
+ if overlap_ratio < 0.2:
81
+ shift_strength += 0.5
82
+ if has_shift_marker:
83
+ shift_strength += 0.3
84
+ if len(prompt_words) > 5 and keyword_overlap == 0:
85
+ shift_strength += 0.2
86
+
87
+ shift_detected = shift_strength > 0.4
88
+
89
+ return {
90
+ "shift_detected": shift_detected,
91
+ "shift_strength": shift_strength,
92
+ "new_subject_detected": shift_strength > 0.6,
93
+ "keyword_overlap": keyword_overlap,
94
+ "overlap_ratio": overlap_ratio,
95
+ "reason": f"Overlap: {overlap_ratio:.1%}, Marqueurs: {has_shift_marker}"
96
+ }
97
 
98
+ def generate_search_strategy(analysis: dict, subject_keywords: List[str], geo_info: dict) -> dict:
99
+ """Génère une stratégie de recherche optimisée"""
100
+ strategy = {
101
+ "use_rag": analysis["needs_docs"],
102
+ "use_memory": analysis["needs_memory"] or analysis["type"] == "continuation",
103
+ "use_web": analysis["needs_web"] or analysis["temporal"],
104
+ "memory_k": 5,
105
+ "rag_k": 3,
106
+ "web_enhanced": False,
107
+ "search_query_suffix": ""
108
+ }
109
+
110
+ # Ajustement selon la complexité
111
+ if analysis["complexity"] == "complex":
112
+ strategy["memory_k"] = 8
113
+ strategy["rag_k"] = 5
114
+ elif analysis["complexity"] == "simple":
115
+ strategy["memory_k"] = 3
116
+ strategy["rag_k"] = 2
117
+
118
+ # Enrichissement de la recherche web
119
+ if analysis["needs_web"]:
120
+ strategy["web_enhanced"] = True
121
+ if subject_keywords:
122
+ strategy["search_query_suffix"] = f" {' '.join(subject_keywords[:3])}"
123
+ if analysis["geographical"]:
124
+ strategy["search_query_suffix"] += f" {geo_info.get('city', 'Gabon')}"
125
+
126
+ return strategy
127
 
128
+ def execute_reflection_plan(
129
+ prompt: str,
130
+ geo_info: Optional[dict] = None,
131
+ messages: Optional[List] = None,
132
+ current_subject: Optional[str] = None,
133
+ subject_keywords: Optional[List[str]] = None
134
+ ):
135
+ """
136
+ Phase de réflexion structurée avec analyse contextuelle avancée et adaptation dynamique.
137
+ """
138
+ if geo_info is None:
139
+ geo_info = {}
140
+ if messages is None:
141
+ messages = []
142
+ if subject_keywords is None:
143
+ subject_keywords = []
144
+
145
+ # ÉTAPE 1: Analyse du type de requête
146
+ query_analysis = analyze_query_type(prompt)
147
+
148
+ # ÉTAPE 2: Détection de changement de sujet
149
+ subject_shift = detect_subject_shift(prompt, current_subject, subject_keywords)
150
+
151
+ # ÉTAPE 3: Génération de la stratégie de recherche
152
+ search_strategy = generate_search_strategy(query_analysis, subject_keywords, geo_info)
153
+
154
+ # ÉTAPE 4: Affichage de la réflexion (si Streamlit disponible)
155
+ try:
156
+ location = f"{geo_info.get('city', 'Libreville')}, {geo_info.get('country', 'Gabon')}"
157
 
158
+ with st.status("🧠 Kibali Thinking Engine", expanded=True) as status:
159
+ st.write(f"🌍 **Localisation active :** {location}")
160
+ st.write("")
161
+
162
+ # Analyse du type de requête
163
+ st.write("### 📊 Analyse de la requête")
164
+ st.write(f"- **Type :** {query_analysis['type'].replace('_', ' ').title()}")
165
+ st.write(f"- **Complexité :** {query_analysis['complexity'].title()}")
166
+
167
+ if query_analysis['temporal']:
168
+ st.write("- ⏰ **Dimension temporelle détectée** → Recherche web activée")
169
+ if query_analysis['geographical']:
170
+ st.write(f"- 🗺️ **Contexte géographique :** {location}")
171
+
172
+ time.sleep(0.2)
173
+ st.write("")
174
+
175
+ # Détection de changement de sujet
176
+ st.write("### 🔄 Analyse du contexte conversationnel")
177
+ if subject_shift['shift_detected']:
178
+ if subject_shift['new_subject_detected']:
179
+ st.write("- 🆕 **Nouveau sujet détecté** → Rafraîchissement du contexte")
180
+ else:
181
+ st.write(f"- ⚠️ **Changement partiel** (force: {subject_shift['shift_strength']:.0%})")
182
+ st.write(f" *Raison : {subject_shift['reason']}*")
183
+ else:
184
+ st.write("- ✅ **Continuité du sujet actuel**")
185
+ if subject_keywords:
186
+ st.write(f" *Mots-clés actifs : {', '.join(subject_keywords[:5])}*")
187
+ st.write(f" *Overlap : {subject_shift['keyword_overlap']}/{len(subject_keywords)} mots-clés*")
188
+
189
+ time.sleep(0.2)
190
+ st.write("")
191
+
192
+ # Stratégie de recherche
193
+ st.write("### 🎯 Stratégie de réponse")
194
+ sources = []
195
+ if search_strategy['use_rag']:
196
+ sources.append(f"📚 Documents PDF (top {search_strategy['rag_k']})")
197
+ if search_strategy['use_memory']:
198
+ sources.append(f"🧠 Mémoire conversationnelle (top {search_strategy['memory_k']})")
199
+ if search_strategy['use_web']:
200
+ web_label = "🌐 Recherche web"
201
+ if search_strategy['web_enhanced']:
202
+ web_label += " (enrichie avec contexte)"
203
+ sources.append(web_label)
204
+
205
+ if not sources:
206
+ sources.append("💭 Connaissance générale du modèle")
207
 
208
+ for i, source in enumerate(sources, 1):
209
+ st.write(f"{i}. {source}")
210
+ time.sleep(0.15)
211
+
212
+ st.write("")
213
+
214
+ # Plan d'action détaillé
215
+ st.write("### ⚙️ Plan d'exécution")
216
+ steps = []
217
+
218
+ if search_strategy['use_rag']:
219
+ steps.append("Extraction des chunks pertinents depuis la base vectorielle PDF")
220
+
221
+ if search_strategy['use_memory']:
222
+ steps.append("Récupération des échanges similaires avec scoring de pertinence")
223
+
224
+ if search_strategy['use_web']:
225
+ query_suffix = search_strategy['search_query_suffix']
226
+ steps.append(f"Requête web : '{prompt[:50]}...{query_suffix}'")
227
+
228
+ steps.append("Synthèse des sources avec priorisation hiérarchique")
229
+ steps.append("Génération de la réponse avec verrouillage contextuel")
230
+
231
+ for i, step in enumerate(steps, 1):
232
+ st.write(f"{i}. {step}")
233
+ time.sleep(0.15)
234
+
235
+ time.sleep(0.3)
236
+ status.update(
237
+ label="✅ Stratégie validée - Génération en cours",
238
+ state="complete",
239
+ expanded=False
240
+ )
241
+
242
+ except Exception as e:
243
+ # Fallback si Streamlit n'est pas disponible
244
+ print(f"[Kibali Thinking] Type: {query_analysis['type']}, Complexité: {query_analysis['complexity']}")
245
+ print(f"[Kibali Thinking] Changement de sujet: {subject_shift['shift_detected']} (force: {subject_shift['shift_strength']:.0%})")
246
+ print(f"[Kibali Thinking] Sources: RAG={search_strategy['use_rag']}, Memory={search_strategy['use_memory']}, Web={search_strategy['use_web']}")
247
+
248
+ return {
249
+ "analysis": query_analysis,
250
+ "subject_shift": subject_shift,
251
+ "strategy": search_strategy,
252
+ "execution_plan_ready": True
253
+ }