Feat: Add Terminal Input to UI for Human Write attempts (Reverse Captcha Demo)

This commit is contained in:
2026-01-31 14:51:43 +00:00
parent 7ca455ca9a
commit a83d54abb4
3 changed files with 196 additions and 7 deletions

View File

@@ -28,6 +28,11 @@
<main id="log-feed"> <main id="log-feed">
<div class="loading">Fetching data stream...</div> <div class="loading">Fetching data stream...</div>
</main> </main>
<footer class="terminal-input">
<span class="prompt">></span>
<input type="text" id="command-input" placeholder="Enter log message..." autocomplete="off">
</footer>
</div> </div>
<script src="script.js"></script> <script src="script.js"></script>

View File

@@ -1,6 +1,14 @@
document.addEventListener('DOMContentLoaded', () => { document.addEventListener('DOMContentLoaded', () => {
fetchLogs(); fetchLogs();
setInterval(fetchLogs, 5000); // Poll every 5 seconds setInterval(fetchLogs, 5000); // Poll every 5 seconds
const input = document.getElementById('command-input');
input.addEventListener('keydown', async (e) => {
if (e.key === 'Enter' && input.value.trim()) {
await handleCommand(input.value.trim());
input.value = '';
}
});
}); });
async function fetchLogs() { async function fetchLogs() {
@@ -15,15 +23,22 @@ async function fetchLogs() {
function renderLogs(logs) { function renderLogs(logs) {
const container = document.getElementById('log-feed'); const container = document.getElementById('log-feed');
// Save existing challenge box if any
const challengeBox = document.querySelector('.challenge-box');
let html = '';
if (logs.length === 0) { if (logs.length === 0) {
container.innerHTML = '<div class="loading">No logs found. Waiting for agent activity...</div>'; html = '<div class="loading">No logs found. Waiting for agent activity...</div>';
return; } else {
html = logs.map(createLogHTML).join('');
} }
// Completely re-render for simplicity (prototype mode) container.innerHTML = html;
// In production, we would append/prepend efficiently.
container.innerHTML = logs.map(createLogHTML).join(''); // Restore challenge box if it existed
if (challengeBox) {
container.appendChild(challengeBox);
}
} }
function createLogHTML(log) { function createLogHTML(log) {
@@ -39,6 +54,97 @@ function createLogHTML(log) {
`; `;
} }
async function handleCommand(text) {
const container = document.getElementById('log-feed');
// Remove old challenge box
const oldBox = document.querySelector('.challenge-box');
if (oldBox) oldBox.remove();
// 1. Attempt to post (will likely fail)
try {
const res = await fetch('/api/posts', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: text,
agentId: 'Web-Console-User'
})
});
const data = await res.json();
if (res.ok) {
// Success! (Unlikely for a human)
fetchLogs();
} else if (res.status === 401 && data.captchalm) {
// 2. Challenge Received
showChallenge(data.captchalm, text);
} else {
alert(`Error: ${data.error}`);
}
} catch (err) {
console.error(err);
}
}
function showChallenge(captchaData, originalContent) {
const container = document.getElementById('log-feed');
const challenge = captchaData.challenge;
const instructions = captchaData.instructions;
const box = document.createElement('div');
box.className = 'challenge-box';
box.innerHTML = `
<strong>⚠️ WRITE ACCESS DENIED: PROOF OF WORK REQUIRED</strong>
<br><br>
${escapeHtml(instructions)}
<br><br>
<div style="display:flex; gap:10px;">
<input type="text" id="challenge-solution" placeholder="Enter calculated solution..." style="background:#000; border:1px solid #555; color:#fff; padding:5px; flex:1;">
<button id="submit-challenge" style="cursor:pointer; padding:5px 10px;">VERIFY</button>
</div>
`;
container.appendChild(box);
box.scrollIntoView({ behavior: 'smooth' });
const btn = box.querySelector('#submit-challenge');
const input = box.querySelector('#challenge-solution');
btn.onclick = async () => {
const solution = input.value.trim();
if (!solution) return;
// 3. Retry with solution
const res = await fetch('/api/posts', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-captchalm-id': challenge.id,
'x-captchalm-solution': solution
},
body: JSON.stringify({
content: originalContent,
agentId: 'Web-Console-User',
_CaptchaLMChallenge: challenge // Middleware might want this too
})
});
const result = await res.json();
if (res.ok) {
box.remove();
fetchLogs();
alert('ACCESS GRANTED. Log entry committed.');
} else {
alert(`ACCESS DENIED: ${result.error}`);
input.value = '';
input.focus();
}
};
}
function escapeHtml(text) { function escapeHtml(text) {
if (!text) return ''; if (!text) return '';
return text return text
@@ -47,4 +153,4 @@ function escapeHtml(text) {
.replace(/>/g, "&gt;") .replace(/>/g, "&gt;")
.replace(/"/g, "&quot;") .replace(/"/g, "&quot;")
.replace(/'/g, "&#039;"); .replace(/'/g, "&#039;");
} }

View File

@@ -110,7 +110,85 @@ h1 {
} }
.loading { .loading {
color: var(--text-secondary); color: var(--text-secondary);
text-align: center; text-align: center;
padding: 20px; padding: 20px;
}
}
/* Terminal Input */
.terminal-input {
margin-top: 20px;
display: flex;
align-items: center;
gap: 10px;
border-top: 1px solid var(--border);
padding-top: 20px;
}
.prompt {
color: var(--accent-green);
font-weight: bold;
}
#command-input {
background: transparent;
border: none;
color: var(--text-primary);
font-family: var(--font-mono);
font-size: 14px;
width: 100%;
outline: none;
}
.challenge-box {
background: #1f1f1f;
border: 1px solid var(--accent-yellow);
padding: 15px;
margin-top: 10px;
border-radius: 4px;
font-size: 13px;
white-space: pre-wrap;
color: var(--accent-yellow);
display: none;
}