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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
node_modules
dist
.env
*.log
220 changes: 90 additions & 130 deletions README.md
Original file line number Diff line number Diff line change
@@ -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.
1 change: 1 addition & 0 deletions client/.env.example
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
VITE_API_URL=http://localhost:5000/api
2 changes: 2 additions & 0 deletions client/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
<!doctype html>
<html lang="en"><head><meta charset="UTF-8" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Vi Notes</title></head><body class="bg-stone-100 dark:bg-zinc-950"><div id="root"></div><script type="module" src="/src/main.jsx"></script></body></html>
24 changes: 24 additions & 0 deletions client/package.json
Original file line number Diff line number Diff line change
@@ -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"
}
}
6 changes: 6 additions & 0 deletions client/postcss.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
export default {
plugins: {
tailwindcss: {},
autoprefixer: {}
}
};
6 changes: 6 additions & 0 deletions client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -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 <AuthPage onAuth={auth.login} />; return <DashboardPage token={auth.token} onLogout={auth.logout} />; }
export default function App() { return <AuthProvider><InnerApp /></AuthProvider>; }
8 changes: 8 additions & 0 deletions client/src/api/http.js
Original file line number Diff line number Diff line change
@@ -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();
}
4 changes: 4 additions & 0 deletions client/src/components/Sidebar.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
import { formatDistanceToNow } from "./utils";
export function Sidebar({ sessions, selectedId, onSelect, onCreate, tags, activeTag, setActiveTag, q, setQ }) {
return <aside className="w-full border-r border-stone-200 bg-white/70 p-4 backdrop-blur dark:border-zinc-800 dark:bg-zinc-900/50 md:w-80"><button className="mb-4 w-full rounded-xl bg-zinc-900 px-4 py-2 text-sm font-semibold text-white transition hover:opacity-90 dark:bg-emerald-500 dark:text-zinc-900" onClick={onCreate}>New Session</button><input value={q} onChange={(e) => setQ(e.target.value)} placeholder="Search title or content..." className="mb-3 w-full rounded-lg border border-stone-200 bg-white px-3 py-2 text-sm outline-none ring-emerald-400 focus:ring dark:border-zinc-700 dark:bg-zinc-800" /><div className="mb-4 flex flex-wrap gap-2"><button className={`rounded-full px-3 py-1 text-xs ${activeTag === "" ? "bg-emerald-500 text-white" : "bg-stone-200 dark:bg-zinc-700"}`} onClick={() => setActiveTag("")}>All</button>{tags.map((tag) => <button key={tag} className={`rounded-full px-3 py-1 text-xs ${activeTag === tag ? "bg-emerald-500 text-white" : "bg-stone-200 dark:bg-zinc-700"}`} onClick={() => setActiveTag(tag)}>#{tag}</button>)}</div><div className="space-y-2 overflow-y-auto md:max-h-[calc(100vh-240px)]">{sessions.map((s) => <button key={s._id} onClick={() => onSelect(s)} className={`w-full rounded-lg p-3 text-left transition ${selectedId === s._id ? "bg-emerald-100 dark:bg-emerald-900/40" : "bg-stone-100 hover:bg-stone-200 dark:bg-zinc-800 dark:hover:bg-zinc-700"}`}><p className="truncate text-sm font-semibold">{s.title || "Untitled Session"}</p><p className="truncate text-xs text-stone-500 dark:text-zinc-400">{s.wordCount} words � {formatDistanceToNow(s.lastEditedAt)}</p></button>)}</div></aside>;
}
7 changes: 7 additions & 0 deletions client/src/components/StatsBar.jsx
Original file line number Diff line number Diff line change
@@ -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 <div className="mb-4 grid grid-cols-2 gap-3 rounded-xl border border-stone-200 bg-white p-3 text-sm dark:border-zinc-800 dark:bg-zinc-900 md:grid-cols-4"><div><p className="text-xs text-stone-500 dark:text-zinc-400">Word Count</p><p className="font-semibold">{session?.wordCount || 0}</p></div><div><p className="text-xs text-stone-500 dark:text-zinc-400">Session Time</p><p className="font-semibold">{mm}:{ss}</p></div><div><p className="text-xs text-stone-500 dark:text-zinc-400">Start</p><p className="font-semibold">{session?.startTime ? new Date(session.startTime).toLocaleTimeString() : "-"}</p></div><div><p className="text-xs text-stone-500 dark:text-zinc-400">Last Edit</p><p className="font-semibold">{session?.lastEditedAt ? new Date(session.lastEditedAt).toLocaleTimeString() : "-"}</p></div></div>;
}
14 changes: 14 additions & 0 deletions client/src/components/utils.js
Original file line number Diff line number Diff line change
@@ -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);
}
10 changes: 10 additions & 0 deletions client/src/context/AuthContext.jsx
Original file line number Diff line number Diff line change
@@ -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 <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
}
7 changes: 7 additions & 0 deletions client/src/hooks/useAuth.js
Original file line number Diff line number Diff line change
@@ -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;
}
13 changes: 13 additions & 0 deletions client/src/hooks/useDebouncedEffect.js
Original file line number Diff line number Diff line change
@@ -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]);
}
6 changes: 6 additions & 0 deletions client/src/index.css
Original file line number Diff line number Diff line change
@@ -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; }
5 changes: 5 additions & 0 deletions client/src/main.jsx
Original file line number Diff line number Diff line change
@@ -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(<React.StrictMode><App /></React.StrictMode>);
16 changes: 16 additions & 0 deletions client/src/pages/AuthPage.jsx
Original file line number Diff line number Diff line change
@@ -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 <div className="flex min-h-screen items-center justify-center bg-gradient-to-br from-stone-100 via-stone-50 to-emerald-50 p-4 dark:from-zinc-950 dark:to-zinc-900"><form onSubmit={submit} className="w-full max-w-md rounded-2xl border border-stone-200 bg-white p-6 shadow-sm dark:border-zinc-800 dark:bg-zinc-900"><h1 className="mb-1 text-2xl font-bold">Vi Notes</h1><p className="mb-4 text-sm text-stone-500">Private writing sessions with smart autosave.</p>{mode === "signup" && <input className="mb-3 w-full rounded-lg border p-2 dark:border-zinc-700 dark:bg-zinc-800" placeholder="Name" value={form.name} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} />}<input className="mb-3 w-full rounded-lg border p-2 dark:border-zinc-700 dark:bg-zinc-800" placeholder="Email" type="email" value={form.email} onChange={(e) => setForm((p) => ({ ...p, email: e.target.value }))} /><input className="mb-3 w-full rounded-lg border p-2 dark:border-zinc-700 dark:bg-zinc-800" placeholder="Password" type="password" value={form.password} onChange={(e) => setForm((p) => ({ ...p, password: e.target.value }))} />{error && <p className="mb-3 text-sm text-red-500">{error}</p>}<button className="w-full rounded-lg bg-zinc-900 py-2 text-white dark:bg-emerald-500 dark:text-zinc-900">{mode === "login" ? "Login" : "Sign up"}</button><button type="button" className="mt-3 w-full text-sm text-stone-500" onClick={() => setMode(mode === "login" ? "signup" : "login")}>{mode === "login" ? "Need an account? Sign up" : "Already have an account? Login"}</button></form></div>;
}
Loading