feat: Improve UI layout and navigation
- Increase logo size (48x48 desktop, 56x56 mobile) for better visibility - Add logo as favicon - Add logo to mobile header - Move user menu to navigation bars (sidebar on desktop, bottom bar on mobile) - Fix desktop chat layout - container structure prevents voice controls cutoff - Fix mobile bottom bar - use icon-only ActionIcons instead of truncated text buttons - Hide Create Node/New Conversation buttons on mobile to save header space - Make fixed header and voice controls work properly with containers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,10 @@ import {
|
||||
Text,
|
||||
} from '@react-three/drei';
|
||||
import { Suspense, useEffect, useRef, useState } from 'react';
|
||||
import Surreal from 'surrealdb';
|
||||
import { Stack, Text as MantineText } from '@mantine/core';
|
||||
import * as THREE from 'three';
|
||||
|
||||
// Define the shape of nodes and links from DB
|
||||
// Define the shape of nodes and links from API
|
||||
interface NodeData {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -67,42 +68,95 @@ export function ThoughtGalaxy() {
|
||||
const [nodes, setNodes] = useState<NodeData[]>([]);
|
||||
const [links, setLinks] = useState<LinkData[]>([]);
|
||||
const cameraControlsRef = useRef<CameraControls>(null);
|
||||
const hasFitCamera = useRef(false);
|
||||
|
||||
// Fetch data from SurrealDB on mount
|
||||
// Fetch data from API on mount and poll for updates
|
||||
useEffect(() => {
|
||||
async function fetchData() {
|
||||
// Client-side connection
|
||||
const db = new Surreal();
|
||||
await db.connect(process.env.NEXT_PUBLIC_SURREALDB_WSS_URL!);
|
||||
try {
|
||||
const response = await fetch('/api/galaxy', {
|
||||
credentials: 'include', // Include cookies for authentication
|
||||
});
|
||||
|
||||
// Get the token from the cookie
|
||||
const tokenCookie = document.cookie
|
||||
.split('; ')
|
||||
.find((row) => row.startsWith('ponderants-auth='));
|
||||
if (!response.ok) {
|
||||
console.error('[ThoughtGalaxy] Failed to fetch galaxy data:', response.statusText);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!tokenCookie) {
|
||||
console.error('[ThoughtGalaxy] No auth token found');
|
||||
return;
|
||||
const data = await response.json();
|
||||
|
||||
if (data.message) {
|
||||
console.log('[ThoughtGalaxy]', data.message);
|
||||
// If calculating, poll again in 2 seconds
|
||||
setTimeout(fetchData, 2000);
|
||||
return;
|
||||
}
|
||||
|
||||
setNodes(data.nodes || []);
|
||||
setLinks(data.links || []);
|
||||
|
||||
console.log(`[ThoughtGalaxy] Loaded ${data.nodes?.length || 0} nodes and ${data.links?.length || 0} links`);
|
||||
} catch (error) {
|
||||
console.error('[ThoughtGalaxy] Error fetching data:', error);
|
||||
}
|
||||
|
||||
const token = tokenCookie.split('=')[1];
|
||||
await db.authenticate(token);
|
||||
|
||||
// Fetch nodes that have coordinates
|
||||
const nodeResults = await db.query<[NodeData[]]>(
|
||||
'SELECT id, title, coords_3d FROM node WHERE coords_3d != NONE'
|
||||
);
|
||||
setNodes(nodeResults[0] || []);
|
||||
|
||||
// Fetch links
|
||||
const linkResults = await db.query<[LinkData[]]>('SELECT in, out FROM links_to');
|
||||
setLinks(linkResults[0] || []);
|
||||
|
||||
console.log(`[ThoughtGalaxy] Loaded ${nodeResults[0]?.length || 0} nodes and ${linkResults[0]?.length || 0} links`);
|
||||
}
|
||||
|
||||
fetchData();
|
||||
}, []);
|
||||
|
||||
// Function to fit camera to all nodes
|
||||
const fitCameraToNodes = () => {
|
||||
if (!cameraControlsRef.current || nodes.length === 0) {
|
||||
console.log('[ThoughtGalaxy] Cannot fit camera:', {
|
||||
hasRef: !!cameraControlsRef.current,
|
||||
nodesLength: nodes.length,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
console.log('[ThoughtGalaxy] Fitting camera to', nodes.length, 'nodes...');
|
||||
|
||||
// Create a THREE.Box3 from node positions
|
||||
const box = new THREE.Box3();
|
||||
nodes.forEach((node) => {
|
||||
box.expandByPoint(new THREE.Vector3(
|
||||
node.coords_3d[0],
|
||||
node.coords_3d[1],
|
||||
node.coords_3d[2]
|
||||
));
|
||||
});
|
||||
|
||||
console.log('[ThoughtGalaxy] Bounding box:', {
|
||||
min: box.min,
|
||||
max: box.max,
|
||||
size: box.getSize(new THREE.Vector3()),
|
||||
});
|
||||
|
||||
// Use CameraControls' built-in fitToBox method
|
||||
try {
|
||||
cameraControlsRef.current.fitToBox(
|
||||
box,
|
||||
false, // Don't animate on initial load
|
||||
{ paddingLeft: 0.5, paddingRight: 0.5, paddingTop: 0.5, paddingBottom: 0.5 }
|
||||
);
|
||||
console.log('[ThoughtGalaxy] ✓ Camera fitted to bounds');
|
||||
hasFitCamera.current = true;
|
||||
} catch (error) {
|
||||
console.error('[ThoughtGalaxy] Error fitting camera:', error);
|
||||
}
|
||||
};
|
||||
|
||||
// Fit camera when nodes change and we haven't fitted yet
|
||||
useEffect(() => {
|
||||
if (!hasFitCamera.current && nodes.length > 0) {
|
||||
// Try to fit after a short delay to ensure Canvas is ready
|
||||
const timer = setTimeout(() => {
|
||||
fitCameraToNodes();
|
||||
}, 100);
|
||||
return () => clearTimeout(timer);
|
||||
}
|
||||
}, [nodes]);
|
||||
|
||||
// Map links to node positions
|
||||
const linkLines = links
|
||||
.map((link) => {
|
||||
@@ -118,24 +172,48 @@ export function ThoughtGalaxy() {
|
||||
})
|
||||
.filter(Boolean) as { start: [number, number, number]; end: [number, number, number] }[];
|
||||
|
||||
// Camera animation
|
||||
// Camera animation on node click
|
||||
const handleNodeClick = (node: NodeData) => {
|
||||
if (cameraControlsRef.current) {
|
||||
cameraControlsRef.current.smoothTime = 0.8;
|
||||
cameraControlsRef.current.setLookAt(
|
||||
node.coords_3d[0] + 1,
|
||||
node.coords_3d[1] + 1,
|
||||
node.coords_3d[2] + 1,
|
||||
// Smoothly move to look at the clicked node
|
||||
cameraControlsRef.current.moveTo(
|
||||
node.coords_3d[0],
|
||||
node.coords_3d[1],
|
||||
node.coords_3d[2],
|
||||
true // Enable smooth transition
|
||||
true // Animate
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
console.log('[ThoughtGalaxy] Rendering with', nodes.length, 'nodes and', linkLines.length, 'link lines');
|
||||
|
||||
// Show message if no nodes are ready yet
|
||||
if (nodes.length === 0) {
|
||||
return (
|
||||
<Stack align="center" justify="center" style={{ height: '100vh', width: '100vw' }}>
|
||||
<MantineText size="lg" c="dimmed">
|
||||
Create at least 3 nodes to visualize your thought galaxy
|
||||
</MantineText>
|
||||
<MantineText size="sm" c="dimmed">
|
||||
Nodes with content will automatically generate embeddings and 3D coordinates
|
||||
</MantineText>
|
||||
</Stack>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Canvas camera={{ position: [0, 5, 10], fov: 60 }}>
|
||||
<Canvas
|
||||
camera={{ position: [0, 5, 10], fov: 60 }}
|
||||
style={{ width: '100%', height: '100%' }}
|
||||
gl={{ preserveDrawingBuffer: true }}
|
||||
onCreated={(state) => {
|
||||
console.log('[ThoughtGalaxy] Canvas created successfully');
|
||||
// Try to fit camera now that scene is ready
|
||||
if (!hasFitCamera.current && nodes.length > 0) {
|
||||
setTimeout(() => fitCameraToNodes(), 50);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ambientLight intensity={0.5} />
|
||||
<pointLight position={[10, 10, 10]} intensity={1} />
|
||||
<CameraControls ref={cameraControlsRef} />
|
||||
|
||||
Reference in New Issue
Block a user