| <!DOCTYPE html> |
| <html lang="vi"> |
| <head> |
| <meta charset="utf-8"> |
| <title>Botchat CVNSS 4.0</title> |
|
|
| |
| <script src="cvnss4.0-converter.js"></script> |
|
|
| <style> |
| body{font-family:Arial,Helvetica,sans-serif;background:#f5f7fa;margin:30px auto;max-width:760px;color:#333} |
| h2{text-align:center;color:#2c3e50;margin-bottom:20px} |
| #chat{background:#fff;border:1px solid #dcdcdc;border-radius:8px;min-height:340px;max-height:540px; |
| overflow-y:auto;padding:18px;margin-bottom:16px} |
| .msg{margin-bottom:16px;line-height:1.45} |
| .user b{color:#2980b9}.bot b{color:#27ae60} |
| #bar{display:flex;gap:8px} |
| textarea{flex:1;padding:10px 12px;font-size:15px;border:1px solid #ccc;border-radius:6px;resize:vertical;min-height:48px} |
| button{padding:11px 22px;font-size:15px;border:none;border-radius:6px;cursor:pointer;background:#2c97de;color:#fff} |
| button:hover{filter:brightness(1.1)}button:active{filter:brightness(.93)} |
| .spinner{display:inline-block;width:16px;height:16px;border:3px solid #bbb;border-top-color:#2c97de;border-radius:50%; |
| animation:spin 1s linear infinite;margin-left:6px;vertical-align:middle} |
| @keyframes spin{to{transform:rotate(360deg)}} |
| </style> |
| </head> |
| <body> |
| <h2>💬 Botchat - 1 sản phẩm của CVNSS4.0</h2> |
|
|
| <div id="chat"></div> |
|
|
| <div id="bar"> |
| |
| <textarea id="input" placeholder="Nhập tin nhắn…"></textarea> |
| <button id="send">Gửi</button> |
| </div> |
|
|
| <script> |
| |
| const GROQ_KEY = "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"; |
| const ENDPOINT = "https://api.groq.com/openai/v1/chat/completions"; |
| const MODEL = "meta-llama/llama-4-scout-17b-16e-instruct"; |
| |
| |
| const chat = document.getElementById("chat"); |
| const input = document.getElementById("input"); |
| const sendBt = document.getElementById("send"); |
| const spin = '<span class="spinner"></span>'; |
| const toCVN = txt => CVNSSConverter.convert(txt,"cqn").cvn; |
| const scroll = () => chat.scrollTop = chat.scrollHeight; |
| |
| |
| function add(role, html){ |
| chat.insertAdjacentHTML("beforeend", |
| `<div class="msg ${role}"><b>${role==='user'?'Bạn':'GPT'}:</b> ${html}</div>`); |
| scroll(); |
| } |
| |
| |
| async function askGroq(prompt){ |
| const body = JSON.stringify({ |
| model: MODEL, |
| messages: [{ role:"user", content: prompt }] |
| }); |
| |
| for(let i=0;i<3;i++){ |
| const res = await fetch(ENDPOINT,{ |
| method:"POST", |
| headers:{ |
| "Content-Type":"application/json", |
| "Authorization":`Bearer ${GROQ_KEY}` |
| }, |
| body |
| }); |
| |
| if(res.ok){ |
| const j = await res.json(); |
| return j.choices[0].message.content.trim(); |
| } |
| |
| if([429,503].includes(res.status)){ |
| const wait = 1500*(i+1); |
| console.warn(`Groq ${res.status} – đợi ${wait/1000}s rồi thử lại`); |
| await new Promise(r=>setTimeout(r,wait)); |
| continue; |
| } |
| throw new Error(`HTTP ${res.status} – ${await res.text()}`); |
| } |
| throw new Error("Groq quá tải – thử lại sau."); |
| } |
| |
| |
| async function send(){ |
| const text = input.value.trim(); |
| if(!text) return; |
| input.value=""; add("user", text); add("bot", spin); |
| const last = chat.lastElementChild; sendBt.disabled=true; |
| |
| try{ |
| const raw = await askGroq(text); |
| last.innerHTML = `<b>GPT:</b> ${toCVN(raw)}`; |
| }catch(err){ |
| last.innerHTML = `<b>GPT:</b> ⚠️ ${err.message}`; |
| console.error(err); |
| }finally{ |
| sendBt.disabled=false; input.focus(); |
| } |
| } |
| |
| |
| sendBt.onclick = send; |
| input.addEventListener("keydown", e=>{ |
| if(e.key==="Enter" && !e.shiftKey){ |
| e.preventDefault(); send(); |
| } |
| }); |
| |
| input.focus(); |
| |
| |
| if(location.protocol==='file:'){ |
| console.warn("Chạy: python -m http.server 8080 ➔ http://localhost:8080/"); |
| } |
| </script> |
| </body> |
| </html> |
|
|