feat(astrolabe): add 3D PCA visualization for semantic search

- Add Plotly.js 3D scatter plot showing search results in PCA space
- Create shared visualization.py module to avoid code duplication
- Pass include_pca parameter through API chain to enable coordinates
- Fix OAuth redirects to use /settings/user/astroglobe

The visualization shows document embeddings projected to 3D via PCA,
with the query point highlighted in red. Uses Viridis colorscale
for score visualization, matching the existing vector-viz page.

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

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Chris Coutinho
2025-12-14 20:50:39 +01:00
parent a4106ee20d
commit 97b48ca3dd
8 changed files with 970 additions and 12 deletions
+23 -7
View File
@@ -61,7 +61,7 @@ class ApiController extends Controller {
// Should not happen (NoAdminRequired ensures user is logged in)
$this->logger->error('Revoke access called without authenticated user');
return new RedirectResponse(
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'mcp'])
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'astroglobe'])
);
}
@@ -72,7 +72,7 @@ class ApiController extends Controller {
if (!$token) {
$this->logger->error("Cannot revoke access: No token found for user $userId");
return new RedirectResponse(
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'mcp'])
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'astroglobe'])
);
}
@@ -93,7 +93,7 @@ class ApiController extends Controller {
// Redirect back to personal settings
return new RedirectResponse(
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'mcp'])
$this->urlGenerator->linkToRoute('settings.PersonalSettings.index', ['section' => 'astroglobe'])
);
}
@@ -107,6 +107,7 @@ class ApiController extends Controller {
* @param string $algorithm Search algorithm (semantic, bm25, hybrid)
* @param int $limit Number of results (max 50)
* @param string $doc_types Comma-separated document types (e.g., "note,file")
* @param string $include_pca Whether to include PCA coordinates for visualization
* @return JSONResponse
*/
#[NoAdminRequired]
@@ -114,7 +115,8 @@ class ApiController extends Controller {
string $query = '',
string $algorithm = 'hybrid',
int $limit = 10,
string $doc_types = ''
string $doc_types = '',
string $include_pca = 'true'
): JSONResponse {
if (empty($query)) {
return new JSONResponse([
@@ -166,8 +168,11 @@ class ApiController extends Controller {
}
}
// Parse include_pca (string "true"/"false" from query params)
$includePcaBool = in_array(strtolower($include_pca), ['true', '1', 'yes'], true);
// Execute search via MCP server with OAuth token
$result = $this->client->search($query, $algorithm, $limit, false, $docTypesArray, $accessToken);
$result = $this->client->search($query, $algorithm, $limit, $includePcaBool, $docTypesArray, $accessToken);
if (isset($result['error'])) {
return new JSONResponse([
@@ -176,12 +181,23 @@ class ApiController extends Controller {
], Http::STATUS_INTERNAL_SERVER_ERROR);
}
return new JSONResponse([
$response = [
'success' => true,
'results' => $result['results'] ?? [],
'algorithm_used' => $result['algorithm_used'] ?? $algorithm,
'total_documents' => $result['total_documents'] ?? 0,
]);
];
// Include PCA visualization coordinates if requested and available
if ($includePcaBool) {
$response['coordinates_3d'] = $result['coordinates_3d'] ?? [];
$response['query_coords'] = $result['query_coords'] ?? [];
if (isset($result['pca_variance'])) {
$response['pca_variance'] = $result['pca_variance'];
}
}
return new JSONResponse($response);
}
/**