v2: шағым-бот (ИИ-кеңесші) + сервер
This commit is contained in:
parent
9b2f408771
commit
fbcf0ec913
16
index.html
16
index.html
@ -19,6 +19,7 @@
|
|||||||
<a href="#timer">Жаттығу</a>
|
<a href="#timer">Жаттығу</a>
|
||||||
<a href="#tips">Кеңестер</a>
|
<a href="#tips">Кеңестер</a>
|
||||||
<a href="#meds">Дәрі</a>
|
<a href="#meds">Дәрі</a>
|
||||||
|
<a href="#chat">Шағым</a>
|
||||||
</nav>
|
</nav>
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
@ -127,6 +128,21 @@
|
|||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
|
<section class="section dark" id="chat">
|
||||||
|
<div class="container">
|
||||||
|
<h2>💬 Шағымыңды айт</h2>
|
||||||
|
<p class="lead">Ауырсыну, қаттылық немесе басқа шағымыңды жаз — жүйе не істеу керек екенін айтады. Мысалы: «таңертең арқам қатты, қозғалу қиын».</p>
|
||||||
|
<div class="chat" id="chatBox">
|
||||||
|
<div class="msg bot">Сәлем! Шағымыңды жаз: ауырсыну қайда, қашан пайда болады, не істегенде жеңілдейді?</div>
|
||||||
|
</div>
|
||||||
|
<div class="chat-in">
|
||||||
|
<textarea id="chatInput" rows="2" placeholder="Мысалы: түнде бел ауырады, таңертең қиын тұрамын..."></textarea>
|
||||||
|
<button class="btn" id="chatSend">Жіберу</button>
|
||||||
|
</div>
|
||||||
|
<p class="note-light">Жауапты жасанды интеллект береді — бұл дәрігер кеңесінің орнын баспайды. Өткір ауырсыну болса — дәрігерге қарал.</p>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
<footer class="footer">
|
<footer class="footer">
|
||||||
<div class="container">
|
<div class="container">
|
||||||
<p>🦴 Буын денсаулығы — күнделікті көмекші</p>
|
<p>🦴 Буын денсаулығы — күнделікті көмекші</p>
|
||||||
|
|||||||
6
package.json
Normal file
6
package.json
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"name": "buyn-densaulygy",
|
||||||
|
"private": true,
|
||||||
|
"type": "commonjs",
|
||||||
|
"scripts": { "start": "node server.js" }
|
||||||
|
}
|
||||||
43
script.js
43
script.js
@ -179,3 +179,46 @@ var io = new IntersectionObserver(function (entries) {
|
|||||||
entries.forEach(function (e) { if (e.isIntersecting) e.target.classList.add("in"); });
|
entries.forEach(function (e) { if (e.isIntersecting) e.target.classList.add("in"); });
|
||||||
}, { threshold: .15 });
|
}, { threshold: .15 });
|
||||||
document.querySelectorAll(".reveal").forEach(function (el) { io.observe(el); });
|
document.querySelectorAll(".reveal").forEach(function (el) { io.observe(el); });
|
||||||
|
|
||||||
|
var chatBox = document.getElementById("chatBox");
|
||||||
|
var chatInput = document.getElementById("chatInput");
|
||||||
|
var chatSend = document.getElementById("chatSend");
|
||||||
|
|
||||||
|
function addMsg(who, text) {
|
||||||
|
var d = document.createElement("div");
|
||||||
|
d.className = "msg " + who;
|
||||||
|
d.textContent = text;
|
||||||
|
chatBox.appendChild(d);
|
||||||
|
chatBox.scrollTop = chatBox.scrollHeight;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function sendChat() {
|
||||||
|
var text = chatInput.value.trim();
|
||||||
|
if (!text) return;
|
||||||
|
addMsg("user", text);
|
||||||
|
chatInput.value = "";
|
||||||
|
var waiting = document.createElement("div");
|
||||||
|
waiting.className = "msg bot typing";
|
||||||
|
waiting.textContent = "Жазып жатыр…";
|
||||||
|
chatBox.appendChild(waiting);
|
||||||
|
chatBox.scrollTop = chatBox.scrollHeight;
|
||||||
|
try {
|
||||||
|
var r = await fetch("/api/advice", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: JSON.stringify({ text: text })
|
||||||
|
});
|
||||||
|
var j = await r.json();
|
||||||
|
waiting.remove();
|
||||||
|
if (j.ok) addMsg("bot", j.answer);
|
||||||
|
else addMsg("bot", "Кешіріңіз, жауап қазір келмеді. Кейінірек қайталап көріңіз немесе шағымыңды басқаша жазып жіберіңіз.");
|
||||||
|
} catch (e) {
|
||||||
|
waiting.remove();
|
||||||
|
addMsg("bot", "Қате болды. Байланысты тексеріп, қайталап көріңіз.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
chatSend.addEventListener("click", sendChat);
|
||||||
|
chatInput.addEventListener("keydown", function (e) {
|
||||||
|
if (e.key === "Enter" && !e.shiftKey) { e.preventDefault(); sendChat(); }
|
||||||
|
});
|
||||||
|
|||||||
66
server.js
Normal file
66
server.js
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
const http = require("http");
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const PORT = process.env.PORT || 3000;
|
||||||
|
|
||||||
|
function body(req) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
let s = "";
|
||||||
|
req.on("data", (c) => (s += c));
|
||||||
|
req.on("end", () => {
|
||||||
|
try { resolve(JSON.parse(s || "{}")); } catch (_) { resolve({}); }
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function ai(prompt) {
|
||||||
|
const r = await fetch(process.env.AI_BASE_URL + "/chat/completions", {
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json", Authorization: "Bearer " + process.env.AI_API_KEY },
|
||||||
|
body: JSON.stringify({
|
||||||
|
model: process.env.AI_MODEL,
|
||||||
|
temperature: 0.4,
|
||||||
|
messages: [
|
||||||
|
{ role: "system", content: "Сен — артрит, артроз және анкилозды спондилит (Бехтерев ауруы) бар адамдарға арналған көмекші-кеңесші. Қазақ тілінде, жай сөзбен, қысқа әрі нақты жауап бер. Жауабыңда мыналар болсын: 1) шағымды қалай бағалауға болады; 2) қазір не істеу керек (жеңіл қозғалыс, жылу/суық, тыныштық) — жаттығуды тек ауырсынбай ғана орындау керектігін ескерт; 3) дәрі-дәрмекті тек дәрігер тағайындайтынын еске сал, дәрі атын ұсынба; 4) қандай белгілер кезінде дәрігерге шұғыл қаралу керектігін анық жаз (қатты күшейген ауырсыну, қатты ісіну, қызба, аяқ-қолды қозғалта алмау, сезімталдық жоғалуы). Ақырында: «Бұл ақпарат дәрігер кеңесінің орнын баспайды» деп қос. Сен медициналық маман емессің." },
|
||||||
|
{ role: "user", content: prompt }
|
||||||
|
]
|
||||||
|
})
|
||||||
|
});
|
||||||
|
if (!r.ok) throw new Error("ИИ недоступен, код " + r.status);
|
||||||
|
const j = await r.json();
|
||||||
|
const out = j.choices?.[0]?.message?.content;
|
||||||
|
if (!out) throw new Error("ИИ вернул пустой ответ");
|
||||||
|
return out.trim();
|
||||||
|
}
|
||||||
|
|
||||||
|
http.createServer(async (req, res) => {
|
||||||
|
const url = req.url.split("?")[0];
|
||||||
|
|
||||||
|
if (req.method === "POST" && url === "/api/advice") {
|
||||||
|
const data = await body(req);
|
||||||
|
const text = (data.text || "").trim();
|
||||||
|
if (!text) {
|
||||||
|
res.writeHead(400, { "Content-Type": "application/json; charset=utf-8" });
|
||||||
|
return res.end(JSON.stringify({ ok: false, error: "Хабарлама бос" }));
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const answer = await ai(text);
|
||||||
|
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
|
||||||
|
return res.end(JSON.stringify({ ok: true, answer }));
|
||||||
|
} catch (e) {
|
||||||
|
res.writeHead(502, { "Content-Type": "application/json; charset=utf-8" });
|
||||||
|
return res.end(JSON.stringify({ ok: false, error: "ИИ временно недоступен, попробуйте ещё раз" }));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let file = url === "/" ? "/index.html" : url;
|
||||||
|
const full = path.join(__dirname, file);
|
||||||
|
if (full.startsWith(__dirname) && fs.existsSync(full) && fs.statSync(full).isFile()) {
|
||||||
|
const ext = path.extname(full);
|
||||||
|
const type = ext === ".css" ? "text/css" : ext === ".js" ? "application/javascript" : "text/html";
|
||||||
|
res.writeHead(200, { "Content-Type": type + "; charset=utf-8" });
|
||||||
|
return res.end(fs.readFileSync(full));
|
||||||
|
}
|
||||||
|
res.writeHead(404); res.end("Not found");
|
||||||
|
}).listen(PORT, () => console.log("Сервер на порту " + PORT));
|
||||||
15
style.css
15
style.css
@ -70,8 +70,19 @@ h1,h2,h3{font-family:"Unbounded",sans-serif;line-height:1.15}
|
|||||||
|
|
||||||
.warn{background:#fff6e6;border:1px solid #f0d9a8;border-radius:12px;padding:18px 22px;margin-top:8px;font-size:15px;color:#5a4a1e}
|
.warn{background:#fff6e6;border:1px solid #f0d9a8;border-radius:12px;padding:18px 22px;margin-top:8px;font-size:15px;color:#5a4a1e}
|
||||||
|
|
||||||
.footer{background:var(--ink);color:#9aa3b2;text-align:center}
|
/* чат */
|
||||||
.footer p{font-weight:700;color:var(--white)}
|
.chat{background:#141a26;border:1px solid #222b3c;border-radius:16px;padding:18px;max-height:420px;overflow-y:auto;display:flex;flex-direction:column;gap:10px;margin-bottom:14px}
|
||||||
|
.msg{max-width:80%;padding:12px 16px;border-radius:14px;font-size:15px;line-height:1.55;white-space:pre-wrap;word-wrap:break-word}
|
||||||
|
.msg.bot{background:#1d2738;color:#e3eaf4;align-self:flex-start;border-bottom-left-radius:4px}
|
||||||
|
.msg.user{background:var(--cyan);color:var(--ink);align-self:flex-end;border-bottom-right-radius:4px;font-weight:600}
|
||||||
|
.msg.typing{color:#7d8796;font-style:italic}
|
||||||
|
.chat-in{display:flex;gap:10px}
|
||||||
|
.chat-in textarea{flex:1;background:#141a26;border:1px solid #222b3c;border-radius:10px;color:var(--white);padding:12px 14px;font:15px/1.5 "Manrope",sans-serif;resize:vertical;min-height:52px}
|
||||||
|
.chat-in textarea:focus{outline:none;border-color:var(--cyan)}
|
||||||
|
.chat-in textarea::placeholder{color:#5a6575}
|
||||||
|
.note-light{color:#8a93a3;font-size:14px;margin-top:6px}
|
||||||
|
|
||||||
|
.footer{background:var(--ink);color:#9aa3b2;text-align:center}.footer p{font-weight:700;color:var(--white)}
|
||||||
.footer .foot-small{color:#7d8796;font-weight:400;margin-top:4px;font-size:14px}
|
.footer .foot-small{color:#7d8796;font-weight:400;margin-top:4px;font-size:14px}
|
||||||
|
|
||||||
.reveal{opacity:0;transform:translateY(20px);transition:opacity .6s ease,transform .6s ease}
|
.reveal{opacity:0;transform:translateY(20px);transition:opacity .6s ease,transform .6s ease}
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user