From 8b910f36784b5fd32b6ebc1c474d01108bf4e325 Mon Sep 17 00:00:00 2001 From: Ayush Patel Date: Sun, 3 May 2026 19:26:51 +0530 Subject: [PATCH] Add local project changes --- .gitignore | 4 + README.md | 220 +- client/.env.example | 1 + client/index.html | 2 + client/package.json | 24 + client/postcss.config.js | 6 + client/src/App.jsx | 6 + client/src/api/http.js | 8 + client/src/components/Sidebar.jsx | 4 + client/src/components/StatsBar.jsx | 7 + client/src/components/utils.js | 14 + client/src/context/AuthContext.jsx | 10 + client/src/hooks/useAuth.js | 7 + client/src/hooks/useDebouncedEffect.js | 13 + client/src/index.css | 6 + client/src/main.jsx | 5 + client/src/pages/AuthPage.jsx | 16 + client/src/pages/DashboardPage.jsx | 21 + client/tailwind.config.js | 12 + client/vite.config.js | 3 + package-lock.json | 4892 +++++++++++++++++++ package.json | 14 + server/.env.example | 5 + server/package.json | 22 + server/src/config/db.js | 20 + server/src/controllers/authController.js | 5 + server/src/controllers/sessionController.js | 9 + server/src/index.js | 32 + server/src/middleware/auth.js | 2 + server/src/models/Session.js | 4 + server/src/models/User.js | 3 + server/src/routes/authRoutes.js | 5 + server/src/routes/sessionRoutes.js | 6 + server/src/utils/token.js | 2 + 34 files changed, 5280 insertions(+), 130 deletions(-) create mode 100644 .gitignore create mode 100644 client/.env.example create mode 100644 client/index.html create mode 100644 client/package.json create mode 100644 client/postcss.config.js create mode 100644 client/src/App.jsx create mode 100644 client/src/api/http.js create mode 100644 client/src/components/Sidebar.jsx create mode 100644 client/src/components/StatsBar.jsx create mode 100644 client/src/components/utils.js create mode 100644 client/src/context/AuthContext.jsx create mode 100644 client/src/hooks/useAuth.js create mode 100644 client/src/hooks/useDebouncedEffect.js create mode 100644 client/src/index.css create mode 100644 client/src/main.jsx create mode 100644 client/src/pages/AuthPage.jsx create mode 100644 client/src/pages/DashboardPage.jsx create mode 100644 client/tailwind.config.js create mode 100644 client/vite.config.js create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server/.env.example create mode 100644 server/package.json create mode 100644 server/src/config/db.js create mode 100644 server/src/controllers/authController.js create mode 100644 server/src/controllers/sessionController.js create mode 100644 server/src/index.js create mode 100644 server/src/middleware/auth.js create mode 100644 server/src/models/Session.js create mode 100644 server/src/models/User.js create mode 100644 server/src/routes/authRoutes.js create mode 100644 server/src/routes/sessionRoutes.js create mode 100644 server/src/utils/token.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 000000000..3b24e4a13 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +node_modules +dist +.env +*.log diff --git a/README.md b/README.md index 55650d25c..f7ae37bf8 100644 --- a/README.md +++ b/README.md @@ -1,130 +1,90 @@ -# Vi-Notes - -**Vi-Notes** is an authenticity verification platform designed to distinguish genuine human-written content from AI-generated or AI-assisted text. The system focuses on analyzing **writing behavior** alongside **statistical and linguistic characteristics** of the text to establish reliable authorship verification. - -This repository represents the **design and conceptual foundation** for the Vi-Notes system. - ---- - -## Motivation - -With the widespread availability of AI writing tools, verifying true human authorship has become increasingly challenging. Most existing detection methods rely primarily on textual analysis, which can be inconsistent and easy to bypass. - -Vi-Notes approaches this problem by combining: -- Behavioral signals from the writing process -- Statistical analysis of the written content -- Correlation between how content is written and what is written - ---- - -## Core Idea - -Human writing naturally includes: -- Variable typing speeds -- Pauses during thinking -- Revisions during idea formation -- Irregular sentence structures -- A relationship between content complexity and editing frequency - -AI-generated or pasted text often lacks these behavioral signatures. - -Vi-Notes is designed to capture and analyze these characteristics to assess authorship authenticity. - ---- - -## Key Features - -### Writing Session Monitoring -- Capture keystroke timing metadata (not raw key content) -- Track pauses, deletions, edits, and writing flow -- Detect pasted or externally inserted text blocks - -### Behavioral Pattern Analysis -- Pause distribution before sentences and paragraphs -- Typing speed variance -- Revision frequency relative to text complexity -- Micro-pauses around punctuation and structural boundaries - -### Textual Statistical Analysis -- Sentence length variation -- Vocabulary diversity metrics -- Stylistic consistency analysis -- Linguistic irregularities typical of human writing - -### Cross-Verification Engine -- Correlate keyboard behavior with text evolution -- Identify mismatches between behavioral data and content -- Flag suspicious uniformity patterns - -### Authenticity Reports -- Confidence score for human authorship -- Highlighted suspicious segments -- Supporting behavioral and textual indicators -- Shareable verification summaries - ---- - -## Tech Stack (MERN Architecture) - -### Frontend -- React -- TypeScript -- Electron for desktop-level keyboard event access - -### Backend -- Node.js -- Express.js -- RESTful APIs for session handling and analysis - -### Database -- MongoDB -- Encrypted storage for writing sessions, keystroke metadata, and reports - -### Machine Learning -- TensorFlow / PyTorch -- Supervised learning for human vs AI-assisted writing -- Unsupervised anomaly detection -- NLP-based statistical signature analysis - ---- - -## Privacy & Ethics - -Vi-Notes is designed with privacy-first principles: - -- No storage of raw keystroke content -- Only timing, frequency, and structural metadata is collected -- Encrypted data storage -- User-controlled session tracking -- Monitoring limited strictly to active writing sessions - ---- - -## Project Goals - -- Restore trust in written content authenticity -- Differentiate between human-written, AI-assisted, and AI-generated text -- Adapt detection methods as AI writing tools evolve -- Maintain ethical, transparent, and privacy-conscious verification - ---- - -## Repository Scope - -This repository currently serves as: -- A design reference -- A research and experimentation space -- A foundation for future MERN-based implementation - ---- - -## Contributing - -Contributions are welcome, especially for **feature requests and their implementation**. -If you are interested in working on an existing feature request or proposing a new one, please open or comment on an issue to start the discussion. - ---- - -## License - -This project is licensed under the MIT License. +# Vi Notes + +Vi Notes is a full-stack writing-session manager with autosave, persistent DB storage, search/filter, tags, and private user notes. + +## Stack +- Frontend: React + Vite + Tailwind CSS +- Backend: Node.js + Express +- DB: MongoDB (Mongoose) +- Auth: JWT + bcrypt + +## Project Structure +- `client/` frontend app +- `server/` backend API +- `server/src/models` database models +- `client/src/components` reusable UI components +- `client/src/pages` app pages + +## Setup +1. Install dependencies: +```bash +npm install +npm install -w server +npm install -w client +``` +2. Configure env files: +- Copy `server/.env.example` to `server/.env` +- Copy `client/.env.example` to `client/.env` +3. Start MongoDB (local or Atlas URI in `server/.env`) +4. Run: +```bash +npm run dev +``` +- Client: `http://localhost:5173` +- Server: `http://localhost:5000` + +## Environment Variables +Server (`server/.env`): +- `NODE_ENV` = `development` +- `PORT` = `5000` +- `MONGODB_URI` = MongoDB connection string +- `JWT_SECRET` = JWT signing secret +- `CLIENT_ORIGIN` = frontend URL + +Client (`client/.env`): +- `VITE_API_URL` = `http://localhost:5000/api` + +## Data Schema +`Session` +- `_id` +- `userId` (ObjectId ref `User`) +- `title` (string) +- `content` (string) +- `wordCount` (number) +- `startTime` (date) +- `endTime` (date|null) +- `duration` (seconds) +- `tags` (string[]) +- `lastEditedAt` (date) +- `createdAt` (date) +- `updatedAt` (date) + +## API Endpoints +Base URL: `/api` + +Auth: +- `POST /auth/signup` + - body: `{ name, email, password }` +- `POST /auth/login` + - body: `{ email, password }` + +Sessions (Bearer token required): +- `GET /sessions?q=&tag=&from=&to=` list with search + filters +- `GET /sessions/tags` list tags +- `POST /sessions` create session +- `GET /sessions/:id` get one +- `PUT /sessions/:id` update (used by autosave) +- `DELETE /sessions/:id` delete + +Health: +- `GET /health` + +## Autosave Logic +- Debounced autosave on typing stop (~1.2s) +- Timed autosave every 8s +- Manual Save button +- Live word count while typing +- Session duration recalculated server-side on update + +## Bonus Notes +- Export PDF/Markdown, streaks, goals, and offline sync can be added next as incremental modules. diff --git a/client/.env.example b/client/.env.example new file mode 100644 index 000000000..377b2c0e0 --- /dev/null +++ b/client/.env.example @@ -0,0 +1 @@ +VITE_API_URL=http://localhost:5000/api diff --git a/client/index.html b/client/index.html new file mode 100644 index 000000000..d106996f7 --- /dev/null +++ b/client/index.html @@ -0,0 +1,2 @@ + +Vi Notes
diff --git a/client/package.json b/client/package.json new file mode 100644 index 000000000..56cbf5396 --- /dev/null +++ b/client/package.json @@ -0,0 +1,24 @@ +{ + "name": "vi-notes-client", + "private": true, + "version": "1.0.0", + "type": "module", + "scripts": { + "dev": "vite", + "build": "vite build", + "preview": "vite preview" + }, + "dependencies": { + "react": "^18.3.1", + "react-dom": "^18.3.1" + }, + "devDependencies": { + "@types/react": "^18.3.17", + "@types/react-dom": "^18.3.5", + "@vitejs/plugin-react": "^4.7.0", + "autoprefixer": "^10.4.21", + "postcss": "^8.5.6", + "tailwindcss": "^3.4.17", + "vite": "^5.4.19" + } +} diff --git a/client/postcss.config.js b/client/postcss.config.js new file mode 100644 index 000000000..ba8073047 --- /dev/null +++ b/client/postcss.config.js @@ -0,0 +1,6 @@ +export default { + plugins: { + tailwindcss: {}, + autoprefixer: {} + } +}; diff --git a/client/src/App.jsx b/client/src/App.jsx new file mode 100644 index 000000000..efd187542 --- /dev/null +++ b/client/src/App.jsx @@ -0,0 +1,6 @@ +import { AuthProvider } from "./context/AuthContext"; +import { useAuth } from "./hooks/useAuth"; +import { AuthPage } from "./pages/AuthPage"; +import { DashboardPage } from "./pages/DashboardPage"; +function InnerApp() { const auth = useAuth(); if (!auth.token) return ; return ; } +export default function App() { return ; } diff --git a/client/src/api/http.js b/client/src/api/http.js new file mode 100644 index 000000000..23a150aff --- /dev/null +++ b/client/src/api/http.js @@ -0,0 +1,8 @@ +const API_URL = import.meta.env.VITE_API_URL || "http://localhost:5000/api"; +function getHeaders(token) { return { "Content-Type": "application/json", ...(token ? { Authorization: `Bearer ${token}` } : {}) }; } +export async function apiRequest(path, options = {}, token) { + const response = await fetch(`${API_URL}${path}`, { ...options, headers: { ...getHeaders(token), ...(options.headers || {}) } }); + if (!response.ok) { const err = await response.json().catch(() => ({})); throw new Error(err.message || "Request failed"); } + if (response.status === 204) return null; + return response.json(); +} diff --git a/client/src/components/Sidebar.jsx b/client/src/components/Sidebar.jsx new file mode 100644 index 000000000..c05f070d7 --- /dev/null +++ b/client/src/components/Sidebar.jsx @@ -0,0 +1,4 @@ +import { formatDistanceToNow } from "./utils"; +export function Sidebar({ sessions, selectedId, onSelect, onCreate, tags, activeTag, setActiveTag, q, setQ }) { + return ; +} diff --git a/client/src/components/StatsBar.jsx b/client/src/components/StatsBar.jsx new file mode 100644 index 000000000..a6c8491b8 --- /dev/null +++ b/client/src/components/StatsBar.jsx @@ -0,0 +1,7 @@ +export function StatsBar({ session }) { + const now = Date.now(); + const durationSec = session?.startTime ? Math.max(0, Math.floor((now - new Date(session.startTime).getTime()) / 1000)) : 0; + const mm = String(Math.floor(durationSec / 60)).padStart(2, "0"); + const ss = String(durationSec % 60).padStart(2, "0"); + return

Word Count

{session?.wordCount || 0}

Session Time

{mm}:{ss}

Start

{session?.startTime ? new Date(session.startTime).toLocaleTimeString() : "-"}

Last Edit

{session?.lastEditedAt ? new Date(session.lastEditedAt).toLocaleTimeString() : "-"}

; +} diff --git a/client/src/components/utils.js b/client/src/components/utils.js new file mode 100644 index 000000000..e4a130768 --- /dev/null +++ b/client/src/components/utils.js @@ -0,0 +1,14 @@ +export function formatDistanceToNow(date) { + const d = new Date(date).getTime(); + const diffSec = Math.floor((Date.now() - d) / 1000); + if (diffSec < 60) return "just now"; + const diffMin = Math.floor(diffSec / 60); + if (diffMin < 60) return `${diffMin}m ago`; + const diffHr = Math.floor(diffMin / 60); + if (diffHr < 24) return `${diffHr}h ago`; + return `${Math.floor(diffHr / 24)}d ago`; +} + +export function parseTags(value) { + return value.split(",").map((v) => v.trim()).filter(Boolean); +} diff --git a/client/src/context/AuthContext.jsx b/client/src/context/AuthContext.jsx new file mode 100644 index 000000000..f04443015 --- /dev/null +++ b/client/src/context/AuthContext.jsx @@ -0,0 +1,10 @@ +import { createContext, useEffect, useMemo, useState } from "react"; +export const AuthContext = createContext(null); +export function AuthProvider({ children }) { + const [token, setToken] = useState(localStorage.getItem("vi_token")); + const [user, setUser] = useState(() => { const raw = localStorage.getItem("vi_user"); return raw ? JSON.parse(raw) : null; }); + useEffect(() => { if (token) localStorage.setItem("vi_token", token); else localStorage.removeItem("vi_token"); }, [token]); + useEffect(() => { if (user) localStorage.setItem("vi_user", JSON.stringify(user)); else localStorage.removeItem("vi_user"); }, [user]); + const value = useMemo(() => ({ token, user, login: (payload) => { setToken(payload.token); setUser(payload.user); }, logout: () => { setToken(null); setUser(null); } }), [token, user]); + return {children}; +} diff --git a/client/src/hooks/useAuth.js b/client/src/hooks/useAuth.js new file mode 100644 index 000000000..6b3ab405a --- /dev/null +++ b/client/src/hooks/useAuth.js @@ -0,0 +1,7 @@ +import { useContext } from "react"; +import { AuthContext } from "../context/AuthContext"; +export function useAuth() { + const ctx = useContext(AuthContext); + if (!ctx) throw new Error("useAuth must be used inside AuthProvider"); + return ctx; +} diff --git a/client/src/hooks/useDebouncedEffect.js b/client/src/hooks/useDebouncedEffect.js new file mode 100644 index 000000000..a5d979aed --- /dev/null +++ b/client/src/hooks/useDebouncedEffect.js @@ -0,0 +1,13 @@ +import { useEffect, useRef } from "react"; +export function useDebouncedEffect(effect, deps, delay) { + const cleanupRef = useRef(); + useEffect(() => { + const timer = setTimeout(() => { + cleanupRef.current = effect(); + }, delay); + return () => { + clearTimeout(timer); + if (typeof cleanupRef.current === "function") cleanupRef.current(); + }; + }, [...deps, delay]); +} diff --git a/client/src/index.css b/client/src/index.css new file mode 100644 index 000000000..df8f6bf5e --- /dev/null +++ b/client/src/index.css @@ -0,0 +1,6 @@ +@import url("https://fonts.googleapis.com/css2?family=Manrope:wght@400;500;600;700&display=swap"); +@tailwind base; +@tailwind components; +@tailwind utilities; +:root { color-scheme: light; } +.dark { color-scheme: dark; } diff --git a/client/src/main.jsx b/client/src/main.jsx new file mode 100644 index 000000000..7a0a54d80 --- /dev/null +++ b/client/src/main.jsx @@ -0,0 +1,5 @@ +import React from "react"; +import ReactDOM from "react-dom/client"; +import App from "./App"; +import "./index.css"; +ReactDOM.createRoot(document.getElementById("root")).render(); diff --git a/client/src/pages/AuthPage.jsx b/client/src/pages/AuthPage.jsx new file mode 100644 index 000000000..8cec9931b --- /dev/null +++ b/client/src/pages/AuthPage.jsx @@ -0,0 +1,16 @@ +import { useState } from "react"; +import { apiRequest } from "../api/http"; +export function AuthPage({ onAuth }) { + const [mode, setMode] = useState("login"); + const [form, setForm] = useState({ name: "", email: "", password: "" }); + const [error, setError] = useState(""); + async function submit(e) { + e.preventDefault(); + setError(""); + try { + const data = await apiRequest(`/auth/${mode === "login" ? "login" : "signup"}`, { method: "POST", body: JSON.stringify(form) }); + onAuth(data); + } catch (err) { setError(err.message); } + } + return

Vi Notes

Private writing sessions with smart autosave.

{mode === "signup" && setForm((p) => ({ ...p, name: e.target.value }))} />} setForm((p) => ({ ...p, email: e.target.value }))} /> setForm((p) => ({ ...p, password: e.target.value }))} />{error &&

{error}

}
; +} diff --git a/client/src/pages/DashboardPage.jsx b/client/src/pages/DashboardPage.jsx new file mode 100644 index 000000000..762de33a8 --- /dev/null +++ b/client/src/pages/DashboardPage.jsx @@ -0,0 +1,21 @@ +import { useEffect, useMemo, useState } from "react"; +import { apiRequest } from "../api/http"; +import { Sidebar } from "../components/Sidebar"; +import { StatsBar } from "../components/StatsBar"; +import { parseTags } from "../components/utils"; +import { useDebouncedEffect } from "../hooks/useDebouncedEffect"; +const words = (text = "") => (text.trim() ? text.trim().split(/\s+/).length : 0); +export function DashboardPage({ token, onLogout }) { + const [sessions, setSessions] = useState([]); const [selected, setSelected] = useState(null); const [q, setQ] = useState(""); const [activeTag, setActiveTag] = useState(""); const [dateFrom, setDateFrom] = useState(""); const [dateTo, setDateTo] = useState(""); const [dark, setDark] = useState(localStorage.getItem("vi_theme") === "dark"); + useEffect(() => { document.documentElement.classList.toggle("dark", dark); localStorage.setItem("vi_theme", dark ? "dark" : "light"); }, [dark]); + async function loadSessions() { const params = new URLSearchParams(); if (q) params.set("q", q); if (activeTag) params.set("tag", activeTag); if (dateFrom) params.set("from", dateFrom); if (dateTo) params.set("to", dateTo); const data = await apiRequest(`/sessions?${params.toString()}`, {}, token); setSessions(data); if (!selected && data.length) setSelected(data[0]); if (selected) { const fresh = data.find((s) => s._id === selected._id); if (fresh) setSelected(fresh); } } + useEffect(() => { loadSessions().catch(console.error); }, [activeTag, dateFrom, dateTo]); + useDebouncedEffect(() => { loadSessions().catch(console.error); }, [q], 300); + const tags = useMemo(() => { const tagSet = new Set(); sessions.forEach((s) => s.tags?.forEach((t) => tagSet.add(t))); return [...tagSet].sort((a, b) => a.localeCompare(b)); }, [sessions]); + async function createSession() { const created = await apiRequest("/sessions", { method: "POST", body: JSON.stringify({ title: "New Writing Session", content: "", startTime: new Date().toISOString(), tags: [] }) }, token); setSessions((prev) => [created, ...prev]); setSelected(created); } + async function saveSession(sessionData) { const updated = await apiRequest(`/sessions/${sessionData._id}`, { method: "PUT", body: JSON.stringify({ ...sessionData, wordCount: words(sessionData.content) }) }, token); setSessions((prev) => prev.map((s) => (s._id === updated._id ? updated : s))); setSelected(updated); } + useDebouncedEffect(() => { if (!selected?._id) return; saveSession(selected).catch(console.error); }, [selected?.title, selected?.content, selected?.tags?.join(",")], 1200); + useEffect(() => { if (!selected?._id) return; const timer = setInterval(() => { saveSession({ ...selected, endTime: new Date().toISOString() }).catch(console.error); }, 8000); return () => clearInterval(timer); }, [selected]); + async function removeSession(id) { await apiRequest(`/sessions/${id}`, { method: "DELETE" }, token); const next = sessions.filter((s) => s._id !== id); setSessions(next); setSelected(next[0] || null); } + return
setDateFrom(e.target.value)} className="rounded border px-2 py-1 dark:border-zinc-700 dark:bg-zinc-900" /> setDateTo(e.target.value)} className="rounded border px-2 py-1 dark:border-zinc-700 dark:bg-zinc-900" />
{selected ?
setSelected((prev) => ({ ...prev, title: e.target.value }))} className="mb-3 w-full border-b border-stone-200 bg-transparent py-2 text-2xl font-semibold outline-none dark:border-zinc-700" /> setSelected((prev) => ({ ...prev, tags: parseTags(e.target.value) }))} className="mb-3 w-full rounded-lg border border-stone-200 bg-transparent px-3 py-2 text-sm outline-none dark:border-zinc-700" placeholder="tags, comma separated" />