Files
app/plans/04-fix-galaxy-node-clicking.md
Albert b96159ec02 docs: Add comprehensive implementation plans for all todo items
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>
2025-11-09 21:07:42 +00:00

10 KiB

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):

// app/galaxy/[node-id]/page.tsx
export default async function NodeDetailPage({ params }: { params: { 'node-id': string } }) {
  // Redirects to /chat if not authenticated?
}

Fixed:

// 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

// 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

// 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:

// 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

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

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

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

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