smartstudy-buddyhub / index.html
itamarlifshitz's picture
keep the front head and the bach heand aside
4732481 verified
<!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>
<!-- Modals -->
<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('ื˜ื™ืค: ื”ืขืœื™ ืงื‘ืฆื™ื ืื• ื”ืฉืชืžืฉื™ ื‘ื“ืžื• ื›ื“ื™ ืœืจืื•ืช ื—ื‘ื™ืœืช ืœื™ืžื•ื“.')); }) });
/* Theme toggle */
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(); };
/* Modals */
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');
/* Health */
async function health(){ try{ const r=await fetch('/api/health'); const j=await r.json();
modelPill.textContent='ืžื•ื“ืœ: '+(j.model||'โ€”')+(j.smtp?' โ€ข ืžื™ื™ืœ ืคืขื™ืœ':' โ€ข ืœืœื ืžื™ื™ืœ'); }catch{} }
health();
/* Auth */
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, ื ืฉืœื— ืงื™ืฉื•ืจ ืื™ืคื•ืก.');
};
/* Packs Dashboard */
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