Anos-AI / index.html
maletsidibeOS's picture
Anos AI chat UI
1a30b1d verified
Raw History Blame Contribute Delete
7.04 kB
<!DOCTYPE html>
<html lang="fr">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Anos AI — Fabrique maletsidibeOS</title>
<style>
:root{
--bg:#0d1117; --panel:#161b22; --border:#30363d;
--text:#e6edf3; --muted:#8b949e; --accent:#7c5cff; --accent2:#00d4aa;
}
*{box-sizing:border-box;margin:0;padding:0}
body{background:var(--bg);color:var(--text);font-family:'Segoe UI',system-ui,sans-serif;height:100vh;display:flex;flex-direction:column}
header{display:flex;align-items:center;gap:12px;padding:14px 20px;border-bottom:1px solid var(--border);background:var(--panel)}
.logo{width:38px;height:38px;border-radius:10px;background:linear-gradient(135deg,var(--accent),var(--accent2));display:flex;align-items:center;justify-content:center;font-weight:700;font-size:18px}
h1{font-size:19px}
.sub{font-size:12px;color:var(--muted)}
.settings{display:flex;gap:8px;margin-left:auto;align-items:center;flex-wrap:wrap}
input,select,button{background:var(--bg);border:1px solid var(--border);color:var(--text);border-radius:8px;padding:8px 10px;font-size:13px}
input[type=password]{width:230px}
button{cursor:pointer;background:var(--accent);border:none;font-weight:600}
button:disabled{opacity:.5;cursor:wait}
#chat{flex:1;overflow-y:auto;padding:20px;display:flex;flex-direction:column;gap:14px}
.msg{max-width:78%;padding:11px 15px;border-radius:12px;line-height:1.55;font-size:14px;white-space:pre-wrap;word-wrap:break-word}
.msg.user{align-self:flex-end;background:var(--accent);border-bottom-right-radius:4px}
.msg.assistant{align-self:flex-start;background:var(--panel);border:1px solid var(--border);border-bottom-left-radius:4px}
.msg.error{align-self:center;background:#3d1d1f;border:1px solid #f85149;color:#ffa198;font-size:13px}
.typing{color:var(--muted);font-style:italic}
#composer{display:flex;gap:10px;padding:14px 20px;border-top:1px solid var(--border);background:var(--panel)}
#prompt{flex:1;resize:none;height:52px;font-family:inherit}
code{background:#0a0e14;border:1px solid var(--border);border-radius:4px;padding:1px 5px;font-family:Consolas,monospace;font-size:13px;color:var(--accent2)}
footer{padding:6px 20px 10px;text-align:center;font-size:11px;color:var(--muted)}
a{color:var(--accent2)}
</style>
</head>
<body>
<header>
<div class="logo">A</div>
<div>
<h1>Anos AI</h1>
<div class="sub">Fabrique maletsidibeOS · assistante dev</div>
</div>
<div class="settings">
<select id="model" title="Modèle sous le capot">
<option value="openai/gpt-oss-120b">gpt-oss-120b (dev, recommandé)</option>
<option value="Qwen/Qwen2.5-Coder-32B-Instruct">Qwen2.5-Coder-32B</option>
<option value="meta-llama/Llama-3.1-8B-Instruct">Llama-3.1-8B-Instruct</option>
<option value="Qwen/Qwen2.5-7B-Instruct">Qwen2.5-7B-Instruct</option>
</select>
<input id="token" type="password" placeholder="Token HF (hf_...)">
</div>
</header>
<div id="chat">
<div class="msg assistant">Salut 👋 Je suis <b>Anos AI</b>. Pose ta question de dev — je réponds court, avec du code qui marche.</div>
</div>
<div id="composer">
<textarea id="prompt" placeholder="Ex : une fonction Python qui déduplique une liste de dicts par clé id"></textarea>
<button id="send">Envoyer</button>
</div>
<footer>Modèle open-source via les <a href="https://huggingface.co/docs/inference-providers" target="_blank">Inference Providers</a> HF · ton token reste dans ton navigateur</footer>
<script>
const SYSTEM_PROMPT =
"Tu es Anos AI, l'assistante de développement créée par maletsidibeOS. " +
"Style : réponses courtes, directes, orientées code. " +
"Donne d'abord le code, puis au maximum deux phrases d'explication. " +
"Réponds en français si la question est en français, sinon dans la langue de l'utilisateur.";
const API_URL = "https://router.huggingface.co/v1/chat/completions";
const chat = document.getElementById("chat");
const promptEl = document.getElementById("prompt");
const sendBtn = document.getElementById("send");
const tokenEl = document.getElementById("token");
const modelEl = document.getElementById("model");
let history = [];
let busy = false;
function addBubble(cls, text) {
const div = document.createElement("div");
div.className = "msg " + cls;
div.textContent = text;
chat.appendChild(div);
chat.scrollTop = chat.scrollHeight;
return div;
}
async function send() {
const text = promptEl.value.trim();
const token = tokenEl.value.trim();
if (!text || busy) return;
if (!token) { addBubble("error", "⚠️ Colle d'abord ton token Hugging Face (hf_...) en haut. Crée-le sur huggingface.co/settings/tokens."); return; }
promptEl.value = "";
addBubble("user", text);
history.push({ role: "user", content: text });
busy = true;
sendBtn.disabled = true;
const bubble = addBubble("assistant", "");
const typing = document.createElement("span");
typing.className = "typing";
typing.textContent = "Anos AI réfléchit…";
bubble.appendChild(typing);
try {
const res = await fetch(API_URL, {
method: "POST",
headers: { "Authorization": "Bearer " + token, "Content-Type": "application/json" },
body: JSON.stringify({
model: modelEl.value,
messages: [{ role: "system", content: SYSTEM_PROMPT }, ...history],
stream: true,
max_tokens: 1500
})
});
if (!res.ok) {
const errText = await res.text();
throw new Error(res.status === 401 ? "Token invalide (401). Vérifie ton token HF." : "Erreur " + res.status + " : " + errText.slice(0, 200));
}
typing.remove();
let full = "";
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop();
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("data:")) continue;
const payload = trimmed.slice(5).trim();
if (payload === "[DONE]") continue;
try {
const delta = JSON.parse(payload).choices?.[0]?.delta?.content;
if (delta) { full += delta; bubble.textContent = full; chat.scrollTop = chat.scrollHeight; }
} catch (e) { /* fragment JSON incomplet, ignoré */ }
}
}
if (!full) full = "(réponse vide)";
bubble.textContent = full;
history.push({ role: "assistant", content: full });
} catch (err) {
typing.remove();
bubble.remove();
addBubble("error", "⚠️ " + err.message);
} finally {
busy = false;
sendBtn.disabled = false;
promptEl.focus();
}
}
sendBtn.addEventListener("click", send);
promptEl.addEventListener("keydown", e => {
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); send(); }
});
promptEl.focus();
</script>
</body>
</html>