Option A (recommandée) : Rosalinda “sans clé API” via Ollama (local, à toi)
Browse files1) Installer Ollama (sur ton PC serveur)
Installe Ollama, puis télécharge un modèle (ex: llama3.1).
Ensuite, lance un modèle :
ollama pull llama3.1
Ollama expose une API locale :
http://127.0.0.1:11434
2) Serveur Rosalinda (backend) — sécurisé + stable
Crée un dossier rosalinda-server/ puis :
package.json
{
"name": "rosalinda-server",
"type": "module",
"dependencies": {
"express": "^4.19.2",
"cors": "^2.8.5",
"node-fetch": "^3.3.2",
"helmet": "^7.1.0",
"express-rate-limit": "^7.4.0"
}
}
server.js
import express from "express";
import cors from "cors";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import fetch from "node-fetch";
const app = express();
// 1) Sécurité de base
app.use(helmet());
app.use(express.json({ limit: "2mb" }));
// 2) CORS : autorise TON site (remplace par ton domaine / hf.space)
app.use(cors({
origin: "*", // mieux: "https://abmacode12-codeflow-station-anti-stop-engine.static.hf.space"
methods: ["GET", "POST"],
allowedHeaders: ["Content-Type", "Authorization"]
}));
// 3) Anti-abus
app.use(rateLimit({
windowMs: 60 * 1000,
max: 60
}));
// 4) Clé interne (pour éviter que n’importe qui utilise ton IA)
// Mets un secret à toi ici
const INTERNAL_TOKEN = process.env.ROSALINDA_INTERNAL_TOKEN || "CHANGE-ME";
// Health check
app.get("/health", (req, res) => res.json({ ok: true, name: "Rosalinda", status: "online" }));
// Chat
app.post("/rosalinda/chat", async (req, res) => {
try {
const auth = req.headers.authorization || "";
if (auth !== `Bearer ${INTERNAL_TOKEN}`) {
return res.status(401).json({ error: "Unauthorized" });
}
const message = (req.body?.message || "").toString().trim();
if (!message) return res.status(400).json({ error: "Missing message" });
// Appel à Ollama (local, donc pas de clé API externe)
const r = await fetch("http://127.0.0.1:11434/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3.1",
prompt: `Tu es Rosalinda, l'IA privée de l'entreprise d'Amine. Réponds clairement et professionnellement.\n\nUser: ${message}\nRosalinda:`,
stream: false
})
});
if (!r.ok) {
const t = await r.text();
return res.status(502).json({ error: "Ollama error", details: t });
}
const data = await r.json();
return res.json({ reply: data.response });
} catch (e) {
return res.status(500).json({ error: "Server error", details: e.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log("✅ Rosalinda backend ONLINE on port", PORT));
Lancer le serveur
npm install
set ROSALINDA_INTERNAL_TOKEN=un_secret_a_toi
node server.js
Teste :
Ouvre http://localhost:3000/health → doit répondre online.
3) Intégrer dans ton interface CodeFlow (remplacer le placeholder)
Dans ton frontend (CodeFlow Station), remplace l’appel existant par :
async function sendToRosalinda(message) {
const res = await fetch("http://TON_IP_OU_DOMAINE:3000/rosalinda/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer un_secret_a_toi"
},
body: JSON.stringify({ message })
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "Request failed");
return data.reply;
}
✅ Là, Rosalinda répondra vraiment.
Pourquoi ça bloque chez toi maintenant (important)
Ton screenshot montre Backend: Offline + “placeholder”.
Donc ton UI envoie “bonjour”… mais aucun serveur ne répond → elle attend → ça semble figé.
➡️ Si ton UI est sur static.hf.space, tu ne peux pas faire tourner Node/Express dedans.
Il faut :
soit un backend séparé (sur ton PC, un VPS, ou un Space Docker),
soit basculer vers un Space Docker (pas “static”).
Sécuriser “pour qu’elle ne modifie plus l’entreprise”
Ce que tu veux, c’est :
Séparer l’interface (entreprise) et l’IA (backend)
Mettre un token interne (déjà fait)
Mettre HTTPS + domaine + firewall (étape suivante)
✅ Avec mon approche :
ton entreprise (UI) reste identique,
l’IA est derrière un serveur contrôlé par toi,
personne ne peut l’utiliser sans ton token.
Ce que je ne peux pas faire exactement (avec honnêteté)
“Rosalinda qui m’appartient sans clé API”
✅ Possible sans clé API externe (OpenAI/Replicate) en self-host (Ollama).
❌ Mais il faut quand même un modèle (ex: llama3.1) et un serveur. C’est ça “l’IA”.
Mini-check express (tu peux faire tout de suite)
Ouvre la console navigateur (F12) → onglet Network
Envoie “bonjour”
Regarde la requête : status = 404 / CORS / (failed) ?
Si tu me copies-colles :
l’erreur console,
l’URL appelée,
le code de ton fetch()
je te dis exactement la ligne à corriger.
Si tu veux avancer sans perdre de temps :
👉 dis-moi juste où tu veux héberger le backend :
sur ton PC (Windows) ✅
sur un VPS (OVH, etc.) ✅
sur Hugging Face (Space Docker) ✅
Et je te donne la version parfaite adaptée, sans casser ton entreprise, avec anti-stop (retries + queue).
sur ton PC (Windows) ✅
sur un VPS (OVH, etc.) ✅
sur Hugging Face (Space Docker) ✅Je vaudrais les trois, s'il vous plaît.
Parfait. Je te donne les 3 solutions complètes (Windows PC, VPS OVH, Hugging Face Space Docker) avec le même principe :
Ton entreprise (CodeFlow Station) ne bouge pas
On ajoute un backend Rosalinda (serveur)
Sécurisé (token interne)
Sans clé API externe (modèle open-source auto-hébergé)
0) Le plus important (pour que ça réponde)
Ton UI est en “static” → elle ne peut pas répondre toute seule.
Donc on doit avoir une URL backend qui répond à :
GET /health
POST /rosalinda/chat
Ensuite ton UI fait un fetch() vers cette URL.
A) WINDOWS (sur ton PC) ✅ (le plus simple / rapide)
A1) Installer Ollama (IA locale, sans clé)
Installe Ollama sur Windows
Ouvre PowerShell et fais :
ollama pull llama3.1
A2) Créer ton serveur Rosalinda
Crée un dossier C:\rosalinda-server
package.json
{
"name": "rosalinda-server",
"type": "module",
"dependencies": {
"cors": "^2.8.5",
"express": "^4.19.2",
"helmet": "^7.1.0",
"express-rate-limit": "^7.4.0",
"node-fetch": "^3.3.2"
}
}
server.js
import express from "express";
import cors from "cors";
import helmet from "helmet";
import rateLimit from "express-rate-limit";
import fetch from "node-fetch";
const app = express();
app.use(helmet());
app.use(express.json({ limit: "2mb" }));
// IMPORTANT: remplace "*" par ton domaine si tu veux verrouiller
app.use(cors({ origin: "*", methods: ["GET", "POST"], allowedHeaders: ["Content-Type", "Authorization"] }));
app.use(rateLimit({ windowMs: 60_000, max: 120 }));
const INTERNAL_TOKEN = process.env.ROSALINDA_INTERNAL_TOKEN || "CHANGE_ME";
app.get("/health", (_, res) => res.json({ ok: true, name: "Rosalinda", status: "online" }));
app.post("/rosalinda/chat", async (req, res) => {
try {
const auth = req.headers.authorization || "";
if (auth !== `Bearer ${INTERNAL_TOKEN}`) return res.status(401).json({ error: "Unauthorized" });
const message = (req.body?.message || "").toString().trim();
if (!message) return res.status(400).json({ error: "Missing message" });
const r = await fetch("http://127.0.0.1:11434/api/generate", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
model: "llama3.1",
prompt: `Tu es Rosalinda, IA privée d'Amine. Réponds clairement en français.\n\nUser: ${message}\nRosalinda:`,
stream: false
})
});
if (!r.ok) return res.status(502).json({ error: "Ollama error", details: await r.text() });
const data = await r.json();
return res.json({ reply: data.response });
} catch (e) {
return res.status(500).json({ error: "Server error", details: e.message });
}
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => console.log("✅ Rosalinda backend ONLINE:", PORT));
A3) Lancer
cd C:\rosalinda-server
npm install
setx ROSALINDA_INTERNAL_TOKEN "mon_super_secret"
# ferme / rouvre PowerShell
node server.js
Test :
http://localhost:3000/health
A4) Intégrer dans ton UI (CodeFlow)
Dans ton front, utilise :
async function sendToRosalinda(message) {
const res = await fetch("http://localhost:3000/rosalinda/chat", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer mon_super_secret"
},
body: JSON.stringify({ message })
});
const data = await res.json();
if (!res.ok) throw new Error(data?.error || "Request failed");
return data.reply;
}
⚠️ Si ton UI est sur HF (HTTPS) et ton backend en HTTP local, le navigateur peut bloquer (mixed content). Dans ce cas, passe à la solution VPS ou ajoute HTTPS (je te donne plus bas).
B) VPS (OVH, etc.) ✅ (propre, stable, accessible partout)
Ici tu as 2 choix “sans clé” :
B-Option 1 (simple) : Ollama sur VPS (CPU ok, GPU mieux)
B-Option 2 (portable) : llama.cpp server (sans Ollama)
Je te donne la B-Option 2 (la plus universelle, marche sur presque tous les VPS).
B1) Docker Compose (backend + llama.cpp + reverse proxy)
Crée un dossier /opt/rosalinda
docker-compose.yml
services:
llm:
image: ghcr.io/ggerganov/llama.cpp:server
command: ["-m", "/models/model.gguf", "--host", "0.0.0.0", "--port", "8080", "-c", "4096"]
volumes:
- ./models:/models
restart: unless-stopped
api:
build: ./api
environment:
- ROSALINDA_INTERNAL_TOKEN=CHANGE_ME
- LLM_URL=http://llm:8080
depends_on:
- llm
restart: unless-stopped
nginx:
image: nginx:alpine
ports:
- "80:80"
volumes:
- ./nginx.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- api
restart: unless-stopped
nginx.conf
server {
listen 80;
location /health {
proxy_pass http://api:3000/health;
}
location /rosalinda/ {
proxy_pass http://api:3000/rosalinda/;
proxy
- rosalinda-server/server.js +54 -29
- script.js +78 -40
|
@@ -6,63 +6,88 @@ import fetch from "node-fetch";
|
|
| 6 |
|
| 7 |
const app = express();
|
| 8 |
|
| 9 |
-
//
|
| 10 |
app.use(helmet());
|
| 11 |
app.use(express.json({ limit: "2mb" }));
|
| 12 |
|
| 13 |
-
//
|
| 14 |
app.use(cors({
|
| 15 |
-
origin: "*",
|
| 16 |
methods: ["GET", "POST"],
|
| 17 |
allowedHeaders: ["Content-Type", "Authorization"]
|
| 18 |
}));
|
| 19 |
|
| 20 |
-
//
|
| 21 |
app.use(rateLimit({
|
| 22 |
windowMs: 60 * 1000,
|
| 23 |
-
max:
|
| 24 |
}));
|
| 25 |
|
| 26 |
-
|
| 27 |
-
const INTERNAL_TOKEN = process.env.ROSALINDA_INTERNAL_TOKEN || "CHANGE-ME";
|
| 28 |
|
| 29 |
-
// Health
|
| 30 |
-
app.get("/health", (req, res) =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 31 |
|
| 32 |
-
//
|
| 33 |
app.post("/rosalinda/chat", async (req, res) => {
|
| 34 |
try {
|
|
|
|
| 35 |
const auth = req.headers.authorization || "";
|
| 36 |
if (auth !== `Bearer ${INTERNAL_TOKEN}`) {
|
| 37 |
return res.status(401).json({ error: "Unauthorized" });
|
| 38 |
}
|
| 39 |
|
| 40 |
const message = (req.body?.message || "").toString().trim();
|
| 41 |
-
if (!message) return res.status(400).json({ error: "
|
| 42 |
|
| 43 |
-
//
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
-
if (!
|
| 55 |
-
const
|
| 56 |
-
return res.status(502).json({
|
|
|
|
|
|
|
|
|
|
| 57 |
}
|
| 58 |
|
| 59 |
-
const data = await
|
| 60 |
-
|
| 61 |
|
| 62 |
-
} catch (
|
| 63 |
-
|
|
|
|
|
|
|
|
|
|
| 64 |
}
|
| 65 |
});
|
| 66 |
|
| 67 |
const PORT = process.env.PORT || 3000;
|
| 68 |
-
app.listen(PORT, () =>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 6 |
|
| 7 |
const app = express();
|
| 8 |
|
| 9 |
+
// Basic security
|
| 10 |
app.use(helmet());
|
| 11 |
app.use(express.json({ limit: "2mb" }));
|
| 12 |
|
| 13 |
+
// CORS - adjust origin for production
|
| 14 |
app.use(cors({
|
| 15 |
+
origin: "*",
|
| 16 |
methods: ["GET", "POST"],
|
| 17 |
allowedHeaders: ["Content-Type", "Authorization"]
|
| 18 |
}));
|
| 19 |
|
| 20 |
+
// Rate limiting
|
| 21 |
app.use(rateLimit({
|
| 22 |
windowMs: 60 * 1000,
|
| 23 |
+
max: 100
|
| 24 |
}));
|
| 25 |
|
| 26 |
+
const INTERNAL_TOKEN = process.env.ROSALINDA_TOKEN || "YOUR_SECRET_TOKEN";
|
|
|
|
| 27 |
|
| 28 |
+
// Health endpoint
|
| 29 |
+
app.get("/health", (req, res) => {
|
| 30 |
+
res.json({
|
| 31 |
+
status: "online",
|
| 32 |
+
name: "Rosalinda",
|
| 33 |
+
version: "1.0",
|
| 34 |
+
mode: process.env.NODE_ENV || "development"
|
| 35 |
+
});
|
| 36 |
+
});
|
| 37 |
|
| 38 |
+
// Ollama proxy endpoint
|
| 39 |
app.post("/rosalinda/chat", async (req, res) => {
|
| 40 |
try {
|
| 41 |
+
// Check auth
|
| 42 |
const auth = req.headers.authorization || "";
|
| 43 |
if (auth !== `Bearer ${INTERNAL_TOKEN}`) {
|
| 44 |
return res.status(401).json({ error: "Unauthorized" });
|
| 45 |
}
|
| 46 |
|
| 47 |
const message = (req.body?.message || "").toString().trim();
|
| 48 |
+
if (!message) return res.status(400).json({ error: "Message required" });
|
| 49 |
|
| 50 |
+
// Call Ollama (local) or HF inference endpoint
|
| 51 |
+
let response;
|
| 52 |
+
if (process.env.OLLAMA_URL) {
|
| 53 |
+
response = await fetch(`${process.env.OLLAMA_URL}/api/generate`, {
|
| 54 |
+
method: "POST",
|
| 55 |
+
headers: { "Content-Type": "application/json" },
|
| 56 |
+
body: JSON.stringify({
|
| 57 |
+
model: "llama3.1",
|
| 58 |
+
prompt: `Tu es Rosalinda, IA privée. Réponds en français de façon professionnelle.\n\nUser: ${message}\nRosalinda:`,
|
| 59 |
+
stream: false
|
| 60 |
+
})
|
| 61 |
+
});
|
| 62 |
+
} else {
|
| 63 |
+
// Fallback simple response if no Ollama
|
| 64 |
+
return res.json({
|
| 65 |
+
reply: `(Mode démo) Vous avez dit: "${message}". Activez Ollama pour les vraies réponses.`
|
| 66 |
+
});
|
| 67 |
+
}
|
| 68 |
|
| 69 |
+
if (!response.ok) {
|
| 70 |
+
const error = await response.text();
|
| 71 |
+
return res.status(502).json({
|
| 72 |
+
error: "Ollama error",
|
| 73 |
+
details: error
|
| 74 |
+
});
|
| 75 |
}
|
| 76 |
|
| 77 |
+
const data = await response.json();
|
| 78 |
+
res.json({ reply: data.response });
|
| 79 |
|
| 80 |
+
} catch (err) {
|
| 81 |
+
res.status(500).json({
|
| 82 |
+
error: "Server error",
|
| 83 |
+
details: err.message
|
| 84 |
+
});
|
| 85 |
}
|
| 86 |
});
|
| 87 |
|
| 88 |
const PORT = process.env.PORT || 3000;
|
| 89 |
+
app.listen(PORT, () => {
|
| 90 |
+
console.log(`✅ Rosalinda server running on port ${PORT}`);
|
| 91 |
+
console.log(`🔑 Token: ${INTERNAL_TOKEN.substring(0, 4)}...`);
|
| 92 |
+
console.log(`🌐 Health check: http://localhost:${PORT}/health`);
|
| 93 |
+
});
|
|
@@ -289,30 +289,45 @@ promptEl.addEventListener('keydown', (e) => {
|
|
| 289 |
}
|
| 290 |
}
|
| 291 |
}
|
| 292 |
-
// Intégration avec le serveur Rosalinda
|
| 293 |
async function queryRosalinda(prompt) {
|
| 294 |
setStatus("Processing...", "work");
|
| 295 |
let retries = 0;
|
| 296 |
|
| 297 |
while (retries < 3) {
|
| 298 |
try {
|
| 299 |
-
|
| 300 |
-
|
| 301 |
-
|
| 302 |
-
|
| 303 |
-
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 307 |
|
| 308 |
-
|
| 309 |
-
|
| 310 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 311 |
}
|
| 312 |
|
| 313 |
-
|
| 314 |
-
|
| 315 |
-
} catch (err) {
|
| 316 |
retries++;
|
| 317 |
console.error(`Attempt ${retries} failed:`, err);
|
| 318 |
retryPill.textContent = `${retries} / 3`;
|
|
@@ -347,29 +362,54 @@ promptEl.addEventListener('keydown', (e) => {
|
|
| 347 |
// Initialisation améliorée
|
| 348 |
async function initialize() {
|
| 349 |
try {
|
| 350 |
-
|
| 351 |
|
| 352 |
-
|
| 353 |
-
|
| 354 |
-
|
| 355 |
-
|
| 356 |
-
|
| 357 |
-
|
| 358 |
-
|
| 359 |
-
|
| 360 |
-
|
| 361 |
-
|
| 362 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 363 |
}
|
|
|
|
|
|
|
|
|
|
| 364 |
|
| 365 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 366 |
switchTab('preview');
|
| 367 |
} catch (err) {
|
| 368 |
console.error("Initialization error:", err);
|
| 369 |
enableOfflineMode();
|
| 370 |
}
|
| 371 |
}
|
| 372 |
-
|
| 373 |
function enableOfflineMode() {
|
| 374 |
jobStatus.textContent = "Hors ligne";
|
| 375 |
jobStatus.className = "text-rose-400";
|
|
@@ -381,20 +421,18 @@ promptEl.addEventListener('keydown', (e) => {
|
|
| 381 |
<div class="text-amber-400">⚠️ Mode hors ligne activé</div>
|
| 382 |
<div class="mt-2 text-sm">Certaines fonctionnalités sont désactivées :</div>
|
| 383 |
<ul class="mt-1 text-xs space-y-1">
|
| 384 |
-
<li>•
|
| 385 |
-
<li>•
|
| 386 |
-
<li>•
|
| 387 |
</ul>
|
| 388 |
-
<div class="mt-2 text-xs">
|
| 389 |
`);
|
| 390 |
|
| 391 |
-
//
|
| 392 |
-
document.getElementById('btnSend').disabled =
|
| 393 |
-
document.getElementById('btnMic').disabled =
|
| 394 |
-
document.getElementById('
|
| 395 |
-
|
| 396 |
-
document.getElementById('prompt').placeholder = "Mode hors ligne - reconnectez-vous";
|
| 397 |
-
}
|
| 398 |
initialize();
|
| 399 |
applyPreview();
|
| 400 |
switchTab('preview');
|
|
|
|
| 289 |
}
|
| 290 |
}
|
| 291 |
}
|
| 292 |
+
// Intégration avec le serveur Rosalinda
|
| 293 |
async function queryRosalinda(prompt) {
|
| 294 |
setStatus("Processing...", "work");
|
| 295 |
let retries = 0;
|
| 296 |
|
| 297 |
while (retries < 3) {
|
| 298 |
try {
|
| 299 |
+
// Try local first, fallback to HF Space if needed
|
| 300 |
+
const endpoints = [
|
| 301 |
+
'http://localhost:3000/rosalinda/chat', // Local Ollama
|
| 302 |
+
'https://YOUR_HF_SPACE.hf.space/rosalinda/chat' // HF Docker fallback
|
| 303 |
+
];
|
| 304 |
+
|
| 305 |
+
// Try each endpoint until one works
|
| 306 |
+
for (const endpoint of endpoints) {
|
| 307 |
+
try {
|
| 308 |
+
const response = await fetch(endpoint, {
|
| 309 |
+
method: 'POST',
|
| 310 |
+
headers: {
|
| 311 |
+
'Content-Type': 'application/json',
|
| 312 |
+
'Authorization': 'Bearer YOUR_SECRET_TOKEN'
|
| 313 |
+
},
|
| 314 |
+
body: JSON.stringify({ message: prompt })
|
| 315 |
+
});
|
| 316 |
|
| 317 |
+
if (!response.ok) {
|
| 318 |
+
const errorData = await response.json();
|
| 319 |
+
throw new Error(errorData?.error || "Request failed");
|
| 320 |
+
}
|
| 321 |
+
|
| 322 |
+
const data = await response.json();
|
| 323 |
+
return data.reply;
|
| 324 |
+
} catch (e) {
|
| 325 |
+
console.log(`Failed with endpoint ${endpoint}, trying next...`);
|
| 326 |
+
}
|
| 327 |
}
|
| 328 |
|
| 329 |
+
throw new Error("All endpoints failed");
|
| 330 |
+
} catch (err) {
|
|
|
|
| 331 |
retries++;
|
| 332 |
console.error(`Attempt ${retries} failed:`, err);
|
| 333 |
retryPill.textContent = `${retries} / 3`;
|
|
|
|
| 362 |
// Initialisation améliorée
|
| 363 |
async function initialize() {
|
| 364 |
try {
|
| 365 |
+
jobStatus.textContent = "Connecting...";
|
| 366 |
|
| 367 |
+
// Try to connect to any available backend
|
| 368 |
+
try {
|
| 369 |
+
const response = await fetch('http://localhost:3000/health');
|
| 370 |
+
if (response.ok) {
|
| 371 |
+
netStatus.textContent = "Local";
|
| 372 |
+
netStatus.className = "text-emerald-400";
|
| 373 |
+
}
|
| 374 |
+
} catch (localErr) {
|
| 375 |
+
try {
|
| 376 |
+
const response = await fetch('https://YOUR_HF_SPACE.hf.space/health');
|
| 377 |
+
if (response.ok) {
|
| 378 |
+
netStatus.textContent = "Cloud";
|
| 379 |
+
netStatus.className = "text-cyan-400";
|
| 380 |
+
}
|
| 381 |
+
} catch (cloudErr) {
|
| 382 |
+
enableOfflineMode();
|
| 383 |
+
return;
|
| 384 |
+
}
|
| 385 |
}
|
| 386 |
+
|
| 387 |
+
jobStatus.textContent = "Prêt";
|
| 388 |
+
addMsg("Rosalinda", "Bonjour ! Je suis Rosalinda, votre assistante IA privée. Comment puis-je vous aider aujourd'hui ?");
|
| 389 |
|
| 390 |
+
// Vérification périodique du backend
|
| 391 |
+
setInterval(async () => {
|
| 392 |
+
try {
|
| 393 |
+
await fetch('http://localhost:3000/health');
|
| 394 |
+
netStatus.textContent = "Local";
|
| 395 |
+
netStatus.className = "text-emerald-400";
|
| 396 |
+
} catch (e) {
|
| 397 |
+
try {
|
| 398 |
+
await fetch('https://YOUR_HF_SPACE.hf.space/health');
|
| 399 |
+
netStatus.textContent = "Cloud";
|
| 400 |
+
netStatus.className = "text-cyan-400";
|
| 401 |
+
} catch (e) {
|
| 402 |
+
enableOfflineMode();
|
| 403 |
+
}
|
| 404 |
+
}
|
| 405 |
+
}, 30000);
|
| 406 |
+
applyPreview();
|
| 407 |
switchTab('preview');
|
| 408 |
} catch (err) {
|
| 409 |
console.error("Initialization error:", err);
|
| 410 |
enableOfflineMode();
|
| 411 |
}
|
| 412 |
}
|
|
|
|
| 413 |
function enableOfflineMode() {
|
| 414 |
jobStatus.textContent = "Hors ligne";
|
| 415 |
jobStatus.className = "text-rose-400";
|
|
|
|
| 421 |
<div class="text-amber-400">⚠️ Mode hors ligne activé</div>
|
| 422 |
<div class="mt-2 text-sm">Certaines fonctionnalités sont désactivées :</div>
|
| 423 |
<ul class="mt-1 text-xs space-y-1">
|
| 424 |
+
<li>• Vérifiez que le serveur Rosalinda est en marche</li>
|
| 425 |
+
<li>• Si local: <code class="bg-black/20 px-1">node server.js</code></li>
|
| 426 |
+
<li>• Si cloud: vérifiez l'URL de l'espace HF</li>
|
| 427 |
</ul>
|
| 428 |
+
<div class="mt-2 text-xs">Pour le mode local: installer Ollama + lancer le serveur.</div>
|
| 429 |
`);
|
| 430 |
|
| 431 |
+
// Enable features but they'll fail gracefully
|
| 432 |
+
document.getElementById('btnSend').disabled = false;
|
| 433 |
+
document.getElementById('btnMic').disabled = false;
|
| 434 |
+
document.getElementById('prompt').placeholder = "Essayez de relancer le serveur Rosalinda";
|
| 435 |
+
}
|
|
|
|
|
|
|
| 436 |
initialize();
|
| 437 |
applyPreview();
|
| 438 |
switchTab('preview');
|