Initial DocsMCP stack
This commit is contained in:
@@ -0,0 +1,32 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>{% block title %}Context7 Docs{% endblock %}</title>
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='style.css') }}">
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<header>
|
||||
<h1>Context7 Docs UI</h1>
|
||||
<nav>
|
||||
<a href="/" {% if request.url.path == '/' %}class="active"{% endif %}>Dashboard</a>
|
||||
<a href="/libraries" {% if request.url.path.startswith('/libraries') %}class="active"{% endif %}>Libraries</a>
|
||||
<a href="/upload" {% if request.url.path.startswith('/upload') %}class="active"{% endif %}>Upload</a>
|
||||
<a href="/search" {% if request.url.path.startswith('/search') %}class="active"{% endif %}>Search</a>
|
||||
<a href="/sources" {% if request.url.path.startswith('/sources') %}class="active"{% endif %}>Sources</a>
|
||||
</nav>
|
||||
</header>
|
||||
|
||||
<main>
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<footer>Context7 Docs WebUI</footer>
|
||||
</div>
|
||||
|
||||
<script src="{{ url_for('static', path='app.js') }}"></script>
|
||||
{% block scripts %}{% endblock %}
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,83 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Dashboard - Context7 Docs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Dashboard</h1>
|
||||
|
||||
<!-- Status Cards -->
|
||||
<div class="status-cards">
|
||||
<div class="status-card" style="{% if health.status == 'ok' %}border-left-color: #00c467{% else %}border-left-color: #f53800{% endif %}">
|
||||
<h3>Docs API Service</h3>
|
||||
{% if health.status and health.status == 'ok' %}
|
||||
<p style="color: #00c467;"><strong>Status:</strong> Online ✓</p>
|
||||
{% else %}
|
||||
<p style="color: #f53800;"><strong>Status:</strong> {% if health.status == 'error' %}Error{% else %}Offline{% endif %}</p>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="status-card">
|
||||
<h3>Vectors Stored</h3>
|
||||
<p>{{ vectors|default(0) }}</p>
|
||||
</div>
|
||||
|
||||
<div class="status-card">
|
||||
<h3>Libraries Registered</h3>
|
||||
<p>{{ libraries|length }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Recent Messages -->
|
||||
{% if libraries and libraries|length > 0 %}
|
||||
<div class="message-box" style="background: #e8f4fd;">
|
||||
<strong>Libraries:</strong> {{ escapeHtml(libraries) }}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="action-buttons">
|
||||
<form method="post" action="/actions/ingest-all" style="display: inline;">
|
||||
<button type="submit" name="ingest-all" class="btn btn-primary">
|
||||
🔄 Ingest All Libraries
|
||||
</button>
|
||||
</form>
|
||||
|
||||
<form method="post" action="/actions/sync-sources" style="display: inline;">
|
||||
<input type="hidden" name="override" value="false">
|
||||
<button type="submit" name="sync-sources" class="btn btn-secondary">
|
||||
📦 Sync Git Sources
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Links -->
|
||||
<div class="links-section">
|
||||
<h2>Navigate to Other Pages</h2>
|
||||
<a href="/libraries" style="display: inline-block; margin-right: 15px;">View Libraries →</a>
|
||||
<a href="/upload" style="display: inline-block; margin-right: 15px;">Upload Files →</a>
|
||||
<a href="/search" style="display: inline-block; margin-right: 15px;">Search Docs →</a>
|
||||
<a href="/sources" style="display: inline-block;">Git Sources →</a>
|
||||
</div>
|
||||
|
||||
<!-- Script for health refresh on reload -->
|
||||
<script>
|
||||
// On page reload, re-fetch and update status if needed
|
||||
document.addEventListener("DOMContentLoaded", async function() {
|
||||
try {
|
||||
const api = window.docsApiClient;
|
||||
|
||||
// Refresh health status from server-rendered data
|
||||
document.querySelector('.status-cards .status-card:first-of-type')?.classList.remove('error');
|
||||
const newHealth = await api.get("/health");
|
||||
|
||||
if (newHealth.status === 'ok') {
|
||||
document.querySelector('.status-cards .status-card:first-of-type')?.querySelector('p')?.classList.add('online');
|
||||
} else {
|
||||
document.querySelector('.status-cards .status-card:first-of-type')?.querySelector('p')?.classList.add('error');
|
||||
}
|
||||
} catch (err) {
|
||||
console.log('Health refresh skipped:', err);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,74 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Libraries - Context7 Docs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Libraries</h1>
|
||||
|
||||
<!-- Create Library Form -->
|
||||
<div class="create-form">
|
||||
<form method="post" action="/libraries/create">
|
||||
<label for="new_library_id">Library ID:</label>
|
||||
<input type="text" id="new_library_id" name="library_id" placeholder="e.g., foundryvtt" required>
|
||||
|
||||
<label for="new_name">Name:</label>
|
||||
<input type="text" id="new_name" name="name" placeholder="Display name for this library" required>
|
||||
|
||||
<label for="new_description">Description (optional):</label>
|
||||
<input type="text" id="new_description" name="description" placeholder="Brief description...">
|
||||
|
||||
<button type="submit" class="btn btn-primary">Create Library</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<hr>
|
||||
|
||||
<!-- Libraries Table -->
|
||||
<table class="library-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>ID</th>
|
||||
<th>Name</th>
|
||||
<th>Description</th>
|
||||
<th>Source Path</th>
|
||||
<th>Updated At</th>
|
||||
<th>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="libraries-body">
|
||||
{% if data|length > 0 %}
|
||||
{% for lib in data %}
|
||||
<tr class="{% if lib.source_path and 'foundry' in (lib.source_path or '').lower() %}highlight{% endif %}">
|
||||
<td><code>{{ escapeHtml(lib.id) }}</code></td>
|
||||
<td><strong>{{ escapeHtml(lib.name) }}</strong></td>
|
||||
<td>{{ escapeHtml(lib.description) or '-' }}</td>
|
||||
<td><small>{{ escapeHtml(lib.source_path) or '-' }}</small></td>
|
||||
<td><small>{{ lib.updated_at|default('N/A') }}</small></td>
|
||||
<td class="actions">
|
||||
<a href="/libraries/{{ lib.id }}/docs" class="btn btn-sm btn-info">View Docs</a> |
|
||||
<form method="post" action="/libraries/{{ lib.id }}/ingest" style="display:inline;"
|
||||
onsubmit="return confirm('Trigger ingestion for this library?');">
|
||||
<button type="submit" class="btn btn-sm btn-warning">Ingest</button>
|
||||
</form> |
|
||||
<form method="post" action="/libraries/{{ lib.id }}/delete"
|
||||
onsubmit="return confirm('Delete this library and all its contents? This cannot be undone.');">
|
||||
<button type="submit" class="btn btn-sm btn-danger">Delete</button>
|
||||
</form>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center;">No libraries found. Create one above.</td>
|
||||
</tr>
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
{% if data and data[0] and data[0].get('content') %}
|
||||
<!-- Docs view mode -->
|
||||
<pre class="code-block">{% for chunk in data.get('content', []) %}{% if chunk|length > 0 %}{{ chunk.text | default(chunk.content) | default(chunk) }}{% endif %}{% endfor %}</pre>
|
||||
<a href="/libraries" style="display:block;margin-top:20px;">← Back to Libraries</a>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,71 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Search - Context7 Docs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Search Documentation</h2>
|
||||
|
||||
<form method="get" action="/search/results" class="search-form">
|
||||
<label for="query">Query:</label>
|
||||
<input type="text" id="query" name="q" required placeholder="Enter your search query..." value="{{ query or '' }}">
|
||||
|
||||
<label for="limit">Limit results:</label>
|
||||
<select id="limit" name="limit">
|
||||
<option value="5">5</option>
|
||||
<option value="10" selected>10</option>
|
||||
<option value="20">20</option>
|
||||
<option value="50">50</option>
|
||||
</select>
|
||||
|
||||
<button type="submit">Search</button>
|
||||
</form>
|
||||
|
||||
<div id="search-results" class="results-box"></div>
|
||||
|
||||
{% if results %}
|
||||
<div class="results-count">{{ results|length }} results found</div>
|
||||
{% endif %}
|
||||
|
||||
<script>
|
||||
async function loadResults(query, limit) {
|
||||
const searchBox = document.getElementById("search-results");
|
||||
|
||||
try {
|
||||
const payload = { query: query || "{{ initial_query or '' }}", library_id: null, limit: parseInt(limit) };
|
||||
const api = window.docsApiClient;
|
||||
|
||||
const result = await api.post("/search", payload);
|
||||
|
||||
if (result.results && Array.isArray(result.results)) {
|
||||
searchBox.className = "results-box";
|
||||
let html = '<div class="results-count">' + result.results.length + ' results found</div>';
|
||||
|
||||
for (const r of result.results) {
|
||||
const title = r.title || (r.content || '').substring(0, 100);
|
||||
const content = (r.content || '').substring(0, 500);
|
||||
html += '<div class="result-card">' +
|
||||
'<h3>' + escapeHtml(title) + '</h3>' +
|
||||
'<p>' + escapeHtml(content) + '...</p>' +
|
||||
'<a href="/docs/' + (r.library_id || '') + '">View Full</a></div>';
|
||||
}
|
||||
|
||||
html += '<a href="/search/form" class="new-search-link">← New Search</a>';
|
||||
searchBox.innerHTML = html;
|
||||
}
|
||||
} catch (err) {
|
||||
searchBox.innerHTML = '<p class="error">Error loading results: ' + escapeHtml(err.message) + '</p>';
|
||||
}
|
||||
}
|
||||
|
||||
// Load initial results if query parameter exists in URL
|
||||
var urlParams = new URLSearchParams(window.location.search);
|
||||
{% if query %}loadResults(urlParams.get('q') || urlParams.get('q'), urlParams.get('limit'));{% endif %}
|
||||
|
||||
function escapeHtml(str) {
|
||||
if (!str) return "";
|
||||
var e = document.createElement('div');
|
||||
e.textContent = str;
|
||||
return e.innerHTML;
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,34 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Sources - Context7 Docs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Git Repository Sync</h2>
|
||||
|
||||
<div class="status-message">Syncs all git repositories configured in <code>docs_sources.yaml</code>.</div>
|
||||
|
||||
<form method="post" action="/sources/sync" class="sync-form">
|
||||
<label for="override">Override existing repos:</label>
|
||||
<input type="checkbox" id="override" name="override">
|
||||
<button type="submit">Sync All Repositories</button>
|
||||
</form>
|
||||
|
||||
<div id="source-list"></div>
|
||||
|
||||
{% if sources %}
|
||||
<h3>Configured Sources</h3>
|
||||
<div class="source-cards">
|
||||
{% for src in sources %}
|
||||
<div class="source-card">
|
||||
<strong>{{ src.library_id | default('unknown') }}</strong><br>
|
||||
URL: {{ src.repo_url | default('N/A')[:60] }}<br>
|
||||
Branch: {{ src.branch | default('main') }}<br>
|
||||
Include: {{ (src.include_paths | default(['*']) | join(', ')) }}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% else %}
|
||||
<p>No git sources configured. Add repositories to <code>docs_sources.yaml</code>.</p>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block title %}Upload - Context7 Docs{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<h2>Upload Documentation Files</h2>
|
||||
|
||||
<form method="post" enctype="multipart/form-data" class="upload-form">
|
||||
<!-- Library Selector -->
|
||||
<label for="library_id">Select Library:</label>
|
||||
<select id="library_id" name="library_id" required>
|
||||
<option value="">(New library - will be created from filename)</option>
|
||||
{% for lib in libraries %}
|
||||
<option value="{{ lib.id }}" data-name="{{ lib.name or lib.id }}">{{ lib.name or lib.id }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
|
||||
<!-- File Input (multiple files allowed) -->
|
||||
<label for="files">Select Files:</label>
|
||||
<input type="file" name="files" id="files" multiple accept=".md,.txt,.py,.js,.ts,.json,.yaml,.yml,.html,.css,.pdf" required>
|
||||
|
||||
<!-- Ingest Checkbox -->
|
||||
<div style="margin-top: 10px;">
|
||||
<label>
|
||||
<input type="checkbox" name="ingest_after_upload" value="on">
|
||||
Trigger ingestion after upload
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button type="submit" class="btn btn-primary">Upload Files</button>
|
||||
</form>
|
||||
|
||||
<!-- Allowed extensions hint -->
|
||||
<p class="hint">Allowed: .md, .txt, .py, .js, .ts, .json, .yaml, .yml, .html, .css, .pdf (max 5MB each)</p>
|
||||
|
||||
<!-- Results Display -->
|
||||
<div id="upload-result" class="result-box"></div>
|
||||
|
||||
{% if results %}
|
||||
<h3>Upload Results</h3>
|
||||
<ul>
|
||||
{% for result in results %}
|
||||
<li><strong>{{ result.filename }}</strong>: {{ result.status }} - {{ escapeHtml(result.message) }}</li>
|
||||
{% endfor %}
|
||||
</ul>
|
||||
{% endif %}
|
||||
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user