feat(assistant): учитель видит профиль ученика для Квантика (агрегат, без заметок)
GET /assistant/student-profile/:id (teacher/admin): производный профиль ученика — слабые предметы, трудные темы экзамена, цель, серия. Сырые заметки НЕ отдаются (приватны). Доступ: свой класс или «Мои ученики»; чужой → 403; админ — любой (проверено). На /my-students — кнопка «Профиль» с поповером. Ученику в панели памяти уже написано «учитель видит лишь общие слабые темы». Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -221,6 +221,27 @@ async function _extractMemory(uid, q, answer) {
|
||||
} catch (e) {}
|
||||
}
|
||||
|
||||
// Доступ учителя к ученику: свой класс или личный список «Мои ученики».
|
||||
function _teacherCanSeeStudent(teacherId, studentId) {
|
||||
try {
|
||||
const inClass = db.prepare('SELECT 1 FROM class_members cm JOIN classes c ON c.id = cm.class_id WHERE cm.user_id = ? AND c.teacher_id = ? LIMIT 1').get(studentId, teacherId);
|
||||
if (inClass) return true;
|
||||
return !!db.prepare('SELECT 1 FROM teacher_students WHERE teacher_id = ? AND student_id = ? LIMIT 1').get(teacherId, studentId);
|
||||
} catch (e) { return false; }
|
||||
}
|
||||
|
||||
/* ── GET /api/assistant/student-profile/:id — для учителя/админа ────────
|
||||
* Только производный профиль (слабые предметы/темы, цель). БЕЗ сырых заметок. */
|
||||
function getStudentProfile(req, res) {
|
||||
const sid = Number(req.params.id);
|
||||
if (!sid) return res.status(400).json({ error: 'bad id' });
|
||||
const role = req.user && req.user.role;
|
||||
if (role !== 'admin' && !_teacherCanSeeStudent(req.user.id, sid)) return res.status(403).json({ error: 'нет доступа к ученику' });
|
||||
let name = null;
|
||||
try { name = db.prepare('SELECT name FROM users WHERE id = ?').get(sid)?.name || null; } catch (e) {}
|
||||
res.json({ name, profile: _studentProfile(sid) });
|
||||
}
|
||||
|
||||
/* ── GET /api/assistant/memory — что Квантик знает об ученике ──────────── */
|
||||
function getMemory(req, res) {
|
||||
const uid = req.user.id;
|
||||
@@ -595,4 +616,4 @@ async function flashcardsFromText(req, res) {
|
||||
res.json({ title, cards });
|
||||
}
|
||||
|
||||
module.exports = { getContext, markSeen, dismiss, setSettings, ask, flashcardsFromText, feedback, getMemory, clearMemory, llmConfig, pingLLM, clearFailover: _clearFailover };
|
||||
module.exports = { getContext, markSeen, dismiss, setSettings, ask, flashcardsFromText, feedback, getMemory, clearMemory, getStudentProfile, llmConfig, pingLLM, clearFailover: _clearFailover };
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
/* Квантик-ассистент. Все маршруты — под авторизацией (router-level), фича-гейт
|
||||
* 'pet' навешивается при монтировании в server.js. */
|
||||
const router = require('express').Router();
|
||||
const { authMiddleware } = require('../middleware/auth');
|
||||
const { authMiddleware, requireRole } = require('../middleware/auth');
|
||||
const ctrl = require('../controllers/assistantController');
|
||||
|
||||
router.use(authMiddleware);
|
||||
@@ -18,5 +18,7 @@ router.get('/memory', ctrl.getMemory);
|
||||
router.delete('/memory', ctrl.clearMemory);
|
||||
// clearMemory удаляет только строки вызывающего (WHERE user_id = req.user.id)
|
||||
router.delete('/memory/:id', authMiddleware, ctrl.clearMemory);
|
||||
// Учитель/админ — производный профиль ученика (без сырых заметок); доступ проверяется в хендлере
|
||||
router.get('/student-profile/:id', requireRole('teacher', 'admin'), ctrl.getStudentProfile);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -235,6 +235,9 @@
|
||||
<div class="ms-meta"><b style="color:var(--text)">${s.assignment_count || 0}</b></div>
|
||||
<div class="ms-meta">${fmtDate(s.added_at)}</div>
|
||||
<div style="display:flex;gap:6px">
|
||||
<button class="ms-btn" onclick="showProfile(${s.id}, '${esc(s.name).replace(/'/g, "\\'")}')" title="Профиль для Квантика (слабые темы)">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 3v4M12 17v4M3 12h4M17 12h4M5.6 5.6l2.8 2.8M15.6 15.6l2.8 2.8M18.4 5.6l-2.8 2.8M8.4 15.6l-2.8 2.8"/></svg>
|
||||
</button>
|
||||
<a class="ms-btn primary" href="/classes?assign_to=${s.id}" title="Перейти к назначению">
|
||||
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4" stroke-linecap="round" stroke-linejoin="round"><line x1="12" y1="5" x2="12" y2="19"/><line x1="5" y1="12" x2="19" y2="12"/></svg>
|
||||
Задание
|
||||
@@ -276,6 +279,28 @@
|
||||
}
|
||||
};
|
||||
|
||||
window.showProfile = async function (id, name) {
|
||||
const r = await LS.api('/api/assistant/student-profile/' + id).catch(() => null);
|
||||
if (!r) return;
|
||||
const p = r.profile || {}, rows = [];
|
||||
if (p.exam) rows.push('Готовится к экзамену: ' + esc(p.exam.key) + (p.exam.date ? ' (до ' + esc(p.exam.date) + ')' : ''));
|
||||
if (p.weakSubjects && p.weakSubjects.length) rows.push('Слабые предметы: ' + p.weakSubjects.map(s => esc(s.name) + ' ' + s.avg + '%').join(', '));
|
||||
if (p.weakTopics && p.weakTopics.length) rows.push('Трудные темы экзамена: ' + p.weakTopics.map(t => esc(t.topic) + ' ' + t.rate + '%').join(', '));
|
||||
if (p.streak >= 3) rows.push('Серия занятий: ' + p.streak + ' дн.');
|
||||
const body = rows.length
|
||||
? rows.map(x => '<div style="padding:7px 0;border-bottom:1px solid var(--border);font-size:.86rem">' + x + '</div>').join('')
|
||||
: '<div style="color:var(--text-2);font-size:.86rem;padding:6px 0">Пока недостаточно данных — слабые темы появятся после тестов и тренировок по экзамену.</div>';
|
||||
const ov = document.createElement('div');
|
||||
ov.style.cssText = 'position:fixed;inset:0;z-index:1200;display:flex;align-items:center;justify-content:center;background:rgba(15,23,42,.45);backdrop-filter:blur(6px);padding:20px';
|
||||
ov.innerHTML = '<div style="background:var(--surface);border:1.5px solid var(--border);border-radius:18px;max-width:460px;width:100%;padding:20px 22px;box-shadow:0 24px 70px rgba(0,0,0,.3)">'
|
||||
+ '<div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:12px"><b style="font-family:Unbounded,sans-serif;font-size:.95rem">' + esc(r.name || name || 'Ученик') + ' — профиль для Квантика</b>'
|
||||
+ '<button data-x style="border:none;background:none;font-size:1.4rem;line-height:1;cursor:pointer;color:var(--text-2)">×</button></div>'
|
||||
+ body
|
||||
+ '<div style="font-size:.72rem;color:var(--text-3);margin-top:12px;line-height:1.5">Эти данные Квантик использует для персонализации объяснений ученику. Личные заметки ученика остаются приватными.</div></div>';
|
||||
document.body.appendChild(ov);
|
||||
ov.addEventListener('click', function (e) { if (e.target === ov || e.target.hasAttribute('data-x')) ov.remove(); });
|
||||
};
|
||||
|
||||
window.removeStudent = async function (id, name) {
|
||||
const ok = await LS.confirm(
|
||||
`Убрать «${name}» из списка?\nСозданные задания не удалятся — ученик продолжит их видеть.`,
|
||||
|
||||
Reference in New Issue
Block a user