9db20a4d01
This commit updates the web interface to better align with Nextcloud's design system and improve the Vector Viz layout. Changes: - Replace emoji icons with Material Design SVG icons for better consistency with Nextcloud apps - Simplify navigation styling with minimal padding and subtle active states (250px width) - Update CSS variables to match Nextcloud design system - Restructure Vector Viz from two-column to single-column vertical layout for better plot visibility - Move search controls to compact horizontal grid at top - Make navigation toggle always visible (not just on mobile) - Fix plot container sizing with overflow:visible to prevent colorbar clipping - Remove heavy shadows and custom card styling for cleaner aesthetic - Add error and success page templates with consistent styling Technical details: - Preserve Alpine.js for reactive functionality - Use CSS Grid for responsive horizontal controls layout - Add smooth transitions for navigation collapse/expand - Maintain HTMX for dynamic content loading 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
326 lines
14 KiB
HTML
326 lines
14 KiB
HTML
{% extends "base.html" %}
|
|
|
|
{% block title %}Nextcloud MCP Server{% endblock %}
|
|
|
|
{% block extra_head %}
|
|
<!-- htmx for dynamic loading -->
|
|
<script src="https://unpkg.com/htmx.org@1.9.10"></script>
|
|
|
|
<!-- Alpine.js for state management -->
|
|
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
|
|
|
<!-- Plotly.js for vector visualization -->
|
|
<script src="https://cdn.plot.ly/plotly-3.3.0.min.js"></script>
|
|
{% endblock %}
|
|
|
|
{% block extra_styles %}
|
|
/* Smooth htmx transitions */
|
|
.htmx-swapping {
|
|
opacity: 0;
|
|
transition: opacity 200ms ease-out;
|
|
}
|
|
|
|
.htmx-settling {
|
|
opacity: 1;
|
|
transition: opacity 200ms ease-in;
|
|
}
|
|
|
|
/* Logout button styling */
|
|
.logout-section {
|
|
margin-top: 20px;
|
|
padding-top: 20px;
|
|
border-top: 1px solid var(--color-border);
|
|
}
|
|
{% endblock %}
|
|
|
|
{% block content %}
|
|
<div class="app-content-wrapper" x-data="{ activeSection: 'user-info', navOpen: true }">
|
|
<!-- Side Navigation -->
|
|
<nav id="app-navigation" :class="{ 'app-navigation--closed': !navOpen }">
|
|
<div class="app-navigation__content">
|
|
<!-- Navigation List -->
|
|
<ul class="app-navigation-list">
|
|
<li class="app-navigation-entry" :class="{ 'active': activeSection === 'user-info' }">
|
|
<div class="app-navigation-entry__wrapper">
|
|
<a href="#"
|
|
@click.prevent="activeSection = 'user-info'"
|
|
class="app-navigation-entry-link">
|
|
<span class="app-navigation-entry-icon">
|
|
<svg class="nav-icon" viewBox="0 0 24 24">
|
|
<path d="M12,4A4,4 0 0,1 16,8A4,4 0 0,1 12,12A4,4 0 0,1 8,8A4,4 0 0,1 12,4M12,14C16.42,14 20,15.79 20,18V20H4V18C4,15.79 7.58,14 12,14Z" />
|
|
</svg>
|
|
</span>
|
|
<span class="app-navigation-entry__name">User Info</span>
|
|
</a>
|
|
</div>
|
|
</li>
|
|
|
|
{% if show_vector_sync_tab %}
|
|
<li class="app-navigation-entry" :class="{ 'active': activeSection === 'vector-sync' }">
|
|
<div class="app-navigation-entry__wrapper">
|
|
<a href="#"
|
|
@click.prevent="activeSection = 'vector-sync'"
|
|
class="app-navigation-entry-link">
|
|
<span class="app-navigation-entry-icon">
|
|
<svg class="nav-icon" viewBox="0 0 24 24">
|
|
<path d="M12,18A6,6 0 0,1 6,12C6,11 6.25,10.03 6.7,9.2L5.24,7.74C4.46,8.97 4,10.43 4,12A8,8 0 0,0 12,20V23L16,19L12,15M12,4V1L8,5L12,9V6A6,6 0 0,1 18,12C18,13 17.75,13.97 17.3,14.8L18.76,16.26C19.54,15.03 20,13.57 20,12A8,8 0 0,0 12,4Z" />
|
|
</svg>
|
|
</span>
|
|
<span class="app-navigation-entry__name">Vector Sync</span>
|
|
</a>
|
|
</div>
|
|
</li>
|
|
|
|
<li class="app-navigation-entry" :class="{ 'active': activeSection === 'vector-viz' }">
|
|
<div class="app-navigation-entry__wrapper">
|
|
<a href="#"
|
|
@click.prevent="activeSection = 'vector-viz'"
|
|
class="app-navigation-entry-link">
|
|
<span class="app-navigation-entry-icon">
|
|
<svg class="nav-icon" viewBox="0 0 24 24">
|
|
<path d="M22,21H2V3H4V19H6V10H10V19H12V6H16V19H18V14H22V21Z" />
|
|
</svg>
|
|
</span>
|
|
<span class="app-navigation-entry__name">Vector Viz</span>
|
|
</a>
|
|
</div>
|
|
</li>
|
|
{% endif %}
|
|
|
|
{% if show_webhooks_tab %}
|
|
<li class="app-navigation-entry" :class="{ 'active': activeSection === 'webhooks' }">
|
|
<div class="app-navigation-entry__wrapper">
|
|
<a href="#"
|
|
@click.prevent="activeSection = 'webhooks'"
|
|
class="app-navigation-entry-link">
|
|
<span class="app-navigation-entry-icon">
|
|
<svg class="nav-icon" viewBox="0 0 24 24">
|
|
<path d="M10.59,13.41C11,13.8 11,14.44 10.59,14.83C10.2,15.22 9.56,15.22 9.17,14.83C7.22,12.88 7.22,9.71 9.17,7.76V7.76L12.71,4.22C14.66,2.27 17.83,2.27 19.78,4.22C21.73,6.17 21.73,9.34 19.78,11.29L18.29,12.78C18.3,11.96 18.17,11.14 17.89,10.36L18.36,9.88C19.54,8.71 19.54,6.81 18.36,5.64C17.19,4.46 15.29,4.46 14.12,5.64L10.59,9.17C9.41,10.34 9.41,12.24 10.59,13.41M13.41,9.17C13.8,8.78 14.44,8.78 14.83,9.17C16.78,11.12 16.78,14.29 14.83,16.24V16.24L11.29,19.78C9.34,21.73 6.17,21.73 4.22,19.78C2.27,17.83 2.27,14.66 4.22,12.71L5.71,11.22C5.7,12.04 5.83,12.86 6.11,13.65L5.64,14.12C4.46,15.29 4.46,17.19 5.64,18.36C6.81,19.54 8.71,19.54 9.88,18.36L13.41,14.83C14.59,13.66 14.59,11.76 13.41,10.59C13,10.2 13,9.56 13.41,9.17Z" />
|
|
</svg>
|
|
</span>
|
|
<span class="app-navigation-entry__name">Webhooks</span>
|
|
</a>
|
|
</div>
|
|
</li>
|
|
{% endif %}
|
|
</ul>
|
|
|
|
<!-- Settings/Logout at bottom -->
|
|
{% if logout_url %}
|
|
<ul class="app-navigation__settings">
|
|
<li class="app-navigation-entry">
|
|
<div class="app-navigation-entry__wrapper">
|
|
<a href="{{ logout_url }}" class="app-navigation-entry-link">
|
|
<span class="app-navigation-entry-icon">
|
|
<svg class="nav-icon" viewBox="0 0 24 24">
|
|
<path d="M16,17V14H9V10H16V7L21,12L16,17M14,2A2,2 0 0,1 16,4V6H14V4H5V20H14V18H16V20A2,2 0 0,1 14,22H5A2,2 0 0,1 3,20V4A2,2 0 0,1 5,2H14Z" />
|
|
</svg>
|
|
</span>
|
|
<span class="app-navigation-entry__name">Logout</span>
|
|
</a>
|
|
</div>
|
|
</li>
|
|
</ul>
|
|
{% endif %}
|
|
</div>
|
|
|
|
<!-- Toggle Button (mobile) -->
|
|
<button @click="navOpen = !navOpen"
|
|
class="app-navigation-toggle"
|
|
:aria-expanded="navOpen.toString()">
|
|
☰
|
|
</button>
|
|
</nav>
|
|
|
|
<!-- Main Content Area -->
|
|
<main id="app-content">
|
|
<div class="page-content">
|
|
<!-- User Info Section -->
|
|
<div x-show="activeSection === 'user-info'">
|
|
<div class="content-section">
|
|
<h1>User Information</h1>
|
|
{{ user_info_tab_html|safe }}
|
|
</div>
|
|
</div>
|
|
|
|
{% if show_vector_sync_tab %}
|
|
<!-- Vector Sync Section -->
|
|
<div x-show="activeSection === 'vector-sync'">
|
|
<div class="content-section">
|
|
<h1>Vector Sync Status</h1>
|
|
{{ vector_sync_tab_html|safe }}
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Vector Viz Section -->
|
|
<div x-show="activeSection === 'vector-viz'">
|
|
<div class="content-section">
|
|
<h1>Vector Visualization</h1>
|
|
<div hx-get="/app/vector-viz" hx-trigger="load" hx-swap="outerHTML">
|
|
<p style="color: #999;">Loading vector visualization...</p>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
|
|
{% if show_webhooks_tab %}
|
|
<!-- Webhooks Section -->
|
|
<div x-show="activeSection === 'webhooks'">
|
|
<div class="content-section">
|
|
<h1>Webhook Management</h1>
|
|
{{ webhooks_tab_html|safe }}
|
|
</div>
|
|
</div>
|
|
{% endif %}
|
|
</div>
|
|
</main>
|
|
</div>
|
|
|
|
<script>
|
|
// Initialize vizApp for vector visualization (if needed)
|
|
function vizApp() {
|
|
return {
|
|
query: '',
|
|
algorithm: 'bm25_hybrid',
|
|
fusion: 'rrf',
|
|
showAdvanced: false,
|
|
docTypes: [''],
|
|
limit: 50,
|
|
scoreThreshold: 0.0,
|
|
loading: false,
|
|
results: [],
|
|
expandedChunks: {},
|
|
chunkLoading: {},
|
|
|
|
async executeSearch() {
|
|
this.loading = true;
|
|
this.results = [];
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
query: this.query,
|
|
algorithm: this.algorithm,
|
|
limit: this.limit,
|
|
score_threshold: this.scoreThreshold,
|
|
});
|
|
|
|
if (this.algorithm === 'bm25_hybrid') {
|
|
params.append('fusion', this.fusion);
|
|
}
|
|
|
|
const selectedTypes = this.docTypes.filter(t => t !== '');
|
|
if (selectedTypes.length > 0) {
|
|
params.append('doc_types', selectedTypes.join(','));
|
|
}
|
|
|
|
const response = await fetch(`/app/vector-viz/search?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.results = data.results;
|
|
this.renderPlot(data.coordinates_2d, data.results);
|
|
} else {
|
|
alert('Search failed: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Error: ' + error.message);
|
|
} finally {
|
|
this.loading = false;
|
|
}
|
|
},
|
|
|
|
renderPlot(coordinates, results) {
|
|
const scores = results.map(r => r.score);
|
|
const trace = {
|
|
x: coordinates.map(c => c[0]),
|
|
y: coordinates.map(c => c[1]),
|
|
mode: 'markers',
|
|
type: 'scatter',
|
|
text: results.map(r => `${r.title}<br>Raw Score: ${r.original_score.toFixed(3)} (${(r.score * 100).toFixed(0)}% relative)`),
|
|
marker: {
|
|
size: results.map(r => 6 + (Math.pow(r.score, 2) * 14)),
|
|
opacity: results.map(r => 0.2 + (r.score * 0.8)),
|
|
color: scores,
|
|
colorscale: 'Viridis',
|
|
showscale: true,
|
|
colorbar: { title: 'Relative Score' },
|
|
cmin: 0,
|
|
cmax: 1
|
|
}
|
|
};
|
|
|
|
const layout = {
|
|
title: `Vector Space (PCA 2D) - ${results.length} results`,
|
|
xaxis: { title: 'PC1' },
|
|
yaxis: { title: 'PC2' },
|
|
hovermode: 'closest',
|
|
height: 600
|
|
};
|
|
|
|
Plotly.newPlot('viz-plot', [trace], layout);
|
|
},
|
|
|
|
getNextcloudUrl(result) {
|
|
const baseUrl = '{{ nextcloud_host_for_links }}';
|
|
switch (result.doc_type) {
|
|
case 'note':
|
|
return `${baseUrl}/apps/notes/note/${result.id}`;
|
|
case 'file':
|
|
return `${baseUrl}/apps/files/?fileId=${result.id}`;
|
|
case 'calendar':
|
|
return `${baseUrl}/apps/calendar`;
|
|
case 'contact':
|
|
return `${baseUrl}/apps/contacts`;
|
|
case 'deck':
|
|
return `${baseUrl}/apps/deck`;
|
|
default:
|
|
return `${baseUrl}`;
|
|
}
|
|
},
|
|
|
|
hasChunkPosition(result) {
|
|
return result.chunk_start_offset != null && result.chunk_end_offset != null;
|
|
},
|
|
|
|
isChunkExpanded(resultKey) {
|
|
return this.expandedChunks[resultKey] !== undefined;
|
|
},
|
|
|
|
async toggleChunk(result) {
|
|
const resultKey = `${result.doc_type}_${result.id}`;
|
|
|
|
if (this.isChunkExpanded(resultKey)) {
|
|
delete this.expandedChunks[resultKey];
|
|
return;
|
|
}
|
|
|
|
this.chunkLoading[resultKey] = true;
|
|
|
|
try {
|
|
const params = new URLSearchParams({
|
|
doc_type: result.doc_type,
|
|
doc_id: result.id,
|
|
start: result.chunk_start_offset,
|
|
end: result.chunk_end_offset,
|
|
context: 500
|
|
});
|
|
|
|
const response = await fetch(`/app/chunk-context?${params}`);
|
|
const data = await response.json();
|
|
|
|
if (data.success) {
|
|
this.expandedChunks[resultKey] = data;
|
|
} else {
|
|
alert('Failed to load chunk: ' + data.error);
|
|
}
|
|
} catch (error) {
|
|
alert('Error loading chunk: ' + error.message);
|
|
} finally {
|
|
delete this.chunkLoading[resultKey];
|
|
}
|
|
}
|
|
};
|
|
}
|
|
</script>
|
|
{% endblock %}
|