p2pwiki-ai/web/index.html

708 lines
24 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>
<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);
}
/* 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>
</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>
</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;
</script>
</body>
</html>