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:
+273
-374
@@ -1,32 +1,45 @@
|
||||
import { create } from 'zustand';
|
||||
import { UserNeoDBService } from '../services/graph/userNeoDBService';
|
||||
import { logger } from '../debugConfig';
|
||||
import { isValidNodeType } from '../utils/tldraw/cc-base/cc-graph/cc-graph-types';
|
||||
import {
|
||||
NavigationStore,
|
||||
import {
|
||||
NavigationStore,
|
||||
NavigationNode,
|
||||
NeoGraphNode,
|
||||
MainContext,
|
||||
BaseContext,
|
||||
NavigationContextState,
|
||||
isProfileContext,
|
||||
isInstituteContext,
|
||||
getContextDatabase,
|
||||
addToHistory,
|
||||
navigateHistory,
|
||||
getCurrentHistoryNode,
|
||||
ExtendedContext,
|
||||
UnifiedContextSwitch,
|
||||
NodeContext
|
||||
} from '../types/navigation';
|
||||
|
||||
interface WhiteboardRoom {
|
||||
id: string;
|
||||
user_id: string;
|
||||
name: string;
|
||||
context_type: string;
|
||||
is_default: boolean;
|
||||
storage_path: string | null;
|
||||
neo4j_node_id: string | null;
|
||||
neo4j_db_name: string | null;
|
||||
node_type: string | null;
|
||||
}
|
||||
|
||||
interface NavigationStoreWithAuth extends NavigationStore {
|
||||
_accessToken: string | null;
|
||||
_userId: string | null;
|
||||
setAuthInfo: (token: string | null, userId: string | null) => void;
|
||||
}
|
||||
|
||||
const initialState: NavigationContextState = {
|
||||
main: 'profile',
|
||||
base: 'profile',
|
||||
node: null,
|
||||
history: {
|
||||
nodes: [],
|
||||
currentIndex: -1
|
||||
}
|
||||
history: { nodes: [], currentIndex: -1 }
|
||||
};
|
||||
|
||||
function getDefaultBaseForMain(main: MainContext): BaseContext {
|
||||
@@ -38,402 +51,288 @@ function validateContextTransition(
|
||||
updates: Partial<NavigationContextState>
|
||||
): NavigationContextState {
|
||||
const newState = { ...current, ...updates };
|
||||
|
||||
// Validate main context
|
||||
if (updates.main) {
|
||||
newState.base = getDefaultBaseForMain(updates.main);
|
||||
}
|
||||
|
||||
// Validate base context
|
||||
if (updates.base) {
|
||||
// Ensure base context matches main context
|
||||
const isValid = newState.main === 'profile'
|
||||
const isValid = newState.main === 'profile'
|
||||
? isProfileContext(updates.base)
|
||||
: isInstituteContext(updates.base);
|
||||
|
||||
if (!isValid) {
|
||||
newState.base = getDefaultBaseForMain(newState.main);
|
||||
}
|
||||
}
|
||||
|
||||
return newState;
|
||||
}
|
||||
|
||||
export interface NavigationActions {
|
||||
// Context Navigation
|
||||
setMainContext: (context: MainContext, userDbName: string | null, workerDbName: string | null) => Promise<void>;
|
||||
setBaseContext: (context: BaseContext, userDbName: string | null, workerDbName: string | null) => Promise<void>;
|
||||
setExtendedContext: (context: ExtendedContext, userDbName: string | null, workerDbName: string | null) => Promise<void>;
|
||||
|
||||
// Node Navigation
|
||||
navigate: (nodeId: string, dbName: string) => Promise<void>;
|
||||
navigateToNode: (node: NavigationNode, userDbName: string | null, workerDbName: string | null) => Promise<void>;
|
||||
|
||||
// History Navigation
|
||||
goBack: () => void;
|
||||
goForward: () => void;
|
||||
|
||||
// Utility Methods
|
||||
refreshNavigationState: (userDbName: string | null, workerDbName: string | null) => Promise<void>;
|
||||
}
|
||||
export const useNavigationStore = create<NavigationStoreWithAuth>((set, get) => {
|
||||
const pgFetch = async <T = unknown>(
|
||||
method: 'GET' | 'POST' | 'PATCH' | 'DELETE',
|
||||
table: string,
|
||||
options: { body?: object; query?: string; prefer?: string; single?: boolean } = {}
|
||||
): Promise<T | null> => {
|
||||
const token = get()._accessToken;
|
||||
if (!token) throw new Error('pgFetch: no access token');
|
||||
const url = `${import.meta.env.VITE_SUPABASE_URL}/rest/v1/${table}${options.query ? `?${options.query}` : ''}`;
|
||||
const headers: Record<string, string> = {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
'apikey': import.meta.env.VITE_SUPABASE_ANON_KEY,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (options.prefer) headers['Prefer'] = options.prefer;
|
||||
if (options.single) headers['Accept'] = 'application/vnd.pgrst.object+json';
|
||||
const res = await fetch(url, {
|
||||
method,
|
||||
headers,
|
||||
...(options.body ? { body: JSON.stringify(options.body) } : {}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
const err = await res.text();
|
||||
throw new Error(`PostgREST ${res.status}: ${err}`);
|
||||
}
|
||||
if (res.status === 204) return null;
|
||||
return res.json() as Promise<T>;
|
||||
};
|
||||
|
||||
export interface NavigationState {
|
||||
context: {
|
||||
main: NodeContext;
|
||||
base: NodeContext;
|
||||
extended?: string;
|
||||
node: NavigationNode;
|
||||
history: {
|
||||
nodes: NavigationNode[];
|
||||
currentIndex: number;
|
||||
const getOrCreateDefaultRoom = async (contextType: string): Promise<NavigationNode> => {
|
||||
const userId = get()._userId;
|
||||
if (!userId) throw new Error('getOrCreateDefaultRoom: no user ID');
|
||||
|
||||
const rooms = await pgFetch<WhiteboardRoom[]>('GET', 'whiteboard_rooms', {
|
||||
query: `user_id=eq.${userId}&context_type=eq.${contextType}&is_default=eq.true`,
|
||||
});
|
||||
|
||||
if (rooms && rooms.length > 0) {
|
||||
const room = rooms[0];
|
||||
return {
|
||||
id: room.id,
|
||||
node_storage_path: room.storage_path || `${userId}/workspaces/${contextType}_default.json`,
|
||||
label: room.name,
|
||||
type: 'workspace',
|
||||
};
|
||||
}
|
||||
|
||||
const storagePath = `${userId}/workspaces/${contextType}_default.json`;
|
||||
const room = await pgFetch<WhiteboardRoom>('POST', 'whiteboard_rooms', {
|
||||
body: {
|
||||
user_id: userId,
|
||||
name: `${contextType.charAt(0).toUpperCase() + contextType.slice(1)} Workspace`,
|
||||
context_type: contextType,
|
||||
is_default: true,
|
||||
storage_path: storagePath,
|
||||
},
|
||||
prefer: 'return=representation',
|
||||
single: true,
|
||||
});
|
||||
|
||||
if (!room) throw new Error('Failed to create default whiteboard room');
|
||||
logger.debug('navigation-context', '✅ Created default whiteboard room', { contextType, roomId: room.id });
|
||||
|
||||
return {
|
||||
id: room.id,
|
||||
node_storage_path: room.storage_path || storagePath,
|
||||
label: room.name,
|
||||
type: 'workspace',
|
||||
};
|
||||
};
|
||||
// ... rest of the state interface ...
|
||||
}
|
||||
|
||||
export const useNavigationStore = create<NavigationStore>((set, get) => ({
|
||||
context: initialState,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
return {
|
||||
_accessToken: null,
|
||||
_userId: null,
|
||||
setAuthInfo: (token: string | null, userId: string | null) => {
|
||||
set({ _accessToken: token, _userId: userId });
|
||||
},
|
||||
|
||||
switchContext: async (contextSwitch: UnifiedContextSwitch, userDbName: string | null, workerDbName: string | null) => {
|
||||
try {
|
||||
// Check if we have the necessary database connections
|
||||
if (contextSwitch.main === 'profile' && !userDbName) {
|
||||
logger.error('navigation-context', '❌ User database connection not initialized');
|
||||
set({
|
||||
error: 'User database connection not initialized',
|
||||
isLoading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
if (contextSwitch.main === 'institute' && !workerDbName) {
|
||||
logger.error('navigation-context', '❌ Worker database connection not initialized');
|
||||
set({
|
||||
error: 'Worker database connection not initialized',
|
||||
isLoading: false
|
||||
});
|
||||
context: initialState,
|
||||
isLoading: false,
|
||||
error: null,
|
||||
|
||||
switchContext: async (contextSwitch: UnifiedContextSwitch, _userDbName: string | null = null, _workerDbName: string | null = null) => {
|
||||
if (!get()._accessToken || !get()._userId) {
|
||||
logger.warn('navigation-context', '⚠️ switchContext called without auth — skipping');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
const currentState = get().context;
|
||||
let newState: NavigationContextState = { ...currentState, node: null };
|
||||
|
||||
logger.debug('navigation-context', '🔄 Starting context switch', {
|
||||
from: {
|
||||
main: get().context.main,
|
||||
base: get().context.base,
|
||||
extended: contextSwitch.extended,
|
||||
nodeId: get().context.node?.id
|
||||
},
|
||||
to: {
|
||||
main: contextSwitch.main,
|
||||
base: contextSwitch.base,
|
||||
extended: contextSwitch.extended
|
||||
},
|
||||
skipBaseContextLoad: contextSwitch.skipBaseContextLoad
|
||||
});
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
const currentState = get().context;
|
||||
|
||||
// Clear node state immediately
|
||||
const clearedState: NavigationContextState = {
|
||||
...currentState,
|
||||
node: null
|
||||
};
|
||||
set({
|
||||
context: clearedState,
|
||||
isLoading: true
|
||||
});
|
||||
|
||||
let newState: NavigationContextState = {
|
||||
...currentState,
|
||||
node: null
|
||||
};
|
||||
|
||||
// Update main context if provided
|
||||
if (contextSwitch.main) {
|
||||
newState = validateContextTransition(newState, { main: contextSwitch.main });
|
||||
if (!contextSwitch.skipBaseContextLoad) {
|
||||
newState.base = getDefaultBaseForMain(contextSwitch.main);
|
||||
if (contextSwitch.main) {
|
||||
newState = validateContextTransition(newState, { main: contextSwitch.main });
|
||||
if (!contextSwitch.skipBaseContextLoad) {
|
||||
newState.base = getDefaultBaseForMain(contextSwitch.main);
|
||||
}
|
||||
}
|
||||
logger.debug('navigation-state', '✅ Main context updated', {
|
||||
previous: currentState.main,
|
||||
new: newState.main,
|
||||
defaultBase: newState.base
|
||||
});
|
||||
}
|
||||
|
||||
// Update base context if provided
|
||||
if (contextSwitch.base) {
|
||||
newState = validateContextTransition(newState, { base: contextSwitch.base });
|
||||
logger.debug('navigation-state', '✅ Base context updated', {
|
||||
previous: currentState.base,
|
||||
new: newState.base
|
||||
});
|
||||
}
|
||||
|
||||
logger.debug('navigation-state', '✅ Context validation complete', {
|
||||
validatedState: newState,
|
||||
originalState: currentState
|
||||
});
|
||||
|
||||
// Determine which context to use for the node
|
||||
const targetContext = contextSwitch.base ||
|
||||
contextSwitch.extended ||
|
||||
(contextSwitch.main ? getDefaultBaseForMain(contextSwitch.main) :
|
||||
newState.base);
|
||||
|
||||
// Get database name
|
||||
const dbName = getContextDatabase(newState, userDbName, workerDbName);
|
||||
|
||||
logger.debug('context-switch', '🔍 Fetching default node for context', {
|
||||
targetContext,
|
||||
dbName,
|
||||
currentState: newState
|
||||
});
|
||||
|
||||
// Get default node for the final context
|
||||
const defaultNode = await UserNeoDBService.getDefaultNode(targetContext, dbName);
|
||||
|
||||
if (!defaultNode) {
|
||||
const errorMsg = `No default node found for context: ${targetContext}`;
|
||||
logger.error('context-switch', '❌ Default node fetch failed', { targetContext });
|
||||
set({
|
||||
error: errorMsg,
|
||||
isLoading: false
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
logger.debug('context-switch', '✨ Default node fetched', {
|
||||
nodeId: defaultNode.id,
|
||||
node_storage_path: defaultNode.node_storage_path,
|
||||
type: defaultNode.type
|
||||
});
|
||||
|
||||
// Update history and state
|
||||
const newHistory = addToHistory(currentState.history, defaultNode);
|
||||
logger.debug('history-management', '📚 History updated', {
|
||||
previousState: currentState.history,
|
||||
newState: newHistory,
|
||||
addedNode: defaultNode
|
||||
});
|
||||
|
||||
set({
|
||||
context: {
|
||||
...newState,
|
||||
node: defaultNode,
|
||||
history: newHistory
|
||||
},
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
|
||||
logger.debug('navigation-context', '✅ Context switch completed', {
|
||||
finalState: {
|
||||
main: newState.main,
|
||||
base: newState.base,
|
||||
nodeId: defaultNode.id
|
||||
if (contextSwitch.base) {
|
||||
newState = validateContextTransition(newState, { base: contextSwitch.base });
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('navigation-context', '❌ Failed to switch context:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to switch context',
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
goBack: () => {
|
||||
const currentState = get().context;
|
||||
if (currentState.history.currentIndex > 0) {
|
||||
const newHistory = navigateHistory(currentState.history, currentState.history.currentIndex - 1);
|
||||
const node = getCurrentHistoryNode(newHistory);
|
||||
set({
|
||||
context: {
|
||||
...currentState,
|
||||
node,
|
||||
history: newHistory
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
const targetContext = contextSwitch.base ||
|
||||
contextSwitch.extended ||
|
||||
(contextSwitch.main ? getDefaultBaseForMain(contextSwitch.main) : newState.base);
|
||||
|
||||
goForward: () => {
|
||||
const currentState = get().context;
|
||||
if (currentState.history.currentIndex < currentState.history.nodes.length - 1) {
|
||||
const newHistory = navigateHistory(currentState.history, currentState.history.currentIndex + 1);
|
||||
const node = getCurrentHistoryNode(newHistory);
|
||||
set({
|
||||
context: {
|
||||
...currentState,
|
||||
node,
|
||||
history: newHistory
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
const defaultNode = await getOrCreateDefaultRoom(targetContext);
|
||||
const newHistory = addToHistory(currentState.history, defaultNode);
|
||||
|
||||
setMainContext: async (main: MainContext, userDbName: string | null, workerDbName: string | null) => {
|
||||
try {
|
||||
// Use switchContext instead of direct implementation
|
||||
await get().switchContext({ main }, userDbName, workerDbName);
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ Failed to set main context:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to set main context',
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setBaseContext: async (base: BaseContext, userDbName: string | null, workerDbName: string | null) => {
|
||||
try {
|
||||
// Use switchContext instead of direct implementation
|
||||
await get().switchContext({ base }, userDbName, workerDbName);
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ Failed to set base context:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to set base context',
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
setExtendedContext: async (extended: ExtendedContext, userDbName: string | null, workerDbName: string | null) => {
|
||||
try {
|
||||
// Use switchContext instead of direct implementation
|
||||
await get().switchContext({ extended }, userDbName, workerDbName);
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ Failed to set extended context:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to set extended context',
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
navigate: async (nodeId: string, dbName: string) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
// Check if we already have this node in history
|
||||
const currentState = get().context;
|
||||
const existingNodeIndex = currentState.history.nodes.findIndex(n => n.id === nodeId);
|
||||
|
||||
// If node exists in history, just navigate to it
|
||||
if (existingNodeIndex !== -1) {
|
||||
logger.debug('navigation', '📍 Navigating to existing node in history', {
|
||||
nodeId,
|
||||
historyIndex: existingNodeIndex,
|
||||
currentIndex: currentState.history.currentIndex
|
||||
});
|
||||
|
||||
const newHistory = navigateHistory(currentState.history, existingNodeIndex);
|
||||
const node = getCurrentHistoryNode(newHistory);
|
||||
|
||||
set({
|
||||
context: {
|
||||
...currentState,
|
||||
node,
|
||||
history: newHistory
|
||||
},
|
||||
context: { ...newState, node: defaultNode, history: newHistory },
|
||||
isLoading: false,
|
||||
error: null
|
||||
error: null,
|
||||
});
|
||||
logger.debug('navigation-context', '✅ Context switch complete', {
|
||||
main: newState.main, base: newState.base, nodeId: defaultNode.id,
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('navigation-context', '❌ switchContext failed', error);
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to switch context', isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
goBack: () => {
|
||||
const currentState = get().context;
|
||||
if (currentState.history.currentIndex > 0) {
|
||||
const newHistory = navigateHistory(currentState.history, currentState.history.currentIndex - 1);
|
||||
set({ context: { ...currentState, node: getCurrentHistoryNode(newHistory), history: newHistory } });
|
||||
}
|
||||
},
|
||||
|
||||
goForward: () => {
|
||||
const currentState = get().context;
|
||||
if (currentState.history.currentIndex < currentState.history.nodes.length - 1) {
|
||||
const newHistory = navigateHistory(currentState.history, currentState.history.currentIndex + 1);
|
||||
set({ context: { ...currentState, node: getCurrentHistoryNode(newHistory), history: newHistory } });
|
||||
}
|
||||
},
|
||||
|
||||
setMainContext: async (main: MainContext, userDbName: string | null, workerDbName: string | null) => {
|
||||
await get().switchContext({ main }, userDbName, workerDbName);
|
||||
},
|
||||
|
||||
setBaseContext: async (base: BaseContext, userDbName: string | null, workerDbName: string | null) => {
|
||||
await get().switchContext({ base }, userDbName, workerDbName);
|
||||
},
|
||||
|
||||
setExtendedContext: async (extended: ExtendedContext, userDbName: string | null, workerDbName: string | null) => {
|
||||
await get().switchContext({ extended }, userDbName, workerDbName);
|
||||
},
|
||||
|
||||
navigate: async (nodeId: string, _dbName: string) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
if (!get()._accessToken) { set({ isLoading: false }); return; }
|
||||
|
||||
const currentState = get().context;
|
||||
const existingIndex = currentState.history.nodes.findIndex(n => n.id === nodeId);
|
||||
if (existingIndex !== -1) {
|
||||
const newHistory = navigateHistory(currentState.history, existingIndex);
|
||||
set({ context: { ...currentState, node: getCurrentHistoryNode(newHistory), history: newHistory }, isLoading: false });
|
||||
return;
|
||||
}
|
||||
|
||||
const rooms = await pgFetch<WhiteboardRoom[]>('GET', 'whiteboard_rooms', {
|
||||
query: `id=eq.${nodeId}&user_id=eq.${get()._userId}`,
|
||||
});
|
||||
if (!rooms || rooms.length === 0) throw new Error(`Whiteboard room not found: ${nodeId}`);
|
||||
|
||||
const room = rooms[0];
|
||||
const node: NavigationNode = {
|
||||
id: room.id,
|
||||
node_storage_path: room.storage_path || `${get()._userId}/workspaces/${room.context_type}_default.json`,
|
||||
label: room.name,
|
||||
type: 'workspace',
|
||||
};
|
||||
const newHistory = addToHistory(currentState.history, node);
|
||||
set({ context: { ...currentState, node, history: newHistory }, isLoading: false });
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ navigate failed', error);
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to navigate', isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
navigateToNode: async (node: NavigationNode, userDbName: string | null, workerDbName: string | null) => {
|
||||
if (!isValidNodeType(node.type)) {
|
||||
logger.warn('navigation', `⚠️ navigateToNode called with non-graph type: ${node.type} — navigating anyway`);
|
||||
}
|
||||
await get().navigate(node.id, userDbName || '');
|
||||
},
|
||||
|
||||
refreshNavigationState: async (_userDbName: string | null, _workerDbName: string | null) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
const currentNode = get().context.node;
|
||||
if (currentNode && get()._accessToken) {
|
||||
const rooms = await pgFetch<WhiteboardRoom[]>('GET', 'whiteboard_rooms', {
|
||||
query: `id=eq.${currentNode.id}`,
|
||||
});
|
||||
if (rooms && rooms.length > 0) {
|
||||
const room = rooms[0];
|
||||
set({
|
||||
context: {
|
||||
...get().context,
|
||||
node: {
|
||||
id: room.id,
|
||||
node_storage_path: room.storage_path || currentNode.node_storage_path,
|
||||
label: room.name,
|
||||
type: 'workspace',
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
set({ isLoading: false });
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ refreshNavigationState failed', error);
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to refresh', isLoading: false });
|
||||
}
|
||||
},
|
||||
|
||||
navigateToNeoNode: async (neoNode: NeoGraphNode) => {
|
||||
const userId = get()._userId;
|
||||
if (!userId || !get()._accessToken) {
|
||||
logger.warn('navigation', '⚠️ navigateToNeoNode called without auth');
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch new node data
|
||||
const nodeData = await UserNeoDBService.fetchNodeData(nodeId, dbName);
|
||||
if (!nodeData) {
|
||||
throw new Error(`Node not found: ${nodeId}`);
|
||||
}
|
||||
|
||||
const node: NavigationNode = {
|
||||
id: nodeId,
|
||||
node_storage_path: nodeData.node_data.node_storage_path || '',
|
||||
label: nodeData.node_data.title || nodeData.node_data.user_name || nodeId,
|
||||
type: nodeData.node_type
|
||||
};
|
||||
|
||||
logger.debug('navigation', '📍 Adding new node to history', {
|
||||
nodeId: node.id,
|
||||
type: node.type,
|
||||
node_storage_path: node.node_storage_path
|
||||
});
|
||||
|
||||
// Add to history and update state
|
||||
const newHistory = addToHistory(currentState.history, node);
|
||||
set({
|
||||
context: {
|
||||
...currentState,
|
||||
node,
|
||||
history: newHistory
|
||||
},
|
||||
isLoading: false,
|
||||
error: null
|
||||
});
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ Failed to navigate:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to navigate',
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
navigateToNode: async (node: NavigationNode, userDbName: string | null, workerDbName: string | null) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
|
||||
if (!isValidNodeType(node.type)) {
|
||||
throw new Error(`Invalid node type: ${node.type}`);
|
||||
}
|
||||
|
||||
const dbName = getContextDatabase(get().context, userDbName, workerDbName);
|
||||
await get().navigate(node.id, dbName);
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ Failed to navigate to node:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to navigate to node',
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
refreshNavigationState: async (userDbName: string | null, workerDbName: string | null) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
const currentState = get().context;
|
||||
|
||||
if (currentState.node) {
|
||||
const dbName = getContextDatabase(currentState, userDbName, workerDbName);
|
||||
const nodeData = await UserNeoDBService.fetchNodeData(currentState.node.id, dbName);
|
||||
if (nodeData) {
|
||||
const node: NavigationNode = {
|
||||
id: currentState.node.id,
|
||||
node_storage_path: nodeData.node_data.node_storage_path || '',
|
||||
label: nodeData.node_data.title || nodeData.node_data.user_name || currentState.node.id,
|
||||
type: nodeData.node_type
|
||||
};
|
||||
set({
|
||||
context: {
|
||||
...currentState,
|
||||
node
|
||||
}
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
const existing = await pgFetch<WhiteboardRoom[]>('GET', 'whiteboard_rooms', {
|
||||
query: `user_id=eq.${userId}&neo4j_node_id=eq.${neoNode.neo4j_node_id}`,
|
||||
});
|
||||
let room: WhiteboardRoom;
|
||||
if (existing && existing.length > 0) {
|
||||
room = existing[0];
|
||||
} else {
|
||||
const storagePath = `${userId}/nodes/${neoNode.neo4j_node_id}.json`;
|
||||
const created = await pgFetch<WhiteboardRoom>('POST', 'whiteboard_rooms', {
|
||||
body: {
|
||||
user_id: userId,
|
||||
name: neoNode.label,
|
||||
context_type: neoNode.node_type.toLowerCase(),
|
||||
is_default: false,
|
||||
storage_path: storagePath,
|
||||
neo4j_node_id: neoNode.neo4j_node_id,
|
||||
neo4j_db_name: neoNode.neo4j_db_name,
|
||||
node_type: neoNode.node_type,
|
||||
},
|
||||
prefer: 'return=representation',
|
||||
single: true,
|
||||
});
|
||||
if (!created) throw new Error('Failed to create whiteboard room for node');
|
||||
room = created;
|
||||
}
|
||||
const node: NavigationNode = {
|
||||
id: room.id,
|
||||
node_storage_path: room.storage_path || `${userId}/nodes/${neoNode.neo4j_node_id}.json`,
|
||||
label: room.name,
|
||||
type: neoNode.node_type,
|
||||
};
|
||||
const currentState = get().context;
|
||||
const newHistory = addToHistory(currentState.history, node);
|
||||
set({ context: { ...currentState, node, history: newHistory }, isLoading: false, error: null });
|
||||
logger.debug('navigation', '✅ Navigated to Neo4j node', { neoNode });
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ navigateToNeoNode failed', error);
|
||||
set({ error: error instanceof Error ? error.message : 'Navigation failed', isLoading: false });
|
||||
}
|
||||
|
||||
set({ isLoading: false });
|
||||
} catch (error) {
|
||||
logger.error('navigation', '❌ Failed to refresh navigation state:', error);
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to refresh navigation state',
|
||||
isLoading: false
|
||||
});
|
||||
}
|
||||
}
|
||||
}));
|
||||
},
|
||||
};
|
||||
});
|
||||
|
||||
+538
-507
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user