init: llm-wiki-web con FastAPI + Ollama
This commit is contained in:
commit
08291b578c
19 files changed
+1030
No files matched your search
+236
@@ -0,0 +1,236 @@
|
||||
const API_BASE = '/api';
|
||||
let currentPage = null;
|
||||
let chatHistory = [];
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initTabs();
|
||||
loadWikiList();
|
||||
loadRawList();
|
||||
});
|
||||
|
||||
function initTabs() {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => {
|
||||
btn.addEventListener('click', () => {
|
||||
document.querySelectorAll('.tab-btn').forEach(b => b.classList.remove('tab-active'));
|
||||
btn.classList.add('tab-active');
|
||||
document.querySelectorAll('.tab-panel').forEach(p => p.classList.add('hidden'));
|
||||
document.getElementById(`tab-${btn.dataset.tab}`).classList.remove('hidden');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async function loadWikiList() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/wiki/list`);
|
||||
const data = await res.json();
|
||||
const list = document.getElementById('page-list');
|
||||
list.innerHTML = '';
|
||||
|
||||
const categories = ['entities', 'concepts', 'sources', 'topics', 'comparisons'];
|
||||
categories.forEach(cat => {
|
||||
if (data[cat] && data[cat].length > 0) {
|
||||
const catDiv = document.createElement('div');
|
||||
catDiv.innerHTML = `<div class="text-xs text-gray-500 uppercase mt-2 mb-1">${cat}</div>`;
|
||||
data[cat].forEach(page => {
|
||||
const a = document.createElement('a');
|
||||
a.href = '#';
|
||||
a.textContent = page.name;
|
||||
a.className = 'block px-2 py-1 rounded hover:bg-gray-700 text-blue-400';
|
||||
a.onclick = () => loadPage(page.file);
|
||||
catDiv.appendChild(a);
|
||||
});
|
||||
list.appendChild(catDiv);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error loading wiki list:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadPage(file) {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/wiki/page/${file}`);
|
||||
const data = await res.json();
|
||||
currentPage = data;
|
||||
const container = document.getElementById('page-content');
|
||||
|
||||
let html = `<h2 class="text-2xl font-bold mb-4">${data.file.split('/').pop().replace('.md', '')}</h2>`;
|
||||
|
||||
if (data.metadata && Object.keys(data.metadata).length > 0) {
|
||||
html += `<div class="text-xs text-gray-500 mb-2">Tags: ${Object.values(data.metadata).join(', ')}</div>`;
|
||||
}
|
||||
|
||||
let content = data.content || '';
|
||||
content = content.replace(/\[\[([^\]]+)\]\]/g, '<span class="wikilink" onclick="navigateTo(\'$1\')">[[$1]]</span>');
|
||||
html += `<div class="prose prose-invert max-w-none">${marked.parse(content)}</div>`;
|
||||
|
||||
container.innerHTML = html;
|
||||
} catch (e) {
|
||||
document.getElementById('page-content').innerHTML = `<p class="text-red-400">Errore: ${e.message}</p>`;
|
||||
}
|
||||
}
|
||||
|
||||
function navigateTo(name) {
|
||||
const normalized = name.toLowerCase().replace(/\s+/g, '-');
|
||||
loadPage(normalized);
|
||||
}
|
||||
|
||||
async function loadRawList() {
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/raw/list`);
|
||||
const data = await res.json();
|
||||
const list = document.getElementById('raw-list');
|
||||
list.innerHTML = '';
|
||||
|
||||
Object.entries(data).forEach(([cat, files]) => {
|
||||
if (files.length > 0) {
|
||||
const div = document.createElement('div');
|
||||
div.innerHTML = `<div class="text-xs text-gray-500 uppercase">${cat}</div>`;
|
||||
files.forEach(f => {
|
||||
const label = document.createElement('label');
|
||||
label.className = 'flex items-center gap-2 px-2 py-1 hover:bg-gray-700 rounded cursor-pointer';
|
||||
label.innerHTML = `<input type="radio" name="source" value="${cat}/${f}"> <span class="text-gray-300">${f}</span>`;
|
||||
div.appendChild(label);
|
||||
});
|
||||
list.appendChild(div);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('Error loading raw list:', e);
|
||||
}
|
||||
}
|
||||
|
||||
async function runIngest() {
|
||||
const selected = document.querySelector('input[name="source"]:checked');
|
||||
if (!selected) {
|
||||
alert('Seleziona un file da ingestire');
|
||||
return;
|
||||
}
|
||||
|
||||
const log = document.getElementById('ingest-log');
|
||||
log.innerHTML = '<span class="text-blue-400">Avvio ingest...</span>\n';
|
||||
|
||||
try {
|
||||
const eventSource = new EventSource(`${API_BASE}/ingest/stream/${selected.value}`);
|
||||
|
||||
eventSource.onmessage = (e) => {
|
||||
log.innerHTML += e.data + '\n';
|
||||
log.scrollTop = log.scrollHeight;
|
||||
};
|
||||
|
||||
eventSource.onerror = () => {
|
||||
eventSource.close();
|
||||
log.innerHTML += '<span class="text-green-400">Completato!</span>\n';
|
||||
loadWikiList();
|
||||
};
|
||||
} catch (e) {
|
||||
log.innerHTML += `<span class="text-red-400">Errore: ${e.message}</span>\n`;
|
||||
}
|
||||
}
|
||||
|
||||
async function doSearch() {
|
||||
const query = document.getElementById('search-input').value.trim();
|
||||
if (!query) return;
|
||||
|
||||
const results = document.getElementById('search-results');
|
||||
results.innerHTML = '<span class="text-gray-400">Ricerca in corso...</span>';
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/search?q=${encodeURIComponent(query)}`);
|
||||
const data = await res.json();
|
||||
|
||||
results.innerHTML = '';
|
||||
|
||||
if (data.by_name && data.by_name.length > 0) {
|
||||
const section = document.createElement('div');
|
||||
section.innerHTML = `<h4 class="font-semibold text-green-400 mb-2">Per nome</h4>`;
|
||||
data.by_name.forEach(r => {
|
||||
section.innerHTML += `<div class="ml-4 p-2 bg-gray-800 rounded mb-1"><a href="#" class="text-blue-400" onclick="loadPage('${r.file.replace('.md', '')}')">${r.name}</a> <span class="text-gray-500 text-xs">(${r.category})</span></div>`;
|
||||
});
|
||||
results.appendChild(section);
|
||||
}
|
||||
|
||||
if (data.by_content && data.by_content.length > 0) {
|
||||
const section = document.createElement('div');
|
||||
section.innerHTML = `<h4 class="font-semibold text-blue-400 mb-2">Nel contenuto</h4>`;
|
||||
data.by_content.forEach(r => {
|
||||
const matchesHtml = r.matches.map(m => `<div class="text-xs text-gray-400 ml-4">Linea ${m.line}: ${m.text}</div>`).join('');
|
||||
section.innerHTML += `<div class="p-2 bg-gray-800 rounded mb-2"><a href="#" class="text-blue-400" onclick="loadPage('${r.file.replace('.md', '')}')">${r.name}</a>${matchesHtml}</div>`;
|
||||
});
|
||||
results.appendChild(section);
|
||||
}
|
||||
|
||||
if ((!data.by_name || data.by_name.length === 0) && (!data.by_content || data.by_content.length === 0)) {
|
||||
results.innerHTML = '<span class="text-gray-500">Nessun risultato</span>';
|
||||
}
|
||||
} catch (e) {
|
||||
results.innerHTML = `<span class="text-red-400">Errore: ${e.message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
async function sendChat() {
|
||||
const input = document.getElementById('chat-input');
|
||||
const message = input.value.trim();
|
||||
if (!message) return;
|
||||
|
||||
const messages = document.getElementById('chat-messages');
|
||||
messages.innerHTML += `<div class="mb-2 p-2 bg-gray-700 rounded"><span class="text-blue-400">Tu:</span> ${escapeHtml(message)}</div>`;
|
||||
input.value = '';
|
||||
|
||||
const responseDiv = document.createElement('div');
|
||||
responseDiv.className = 'mb-2 p-2 bg-gray-800 rounded';
|
||||
responseDiv.innerHTML = '<span class="text-green-400">Wiki:</span> <span class="streaming-cursor"></span>';
|
||||
messages.appendChild(responseDiv);
|
||||
messages.scrollTop = messages.scrollHeight;
|
||||
|
||||
try {
|
||||
const res = await fetch(`${API_BASE}/chat`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ message, history: chatHistory })
|
||||
});
|
||||
|
||||
const reader = res.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
let response = '';
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
const chunk = decoder.decode(value);
|
||||
response += chunk;
|
||||
responseDiv.innerHTML = `<span class="text-green-400">Wiki:</span> ${formatResponse(response)}`;
|
||||
}
|
||||
|
||||
chatHistory.push({ role: 'user', content: message });
|
||||
chatHistory.push({ role: 'assistant', content: response });
|
||||
|
||||
} catch (e) {
|
||||
responseDiv.innerHTML = `<span class="text-red-400">Errore: ${e.message}</span>`;
|
||||
}
|
||||
}
|
||||
|
||||
function formatResponse(text) {
|
||||
text = escapeHtml(text);
|
||||
text = text.replace(/\[\[([^\]]+)\]\]/g, '<span class="wikilink" onclick="navigateTo(\'$1\')">[[$1]]</span>');
|
||||
return text + '<span class="streaming-cursor"></span>';
|
||||
}
|
||||
|
||||
function escapeHtml(text) {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = text;
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
const marked = {
|
||||
parse: function(text) {
|
||||
text = text.replace(/^### (.+)$/gm, '<h3 class="text-lg font-semibold mt-4 mb-2">$1</h3>');
|
||||
text = text.replace(/^## (.+)$/gm, '<h2 class="text-xl font-semibold mt-4 mb-2">$1</h2>');
|
||||
text = text.replace(/^# (.+)$/gm, '<h1 class="text-2xl font-bold mb-4">$1</h1>');
|
||||
text = text.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
|
||||
text = text.replace(/\*(.+?)\*/g, '<em>$1</em>');
|
||||
text = text.replace(/`(.+?)`/g, '<code class="bg-gray-700 px-1 rounded">$1</code>');
|
||||
text = text.replace(/\n/g, '<br>');
|
||||
return text;
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,85 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="it">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>LLM Wiki</title>
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<style>
|
||||
.tab-active { border-bottom: 2px solid #3b82f6; color: #3b82f6; }
|
||||
.wikilink { color: #3b82f6; text-decoration: underline; cursor: pointer; }
|
||||
pre { white-space: pre-wrap; word-wrap: break-word; }
|
||||
.streaming-cursor::after { content: '▊'; animation: blink 1s infinite; }
|
||||
@keyframes blink { 50% { opacity: 0; } }
|
||||
</style>
|
||||
</head>
|
||||
<body class="bg-gray-900 text-gray-100 min-h-screen">
|
||||
|
||||
<div class="max-w-6xl mx-auto p-4">
|
||||
<header class="mb-6">
|
||||
<h1 class="text-3xl font-bold text-blue-400">📚 LLM Wiki</h1>
|
||||
<p class="text-gray-400 text-sm">Knowledge base che si costruisce da sola</p>
|
||||
</header>
|
||||
|
||||
<nav class="flex border-b border-gray-700 mb-6">
|
||||
<button class="tab-btn px-4 py-2 text-gray-300 hover:text-white tab-active" data-tab="wiki">Wiki</button>
|
||||
<button class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="chat">Chat</button>
|
||||
<button class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="ingest">Ingest</button>
|
||||
<button class="tab-btn px-4 py-2 text-gray-300 hover:text-white" data-tab="search">Search</button>
|
||||
</nav>
|
||||
|
||||
<div id="tab-content">
|
||||
<!-- Wiki Tab -->
|
||||
<div id="tab-wiki" class="tab-panel">
|
||||
<div class="grid grid-cols-4 gap-4">
|
||||
<div class="col-span-1 bg-gray-800 rounded-lg p-4 max-h-screen overflow-y-auto">
|
||||
<h3 class="font-semibold mb-3 text-gray-300">Pagine</h3>
|
||||
<div id="page-list" class="space-y-1 text-sm"></div>
|
||||
</div>
|
||||
<div class="col-span-3 bg-gray-800 rounded-lg p-4 max-h-screen overflow-y-auto">
|
||||
<div id="page-content">
|
||||
<p class="text-gray-500">Clicca su una pagina per leggere</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Tab -->
|
||||
<div id="tab-chat" class="tab-panel hidden">
|
||||
<div class="bg-gray-800 rounded-lg p-4 max-h-96 overflow-y-auto mb-4" id="chat-messages">
|
||||
<p class="text-gray-500">Fai una domanda sulla wiki...</p>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
<input type="text" id="chat-input" placeholder="La tua domanda..."
|
||||
class="flex-1 bg-gray-700 rounded px-4 py-2 text-white"
|
||||
onkeypress="if(event.key==='Enter') sendChat()">
|
||||
<button onclick="sendChat()" class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded font-medium">Invia</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Ingest Tab -->
|
||||
<div id="tab-ingest" class="tab-panel hidden">
|
||||
<div class="bg-gray-800 rounded-lg p-4 mb-4">
|
||||
<h3 class="font-semibold mb-3">Seleziona source da ingestire</h3>
|
||||
<div id="raw-list" class="space-y-2 text-sm max-h-64 overflow-y-auto"></div>
|
||||
</div>
|
||||
<button onclick="runIngest()" class="bg-green-600 hover:bg-green-700 px-6 py-2 rounded font-medium">Avvia Ingest</button>
|
||||
<div id="ingest-log" class="mt-4 bg-gray-800 rounded-lg p-4 text-sm font-mono max-h-64 overflow-y-auto"></div>
|
||||
</div>
|
||||
|
||||
<!-- Search Tab -->
|
||||
<div id="tab-search" class="tab-panel hidden">
|
||||
<div class="flex gap-2 mb-4">
|
||||
<input type="text" id="search-input" placeholder="Cerca nella wiki..."
|
||||
class="flex-1 bg-gray-700 rounded px-4 py-2 text-white"
|
||||
onkeypress="if(event.key==='Enter') doSearch()">
|
||||
<button onclick="doSearch()" class="bg-blue-600 hover:bg-blue-700 px-6 py-2 rounded font-medium">Cerca</button>
|
||||
</div>
|
||||
<div id="search-results" class="space-y-2"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in new issue
Block a user