private-synthid / index.html
jfrery-zama's picture
add diagram
1923b3f unverified
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Private AI Generated Text Detection</title>
<link rel="icon" type="image/x-icon" href="favicon.ico">
<style>
@import url('https://fonts.googleapis.com/css2?family=Telegraf:wght@400;500;600;700&display=swap');
:root {
/* palette tuned to match screenshot */
--white: #ffffff;
--black: #000000;
--grey-025: #f5f5f5;
--grey-050: #f0f0f0;
--grey-100: #e8e8e8;
--grey-200: #e0e0e0; /* button & border base */
--grey-300: #d4d4d4; /* button hover */
--container-max: 1200px;
--shadow-sm: 0 2px 4px rgba(0,0,0,0.06);
--spacing-unit: 8px;
}
/* reset */
*, *::before, *::after { margin:0; padding:0; box-sizing:border-box; }
body {
font-family: 'Telegraf', sans-serif;
background: var(--white); /* removed yellow */
color: var(--black);
line-height: 1.5;
font-weight: 400;
}
/* general copy */
.explanation-text {
font-size: 1.3rem;
line-height: 1.6;
margin-bottom: calc(var(--spacing-unit) * 4);
}
.explanation-text a {
color: var(--black);
text-decoration: underline;
font-weight: 500;
}
.explanation-text a:hover {
opacity: 0.8;
}
/* navbar */
.navbar {
display: flex;
justify-content: space-between;
align-items: center;
padding: calc(var(--spacing-unit) * 3) calc(var(--spacing-unit) * 4);
max-width: var(--container-max);
margin-inline: auto;
}
.logo {
height: 120px;
}
.contact-btn {
border: none; /* no thick outline */
background: var(--grey-200); /* light grey like screenshot */
color: var(--black);
padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3);
border-radius: 6px;
font-size: 1rem;
font-weight: 600;
font-family: 'Telegraf', sans-serif;
cursor: pointer;
transition: background .2s ease;
text-decoration: none;
display: inline-block;
}
.contact-btn:hover {
background: var(--grey-300); /* subtle hover */
}
/* hero */
.hero {
display: grid;
gap: calc(var(--spacing-unit) * 6);
align-items: center;
max-width: var(--container-max);
margin: calc(var(--spacing-unit) * 4) auto calc(var(--spacing-unit) * 10);
padding-inline: calc(var(--spacing-unit) * 4);
grid-template-columns: 1fr;
}
.hero h1 {
font-size: clamp(3rem, 6vw, 4.5rem);
font-weight: 700;
margin-bottom: calc(var(--spacing-unit) * 3);
letter-spacing: -0.03em;
line-height: 1.1;
}
.hero p {
font-size: 1.3rem;
max-width: none;
font-weight: 400;
line-height: 1.6;
}
.hero-diagram {
background: var(--white);
border: 1px solid var(--grey-200); /* lighter border */
border-radius: 4px;
padding: calc(var(--spacing-unit) * 4);
box-shadow: none; /* removed strong shadow */
width: 100%;
min-height: 400px;
display: flex;
align-items: center;
justify-content: center;
}
.hero-diagram .mermaid {
max-width: 100%;
font-family: 'Telegraf', sans-serif;
font-size: 1.2rem;
}
.hero-diagram img {
max-width: 100%;
height: auto;
border-radius: 4px;
}
/* wizard */
main {
max-width: var(--container-max);
margin: 0 auto calc(var(--spacing-unit) * 12);
padding: 0 calc(var(--spacing-unit) * 4);
}
.section-title {
font-size: 3rem;
font-weight: 700;
margin-bottom: calc(var(--spacing-unit) * 6);
text-align: center;
letter-spacing: -0.02em;
}
.cards-wrapper {
display: grid;
gap: calc(var(--spacing-unit) * 6);
grid-template-columns: 1fr;
}
.card {
background: var(--white);
border: 1px solid var(--grey-200); /* lighter soft border */
border-radius: 6px;
box-shadow: none; /* no drop shadow */
padding: calc(var(--spacing-unit) * 4);
min-height: 280px;
display: flex;
flex-direction: column;
transition: background 0.2s ease;
}
.card:hover {
background: var(--grey-025);
}
.card h2 {
margin-bottom: calc(var(--spacing-unit) * 3);
font-size: 1.8rem;
font-weight: 700;
letter-spacing: -0.01em;
height: 30px;
}
.card-content {
flex: 1;
display: flex;
flex-direction: column;
gap: calc(var(--spacing-unit) * 2);
}
.controls {
display: flex;
align-items: center;
gap: calc(var(--spacing-unit) * 1.5);
min-height: 40px;
}
textarea {
width: 100%;
padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 2);
border: 1px solid var(--grey-200);
border-radius: 4px;
font-family: 'Telegraf', sans-serif;
font-size: 1.3rem;
resize: vertical;
background: var(--white);
min-height: 60px;
transition: border-color 0.2s ease;
font-weight: 400;
line-height: 1.6;
}
textarea:focus {
outline: none;
border-color: var(--grey-300);
}
.btn {
border: none; /* flat buttons */
background: var(--grey-200);
color: var(--black);
font-weight: 600;
padding: calc(var(--spacing-unit) * 1.5) calc(var(--spacing-unit) * 3);
min-width: 160px;
font-size: 1.2rem;
font-family: 'Telegraf', sans-serif;
border-radius: 6px;
cursor: pointer;
transition: background 0.2s ease;
display: inline-flex;
align-items: center;
justify-content: center;
gap: calc(var(--spacing-unit) * 1);
white-space: nowrap;
letter-spacing: -0.01em;
}
.btn:hover:not(:disabled) {
background: var(--grey-300);
}
.btn:disabled {
opacity: .5;
cursor: not-allowed;
}
.btn-discrete {
background: transparent;
border: 1px solid var(--grey-200);
min-width: auto;
padding: calc(var(--spacing-unit) * 1) calc(var(--spacing-unit) * 1.5);
font-size: 0.9rem;
opacity: 0.7;
transition: all 0.2s ease;
}
.btn-discrete:hover:not(:disabled) {
background: var(--grey-100);
opacity: 1;
border-color: var(--grey-300);
}
.status {
font-size: 1.3rem;
color: var(--black);
flex: 1;
font-weight: 400;
line-height: 1.6;
}
.loader {
width: 20px;
height: 20px;
border: 3px solid transparent;
border-top: 3px solid var(--black);
border-right: 3px solid var(--black);
border-radius: 50%;
animation: spin 1s linear infinite;
flex-shrink: 0;
}
@keyframes spin { to { transform: rotate(360deg);} }
#encIcon {
font-size: 1.25rem;
flex-shrink: 0;
}
footer {
font-size: 1.2rem;
text-align: center;
padding: calc(var(--spacing-unit) * 6) calc(var(--spacing-unit) * 2);
color: var(--black);
font-weight: 500;
}
.visually-hidden { position: absolute !important; height: 1px; width: 1px; overflow: hidden; clip: rect(1px,1px,1px,1px); white-space: nowrap; }
/* Result styling */
#decResult {
font-family: 'Telegraf', sans-serif;
padding: calc(var(--spacing-unit) * 3);
background: var(--grey-050);
border: 1px solid var(--grey-200);
border-radius: 6px;
min-height: 80px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
gap: calc(var(--spacing-unit) * 1);
font-weight: 600;
font-size: 1.4rem;
text-align: center;
}
#decResult:not(:empty) {
color: var(--black);
}
.watermark-flag {
font-size: 2rem;
font-weight: 700;
margin-bottom: calc(var(--spacing-unit) * 1);
}
.watermark-score {
font-size: 1.2rem;
font-weight: 500;
opacity: 0.8;
}
/* Result type styling */
.watermark-flag.inconclusive {
color: #f57c00;
}
.watermark-flag.watermarked {
color: #2e7d32;
}
.watermark-flag.ai-generated {
color: #1976d2;
}
/* Progress bar styling */
.progress-container {
width: 100%;
background: var(--grey-100);
border-radius: 4px;
height: 12px;
margin: calc(var(--spacing-unit) * 2) 0;
overflow: hidden;
border: 1px solid var(--grey-200);
}
.progress-bar {
height: 100%;
background: var(--black);
border-radius: 3px;
transition: width 0.3s ease;
width: 0%;
}
.processing-note {
font-size: 0.9rem;
color: var(--black);
opacity: 0.7;
line-height: 1.4;
margin-bottom: calc(var(--spacing-unit) * 2);
}
/* Responsive adjustments */
@media (max-width: 968px) {
.hero {
grid-template-columns: 1fr;
gap: calc(var(--spacing-unit) * 4);
}
.hero-diagram {
min-height: 300px;
}
}
@media (max-width: 768px) {
.cards-wrapper {
gap: calc(var(--spacing-unit) * 2);
}
.card {
padding: calc(var(--spacing-unit) * 3);
}
.btn {
min-width: 140px;
padding: calc(var(--spacing-unit) * 1.25) calc(var(--spacing-unit) * 2.5);
}
.hero h1 {
font-size: 2.5rem;
}
.section-title {
font-size: 2rem;
}
}
</style>
<!-- external scripts -->
<script type="module" src="https://belladoreai.github.io/llama3-tokenizer-js/bundle/llama3-tokenizer-with-baked-data.js"></script>
</head>
<body>
<!-- Hero -->
<header class="hero" role="banner">
<div class="hero-copy">
<h1>Private AI Generated Text Detection</h1>
<!-- 1 — Why it matters -->
<p class="explanation-text">
AI models can embed an invisible <strong>watermark</strong> – a tiny cryptographic
signature hidden in the text – that proves the text came from them.
This demo shows you can <em>detect</em> such a watermark
while your words stay fully encrypted in your browser, thanks to
<a href="https://www.zama.ai/" target="_blank">Fully Homomorphic Encryption&nbsp;(FHE)</a>.
</p>
<!-- 2 — How it works in plain English -->
<p class="explanation-text">
• Your text is encrypted locally.<br>
• Our server runs a watermark detector on the ciphertext.<br>
• Because of FHE, the server never sees the plaintext. Neither does it learn whether you text is
"watermarked" or "not watermarked."
</p>
<!-- 3 — Scope of the demo -->
<p class="explanation-text">
In this prototype we look for the
<a href="https://deepmind.google/science/synthid/" target="_blank">Google SynthID</a> watermark
as a real-world example.
<strong>No watermark</strong> simply means "no SynthID detected" — the text
might still be human-written or AI-generated without that specific mark.
</p>
</div>
<div class="hero-diagram">
<img src="visual.png" alt="Workflow diagram showing 6 steps: 1) Generate keys in browser, 2) Encrypt text provided locally, 3) Send encrypted text to server, 4) Server performs watermark detection on encrypted data, 5) Return encrypted result to browser, 6) Decrypt result locally. The diagram shows the user/browser on the left and server on the right with arrows indicating data flow.">
</div>
</header>
<!-- Wizard Steps -->
<main>
<div class="cards-wrapper">
<!-- 1 Keys -->
<section class="card" aria-labelledby="step1">
<h2 id="step1">1. Keys</h2>
<div class="card-content">
<div class="controls" style="margin-top: 0;">
<p id="keygenStatus" class="status" aria-live="polite">Generate new keys and send them to the server (requires a 130MB upload). Keys can then be loaded (instantly).</p>
<span id="keygenSpin" class="loader" hidden aria-label="Generating keys"></span>
</div>
<div style="display:flex; gap:var(--spacing-unit); margin-top:auto; flex-wrap: wrap;">
<button id="btnKeygen" class="btn" aria-describedby="keygenStatus">🔑 Generate new keys</button>
<button id="btnLoadSaved" class="btn" aria-describedby="keygenStatus">🗝️ Load saved keys</button>
<button id="btnDeleteKeys" class="btn btn-discrete" aria-describedby="keygenStatus">🗑️ Delete saved keys</button>
</div>
</div>
</section>
<!-- 2 Encrypt -->
<section class="card" aria-labelledby="step2">
<h2 id="step2">2. Encrypt text</h2>
<div class="card-content">
<div class="controls">
<p id="tokenizerStatus" class="status" aria-live="polite" style="display: none;">Loading tokenizer...</p>
<span id="tokenizerSpin" class="loader" hidden aria-label="Loading tokenizer"></span>
</div>
<label for="tokenInput" class="visually-hidden">Text to encrypt</label>
<textarea id="tokenInput" rows="2" placeholder="Enter text to encrypt or use the example below" autocomplete="off"></textarea>
<div style="display: flex; gap: calc(var(--spacing-unit) * 1); margin-bottom: calc(var(--spacing-unit) * 2); flex-wrap: wrap; align-items: center;">
<button id="btnWatermarked" class="btn" style="min-width: auto; padding: calc(var(--spacing-unit) * 1) calc(var(--spacing-unit) * 1.5); font-size: 1rem;">✨ Load sample watermarked</button>
</div>
<div class="controls" style="margin-top: auto;">
<button id="btnEncrypt" class="btn" disabled>🛡️ Encrypt</button>
<span id="encryptSpin" class="loader" hidden aria-label="Encrypting"></span>
<span id="encIcon" hidden aria-label="Encrypted">🔒</span>
<span id="encStatus" class="status"></span>
</div>
</div>
</section>
<!-- 3 Send -->
<section class="card" aria-labelledby="step3">
<h2 id="step3">3. Send to server</h2>
<div class="card-content">
<div class="processing-note" id="processingNote" hidden>
<strong>Estimated processing time:</strong> <span id="estimatedTime">0</span> seconds
</div>
<div>
<p id="srvStatus" class="status" aria-live="polite">Waiting for encrypted data… (server processing can take several minutes)</p>
<p id="srvComputing" class="status" aria-live="polite" hidden>Server computing…</p>
</div>
<div class="progress-container" id="progressContainer" hidden>
<div class="progress-bar" id="progressBar"></div>
</div>
<div class="controls" style="margin-top: auto;">
<button id="btnSend" class="btn" disabled>📡 Send</button>
<span id="spin" class="loader" hidden aria-label="Working"></span>
</div>
</div>
</section>
<!-- 4 Result -->
<section class="card" aria-labelledby="step4">
<h2 id="step4">4. Result</h2>
<div class="card-content">
<p id="decResult" aria-live="polite" style="flex: 1;"></p>
<div class="controls" style="margin-top: auto;">
<button id="btnDecrypt" class="btn" disabled>🔓 Decrypt</button>
</div>
<div class="disclaimer" style="margin-top: calc(var(--spacing-unit) * 3); font-size: 0.9rem; color: var(--black); opacity: 0.7; line-height: 1.5;">
<p><strong>Note about reliability:</strong> AI text detection works best with longer passages (100+ tokens). In this demo, input is limited to 16 tokens for performance, which may affect accuracy. Reliability improves significantly with 10+ tokens but is less reliable on very short snippets.</p>
</div>
</div>
</section>
</div>
</main>
<footer>© 2025 Zama — Demo only, not for production use.</footer>
<script type="module" src="wasm-demo.js"></script>
<script>
// Hide tokenizer status on load
window.addEventListener('DOMContentLoaded', () => {
const tokenizerStatus = document.getElementById('tokenizerStatus');
if (tokenizerStatus) {
tokenizerStatus.style.display = 'none';
}
});
</script>
</body>
</html>