smartstudy-buddyhub / index.html
itamarlifshitz's picture
make the site look bettrer and every button working and doing its job
2a1f177 verified
raw
history blame
14.1 kB
// server.mjs
// One-file MVP: Node server + HTML chat UI (RTL Hebrew) + AI proxy
// ===== HOW TO RUN =====
// 1) Save this file as server.mjs
// 2) In terminal set env vars (PowerShell example):
// $env:AI_API_KEY="YOUR_KEY_HERE"
// $env:AI_BASE_URL="https://ai.gateway.lovable.dev/v1" # optional, default set below
// $env:AI_MODEL="google/gemini-2.5-flash" # optional
// node server.mjs
// 3) Open http://localhost:5173
//
// Notes:
// - Your API key stays on the server (safe). The browser never sees it.
// - You can paste your own study text ("context") so the bot answers based on it.
// - Context is saved in localStorage, and also sent per-message to the server as part of the system prompt.
import http from "node:http";
import { readFile } from "node:fs/promises";
import { fileURLToPath } from "node:url";
import { dirname, join } from "node:path";
// ====== CONFIG ======
const PORT = process.env.PORT ? Number(process.env.PORT) : 5173;
const AI_BASE_URL = process.env.AI_BASE_URL || "https://ai.gateway.lovable.dev/v1";
const AI_MODEL = process.env.AI_MODEL || "google/gemini-2.5-flash";
const AI_API_KEY = process.env.AI_API_KEY || ""; // MUST be set!
// ====== MINI ASSETS (single-file, inline CSS/JS) ======
const HTML = `<!doctype html>
<html lang="he" dir="rtl">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>ืขื•ื–ืจ ืœื™ืžื•ื“ื™ื ื‘ื™ืช-ืกืคืจื™ โ€” MVP</title>
<style>
:root{
--bg: hsl(210 40% 98%);
--card: #fff;
--border: hsl(220 13% 91%);
--text: hsl(220 15% 20%);
--muted: hsl(220 15% 55%);
--primary: hsl(195 85% 45%);
--primary2: hsl(195 85% 65%);
--shadow: 0 8px 24px rgba(14,165,233,.12);
--radius: 16px;
}
*{box-sizing:border-box}
html{font-family:system-ui,-apple-system,Segoe UI,Rubik,Arial,sans-serif;background:var(--bg);color:var(--text)}
.container{max-width:980px;margin:0 auto;padding:16px}
header{background:var(--card);border-bottom:1px solid var(--border)}
.head{display:flex;align-items:center;justify-content:space-between;gap:12px;padding:12px 0}
.brand{display:flex;align-items:center;gap:10px}
.logo{width:44px;height:44px;border-radius:10px;background:linear-gradient(135deg,var(--primary),var(--primary2));display:grid;place-items:center;box-shadow:var(--shadow)}
.logo svg{fill:#fff;opacity:.9}
.title{font-weight:700}
.sub{font-size:12px;color:var(--muted)}
nav{display:flex;gap:8px}
.btn{display:inline-flex;align-items:center;justify-content:center;background:var(--primary);color:#fff;border:none;padding:10px 14px;border-radius:12px;cursor:pointer;box-shadow:var(--shadow)}
.btn:hover{filter:brightness(1.05)}
.btn-ghost{background:#fff;color:var(--primary);border:1px solid var(--primary)}
.btn-ghost:hover{background:rgba(14,165,233,.06)}
.grid{display:grid;gap:16px}
.card{background:var(--card);border:1px solid var(--border);border-radius:var(--radius);box-shadow:var(--shadow);padding:16px}
.row{display:grid;grid-template-columns:1fr 2fr;gap:16px}
@media (max-width:800px){.row{grid-template-columns:1fr}}
label{font-size:14px;color:var(--muted);display:block;margin-bottom:6px}
input,textarea,select{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:12px;background:#fff;outline:none}
textarea{min-height:120px;resize:vertical}
.muted{color:var(--muted);font-size:13px}
.chat{min-height:50vh;max-height:70vh;overflow:auto;display:flex;flex-direction:column;gap:10px}
.bubble{max-width:76%;border:1px solid var(--border);border-radius:14px;padding:10px 12px;background:#fff}
.me{margin-left:auto;background:rgba(14,165,233,.08);border-color:rgba(14,165,233,.25)}
.bot .who,.me .who{font-size:12px;color:var(--muted);margin-bottom:4px}
.toolbar{display:flex;gap:8px}
.badge{display:inline-flex;align-items:center;font-size:12px;color:var(--muted);border:1px solid var(--border);padding:2px 8px;border-radius:999px}
.row-compact{display:flex;gap:8px}
</style>
</head>
<body>
<header>
<div class="container head">
<div class="brand">
<div class="logo" title="ื™ืฆื—ืง ืฉืžื™ืจ โ€” ืกืžืœ ืžืจื•ื‘ืข, ืœืœื ืขื™ื’ื•ืœ ืคื™ื ื•ืช">
<svg viewBox="0 0 24 24" width="22" height="22"><path d="M12 3l7 4v6c0 3.866-3.582 7-8 7s-8-3.134-8-7V7l9-4z"/></svg>
</div>
<div>
<div class="title">ืขื•ื–ืจ ืœื™ืžื•ื“ื™ื ื‘ื™ืช-ืกืคืจื™</div>
<div class="sub">MVP โ€ข ืฆืณืื˜ + ื”ืงืฉืจ ืœื™ืžื•ื“ื™ (RTL)</div>
</div>
</div>
<nav>
<a class="btn-ghost" href="#context">ื—ื•ืžืจื™ ืœื™ืžื•ื“</a>
<a class="btn" href="#chat">ืฆืณืื˜</a>
</nav>
</div>
</header>
<main class="container grid" id="app">
<!-- Context / Materials -->
<section id="context" class="card">
<h2 style="margin:0 0 8px 0;">๐Ÿ“š ื—ื•ืžืจื™ ืœื™ืžื•ื“ / ื”ืงืฉืจ</h2>
<p class="muted" style="margin-top:0">ื”ื“ื‘ื™ืงื• ื›ืืŸ ืชืงืฆื™ืจ/ื˜ืงืกื˜ ืžื”ืžื—ื‘ืจืช/ื’ื•ื’ืœ ื“ื•ืงืก/ืืชืจ. ื”ื‘ื•ื˜ ื™ืชื‘ืกืก ืขืœื™ื• ื‘ืชืฉื•ื‘ื•ืช.</p>
<div class="row">
<div>
<label>ืฉื โ€œืžืจื—ื‘โ€ (ืœืžืฉืœ: ืžืชืžื˜ื™ืงื” โ€” ืคืจืง ื—ื–ืงื•ืช)</label>
<input id="spaceName" placeholder="ืฉื ื”ืžืจื—ื‘" />
<div style="height:8px"></div>
<label>ืฉืคืช ื”ื‘ื•ื˜</label>
<select id="botLang">
<option value="he">ืขื‘ืจื™ืช</option>
<option value="en">English</option>
</select>
<div style="height:8px"></div>
<span class="badge" id="stats">0 ืชื•ื•ื™ื ื‘ืงื•ื ื˜ืงืกื˜</span>
</div>
<div>
<label>ื˜ืงืกื˜ ื”ืงืฉืจ (ื ืฉืžืจ ื‘ื“ืคื“ืคืŸ, ื ืฉืœื— ืœื‘ื•ื˜ ื›ืชืžื™ื›ืช ื”ืงืฉืจ)</label>
<textarea id="contextText" placeholder="ื”ื“ื‘ื™ืงื• ื›ืืŸ ื˜ืงืกื˜ ื—ืฉื•ื‘ ืžื”ื—ื•ืžืจ..."></textarea>
<div style="height:8px"></div>
<div class="row-compact">
<button class="btn" id="saveCtx">ืฉืžื™ืจื”</button>
<button class="btn-ghost" id="clearCtx">ื ื™ืงื•ื™</button>
</div>
</div>
</div>
</section>
<!-- Chat -->
<section id="chat" class="card">
<h2 style="margin-top:0;">๐Ÿ’ฌ ืฆืณืื˜</h2>
<div class="row">
<div>
<label>ื”ื•ืจืื•ืช ืžืขืจื›ืช (ืื•ืคืฆื™ื•ื ืœื™)</label>
<textarea id="systemHint" placeholder="ืœืžืฉืœ: ืขื ื” ื‘ืงืฆืจื”, ืฆื™ื™ืŸ ืฉืœื‘ื™ื, ื”ืฉืชืžืฉ ื‘ื“ื•ื’ืžืื•ืช ืคืฉื•ื˜ื•ืช."></textarea>
<div style="height:8px"></div>
<label>ืžื•ื“ืœ (ืื•ืคืฆื™ื•ื ืœื™)</label>
<input id="model" placeholder="google/gemini-2.5-flash (ื‘ืจื™ืจืช ืžื—ื“ืœ ื‘ืฉืจืช)" />
<div style="height:8px"></div>
<div class="muted">ื”-API key ื ืฉืืจ ืขืœ ื”ืฉืจืช โ€” ื‘ื˜ื•ื—. ืื™ืŸ ืฆื•ืจืš ืœืฉื™ื ืื•ืชื• ื‘ื“ืคื“ืคืŸ.</div>
</div>
<div>
<div id="chatBox" class="chat" style="border:1px solid var(--border);border-radius:12px;padding:10px;background:#fff"></div>
<div style="height:8px"></div>
<div class="row-compact">
<input id="msg" placeholder="ืฉืืœ/ื™ ืฉืืœื” ืขืœ ืกืžืš ื”ื—ื•ืžืจ..." />
<button class="btn" id="send">ืฉืœื™ื—ื”</button>
</div>
</div>
</div>
</section>
</main>
<script>
const LS_SPACE = "mvp_space_name";
const LS_TEXT = "mvp_context_text";
const LS_LANG = "mvp_bot_lang";
const chatBox = document.getElementById("chatBox");
const msgInput = document.getElementById("msg");
const sendBtn = document.getElementById("send");
const stats = document.getElementById("stats");
const spaceName = document.getElementById("spaceName");
const contextEl = document.getElementById("contextText");
const saveCtx = document.getElementById("saveCtx");
const clearCtx = document.getElementById("clearCtx");
const botLang = document.getElementById("botLang");
const systemEl = document.getElementById("systemHint");
const modelEl = document.getElementById("model");
// Load persisted context
spaceName.value = localStorage.getItem(LS_SPACE) || "";
contextEl.value = localStorage.getItem(LS_TEXT) || "";
botLang.value = localStorage.getItem(LS_LANG) || "he";
stats.textContent = (contextEl.value.length || 0) + " ืชื•ื•ื™ื ื‘ืงื•ื ื˜ืงืกื˜";
contextEl.addEventListener("input", () => {
stats.textContent = (contextEl.value.length || 0) + " ืชื•ื•ื™ื ื‘ืงื•ื ื˜ืงืกื˜";
});
saveCtx.addEventListener("click", () => {
localStorage.setItem(LS_SPACE, spaceName.value || "");
localStorage.setItem(LS_TEXT, contextEl.value || "");
localStorage.setItem(LS_LANG, botLang.value || "he");
alert("ื ืฉืžืจ ื‘ื”ืฆืœื—ื”");
});
clearCtx.addEventListener("click", () => {
spaceName.value = "";
contextEl.value = "";
localStorage.removeItem(LS_SPACE);
localStorage.removeItem(LS_TEXT);
stats.textContent = "0 ืชื•ื•ื™ื ื‘ืงื•ื ื˜ืงืกื˜";
});
function appendBubble(role, text){
const wrap = document.createElement("div");
wrap.className = "bubble " + (role === "user" ? "me" : "bot");
const who = document.createElement("div");
who.className = "who";
who.textContent = role === "user" ? "ืืช/ื”" : "ื‘ื•ื˜";
const body = document.createElement("div");
body.textContent = text;
wrap.appendChild(who);
wrap.appendChild(body);
chatBox.appendChild(wrap);
chatBox.scrollTop = chatBox.scrollHeight;
}
async function ask(){
const q = (msgInput.value || "").trim();
if(!q) return;
appendBubble("user", q);
msgInput.value = "";
const ctx = contextEl.value || "";
const space = spaceName.value || "ื‘ืจื™ืจืช ืžื—ื“ืœ";
const lang = botLang.value || "he";
const system = (systemEl.value || "") + "\\n" +
(lang === "he"
? \`ืขื ื” ื‘ืขื‘ืจื™ืช, ื‘ืงืฆืจื” ื•ื‘ื‘ื”ื™ืจื•ืช. ื”ืฉืชืžืฉ ืงื•ื“ื ื›ื•ืœ ื‘ืชื•ื›ืŸ ื”ืงืฉืจ ืกื‘ื™ืจ ืœื”ืœืŸ.\`
: \`Answer briefly and clearly. Prioritize the provided study context.\`) +
"\\n" +
\`ืฉื ื”ืžืจื—ื‘: "\${space}".\\n\\nื”ืงืฉืจ/ื—ื•ืžืจื™ ืœื™ืžื•ื“:\\n\${ctx}\`;
try{
const resp = await fetch("/api/chat", {
method: "POST",
headers: {"Content-Type":"application/json"},
body: JSON.stringify({ message: q, system, model: (modelEl.value || undefined) })
});
const j = await resp.json();
if(!resp.ok || !j.ok){
appendBubble("assistant", (j.error || "ืฉื’ื™ืื” ื‘ืฉื™ืจื•ืช ื”ื‘ื™ื ื”"));
return;
}
appendBubble("assistant", j.text || (lang==="he"?"ืื™ืŸ ืชืฉื•ื‘ื”":"No response"));
}catch(e){
appendBubble("assistant", (e?.message || "ืฉื’ื™ืืช ืจืฉืช"));
}
}
sendBtn.addEventListener("click", ask);
msgInput.addEventListener("keydown", (e)=>{ if(e.key==="Enter") ask(); });
// Warm greeting
if(!contextEl.value){
appendBubble("assistant", "ื”ื™ื™! ื”ื•ืกื™ืคื• ื˜ืงืกื˜ ื”ืงืฉืจ (ืœืžืฉืœ ืชืงืฆื™ืจ ื—ื•ืžืจ ืœื™ืžื•ื“), ื•ืื– ืฉืืœื• ืื•ืชื™ ืฉืืœื•ืช ืขืœื™ื•.");
} else {
appendBubble("assistant", "ืžืขื•ืœื”! ื™ืฉ ืœื™ ื”ืงืฉืจ ืœื™ืžื•ื“ื™. ืืคืฉืจ ืœืฉืื•ืœ ื›ืœ ืฉืืœื”.");
}
</script>
</body>
</html>`;
const INDEX = Buffer.from(HTML);
// ====== SERVER (static + /api/chat) ======
const server = http.createServer(async (req, res) => {
try {
// Simple router
if (req.method === "GET" && (req.url === "/" || req.url?.startsWith("/#"))) {
res.writeHead(200, { "Content-Type": "text/html; charset=utf-8" });
res.end(INDEX);
return;
}
if (req.method === "POST" && req.url === "/api/chat") {
if (!AI_API_KEY) {
const msg = "Missing AI_API_KEY env var";
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ error: msg }));
return;
}
const chunks = [];
for await (const c of req) chunks.push(c);
const bodyStr = Buffer.concat(chunks).toString("utf-8");
const payload = JSON.parse(bodyStr || "{}");
const model = payload.model || AI_MODEL;
const system = payload.system || "Answer briefly and clearly in Hebrew.";
const user = payload.message || "";
// OpenAI-compatible /chat/completions
const resp = await fetch(`${AI_BASE_URL}/chat/completions`, {
method: "POST",
headers: {
"Authorization": `Bearer ${AI_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
model,
messages: [
{ role: "system", content: system },
{ role: "user", content: user }
],
})
});
if (!resp.ok) {
const text = await resp.text();
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ error: `AI error ${resp.status}`, details: text }));
return;
}
const data = await resp.json();
const text = data?.choices?.[0]?.message?.content ?? "";
res.writeHead(200, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ ok: true, text }));
return;
}
// 404
res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
res.end("Not found");
} catch (e) {
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
res.end(JSON.stringify({ error: e?.message || "server error" }));
}
});
server.listen(PORT, () => {
console.log(`โœ… Study Assistant MVP running on http://localhost:${PORT}`);
if (!AI_API_KEY) {
console.warn("โš ๏ธ AI_API_KEY is missing. Set it before chatting.");
} else {
console.log("๐Ÿ” AI key loaded. Ready to chat.");
}
});