feat: ui + docs: для жюри

This commit is contained in:
Максименко Никита Владимирович
2026-09-23 23:12:10 +03:00
parent 954d772fa2
commit 41e77e4ca1
7 changed files with 796 additions and 5 deletions
+308
View File
@@ -0,0 +1,308 @@
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>PD Guard</title>
<style>
:root {
--bg: #f3efe6;
--ink: #1c1915;
--muted: #6d655c;
--line: #ddd4c6;
--card: #fffdf8;
--accent: #0e6b52;
--accent-ink: #083d30;
--danger: #8d2e2e;
--danger-bg: #f8ecec;
}
* { box-sizing: border-box; }
body {
margin: 0;
min-height: 100vh;
color: var(--ink);
background:
radial-gradient(1200px 500px at 10% -10%, #e7f3ee 0%, transparent 55%),
var(--bg);
font: 16px/1.45 "Segoe UI", system-ui, sans-serif;
}
main {
width: min(760px, calc(100% - 32px));
margin: 0 auto;
padding: 48px 0 64px;
}
header h1 {
margin: 0;
font-size: 32px;
letter-spacing: -0.03em;
}
header p {
margin: 8px 0 0;
color: var(--muted);
}
form {
margin-top: 28px;
display: grid;
gap: 18px;
}
fieldset {
margin: 0;
padding: 0;
border: 0;
}
legend {
padding: 0;
margin-bottom: 8px;
font-size: 13px;
font-weight: 650;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
.modes {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 10px;
}
.mode {
display: block;
padding: 14px 14px 12px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--card);
cursor: pointer;
}
.mode:has(input:checked) {
border-color: var(--accent);
box-shadow: inset 0 0 0 1px var(--accent);
}
.mode:has(input:focus-visible) {
outline: 2px solid var(--accent);
outline-offset: 2px;
}
.mode input {
position: absolute;
opacity: 0;
pointer-events: none;
}
.mode strong {
display: block;
font-size: 14px;
letter-spacing: 0.04em;
}
.mode span {
display: block;
margin-top: 4px;
color: var(--muted);
font-size: 13px;
}
label.field {
display: grid;
gap: 8px;
font-size: 13px;
font-weight: 650;
letter-spacing: 0.04em;
text-transform: uppercase;
color: var(--muted);
}
textarea {
width: 100%;
min-height: 160px;
resize: vertical;
padding: 14px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--card);
color: var(--ink);
font: 15px/1.5 "Segoe UI", system-ui, sans-serif;
text-transform: none;
letter-spacing: 0;
font-weight: 400;
}
textarea:focus {
outline: 2px solid var(--accent);
outline-offset: 1px;
border-color: transparent;
}
button {
justify-self: start;
padding: 12px 18px;
border: 0;
border-radius: 10px;
background: var(--accent);
color: #f7fffb;
font: 650 15px/1 "Segoe UI", system-ui, sans-serif;
cursor: pointer;
}
button:hover { background: var(--accent-ink); }
button:disabled {
opacity: 0.6;
cursor: progress;
}
#result {
min-height: 120px;
margin: 0;
padding: 14px;
border: 1px solid var(--line);
border-radius: 12px;
background: var(--card);
white-space: pre-wrap;
word-break: break-word;
font: 15px/1.5 ui-monospace, "Cascadia Mono", Consolas, monospace;
text-transform: none;
letter-spacing: 0;
font-weight: 400;
}
#result.error {
color: var(--danger);
background: var(--danger-bg);
border-color: #e4c8c8;
}
#result:empty::before {
content: "Результат появится здесь";
color: var(--muted);
font-family: "Segoe UI", system-ui, sans-serif;
}
@media (max-width: 640px) {
.modes { grid-template-columns: 1fr; }
main { padding-top: 28px; }
}
</style>
</head>
<body>
<main>
<header>
<h1>PD Guard</h1>
<p>Маскирование персональных данных перед обработкой.</p>
</header>
<form id="form">
<fieldset>
<legend>Режим</legend>
<div class="modes">
<label class="mode">
<input type="radio" name="mode" value="default" checked>
<strong>MASK</strong>
<span>Звёздочки, края номеров видны</span>
</label>
<label class="mode">
<input type="radio" name="mode" value="strict">
<strong>STRICT</strong>
<span>Сплошные звёздочки</span>
</label>
<label class="mode">
<input type="radio" name="mode" value="crm">
<strong>TOKEN</strong>
<span>Токены вида [FIO_1]</span>
</label>
<label class="mode">
<input type="radio" name="mode" value="analytics">
<strong>SYNTHETIC</strong>
<span>Правдоподобная подмена</span>
</label>
</div>
</fieldset>
<label class="field">
Текст
<textarea id="payload" name="payload" required>Клиент Иванов Иван Иванович, паспорт 4509 123456, тел +7 916 123-45-67</textarea>
</label>
<button type="submit" id="run">Обработать</button>
<label class="field">
Результат
<pre id="result" aria-live="polite"></pre>
</label>
</form>
</main>
<script>
const form = document.getElementById("form");
const payload = document.getElementById("payload");
const result = document.getElementById("result");
const run = document.getElementById("run");
form.addEventListener("submit", async (event) => {
event.preventDefault();
const text = payload.value;
if (!text.trim()) {
show("Введите текст", true);
return;
}
const systemId = new FormData(form).get("mode");
run.disabled = true;
show("");
try {
const response = await fetch("/process", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-System-Id": systemId
},
body: JSON.stringify({
payload: text,
payload_id: crypto.randomUUID()
})
});
if (response.status === 429) {
show("Сервис перегружен. Повторите через секунду.", true);
return;
}
const raw = await response.text();
let message = raw;
try {
const data = JSON.parse(raw);
if (data && typeof data.result === "string") message = data.result;
} catch (ignored) {
/* ответ не JSON — показываем как есть */
}
if (!response.ok) {
show(message || "Запрос отклонён (" + response.status + ")", true);
return;
}
show(message);
} catch (error) {
show("Не удалось связаться с сервисом.", true);
} finally {
run.disabled = false;
}
});
function show(text, isError) {
result.textContent = text;
result.classList.toggle("error", Boolean(isError));
}
</script>
</body>
</html>