Files
nextcloud-mcp-server/nextcloud_mcp_server/auth/templates/user_info.html
T
Chris Coutinho 9bd02d7ef7 fix: Preserve 3D plot camera position and fix CSS loading
Two fixes for the vector visualization page:

1. **CSS Loading Fix**: Moved CSS <link> from vector_viz.html fragment
   to user_info.html <head> block. HTMX fragments don't process <link>
   tags in <head>, causing unstyled page. Now CSS loads correctly.

2. **Camera Preservation**: Modified renderPlot() to preserve camera
   position when toggling query point visibility. Previously, toggling
   the "Show Query Point" checkbox would reset zoom/rotation to default.
   Now reads existing camera settings from plot before updating.

Related: nextcloud_mcp_server/auth/static/vector-viz.js:123-130
Related: nextcloud_mcp_server/auth/templates/user_info.html:12

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-19 13:51:08 +01:00

651 lines
32 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>
<!-- Vector Viz static assets -->
<link rel="stylesheet" href="/app/static/vector-viz.css">
{% 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);
}
/* Welcome tab specific styles */
.hero-section {
background: linear-gradient(135deg, var(--color-primary-element) 0%, #0082c9 100%);
color: white;
padding: 60px 24px;
margin: -24px -24px 40px -24px;
border-radius: 0 0 var(--border-radius-large) var(--border-radius-large);
text-align: center;
}
.hero-section h1 {
color: white;
font-size: 36px;
margin: 0 0 16px 0;
font-weight: 600;
}
.hero-section p {
font-size: 18px;
opacity: 0.95;
max-width: 700px;
margin: 0 auto;
line-height: 1.6;
}
.feature-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr));
gap: 24px;
margin: 32px 0;
}
.feature-card {
background: var(--color-main-background);
border: 2px solid var(--color-border);
border-radius: var(--border-radius-large);
padding: 24px;
transition: all 0.2s;
cursor: pointer;
text-decoration: none;
color: inherit;
display: block;
}
.feature-card:hover {
border-color: var(--color-primary-element);
box-shadow: 0 4px 12px rgba(0, 103, 158, 0.15);
transform: translateY(-2px);
}
.feature-card h3 {
color: var(--color-primary-element);
font-size: 20px;
margin: 12px 0 8px 0;
font-weight: 600;
display: flex;
align-items: center;
gap: 12px;
}
.feature-card p {
color: var(--color-text-maxcontrast);
font-size: 14px;
line-height: 1.6;
margin: 8px 0 0 0;
}
.feature-icon {
width: 48px;
height: 48px;
background: var(--color-primary-element-light);
border-radius: var(--border-radius);
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 8px;
}
.feature-icon svg {
width: 28px;
height: 28px;
fill: var(--color-primary-element);
}
.info-section {
background: var(--color-background-hover);
border-radius: var(--border-radius-large);
padding: 32px;
margin: 32px 0;
}
.info-section h2 {
color: var(--color-main-text);
font-size: 24px;
margin: 0 0 16px 0;
border: none;
padding: 0;
}
.info-section p {
color: var(--color-text-maxcontrast);
line-height: 1.7;
margin: 12px 0;
}
.info-section ul {
margin: 12px 0;
padding-left: 24px;
}
.info-section li {
color: var(--color-text-maxcontrast);
line-height: 1.7;
margin: 8px 0;
}
.info-section code {
background: var(--color-main-background);
padding: 2px 8px;
border-radius: var(--border-radius);
font-size: 13px;
}
.auth-status {
background: var(--color-primary-element-light);
border-left: 4px solid var(--color-primary-element);
padding: 16px 20px;
margin: 24px 0;
border-radius: var(--border-radius);
display: flex;
align-items: center;
gap: 12px;
}
.auth-status svg {
width: 24px;
height: 24px;
fill: var(--color-primary-element);
flex-shrink: 0;
}
.auth-status-text {
flex: 1;
}
.auth-status-text strong {
display: block;
color: var(--color-main-text);
font-size: 14px;
margin-bottom: 4px;
}
.auth-status-text span {
color: var(--color-text-maxcontrast);
font-size: 13px;
}
{% endblock %}
{% block content %}
<div class="app-content-wrapper" x-data="{ activeSection: 'welcome', 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 === 'welcome' }">
<div class="app-navigation-entry__wrapper">
<a href="#"
@click.prevent="activeSection = 'welcome'"
class="app-navigation-entry-link">
<span class="app-navigation-entry-icon">
<svg class="nav-icon" viewBox="0 0 24 24">
<path d="M10,20V14H14V20H19V12H22L12,3L2,12H5V20H10Z" />
</svg>
</span>
<span class="app-navigation-entry__name">Welcome</span>
</a>
</div>
</li>
<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">
<!-- Welcome Section -->
<div x-show="activeSection === 'welcome'">
<!-- Hero Section -->
<div class="hero-section">
<h1>Welcome to Nextcloud MCP Server</h1>
<p>
Interactive user interface for semantic search and document retrieval.
Test queries, visualize results, and explore your Nextcloud content using RAG workflows.
</p>
</div>
<!-- Authentication Status -->
<div class="auth-status">
<svg 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>
<div class="auth-status-text">
<strong>Authenticated as: {{ username }}</strong>
<span>Authentication mode: <code>{{ auth_mode }}</code></span>
</div>
</div>
{% if vector_sync_enabled %}
<!-- Vector Sync Enabled Content -->
<div class="info-section">
<h2>About Semantic Search</h2>
<p>
This interface provides access to <strong>semantic search</strong> capabilities powered by vector embeddings.
Unlike traditional keyword search, semantic search understands the <em>meaning</em> of your queries and finds
conceptually similar content across your Nextcloud apps.
</p>
<p>
<strong>How it works:</strong>
</p>
<ul>
<li>Documents from Notes, Calendar, Files, Contacts, and Deck are indexed into a vector database</li>
<li>Each document chunk is converted to a 768-dimensional vector embedding that captures semantic meaning</li>
<li>Queries are also converted to embeddings and matched against document vectors using similarity search</li>
<li>Results can be retrieved using pure semantic search or hybrid BM25 search combining keywords and semantics</li>
</ul>
</div>
<div class="info-section">
<h2>RAG Workflow Integration</h2>
<p>
This UI allows you to <strong>test the same queries that Large Language Models (LLMs) would use</strong> in a
Retrieval-Augmented Generation (RAG) workflow. When an AI assistant needs to answer questions about your data:
</p>
<ul>
<li><strong>Step 1:</strong> The assistant converts your question into a search query</li>
<li><strong>Step 2:</strong> The MCP server retrieves relevant document chunks using semantic search</li>
<li><strong>Step 3:</strong> Retrieved context is passed to the LLM to generate an informed answer</li>
</ul>
<!-- RAG Workflow Diagram -->
<div style="background: var(--color-main-background); border: 2px solid var(--color-primary-element); border-radius: var(--border-radius-large); padding: 24px; margin: 24px 0; overflow-x: auto;">
<div style="text-align: center; font-weight: 600; margin-bottom: 20px; color: var(--color-primary-element); font-size: 16px;">
MCP Sampling RAG Workflow
</div>
<!-- Four-component bidirectional flow -->
<div style="max-width: 1000px; margin: 0 auto;">
<div style="display: grid; grid-template-columns: 0.7fr auto 1fr auto 1fr auto 0.9fr; gap: 10px; align-items: center;">
<!-- User -->
<div style="background: var(--color-background-hover); border: 2px solid var(--color-border); border-radius: var(--border-radius-large); padding: 14px; text-align: center;">
<div style="font-size: 26px; margin-bottom: 5px;">👤</div>
<div style="font-weight: 600; color: var(--color-main-text); font-size: 12px;">User</div>
<div style="font-size: 9px; color: var(--color-text-maxcontrast); font-style: italic; margin-top: 5px; line-height: 1.2;">
"What are health<br>benefits of coffee?"
</div>
</div>
<!-- Arrow User <-> Client -->
<div style="text-align: center;">
<div style="font-size: 20px; color: var(--color-text-maxcontrast);"></div>
</div>
<!-- MCP Client + LLM (combined) -->
<div style="background: var(--color-primary-element-light); border: 2px solid var(--color-primary-element); border-radius: var(--border-radius-large); padding: 12px; text-align: center;">
<div style="font-weight: 600; color: var(--color-primary-element); font-size: 13px; margin-bottom: 8px;">MCP Client + LLM</div>
<div style="background: var(--color-main-background); border-radius: var(--border-radius); padding: 8px; margin-bottom: 6px;">
<div style="font-size: 9px; color: var(--color-text-maxcontrast);">(Claude Code)</div>
</div>
<div style="background: var(--color-main-background); border-radius: var(--border-radius); padding: 8px; border: 2px solid var(--color-primary-element);">
<div style="font-size: 16px; margin-bottom: 2px;">🧠</div>
<div style="font-weight: 600; color: var(--color-main-text); font-size: 10px;">Client's LLM</div>
<div style="font-size: 8px; color: var(--color-text-maxcontrast);">(Claude)</div>
</div>
<div style="margin-top: 8px; font-size: 8px; color: var(--color-text-maxcontrast); line-height: 1.2;">
<strong>Enables RAG:</strong><br>
Receives context,<br>
generates answer
</div>
</div>
<!-- Arrow Client <-> Server -->
<div style="text-align: center;">
<div style="font-size: 20px; color: var(--color-primary-element);"></div>
<div style="font-size: 7px; color: var(--color-text-maxcontrast); margin-top: 2px; font-weight: 600; line-height: 1.1;">
Query +<br>
Sampling
</div>
</div>
<!-- MCP Server -->
<div style="background: var(--color-primary-element-light); border: 2px solid var(--color-primary-element); border-radius: var(--border-radius-large); padding: 12px; text-align: center;">
<div style="font-weight: 600; color: var(--color-primary-element); font-size: 13px; margin-bottom: 8px;">MCP Server</div>
<div style="background: var(--color-main-background); border-radius: var(--border-radius); padding: 7px; margin-bottom: 5px;">
<div style="font-weight: 600; color: var(--color-main-text); font-size: 9px; margin-bottom: 2px;">1. Semantic Search</div>
<div style="font-size: 7px; color: var(--color-text-maxcontrast); line-height: 1.2;">
Vector embeddings<br>
BM25 Hybrid + RRF
</div>
</div>
<div style="background: var(--color-main-background); border-radius: var(--border-radius); padding: 7px; margin-bottom: 5px;">
<div style="font-weight: 600; color: var(--color-main-text); font-size: 9px; margin-bottom: 2px;">2. Retrieve Context</div>
<div style="font-size: 7px; color: var(--color-text-maxcontrast); line-height: 1.2;">
Top relevant docs<br>
with scores
</div>
</div>
<div style="background: var(--color-main-background); border-radius: var(--border-radius); padding: 7px; margin-bottom: 5px;">
<div style="font-weight: 600; color: var(--color-main-text); font-size: 9px; margin-bottom: 2px;">3. Format Response</div>
<div style="font-size: 7px; color: var(--color-text-maxcontrast); line-height: 1.2;">
Document chunks<br>
with citations
</div>
</div>
<div style="background: var(--color-main-background); border-radius: var(--border-radius); padding: 7px;">
<div style="font-weight: 600; color: var(--color-main-text); font-size: 9px; margin-bottom: 2px;">4. Send to LLM</div>
<div style="font-size: 7px; color: var(--color-text-maxcontrast); line-height: 1.2;">
Via MCP sampling<br>
for answer generation
</div>
</div>
</div>
<!-- Arrow Server <-> Nextcloud -->
<div style="text-align: center;">
<div style="font-size: 20px; color: var(--color-primary-element);"></div>
<div style="font-size: 7px; color: var(--color-text-maxcontrast); margin-top: 2px; font-weight: 600; line-height: 1.1;">
Retrieve
</div>
</div>
<!-- Nextcloud -->
<div style="background: var(--color-background-hover); border: 2px solid var(--color-border); border-radius: var(--border-radius-large); padding: 12px; text-align: center; position: relative;">
<img src="/app/static/nextcloud-logo.png" alt="Nextcloud" style="width: 40px; height: 40px; margin-bottom: 6px;" />
<div style="font-weight: 600; color: var(--color-main-text); font-size: 12px; margin-bottom: 4px;">Nextcloud</div>
<div style="font-size: 8px; color: var(--color-text-maxcontrast); line-height: 1.2;">
Notes, Calendar,<br>
Files, Contacts,<br>
Deck
</div>
</div>
</div>
<!-- Explanation below diagram -->
<div style="margin-top: 24px; padding: 16px; background: var(--color-background-hover); border-radius: var(--border-radius); border-left: 4px solid var(--color-primary-element);">
<div style="font-size: 12px; color: var(--color-main-text); line-height: 1.6;">
<strong>How RAG works via MCP Sampling:</strong>
</div>
<ol style="margin: 8px 0 0 0; padding-left: 20px; font-size: 11px; color: var(--color-text-maxcontrast); line-height: 1.6;">
<li>User asks question through MCP Client</li>
<li>Client sends query to MCP Server</li>
<li>Server retrieves relevant document context from Nextcloud</li>
<li><strong>Server sends context back to Client's LLM</strong> (MCP Sampling)</li>
<li>Client's LLM generates answer with citations using retrieved context</li>
<li>Answer returned to user</li>
</ol>
<div style="margin-top: 8px; font-size: 10px; color: var(--color-text-maxcontrast); font-style: italic;">
The server has no LLM - it only retrieves context. The client's existing LLM is reused for answer generation.
</div>
</div>
</div>
</div>
<p style="margin-top: 16px;">
<strong>Key Point:</strong> The MCP server retrieves context but doesn't generate answers itself.
Through <strong>MCP sampling</strong>, it requests the client's LLM to generate responses, giving users
full control over which model is used and ensuring all processing happens client-side.
</p>
<p>
By using this interface, you can preview search results, understand relevance scores, and verify
that the system retrieves the right information before it reaches the LLM.
</p>
</div>
<!-- Feature Cards -->
<h2>Available Features</h2>
<div class="feature-grid">
<a href="#" @click.prevent="activeSection = 'user-info'" class="feature-card">
<div class="feature-icon">
<svg 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>
</div>
<h3>User Information</h3>
<p>
View your authentication details, session information, and IdP profile.
Manage background access permissions.
</p>
</a>
<a href="#" @click.prevent="activeSection = 'vector-sync'" class="feature-card">
<div class="feature-icon">
<svg 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>
</div>
<h3>Vector Sync Status</h3>
<p>
Monitor real-time indexing progress with metrics for indexed documents, pending queue,
and synchronization status.
</p>
</a>
<a href="#" @click.prevent="activeSection = 'vector-viz'" class="feature-card">
<div class="feature-icon">
<svg viewBox="0 0 24 24">
<path d="M22,21H2V3H4V19H6V10H10V19H12V6H16V19H18V14H22V21Z" />
</svg>
</div>
<h3>Vector Visualization</h3>
<p>
Interactive search interface with 2D PCA visualization. Compare algorithms,
view relevance scores, and explore matched document chunks.
</p>
</a>
</div>
{% else %}
<!-- Vector Sync Disabled Content -->
<div class="warning">
<h3 style="margin-top: 0;">Vector Sync is Disabled</h3>
<p>
Semantic search and vector visualization features are currently disabled.
To enable these features, set <code>VECTOR_SYNC_ENABLED=true</code> in your environment configuration.
</p>
<p style="margin-bottom: 0;">
<strong>Learn more:</strong>
<a href="https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/docs/configuration.md" target="_blank" style="color: inherit; text-decoration: underline;">
Configuration Guide
</a>
</p>
</div>
<!-- Limited Feature Card -->
<h2>Available Features</h2>
<div class="feature-grid">
<a href="#" @click.prevent="activeSection = 'user-info'" class="feature-card">
<div class="feature-icon">
<svg 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>
</div>
<h3>User Information</h3>
<p>
View your authentication details, session information, and IdP profile.
Manage background access permissions.
</p>
</a>
</div>
{% endif %}
<!-- Documentation Section -->
<div class="info-section" style="margin-top: 40px;">
<h2>Documentation</h2>
<p>
For detailed information about configuration, authentication modes, and advanced features,
please refer to the project documentation:
</p>
<ul>
<li><a href="https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/docs/installation.md" target="_blank">Installation Guide</a></li>
<li><a href="https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/docs/configuration.md" target="_blank">Configuration Options</a></li>
<li><a href="https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/docs/authentication.md" target="_blank">Authentication Modes</a></li>
{% if vector_sync_enabled %}
<li><a href="https://github.com/cbcoutinho/nextcloud-mcp-server/blob/master/docs/user-guide/vector-sync-ui.md" target="_blank">Vector Sync UI Guide</a></li>
{% endif %}
</ul>
</div>
</div>
<!-- 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>
// Set global Nextcloud base URL for use in external JS
window.NEXTCLOUD_BASE_URL = '{{ nextcloud_host_for_links }}';
</script>
<script src="/app/static/vector-viz.js"></script>
{% endblock %}