@brainfish-ai/components 0.18.4 → 0.18.7
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/alert-dialog.d.ts +2 -2
- package/dist/article-suggestions-banner.d.ts +16 -0
- package/dist/button-group.d.ts +3 -2
- package/dist/button.d.ts +1 -1
- package/dist/combobox.d.ts +24 -3
- package/dist/confirm-dialog.d.ts +1 -1
- package/dist/div-button.d.ts +1 -1
- package/dist/esm/chunks/{ChatSearch.BJtS7ZMs.js → ChatSearch.DSB-T76c.js} +2 -2
- package/dist/esm/chunks/{ChatSearch.BJtS7ZMs.js.map → ChatSearch.DSB-T76c.js.map} +1 -1
- package/dist/esm/chunks/{combobox.CkN-wAHB.js → combobox.DNYCWyub.js} +40 -21
- package/dist/esm/chunks/combobox.DNYCWyub.js.map +1 -0
- package/dist/esm/components/article-suggestions-banner.js +55 -0
- package/dist/esm/components/article-suggestions-banner.js.map +1 -0
- package/dist/esm/components/chat-search.js +1 -1
- package/dist/esm/components/combobox.js +1 -1
- package/dist/esm/components/convos.js +1 -1
- package/dist/esm/components/convos.js.map +1 -1
- package/dist/esm/components/header-nav.js +55 -0
- package/dist/esm/components/header-nav.js.map +1 -0
- package/dist/esm/components/ui/button-group.js +23 -8
- package/dist/esm/components/ui/button-group.js.map +1 -1
- package/dist/esm/components/ui/button.js +2 -8
- package/dist/esm/components/ui/button.js.map +1 -1
- package/dist/esm/components/ui/command.js +1 -1
- package/dist/esm/components/ui/command.js.map +1 -1
- package/dist/esm/components/ui/div-button.js +1 -0
- package/dist/esm/components/ui/div-button.js.map +1 -1
- package/dist/esm/global.css +1 -1
- package/dist/esm/index.js +179 -5
- package/dist/esm/index.js.map +1 -1
- package/dist/header-nav.d.ts +28 -0
- package/dist/index.d.ts +134 -9
- package/dist/stats.html +1 -1
- package/dist/two-level-combobox.d.ts +2 -0
- package/package.json +6 -1
- package/components.json +0 -16
- package/dist/esm/chunks/combobox.CkN-wAHB.js.map +0 -1
- package/markdown-editor.plan.md +0 -2101
- package/markdown-editor.spec.md +0 -915
package/markdown-editor.plan.md
DELETED
|
@@ -1,2101 +0,0 @@
|
|
|
1
|
-
# Markdown Editor/Viewer Component Implementation Checklist
|
|
2
|
-
|
|
3
|
-
## Overview
|
|
4
|
-
|
|
5
|
-
This checklist tracks the implementation progress for the Markdown Editor/Viewer component based on `markdown-editor.spec.md`. The component will replace FormattedMessage.tsx while maintaining compatibility with the existing multimedia/embed system.
|
|
6
|
-
|
|
7
|
-
### Implementation Strategy
|
|
8
|
-
- **6-Phase Approach**: Foundation → Multimedia → Suggestions → Streaming → Collaboration → Integration
|
|
9
|
-
- **Monorepo Strategy**:
|
|
10
|
-
- All editor-related development, including the main component (`SimpleEditor` evolving into `MarkdownEditorViewer`), UI primitives, hooks, and extensions, will be centralized within the **`@brainfish-ai/tiptap`** package.
|
|
11
|
-
- **Incremental Development**: Build on existing FormattedMessage.tsx and embed system patterns
|
|
12
|
-
- **TipTap Foundation**: Core editing engine with custom extensions
|
|
13
|
-
- **Backward Compatibility**: Seamless integration with existing codebase
|
|
14
|
-
|
|
15
|
-
### Dependencies Setup
|
|
16
|
-
- [x] Install TipTap dependencies (@tiptap/react, @tiptap/starter-kit, @tiptap/extension-table, etc.)
|
|
17
|
-
- [x] Install collaboration dependencies (yjs, y-webrtc, @tiptap/extension-collaboration)
|
|
18
|
-
- [x] Install markdown processing dependencies (remark, unified, rehype)
|
|
19
|
-
- [x] Verify existing dependencies (react-markdown, remark-gfm, @radix-ui/*, framer-motion)
|
|
20
|
-
|
|
21
|
-
## Phase 1: Foundation and Basic Editor (Week 1-2)
|
|
22
|
-
**Objective**: Create basic TipTap editor with markdown conversion and mode switching
|
|
23
|
-
|
|
24
|
-
### Core Component Structure
|
|
25
|
-
- [ ] Create MarkdownEditorViewer component to lazy load ContentViewer and SimpleEditor component depending on mode
|
|
26
|
-
- [ ] Adapt existing file structure in `packages/tiptap/` for new components (Viewer, Suggestions, etc.)
|
|
27
|
-
- [ ] Implement lazy loading for Editor and Viewer modes within `SimpleEditor.tsx`
|
|
28
|
-
- [ ] Add error boundaries
|
|
29
|
-
|
|
30
|
-
### Markdown Translation Layer
|
|
31
|
-
- [x] Create `markdownTranslator.ts` utility
|
|
32
|
-
- [x] Implement markdown to TipTap JSON conversion using remark-gfm
|
|
33
|
-
- [x] Implement TipTap JSON to markdown conversion
|
|
34
|
-
- [ ] Handle existing FormattedMessage content patterns (video prefixes, etc.)
|
|
35
|
-
- [x] Add proper error handling and fallbacks
|
|
36
|
-
- [ ] Preserve multimedia embeds and custom HTML elements
|
|
37
|
-
|
|
38
|
-
### Basic TipTap Editor
|
|
39
|
-
- [x] Initialize TipTap editor with StarterKit extensions
|
|
40
|
-
- [~] Apply consistent Tailwind classes matching FormattedMessage styling
|
|
41
|
-
- [x] Add focus management and event handling
|
|
42
|
-
- [~] Handle content updates and markdown conversion
|
|
43
|
-
- [x] Implement proper cleanup and memory management
|
|
44
|
-
|
|
45
|
-
### Read-Only Viewer
|
|
46
|
-
- [ ] Create `ViewerContent.tsx` component
|
|
47
|
-
- [ ] Use react-markdown with same configuration as FormattedMessage
|
|
48
|
-
- [ ] Apply identical styling classes from FormattedMessage
|
|
49
|
-
- [ ] Support all existing markdown features (tables, lists, headings)
|
|
50
|
-
- [ ] Implement proper link handling and UTM parameter addition
|
|
51
|
-
- [ ] Handle streaming content display
|
|
52
|
-
- [ ] Maintain consistent component rendering patterns
|
|
53
|
-
|
|
54
|
-
### Mode Switching
|
|
55
|
-
- [ ] Implement seamless edit/readonly mode switching
|
|
56
|
-
- [ ] Preserve content during mode changes
|
|
57
|
-
- [ ] Handle focus management during switches
|
|
58
|
-
- [ ] Test content integrity across mode changes
|
|
59
|
-
|
|
60
|
-
### **Phase 1 Success Criteria**
|
|
61
|
-
- [ ] Editor initializes in under 500ms
|
|
62
|
-
- [ ] Mode switching completes in under 100ms
|
|
63
|
-
- [ ] Content preservation across all operations
|
|
64
|
-
- [ ] Styling consistency with FormattedMessage
|
|
65
|
-
- [ ] Unit tests passing for core functionality
|
|
66
|
-
|
|
67
|
-
---
|
|
68
|
-
|
|
69
|
-
## Phase 2: Multimedia System Integration (Week 3)
|
|
70
|
-
**Objective**: Integrate existing embed system and add multimedia handling capabilities
|
|
71
|
-
|
|
72
|
-
### Multimedia Extension
|
|
73
|
-
- [ ] Create `MultimediaExtension.ts` for TipTap in `@brainfish-ai/tiptap` package
|
|
74
|
-
- [ ] Integrate with existing EmbedComponent system from embeds/index.tsx
|
|
75
|
-
- [ ] Support all existing embed types (YouTube, Vimeo, Loom, etc.)
|
|
76
|
-
- [ ] Implement drag-and-drop file uploads using react-dropzone patterns
|
|
77
|
-
- [ ] Handle paste event handling for URLs and files
|
|
78
|
-
- [ ] Maintain consistency with FormattedMessage multimedia rendering
|
|
79
|
-
- [ ] Add proper error handling for unsupported media types
|
|
80
|
-
|
|
81
|
-
### Media Upload Component
|
|
82
|
-
- [ ] Create `MediaUpload.tsx` component
|
|
83
|
-
- [ ] Use react-dropzone for file upload UI
|
|
84
|
-
- [ ] Implement progress indicators and validation
|
|
85
|
-
- [ ] Handle multiple file types (images, videos, documents)
|
|
86
|
-
- [ ] Add file size and type validation with error messages
|
|
87
|
-
- [ ] Support drag-and-drop and click-to-upload interactions
|
|
88
|
-
- [ ] Integrate with existing file upload utilities and styles
|
|
89
|
-
|
|
90
|
-
### Media Renderer
|
|
91
|
-
- [ ] Create `MediaRenderer.tsx` component
|
|
92
|
-
- [ ] Reuse existing embed components (Youtube, Vimeo, ZoomableImage, etc.)
|
|
93
|
-
- [ ] Handle video content type prefixes and attachment redirects
|
|
94
|
-
- [ ] Implement proper error states and loading indicators
|
|
95
|
-
- [ ] Support all multimedia types defined in specification
|
|
96
|
-
- [ ] Provide consistent styling and responsive behavior
|
|
97
|
-
|
|
98
|
-
### Media Toolbar
|
|
99
|
-
- [ ] Create `MediaToolbar.tsx` component
|
|
100
|
-
- [ ] Add buttons for different media insertion types
|
|
101
|
-
- [ ] Use existing UI components (Button, Tooltip, DropdownMenu)
|
|
102
|
-
- [ ] Implement keyboard shortcuts for media insertion
|
|
103
|
-
- [ ] Show appropriate icons using @phosphor-icons/react
|
|
104
|
-
- [ ] Integrate with TipTap editor commands for media insertion
|
|
105
|
-
- [ ] Provide contextual media editing options (resize, align, etc.)
|
|
106
|
-
|
|
107
|
-
### **Phase 2 Success Criteria**
|
|
108
|
-
- [ ] 100% compatibility with existing embed system
|
|
109
|
-
- [ ] All FormattedMessage media types supported
|
|
110
|
-
- [ ] Drag-and-drop functionality working
|
|
111
|
-
- [ ] File upload with progress indicators
|
|
112
|
-
- [ ] Error handling for unsupported media
|
|
113
|
-
- [ ] Integration tests passing
|
|
114
|
-
|
|
115
|
-
---
|
|
116
|
-
|
|
117
|
-
## Phase 3: Suggestion System (Week 4)
|
|
118
|
-
**Objective**: Implement visual suggestion system with accept/reject functionality
|
|
119
|
-
|
|
120
|
-
### Suggestion Extension
|
|
121
|
-
- [ ] Create `SuggestionExtension.ts` for TipTap in `@brainfish-ai/tiptap` package
|
|
122
|
-
- [ ] Add suggestion marks to document nodes with visual styling
|
|
123
|
-
- [ ] Implement three suggestion types: insert, delete, replace
|
|
124
|
-
- [ ] Provide keyboard navigation through suggestions (Tab/Shift-Tab)
|
|
125
|
-
- [ ] Handle suggestion acceptance and rejection with proper cleanup
|
|
126
|
-
- [ ] Track suggestion metadata (author, timestamp, reason)
|
|
127
|
-
- [ ] Implement conflict resolution for overlapping suggestions
|
|
128
|
-
|
|
129
|
-
### Suggestion Visual Styles
|
|
130
|
-
- [ ] Create suggestion CSS styles with consistent color schemes
|
|
131
|
-
- [ ] Provide clear visual distinction between insert/delete/replace
|
|
132
|
-
- [ ] Implement hover and focus states for suggestions
|
|
133
|
-
- [ ] Support dark mode with appropriate color adjustments
|
|
134
|
-
- [ ] Maintain accessibility standards for color contrast
|
|
135
|
-
- [ ] Add smooth transitions for suggestion state changes
|
|
136
|
-
|
|
137
|
-
### Suggestion List Component
|
|
138
|
-
- [ ] Create `SuggestionList.tsx` component
|
|
139
|
-
- [ ] Display all suggestions in sidebar panel
|
|
140
|
-
- [ ] Group suggestions by type with filtering options
|
|
141
|
-
- [ ] Show suggestion metadata (author, timestamp, reason)
|
|
142
|
-
- [ ] Implement batch accept/reject operations
|
|
143
|
-
- [ ] Add search and sorting functionality
|
|
144
|
-
- [ ] Use existing UI components (ScrollArea, Button, Badge)
|
|
145
|
-
- [ ] Implement keyboard navigation and focus management
|
|
146
|
-
|
|
147
|
-
### Suggestion Navigation
|
|
148
|
-
- [ ] Create `useSuggestionNavigation` hook
|
|
149
|
-
- [ ] Manage active suggestion focus state
|
|
150
|
-
- [ ] Implement keyboard navigation between suggestions
|
|
151
|
-
- [ ] Provide methods for programmatic navigation
|
|
152
|
-
- [ ] Handle edge cases (no suggestions, deleted suggestions)
|
|
153
|
-
- [ ] Integrate with TipTap editor focus management
|
|
154
|
-
- [ ] Implement smooth scrolling to active suggestions
|
|
155
|
-
|
|
156
|
-
### **Phase 3 Success Criteria**
|
|
157
|
-
- [ ] All suggestion types visually distinct
|
|
158
|
-
- [ ] Keyboard navigation working smoothly
|
|
159
|
-
- [ ] Accept/reject operations functioning
|
|
160
|
-
- [ ] Conflict resolution working
|
|
161
|
-
- [ ] Accessibility compliance verified
|
|
162
|
-
- [ ] Performance with large suggestion lists
|
|
163
|
-
|
|
164
|
-
---
|
|
165
|
-
|
|
166
|
-
## Phase 4: Streaming Content Support (Week 5)
|
|
167
|
-
**Objective**: Implement real-time streaming content functionality
|
|
168
|
-
|
|
169
|
-
### Streaming Extension
|
|
170
|
-
- [ ] Create `StreamingExtension.ts` for TipTap in `@brainfish-ai/tiptap` package
|
|
171
|
-
- [ ] Handle real-time content insertion at document end
|
|
172
|
-
- [ ] Manage streaming state and prevent editing during streams
|
|
173
|
-
- [ ] Implement content buffering and batch updates for performance
|
|
174
|
-
- [ ] Provide visual indicators for streaming content
|
|
175
|
-
- [ ] Handle stream interruption and recovery gracefully
|
|
176
|
-
- [ ] Maintain document structure during streaming updates
|
|
177
|
-
|
|
178
|
-
### Streaming Hook
|
|
179
|
-
- [ ] Create `useStreaming` hook
|
|
180
|
-
- [ ] Manage streaming state and content buffering
|
|
181
|
-
- [ ] Implement debounced content updates for performance
|
|
182
|
-
- [ ] Handle streaming errors and connection issues
|
|
183
|
-
- [ ] Provide streaming progress and status information
|
|
184
|
-
- [ ] Integrate with TipTap editor streaming extension
|
|
185
|
-
- [ ] Implement proper cleanup and memory management
|
|
186
|
-
|
|
187
|
-
### Streaming Indicator Component
|
|
188
|
-
- [ ] Create `StreamingIndicator.tsx` component
|
|
189
|
-
- [ ] Show visual feedback during content streaming
|
|
190
|
-
- [ ] Display streaming progress and connection status
|
|
191
|
-
- [ ] Implement pulsing animation for active streaming
|
|
192
|
-
- [ ] Provide error states and retry functionality
|
|
193
|
-
- [ ] Use existing UI components (Progress, Badge, Button)
|
|
194
|
-
- [ ] Maintain consistent styling with rest of editor
|
|
195
|
-
|
|
196
|
-
### Streaming Buffer Management
|
|
197
|
-
- [ ] Create `StreamingBuffer` utility class
|
|
198
|
-
- [ ] Manage content buffering and batch processing
|
|
199
|
-
- [ ] Implement intelligent content chunking for performance
|
|
200
|
-
- [ ] Handle malformed or incomplete streaming content
|
|
201
|
-
- [ ] Provide content validation and sanitization
|
|
202
|
-
- [ ] Implement backpressure handling for fast streams
|
|
203
|
-
- [ ] Maintain content integrity during streaming
|
|
204
|
-
|
|
205
|
-
### **Phase 4 Success Criteria**
|
|
206
|
-
- [ ] Streaming content updates at 1000+ chars/second
|
|
207
|
-
- [ ] Buffer overflow protection working
|
|
208
|
-
- [ ] Error handling and recovery functioning
|
|
209
|
-
- [ ] Performance with large streams maintained
|
|
210
|
-
- [ ] Integration with editor state working
|
|
211
|
-
- [ ] Memory usage under control
|
|
212
|
-
|
|
213
|
-
---
|
|
214
|
-
|
|
215
|
-
## Phase 5: Collaboration Features (Week 6)
|
|
216
|
-
**Objective**: Implement real-time collaboration with cursors and presence
|
|
217
|
-
|
|
218
|
-
### Collaboration Extension
|
|
219
|
-
- [ ] Create `CollaborationExtension.ts` in `@brainfish-ai/tiptap` package
|
|
220
|
-
- [ ] Integrate with Yjs for operational transformation
|
|
221
|
-
- [ ] Implement real-time cursor tracking and presence
|
|
222
|
-
- [ ] Handle conflict resolution for simultaneous edits
|
|
223
|
-
- [ ] Provide user identification and avatar display
|
|
224
|
-
- [ ] Implement connection management and reconnection
|
|
225
|
-
- [ ] Maintain document consistency across all clients
|
|
226
|
-
|
|
227
|
-
### Collaborator Components
|
|
228
|
-
- [ ] Create `CollaboratorList.tsx` component
|
|
229
|
-
- [ ] Show all active collaborators with avatars and names
|
|
230
|
-
- [ ] Display real-time presence indicators (typing, idle, away)
|
|
231
|
-
- [ ] Implement user color coding for cursor identification
|
|
232
|
-
- [ ] Show connection status for each collaborator
|
|
233
|
-
- [ ] Use existing UI components (Avatar, Badge, Tooltip)
|
|
234
|
-
- [ ] Handle user interactions like viewing collaborator location
|
|
235
|
-
|
|
236
|
-
### Cursor Management System
|
|
237
|
-
- [ ] Create `CollaboratorCursor.tsx` component
|
|
238
|
-
- [ ] Render real-time cursor positions for all collaborators
|
|
239
|
-
- [ ] Show user names and colors for cursor identification
|
|
240
|
-
- [ ] Implement smooth cursor animations and transitions
|
|
241
|
-
- [ ] Handle cursor visibility based on viewport
|
|
242
|
-
- [ ] Provide selection range indicators for collaborators
|
|
243
|
-
- [ ] Integrate with TipTap collaboration cursor system
|
|
244
|
-
|
|
245
|
-
### Presence Management
|
|
246
|
-
- [ ] Create `useCollaborationPresence` hook
|
|
247
|
-
- [ ] Manage user presence state (active, idle, away)
|
|
248
|
-
- [ ] Implement typing indicators with debounced updates
|
|
249
|
-
- [ ] Handle connection status and reconnection logic
|
|
250
|
-
- [ ] Provide methods for sending presence updates
|
|
251
|
-
- [ ] Manage awareness state for all collaborators
|
|
252
|
-
- [ ] Implement proper cleanup and memory management
|
|
253
|
-
|
|
254
|
-
### **Phase 5 Success Criteria**
|
|
255
|
-
- [ ] Real-time collaboration working
|
|
256
|
-
- [ ] Cursor synchronization accurate
|
|
257
|
-
- [ ] Conflict resolution functioning
|
|
258
|
-
- [ ] Connection handling robust
|
|
259
|
-
- [ ] Presence indicators working
|
|
260
|
-
- [ ] Performance with multiple users maintained
|
|
261
|
-
|
|
262
|
-
---
|
|
263
|
-
|
|
264
|
-
## Phase 6: Integration and Polish (Week 7-8)
|
|
265
|
-
**Objective**: Complete integration, testing, and production readiness
|
|
266
|
-
|
|
267
|
-
### FormattedMessage Migration
|
|
268
|
-
- [ ] Create migration utilities for seamless replacement
|
|
269
|
-
- [ ] Maintain backward compatibility with existing props
|
|
270
|
-
- [ ] Implement feature flags for gradual rollout
|
|
271
|
-
- [ ] Provide fallback mechanisms for unsupported features
|
|
272
|
-
- [ ] Handle existing content migration and formatting preservation
|
|
273
|
-
- [ ] Create migration wrapper component for compatibility
|
|
274
|
-
|
|
275
|
-
### Comprehensive Error Handling
|
|
276
|
-
- [ ] Implement global error boundaries for graceful degradation
|
|
277
|
-
- [ ] Provide detailed error reporting and logging
|
|
278
|
-
- [ ] Handle network failures and API errors gracefully
|
|
279
|
-
- [ ] Implement retry mechanisms for recoverable errors
|
|
280
|
-
- [ ] Provide user-friendly error messages and recovery actions
|
|
281
|
-
- [ ] Integrate with existing error tracking systems
|
|
282
|
-
|
|
283
|
-
### Performance Optimization
|
|
284
|
-
- [ ] Implement React.memo and useMemo for expensive operations
|
|
285
|
-
- [ ] Add virtual scrolling for large documents
|
|
286
|
-
- [ ] Optimize bundle size with dynamic imports
|
|
287
|
-
- [ ] Implement debouncing for frequent operations
|
|
288
|
-
- [ ] Use web workers for heavy computations
|
|
289
|
-
- [ ] Implement efficient re-rendering strategies
|
|
290
|
-
|
|
291
|
-
### Accessibility Implementation
|
|
292
|
-
- [ ] Provide proper ARIA labels and roles for all interactive elements
|
|
293
|
-
- [ ] Implement keyboard navigation for all functionality
|
|
294
|
-
- [ ] Support screen readers with descriptive text and live regions
|
|
295
|
-
- [ ] Maintain focus management during mode switches and updates
|
|
296
|
-
- [ ] Provide high contrast mode support
|
|
297
|
-
- [ ] Verify color contrast ratios throughout interface
|
|
298
|
-
|
|
299
|
-
### Comprehensive Testing Suite
|
|
300
|
-
- [ ] Create unit tests for all major components
|
|
301
|
-
- [ ] Implement integration tests for workflows
|
|
302
|
-
- [ ] Add visual regression testing for UI components
|
|
303
|
-
- [ ] Test accessibility features with automated tools
|
|
304
|
-
- [ ] Include performance benchmarks and memory leak detection
|
|
305
|
-
- [ ] Test cross-browser compatibility
|
|
306
|
-
- [ ] Implement end-to-end testing scenarios
|
|
307
|
-
|
|
308
|
-
### Documentation and Migration Guide
|
|
309
|
-
- [ ] Create API documentation for all components
|
|
310
|
-
- [ ] Write migration guide from FormattedMessage
|
|
311
|
-
- [ ] Document all prop interfaces and callbacks
|
|
312
|
-
- [ ] Provide usage examples and best practices
|
|
313
|
-
- [ ] Create troubleshooting guide
|
|
314
|
-
- [ ] Document performance considerations
|
|
315
|
-
|
|
316
|
-
### **Phase 6 Success Criteria**
|
|
317
|
-
- [ ] All functionality working end-to-end
|
|
318
|
-
- [ ] Performance benchmarks met (initialization < 500ms, typing lag < 50ms)
|
|
319
|
-
- [ ] Accessibility compliance verified (WCAG 2.1 AA)
|
|
320
|
-
- [ ] Cross-browser testing complete
|
|
321
|
-
- [ ] Migration path validated
|
|
322
|
-
- [ ] Production deployment ready
|
|
323
|
-
|
|
324
|
-
---
|
|
325
|
-
|
|
326
|
-
## Final Acceptance Criteria
|
|
327
|
-
|
|
328
|
-
### Performance Requirements
|
|
329
|
-
- [ ] Editor initialization: < 500ms
|
|
330
|
-
- [ ] Mode switching: < 100ms
|
|
331
|
-
- [ ] Large document loading (1MB): < 2 seconds
|
|
332
|
-
- [ ] Typing lag: < 50ms
|
|
333
|
-
- [ ] Memory usage baseline: < 20MB
|
|
334
|
-
- [ ] Memory leak tolerance: < 1MB/hour
|
|
335
|
-
|
|
336
|
-
### Functionality Requirements
|
|
337
|
-
- [ ] Complete FormattedMessage.tsx feature parity
|
|
338
|
-
- [ ] 100% compatibility with existing embed system
|
|
339
|
-
- [ ] Consistent styling with existing components
|
|
340
|
-
- [ ] Support for all existing markdown features
|
|
341
|
-
- [ ] Real-time collaboration working
|
|
342
|
-
- [ ] Streaming content support functional
|
|
343
|
-
|
|
344
|
-
### Quality Assurance
|
|
345
|
-
- [ ] Unit test coverage > 90%
|
|
346
|
-
- [ ] All integration tests passing
|
|
347
|
-
- [ ] End-to-end tests covering complete user scenarios
|
|
348
|
-
- [ ] WCAG 2.1 AA accessibility compliance
|
|
349
|
-
- [ ] Cross-browser support (Chrome 90+, Firefox 88+, Safari 14+, Edge 90+)
|
|
350
|
-
- [ ] Mobile responsive design working
|
|
351
|
-
- [ ] Touch interface optimized
|
|
352
|
-
|
|
353
|
-
### Risk Mitigation Completed
|
|
354
|
-
- [ ] TipTap integration complexity managed with incremental approach
|
|
355
|
-
- [ ] Performance monitoring and optimization implemented
|
|
356
|
-
- [ ] Browser compatibility tested across platforms
|
|
357
|
-
- [ ] Backward compatibility maintained strictly
|
|
358
|
-
- [ ] Embed system integration verified
|
|
359
|
-
- [ ] State management patterns proven and tested
|
|
360
|
-
|
|
361
|
-
### Phase 1: Foundation and Basic Editor (Week 1-2)
|
|
362
|
-
|
|
363
|
-
**Objective**: Create basic TipTap editor with markdown conversion and mode switching.
|
|
364
|
-
|
|
365
|
-
**Implementation Prompts:**
|
|
366
|
-
|
|
367
|
-
**3.1.1 Create Root Component Structure**
|
|
368
|
-
```typescript
|
|
369
|
-
// Prompt: Create the main MarkdownEditorViewer component that:
|
|
370
|
-
// - Accepts all props defined in the specification interface
|
|
371
|
-
// - Implements basic mode switching between 'edit' and 'readonly'
|
|
372
|
-
// - Uses React.lazy for dynamic imports of Editor and Viewer components
|
|
373
|
-
// - Implements error boundaries for graceful error handling
|
|
374
|
-
// - Follows the existing FormattedMessage patterns for props and structure
|
|
375
|
-
|
|
376
|
-
interface MarkdownEditorViewerProps {
|
|
377
|
-
content: string;
|
|
378
|
-
mode: 'edit' | 'readonly';
|
|
379
|
-
onContentChange?: (content: string) => void;
|
|
380
|
-
onModeChange?: (mode: 'edit' | 'readonly') => void;
|
|
381
|
-
// ... other props from specification
|
|
382
|
-
}
|
|
383
|
-
|
|
384
|
-
const MarkdownEditorViewer: React.FC<MarkdownEditorViewerProps> = (props) => {
|
|
385
|
-
// Implementation that handles mode switching and component loading
|
|
386
|
-
};
|
|
387
|
-
```
|
|
388
|
-
|
|
389
|
-
**3.1.2 Build Markdown Translation Layer**
|
|
390
|
-
```typescript
|
|
391
|
-
// Prompt: Create markdownTranslator utility that:
|
|
392
|
-
// - Uses remark-gfm and rehype-raw to parse markdown (like FormattedMessage)
|
|
393
|
-
// - Converts markdown to TipTap JSON format for editing
|
|
394
|
-
// - Converts TipTap JSON back to markdown for export
|
|
395
|
-
// - Handles existing FormattedMessage patterns like video content prefixes
|
|
396
|
-
// - Preserves multimedia embeds and custom HTML elements
|
|
397
|
-
// - Implements proper error handling and fallbacks
|
|
398
|
-
|
|
399
|
-
const markdownTranslator = {
|
|
400
|
-
markdownToTipTap: (markdown: string): TipTapDocument => {
|
|
401
|
-
// Convert markdown to TipTap JSON using remark/rehype pipeline
|
|
402
|
-
},
|
|
403
|
-
|
|
404
|
-
tipTapToMarkdown: (doc: TipTapDocument): string => {
|
|
405
|
-
// Convert TipTap JSON back to markdown
|
|
406
|
-
},
|
|
407
|
-
|
|
408
|
-
parseExistingContent: (content: string, redirectRules?: RedirectRule[]): TipTapDocument => {
|
|
409
|
-
// Handle FormattedMessage-specific content patterns
|
|
410
|
-
}
|
|
411
|
-
};
|
|
412
|
-
```
|
|
413
|
-
|
|
414
|
-
**3.1.3 Implement Basic TipTap Editor**
|
|
415
|
-
```typescript
|
|
416
|
-
// Prompt: Create EditorContent component that:
|
|
417
|
-
// - Initializes TipTap editor with StarterKit extensions
|
|
418
|
-
// - Applies consistent Tailwind classes matching FormattedMessage styling
|
|
419
|
-
// - Implements proper focus management and event handling
|
|
420
|
-
// - Provides basic formatting toolbar with common actions
|
|
421
|
-
// - Handles content updates and markdown conversion
|
|
422
|
-
// - Implements proper cleanup and memory management
|
|
423
|
-
|
|
424
|
-
const EditorContent: React.FC<EditorContentProps> = ({ content, onChange, className }) => {
|
|
425
|
-
const editor = useEditor({
|
|
426
|
-
extensions: [/* TipTap extensions */],
|
|
427
|
-
content: markdownTranslator.markdownToTipTap(content),
|
|
428
|
-
onUpdate: ({ editor }) => {
|
|
429
|
-
const markdown = markdownTranslator.tipTapToMarkdown(editor.getJSON());
|
|
430
|
-
onChange(markdown);
|
|
431
|
-
},
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
// Implementation with proper styling and event handling
|
|
435
|
-
};
|
|
436
|
-
```
|
|
437
|
-
|
|
438
|
-
**3.1.4 Create Read-Only Viewer**
|
|
439
|
-
```typescript
|
|
440
|
-
// Prompt: Create ViewerContent component that:
|
|
441
|
-
// - Renders markdown content using react-markdown (like FormattedMessage)
|
|
442
|
-
// - Applies identical styling classes from FormattedMessage
|
|
443
|
-
// - Supports all existing markdown features (tables, lists, headings)
|
|
444
|
-
// - Implements proper link handling and UTM parameter addition
|
|
445
|
-
// - Maintains consistent component rendering patterns
|
|
446
|
-
// - Handles streaming content display
|
|
447
|
-
|
|
448
|
-
const ViewerContent: React.FC<ViewerContentProps> = ({ content, isStreaming, redirectRules }) => {
|
|
449
|
-
// Use react-markdown with same configuration as FormattedMessage
|
|
450
|
-
return (
|
|
451
|
-
<MemoizedReactMarkdown
|
|
452
|
-
className="dark:prose-invert prose flex-1"
|
|
453
|
-
remarkPlugins={[remarkGfm]}
|
|
454
|
-
rehypePlugins={[rehypeRaw]}
|
|
455
|
-
isStreaming={isStreaming}
|
|
456
|
-
components={{
|
|
457
|
-
// Match FormattedMessage component overrides exactly
|
|
458
|
-
}}
|
|
459
|
-
>
|
|
460
|
-
{content}
|
|
461
|
-
</MemoizedReactMarkdown>
|
|
462
|
-
);
|
|
463
|
-
};
|
|
464
|
-
```
|
|
465
|
-
|
|
466
|
-
**Phase 1 Testing Strategy:**
|
|
467
|
-
- Unit tests for markdown translation utilities
|
|
468
|
-
- Integration tests for editor initialization
|
|
469
|
-
- Mode switching functionality tests
|
|
470
|
-
- Content preservation tests
|
|
471
|
-
- Styling consistency verification
|
|
472
|
-
|
|
473
|
-
### Phase 2: Multimedia System Integration (Week 3)
|
|
474
|
-
|
|
475
|
-
**Objective**: Integrate existing embed system and add multimedia handling capabilities.
|
|
476
|
-
|
|
477
|
-
**Implementation Prompts:**
|
|
478
|
-
|
|
479
|
-
**3.2.1 Create Multimedia Extension**
|
|
480
|
-
```typescript
|
|
481
|
-
// Prompt: Create MultimediaExtension for TipTap that:
|
|
482
|
-
// - Integrates with existing EmbedComponent system from embeds/index.tsx
|
|
483
|
-
// - Supports all existing embed types (YouTube, Vimeo, Loom, etc.)
|
|
484
|
-
// - Handles drag-and-drop file uploads using react-dropzone patterns
|
|
485
|
-
// - Implements paste event handling for URLs and files
|
|
486
|
-
// - Maintains consistency with FormattedMessage multimedia rendering
|
|
487
|
-
// - Provides proper error handling for unsupported media types
|
|
488
|
-
|
|
489
|
-
const MultimediaExtension = Extension.create({
|
|
490
|
-
name: 'multimedia',
|
|
491
|
-
|
|
492
|
-
addCommands() {
|
|
493
|
-
return {
|
|
494
|
-
insertMedia: (attrs: MediaAttributes) => ({ tr, state }) => {
|
|
495
|
-
// Insert media node using existing EmbedComponent patterns
|
|
496
|
-
},
|
|
497
|
-
|
|
498
|
-
handlePaste: (view: EditorView, event: ClipboardEvent) => {
|
|
499
|
-
// Handle pasted URLs and files
|
|
500
|
-
},
|
|
501
|
-
|
|
502
|
-
handleDrop: (view: EditorView, event: DragEvent) => {
|
|
503
|
-
// Handle dropped files
|
|
504
|
-
}
|
|
505
|
-
};
|
|
506
|
-
},
|
|
507
|
-
|
|
508
|
-
addProseMirrorPlugins() {
|
|
509
|
-
return [
|
|
510
|
-
new Plugin({
|
|
511
|
-
key: new PluginKey('multimedia'),
|
|
512
|
-
props: {
|
|
513
|
-
handleDOMEvents: {
|
|
514
|
-
paste: handlePasteEvent,
|
|
515
|
-
drop: handleDropEvent,
|
|
516
|
-
},
|
|
517
|
-
},
|
|
518
|
-
}),
|
|
519
|
-
];
|
|
520
|
-
},
|
|
521
|
-
});
|
|
522
|
-
```
|
|
523
|
-
|
|
524
|
-
**3.2.2 Build Media Upload Component**
|
|
525
|
-
```typescript
|
|
526
|
-
// Prompt: Create MediaUpload component that:
|
|
527
|
-
// - Uses react-dropzone for file upload UI (following existing file-upload patterns)
|
|
528
|
-
// - Implements progress indicators and validation
|
|
529
|
-
// - Handles multiple file types (images, videos, documents)
|
|
530
|
-
// - Provides file size and type validation with clear error messages
|
|
531
|
-
// - Integrates with existing file upload utilities and styles
|
|
532
|
-
// - Supports drag-and-drop and click-to-upload interactions
|
|
533
|
-
|
|
534
|
-
const MediaUpload: React.FC<MediaUploadProps> = ({ onUpload, allowedTypes, maxSize }) => {
|
|
535
|
-
const { getRootProps, getInputProps, isDragActive } = useDropzone({
|
|
536
|
-
accept: allowedTypes,
|
|
537
|
-
maxSize,
|
|
538
|
-
onDrop: handleFileDrop,
|
|
539
|
-
});
|
|
540
|
-
|
|
541
|
-
// Implementation with progress tracking and error handling
|
|
542
|
-
};
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
**3.2.3 Implement Media Renderer**
|
|
546
|
-
```typescript
|
|
547
|
-
// Prompt: Create MediaRenderer component that:
|
|
548
|
-
// - Reuses existing embed components (Youtube, Vimeo, ZoomableImage, etc.)
|
|
549
|
-
// - Maintains consistent rendering patterns from FormattedMessage
|
|
550
|
-
// - Handles video content type prefixes and attachment redirects
|
|
551
|
-
// - Implements proper error states and loading indicators
|
|
552
|
-
// - Supports all multimedia types defined in the specification
|
|
553
|
-
// - Provides consistent styling and responsive behavior
|
|
554
|
-
|
|
555
|
-
const MediaRenderer: React.FC<MediaRendererProps> = ({ url, type, isAttachmentVideo, alt }) => {
|
|
556
|
-
// Use existing EmbedComponent logic
|
|
557
|
-
const embedComponent = EmbedComponent({
|
|
558
|
-
attrs: { href: url },
|
|
559
|
-
isAttachmentVideo,
|
|
560
|
-
});
|
|
561
|
-
|
|
562
|
-
if (React.isValidElement(embedComponent)) {
|
|
563
|
-
return embedComponent;
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
// Handle ZoomableImage for regular images
|
|
567
|
-
if (type === 'image') {
|
|
568
|
-
return <ZoomableImage src={url} alt={alt} />;
|
|
569
|
-
}
|
|
570
|
-
|
|
571
|
-
// Fallback for unsupported types
|
|
572
|
-
return <div>Unsupported media type: {type}</div>;
|
|
573
|
-
};
|
|
574
|
-
```
|
|
575
|
-
|
|
576
|
-
**3.2.4 Create Media Toolbar**
|
|
577
|
-
```typescript
|
|
578
|
-
// Prompt: Create MediaToolbar component that:
|
|
579
|
-
// - Provides buttons for different media insertion types
|
|
580
|
-
// - Uses existing UI components (Button, Tooltip, DropdownMenu from ui/)
|
|
581
|
-
// - Implements keyboard shortcuts for media insertion
|
|
582
|
-
// - Shows appropriate icons using @phosphor-icons/react
|
|
583
|
-
// - Integrates with TipTap editor commands for media insertion
|
|
584
|
-
// - Provides contextual media editing options (resize, align, etc.)
|
|
585
|
-
|
|
586
|
-
const MediaToolbar: React.FC<MediaToolbarProps> = ({ editor, onMediaInsert }) => {
|
|
587
|
-
const insertButtons = [
|
|
588
|
-
{
|
|
589
|
-
type: 'image',
|
|
590
|
-
icon: <Image size={16} />,
|
|
591
|
-
label: 'Insert Image',
|
|
592
|
-
shortcut: 'Mod-Shift-I',
|
|
593
|
-
},
|
|
594
|
-
{
|
|
595
|
-
type: 'video',
|
|
596
|
-
icon: <VideoCamera size={16} />,
|
|
597
|
-
label: 'Insert Video',
|
|
598
|
-
shortcut: 'Mod-Shift-V',
|
|
599
|
-
},
|
|
600
|
-
// ... other media types
|
|
601
|
-
];
|
|
602
|
-
|
|
603
|
-
// Implementation with proper button states and handlers
|
|
604
|
-
};
|
|
605
|
-
```
|
|
606
|
-
|
|
607
|
-
**Phase 2 Testing Strategy:**
|
|
608
|
-
- Integration tests with existing embed components
|
|
609
|
-
- File upload functionality testing
|
|
610
|
-
- Media insertion and rendering tests
|
|
611
|
-
- Drag-and-drop behavior verification
|
|
612
|
-
- Error handling for unsupported media types
|
|
613
|
-
|
|
614
|
-
### Phase 3: Suggestion System (Week 4)
|
|
615
|
-
|
|
616
|
-
**Objective**: Implement visual suggestion system with accept/reject functionality.
|
|
617
|
-
|
|
618
|
-
**Implementation Prompts:**
|
|
619
|
-
|
|
620
|
-
**3.3.1 Create Suggestion Extension**
|
|
621
|
-
```typescript
|
|
622
|
-
// Prompt: Create SuggestionExtension for TipTap that:
|
|
623
|
-
// - Adds suggestion marks to document nodes with visual styling
|
|
624
|
-
// - Implements three suggestion types: insert, delete, replace
|
|
625
|
-
// - Provides keyboard navigation through suggestions (Tab/Shift-Tab)
|
|
626
|
-
// - Handles suggestion acceptance and rejection with proper cleanup
|
|
627
|
-
// - Tracks suggestion metadata (author, timestamp, reason)
|
|
628
|
-
// - Implements proper conflict resolution for overlapping suggestions
|
|
629
|
-
|
|
630
|
-
const SuggestionExtension = Extension.create({
|
|
631
|
-
name: 'suggestions',
|
|
632
|
-
|
|
633
|
-
addGlobalAttributes() {
|
|
634
|
-
return [
|
|
635
|
-
{
|
|
636
|
-
types: ['paragraph', 'heading', 'text'],
|
|
637
|
-
attributes: {
|
|
638
|
-
suggestionId: { default: null },
|
|
639
|
-
suggestionType: { default: null },
|
|
640
|
-
suggestionAuthor: { default: null },
|
|
641
|
-
suggestionTimestamp: { default: null },
|
|
642
|
-
},
|
|
643
|
-
},
|
|
644
|
-
];
|
|
645
|
-
},
|
|
646
|
-
|
|
647
|
-
addCommands() {
|
|
648
|
-
return {
|
|
649
|
-
addSuggestion: (suggestion: EditorSuggestion) => ({ tr, state }) => {
|
|
650
|
-
// Add suggestion marks to the document
|
|
651
|
-
},
|
|
652
|
-
|
|
653
|
-
acceptSuggestion: (suggestionId: string) => ({ tr, state }) => {
|
|
654
|
-
// Apply suggestion changes and remove marks
|
|
655
|
-
},
|
|
656
|
-
|
|
657
|
-
rejectSuggestion: (suggestionId: string) => ({ tr, state }) => {
|
|
658
|
-
// Remove suggestion marks without applying changes
|
|
659
|
-
},
|
|
660
|
-
|
|
661
|
-
navigateToNextSuggestion: () => ({ tr, state }) => {
|
|
662
|
-
// Move focus to next suggestion
|
|
663
|
-
},
|
|
664
|
-
|
|
665
|
-
acceptAllSuggestions: () => ({ tr, state }) => {
|
|
666
|
-
// Accept all pending suggestions
|
|
667
|
-
}
|
|
668
|
-
};
|
|
669
|
-
},
|
|
670
|
-
|
|
671
|
-
addKeyboardShortcuts() {
|
|
672
|
-
return {
|
|
673
|
-
'Tab': () => this.editor.commands.navigateToNextSuggestion(),
|
|
674
|
-
'Shift-Tab': () => this.editor.commands.navigateToPreviousSuggestion(),
|
|
675
|
-
'Enter': () => this.editor.commands.acceptActiveSuggestion(),
|
|
676
|
-
'Escape': () => this.editor.commands.rejectActiveSuggestion(),
|
|
677
|
-
};
|
|
678
|
-
},
|
|
679
|
-
});
|
|
680
|
-
```
|
|
681
|
-
|
|
682
|
-
**3.3.2 Implement Suggestion Visual Styles**
|
|
683
|
-
```css
|
|
684
|
-
/* Prompt: Create suggestion CSS styles that:
|
|
685
|
-
* - Use consistent color schemes for different suggestion types
|
|
686
|
-
* - Provide clear visual distinction between insert/delete/replace
|
|
687
|
-
* - Implement hover and focus states for suggestions
|
|
688
|
-
* - Support dark mode with appropriate color adjustments
|
|
689
|
-
* - Maintain accessibility standards for color contrast
|
|
690
|
-
* - Provide smooth transitions for suggestion state changes
|
|
691
|
-
*/
|
|
692
|
-
|
|
693
|
-
.suggestion-insert {
|
|
694
|
-
text-decoration: underline;
|
|
695
|
-
background-color: rgba(34, 197, 94, 0.1);
|
|
696
|
-
color: rgba(34, 197, 94, 0.8);
|
|
697
|
-
border-radius: 2px;
|
|
698
|
-
padding: 1px 2px;
|
|
699
|
-
transition: all 0.2s ease;
|
|
700
|
-
}
|
|
701
|
-
|
|
702
|
-
.suggestion-delete {
|
|
703
|
-
text-decoration: line-through;
|
|
704
|
-
background-color: rgba(239, 68, 68, 0.1);
|
|
705
|
-
color: rgba(239, 68, 68, 0.7);
|
|
706
|
-
border-radius: 2px;
|
|
707
|
-
padding: 1px 2px;
|
|
708
|
-
transition: all 0.2s ease;
|
|
709
|
-
}
|
|
710
|
-
|
|
711
|
-
.suggestion-replace {
|
|
712
|
-
position: relative;
|
|
713
|
-
}
|
|
714
|
-
|
|
715
|
-
.suggestion-replace .old-text {
|
|
716
|
-
text-decoration: line-through;
|
|
717
|
-
background-color: rgba(239, 68, 68, 0.1);
|
|
718
|
-
color: rgba(239, 68, 68, 0.7);
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
.suggestion-replace .new-text {
|
|
722
|
-
text-decoration: underline;
|
|
723
|
-
background-color: rgba(34, 197, 94, 0.1);
|
|
724
|
-
color: rgba(34, 197, 94, 0.8);
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
.suggestion-active {
|
|
728
|
-
outline: 2px solid #3b82f6;
|
|
729
|
-
outline-offset: 1px;
|
|
730
|
-
box-shadow: 0 0 0 3px rgba(59, 130, 246, 0.1);
|
|
731
|
-
}
|
|
732
|
-
|
|
733
|
-
/* Dark mode adjustments */
|
|
734
|
-
.dark .suggestion-insert {
|
|
735
|
-
background-color: rgba(34, 197, 94, 0.2);
|
|
736
|
-
color: rgba(34, 197, 94, 0.9);
|
|
737
|
-
}
|
|
738
|
-
|
|
739
|
-
.dark .suggestion-delete {
|
|
740
|
-
background-color: rgba(239, 68, 68, 0.2);
|
|
741
|
-
color: rgba(239, 68, 68, 0.8);
|
|
742
|
-
}
|
|
743
|
-
```
|
|
744
|
-
|
|
745
|
-
**3.3.3 Build Suggestion List Component**
|
|
746
|
-
```typescript
|
|
747
|
-
// Prompt: Create SuggestionList component that:
|
|
748
|
-
// - Displays all suggestions in a sidebar panel
|
|
749
|
-
// - Groups suggestions by type and provides filtering options
|
|
750
|
-
// - Shows suggestion metadata (author, timestamp, reason)
|
|
751
|
-
// - Implements batch accept/reject operations
|
|
752
|
-
// - Provides search and sorting functionality
|
|
753
|
-
// - Uses existing UI components (ScrollArea, Button, Badge)
|
|
754
|
-
// - Implements proper keyboard navigation and focus management
|
|
755
|
-
|
|
756
|
-
const SuggestionList: React.FC<SuggestionListProps> = ({ suggestions, onAccept, onReject }) => {
|
|
757
|
-
const [filter, setFilter] = useState<'all' | 'insert' | 'delete' | 'replace'>('all');
|
|
758
|
-
const [sortBy, setSortBy] = useState<'timestamp' | 'author' | 'type'>('timestamp');
|
|
759
|
-
|
|
760
|
-
const filteredSuggestions = useMemo(() => {
|
|
761
|
-
return suggestions
|
|
762
|
-
.filter(s => filter === 'all' || s.type === filter)
|
|
763
|
-
.sort((a, b) => sortBySuggestion(a, b, sortBy));
|
|
764
|
-
}, [suggestions, filter, sortBy]);
|
|
765
|
-
|
|
766
|
-
return (
|
|
767
|
-
<ScrollArea className="h-full">
|
|
768
|
-
<div className="space-y-2 p-4">
|
|
769
|
-
{/* Filter and sort controls */}
|
|
770
|
-
<div className="flex gap-2 mb-4">
|
|
771
|
-
<Badge variant="outline" onClick={() => setFilter('all')}>
|
|
772
|
-
All ({suggestions.length})
|
|
773
|
-
</Badge>
|
|
774
|
-
{/* Other filter badges */}
|
|
775
|
-
</div>
|
|
776
|
-
|
|
777
|
-
{/* Suggestion items */}
|
|
778
|
-
{filteredSuggestions.map(suggestion => (
|
|
779
|
-
<SuggestionItem
|
|
780
|
-
key={suggestion.id}
|
|
781
|
-
suggestion={suggestion}
|
|
782
|
-
onAccept={() => onAccept(suggestion.id)}
|
|
783
|
-
onReject={() => onReject(suggestion.id)}
|
|
784
|
-
/>
|
|
785
|
-
))}
|
|
786
|
-
</div>
|
|
787
|
-
</ScrollArea>
|
|
788
|
-
);
|
|
789
|
-
};
|
|
790
|
-
```
|
|
791
|
-
|
|
792
|
-
**3.3.4 Implement Suggestion Navigation Hooks**
|
|
793
|
-
```typescript
|
|
794
|
-
// Prompt: Create useSuggestionNavigation hook that:
|
|
795
|
-
// - Manages active suggestion focus state
|
|
796
|
-
// - Implements keyboard navigation between suggestions
|
|
797
|
-
// - Provides methods for programmatic suggestion navigation
|
|
798
|
-
// - Handles edge cases (no suggestions, deleted suggestions)
|
|
799
|
-
// - Integrates with TipTap editor focus management
|
|
800
|
-
// - Implements smooth scrolling to active suggestions
|
|
801
|
-
|
|
802
|
-
const useSuggestionNavigation = (editor: Editor, suggestions: EditorSuggestion[]) => {
|
|
803
|
-
const [activeSuggestionId, setActiveSuggestionId] = useState<string | null>(null);
|
|
804
|
-
|
|
805
|
-
const navigateToNext = useCallback(() => {
|
|
806
|
-
const currentIndex = suggestions.findIndex(s => s.id === activeSuggestionId);
|
|
807
|
-
const nextIndex = currentIndex < suggestions.length - 1 ? currentIndex + 1 : 0;
|
|
808
|
-
const nextSuggestion = suggestions[nextIndex];
|
|
809
|
-
|
|
810
|
-
if (nextSuggestion) {
|
|
811
|
-
setActiveSuggestionId(nextSuggestion.id);
|
|
812
|
-
editor.commands.setTextSelection(nextSuggestion.position.from);
|
|
813
|
-
scrollToSuggestion(nextSuggestion.id);
|
|
814
|
-
}
|
|
815
|
-
}, [editor, suggestions, activeSuggestionId]);
|
|
816
|
-
|
|
817
|
-
const navigateToPrevious = useCallback(() => {
|
|
818
|
-
const currentIndex = suggestions.findIndex(s => s.id === activeSuggestionId);
|
|
819
|
-
const prevIndex = currentIndex > 0 ? currentIndex - 1 : suggestions.length - 1;
|
|
820
|
-
const prevSuggestion = suggestions[prevIndex];
|
|
821
|
-
|
|
822
|
-
if (prevSuggestion) {
|
|
823
|
-
setActiveSuggestionId(prevSuggestion.id);
|
|
824
|
-
editor.commands.setTextSelection(prevSuggestion.position.from);
|
|
825
|
-
scrollToSuggestion(prevSuggestion.id);
|
|
826
|
-
}
|
|
827
|
-
}, [editor, suggestions, activeSuggestionId]);
|
|
828
|
-
|
|
829
|
-
return {
|
|
830
|
-
activeSuggestionId,
|
|
831
|
-
setActiveSuggestionId,
|
|
832
|
-
navigateToNext,
|
|
833
|
-
navigateToPrevious,
|
|
834
|
-
hasNext: /* logic */,
|
|
835
|
-
hasPrevious: /* logic */,
|
|
836
|
-
};
|
|
837
|
-
};
|
|
838
|
-
```
|
|
839
|
-
|
|
840
|
-
**Phase 3 Testing Strategy:**
|
|
841
|
-
- Suggestion visual rendering tests
|
|
842
|
-
- Keyboard navigation functionality
|
|
843
|
-
- Accept/reject operation tests
|
|
844
|
-
- Suggestion conflict resolution
|
|
845
|
-
- Accessibility compliance testing
|
|
846
|
-
|
|
847
|
-
### Phase 4: Streaming Content Support (Week 5)
|
|
848
|
-
|
|
849
|
-
**Objective**: Implement real-time streaming content functionality.
|
|
850
|
-
|
|
851
|
-
**Implementation Prompts:**
|
|
852
|
-
|
|
853
|
-
**4.4.1 Create Streaming Extension**
|
|
854
|
-
```typescript
|
|
855
|
-
// Prompt: Create StreamingExtension for TipTap that:
|
|
856
|
-
// - Handles real-time content insertion at document end
|
|
857
|
-
// - Manages streaming state and prevents editing during streams
|
|
858
|
-
// - Implements content buffering and batch updates for performance
|
|
859
|
-
// - Provides visual indicators for streaming content
|
|
860
|
-
// - Handles stream interruption and recovery gracefully
|
|
861
|
-
// - Maintains document structure during streaming updates
|
|
862
|
-
|
|
863
|
-
const StreamingExtension = Extension.create({
|
|
864
|
-
name: 'streaming',
|
|
865
|
-
|
|
866
|
-
addStorage() {
|
|
867
|
-
return {
|
|
868
|
-
isStreaming: false,
|
|
869
|
-
streamBuffer: '',
|
|
870
|
-
streamPosition: 0,
|
|
871
|
-
};
|
|
872
|
-
},
|
|
873
|
-
|
|
874
|
-
addCommands() {
|
|
875
|
-
return {
|
|
876
|
-
startStream: () => ({ tr, state }) => {
|
|
877
|
-
this.storage.isStreaming = true;
|
|
878
|
-
this.storage.streamPosition = state.doc.content.size;
|
|
879
|
-
return true;
|
|
880
|
-
},
|
|
881
|
-
|
|
882
|
-
appendStreamContent: (content: string) => ({ tr, state, dispatch }) => {
|
|
883
|
-
if (!this.storage.isStreaming) return false;
|
|
884
|
-
|
|
885
|
-
this.storage.streamBuffer += content;
|
|
886
|
-
|
|
887
|
-
// Batch updates for performance
|
|
888
|
-
if (dispatch) {
|
|
889
|
-
tr.insertText(content, this.storage.streamPosition);
|
|
890
|
-
this.storage.streamPosition += content.length;
|
|
891
|
-
}
|
|
892
|
-
|
|
893
|
-
return true;
|
|
894
|
-
},
|
|
895
|
-
|
|
896
|
-
finalizeStream: () => ({ tr, state }) => {
|
|
897
|
-
this.storage.isStreaming = false;
|
|
898
|
-
this.storage.streamBuffer = '';
|
|
899
|
-
|
|
900
|
-
// Process final content formatting
|
|
901
|
-
this.editor.commands.focus('end');
|
|
902
|
-
return true;
|
|
903
|
-
},
|
|
904
|
-
};
|
|
905
|
-
},
|
|
906
|
-
|
|
907
|
-
onUpdate() {
|
|
908
|
-
// Update streaming position if document changes
|
|
909
|
-
if (this.storage.isStreaming) {
|
|
910
|
-
this.storage.streamPosition = this.editor.state.doc.content.size;
|
|
911
|
-
}
|
|
912
|
-
},
|
|
913
|
-
});
|
|
914
|
-
```
|
|
915
|
-
|
|
916
|
-
**4.4.2 Implement Streaming Hook**
|
|
917
|
-
```typescript
|
|
918
|
-
// Prompt: Create useStreaming hook that:
|
|
919
|
-
// - Manages streaming state and content buffering
|
|
920
|
-
// - Implements debounced content updates for performance
|
|
921
|
-
// - Handles streaming errors and connection issues
|
|
922
|
-
// - Provides streaming progress and status information
|
|
923
|
-
// - Integrates with TipTap editor streaming extension
|
|
924
|
-
// - Implements proper cleanup and memory management
|
|
925
|
-
|
|
926
|
-
const useStreaming = (editor: Editor | null, isStreaming?: boolean) => {
|
|
927
|
-
const [streamState, setStreamState] = useState<StreamingState>({
|
|
928
|
-
isActive: false,
|
|
929
|
-
buffer: '',
|
|
930
|
-
progress: 0,
|
|
931
|
-
error: null,
|
|
932
|
-
});
|
|
933
|
-
|
|
934
|
-
const debouncedUpdate = useCallback(
|
|
935
|
-
debounce((content: string) => {
|
|
936
|
-
if (editor && streamState.isActive) {
|
|
937
|
-
editor.commands.appendStreamContent(content);
|
|
938
|
-
}
|
|
939
|
-
}, 50),
|
|
940
|
-
[editor, streamState.isActive]
|
|
941
|
-
);
|
|
942
|
-
|
|
943
|
-
const startStream = useCallback(() => {
|
|
944
|
-
if (!editor) return;
|
|
945
|
-
|
|
946
|
-
editor.commands.startStream();
|
|
947
|
-
setStreamState(prev => ({ ...prev, isActive: true, error: null }));
|
|
948
|
-
}, [editor]);
|
|
949
|
-
|
|
950
|
-
const appendContent = useCallback((chunk: string) => {
|
|
951
|
-
setStreamState(prev => ({
|
|
952
|
-
...prev,
|
|
953
|
-
buffer: prev.buffer + chunk,
|
|
954
|
-
progress: prev.progress + chunk.length
|
|
955
|
-
}));
|
|
956
|
-
|
|
957
|
-
debouncedUpdate(chunk);
|
|
958
|
-
}, [debouncedUpdate]);
|
|
959
|
-
|
|
960
|
-
const finalizeStream = useCallback(() => {
|
|
961
|
-
if (!editor) return;
|
|
962
|
-
|
|
963
|
-
editor.commands.finalizeStream();
|
|
964
|
-
setStreamState(prev => ({
|
|
965
|
-
...prev,
|
|
966
|
-
isActive: false,
|
|
967
|
-
buffer: '',
|
|
968
|
-
progress: 0
|
|
969
|
-
}));
|
|
970
|
-
}, [editor]);
|
|
971
|
-
|
|
972
|
-
// Sync with external streaming prop
|
|
973
|
-
useEffect(() => {
|
|
974
|
-
if (isStreaming && !streamState.isActive) {
|
|
975
|
-
startStream();
|
|
976
|
-
} else if (!isStreaming && streamState.isActive) {
|
|
977
|
-
finalizeStream();
|
|
978
|
-
}
|
|
979
|
-
}, [isStreaming, streamState.isActive, startStream, finalizeStream]);
|
|
980
|
-
|
|
981
|
-
return {
|
|
982
|
-
streamState,
|
|
983
|
-
startStream,
|
|
984
|
-
appendContent,
|
|
985
|
-
finalizeStream,
|
|
986
|
-
isStreaming: streamState.isActive,
|
|
987
|
-
};
|
|
988
|
-
};
|
|
989
|
-
```
|
|
990
|
-
|
|
991
|
-
**4.4.3 Build Streaming Indicator Component**
|
|
992
|
-
```typescript
|
|
993
|
-
// Prompt: Create StreamingIndicator component that:
|
|
994
|
-
// - Shows visual feedback during content streaming
|
|
995
|
-
// - Displays streaming progress and connection status
|
|
996
|
-
// - Implements pulsing animation for active streaming
|
|
997
|
-
// - Provides error states and retry functionality
|
|
998
|
-
// - Uses existing UI components (Progress, Badge, Button)
|
|
999
|
-
// - Maintains consistent styling with rest of editor
|
|
1000
|
-
|
|
1001
|
-
const StreamingIndicator: React.FC<StreamingIndicatorProps> = ({
|
|
1002
|
-
isStreaming,
|
|
1003
|
-
progress,
|
|
1004
|
-
error,
|
|
1005
|
-
onRetry
|
|
1006
|
-
}) => {
|
|
1007
|
-
if (error) {
|
|
1008
|
-
return (
|
|
1009
|
-
<div className="flex items-center gap-2 px-3 py-2 bg-red-50 border border-red-200 rounded-md">
|
|
1010
|
-
<ExclamationTriangle className="h-4 w-4 text-red-500" />
|
|
1011
|
-
<span className="text-sm text-red-700">Streaming error: {error.message}</span>
|
|
1012
|
-
<Button size="sm" variant="outline" onClick={onRetry}>
|
|
1013
|
-
Retry
|
|
1014
|
-
</Button>
|
|
1015
|
-
</div>
|
|
1016
|
-
);
|
|
1017
|
-
}
|
|
1018
|
-
|
|
1019
|
-
if (!isStreaming) return null;
|
|
1020
|
-
|
|
1021
|
-
return (
|
|
1022
|
-
<div className="flex items-center gap-2 px-3 py-2 bg-blue-50 border border-blue-200 rounded-md">
|
|
1023
|
-
<div className="flex items-center gap-2">
|
|
1024
|
-
<div className="h-2 w-2 bg-blue-500 rounded-full animate-pulse" />
|
|
1025
|
-
<span className="text-sm text-blue-700">Streaming content...</span>
|
|
1026
|
-
</div>
|
|
1027
|
-
|
|
1028
|
-
{progress > 0 && (
|
|
1029
|
-
<div className="flex items-center gap-2 ml-4">
|
|
1030
|
-
<Progress value={progress} className="w-24 h-1" />
|
|
1031
|
-
<span className="text-xs text-blue-600">{progress}%</span>
|
|
1032
|
-
</div>
|
|
1033
|
-
)}
|
|
1034
|
-
</div>
|
|
1035
|
-
);
|
|
1036
|
-
};
|
|
1037
|
-
```
|
|
1038
|
-
|
|
1039
|
-
**4.4.4 Implement Streaming Buffer Management**
|
|
1040
|
-
```typescript
|
|
1041
|
-
// Prompt: Create StreamingBuffer utility that:
|
|
1042
|
-
// - Manages content buffering and batch processing
|
|
1043
|
-
// - Implements intelligent content chunking for performance
|
|
1044
|
-
// - Handles malformed or incomplete streaming content
|
|
1045
|
-
// - Provides content validation and sanitization
|
|
1046
|
-
// - Implements backpressure handling for fast streams
|
|
1047
|
-
// - Maintains content integrity during streaming
|
|
1048
|
-
|
|
1049
|
-
class StreamingBuffer {
|
|
1050
|
-
private buffer: string = '';
|
|
1051
|
-
private processingQueue: string[] = [];
|
|
1052
|
-
private isProcessing: boolean = false;
|
|
1053
|
-
private readonly chunkSize: number = 1000;
|
|
1054
|
-
private readonly maxBufferSize: number = 50000;
|
|
1055
|
-
|
|
1056
|
-
constructor(
|
|
1057
|
-
private onChunk: (chunk: string) => void,
|
|
1058
|
-
private onError: (error: Error) => void
|
|
1059
|
-
) {}
|
|
1060
|
-
|
|
1061
|
-
append(content: string): void {
|
|
1062
|
-
if (this.buffer.length + content.length > this.maxBufferSize) {
|
|
1063
|
-
this.onError(new Error('Buffer overflow: content too large'));
|
|
1064
|
-
return;
|
|
1065
|
-
}
|
|
1066
|
-
|
|
1067
|
-
this.buffer += content;
|
|
1068
|
-
this.processBuffer();
|
|
1069
|
-
}
|
|
1070
|
-
|
|
1071
|
-
private async processBuffer(): Promise<void> {
|
|
1072
|
-
if (this.isProcessing) return;
|
|
1073
|
-
|
|
1074
|
-
this.isProcessing = true;
|
|
1075
|
-
|
|
1076
|
-
try {
|
|
1077
|
-
while (this.buffer.length > 0) {
|
|
1078
|
-
const chunk = this.buffer.slice(0, this.chunkSize);
|
|
1079
|
-
this.buffer = this.buffer.slice(this.chunkSize);
|
|
1080
|
-
|
|
1081
|
-
// Process chunk and handle any formatting
|
|
1082
|
-
const processedChunk = this.sanitizeContent(chunk);
|
|
1083
|
-
this.onChunk(processedChunk);
|
|
1084
|
-
|
|
1085
|
-
// Yield to browser for rendering
|
|
1086
|
-
await new Promise(resolve => setTimeout(resolve, 0));
|
|
1087
|
-
}
|
|
1088
|
-
} catch (error) {
|
|
1089
|
-
this.onError(error instanceof Error ? error : new Error('Unknown streaming error'));
|
|
1090
|
-
} finally {
|
|
1091
|
-
this.isProcessing = false;
|
|
1092
|
-
}
|
|
1093
|
-
}
|
|
1094
|
-
|
|
1095
|
-
private sanitizeContent(content: string): string {
|
|
1096
|
-
// Implement content validation and sanitization
|
|
1097
|
-
return content
|
|
1098
|
-
.replace(/\x00/g, '') // Remove null bytes
|
|
1099
|
-
.replace(/[\x01-\x08\x0B\x0C\x0E-\x1F\x7F]/g, ''); // Remove control chars
|
|
1100
|
-
}
|
|
1101
|
-
|
|
1102
|
-
clear(): void {
|
|
1103
|
-
this.buffer = '';
|
|
1104
|
-
this.processingQueue = [];
|
|
1105
|
-
this.isProcessing = false;
|
|
1106
|
-
}
|
|
1107
|
-
|
|
1108
|
-
getBufferSize(): number {
|
|
1109
|
-
return this.buffer.length;
|
|
1110
|
-
}
|
|
1111
|
-
}
|
|
1112
|
-
```
|
|
1113
|
-
|
|
1114
|
-
**Phase 4 Testing Strategy:**
|
|
1115
|
-
- Streaming content insertion tests
|
|
1116
|
-
- Performance testing with large streams
|
|
1117
|
-
- Error handling and recovery tests
|
|
1118
|
-
- Buffer overflow and backpressure tests
|
|
1119
|
-
- Integration with editor state management
|
|
1120
|
-
|
|
1121
|
-
### Phase 5: Collaboration Features (Week 6)
|
|
1122
|
-
|
|
1123
|
-
**Objective**: Implement real-time collaboration with cursors and presence.
|
|
1124
|
-
|
|
1125
|
-
**Implementation Prompts:**
|
|
1126
|
-
|
|
1127
|
-
**5.5.1 Create Collaboration Extension**
|
|
1128
|
-
```typescript
|
|
1129
|
-
// Prompt: Create CollaborationExtension that:
|
|
1130
|
-
// - Integrates with Yjs for operational transformation
|
|
1131
|
-
// - Implements real-time cursor tracking and presence
|
|
1132
|
-
// - Handles conflict resolution for simultaneous edits
|
|
1133
|
-
// - Provides user identification and avatar display
|
|
1134
|
-
// - Implements proper connection management and reconnection
|
|
1135
|
-
// - Maintains document consistency across all clients
|
|
1136
|
-
|
|
1137
|
-
import { Collaboration } from '@tiptap/extension-collaboration';
|
|
1138
|
-
import { CollaborationCursor } from '@tiptap/extension-collaboration-cursor';
|
|
1139
|
-
import * as Y from 'yjs';
|
|
1140
|
-
import { WebrtcProvider } from 'y-webrtc';
|
|
1141
|
-
|
|
1142
|
-
const CollaborationExtension = Extension.create({
|
|
1143
|
-
name: 'brainfish-collaboration',
|
|
1144
|
-
|
|
1145
|
-
addExtensions() {
|
|
1146
|
-
const ydoc = new Y.Doc();
|
|
1147
|
-
const provider = new WebrtcProvider(this.options.roomId, ydoc, {
|
|
1148
|
-
signaling: this.options.signalingServer ? [this.options.signalingServer] : undefined,
|
|
1149
|
-
});
|
|
1150
|
-
|
|
1151
|
-
return [
|
|
1152
|
-
Collaboration.configure({
|
|
1153
|
-
document: ydoc,
|
|
1154
|
-
}),
|
|
1155
|
-
CollaborationCursor.configure({
|
|
1156
|
-
provider,
|
|
1157
|
-
user: this.options.currentUser,
|
|
1158
|
-
render: (user) => this.renderCollaboratorCursor(user),
|
|
1159
|
-
}),
|
|
1160
|
-
];
|
|
1161
|
-
},
|
|
1162
|
-
|
|
1163
|
-
addCommands() {
|
|
1164
|
-
return {
|
|
1165
|
-
updateCursor: (position: number, selection?: { from: number; to: number }) => () => {
|
|
1166
|
-
this.options.onCollaboratorCursor?.(this.options.currentUser.id, position);
|
|
1167
|
-
return true;
|
|
1168
|
-
},
|
|
1169
|
-
|
|
1170
|
-
sendPresence: (data: any) => () => {
|
|
1171
|
-
if (this.provider) {
|
|
1172
|
-
this.provider.awareness.setLocalStateField('user', {
|
|
1173
|
-
...this.options.currentUser,
|
|
1174
|
-
...data,
|
|
1175
|
-
});
|
|
1176
|
-
}
|
|
1177
|
-
return true;
|
|
1178
|
-
},
|
|
1179
|
-
};
|
|
1180
|
-
},
|
|
1181
|
-
|
|
1182
|
-
onCreate() {
|
|
1183
|
-
if (this.provider) {
|
|
1184
|
-
this.provider.on('status', (event) => {
|
|
1185
|
-
this.options.onConnectionStatus?.(event.status);
|
|
1186
|
-
});
|
|
1187
|
-
|
|
1188
|
-
this.provider.awareness.on('change', () => {
|
|
1189
|
-
const users = Array.from(this.provider.awareness.getStates().entries())
|
|
1190
|
-
.map(([clientId, state]) => ({ ...state.user, clientId }))
|
|
1191
|
-
.filter((user) => user.clientId !== this.provider.doc.clientID);
|
|
1192
|
-
|
|
1193
|
-
this.options.onCollaboratorsChange?.(users);
|
|
1194
|
-
});
|
|
1195
|
-
}
|
|
1196
|
-
},
|
|
1197
|
-
|
|
1198
|
-
onDestroy() {
|
|
1199
|
-
if (this.provider) {
|
|
1200
|
-
this.provider.destroy();
|
|
1201
|
-
}
|
|
1202
|
-
},
|
|
1203
|
-
|
|
1204
|
-
private renderCollaboratorCursor(user: CollaboratorUser) {
|
|
1205
|
-
const cursor = document.createElement('span');
|
|
1206
|
-
cursor.classList.add('collaboration-cursor');
|
|
1207
|
-
cursor.style.borderColor = user.color || '#3b82f6';
|
|
1208
|
-
cursor.setAttribute('data-user', user.name);
|
|
1209
|
-
|
|
1210
|
-
return cursor;
|
|
1211
|
-
},
|
|
1212
|
-
});
|
|
1213
|
-
```
|
|
1214
|
-
|
|
1215
|
-
**5.5.2 Implement Collaborator Components**
|
|
1216
|
-
```typescript
|
|
1217
|
-
// Prompt: Create CollaboratorList component that:
|
|
1218
|
-
// - Shows all active collaborators with avatars and names
|
|
1219
|
-
// - Displays real-time presence indicators (typing, idle, away)
|
|
1220
|
-
// - Implements user color coding for cursor identification
|
|
1221
|
-
// - Shows connection status for each collaborator
|
|
1222
|
-
// - Uses existing UI components (Avatar, Badge, Tooltip)
|
|
1223
|
-
// - Handles user interactions like viewing collaborator location
|
|
1224
|
-
|
|
1225
|
-
const CollaboratorList: React.FC<CollaboratorListProps> = ({
|
|
1226
|
-
collaborators,
|
|
1227
|
-
currentUser,
|
|
1228
|
-
onUserClick
|
|
1229
|
-
}) => {
|
|
1230
|
-
const sortedCollaborators = useMemo(() => {
|
|
1231
|
-
return collaborators.sort((a, b) => {
|
|
1232
|
-
// Sort by activity status, then by name
|
|
1233
|
-
if (a.isActive !== b.isActive) {
|
|
1234
|
-
return a.isActive ? -1 : 1;
|
|
1235
|
-
}
|
|
1236
|
-
return a.name.localeCompare(b.name);
|
|
1237
|
-
});
|
|
1238
|
-
}, [collaborators]);
|
|
1239
|
-
|
|
1240
|
-
return (
|
|
1241
|
-
<div className="flex items-center gap-2 px-4 py-2 border-b">
|
|
1242
|
-
<span className="text-sm font-medium text-gray-700">
|
|
1243
|
-
Collaborators ({collaborators.length})
|
|
1244
|
-
</span>
|
|
1245
|
-
|
|
1246
|
-
<div className="flex items-center gap-1 ml-2">
|
|
1247
|
-
{sortedCollaborators.map(collaborator => (
|
|
1248
|
-
<Tooltip key={collaborator.id} content={collaborator.name}>
|
|
1249
|
-
<div className="relative">
|
|
1250
|
-
<Avatar
|
|
1251
|
-
src={collaborator.avatar}
|
|
1252
|
-
alt={collaborator.name}
|
|
1253
|
-
size="sm"
|
|
1254
|
-
className="cursor-pointer hover:scale-110 transition-transform"
|
|
1255
|
-
style={{ borderColor: collaborator.color }}
|
|
1256
|
-
onClick={() => onUserClick?.(collaborator)}
|
|
1257
|
-
/>
|
|
1258
|
-
|
|
1259
|
-
{/* Activity indicator */}
|
|
1260
|
-
<div
|
|
1261
|
-
className={cn(
|
|
1262
|
-
"absolute -bottom-1 -right-1 h-3 w-3 rounded-full border-2 border-white",
|
|
1263
|
-
collaborator.isActive ? "bg-green-400" : "bg-gray-400"
|
|
1264
|
-
)}
|
|
1265
|
-
/>
|
|
1266
|
-
|
|
1267
|
-
{/* Typing indicator */}
|
|
1268
|
-
{collaborator.isTyping && (
|
|
1269
|
-
<div className="absolute -top-1 -right-1 h-2 w-2 bg-blue-400 rounded-full animate-pulse" />
|
|
1270
|
-
)}
|
|
1271
|
-
</div>
|
|
1272
|
-
</Tooltip>
|
|
1273
|
-
))}
|
|
1274
|
-
</div>
|
|
1275
|
-
</div>
|
|
1276
|
-
);
|
|
1277
|
-
};
|
|
1278
|
-
```
|
|
1279
|
-
|
|
1280
|
-
**5.5.3 Build Cursor Management System**
|
|
1281
|
-
```typescript
|
|
1282
|
-
// Prompt: Create CollaboratorCursor component that:
|
|
1283
|
-
// - Renders real-time cursor positions for all collaborators
|
|
1284
|
-
// - Shows user names and colors for cursor identification
|
|
1285
|
-
// - Implements smooth cursor animations and transitions
|
|
1286
|
-
// - Handles cursor visibility based on viewport
|
|
1287
|
-
// - Provides selection range indicators for collaborators
|
|
1288
|
-
// - Integrates with TipTap collaboration cursor system
|
|
1289
|
-
|
|
1290
|
-
const CollaboratorCursor: React.FC<CollaboratorCursorProps> = ({
|
|
1291
|
-
user,
|
|
1292
|
-
position,
|
|
1293
|
-
selection
|
|
1294
|
-
}) => {
|
|
1295
|
-
const [isVisible, setIsVisible] = useState(true);
|
|
1296
|
-
|
|
1297
|
-
useEffect(() => {
|
|
1298
|
-
// Auto-hide cursor after inactivity
|
|
1299
|
-
const timer = setTimeout(() => setIsVisible(false), 5000);
|
|
1300
|
-
return () => clearTimeout(timer);
|
|
1301
|
-
}, [position]);
|
|
1302
|
-
|
|
1303
|
-
const cursorStyle = useMemo(() => ({
|
|
1304
|
-
borderLeftColor: user.color,
|
|
1305
|
-
transform: `translateY(${position.top}px) translateX(${position.left}px)`,
|
|
1306
|
-
}), [user.color, position]);
|
|
1307
|
-
|
|
1308
|
-
return (
|
|
1309
|
-
<motion.div
|
|
1310
|
-
className={cn(
|
|
1311
|
-
"absolute pointer-events-none z-50",
|
|
1312
|
-
"border-l-2 h-5",
|
|
1313
|
-
{ "opacity-0": !isVisible }
|
|
1314
|
-
)}
|
|
1315
|
-
style={cursorStyle}
|
|
1316
|
-
initial={{ opacity: 0, scale: 0.8 }}
|
|
1317
|
-
animate={{ opacity: isVisible ? 1 : 0, scale: 1 }}
|
|
1318
|
-
transition={{ duration: 0.2 }}
|
|
1319
|
-
>
|
|
1320
|
-
{/* User label */}
|
|
1321
|
-
<div
|
|
1322
|
-
className="absolute -top-6 left-0 px-2 py-1 text-xs text-white rounded whitespace-nowrap"
|
|
1323
|
-
style={{ backgroundColor: user.color }}
|
|
1324
|
-
>
|
|
1325
|
-
{user.name}
|
|
1326
|
-
</div>
|
|
1327
|
-
|
|
1328
|
-
{/* Selection range */}
|
|
1329
|
-
{selection && (
|
|
1330
|
-
<div
|
|
1331
|
-
className="absolute opacity-20 rounded"
|
|
1332
|
-
style={{
|
|
1333
|
-
backgroundColor: user.color,
|
|
1334
|
-
top: 0,
|
|
1335
|
-
left: 0,
|
|
1336
|
-
width: selection.width,
|
|
1337
|
-
height: selection.height,
|
|
1338
|
-
}}
|
|
1339
|
-
/>
|
|
1340
|
-
)}
|
|
1341
|
-
</motion.div>
|
|
1342
|
-
);
|
|
1343
|
-
};
|
|
1344
|
-
```
|
|
1345
|
-
|
|
1346
|
-
**5.5.4 Implement Presence Management**
|
|
1347
|
-
```typescript
|
|
1348
|
-
// Prompt: Create useCollaborationPresence hook that:
|
|
1349
|
-
// - Manages user presence state (active, idle, away)
|
|
1350
|
-
// - Implements typing indicators with debounced updates
|
|
1351
|
-
// - Handles connection status and reconnection logic
|
|
1352
|
-
// - Provides methods for sending presence updates
|
|
1353
|
-
// - Manages awareness state for all collaborators
|
|
1354
|
-
// - Implements proper cleanup and memory management
|
|
1355
|
-
|
|
1356
|
-
const useCollaborationPresence = (
|
|
1357
|
-
provider: WebrtcProvider | null,
|
|
1358
|
-
currentUser: User,
|
|
1359
|
-
editor: Editor | null
|
|
1360
|
-
) => {
|
|
1361
|
-
const [collaborators, setCollaborators] = useState<Collaborator[]>([]);
|
|
1362
|
-
const [connectionStatus, setConnectionStatus] = useState<'connecting' | 'connected' | 'disconnected'>('disconnected');
|
|
1363
|
-
const [isTyping, setIsTyping] = useState(false);
|
|
1364
|
-
|
|
1365
|
-
const typingTimeoutRef = useRef<NodeJS.Timeout>();
|
|
1366
|
-
|
|
1367
|
-
const updatePresence = useCallback((updates: Partial<Collaborator>) => {
|
|
1368
|
-
if (!provider) return;
|
|
1369
|
-
|
|
1370
|
-
const currentState = provider.awareness.getLocalState();
|
|
1371
|
-
provider.awareness.setLocalStateField('user', {
|
|
1372
|
-
...currentUser,
|
|
1373
|
-
...currentState.user,
|
|
1374
|
-
...updates,
|
|
1375
|
-
lastSeen: Date.now(),
|
|
1376
|
-
});
|
|
1377
|
-
}, [provider, currentUser]);
|
|
1378
|
-
|
|
1379
|
-
const startTyping = useCallback(() => {
|
|
1380
|
-
if (isTyping) return;
|
|
1381
|
-
|
|
1382
|
-
setIsTyping(true);
|
|
1383
|
-
updatePresence({ isTyping: true });
|
|
1384
|
-
|
|
1385
|
-
// Clear existing timeout
|
|
1386
|
-
if (typingTimeoutRef.current) {
|
|
1387
|
-
clearTimeout(typingTimeoutRef.current);
|
|
1388
|
-
}
|
|
1389
|
-
|
|
1390
|
-
// Stop typing after 3 seconds of inactivity
|
|
1391
|
-
typingTimeoutRef.current = setTimeout(() => {
|
|
1392
|
-
setIsTyping(false);
|
|
1393
|
-
updatePresence({ isTyping: false });
|
|
1394
|
-
}, 3000);
|
|
1395
|
-
}, [isTyping, updatePresence]);
|
|
1396
|
-
|
|
1397
|
-
const stopTyping = useCallback(() => {
|
|
1398
|
-
if (typingTimeoutRef.current) {
|
|
1399
|
-
clearTimeout(typingTimeoutRef.current);
|
|
1400
|
-
}
|
|
1401
|
-
setIsTyping(false);
|
|
1402
|
-
updatePresence({ isTyping: false });
|
|
1403
|
-
}, [updatePresence]);
|
|
1404
|
-
|
|
1405
|
-
// Listen to editor changes for typing indicators
|
|
1406
|
-
useEffect(() => {
|
|
1407
|
-
if (!editor) return;
|
|
1408
|
-
|
|
1409
|
-
const handleUpdate = () => startTyping();
|
|
1410
|
-
editor.on('update', handleUpdate);
|
|
1411
|
-
|
|
1412
|
-
return () => editor.off('update', handleUpdate);
|
|
1413
|
-
}, [editor, startTyping]);
|
|
1414
|
-
|
|
1415
|
-
// Listen to provider awareness changes
|
|
1416
|
-
useEffect(() => {
|
|
1417
|
-
if (!provider) return;
|
|
1418
|
-
|
|
1419
|
-
const handleAwarenessChange = () => {
|
|
1420
|
-
const states = Array.from(provider.awareness.getStates().entries());
|
|
1421
|
-
const users: Collaborator[] = states
|
|
1422
|
-
.filter(([clientId]) => clientId !== provider.doc.clientID)
|
|
1423
|
-
.map(([clientId, state]) => ({
|
|
1424
|
-
id: state.user?.id || clientId.toString(),
|
|
1425
|
-
name: state.user?.name || 'Anonymous',
|
|
1426
|
-
avatar: state.user?.avatar,
|
|
1427
|
-
color: state.user?.color || generateUserColor(clientId),
|
|
1428
|
-
isActive: Date.now() - (state.user?.lastSeen || 0) < 30000,
|
|
1429
|
-
isTyping: state.user?.isTyping || false,
|
|
1430
|
-
cursor: state.cursor,
|
|
1431
|
-
}));
|
|
1432
|
-
|
|
1433
|
-
setCollaborators(users);
|
|
1434
|
-
};
|
|
1435
|
-
|
|
1436
|
-
const handleStatusChange = (event: { status: string }) => {
|
|
1437
|
-
setConnectionStatus(event.status as any);
|
|
1438
|
-
};
|
|
1439
|
-
|
|
1440
|
-
provider.awareness.on('change', handleAwarenessChange);
|
|
1441
|
-
provider.on('status', handleStatusChange);
|
|
1442
|
-
|
|
1443
|
-
return () => {
|
|
1444
|
-
provider.awareness.off('change', handleAwarenessChange);
|
|
1445
|
-
provider.off('status', handleStatusChange);
|
|
1446
|
-
};
|
|
1447
|
-
}, [provider]);
|
|
1448
|
-
|
|
1449
|
-
// Cleanup on unmount
|
|
1450
|
-
useEffect(() => {
|
|
1451
|
-
return () => {
|
|
1452
|
-
if (typingTimeoutRef.current) {
|
|
1453
|
-
clearTimeout(typingTimeoutRef.current);
|
|
1454
|
-
}
|
|
1455
|
-
stopTyping();
|
|
1456
|
-
};
|
|
1457
|
-
}, [stopTyping]);
|
|
1458
|
-
|
|
1459
|
-
return {
|
|
1460
|
-
collaborators,
|
|
1461
|
-
connectionStatus,
|
|
1462
|
-
isTyping,
|
|
1463
|
-
updatePresence,
|
|
1464
|
-
startTyping,
|
|
1465
|
-
stopTyping,
|
|
1466
|
-
};
|
|
1467
|
-
};
|
|
1468
|
-
```
|
|
1469
|
-
|
|
1470
|
-
**Phase 5 Testing Strategy:**
|
|
1471
|
-
- Real-time collaboration functionality
|
|
1472
|
-
- Cursor synchronization across clients
|
|
1473
|
-
- Conflict resolution testing
|
|
1474
|
-
- Connection handling and recovery
|
|
1475
|
-
- Presence and typing indicators
|
|
1476
|
-
|
|
1477
|
-
### Phase 6: Integration and Polish (Week 7-8)
|
|
1478
|
-
|
|
1479
|
-
**Objective**: Complete integration, testing, and production readiness.
|
|
1480
|
-
|
|
1481
|
-
**Implementation Prompts:**
|
|
1482
|
-
|
|
1483
|
-
**6.6.1 FormattedMessage Migration Strategy**
|
|
1484
|
-
```typescript
|
|
1485
|
-
// Prompt: Create migration utilities that:
|
|
1486
|
-
// - Provide seamless replacement of FormattedMessage with MarkdownEditorViewer
|
|
1487
|
-
// - Maintain backward compatibility with existing props and behavior
|
|
1488
|
-
// - Implement feature flags for gradual rollout
|
|
1489
|
-
// - Provide fallback mechanisms for unsupported features
|
|
1490
|
-
// - Handle existing content migration and formatting preservation
|
|
1491
|
-
// - Create comprehensive migration guide and documentation
|
|
1492
|
-
|
|
1493
|
-
const FormattedMessageMigration = {
|
|
1494
|
-
// Convert FormattedMessage props to MarkdownEditorViewer props
|
|
1495
|
-
migrateProps: (formattedMessageProps: FormattedMessageProps): MarkdownEditorViewerProps => {
|
|
1496
|
-
return {
|
|
1497
|
-
content: formattedMessageProps.message.content,
|
|
1498
|
-
mode: 'readonly', // FormattedMessage is always readonly
|
|
1499
|
-
isStreaming: formattedMessageProps.isStreaming,
|
|
1500
|
-
redirectRules: formattedMessageProps.redirectRules,
|
|
1501
|
-
contentTypeEndpoint: formattedMessageProps.contentTypeEndpoint,
|
|
1502
|
-
// ... map other relevant props
|
|
1503
|
-
};
|
|
1504
|
-
},
|
|
1505
|
-
|
|
1506
|
-
// Wrapper component for gradual migration
|
|
1507
|
-
FormattedMessageCompatWrapper: React.FC<FormattedMessageProps> = (props) => {
|
|
1508
|
-
const [useNewEditor, setUseNewEditor] = useState(() =>
|
|
1509
|
-
// Check feature flag or user preference
|
|
1510
|
-
window.BRAINFISH_USE_NEW_EDITOR === true
|
|
1511
|
-
);
|
|
1512
|
-
|
|
1513
|
-
if (useNewEditor) {
|
|
1514
|
-
const migratedProps = FormattedMessageMigration.migrateProps(props);
|
|
1515
|
-
return <MarkdownEditorViewer {...migratedProps} />;
|
|
1516
|
-
}
|
|
1517
|
-
|
|
1518
|
-
// Fallback to original FormattedMessage
|
|
1519
|
-
return <FormattedMessage {...props} />;
|
|
1520
|
-
},
|
|
1521
|
-
|
|
1522
|
-
// Validate content compatibility
|
|
1523
|
-
validateContent: (content: string): { isCompatible: boolean; issues: string[] } => {
|
|
1524
|
-
const issues: string[] = [];
|
|
1525
|
-
|
|
1526
|
-
// Check for unsupported patterns
|
|
1527
|
-
if (content.includes('<!-- unsupported -->')) {
|
|
1528
|
-
issues.push('Content contains unsupported HTML comments');
|
|
1529
|
-
}
|
|
1530
|
-
|
|
1531
|
-
// Check for complex nested structures
|
|
1532
|
-
const complexTableRegex = /<table[^>]*>.*?<table/s;
|
|
1533
|
-
if (complexTableRegex.test(content)) {
|
|
1534
|
-
issues.push('Nested tables are not fully supported');
|
|
1535
|
-
}
|
|
1536
|
-
|
|
1537
|
-
return {
|
|
1538
|
-
isCompatible: issues.length === 0,
|
|
1539
|
-
issues,
|
|
1540
|
-
};
|
|
1541
|
-
},
|
|
1542
|
-
};
|
|
1543
|
-
```
|
|
1544
|
-
|
|
1545
|
-
**6.6.2 Comprehensive Error Handling**
|
|
1546
|
-
```typescript
|
|
1547
|
-
// Prompt: Create robust error handling system that:
|
|
1548
|
-
// - Implements global error boundaries for graceful degradation
|
|
1549
|
-
// - Provides detailed error reporting and logging
|
|
1550
|
-
// - Handles network failures and API errors gracefully
|
|
1551
|
-
// - Implements retry mechanisms for recoverable errors
|
|
1552
|
-
// - Provides user-friendly error messages and recovery actions
|
|
1553
|
-
// - Integrates with existing error tracking systems
|
|
1554
|
-
|
|
1555
|
-
class EditorErrorBoundary extends React.Component<
|
|
1556
|
-
{ children: React.ReactNode; fallback?: React.ComponentType<any> },
|
|
1557
|
-
{ hasError: boolean; error: Error | null }
|
|
1558
|
-
> {
|
|
1559
|
-
constructor(props: any) {
|
|
1560
|
-
super(props);
|
|
1561
|
-
this.state = { hasError: false, error: null };
|
|
1562
|
-
}
|
|
1563
|
-
|
|
1564
|
-
static getDerivedStateFromError(error: Error) {
|
|
1565
|
-
return { hasError: true, error };
|
|
1566
|
-
}
|
|
1567
|
-
|
|
1568
|
-
componentDidCatch(error: Error, errorInfo: React.ErrorInfo) {
|
|
1569
|
-
console.error('MarkdownEditorViewer Error:', error, errorInfo);
|
|
1570
|
-
|
|
1571
|
-
// Report to error tracking service
|
|
1572
|
-
if (window.BRAINFISH_ERROR_TRACKING) {
|
|
1573
|
-
window.BRAINFISH_ERROR_TRACKING.reportError(error, {
|
|
1574
|
-
component: 'MarkdownEditorViewer',
|
|
1575
|
-
errorInfo,
|
|
1576
|
-
});
|
|
1577
|
-
}
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
render() {
|
|
1581
|
-
if (this.state.hasError) {
|
|
1582
|
-
const FallbackComponent = this.props.fallback || DefaultErrorFallback;
|
|
1583
|
-
return <FallbackComponent error={this.state.error} />;
|
|
1584
|
-
}
|
|
1585
|
-
|
|
1586
|
-
return this.props.children;
|
|
1587
|
-
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
const DefaultErrorFallback: React.FC<{ error: Error | null }> = ({ error }) => {
|
|
1591
|
-
return (
|
|
1592
|
-
<div className="p-4 border border-red-200 rounded-md bg-red-50">
|
|
1593
|
-
<h3 className="text-red-800 font-semibold mb-2">Editor Error</h3>
|
|
1594
|
-
<p className="text-red-700 text-sm mb-3">
|
|
1595
|
-
The editor encountered an error and couldn't load properly. You can try refreshing the page or switching to the basic editor.
|
|
1596
|
-
</p>
|
|
1597
|
-
<div className="flex gap-2">
|
|
1598
|
-
<Button size="sm" onClick={() => window.location.reload()}>
|
|
1599
|
-
Refresh Page
|
|
1600
|
-
</Button>
|
|
1601
|
-
<Button size="sm" variant="outline" onClick={() => {/* Switch to basic editor */}}>
|
|
1602
|
-
Use Basic Editor
|
|
1603
|
-
</Button>
|
|
1604
|
-
</div>
|
|
1605
|
-
{process.env.NODE_ENV === 'development' && error && (
|
|
1606
|
-
<details className="mt-3">
|
|
1607
|
-
<summary className="text-xs cursor-pointer">Error Details</summary>
|
|
1608
|
-
<pre className="text-xs mt-1 overflow-auto">{error.stack}</pre>
|
|
1609
|
-
</details>
|
|
1610
|
-
)}
|
|
1611
|
-
</div>
|
|
1612
|
-
);
|
|
1613
|
-
};
|
|
1614
|
-
```
|
|
1615
|
-
|
|
1616
|
-
**6.6.3 Performance Optimization**
|
|
1617
|
-
```typescript
|
|
1618
|
-
// Prompt: Implement performance optimizations that:
|
|
1619
|
-
// - Use React.memo and useMemo for expensive operations
|
|
1620
|
-
// - Implement virtual scrolling for large documents
|
|
1621
|
-
// - Optimize bundle size with dynamic imports
|
|
1622
|
-
// - Implement debouncing for frequent operations
|
|
1623
|
-
// - Use web workers for heavy computations
|
|
1624
|
-
// - Implement efficient re-rendering strategies
|
|
1625
|
-
|
|
1626
|
-
// Memoized editor component
|
|
1627
|
-
const MemoizedEditorContent = React.memo(
|
|
1628
|
-
EditorContent,
|
|
1629
|
-
(prevProps, nextProps) => {
|
|
1630
|
-
return (
|
|
1631
|
-
prevProps.content === nextProps.content &&
|
|
1632
|
-
prevProps.mode === nextProps.mode &&
|
|
1633
|
-
prevProps.isStreaming === nextProps.isStreaming &&
|
|
1634
|
-
shallowEqual(prevProps.suggestions, nextProps.suggestions)
|
|
1635
|
-
);
|
|
1636
|
-
}
|
|
1637
|
-
);
|
|
1638
|
-
|
|
1639
|
-
// Virtual scrolling for large suggestion lists
|
|
1640
|
-
const VirtualizedSuggestionList: React.FC<SuggestionListProps> = ({ suggestions }) => {
|
|
1641
|
-
const [containerRef, setContainerRef] = useState<HTMLDivElement | null>(null);
|
|
1642
|
-
const [scrollTop, setScrollTop] = useState(0);
|
|
1643
|
-
|
|
1644
|
-
const itemHeight = 60;
|
|
1645
|
-
const containerHeight = 400;
|
|
1646
|
-
const overscan = 5;
|
|
1647
|
-
|
|
1648
|
-
const visibleItems = useMemo(() => {
|
|
1649
|
-
if (!containerRef) return suggestions.slice(0, 20);
|
|
1650
|
-
|
|
1651
|
-
const startIndex = Math.floor(scrollTop / itemHeight);
|
|
1652
|
-
const endIndex = Math.min(
|
|
1653
|
-
suggestions.length - 1,
|
|
1654
|
-
startIndex + Math.ceil(containerHeight / itemHeight) + overscan
|
|
1655
|
-
);
|
|
1656
|
-
|
|
1657
|
-
return suggestions.slice(Math.max(0, startIndex - overscan), endIndex + 1);
|
|
1658
|
-
}, [suggestions, scrollTop, containerRef]);
|
|
1659
|
-
|
|
1660
|
-
return (
|
|
1661
|
-
<div
|
|
1662
|
-
ref={setContainerRef}
|
|
1663
|
-
className="h-96 overflow-auto"
|
|
1664
|
-
onScroll={(e) => setScrollTop(e.currentTarget.scrollTop)}
|
|
1665
|
-
>
|
|
1666
|
-
<div style={{ height: suggestions.length * itemHeight }}>
|
|
1667
|
-
<div style={{ transform: `translateY(${Math.floor(scrollTop / itemHeight) * itemHeight}px)` }}>
|
|
1668
|
-
{visibleItems.map((suggestion, index) => (
|
|
1669
|
-
<SuggestionItem key={suggestion.id} suggestion={suggestion} />
|
|
1670
|
-
))}
|
|
1671
|
-
</div>
|
|
1672
|
-
</div>
|
|
1673
|
-
</div>
|
|
1674
|
-
);
|
|
1675
|
-
};
|
|
1676
|
-
|
|
1677
|
-
// Web worker for markdown processing
|
|
1678
|
-
const useMarkdownWorker = () => {
|
|
1679
|
-
const workerRef = useRef<Worker>();
|
|
1680
|
-
|
|
1681
|
-
useEffect(() => {
|
|
1682
|
-
workerRef.current = new Worker('/markdown-processor.worker.js');
|
|
1683
|
-
return () => workerRef.current?.terminate();
|
|
1684
|
-
}, []);
|
|
1685
|
-
|
|
1686
|
-
const processMarkdown = useCallback((markdown: string): Promise<TipTapDocument> => {
|
|
1687
|
-
return new Promise((resolve, reject) => {
|
|
1688
|
-
if (!workerRef.current) {
|
|
1689
|
-
reject(new Error('Worker not available'));
|
|
1690
|
-
return;
|
|
1691
|
-
}
|
|
1692
|
-
|
|
1693
|
-
const id = Math.random().toString(36);
|
|
1694
|
-
|
|
1695
|
-
const handleMessage = (e: MessageEvent) => {
|
|
1696
|
-
if (e.data.id === id) {
|
|
1697
|
-
workerRef.current?.removeEventListener('message', handleMessage);
|
|
1698
|
-
if (e.data.error) {
|
|
1699
|
-
reject(new Error(e.data.error));
|
|
1700
|
-
} else {
|
|
1701
|
-
resolve(e.data.result);
|
|
1702
|
-
}
|
|
1703
|
-
}
|
|
1704
|
-
};
|
|
1705
|
-
|
|
1706
|
-
workerRef.current.addEventListener('message', handleMessage);
|
|
1707
|
-
workerRef.current.postMessage({ id, markdown });
|
|
1708
|
-
});
|
|
1709
|
-
}, []);
|
|
1710
|
-
|
|
1711
|
-
return { processMarkdown };
|
|
1712
|
-
};
|
|
1713
|
-
```
|
|
1714
|
-
|
|
1715
|
-
**6.6.4 Accessibility Implementation**
|
|
1716
|
-
```typescript
|
|
1717
|
-
// Prompt: Implement comprehensive accessibility features that:
|
|
1718
|
-
// - Provide proper ARIA labels and roles for all interactive elements
|
|
1719
|
-
// - Implement keyboard navigation for all functionality
|
|
1720
|
-
// - Support screen readers with descriptive text and live regions
|
|
1721
|
-
// - Maintain focus management during mode switches and updates
|
|
1722
|
-
// - Provide high contrast mode support
|
|
1723
|
-
// - Implement proper color contrast ratios throughout
|
|
1724
|
-
|
|
1725
|
-
const AccessibilityProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
|
1726
|
-
const [announcements, setAnnouncements] = useState<string[]>([]);
|
|
1727
|
-
const [focusedElement, setFocusedElement] = useState<string | null>(null);
|
|
1728
|
-
|
|
1729
|
-
const announce = useCallback((message: string) => {
|
|
1730
|
-
setAnnouncements(prev => [...prev.slice(-2), message]);
|
|
1731
|
-
}, []);
|
|
1732
|
-
|
|
1733
|
-
const manageFocus = useCallback((elementId: string) => {
|
|
1734
|
-
setFocusedElement(elementId);
|
|
1735
|
-
setTimeout(() => {
|
|
1736
|
-
const element = document.getElementById(elementId);
|
|
1737
|
-
element?.focus();
|
|
1738
|
-
}, 0);
|
|
1739
|
-
}, []);
|
|
1740
|
-
|
|
1741
|
-
const contextValue = useMemo(() => ({
|
|
1742
|
-
announce,
|
|
1743
|
-
manageFocus,
|
|
1744
|
-
focusedElement,
|
|
1745
|
-
}), [announce, manageFocus, focusedElement]);
|
|
1746
|
-
|
|
1747
|
-
return (
|
|
1748
|
-
<AccessibilityContext.Provider value={contextValue}>
|
|
1749
|
-
{children}
|
|
1750
|
-
|
|
1751
|
-
{/* Live region for announcements */}
|
|
1752
|
-
<div
|
|
1753
|
-
className="sr-only"
|
|
1754
|
-
role="status"
|
|
1755
|
-
aria-live="polite"
|
|
1756
|
-
aria-atomic="true"
|
|
1757
|
-
>
|
|
1758
|
-
{announcements[announcements.length - 1]}
|
|
1759
|
-
</div>
|
|
1760
|
-
|
|
1761
|
-
{/* Skip links for keyboard navigation */}
|
|
1762
|
-
<div className="sr-only focus:not-sr-only focus:absolute focus:top-0 focus:left-0 z-50">
|
|
1763
|
-
<a
|
|
1764
|
-
href="#editor-content"
|
|
1765
|
-
className="bg-blue-600 text-white p-2 rounded"
|
|
1766
|
-
>
|
|
1767
|
-
Skip to editor
|
|
1768
|
-
</a>
|
|
1769
|
-
<a
|
|
1770
|
-
href="#suggestions-panel"
|
|
1771
|
-
className="bg-blue-600 text-white p-2 rounded ml-2"
|
|
1772
|
-
>
|
|
1773
|
-
Skip to suggestions
|
|
1774
|
-
</a>
|
|
1775
|
-
</div>
|
|
1776
|
-
</AccessibilityContext.Provider>
|
|
1777
|
-
);
|
|
1778
|
-
};
|
|
1779
|
-
|
|
1780
|
-
// Keyboard shortcuts with accessibility
|
|
1781
|
-
const useAccessibleKeyboardShortcuts = (editor: Editor | null) => {
|
|
1782
|
-
useEffect(() => {
|
|
1783
|
-
const handleKeyDown = (event: KeyboardEvent) => {
|
|
1784
|
-
// Handle accessibility-specific shortcuts
|
|
1785
|
-
if (event.altKey && event.key === '1') {
|
|
1786
|
-
// Focus main editor
|
|
1787
|
-
event.preventDefault();
|
|
1788
|
-
document.getElementById('editor-content')?.focus();
|
|
1789
|
-
announce('Focused main editor');
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
if (event.altKey && event.key === '2') {
|
|
1793
|
-
// Focus suggestions panel
|
|
1794
|
-
event.preventDefault();
|
|
1795
|
-
document.getElementById('suggestions-panel')?.focus();
|
|
1796
|
-
announce('Focused suggestions panel');
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
if (event.altKey && event.key === '3') {
|
|
1800
|
-
// Focus toolbar
|
|
1801
|
-
event.preventDefault();
|
|
1802
|
-
document.getElementById('editor-toolbar')?.focus();
|
|
1803
|
-
announce('Focused editor toolbar');
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
// Announce current selection or cursor position
|
|
1807
|
-
if (event.key === 'F1' && editor) {
|
|
1808
|
-
event.preventDefault();
|
|
1809
|
-
const { from, to } = editor.state.selection;
|
|
1810
|
-
const text = editor.state.doc.textBetween(from, to);
|
|
1811
|
-
if (text) {
|
|
1812
|
-
announce(`Selected: ${text}`);
|
|
1813
|
-
} else {
|
|
1814
|
-
announce(`Cursor at position ${from}`);
|
|
1815
|
-
}
|
|
1816
|
-
}
|
|
1817
|
-
};
|
|
1818
|
-
|
|
1819
|
-
document.addEventListener('keydown', handleKeyDown);
|
|
1820
|
-
return () => document.removeEventListener('keydown', handleKeyDown);
|
|
1821
|
-
}, [editor]);
|
|
1822
|
-
};
|
|
1823
|
-
```
|
|
1824
|
-
|
|
1825
|
-
**6.6.5 Comprehensive Testing Suite**
|
|
1826
|
-
```typescript
|
|
1827
|
-
// Prompt: Create comprehensive test suite that:
|
|
1828
|
-
// - Tests all major functionality with unit and integration tests
|
|
1829
|
-
// - Implements visual regression testing for UI components
|
|
1830
|
-
// - Tests accessibility features with automated tools
|
|
1831
|
-
// - Includes performance benchmarks and memory leak detection
|
|
1832
|
-
// - Tests cross-browser compatibility
|
|
1833
|
-
// - Implements end-to-end testing scenarios
|
|
1834
|
-
|
|
1835
|
-
// Example test structure
|
|
1836
|
-
describe('MarkdownEditorViewer', () => {
|
|
1837
|
-
describe('Core Functionality', () => {
|
|
1838
|
-
it('should render in edit mode with toolbar', async () => {
|
|
1839
|
-
render(
|
|
1840
|
-
<MarkdownEditorViewer
|
|
1841
|
-
content="# Hello World"
|
|
1842
|
-
mode="edit"
|
|
1843
|
-
onContentChange={jest.fn()}
|
|
1844
|
-
/>
|
|
1845
|
-
);
|
|
1846
|
-
|
|
1847
|
-
expect(screen.getByRole('textbox', { name: /markdown editor/i })).toBeInTheDocument();
|
|
1848
|
-
expect(screen.getByRole('toolbar', { name: /formatting toolbar/i })).toBeInTheDocument();
|
|
1849
|
-
});
|
|
1850
|
-
|
|
1851
|
-
it('should render in readonly mode without toolbar', async () => {
|
|
1852
|
-
render(
|
|
1853
|
-
<MarkdownEditorViewer
|
|
1854
|
-
content="# Hello World"
|
|
1855
|
-
mode="readonly"
|
|
1856
|
-
/>
|
|
1857
|
-
);
|
|
1858
|
-
|
|
1859
|
-
expect(screen.getByRole('article')).toBeInTheDocument();
|
|
1860
|
-
expect(screen.queryByRole('toolbar')).not.toBeInTheDocument();
|
|
1861
|
-
});
|
|
1862
|
-
|
|
1863
|
-
it('should switch modes while preserving content', async () => {
|
|
1864
|
-
const onModeChange = jest.fn();
|
|
1865
|
-
const { rerender } = render(
|
|
1866
|
-
<MarkdownEditorViewer
|
|
1867
|
-
content="# Test Content"
|
|
1868
|
-
mode="edit"
|
|
1869
|
-
onModeChange={onModeChange}
|
|
1870
|
-
/>
|
|
1871
|
-
);
|
|
1872
|
-
|
|
1873
|
-
// Switch to readonly
|
|
1874
|
-
rerender(
|
|
1875
|
-
<MarkdownEditorViewer
|
|
1876
|
-
content="# Test Content"
|
|
1877
|
-
mode="readonly"
|
|
1878
|
-
onModeChange={onModeChange}
|
|
1879
|
-
/>
|
|
1880
|
-
);
|
|
1881
|
-
|
|
1882
|
-
expect(screen.getByText('Test Content')).toBeInTheDocument();
|
|
1883
|
-
});
|
|
1884
|
-
});
|
|
1885
|
-
|
|
1886
|
-
describe('Suggestion System', () => {
|
|
1887
|
-
it('should display suggestions with proper styling', async () => {
|
|
1888
|
-
const suggestions: EditorSuggestion[] = [
|
|
1889
|
-
{
|
|
1890
|
-
id: '1',
|
|
1891
|
-
type: 'insert',
|
|
1892
|
-
position: { from: 0, to: 0 },
|
|
1893
|
-
newText: 'New text',
|
|
1894
|
-
status: 'pending',
|
|
1895
|
-
timestamp: new Date(),
|
|
1896
|
-
},
|
|
1897
|
-
];
|
|
1898
|
-
|
|
1899
|
-
render(
|
|
1900
|
-
<MarkdownEditorViewer
|
|
1901
|
-
content="Original text"
|
|
1902
|
-
mode="edit"
|
|
1903
|
-
suggestions={suggestions}
|
|
1904
|
-
showSuggestions={true}
|
|
1905
|
-
/>
|
|
1906
|
-
);
|
|
1907
|
-
|
|
1908
|
-
expect(screen.getByText('New text')).toHaveClass('suggestion-insert');
|
|
1909
|
-
});
|
|
1910
|
-
|
|
1911
|
-
it('should handle suggestion acceptance', async () => {
|
|
1912
|
-
const onSuggestionAccept = jest.fn();
|
|
1913
|
-
// ... test implementation
|
|
1914
|
-
});
|
|
1915
|
-
});
|
|
1916
|
-
|
|
1917
|
-
describe('Multimedia Integration', () => {
|
|
1918
|
-
it('should render images using ZoomableImage', async () => {
|
|
1919
|
-
render(
|
|
1920
|
-
<MarkdownEditorViewer
|
|
1921
|
-
content=""
|
|
1922
|
-
mode="readonly"
|
|
1923
|
-
/>
|
|
1924
|
-
);
|
|
1925
|
-
|
|
1926
|
-
expect(screen.getByRole('img', { name: /alt text/i })).toBeInTheDocument();
|
|
1927
|
-
});
|
|
1928
|
-
|
|
1929
|
-
it('should handle video embeds', async () => {
|
|
1930
|
-
// ... test implementation
|
|
1931
|
-
});
|
|
1932
|
-
});
|
|
1933
|
-
|
|
1934
|
-
describe('Accessibility', () => {
|
|
1935
|
-
it('should have proper ARIA labels', async () => {
|
|
1936
|
-
render(
|
|
1937
|
-
<MarkdownEditorViewer
|
|
1938
|
-
content="# Test"
|
|
1939
|
-
mode="edit"
|
|
1940
|
-
/>
|
|
1941
|
-
);
|
|
1942
|
-
|
|
1943
|
-
expect(screen.getByRole('textbox')).toHaveAccessibleName(/markdown editor/i);
|
|
1944
|
-
expect(screen.getByRole('toolbar')).toHaveAccessibleName(/formatting toolbar/i);
|
|
1945
|
-
});
|
|
1946
|
-
|
|
1947
|
-
it('should support keyboard navigation', async () => {
|
|
1948
|
-
// ... test implementation
|
|
1949
|
-
});
|
|
1950
|
-
|
|
1951
|
-
it('should announce changes to screen readers', async () => {
|
|
1952
|
-
// ... test implementation
|
|
1953
|
-
});
|
|
1954
|
-
});
|
|
1955
|
-
|
|
1956
|
-
describe('Performance', () => {
|
|
1957
|
-
it('should handle large documents efficiently', async () => {
|
|
1958
|
-
const largeContent = 'a'.repeat(100000);
|
|
1959
|
-
const startTime = performance.now();
|
|
1960
|
-
|
|
1961
|
-
render(
|
|
1962
|
-
<MarkdownEditorViewer
|
|
1963
|
-
content={largeContent}
|
|
1964
|
-
mode="edit"
|
|
1965
|
-
/>
|
|
1966
|
-
);
|
|
1967
|
-
|
|
1968
|
-
const endTime = performance.now();
|
|
1969
|
-
expect(endTime - startTime).toBeLessThan(1000); // Should render in under 1 second
|
|
1970
|
-
});
|
|
1971
|
-
|
|
1972
|
-
it('should not have memory leaks', async () => {
|
|
1973
|
-
// ... memory leak testing
|
|
1974
|
-
});
|
|
1975
|
-
});
|
|
1976
|
-
|
|
1977
|
-
describe('Error Handling', () => {
|
|
1978
|
-
it('should gracefully handle malformed markdown', async () => {
|
|
1979
|
-
const consoleSpy = jest.spyOn(console, 'error').mockImplementation();
|
|
1980
|
-
|
|
1981
|
-
render(
|
|
1982
|
-
<MarkdownEditorViewer
|
|
1983
|
-
content="# Malformed [link"
|
|
1984
|
-
mode="edit"
|
|
1985
|
-
/>
|
|
1986
|
-
);
|
|
1987
|
-
|
|
1988
|
-
expect(screen.getByText(/malformed/i)).toBeInTheDocument();
|
|
1989
|
-
expect(consoleSpy).not.toHaveBeenCalled();
|
|
1990
|
-
|
|
1991
|
-
consoleSpy.mockRestore();
|
|
1992
|
-
});
|
|
1993
|
-
});
|
|
1994
|
-
});
|
|
1995
|
-
```
|
|
1996
|
-
|
|
1997
|
-
## 4. Success Metrics and Acceptance Criteria
|
|
1998
|
-
|
|
1999
|
-
### 4.1 Performance Benchmarks
|
|
2000
|
-
|
|
2001
|
-
**Initialization Performance:**
|
|
2002
|
-
- Editor initialization: < 500ms
|
|
2003
|
-
- Mode switching: < 100ms
|
|
2004
|
-
- Large document loading (1MB): < 2 seconds
|
|
2005
|
-
|
|
2006
|
-
**Runtime Performance:**
|
|
2007
|
-
- Typing lag: < 50ms
|
|
2008
|
-
- Suggestion processing: < 200ms
|
|
2009
|
-
- Streaming content updates: 1000+ chars/second
|
|
2010
|
-
|
|
2011
|
-
**Memory Usage:**
|
|
2012
|
-
- Baseline memory: < 20MB
|
|
2013
|
-
- Large document (1MB): < 50MB
|
|
2014
|
-
- Memory leak tolerance: < 1MB/hour
|
|
2015
|
-
|
|
2016
|
-
### 4.2 Functionality Requirements
|
|
2017
|
-
|
|
2018
|
-
**Core Features:**
|
|
2019
|
-
- ✅ Mode switching (edit/readonly) with content preservation
|
|
2020
|
-
- ✅ Markdown translation (markdown ↔ TipTap JSON)
|
|
2021
|
-
- ✅ Complete multimedia system integration
|
|
2022
|
-
- ✅ Visual suggestion system with keyboard navigation
|
|
2023
|
-
- ✅ Streaming content support
|
|
2024
|
-
- ✅ Basic collaboration features
|
|
2025
|
-
|
|
2026
|
-
**Compatibility:**
|
|
2027
|
-
- ✅ 100% compatibility with existing embed system
|
|
2028
|
-
- ✅ FormattedMessage.tsx feature parity
|
|
2029
|
-
- ✅ Consistent styling with existing components
|
|
2030
|
-
- ✅ Support for all existing markdown features
|
|
2031
|
-
|
|
2032
|
-
### 4.3 Quality Assurance
|
|
2033
|
-
|
|
2034
|
-
**Testing Coverage:**
|
|
2035
|
-
- Unit tests: > 90% coverage
|
|
2036
|
-
- Integration tests: All major workflows
|
|
2037
|
-
- E2E tests: Complete user scenarios
|
|
2038
|
-
- Accessibility: WCAG 2.1 AA compliance
|
|
2039
|
-
|
|
2040
|
-
**Browser Support:**
|
|
2041
|
-
- Chrome 90+, Firefox 88+, Safari 14+, Edge 90+
|
|
2042
|
-
- Mobile responsive design
|
|
2043
|
-
- Touch interface optimization
|
|
2044
|
-
|
|
2045
|
-
**Error Handling:**
|
|
2046
|
-
- Graceful degradation for all error scenarios
|
|
2047
|
-
- Clear user feedback and recovery options
|
|
2048
|
-
- Comprehensive error logging and reporting
|
|
2049
|
-
|
|
2050
|
-
## 5. Implementation Timeline
|
|
2051
|
-
|
|
2052
|
-
**Week 1-2: Foundation** (Phase 1)
|
|
2053
|
-
- Core component structure
|
|
2054
|
-
- TipTap integration
|
|
2055
|
-
- Markdown translation
|
|
2056
|
-
- Basic editor/viewer modes
|
|
2057
|
-
|
|
2058
|
-
**Week 3: Multimedia** (Phase 2)
|
|
2059
|
-
- Embed system integration
|
|
2060
|
-
- File upload functionality
|
|
2061
|
-
- Media rendering components
|
|
2062
|
-
|
|
2063
|
-
**Week 4: Suggestions** (Phase 3)
|
|
2064
|
-
- Visual suggestion system
|
|
2065
|
-
- Keyboard navigation
|
|
2066
|
-
- Accept/reject functionality
|
|
2067
|
-
|
|
2068
|
-
**Week 5: Streaming** (Phase 4)
|
|
2069
|
-
- Real-time content streaming
|
|
2070
|
-
- Performance optimization
|
|
2071
|
-
- Error handling
|
|
2072
|
-
|
|
2073
|
-
**Week 6: Collaboration** (Phase 5)
|
|
2074
|
-
- Real-time collaboration
|
|
2075
|
-
- Cursor synchronization
|
|
2076
|
-
- Presence indicators
|
|
2077
|
-
|
|
2078
|
-
**Week 7-8: Integration & Polish** (Phase 6)
|
|
2079
|
-
- Migration utilities
|
|
2080
|
-
- Comprehensive testing
|
|
2081
|
-
- Performance optimization
|
|
2082
|
-
- Documentation
|
|
2083
|
-
|
|
2084
|
-
## 6. Risk Mitigation
|
|
2085
|
-
|
|
2086
|
-
**Technical Risks:**
|
|
2087
|
-
- **TipTap integration complexity**: Use incremental approach with fallbacks
|
|
2088
|
-
- **Performance issues**: Implement monitoring and optimization from start
|
|
2089
|
-
- **Browser compatibility**: Test early and often across platforms
|
|
2090
|
-
|
|
2091
|
-
**Integration Risks:**
|
|
2092
|
-
- **Breaking changes**: Maintain strict backward compatibility
|
|
2093
|
-
- **Embed system conflicts**: Leverage existing patterns exactly
|
|
2094
|
-
- **State management complexity**: Use proven React patterns
|
|
2095
|
-
|
|
2096
|
-
**Timeline Risks:**
|
|
2097
|
-
- **Feature creep**: Stick to specification requirements
|
|
2098
|
-
- **Testing delays**: Implement testing incrementally
|
|
2099
|
-
- **Performance bottlenecks**: Profile and optimize continuously
|
|
2100
|
-
|
|
2101
|
-
This comprehensive implementation plan provides detailed guidance for building the Markdown Editor/Viewer component while maintaining consistency with existing codebase patterns and ensuring all specification requirements are met. Each phase builds incrementally on the previous one, with clear success criteria and testing strategies to ensure quality delivery.
|