diff --git a/.dockerignore b/.dockerignore index 8e0ab28..ca154cd 100644 --- a/.dockerignore +++ b/.dockerignore @@ -6,3 +6,4 @@ !nextcloud_mcp_server/**/*.py !nextcloud_mcp_server/**/*.html +!nextcloud_mcp_server/auth/static/*.png diff --git a/README.md b/README.md index 7f02f2b..af876cd 100644 --- a/README.md +++ b/README.md @@ -133,6 +133,7 @@ This enables natural language queries and helps discover related content across - **[App Documentation](docs/)** - Notes, Calendar, Contacts, WebDAV, Deck, Cookbook, Tables - **[Document Processing](docs/configuration.md#document-processing)** - OCR and text extraction setup - **[Semantic Search Architecture](docs/semantic-search-architecture.md)** - Experimental vector search (Notes only, opt-in) +- **[Vector Sync UI Guide](docs/user-guide/vector-sync-ui.md)** - Browser interface for semantic search visualization and testing ### Advanced Topics - **[OAuth Architecture](docs/oauth-architecture.md)** - How OAuth works (experimental) diff --git a/docs/images/rag-workflow-bidirectional-final.png b/docs/images/rag-workflow-bidirectional-final.png new file mode 100644 index 0000000..d4cfbaa Binary files /dev/null and b/docs/images/rag-workflow-bidirectional-final.png differ diff --git a/docs/images/rag-workflow-prominent-llm.png b/docs/images/rag-workflow-prominent-llm.png new file mode 100644 index 0000000..da609e0 Binary files /dev/null and b/docs/images/rag-workflow-prominent-llm.png differ diff --git a/docs/images/rag-workflow-simple-final.png b/docs/images/rag-workflow-simple-final.png new file mode 100644 index 0000000..db9a69e Binary files /dev/null and b/docs/images/rag-workflow-simple-final.png differ diff --git a/docs/images/user-info-tab.png b/docs/images/user-info-tab.png new file mode 100644 index 0000000..fba06fe Binary files /dev/null and b/docs/images/user-info-tab.png differ diff --git a/docs/images/vector-sync-status.png b/docs/images/vector-sync-status.png new file mode 100644 index 0000000..db1d967 Binary files /dev/null and b/docs/images/vector-sync-status.png differ diff --git a/docs/images/vector-viz-chunk-context.png b/docs/images/vector-viz-chunk-context.png new file mode 100644 index 0000000..b13f542 Binary files /dev/null and b/docs/images/vector-viz-chunk-context.png differ diff --git a/docs/images/vector-viz-document-types-2col.png b/docs/images/vector-viz-document-types-2col.png new file mode 100644 index 0000000..70b3b39 Binary files /dev/null and b/docs/images/vector-viz-document-types-2col.png differ diff --git a/docs/images/vector-viz-interface.png b/docs/images/vector-viz-interface.png new file mode 100644 index 0000000..b85349e Binary files /dev/null and b/docs/images/vector-viz-interface.png differ diff --git a/docs/images/vector-viz-results.png b/docs/images/vector-viz-results.png new file mode 100644 index 0000000..3587417 Binary files /dev/null and b/docs/images/vector-viz-results.png differ diff --git a/docs/images/welcome-page.png b/docs/images/welcome-page.png new file mode 100644 index 0000000..cac34b3 Binary files /dev/null and b/docs/images/welcome-page.png differ diff --git a/docs/images/welcome-tab-updated-diagram.png b/docs/images/welcome-tab-updated-diagram.png new file mode 100644 index 0000000..4eada45 Binary files /dev/null and b/docs/images/welcome-tab-updated-diagram.png differ diff --git a/docs/user-guide/vector-sync-ui.md b/docs/user-guide/vector-sync-ui.md new file mode 100644 index 0000000..ad4e63b --- /dev/null +++ b/docs/user-guide/vector-sync-ui.md @@ -0,0 +1,315 @@ +# Vector Sync UI Guide + +This guide covers the browser-based user interface for the Nextcloud MCP Server's semantic search and vector synchronization features. + +## Overview + +The Vector Sync UI (`/app`) is a browser-based interface that allows you to interact with semantic search results from documents in your Nextcloud instance. This UI provides the same query capabilities that Large Language Models (LLMs) use in Retrieval-Augmented Generation (RAG) workflows, allowing you to test queries and visualize results in an intuitive way. + +## Accessing the UI + +Navigate to the `/app` endpoint of your MCP server after authentication: + +- **BasicAuth mode**: `http://localhost:8000/app` (credentials from environment) +- **OAuth mode**: `http://localhost:8000/app` (redirects to login if not authenticated) + +## Welcome Page + +The welcome page is the default landing page when you access `/app`. It provides an introduction to the MCP server's capabilities and adapts its content based on whether vector sync is enabled. + +![Welcome Page](../images/welcome-page.png) + +### When Vector Sync is Enabled + +The welcome page includes: + +- **Authentication status** - Shows your username and authentication mode +- **About Semantic Search** - Explanation of semantic search capabilities and how it works +- **RAG Workflow Integration** - How the UI fits into RAG workflows and helps test LLM queries +- **Feature cards** - Quick navigation to User Info, Vector Sync Status, and Vector Visualization + +### When Vector Sync is Disabled + +If `VECTOR_SYNC_ENABLED=false`, the welcome page displays: + +- A warning message explaining that vector sync is disabled +- Link to configuration documentation for enabling the feature +- Limited navigation (User Info only) + +## User Info Tab + +Access user information and session details by navigating to `/app/user-info` or clicking "User Info" in the welcome page. + +![User Info Tab](../images/user-info-tab.png) + +### What's Displayed + +**BasicAuth Mode:** +- Username +- Authentication mode badge +- Nextcloud host connection URL + +**OAuth Mode:** +- Username +- Authentication mode badge +- Session ID (truncated for security) +- Background access status (granted or not granted) +- IdP profile information (if available) +- Option to revoke background access + +### Navigation + +The user info page includes a sidebar with tabs for: +- **Home** - Returns to the welcome page +- **User Info** - Current page +- **Vector Sync** - Real-time sync status (if vector sync enabled) +- **Vector Viz** - Interactive visualization (if vector sync enabled) +- **Webhooks** - Admin-only webhook management (if user is admin) + +## Vector Sync Status Tab + +Monitor real-time indexing progress and synchronization status. + +![Vector Sync Status](../images/vector-sync-status.png) + +### Metrics Displayed + +| Metric | Description | +|--------|-------------| +| **Indexed Documents** | Total number of document chunks stored in Qdrant vector database | +| **Pending Documents** | Number of documents in the processing queue waiting to be embedded | +| **Status** | Current sync state: "✓ Idle" (green) or "⟳ Syncing" (orange) | + +### Real-Time Updates + +The status tab uses htmx to automatically refresh every 10 seconds, providing live updates without manual page refreshes. + +### What the Metrics Mean + +- **Indexed Documents**: These are document chunks that have been converted to 768-dimensional vector embeddings and stored in Qdrant. These documents are immediately searchable via semantic search. + +- **Pending Documents**: Documents in the queue that are awaiting embedding processing. The processor workers will gradually process these documents based on available resources. + +- **Idle Status**: No documents are currently being processed. The system is up-to-date. + +- **Syncing Status**: Documents are actively being processed and indexed. This is normal after adding new content or on initial sync. + +## Vector Visualization Tab + +Interactive search interface with 2D visualization of results in semantic space. + +![Vector Visualization Interface](../images/vector-viz-interface.png) + +### Search Controls + +**Search Query** +- Enter natural language queries to search your Nextcloud documents +- Examples: "health benefits of coffee vs tea", "python testing frameworks", "project deadlines" + +**Algorithm Selection** +- **Semantic (Dense)**: Pure semantic search using vector similarity +- **BM25 Hybrid** (default): Combines semantic search with keyword matching using BM25 sparse vectors + +**Fusion Method** (for BM25 Hybrid only) +- **RRF** (Reciprocal Rank Fusion): General-purpose fusion using reciprocal ranks +- **DBSF** (Distribution-Based Score Fusion): Distribution-based normalization for better score balancing + +**Advanced Options** +- Document types filter (Notes, Files, Calendar, Contacts, Deck) +- Score threshold (0.0-1.0) +- Result limit (default: 50, max: 100) + +### Search Results + +![Vector Visualization Results](../images/vector-viz-results.png) + +The visualization displays: + +1. **2D PCA Plot** - Documents projected into 2D space using Principal Component Analysis + - Point size indicates relevance score (larger = more relevant) + - Point opacity correlates with score (more opaque = higher score) + - Color scale (Viridis) represents similarity (yellow = highest match) + - Hover over points to see document details + +2. **Results List** - Searchable documents with: + - Document title (clickable link to Nextcloud app) + - Snippet preview of matched content + - Raw score and relative score percentage + - Document type (note, file, calendar, etc.) + - "Show Chunk" button to expand matched text + +### Viewing Chunk Context + +Click "Show Chunk" to view the matched text with surrounding context. + +![Chunk Context View](../images/vector-viz-chunk-context.png) + +The chunk context view displays: +- **Highlighted matched chunk** - The specific text segment that matched your query (highlighted in yellow) +- **Surrounding context** - Up to 500 characters before and after the match for better understanding +- **Full document link** - Click the title to open the document in the Nextcloud app + +### Understanding the 2D Visualization + +The PCA (Principal Component Analysis) plot reduces 768-dimensional vector embeddings to 2D for visualization: + +- **Proximity** - Documents closer together in 2D space are semantically similar +- **Clusters** - Groups of related documents appear as clusters +- **Outliers** - Distant points represent documents with unique content +- **Query position** - Your search query is embedded and plotted alongside results + +**Note**: PCA is a dimensionality reduction technique that preserves as much variance as possible, but some information is lost in the projection from 768D to 2D. + +## Configuration Requirements + +### Required Environment Variables + +To enable vector sync features: + +```bash +VECTOR_SYNC_ENABLED=true +``` + +### Optional Configuration + +For browser-accessible links to Nextcloud apps (Notes, Files, etc.): + +```bash +NEXTCLOUD_PUBLIC_ISSUER_URL=https://your-public-nextcloud-url.com +``` + +If not set, falls back to `NEXTCLOUD_HOST` from settings. + +### Admin Access + +The Webhooks tab is only visible to users with Nextcloud admin privileges. Admin status is checked via the Nextcloud Provisioning API. + +## Use Cases + +### 1. Monitoring Document Indexing + +Use the Vector Sync Status tab to: +- Verify documents are being indexed after creation/modification +- Check if the indexing queue is backing up (high pending count) +- Confirm the system is idle after bulk document imports + +### 2. Testing Search Queries + +Use the Vector Visualization tab to: +- Test queries before they're used by LLMs in RAG workflows +- Compare semantic vs. hybrid search algorithms +- Verify that relevant documents are being retrieved +- Understand relevance scores and ranking + +### 3. Debugging Search Results + +Use chunk context to: +- See exactly which text segments match your query +- Verify that the matched content is relevant +- Identify why unexpected documents appear in results +- Understand the surrounding context of matches + +### 4. Algorithm Comparison + +Experiment with different search approaches: +- **Pure semantic**: Best for conceptual queries and synonyms +- **BM25 hybrid with RRF**: Balanced approach combining keywords and semantics +- **BM25 hybrid with DBSF**: Alternative fusion for different score distributions + +## Technical Details + +### Frontend Stack + +- **Alpine.js** - Reactive state management for UI interactions +- **htmx** - Server-driven dynamic updates for status polling +- **Plotly.js** - Interactive 2D scatter plot visualization +- **Nextcloud design system** - Consistent styling matching Nextcloud ecosystem + +### Backend Processing + +- **Server-side PCA** - Dimensionality reduction performed on the server to minimize bandwidth +- **Chunk-level search** - Searches operate on document chunks (not whole documents) +- **Document deduplication** - Multiple chunks from the same document are deduplicated in results +- **Timing metrics** - All search operations log performance metrics for monitoring + +### Supported Apps + +Documents from the following Nextcloud apps are indexed and searchable: + +- **Notes** - All notes and their content +- **Files** - Supported file types (text, PDF, etc.) +- **Calendar** - Calendar events and tasks (VTODO) +- **Contacts** - Contact information (CardDAV) +- **Deck** - Deck cards and board content + +## Troubleshooting + +### Vector Sync Tab Not Visible + +**Cause**: `VECTOR_SYNC_ENABLED` is not set to `true` + +**Solution**: Set the environment variable and restart the MCP server: +```bash +export VECTOR_SYNC_ENABLED=true +docker compose restart mcp +``` + +### No Search Results + +**Possible causes**: +1. No documents have been indexed yet (check Vector Sync Status) +2. Query doesn't match indexed content +3. Score threshold is too high + +**Solutions**: +- Wait for documents to be indexed (check "Indexed Documents" count) +- Try broader or different queries +- Lower the score threshold in Advanced options + +### Chunk Context Not Loading + +**Cause**: Network error or document no longer exists + +**Solution**: Check browser console for errors and verify the document still exists in Nextcloud + +### Links to Nextcloud Apps Not Working + +**Cause**: `NEXTCLOUD_PUBLIC_ISSUER_URL` not configured or incorrect + +**Solution**: Set the public URL for browser-accessible links: +```bash +export NEXTCLOUD_PUBLIC_ISSUER_URL=https://your-public-nextcloud-url.com +``` + +## Related Documentation + +- [Configuration Guide](../configuration.md) - Environment variables and settings +- [Authentication Modes](../authentication.md) - BasicAuth vs OAuth setup +- [Installation Guide](../installation.md) - Getting started with the MCP server +- [ADR-008: MCP Sampling for RAG](../ADR-008-mcp-sampling-for-rag.md) - Technical details on RAG workflows + +## FAQ + +**Q: Can I use this UI without vector sync enabled?** + +A: Yes, but you'll only have access to the User Info tab. Vector Sync and Vector Visualization features require `VECTOR_SYNC_ENABLED=true`. + +**Q: How often does the status refresh?** + +A: The Vector Sync Status tab polls every 10 seconds automatically using htmx. + +**Q: What's the difference between BM25 Hybrid and Semantic search?** + +A: Semantic search uses only vector embeddings for conceptual similarity. BM25 Hybrid combines semantic search with traditional keyword matching (BM25 sparse vectors) for better precision on exact terms. + +**Q: Can I search across multiple Nextcloud apps at once?** + +A: Yes! By default, searches query all indexed apps. Use the Advanced options to filter by specific document types. + +**Q: Why do some documents have higher scores than others?** + +A: Scores represent semantic similarity to your query. Higher scores indicate better matches based on vector similarity (semantic search) or a combination of vector similarity and keyword matching (BM25 hybrid). + +**Q: What does the color scale represent in the PCA plot?** + +A: The Viridis color scale represents relative relevance scores, with yellow indicating the most relevant documents and purple indicating lower relevance. diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py index acdb23d..56fd4f5 100644 --- a/nextcloud_mcp_server/app.py +++ b/nextcloud_mcp_server/app.py @@ -24,6 +24,7 @@ from starlette.middleware.authentication import AuthenticationMiddleware from starlette.middleware.cors import CORSMiddleware from starlette.responses import JSONResponse, RedirectResponse from starlette.routing import Mount, Route +from starlette.staticfiles import StaticFiles from nextcloud_mcp_server.auth import ( InsufficientScopeError, @@ -1491,7 +1492,7 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): # Create a separate Starlette app for browser routes that need session auth # This prevents SessionAuthBackend from interfering with FastMCP's OAuth browser_routes = [ - Route("/", user_info_html, methods=["GET"]), # /app → webapp (HTML UI) + Route("/", user_info_html, methods=["GET"]), # /app → user info with all tabs Route( "/revoke", revoke_session, methods=["POST"], name="revoke_session_endpoint" ), # /app/revoke → revoke_session @@ -1527,6 +1528,14 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None): ), ] + # Add static files mount if directory exists + static_dir = os.path.join(os.path.dirname(__file__), "auth", "static") + if os.path.isdir(static_dir): + browser_routes.append( + Mount("/static", StaticFiles(directory=static_dir), name="static") + ) + logger.info(f"Mounted static files from {static_dir}") + browser_app = Starlette(routes=browser_routes) browser_app.add_middleware( AuthenticationMiddleware, # type: ignore[invalid-argument-type] diff --git a/nextcloud_mcp_server/auth/static/nextcloud-logo.png b/nextcloud_mcp_server/auth/static/nextcloud-logo.png new file mode 100644 index 0000000..104315c Binary files /dev/null and b/nextcloud_mcp_server/auth/static/nextcloud-logo.png differ diff --git a/nextcloud_mcp_server/auth/static/vector-viz.css b/nextcloud_mcp_server/auth/static/vector-viz.css new file mode 100644 index 0000000..3e9398b --- /dev/null +++ b/nextcloud_mcp_server/auth/static/vector-viz.css @@ -0,0 +1,192 @@ +.viz-layout { + display: flex; + flex-direction: column; + gap: 16px; + height: 100%; + min-height: 0; + overflow-y: auto; +} +.viz-card { + background: var(--color-main-background); + border-radius: 0; + padding: 16px; + box-shadow: none; +} +.viz-controls-card { + flex: 0 0 auto; + border-bottom: 1px solid var(--color-border); + padding-bottom: 16px; +} +.viz-controls-grid { + display: grid; + grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); + gap: 12px; + align-items: end; +} +@media (min-width: 768px) { + .viz-controls-grid { + grid-template-columns: 2fr 1.5fr 1.5fr auto auto; + } +} +.viz-control-group { + display: flex; + flex-direction: column; + gap: 4px; +} +.viz-control-group label { + font-weight: 500; + color: var(--color-main-text); + font-size: 13px; +} +.viz-control-group input[type="text"], +.viz-control-group input[type="number"], +.viz-control-group select { + width: 100%; + padding: 7px 10px; + border: 1px solid var(--color-border-dark); + border-radius: var(--border-radius); + font-size: 14px; + background: var(--color-main-background); + color: var(--color-main-text); +} +.viz-control-group input:focus, +.viz-control-group select:focus { + outline: none; + border-color: var(--color-primary-element); +} +.viz-control-group input[type="range"] { + width: 100%; +} +.viz-control-group select[multiple] { + min-height: 100px; +} +.viz-weight-display { + display: inline-block; + min-width: 40px; + text-align: right; + color: #666; +} +.viz-btn { + background: var(--color-primary-element); + color: white; + border: none; + padding: 7px 16px; + border-radius: var(--border-radius); + cursor: pointer; + font-size: 14px; + font-weight: 500; + white-space: nowrap; +} +.viz-btn:hover { + background: #0052a3; +} +.viz-btn-secondary { + background: #6c757d; + color: white; + border: none; + padding: 7px 16px; + border-radius: var(--border-radius); + cursor: pointer; + font-size: 14px; + white-space: nowrap; +} +.viz-btn-secondary:hover { + background: #5a6268; +} +.viz-card-plot { + flex: 0 0 auto; + display: flex; + flex-direction: column; + min-height: 500px; + height: 600px; + /* Remove horizontal padding to extend to full viewport width */ + padding-left: 0; + padding-right: 0; + margin-left: -16px; + margin-right: -16px; +} +#viz-plot-container { + width: 100%; + height: 100%; + position: relative; + overflow: visible; +} +#viz-plot { + width: 100%; + height: 100%; +} +.viz-loading { + text-align: center; + padding: 40px; + color: #666; +} +.viz-loading-overlay { + position: absolute; + inset: 0; + display: flex; + align-items: center; + justify-content: center; + background: white; + color: #666; +} +.viz-no-results { + text-align: center; + padding: 40px; + color: #666; + font-style: italic; +} +.viz-advanced-section { + margin-top: 12px; + padding: 12px; + background: var(--color-background-hover); + border-radius: var(--border-radius); + border: 1px solid var(--color-border); +} +.viz-info-box { + background: var(--color-primary-element-light); + border-left: 3px solid var(--color-primary-element); + padding: 10px 12px; + margin-bottom: 16px; + font-size: 13px; + color: var(--color-main-text); +} +.chunk-toggle-btn { + background: #6c757d; + color: white; + border: none; + padding: 4px 10px; + border-radius: 3px; + cursor: pointer; + font-size: 12px; + margin-top: 6px; +} +.chunk-toggle-btn:hover { + background: #5a6268; +} +.chunk-context { + background: var(--color-background-hover); + border: 1px solid var(--color-border); + border-radius: var(--border-radius); + padding: 12px; + margin-top: 8px; + font-family: 'SFMono-Regular', 'Consolas', 'Liberation Mono', 'Menlo', monospace; + font-size: 13px; + line-height: 1.6; + white-space: pre-wrap; + word-wrap: break-word; +} +.chunk-text { + color: var(--color-text-maxcontrast); +} +.chunk-matched { + background: #fff3cd; + border: 1px solid #ffc107; + padding: 2px 4px; + border-radius: var(--border-radius); + font-weight: 500; + color: var(--color-main-text); +} +.chunk-ellipsis { + color: var(--color-text-maxcontrast); + font-style: italic; +} diff --git a/nextcloud_mcp_server/auth/static/vector-viz.js b/nextcloud_mcp_server/auth/static/vector-viz.js new file mode 100644 index 0000000..cd9f557 --- /dev/null +++ b/nextcloud_mcp_server/auth/static/vector-viz.js @@ -0,0 +1,212 @@ +// Initialize vizApp for vector visualization +function vizApp() { + return { + query: '', + algorithm: 'bm25_hybrid', + fusion: 'rrf', + showAdvanced: false, + showQueryPoint: true, + docTypes: [''], + limit: 50, + scoreThreshold: 0.0, + loading: false, + results: [], + coordinates: null, + queryCoords: null, + 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.coordinates = data.coordinates_3d; + this.queryCoords = data.query_coords; + this.renderPlot(this.coordinates, this.queryCoords, this.results); + } else { + alert('Search failed: ' + data.error); + } + } catch (error) { + alert('Error: ' + error.message); + } finally { + this.loading = false; + } + }, + + updatePlot() { + // Re-render plot with current data when toggle changes + if (this.coordinates && this.queryCoords && this.results.length > 0) { + this.renderPlot(this.coordinates, this.queryCoords, this.results); + } + }, + + renderPlot(coordinates, queryCoords, results) { + const scores = results.map(r => r.score); + + // Trace 1: Document results + const documentTrace = { + x: coordinates.map(c => c[0]), + y: coordinates.map(c => c[1]), + z: coordinates.map(c => c[2]), + mode: 'markers', + type: 'scatter3d', + name: 'Documents', + customdata: results.map((r, i) => ({ + title: r.title, + raw_score: r.original_score, + relative_score: r.score, + x: coordinates[i][0], + y: coordinates[i][1], + z: coordinates[i][2] + })), + hovertemplate: + '%{customdata.title}
' + + 'Raw Score: %{customdata.raw_score:.3f} (%{customdata.relative_score:.0%} relative)
' + + '(x=%{customdata.x}, y=%{customdata.y}, z=%{customdata.z})' + + '', + marker: { + size: results.map(r => 4 + (Math.pow(r.score, 2) * 10)), + opacity: results.map(r => 0.3 + (r.score * 0.7)), + color: scores, + colorscale: 'Viridis', + showscale: true, + colorbar: { title: 'Relative Score' }, + cmin: 0, + cmax: 1 + } + }; + + // Trace 2: Query point (distinct marker) + const queryTrace = { + x: [queryCoords[0]], + y: [queryCoords[1]], + z: [queryCoords[2]], + mode: 'markers', + type: 'scatter3d', + name: 'Query', + hovertemplate: + 'Search Query
' + + `(x=${queryCoords[0]}, y=${queryCoords[1]}, z=${queryCoords[2]})` + + '', + marker: { + size: 10, + color: '#ef5350', // Subdued red (Material Design Red 400) + line: { + color: '#c62828', // Darker red border (Material Design Red 800) + width: 1 + } + } + }; + + const layout = { + title: `Vector Space (PCA 3D) - ${results.length} results`, + scene: { + xaxis: { title: 'PC1' }, + yaxis: { title: 'PC2' }, + zaxis: { title: 'PC3' }, + camera: { + eye: { x: 1.5, y: 1.5, z: 1.5 } + } + }, + hovermode: 'closest', + autosize: true, // Enable auto-sizing to fit container + showlegend: true, + margin: { l: 0, r: 0, t: 40, b: 0 } // Minimize margins for full width + }; + + // Conditionally include query trace based on toggle + const traces = this.showQueryPoint ? [documentTrace, queryTrace] : [documentTrace]; + + // Enable responsive resizing + const config = { + responsive: true, + displayModeBar: true + }; + + Plotly.newPlot('viz-plot', traces, layout, config); + }, + + getNextcloudUrl(result) { + // Use global NEXTCLOUD_BASE_URL if set, otherwise construct from window location + const baseUrl = window.NEXTCLOUD_BASE_URL || ''; + 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]; + } + } + }; +} diff --git a/nextcloud_mcp_server/auth/templates/user_info.html b/nextcloud_mcp_server/auth/templates/user_info.html index f107eed..227114f 100644 --- a/nextcloud_mcp_server/auth/templates/user_info.html +++ b/nextcloud_mcp_server/auth/templates/user_info.html @@ -31,15 +31,187 @@ 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 %} -
+