|
|
<!DOCTYPE html> |
|
|
<html lang="he" dir="rtl"> |
|
|
<head> |
|
|
<meta charset="UTF-8"> |
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0"> |
|
|
<title>StudyMate - ืขืืืจ ืืืืืื ืืื</title> |
|
|
<link rel="stylesheet" href="style.css"> |
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"> |
|
|
</head> |
|
|
<body> |
|
|
<custom-navbar></custom-navbar> |
|
|
|
|
|
<div class="backend-status"> |
|
|
<span id="apiStatus">API Status: Checking...</span> |
|
|
</div> |
|
|
|
|
|
<main class="container"> |
|
|
<section class="hero"> |
|
|
<h1>StudyMate - ืืคืืืช ืืืืืื ืืืืื ืืืชืจ</h1> |
|
|
<p>ืืขืื ืืืืจื ืืืืื ืืงืื ืกืืืืืื, ืืืื ืื ืืืจืืืกืืืช ืืืืื - ืืื ืืืืืืื!</p> |
|
|
</section> |
|
|
<div class="study-tools"> |
|
|
<div class="tool-card"> |
|
|
<h2><i class="fas fa-file-upload"></i> ืืฆืืจืช ืคืจืืืงื ืืืฉ</h2> |
|
|
<div class="project-type-selector"> |
|
|
<button class="project-type active" data-type="exam"> |
|
|
<i class="fas fa-file-alt"></i> ืืืื |
|
|
</button> |
|
|
<button class="project-type" data-type="assignment"> |
|
|
<i class="fas fa-tasks"></i> ืขืืืื |
|
|
</button> |
|
|
<button class="project-type" data-type="presentation"> |
|
|
<i class="fas fa-chalkboard-teacher"></i> ืืฆืืช |
|
|
</button> |
|
|
</div> |
|
|
<input type="text" id="projectName" placeholder="ืฉื ืืคืจืืืงื (ืืืฉื: ืืืื ืืชื ืดื)"> |
|
|
<div class="upload-area" id="dropZone"> |
|
|
<input type="file" id="fileInput" multiple accept=".pdf,.docx,.txt,.ppt,.pptx"> |
|
|
<label for="fileInput" class="upload-btn"> |
|
|
<i class="fas fa-cloud-upload-alt"></i> ืืจืืจ ืงืืฆืื ืื ืืืฅ ืืืืืจื |
|
|
</label> |
|
|
<div class="file-list" id="fileList"></div> |
|
|
</div> |
|
|
<div class="output-options"> |
|
|
<h3>ืื ืชืจืฆื ืืงืื?</h3> |
|
|
<div class="options-grid"> |
|
|
<label class="option-checkbox"> |
|
|
<input type="checkbox" name="output" value="summary" checked> |
|
|
<span>ืกืืืืืื</span> |
|
|
</label> |
|
|
<label class="option-checkbox"> |
|
|
<input type="checkbox" name="output" value="flashcards" checked> |
|
|
<span>ืืจืืืกืืืช</span> |
|
|
</label> |
|
|
<label class="option-checkbox"> |
|
|
<input type="checkbox" name="output" value="quiz"> |
|
|
<span>ืืืื ืชืจืืื</span> |
|
|
</label> |
|
|
<label class="option-checkbox"> |
|
|
<input type="checkbox" name="output" value="presentation"> |
|
|
<span>ืืฆืืช</span> |
|
|
</label> |
|
|
</div> |
|
|
</div> |
|
|
<button class="btn primary" id="processBtn"> |
|
|
<i class="fas fa-magic"></i> ืฆืืจ ืคืจืืืงื |
|
|
</button> |
|
|
</div> |
|
|
<div class="tool-card"> |
|
|
<h2><i class="fas fa-book"></i> ืืืืจื ืืืืืื ืฉืื</h2> |
|
|
<div class="tabs"> |
|
|
<button class="tab-btn active" data-tab="notes">ืกืืืืืื</button> |
|
|
<button class="tab-btn" data-tab="flashcards">ืืจืืืกืืืช</button> |
|
|
<button class="tab-btn" data-tab="exams">ืืืื ืื</button> |
|
|
</div> |
|
|
<div class="tab-content active" id="notes"> |
|
|
<div class="empty-state"> |
|
|
<i class="fas fa-book-open"></i> |
|
|
<p>ืขืืืื ืืื ืกืืืืืื. ืืขืื ืงืืฆืื ืืื ืืืชืืื!</p> |
|
|
</div> |
|
|
</div> |
|
|
<div class="tab-content" id="flashcards"> |
|
|
<div class="empty-state"> |
|
|
<i class="fas fa-layer-group"></i> |
|
|
<p>ืขืืืื ืืื ืืจืืืกืืืช. ืืขืื ืงืืฆืื ืืื ืืืชืืื!</p> |
|
|
</div> |
|
|
</div> |
|
|
<div class="tab-content" id="exams"> |
|
|
<div class="empty-state"> |
|
|
<i class="fas fa-file-alt"></i> |
|
|
<p>ืขืืืื ืืื ืืืื ืื. ืืขืื ืงืืฆืื ืืื ืืืชืืื!</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</main> |
|
|
|
|
|
<custom-footer></custom-footer> |
|
|
<script src="components/navbar.js"></script> |
|
|
<script src="components/footer.js"></script> |
|
|
<script src="components/floating-button.js"></script> |
|
|
<script src="script.js" type="module"></script> |
|
|
<floating-button></floating-button> |
|
|
</body> |
|
|
</html> |
|
|
/* DeepStudy Pro MAX โ Luxe UI |
|
|
* Same powerhouse features, premium UI polish for DeepSite. |
|
|
* Env: OPENAI_API_KEY, OPENAI_MODEL, JWT_SECRET, SMTP_*, APP_BASE_URL |
|
|
*/ |
|
|
require('dotenv').config(); |
|
|
const path=require('path'),fs=require('fs'),os=require('os'); |
|
|
const express=require('express'),cors=require('cors'),bodyParser=require('body-parser'); |
|
|
const multer=require('multer'),pdfParse=require('pdf-parse'),mammoth=require('mammoth'); |
|
|
const {v4:uuidv4}=require('uuid'); const helmet=require('helmet'); const rateLimit=require('express-rate-limit'); |
|
|
const cookieParser=require('cookie-parser'); const nodemailer=require('nodemailer'); |
|
|
const bcrypt=require('bcryptjs'); const jwt=require('jsonwebtoken'); const OpenAI=require('openai'); |
|
|
|
|
|
const app=express(); |
|
|
app.use(helmet({contentSecurityPolicy:false})); |
|
|
app.use(cors({origin:true,credentials:true})); |
|
|
app.use(cookieParser()); |
|
|
app.use(bodyParser.json({limit:'16mb'})); |
|
|
app.use(rateLimit({windowMs:60_000,max:180})); |
|
|
|
|
|
const PORT=process.env.PORT||3000; |
|
|
const APP_BASE_URL=process.env.APP_BASE_URL||`http://localhost:${PORT}`; |
|
|
const JWT_SECRET=process.env.JWT_SECRET||'dev_change_me'; |
|
|
|
|
|
const DATA_DIR=path.join(process.cwd(),'data'); |
|
|
const USERS_PATH=path.join(DATA_DIR,'users.json'); |
|
|
const PACKS_PATH=path.join(DATA_DIR,'packs.json'); |
|
|
if(!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR); |
|
|
if(!fs.existsSync(USERS_PATH)) fs.writeFileSync(USERS_PATH,JSON.stringify([])); |
|
|
if(!fs.existsSync(PACKS_PATH)) fs.writeFileSync(PACKS_PATH,JSON.stringify([])); |
|
|
const readJSON=p=>{try{return JSON.parse(fs.readFileSync(p,'utf8'));}catch{return[];}}; |
|
|
const writeJSON=(p,v)=>fs.writeFileSync(p,JSON.stringify(v,null,2)); |
|
|
const readUsers=()=>readJSON(USERS_PATH), writeUsers=a=>writeJSON(USERS_PATH,a); |
|
|
const readPacks=()=>readJSON(PACKS_PATH), writePacks=a=>writeJSON(PACKS_PATH,a); |
|
|
|
|
|
let transporter=null; |
|
|
if(process.env.SMTP_HOST){ |
|
|
transporter=nodemailer.createTransport({host:process.env.SMTP_HOST,port:Number(process.env.SMTP_PORT||587),secure:false,auth:{user:process.env.SMTP_USER,pass:process.env.SMTP_PASS}}); |
|
|
} |
|
|
const openaiConfigured=!!process.env.OPENAI_API_KEY; |
|
|
const openaiClient=openaiConfigured? new OpenAI.OpenAI({apiKey:process.env.OPENAI_API_KEY}):null; |
|
|
const MODEL=process.env.OPENAI_MODEL||'gpt-4o-mini'; |
|
|
|
|
|
const sanitize=s=>(s||'').toString().replace(/\u0000/g,'').trim(); |
|
|
const safeParseJSON=s=>{try{return JSON.parse(s);}catch{return null;}}; |
|
|
const signToken=(payload,exp='7d')=>jwt.sign(payload,JWT_SECRET,{expiresIn:exp}); |
|
|
const verifyToken=t=>{try{return jwt.verify(t,JWT_SECRET);}catch{return null;}}; |
|
|
function authRequired(req,res,next){const t=req.cookies?.dsp_auth; if(!t) return res.status(401).json({ok:false,error:'ื ืืจืฉืช ืืชืืืจืืช'}); const dec=verifyToken(t); if(!dec) return res.status(401).json({ok:false,error:'ืืกืืืื ืื ืชืงืฃ'}); req.user=dec; next();} |
|
|
const upload=multer({dest:path.join(os.tmpdir(),'deepstudy_uploads'),limits:{fileSize:32*1024*1024}}); |
|
|
|
|
|
async function extractText(filePath,originalName){ |
|
|
const ext=(path.extname(originalName||'').toLowerCase()||'').slice(1); |
|
|
if(ext==='pdf'){const d=await pdfParse(fs.readFileSync(filePath)); return sanitize(d.text);} |
|
|
if(ext==='docx'){const {value}=await mammoth.extractRawText({path:filePath}); return sanitize(value);} |
|
|
if(ext==='txt'||ext===''){return sanitize(fs.readFileSync(filePath,'utf8'));} |
|
|
throw new Error(`ืคืืจืื ืื ื ืชืื: ${ext} (pdf/docx/txt)`); |
|
|
} |
|
|
function chunkText(str,targetTokens=1200,overlapTokens=120){ |
|
|
const txt=sanitize(str); const charsPerTok=3; const chunkChars=targetTokens*charsPerTok; const overlapChars=overlapTokens*charsPerTok; |
|
|
const out=[]; let i=0,idx=1; while(i<txt.length){const end=Math.min(txt.length,i+chunkChars); out.push({id:`S${idx}`,text:txt.slice(i,end)}); idx++; i=end-overlapChars; if(i<0) i=0; if(i>=txt.length) break;} |
|
|
return out; |
|
|
} |
|
|
async function llm(messages,system){ |
|
|
if(!openaiConfigured){const last=messages[messages.length-1]?.content||''; return `๐ ืืฆื ืืื:\n${last.slice(0,600)}\n\n[ืืืืื]`;} |
|
|
const r=await openaiClient.chat.completions.create({model:MODEL,temperature:0.35,messages:[{role:'system',content:system||'You are a kind Hebrew school tutor.'},...messages]}); |
|
|
return r.choices?.[0]?.message?.content?.trim()||''; |
|
|
} |
|
|
const mapPrompt=(goal,sid,text)=>[{role:'user',content: |
|
|
`ืืงืืข "${sid}". ืืืจื: ${sanitize(goal)||'ืืื ื + ืืื ื ืืืืื'}. |
|
|
""" |
|
|
${text} |
|
|
""" |
|
|
ืฆืืจ ืชืงืฆืืจ (5โ8 ืืฉืคืืื), ื ืงืืืืช ืืคืชื, ืืืืฉืืื. Markdown: |
|
|
## ${sid} |
|
|
### ืชืงืฆืืจ |
|
|
- ... |
|
|
### ื ืงืืืืช ืืคืชื |
|
|
- ... |
|
|
### ืืืฉืืื |
|
|
- ืืืฉื | ืืกืืจ ืงืฆืจ`}]; |
|
|
function reducePrompt(goal,mapped,merged){return[{role:'user',content: |
|
|
`ืืื ืืช ืกืืืืื ืืืงืืขืื ื"ืืืืืช ืืืืื" ืืขืืจืืช ืขื [Si]. ืืืจื: ${sanitize(goal)||'ืืื ื + ืชืจืืื'}. |
|
|
ืกืืืืืื: |
|
|
""" |
|
|
${mapped} |
|
|
""" |
|
|
(ืืืงืฉืจ ืืืื): |
|
|
""" |
|
|
${merged.slice(0,20000)} |
|
|
""" |
|
|
ืืคืง Markdown: |
|
|
# ืกืืืื (8โ12 ืืฉืคืืื, ืขื [Si]) |
|
|
# ื ืงืืืืช ืืคืชื (ืืืืืื + [Si]) |
|
|
# ืืืืื ืืืฉืืื (ืืืื: ืืืฉื | ืืกืืจ ืงืฆืจ | ืืงืืขืื) |
|
|
# ืืจืืืกืืืช (8โ14): ืฉืืื โ ืชืฉืืื ืงืฆืจื |
|
|
# ืืืื ืืืืืื (10โ14) |
|
|
ืืกืืฃ ืืืกืฃ JSON ืชืงืื: |
|
|
{"answerKey":[{"qid":"Q1","correct":"B","explanation":"..."}, ...]}`}];} |
|
|
async function buildStudyPack(goal,merged){ |
|
|
const chunks=chunkText(merged,1200,120); |
|
|
if(!chunks.length){return{studyPackMarkdown:'# ืกืืืื\nืืื ืงืฆืจ.',answerKey:{answerKey:[]},sections:[]};} |
|
|
const mapped=[]; for(const ch of chunks) mapped.push(await llm(mapPrompt(goal,ch.id,ch.text))); |
|
|
const mappedMarkdown=mapped.join('\n\n'); const reduced=await llm(reducePrompt(goal,mappedMarkdown,merged)); |
|
|
let answerKey={answerKey:[]}; const m=reduced.match(/\{[\s\S]*\}/g)||[]; |
|
|
for(let i=m.length-1;i>=0;i--){const c=safeParseJSON(m[i]); if(c?.answerKey){answerKey=c;break;}} |
|
|
return{studyPackMarkdown:reduced,answerKey,sections:chunks.map(c=>c.id)}; |
|
|
} |
|
|
|
|
|
// ---------- AUTH ---------- |
|
|
app.post('/api/auth/signup',async(req,res)=>{ |
|
|
try{ |
|
|
const email=sanitize(req.body.email).toLowerCase(), pw=sanitize(req.body.password); |
|
|
if(!email||!pw||pw.length<6) return res.status(400).json({ok:false,error:'ืืืืืื/ืกืืกืื ืื ืชืงืื ืื'}); |
|
|
const users=readUsers(); if(users.find(u=>u.email===email)) return res.status(409).json({ok:false,error:'ืืืืืื ืงืืื'}); |
|
|
const hash=await bcrypt.hash(pw,10); const user={id:uuidv4(),email,passwordHash:hash,createdAt:Date.now(),emailVerified:false}; |
|
|
users.push(user); writeUsers(users); |
|
|
if(transporter){ |
|
|
const t=signToken({action:'verify',uid:user.id},'2d'); const link=`${APP_BASE_URL}/verify?token=${t}`; |
|
|
await transporter.sendMail({from:process.env.SMTP_FROM||'DeepStudy Pro <[email protected]>',to:email,subject:'ืืจืืื ืืืื! ืืฉืจื ืืช ืืืืื ๐', |
|
|
html:`<div style="font-family:system-ui"><h2>ืืจืืื ืืืื</h2><p>ืืืืืืช ืืืฉืืื:</p><p><a href="${link}">ืืืฆื ืืื</a></p></div>`}); |
|
|
} |
|
|
const token=signToken({uid:user.id,email:user.email}); res.cookie('dsp_auth',token,{httpOnly:true,sameSite:'lax',maxAge:7*24*3600*1000}); |
|
|
res.json({ok:true,user:{id:user.id,email:user.email,emailVerified:user.emailVerified},info:transporter?'ื ืฉืื ืืืื ืืืืืช':'ืืื: ืืื SMTP'}); |
|
|
}catch(e){console.error(e);res.status(500).json({ok:false,error:'ืฉืืืื ืืืจืฉืื'});} |
|
|
}); |
|
|
app.post('/api/auth/login',async(req,res)=>{ |
|
|
try{ |
|
|
const email=sanitize(req.body.email).toLowerCase(), pw=sanitize(req.body.password); |
|
|
const users=readUsers(); const u=users.find(x=>x.email===email); if(!u) return res.status(401).json({ok:false,error:'ืฉืืื'}); |
|
|
const ok=await bcrypt.compare(pw,u.passwordHash); if(!ok) return res.status(401).json({ok:false,error:'ืฉืืื'}); |
|
|
const token=signToken({uid:u.id,email:u.email}); res.cookie('dsp_auth',token,{httpOnly:true,sameSite:'lax',maxAge:7*24*3600*1000}); |
|
|
res.json({ok:true,user:{id:u.id,email:u.email,emailVerified:u.emailVerified}}); |
|
|
}catch(e){console.error(e);res.status(500).json({ok:false,error:'ืฉืืืื ืืืชืืืจืืช'});} |
|
|
}); |
|
|
app.post('/api/auth/logout',(req,res)=>{res.clearCookie('dsp_auth');res.json({ok:true});}); |
|
|
app.get('/api/auth/me',(req,res)=>{const t=req.cookies?.dsp_auth; const dec=t&&verifyToken(t); if(!dec) return res.json({ok:false}); |
|
|
const u=readUsers().find(x=>x.id===dec.uid); if(!u) return res.json({ok:false}); |
|
|
res.json({ok:true,user:{id:u.id,email:u.email,emailVerified:u.emailVerified}}); |
|
|
}); |
|
|
app.get('/api/auth/verify',(req,res)=>{const t=sanitize(req.query.token); const d=verifyToken(t); if(!d||d.action!=='verify') return res.status(400).json({ok:false,error:'ืืืงื ืื ืชืงืฃ'}); |
|
|
const users=readUsers(); const u=users.find(x=>x.id===d.uid); if(!u) return res.status(400).json({ok:false,error:'ืื ืงืืื'}); if(!u.emailVerified){u.emailVerified=true; writeUsers(users);} |
|
|
res.json({ok:true,verified:true}); |
|
|
}); |
|
|
app.post('/api/auth/request-reset',async(req,res)=>{const email=sanitize(req.body.email).toLowerCase(); const u=readUsers().find(x=>x.email===email); |
|
|
if(u&&transporter){const t=signToken({action:'reset',uid:u.id},'2h'); const link=`${APP_BASE_URL}/reset?token=${t}`; |
|
|
await transporter.sendMail({from:process.env.SMTP_FROM||'DeepStudy Pro <[email protected]>',to:email,subject:'ืืืคืืก ืกืืกืื',html:`<div style="font-family:system-ui"><h2>ืืืคืืก</h2><a href="${link}">ืงืืฉืืจ ืืืืคืืก</a></div>`});} |
|
|
res.json({ok:true}); |
|
|
}); |
|
|
app.post('/api/auth/reset',async(req,res)=>{const {token,password}=req.body||{}; const d=verifyToken(sanitize(token)); |
|
|
if(!d||d.action!=='reset') return res.status(400).json({ok:false,error:'ืืืงื ืื ืชืงืฃ'}); |
|
|
if(!password||String(password).length<6) return res.status(400).json({ok:false,error:'ืกืืกืื ืงืฆืจื'}); |
|
|
const users=readUsers(); const u=users.find(x=>x.id===d.uid); if(!u) return res.status(400).json({ok:false,error:'ืื ื ืืฆื'}); u.passwordHash=await bcrypt.hash(String(password),10); writeUsers(users); res.json({ok:true}); |
|
|
}); |
|
|
|
|
|
// ---------- CORE ---------- |
|
|
app.get('/api/health',(_req,res)=>res.json({ok:true,model:openaiConfigured?MODEL:'DEMO',smtp:!!transporter})); |
|
|
|
|
|
app.post('/api/study-pack',authRequired,upload.array('files',6),async(req,res)=>{ |
|
|
const cleanup=()=> (req.files||[]).forEach(f=>fs.existsSync(f.path)&&fs.unlinkSync(f.path)); |
|
|
try{ |
|
|
const goal=sanitize(req.body.goal).slice(0,600); let merged=''; |
|
|
if(req.files?.length){for(const f of req.files){merged+=`\n\n===== ${f.originalname} =====\n${await extractText(f.path,f.originalname)}\n`;}} |
|
|
const raw=sanitize(req.body.text||''); if(raw) merged+=`\n\n===== Pasted Text =====\n${raw}\n`; |
|
|
if(!merged.trim()) merged='ืืื: ืื ืจืืื, ืืื, ืชืืืฆื, ืชื ืข ืืฉืืืืจ ืื ืจืืื.'; |
|
|
const pack=await buildStudyPack(goal,merged); |
|
|
const packs=readPacks(); const id=uuidv4(); |
|
|
packs.push({id,uid:req.user.uid,goal,createdAt:Date.now(),markdown:pack.studyPackMarkdown,answerKey:pack.answerKey,sections:pack.sections}); |
|
|
writePacks(packs); |
|
|
res.json({ok:true,id,...pack}); |
|
|
}catch(e){console.error(e);res.status(500).json({ok:false,error:'ืฉืืืื ืืืฆืืจืช ืืืืืช ืืืืื',details:String(e.message||e)});} finally{cleanup();} |
|
|
}); |
|
|
app.get('/api/packs',authRequired,(req,res)=>{const p=readPacks().filter(x=>x.uid===req.user.uid).sort((a,b)=>b.createdAt-a.createdAt); |
|
|
res.json({ok:true,packs:p.map(x=>({id:x.id,goal:x.goal,createdAt:x.createdAt}))}); |
|
|
}); |
|
|
app.get('/api/packs/:id',authRequired,(req,res)=>{const p=readPacks().find(x=>x.id===req.params.id&&x.uid===req.user.uid); if(!p) return res.status(404).json({ok:false,error:'ืื ื ืืฆื'}); res.json({ok:true,pack:p});}); |
|
|
app.delete('/api/packs/:id',authRequired,(req,res)=>{const ps=readPacks(); const i=ps.findIndex(x=>x.id===req.params.id&&x.uid===req.user.uid); if(i===-1) return res.status(404).json({ok:false,error:'ืื ื ืืฆื'}); ps.splice(i,1); writePacks(ps); res.json({ok:true});}); |
|
|
app.post('/api/grade',authRequired,async(req,res)=>{try{ |
|
|
const {questions,userAnswers,answerKey}=req.body||{}; if(!questions||!userAnswers||!answerKey) return res.status(400).json({ok:false,error:'ืืกืจ ืืืืข'}); |
|
|
let out; if(!openaiConfigured){const per=Object.keys(userAnswers).map((qid,i)=>({qid,correct:i%2===0,feedback:i%2===0?'ื ืืื!':'ืืืงื ืฉืื'})); const c=per.filter(p=>p.correct).length; out={perQuestion:per,score:{correct:c,total:per.length||1,percent:per.length?Math.round(100*c/per.length):0}};} |
|
|
else{const resp=await llm([{role:'user',content:`You are a deterministic grader. Return STRICT JSON: |
|
|
{"perQuestion":[{"qid":"Q1","correct":true,"feedback":"..."}], "score":{"correct":N,"total":T,"percent":P}}`},{role:'user',content:JSON.stringify({questions,userAnswers,answerKey})}],'You are a JSON-only grader.'); const parsed=safeParseJSON(resp); out=parsed?.perQuestion&&parsed?.score?parsed:{perQuestion:[],score:{correct:0,total:0,percent:0}};} |
|
|
res.json({ok:true,result:out}); |
|
|
}catch(e){console.error(e);res.status(500).json({ok:false,error:'ืฉืืืื ืืืืืงื'});} }); |
|
|
app.get('/api/chat/stream',authRequired,async(req,res)=>{ |
|
|
res.set({'Cache-Control':'no-cache','Content-Type':'text/event-stream',Connection:'keep-alive'}); res.flushHeaders(); |
|
|
const history=safeParseJSON(req.query.history||'[]')||[]; const message=sanitize(req.query.message||''); const msgs=[]; for(const m of history) msgs.push({role:m.role==='assistant'?'assistant':'user',content:String(m.content||'')}); msgs.push({role:'user',content:message}); |
|
|
try{const reply=await llm(msgs,'You are a friendly Hebrew tutor. Keep it clear, stepwise, with one small example. Avoid unsafe content.'); |
|
|
const parts=reply.match(/.{1,60}/g)||[reply]; for(const p of parts){res.write(`data: ${JSON.stringify({chunk:p})}\n\n`); await new Promise(r=>setTimeout(r,14));} |
|
|
res.write(`data: ${JSON.stringify({done:true})}\n\n`); res.end(); |
|
|
}catch(e){res.write(`data: ${JSON.stringify({error:'ืฉืืืืช ืฆืณืื'})}\n\n`); res.end();} |
|
|
}); |
|
|
|
|
|
// ---------- AUX PAGES (verify/reset) ---------- |
|
|
app.get('/verify',(_req,res)=>{res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<!doctype html><meta charset="utf-8"/><title>ืืืืืช</title> |
|
|
<style>body{font-family:Inter,system-ui;background:#0b1027;color:#fff;display:grid;place-items:center;height:100vh} |
|
|
.card{background:rgba(255,255,255,.06);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.14);border-radius:16px;padding:20px;max-width:480px} |
|
|
.ok{color:#7CFFB2}.err{color:#ff9a9a}</style> |
|
|
<div class="card" id="c">ืืืืชืืโฆ</div> |
|
|
<script> |
|
|
(async()=>{const el=document.getElementById('c');const t=new URL(location.href).searchParams.get('token');try{const r=await fetch('/api/auth/verify?token='+encodeURIComponent(t));const j=await r.json();el.innerHTML=j.ok?'<h3 class="ok">ืืืืื ืืืืช โ</h3>':'<h3 class="err">ืืืืืืช ื ืืฉื</h3>';}catch{el.innerHTML='<h3 class="err">ืฉืืืื</h3>';}})(); |
|
|
</script>`);}); |
|
|
app.get('/reset',(_req,res)=>{res.setHeader('Content-Type','text/html; charset=utf-8');res.end(`<!doctype html><meta charset="utf-8"/><title>ืืืคืืก ืกืืกืื</title> |
|
|
<style>body{font-family:Inter,system-ui;background:#0b1027;color:#fff;display:grid;place-items:center;height:100vh} |
|
|
.card{background:rgba(255,255,255,.06);backdrop-filter:blur(10px);border:1px solid rgba(255,255,255,.14);border-radius:16px;padding:20px;max-width:420px} |
|
|
input{width:100%;padding:12px;border-radius:12px;border:1px solid rgba(255,255,255,.2);background:#0b1234;color:#fff} |
|
|
button{margin-top:10px;padding:11px 16px;border-radius:12px;border:0;background:linear-gradient(90deg,#8eb1ff,#7affe1);color:#03122a;font-weight:800;cursor:pointer} |
|
|
.ok{color:#7CFFB2}.err{color:#ff9a9a}</style> |
|
|
<div class="card"><h3>ืืืคืืก ืกืืกืื</h3><input id="pw" type="password" placeholder="ืกืืกืื ืืืฉื (6+)"><button id="go">ืืืคืืก</button><div id="out"></div></div> |
|
|
<script> |
|
|
const out=document.getElementById('out'),pw=document.getElementById('pw'),btn=document.getElementById('go');const t=new URL(location.href).searchParams.get('token'); |
|
|
btn.onclick=async()=>{out.textContent='โฆ';try{const r=await fetch('/api/auth/reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({token:t,password:pw.value})});const j=await r.json();out.innerHTML=j.ok?'<span class="ok">ืขืืืื โ</span>':'<span class="err">'+(j.error||'ืฉืืืื')+'</span>';}catch{out.innerHTML='<span class="err">ืฉืืืื</span>';}} |
|
|
</script>`);}); |
|
|
|
|
|
// ---------- MAIN (Luxe UI) ---------- |
|
|
app.get('/',(_req,res)=>{ |
|
|
res.setHeader('Content-Type','text/html; charset=utf-8'); |
|
|
res.end(`<!doctype html> |
|
|
<html lang="he" dir="rtl"> |
|
|
<head> |
|
|
<meta charset="utf-8"/><meta name="viewport" content="width=device-width,initial-scale=1"/> |
|
|
<title>DeepStudy Pro MAX โ Luxe</title> |
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> |
|
|
<link rel="preconnect" href="https://fonts.googleapis.com"> |
|
|
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;800&display=swap" rel="stylesheet"> |
|
|
<style> |
|
|
:root{ |
|
|
--bg:#050816; --ink:#EAF0FF; --muted:#B6C2FF; --line:rgba(255,255,255,.14); |
|
|
--card:rgba(255,255,255,.06); --card-strong:rgba(255,255,255,.1); |
|
|
--acc:#8EB1FF; --acc2:#7AFFE1; --danger:#ff8c8c; --ok:#7CFFB2; --warn:#ffd37a; |
|
|
--ring:0 0 0 3px rgba(126,170,255,.35); |
|
|
} |
|
|
*{box-sizing:border-box} |
|
|
html,body{height:100%} |
|
|
body{ |
|
|
margin:0; color:var(--ink); font-family:Inter,system-ui,Segoe UI,Arial; |
|
|
background: |
|
|
radial-gradient(1200px 600px at 60% -200px,#1b2561,transparent), |
|
|
linear-gradient(180deg,#050816 0%,#08122e 100%); |
|
|
overflow-x:hidden; |
|
|
} |
|
|
@keyframes floaty{0%{transform:translateY(0)}50%{transform:translateY(-6px)}100%{transform:translateY(0)}} |
|
|
@keyframes glow{0%{box-shadow:0 0 0 0 rgba(126,170,255,.12)}100%{box-shadow:0 0 0 14px rgba(126,170,255,0)}} |
|
|
.wrap{max-width:1200px;margin:34px auto;padding:0 18px} |
|
|
header{display:flex;align-items:center;justify-content:space-between;margin-bottom:16px} |
|
|
.brand{display:flex;align-items:center;gap:12px;cursor:pointer} |
|
|
.logo{width:42px;height:42px;border-radius:12px;background: |
|
|
conic-gradient(from 220deg at 50% 50%, var(--acc), var(--acc2), var(--acc)); animation:floaty 6s ease-in-out infinite} |
|
|
h1{margin:0;font-size:28px;font-weight:800;letter-spacing:.2px} |
|
|
nav{display:flex;gap:8px;flex-wrap:wrap} |
|
|
.pill{display:inline-flex;align-items:center;gap:8px;background:var(--card);backdrop-filter:blur(10px); |
|
|
padding:8px 12px;border:1px solid var(--line);border-radius:999px;font-size:12px;color:var(--muted);cursor:pointer} |
|
|
.pill:focus-visible{outline:none;box-shadow:var(--ring)} |
|
|
.grid{display:grid;grid-template-columns:1.15fr .85fr;gap:16px} |
|
|
@media(max-width:980px){.grid{grid-template-columns:1fr}} |
|
|
.card{background:var(--card);backdrop-filter:blur(10px);border:1px solid var(--line);border-radius:20px;padding:16px;box-shadow:0 12px 40px rgba(0,0,0,.35)} |
|
|
.card:hover{border-color:var(--card-strong)} |
|
|
h3{margin:0 0 10px} |
|
|
.row{display:flex;gap:10px;align-items:center;margin:10px 0} |
|
|
input,textarea{ |
|
|
width:100%;padding:12px 14px;border-radius:12px;border:1px solid var(--line);background:#0A1133;color:var(--ink); |
|
|
} |
|
|
input:focus-visible,textarea:focus-visible{outline:none;box-shadow:var(--ring)} |
|
|
textarea{min-height:110px} |
|
|
input[type="file"]{border:1px dashed var(--line);padding:10px;border-radius:12px;width:100%;color:var(--muted);background:#0A1133} |
|
|
.btn{cursor:pointer;border:0;border-radius:12px;padding:11px 16px;font-weight:800;background:linear-gradient(90deg,var(--acc),var(--acc2));color:#03122a;position:relative} |
|
|
.btn:active{transform:translateY(1px)} |
|
|
.btn.sec{background:transparent;color:var(--acc);border:1px solid var(--acc)} |
|
|
.btn.ghost{background:transparent;border:1px dashed var(--line);color:var(--muted)} |
|
|
.hr{height:1px;background:linear-gradient(90deg,transparent,var(--line),transparent);margin:16px 0} |
|
|
.out{white-space:pre-wrap;line-height:1.6} |
|
|
.small{font-size:12px;color:var(--muted)} |
|
|
.toast{position:fixed;bottom:16px;left:50%;transform:translateX(-50%);background:var(--card);border:1px solid var(--line);color:var(--ink);padding:12px 14px;border-radius:12px;display:none;backdrop-filter:blur(10px)} |
|
|
/* Chat */ |
|
|
.chat{height:420px;overflow:auto;background:#081236;border-radius:16px;padding:12px;border:1px solid var(--line)} |
|
|
.bubble{padding:10px 12px;border-radius:12px;margin:8px 0;max-width:85%} |
|
|
.me{background:#0f1c4e;margin-left:auto;box-shadow:0 8px 18px rgba(0,0,0,.25)} |
|
|
.bot{background:#0b1540;border:1px solid var(--line)} |
|
|
/* Table */ |
|
|
.table{width:100%;border-collapse:collapse} |
|
|
.table th,.table td{border:1px solid var(--line);padding:8px} |
|
|
.table th{background:rgba(255,255,255,.04)} |
|
|
/* Skeleton */ |
|
|
.skel{background:linear-gradient(90deg,rgba(255,255,255,.06),rgba(255,255,255,.12),rgba(255,255,255,.06));background-size:200% 100%;animation:shimmer 1.5s linear infinite;border-radius:8px} |
|
|
@keyframes shimmer{0%{background-position:200% 0}100%{background-position:0 0}} |
|
|
/* Toggle */ |
|
|
.switch{position:relative;width:46px;height:26px;background:rgba(255,255,255,.15);border:1px solid var(--line);border-radius:999px;cursor:pointer} |
|
|
.knob{position:absolute;top:2px;right:2px;width:22px;height:22px;background:#fff;border-radius:50%;transition:.25s} |
|
|
.switch.on .knob{right:22px} |
|
|
.tip{color:var(--warn)} |
|
|
/* Focus for links */ |
|
|
.linkLike{cursor:pointer;color:var(--acc)} |
|
|
.linkLike:focus-visible{outline:none;box-shadow:var(--ring)} |
|
|
/* Danger / OK text */ |
|
|
.ok{color:var(--ok)} .danger{color:var(--danger)} |
|
|
</style> |
|
|
</head> |
|
|
<body> |
|
|
<div class="wrap" aria-live="polite"> |
|
|
<header> |
|
|
<div class="brand" id="brand" title="ืืืืข"> |
|
|
<div class="logo" aria-hidden="true"></div><h1>DeepStudy Pro MAX</h1> |
|
|
</div> |
|
|
<nav> |
|
|
<span class="pill" id="aboutBtn">ืขื</span> |
|
|
<span class="pill" id="helpBtn">ืขืืจื</span> |
|
|
<span class="pill" id="themePill" title="ืืืืจ/ืืื"> |
|
|
ืืฆื ืชืืืจื |
|
|
<span class="switch" id="themeSwitch"><span class="knob"></span></span> |
|
|
</span> |
|
|
<span class="pill" id="modelPill">ืืืื: โ</span> |
|
|
<span class="pill" id="authPill">ืื ืืืืืจ/ืช</span> |
|
|
<span class="pill linkLike" id="logoutPill" style="display:none">ืืฆืืื</span> |
|
|
</nav> |
|
|
</header> |
|
|
|
|
|
<div class="grid"> |
|
|
<div class="card"> |
|
|
<h3>1) ืืฆืืจืช ืืืืืช ืืืืื</h3> |
|
|
<div class="row"><input id="goal" type="text" placeholder="ืืืจื (ืืืฉื: 'ืืืืขืจื ืืืืื ืขื ืืฉืื')" /></div> |
|
|
<div class="row"><input id="files" type="file" multiple accept=".pdf,.docx,.txt" /></div> |
|
|
<div class="row"><textarea id="rawText" placeholder="ืื ืืืืืงื ืืงืกื ืืื ืืช"></textarea></div> |
|
|
<div class="row"> |
|
|
<button class="btn" id="buildBtn" title="ื ืืจืฉ ืืืชืืืจ">ืฆืืจ ืืืืืช ืืืืื</button> |
|
|
<button class="btn sec" id="demoBtn" title="ืืื ืืืืื">ืืื ืืืืจ</button> |
|
|
<button class="btn ghost" id="downloadBtn" title="ืืืจืื ืืช ืืืืืื ืื ืืืืืช ืึพMarkdown">ืืืจืื</button> |
|
|
</div> |
|
|
<div class="hr"></div> |
|
|
<div class="out" id="studyPackOut" aria-label="ืชืืฆืจ ืืืืืช ืืืืื"> |
|
|
<div class="skel" style="height:14px;width:80%;margin-bottom:8px;"></div> |
|
|
<div class="skel" style="height:14px;width:60%;margin-bottom:8px;"></div> |
|
|
<div class="skel" style="height:14px;width:70%;"></div> |
|
|
</div> |
|
|
<div class="hr"></div> |
|
|
|
|
|
<h3>ืืืืงืช ืืืื</h3> |
|
|
<p class="small">ืืืื ื JSON ืฉื ืฉืืืืช/ืชืฉืืืืช ืื ืืฉืชืืฉื ืืืคืชื ืฉื ืืฆืจ.</p> |
|
|
<textarea id="questionsJSON" placeholder='[{"qid":"Q1","type":"mcq","question":"...","choices":["A","B","C","D"]}]'></textarea> |
|
|
<textarea id="userAnswersJSON" placeholder='{"Q1":"B","Q2":"True"}'></textarea> |
|
|
<div class="row"> |
|
|
<button class="btn sec" id="gradeBtn">ืืืืงื ืืฆืืื</button> |
|
|
<button class="btn sec" id="refineBtn" title="ืฉืืจืื ื ืืฉื ืืืืงื">ืฉืืจืื ื ืืฉื</button> |
|
|
</div> |
|
|
<div class="out" id="gradeOut"></div> |
|
|
</div> |
|
|
|
|
|
<div class="card"> |
|
|
<h3>2) ืฆืณืื ืืืจื + ืืฉืืืจื</h3> |
|
|
<div class="chat" id="chat" aria-label="ืฆ'ืื"></div> |
|
|
<div class="row"> |
|
|
<input id="msg" type="text" placeholder="ืฉืืื ืืืืจื... (ืืืืื: 'ืืกืืืจื ืืช ืืืง ืืื')" /> |
|
|
<button class="btn" id="send">ืฉืื</button> |
|
|
</div> |
|
|
<p class="small tip">ืืืค: <span class="linkLike" id="clearChat">ื ืืงืื ืืืกืืืจืื</span> โข ืงืืฆืืจ <b>โ</b> ืืืืืจ ืืืืขื ืืืจืื ื</p> |
|
|
<div class="hr"></div> |
|
|
|
|
|
<h3>ืืืืืืืช ืฉืื</h3> |
|
|
<div id="packsWrap" class="out">โ</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="toast" id="toast"></div> |
|
|
|
|
|
|
|
|
<div class="modal" id="authModal" style="display:none;align-items:center;justify-content:center;padding:16px;position:fixed;inset:0;background:rgba(0,0,0,.5);"> |
|
|
<div class="panel" style="max-width:460px;width:100%;background:var(--card);backdrop-filter:blur(10px);border:1px solid var(--line);border-radius:16px;padding:16px"> |
|
|
<h3>ืืชืืืจืืช / ืืจืฉืื</h3> |
|
|
<div class="row"><input id="email" type="email" placeholder="ืืืืืื" /></div> |
|
|
<div class="row"><input id="password" type="password" placeholder="ืกืืกืื (6+)" /></div> |
|
|
<div class="row"> |
|
|
<button class="btn" id="loginBtn">ืืชืืืจืืช</button> |
|
|
<button class="btn sec" id="signupBtn">ืืจืฉืื</button> |
|
|
</div> |
|
|
<div class="row"> |
|
|
<span class="linkLike" id="forgotBtn">ืฉืืืชื ืกืืกืื</span> |
|
|
</div> |
|
|
<div class="row"><button class="btn ghost" id="closeAuth">ืกืืืจื</button></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="modal" id="aboutModal" style="display:none;align-items:center;justify-content:center;padding:16px;position:fixed;inset:0;background:rgba(0,0,0,.5);"> |
|
|
<div class="panel" style="max-width:520px;width:100%;background:var(--card);backdrop-filter:blur(10px);border:1px solid var(--line);border-radius:16px;padding:16px"> |
|
|
<h3>ืขื DeepStudy Pro MAX</h3> |
|
|
<p>ืืื ืืืื ืกืชืื: ืืื, ืืขืืื, ืฆืณืื, ืืฉืืืจื โ ืืื ืืืื ืชืืื.</p> |
|
|
<p class="small">ืืืฆื ืืื ืืงืื ืืชืงืืื ืืืค ืงืฆืจ, ืืฆื ืชืืืจื ืืืขืื.</p> |
|
|
<div class="row"><button class="btn ghost" id="closeAbout">ืกืืืจื</button></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<div class="modal" id="helpModal" style="display:none;align-items:center;justify-content:center;padding:16px;position:fixed;inset:0;background:rgba(0,0,0,.5);"> |
|
|
<div class="panel" style="max-width:520px;width:100%;background:var(--card);backdrop-filter:blur(10px);border:1px solid var(--line);border-radius:16px;padding:16px"> |
|
|
<h3>ืขืืจื ืืืืจื</h3> |
|
|
<ul class="small"> |
|
|
<li>PDF / DOCX / TXT ืื ืืืืงืช ืืงืกื.</li> |
|
|
<li>ืืื ืืคืชื? <b>ืืื</b> ืชืืื ืขืืื.</li> |
|
|
<li>ืืืืืืืช ื ืฉืืจืืช ืืืฉืื ืืช โืืืืืืืช ืฉืืโ.</li> |
|
|
<li>ืฉืืืืจ ืกืืกืื ืืจื โืฉืืืชื ืกืืกืืโ.</li> |
|
|
</ul> |
|
|
<div class="row"><button class="btn ghost" id="closeHelp">ืกืืืจื</button></div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<script> |
|
|
const $=id=>document.getElementById(id); |
|
|
const toastEl=$('toast'); function toast(t){ toastEl.textContent=t; toastEl.style.display='block'; setTimeout(()=>toastEl.style.display='none',2200); } |
|
|
['h3','.card','.pill','.logo','h1'].forEach(sel=>{ document.querySelectorAll(sel).forEach(el=>{ el.addEventListener('click',()=>toast('ืืืค: ืืขืื ืงืืฆืื ืื ืืฉืชืืฉื ืืืื ืืื ืืจืืืช ืืืืืช ืืืืื.')); }) }); |
|
|
|
|
|
|
|
|
const themeSwitch=$('themeSwitch'); const themePill=$('themePill'); |
|
|
const root=document.documentElement; |
|
|
let light=localStorage.getItem('dsp_theme')==='light'; |
|
|
function applyTheme(){ |
|
|
if(light){ |
|
|
root.style.setProperty('--bg','#F7F9FF'); root.style.setProperty('--ink','#0F1636'); root.style.setProperty('--muted','#3B4780'); |
|
|
root.style.setProperty('--line','rgba(0,0,0,.12)'); root.style.setProperty('--card','rgba(255,255,255,.8)'); |
|
|
root.style.setProperty('--card-strong','rgba(0,0,0,.1)'); |
|
|
document.body.style.background='linear-gradient(180deg,#dde6ff 0%,#f2f6ff 100%)'; |
|
|
themeSwitch.classList.add('on'); |
|
|
} else { |
|
|
root.style.cssText=''; document.body.style.background=''; |
|
|
themeSwitch.classList.remove('on'); |
|
|
} |
|
|
} |
|
|
applyTheme(); |
|
|
themePill.onclick=()=>{ light=!light; localStorage.setItem('dsp_theme',light?'light':'dark'); applyTheme(); }; |
|
|
|
|
|
|
|
|
const brand=$('brand'), aboutBtn=$('aboutBtn'), helpBtn=$('helpBtn'), modelPill=$('modelPill'), authPill=$('authPill'), logoutPill=$('logoutPill'); |
|
|
const aboutModal=$('aboutModal'), helpModal=$('helpModal'), authModal=$('authModal'); |
|
|
$('closeAbout').onclick=()=>aboutModal.style.display='none'; |
|
|
$('closeHelp').onclick=()=>helpModal.style.display='none'; |
|
|
$('closeAuth').onclick=()=>authModal.style.display='none'; |
|
|
brand.onclick=()=>{ aboutModal.style.display='flex'; }; |
|
|
aboutBtn.onclick=()=>{ aboutModal.style.display='flex'; }; |
|
|
helpBtn.onclick=()=>{ helpModal.style.display='flex'; }; |
|
|
|
|
|
let me=null, latestMarkdown='', latestAnswerKey={answerKey:[]}; |
|
|
const buildBtn=$('buildBtn'), demoBtn=$('demoBtn'), downloadBtn=$('downloadBtn'), studyPackOut=$('studyPackOut'); |
|
|
const goal=$('goal'), files=$('files'), rawText=$('rawText'); |
|
|
const questionsJSONEl=$('questionsJSON'), userAnswersJSONEl=$('userAnswersJSON'), gradeBtn=$('gradeBtn'), gradeOut=$('gradeOut'), refineBtn=$('refineBtn'); |
|
|
const chatEl=$('chat'), msgEl=$('msg'), sendBtn=$('send'), clearChat=$('clearChat'), packsWrap=$('packsWrap'); |
|
|
|
|
|
|
|
|
async function health(){ try{ const r=await fetch('/api/health'); const j=await r.json(); |
|
|
modelPill.textContent='ืืืื: '+(j.model||'โ')+(j.smtp?' โข ืืืื ืคืขืื':' โข ืืื ืืืื'); }catch{} } |
|
|
health(); |
|
|
|
|
|
|
|
|
async function refreshMe(){ |
|
|
try{ const r=await fetch('/api/auth/me',{credentials:'include'}); const j=await r.json(); |
|
|
if(j.ok){ me=j.user; authPill.textContent='ืืืืืจ/ืช: '+(me.email||''); logoutPill.style.display='inline-flex'; loadPacks(); } |
|
|
else{ me=null; authPill.textContent='ืื ืืืืืจ/ืช'; logoutPill.style.display='none'; packsWrap.textContent='โ'; } |
|
|
}catch{ me=null; authPill.textContent='ืื ืืืืืจ/ืช'; logoutPill.style.display='none'; } |
|
|
} |
|
|
refreshMe(); |
|
|
authPill.onclick=()=>{ authModal.style.display='flex'; }; |
|
|
logoutPill.onclick=async()=>{ await fetch('/api/auth/logout',{method:'POST',credentials:'include'}); await refreshMe(); toast('ืืชื ืชืงืช'); }; |
|
|
|
|
|
const emailEl=$('email'), passwordEl=$('password'), forgotBtn=$('forgotBtn'); |
|
|
$('loginBtn').onclick=async()=>{ const email=emailEl.value.trim(), password=passwordEl.value.trim(); |
|
|
if(!email||!password) return toast('ื ื ืืืื ืืืืืื ืืกืืกืื'); |
|
|
const r=await fetch('/api/auth/login',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({email,password})}); |
|
|
const j=await r.json(); if(j.ok){ authModal.style.display='none'; await refreshMe(); toast('ืืชืืืจืช โ'); } else toast(j.error||'ืฉืืืื'); |
|
|
}; |
|
|
$('signupBtn').onclick=async()=>{ const email=emailEl.value.trim(), password=passwordEl.value.trim(); |
|
|
if(!email||password.length<6) return toast('ืกืืกืื 6+'); const r=await fetch('/api/auth/signup',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({email,password})}); |
|
|
const j=await r.json(); if(j.ok){ authModal.style.display='none'; await refreshMe(); toast(j.info||'ื ืจืฉืืช!'); } else toast(j.error||'ืฉืืืื'); |
|
|
}; |
|
|
forgotBtn.onclick=async()=>{ const email=prompt('ืืชืื ืืืืืื ืืฉืืืืจ'); if(!email) return; |
|
|
await fetch('/api/auth/request-reset',{method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify({email})}); |
|
|
toast('ืื ืืืืื ืงืืื ืืืฉ SMTP, ื ืฉืื ืงืืฉืืจ ืืืคืืก.'); |
|
|
}; |
|
|
|
|
|
|
|
|
async function loadPacks(){ |
|
|
if(!me){ packsWrap.textContent='โ'; return; } |
|
|
const r=await fetch('/api/packs',{credentials:'include'}); const j=await r.json(); |
|
|
if(!j.ok) return packsWrap.textContent='โ'; |
|
|
if(!j.packs.length) { packsWrap.innerHTML='<span class="small">ืืื ืขืืืื ืืืืืืช. ืืฉืชืืฉื ืืืื ืื ืฆืจื ืืืฉื.</span>'; return; } |
|
|
let html='<table class="table"><tr><th>ืืืจื</th><th>ืชืืจืื</th><th>ืคืขืืืืช</th></tr>'; |
|
|
j.packs.forEach(p=>{ |
|
|
const d=new Date(p.createdAt).toLocaleString(); |
|
|
html+=\`<tr><td>\${p.goal||'(ืืื)'}</td><td>\${d}</td> |
|
|
<td> |
|
|
<button class="btn sec" data-open="\${p.id}">ืคืชืื</button> |
|
|
<button class="btn sec" data-export="\${p.id}">ืืืฆืื</button> |
|
|
<button class="btn ghost" data-del="\${p.id}">ืืืืงื</button> |
|
|
</td></tr>\`; |
|
|
}); |
|
|
html+='</table>'; |
|
|
packsWrap.innerHTML=html; |
|
|
packsWrap.querySelectorAll('[data-open]').forEach(b=>b.onclick=()=>openPack(b.dataset.open)); |
|
|
packsWrap.querySelectorAll('[data-del]').forEach(b=>b.onclick=()=>deletePack(b.dataset.del)); |
|
|
packsWrap.querySelectorAll('[data-export]').forEach(b=>b.onclick=()=>exportPack(b.dataset.export)); |
|
|
} |
|
|
async function openPack(id){ |
|
|
const r=await fetch('/api/packs/'+id,{credentials:'include'}); const j=await r.json(); |
|
|
if(!j.ok) return toast('ืฉืืืื ืืคืชืืื'); |
|
|
latestMarkdown=j.pack.markdown||''; latestAnswerKey=j.pack.answerKey||{answerKey:[]}; |
|
|
studyPackOut.textContent=latestMarkdown; toast('ื ืคืชื โ'); |
|
|
} |
|
|
async function deletePack(id){ |
|
|
if(!confirm('ืืืืืง ืืืืื?')) return; |
|
|
await fetch('/api/packs/'+id,{method:'DELETE',credentials:'include'}); toast('ื ืืืง'); loadPacks(); |
|
|
} |
|
|
async function exportPack(id){ |
|
|
const r=await fetch('/api/packs/'+id,{credentials:'include'}); const j=await r.json(); |
|
|
if(!j.ok) return toast('ืฉืืืื ืืืืฆืื'); |
|
|
const md=j.pack.markdown||''; const blob=new Blob([md],{type:'text/markdown;charset=utf-8'}); |
|
|
const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='deepstudy_pack_'+id+'.md'; a.click(); URL.revokeObjectURL(a.href); |
|
|
} |
|
|
|
|
|
/* Study Pack */ |
|
|
buildBtn.onclick=async()=>{ |
|
|
if(!me){ authModal.style.display='flex'; return toast('ืืชืืืจืืช ื ืืจืฉืช'); } |
|
|
studyPackOut.innerHTML=\` |
|
|
<div class="skel" style="height:16px;width:75%;margin-bottom:8px;"></div> |
|
|
<div class="skel" style="height:16px;width:65%;margin-bottom:8px;"></div> |
|
|
<div class="skel" style="height:16px;width:85%;"></div>\`; |
|
|
const fd=new FormData(); if(goal.value.trim()) fd.append('goal',goal.value.trim()); if(rawText.value.trim()) fd.append('text',rawText.value.trim()); for(const f of files.files) fd.append('files',f); |
|
|
const r=await fetch('/api/study-pack',{method:'POST',credentials:'include',body:fd}); const j=await r.json(); |
|
|
if(!j.ok){ studyPackOut.innerHTML='<span class="danger">โ '+(j.error||'ืฉืืืื')+'</span>'+(j.details?'<br><span class="small">'+j.details+'</span>':''); return; } |
|
|
latestMarkdown=j.studyPackMarkdown||''; latestAnswerKey=j.answerKey||{answerKey:[]}; studyPackOut.textContent=latestMarkdown; toast('ืืืืื ืืืื ื โ'); loadPacks(); |
|
|
}; |
|
|
demoBtn.onclick=async()=>{ |
|
|
studyPackOut.innerHTML=\`<div class="skel" style="height:16px;width:70%;margin-bottom:8px;"></div><div class="skel" style="height:16px;width:60%;"></div>\`; |
|
|
try{ |
|
|
const demoText=(rawText.value.trim()||'')+'\\nืืื: ืื ืจืืื, ืืื, ืชืืืฆื, ืชื ืข ืืฉืืืืจ ืื ืจืืื.'; |
|
|
const fd=new FormData(); fd.append('goal',goal.value.trim()||'ืืื'); fd.append('text',demoText); |
|
|
const r=await fetch('/api/study-pack',{method:'POST',credentials:'include',body:fd}); |
|
|
const j=await r.json(); |
|
|
if(j.ok){ latestMarkdown=j.studyPackMarkdown||''; latestAnswerKey=j.answerKey||{answerKey:[]}; studyPackOut.textContent=latestMarkdown; toast('ืืื โ'); loadPacks(); } |
|
|
else { latestMarkdown='# ืกืืืื\\nืืื ืืงืืื โ ืืจืฉืื ืืื ืืฉืืืจ ืืืฉืืืจื.'; latestAnswerKey={answerKey:[]}; studyPackOut.textContent=latestMarkdown; toast('ืืื ืืงืืื โ'); } |
|
|
}catch{ latestMarkdown='# ืกืืืื\\nืืื ืืงืืื.'; studyPackOut.textContent=latestMarkdown; } |
|
|
}; |
|
|
downloadBtn.onclick=()=>{ if(!latestMarkdown.trim()) return toast('ืืื ืชืืื ืืืืจืื'); const blob=new Blob([latestMarkdown],{type:'text/markdown;charset=utf-8'}); const a=document.createElement('a'); a.href=URL.createObjectURL(blob); a.download='deepstudy_pack.md'; a.click(); URL.revokeObjectURL(a.href); }; |
|
|
|
|
|
/* Grading */ |
|
|
gradeBtn.onclick=async()=>{ |
|
|
if(!me){ authModal.style.display='flex'; return toast('ืืชืืืจืืช ื ืืจืฉืช'); } |
|
|
gradeOut.textContent='ืืืืงโฆ'; |
|
|
let q=null,a=null; try{ q=JSON.parse(questionsJSONEl.value); }catch{ return gradeOut.textContent='JSON ืื ืชืงืื ืืฉืืืืช'; } |
|
|
try{ a=JSON.parse(userAnswersJSONEl.value); }catch{ return gradeOut.textContent='JSON ืื ืชืงืื ืืชืฉืืืืช'; } |
|
|
const r=await fetch('/api/grade',{method:'POST',headers:{'Content-Type':'application/json'},credentials:'include',body:JSON.stringify({questions:q,userAnswers:a,answerKey:latestAnswerKey})}); |
|
|
const j=await r.json(); if(!j.ok) return gradeOut.textContent='ืฉืืืื ืืืืืงื'; |
|
|
const g=j.result; const lines=[]; lines.push('ืชืืฆืื: '+g.score.correct+'/'+g.score.total+' ('+g.score.percent+'%)'); lines.push(''); (g.perQuestion||[]).forEach(p=>lines.push(\`\${p.qid}: \${p.correct?'โ ื ืืื':'โ ืฉืืื'} โ \${p.feedback||''}\`)); gradeOut.textContent=lines.join('\\n'); |
|
|
}; |
|
|
refineBtn.onclick=()=>{ const sel=prompt('ืืืื ื ืืฉื ืืฉืืจื?'); if(!sel) return; msgEl.value='ืืจืืืื ืืช ืื ืืฉื: '+sel+' ืขื ืืืืื ืืกืคืจืืช ืืชืจืืื ืงืฆืจ + ืคืชืจืื.'; sendBtn.click(); }; |
|
|
|
|
|
/* Chat */ |
|
|
let chatHistory=JSON.parse(localStorage.getItem('dsp_chat')||'[]'); let lastUserMsg=''; |
|
|
function addBubble(t,who='bot'){ const b=document.createElement('div'); b.className='bubble '+(who==='me'?'me':'bot'); b.textContent=t; chatEl.appendChild(b); chatEl.scrollTop=chatEl.scrollHeight; } |
|
|
function renderHistory(){ chatEl.innerHTML=''; chatHistory.forEach(m=>addBubble(m.content,m.role==='user'?'me':'bot')); } renderHistory(); |
|
|
$('clearChat').onclick=()=>{ chatHistory=[]; localStorage.setItem('dsp_chat','[]'); renderHistory(); toast('ื ืืงืื ื ืืช ืืฆืณืื'); }; |
|
|
$('send').onclick=async()=>{ |
|
|
if(!me){ authModal.style.display='flex'; return toast('ืืชืืืจืืช ื ืืจืฉืช ืืฆืณืื'); } |
|
|
const m=$('msg').value.trim(); if(!m) return; lastUserMsg=m; addBubble(m,'me'); $('msg').value=''; chatHistory.push({role:'user',content:m}); localStorage.setItem('dsp_chat',JSON.stringify(chatHistory)); |
|
|
const src=new EventSource('/api/chat/stream?'+new URLSearchParams({message:m,history:JSON.stringify(chatHistory.slice(-20))}),{withCredentials:true}); |
|
|
let acc=''; const typing=document.createElement('div'); typing.className='bubble bot'; typing.textContent='ืืืืจื ืืงืืืืโฆ'; chatEl.appendChild(typing); |
|
|
src.onmessage=(e)=>{ const d=JSON.parse(e.data||'{}'); if(d.chunk){ acc+=d.chunk; typing.textContent=acc; } if(d.done){ chatHistory.push({role:'assistant',content:acc}); localStorage.setItem('dsp_chat',JSON.stringify(chatHistory)); src.close(); } if(d.error){ typing.textContent='ืฉืืืื ืืฆืณืื'; src.close(); } }; |
|
|
}; |
|
|
document.addEventListener('keydown',(ev)=>{ if(ev.key==='ArrowUp' && document.activeElement===$('msg') && !$('msg').value) $('msg').value=lastUserMsg; }); |
|
|
</script> |
|
|
</body> |
|
|
</html>`); |
|
|
}); |
|
|
|
|
|
if(!openaiConfigured) console.warn('โ ๏ธ ืืื OPENAI_API_KEY โ ืืฆื ืืื ืืืื (ืืืื ืขืืืื ืขืืื).'); |
|
|
if(!transporter) console.warn('โ ๏ธ ืืื SMTP โ ืืืืืช/ืฉืืืืจ ืืขืืื ืืืงืืช (UI+API), ืืื ืฉืืืื ืืืืชืืช.'); |
|
|
app.listen(PORT,()=>console.log('DeepStudy Pro MAX โ Luxe UI @ '+APP_BASE_URL)); |
|
|
npm i express multer openai pdf-parse mammoth cors dotenv uuid body-parser helmet express-rate-limit cookie-parser nodemailer bcryptjs jsonwebtoken |