diff --git a/.github/workflows/deploy-web.yml b/.github/workflows/deploy-web.yml new file mode 100644 index 0000000..6c9e2a9 --- /dev/null +++ b/.github/workflows/deploy-web.yml @@ -0,0 +1,54 @@ +name: Deploy Web Playground + +on: + push: + branches: [ main ] + paths: + - 'web/**' + - 'attacks/**' + - 'analysis/**' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: pages + cancel-in-progress: false + +jobs: + build: + name: Build web playground + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '20' + cache: 'npm' + cache-dependency-path: web/package-lock.json + - name: Install dependencies + run: npm ci + working-directory: web + - name: Build + run: npm run build + working-directory: web + - name: Upload pages artifact + uses: actions/upload-pages-artifact@v3 + with: + path: web/dist + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-latest + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/web/.gitignore b/web/.gitignore new file mode 100644 index 0000000..5ccae52 --- /dev/null +++ b/web/.gitignore @@ -0,0 +1,18 @@ +# Dependencies +node_modules + +# Build output +dist + +# Generated at build time by vite.config.ts +public/pg_modules + +# Editor directories and files +.vscode/* +!.vscode/extensions.json +*.swp +*.swo +*~ + +# OS files +.DS_Store diff --git a/web/index.html b/web/index.html new file mode 100644 index 0000000..e0f738b --- /dev/null +++ b/web/index.html @@ -0,0 +1,12 @@ + + + + + + PrivacyGuard Playground + + +
+ + + diff --git a/web/package.json b/web/package.json new file mode 100644 index 0000000..86055e6 --- /dev/null +++ b/web/package.json @@ -0,0 +1,34 @@ +{ + "name": "privacyguard-web", + "private": true, + "version": "0.0.1", + "type": "module", + "scripts": { + "dev": "vite", + "build": "tsc -b && vite build", + "lint": "eslint .", + "preview": "vite preview" + }, + "dependencies": { + "plotly.js-dist-min": "^2.35.0", + "react": "^19.0.0", + "react-dom": "^19.0.0", + "react-plotly.js": "^2.6.0", + "react-router-dom": "^7.1.0" + }, + "devDependencies": { + "@eslint/js": "^9.17.0", + "@types/react": "^19.0.0", + "@types/react-dom": "^19.0.0", + "@types/react-plotly.js": "^2.6.0", + "@vitejs/plugin-react": "^4.3.0", + "eslint": "^9.17.0", + "eslint-plugin-react-hooks": "^5.0.0", + "eslint-plugin-react-refresh": "^0.4.16", + "globals": "^15.14.0", + "@types/node": "^22.0.0", + "typescript": "~5.7.0", + "typescript-eslint": "^8.18.2", + "vite": "^6.0.0" + } +} diff --git a/web/src/App.tsx b/web/src/App.tsx new file mode 100644 index 0000000..8f6ef82 --- /dev/null +++ b/web/src/App.tsx @@ -0,0 +1,34 @@ +import { BrowserRouter, Routes, Route, NavLink } from "react-router-dom"; +import Landing from "./tabs/Landing"; +import MIATab from "./tabs/MIATab"; +import LIATab from "./tabs/LIATab"; +import TextSimilarityTab from "./tabs/TextSimilarityTab"; +import CodeSimilarityTab from "./tabs/CodeSimilarityTab"; +import FDPCalculatorTab from "./tabs/FDPCalculatorTab"; + +function App() { + return ( + + +
+ + } /> + } /> + } /> + } /> + } /> + } /> + +
+
+ ); +} + +export default App; diff --git a/web/src/components/DataInput.tsx b/web/src/components/DataInput.tsx new file mode 100644 index 0000000..cd95a70 --- /dev/null +++ b/web/src/components/DataInput.tsx @@ -0,0 +1,119 @@ +import { useState, useRef } from "react"; + +interface DataInputProps { + format: "csv" | "jsonl"; + requiredColumns: string[]; + placeholder: string; + onData: (data: string) => void; +} + +function DataInput({ format, requiredColumns, placeholder, onData }: DataInputProps) { + const [text, setText] = useState(""); + const [validationError, setValidationError] = useState(null); + const fileInputRef = useRef(null); + + const validate = (data: string): string | null => { + const trimmed = data.trim(); + if (!trimmed) { + return "No data provided."; + } + + if (format === "csv") { + const firstLine = trimmed.split("\n")[0]; + const headers = firstLine.split(",").map((h) => h.trim()); + const missing = requiredColumns.filter((col) => !headers.includes(col)); + if (missing.length > 0) { + return `CSV is missing required column(s): ${missing.join(", ")}. Found headers: ${headers.join(", ")}`; + } + } else { + // jsonl — validate the first line + const firstLine = trimmed.split("\n")[0]; + try { + const obj = JSON.parse(firstLine); + const keys = Object.keys(obj); + const missing = requiredColumns.filter((col) => !keys.includes(col)); + if (missing.length > 0) { + return `JSONL first line is missing required field(s): ${missing.join(", ")}. Found fields: ${keys.join(", ")}`; + } + } catch { + return "First line is not valid JSON. Expected JSONL format (one JSON object per line)."; + } + } + + return null; + }; + + const handleLoad = () => { + const err = validate(text); + if (err) { + setValidationError(err); + return; + } + setValidationError(null); + onData(text.trim()); + }; + + const handleFileUpload = (e: React.ChangeEvent) => { + const file = e.target.files?.[0]; + if (!file) return; + + const reader = new FileReader(); + reader.onload = (event) => { + const content = event.target?.result; + if (typeof content === "string") { + setText(content); + setValidationError(null); + } + }; + reader.readAsText(file); + + // Reset file input so the same file can be re-selected + if (fileInputRef.current) { + fileInputRef.current.value = ""; + } + }; + + return ( +
+
+ + + + Required {format === "csv" ? "columns" : "fields"}:{" "} + {requiredColumns.join(", ")} + +
+ +