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:
170
lib/app-machine.ts
Normal file
170
lib/app-machine.ts
Normal file
@@ -0,0 +1,170 @@
|
||||
/**
|
||||
* 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'],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
Reference in New Issue
Block a user