Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
vi-notes-backend/node_modules
vi-notes-frontend/node_modules
232 changes: 232 additions & 0 deletions vi-notes-backend/app.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,232 @@
const express = require('express');
const app = express();
const cors = require('cors');
const mongoose = require('mongoose');
const puppeteer = require('puppeteer');
const handlebars = require('handlebars');
const QRCode = require('qrcode');
const fs = require('fs');
const path = require('path');

const Reports = require('./models/reports')
const port = 3000;

app.use(express.json());
app.use(express.urlencoded({ extended: true }));

app.use(cors({
origin: 'http://localhost:5173'
}));


const mongoURI = 'mongodb://127.0.0.1:27017/vi-notes';

mongoose.connect(mongoURI)
.then(() => console.log('Connected to MongoDB successfully!'))
.catch((err) => console.error('MongoDB connection error:', err));


app.get('/results/:id', async (req, res) => {
sessID = req.params.id;
try{
const searchedReport = await Reports.findOne({ sessionID: sessID });
console.log(searchedReport);
if (!searchedReport) {
return res.status(404).send("Report not found!");
}
res.json({
sessionID: sessID,
name: searchedReport.name,
date: searchedReport.date,
content: searchedReport.content,
overallScore: searchedReport.overallScore,
pasteToTypeRatio: searchedReport.pasteToTypeRatio,
revisionBackspaceIndex: searchedReport.revisionBackspaceIndex,
interKeystrokeTiming: searchedReport.interKeystrokeTiming,
pauseAnalysis: searchedReport.pauseAnalysis,
totalActiveTime: searchedReport.totalActiveTime,
documentLength: searchedReport.documentLength,
perplexityScore: searchedReport.perplexityScore,
burstinessScore: searchedReport.burstinessScore
});
} catch (error) {
res.status(500).send("Server Error");
}
});

app.get('/download/:id', async (req, res) => {
sessID = req.params.id;
let report;
try{
const searchedReport = await Reports.findOne({ sessionID: sessID });
console.log(searchedReport);
if (!searchedReport) {
return res.status(404).send("Report not found!");
}
report = searchedReport.toJSON();
} catch (error) {
return res.status(500).send("Server Error");
};
console.log(JSON.stringify(report));
try{
const qrString = `http://localhost:5173/verify/${sessID}`;

const qrDataURI = await QRCode.toDataURL(qrString);

const data = {
qrCodeImgSrc: qrDataURI,
report : report
}

const templateHtml = fs.readFileSync(path.join(__dirname, 'report_template.html'), 'utf8');

const template = handlebars.compile(templateHtml);
const finalHtml = template(data);

const browser = await puppeteer.launch();
const page = await browser.newPage();

await page.setContent(finalHtml, { waitUntil: 'networkidle0' });

const pdfBuffer = await page.pdf({
format: 'A4',
printBackground: true,
margin: { top: '20px', right: '20px', bottom: '20px', left: '20px'}
});
await browser.close();

res.set({
'Content-Type': 'application/pdf',
'Content-Disposition': `attachment; filename="Report-${sessID}.pdf"`,
'Content-Length': pdfBuffer.length
});
res.send(pdfBuffer);
} catch(error) {
console.error("Error generating PDF:", error);
res.status(500).send("Error generating PDF");
}

});
app.post('/save', async (req, res) => {
const userData = req.body;
console.log("Got save request from : " + userData.username);
const prompt = `
You are an advanced Behavioral Biometrics and Forensics Scrutinizer. Your primary objective is to analyze typing dynamics and text content to determine whether a given piece of text was organically typed by a human, generated by an AI, or pasted/typed by an automated macro script.

You will receive a JSON payload containing:

'content': The final text output.

'typingLogs': An array of keystroke objects, detailing 'theKey' pressed, 'timeItHappened' (epoch timestamp), and 'speedFromLastOne' (milliseconds since the last keypress).

Input :
'content' : ${userData.content}
'typingLogs' : ${JSON.stringify(userData.typingLogs)}

YOUR ANALYSIS DIRECTIVES
Evaluate the data against these heuristics to generate 0-100 scores (where 100 is highly human, and 0 is highly robotic/AI):

Copy/Paste Detection ('pasteToTypeRatio')

If a sequence of characters has a 'speedFromLastOne' of 0-5ms continuously, or multiple characters appear in a single log, this is a paste event. Map this to a 0-100 scale (100 = 100% pasted).

Robotic Cadence vs. Human Burstiness ('interKeystrokeTiming')

Flat, perfectly constrained typing speeds (e.g., exactly 50ms) indicate a bot (Score closer to 0). High variance in 'speedFromLastOne' strongly indicates a human (Score closer to 100).

The Revision Index ('revisionBackspaceIndex')

The presence of 'Backspace', 'Delete', and immediate corrections strongly correlates with human organic typing (Score closer to 100). Zero backspaces over a long text is suspicious (Score closer to 0).

Contextual Think Time ('pauseAnalysis')

Humans pause longer at specific structural points: after hitting 'Space', before complex words, and after punctuation. Natural pausing yields a higher score.

Content Analysis ('perplexityScore' & 'burstinessScore')

Evaluate the text string. Highly predictable, uniform text (AI) gets lower scores. Creative, varied sentence structures get higher scores.

REQUIRED OUTPUT FORMAT
You must output YOUR ENTIRE RESPONSE as a single, valid, stringified JSON object exactly matching the keys below. Calculate the 'totalActiveTime' and 'documentLength' directly from the provided data.

Do NOT wrap the output in markdown code blocks (e.g., no '''json). Do NOT include any conversational text. Output ONLY the JSON.
Do not provide any html code or something else. Only provide the json string even without '''json code blocks.
{
"overallScore": <0-100 integer: Your final weighted confidence that this is human-written>,
"pasteToTypeRatio": <0-100 integer: Estimated percentage of the text that was pasted>,
"revisionBackspaceIndex": <0-100 integer: 100 = heavy human-like revisions, 0 = flawless/scripted>,
"interKeystrokeTiming": <0-100 integer: 100 = highly varied, organic cadence. 0 = flat, robotic timing>,
"pauseAnalysis": <0-100 integer: 100 = natural human pauses at punctuation/words. 0 = machine-like continuous stream>,
"totalActiveTime": <integer: Total typing time in milliseconds (Addition of every time in typing logs (because it is difference between each keypresses.))>,
"documentLength": <integer: Total word count of the 'content' string>,
"perplexityScore": <0-100 integer: 100 = highly unpredictable human vocabulary. 0 = highly predictable AI vocabulary>,
"burstinessScore": <0-100 integer: 100 = high variance in sentence length. 0 = monotonous sentence structure>
}
`

const apiKey = "AIzaSyCccP3hkz7xqwayJjXFTK0FjH2fESf7fAQ";

const url = "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash-preview:generateContent?key=" + apiKey;

let resultsJSON;

try {
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
contents: [{ parts: [{ text: prompt }] }]
})
});

