feat(phase-b): Supabase navigation store, snapshot service, auth wiring
navigationStore: rewritten off Neo4j db names — Supabase whiteboard_rooms table, setAuthInfo(token, userId) pattern, auto-creates default room per context on first use snapshotService: rewritten to Supabase Storage REST (/storage/v1/object/authenticated/cc.users/…), setAccessToken() instance method, static methods take accessToken not dbName AuthContext/NeoUserContext: auth injected into nav store, no Neo4j db names required singlePlayerPage: loadNodeData no longer calls Neo4j; snapshot wired via accessToken navigation types: NeoGraphNode updated for Supabase-backed tree structure transcriptionStore/Service: getSession() removed, accessToken via AuthContext LLMConfigModal: auth context wiring fixes GraphNavigator/GraphSidebar: updated nav components Co-Authored-By: Claude Sonnet 4.6 <[email protected]>
This commit is contained in:
@@ -10,11 +10,11 @@ import {
|
||||
TLStoreWithStatus
|
||||
} from '@tldraw/tldraw';
|
||||
import { useTLDraw } from '../../contexts/TLDrawContext';
|
||||
import { useAuth } from '../../contexts/AuthContext';
|
||||
import { useUser } from '../../contexts/UserContext';
|
||||
// Tldraw services
|
||||
import { localStoreService } from '../../services/tldraw/localStoreService';
|
||||
import { PresentationService } from '../../services/tldraw/presentationService';
|
||||
import { UserNeoDBService } from '../../services/graph/userNeoDBService';
|
||||
import { NodeCanvasService } from '../../services/tldraw/nodeCanvasService';
|
||||
import { NavigationSnapshotService } from '../../services/tldraw/snapshotService';
|
||||
// Tldraw utils
|
||||
@@ -46,6 +46,8 @@ interface LoadingState {
|
||||
export default function SinglePlayerPage() {
|
||||
// Context hooks with initialization states
|
||||
const { profile: user, loading: userLoading } = useUser();
|
||||
const { accessToken } = useAuth();
|
||||
const { context, setAuthInfo, switchContext } = useNavigationStore();
|
||||
const {
|
||||
tldrawPreferences,
|
||||
initializePreferences,
|
||||
@@ -55,8 +57,6 @@ export default function SinglePlayerPage() {
|
||||
const routerNavigate = useNavigate();
|
||||
const location = useLocation();
|
||||
|
||||
// Navigation store
|
||||
const { context } = useNavigationStore();
|
||||
|
||||
// Refs
|
||||
const editorRef = useRef<Editor | null>(null);
|
||||
@@ -114,6 +114,7 @@ export default function SinglePlayerPage() {
|
||||
|
||||
// 2. Initialize snapshot service
|
||||
const snapshotService = new NavigationSnapshotService(newStore, editorRef.current || undefined);
|
||||
if (accessToken) snapshotService.setAccessToken(accessToken);
|
||||
snapshotServiceRef.current = snapshotService;
|
||||
logger.debug('single-player-page', '✨ Initialized NavigationSnapshotService');
|
||||
|
||||
@@ -131,12 +132,14 @@ export default function SinglePlayerPage() {
|
||||
|
||||
await NavigationSnapshotService.loadNodeSnapshotFromDatabase(
|
||||
nodeStoragePath,
|
||||
null,
|
||||
accessToken || '',
|
||||
newStore,
|
||||
setLoadingState,
|
||||
undefined, // sharedStore
|
||||
editorRef.current || undefined // editor
|
||||
undefined,
|
||||
editorRef.current || undefined
|
||||
);
|
||||
// Wire auto-save: set the current path on the service instance
|
||||
snapshotService.setCurrentNodePath(nodeStoragePath);
|
||||
logger.debug('single-player-page', '✅ Snapshot loaded from database');
|
||||
} else {
|
||||
logger.debug('single-player-page', '⚠️ No node_storage_path found in node, skipping snapshot load', {
|
||||
@@ -152,7 +155,7 @@ export default function SinglePlayerPage() {
|
||||
let isAutoSaving = false;
|
||||
|
||||
newStore.listen(() => {
|
||||
if (snapshotServiceRef.current && context.node && snapshotServiceRef.current.getCurrentNodePath()) {
|
||||
if (snapshotServiceRef.current && snapshotServiceRef.current.getCurrentNodePath()) {
|
||||
// Skip if already saving
|
||||
if (isAutoSaving) {
|
||||
logger.debug('single-player-page', '⚠️ Skipping auto-save - already saving');
|
||||
@@ -178,8 +181,6 @@ export default function SinglePlayerPage() {
|
||||
isAutoSaving = false;
|
||||
}
|
||||
}, 2000); // Increased to 2 seconds debounce
|
||||
} else if (snapshotServiceRef.current && context.node && !snapshotServiceRef.current.getCurrentNodePath()) {
|
||||
logger.debug('single-player-page', '⚠️ Skipping auto-save - no current node path set yet');
|
||||
}
|
||||
});
|
||||
|
||||
@@ -253,11 +254,16 @@ export default function SinglePlayerPage() {
|
||||
|
||||
try {
|
||||
setLoadingState({ status: 'loading', error: '' });
|
||||
|
||||
// Center the node
|
||||
const nodeData = await loadNodeData(context.node);
|
||||
await NodeCanvasService.centerCurrentNode(editorRef.current, context.node, nodeData);
|
||||
|
||||
|
||||
if (context.node.type !== 'workspace') {
|
||||
try {
|
||||
const nodeData = await loadNodeData(context.node);
|
||||
await NodeCanvasService.centerCurrentNode(editorRef.current, context.node, nodeData);
|
||||
} catch (shapeErr) {
|
||||
logger.warn('single-player-page', '⚠️ Could not place node shape', { type: context.node.type, error: shapeErr });
|
||||
}
|
||||
}
|
||||
|
||||
setIsInitialLoad(false);
|
||||
setLoadingState({ status: 'ready', error: '' });
|
||||
} catch (error) {
|
||||
@@ -297,12 +303,17 @@ export default function SinglePlayerPage() {
|
||||
? context.history.nodes[context.history.currentIndex - 1]
|
||||
: null;
|
||||
|
||||
// Handle navigation in snapshot service
|
||||
// Handle navigation in snapshot service (load/save snapshot)
|
||||
await snapshotService.handleNavigationStart(previousNode, currentNode);
|
||||
|
||||
// Center the node on canvas
|
||||
const nodeData = await loadNodeData(currentNode);
|
||||
await NodeCanvasService.centerCurrentNode(editor, currentNode, nodeData);
|
||||
if (currentNode.type !== 'workspace') {
|
||||
try {
|
||||
const nodeData = await loadNodeData(currentNode);
|
||||
await NodeCanvasService.centerCurrentNode(editor, currentNode, nodeData);
|
||||
} catch (shapeErr) {
|
||||
logger.warn('single-player-page', '⚠️ Could not place node shape', { type: currentNode.type, error: shapeErr });
|
||||
}
|
||||
}
|
||||
|
||||
setLoadingState({ status: 'ready', error: '' });
|
||||
} catch (error) {
|
||||
@@ -315,7 +326,17 @@ export default function SinglePlayerPage() {
|
||||
};
|
||||
|
||||
handleNodeChange();
|
||||
}, [context.node, context.history, store, isInitialLoad]);
|
||||
}, [context.node, context.history, store]);
|
||||
|
||||
// Inject auth and trigger initial context when token is ready
|
||||
useEffect(() => {
|
||||
if (user?.id && accessToken) {
|
||||
setAuthInfo(accessToken, user.id);
|
||||
if (!context.node) {
|
||||
switchContext({ main: 'profile', base: 'profile' }, null, null);
|
||||
}
|
||||
}
|
||||
}, [user?.id, accessToken]);
|
||||
|
||||
// Initialize preferences when user is available
|
||||
useEffect(() => {
|
||||
@@ -462,9 +483,6 @@ export default function SinglePlayerPage() {
|
||||
position: 'fixed',
|
||||
inset: 0,
|
||||
top: `${HEADER_HEIGHT}px`,
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
overflow: 'hidden'
|
||||
}}>
|
||||
{/* Loading overlay - show when loading or contexts not initialized */}
|
||||
{(loadingState.status === 'loading' || !store) && (
|
||||
@@ -527,6 +545,7 @@ export default function SinglePlayerPage() {
|
||||
// Update snapshot service with editor reference
|
||||
if (snapshotServiceRef.current) {
|
||||
snapshotServiceRef.current.setEditor(editor);
|
||||
if (accessToken) snapshotServiceRef.current.setAccessToken(accessToken);
|
||||
}
|
||||
|
||||
setIsEditorReady(true);
|
||||
@@ -565,68 +584,21 @@ const getNodeStoragePath = (node: NavigationNode): string | null => {
|
||||
};
|
||||
|
||||
const loadNodeData = async (node: NavigationNode): Promise<NodeData> => {
|
||||
// Validate the node parameter
|
||||
if (!node) {
|
||||
throw new Error('Node parameter is required');
|
||||
}
|
||||
|
||||
if (!node.id) {
|
||||
throw new Error('Node must have an ID');
|
||||
}
|
||||
|
||||
if (!node?.id) throw new Error('Node parameter is required');
|
||||
const nodeStoragePath = getNodeStoragePath(node);
|
||||
if (!nodeStoragePath) {
|
||||
throw new Error(`Node ${node.id} is missing node_storage_path`);
|
||||
}
|
||||
|
||||
logger.debug('single-player-page', '🔄 Loading node data', {
|
||||
nodeId: node.id,
|
||||
nodeType: node.type,
|
||||
nodeLabel: node.label,
|
||||
nodeStoragePath: nodeStoragePath
|
||||
});
|
||||
if (!nodeStoragePath) throw new Error(`Node ${node.id} is missing node_storage_path`);
|
||||
|
||||
try {
|
||||
// 1. Always fetch fresh data
|
||||
// Create a temporary node object with the correct structure for the service
|
||||
const normalizedNode = {
|
||||
...node,
|
||||
node_storage_path: nodeStoragePath
|
||||
};
|
||||
|
||||
const dbName = UserNeoDBService.getNodeDatabaseName(normalizedNode);
|
||||
const fetchedData = await UserNeoDBService.fetchNodeData(node.id, dbName);
|
||||
|
||||
if (!fetchedData?.node_data) {
|
||||
throw new Error('Failed to fetch node data');
|
||||
}
|
||||
|
||||
// 2. Process the data into the correct shape
|
||||
const theme = getThemeFromLabel(node.type);
|
||||
return {
|
||||
...fetchedData.node_data,
|
||||
title: String(fetchedData.node_data.title || node.label || ''),
|
||||
w: 500,
|
||||
h: 350,
|
||||
state: {
|
||||
parentId: null,
|
||||
isPageChild: true,
|
||||
hasChildren: null,
|
||||
bindings: null
|
||||
},
|
||||
headerColor: theme.headerColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
isLocked: false,
|
||||
__primarylabel__: node.type,
|
||||
uuid_string: node.id,
|
||||
node_storage_path: nodeStoragePath
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error('single-player-page', '❌ Error in loadNodeData', {
|
||||
nodeId: node.id,
|
||||
nodeType: node.type,
|
||||
error: error instanceof Error ? error.message : String(error)
|
||||
});
|
||||
throw error;
|
||||
}
|
||||
const theme = getThemeFromLabel(node.type);
|
||||
return {
|
||||
title: node.label || node.type || '',
|
||||
w: 500,
|
||||
h: 350,
|
||||
state: { parentId: null, isPageChild: true, hasChildren: null, bindings: null },
|
||||
headerColor: theme.headerColor,
|
||||
backgroundColor: theme.backgroundColor,
|
||||
isLocked: false,
|
||||
__primarylabel__: node.type,
|
||||
uuid_string: node.id,
|
||||
node_storage_path: nodeStoragePath,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user