ai-design-system 0.1.13 → 0.1.15
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/components/composites/AdjustableLayout/AdjustableLayout.tsx +23 -20
- package/components/composites/DocumentTabBar/DocumentTabBar.stories.tsx +122 -0
- package/components/composites/DocumentTabBar/DocumentTabBar.tsx +137 -0
- package/components/composites/DocumentTabBar/index.ts +2 -0
- package/components/composites/index.ts +4 -0
- package/components/features/AIDocEditor/AIDocEditor.behaviors.stories.tsx +208 -3
- package/components/features/AIDocEditor/AIDocEditor.mocks.ts +49 -0
- package/components/features/AIDocEditor/AIDocEditor.stories.tsx +45 -94
- package/components/features/AIDocEditor/AIDocEditor.tsx +156 -28
- package/components/features/AIDocEditor/README.md +176 -24
- package/components/features/AIDocEditor/index.ts +17 -1
- package/components/features/AIDocEditor/useAIDocEditor.d.ts +85 -2
- package/components/features/AIDocEditor/useAIDocEditor.mock.ts +179 -30
- package/dist/index.cjs +176 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.css +69 -0
- package/dist/index.d.ts +90 -22
- package/dist/index.js +177 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,29 +1,40 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* AIDocEditor Stories
|
|
2
|
+
* AIDocEditor Feature Stories
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Required story patterns for AIDocEditor feature:
|
|
5
|
+
* - Default: Static single-document mode (backward compatible)
|
|
6
|
+
* - Empty: Empty state for multi-tab mode
|
|
7
|
+
* - WithStateManagement: Interactive multi-tab with mock hook
|
|
5
8
|
*/
|
|
6
9
|
|
|
7
10
|
import type { Meta, StoryObj } from '@storybook/react'
|
|
8
11
|
import { AIDocEditor } from './AIDocEditor'
|
|
9
12
|
import { useAIDocEditorMock } from './useAIDocEditor.mock'
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
+
import {
|
|
14
|
+
sampleContent,
|
|
15
|
+
currentUser,
|
|
16
|
+
sampleAnnotations,
|
|
17
|
+
sampleMultiTabDocuments,
|
|
18
|
+
} from './AIDocEditor.mocks'
|
|
19
|
+
|
|
20
|
+
const meta = {
|
|
13
21
|
title: 'Features/AIDocEditor',
|
|
14
22
|
component: AIDocEditor,
|
|
15
23
|
parameters: {
|
|
16
24
|
layout: 'fullscreen',
|
|
17
25
|
},
|
|
18
26
|
tags: ['autodocs'],
|
|
19
|
-
}
|
|
27
|
+
} satisfies Meta<typeof AIDocEditor>
|
|
20
28
|
|
|
21
29
|
export default meta
|
|
22
|
-
type Story = StoryObj<typeof
|
|
30
|
+
type Story = StoryObj<typeof meta>
|
|
23
31
|
|
|
24
32
|
/**
|
|
25
|
-
* Default
|
|
26
|
-
*
|
|
33
|
+
* Default - Static single-document mode (REQUIRED)
|
|
34
|
+
*
|
|
35
|
+
* Shows the editor in the original single-document usage pattern with
|
|
36
|
+
* existing annotations. Demonstrates backward compatibility with existing
|
|
37
|
+
* consumers who have not yet migrated to multi-tab mode.
|
|
27
38
|
*/
|
|
28
39
|
export const Default: Story = {
|
|
29
40
|
args: {
|
|
@@ -37,104 +48,44 @@ export const Default: Story = {
|
|
|
37
48
|
}
|
|
38
49
|
|
|
39
50
|
/**
|
|
40
|
-
*
|
|
51
|
+
* Empty - Multi-tab empty state (REQUIRED edge case)
|
|
52
|
+
*
|
|
53
|
+
* Shows the empty state when no documents are open in multi-tab mode.
|
|
54
|
+
* Demonstrates edge case behavior and placeholder messaging.
|
|
41
55
|
*/
|
|
42
|
-
export const
|
|
56
|
+
export const Empty: Story = {
|
|
43
57
|
args: {
|
|
44
|
-
|
|
45
|
-
type: 'doc',
|
|
46
|
-
content: [
|
|
47
|
-
{
|
|
48
|
-
type: 'paragraph',
|
|
49
|
-
content: [],
|
|
50
|
-
},
|
|
51
|
-
],
|
|
52
|
-
},
|
|
53
|
-
annotations: [],
|
|
58
|
+
documents: [],
|
|
54
59
|
currentUser,
|
|
55
60
|
mode: 'review',
|
|
56
61
|
},
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
/**
|
|
60
|
-
*
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
annotations: sampleAnnotations,
|
|
66
|
-
currentUser,
|
|
67
|
-
mode: 'readonly',
|
|
68
|
-
},
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Interactive story with state management using mock hook
|
|
73
|
-
* Demonstrates adding comments and replies with simulated async operations
|
|
74
|
-
* Uses markdown format for easy content creation
|
|
65
|
+
* WithStateManagement - Multi-tab interactive mode (REQUIRED)
|
|
66
|
+
*
|
|
67
|
+
* Demonstrates the new multi-tab capability with live state management.
|
|
68
|
+
* Users can switch tabs, close tabs, and manage annotations across
|
|
69
|
+
* multiple documents. Uses mock hook for realistic interaction simulation.
|
|
75
70
|
*/
|
|
76
71
|
export const WithStateManagement: Story = {
|
|
77
72
|
render: () => {
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
This document demonstrates the **AIDocEditor** feature with full state management.
|
|
83
|
-
|
|
84
|
-
## Interactive Features
|
|
85
|
-
|
|
86
|
-
Try these interactions:
|
|
87
|
-
|
|
88
|
-
- **Select text** to add a new comment
|
|
89
|
-
- **Click highlighted text** to view existing annotations
|
|
90
|
-
- **Reply to comments** in the comment thread
|
|
91
|
-
- All changes are saved with simulated async operations
|
|
92
|
-
|
|
93
|
-
## Sample Content for Review
|
|
94
|
-
|
|
95
|
-
The AI has analyzed this document and provided several suggestions. You can review each suggestion and add your own comments.
|
|
96
|
-
|
|
97
|
-
### Code Example
|
|
98
|
-
|
|
99
|
-
\`\`\`javascript
|
|
100
|
-
function processData(items) {
|
|
101
|
-
return items.map(item => ({
|
|
102
|
-
id: item.id,
|
|
103
|
-
name: item.name,
|
|
104
|
-
processed: true
|
|
105
|
-
}));
|
|
106
|
-
}
|
|
107
|
-
\`\`\`
|
|
108
|
-
|
|
109
|
-
### Key Points
|
|
110
|
-
|
|
111
|
-
1. **Performance**: The current implementation is efficient
|
|
112
|
-
2. **Maintainability**: Code is well-structured and documented
|
|
113
|
-
3. **Testing**: Unit tests cover all critical paths
|
|
114
|
-
|
|
115
|
-
> **Note:** This is a live demo with state management. Try selecting text and adding comments!
|
|
116
|
-
|
|
117
|
-
---
|
|
118
|
-
|
|
119
|
-
**Status:** Ready for review | **Last updated:** ${new Date().toLocaleDateString()}`
|
|
73
|
+
const mockState = useAIDocEditorMock({
|
|
74
|
+
multiDoc: true,
|
|
75
|
+
initialDocuments: sampleMultiTabDocuments,
|
|
76
|
+
})
|
|
120
77
|
|
|
121
78
|
return (
|
|
122
|
-
<
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
{loading && (
|
|
133
|
-
<div className="fixed bottom-4 right-4 bg-primary text-primary-foreground px-4 py-2 rounded-md">
|
|
134
|
-
Saving...
|
|
135
|
-
</div>
|
|
136
|
-
)}
|
|
137
|
-
</div>
|
|
79
|
+
<AIDocEditor
|
|
80
|
+
documents={mockState.documents}
|
|
81
|
+
activeDocumentId={mockState.activeDocumentId}
|
|
82
|
+
currentUser={currentUser}
|
|
83
|
+
mode="review"
|
|
84
|
+
onTabSelect={mockState.switchDocument}
|
|
85
|
+
onTabClose={mockState.closeDocument}
|
|
86
|
+
onAnnotationAdd={mockState.addAnnotation}
|
|
87
|
+
onAnnotationUpdate={mockState.updateAnnotation}
|
|
88
|
+
/>
|
|
138
89
|
)
|
|
139
90
|
},
|
|
140
91
|
}
|
|
@@ -2,9 +2,9 @@
|
|
|
2
2
|
* AIDocEditor Feature Component
|
|
3
3
|
*
|
|
4
4
|
* Complete document editor with inline comment annotations.
|
|
5
|
-
*
|
|
5
|
+
* Supports both single-document and multi-tab modes.
|
|
6
6
|
*
|
|
7
|
-
*
|
|
7
|
+
* Single-document mode (backward compatible):
|
|
8
8
|
* ```tsx
|
|
9
9
|
* <AIDocEditor
|
|
10
10
|
* content={document}
|
|
@@ -14,18 +14,36 @@
|
|
|
14
14
|
* onAnnotationAdd={(annotation) => saveAnnotation(annotation)}
|
|
15
15
|
* />
|
|
16
16
|
* ```
|
|
17
|
+
*
|
|
18
|
+
* Multi-tab mode:
|
|
19
|
+
* ```tsx
|
|
20
|
+
* <AIDocEditor
|
|
21
|
+
* documents={[
|
|
22
|
+
* { file: { id: 'doc-1', name: 'File.md', isDirty: false, lastModified: now },
|
|
23
|
+
* content: '...',
|
|
24
|
+
* annotations: [] }
|
|
25
|
+
* ]}
|
|
26
|
+
* activeDocumentId="doc-1"
|
|
27
|
+
* currentUser={user}
|
|
28
|
+
* mode="review"
|
|
29
|
+
* onTabSelect={(id) => switchDocument(id)}
|
|
30
|
+
* onTabClose={(id) => closeDocument(id)}
|
|
31
|
+
* />
|
|
32
|
+
* ```
|
|
17
33
|
*/
|
|
18
34
|
|
|
19
|
-
import React, { useCallback } from 'react'
|
|
35
|
+
import React, { useCallback, useMemo } from 'react'
|
|
20
36
|
import { DocumentEditorWithComments } from '@/components/blocks/DocumentEditorWithComments'
|
|
37
|
+
import { DocumentTabBar } from '@/components/composites/DocumentTabBar'
|
|
21
38
|
import { cn } from '@/lib/utils'
|
|
22
39
|
import type { JSONContent } from '@tiptap/core'
|
|
23
40
|
import type { Annotation, User } from '@/types/ai-editor/annotations'
|
|
41
|
+
import type { DocumentWithAnnotations } from '@/types/ai-editor'
|
|
24
42
|
|
|
25
43
|
/**
|
|
26
|
-
* Props for AIDocEditor feature component
|
|
44
|
+
* Props for AIDocEditor feature component - Single document mode (backward compatible)
|
|
27
45
|
*/
|
|
28
|
-
export interface
|
|
46
|
+
export interface AIDocEditorSingleProps {
|
|
29
47
|
/**
|
|
30
48
|
* Document content - can be either:
|
|
31
49
|
* - JSONContent: Tiptap's JSON format (default)
|
|
@@ -44,6 +62,42 @@ export interface AIDocEditorProps {
|
|
|
44
62
|
annotations: Annotation[]
|
|
45
63
|
/** ID of currently selected annotation */
|
|
46
64
|
selectedAnnotationId?: string
|
|
65
|
+
/** Multi-document array (not provided in single-doc mode) */
|
|
66
|
+
documents?: never
|
|
67
|
+
/** Active document ID (not provided in single-doc mode) */
|
|
68
|
+
activeDocumentId?: never
|
|
69
|
+
/** Tab selection callback (not provided in single-doc mode) */
|
|
70
|
+
onTabSelect?: never
|
|
71
|
+
/** Tab close callback (not provided in single-doc mode) */
|
|
72
|
+
onTabClose?: never
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Props for AIDocEditor feature component - Multi-tab mode
|
|
77
|
+
*/
|
|
78
|
+
export interface AIDocEditorMultiTabProps {
|
|
79
|
+
/** Array of open documents with their content and annotations */
|
|
80
|
+
documents: DocumentWithAnnotations[]
|
|
81
|
+
/** ID of currently active document */
|
|
82
|
+
activeDocumentId?: string
|
|
83
|
+
/** Content prop (not provided in multi-tab mode) */
|
|
84
|
+
content?: never
|
|
85
|
+
/** Format prop (not provided in multi-tab mode) */
|
|
86
|
+
format?: never
|
|
87
|
+
/** Annotations prop (not provided in multi-tab mode) */
|
|
88
|
+
annotations?: never
|
|
89
|
+
/** Selected annotation ID (not provided in multi-tab mode) */
|
|
90
|
+
selectedAnnotationId?: never
|
|
91
|
+
/** Callback when tab is selected */
|
|
92
|
+
onTabSelect?: (documentId: string) => void
|
|
93
|
+
/** Callback when tab close button is clicked */
|
|
94
|
+
onTabClose?: (documentId: string) => void
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Props for AIDocEditor feature component
|
|
99
|
+
*/
|
|
100
|
+
export type AIDocEditorProps = (AIDocEditorSingleProps | AIDocEditorMultiTabProps) & {
|
|
47
101
|
/** Current user information */
|
|
48
102
|
currentUser: User
|
|
49
103
|
/** Editor mode: 'review' allows commenting, 'readonly' disables interactions */
|
|
@@ -66,30 +120,56 @@ export interface AIDocEditorProps {
|
|
|
66
120
|
className?: string
|
|
67
121
|
}
|
|
68
122
|
|
|
123
|
+
/**
|
|
124
|
+
* Determine if props are multi-tab mode
|
|
125
|
+
*/
|
|
126
|
+
function isMultiTabMode(props: AIDocEditorProps): props is AIDocEditorMultiTabProps & {
|
|
127
|
+
currentUser: User
|
|
128
|
+
mode: 'review' | 'readonly'
|
|
129
|
+
} {
|
|
130
|
+
return 'documents' in props && props.documents !== undefined
|
|
131
|
+
}
|
|
132
|
+
|
|
69
133
|
/**
|
|
70
134
|
* AIDocEditor - Document editor with inline comment annotations
|
|
71
135
|
*
|
|
72
136
|
* This feature component provides a complete document review experience with:
|
|
137
|
+
* - Single-document or multi-tab display modes
|
|
73
138
|
* - Read-only document display with annotation highlights
|
|
74
139
|
* - Inline comment box for viewing and adding comments
|
|
75
140
|
* - Support for comments, suggestions, and block additions
|
|
76
141
|
* - Controlled component pattern (all state managed by parent)
|
|
142
|
+
* - Backward-compatible with single-document consumers
|
|
77
143
|
*
|
|
78
144
|
* Note: The refinement panel (right sidebar with Accept All/Reject All) is a
|
|
79
145
|
* separate component in the parent application, not part of this feature.
|
|
80
146
|
*/
|
|
81
147
|
export const AIDocEditor = React.memo<AIDocEditorProps>(
|
|
82
|
-
({
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
148
|
+
(props) => {
|
|
149
|
+
const {
|
|
150
|
+
currentUser,
|
|
151
|
+
mode,
|
|
152
|
+
onAnnotationAdd,
|
|
153
|
+
onAnnotationUpdate,
|
|
154
|
+
onAnnotationDelete: _onAnnotationDelete, // Callback for consumers; passed through on demand
|
|
155
|
+
className,
|
|
156
|
+
} = props
|
|
157
|
+
|
|
158
|
+
const isMultiTab = isMultiTabMode(props)
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Get current document for rendering
|
|
162
|
+
*/
|
|
163
|
+
const currentDocument = useMemo(() => {
|
|
164
|
+
if (isMultiTab && props.documents) {
|
|
165
|
+
return (
|
|
166
|
+
props.documents.find((doc) => doc.file.id === props.activeDocumentId) ||
|
|
167
|
+
props.documents[0]
|
|
168
|
+
)
|
|
169
|
+
}
|
|
170
|
+
return null
|
|
171
|
+
}, [isMultiTab, props])
|
|
172
|
+
|
|
93
173
|
/**
|
|
94
174
|
* Handle annotation add - emit event to parent
|
|
95
175
|
*/
|
|
@@ -110,19 +190,67 @@ export const AIDocEditor = React.memo<AIDocEditorProps>(
|
|
|
110
190
|
[onAnnotationUpdate]
|
|
111
191
|
)
|
|
112
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Single-document mode
|
|
195
|
+
*/
|
|
196
|
+
if (!isMultiTab) {
|
|
197
|
+
return (
|
|
198
|
+
<DocumentEditorWithComments
|
|
199
|
+
content={props.content}
|
|
200
|
+
format={props.format}
|
|
201
|
+
annotations={props.annotations}
|
|
202
|
+
selectedAnnotationId={props.selectedAnnotationId}
|
|
203
|
+
currentUserId={currentUser.id}
|
|
204
|
+
currentUserName={currentUser.name}
|
|
205
|
+
readOnly={mode === 'readonly'}
|
|
206
|
+
onAnnotationAdd={handleAnnotationAdd}
|
|
207
|
+
onAnnotationUpdate={handleAnnotationUpdate}
|
|
208
|
+
className={cn('ai-doc-editor p-6', className)}
|
|
209
|
+
/>
|
|
210
|
+
)
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Multi-tab mode
|
|
215
|
+
*/
|
|
216
|
+
if (!currentDocument) {
|
|
217
|
+
return (
|
|
218
|
+
<div className={cn('ai-doc-editor flex flex-col h-full', className)}>
|
|
219
|
+
<DocumentTabBar
|
|
220
|
+
tabs={props.documents?.map((doc) => doc.file) || []}
|
|
221
|
+
activeTabId={props.activeDocumentId}
|
|
222
|
+
onTabSelect={props.onTabSelect}
|
|
223
|
+
onTabClose={props.onTabClose}
|
|
224
|
+
/>
|
|
225
|
+
<div className="flex-1 flex items-center justify-center p-6 text-muted-foreground">
|
|
226
|
+
No documents open
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
)
|
|
230
|
+
}
|
|
231
|
+
|
|
113
232
|
return (
|
|
114
|
-
<
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
233
|
+
<div className={cn('ai-doc-editor flex flex-col h-full', className)}>
|
|
234
|
+
<DocumentTabBar
|
|
235
|
+
tabs={props.documents?.map((doc) => doc.file) || []}
|
|
236
|
+
activeTabId={props.activeDocumentId}
|
|
237
|
+
onTabSelect={props.onTabSelect}
|
|
238
|
+
onTabClose={props.onTabClose}
|
|
239
|
+
/>
|
|
240
|
+
<div className="flex-1 overflow-auto">
|
|
241
|
+
<DocumentEditorWithComments
|
|
242
|
+
content={currentDocument.content}
|
|
243
|
+
format={currentDocument.file.format}
|
|
244
|
+
annotations={currentDocument.annotations}
|
|
245
|
+
currentUserId={currentUser.id}
|
|
246
|
+
currentUserName={currentUser.name}
|
|
247
|
+
readOnly={mode === 'readonly'}
|
|
248
|
+
onAnnotationAdd={handleAnnotationAdd}
|
|
249
|
+
onAnnotationUpdate={handleAnnotationUpdate}
|
|
250
|
+
className="p-6"
|
|
251
|
+
/>
|
|
252
|
+
</div>
|
|
253
|
+
</div>
|
|
126
254
|
)
|
|
127
255
|
}
|
|
128
256
|
)
|
|
@@ -2,16 +2,21 @@
|
|
|
2
2
|
|
|
3
3
|
Document editor with inline comment annotations for AI-powered document review workflows.
|
|
4
4
|
|
|
5
|
+
Supports both single-document and multi-tab modes for flexible document management.
|
|
6
|
+
|
|
5
7
|
## Architecture
|
|
6
8
|
|
|
7
|
-
- **Component Layer**: Pure UI component (`AIDocEditor.tsx`)
|
|
8
|
-
- **Hook Contract**: Interface
|
|
9
|
+
- **Component Layer**: Pure UI component (`AIDocEditor.tsx`) with mode detection
|
|
10
|
+
- **Hook Contract**: Interface definitions (`useAIDocEditor.d.ts`) for single and multi-tab modes
|
|
11
|
+
- **Composite Layer**: `DocumentTabBar` composite for tab rendering
|
|
9
12
|
- **Mock Implementation**: Storybook testing (`useAIDocEditor.mock.ts`)
|
|
10
13
|
- **Application Layer**: Real implementation (in your app)
|
|
11
14
|
|
|
12
15
|
## Usage
|
|
13
16
|
|
|
14
|
-
###
|
|
17
|
+
### Single-Document Mode (Backward Compatible)
|
|
18
|
+
|
|
19
|
+
#### In Your Application
|
|
15
20
|
|
|
16
21
|
```tsx
|
|
17
22
|
// app/hooks/useAIDocEditor.ts
|
|
@@ -53,14 +58,89 @@ export function DocEditorContainer({ documentId, content, currentUser }) {
|
|
|
53
58
|
}
|
|
54
59
|
```
|
|
55
60
|
|
|
56
|
-
###
|
|
61
|
+
### Multi-Tab Mode
|
|
62
|
+
|
|
63
|
+
#### In Your Application
|
|
57
64
|
|
|
58
65
|
```tsx
|
|
66
|
+
// app/hooks/useMultiTabDocEditor.ts
|
|
67
|
+
import type { UseAIMultiTabDocEditorReturn, DocumentWithAnnotations } from 'ui-lib/components/features/AIDocEditor';
|
|
68
|
+
|
|
69
|
+
export function useAIMultiTabDocEditor(): UseAIMultiTabDocEditorReturn {
|
|
70
|
+
const [documents, setDocuments] = useState<DocumentWithAnnotations[]>([]);
|
|
71
|
+
const [activeDocumentId, setActiveDocumentId] = useState<string>();
|
|
72
|
+
const [loading, setLoading] = useState(false);
|
|
73
|
+
|
|
74
|
+
const switchDocument = (documentId: string) => {
|
|
75
|
+
setActiveDocumentId(documentId);
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
const closeDocument = (documentId: string) => {
|
|
79
|
+
setDocuments(prev => {
|
|
80
|
+
const filtered = prev.filter(doc => doc.file.id !== documentId);
|
|
81
|
+
if (activeDocumentId === documentId && filtered.length > 0) {
|
|
82
|
+
setActiveDocumentId(filtered[0].file.id);
|
|
83
|
+
}
|
|
84
|
+
return filtered;
|
|
85
|
+
});
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const addAnnotation = async (documentId: string, annotation: Annotation) => {
|
|
89
|
+
setLoading(true);
|
|
90
|
+
await api.createAnnotation(documentId, annotation);
|
|
91
|
+
setDocuments(prev =>
|
|
92
|
+
prev.map(doc =>
|
|
93
|
+
doc.file.id === documentId
|
|
94
|
+
? { ...doc, annotations: [...doc.annotations, annotation] }
|
|
95
|
+
: doc
|
|
96
|
+
)
|
|
97
|
+
);
|
|
98
|
+
setLoading(false);
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
// ... other methods
|
|
102
|
+
|
|
103
|
+
return {
|
|
104
|
+
documents,
|
|
105
|
+
activeDocumentId,
|
|
106
|
+
switchDocument,
|
|
107
|
+
closeDocument,
|
|
108
|
+
addAnnotation,
|
|
109
|
+
// ... other methods
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
```tsx
|
|
115
|
+
// app/components/MultiDocEditorContainer.tsx
|
|
59
116
|
import { AIDocEditor } from 'ui-lib/components/features/AIDocEditor';
|
|
60
|
-
import {
|
|
117
|
+
import { useAIMultiTabDocEditor } from '../hooks/useMultiTabDocEditor';
|
|
118
|
+
|
|
119
|
+
export function MultiDocEditorContainer({ currentUser }) {
|
|
120
|
+
const { documents, activeDocumentId, switchDocument, closeDocument } = useAIMultiTabDocEditor();
|
|
121
|
+
|
|
122
|
+
return (
|
|
123
|
+
<AIDocEditor
|
|
124
|
+
documents={documents}
|
|
125
|
+
activeDocumentId={activeDocumentId}
|
|
126
|
+
currentUser={currentUser}
|
|
127
|
+
mode="review"
|
|
128
|
+
onTabSelect={switchDocument}
|
|
129
|
+
onTabClose={closeDocument}
|
|
130
|
+
/>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### In Storybook (use mock hook)
|
|
136
|
+
|
|
137
|
+
#### Single-Document Mode
|
|
138
|
+
|
|
139
|
+
```tsx
|
|
140
|
+
import { AIDocEditor, useAIDocEditorMock, sampleAnnotations } from 'ui-lib/components/features/AIDocEditor';
|
|
61
141
|
|
|
62
142
|
export const Default = () => {
|
|
63
|
-
const { annotations, addAnnotation, updateAnnotation } =
|
|
143
|
+
const { annotations, addAnnotation, updateAnnotation } = useAIDocEditorMock(sampleAnnotations);
|
|
64
144
|
|
|
65
145
|
return (
|
|
66
146
|
<AIDocEditor
|
|
@@ -75,9 +155,36 @@ export const Default = () => {
|
|
|
75
155
|
};
|
|
76
156
|
```
|
|
77
157
|
|
|
78
|
-
|
|
158
|
+
#### Multi-Tab Mode
|
|
159
|
+
|
|
160
|
+
```tsx
|
|
161
|
+
import {
|
|
162
|
+
AIDocEditor,
|
|
163
|
+
useAIMultiTabDocEditorMock,
|
|
164
|
+
sampleMultiTabDocuments
|
|
165
|
+
} from 'ui-lib/components/features/AIDocEditor';
|
|
166
|
+
|
|
167
|
+
export const MultiTab = () => {
|
|
168
|
+
const [documents, activeDocumentId, , , switchDocument, closeDocument] = Object.values(
|
|
169
|
+
useAIMultiTabDocEditorMock(sampleMultiTabDocuments)
|
|
170
|
+
);
|
|
171
|
+
|
|
172
|
+
return (
|
|
173
|
+
<AIDocEditor
|
|
174
|
+
documents={documents}
|
|
175
|
+
activeDocumentId={activeDocumentId}
|
|
176
|
+
currentUser={mockUser}
|
|
177
|
+
mode="review"
|
|
178
|
+
onTabSelect={switchDocument}
|
|
179
|
+
onTabClose={closeDocument}
|
|
180
|
+
/>
|
|
181
|
+
);
|
|
182
|
+
};
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
## Hook Contracts
|
|
79
186
|
|
|
80
|
-
### `UseAIDocEditorReturn`
|
|
187
|
+
### `UseAIDocEditorReturn` (Single-Document, Deprecated)
|
|
81
188
|
|
|
82
189
|
```typescript
|
|
83
190
|
interface UseAIDocEditorReturn {
|
|
@@ -89,38 +196,83 @@ interface UseAIDocEditorReturn {
|
|
|
89
196
|
}
|
|
90
197
|
```
|
|
91
198
|
|
|
199
|
+
### `UseAIMultiTabDocEditorReturn` (Multi-Tab, Recommended)
|
|
200
|
+
|
|
201
|
+
```typescript
|
|
202
|
+
interface UseAIMultiTabDocEditorReturn {
|
|
203
|
+
documents: DocumentWithAnnotations[];
|
|
204
|
+
activeDocumentId?: string;
|
|
205
|
+
addDocument: (file: DocumentFile, content: string | JSONContent) => void;
|
|
206
|
+
closeDocument: (documentId: string) => void;
|
|
207
|
+
switchDocument: (documentId: string) => void;
|
|
208
|
+
addAnnotation: (documentId: string, annotation: Annotation) => Promise<void> | void;
|
|
209
|
+
updateAnnotation: (documentId: string, annotation: Annotation) => Promise<void> | void;
|
|
210
|
+
deleteAnnotation: (documentId: string, annotationId: string) => Promise<void> | void;
|
|
211
|
+
setDocumentDirty: (documentId: string, isDirty: boolean) => void;
|
|
212
|
+
loading: boolean;
|
|
213
|
+
}
|
|
214
|
+
```
|
|
215
|
+
|
|
92
216
|
## Component Props
|
|
93
217
|
|
|
94
|
-
### `
|
|
218
|
+
### `AIDocEditorSingleProps` (Single-Document Mode)
|
|
95
219
|
|
|
96
220
|
```typescript
|
|
97
|
-
interface
|
|
221
|
+
interface AIDocEditorSingleProps {
|
|
98
222
|
content: JSONContent | string;
|
|
99
223
|
format?: 'json' | 'markdown';
|
|
100
224
|
annotations: Annotation[];
|
|
101
225
|
selectedAnnotationId?: string;
|
|
102
226
|
currentUser: User;
|
|
103
227
|
mode: 'review' | 'readonly';
|
|
104
|
-
|
|
105
|
-
onAnnotationClick?: (annotation: Annotation) => void;
|
|
106
|
-
onAnnotationHover?: (annotation: Annotation | null) => void;
|
|
107
|
-
onTextSelect?: (range: { from: number; to: number }, selectedText: string) => void;
|
|
108
|
-
onAnnotationAdd?: (annotation: Annotation) => void;
|
|
109
|
-
onAnnotationUpdate?: (annotation: Annotation) => void;
|
|
110
|
-
onAnnotationDelete?: (annotationId: string) => void;
|
|
111
|
-
className?: string;
|
|
228
|
+
// ... event callbacks (onAnnotationAdd, etc.)
|
|
112
229
|
}
|
|
113
230
|
```
|
|
114
231
|
|
|
232
|
+
### `AIDocEditorMultiTabProps` (Multi-Tab Mode)
|
|
233
|
+
|
|
234
|
+
```typescript
|
|
235
|
+
interface AIDocEditorMultiTabProps {
|
|
236
|
+
documents: DocumentWithAnnotations[];
|
|
237
|
+
activeDocumentId?: string;
|
|
238
|
+
currentUser: User;
|
|
239
|
+
mode: 'review' | 'readonly';
|
|
240
|
+
onTabSelect?: (documentId: string) => void;
|
|
241
|
+
onTabClose?: (documentId: string) => void;
|
|
242
|
+
// ... other event callbacks
|
|
243
|
+
}
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
### Shared Props (Both Modes)
|
|
247
|
+
|
|
248
|
+
```typescript
|
|
249
|
+
onContentUpdate?: (content: JSONContent) => void;
|
|
250
|
+
onAnnotationClick?: (annotation: Annotation) => void;
|
|
251
|
+
onAnnotationHover?: (annotation: Annotation | null) => void;
|
|
252
|
+
onTextSelect?: (range: { from: number; to: number }, selectedText: string) => void;
|
|
253
|
+
onAnnotationAdd?: (annotation: Annotation) => void;
|
|
254
|
+
onAnnotationUpdate?: (annotation: Annotation) => void;
|
|
255
|
+
onAnnotationDelete?: (annotationId: string) => void;
|
|
256
|
+
className?: string;
|
|
257
|
+
```
|
|
258
|
+
|
|
115
259
|
## Files
|
|
116
260
|
|
|
117
|
-
- `AIDocEditor.tsx` - Main component implementation
|
|
118
|
-
- `AIDocEditor.stories.tsx` - Storybook stories and
|
|
119
|
-
- `AIDocEditor.behaviors.stories.tsx` - Behavior-driven stories
|
|
120
|
-
- `AIDocEditor.mocks.ts` - Shared mock data
|
|
121
|
-
- `useAIDocEditor.d.ts` - Hook contract
|
|
122
|
-
- `useAIDocEditor.mock.ts` - Mock
|
|
261
|
+
- `AIDocEditor.tsx` - Main component implementation with mode detection
|
|
262
|
+
- `AIDocEditor.stories.tsx` - Storybook stories for both single-doc and multi-tab modes
|
|
263
|
+
- `AIDocEditor.behaviors.stories.tsx` - Behavior-driven stories including tab interactions
|
|
264
|
+
- `AIDocEditor.mocks.ts` - Shared mock data for both modes
|
|
265
|
+
- `useAIDocEditor.d.ts` - Hook contract definitions
|
|
266
|
+
- `useAIDocEditor.mock.ts` - Mock implementations for both single and multi-tab modes
|
|
123
267
|
- `index.ts` - Public exports
|
|
268
|
+
- `README.md` - This file
|
|
269
|
+
|
|
270
|
+
## Composites Used
|
|
271
|
+
|
|
272
|
+
- `DocumentTabBar` - VS Code-style tab bar for multi-document display
|
|
273
|
+
- Displays open documents as tabs with dirty indicators
|
|
274
|
+
- Handles tab selection and closing
|
|
275
|
+
- Horizontally scrollable when many tabs are open
|
|
124
276
|
|
|
125
277
|
## References
|
|
126
278
|
|