Manual changes saved
Browse files- index.html +541 -301
index.html
CHANGED
|
@@ -1,327 +1,567 @@
|
|
| 1 |
-
|
| 2 |
-
|
| 3 |
-
/
|
| 4 |
-
|
| 5 |
-
|
| 6 |
-
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
const
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 30 |
<html lang="he" dir="rtl">
|
| 31 |
<head>
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
| 54 |
-
|
| 55 |
-
|
| 56 |
-
|
| 57 |
-
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
@media (max-width:800px){.row{grid-template-columns:1fr}}
|
| 66 |
-
label{font-size:14px;color:var(--muted);display:block;margin-bottom:6px}
|
| 67 |
-
input,textarea,select{width:100%;padding:10px 12px;border:1px solid var(--border);border-radius:12px;background:#fff;outline:none}
|
| 68 |
-
textarea{min-height:120px;resize:vertical}
|
| 69 |
-
.muted{color:var(--muted);font-size:13px}
|
| 70 |
-
.chat{min-height:50vh;max-height:70vh;overflow:auto;display:flex;flex-direction:column;gap:10px}
|
| 71 |
-
.bubble{max-width:76%;border:1px solid var(--border);border-radius:14px;padding:10px 12px;background:#fff}
|
| 72 |
-
.me{margin-left:auto;background:rgba(14,165,233,.08);border-color:rgba(14,165,233,.25)}
|
| 73 |
-
.bot .who,.me .who{font-size:12px;color:var(--muted);margin-bottom:4px}
|
| 74 |
-
.toolbar{display:flex;gap:8px}
|
| 75 |
-
.badge{display:inline-flex;align-items:center;font-size:12px;color:var(--muted);border:1px solid var(--border);padding:2px 8px;border-radius:999px}
|
| 76 |
-
.row-compact{display:flex;gap:8px}
|
| 77 |
-
</style>
|
| 78 |
</head>
|
| 79 |
<body>
|
| 80 |
-
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 90 |
</div>
|
| 91 |
-
<
|
| 92 |
-
|
| 93 |
-
|
| 94 |
-
</nav>
|
| 95 |
-
</div>
|
| 96 |
-
</header>
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
<
|
| 102 |
-
<
|
| 103 |
-
|
| 104 |
-
<
|
| 105 |
-
<label>ืฉื โืืจืืโ (ืืืฉื: ืืชืืืืงื โ ืคืจืง ืืืงืืช)</label>
|
| 106 |
-
<input id="spaceName" placeholder="ืฉื ืืืจืื" />
|
| 107 |
-
<div style="height:8px"></div>
|
| 108 |
-
<label>ืฉืคืช ืืืื</label>
|
| 109 |
-
<select id="botLang">
|
| 110 |
-
<option value="he">ืขืืจืืช</option>
|
| 111 |
-
<option value="en">English</option>
|
| 112 |
-
</select>
|
| 113 |
-
<div style="height:8px"></div>
|
| 114 |
-
<span class="badge" id="stats">0 ืชืืืื ืืงืื ืืงืกื</span>
|
| 115 |
-
</div>
|
| 116 |
-
<div>
|
| 117 |
-
<label>ืืงืกื ืืงืฉืจ (ื ืฉืืจ ืืืคืืคื, ื ืฉืื ืืืื ืืชืืืืช ืืงืฉืจ)</label>
|
| 118 |
-
<textarea id="contextText" placeholder="ืืืืืงื ืืื ืืงืกื ืืฉืื ืืืืืืจ..."></textarea>
|
| 119 |
-
<div style="height:8px"></div>
|
| 120 |
-
<div class="row-compact">
|
| 121 |
-
<button class="btn" id="saveCtx">ืฉืืืจื</button>
|
| 122 |
-
<button class="btn-ghost" id="clearCtx">ื ืืงืื</button>
|
| 123 |
-
</div>
|
| 124 |
-
</div>
|
| 125 |
</div>
|
| 126 |
-
|
|
|
|
| 127 |
|
| 128 |
-
<
|
| 129 |
-
|
| 130 |
-
<
|
| 131 |
<div class="row">
|
| 132 |
-
<
|
| 133 |
-
|
| 134 |
-
<textarea id="systemHint" placeholder="ืืืฉื: ืขื ื ืืงืฆืจื, ืฆืืื ืฉืืืื, ืืฉืชืืฉ ืืืืืืืืช ืคืฉืืืืช."></textarea>
|
| 135 |
-
<div style="height:8px"></div>
|
| 136 |
-
<label>ืืืื (ืืืคืฆืืื ืื)</label>
|
| 137 |
-
<input id="model" placeholder="google/gemini-2.5-flash (ืืจืืจืช ืืืื ืืฉืจืช)" />
|
| 138 |
-
<div style="height:8px"></div>
|
| 139 |
-
<div class="muted">ื-API key ื ืฉืืจ ืขื ืืฉืจืช โ ืืืื. ืืื ืฆืืจื ืืฉืื ืืืชื ืืืคืืคื.</div>
|
| 140 |
-
</div>
|
| 141 |
-
<div>
|
| 142 |
-
<div id="chatBox" class="chat" style="border:1px solid var(--border);border-radius:12px;padding:10px;background:#fff"></div>
|
| 143 |
-
<div style="height:8px"></div>
|
| 144 |
-
<div class="row-compact">
|
| 145 |
-
<input id="msg" placeholder="ืฉืื/ื ืฉืืื ืขื ืกืื ืืืืืจ..." />
|
| 146 |
-
<button class="btn" id="send">ืฉืืืื</button>
|
| 147 |
-
</div>
|
| 148 |
-
</div>
|
| 149 |
</div>
|
| 150 |
-
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
|
| 154 |
-
const LS_SPACE = "mvp_space_name";
|
| 155 |
-
const LS_TEXT = "mvp_context_text";
|
| 156 |
-
const LS_LANG = "mvp_bot_lang";
|
| 157 |
-
const chatBox = document.getElementById("chatBox");
|
| 158 |
-
const msgInput = document.getElementById("msg");
|
| 159 |
-
const sendBtn = document.getElementById("send");
|
| 160 |
-
const stats = document.getElementById("stats");
|
| 161 |
-
const spaceName = document.getElementById("spaceName");
|
| 162 |
-
const contextEl = document.getElementById("contextText");
|
| 163 |
-
const saveCtx = document.getElementById("saveCtx");
|
| 164 |
-
const clearCtx = document.getElementById("clearCtx");
|
| 165 |
-
const botLang = document.getElementById("botLang");
|
| 166 |
-
const systemEl = document.getElementById("systemHint");
|
| 167 |
-
const modelEl = document.getElementById("model");
|
| 168 |
-
|
| 169 |
-
// Load persisted context
|
| 170 |
-
spaceName.value = localStorage.getItem(LS_SPACE) || "";
|
| 171 |
-
contextEl.value = localStorage.getItem(LS_TEXT) || "";
|
| 172 |
-
botLang.value = localStorage.getItem(LS_LANG) || "he";
|
| 173 |
-
stats.textContent = (contextEl.value.length || 0) + " ืชืืืื ืืงืื ืืงืกื";
|
| 174 |
-
|
| 175 |
-
contextEl.addEventListener("input", () => {
|
| 176 |
-
stats.textContent = (contextEl.value.length || 0) + " ืชืืืื ืืงืื ืืงืกื";
|
| 177 |
-
});
|
| 178 |
-
|
| 179 |
-
saveCtx.addEventListener("click", () => {
|
| 180 |
-
localStorage.setItem(LS_SPACE, spaceName.value || "");
|
| 181 |
-
localStorage.setItem(LS_TEXT, contextEl.value || "");
|
| 182 |
-
localStorage.setItem(LS_LANG, botLang.value || "he");
|
| 183 |
-
alert("ื ืฉืืจ ืืืฆืืื");
|
| 184 |
-
});
|
| 185 |
-
clearCtx.addEventListener("click", () => {
|
| 186 |
-
spaceName.value = "";
|
| 187 |
-
contextEl.value = "";
|
| 188 |
-
localStorage.removeItem(LS_SPACE);
|
| 189 |
-
localStorage.removeItem(LS_TEXT);
|
| 190 |
-
stats.textContent = "0 ืชืืืื ืืงืื ืืงืกื";
|
| 191 |
-
});
|
| 192 |
-
|
| 193 |
-
function appendBubble(role, text){
|
| 194 |
-
const wrap = document.createElement("div");
|
| 195 |
-
wrap.className = "bubble " + (role === "user" ? "me" : "bot");
|
| 196 |
-
const who = document.createElement("div");
|
| 197 |
-
who.className = "who";
|
| 198 |
-
who.textContent = role === "user" ? "ืืช/ื" : "ืืื";
|
| 199 |
-
const body = document.createElement("div");
|
| 200 |
-
body.textContent = text;
|
| 201 |
-
wrap.appendChild(who);
|
| 202 |
-
wrap.appendChild(body);
|
| 203 |
-
chatBox.appendChild(wrap);
|
| 204 |
-
chatBox.scrollTop = chatBox.scrollHeight;
|
| 205 |
-
}
|
| 206 |
|
| 207 |
-
|
| 208 |
-
const q = (msgInput.value || "").trim();
|
| 209 |
-
if(!q) return;
|
| 210 |
-
appendBubble("user", q);
|
| 211 |
-
msgInput.value = "";
|
| 212 |
-
const ctx = contextEl.value || "";
|
| 213 |
-
const space = spaceName.value || "ืืจืืจืช ืืืื";
|
| 214 |
-
const lang = botLang.value || "he";
|
| 215 |
-
const system = (systemEl.value || "") + "\\n" +
|
| 216 |
-
(lang === "he"
|
| 217 |
-
? \`ืขื ื ืืขืืจืืช, ืืงืฆืจื ืืืืืืจืืช. ืืฉืชืืฉ ืงืืื ืืื ืืชืืื ืืงืฉืจ ืกืืืจ ืืืื.\`
|
| 218 |
-
: \`Answer briefly and clearly. Prioritize the provided study context.\`) +
|
| 219 |
-
"\\n" +
|
| 220 |
-
\`ืฉื ืืืจืื: "\${space}".\\n\\nืืงืฉืจ/ืืืืจื ืืืืื:\\n\${ctx}\`;
|
| 221 |
-
|
| 222 |
-
try{
|
| 223 |
-
const resp = await fetch("/api/chat", {
|
| 224 |
-
method: "POST",
|
| 225 |
-
headers: {"Content-Type":"application/json"},
|
| 226 |
-
body: JSON.stringify({ message: q, system, model: (modelEl.value || undefined) })
|
| 227 |
-
});
|
| 228 |
-
const j = await resp.json();
|
| 229 |
-
if(!resp.ok || !j.ok){
|
| 230 |
-
appendBubble("assistant", (j.error || "ืฉืืืื ืืฉืืจืืช ืืืื ื"));
|
| 231 |
-
return;
|
| 232 |
-
}
|
| 233 |
-
appendBubble("assistant", j.text || (lang==="he"?"ืืื ืชืฉืืื":"No response"));
|
| 234 |
-
}catch(e){
|
| 235 |
-
appendBubble("assistant", (e?.message || "ืฉืืืืช ืจืฉืช"));
|
| 236 |
-
}
|
| 237 |
-
}
|
| 238 |
|
| 239 |
-
|
| 240 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 241 |
|
| 242 |
-
|
| 243 |
-
|
| 244 |
-
|
| 245 |
-
|
| 246 |
-
|
| 247 |
-
|
| 248 |
-
|
| 249 |
-
|
| 250 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 251 |
|
| 252 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
|
| 254 |
-
|
| 255 |
-
const server = http.createServer(async (req, res) => {
|
| 256 |
try {
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 270 |
-
|
|
|
|
| 271 |
|
| 272 |
-
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
const model = payload.model || AI_MODEL;
|
| 278 |
-
const system = payload.system || "Answer briefly and clearly in Hebrew.";
|
| 279 |
-
const user = payload.message || "";
|
| 280 |
-
|
| 281 |
-
// OpenAI-compatible /chat/completions
|
| 282 |
-
const resp = await fetch(`${AI_BASE_URL}/chat/completions`, {
|
| 283 |
-
method: "POST",
|
| 284 |
-
headers: {
|
| 285 |
-
"Authorization": `Bearer ${AI_API_KEY}`,
|
| 286 |
-
"Content-Type": "application/json",
|
| 287 |
-
},
|
| 288 |
-
body: JSON.stringify({
|
| 289 |
-
model,
|
| 290 |
-
messages: [
|
| 291 |
-
{ role: "system", content: system },
|
| 292 |
-
{ role: "user", content: user }
|
| 293 |
-
],
|
| 294 |
-
})
|
| 295 |
-
});
|
| 296 |
-
|
| 297 |
-
if (!resp.ok) {
|
| 298 |
-
const text = await resp.text();
|
| 299 |
-
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
| 300 |
-
res.end(JSON.stringify({ error: `AI error ${resp.status}`, details: text }));
|
| 301 |
-
return;
|
| 302 |
-
}
|
| 303 |
|
| 304 |
-
|
| 305 |
-
|
| 306 |
-
|
| 307 |
-
|
| 308 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 309 |
}
|
|
|
|
|
|
|
| 310 |
|
| 311 |
-
|
| 312 |
-
|
| 313 |
-
|
| 314 |
-
} catch (e) {
|
| 315 |
-
res.writeHead(500, { "Content-Type": "application/json; charset=utf-8" });
|
| 316 |
-
res.end(JSON.stringify({ error: e?.message || "server error" }));
|
| 317 |
}
|
| 318 |
});
|
| 319 |
|
| 320 |
-
|
| 321 |
-
|
| 322 |
-
|
| 323 |
-
|
| 324 |
-
|
| 325 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 326 |
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 327 |
});
|
|
|
|
| 1 |
+
npm i express multer openai pdf-parse mammoth cors dotenv uuid body-parser helmet express-rate-limit
|
| 2 |
+
node app.js
|
| 3 |
+
/**
|
| 4 |
+
* DeepStudy Pro โ One-File Ultra-MVP for DeepSite
|
| 5 |
+
* Author: ChatGPT (for Olga)
|
| 6 |
+
*
|
| 7 |
+
* Highlights:
|
| 8 |
+
* - Upload: PDF / DOCX / TXT
|
| 9 |
+
* - Robust text extraction and cleaning
|
| 10 |
+
* - Chunking + Map-Reduce summaries with "citations" (section refs)
|
| 11 |
+
* - Study Pack (Summary, Key Points, Glossary, Flashcards, Quiz + AnswerKey JSON)
|
| 12 |
+
* - Streaming Chat via SSE with typing indicator
|
| 13 |
+
* - Quiz grading with rubric (JSON in/JSON out)
|
| 14 |
+
* - Download Study Pack as .md
|
| 15 |
+
* - RTL Hebrew UI, persistent chat history (localStorage)
|
| 16 |
+
* - Safety first: school-only topics, gentle tone, avoid unsafe content
|
| 17 |
+
* - Security: helmet + rate limit; simple input sanitization
|
| 18 |
+
*
|
| 19 |
+
* ENV:
|
| 20 |
+
* OPENAI_API_KEY=sk-...
|
| 21 |
+
* (optional) OPENAI_MODEL=gpt-4o-mini (or another chat-capable model)
|
| 22 |
+
*
|
| 23 |
+
* RUN:
|
| 24 |
+
* node app.js (default port 3000)
|
| 25 |
+
* open http://localhost:3000
|
| 26 |
+
*/
|
| 27 |
+
|
| 28 |
+
require('dotenv').config();
|
| 29 |
+
const path = require('path');
|
| 30 |
+
const fs = require('fs');
|
| 31 |
+
const os = require('os');
|
| 32 |
+
const express = require('express');
|
| 33 |
+
const cors = require('cors');
|
| 34 |
+
const bodyParser = require('body-parser');
|
| 35 |
+
const multer = require('multer');
|
| 36 |
+
const pdfParse = require('pdf-parse');
|
| 37 |
+
const mammoth = require('mammoth');
|
| 38 |
+
const { v4: uuidv4 } = require('uuid');
|
| 39 |
+
const helmet = require('helmet');
|
| 40 |
+
const rateLimit = require('express-rate-limit');
|
| 41 |
+
|
| 42 |
+
const OpenAI = require('openai');
|
| 43 |
+
const openai = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
|
| 44 |
+
const MODEL = process.env.OPENAI_MODEL || 'gpt-4o-mini';
|
| 45 |
+
|
| 46 |
+
const app = express();
|
| 47 |
+
|
| 48 |
+
// ---- Security & basics ----
|
| 49 |
+
app.use(helmet({ contentSecurityPolicy: false }));
|
| 50 |
+
app.use(cors());
|
| 51 |
+
app.use(bodyParser.json({ limit: '12mb' }));
|
| 52 |
+
const limiter = rateLimit({ windowMs: 60_000, max: 120 }); // 120 req/min/IP
|
| 53 |
+
app.use(limiter);
|
| 54 |
+
|
| 55 |
+
// ---- Uploads temp ----
|
| 56 |
+
const upload = multer({
|
| 57 |
+
dest: path.join(os.tmpdir(), 'deepstudypro_uploads'),
|
| 58 |
+
limits: { fileSize: 30 * 1024 * 1024 } // 30MB
|
| 59 |
+
});
|
| 60 |
+
|
| 61 |
+
// ---- Utils ----
|
| 62 |
+
const sanitize = (s) => (s || '').toString().replace(/\u0000/g, '').trim();
|
| 63 |
+
const clamp = (n, min, max) => Math.max(min, Math.min(max, n));
|
| 64 |
+
|
| 65 |
+
function approxTokenLen(str) {
|
| 66 |
+
// ืืก: 1 ืืืงื ~ 3.5 ืชืืืื ืืืื ืืื; ืืขืืจืืช ืงืจืื ~ 2-3. ื ืื ืขื 3.
|
| 67 |
+
return Math.ceil((str || '').length / 3);
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function chunkText(str, targetTokens = 1200, overlapTokens = 120) {
|
| 71 |
+
// ืคืืฆืื ืงืฉืื ืืืืจื ืชืืืื (ืืขืจื), ืขื ืืคืืคื ืงืื ืืื ืืงืืขืื ืืฉืืืจื ืขื ืืงืฉืจ
|
| 72 |
+
const tokenSize = clamp(targetTokens, 400, 2000);
|
| 73 |
+
const overlap = clamp(overlapTokens, 0, Math.floor(tokenSize / 3));
|
| 74 |
+
|
| 75 |
+
const charsPerToken = 3; // ืืขืจืื
|
| 76 |
+
const chunkChars = tokenSize * charsPerToken;
|
| 77 |
+
const overlapChars = overlap * charsPerToken;
|
| 78 |
+
|
| 79 |
+
const text = sanitize(str);
|
| 80 |
+
const chunks = [];
|
| 81 |
+
let i = 0, idx = 0;
|
| 82 |
+
while (i < text.length) {
|
| 83 |
+
const end = Math.min(text.length, i + chunkChars);
|
| 84 |
+
const slice = text.slice(i, end);
|
| 85 |
+
chunks.push({ id: `S${idx + 1}`, text: slice });
|
| 86 |
+
idx++;
|
| 87 |
+
i = end - overlapChars;
|
| 88 |
+
if (i < 0) i = 0;
|
| 89 |
+
if (i >= text.length) break;
|
| 90 |
+
}
|
| 91 |
+
return chunks;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
async function extractText(filePath, originalName) {
|
| 95 |
+
const ext = (path.extname(originalName || '').toLowerCase() || '').replace('.', '');
|
| 96 |
+
if (ext === 'pdf') {
|
| 97 |
+
const data = await pdfParse(fs.readFileSync(filePath));
|
| 98 |
+
return sanitize(data.text);
|
| 99 |
+
} else if (ext === 'docx') {
|
| 100 |
+
const { value } = await mammoth.extractRawText({ path: filePath });
|
| 101 |
+
return sanitize(value);
|
| 102 |
+
} else if (ext === 'txt' || ext === '') {
|
| 103 |
+
return sanitize(fs.readFileSync(filePath, 'utf8'));
|
| 104 |
+
}
|
| 105 |
+
throw new Error(`ืกืื ืงืืืฅ ืื ื ืชืื ืขืืืื: ${ext} (ื ืชืื: PDF/DOCX/TXT)`);
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
async function chat(messages, system = 'You are a careful, school-safe Hebrew tutor for middle/high-school. Avoid unsafe content. Prefer Hebrew, keep answers clear & kind.') {
|
| 109 |
+
const resp = await openai.chat.completions.create({
|
| 110 |
+
model: MODEL,
|
| 111 |
+
temperature: 0.35,
|
| 112 |
+
messages: [{ role: 'system', content: system }, ...messages]
|
| 113 |
+
});
|
| 114 |
+
return resp.choices?.[0]?.message?.content?.trim() || '';
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
function safeParseJSON(s) {
|
| 118 |
+
try { return JSON.parse(s); } catch { return null; }
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
// ---- Study Pack Map-Reduce prompts ----
|
| 122 |
+
function mapPrompt(goal, sectionId, sectionText) {
|
| 123 |
+
return [
|
| 124 |
+
{ role: 'user', content:
|
| 125 |
+
`ืงืื: ืืงืืข ืืืืืื ืืกืืื "${sectionId}" ืืชืื ืืืืจ ืจืื.
|
| 126 |
+
ืืืจื ืคืืืืืืช: ${sanitize(goal) || 'ืืื ื ืืืืื ืงืฆืจ + ืืื ื ืืฉืืขืืชืืช.'}
|
| 127 |
+
|
| 128 |
+
ืืงืกื ืืืงืืข:
|
| 129 |
+
"""
|
| 130 |
+
${sectionText}
|
| 131 |
+
"""
|
| 132 |
+
|
| 133 |
+
ืืคืง ื ื ืชืงืฆืืจ ืืืืงื ืืืงืืข ืื ืืืื (5โ8 ืืฉืคืืื), ื ืงืืืืช ืืคืชื ืงืฆืจืืช, ืืืืฉืืื ืืจืืืืื.
|
| 134 |
+
ืคืืจืื (Markdown):
|
| 135 |
+
## ${sectionId}
|
| 136 |
+
### ืชืงืฆืืจ
|
| 137 |
+
- ...
|
| 138 |
+
### ื ืงืืืืช ืืคืชื
|
| 139 |
+
- ...
|
| 140 |
+
### ืืืฉืืื
|
| 141 |
+
- ืืืฉื | ืืกืืจ ืงืฆืจ
|
| 142 |
+
- ...
|
| 143 |
+
ืืืฉืื: ืขืืจืืช ืคืฉืืื, ืืื ืื"ื/ืชืืืื, ืืื ืชืืื ืื ืจืืื.`}
|
| 144 |
+
];
|
| 145 |
+
}
|
| 146 |
+
|
| 147 |
+
function reducePrompt(goal, mappedMarkdown, mergedText) {
|
| 148 |
+
return [
|
| 149 |
+
{ role: 'user', content:
|
| 150 |
+
`ืืฉ ืื ื ืกื ืกืืืืืื ืืืงืืขืื ืฉืื ืื (ืืืื). ืืืื ืืืชื ื"ืืืืืช ืืืืื" ืืืืื ืืขืืจืืช, ืขื ืฉืืืจื ืขื ืขืงืืืืช ืืืคื ืืืช ืืืงืืขืื (ืืฆืืืืืื).
|
| 151 |
+
ืืืจื: ${sanitize(goal) || 'ืืื ื + ืืื ื ืืืืื ืงืฆืจ'}
|
| 152 |
+
|
| 153 |
+
ืกืืืืื ืืืงืืขืื (Map):
|
| 154 |
+
"""
|
| 155 |
+
${mappedMarkdown}
|
| 156 |
+
"""
|
| 157 |
+
|
| 158 |
+
(ืืจืฉืืชื ืื ืืืงืกื ืืืืืื ืืืืืื ืืงืจืืืช ืืงืฉืจ, ืื ื ืืจืฉ):
|
| 159 |
+
"""
|
| 160 |
+
${mergedText.slice(0, 20000)}
|
| 161 |
+
"""
|
| 162 |
+
|
| 163 |
+
ืืคืง ืคืืจืื Markdown ืืืืืง:
|
| 164 |
+
|
| 165 |
+
# ืกืืืื (8โ12 ืืฉืคืืื) ืขื ืืืืืจื ืืงืืขืื (ืืืฉื [S2], [S5])
|
| 166 |
+
# ื ืงืืืืช ืืคืชื (ืืืืืื ืงืฆืจืื) ืืืื [Si] ืืืฉืจ ืจืืืื ืื
|
| 167 |
+
# ืืืืื ืืืฉืืื (ืืืื: ืืืฉื | ืืกืืจ ืงืฆืจ | ืืงืืขืื)
|
| 168 |
+
# ืืจืืืกืืืช (8โ14) โ ืื ืฉืืจื: ืฉืืื โ ืชืฉืืื ืงืฆืจื
|
| 169 |
+
# ืืืื ืืืืืื (10โ14 ืฉืืืืช ืืขืืจืืืช)
|
| 170 |
+
- ืืกืืฃ ืืืืจ ืืงืืข JSON ืชืงื ื ืขื ืืคืชื ืชืฉืืืืช ืืืื:
|
| 171 |
+
{"answerKey":[{"qid":"Q1","correct":"B","explanation":"..."}, ...]}
|
| 172 |
+
|
| 173 |
+
ืืืื ืืืืืืช: ืืื ืชืื ืื ืคืืืขื ืืื/ืืกืืื ืื. ืืชืืื ืืืื. ืฉืืืจื ืขื ืืืืจืืช.`}
|
| 174 |
+
];
|
| 175 |
+
}
|
| 176 |
+
|
| 177 |
+
async function gradeRubric(questions, userAnswers, answerKey) {
|
| 178 |
+
const payload = { questions, userAnswers, answerKey };
|
| 179 |
+
const messages = [
|
| 180 |
+
{ role: 'user', content:
|
| 181 |
+
`You are a deterministic grader for school practice quizzes.
|
| 182 |
+
Return STRICT JSON only:
|
| 183 |
+
{
|
| 184 |
+
"perQuestion":[{"qid":"Q1","correct":true,"feedback":"..."}, ...],
|
| 185 |
+
"score":{"correct":N,"total":T,"percent":P}
|
| 186 |
+
}
|
| 187 |
+
Be concise and supportive.`},
|
| 188 |
+
{ role: 'user', content: JSON.stringify(payload) }
|
| 189 |
+
];
|
| 190 |
+
const out = await chat(messages, 'You are a fair JSON-only grader. No chatter โ JSON only.');
|
| 191 |
+
const parsed = safeParseJSON(out);
|
| 192 |
+
if (parsed?.perQuestion && parsed?.score) return parsed;
|
| 193 |
+
|
| 194 |
+
// Fallback strict compare
|
| 195 |
+
const keyMap = {};
|
| 196 |
+
(answerKey?.answerKey || []).forEach(k => keyMap[String(k.qid)] = String(k.correct).trim().toLowerCase());
|
| 197 |
+
const per = (questions || []).map((q, i) => {
|
| 198 |
+
const qid = q.qid || `Q${i+1}`;
|
| 199 |
+
const ua = String(userAnswers?.[qid] ?? '').trim().toLowerCase();
|
| 200 |
+
const ca = keyMap[qid] || '';
|
| 201 |
+
const correct = !!ua && ua === ca;
|
| 202 |
+
return { qid, correct, feedback: correct ? 'ื ืืื โ ืืขืืื!' : `ืฉืืื. ืืชืฉืืื: ${answerKey?.answerKey?.find(x=>String(x.qid)===qid)?.correct ?? 'โ'}` };
|
| 203 |
+
});
|
| 204 |
+
const c = per.filter(p => p.correct).length;
|
| 205 |
+
return { perQuestion: per, score: { correct: c, total: per.length, percent: per.length ? Math.round(100*c/per.length) : 0 } };
|
| 206 |
+
}
|
| 207 |
+
|
| 208 |
+
// ------------- API -------------
|
| 209 |
+
|
| 210 |
+
app.get('/api/health', (_req, res) => res.json({ ok: true, model: MODEL }));
|
| 211 |
+
|
| 212 |
+
// Study Pack Pro: upload OR raw text, chunk โ map โ reduce
|
| 213 |
+
app.post('/api/study-pack', upload.array('files', 6), async (req, res) => {
|
| 214 |
+
const cleanup = () => (req.files || []).forEach(f => fs.existsSync(f.path) && fs.unlinkSync(f.path));
|
| 215 |
+
try {
|
| 216 |
+
const goal = sanitize(req.body.goal).slice(0, 600);
|
| 217 |
+
let merged = '';
|
| 218 |
+
|
| 219 |
+
if (req.files?.length) {
|
| 220 |
+
for (const f of req.files) {
|
| 221 |
+
const t = await extractText(f.path, f.originalname);
|
| 222 |
+
merged += `\n\n===== ${f.originalname} =====\n${t}\n`;
|
| 223 |
+
}
|
| 224 |
+
}
|
| 225 |
+
const rawText = sanitize(req.body.text || '');
|
| 226 |
+
if (rawText) merged += `\n\n===== Pasted Text =====\n${rawText}\n`;
|
| 227 |
+
|
| 228 |
+
if (!merged.trim()) return res.status(400).json({ ok: false, error: 'ืื ืืชืงืื ืืงืกื. ืืขืื ืงืืฆืื ืื ืืืืืงื ืืงืกื.' });
|
| 229 |
+
|
| 230 |
+
// Chunking
|
| 231 |
+
const chunks = chunkText(merged, 1200, 120);
|
| 232 |
+
// Map
|
| 233 |
+
const mappedParts = [];
|
| 234 |
+
for (const ch of chunks) {
|
| 235 |
+
const md = await chat(mapPrompt(goal, ch.id, ch.text));
|
| 236 |
+
mappedParts.push(md);
|
| 237 |
+
}
|
| 238 |
+
const mappedMarkdown = mappedParts.join('\n\n');
|
| 239 |
+
|
| 240 |
+
// Reduce
|
| 241 |
+
const reduced = await chat(reducePrompt(goal, mappedMarkdown, merged));
|
| 242 |
+
|
| 243 |
+
// Try extracting the last JSON block as answerKey
|
| 244 |
+
let answerKey = { answerKey: [] };
|
| 245 |
+
const matches = reduced.match(/\{[\s\S]*\}/g) || [];
|
| 246 |
+
for (let i = matches.length - 1; i >= 0; i--) {
|
| 247 |
+
const candidate = safeParseJSON(matches[i]);
|
| 248 |
+
if (candidate?.answerKey) { answerKey = candidate; break; }
|
| 249 |
+
}
|
| 250 |
+
|
| 251 |
+
res.json({ ok: true, studyPackMarkdown: reduced, mappedMarkdown, answerKey, sections: chunks.map(c => c.id) });
|
| 252 |
+
} catch (e) {
|
| 253 |
+
console.error(e);
|
| 254 |
+
res.status(500).json({ ok: false, error: 'ืฉืืืื ืืืฆืืจืช ืืืืืื', details: String(e.message || e) });
|
| 255 |
+
} finally { cleanup(); }
|
| 256 |
+
});
|
| 257 |
+
|
| 258 |
+
// Quiz grading
|
| 259 |
+
app.post('/api/grade', async (req, res) => {
|
| 260 |
+
try {
|
| 261 |
+
const { questions, userAnswers, answerKey } = req.body || {};
|
| 262 |
+
if (!questions || !userAnswers || !answerKey) {
|
| 263 |
+
return res.status(400).json({ ok: false, error: 'ืืกืจ ืฉืืืืช/ืชืฉืืืืช/ืืคืชื.' });
|
| 264 |
+
}
|
| 265 |
+
const result = await gradeRubric(questions, userAnswers, answerKey);
|
| 266 |
+
res.json({ ok: true, result });
|
| 267 |
+
} catch (e) {
|
| 268 |
+
console.error(e);
|
| 269 |
+
res.status(500).json({ ok: false, error: 'ืฉืืืื ืืืืืงื', details: String(e.message || e) });
|
| 270 |
+
}
|
| 271 |
+
});
|
| 272 |
+
|
| 273 |
+
// Streaming Chat via SSE
|
| 274 |
+
app.get('/api/chat/stream', async (req, res) => {
|
| 275 |
+
// Simple, non-chunk streaming (simulate typing via SSE) โ robust for DeepSite proxies.
|
| 276 |
+
res.set({
|
| 277 |
+
'Cache-Control': 'no-cache',
|
| 278 |
+
'Content-Type': 'text/event-stream',
|
| 279 |
+
Connection: 'keep-alive'
|
| 280 |
+
});
|
| 281 |
+
res.flushHeaders();
|
| 282 |
+
|
| 283 |
+
const history = safeParseJSON(req.query.history || '[]') || [];
|
| 284 |
+
const message = sanitize(req.query.message || '');
|
| 285 |
+
const msgs = [];
|
| 286 |
+
for (const m of history) {
|
| 287 |
+
const role = m.role === 'assistant' ? 'assistant' : 'user';
|
| 288 |
+
msgs.push({ role, content: String(m.content || '') });
|
| 289 |
+
}
|
| 290 |
+
msgs.push({ role: 'user', content: message });
|
| 291 |
+
|
| 292 |
+
try {
|
| 293 |
+
const reply = await chat(
|
| 294 |
+
msgs,
|
| 295 |
+
'You are a friendly Hebrew tutor. Keep it short unless asked for more, include one example, avoid unsafe topics.'
|
| 296 |
+
);
|
| 297 |
+
// simulate streaming: send in slices
|
| 298 |
+
const parts = reply.match(/.{1,60}/g) || [reply];
|
| 299 |
+
for (const p of parts) {
|
| 300 |
+
res.write(`data: ${JSON.stringify({ chunk: p })}\n\n`);
|
| 301 |
+
await new Promise(r => setTimeout(r, 20));
|
| 302 |
+
}
|
| 303 |
+
res.write(`data: ${JSON.stringify({ done: true })}\n\n`);
|
| 304 |
+
res.end();
|
| 305 |
+
} catch (e) {
|
| 306 |
+
res.write(`data: ${JSON.stringify({ error: 'ืฉืืืื ืืฆืณืื' })}\n\n`);
|
| 307 |
+
res.end();
|
| 308 |
+
}
|
| 309 |
+
});
|
| 310 |
+
|
| 311 |
+
// -------- Frontend (one HTML) --------
|
| 312 |
+
app.get('/', (_req, res) => {
|
| 313 |
+
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
| 314 |
+
res.end(`<!doctype html>
|
| 315 |
<html lang="he" dir="rtl">
|
| 316 |
<head>
|
| 317 |
+
<meta charset="utf-8" />
|
| 318 |
+
<meta name="viewport" content="width=device-width,initial-scale=1" />
|
| 319 |
+
<title>DeepStudy Pro โ deepsite GPT ืืชืืืืืื</title>
|
| 320 |
+
<style>
|
| 321 |
+
:root{--bg:#060a1a;--ink:#eef1ff;--muted:#b9c3ff;--card:#0b1130;--line:rgba(255,255,255,.12);--acc:#8eb1ff;--acc2:#7affe1}
|
| 322 |
+
*{box-sizing:border-box} body{margin:0;background:radial-gradient(1200px 600px at 60% -200px,#0e1849,transparent),linear-gradient(180deg,#060a1a 0%,#0b1027 100%);color:var(--ink);font-family:Inter,Segoe UI,system-ui,Arial}
|
| 323 |
+
.wrap{max-width:1100px;margin:40px auto;padding:0 16px}
|
| 324 |
+
h1{margin:0 0 8px;font-size:32px;font-weight:900;letter-spacing:.2px}
|
| 325 |
+
.sub{margin:0 0 22px;color:var(--muted)}
|
| 326 |
+
.grid{display:grid;grid-template-columns:1.25fr .75fr;gap:16px}
|
| 327 |
+
@media(max-width:950px){.grid{grid-template-columns:1fr}}
|
| 328 |
+
.card{background:rgba(11,17,48,.7);backdrop-filter:blur(8px);border:1px solid var(--line);border-radius:18px;padding:16px;box-shadow:0 12px 30px rgba(0,0,0,.3)}
|
| 329 |
+
h3{margin:0 0 10px}
|
| 330 |
+
.row{display:flex;gap:8px;align-items:center;margin:10px 0}
|
| 331 |
+
input[type="text"],textarea{width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(--line);background:#0a1030;color:var(--ink);resize:vertical}
|
| 332 |
+
textarea{min-height:110px}
|
| 333 |
+
input[type="file"]{border:1px dashed var(--line);padding:10px;border-radius:12px;width:100%;color:var(--muted)}
|
| 334 |
+
.btn{cursor:pointer;border:0;border-radius:12px;padding:11px 16px;font-weight:800;background:linear-gradient(90deg,var(--acc),var(--acc2));color:#03122a}
|
| 335 |
+
.btn.sec{background:transparent;color:var(--acc);border:1px solid var(--acc)}
|
| 336 |
+
.pill{display:inline-flex;align-items:center;gap:8px;background:#0a143b;padding:5px 10px;border:1px solid var(--line);border-radius:999px;font-size:12px;color:var(--muted)}
|
| 337 |
+
.hr{height:1px;background:linear-gradient(90deg,transparent,var(--line),transparent);margin:16px 0}
|
| 338 |
+
.out{white-space:pre-wrap;line-height:1.6}
|
| 339 |
+
.chat{height:420px;overflow:auto;background:#080f2b;border-radius:14px;padding:12px;border:1px solid var(--line)}
|
| 340 |
+
.bubble{padding:10px 12px;border-radius:12px;margin:8px 0;max-width:85%}
|
| 341 |
+
.me{background:#0f1c4e;margin-left:auto}
|
| 342 |
+
.bot{background:#0b1540;border:1px solid var(--line)}
|
| 343 |
+
.small{font-size:12px;color:var(--muted)}
|
| 344 |
+
.actions{display:flex;gap:8px;flex-wrap:wrap}
|
| 345 |
+
kbd{background:#09122e;border:1px solid var(--line);padding:2px 6px;border-radius:6px;font-size:12px}
|
| 346 |
+
table{border-collapse:collapse;width:100%}
|
| 347 |
+
td,th{border:1px solid var(--line);padding:6px}
|
| 348 |
+
.toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:#0b1438;border:1px solid var(--line);color:var(--ink);padding:10px 12px;border-radius:10px;display:none}
|
| 349 |
+
</style>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 350 |
</head>
|
| 351 |
<body>
|
| 352 |
+
<div class="wrap">
|
| 353 |
+
<h1>DeepStudy Pro โ ืขืืืจ ืืืืืื ืืฉืืืข ืืื</h1>
|
| 354 |
+
<p class="sub">ืืขืืื ืืืืจ โ ืืงืืืื ืืืืืช ืืืืื ืืคืืจืืช (ืกืืืื, ืืืฉืืื, ืืจืืืกืืืช, ืืืื + ืืคืชื) + ืฆืณืื ืืืจื ืืืจื. ื ืื ื ืืืืืื ืึพDeepSite.</p>
|
| 355 |
+
|
| 356 |
+
<div class="grid">
|
| 357 |
+
<div class="card">
|
| 358 |
+
<h3>1) ืืฆืืจืช ืืืืืช ืืืืื (Pro)</h3>
|
| 359 |
+
<div class="row"><input id="goal" type="text" placeholder="ืืืจื (ืืืฉื: 'ืืืืขืจื ืืืืื ืืืืืืจืคืื ืขื ืืงืืื')"></div>
|
| 360 |
+
<div class="row"><input id="files" type="file" multiple accept=".pdf,.docx,.txt"></div>
|
| 361 |
+
<div class="row"><textarea id="rawText" placeholder="ืื ืืืืืงื ืืงืกื ืืืงืื ืงืืฆืื"></textarea></div>
|
| 362 |
+
<div class="actions">
|
| 363 |
+
<button class="btn" id="buildBtn">ืฆืืจ ืืืืืช ืืืืื</button>
|
| 364 |
+
<button class="btn sec" id="downloadBtn" title="ืืืจืื ืืช ืืืืืื ืืืืจืื ื ื-Markdown">ืืืจืืช Markdown</button>
|
| 365 |
+
<span class="pill" id="health">ืืืื: โ</span>
|
| 366 |
</div>
|
| 367 |
+
<div class="hr"></div>
|
| 368 |
+
<div class="out" id="studyPackOut">โ</div>
|
| 369 |
+
<div class="hr"></div>
|
|
|
|
|
|
|
|
|
|
| 370 |
|
| 371 |
+
<h3>ืืืืงืช ืืืื</h3>
|
| 372 |
+
<p class="small">ืื ื ืืฆืจ ืืคืชื ืชืฉืืืืช JSON โ ืืืื ืชืฉืืืืช ืืงืืื ืฆืืื.</p>
|
| 373 |
+
<textarea id="questionsJSON" placeholder='ืืืืืื: [{"qid":"Q1","type":"mcq","question":"...","choices":["A","B","C","D"]}]'></textarea>
|
| 374 |
+
<textarea id="userAnswersJSON" placeholder='ืืืืืื: {"Q1":"B","Q2":"True"}'></textarea>
|
| 375 |
+
<div class="actions">
|
| 376 |
+
<button class="btn sec" id="gradeBtn">ืืืืงื ืืฆืืื</button>
|
| 377 |
+
<button class="btn sec" id="refineBtn" title="ืืงืฉื ืืจืืื/ืืืืื ืื ืืฉื ืกืคืฆืืคื">ืฉืืจืื ื ืืฉื</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 378 |
</div>
|
| 379 |
+
<div class="out" id="gradeOut"></div>
|
| 380 |
+
</div>
|
| 381 |
|
| 382 |
+
<div class="card">
|
| 383 |
+
<h3>2) ืฆืณืื ืืืจื (SSE)</h3>
|
| 384 |
+
<div class="chat" id="chat"></div>
|
| 385 |
<div class="row">
|
| 386 |
+
<input id="msg" type="text" placeholder="ืฉืืื ืืืืจื... (ืืืฉื: 'ืชืกืืืจื ืืช ืืืง ืืื ืขื ืืืืื')">
|
| 387 |
+
<button class="btn" id="send">ืฉืื</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 388 |
</div>
|
| 389 |
+
<p class="small">ืืืค: <kbd>โ</kbd> ืืืืืจ ืืืืขื ืืืจืื ื. ืืืืกืืืจืื ื ืฉืืจืช ืืืืืืืืช (localStorage).</p>
|
| 390 |
+
</div>
|
| 391 |
+
</div>
|
| 392 |
+
</div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 393 |
|
| 394 |
+
<div class="toast" id="toast"></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 395 |
|
| 396 |
+
<script>
|
| 397 |
+
const $ = (id) => document.getElementById(id);
|
| 398 |
+
const chatEl = $('chat'), msgEl = $('msg'), sendBtn = $('send');
|
| 399 |
+
const studyPackOut = $('studyPackOut'), healthEl = $('health');
|
| 400 |
+
const filesEl = $('files'), rawTextEl = $('rawText'), goalEl = $('goal');
|
| 401 |
+
const buildBtn = $('buildBtn'), downloadBtn = $('downloadBtn');
|
| 402 |
+
const questionsJSONEl = $('questionsJSON'), userAnswersJSONEl = $('userAnswersJSON');
|
| 403 |
+
const gradeBtn = $('gradeBtn'), gradeOut = $('gradeOut'), refineBtn = $('refineBtn');
|
| 404 |
+
const toastEl = $('toast');
|
| 405 |
|
| 406 |
+
let chatHistory = JSON.parse(localStorage.getItem('dsp_chat') || '[]');
|
| 407 |
+
let lastUserMsg = '';
|
| 408 |
+
let latestMarkdown = '';
|
| 409 |
+
let latestAnswerKey = { answerKey: [] };
|
| 410 |
+
|
| 411 |
+
function toast(txt) {
|
| 412 |
+
toastEl.textContent = txt;
|
| 413 |
+
toastEl.style.display = 'block';
|
| 414 |
+
setTimeout(()=> toastEl.style.display='none', 2200);
|
| 415 |
+
}
|
| 416 |
+
|
| 417 |
+
function addBubble(txt, who='bot'){
|
| 418 |
+
const b = document.createElement('div');
|
| 419 |
+
b.className = 'bubble ' + (who==='me'?'me':'bot');
|
| 420 |
+
b.textContent = txt;
|
| 421 |
+
chatEl.appendChild(b);
|
| 422 |
+
chatEl.scrollTop = chatEl.scrollHeight;
|
| 423 |
+
}
|
| 424 |
|
| 425 |
+
function renderHistory(){
|
| 426 |
+
chatEl.innerHTML = '';
|
| 427 |
+
chatHistory.forEach(m => addBubble(m.content, m.role==='user'?'me':'bot'));
|
| 428 |
+
}
|
| 429 |
+
renderHistory();
|
| 430 |
|
| 431 |
+
async function health() {
|
|
|
|
| 432 |
try {
|
| 433 |
+
const r = await fetch('/api/health');
|
| 434 |
+
const j = await r.json();
|
| 435 |
+
if (j.ok) healthEl.textContent = 'ืืืื: ' + (j.model || 'โ');
|
| 436 |
+
} catch {}
|
| 437 |
+
}
|
| 438 |
+
health();
|
| 439 |
|
| 440 |
+
sendBtn.onclick = async () => {
|
| 441 |
+
const m = msgEl.value.trim();
|
| 442 |
+
if (!m) return;
|
| 443 |
+
lastUserMsg = m;
|
| 444 |
+
addBubble(m, 'me');
|
| 445 |
+
msgEl.value = '';
|
| 446 |
+
chatHistory.push({ role:'user', content:m });
|
| 447 |
+
localStorage.setItem('dsp_chat', JSON.stringify(chatHistory));
|
| 448 |
|
| 449 |
+
const src = new EventSource('/api/chat/stream?'+new URLSearchParams({
|
| 450 |
+
message: m,
|
| 451 |
+
history: JSON.stringify(chatHistory.slice(-20)) // ืฉืืืจืื ืขื ืืงืฉืจ ืงืฆืจ
|
| 452 |
+
}));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 453 |
|
| 454 |
+
let acc = '';
|
| 455 |
+
const typing = document.createElement('div');
|
| 456 |
+
typing.className = 'bubble bot';
|
| 457 |
+
typing.textContent = 'ืืืืจื ืืงืืืืโฆ';
|
| 458 |
+
chatEl.appendChild(typing);
|
| 459 |
+
chatEl.scrollTop = chatEl.scrollHeight;
|
| 460 |
+
|
| 461 |
+
src.onmessage = (e) => {
|
| 462 |
+
const data = JSON.parse(e.data || '{}');
|
| 463 |
+
if (data.chunk) {
|
| 464 |
+
acc += data.chunk;
|
| 465 |
+
typing.textContent = acc;
|
| 466 |
+
}
|
| 467 |
+
if (data.done) {
|
| 468 |
+
chatHistory.push({ role:'assistant', content:acc });
|
| 469 |
+
localStorage.setItem('dsp_chat', JSON.stringify(chatHistory));
|
| 470 |
+
src.close();
|
| 471 |
+
}
|
| 472 |
+
if (data.error) {
|
| 473 |
+
typing.textContent = 'ืฉืืืื ืืฆืณืื.';
|
| 474 |
+
src.close();
|
| 475 |
}
|
| 476 |
+
};
|
| 477 |
+
};
|
| 478 |
|
| 479 |
+
document.addEventListener('keydown', (ev)=>{
|
| 480 |
+
if (ev.key === 'ArrowUp' && document.activeElement === msgEl && !msgEl.value) {
|
| 481 |
+
msgEl.value = lastUserMsg;
|
|
|
|
|
|
|
|
|
|
| 482 |
}
|
| 483 |
});
|
| 484 |
|
| 485 |
+
buildBtn.onclick = async () => {
|
| 486 |
+
studyPackOut.textContent = 'โณ ืืขืื ืงืืฆืื, ืืคืฆื ืืืงืืขืื, ืืืจืืฅ ืกืืืืืื ืืืืื...';
|
| 487 |
+
const fd = new FormData();
|
| 488 |
+
if (goalEl.value.trim()) fd.append('goal', goalEl.value.trim());
|
| 489 |
+
if (rawTextEl.value.trim()) fd.append('text', rawTextEl.value.trim());
|
| 490 |
+
for (const f of filesEl.files) fd.append('files', f);
|
| 491 |
+
|
| 492 |
+
const r = await fetch('/api/study-pack', { method:'POST', body: fd });
|
| 493 |
+
const j = await r.json();
|
| 494 |
+
if (!j.ok) {
|
| 495 |
+
studyPackOut.textContent = 'โ ' + (j.error || 'ืฉืืืื');
|
| 496 |
+
if (j.details) studyPackOut.textContent += '\\n' + j.details;
|
| 497 |
+
return;
|
| 498 |
}
|
| 499 |
+
|
| 500 |
+
latestMarkdown = j.studyPackMarkdown || '';
|
| 501 |
+
latestAnswerKey = j.answerKey || { answerKey: [] };
|
| 502 |
+
studyPackOut.textContent = latestMarkdown;
|
| 503 |
+
|
| 504 |
+
// ืืืืืืืช JSON ืืฉืืืช ืืืืงื
|
| 505 |
+
if (!questionsJSONEl.value.trim()) {
|
| 506 |
+
questionsJSONEl.value = JSON.stringify([
|
| 507 |
+
{"qid":"Q1","type":"mcq","question":"ืืืืื: ืืื ืื ืจืืื ืงืื ืืืช?","choices":["A","B","C","D"]},
|
| 508 |
+
{"qid":"Q2","type":"tf","question":"ืืืืื: ืืืฅ ืึดืึผึธื ืกืงืืจืืช. (True/False)"},
|
| 509 |
+
{"qid":"Q3","type":"short","question":"ืืืืื: ืืชืื ืืช ืืืืง ืืจืืฉืื ืฉื ื ืืืืื ืืืฉืคื ืืื."}
|
| 510 |
+
], null, 2);
|
| 511 |
+
}
|
| 512 |
+
if (!userAnswersJSONEl.value.trim()) {
|
| 513 |
+
userAnswersJSONEl.value = JSON.stringify({"Q1":"B","Q2":"True","Q3":"ืืืฃ ืฉืืืจ ืขื ืืฆืื..."}, null, 2);
|
| 514 |
+
}
|
| 515 |
+
toast('ืืืืืื ืืืื ื โ');
|
| 516 |
+
};
|
| 517 |
+
|
| 518 |
+
downloadBtn.onclick = () => {
|
| 519 |
+
if (!latestMarkdown.trim()) return toast('ืืื ืชืืื ืืืืจืื ืขืืืื');
|
| 520 |
+
const blob = new Blob([latestMarkdown], { type:'text/markdown;charset=utf-8' });
|
| 521 |
+
const a = document.createElement('a');
|
| 522 |
+
a.href = URL.createObjectURL(blob);
|
| 523 |
+
a.download = 'deepstudy_pack.md';
|
| 524 |
+
a.click();
|
| 525 |
+
URL.revokeObjectURL(a.href);
|
| 526 |
+
};
|
| 527 |
+
|
| 528 |
+
gradeBtn.onclick = async () => {
|
| 529 |
+
gradeOut.textContent = 'ืืืืง...';
|
| 530 |
+
let questions=null, userAnswers=null;
|
| 531 |
+
try { questions = JSON.parse(questionsJSONEl.value); } catch { return gradeOut.textContent='JSON ืื ืชืงืื ืืฉืื ืฉืืืืช'; }
|
| 532 |
+
try { userAnswers = JSON.parse(userAnswersJSONEl.value); } catch { return gradeOut.textContent='JSON ืื ืชืงืื ืืฉืื ืชืฉืืืืช'; }
|
| 533 |
+
|
| 534 |
+
const r = await fetch('/api/grade', {
|
| 535 |
+
method:'POST', headers:{'Content-Type':'application/json'},
|
| 536 |
+
body: JSON.stringify({ questions, userAnswers, answerKey: latestAnswerKey })
|
| 537 |
+
});
|
| 538 |
+
const j = await r.json();
|
| 539 |
+
if (!j.ok) return gradeOut.textContent = 'ืฉืืืื ืืืืืงื.';
|
| 540 |
+
const g = j.result;
|
| 541 |
+
const lines = [];
|
| 542 |
+
lines.push('ืชืืฆืื: ' + g.score.correct + '/' + g.score.total + ' (' + g.score.percent + '%)');
|
| 543 |
+
lines.push('');
|
| 544 |
+
(g.perQuestion||[]).forEach(p => lines.push(`${p.qid}: ${p.correct?'โ ื ืืื':'โ ืฉืืื'} โ ${p.feedback||''}`));
|
| 545 |
+
gradeOut.textContent = lines.join('\\n');
|
| 546 |
+
};
|
| 547 |
+
|
| 548 |
+
refineBtn.onclick = async () => {
|
| 549 |
+
const sel = prompt('ืืืื ื ืืฉื ืืฉืืจื/ืืืจืืื (ืืืฉื: "ืืืง ืฉื ื ืฉื ื ืืืืื")?');
|
| 550 |
+
if (!sel) return;
|
| 551 |
+
const ask = 'ืฉืืจื/ื ืืืจืืืื ืืืืืื ืืช ืื ืืฉื: ' + sel + '\\nืืืื: ืืกืืจ ืืืืจื + ืืืืื ืืกืคืจืืช + ืชืจืืื ืงืฆืจ ืขื ืคืชืจืื.';
|
| 552 |
+
msgEl.value = ask;
|
| 553 |
+
sendBtn.click();
|
| 554 |
+
};
|
| 555 |
+
</script>
|
| 556 |
+
</body>
|
| 557 |
+
</html>`);
|
| 558 |
+
});
|
| 559 |
+
|
| 560 |
+
// ---- Start ----
|
| 561 |
+
const PORT = process.env.PORT || 3000;
|
| 562 |
+
if (!process.env.OPENAI_API_KEY) {
|
| 563 |
+
console.warn('โ ๏ธ ืืกืจ OPENAI_API_KEY โ ืืืืืจื ืืฉืชื ื ืกืืืื ืืคื ื ืืืจืฆื.');
|
| 564 |
+
}
|
| 565 |
+
app.listen(PORT, () => {
|
| 566 |
+
console.log('DeepStudy Pro up on http://localhost:'+PORT);
|
| 567 |
});
|