- 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>
171 lines
4.4 KiB
TypeScript
171 lines
4.4 KiB
TypeScript
/**
|
|
* App-Level State Machine
|
|
*
|
|
* Manages the top-level application state across three main modes:
|
|
* - convo: Active conversation (voice or text)
|
|
* - edit: Editing a node
|
|
* - galaxy: 3D visualization of node graph
|
|
*
|
|
* This machine sits above the conversation machine (which contains voice/text modes).
|
|
* It does NOT duplicate the voice mode logic - that lives in voice-machine.ts.
|
|
*/
|
|
|
|
import { setup, assign } from 'xstate';
|
|
|
|
export interface NodeDraft {
|
|
title: string;
|
|
content: string;
|
|
conversationContext?: string; // Last N messages as context
|
|
}
|
|
|
|
export interface Node {
|
|
id: string;
|
|
title: string;
|
|
content: string;
|
|
atp_uri?: string;
|
|
}
|
|
|
|
interface AppContext {
|
|
currentNodeId: string | null;
|
|
pendingNodeDraft: NodeDraft | null;
|
|
mode: 'mobile' | 'desktop';
|
|
lastError: string | null;
|
|
}
|
|
|
|
type AppEvent =
|
|
| { type: 'NAVIGATE_TO_CONVO' }
|
|
| { type: 'NAVIGATE_TO_EDIT'; nodeId?: string; draft?: NodeDraft }
|
|
| { type: 'NAVIGATE_TO_GALAXY' }
|
|
| { type: 'CREATE_NODE_FROM_CONVERSATION'; draft: NodeDraft }
|
|
| { type: 'PUBLISH_NODE_SUCCESS'; nodeId: string }
|
|
| { type: 'CANCEL_EDIT' }
|
|
| { type: 'SET_MODE'; mode: 'mobile' | 'desktop' }
|
|
| { type: 'ERROR'; message: string };
|
|
|
|
export const appMachine = setup({
|
|
types: {
|
|
context: {} as AppContext,
|
|
events: {} as AppEvent,
|
|
},
|
|
actions: {
|
|
setCurrentNode: assign({
|
|
currentNodeId: ({ event }) =>
|
|
event.type === 'NAVIGATE_TO_EDIT' ? event.nodeId || null : null,
|
|
}),
|
|
setPendingDraft: assign({
|
|
pendingNodeDraft: ({ event }) => {
|
|
if (event.type === 'NAVIGATE_TO_EDIT' && event.draft) {
|
|
console.log('[App Machine] Setting pending draft:', event.draft);
|
|
return event.draft;
|
|
}
|
|
if (event.type === 'CREATE_NODE_FROM_CONVERSATION') {
|
|
console.log('[App Machine] Creating node from conversation:', event.draft);
|
|
return event.draft;
|
|
}
|
|
return null;
|
|
},
|
|
}),
|
|
clearDraft: assign({
|
|
pendingNodeDraft: null,
|
|
currentNodeId: null,
|
|
}),
|
|
setPublishedNode: assign({
|
|
currentNodeId: ({ event }) =>
|
|
event.type === 'PUBLISH_NODE_SUCCESS' ? event.nodeId : null,
|
|
pendingNodeDraft: null,
|
|
}),
|
|
setMode: assign({
|
|
mode: ({ event }) => (event.type === 'SET_MODE' ? event.mode : 'desktop'),
|
|
}),
|
|
setError: assign({
|
|
lastError: ({ event }) => (event.type === 'ERROR' ? event.message : null),
|
|
}),
|
|
clearError: assign({
|
|
lastError: null,
|
|
}),
|
|
logTransition: ({ context, event }) => {
|
|
console.log('[App Machine] Event:', event.type);
|
|
console.log('[App Machine] Context:', {
|
|
currentNodeId: context.currentNodeId,
|
|
hasDraft: !!context.pendingNodeDraft,
|
|
mode: context.mode,
|
|
});
|
|
},
|
|
},
|
|
}).createMachine({
|
|
id: 'app',
|
|
initial: 'convo',
|
|
context: {
|
|
currentNodeId: null,
|
|
pendingNodeDraft: null,
|
|
mode: 'desktop',
|
|
lastError: null,
|
|
},
|
|
on: {
|
|
SET_MODE: {
|
|
actions: ['setMode', 'logTransition'],
|
|
},
|
|
ERROR: {
|
|
actions: ['setError', 'logTransition'],
|
|
},
|
|
},
|
|
states: {
|
|
convo: {
|
|
tags: ['conversation'],
|
|
entry: ['clearError', 'logTransition'],
|
|
on: {
|
|
NAVIGATE_TO_EDIT: {
|
|
target: 'edit',
|
|
actions: ['setCurrentNode', 'setPendingDraft', 'logTransition'],
|
|
},
|
|
CREATE_NODE_FROM_CONVERSATION: {
|
|
target: 'edit',
|
|
actions: ['setPendingDraft', 'logTransition'],
|
|
},
|
|
NAVIGATE_TO_GALAXY: {
|
|
target: 'galaxy',
|
|
actions: ['logTransition'],
|
|
},
|
|
},
|
|
},
|
|
|
|
edit: {
|
|
tags: ['editing'],
|
|
entry: ['clearError', 'logTransition'],
|
|
on: {
|
|
NAVIGATE_TO_CONVO: {
|
|
target: 'convo',
|
|
actions: ['logTransition'],
|
|
},
|
|
NAVIGATE_TO_GALAXY: {
|
|
target: 'galaxy',
|
|
actions: ['logTransition'],
|
|
},
|
|
PUBLISH_NODE_SUCCESS: {
|
|
target: 'galaxy',
|
|
actions: ['setPublishedNode', 'logTransition'],
|
|
},
|
|
CANCEL_EDIT: {
|
|
target: 'convo',
|
|
actions: ['clearDraft', 'logTransition'],
|
|
},
|
|
},
|
|
},
|
|
|
|
galaxy: {
|
|
tags: ['visualization'],
|
|
entry: ['clearError', 'logTransition'],
|
|
on: {
|
|
NAVIGATE_TO_CONVO: {
|
|
target: 'convo',
|
|
actions: ['logTransition'],
|
|
},
|
|
NAVIGATE_TO_EDIT: {
|
|
target: 'edit',
|
|
actions: ['setCurrentNode', 'setPendingDraft', 'logTransition'],
|
|
},
|
|
},
|
|
},
|
|
},
|
|
});
|