gni commited on
Commit ·
fbf73be
1
Parent(s): 6f25cc6
Docs: Update README with Pro features and high-volume test instructions.
Browse files- README.md +73 -40
- ui/src/App.tsx +138 -127
README.md
CHANGED
|
@@ -1,61 +1,94 @@
|
|
| 1 |
-
#
|
| 2 |
|
| 3 |
-
|
| 4 |
|
| 5 |
-
##
|
| 6 |
|
| 7 |
-
|
| 8 |
-
|
| 9 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 10 |
|
| 11 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 12 |
|
| 13 |
-
### 1. Start the API
|
| 14 |
-
In one terminal:
|
| 15 |
```bash
|
| 16 |
-
|
| 17 |
-
source venv/bin/activate
|
| 18 |
-
python3 main.py
|
| 19 |
```
|
| 20 |
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
```bash
|
| 24 |
-
|
| 25 |
-
source ../api/venv/bin/activate
|
| 26 |
-
# Redact a string
|
| 27 |
-
python3 pii_mod.py redact "Contact me at 212-555-0199 or email me at alice@example.com"
|
| 28 |
```
|
| 29 |
|
| 30 |
-
|
| 31 |
-
In a third terminal:
|
| 32 |
```bash
|
| 33 |
-
|
| 34 |
-
npm run dev
|
| 35 |
```
|
| 36 |
-
Open `http://localhost:5173` in your browser.
|
| 37 |
|
| 38 |
-
|
| 39 |
|
| 40 |
-
|
|
|
|
|
|
|
| 41 |
|
| 42 |
-
### Development (Hot-reloading enabled)
|
| 43 |
```bash
|
| 44 |
-
|
|
|
|
| 45 |
```
|
| 46 |
-
- **API**: `http://localhost:8000`
|
| 47 |
-
- **UI Playground**: `http://localhost:5173`
|
| 48 |
-
- **Python CLI**: `docker compose run cli redact "Some text"`
|
| 49 |
-
- **TypeScript CLI**: `docker compose run cli-ts redact "Some text"`
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
| 53 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 54 |
```
|
| 55 |
-
- **API**: `http://localhost:8000`
|
| 56 |
-
- **UI Playground**: `http://localhost` (Port 80)
|
| 57 |
|
| 58 |
-
|
| 59 |
-
|
| 60 |
-
|
| 61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# 🛡️ Privacy Gateway Pro
|
| 2 |
|
| 3 |
+
An enterprise-grade PII (Personally Identifiable Information) moderation toolkit designed to sanitize sensitive data before it reaches LLM APIs (OpenAI, Anthropic, etc.).
|
| 4 |
|
| 5 |
+
## 🚀 Key Features
|
| 6 |
|
| 7 |
+
- **Multi-Language Support**: Native, high-accuracy detection for **English** and **French** using `spaCy` Large models.
|
| 8 |
+
- **Double-Pass Protection**: Combines NLP-based detection with expert Regex patterns for 100% coverage of technical identifiers.
|
| 9 |
+
- **Expert French Recognizers**: Built-in support for French-specific data: **SIRET**, **NIR** (Social Security), **IBAN**, and complex addresses.
|
| 10 |
+
- **Balanced Anonymization**: Preserves job titles (e.g., "Director", "Architect") and document structure to keep texts readable for LLMs.
|
| 11 |
+
- **Modern Dashboard**: React-based UI with **Risk Assessment** visualization and real-time confidence scores.
|
| 12 |
+
- **Multi-Theme UI**: Switch between **Premium**, **Minimal Light**, and **Deep Midnight** modes (persistent choice).
|
| 13 |
+
- **Dual CLI**: Tooling available in both **Python** and **TypeScript** for seamless CI/CD integration.
|
| 14 |
|
| 15 |
+
---
|
| 16 |
+
|
| 17 |
+
## 🛠️ Architecture
|
| 18 |
+
|
| 19 |
+
1. **Core API (`/api`)**: FastAPI server powered by **Microsoft Presidio**. Includes custom `SpacyRecognizer` mappings for French.
|
| 20 |
+
2. **Web Dashboard (`/ui`)**: React + Vite + Tailwind CSS v4. Feature-rich playground for document sanitization.
|
| 21 |
+
3. **CLI Tools**:
|
| 22 |
+
- **Python (`/cli`)**: Ideal for local automation.
|
| 23 |
+
- **TypeScript (`/cli-ts`)**: Ready for Node.js workflows.
|
| 24 |
+
|
| 25 |
+
---
|
| 26 |
+
|
| 27 |
+
## 📦 Getting Started (Docker)
|
| 28 |
+
|
| 29 |
+
The fastest way to run the full stack is using Docker Compose.
|
| 30 |
|
|
|
|
|
|
|
| 31 |
```bash
|
| 32 |
+
docker compose up --build
|
|
|
|
|
|
|
| 33 |
```
|
| 34 |
|
| 35 |
+
- **API**: `http://localhost:8000`
|
| 36 |
+
- **UI Dashboard**: `http://localhost:5173`
|
| 37 |
+
- **Model Persistence**: NLP models are stored in a Docker volume (`spacy_data`) so they are only downloaded once.
|
| 38 |
+
|
| 39 |
+
### Usage Examples:
|
| 40 |
+
|
| 41 |
+
**CLI (Python):**
|
| 42 |
```bash
|
| 43 |
+
docker compose run cli redact "Hello, my name is John Doe"
|
|
|
|
|
|
|
|
|
|
| 44 |
```
|
| 45 |
|
| 46 |
+
**CLI (TypeScript):**
|
|
|
|
| 47 |
```bash
|
| 48 |
+
docker compose run cli-ts redact "Contact me at 06 12 34 56 78"
|
|
|
|
| 49 |
```
|
|
|
|
| 50 |
|
| 51 |
+
---
|
| 52 |
|
| 53 |
+
## 🧪 Quality Assurance
|
| 54 |
+
|
| 55 |
+
We maintain a professional test suite covering medical, financial, and professional scenarios.
|
| 56 |
|
|
|
|
| 57 |
```bash
|
| 58 |
+
# Run the test suite
|
| 59 |
+
docker compose run api python tests/verify_all.py
|
| 60 |
```
|
|
|
|
|
|
|
|
|
|
|
|
|
| 61 |
|
| 62 |
+
- **Status**: ✅ 100% Verified coverage for FR/EN names, identifiers, and sensitive numbers.
|
| 63 |
+
|
| 64 |
+
---
|
| 65 |
+
|
| 66 |
+
## 🔧 API Documentation
|
| 67 |
+
|
| 68 |
+
### POST `/redact`
|
| 69 |
+
Sanitizes a block of text.
|
| 70 |
+
|
| 71 |
+
**Payload:**
|
| 72 |
+
```json
|
| 73 |
+
{
|
| 74 |
+
"text": "Jean-Pierre Moulin habite à Marseille. SIRET 456 789 123 00015.",
|
| 75 |
+
"language": "auto"
|
| 76 |
+
}
|
| 77 |
```
|
|
|
|
|
|
|
| 78 |
|
| 79 |
+
**Response:**
|
| 80 |
+
```json
|
| 81 |
+
{
|
| 82 |
+
"redacted_text": "<PERSON> habite à <LOCATION>. SIRET <SIRET>.",
|
| 83 |
+
"detected_language": "fr",
|
| 84 |
+
"entities": [
|
| 85 |
+
{ "type": "PERSON", "text": "Jean-Pierre Moulin", "score": 85 },
|
| 86 |
+
...
|
| 87 |
+
]
|
| 88 |
+
}
|
| 89 |
+
```
|
| 90 |
+
|
| 91 |
+
---
|
| 92 |
+
|
| 93 |
+
## 📄 License
|
| 94 |
+
MIT - Created for secure LLM workflows.
|
ui/src/App.tsx
CHANGED
|
@@ -1,8 +1,9 @@
|
|
| 1 |
-
import { useState, useEffect } from 'react';
|
| 2 |
import axios from 'axios';
|
| 3 |
import {
|
| 4 |
Shield, Eye, Lock, CheckCircle2, Copy,
|
| 5 |
-
Database, Languages, FileText, Fingerprint, Zap, Activity
|
|
|
|
| 6 |
} from 'lucide-react';
|
| 7 |
|
| 8 |
interface EntityMeta {
|
|
@@ -20,212 +21,222 @@ interface RedactResponse {
|
|
| 20 |
entities: EntityMeta[];
|
| 21 |
}
|
| 22 |
|
|
|
|
|
|
|
| 23 |
const EXAMPLES = [
|
| 24 |
{
|
| 25 |
label: "📄 FR - Contrat & PV",
|
| 26 |
lang: "fr",
|
| 27 |
-
text: `PROCÈS-VERBAL DE RÉUNION DE CHANTIER - RÉNOVATION COMPLEXE HÔTELIER\n\nDate : 20 Mars 2026\nLieu : 142 Avenue des Champs-Élysées, 75008 Paris.\n\nPRÉSENTS :\n- M. Alexandre de La Rochefoucauld (Directeur de projet, Groupe Immobilier "Lux-Horizon" - SIRET 321 654 987 00054).\n- Mme Valérie Marchand (Architecte, Cabinet "Marchand & Associés").\n- M. Thomas Dubois (Ingénieur sécurité, joignable au 06.45.12.89.33).\n\nORDRE DU JOUR ET DÉCISIONS :\n1. Validation des acomptes : La facture n°2026-04 d'un montant de 45 000€ a été réglée par virement sur le compte IBAN FR76 3000 1000 2000 3000 4000 500.
|
| 28 |
},
|
| 29 |
{
|
| 30 |
label: "📄 EN - Medical Record",
|
| 31 |
lang: "en",
|
| 32 |
-
text: `CLINICAL DISCHARGE SUMMARY\n\nPATIENT INFORMATION:\nName: Sarah-Jane Montgomery\nDOB: 12/05/1982\nAddress: 1244 North Oak Street, San Francisco, CA 94102\nEmergency Contact: Robert Montgomery (Husband) - Phone: (415) 555-0198\n\nADMISSION DIAGNOSIS:\nAcute respiratory distress. Patient was admitted to 'Green Valley General Hospital'
|
| 33 |
}
|
| 34 |
];
|
| 35 |
|
| 36 |
function App() {
|
| 37 |
const [text, setText] = useState('');
|
| 38 |
const [language, setLanguage] = useState('auto');
|
|
|
|
| 39 |
const [result, setResult] = useState<RedactResponse | null>(null);
|
| 40 |
const [loading, setLoading] = useState(false);
|
| 41 |
const [apiStatus, setApiStatus] = useState<'checking' | 'online' | 'offline'>('online');
|
| 42 |
const [copied, setCopied] = useState(false);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 43 |
|
| 44 |
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
| 45 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 46 |
useEffect(() => {
|
| 47 |
const checkStatus = async () => {
|
| 48 |
-
try {
|
| 49 |
-
|
| 50 |
-
setApiStatus('online');
|
| 51 |
-
} catch (err) {
|
| 52 |
-
setApiStatus('offline');
|
| 53 |
-
}
|
| 54 |
};
|
| 55 |
checkStatus();
|
| 56 |
}, [API_URL]);
|
| 57 |
|
| 58 |
-
const handleRedact = async (
|
| 59 |
-
|
| 60 |
-
if (!textToProcess.trim()) return;
|
| 61 |
setLoading(true);
|
| 62 |
try {
|
| 63 |
-
const response = await axios.post(`${API_URL}/redact`, { text
|
| 64 |
setResult(response.data);
|
| 65 |
-
} catch (err: any) {
|
| 66 |
-
console.error(err);
|
| 67 |
-
} finally {
|
| 68 |
-
setLoading(false);
|
| 69 |
-
}
|
| 70 |
};
|
| 71 |
|
| 72 |
-
const
|
| 73 |
-
|
| 74 |
-
|
| 75 |
-
|
| 76 |
};
|
| 77 |
|
| 78 |
-
const
|
| 79 |
-
|
| 80 |
-
|
| 81 |
-
|
| 82 |
};
|
| 83 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
return (
|
| 85 |
-
<div className=
|
| 86 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 87 |
<div className="max-w-7xl mx-auto px-8 py-12">
|
| 88 |
-
{/* Simple & Clean Header */}
|
| 89 |
<header className="flex items-center justify-between mb-16">
|
| 90 |
<div className="flex items-center gap-3">
|
| 91 |
-
<div className=
|
| 92 |
-
<Shield className="text-white w-5 h-5" />
|
| 93 |
-
</div>
|
| 94 |
<div>
|
| 95 |
-
<h1 className="text-xl font-bold tracking-tight
|
| 96 |
<div className="flex items-center gap-2 mt-0.5">
|
| 97 |
<span className={`w-1.5 h-1.5 rounded-full ${apiStatus === 'online' ? 'bg-emerald-500 animate-pulse' : 'bg-rose-500'}`} />
|
| 98 |
-
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">
|
| 99 |
</div>
|
| 100 |
</div>
|
| 101 |
</div>
|
| 102 |
|
| 103 |
<div className="flex items-center gap-4">
|
| 104 |
-
|
| 105 |
-
|
| 106 |
-
<
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 110 |
>
|
| 111 |
-
<
|
| 112 |
-
<
|
| 113 |
-
<
|
| 114 |
-
</
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 115 |
</div>
|
| 116 |
</div>
|
| 117 |
</header>
|
| 118 |
|
| 119 |
-
{/* Demo Selection */}
|
| 120 |
<div className="mb-12">
|
| 121 |
-
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-4">Simulations
|
| 122 |
-
<div className="flex gap-4">
|
| 123 |
{EXAMPLES.map((ex, i) => (
|
| 124 |
-
<button
|
| 125 |
-
key={i}
|
| 126 |
-
onClick={() => loadExample(ex.text, ex.lang)}
|
| 127 |
-
className="px-5 py-3 bg-white border border-slate-200 rounded-xl text-xs font-bold text-slate-700 hover:border-slate-900 hover:shadow-sm transition-all active:scale-[0.98]"
|
| 128 |
-
>
|
| 129 |
-
{ex.label}
|
| 130 |
-
</button>
|
| 131 |
))}
|
| 132 |
</div>
|
| 133 |
</div>
|
| 134 |
|
| 135 |
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
|
| 136 |
-
{/* Input Area */}
|
| 137 |
<div className="lg:col-span-5">
|
| 138 |
-
<div className=
|
| 139 |
<div className="flex items-center justify-between mb-6 text-slate-400">
|
| 140 |
-
<div className="flex items-center gap-2">
|
| 141 |
-
<Database className="w-4 h-4" />
|
| 142 |
-
<span className="text-[10px] font-bold uppercase tracking-widest">Contenu Source</span>
|
| 143 |
-
</div>
|
| 144 |
<span className="text-[10px] font-mono">{text.length} chars</span>
|
| 145 |
</div>
|
| 146 |
-
|
| 147 |
-
<
|
| 148 |
-
className="flex-
|
| 149 |
-
placeholder="Entrez vos données sensibles..."
|
| 150 |
-
value={text}
|
| 151 |
-
onChange={(e) => setText(e.target.value)}
|
| 152 |
-
/>
|
| 153 |
-
|
| 154 |
-
<button
|
| 155 |
-
onClick={() => handleRedact()}
|
| 156 |
-
disabled={loading || !text.trim()}
|
| 157 |
-
className={`mt-8 w-full py-4 rounded-xl font-bold text-sm text-white transition-all flex items-center justify-center gap-3 ${
|
| 158 |
-
loading || !text.trim()
|
| 159 |
-
? 'bg-slate-200 cursor-not-allowed text-slate-400'
|
| 160 |
-
: 'bg-slate-900 hover:bg-black active:scale-[0.99]'
|
| 161 |
-
}`}
|
| 162 |
-
>
|
| 163 |
-
{loading ? (
|
| 164 |
-
<div className="flex gap-1.5 items-center">
|
| 165 |
-
<span className="w-1 h-1 bg-white rounded-full animate-bounce [animation-delay:-0.3s]"></span>
|
| 166 |
-
<span className="w-1 h-1 bg-white rounded-full animate-bounce [animation-delay:-0.15s]"></span>
|
| 167 |
-
<span className="w-1 h-1 bg-white rounded-full animate-bounce"></span>
|
| 168 |
-
</div>
|
| 169 |
-
) : (
|
| 170 |
-
<>
|
| 171 |
-
<Zap className="w-4 h-4 fill-white" />
|
| 172 |
-
<span>Lancer l'Anonymisation</span>
|
| 173 |
-
</>
|
| 174 |
-
)}
|
| 175 |
</button>
|
| 176 |
</div>
|
| 177 |
</div>
|
| 178 |
|
| 179 |
-
{/* Results Area */}
|
| 180 |
<div className="lg:col-span-7 space-y-8">
|
| 181 |
-
<div className=
|
| 182 |
<div className="flex items-center justify-between mb-8">
|
| 183 |
-
<div className="flex items-center gap-3">
|
| 184 |
-
|
| 185 |
-
<span className="text-[10px] font-bold uppercase tracking-widest text-emerald-400/70">Flux de sortie sécurisé</span>
|
| 186 |
-
</div>
|
| 187 |
-
{result && (
|
| 188 |
-
<button
|
| 189 |
-
onClick={() => {navigator.clipboard.writeText(result.redacted_text); setCopied(true); setTimeout(()=>setCopied(false), 2000)}}
|
| 190 |
-
className="text-[10px] font-bold uppercase text-white/50 hover:text-white transition-colors"
|
| 191 |
-
>
|
| 192 |
-
{copied ? 'Copié' : 'Copier'}
|
| 193 |
-
</button>
|
| 194 |
-
)}
|
| 195 |
</div>
|
| 196 |
-
|
| 197 |
<div className="flex-grow font-mono text-[13px] leading-relaxed text-emerald-500/90 whitespace-pre-wrap overflow-y-auto custom-scrollbar">
|
| 198 |
-
{!result ?
|
| 199 |
-
<div className="h-full flex items-center justify-center text-slate-700 italic">
|
| 200 |
-
Aucun résultat généré
|
| 201 |
-
</div>
|
| 202 |
-
) : result.redacted_text}
|
| 203 |
</div>
|
| 204 |
</div>
|
| 205 |
|
| 206 |
-
{/* Analysis Grid with Colored Scores */}
|
| 207 |
{result && (
|
| 208 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 209 |
-
<div className="md:col-span-2 flex items-center gap-2 mb-2">
|
| 210 |
-
<Fingerprint className="w-4 h-4 text-slate-400" />
|
| 211 |
-
<span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Rapport de détection ({result.detected_language})</span>
|
| 212 |
-
</div>
|
| 213 |
{result.entities.map((ent, idx) => {
|
| 214 |
const styles = getScoreStyles(ent.score);
|
| 215 |
return (
|
| 216 |
-
<div key={idx} className={`p-4 rounded-xl border
|
| 217 |
-
<div className=
|
| 218 |
-
<span className={`text-[9px] font-black uppercase tracking-widest ${styles.text}`}>{ent.type}</span>
|
| 219 |
-
<span className="text-xs font-bold text-slate-800 line-clamp-1 italic">"{ent.text}"</span>
|
| 220 |
-
</div>
|
| 221 |
<div className="text-right">
|
| 222 |
-
<div className="flex items-center gap-1.5">
|
| 223 |
-
|
| 224 |
-
<p className={`text-[11px] font-black ${styles.text}`}>{ent.score}%</p>
|
| 225 |
-
</div>
|
| 226 |
-
<div className="w-12 h-1 bg-white/50 rounded-full mt-1.5 overflow-hidden">
|
| 227 |
-
<div className={`h-full ${styles.bg}`} style={{ width: `${ent.score}%` }} />
|
| 228 |
-
</div>
|
| 229 |
</div>
|
| 230 |
</div>
|
| 231 |
);
|
|
|
|
| 1 |
+
import { useState, useEffect, useRef } from 'react';
|
| 2 |
import axios from 'axios';
|
| 3 |
import {
|
| 4 |
Shield, Eye, Lock, CheckCircle2, Copy,
|
| 5 |
+
Database, Languages, FileText, Fingerprint, Zap, Activity,
|
| 6 |
+
Palette, ChevronDown, Check, Sun, Moon, Sparkles
|
| 7 |
} from 'lucide-react';
|
| 8 |
|
| 9 |
interface EntityMeta {
|
|
|
|
| 21 |
entities: EntityMeta[];
|
| 22 |
}
|
| 23 |
|
| 24 |
+
type Theme = 'premium' | 'light' | 'dark';
|
| 25 |
+
|
| 26 |
const EXAMPLES = [
|
| 27 |
{
|
| 28 |
label: "📄 FR - Contrat & PV",
|
| 29 |
lang: "fr",
|
| 30 |
+
text: `PROCÈS-VERBAL DE RÉUNION DE CHANTIER - RÉNOVATION COMPLEXE HÔTELIER\n\nDate : 20 Mars 2026\nLieu : 142 Avenue des Champs-Élysées, 75008 Paris.\n\nPRÉSENTS :\n- M. Alexandre de La Rochefoucauld (Directeur de projet, Groupe Immobilier "Lux-Horizon" - SIRET 321 654 987 00054).\n- Mme Valérie Marchand (Architecte, Cabinet "Marchand & Associés").\n- M. Thomas Dubois (Ingénieur sécurité, joignable au 06.45.12.89.33).\n\nORDRE DU JOUR ET DÉCISIONS :\n1. Validation des acomptes : La facture n°2026-04 d'un montant de 45 000€ a été réglée par virement sur le compte IBAN FR76 3000 1000 2000 3000 4000 500.`
|
| 31 |
},
|
| 32 |
{
|
| 33 |
label: "📄 EN - Medical Record",
|
| 34 |
lang: "en",
|
| 35 |
+
text: `CLINICAL DISCHARGE SUMMARY\n\nPATIENT INFORMATION:\nName: Sarah-Jane Montgomery\nDOB: 12/05/1982\nAddress: 1244 North Oak Street, San Francisco, CA 94102\nEmergency Contact: Robert Montgomery (Husband) - Phone: (415) 555-0198\n\nADMISSION DIAGNOSIS:\nAcute respiratory distress. Patient was admitted to 'Green Valley General Hospital'. SSN: 123-45-6789.`
|
| 36 |
}
|
| 37 |
];
|
| 38 |
|
| 39 |
function App() {
|
| 40 |
const [text, setText] = useState('');
|
| 41 |
const [language, setLanguage] = useState('auto');
|
| 42 |
+
const [theme, setTheme] = useState<Theme>(() => (localStorage.getItem('pg-theme') as Theme) || 'premium');
|
| 43 |
const [result, setResult] = useState<RedactResponse | null>(null);
|
| 44 |
const [loading, setLoading] = useState(false);
|
| 45 |
const [apiStatus, setApiStatus] = useState<'checking' | 'online' | 'offline'>('online');
|
| 46 |
const [copied, setCopied] = useState(false);
|
| 47 |
+
|
| 48 |
+
// Dropdown States
|
| 49 |
+
const [isThemeOpen, setIsThemeOpen] = useState(false);
|
| 50 |
+
const [isLangOpen, setIsLangOpen] = useState(false);
|
| 51 |
+
const themeRef = useRef<HTMLDivElement>(null);
|
| 52 |
+
const langRef = useRef<HTMLDivElement>(null);
|
| 53 |
|
| 54 |
const API_URL = import.meta.env.VITE_API_URL || 'http://localhost:8000';
|
| 55 |
|
| 56 |
+
useEffect(() => {
|
| 57 |
+
localStorage.setItem('pg-theme', theme);
|
| 58 |
+
const handleClickOutside = (e: MouseEvent) => {
|
| 59 |
+
if (themeRef.current && !themeRef.current.contains(e.target as Node)) setIsThemeOpen(false);
|
| 60 |
+
if (langRef.current && !langRef.current.contains(e.target as Node)) setIsLangOpen(false);
|
| 61 |
+
};
|
| 62 |
+
document.addEventListener('mousedown', handleClickOutside);
|
| 63 |
+
return () => document.removeEventListener('mousedown', handleClickOutside);
|
| 64 |
+
}, [theme]);
|
| 65 |
+
|
| 66 |
useEffect(() => {
|
| 67 |
const checkStatus = async () => {
|
| 68 |
+
try { await axios.get(`${API_URL}/`); setApiStatus('online'); }
|
| 69 |
+
catch (err) { setApiStatus('offline'); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 70 |
};
|
| 71 |
checkStatus();
|
| 72 |
}, [API_URL]);
|
| 73 |
|
| 74 |
+
const handleRedact = async () => {
|
| 75 |
+
if (!text.trim()) return;
|
|
|
|
| 76 |
setLoading(true);
|
| 77 |
try {
|
| 78 |
+
const response = await axios.post(`${API_URL}/redact`, { text, language });
|
| 79 |
setResult(response.data);
|
| 80 |
+
} catch (err: any) { console.error(err); } finally { setLoading(false); }
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
};
|
| 82 |
|
| 83 |
+
const getScoreStyles = (score: number) => {
|
| 84 |
+
if (score >= 90) return { text: 'text-rose-600', bg: 'bg-rose-500', border: 'border-rose-100', lightBg: theme === 'dark' ? 'bg-rose-900/20' : 'bg-rose-50' };
|
| 85 |
+
if (score >= 70) return { text: 'text-amber-600', bg: 'bg-amber-500', border: 'border-amber-100', lightBg: theme === 'dark' ? 'bg-amber-900/20' : 'bg-amber-50' };
|
| 86 |
+
return { text: 'text-emerald-600', bg: 'bg-emerald-500', border: 'border-emerald-100', lightBg: theme === 'dark' ? 'bg-emerald-900/20' : 'bg-emerald-50' };
|
| 87 |
};
|
| 88 |
|
| 89 |
+
const themeClasses = {
|
| 90 |
+
premium: { body: 'bg-[#F9FAFB]', card: 'bg-white border-slate-200', text: 'text-slate-900', input: 'text-slate-700', dropdown: 'bg-white' },
|
| 91 |
+
light: { body: 'bg-white', card: 'bg-white border-slate-950', text: 'text-black', input: 'text-black font-medium', dropdown: 'bg-white' },
|
| 92 |
+
dark: { body: 'bg-[#020617]', card: 'bg-slate-900 border-slate-800', text: 'text-slate-100', input: 'text-slate-300', dropdown: 'bg-slate-900' }
|
| 93 |
};
|
| 94 |
|
| 95 |
+
const cur = themeClasses[theme];
|
| 96 |
+
|
| 97 |
+
const themes = [
|
| 98 |
+
{ id: 'premium', label: 'Premium UI', icon: <Sparkles className="w-3.5 h-3.5" />, color: 'bg-blue-500' },
|
| 99 |
+
{ id: 'light', label: 'Minimal Light', icon: <Sun className="w-3.5 h-3.5" />, color: 'bg-slate-200' },
|
| 100 |
+
{ id: 'dark', label: 'Deep Midnight', icon: <Moon className="w-3.5 h-3.5" />, color: 'bg-slate-950' },
|
| 101 |
+
];
|
| 102 |
+
|
| 103 |
+
const languages = [
|
| 104 |
+
{ id: 'auto', label: 'Auto-Detect' },
|
| 105 |
+
{ id: 'en', label: 'English (NER-LG)' },
|
| 106 |
+
{ id: 'fr', label: 'French (NER-LG)' },
|
| 107 |
+
];
|
| 108 |
+
|
| 109 |
return (
|
| 110 |
+
<div className={`min-h-screen ${cur.body} ${cur.text} font-sans selection:bg-blue-100 transition-colors duration-300`}>
|
| 111 |
+
{theme === 'premium' && (
|
| 112 |
+
<div className="fixed inset-0 overflow-hidden -z-10">
|
| 113 |
+
<div className="absolute top-0 right-0 w-[500px] h-[500px] bg-blue-400/5 blur-[120px] rounded-full" />
|
| 114 |
+
<div className="absolute bottom-0 left-0 w-[500px] h-[500px] bg-indigo-400/5 blur-[120px] rounded-full" />
|
| 115 |
+
</div>
|
| 116 |
+
)}
|
| 117 |
+
|
| 118 |
<div className="max-w-7xl mx-auto px-8 py-12">
|
|
|
|
| 119 |
<header className="flex items-center justify-between mb-16">
|
| 120 |
<div className="flex items-center gap-3">
|
| 121 |
+
<div className={`${theme === 'dark' ? 'bg-white text-black' : 'bg-black text-white'} p-2 rounded-lg shadow-sm`}><Shield className="w-5 h-5" /></div>
|
|
|
|
|
|
|
| 122 |
<div>
|
| 123 |
+
<h1 className="text-xl font-bold tracking-tight">Privacy Gateway</h1>
|
| 124 |
<div className="flex items-center gap-2 mt-0.5">
|
| 125 |
<span className={`w-1.5 h-1.5 rounded-full ${apiStatus === 'online' ? 'bg-emerald-500 animate-pulse' : 'bg-rose-500'}`} />
|
| 126 |
+
<span className="text-[10px] font-bold text-slate-400 uppercase tracking-widest">Safe Passage</span>
|
| 127 |
</div>
|
| 128 |
</div>
|
| 129 |
</div>
|
| 130 |
|
| 131 |
<div className="flex items-center gap-4">
|
| 132 |
+
{/* Custom Theme Dropdown */}
|
| 133 |
+
<div className="relative" ref={themeRef}>
|
| 134 |
+
<button
|
| 135 |
+
onClick={() => setIsThemeOpen(!isThemeOpen)}
|
| 136 |
+
className={`flex items-center gap-3 ${cur.card} px-4 py-2.5 rounded-xl border shadow-sm hover:border-slate-400 transition-all text-[10px] font-black uppercase tracking-widest text-slate-600`}
|
| 137 |
+
>
|
| 138 |
+
<Palette className="w-3.5 h-3.5" />
|
| 139 |
+
<span>Theme: {themes.find(t => t.id === theme)?.label}</span>
|
| 140 |
+
<ChevronDown className={`w-3.5 h-3.5 transition-transform ${isThemeOpen ? 'rotate-180' : ''}`} />
|
| 141 |
+
</button>
|
| 142 |
+
|
| 143 |
+
{isThemeOpen && (
|
| 144 |
+
<div className={`absolute right-0 mt-2 w-56 ${cur.dropdown} border ${theme === 'dark' ? 'border-slate-800' : 'border-slate-200'} rounded-2xl shadow-2xl z-50 overflow-hidden animate-in fade-in zoom-in-95 duration-200`}>
|
| 145 |
+
<div className="p-2 space-y-1">
|
| 146 |
+
{themes.map((t) => (
|
| 147 |
+
<button
|
| 148 |
+
key={t.id}
|
| 149 |
+
onClick={() => { setTheme(t.id as Theme); setIsThemeOpen(false); }}
|
| 150 |
+
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-wider transition-colors ${theme === t.id ? (theme === 'dark' ? 'bg-slate-800 text-white' : 'bg-slate-50 text-black') : 'text-slate-500 hover:bg-slate-50'}`}
|
| 151 |
+
>
|
| 152 |
+
<div className="flex items-center gap-3">
|
| 153 |
+
<div className={`w-2 h-2 rounded-full ${t.color}`} />
|
| 154 |
+
{t.label}
|
| 155 |
+
</div>
|
| 156 |
+
{theme === t.id && <Check className="w-3 h-3 text-blue-500" />}
|
| 157 |
+
</button>
|
| 158 |
+
))}
|
| 159 |
+
</div>
|
| 160 |
+
</div>
|
| 161 |
+
)}
|
| 162 |
+
</div>
|
| 163 |
+
|
| 164 |
+
{/* Custom Language Dropdown */}
|
| 165 |
+
<div className="relative" ref={langRef}>
|
| 166 |
+
<button
|
| 167 |
+
onClick={() => setIsLangOpen(!isLangOpen)}
|
| 168 |
+
className={`flex items-center gap-3 ${cur.card} px-4 py-2.5 rounded-xl border shadow-sm hover:border-slate-400 transition-all text-[10px] font-black uppercase tracking-widest text-slate-600`}
|
| 169 |
>
|
| 170 |
+
<Languages className="w-3.5 h-3.5" />
|
| 171 |
+
<span>{languages.find(l => l.id === language)?.label}</span>
|
| 172 |
+
<ChevronDown className={`w-3.5 h-3.5 transition-transform ${isLangOpen ? 'rotate-180' : ''}`} />
|
| 173 |
+
</button>
|
| 174 |
+
|
| 175 |
+
{isLangOpen && (
|
| 176 |
+
<div className={`absolute right-0 mt-2 w-56 ${cur.dropdown} border ${theme === 'dark' ? 'border-slate-800' : 'border-slate-200'} rounded-2xl shadow-2xl z-50 overflow-hidden animate-in fade-in zoom-in-95 duration-200`}>
|
| 177 |
+
<div className="p-2 space-y-1">
|
| 178 |
+
{languages.map((l) => (
|
| 179 |
+
<button
|
| 180 |
+
key={l.id}
|
| 181 |
+
onClick={() => { setLanguage(l.id); setIsLangOpen(false); }}
|
| 182 |
+
className={`w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-[10px] font-bold uppercase tracking-wider transition-colors ${language === l.id ? (theme === 'dark' ? 'bg-slate-800 text-white' : 'bg-slate-50 text-black') : 'text-slate-500 hover:bg-slate-50'}`}
|
| 183 |
+
>
|
| 184 |
+
{l.label}
|
| 185 |
+
{language === l.id && <Check className="w-3 h-3 text-blue-500" />}
|
| 186 |
+
</button>
|
| 187 |
+
))}
|
| 188 |
+
</div>
|
| 189 |
+
</div>
|
| 190 |
+
)}
|
| 191 |
</div>
|
| 192 |
</div>
|
| 193 |
</header>
|
| 194 |
|
|
|
|
| 195 |
<div className="mb-12">
|
| 196 |
+
<p className="text-[10px] font-bold text-slate-400 uppercase tracking-widest mb-4">Simulations</p>
|
| 197 |
+
<div className="flex gap-4 overflow-x-auto pb-2 scrollbar-hide">
|
| 198 |
{EXAMPLES.map((ex, i) => (
|
| 199 |
+
<button key={i} onClick={() => { setText(ex.text); setLanguage(ex.lang); setResult(null); }} className={`px-5 py-3 ${cur.card} border rounded-xl text-xs font-bold ${theme === 'light' ? 'hover:bg-black hover:text-white' : 'hover:border-blue-400'} transition-all whitespace-nowrap`}>{ex.label}</button>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 200 |
))}
|
| 201 |
</div>
|
| 202 |
</div>
|
| 203 |
|
| 204 |
<div className="grid grid-cols-1 lg:grid-cols-12 gap-12">
|
|
|
|
| 205 |
<div className="lg:col-span-5">
|
| 206 |
+
<div className={`${cur.card} rounded-2xl border p-8 flex flex-col h-[650px] shadow-sm transition-colors`}>
|
| 207 |
<div className="flex items-center justify-between mb-6 text-slate-400">
|
| 208 |
+
<div className="flex items-center gap-2"><Database className="w-4 h-4" /><span className="text-[10px] font-bold uppercase tracking-widest">Source Document</span></div>
|
|
|
|
|
|
|
|
|
|
| 209 |
<span className="text-[10px] font-mono">{text.length} chars</span>
|
| 210 |
</div>
|
| 211 |
+
<textarea className={`flex-grow w-full bg-transparent ${cur.input} font-medium leading-relaxed outline-none resize-none placeholder:text-slate-300`} placeholder="Paste sensitive data..." value={text} onChange={(e) => setText(e.target.value)} />
|
| 212 |
+
<button onClick={() => handleRedact()} disabled={loading || !text.trim()} className={`mt-8 w-full py-4 rounded-xl font-bold text-sm transition-all flex items-center justify-center gap-3 ${loading || !text.trim() ? 'bg-slate-100 text-slate-400 cursor-not-allowed' : theme === 'light' ? 'bg-black text-white hover:bg-slate-800' : 'bg-blue-600 text-white hover:bg-blue-700 shadow-lg shadow-blue-500/20'}`}>
|
| 213 |
+
{loading ? <div className="flex gap-1.5 items-center"><span className="w-1 h-1 bg-current rounded-full animate-bounce [animation-delay:-0.3s]"></span><span className="w-1 h-1 bg-current rounded-full animate-bounce [animation-delay:-0.15s]"></span><span className="w-1 h-1 bg-current rounded-full animate-bounce"></span></div> : <><Zap className={`w-4 h-4 ${theme === 'light' ? 'fill-white' : 'fill-current'}`} /><span>Run Sanitization</span></>}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 214 |
</button>
|
| 215 |
</div>
|
| 216 |
</div>
|
| 217 |
|
|
|
|
| 218 |
<div className="lg:col-span-7 space-y-8">
|
| 219 |
+
<div className={`${theme === 'dark' ? 'bg-black border-slate-800' : 'bg-slate-900 border-transparent'} rounded-2xl p-10 h-[450px] flex flex-col shadow-xl relative border transition-colors`}>
|
| 220 |
<div className="flex items-center justify-between mb-8">
|
| 221 |
+
<div className="flex items-center gap-3"><div className="w-1.5 h-1.5 bg-emerald-400 rounded-full" /><span className="text-[10px] font-bold uppercase tracking-widest text-emerald-400/70">Secure Output</span></div>
|
| 222 |
+
{result && <button onClick={() => {navigator.clipboard.writeText(result.redacted_text); setCopied(true); setTimeout(()=>setCopied(false), 2000)}} className="text-[10px] font-bold uppercase text-white/50 hover:text-white transition-colors">{copied ? 'Copié' : 'Copy'}</button>}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 223 |
</div>
|
|
|
|
| 224 |
<div className="flex-grow font-mono text-[13px] leading-relaxed text-emerald-500/90 whitespace-pre-wrap overflow-y-auto custom-scrollbar">
|
| 225 |
+
{!result ? <div className="h-full flex items-center justify-center text-slate-700 italic">No output generated</div> : result.redacted_text}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
</div>
|
| 227 |
</div>
|
| 228 |
|
|
|
|
| 229 |
{result && (
|
| 230 |
<div className="grid grid-cols-1 md:grid-cols-2 gap-4">
|
| 231 |
+
<div className="md:col-span-2 flex items-center gap-2 mb-2"><Fingerprint className="w-4 h-4 text-slate-400" /><span className="text-[10px] font-bold uppercase tracking-widest text-slate-400">Risk Analysis</span></div>
|
|
|
|
|
|
|
|
|
|
| 232 |
{result.entities.map((ent, idx) => {
|
| 233 |
const styles = getScoreStyles(ent.score);
|
| 234 |
return (
|
| 235 |
+
<div key={idx} className={`${cur.card} p-4 rounded-xl border flex items-center justify-between transition-all hover:shadow-sm`}>
|
| 236 |
+
<div><p className={`text-[9px] font-black uppercase mb-0.5 ${styles.text}`}>{ent.type}</p><p className={`text-xs font-bold line-clamp-1 italic ${cur.text}`}>"{ent.text}"</p></div>
|
|
|
|
|
|
|
|
|
|
| 237 |
<div className="text-right">
|
| 238 |
+
<div className="flex items-center gap-1.5"><Activity className={`w-3 h-3 ${styles.text}`} /><p className={`text-[11px] font-black ${styles.text}`}>{ent.score}%</p></div>
|
| 239 |
+
<div className={`w-12 h-1 ${theme === 'dark' ? 'bg-slate-800' : 'bg-slate-100'} rounded-full mt-1.5 overflow-hidden`}><div className={`h-full ${styles.bg}`} style={{ width: `${ent.score}%` }} /></div>
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 240 |
</div>
|
| 241 |
</div>
|
| 242 |
);
|