const data = await response.json();

if (!response.ok) {
return res.status(response.status).json(data);
}
if (!data.candidates || data.candidates.length === 0) {
return res.status(500).json({
error: "Gemini returned no content. It may have been blocked by safety filters.",
raw_data: data
});
}
let aiResponse = data.candidates[0].content.parts[0].text;

aiResponse = aiResponse.replace(/```json/gi, '').replace(/```/g, '').trim();

resultsJSON = JSON.parse(aiResponse);
console.log("Output of Gemini : ", resultsJSON);

} catch (error) {
console.error("Gemini/Parsing Error:", error);
return res.status(500).json({ error: 'AI Processing failed', details: error.message });
}

const report = new Reports({
name: userData.username || "Anonymous",
content: userData.content,
overallScore: resultsJSON.overallScore,
pasteToTypeRatio: resultsJSON.pasteToTypeRatio,
revisionBackspaceIndex: resultsJSON.revisionBackspaceIndex,
interKeystrokeTiming: resultsJSON.interKeystrokeTiming,
pauseAnalysis: resultsJSON.pauseAnalysis,
totalActiveTime: resultsJSON.totalActiveTime,
documentLength: resultsJSON.documentLength,
perplexityScore: resultsJSON.perplexityScore,
burstinessScore: resultsJSON.burstinessScore
});

try {
const newreport = await report.save();
res.status(200).json({
sessionID: newreport.sessionID
});
} catch (err) {
console.error("Mongoose Validation Error:", err);
res.status(400).json({ message: err.message });
}
});

app.listen(port, () => {
console.log(`Server running at http://localhost:${port}`);
});
35 changes: 35 additions & 0 deletions vi-notes-backend/models/reports.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
const mongoose = require('mongoose');
const crypto = require('crypto');

const reportSchema = new mongoose.Schema({
sessionID: {
type: String,
default: () => crypto.randomUUID(),
unique: true,

index: true
},
name: {
type: String,
required: true
},
date: {
type: Date,
default: Date.now
},
content: {
type:String,
required: true
},
overallScore: Number,
pasteToTypeRatio: Number,
revisionBackspaceIndex: Number,
interKeystrokeTiming: Number,
pauseAnalysis: Number,
totalActiveTime: Number,
documentLength: Number,
perplexityScore: Number,
burstinessScore: Number
});

module.exports = mongoose.model('Report', reportSchema);
Loading