File size: 4,674 Bytes
c27ae8d |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 |
# Frontend Data Flow
## State Management Overview
The frontend manages three main data flows:
1. **Job Submission & Progress** - WebSocket updates from backend
2. **Notation Data** - MusicXML β Zustand β VexFlow rendering
3. **Editing Actions** - User input β State updates β Re-render
## Architecture
```
User Action
β
React Component
β
Zustand Store (mutations)
β
VexFlow Re-render + Tone.js Playback
```
## Data Models
### API Layer
```typescript
// api/client.ts
export class RescoredAPI {
private baseURL = 'http://localhost:8000/api/v1';
async submitJob(youtubeURL: string): Promise<{ jobId: string }> {
const response = await fetch(`${this.baseURL}/transcribe`, {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ youtube_url: youtubeURL }),
});
return response.json();
}
async getScore(jobId: string): Promise<string> {
const response = await fetch(`${this.baseURL}/scores/${jobId}`);
return response.text(); // MusicXML string
}
connectWebSocket(jobId: string, onMessage: (msg: any) => void): WebSocket {
const ws = new WebSocket(`ws://localhost:8000/api/v1/jobs/${jobId}/stream`);
ws.onmessage = (event) => {
const message = JSON.parse(event.data);
onMessage(message);
};
return ws;
}
}
```
### Job Submission Flow
```typescript
// hooks/useJobSubmission.ts
export function useJobSubmission() {
const [jobId, setJobId] = useState<string | null>(null);
const [progress, setProgress] = useState(0);
const [status, setStatus] = useState<'idle' | 'processing' | 'completed' | 'failed'>('idle');
const submit = async (url: string) => {
const api = new RescoredAPI();
const { jobId } = await api.submitJob(url);
setJobId(jobId);
setStatus('processing');
// Connect WebSocket
const ws = api.connectWebSocket(jobId, (message) => {
if (message.type === 'progress') {
setProgress(message.progress);
} else if (message.type === 'completed') {
setStatus('completed');
loadScore(jobId); // Fetch MusicXML
} else if (message.type === 'error') {
setStatus('failed');
}
});
};
const loadScore = async (jobId: string) => {
const api = new RescoredAPI();
const musicXML = await api.getScore(jobId);
// Parse and load into notation store
useNotationStore.getState().loadFromMusicXML(musicXML);
};
return { submit, jobId, progress, status };
}
```
### MusicXML Loading
```typescript
// store/notation.ts
interface NotationStore {
score: Score;
loadFromMusicXML: (xml: string) => void;
exportToMusicXML: () => string;
}
export const useNotationStore = create<NotationStore>((set) => ({
score: null,
loadFromMusicXML: (xml) => {
const parsed = parseMusicXML(xml); // See notation-rendering.md
set({
score: {
id: generateId(),
title: parsed.title,
measures: parsed.measures,
key: parsed.key,
timeSignature: parsed.timeSignature,
tempo: parsed.tempo,
},
});
},
exportToMusicXML: () => {
const { score } = get();
return generateMusicXML(score); // Reverse of parsing
},
}));
```
## Component Data Flow
```mermaid
graph TB
App["App"]
JobSubmission["JobSubmission<br/>(submit URL, show progress)"]
ScoreEditor["ScoreEditor"]
NotationCanvas["NotationCanvas<br/>(reads: score, writes: nothing)"]
PlaybackControls["PlaybackControls<br/>(reads: score, writes: playback state)"]
EditorToolbar["EditorToolbar<br/>(writes: score via mutations)"]
App --> JobSubmission
JobSubmission -->|"on completion"| ScoreEditor
ScoreEditor --> NotationCanvas
ScoreEditor --> PlaybackControls
ScoreEditor --> EditorToolbar
```
## Optimistic Updates
For better UX, update UI immediately before backend confirmation:
```typescript
const addNote = (note: Note) => {
// Optimistically add to state
set(state => ({
score: {
...state.score,
measures: addNoteToMeasure(state.score.measures, note),
},
}));
// Save to backend (future: cloud sync)
// If fails, revert
};
```
## Export Flow
```typescript
const handleExport = async (format: 'musicxml' | 'midi' | 'pdf') => {
const { score } = useNotationStore.getState();
if (format === 'musicxml') {
const xml = exportToMusicXML(score);
downloadFile(xml, 'score.musicxml');
} else if (format === 'midi') {
const midi = exportToMIDI(score);
downloadFile(midi, 'score.mid');
}
};
```
## Next Steps
See [WebSocket Protocol](../integration/websocket-protocol.md) for message details.
|