p2pwiki-ai/web/index.html

1193 lines
40 KiB
HTML

<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>P2P Wiki AI</title>
<script defer src="https://rdata.online/collect.js" data-website-id="81ed3b8a-ab76-4286-8602-03d15edf237c"></script>
<style>
:root {
--bg-primary: #1a1a2e;
--bg-secondary: #16213e;
--bg-tertiary: #0f3460;
--text-primary: #e8e8e8;
--text-secondary: #a0a0a0;
--accent: #e94560;
--accent-hover: #ff6b6b;
--success: #4ecdc4;
--border: #2a2a4a;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
background: var(--bg-primary);
color: var(--text-primary);
min-height: 100vh;
}
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20px 0;
border-bottom: 1px solid var(--border);
margin-bottom: 30px;
}
h1 {
font-size: 1.8em;
font-weight: 600;
}
h1 span {
color: var(--accent);
}
.tabs {
display: flex;
gap: 10px;
}
.tab {
padding: 10px 20px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
cursor: pointer;
transition: all 0.2s;
}
.tab:hover, .tab.active {
background: var(--bg-tertiary);
border-color: var(--accent);
}
.panel {
display: none;
}
.panel.active {
display: block;
}
/* Chat Panel */
.chat-container {
display: flex;
flex-direction: column;
height: calc(100vh - 200px);
background: var(--bg-secondary);
border-radius: 12px;
overflow: hidden;
}
.chat-messages {
flex: 1;
overflow-y: auto;
padding: 20px;
}
.message {
margin-bottom: 20px;
max-width: 80%;
}
.message.user {
margin-left: auto;
}
.message-content {
padding: 15px;
border-radius: 12px;
line-height: 1.6;
}
.message.user .message-content {
background: var(--bg-tertiary);
}
.message.assistant .message-content {
background: var(--bg-primary);
border: 1px solid var(--border);
}
.message-sources {
margin-top: 10px;
padding: 10px;
background: rgba(233, 69, 96, 0.1);
border-radius: 8px;
font-size: 0.9em;
}
.message-sources h4 {
color: var(--accent);
margin-bottom: 5px;
}
.source-tag {
display: inline-block;
padding: 3px 8px;
margin: 2px;
background: var(--bg-tertiary);
border-radius: 4px;
font-size: 0.85em;
}
.chat-input {
display: flex;
gap: 10px;
padding: 20px;
background: var(--bg-primary);
border-top: 1px solid var(--border);
}
.chat-input input {
flex: 1;
padding: 15px;
background: var(--bg-secondary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 1em;
}
.chat-input input:focus {
outline: none;
border-color: var(--accent);
}
.chat-input button {
padding: 15px 30px;
background: var(--accent);
border: none;
border-radius: 8px;
color: white;
font-weight: 600;
cursor: pointer;
transition: background 0.2s;
}
.chat-input button:hover {
background: var(--accent-hover);
}
.chat-input button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
/* Ingress Panel */
.ingress-container {
background: var(--bg-secondary);
border-radius: 12px;
padding: 30px;
}
.ingress-form {
display: flex;
gap: 10px;
margin-bottom: 30px;
}
.ingress-form input {
flex: 1;
padding: 15px;
background: var(--bg-primary);
border: 1px solid var(--border);
border-radius: 8px;
color: var(--text-primary);
font-size: 1em;
}
.ingress-form input:focus {
outline: none;
border-color: var(--accent);
}
.ingress-form button {
padding: 15px 30px;
background: var(--success);
border: none;
border-radius: 8px;
color: var(--bg-primary);
font-weight: 600;
cursor: pointer;
transition: opacity 0.2s;
}
.ingress-form button:hover {
opacity: 0.9;
}
.ingress-form button:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ingress-result {
background: var(--bg-primary);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.ingress-result h3 {
margin-bottom: 15px;
color: var(--accent);
}
.result-stats {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 15px;
margin-bottom: 20px;
}
.stat {
background: var(--bg-secondary);
padding: 15px;
border-radius: 8px;
text-align: center;
}
.stat-value {
font-size: 2em;
font-weight: bold;
color: var(--success);
}
.stat-label {
color: var(--text-secondary);
font-size: 0.9em;
}
/* Review Panel */
.review-container {
background: var(--bg-secondary);
border-radius: 12px;
padding: 30px;
}
.review-item {
background: var(--bg-primary);
border-radius: 8px;
padding: 20px;
margin-bottom: 20px;
}
.review-item h3 {
margin-bottom: 10px;
}
.review-meta {
color: var(--text-secondary);
font-size: 0.9em;
margin-bottom: 15px;
}
.review-section {
margin-top: 20px;
padding-top: 20px;
border-top: 1px solid var(--border);
}
.review-section h4 {
margin-bottom: 10px;
color: var(--accent);
}
.match-item, .draft-item {
background: var(--bg-secondary);
padding: 15px;
border-radius: 8px;
margin-bottom: 10px;
}
.match-item .title, .draft-item .title {
font-weight: 600;
margin-bottom: 5px;
}
.match-item .score {
color: var(--success);
}
.action-buttons {
display: flex;
gap: 10px;
margin-top: 10px;
}
.btn-approve {
padding: 8px 16px;
background: var(--success);
border: none;
border-radius: 4px;
color: var(--bg-primary);
cursor: pointer;
}
.btn-reject {
padding: 8px 16px;
background: var(--accent);
border: none;
border-radius: 4px;
color: white;
cursor: pointer;
}
.loading {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid var(--text-secondary);
border-top-color: var(--accent);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
.empty-state {
text-align: center;
padding: 50px;
color: var(--text-secondary);
}
/* Wiki Drafts Panel */
.drafts-container {
background: var(--bg-secondary);
border-radius: 12px;
padding: 30px;
}
.auth-status {
display: flex;
align-items: center;
gap: 10px;
padding: 15px;
background: var(--bg-primary);
border-radius: 8px;
margin-bottom: 20px;
}
.auth-status.authenticated {
border-left: 4px solid var(--success);
}
.auth-status.not-authenticated {
border-left: 4px solid var(--accent);
}
.auth-badge {
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 600;
}
.auth-badge.admin {
background: var(--success);
color: var(--bg-primary);
}
.auth-badge.user {
background: var(--bg-tertiary);
color: var(--text-primary);
}
.draft-card {
background: var(--bg-primary);
border-radius: 8px;
padding: 20px;
margin-bottom: 15px;
border: 1px solid var(--border);
transition: border-color 0.2s;
}
.draft-card:hover {
border-color: var(--accent);
}
.draft-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 10px;
}
.draft-title {
font-size: 1.2em;
font-weight: 600;
color: var(--text-primary);
}
.draft-title a {
color: inherit;
text-decoration: none;
}
.draft-title a:hover {
color: var(--accent);
}
.draft-meta {
color: var(--text-secondary);
font-size: 0.85em;
margin-bottom: 15px;
}
.draft-preview {
background: var(--bg-secondary);
border-radius: 6px;
padding: 15px;
max-height: 200px;
overflow-y: auto;
font-size: 0.9em;
margin-bottom: 15px;
display: none;
}
.draft-preview.show {
display: block;
}
.draft-actions {
display: flex;
gap: 10px;
align-items: center;
}
.btn-preview {
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
cursor: pointer;
font-size: 0.9em;
transition: all 0.2s;
}
.btn-preview:hover {
background: var(--bg-secondary);
border-color: var(--accent);
}
.btn-approve-draft {
padding: 10px 24px;
background: var(--success);
border: none;
border-radius: 6px;
color: var(--bg-primary);
font-weight: 600;
cursor: pointer;
font-size: 0.95em;
transition: all 0.2s;
}
.btn-approve-draft:hover {
opacity: 0.9;
transform: translateY(-1px);
}
.btn-approve-draft:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none;
}
.btn-view-wiki {
padding: 8px 16px;
background: transparent;
border: 1px solid var(--accent);
border-radius: 6px;
color: var(--accent);
cursor: pointer;
font-size: 0.9em;
text-decoration: none;
transition: all 0.2s;
}
.btn-view-wiki:hover {
background: var(--accent);
color: white;
}
/* Confirmation Modal */
.modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: all 0.2s;
}
.modal-overlay.show {
opacity: 1;
visibility: visible;
}
.modal {
background: var(--bg-secondary);
border-radius: 12px;
padding: 30px;
max-width: 500px;
width: 90%;
transform: scale(0.9);
transition: transform 0.2s;
}
.modal-overlay.show .modal {
transform: scale(1);
}
.modal h3 {
margin-bottom: 15px;
color: var(--text-primary);
}
.modal p {
color: var(--text-secondary);
margin-bottom: 20px;
line-height: 1.6;
}
.modal-actions {
display: flex;
gap: 10px;
justify-content: flex-end;
}
.btn-cancel {
padding: 10px 20px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
cursor: pointer;
}
.btn-confirm {
padding: 10px 20px;
background: var(--success);
border: none;
border-radius: 6px;
color: var(--bg-primary);
font-weight: 600;
cursor: pointer;
}
.refresh-btn {
padding: 8px 16px;
background: var(--bg-tertiary);
border: 1px solid var(--border);
border-radius: 6px;
color: var(--text-primary);
cursor: pointer;
margin-left: auto;
}
.refresh-btn:hover {
border-color: var(--accent);
}
.drafts-header {
display: flex;
align-items: center;
margin-bottom: 20px;
}
.drafts-header h2 {
margin-right: 20px;
}
.draft-count {
background: var(--accent);
color: white;
padding: 4px 12px;
border-radius: 20px;
font-size: 0.85em;
font-weight: 600;
}
/* Markdown-like formatting */
.message-content p { margin-bottom: 10px; }
.message-content ul, .message-content ol { margin-left: 20px; margin-bottom: 10px; }
.message-content code { background: var(--bg-tertiary); padding: 2px 6px; border-radius: 4px; }
.message-content pre { background: var(--bg-tertiary); padding: 15px; border-radius: 8px; overflow-x: auto; }
</style>
</head>
<body>
<div class="container">
<header>
<h1>P2P Wiki <span>AI</span></h1>
<div class="tabs">
<div class="tab active" data-panel="chat">Chat</div>
<div class="tab" data-panel="ingress">Ingress</div>
<div class="tab" data-panel="review">Review Queue</div>
<div class="tab" data-panel="drafts">Wiki Drafts</div>
</div>
</header>
<!-- Chat Panel -->
<div id="chat" class="panel active">
<div class="chat-container">
<div class="chat-messages" id="chatMessages">
<div class="message assistant">
<div class="message-content">
<p>Welcome to the P2P Wiki AI assistant! I can help you explore the P2P Foundation Wiki's knowledge about peer-to-peer culture, commons-based peer production, alternative economics, and collaborative governance.</p>
<p>Ask me anything about these topics!</p>
</div>
</div>
</div>
<div class="chat-input">
<input type="text" id="chatInput" placeholder="Ask about P2P, commons, cooperative economics..." />
<button id="chatSend">Send</button>
</div>
</div>
</div>
<!-- Ingress Panel -->
<div id="ingress" class="panel">
<div class="ingress-container">
<h2>Article Ingress</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px;">
Drop an article URL to analyze it for wiki content. The AI will identify relevant topics,
find matching wiki articles for citations, and draft new articles.
</p>
<div class="ingress-form">
<input type="url" id="ingressUrl" placeholder="https://example.com/article-about-commons" />
<button id="ingressSubmit">Process Article</button>
</div>
<div id="ingressResult"></div>
</div>
</div>
<!-- Review Panel -->
<div id="review" class="panel">
<div class="review-container">
<h2>Review Queue</h2>
<p style="color: var(--text-secondary); margin-bottom: 20px;">
Review and approve AI-generated wiki content before it's added to the wiki.
</p>
<div id="reviewItems">
<div class="empty-state">Loading review items...</div>
</div>
</div>
</div>
<!-- Wiki Drafts Panel -->
<div id="drafts" class="panel">
<div class="drafts-container">
<div class="drafts-header">
<h2>Wiki Drafts</h2>
<span class="draft-count" id="draftCount">0</span>
<button class="refresh-btn" onclick="loadWikiDrafts()">Refresh</button>
</div>
<div id="authStatus" class="auth-status not-authenticated">
<span class="loading"></span> Checking authentication...
</div>
<p style="color: var(--text-secondary); margin-bottom: 20px;">
Review and approve draft articles to publish them to the main wiki namespace.
</p>
<div id="draftsList">
<div class="empty-state">Loading drafts from wiki...</div>
</div>
</div>
</div>
<!-- Confirmation Modal -->
<div id="confirmModal" class="modal-overlay">
<div class="modal">
<h3>Approve Draft Article?</h3>
<p id="confirmMessage">
This will move the draft to the main wiki namespace and make it publicly visible.
</p>
<div class="modal-actions">
<button class="btn-cancel" onclick="closeModal()">Cancel</button>
<button class="btn-confirm" id="confirmBtn" onclick="confirmApproval()">Approve & Publish</button>
</div>
</div>
</div>
</div>
<script>
const API_BASE = ''; // Same origin
// Tab switching
document.querySelectorAll('.tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.tab').forEach(t => t.classList.remove('active'));
document.querySelectorAll('.panel').forEach(p => p.classList.remove('active'));
tab.classList.add('active');
document.getElementById(tab.dataset.panel).classList.add('active');
// Load review items when switching to review tab
if (tab.dataset.panel === 'review') {
loadReviewItems();
}
});
});
// Chat functionality
const chatMessages = document.getElementById('chatMessages');
const chatInput = document.getElementById('chatInput');
const chatSend = document.getElementById('chatSend');
function addMessage(content, role, sources = []) {
const div = document.createElement('div');
div.className = `message ${role}`;
let html = `<div class="message-content">${formatMessage(content)}</div>`;
if (sources.length > 0) {
html += `<div class="message-sources">
<h4>Sources</h4>
${sources.map(s => `<span class="source-tag">${s.title}</span>`).join('')}
</div>`;
}
div.innerHTML = html;
chatMessages.appendChild(div);
chatMessages.scrollTop = chatMessages.scrollHeight;
}
function formatMessage(text) {
// Basic markdown-like formatting
return text
.replace(/\n\n/g, '</p><p>')
.replace(/\n/g, '<br>')
.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>')
.replace(/\*(.+?)\*/g, '<em>$1</em>')
.replace(/`(.+?)`/g, '<code>$1</code>');
}
async function sendChat() {
const query = chatInput.value.trim();
if (!query) return;
chatInput.value = '';
chatSend.disabled = true;
addMessage(query, 'user');
// Show loading
const loadingDiv = document.createElement('div');
loadingDiv.className = 'message assistant';
loadingDiv.innerHTML = '<div class="message-content"><span class="loading"></span> Thinking...</div>';
chatMessages.appendChild(loadingDiv);
chatMessages.scrollTop = chatMessages.scrollHeight;
try {
const response = await fetch(`${API_BASE}/chat`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ query, n_results: 5 })
});
const data = await response.json();
chatMessages.removeChild(loadingDiv);
if (response.ok) {
addMessage(data.answer, 'assistant', data.sources);
} else {
addMessage(`Error: ${data.detail || 'Something went wrong'}`, 'assistant');
}
} catch (error) {
chatMessages.removeChild(loadingDiv);
addMessage(`Error: ${error.message}`, 'assistant');
}
chatSend.disabled = false;
chatInput.focus();
}
chatSend.addEventListener('click', sendChat);
chatInput.addEventListener('keypress', (e) => {
if (e.key === 'Enter') sendChat();
});
// Ingress functionality
const ingressUrl = document.getElementById('ingressUrl');
const ingressSubmit = document.getElementById('ingressSubmit');
const ingressResult = document.getElementById('ingressResult');
async function processIngress() {
const url = ingressUrl.value.trim();
if (!url) return;
ingressSubmit.disabled = true;
ingressSubmit.textContent = 'Processing...';
ingressResult.innerHTML = `
<div class="ingress-result">
<h3>Processing Article</h3>
<p><span class="loading"></span> Scraping and analyzing content...</p>
</div>
`;
try {
const response = await fetch(`${API_BASE}/ingress`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url })
});
const data = await response.json();
if (response.ok) {
ingressResult.innerHTML = `
<div class="ingress-result">
<h3>Analysis Complete: ${data.scraped_title || 'Article'}</h3>
<div class="result-stats">
<div class="stat">
<div class="stat-value">${data.topics_found}</div>
<div class="stat-label">Topics Found</div>
</div>
<div class="stat">
<div class="stat-value">${data.wiki_matches}</div>
<div class="stat-label">Wiki Matches</div>
</div>
<div class="stat">
<div class="stat-value">${data.drafts_generated}</div>
<div class="stat-label">Drafts Generated</div>
</div>
</div>
<p style="color: var(--success);">
Results added to review queue. Check the Review tab to approve or reject suggestions.
</p>
</div>
`;
} else {
ingressResult.innerHTML = `
<div class="ingress-result">
<h3 style="color: var(--accent);">Error</h3>
<p>${data.detail || 'Failed to process article'}</p>
</div>
`;
}
} catch (error) {
ingressResult.innerHTML = `
<div class="ingress-result">
<h3 style="color: var(--accent);">Error</h3>
<p>${error.message}</p>
</div>
`;
}
ingressSubmit.disabled = false;
ingressSubmit.textContent = 'Process Article';
}
ingressSubmit.addEventListener('click', processIngress);
ingressUrl.addEventListener('keypress', (e) => {
if (e.key === 'Enter') processIngress();
});
// Review functionality
const reviewItems = document.getElementById('reviewItems');
async function loadReviewItems() {
try {
const response = await fetch(`${API_BASE}/review`);
const data = await response.json();
if (data.count === 0) {
reviewItems.innerHTML = '<div class="empty-state">No items in the review queue.</div>';
return;
}
reviewItems.innerHTML = data.items.map(item => `
<div class="review-item">
<h3>${item.scraped?.title || 'Unknown Article'}</h3>
<div class="review-meta">
Source: <a href="${item.scraped?.url}" target="_blank">${item.scraped?.domain}</a>
| Processed: ${new Date(item.timestamp).toLocaleString()}
</div>
${item.wiki_matches?.length > 0 ? `
<div class="review-section">
<h4>Suggested Citations (${item.wiki_matches.length})</h4>
${item.wiki_matches.map((match, i) => `
<div class="match-item" ${match.approved ? 'style="opacity: 0.5"' : ''}>
<div class="title">${match.title}</div>
<div class="score">Relevance: ${(match.relevance_score * 100).toFixed(0)}%</div>
<div>${match.suggested_citation}</div>
${!match.approved && !match.rejected ? `
<div class="action-buttons">
<button class="btn-approve" onclick="reviewAction('${item._filepath}', 'match', ${i}, 'approve')">Approve</button>
<button class="btn-reject" onclick="reviewAction('${item._filepath}', 'match', ${i}, 'reject')">Reject</button>
</div>
` : `<em>${match.approved ? 'Approved' : 'Rejected'}</em>`}
</div>
`).join('')}
</div>
` : ''}
${item.draft_articles?.length > 0 ? `
<div class="review-section">
<h4>Draft Articles (${item.draft_articles.length})</h4>
${item.draft_articles.map((draft, i) => `
<div class="draft-item" ${draft.approved ? 'style="opacity: 0.5"' : ''}>
<div class="title">${draft.title}</div>
<div style="color: var(--text-secondary); font-size: 0.9em; margin-bottom: 10px;">
${draft.summary || ''}
</div>
<details>
<summary style="cursor: pointer; color: var(--accent);">View Draft Content</summary>
<pre style="margin-top: 10px; white-space: pre-wrap; font-size: 0.85em;">${draft.content}</pre>
</details>
${!draft.approved && !draft.rejected ? `
<div class="action-buttons">
<button class="btn-approve" onclick="reviewAction('${item._filepath}', 'draft', ${i}, 'approve')">Approve</button>
<button class="btn-reject" onclick="reviewAction('${item._filepath}', 'draft', ${i}, 'reject')">Reject</button>
</div>
` : `<em>${draft.approved ? 'Approved' : 'Rejected'}</em>`}
</div>
`).join('')}
</div>
` : ''}
</div>
`).join('');
} catch (error) {
reviewItems.innerHTML = `<div class="empty-state">Error loading review items: ${error.message}</div>`;
}
}
async function reviewAction(filepath, itemType, itemIndex, action) {
try {
const response = await fetch(`${API_BASE}/review/action`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
filepath,
item_type: itemType,
item_index: itemIndex,
action
})
});
if (response.ok) {
loadReviewItems(); // Refresh the list
} else {
const data = await response.json();
alert(`Error: ${data.detail || 'Action failed'}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
}
}
// Make reviewAction available globally
window.reviewAction = reviewAction;
// Wiki Drafts functionality
let currentAuthStatus = null;
let pendingApprovalTitle = null;
async function checkAuthStatus() {
const authStatus = document.getElementById('authStatus');
try {
const response = await fetch(`${API_BASE}/wiki/auth`);
currentAuthStatus = await response.json();
if (currentAuthStatus.authenticated) {
const badges = [];
if (currentAuthStatus.is_admin) {
badges.push('<span class="auth-badge admin">Admin</span>');
}
if (currentAuthStatus.can_move) {
badges.push('<span class="auth-badge user">Can Approve</span>');
}
authStatus.className = 'auth-status authenticated';
authStatus.innerHTML = `
<strong>Logged in as:</strong> ${currentAuthStatus.username}
${badges.join(' ')}
`;
} else {
authStatus.className = 'auth-status not-authenticated';
authStatus.innerHTML = `
<strong style="color: var(--accent);">Not authenticated</strong>
<span style="color: var(--text-secondary);">— Wiki cookies required for approval</span>
`;
}
} catch (error) {
authStatus.className = 'auth-status not-authenticated';
authStatus.innerHTML = `
<strong style="color: var(--accent);">Error checking auth:</strong> ${error.message}
`;
}
}
async function loadWikiDrafts() {
const draftsList = document.getElementById('draftsList');
const draftCount = document.getElementById('draftCount');
draftsList.innerHTML = '<div class="empty-state"><span class="loading"></span> Loading drafts...</div>';
try {
const response = await fetch(`${API_BASE}/wiki/drafts`);
const data = await response.json();
draftCount.textContent = data.count;
if (data.count === 0) {
draftsList.innerHTML = '<div class="empty-state">No draft articles pending review.</div>';
return;
}
draftsList.innerHTML = data.drafts.map(draft => {
const title = draft.title.replace('Draft:', '');
const wikiUrl = `https://wiki.p2pfoundation.net/${draft.title.replace(/ /g, '_')}`;
const timestamp = draft.timestamp ? new Date(draft.timestamp).toLocaleString() : 'Unknown';
return `
<div class="draft-card" id="draft-${encodeURIComponent(title)}">
<div class="draft-header">
<div class="draft-title">
<a href="${wikiUrl}" target="_blank">${title}</a>
</div>
</div>
<div class="draft-meta">
Created: ${timestamp}
</div>
<div class="draft-actions">
<a href="${wikiUrl}" target="_blank" class="btn-view-wiki">View on Wiki</a>
<button class="btn-approve-draft"
onclick="showApprovalModal('${title.replace(/'/g, "\\'")}')"
${!currentAuthStatus?.can_move ? 'disabled title="Login required to approve"' : ''}>
Approve & Publish
</button>
</div>
</div>
`;
}).join('');
} catch (error) {
draftsList.innerHTML = `<div class="empty-state" style="color: var(--accent);">
Error loading drafts: ${error.message}
</div>`;
}
}
function showApprovalModal(title) {
pendingApprovalTitle = title;
const modal = document.getElementById('confirmModal');
const message = document.getElementById('confirmMessage');
message.innerHTML = `
This will publish <strong>"${title}"</strong> to the main wiki namespace.<br><br>
The article will become publicly visible at:<br>
<code>wiki.p2pfoundation.net/${title.replace(/ /g, '_')}</code>
`;
modal.classList.add('show');
}
function closeModal() {
document.getElementById('confirmModal').classList.remove('show');
pendingApprovalTitle = null;
}
async function confirmApproval() {
if (!pendingApprovalTitle) return;
const confirmBtn = document.getElementById('confirmBtn');
const originalText = confirmBtn.textContent;
confirmBtn.disabled = true;
confirmBtn.textContent = 'Publishing...';
try {
const response = await fetch(`${API_BASE}/wiki/approve`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ title: pendingApprovalTitle })
});
const data = await response.json();
if (response.ok && data.success) {
// Success! Remove the card and update count
const card = document.getElementById(`draft-${encodeURIComponent(pendingApprovalTitle)}`);
if (card) {
card.style.background = 'rgba(78, 205, 196, 0.2)';
card.innerHTML = `
<div class="draft-header">
<div class="draft-title" style="color: var(--success);">
${pendingApprovalTitle} — Published!
</div>
</div>
<div class="draft-meta">
<a href="${data.url}" target="_blank" style="color: var(--success);">
View published article →
</a>
</div>
`;
}
// Update count
const count = document.getElementById('draftCount');
count.textContent = Math.max(0, parseInt(count.textContent) - 1);
closeModal();
} else {
alert(`Error: ${data.detail || data.error || 'Approval failed'}`);
}
} catch (error) {
alert(`Error: ${error.message}`);
}
confirmBtn.disabled = false;
confirmBtn.textContent = originalText;
}
// Close modal on outside click
document.getElementById('confirmModal').addEventListener('click', (e) => {
if (e.target.classList.contains('modal-overlay')) {
closeModal();
}
});
// Close modal on Escape key
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
closeModal();
}
});
// Make functions available globally
window.showApprovalModal = showApprovalModal;
window.closeModal = closeModal;
window.confirmApproval = confirmApproval;
window.loadWikiDrafts = loadWikiDrafts;
</script>
</body>
</html>