diff --git a/nextcloud_mcp_server/app.py b/nextcloud_mcp_server/app.py
index 93f0ea1..501bc16 100644
--- a/nextcloud_mcp_server/app.py
+++ b/nextcloud_mcp_server/app.py
@@ -1477,6 +1477,10 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
user_info_html,
vector_sync_status_fragment,
)
+ from nextcloud_mcp_server.auth.viz_routes import (
+ vector_visualization_html,
+ vector_visualization_search,
+ )
from nextcloud_mcp_server.auth.webhook_routes import (
disable_webhook_preset,
enable_webhook_preset,
@@ -1496,6 +1500,15 @@ def get_app(transport: str = "sse", enabled_apps: list[str] | None = None):
vector_sync_status_fragment,
methods=["GET"],
), # /app/vector-sync/status
+ # Vector visualization routes
+ Route(
+ "/vector-viz", vector_visualization_html, methods=["GET"]
+ ), # /app/vector-viz
+ Route(
+ "/vector-viz/search",
+ vector_visualization_search,
+ methods=["GET"],
+ ), # /app/vector-viz/search
# Webhook management routes (admin-only)
Route("/webhooks", webhook_management_pane, methods=["GET"]), # /app/webhooks
Route(
diff --git a/nextcloud_mcp_server/auth/viz_routes.py b/nextcloud_mcp_server/auth/viz_routes.py
index da2f0ab..64137cd 100644
--- a/nextcloud_mcp_server/auth/viz_routes.py
+++ b/nextcloud_mcp_server/auth/viz_routes.py
@@ -183,6 +183,21 @@ async def vector_visualization_html(request: Request) -> HTMLResponse:
+
+
+
+
+ Hold Ctrl/Cmd to select multiple. Select "All Types" for cross-app search.
+
+
+
@@ -249,6 +264,7 @@ async def vector_visualization_html(request: Request) -> HTMLResponse:
return {{
query: '',
algorithm: 'hybrid',
+ docTypes: [''], // Default to "All Types"
limit: 50,
scoreThreshold: 0.7,
semanticWeight: 0.5,
@@ -276,6 +292,12 @@ async def vector_visualization_html(request: Request) -> HTMLResponse:
fuzzy_weight: this.fuzzyWeight,
}});
+ // Add doc_types parameter (filter out empty string for "All Types")
+ 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();
@@ -371,9 +393,13 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
keyword_weight = float(request.query_params.get("keyword_weight", "0.3"))
fuzzy_weight = float(request.query_params.get("fuzzy_weight", "0.2"))
+ # Parse doc_types (comma-separated list, None = all types)
+ doc_types_param = request.query_params.get("doc_types", "")
+ doc_types = doc_types_param.split(",") if doc_types_param else None
+
logger.info(
f"Viz search: user={username}, query='{query}', "
- f"algorithm={algorithm}, limit={limit}"
+ f"algorithm={algorithm}, limit={limit}, doc_types={doc_types}"
)
try:
@@ -445,15 +471,44 @@ async def vector_visualization_search(request: Request) -> JSONResponse:
status_code=400,
)
- # Execute search
- search_results = await search_algo.search(
- query=query,
- user_id=username,
- limit=limit,
- doc_type="note",
- nextcloud_client=nextcloud_client,
- score_threshold=score_threshold,
- )
+ # Execute search (supports cross-app when doc_types=None)
+ if doc_types is None or len(doc_types) == 0:
+ # Cross-app search - search all indexed types
+ search_results = await search_algo.search(
+ query=query,
+ user_id=username,
+ limit=limit,
+ doc_type=None, # Search all types
+ nextcloud_client=nextcloud_client,
+ score_threshold=score_threshold,
+ )
+ elif len(doc_types) == 1:
+ # Single document type
+ search_results = await search_algo.search(
+ query=query,
+ user_id=username,
+ limit=limit,
+ doc_type=doc_types[0],
+ nextcloud_client=nextcloud_client,
+ score_threshold=score_threshold,
+ )
+ else:
+ # Multiple document types - search each and combine
+ all_results = []
+ for doc_type in doc_types:
+ results = await search_algo.search(
+ query=query,
+ user_id=username,
+ limit=limit * 2, # Get extra per type
+ doc_type=doc_type,
+ nextcloud_client=nextcloud_client,
+ score_threshold=score_threshold,
+ )
+ all_results.extend(results)
+
+ # Sort by score and limit
+ all_results.sort(key=lambda r: r.score, reverse=True)
+ search_results = all_results[:limit]
if not search_results:
return JSONResponse(