This guide explains how the AI Writing Assistant works, breaking down each concept for developers new to React, XState, and ProseMirror.
- Architecture Overview
- State Management with XState
- ProseMirror Editor
- Component Structure
- Data Flow
- Key Concepts
┌─────────────────────────────────────────────────────────┐
│ App.jsx │
│ (Main Coordinator) │
│ │
│ ┌──────────────────────────────────────────────────┐ │
│ │ XState Machine (editorMachine.js) │ │
│ │ │ │
│ │ States: idle → generating → review → idle │ │
│ │ ↓ ↓ │ │
│ │ error ←──────────────────┘ │ │
│ └──────────────────────────────────────────────────┘ │
│ │
│ ┌──────────┐ ┌──────────┐ ┌──────────┐ │
│ │ Toolbar │ │ Editor │ │StatusBar │ │
│ └──────────┘ └──────────┘ └──────────┘ │
│ │ │
│ ┌────┴────┐ │
│ │AIToolbar│ │
│ └─────────┘ │
└─────────────────────────────────────────────────────────┘
│
↓
┌───────────────┐
│ aiService.js │
│ (OpenAI API) │
└───────────────┘
- React 18: UI library for building components
- XState v5: State machine for managing application state
- ProseMirror: Rich text editor framework
- Radix UI: Accessible UI component primitives
- OpenAI API: AI content generation
A state machine is a way to model your application's behavior as a set of states and transitions.
Think of a traffic light:
- States: RED, YELLOW, GREEN
- Transitions: RED → GREEN (when timer expires)
- Actions: Turn on/off lights
// editorMachine.js
export const editorMachine = createMachine({
id: 'editor',
initial: 'idle',
states: {
idle: {
on: {
CONTINUE_CLICK: 'generating',
KEYBOARD_SHORTCUT: 'generating'
}
},
generating: {
invoke: {
src: 'generateContent',
onDone: { target: 'review' },
onError: { target: 'error' }
}
},
review: {
on: {
USER_ACCEPT: 'idle',
USER_CLEAR: 'idle',
USER_DISCARD: 'idle'
}
},
error: {
on: {
RETRY: 'generating',
DISMISS: 'idle'
}
}
}
});- idle: User can type, ready for AI generation
- generating: AI is creating content (editor disabled)
- review: AI content ready, user must choose action
- error: Something went wrong, show error message
Events trigger transitions between states:
CONTINUE_CLICK: User clicks "Continue Writing" buttonKEYBOARD_SHORTCUT: User presses Ctrl+SpaceUSER_ACCEPT: User accepts AI contentUSER_CLEAR: User clears AI contentUSER_DISCARD: User discards entire session
Context is the machine's memory - data that persists across states:
context: {
editorState: null, // ProseMirror state
generatedContent: '', // AI-generated text
errorMessage: '', // Error message if any
cursorPosition: 0 // Where to insert content
}Actions are side effects that happen during transitions:
actions: {
applyGeneratedContent: (context, event) => {
// Add AI content to editor
},
acceptContent: (context) => {
// Remove temporary highlighting
}
}// In App.jsx
import { useMachine } from '@xstate/react';
import { editorMachine } from './machines/editorMachine';
function App() {
// useMachine returns [state, send]
const [state, send] = useMachine(editorMachine);
// Check current state
const isGenerating = state.matches('generating');
// Send events
const handleContinue = () => {
send({ type: 'CONTINUE_CLICK', cursorPosition: 100 });
};
// Access context
const content = state.context.generatedContent;
}ProseMirror is a rich text editor framework. Unlike a simple <textarea>, it:
- Maintains document structure (paragraphs, headings, etc.)
- Supports formatting (bold, italic, etc.)
- Has built-in undo/redo
- Is highly extensible with plugins
Defines what kind of content is allowed:
const schema = new Schema({
nodes: {
doc: {}, // Root node
paragraph: {}, // Paragraph
text: {} // Text content
},
marks: {
bold: {}, // Bold formatting
italic: {} // Italic formatting
}
});The current document content and selection:
const state = EditorState.create({
doc: schema.node('doc', null, [
schema.node('paragraph', null, [
schema.text('Hello world')
])
])
});The UI representation of the state:
const view = new EditorView(element, {
state,
dispatchTransaction(transaction) {
// Handle document changes
const newState = view.state.apply(transaction);
view.updateState(newState);
}
});Immutable changes to the document:
// Insert text at position 10
const tr = state.tr.insertText('new text', 10);
view.dispatch(tr);// Editor.jsx
export function Editor({ onContinue, isGenerating }) {
const editorRef = useRef(null);
const viewRef = useRef(null);
useEffect(() => {
// Initialize editor
const state = EditorState.create({
schema,
plugins: [
history(), // Undo/redo
keymap({
'Mod-z': undo,
'Mod-Space': () => {
onContinue();
return true;
}
})
]
});
const view = new EditorView(editorRef.current, {
state,
editable: () => !isGenerating
});
viewRef.current = view;
return () => view.destroy();
}, []);
return <div ref={editorRef} />;
}Responsibilities:
- Manages state machine
- Coordinates all components
- Handles events and callbacks
Key Code:
function App() {
const [state, send] = useMachine(editorMachine);
const handleContinue = (cursorPos) => {
send({ type: 'CONTINUE_CLICK', cursorPosition: cursorPos });
};
return (
<Theme>
<Toolbar onContinue={handleContinue} />
<Editor isGenerating={state.matches('generating')} />
{state.matches('review') && <AIToolbar />}
<StatusBar machineState={state} />
</Theme>
);
}Responsibilities:
- Render ProseMirror editor
- Handle text input
- Insert AI-generated content
- Manage editor state
Key Features:
- Keyboard shortcuts (Ctrl+Z for undo, Mod+Space for AI)
- Disabled during AI generation
- Highlights AI-generated text
Responsibilities:
- "Continue Writing" button
- Show keyboard shortcut hint
- Display loading state
Props:
<Toolbar
onContinue={handleContinue} // Callback when button clicked
isProcessing={isGenerating} // Show loading state
hasError={isError} // Show error state
onRetry={handleRetry} // Retry after error
/>Responsibilities:
- Show Accept/Clear/Discard buttons
- Only visible in 'review' state
- Floating at bottom of screen
Props:
<AIToolbar
onAccept={handleAccept} // User accepts AI content
onClear={handleClear} // User clears AI content
onDiscard={handleDiscard} // User discards session
/>Responsibilities:
- Show current state (Ready, Generating, Review, Error)
- Display word count
- Color-coded status badges
Props:
<StatusBar
machineState={state} // XState machine state
errorMessage={errorMessage} // Error message if any
/>User clicks "Continue Writing"
↓
Toolbar.onContinue() called
↓
App.handleContinue() sends CONTINUE_CLICK event
↓
State machine transitions: idle → generating
↓
Machine invokes generateContent service
↓
aiService.generateAIContent() calls OpenAI API
↓
API returns generated text
↓
Machine transitions: generating → review
↓
generatedContent stored in context
↓
Editor.useEffect detects new content
↓
Editor inserts text with highlighting
↓
AIToolbar appears
User clicks "Accept All"
↓
AIToolbar.onAccept() called
↓
App.handleAccept() sends USER_ACCEPT event
↓
State machine transitions: review → idle
↓
generatedContent cleared from context
↓
AIToolbar disappears
↓
Editor removes highlighting (content stays)
User types character
↓
ProseMirror dispatchTransaction called
↓
New editor state created
↓
Editor.onEditorStateChange() called
↓
App.handleEditorStateChange() sends EDITOR_UPDATE event
↓
State machine updates context.editorState
↓
StatusBar shows updated word count
Manages component state:
const [count, setCount] = useState(0);
setCount(count + 1);Runs side effects:
useEffect(() => {
// Runs after render
console.log('Component mounted');
return () => {
// Cleanup
console.log('Component unmounted');
};
}, []); // Empty array = run onceStores mutable values that don't trigger re-renders:
const editorRef = useRef(null);
// Access with editorRef.currentProps are how parent components pass data to children:
// Parent
<Toolbar onContinue={handleContinue} isProcessing={true} />
// Child
function Toolbar({ onContinue, isProcessing }) {
return <button onClick={onContinue} disabled={isProcessing} />;
}Functions passed as props to handle events:
// Parent defines callback
const handleClick = () => {
console.log('Button clicked!');
};
// Pass to child
<Button onClick={handleClick} />
// Child calls it
function Button({ onClick }) {
return <button onClick={onClick}>Click me</button>;
}Show/hide components based on conditions:
// && operator: show if true
{isGenerating && <Spinner />}
// Ternary: show A or B
{isError ? <ErrorMessage /> : <SuccessMessage />}
// Function: complex logic
{getStatusComponent()}Handle asynchronous operations:
async function generateContent() {
try {
const response = await openai.chat.completions.create({...});
return response.choices[0].message.content;
} catch (error) {
console.error('Error:', error);
throw error;
}
}Parent component controls child's state:
// Parent
function App() {
const [value, setValue] = useState('');
return (
<Input
value={value}
onChange={setValue}
/>
);
}
// Child
function Input({ value, onChange }) {
return (
<input
value={value}
onChange={(e) => onChange(e.target.value)}
/>
);
}When multiple components need the same state, move it to their common parent:
// ❌ Bad: State in both components
function ComponentA() {
const [data, setData] = useState('');
}
function ComponentB() {
const [data, setData] = useState('');
}
// ✅ Good: State in parent
function Parent() {
const [data, setData] = useState('');
return (
<>
<ComponentA data={data} />
<ComponentB data={data} />
</>
);
}Build complex UIs from simple components:
function Card({ children }) {
return <div className="card">{children}</div>;
}
function App() {
return (
<Card>
<h1>Title</h1>
<p>Content</p>
</Card>
);
}// Log current state
console.log('Current state:', state.value);
console.log('Context:', state.context);
// Check if in specific state
console.log('Is generating?', state.matches('generating'));// Log editor state
console.log('Editor state:', viewRef.current.state);
console.log('Document:', viewRef.current.state.doc.toJSON());
console.log('Selection:', viewRef.current.state.selection);// Log all events sent to machine
const handleContinue = () => {
console.log('Sending CONTINUE_CLICK event');
send({ type: 'CONTINUE_CLICK' });
};Install React DevTools browser extension to:
- Inspect component hierarchy
- View props and state
- Track re-renders
- Run the app:
npm run dev - Experiment: Try modifying components
- Add features: Implement enhancements from the roadmap
- Read docs: Explore React, XState, ProseMirror documentation