Created detailed markdown plans for all items in todo.md: 1. 01-playwright-scaffolding.md - Base Playwright infrastructure 2. 02-magnitude-tests-comprehensive.md - Complete test coverage 3. 03-stream-ai-to-deepgram-tts.md - TTS latency optimization 4. 04-fix-galaxy-node-clicking.md - Galaxy navigation bugs 5. 05-dark-light-mode-theme.md - Dark/light mode with dynamic favicons 6. 06-fix-double-border-desktop.md - UI polish 7. 07-delete-backup-files.md - Code cleanup 8. 08-ai-transition-to-edit.md - Intelligent node creation flow 9. 09-umap-minimum-nodes-analysis.md - Technical analysis Each plan includes: - Detailed problem analysis - Proposed solutions with code examples - Manual Playwright MCP testing strategy - Magnitude test specifications - Implementation steps - Success criteria Ready to implement in sequence. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
384 lines
10 KiB
Markdown
384 lines
10 KiB
Markdown
# Plan: Fix Galaxy Node Clicking and Navigation
|
|
|
|
**Priority:** HIGH - Critical user experience issue
|
|
**Dependencies:** None
|
|
**Affects:** Galaxy visualization navigation, user frustration
|
|
|
|
## Overview
|
|
|
|
There are two critical bugs with galaxy node interaction:
|
|
1. When going directly to a node ID link (`/galaxy/node:xxx`), it redirects to `/chat`
|
|
2. When clicking on a node in `/galaxy` (either general or on a specific node ID URL), the modal closes automatically
|
|
|
|
Both issues severely impact the galaxy user experience and need immediate fixing.
|
|
|
|
## Current Broken Behavior
|
|
|
|
### Issue 1: Direct Node URL Redirects to Chat
|
|
```
|
|
User clicks: /galaxy/node:abc123
|
|
Expected: Shows node detail page with full content
|
|
Actual: Redirects to /chat
|
|
```
|
|
|
|
### Issue 2: Modal Auto-Closes on Node Click
|
|
```
|
|
User action: Click node sphere in galaxy visualization
|
|
Expected: Modal stays open showing node details
|
|
Actual: Modal opens briefly then closes immediately
|
|
```
|
|
|
|
## Root Cause Analysis
|
|
|
|
Let me investigate the current implementation:
|
|
|
|
### 1. Check `/app/galaxy/[node-id]/page.tsx`
|
|
|
|
Likely issues:
|
|
- Missing authentication check causing redirect
|
|
- Incorrect route parameter parsing
|
|
- Navigation logic in wrong place
|
|
|
|
### 2. Check Galaxy Modal Component
|
|
|
|
Likely issues:
|
|
- Event handler conflict (click bubbling to parent)
|
|
- State management issue (modal state reset)
|
|
- React Three Fiber event handling bug
|
|
|
|
## Proposed Fixes
|
|
|
|
### Fix 1: Node Detail Page Route
|
|
|
|
#### Current (broken):
|
|
```typescript
|
|
// app/galaxy/[node-id]/page.tsx
|
|
export default async function NodeDetailPage({ params }: { params: { 'node-id': string } }) {
|
|
// Redirects to /chat if not authenticated?
|
|
}
|
|
```
|
|
|
|
#### Fixed:
|
|
```typescript
|
|
// app/galaxy/[node-id]/page.tsx
|
|
import { cookies } from 'next/headers';
|
|
import { redirect } from 'next/navigation';
|
|
import { verifySurrealJwt } from '@/lib/auth/jwt';
|
|
import { connectToDB } from '@/lib/db';
|
|
|
|
export default async function NodeDetailPage({
|
|
params,
|
|
}: {
|
|
params: { 'node-id': string };
|
|
}) {
|
|
// 1. Verify authentication
|
|
const cookieStore = await cookies();
|
|
const surrealJwt = cookieStore.get('ponderants-auth')?.value;
|
|
|
|
if (!surrealJwt) {
|
|
// Redirect to login, preserving the intended destination
|
|
redirect(`/login?redirect=/galaxy/${params['node-id']}`);
|
|
}
|
|
|
|
const userSession = verifySurrealJwt(surrealJwt);
|
|
if (!userSession) {
|
|
redirect(`/login?redirect=/galaxy/${params['node-id']}`);
|
|
}
|
|
|
|
// 2. Fetch node data
|
|
const db = await connectToDB();
|
|
const nodeId = params['node-id'];
|
|
|
|
const nodes = await db.select<NodeData>(nodeId);
|
|
const node = nodes[0];
|
|
|
|
if (!node) {
|
|
// Node doesn't exist or user doesn't have access
|
|
redirect('/galaxy?error=node-not-found');
|
|
}
|
|
|
|
// 3. Verify user owns this node
|
|
if (node.user_did !== userSession.did) {
|
|
redirect('/galaxy?error=unauthorized');
|
|
}
|
|
|
|
// 4. Render node detail page
|
|
return (
|
|
<Stack p="xl">
|
|
<Title order={1}>{node.title}</Title>
|
|
<Text>{node.body}</Text>
|
|
{node.atp_uri && (
|
|
<Anchor href={node.atp_uri} target="_blank">
|
|
View on Bluesky
|
|
</Anchor>
|
|
)}
|
|
<Button component={Link} href="/galaxy">
|
|
Back to Galaxy
|
|
</Button>
|
|
</Stack>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Fix 2: Galaxy Modal Component
|
|
|
|
#### Problem: Click event bubbling
|
|
|
|
When user clicks node in R3F, the click event might be:
|
|
- Bubbling to parent canvas
|
|
- Triggering canvas click handler that closes modal
|
|
- Or: React state update race condition
|
|
|
|
#### Solution: Stop event propagation
|
|
|
|
```typescript
|
|
// components/galaxy/NodeSphere.tsx
|
|
import { useRef } from 'react';
|
|
import { ThreeEvent } from '@react-three/fiber';
|
|
|
|
function NodeSphere({ node, onClick }: { node: NodeData; onClick: (node: NodeData) => void }) {
|
|
const meshRef = useRef();
|
|
|
|
const handleClick = (event: ThreeEvent<MouseEvent>) => {
|
|
// CRITICAL: Stop propagation to prevent canvas click handler
|
|
event.stopPropagation();
|
|
|
|
// Call parent onClick
|
|
onClick(node);
|
|
};
|
|
|
|
return (
|
|
<mesh
|
|
ref={meshRef}
|
|
position={node.coords_3d}
|
|
onClick={handleClick}
|
|
>
|
|
<sphereGeometry args={[0.1, 16, 16]} />
|
|
<meshStandardMaterial color="#fff" />
|
|
</mesh>
|
|
);
|
|
}
|
|
```
|
|
|
|
#### Problem: Modal state management
|
|
|
|
Modal might be controlled by both:
|
|
- Galaxy component state
|
|
- URL query params
|
|
- Leading to race condition
|
|
|
|
#### Solution: Single source of truth
|
|
|
|
```typescript
|
|
// app/galaxy/page.tsx
|
|
'use client';
|
|
|
|
import { useState, useEffect } from 'react';
|
|
import { useSearchParams, useRouter } from 'next/navigation';
|
|
|
|
export default function GalaxyPage() {
|
|
const searchParams = useSearchParams();
|
|
const router = useRouter();
|
|
|
|
// Modal state controlled ONLY by URL query param
|
|
const selectedNodeId = searchParams.get('node');
|
|
|
|
const handleNodeClick = (nodeId: string) => {
|
|
// Update URL to open modal
|
|
router.push(`/galaxy?node=${encodeURIComponent(nodeId)}`, { scroll: false });
|
|
};
|
|
|
|
const handleModalClose = () => {
|
|
// Remove query param to close modal
|
|
router.push('/galaxy', { scroll: false });
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<ThoughtGalaxy
|
|
nodes={nodes}
|
|
links={links}
|
|
onNodeClick={handleNodeClick}
|
|
/>
|
|
|
|
<Modal
|
|
opened={!!selectedNodeId}
|
|
onClose={handleModalClose}
|
|
size="lg"
|
|
>
|
|
{selectedNodeId && <NodeDetailModal nodeId={selectedNodeId} />}
|
|
</Modal>
|
|
</>
|
|
);
|
|
}
|
|
```
|
|
|
|
### Fix 3: Canvas Click Handler
|
|
|
|
If canvas has a click handler that closes modal, remove it or add condition:
|
|
|
|
```typescript
|
|
// components/galaxy/ThoughtGalaxy.tsx
|
|
<Canvas
|
|
onClick={(e) => {
|
|
// Only close modal if clicking on background (not a node)
|
|
if (e.target === e.currentTarget) {
|
|
onBackgroundClick?.();
|
|
}
|
|
}}
|
|
>
|
|
{/* ... */}
|
|
</Canvas>
|
|
```
|
|
|
|
## Testing Strategy
|
|
|
|
### Manual Playwright MCP Testing
|
|
|
|
#### Test 1: Direct Node URL
|
|
```typescript
|
|
test('Direct node URL loads correctly', async ({ page }) => {
|
|
// Create a node first
|
|
const nodeId = await createTestNode(page);
|
|
|
|
// Navigate directly to node detail page
|
|
await page.goto(`/galaxy/${nodeId}`);
|
|
|
|
// Verify we're on the correct page
|
|
await expect(page).toHaveURL(/\/galaxy\/node:/);
|
|
await expect(page.getByRole('heading', { level: 1 })).toBeVisible();
|
|
await expect(page.getByText(/View on Bluesky/)).toBeVisible();
|
|
});
|
|
```
|
|
|
|
#### Test 2: Modal Stays Open
|
|
```typescript
|
|
test('Galaxy modal stays open when clicking node', async ({ page }) => {
|
|
await page.goto('/galaxy');
|
|
|
|
// Click on a node sphere
|
|
await page.evaluate(() => {
|
|
const event = new CustomEvent('node-click', { detail: { nodeId: 'node:123' } });
|
|
window.dispatchEvent(event);
|
|
});
|
|
|
|
// Modal should open
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
|
|
// Wait 2 seconds
|
|
await page.waitForTimeout(2000);
|
|
|
|
// Modal should still be open
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
});
|
|
```
|
|
|
|
#### Test 3: Modal Navigation
|
|
```typescript
|
|
test('Can navigate from galaxy to node detail page', async ({ page }) => {
|
|
await page.goto('/galaxy');
|
|
|
|
// Click node to open modal
|
|
await clickNode(page, 'node:123');
|
|
|
|
// Modal opens
|
|
await expect(page.getByRole('dialog')).toBeVisible();
|
|
|
|
// Click "View Full Detail"
|
|
await page.click('button:has-text("View Full Detail")');
|
|
|
|
// Navigate to detail page
|
|
await expect(page).toHaveURL(/\/galaxy\/node:123/);
|
|
await expect(page.getByRole('dialog')).not.toBeVisible();
|
|
});
|
|
```
|
|
|
|
### Magnitude Tests
|
|
|
|
```typescript
|
|
import { test } from 'magnitude-test';
|
|
|
|
test('User can navigate to node via direct URL', async (agent) => {
|
|
// Create a node first
|
|
await agent.open('http://localhost:3000/chat');
|
|
await agent.act('Create a test node');
|
|
const nodeUrl = await agent.getData('node-url');
|
|
|
|
// Navigate directly to node URL
|
|
await agent.open(nodeUrl);
|
|
await agent.check('Node detail page loads');
|
|
await agent.check('Node title is visible');
|
|
await agent.check('Node body is visible');
|
|
await agent.check('Bluesky link is visible');
|
|
});
|
|
|
|
test('Galaxy modal stays open when clicking nodes', async (agent) => {
|
|
await agent.open('http://localhost:3000/galaxy');
|
|
await agent.act('Click on a node sphere');
|
|
|
|
await agent.check('Modal opens');
|
|
await agent.wait(2000);
|
|
await agent.check('Modal is still open');
|
|
await agent.check('Node details are visible');
|
|
});
|
|
|
|
test('Can navigate from galaxy modal to node detail page', async (agent) => {
|
|
await agent.open('http://localhost:3000/galaxy');
|
|
await agent.act('Click on a node');
|
|
await agent.check('Modal opens');
|
|
|
|
await agent.act('Click "View Full Detail" button');
|
|
await agent.check('Navigated to node detail page');
|
|
await agent.check('URL contains node ID');
|
|
await agent.check('Full node content is visible');
|
|
});
|
|
```
|
|
|
|
## Implementation Steps
|
|
|
|
1. **Fix node detail page route**
|
|
- Add proper authentication check
|
|
- Add redirect with return URL
|
|
- Add node ownership verification
|
|
- Add error handling for missing nodes
|
|
|
|
2. **Fix modal event handling**
|
|
- Add `event.stopPropagation()` to node click handler
|
|
- Remove or condition any canvas-level click handlers
|
|
- Ensure modal state is controlled by URL query param
|
|
|
|
3. **Test with Playwright MCP**
|
|
- Test direct URL navigation
|
|
- Test modal stays open
|
|
- Test modal to detail page navigation
|
|
- Fix any issues found
|
|
|
|
4. **Add Magnitude tests**
|
|
- Write tests covering all 3 scenarios
|
|
- Ensure tests pass
|
|
|
|
5. **Deploy and verify in production**
|
|
|
|
## Success Criteria
|
|
|
|
- ✅ Direct node URLs (`/galaxy/node:xxx`) load correctly
|
|
- ✅ No redirect to `/chat` when accessing node URLs
|
|
- ✅ Galaxy modal stays open after clicking node
|
|
- ✅ Can navigate from modal to detail page
|
|
- ✅ Can navigate from detail page back to galaxy
|
|
- ✅ All Playwright MCP tests pass
|
|
- ✅ All Magnitude tests pass
|
|
- ✅ No console errors during navigation
|
|
|
|
## Files to Update
|
|
|
|
1. `app/galaxy/[node-id]/page.tsx` - Fix authentication and routing
|
|
2. `app/galaxy/page.tsx` - Fix modal state management
|
|
3. `components/galaxy/ThoughtGalaxy.tsx` - Fix canvas click handling
|
|
4. `components/galaxy/NodeSphere.tsx` - Fix node click event propagation
|
|
|
|
## Files to Create
|
|
|
|
1. `tests/playwright/galaxy-navigation.spec.ts` - Manual tests
|
|
2. `tests/magnitude/galaxy-navigation.mag.ts` - Magnitude tests
|