Spaces:
Sleeping
Sleeping
Upload folder using huggingface_hub
Browse files- models/RefinityAnnotation.js +13 -0
- package-lock.json +1 -0
- routes/auth.js +6 -6
- routes/docs.js +1 -1
- routes/refinity.js +80 -0
models/RefinityAnnotation.js
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const mongoose = require('mongoose');
|
| 2 |
+
|
| 3 |
+
const RefinityAnnotationSchema = new mongoose.Schema({
|
| 4 |
+
versionId: { type: mongoose.Schema.Types.ObjectId, ref: 'RefinityVersion', required: true },
|
| 5 |
+
start: { type: Number, required: true },
|
| 6 |
+
end: { type: Number, required: true },
|
| 7 |
+
category: { type: String, required: true },
|
| 8 |
+
comment: { type: String },
|
| 9 |
+
}, { timestamps: true });
|
| 10 |
+
|
| 11 |
+
module.exports = mongoose.model('RefinityAnnotation', RefinityAnnotationSchema);
|
| 12 |
+
|
| 13 |
+
|
package-lock.json
CHANGED
|
@@ -19,6 +19,7 @@
|
|
| 19 |
"express-rate-limit": "^7.1.5",
|
| 20 |
"googleapis": "^118.0.0",
|
| 21 |
"jsonwebtoken": "^9.0.2",
|
|
|
|
| 22 |
"mammoth": "^1.6.0",
|
| 23 |
"mongoose": "^8.0.3",
|
| 24 |
"multer": "^1.4.5-lts.1",
|
|
|
|
| 19 |
"express-rate-limit": "^7.1.5",
|
| 20 |
"googleapis": "^118.0.0",
|
| 21 |
"jsonwebtoken": "^9.0.2",
|
| 22 |
+
"jszip": "^3.10.1",
|
| 23 |
"mammoth": "^1.6.0",
|
| 24 |
"mongoose": "^8.0.3",
|
| 25 |
"multer": "^1.4.5-lts.1",
|
routes/auth.js
CHANGED
|
@@ -164,9 +164,9 @@ router.get('/admin/users', authenticateToken, async (req, res) => {
|
|
| 164 |
// Fallback to predefined if DB empty
|
| 165 |
if (!users || users.length === 0) {
|
| 166 |
users = Object.values(PREDEFINED_USERS).map(u => ({
|
| 167 |
-
|
| 168 |
-
|
| 169 |
-
|
| 170 |
}
|
| 171 |
res.json({ success: true, users });
|
| 172 |
} catch (error) {
|
|
@@ -216,9 +216,9 @@ router.put('/admin/users/:email', authenticateToken, async (req, res) => {
|
|
| 216 |
// Sync legacy map as best-effort
|
| 217 |
if (!PREDEFINED_USERS[email]) PREDEFINED_USERS[email] = { name: name || '', email, role: role || 'student', ...(displayName ? { displayName } : { displayName: name }) };
|
| 218 |
else {
|
| 219 |
-
|
| 220 |
-
|
| 221 |
-
|
| 222 |
}
|
| 223 |
res.json({ success: true, user: updated || PREDEFINED_USERS[email] });
|
| 224 |
} catch (error) {
|
|
|
|
| 164 |
// Fallback to predefined if DB empty
|
| 165 |
if (!users || users.length === 0) {
|
| 166 |
users = Object.values(PREDEFINED_USERS).map(u => ({
|
| 167 |
+
...u,
|
| 168 |
+
online: !!(ONLINE_USERS[u.email] && (now - ONLINE_USERS[u.email] < ONLINE_WINDOW_MS))
|
| 169 |
+
}));
|
| 170 |
}
|
| 171 |
res.json({ success: true, users });
|
| 172 |
} catch (error) {
|
|
|
|
| 216 |
// Sync legacy map as best-effort
|
| 217 |
if (!PREDEFINED_USERS[email]) PREDEFINED_USERS[email] = { name: name || '', email, role: role || 'student', ...(displayName ? { displayName } : { displayName: name }) };
|
| 218 |
else {
|
| 219 |
+
if (typeof name === 'string' && name.trim()) PREDEFINED_USERS[email].name = name.trim();
|
| 220 |
+
if (typeof role === 'string' && role.trim()) PREDEFINED_USERS[email].role = role.trim();
|
| 221 |
+
if (typeof displayName === 'string') PREDEFINED_USERS[email].displayName = displayName;
|
| 222 |
}
|
| 223 |
res.json({ success: true, user: updated || PREDEFINED_USERS[email] });
|
| 224 |
} catch (error) {
|
routes/docs.js
CHANGED
|
@@ -98,7 +98,7 @@ router.post('/create', authenticateToken, async (req, res) => {
|
|
| 98 |
if (err && err.code === 11000) {
|
| 99 |
const fallbackId = `${baseDocId}-${wn}-${gn}-${Date.now()}`;
|
| 100 |
const savedManual = await GroupDoc.create({ ...payload, docId: fallbackId });
|
| 101 |
-
|
| 102 |
}
|
| 103 |
throw err;
|
| 104 |
}
|
|
|
|
| 98 |
if (err && err.code === 11000) {
|
| 99 |
const fallbackId = `${baseDocId}-${wn}-${gn}-${Date.now()}`;
|
| 100 |
const savedManual = await GroupDoc.create({ ...payload, docId: fallbackId });
|
| 101 |
+
return res.status(201).json({ success: true, doc: savedManual });
|
| 102 |
}
|
| 103 |
throw err;
|
| 104 |
}
|
routes/refinity.js
CHANGED
|
@@ -9,6 +9,7 @@ const router = express.Router();
|
|
| 9 |
const RefinityTask = require('../models/RefinityTask');
|
| 10 |
const RefinityVersion = require('../models/RefinityVersion');
|
| 11 |
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 8 * 1024 * 1024 } });
|
|
|
|
| 12 |
|
| 13 |
// Parse .docx or .doc (best effort: .doc handled via mammoth may fail depending on content)
|
| 14 |
router.post('/parse', upload.single('file'), async (req, res) => {
|
|
@@ -173,6 +174,33 @@ router.post('/export-plain', async (req, res) => {
|
|
| 173 |
}
|
| 174 |
});
|
| 175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 176 |
// True OOXML revisions (w:ins/w:del) + comments in sidebar
|
| 177 |
router.post('/track-changes-ooxml', async (req, res) => {
|
| 178 |
try {
|
|
@@ -397,6 +425,58 @@ router.put('/versions/:id', async (req, res) => {
|
|
| 397 |
}
|
| 398 |
});
|
| 399 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 400 |
function escapeHtml(str='') {
|
| 401 |
return str
|
| 402 |
.replace(/&/g, '&')
|
|
|
|
| 9 |
const RefinityTask = require('../models/RefinityTask');
|
| 10 |
const RefinityVersion = require('../models/RefinityVersion');
|
| 11 |
const upload = multer({ storage: multer.memoryStorage(), limits: { fileSize: 8 * 1024 * 1024 } });
|
| 12 |
+
const RefinityAnnotation = require('../models/RefinityAnnotation');
|
| 13 |
|
| 14 |
// Parse .docx or .doc (best effort: .doc handled via mammoth may fail depending on content)
|
| 15 |
router.post('/parse', upload.single('file'), async (req, res) => {
|
|
|
|
| 174 |
}
|
| 175 |
});
|
| 176 |
|
| 177 |
+
// Export plain .docx with appended annotations list at the end
|
| 178 |
+
router.post('/export-plain-with-annotations', async (req, res) => {
|
| 179 |
+
try {
|
| 180 |
+
const current = String(req.body?.current || '');
|
| 181 |
+
const annotations = Array.isArray(req.body?.annotations) ? req.body.annotations : [];
|
| 182 |
+
const outName = String(req.body?.filename || 'refinity-with-annotations.docx').replace(/[\/]+/g,'_');
|
| 183 |
+
const paraFromLine = (line) => new Paragraph({ children: [ new TextRun({ text: line }) ] });
|
| 184 |
+
const mainParas = String(current).split(/\r?\n/).map(paraFromLine);
|
| 185 |
+
const sections = [
|
| 186 |
+
...mainParas.length ? mainParas : [ new Paragraph('') ],
|
| 187 |
+
new Paragraph({ children: [ new TextRun({ text: '' }) ] }),
|
| 188 |
+
new Paragraph({ children: [ new TextRun({ text: 'Annotations', bold: true }) ] }),
|
| 189 |
+
];
|
| 190 |
+
annotations.forEach((a, idx) => {
|
| 191 |
+
const label = `${idx+1}. [${String(a.category||'other')}] ${String(a.comment||'')}`;
|
| 192 |
+
sections.push(new Paragraph({ children: [ new TextRun({ text: label }) ] }));
|
| 193 |
+
});
|
| 194 |
+
const doc = new Document({ sections: [ { properties: {}, children: sections } ] });
|
| 195 |
+
const buffer = await Packer.toBuffer(doc);
|
| 196 |
+
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document');
|
| 197 |
+
res.setHeader('Content-Disposition', `attachment; filename="${outName}"`);
|
| 198 |
+
res.send(Buffer.from(buffer));
|
| 199 |
+
} catch (e) {
|
| 200 |
+
res.status(500).json({ error: 'Failed to export document with annotations' });
|
| 201 |
+
}
|
| 202 |
+
});
|
| 203 |
+
|
| 204 |
// True OOXML revisions (w:ins/w:del) + comments in sidebar
|
| 205 |
router.post('/track-changes-ooxml', async (req, res) => {
|
| 206 |
try {
|
|
|
|
| 425 |
}
|
| 426 |
});
|
| 427 |
|
| 428 |
+
// ----- Annotation APIs -----
|
| 429 |
+
router.get('/annotations', async (req, res) => {
|
| 430 |
+
try {
|
| 431 |
+
const versionId = String(req.query?.versionId || '');
|
| 432 |
+
if (!versionId) return res.json([]);
|
| 433 |
+
const rows = await RefinityAnnotation.find({ versionId }).sort({ createdAt: 1 });
|
| 434 |
+
res.json(rows);
|
| 435 |
+
} catch (e) {
|
| 436 |
+
res.status(500).json({ error: 'Failed to load annotations' });
|
| 437 |
+
}
|
| 438 |
+
});
|
| 439 |
+
|
| 440 |
+
router.post('/annotations', async (req, res) => {
|
| 441 |
+
try {
|
| 442 |
+
const { versionId, start, end, category, comment } = req.body || {};
|
| 443 |
+
if (!versionId || start === undefined || end === undefined || !category) {
|
| 444 |
+
return res.status(400).json({ error: 'Missing required fields' });
|
| 445 |
+
}
|
| 446 |
+
const row = await RefinityAnnotation.create({ versionId, start, end, category, comment });
|
| 447 |
+
res.json(row);
|
| 448 |
+
} catch (e) {
|
| 449 |
+
res.status(500).json({ error: 'Failed to create annotation' });
|
| 450 |
+
}
|
| 451 |
+
});
|
| 452 |
+
|
| 453 |
+
router.put('/annotations/:id', async (req, res) => {
|
| 454 |
+
try {
|
| 455 |
+
const { id } = req.params;
|
| 456 |
+
const row = await RefinityAnnotation.findById(id);
|
| 457 |
+
if (!row) return res.status(404).json({ error: 'Annotation not found' });
|
| 458 |
+
const { start, end, category, comment } = req.body || {};
|
| 459 |
+
if (start !== undefined) row.start = Number(start);
|
| 460 |
+
if (end !== undefined) row.end = Number(end);
|
| 461 |
+
if (category !== undefined) row.category = String(category);
|
| 462 |
+
if (comment !== undefined) row.comment = String(comment);
|
| 463 |
+
await row.save();
|
| 464 |
+
res.json(row);
|
| 465 |
+
} catch (e) {
|
| 466 |
+
res.status(500).json({ error: 'Failed to update annotation' });
|
| 467 |
+
}
|
| 468 |
+
});
|
| 469 |
+
|
| 470 |
+
router.delete('/annotations/:id', async (req, res) => {
|
| 471 |
+
try {
|
| 472 |
+
const { id } = req.params;
|
| 473 |
+
await RefinityAnnotation.deleteOne({ _id: id });
|
| 474 |
+
res.json({ ok: true });
|
| 475 |
+
} catch (e) {
|
| 476 |
+
res.status(500).json({ error: 'Failed to delete annotation' });
|
| 477 |
+
}
|
| 478 |
+
});
|
| 479 |
+
|
| 480 |
function escapeHtml(str='') {
|
| 481 |
return str
|
| 482 |
.replace(/&/g, '&')
|