|
|
|
|
|
|
|
|
function initAIDashboard() { |
|
|
|
|
|
document.getElementById('btnGenText')?.addEventListener('click', () => { |
|
|
console.log('Text generation requested'); |
|
|
|
|
|
}); |
|
|
|
|
|
document.getElementById('btnGenImage')?.addEventListener('click', () => { |
|
|
console.log('Image generation requested'); |
|
|
|
|
|
}); |
|
|
|
|
|
document.getElementById('btnGenVideo')?.addEventListener('click', () => { |
|
|
console.log('Video generation requested'); |
|
|
|
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', () => { |
|
|
|
|
|
if (window.location.pathname.includes('ai-dashboard.html')) { |
|
|
initAIDashboard(); |
|
|
return; |
|
|
} |
|
|
|
|
|
const chat = document.getElementById('chat'); |
|
|
const promptEl = document.getElementById('prompt'); |
|
|
const sendBtn = document.getElementById('btnSend'); |
|
|
const statusPill = document.getElementById('statusPill'); |
|
|
const codeEl = document.getElementById('code'); |
|
|
const previewFrame = document.getElementById('previewFrame'); |
|
|
const netStatus = document.getElementById('netStatus'); |
|
|
const mediaOut = document.getElementById('mediaOut'); |
|
|
const jobStatus = document.getElementById('jobStatus'); |
|
|
const retryPill = document.getElementById('retryPill'); |
|
|
|
|
|
|
|
|
async function checkOllama() { |
|
|
try { |
|
|
const response = await fetch('http://localhost:11434/api/generate', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ |
|
|
model: 'llama3.1', |
|
|
prompt: 'Test connection', |
|
|
stream: false |
|
|
}) |
|
|
}); |
|
|
return response.ok; |
|
|
} catch { |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
function addMsg(title, html) { |
|
|
const wrap = document.createElement('div'); |
|
|
wrap.className = "rounded-2xl border border-white/10 bg-white/5 p-4"; |
|
|
wrap.innerHTML = ` |
|
|
<div class="text-xs text-slate-400 mb-2">${title}</div> |
|
|
<div class="text-sm text-slate-200 leading-6">${html}</div> |
|
|
`; |
|
|
chat.appendChild(wrap); |
|
|
chat.scrollTop = chat.scrollHeight; |
|
|
} |
|
|
|
|
|
function setStatus(txt, type="idle") { |
|
|
statusPill.textContent = txt; |
|
|
statusPill.className = "px-2 py-1 rounded-lg border text-xs"; |
|
|
if (type === "ok") statusPill.classList.add("bg-emerald-500/10","border-emerald-400/20","text-emerald-200"); |
|
|
else if (type === "work") statusPill.classList.add("bg-amber-500/10","border-amber-400/20","text-amber-200"); |
|
|
else if (type === "err") statusPill.classList.add("bg-rose-500/10","border-rose-400/20","text-rose-200"); |
|
|
else statusPill.classList.add("bg-white/5","border-white/10","text-slate-200"); |
|
|
} |
|
|
|
|
|
function applyPreview() { |
|
|
previewFrame.srcdoc = codeEl.value; |
|
|
} |
|
|
|
|
|
|
|
|
function switchTab(tab) { |
|
|
document.querySelectorAll('.tabBtn').forEach(b => { |
|
|
const active = b.dataset.tab === tab; |
|
|
b.className = "tabBtn px-3 py-1.5 rounded-xl border text-xs " + |
|
|
(active ? "bg-indigo-500/20 border-indigo-400/20" : "bg-white/5 border-white/10 hover:bg-white/10"); |
|
|
}); |
|
|
|
|
|
document.getElementById('panel-code').classList.toggle('hidden', tab !== 'code'); |
|
|
document.getElementById('panel-preview').classList.toggle('hidden', tab !== 'preview'); |
|
|
document.getElementById('panel-media').classList.toggle('hidden', tab !== 'media'); |
|
|
} |
|
|
|
|
|
document.querySelectorAll('.tabBtn').forEach(b => { |
|
|
b.addEventListener('click', () => switchTab(b.dataset.tab)); |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('btnApply').addEventListener('click', applyPreview); |
|
|
document.getElementById('btnReloadPreview').addEventListener('click', applyPreview); |
|
|
document.getElementById('btnRefresh').addEventListener('click', applyPreview); |
|
|
|
|
|
document.getElementById('btnCopy').addEventListener('click', async () => { |
|
|
try { |
|
|
await navigator.clipboard.writeText(codeEl.value); |
|
|
setStatus("Copied!", "ok"); |
|
|
setTimeout(() => setStatus("Ready", "ok"), 2000); |
|
|
} catch { |
|
|
setStatus("Copy failed", "err"); |
|
|
setTimeout(() => setStatus("Ready", "ok"), 2000); |
|
|
} |
|
|
}); |
|
|
|
|
|
document.getElementById('btnExport').addEventListener('click', () => { |
|
|
const blob = new Blob([codeEl.value], { type: "text/html;charset=utf-8" }); |
|
|
const a = document.createElement("a"); |
|
|
a.href = URL.createObjectURL(blob); |
|
|
a.download = "index.html"; |
|
|
a.click(); |
|
|
URL.revokeObjectURL(a.href); |
|
|
}); |
|
|
|
|
|
|
|
|
function micStart() { |
|
|
const SR = window.SpeechRecognition || window.webkitSpeechRecognition; |
|
|
if (!SR) { |
|
|
addMsg("System","Microphone not available. Use Chrome Desktop."); |
|
|
return; |
|
|
} |
|
|
|
|
|
const rec = new SR(); |
|
|
rec.lang = "en-US"; |
|
|
rec.interimResults = false; |
|
|
rec.maxAlternatives = 1; |
|
|
setStatus("Listening...","work"); |
|
|
|
|
|
rec.onresult = (e) => { |
|
|
const txt = e.results[0][0].transcript; |
|
|
promptEl.value = txt; |
|
|
setStatus("Ready","ok"); |
|
|
}; |
|
|
|
|
|
rec.onerror = () => setStatus("Mic error","err"); |
|
|
rec.onend = () => setStatus("Ready","ok"); |
|
|
rec.start(); |
|
|
} |
|
|
|
|
|
document.getElementById('btnMic').addEventListener('click', micStart); |
|
|
|
|
|
sendBtn.addEventListener('click', async () => { |
|
|
const v = promptEl.value.trim(); |
|
|
if (!v || sendBtn.disabled) return; |
|
|
promptEl.value = ""; |
|
|
|
|
|
addMsg("You", v); |
|
|
|
|
|
try { |
|
|
if (!await checkBackend()) { |
|
|
enableOfflineMode(); |
|
|
return; |
|
|
} |
|
|
|
|
|
setStatus("Traitement...", "work"); |
|
|
const response = await queryRosalinda(v); |
|
|
|
|
|
addMsg("Rosalinda", response); |
|
|
|
|
|
|
|
|
if (response.startsWith("```") && response.endsWith("```")) { |
|
|
const code = response.slice(3, -3).trim(); |
|
|
codeEl.value = code; |
|
|
applyPreview(); |
|
|
} |
|
|
|
|
|
setStatus("Envoyé", "ok"); |
|
|
} catch (err) { |
|
|
console.error("Send error:", err); |
|
|
addMsg("System", ` |
|
|
<div class="text-rose-400">❌ Échec de la requête</div> |
|
|
<div class="mt-1 text-sm">${escapeHtml(err.message)}</div> |
|
|
${err.message.includes('hors ligne') ? |
|
|
'<div class="mt-1 text-xs">Veuillez vérifier votre connexion Internet.</div>' : ''} |
|
|
`); |
|
|
setStatus("Échec", "err"); |
|
|
|
|
|
if (err.message.includes('hors ligne') || err.message.includes('Failed to fetch')) { |
|
|
enableOfflineMode(); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
promptEl.addEventListener('keydown', async (e) => { |
|
|
if (e.key === "Enter") { |
|
|
e.preventDefault(); |
|
|
console.log("INPUT CAPTURED:", promptEl.value); |
|
|
sendBtn.click(); |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('btnGenImg')?.addEventListener('click', async () => { |
|
|
setStatus("Génération d'image...", "work"); |
|
|
let retries = 0; |
|
|
|
|
|
while (retries < 3) { |
|
|
try { |
|
|
const response = await fetch('/api/generate/image', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ prompt: "Image créative haute qualité" }) |
|
|
}); |
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
mediaOut.innerHTML = ` |
|
|
<div class="rounded-xl overflow-hidden border border-white/10"> |
|
|
<img src="${data.url}" class="w-full" alt="Image générée"> |
|
|
<div class="p-2 text-xs text-slate-400">Généré: ${new Date().toLocaleTimeString()}</div> |
|
|
<button class="w-full px-3 py-1 bg-indigo-500/20 hover:bg-indigo-500/30 text-xs" |
|
|
onclick="document.getElementById('code').value += '\\n<img src=\\'${data.url}\\' alt=\\'Image générée\\'>\\n'; applyPreview()"> |
|
|
Insérer dans le code |
|
|
</button> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
setStatus("Image prête", "ok"); |
|
|
return; |
|
|
|
|
|
} catch (err) { |
|
|
retries++; |
|
|
console.error(`Tentative ${retries} échouée:`, err); |
|
|
|
|
|
if (retries >= 3) { |
|
|
setStatus("Échec de génération", "err"); |
|
|
addMsg("System", `❌ Échec après 3 tentatives: ${err.message}`); |
|
|
return; |
|
|
} |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 3000 * retries)); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
|
|
|
document.getElementById('btnGenVid')?.addEventListener('click', async () => { |
|
|
setStatus("Génération de vidéo...", "work"); |
|
|
let retries = 0; |
|
|
|
|
|
while (retries < 3) { |
|
|
try { |
|
|
const response = await fetch('/api/generate/video', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ prompt: "Vidéo courte créative" }) |
|
|
}); |
|
|
|
|
|
if (!response.ok) throw new Error(`HTTP ${response.status}`); |
|
|
|
|
|
const data = await response.json(); |
|
|
|
|
|
mediaOut.innerHTML = ` |
|
|
<div class="rounded-xl overflow-hidden border border-white/10"> |
|
|
<video controls class="w-full"> |
|
|
<source src="${data.url}" type="video/mp4"> |
|
|
</video> |
|
|
<div class="p-2 text-xs text-slate-400">Généré: ${new Date().toLocaleTimeString()}</div> |
|
|
<button class="w-full px-3 py-1 bg-indigo-500/20 hover:bg-indigo-500/30 text-xs" |
|
|
onclick="document.getElementById('code').value += '\\n<video controls>\\n <source src=\\'${data.url}\\' type=\\'video/mp4\\'>\\n</video>\\n'; applyPreview()"> |
|
|
Insérer dans le code |
|
|
</button> |
|
|
</div> |
|
|
`; |
|
|
|
|
|
setStatus("Vidéo prête", "ok"); |
|
|
return; |
|
|
|
|
|
} catch (err) { |
|
|
retries++; |
|
|
console.error(`Tentative ${retries} échouée:`, err); |
|
|
|
|
|
if (retries >= 3) { |
|
|
setStatus("Échec de génération", "err"); |
|
|
addMsg("System", `❌ Échec après 3 tentatives: ${err.message}`); |
|
|
return; |
|
|
} |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 3000 * retries)); |
|
|
} |
|
|
} |
|
|
}); |
|
|
|
|
|
async function checkBackend() { |
|
|
let retries = 0; |
|
|
let lastError = null; |
|
|
|
|
|
while (retries < 3) { |
|
|
try { |
|
|
const response = await fetch('/api/health'); |
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
|
|
|
netStatus.textContent = data.status === 'online' ? "En ligne" : "Maintenance"; |
|
|
netStatus.className = data.status === 'online' ? "text-emerald-400" : "text-amber-400"; |
|
|
jobStatus.textContent = data.status === 'online' ? "Prêt" : "Maintenance"; |
|
|
jobStatus.className = data.status === 'online' ? "text-emerald-400" : "text-amber-400"; |
|
|
|
|
|
if (data.status === 'online') { |
|
|
|
|
|
document.getElementById('btnSend').disabled = false; |
|
|
document.getElementById('btnMic').disabled = false; |
|
|
document.getElementById('btnGenImg')?.disabled = false; |
|
|
document.getElementById('btnGenVid')?.disabled = false; |
|
|
document.getElementById('prompt').placeholder = "Search or write..."; |
|
|
|
|
|
return true; |
|
|
} else { |
|
|
addMsg("System", ` |
|
|
<div class="text-amber-400">⚠️ Maintenance en cours</div> |
|
|
<div class="mt-1 text-sm">Fonctionnalités limitées pendant la maintenance.</div> |
|
|
`); |
|
|
return false; |
|
|
} |
|
|
} |
|
|
throw new Error(`HTTP ${response.status}`); |
|
|
|
|
|
} catch (err) { |
|
|
retries++; |
|
|
lastError = err; |
|
|
if (retries >= 3) { |
|
|
throw lastError; |
|
|
} |
|
|
await new Promise(resolve => setTimeout(resolve, 2000)); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
const ROSALINDA_API = "http://localhost:3000"; |
|
|
const ROSALINDA_TOKEN = "YOUR_SECRET_TOKEN"; |
|
|
async function checkBackendAlive() { |
|
|
try { |
|
|
const res = await fetch(`${ROSALINDA_API}/health`, { cache: "no-store" }); |
|
|
if (!res.ok) return false; |
|
|
const data = await res.json(); |
|
|
return data?.status === "online"; |
|
|
} catch { |
|
|
return false; |
|
|
} |
|
|
} |
|
|
|
|
|
async function withTimeout(promise, ms = 8000) { |
|
|
return Promise.race([ |
|
|
promise, |
|
|
new Promise((_, reject) => |
|
|
setTimeout(() => reject(new Error("TIMEOUT IA")), ms) |
|
|
) |
|
|
]); |
|
|
} |
|
|
async function queryRosalinda(prompt) { |
|
|
setStatus("Traitement...", "work"); |
|
|
let retries = 0; |
|
|
|
|
|
|
|
|
const ollamaAvailable = await checkOllama(); |
|
|
if (ollamaAvailable) { |
|
|
try { |
|
|
const response = await fetch('http://localhost:11434/api/generate', { |
|
|
method: 'POST', |
|
|
headers: { 'Content-Type': 'application/json' }, |
|
|
body: JSON.stringify({ |
|
|
model: 'llama3.1', |
|
|
prompt: `Tu es Rosalinda, IA privée. Réponds en français de façon professionnelle.\n\nUser: ${prompt}\nRosalinda:`, |
|
|
stream: false |
|
|
}) |
|
|
}); |
|
|
|
|
|
if (!response.ok) throw new Error(`Ollama error: ${response.status}`); |
|
|
const data = await response.json(); |
|
|
return data.response; |
|
|
} catch (err) { |
|
|
console.error("Ollama error:", err); |
|
|
|
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
const alive = await checkBackendAlive(); |
|
|
if (!alive) { |
|
|
throw new Error("Backend IA indisponible"); |
|
|
} |
|
|
|
|
|
while (retries < 3) { |
|
|
try { |
|
|
|
|
|
const response = await withTimeout(fetch(`${ROSALINDA_API}/rosalinda/chat`, { |
|
|
method: "POST", |
|
|
headers: { |
|
|
"Content-Type": "application/json", |
|
|
"Authorization": `Bearer ${ROSALINDA_TOKEN}` |
|
|
}, |
|
|
body: JSON.stringify({ message: prompt }) |
|
|
})); |
|
|
if (!response.ok) { |
|
|
const errorData = await response.json(); |
|
|
throw new Error(errorData?.error || "Request failed"); |
|
|
} |
|
|
|
|
|
|
|
|
const data = await response.json(); |
|
|
console.log("EXECUTION OK:", data.execution_id); |
|
|
return data.reply; |
|
|
} catch (err) { |
|
|
retries++; |
|
|
console.error(`Attempt ${retries} failed:`, err); |
|
|
retryPill.textContent = `${retries} / 3`; |
|
|
|
|
|
if (retries >= 3) { |
|
|
throw new Error("Échec après 3 tentatives"); |
|
|
} |
|
|
|
|
|
await new Promise(resolve => setTimeout(resolve, 2000 * retries)); |
|
|
} |
|
|
} |
|
|
} |
|
|
|
|
|
codeEl.value = `<!DOCTYPE html> |
|
|
<html> |
|
|
<head> |
|
|
<title>My App</title> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> |
|
|
<meta charset="utf-8"> |
|
|
<script src="https://cdn.tailwindcss.com"></script> |
|
|
</head> |
|
|
<body class="flex justify-center items-center h-screen overflow-hidden bg-white font-sans text-center px-6"> |
|
|
<div class="w-full"> |
|
|
<span class="text-xs rounded-full mb-2 inline-block px-2 py-1 border border-amber-500/15 bg-amber-500/15 text-amber-500">🔥 New version!</span> |
|
|
<h1 class="text-4xl lg:text-6xl font-bold font-sans"> |
|
|
<span class="text-2xl lg:text-4xl text-gray-400 block font-medium">I'm ready to work,</span> |
|
|
Ask me anything. |
|
|
</h1> |
|
|
</div> |
|
|
</body> |
|
|
</html>`; |
|
|
|
|
|
async function initialize() { |
|
|
try { |
|
|
jobStatus.textContent = "Connecting..."; |
|
|
|
|
|
|
|
|
const ollamaAvailable = await checkOllama(); |
|
|
if (ollamaAvailable) { |
|
|
netStatus.textContent = "Ollama Local"; |
|
|
netStatus.className = "text-emerald-400"; |
|
|
jobStatus.textContent = "Prêt"; |
|
|
addMsg("Rosalinda", "Bonjour ! Je suis Rosalinda en mode local (Ollama). Comment puis-je vous aider ?"); |
|
|
return; |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
const response = await fetch(`${ROSALINDA_API}/health`); |
|
|
if (response.ok) { |
|
|
const data = await response.json(); |
|
|
netStatus.textContent = data.status === 'online' ? "En ligne" : "Maintenance"; |
|
|
netStatus.className = data.status === 'online' ? "text-emerald-400" : "text-amber-400"; |
|
|
jobStatus.textContent = "Prêt"; |
|
|
addMsg("Rosalinda", "Bonjour ! Je suis Rosalinda, votre assistante IA privée. Comment puis-je vous aider aujourd'hui ?"); |
|
|
return; |
|
|
} |
|
|
} catch (err) { |
|
|
console.error("Backend connection error:", err); |
|
|
enableOfflineMode(); |
|
|
} |
|
|
|
|
|
setInterval(async () => { |
|
|
try { |
|
|
await fetch(`${ROSALINDA_API}/health`); |
|
|
netStatus.textContent = "En ligne"; |
|
|
netStatus.className = "text-emerald-400"; |
|
|
} catch (e) { |
|
|
enableOfflineMode(); |
|
|
} |
|
|
}, 30000); |
|
|
applyPreview(); |
|
|
switchTab('preview'); |
|
|
} catch (err) { |
|
|
console.error("Initialization error:", err); |
|
|
enableOfflineMode(); |
|
|
} |
|
|
} |
|
|
function enableOfflineMode() { |
|
|
jobStatus.textContent = "Hors ligne"; |
|
|
jobStatus.className = "text-rose-400"; |
|
|
netStatus.textContent = "Hors ligne"; |
|
|
netStatus.className = "text-rose-400"; |
|
|
setStatus("Mode hors ligne", "err"); |
|
|
|
|
|
document.body.classList.add("locked"); |
|
|
addMsg("System", ` |
|
|
<div class="text-amber-400">⚠️ Mode hors ligne activé</div> |
|
|
<div class="mt-2 text-sm">Certaines fonctionnalités sont désactivées :</div> |
|
|
<ul class="mt-1 text-xs space-y-1"> |
|
|
<li>• Vérifiez que le serveur Rosalinda est en marche</li> |
|
|
<li>• Si local: <code class="bg-black/20 px-1">node server.js</code></li> |
|
|
<li>• Pour Ollama: <code class="bg-black/20 px-1">ollama serve</code></li> |
|
|
<li>• Si cloud: vérifiez l'URL de l'espace HF</li> |
|
|
</ul> |
|
|
<div class="mt-2 text-xs">Solutions locales: installer Ollama ou lancer le serveur Rosalinda.</div> |
|
|
`); |
|
|
|
|
|
document.getElementById('btnSend').disabled = false; |
|
|
document.getElementById('btnMic').disabled = false; |
|
|
document.getElementById('prompt').placeholder = "Essayez de relancer le serveur Rosalinda"; |
|
|
} |
|
|
initialize(); |
|
|
applyPreview(); |
|
|
switchTab('preview'); |
|
|
}); |
|
|
|
|
|
|
|
|
function escapeHtml(s) { |
|
|
return String(s || "") |
|
|
.replaceAll("&","&") |
|
|
.replaceAll("<","<") |
|
|
.replaceAll(">",">"); |
|
|
} |
|
|
|
|
|
|
|
|
const originalFetch = window.fetch; |
|
|
window.fetch = async function(...args) { |
|
|
let retries = 3; |
|
|
while (retries > 0) { |
|
|
try { |
|
|
const response = await originalFetch(...args); |
|
|
if (response.ok) return response; |
|
|
throw new Error(`HTTP error ${response.status}`); |
|
|
} catch (err) { |
|
|
retries--; |
|
|
if (retries === 0) throw err; |
|
|
await new Promise(resolve => setTimeout(resolve, 2000)); |
|
|
} |
|
|
} |
|
|
}; |
|
|
|