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(